website/docs/release-forgejo.md

9.6 KiB

C0-B preactivation update: protected entrypoints now require schema-2 controller evidence and consume verified prebuilt artifact bytes. Repository publication workflows refuse; no privileged candidate code is executed. The original port/recovery history below is preserved, while execution closure defines the current activation and artifact boundary. E5 record/main/endpoint/ledger gates remain mandatory.

Post-activation record (2026-10-16): the closure path has since executed verified live publications — most recently 2026-10-15 — run by an authenticated publisher under a separately admitted operator run, with rollback data retained (the rollback path was not exercised) and admission closed afterwards: credentials removed, no pending records. The repository-resident automatic-publication design below, including STATIC_PUBLISH_ENABLED, is not the live boundary: repository jobs always refuse, and merging to protected main never publishes.

Static publication on the oss-oo.io forge

This is the deployment boundary for OSS-Singularity/website on the self-hosted forge (oss-oo.io). It recorded the port of the GitHub publication contract to Forgejo Actions as the production-path design of 2026-09-23. Both earlier provider workflows are now historical reference: repository publication jobs always refuse (see execution closure), and live publication is the separately admitted operator execution recorded in the banner above.

What stays identical

The static publication guide continues to describe the endpoint contract: the restricted fixed-command release interface, the hoster-side journal with generations and retained rollback, the full HTTP acceptance (MISS → HIT, bytes, headers, redirects, 404, Telegram bytes, TLS/origin binding), the Cloudflare zone observation and single purge, and the rule that a failed acceptance rolls the attempt back and keeps the original attempt identity. scripts/forgejo_publication.py does not reimplement any of this: it calls static_publication.transition with the same SSH, HTTP and edge objects, so a rollback on this boundary exercises the identical code path the GitHub client used.

What this boundary replaces

GitHub boundary Forge boundary
Workflow-run provenance (workflow_run chaining, run/artifact identity) The staged forge CI provenance contract (forgejo_trust, docs/forge-ci-provenance.md): server-evidenced run/job rows at the exact protected main commit plus operator-pinned workflow bytes, re-verified before apply (fresh). Candidate-resident checker and consumer scripts remain outside schema-1 closure; protected policy-bundle integration is still required.
Candidate artifact from the separate rehearsal workflow Same-run double build (UTC build, Pacific/Honolulu rebuild, release-artifact.py verify) plus one more clean-environment rebuild before publishing (source_rebuilt)
GitHub deployments API ledger Append-only oss-static-ledger branch in this repository: ledger/<run>-<attempt>/intent.json before mutation, outcome.json after maintenance, same previous/start/finish semantics
GitHub App policy reader Read-only forge API reads with a scoped token and route allowlist
production-static environment The oss-production concurrency group plus the STATIC_PUBLISH_ENABLED repository secret
Contents/trees API reads (historical .htaccess, Commons tree) Local git reads against explicitly fetched commits

The report keeps the same fields and acceptance: source_rebuilt=true, publication_verified=true, rollback_retained=true, plus pipeline: "forgejo" and the ledger entry as deployment_id.

Access table

Setting Purpose
Secret STATIC_FORGE_TOKEN Repository-scoped token for the policy reads and the ledger branch pushes. Read access to branches and commit statuses only; no admin scope.
Secret CF_RELEASE_TOKEN Unchanged from the GitHub boundary: this zone only — cache purge plus the reads needed for zone, settings, DNS and cache-rule verification.
Secrets STATIC_ORIGIN_IP, STATIC_SSH_USER, STATIC_SSH_PORT Unchanged provider connection binding.
Secrets STATIC_SSH_HOST_KEY, STATIC_SSH_KEY Unchanged pinned Ed25519 host key and the restricted fixed-command identity.
Secret STATIC_RUNTIME_SHA256 Unchanged installed nine-file runtime digest. Stored as a secret because this forge instance exposes no write API for action variables.
Secret STATIC_API_RELEASE_SHA Unchanged installed Commons commit. Same reason as above.
Secret STATIC_PUBLISH_ENABLED true enables automatic publication after required checks on protected main; any other value keeps publishing off (the job skips visibly on push). plan dispatches work without it. Enforced again inside the publication client. Historical design only: the closure boundary supersedes it, and repository jobs refuse regardless of this value.
Secret STATIC_TRUST_PIN Operator trust pin for the CI provenance contract (docs/forge-ci-provenance.md): repository, workflow path, workflow bytes digest, events, job name, required context. Absent or invalid pins fail closed (missing_trust_pin / invalid_trust_pin); bootstrapped and re-pinned by the owner, never read from the repository.

The runner executes on the laboratory server and reaches the static origin directly over the internet; the pinned host key is the same binding the GitHub runner used. No cPanel token or operator SSH identity belongs in this workflow.

Ledger discipline

The oss-static-ledger branch is machine-written. Every publication appends exactly one directory (ledger/<zero-padded run id>-<attempt>/) containing the sanitized intent (identity, commit, predecessor hashes, generation, runtime digest, run identity — no credentials, no file contents) and one terminal outcome. An intent without a terminal outcome blocks the next publication exactly like an unresolved GitHub deployment did. Operators never rewrite, rebase or force-push this branch; recovery closes a blocked record, it never deletes one.

Blocked-record recovery

Dispatch mode recover closes a blocked ledger record through the same bounded contract the GitHub boundary had. scripts/forgejo-recovery.py reuses the static_recovery engine cores unchanged: it re-reads the original recorded intent from oss-static-ledger, reconciles a retained remote attempt under its original identity and ticket, and closes the record only after the resulting live bytes pass the same full HTTP acceptance as a publication — either as a verified rollback, or as success when only maintenance or record bookkeeping failed after an already healthy verified site. It never creates a new intent, adopts a foreign attempt, deletes or rebases the ledger, or resets endpoint state.

Only the newest record is examined, and only when it has no outcome yet, its in-progress outcome, or its explicit unresolved outcome; closed, foreign, malformed or ambiguous records are refused without any mutation. A ledger fetch that fails never falls back to the runner folder's earlier copy: it fails closed as forge_git_failed, and only a remote proven to hold no ledger branch at all answers no_unresolved_publication. A changed endpoint runtime digest, operator-modified files or a changed provider configuration refuse recovery and keep the record blocked.

Because the verified-site path executes the recorded tree's build scripts (git archive plus the same double build and release-artifact.py verify the publication boundary uses — the GitHub-era artifact consumer it replaced only ever re-verified downloaded bytes and never executed historical code), the mechanical path is bounded to the one candidate a publication itself would trust: before anything is archived, built or asked of the endpoint, the recorded commit, the dispatch SHA and a freshly read protected main head must all be the same commit, and that commit's pinned required-status context must be green — the publication boundary's own pre-build binding. The loader route refuses any other commit, and the verified-site path additionally requires the endpoint's own report to name this record's candidate_commit before it builds or closes positively. It still requires the recorded descriptor digest; the rollback path reads the recorded predecessor's site/.htaccess from the local repository, and missing local git objects fail closed — recovery never downloads history automatically.

An older record — main has already moved past its commit — refuses with stale_candidate before any execution. Such records, and any record outside this exact-current-candidate bound, keep the manual, evidence-bound operator path in the RUNBOOK; re-dispatching the workflow from an older checkout or a raw SHA is not a supported workaround. This boundary deliberately claims no general historical byte-artifact recovery: a retained-artifact route that could verify older candidates without executing their build scripts would be its own future block.

Recovery shares the oss-production concurrency group with publication, so it never overlaps a running publication, and a competing ledger writer loses nothing: the terminal outcome push fail-closes instead of overwriting. It deliberately does not require STATIC_PUBLISH_ENABLED: an operator may disable automatic publication during an incident and still close the blocked record.

Not ported (yet)

  • CodeQL: the GitHub org-level default setup has no forge equivalent; the required-check policy pins the forge Repository checks status instead.