CI/CD and releases
Pipeline
.forgejo/workflows/ci.yaml runs on pushes to main, on v*.*.* tags and
on pull requests. Every step is a mise task, so a local mise run check
(also run by the pre-push hook) predicts the pipeline result.
| Job | Runs on | Steps |
|---|---|---|
verify | PR, push to main | commits:check → check (fmt, tidy, vet, lint, test + coverage, govulncheck, manifests) → snapshot |
changelog | after verify | PR: changelog:preview --pr · main: release:publish, then release:pr · tag: release:publish |
The changelog job is always last: it only runs once the code is verified.
.forgejo/workflows/docs.yaml publishes this documentation separately, on
pushes to main that touch the docs (or files the docs embed). It runs
docs:check, then docs:publish, which builds the site and syncs it to the
web host with rsync --delete over SSH. Keeping it out of the CI workflow
means the changelog job stays last there, and a failed upload never blocks a
release.
Release flow
Releases are never tagged by hand.
- Merging feature/fix PRs into
mainreleases nothing. - After every push to
main,release:prasks git-cliff for the next semver version. If there are user-facing commits (feat,fix,perfor breaking changes) since the last tag, it regeneratesCHANGELOG.mdon the branchrelease/nextand opens or updates the PRchore(release): :bookmark: vX.Y.Z. If there are none, it closes a stale release PR. - To release, merge that PR (merge, squash or rebase all work).
- The push to
mainrunsrelease:publish. It reads the newest version fromCHANGELOG.md, creates the tag, and runs goreleaser in the same job: binaries for linux/darwin × amd64/arm64, a multi-arch image atgit.bitfoo.de/dvonessen/forgemirror, SBOMs, cosign signatures, and the Forgejo release with git-cliff release notes. Then it packages the Helm chart with the same version, pushes it tooci://git.bitfoo.de/dvonessen/charts/forgemirrorand signs it with cosign.
Why the tag and the release happen in one job: tags and pushes made with the
automatic Actions token start no new workflow run
(forgejo#11446). The same
rule keeps the changelog from triggering an extra pipeline. For the same
reason, release/next is pushed with RELEASE_TOKEN. Pushed with the
automatic token, the release PR would get no checks.
Version bumps (git-cliff, cliff.toml): feat → minor, breaking change
(! or BREAKING CHANGE:) → major, everything else → patch. The first
release is v0.1.0.
One-time setup
Secrets (repository → Settings → Actions → Secrets)
| Secret | Purpose |
|---|---|
RELEASE_TOKEN | PAT with write:repository; pushes release/next and opens the release PR |
PACKAGE_RW_TOKEN | PAT with write:package; pushes image and chart |
COSIGN_PRIVATE_KEY | contents of cosign.key |
COSIGN_PASSWORD | password of cosign.key |
GH_TOKEN | optional; GitHub token so mise’s tool downloads are not rate-limited |
DOCS_SSH_KEY | base64-encoded private SSH key allowed to write the docs document root |
Variables (repository → Settings → Actions → Variables)
| Variable | Purpose |
|---|---|
DOCS_DEPLOY_TARGET | rsync target, user@host:/document/root/; the docs job is skipped while unset |
DOCS_BASE_URL | public URL of the site, e.g. https://forgemirror.example.org/ |
DOCS_SSH_KNOWN_HOSTS | pinned host key line(s) of the web host (ssh-keyscan host, verified once by hand) |
Create the signing key pair locally and commit only the public key:
mise exec -- cosign generate-key-pair # writes cosign.key and cosign.pubStore cosign.key and its password as secrets, commit cosign.pub, and
delete cosign.key from disk.
Keyless signing is not an option: Sigstore’s Fulcio would have to reach the Forgejo OIDC issuer, and the instance is not internet-exposed. Signatures are still recorded in the public Rekor transparency log, which publishes only hashes and the public key.
Branch protection (repository → Settings → Branches)
Rule for main:
- Disable direct pushes; changes land via pull requests only.
- Required status checks:
CI / verify (pull_request)andCI / changelog (pull_request). - Enable “Block merge on outdated branch”, so checks always ran against the
current
main. - Disable force pushes and branch deletion.
- Optional: require one approval. The release bot’s PR then needs your review, which is the point of having a release PR.
Rule for release/*:
- Allow force pushes only for the account that owns
RELEASE_TOKEN; the bot regenerates the branch on every run.
Tags: protect v* so that only the Actions token (release job) and
administrators can create release tags.
Verifying a release
cosign verify-blob --key cosign.pub \
--bundle checksums.txt.sigstore.json checksums.txt
sha256sum --check --ignore-missing checksums.txt
cosign verify --key cosign.pub git.bitfoo.de/dvonessen/forgemirror:<version>