Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/ci-code-quality.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: πŸ” Check code quality

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
quality:
name: πŸ” Code Quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: uv.lock

- run: uv sync --group dev

- name: Check formatting
run: uv run ruff format --check .

- name: Lint code
run: uv run ruff check .

- name: Type check
run: uv run ty check
95 changes: 95 additions & 0 deletions .github/workflows/create-tag.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: πŸš€ Tagged Release

on:
push:
tags:
- "v*.*.*" # Semantic version tags: v1.2.3

permissions:
contents: read
id-token: write
attestations: write

jobs:
build:
name: πŸ“¦ Build distribution
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6

- uses: astral-sh/setup-uv@v7

- run: uv build

- name: Upload distribution artifacts
uses: actions/upload-artifact@v7
with:
name: python-package-distributions
path: dist/

attest-artifacts:
name: πŸ” Generate artifact attestations
needs: build
runs-on: ubuntu-latest

steps:
- name: Download distribution artifacts
uses: actions/download-artifact@v8
with:
name: python-package-distributions
path: dist/

- name: Generate artifact attestations
uses: actions/attest-build-provenance@v4
with:
subject-path: dist/*

publish-pypi:
name: πŸ“€ Publish to PyPI
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/netloom/
permissions:
id-token: write

steps:
- uses: astral-sh/setup-uv@v7

- name: Download distribution artifacts
uses: actions/download-artifact@v8
with:
name: python-package-distributions
path: dist/

- name: Publish to PyPI
run: uv publish --trusted-publishing always

create-release:
name: πŸ“‹ Create GitHub Release
needs: [publish-pypi, attest-artifacts]
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- uses: actions/checkout@v6

- name: Download distribution artifacts
uses: actions/download-artifact@v8
with:
name: python-package-distributions
path: dist/

- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ github.ref_name }}
run: |
gh release create $TAG_NAME \
--title "πŸš€ Release $TAG_NAME" \
Comment on lines +90 to +92
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -nP 'gh release create \$TAG_NAME\b' .github/workflows/create-tag.yaml

Repository: wlix13/NullForge

Length of output: 102


🏁 Script executed:

head -100 .github/workflows/create-tag.yaml | tail -20

Repository: wlix13/NullForge

Length of output: 594


🏁 Script executed:

grep -n "TAG_NAME" .github/workflows/create-tag.yaml | head -20

Repository: wlix13/NullForge

Length of output: 196


Quote TAG_NAME in the release command for consistency and defensive coding.

Line 91 should quote the variable to avoid word-splitting edge cases. Note that line 92 already quotes TAG_NAME in the title, making the unquoted usage on line 91 inconsistent.

Suggested patch
-          gh release create $TAG_NAME \
+          gh release create "$TAG_NAME" \
🧰 Tools
πŸͺ› actionlint (1.7.12)

[error] 90-90: shellcheck reported issue in this script: SC2086:info:1:19: Double quote to prevent globbing and word splitting

(shellcheck)

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/create-tag.yaml around lines 90 - 92, The gh release
create invocation uses an unquoted shell variable TAG_NAME which can cause
word-splitting; update the command that calls gh release create to quote the
variable (use "$TAG_NAME") so it matches the already-quoted title usage and is
defensive against spaces/special characters in TAG_NAME.

--verify-tag \
--generate-notes \
dist/*
9 changes: 7 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ cython_debug/
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
.vscode/

# Ruff stuff:
.ruff_cache/
Expand All @@ -213,4 +213,9 @@ marimo/_lsp/
__marimo__/

# Streamlit
.streamlit/secrets.toml
.streamlit/secrets.toml

# Repository specific (inventories)
nullforge/inventories/*
!nullforge/inventories/example.py
!nullforge/inventories/README.md
Empty file added nullforge/foundry/README.md
Empty file.
51 changes: 51 additions & 0 deletions nullforge/foundry/full_cast.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from pyinfra import local
from pyinfra.context import host

from nullforge.models.dns import DnsMode
from nullforge.molds.utils import ensure_features, ensure_system


def cast_full() -> None:
"""Cast the full NullForge's deployment blueprint."""

host.data.features = ensure_features(getattr(host.data, "features", None))
host.data.system = ensure_system(getattr(host.data, "system", None))

local.include("nullforge/runes/prepare.py")

local.include("nullforge/runes/base.py")

if host.data.features.users.manage:
local.include("nullforge/runes/users.py")

local.include("nullforge/runes/netsec.py")

if host.data.features.profiles.for_root or host.data.features.profiles.for_user:
local.include("nullforge/runes/profiles.py")

if host.data.features.dns.mode != DnsMode.NONE:
local.include("nullforge/runes/dns.py")

if host.data.features.warp.install:
local.include("nullforge/runes/warp.py")

if host.data.features.zerotrust.install:
local.include("nullforge/runes/zerotrust.py")

if host.data.features.haproxy.install:
local.include("nullforge/runes/haproxy.py")

if host.data.features.containers.install:
local.include("nullforge/runes/containers.py")

if host.data.features.tor.install:
local.include("nullforge/runes/tor.py")

if host.data.features.xray.install:
local.include("nullforge/runes/xray.py")

if host.data.features.mtproto.install:
local.include("nullforge/runes/mtproto.py")


cast_full()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Calling cast_full() at the module level causes the deployment logic to execute immediately upon import. This makes the module difficult to reuse or test without triggering side effects. It is recommended to wrap this call in an if __name__ == "__main__": block.

Suggested change
cast_full()
if __name__ == "__main__":
cast_full()

Empty file.
56 changes: 56 additions & 0 deletions nullforge/inventories/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from nullforge.models.dns import DnsMode
from nullforge.models.users import Shell
from nullforge.molds import DnsMold, UserMold, WarpMold
from nullforge.molds.base import BASE_FEATURES, BASE_SYSTEM
from nullforge.molds.utils import merge_features, merge_system


users = UserMold(
manage=True,
name="example",
shell=Shell.ZSH,
)
"""User configuration preset
with user management enabled and the user "example".
with shell set to ZSH (default behavior).
"""

warp = WarpMold(
install=True,
iface="warp-example",
)
"""WARP configuration preset
setup Cloudflare WARP
with default MASQUE engine and interface "warp-example".
"""

dns = DnsMold(
mode=DnsMode.DOH_RAW,
)
"""DNS configuration preset
with DNS over HTTPS raw mode.
"""

overrides = (
users,
warp,
dns,
)
"""Wrappers for the features to be merged with the base features."""

hosts = [
(
"203.0.113.10",
{
"system": merge_system(BASE_SYSTEM, {"hostname": "example-node1.local"}),
"features": merge_features(BASE_FEATURES, *overrides),
},
),
(
"203.0.113.20",
{
"system": merge_system(BASE_SYSTEM, {"hostname": "example-node2.local"}),
"features": merge_features(BASE_FEATURES, *overrides),
},
),
]
1 change: 1 addition & 0 deletions nullforge/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Internal models for NullForge."""
58 changes: 58 additions & 0 deletions nullforge/models/containers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Containers configuration models."""

from enum import StrEnum
from typing import Annotated, Literal

from pydantic import BaseModel, Field


class ContainersBackendType(StrEnum):
DOCKER = "docker"
PODMAN = "podman"
CRIO = "crio"


class ContainersRuntimeType(StrEnum):
DEFAULT = "default"
CRUN = "crun"
GVISOR = "gvisor"


class _ContainersBackendBase(BaseModel):
"""Base for a containers backend."""

type: ContainersBackendType = Field(description="The type of containers backend")
runtime: ContainersRuntimeType = Field(description="The type of containers runtime")


class DockerContainersBackend(_ContainersBackendBase):
type: Literal[ContainersBackendType.DOCKER] = ContainersBackendType.DOCKER
runtime: Literal[ContainersRuntimeType.GVISOR] = ContainersRuntimeType.GVISOR


class PodmanContainersBackend(_ContainersBackendBase):
type: Literal[ContainersBackendType.PODMAN] = ContainersBackendType.PODMAN
runtime: Literal[ContainersRuntimeType.CRUN] = ContainersRuntimeType.CRUN


class CrioContainersBackend(_ContainersBackendBase):
type: Literal[ContainersBackendType.CRIO] = ContainersBackendType.CRIO
runtime: Literal[ContainersRuntimeType.DEFAULT] = ContainersRuntimeType.DEFAULT


ContainersBackend = Annotated[
DockerContainersBackend | PodmanContainersBackend | CrioContainersBackend,
Field(discriminator="type"),
]


def containers_backend_factory(type: ContainersBackendType) -> ContainersBackend:
"""Factory function for containers backends."""

match type:
case ContainersBackendType.DOCKER:
return DockerContainersBackend()
case ContainersBackendType.PODMAN:
return PodmanContainersBackend()
case ContainersBackendType.CRIO:
return CrioContainersBackend()
Loading