fix(release): push via forge.int.wynning.tech, sign + reference as forge.wynning.tech (#75) #76

Merged
james merged 1 commit from 75-internal-registry-url into main 2026-06-17 14:40:23 +00:00
Owner

Closes #75.

Why

The first tag push after the keypair was registered failed: Cloudflare's per-request body-size limit blocks container layer uploads to forge.wynning.tech. The release workflow needed to push via the internal hostname that bypasses CF.

Approach — split URL

Rather than swap every URL reference to forge.int.wynning.tech, the fix splits the URL into two roles. The internal hostname is the transport for big uploads; the public hostname is the canonical identity that flows into image labels, the cosign sign target, the signed docker-reference, the SLSA predicate's builder URL, and the verification command in the release notes.

Role URL Used for
PUSH_REGISTRY forge.int.wynning.tech docker push of image layers (CF would block on public)
PUBLIC_REGISTRY forge.wynning.tech image labels, cosign sign target, signed docker-reference, SLSA predicate, verify command in release notes, cosign.pub URL

Same registry backend, two URLs. The image is tagged with both hostnames locally, pushed only via the internal URL, then signed against the public reference. cosign signature artifacts are tiny (KBs) and upload fine through Cloudflare, so signing the public reference works and produces a signature that any downstream verifier can check against the canonical name.

Files

File Change
.forgejo/workflows/release.yml REGISTRYPUSH_REGISTRY + PUBLIC_REGISTRY; two docker/login-action steps (one per URL); build tags with both, pushes via internal; image_ref output uses public; cosign sign/attest target public; release notes / cosign.pub URL use public
docs/ci.md "Build and push" step explains the int-for-transport / public-for-identity split
CLAUDE.md Release pipeline bullet mentions the split explicitly

Acceptance criteria

  • A fresh tag push (e.g. v0.0.1-rc.1) produces a pushed image, a valid cosign signature, a SLSA attestation, and a Forgejo release page. (CI to confirm on next tag push.)
  • cosign verify --key https://forge.wynning.tech/james/carol/raw/branch/main/cosign.pub forge.wynning.tech/james/carol@<digest> succeeds against the published image.
  • Docs and CLAUDE.md keep the public URL as canonical, internal URL only mentioned as transport for docker push.

Test plan

  • python3 -c 'import yaml; yaml.safe_load(open(".forgejo/workflows/release.yml"))' — passes.
  • semgrep scan --config .semgrep --config p/javascript --config p/typescript --config p/nodejsscan --config p/owasp-top-ten --severity ERROR .forgejo/workflows/release.yml — 0 findings (shell-injection fix from #16 still holds).
  • Push v0.0.1-rc.1 after merge; watch the workflow; confirm the push step completes; verify the signature against the public URL.
Closes #75. ## Why The first tag push after the keypair was registered failed: Cloudflare's per-request body-size limit blocks container layer uploads to `forge.wynning.tech`. The release workflow needed to push via the internal hostname that bypasses CF. ## Approach — split URL Rather than swap *every* URL reference to `forge.int.wynning.tech`, the fix splits the URL into two roles. The internal hostname is the transport for big uploads; the public hostname is the canonical identity that flows into image labels, the cosign sign target, the signed `docker-reference`, the SLSA predicate's builder URL, and the verification command in the release notes. | Role | URL | Used for | |---|---|---| | `PUSH_REGISTRY` | `forge.int.wynning.tech` | `docker push` of image layers (CF would block on public) | | `PUBLIC_REGISTRY` | `forge.wynning.tech` | image labels, cosign sign target, signed `docker-reference`, SLSA predicate, verify command in release notes, `cosign.pub` URL | Same registry backend, two URLs. The image is tagged with both hostnames locally, pushed only via the internal URL, then signed against the public reference. cosign signature artifacts are tiny (KBs) and upload fine through Cloudflare, so signing the public reference works and produces a signature that any downstream verifier can check against the canonical name. ## Files | File | Change | |---|---| | `.forgejo/workflows/release.yml` | `REGISTRY` → `PUSH_REGISTRY` + `PUBLIC_REGISTRY`; two docker/login-action steps (one per URL); build tags with both, pushes via internal; `image_ref` output uses public; cosign sign/attest target public; release notes / `cosign.pub` URL use public | | `docs/ci.md` | "Build and push" step explains the int-for-transport / public-for-identity split | | `CLAUDE.md` | Release pipeline bullet mentions the split explicitly | ## Acceptance criteria - [ ] A fresh tag push (e.g. `v0.0.1-rc.1`) produces a pushed image, a valid cosign signature, a SLSA attestation, and a Forgejo release page. *(CI to confirm on next tag push.)* - [ ] `cosign verify --key https://forge.wynning.tech/james/carol/raw/branch/main/cosign.pub forge.wynning.tech/james/carol@<digest>` succeeds against the published image. - [x] Docs and CLAUDE.md keep the public URL as canonical, internal URL only mentioned as transport for `docker push`. ## Test plan - [x] `python3 -c 'import yaml; yaml.safe_load(open(".forgejo/workflows/release.yml"))'` — passes. - [x] `semgrep scan --config .semgrep --config p/javascript --config p/typescript --config p/nodejsscan --config p/owasp-top-ten --severity ERROR .forgejo/workflows/release.yml` — 0 findings (shell-injection fix from #16 still holds). - [ ] Push `v0.0.1-rc.1` after merge; watch the workflow; confirm the push step completes; verify the signature against the public URL.
fix(release): push via forge.int.wynning.tech, sign + reference as forge.wynning.tech (#75)
All checks were successful
PR / OSV-Scanner (pull_request) Successful in 18s
Secrets / gitleaks (pull_request) Successful in 20s
PR / Trivy (image) (pull_request) Successful in 43s
PR / Typecheck (pull_request) Successful in 53s
PR / Static analysis (Semgrep) (pull_request) Successful in 57s
PR / Lint (pull_request) Successful in 57s
PR / npm audit (pull_request) Successful in 1m8s
PR / Test (sqlite) (pull_request) Successful in 1m9s
PR / Test (postgres) (pull_request) Successful in 1m9s
PR / Build (pull_request) Successful in 1m25s
31da8d1b62
The first tag push after the keypair was registered failed: Cloudflare's
per-request body-size limit blocks container layer uploads to
forge.wynning.tech. forge.int.wynning.tech is the same Forgejo registry
backend reached directly, so artifacts pushed there are still readable
through the public URL.

Split the URL into two roles:

  PUSH_REGISTRY   = forge.int.wynning.tech  (transport for big uploads)
  PUBLIC_REGISTRY = forge.wynning.tech       (canonical identity)

The image is tagged with both names locally, then pushed only via the
internal URL. Everything user-facing — image labels, cosign sign target,
the docker-reference embedded in the signature payload, the SLSA
predicate's builder URL, the verification command in the release notes —
references the public URL. cosign signature artifacts are tiny (KBs) and
upload fine through Cloudflare on the public URL, so signing the public
reference works and produces a signature any downstream verifier can
check against the canonical name.

Closes #75.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
james merged commit 907c67477f into main 2026-06-17 14:40:23 +00:00
Sign in to join this conversation.
No description provided.