build.yml builds the production image twice per release #62

Closed
opened 2026-08-16 12:01:37 +02:00 by myrmidex · 2 comments
Owner

Cutting v0.4.0 produced three action runs from one push. Two were correct; one was waste.

Run Trigger Verdict
build.yml on main branches: [main] Redundant
build.yml on v0.4.0 tags: ['v*'] Correct — the release image
images.yml on main docker/build/** changed Correct — first publish of the CI image to main, one-off

The problem

.forgejo/workflows/build.yml:

on:
  push:
    branches: [main]
    tags: ['v*']

A release merges to main and gets a tag pointing at that same commit, so both conditions match. The production image is built twice from identical source.

Worse, per the Determine tags step both runs push :latest — the branch run pushes :latest alone, the tag run pushes :v0.4.0 and :latest. So :latest is published twice, and this is the multi-arch QEMU build (linux/amd64,linux/arm64), the most expensive job in the repo.

Fix

Drop the branch trigger; tags are what mark a release:

on:
  push:
    tags: ['v*']

The Determine tags step has a branch/tag conditional that becomes dead once the branch trigger is gone — simplify it to the tag case rather than leaving an unreachable branch.

Trade-off, accepted: pushes to main without a tag will no longer refresh :latest. That matches how releases actually happen here (merge, then tag), and the tag run still publishes :latest alongside the version.

Same class of problem as the duplicate ci.yml runs found during v0.4.0 — overlapping triggers matching a single push. There the duplication was kept deliberately, so that pushes to a release branch without an open PR still get CI. Here there is no such benefit: both runs do identical work.

Acceptance criteria

  • build.yml triggers on v* tags only
  • Determine tags simplified; no unreachable branch
  • A tagged release publishes :vX.Y.Z and :latest exactly once
  • Multi-arch (linux/amd64,linux/arm64) still works
Cutting v0.4.0 produced three action runs from one push. Two were correct; one was waste. | Run | Trigger | Verdict | |---|---|---| | `build.yml` on `main` | `branches: [main]` | **Redundant** | | `build.yml` on `v0.4.0` | `tags: ['v*']` | Correct — the release image | | `images.yml` on `main` | `docker/build/**` changed | Correct — first publish of the CI image to `main`, one-off | ## The problem `.forgejo/workflows/build.yml`: ```yaml on: push: branches: [main] tags: ['v*'] ``` A release merges to `main` **and** gets a tag pointing at that same commit, so both conditions match. The production image is built twice from identical source. Worse, per the `Determine tags` step both runs push `:latest` — the branch run pushes `:latest` alone, the tag run pushes `:v0.4.0` and `:latest`. So `:latest` is published twice, and this is the multi-arch QEMU build (`linux/amd64,linux/arm64`), the most expensive job in the repo. ## Fix Drop the branch trigger; tags are what mark a release: ```yaml on: push: tags: ['v*'] ``` The `Determine tags` step has a branch/tag conditional that becomes dead once the branch trigger is gone — simplify it to the tag case rather than leaving an unreachable branch. **Trade-off, accepted:** pushes to `main` without a tag will no longer refresh `:latest`. That matches how releases actually happen here (merge, then tag), and the tag run still publishes `:latest` alongside the version. ## Related Same class of problem as the duplicate `ci.yml` runs found during v0.4.0 — overlapping triggers matching a single push. There the duplication was kept deliberately, so that pushes to a release branch without an open PR still get CI. Here there is no such benefit: both runs do identical work. ## Acceptance criteria - [ ] `build.yml` triggers on `v*` tags only - [ ] `Determine tags` simplified; no unreachable branch - [ ] A tagged release publishes `:vX.Y.Z` and `:latest` exactly once - [ ] Multi-arch (`linux/amd64,linux/arm64`) still works
myrmidex added this to the v0.4.1 milestone 2026-08-16 12:01:37 +02:00
myrmidex added the
enhancement
label 2026-08-16 12:01:37 +02:00
myrmidex self-assigned this 2026-08-16 12:01:37 +02:00
Author
Owner

Measured cost of the duplicate

The v0.4.0 release produced the real numbers:

Run Trigger Duration Result
#49 build.yml on main 36m12s success — the redundant one
#50 images.yml on main 7m56s success — correct, one-off
#51 build.yml on tag v0.4.0 22m+ the release image

So the duplicate costs over half an hour of runner time per release, not the "about a minute" I estimated when writing this ticket. That guess was based on the fast ci job; the production build is multi-arch under QEMU, where arm64 emulation dominates.

Both runs also push :latest, so the tag ends up overwriting what the branch run published minutes earlier — identical content, twice the work.

The fix in 27ca479 removes the branch trigger, leaving one build per release.

## Measured cost of the duplicate The v0.4.0 release produced the real numbers: | Run | Trigger | Duration | Result | |---|---|---|---| | #49 | `build.yml` on `main` | **36m12s** | success — the redundant one | | #50 | `images.yml` on `main` | 7m56s | success — correct, one-off | | #51 | `build.yml` on tag `v0.4.0` | 22m+ | the release image | So the duplicate costs **over half an hour of runner time per release**, not the "about a minute" I estimated when writing this ticket. That guess was based on the fast `ci` job; the production build is multi-arch under QEMU, where arm64 emulation dominates. Both runs also push `:latest`, so the tag ends up overwriting what the branch run published minutes earlier — identical content, twice the work. The fix in `27ca479` removes the branch trigger, leaving one build per release.
Author
Owner

Done — 27ca479

build.yml triggers on v* tags only. The branches: [main] trigger is gone, and Determine tags lost its now-unreachable branch/tag conditional — it always emits incr:<tag>,incr:latest.

Multi-arch (linux/amd64,linux/arm64) untouched.

Acceptance criteria

  • build.yml triggers on v* tags only
  • Determine tags simplified; no unreachable branch
  • A tagged release publishes :vX.Y.Z and :latest exactly once
  • Multi-arch still works

The last two can only be confirmed by an actual release: with the branch trigger gone, nothing exercises build.yml until v0.4.1 is tagged. Closing on the code being correct; if the v0.4.1 tag produces two runs or a broken image, reopen.

Worth watching at that point: exactly one build.yml run should appear for the tag, where v0.4.0 produced two (36m12s on main, plus the tag run).

## Done — `27ca479` `build.yml` triggers on `v*` tags only. The `branches: [main]` trigger is gone, and `Determine tags` lost its now-unreachable branch/tag conditional — it always emits `incr:<tag>,incr:latest`. Multi-arch (`linux/amd64,linux/arm64`) untouched. ### Acceptance criteria - [x] `build.yml` triggers on `v*` tags only - [x] `Determine tags` simplified; no unreachable branch - [ ] A tagged release publishes `:vX.Y.Z` and `:latest` exactly once - [ ] Multi-arch still works The last two can only be confirmed by an actual release: with the branch trigger gone, nothing exercises `build.yml` until v0.4.1 is tagged. Closing on the code being correct; if the v0.4.1 tag produces two runs or a broken image, reopen. Worth watching at that point: exactly one `build.yml` run should appear for the tag, where v0.4.0 produced two (36m12s on `main`, plus the tag run).
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: lvl0/incr#62
No description provided.