> 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](forge-execution-closure.md) 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](release-publication.md) 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](forge-execution-closure.md)), and live publication is the separately admitted operator execution recorded in the banner above. ## What stays identical The [static publication guide](release-publication.md) 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/-/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/-/`) 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.