| Filename | Latest commit message | Latest commit date |
|---|---|---|
Phase 1 replaces the docker/* actions with shell and pins what's left; phase 2 sets up Renovate to keep the pins current. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
| .forgejo/workflows | ||
| cosign.pub | ||
| README.md | ||
| TODO.md | ||
hitoma-image-ci
The shared build pipeline for the hitoma-* image repos, as a Forgejo Actions reusable workflow. Each run:
- builds the image and loads it into the runner's Docker daemon, without pushing it
- runs the calling repo's
test/verify-image.sh IMAGE, thentest/smoke.sh IMAGE - scans the image with Trivy and generates its SBOM
- pushes this build's tags (not on pull requests)
- signs the pushed image with cosign, and attaches the SBOM and the scan as signed attestations (not on pull requests)
- force-pushes a vulnerability badge, the full report and the SBOM to the calling repo's
badgesbranch (mainonly) - fails the run if there are fixable HIGH or CRITICAL findings
A failed build or test stops the run before anything is pushed. A failed scan doesn't block the push: the image is still published, and the red run shows a package needs patching.
This repo must stay public: Forgejo only runs reusable workflows from public repositories.
Using it
The calling repo keeps its triggers and resolves its own upstream version, then hands off:
jobs:
upstream:
runs-on: docker
outputs:
version: ${{ steps.upstream.outputs.version }}
steps: [...]
image:
needs: upstream
uses: root/hitoma-image-ci/.forgejo/workflows/image.yml@v1
with:
version: ${{ needs.upstream.outputs.version }}
build-args: |
FOO_VERSION=${{ needs.upstream.outputs.version }}
secrets: inherit
with: can't read the env context, so pass any values from env through job outputs.
| Input | Default | Purpose |
|---|---|---|
version |
blank | Adds a version tag and the org.opencontainers.image.version label. Leave it blank if there's no upstream version. |
extra-tags |
blank | More metadata-action tag rules, one per line, such as a series or major-version tag |
build-args |
blank | KEY=value lines. CREATED (the image creation time) is always added. |
Tags on main: latest, the version, any extra tags, YYYYMMDD, and sha-<short>. Pull requests only get sha-<short>, and nothing is pushed.
The calling repo needs:
test/verify-image.shandtest/smoke.sh, each taking the image reference as its only argument. Both are required.- Repository secrets
REGISTRY_USERandREGISTRY_TOKEN(a Forgejo access token withwrite:packagescope). - A runner with the
dockerlabel whose job containers can reach a Docker daemon, such as adocker:dindsidecar throughDOCKER_HOST. The smoke tests start their own containers on that daemon.
Signing
Every pushed image is signed with the key pair whose public half is cosign.pub. Two signed attestations go with it: the CycloneDX SBOM (cyclonedx) and the Trivy scan (vuln). Nothing is uploaded to Sigstore's public transparency log (Rekor), so verification needs --insecure-ignore-tlog. The signature is checked against the public key alone.
IMAGE=forgejo.hitoma.com/root/hitoma-caddy:latest
# Signature
cosign verify --key cosign.pub --insecure-ignore-tlog "$IMAGE"
# SBOM: the attestation's payload is base64; .predicate is the CycloneDX document
cosign verify-attestation --key cosign.pub --insecure-ignore-tlog --type cyclonedx "$IMAGE" \
| jq -r .payload | base64 -d | jq .predicate
# Scan results from when the image was built
cosign verify-attestation --key cosign.pub --insecure-ignore-tlog --type vuln "$IMAGE" \
| jq -r .payload | base64 -d | jq .predicate.scanner.result
The registry has no OCI referrers API, so cosign keeps all three in one tag next to the image: sha256-<image digest>.
The pipeline signs only when COSIGN_PRIVATE_KEY and COSIGN_PASSWORD are available. Set them once as user-level Actions secrets of root (Settings → Actions → Secrets), and every repo gets them through secrets: inherit. Without them, the run warns and pushes the image unsigned.
To replace the key, generate a new pair, update both secrets and cosign.pub, and move the v1 tag. Images signed with the old key keep their old signatures until they're rebuilt, which happens weekly.
Registry cleanup
Every build of main pushes a new sha-<short> tag and a date tag. One Forgejo package cleanup rule for the root owner removes them once they're old enough. Configure it under Settings → Packages → Cleanup rules:
| Setting | Value |
|---|---|
| Enabled | yes |
| Type | container |
| Apply pattern to full package name | no |
| Keep the most recent | 10 |
| Keep versions matching | buildcache |
| Remove versions older than | 30 days |
| Remove versions matching | sha-[0-9a-f]+|[0-9]{8}|sha256-[0-9a-f]{64} |
What stays:
- Always kept:
latest(Forgejo never removes it),buildcache, and the upstream version tags (2026.8.3,v0.30.2,15and so on), which don't match the remove pattern. - Removed:
sha-*,YYYYMMDDand signature (sha256-<digest>) tags more than 30 days old, unless they're among a package's 10 newest versions. Every live tag is rebuilt and re-signed weekly, so only signatures of images that have been replaced reach that age. If rebuilds stop for more than 30 days, the current image's signature can be removed too. - Digests: untagged
sha256:versions are never removed by the rule. These are the image and attestation manifests behind the indexes the old buildx pipelines pushed. Forgejo's own cleanup job deletes them once no remaining tag references them. Unreferenced blobs go the same way, which is what frees the disk space.
A date tag points at that day's last build. An earlier build from the same day keeps only its sha-* tag.
Use the rule's preview to check what it would delete before enabling it.
Versions
Callers pin @v1. Changes that don't break callers move the v1 tag forward:
git tag -f v1 && git push -f origin v1
A change that needs callers to change (a new required input, a different test convention) gets v2, and callers move over one at a time.