Release Publishing¶
This page documents the staged package-release workflow for netbox-proxbox and
its companion proxbox-api backend. The workflow deliberately separates package
index validation from final publication so failed published artifacts are never
reused.
For the broader CI job map and Docker E2E matrix, see CI and E2E Workflows.
Release State Machine¶
flowchart TD
Start([Choose target release\nX.Y.Z])
Bump[Bump package version\npyproject.toml + netbox_proxbox/__init__.py + uv.lock]
RCTag[Create release-candidate tag\nvX.Y.ZrcN]
RCCI[Target CI builds a six-file\npublisher-credential-free signed request]
Control[Locked release control verifies\nand publishes exact sealed bytes]
RCUpload[Upload vX.Y.ZrcN to TestPyPI\nwithout --skip-existing]
RCValidate[Install rcN from TestPyPI\nrun package checks]
RCE2E[E2E Docker\nnetbox-proxbox rcN from TestPyPI\nproxbox-api rcN from TestPyPI]
RCFailed{Any TestPyPI\nvalidation failed?}
NextRC[Bump to vX.Y.ZrcN+1]
FinalPrivate[Publish final package to Gitea\nvX.Y.Z]
Deploy[Deploy exact Gitea package\nthrough NMS]
PublicRelease[Create GitHub Release\nafter production validation]
FinalUpload[Upload vX.Y.Z to PyPI]
FinalValidate[Install final from PyPI\nrun post-upload E2E]
FinalFailed{Post-release fix needed?}
Post[Bump to vX.Y.Z.postN\npublish .postN to PyPI]
Done([Release is green])
Start --> Bump --> RCTag --> RCCI --> Control --> RCUpload --> RCValidate --> RCE2E --> RCFailed
RCFailed -- yes --> NextRC --> RCTag
RCFailed -- no --> FinalPrivate --> Deploy --> PublicRelease --> FinalUpload --> FinalValidate --> FinalFailed
FinalFailed -- yes --> Post --> FinalPrivate
FinalFailed -- no --> Done
Cross-Package E2E Contract¶
The plugin does not import proxbox-api as a Python dependency. It consumes the
backend as a runtime HTTP service, so release coupling is validated in Docker
E2E rather than package metadata.
sequenceDiagram
participant Tag as Release Tag
participant WF as netbox-proxbox request workflow
participant Control as Locked release control
participant GP as Gitea package registry
participant PublicWF as GitHub public-publish workflow
participant TP as TestPyPI
participant PY as PyPI
participant E2E as e2e-docker.yml
participant NB as NetBox container
participant API as proxbox-api container
Tag->>WF: vX.Y.ZrcN
WF->>Control: wheel + sdist + release-manifest.json + release-request.json + runner-completion-attestation.json + runner-completion-attestation.sig
Control->>Control: verify run, workflow, request, and sealed bytes
Control->>GP: Publish exact sealed package bytes
Control->>PublicWF: Promote the exact RC tag
PublicWF->>TP: Upload the exact Gitea package bytes
PublicWF->>E2E: install_source=testpypi, dependency_mode=testpypi-package
E2E->>NB: pip install netbox-proxbox==X.Y.ZrcN from TestPyPI
E2E->>API: validate proxbox-api Python and PyO3/Rust runtimes
E2E-->>PublicWF: Release-candidate checks pass for both runtimes
Tag->>PublicWF: published GitHub Release for vX.Y.Z or vX.Y.Z.postN
PublicWF->>PY: Upload netbox-proxbox package
PublicWF->>E2E: install_source=pypi, dependency_mode=pypi-package
E2E->>NB: pip install netbox-proxbox==X.Y.Z or X.Y.Z.postN from PyPI
E2E->>API: validate proxbox-api Python and PyO3/Rust runtimes
E2E-->>PublicWF: Post-publish checks pass for both runtimes
Workflow Rules¶
pyproject.toml,netbox_proxbox/__init__.py,uv.lock, and the Git tag must all describe the same version.rcNtag pushes (patternv*rc*) publish to TestPyPI for release-candidate validation.- Official releases (
vX.Y.Z,vX.Y.Z.postN) are triggered only by GitHub release creation (release: published) cut from thedevelopbranch after the final Gitea package and NMS production gates. Plain non-rc tag pushes do not trigger public publishing. Manual workflow dispatch is TestPyPI-only and requires an RC version. - Package uploads intentionally omit
twine --skip-existing; a consumed version must move forward to the next.postNorrcN. - The target Gitea workflow listens for tag
push, not the overlappingcreateevent, so a tag can start only one immutable release request. - Workflow concurrency is global to this repository rather than per ref. A second RC/final/post request cannot race the sole release label while the validation supervisor is sequencing the active request.
- Both release-request jobs use the repository-unique
ci-release-netbox-proxboxlabel. The replacement registration must expose that label only at repository scope; the broader user-scopedci-untrusted-python312runner is not eligible for release evidence. Before either job processes candidate-controlled bytes, a checksum-pinned gate compares the live Gitea job's runner ID, name, and sole label to.gitea/release-runner-acceptance.json. Validation and build identities have independent canonical repository-registration scope digests, so evidence for one role cannot authorize the other. Its zero ID, empty name, and all-zero key/runtime/image/network/supervisor digests intentionally disable tag releases until live acceptance replaces every sentinel in one reviewed change. Even then, the gate requires a root-owned, freshly signed supervisor attestation bound to the repository, first run attempt, run ID, job ID, source SHA, exact workflow path and digest, runner identity, complete registered-label set, runtime image, and network/runtime policy digests. Missing, stale, mismatched, or invalidly signed evidence fails before candidate execution. - A candidate tag must resolve to the current canonical Gitea
developSHA. The gate ignores writer-controlled commit statuses and selects the newest authenticatedci.ymlActions run for that exact SHA directly from Gitea's run inventory. That run and each required job must prove a successful first push attempt for the exact SHA, trusted actor, job name, and exact soleci-untrusted-python312job label. The two jobs use distinct job-bound ephemeral runner IDs/names. Each registration advertises onlyci-release-netbox-proxbox, accepts one supervisor-authorized assignment, and terminates; the validation identity cannot service the build job. Each RC, final, or post request therefore requires a freshly registered and reviewed identity pair. Both jobs receiveactions: readpluscontents: readonly for their trusted runner/CI evidence gates. The untrusted build fetches the validated public source without checkout credentials, and its step-scoped Gitea token is not passed across the candidate boundary. Gitea's public-repository permission floor can still make public Actions data readable, so this is not an Actions-read confidentiality boundary. The outer job also receives Gitea's artifact runtime token. Candidate-controlled dependency installation, PEP 517 build, Twine check, and manifest generation therefore run as a separate numeric UID with an allowlisted token-free environment, no-new-privileges/resource limits, denial of the root parent's/proc/.../environ, and cleanup of every surviving process for that UID. A fail-closed x86-64 Landlock ABI 3+ rule permits writes only below the per-run build root, preventing candidate writes to runner workflow-command files and shared temporary storage; the runner must match that architecture and expose that ABI or the build fails. A fail-closed x86-64 seccomp filter also returnsEPERMfor every socket syscall, allio_uringentry points, and every x32-tagged syscall; the candidate probes all three paths before dependency or build code runs. Theci-release-netbox-proxboxactivation canary must separately prove that the exact repository-scoped release runner/container denies management and production network access and bind that immutable result plus the runtime digest to the same runner ID in the acceptance record; an online runner label alone is insufficient evidence. The external supervisor must re-attest the live state for each release job; a historical canary cannot authorize a restarted or reconfigured runner. Candidate stdout/stderr is bounded and captured instead of reaching the runner workflow-command parser, with liveset-env/add-pathcanaries checked in the next step. The job fails closed unless cgroup v2 proves hard one-CPU, 2-GiB-memory, zero-swap, and 64-PID ceilings and/nmc-buildis a hard one-GiB/50,000-inode tmpfs. The 900-second wall bound therefore also caps cumulative CPU, while parent accounting includes live and reaped descendants. Logical-size, filesystem-block, file-count, and output checks remain defense in depth; CPU parsing does not trust whitespace in Linux process names. Reviewed outer code uses exact no-follow file descriptors, bounded regular-file inventory, and copy re-hashing before it invokes artifact upload; candidate code receives no job, runtime, package, mirror, or write credential. A disposable target job builds one wheel and one sdist with the runner image's exact Python 3.12.14 and uv 0.12.5 after verifying the baked interpreter/tool versions, the policy-pinneduv.lockdigest, and the build-lock checksum manifest for its read-only wheelhouse. The job revalidates the exact immutable wheel inventory in-container; the publish lock includes Hatchling so the project's configured PEP 517 backend is available without network access. Dependency resolution is offline (--no-index, no Python downloads). The trusted outer steps use image-baked Gitea checkout and artifact clients, so their only network authority is same-origin Gitea access. After candidate process cleanup, the root-only external supervisor signs the exact request/artifact inventory. The job uploads exactly six data files: the wheel, sdist, canonicalrelease-manifest.json, canonicalrelease-request.json, canonicalrunner-completion-attestation.json, andrunner-completion-attestation.sig. The request binds the repository ID, source/tag, initiating run and attempt, workflow digest, manifest digest, and artifact inventory. The job verifies the root-owned completion client digest, executes a sealed in-memory snapshot of those exact bytes, and the client verifies the supervisor signature locally against its policy-pinned public key before the exact six-file upload. It has no package or GitHub-mirror credential. The separately administered release-control repository fetches that exact first-attempt run, verifies the policy-pinned target workflow, supervisor completion signature, and every byte on its isolated builder, then seals the handoff. Only its isolated publisher can read the package credentials and invoke the fixed, digest-locked publication tooling. Public no-authority downloads must match the manifest before the durable publication ledger advances. - GitHub never rebuilds release artifacts. It downloads that exact linked Gitea wheel/sdist, installs both artifact forms on Python 3.12 and 3.13, and uploads the same bytes to TestPyPI or PyPI.
- A final package-first production workflow asks the root-owned fixed deploy helper to emit a schema-2 receipt only after the exact versioned wheel import and NetBox health checks succeed. Workflow code exports and publishes those host-issued bytes; it cannot create a successful-production receipt. The final GitHub release event must match the receipt's source SHA, version, artifact hashes, manifest digest, observed runtime path, production environment, and Gitea run ID.
- TestPyPI and PyPI candidate validation run the mocked suite with
-p no:django; the separate real-NetBox matrix keeps pytest-django enabled. - Release E2E runs with
proxbox_api_runtime: both. The Python backend and the PyO3/Rust backend must both pass before PyPI publication can proceed. - In package-index E2E, Rust mode tries
proxbox-api[pyo3-rust]first and falls back to the matching<version>-pyo3-rustDocker image when the backend package has not published that extra yet. proxbox_api_versioncan be supplied manually. If omitted, the workflow reads repository variables in this order:PROXBOX_API_TESTPYPI_VERSION/PROXBOX_API_PYPI_VERSION,PROXBOX_API_RELEASE_VERSION, then the checked-in default.
Operator Checklist¶
- Before merging the target cutover, require the private control repository's positive policy-pinned ID plus ready protected workflows, host boundaries, sockets, and repository-scoped runners. If readiness is incomplete, leave the existing publisher active and stop.
- Push the reviewed tag and wait for
publish-gitea.ymlto produce therelease-control-requestartifact. Record its run ID and the SHA-256 of its canonicalrelease-request.json. - Dispatch
validate.ymlwith exactly the repository name, target run ID, and request SHA-256. After it succeeds, dispatch the separate irreversiblepublish.ymlwith those same three inputs. For RCs, the control publishes the Gitea package and promotes only that exact RC tag to GitHub. - Publish and validate
proxbox-apion TestPyPI first. - Publish and validate
netbox-proxboxon TestPyPI using that TestPyPIproxbox-apiversion. - Publish each final package in Gitea, link/verify it, and deploy the exact pair
through NMS using
latest_packageby default. - After production integration and health checks pass, dispatch each
repository's
promote-final-tag.ymlfrom canonical Giteamain. The workflow verifies the exact package and protected host-issued deployment receipt before pushing only that tag to the authorized GitHub repository. Then create the proxbox-api and netbox-proxbox GitHub Releases with--verify-tag; those final tags and protected Gitea deployment receipts authorize PyPI/Docker Hub publication. - If any published validation fails, bump to the next
.postNorrcN; never retry the same artifact version.