Skip to content

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-siblings composite action (.github/actions/checkout-siblings/action.yml) fetches the plugin repos anonymously, which now 404s. It gained an optionaltoken input — masked with add-mask and passed as a per-invocation http.https://github.com/.extraheader authorization header, never embedded in the remote URL — and all platform call sites pass token: ${{ secrets.SIBLING_REPOS_TOKEN }}. SIBLING_REPOS_TOKEN is a repository secret holding a fine-grained PAT (resource owner hegemony-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 with git ls-remote and downloads the release's SHA256SUMS through the API asset endpoint (Accept: application/octet-stream), both of which 404 anonymously on a private repo. It reads SIBLING_REPOS_TOKEN (or GITHUB_TOKEN), which release.yml passes into the semantic-release step; the token goes into a GIT_CONFIG_* environment variable and an Authorization header, 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.txt are release assets, which cannot be downloaded anonymously from a private repo. _plugin_wheels_check in hegemony-demo-data/deploy/compose/Taskfile.yml selects the wheel source via HEGEMONY_PLUGIN_WHEELS_SOURCE (build or release), defaulting to build: an empty wheels directory is populated by task compose:demo:plugins:build from the sibling plugin checkouts (which requires uv and the sibling clones — the layout CONTRIBUTING.md prescribes and CI produces). demo:plugins:fetch stays intact as the revert path. Relatedly, hegemony-demo-data/scripts/update_demo_plugin_wheels.py sends an optional Authorization header from GH_TOKEN / GITHUB_TOKEN and reads release assets through the API asset endpoint, so task compose:demo:plugins:pin keeps working with GH_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.py believed the first reading, wrote a manifest of # … no published release yet comments, and exited 0 — an emptied pin file reported as success, which only surfaces later as a broken demo install. It now probes repos/{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.md is commented out, with the restore instructions inline.
  • Public curl … | sh install 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.md carries a clearly-marked temporary note pointing users at the clone-and-task path meanwhile.
  • Release artifact attestations. actions/attest needs 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 first hegemony-mcp-server v0.1.0 attempt. The plugin repositories and the MCP server therefore guard that step in their release.yml on repository visibility, reading it from both github.repository_visibility and github.event.repository.visibility so 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: SHA256SUMS still 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-inventory git provider sync. Not broken — solved structurally. The demo's inventory tree lives in the always-public hegemony-sh/hegemony-demo-inventory repository, 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) ​

  1. Set HEGEMONY_PLUGIN_WHEELS_SOURCE=release — or drop the variable and restore the demo:plugins:fetch default in _plugin_wheels_check in hegemony-demo-data/deploy/compose/Taskfile.yml. This repository's deploy/compose/Taskfile.yml only forwards the demo tasks and has nothing to revert.
  2. Drop the token: inputs from the platform checkout-siblings call 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's token input is optional, so .github/actions/checkout-siblings/action.yml itself needs no change.
  3. Update the demo-data repository's _plugin_wheels_check and E2E plugin_wheels defaults to use released wheels after the repositories are public.
  4. Verify the demo-data installer's default clone URLs work anonymously.
  5. Review its E2E workflow's SIBLING_REPOS_TOKEN dependency.
  6. Restore (uncomment) the release badge in README.md.
  7. Delete the SIBLING_REPOS_TOKEN repository secret and revoke the underlying fine-grained PAT. The SIBLING_REPOS_TOKEN line in the semantic-release step of .github/workflows/release.yml can stay or go: the release gate falls back to anonymous access when it is empty.
  8. Re-enable the public curl | sh install path in docs/demo.md and README.md (delete the temporary notes), then run task docs:generate so the in-app help copy follows.
  9. Make the GHCR packages public. Packages created while the repositories are private default to private, so docker pull of a release image needs authentication; the compose files and docs/demo.md assume it does not. That includes egress-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.
  10. 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_RUNNER variable 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.

WorkflowRunner
ci.ymlCI_RUNNER
ci-heavy.yml (all jobs, docs-screenshots included)CI_RUNNER
demo-e2e.ymlCI_RUNNER
semgrep.ymlCI_RUNNER
docker-publish.ymlCI_RUNNER
release.ymlGitHub-hosted, always
docs-ai-review, docs-pr-review, audit-pr-reviewGitHub-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).

Released as open source under the AGPL-3.0-or-later license. Development is sponsored by Rexonix s.r.o.. Contact — [email protected].