Release automation: tag → build → push → Forgejo release with changelog #4

Closed
opened 2026-07-07 16:51:21 +00:00 by claude-bot · 3 comments
Contributor

Goal

Cutting a release should be a single manual git tag; everything downstream is automated.

Scope

  • .forgejo/workflows/release.yml triggered on v* tags.
  • On tag:
    • Build the linux/amd64 image and push to the Forgejo registry (:<version> and :latest).
    • Create a Forgejo release for the tag with generated/attached release notes.
    • Source notes from CHANGELOG.md for the matching version section.
  • Keep the human step to just git tag vX.Y.Z && git push --tags.

Acceptance criteria

  • Pushing a vX.Y.Z tag builds and publishes images automatically
  • A Forgejo release object is created for the tag
  • Release body reflects the CHANGELOG entry for that version

Depends on: registry push, CHANGELOG.md.

Amended 2026-08-31 by #79 — "Build multi-arch images" is now a single linux/amd64 build; arm64 was dropped.

## Goal Cutting a release should be a single manual `git tag`; everything downstream is automated. ## Scope - `.forgejo/workflows/release.yml` triggered on `v*` tags. - On tag: - Build the `linux/amd64` image and push to the Forgejo registry (`:<version>` and `:latest`). - Create a Forgejo release for the tag with generated/attached release notes. - Source notes from `CHANGELOG.md` for the matching version section. - Keep the human step to just `git tag vX.Y.Z && git push --tags`. ## Acceptance criteria - [ ] Pushing a `vX.Y.Z` tag builds and publishes images automatically - [ ] A Forgejo release object is created for the tag - [ ] Release body reflects the CHANGELOG entry for that version Depends on: registry push, CHANGELOG.md. > **Amended 2026-08-31 by #79** — "Build multi-arch images" is now a single `linux/amd64` build; arm64 was dropped.
claude-bot added this to the v1.0.0 milestone 2026-07-15 15:33:04 +00:00
Author
Contributor

Already implemented in .forgejo/workflows/release.yml: on a v* tag it builds+pushes the multi-arch image (see #3), extracts the matching ## [version] section from CHANGELOG.md as release notes (awk), and creates a Forgejo release via the API. Human step is just git tag vX.Y.Z && git push --tags, as intended.

Before this works at release time, same prerequisites as #3 (registry enabled + Actions token packages:write/contents:write) — the release-creation curl also needs contents:write on the token. And when cutting v1.0.0 (#10), the ## [1.0.0] CHANGELOG section must have its date finalized so the notes extract cleanly.

Recommend the same v0.0.1-rc1 dry-run to validate build→push→release end-to-end before the real tag. Leaving open until validated by a tag run (part of #10's flow).

**Already implemented** in `.forgejo/workflows/release.yml`: on a `v*` tag it builds+pushes the multi-arch image (see #3), extracts the matching `## [version]` section from `CHANGELOG.md` as release notes (awk), and creates a Forgejo release via the API. Human step is just `git tag vX.Y.Z && git push --tags`, as intended. **Before this works at release time**, same prerequisites as #3 (registry enabled + Actions token `packages:write`/`contents:write`) — the release-creation `curl` also needs `contents:write` on the token. And when cutting v1.0.0 (#10), the `## [1.0.0]` CHANGELOG section must have its date finalized so the notes extract cleanly. Recommend the same `v0.0.1-rc1` dry-run to validate build→push→release end-to-end before the real tag. Leaving open until validated by a tag run (part of #10's flow).
Author
Contributor

Release pipeline proven end to end — first execution in the repo's history

Cut a throwaway v0.0.1-rc1 tag against main (run #217, job Publish image & release). Succeeded on the first attempt, and every leg was verified by its effect rather than by reading the log.

Acceptance criteria

  • Pushing a vX.Y.Z tag builds and publishes images automaticallygit.rhoving.com/rbrooks/iris-wled:0.0.1-rc1 landed. Pulled it on the dev host: linux/amd64, 87 MB, digest sha256:d71fa897…, distinct from :main as expected.
  • A Forgejo release object is created for the tag — release id 471, prerelease: true, draft: false.
  • Release body reflects the CHANGELOG entry for that version — exercised the fallback path, correctly: there is no ## [0.0.1-rc1] section, so the awk produced nothing and the guard wrote Release v0.0.1-rc1 rather than an empty body. The happy path (a real version with a matching section) is still unproven; that only happens on a real release.

The parts that were genuinely at risk

Registry auth on the release path worked. This was the main unknown — REGISTRY_TOKEN was swapped in during #64 and release.yml had never run with it. Worth restating why a green login step proves nothing here: docker/login-action succeeds with a wrong-scope token and the failure only appears at the blob upload. The image existing in the registry is the proof.

The Forgejo release API call worked on secrets.GITHUB_TOKEN — a different credential from the registry push, so it could have failed independently even with the image published. It didn't.

The pre-release guard (#105) did its job

Registry tags after the run:

EXISTS   0.0.1-rc1
absent   latest      <- suppressed by the guard added in #105
absent   0.0         <- metadata-action skips {{major}}.{{minor}} for pre-releases
EXISTS   main        <- unaffected

Without #105, this run would have retagged :latest to a disposable RC build. Since the run succeeded and :latest does not exist, the guard demonstrably fired.

Cleanup

  • Forgejo release deleted

  • Git tag deleted, local and remote — the repo is back to zero tags

  • Registry tag :0.0.1-rc1 still exists. Deleting a container package version needs the Forgejo packages API, and the MCP server exposes no package tools while my shell has no FORGEJO_ACCESS_TOKEN. It is harmless but untidy:

    curl -X DELETE -H "Authorization: token $FORGEJO_ACCESS_TOKEN" \
      "$FORGEJO_URL/api/v1/packages/rbrooks/container/iris-wled/0.0.1-rc1"
    

    This is the same registry-hygiene gap as the accumulating :sha-* tags — an owner-level cleanup rule would handle both.

What remains unproven

The {{major}}.{{minor}} and :latest tags have never actually been pushed, because a pre-release deliberately suppresses both. They are the same push step with the same credentials as 0.0.1-rc1, so the risk is low — but "low" is not "verified", and the first real proof will be v1.0.0 itself.

#4 is done. This also unblocks #3's remaining criterion and #104, which needs :X.Y.Z to exist before an IaC role can pin one.

## Release pipeline proven end to end — first execution in the repo's history Cut a throwaway `v0.0.1-rc1` tag against `main` (run [#217](https://git.rhoving.com/rbrooks/Iris-WLED/actions/runs/217), job *Publish image & release*). **Succeeded on the first attempt**, and every leg was verified by its effect rather than by reading the log. ### Acceptance criteria - [x] **Pushing a `vX.Y.Z` tag builds and publishes images automatically** — `git.rhoving.com/rbrooks/iris-wled:0.0.1-rc1` landed. Pulled it on the dev host: `linux/amd64`, 87 MB, digest `sha256:d71fa897…`, distinct from `:main` as expected. - [x] **A Forgejo release object is created for the tag** — release id 471, `prerelease: true`, `draft: false`. - [x] **Release body reflects the CHANGELOG entry for that version** — exercised the *fallback* path, correctly: there is no `## [0.0.1-rc1]` section, so the `awk` produced nothing and the guard wrote `Release v0.0.1-rc1` rather than an empty body. The happy path (a real version with a matching section) is still unproven; that only happens on a real release. ### The parts that were genuinely at risk **Registry auth on the release path worked.** This was the main unknown — `REGISTRY_TOKEN` was swapped in during #64 and `release.yml` had never run with it. Worth restating why a green login step proves nothing here: `docker/login-action` succeeds with a wrong-scope token and the failure only appears at the blob upload. The image existing in the registry is the proof. **The Forgejo release API call worked** on `secrets.GITHUB_TOKEN` — a different credential from the registry push, so it could have failed independently even with the image published. It didn't. ### The pre-release guard (#105) did its job Registry tags after the run: ``` EXISTS 0.0.1-rc1 absent latest <- suppressed by the guard added in #105 absent 0.0 <- metadata-action skips {{major}}.{{minor}} for pre-releases EXISTS main <- unaffected ``` Without #105, this run would have retagged `:latest` to a disposable RC build. Since the run succeeded and `:latest` does not exist, the guard demonstrably fired. ### Cleanup - [x] Forgejo release deleted - [x] Git tag deleted, local and remote — the repo is back to **zero tags** - [ ] **Registry tag `:0.0.1-rc1` still exists.** Deleting a container package version needs the Forgejo packages API, and the MCP server exposes no package tools while my shell has no `FORGEJO_ACCESS_TOKEN`. It is harmless but untidy: ```sh curl -X DELETE -H "Authorization: token $FORGEJO_ACCESS_TOKEN" \ "$FORGEJO_URL/api/v1/packages/rbrooks/container/iris-wled/0.0.1-rc1" ``` This is the same registry-hygiene gap as the accumulating `:sha-*` tags — an owner-level cleanup rule would handle both. ### What remains unproven The `{{major}}.{{minor}}` and `:latest` tags have never actually been pushed, because a pre-release deliberately suppresses both. They are the same push step with the same credentials as `0.0.1-rc1`, so the risk is low — but "low" is not "verified", and the first real proof will be `v1.0.0` itself. **#4 is done.** This also unblocks #3's remaining criterion and #104, which needs `:X.Y.Z` to exist before an IaC role can pin one.
Author
Contributor

The two legs left unproven at close are now proven — by the real v1.0.0 release

The closing comment above was careful to say what the v0.0.1-rc1 dry run could not establish:

The {{major}}.{{minor}} and :latest tags have never actually been pushed, because a pre-release deliberately suppresses both. […] "low" is not "verified", and the first real proof will be v1.0.0 itself.

The happy path (a real version with a matching section) is still unproven; that only happens on a real release.

v1.0.0 has since been cut (tag 0251a6a, release created 2026-09-01T19:59:22Z). Checked by effect rather than by reading the workflow:

Registry tags — queried anonymously via /v2/rbrooks/iris-wled/tags/list:

1.0.0     <- type=semver,pattern={{version}}
1.0       <- type=semver,pattern={{major}}.{{minor}}   ← was never proven
latest    <- type=raw, enabled because v1.0.0 has no "-" suffix   ← was never proven
main      <- unaffected, still the CI-published tag

Both tags the RC run suppressed are present. The !contains(github.ref_name, '-') guard from #105 therefore behaves correctly in both directions: it withheld :latest from v0.0.1-rc1 and released it for v1.0.0. A guard only ever tested in its blocking direction is half-tested, so this closes that gap too.

Release object — id 476, tag_name: v1.0.0, draft: false, prerelease: false. The pre-release detection also inverted correctly: true for the RC, false here.

CHANGELOG extraction, happy path — the release body is 16,218 characters and byte-identical to the ## [1.0.0] section of CHANGELOG.md. Compared programmatically by re-running the workflow's own awk logic against the file and diffing, not by eye:

changelog section chars: 16218
body == extracted section: True

So the awk extracts the right section, stops at the next ## [, and the guard correctly does not fire when there is real content. Both branches of that step are now exercised.

Every acceptance criterion is met, and nothing about this workflow remains unverified. Recording it here because the issue closed with a caveat, and a reader would otherwise have to go and check.

The practical consequence is for #104: :X.Y.Z exists, so that issue is no longer blocked.

## The two legs left unproven at close are now proven — by the real v1.0.0 release The closing comment above was careful to say what the `v0.0.1-rc1` dry run could *not* establish: > The `{{major}}.{{minor}}` and `:latest` tags have never actually been pushed, because a pre-release deliberately suppresses both. […] "low" is not "verified", and the first real proof will be `v1.0.0` itself. > The happy path (a real version with a matching section) is still unproven; that only happens on a real release. `v1.0.0` has since been cut (tag `0251a6a`, release created 2026-09-01T19:59:22Z). Checked by effect rather than by reading the workflow: **Registry tags** — queried anonymously via `/v2/rbrooks/iris-wled/tags/list`: ``` 1.0.0 <- type=semver,pattern={{version}} 1.0 <- type=semver,pattern={{major}}.{{minor}} ← was never proven latest <- type=raw, enabled because v1.0.0 has no "-" suffix ← was never proven main <- unaffected, still the CI-published tag ``` Both tags the RC run suppressed are present. The `!contains(github.ref_name, '-')` guard from #105 therefore behaves correctly in *both* directions: it withheld `:latest` from `v0.0.1-rc1` and released it for `v1.0.0`. A guard only ever tested in its blocking direction is half-tested, so this closes that gap too. **Release object** — id 476, `tag_name: v1.0.0`, `draft: false`, `prerelease: false`. The pre-release detection also inverted correctly: `true` for the RC, `false` here. **CHANGELOG extraction, happy path** — the release body is **16,218 characters and byte-identical** to the `## [1.0.0]` section of `CHANGELOG.md`. Compared programmatically by re-running the workflow's own awk logic against the file and diffing, not by eye: ``` changelog section chars: 16218 body == extracted section: True ``` So the `awk` extracts the right section, stops at the next `## [`, and the guard correctly does *not* fire when there is real content. Both branches of that step are now exercised. **Every acceptance criterion is met, and nothing about this workflow remains unverified.** Recording it here because the issue closed with a caveat, and a reader would otherwise have to go and check. The practical consequence is for #104: `:X.Y.Z` exists, so that issue is no longer blocked.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Iris-WLED#4
No description provided.