Private Period
Status: temporary. This page describes the changes carried while the repositories are private. When they go public at launch, work through the revert checklist and delete this page.
Why
All six repositories are temporarily private until product launch. Five of them — everything except this monorepo — were public before, and the build relied on that: anonymous git fetch of the sibling repos in CI, anonymous release-asset and raw-URL downloads, and a public release badge all stopped working the moment visibility flipped.
Every private-specific change is deliberately thin — an optional input or an environment variable with a switchable default — so the whole period can be reverted with the checklist below. Keep it that way: do not build new functionality that assumes the repositories are private.
What breaks and how we cope
- Sibling checkouts in CI. The
checkout-siblingscomposite action (.github/actions/checkout-siblings/action.yml) fetches the plugin repos anonymously, which now 404s. It gained an optionaltokeninput — masked withadd-maskand passed as a per-invocationhttp.https://github.com/.extraheaderauthorization header, never embedded in the remote URL — and all platform call sites passtoken: ${{ secrets.SIBLING_REPOS_TOKEN }}.SIBLING_REPOS_TOKENis a repository secret holding a fine-grained PAT (resource ownerhegemony-sh, Contents + Metadata read-only on the sibling repos). - Release gate lookups.
scripts/release/build-release-manifest.py build(the release gate, see the release train) lists each plugin repository's release tag withgit ls-remoteand downloads the release'sSHA256SUMSthrough the API asset endpoint (Accept: application/octet-stream), both of which 404 anonymously on a private repo. It readsSIBLING_REPOS_TOKEN(orGITHUB_TOKEN), whichrelease.ymlpasses into the semantic-release step; the token goes into aGIT_CONFIG_*environment variable and anAuthorizationheader, is never on a command line, and is scrubbed from every message the script prints. The same 404-means-unknown caveat as below applies: a missing release and a missing permission look alike, so the failure message says so. Nothing to revert: without a token the script works against public repositories. - Demo deployment and installer checks now live in the demo-data repository. Its E2E workflow checks out the installer source locally and tests sibling clones without passing credentials to the installer.
- Demo plugin wheels. The pinned release wheels in
hegemony-demo-data/deploy/compose/demo-plugin-wheels.txtare release assets, which cannot be downloaded anonymously from a private repo._plugin_wheels_checkinhegemony-demo-data/deploy/compose/Taskfile.ymlselects the wheel source viaHEGEMONY_PLUGIN_WHEELS_SOURCE(buildorrelease), defaulting tobuild: an empty wheels directory is populated bytask compose:demo:plugins:buildfrom the sibling plugin checkouts (which requiresuvand the sibling clones — the layoutCONTRIBUTING.mdprescribes and CI produces).demo:plugins:fetchstays intact as the revert path. Relatedly,hegemony-demo-data/scripts/update_demo_plugin_wheels.pysends an optionalAuthorizationheader fromGH_TOKEN/GITHUB_TOKENand reads release assets through the API asset endpoint, sotask compose:demo:plugins:pinkeeps working withGH_TOKEN=$(gh auth token); that change works against public repos too and needs no revert. - 404 hides an auth failure, and once emptied a manifest. The sharpest edge of the private period: GitHub answers an unauthorized request with 404, not 401, so at
releases/latest"no release published yet" and "you cannot see this repo" are indistinguishable.update_demo_plugin_wheels.pybelieved the first reading, wrote a manifest of# … no published release yetcomments, and exited 0 — an emptied pin file reported as success, which only surfaces later as a broken demo install. It now probesrepos/{repo}before believing a 404 and names the missing or under-scoped token when that fails, and separately refuses to write a manifest that lost pins it previously had. Both guards are covered by tests in demo tests. Nothing to revert: on a public repo the probe simply succeeds, and the guards keep protecting against a partial outage. The general lesson holds for anything else added here — while these repos are private, treat a 404 as unknown, never as absent. - README release badge. shields.io cannot read a private repo, so the release badge in
README.mdis commented out, with the restore instructions inline. - Public
curl … | shinstall path. Anonymous users cannot download release assets from a private repo at all, so the one-command installer is dormant — documented, not worked around.docs/demo.mdcarries a clearly-marked temporary note pointing users at the clone-and-taskpath meanwhile. - Release artifact attestations.
actions/attestneeds a public repository or an Enterprise plan, so build-provenance attestation is unavailable across the org while the repos are private — an ungated step fails the whole release run, which is what stopped the firsthegemony-mcp-serverv0.1.0attempt. The plugin repositories and the MCP server therefore guard that step in theirrelease.ymlon repository visibility, reading it from bothgithub.repository_visibilityandgithub.event.repository.visibilityso that neither source being absent for a given trigger can leave attestation silently off. Private-period release assets consequently carry no provenance attestation, and nothing published during the private period substitutes for one:SHA256SUMSstill ships beside the assets, but it is an integrity check against a manifest published in the same release, so an actor able to replace the assets can replace it too. What actually bounds the exposure meanwhile is that only organization members can publish a release at all. Nothing to revert: the guard turns itself back on when the repositories go public, and provenance returns with it. (An Enterprise plan taken while the repos stay private would need the condition relaxed by hand.) - Demo
lab-inventorygit provider sync. Not broken — solved structurally. The demo's inventory tree lives in the always-publichegemony-sh/hegemony-demo-inventoryrepository, which the provider syncs anonymously from inside the platform containers whatever the visibility of the other repos. Nothing to revert at launch; that repository simply stays public.
Revert checklist (when the repos go public)
- Set
HEGEMONY_PLUGIN_WHEELS_SOURCE=release— or drop the variable and restore thedemo:plugins:fetchdefault in_plugin_wheels_checkinhegemony-demo-data/deploy/compose/Taskfile.yml. This repository'sdeploy/compose/Taskfile.ymlonly forwards the demo tasks and has nothing to revert. - Drop the
token:inputs from the platformcheckout-siblingscall sites:.github/workflows/ci.yml(6),.github/workflows/ci-heavy.yml(6),.github/workflows/docker-publish.yml(2), and.github/workflows/release.yml(1). The action'stokeninput is optional, so.github/actions/checkout-siblings/action.ymlitself needs no change. - Update the demo-data repository's
_plugin_wheels_checkand E2Eplugin_wheelsdefaults to use released wheels after the repositories are public. - Verify the demo-data installer's default clone URLs work anonymously.
- Review its E2E workflow's
SIBLING_REPOS_TOKENdependency. - Restore (uncomment) the release badge in
README.md. - Delete the
SIBLING_REPOS_TOKENrepository secret and revoke the underlying fine-grained PAT. TheSIBLING_REPOS_TOKENline in the semantic-release step of.github/workflows/release.ymlcan stay or go: the release gate falls back to anonymous access when it is empty. - Re-enable the public
curl | shinstall path indocs/demo.mdandREADME.md(delete the temporary notes), then runtask docs:generateso the in-app help copy follows. - Make the GHCR packages public. Packages created while the repositories are private default to private, so
docker pullof a release image needs authentication; the compose files anddocs/demo.mdassume it does not. That includesegress-nflog, which is now part of every stack's Docker-in-Docker sandbox, not only the demo's. Link each package to its repository in the org package settings and set its visibility to public. - Decide what happens to the self-hosted runners (see Actions billing). A public repository accepts pull requests from forks, and a self-hosted runner executes whatever a PR's workflow asks of it on our own host. Either unset the
CI_RUNNERvariable so every job returns to GitHub-hosted runners — free again for public repositories — or keep the runners and require approval for all outside collaborators in the repository's Actions settings.
Actions billing
While the repositories are private, GitHub Actions minutes are billable for all of them — five of them were public, and public repositories run for free — and ci-heavy.yml and demo-e2e.yml are not cheap. Keep an eye on org billing until launch.
Self-hosted runners. To stop paying for hosted minutes, every workflow that can run on our own hardware selects its runner through the CI_RUNNER organization/repository variable: runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }}. Setting it to self-hosted sends those jobs to the organization's own runners (an Ubuntu 24.04 host running eight runner services); unsetting it flips every job back to GitHub-hosted ubuntu-latest without a workflow change, which is the fallback when the host is down.
| Workflow | Runner |
|---|---|
ci.yml | CI_RUNNER |
ci-heavy.yml (all jobs, docs-screenshots included) | CI_RUNNER |
demo-e2e.yml | CI_RUNNER |
semgrep.yml | CI_RUNNER |
docker-publish.yml | CI_RUNNER |
release.yml | GitHub-hosted, always |
docs-ai-review, docs-pr-review, audit-pr-review | GitHub-hosted, always |
release.yml is the one workflow held back on purpose: its credential step writes RELEASE_TOKEN into the workspace's .git/config, which on a non-ephemeral runner outlives the job and is readable by every later job sharing that runner user. It runs a handful of times a month, so the minutes it would save do not pay for leaving a long-lived PAT on disk. The three agentic reviews are held back for two reasons: their .lock.yml files are compiled by gh aw compile and must not be hand-edited, and they run an AI agent with tool access, which belongs on a throwaway machine rather than a host shared with every other job.
The host must provide what the jobs assume of ubuntu-latest: a Docker daemon the runner users may reach, passwordless sudo (the kernel egress tests, playwright install --with-deps), unshare, iproute2, iptables, the Playwright system libraries, and the gh CLI (the docs-screenshots job opens its refresh PR with it). demo-e2e.yml's "Free runner disk space" step is gated on runner.environment == 'github-hosted' so it never prunes our own host.
A job that runs inside a container (semgrep.yml) executes as the image's user, root for semgrep/semgrep, and writes into the runner's shared workspace. Whatever it creates there stays root-owned, and the next job on that runner cannot update a ref or delete a file under a directory root made, so such a job must hand the workspace back to the runner's user in a final step that runs whether the job passed or not, as semgrep.yml does.
Self-hosted runners are only safe while the repositories are private — see item 10 of the revert checklist.
No CI run on pushes to develop. ci.yml triggers on pushes to main only. A PR into develop is already tested on its merge result, so the run on the resulting merge commit tested the same tree twice. What that run did catch is a semantic conflict between two PRs that each passed alone — two Alembic heads, a renamed symbol one branch still calls — which now surfaces on the next PR into develop instead, against an author who did not cause it. If that trade stops being worth it, the cheapest restoration is a nightly scheduled ci.yml run on develop rather than a run per merge.
No CI run on closed pull requests. ci.yml also listens for labeled and unlabeled (the docs-not-needed and coverage labels), and those events fire on closed PRs too. The gate job skips when the PR is not open, every job downstream of it follows, and semgrep.yml skips the same way; the release's released label is off for the same reason (see release-process.md).