Repository navigation
feat(erpact): build ERPact images into the Forgejo registry - #1656
Conversation
The ERPact copy runs images that exist only as prebuilt Docker Hub images built by hand. This adds a reproducible build from the Forgejo sources into the private registry git.mctl.ai/erpact, kept apart from ghcr.io/mctlhq because those images are a customer's code. - erpact-images.yaml: workflow_dispatch on main only. Reads a read-only source token and a package-write token from Vault through GitHub OIDC (new role erpact-images, bound to this file on main), builds to an OCI archive and pushes with regctl in 20 MB chunks, since Cloudflare rejects bodies over 100 MB and Traefik cuts requests at 60 s. Refuses a tag that already exists; only a 404 counts as absent. - Forgejo reconcile: a scoped team whose units are exactly ["repo.packages"] may list no repositories. That is the registry-only account (Forgejo grants org packages by team permission). Proven both ways by tests/test_forgejo_reconcile_teams.py, which evaluates the real expressions with OpenTofu. - Vault policy and role recipe, runbook section on the registry. Claude-Session: https://claude.ai/code/session_01D1kGUJMWeA8hyhQuxKuHd2
|
Claude finished @mashkovd's task in 4m 29s —— View job Review complete ✅
Verdict: APPROVE — 0 P1, 0 P2, 0 P3 findings. Reviewed against
Note: |
The registry shares forgejo-data with the repositories, so cap each owner at 4G and have erpact-images.yaml keep the newest 10 semver tags per image. Forgejo's cleanup rules are web-UI only (no API), so they cannot be declared; the prune step lists all pages before deleting anything and refuses to prune if the version just pushed is not listed. Builds no longer attach provenance/SBOM attestations, so each tag is one plain manifest and pruning leaves no untagged children behind. Claude-Session: https://claude.ai/code/session_01D1kGUJMWeA8hyhQuxKuHd2
Antigravity (agy) review — informationalModel: gemini-3.8-flash-medium · commit No significant issues found. |
|
Claude finished @mashkovd's task in 3m 20s —— View job Review complete ✅
Verdict: APPROVE (commit Reviewed the full diff (
1 P3 nit (non-blocking), posted inline: the top-of-file comment in No P1/P2 findings. |
Antigravity (agy) review — informationalModel: gemini-3.8-flash-medium · commit No significant issues found. |
There was a problem hiding this comment.
No P1/P2 findings (0 P3). Good to merge. erpact-images.yaml, the reconcile.yaml precondition change, and the new OpenTofu-backed test were reviewed in detail: credential handling (OIDC->Vault, scoped tokens, revoke-self, umask 077 temp files, masked secrets, cleanup on always()), input validation (semver, single-line, ref-injection guard before the fetch), the packages-only team precondition (exact-match on units, cross-org repo check preserved), and the 12-case test all check out.
| # source-token read:repository - the only credential the build sees | ||
| # (BuildKit secret in the fetch step of the recipe) | ||
| # registry-token write:package - used only by the push step, after the | ||
| # build has finished; the build never sees it |
There was a problem hiding this comment.
P3: This comment says registry-token is "used only by the push step, after the build has finished." It's actually also read by Refuse an existing tag (before the build even starts) and by Prune old versions (after push). The security property that matters — "the build never sees it" — is still true, but the "only the push step" framing is inaccurate and could mislead someone auditing credential scope later.
Why
The ERPact copy (tenant
erpact) runs images that exist only as prebuiltDocker Hub images, built by hand outside any repository. This adds a
reproducible build from the Forgejo sources into the private registry
git.mctl.ai/erpact/*. It stays out ofghcr.io/mctlhqbecause these imagesare a customer's code: the registry's pull credential must reach the erpact
org only.
The build recipe itself (Containerfile, apps.lock, constraints) lives in
Forgejo
erpact/mctl-appsunderimages/, in a separate Forgejo PR. Nocustomer code is in this repository.
What
.github/workflows/erpact-images.yaml:workflow_dispatchwith image(
frappe-customer/frappe-control/site-deployer), version (semver)and source_ref.
mainonly. It logs into Vault through GitHub OIDC as the newrole
erpact-images, which is bound to this file@refs/heads/main.There are no repo secrets and no fallback.
read:repositoryis the only credentialthe build sees (a BuildKit secret);
write:packageis used by the pushstep only.
regctlin 20 MB chunks.Cloudflare (Free) rejects bodies over 100 MB with 413, and Traefik ends a
request whose body takes over 60 s. This was measured on 2026-10-04 with
a throwaway 300 MB layer, which was then deleted.
answer fails the run.
reconcile.yaml): a scoped team whose units are exactly["repo.packages"]may list no repositories.holds in the org (
services/packages/perm.go), whatever the units. So ateam like this is how an account gets the registry and no repository.
tests/test_forgejo_reconcile_teams.pyand a newforgejo-reconcile-teamsCI job. The test copies the
repos/scoped_teamslocals and theprecondition out of
main.tfverbatim and evaluates them with OpenTofuacross 12 cases.
packages-only cases.
contains(units, "repo.packages")fails the two mixed-unitcases.
github-actions-erpact-imagesand the role recipe invault-config/README.md. These are a manual apply, not done.[packages] LIMIT_TOTAL_OWNER_SIZE = 4Gper owner in the Forgejochart config. Changing it restarts the Forgejo pod once; the strategy is
Recreate, so expect about a minute of downtime on sync.ones after a successful push.
version just pushed is not in the listing.
declared.
manifest.
docs/runbooks/forgejo.md.Known behaviour (accepted)
Forgejo grants org packages by the highest team permission. Org members with
write therefore can push and delete erpact images too, not only
erpact-ci;that includes Developers and the
erpact-deployerbot. This is documented inthe runbook and accepted as is.
Not in this PR / order of operations
erpact-cito the reconcile manifest inVault, with teams ImageSources (read,
repo.code, 6 source repos) andPackagers (write,
repo.packages). The manifest is written with CAS onthe version read. Writing the manifest before step 1 would fail the
reconcile CronJob.
secret/platform/forgejo/erpact-images.The pull secret in ns
erpactand theimagePullSecretswiring are aseparate follow-up PR. Switching the running images is another one after a
successful build.
No overlap with #1648: it reserves
ghcr.io/mctlhqnames, and this workflowneither pushes there nor touches
build-image.yamlorimage-names.txt.Tests
python3 tests/test_forgejo_reconcile_teams.py: 12 cases, green, and redon both mutations.
python3 tests/test_forgejo_reconcile_pii.py: green.actionlintis clean onerpact-images.yaml.🤖 Generated with Claude Code
https://claude.ai/code/session_01D1kGUJMWeA8hyhQuxKuHd2