chore: Publish images from one job in the six multi-image repositories #69

Closed
opened 2026-09-09 06:17:40 +00:00 by ahmad · 6 comments
Owner

Context

#66 serialised the publish job in templates/release.yml so a release cannot race its own image pushes. The template is copied into consumers, so the fix reaches a repository only when its own release.yml changes shape.

Two images from one release share base layers, and the registry inserts a blob by content hash. Pushing the same layer concurrently makes one insert lose a unique-constraint race (UQE_package_blob_blake2b) whose error path dereferences nil on the server; buildkit can only report failed to push …: unknown, so it reads as flaky CI.

The shape is one job with one step per image, not a matrix. max-parallel: 1 and job-level concurrency were both measured on this runner and both are ignored — three legs that should have serialised overlapped either way. Steps within one job are the only ordering this scheduler honours. 94b67b7 established this in the template; the analysis is in the comments below.

Surveyed from the API — every repository the server lists, release.yml fetched through the contents API and its publish job parsed — rather than from local checkouts, which see only what happens to be cloned here.

Repositories to change — six

Each publishes more than one image from a matrix, so their pushes run concurrently today:

Repository Images Becomes
ahmad/namey 3 — marketing, portal, api three steps
ahmad/imamah 2 — api, web two steps
ahmad/portfolio 2 — api, web two steps
ahmad/trip 2 — app, portal two steps
Bayan/kidpoints 2 — api, web two steps
Bayan/bayan-web 2 — into two different packages two steps

The matrix currently supplies matrix.service (or a whole include entry) to the Dockerfile path, both tags and both cache refs; each becomes a literal per step. Bayan/bayan-web and ahmad/namey use include with a per-image package name, so their steps differ by more than a suffix.

Bayan/bayan-web publishes into two separate packages and is still affected — the constraint is on the blob rather than the package, so two images sharing a base layer collide whatever they are named. Worth stating because "different packages, so no shared blob" is the natural and wrong assumption.

Not affected

ahmad/larda, ahmad/expiro and suhba/suhba-web already build two images from two sequential steps in one job — they are the worked example, not merely unaffected. Bayan/stem, suhba/khitta and suhba/suhba-docs publish a single image.

Scope

  • In: converting the publish job of the six repositories above from a matrix to one job with one step per image, carrying the reasoning across rather than the bare shape; one pull request per repository.
  • Out: adding max-parallel: 1 anywhere — it is inert on this runner.
  • Out: the Forgejo-side nil dereference (amtronics/infrastructure-vps); the repositories that already push serially; changing what any repository publishes.

Acceptance criteria

  • Given each of the six repositories, when its release.yml is read, then the publish job declares no strategy/matrix and has one docker/build-push-action step per image.
  • Given each of them, when the publish job is compared against the matrix it replaced, then every image is still built from the same context and Dockerfile, with the same tags, build-args and cache refs.
  • Given a release in one of them, when it runs, then every image it declares is published under both the version tag and the commit tag.
  • Given a release in one of them, when the step timings are read, then the pushes did not overlap.
  • Given the three repositories that already push serially, then they are unchanged.
### Context `#66` serialised the `publish` job in `templates/release.yml` so a release cannot race its own image pushes. The template is **copied** into consumers, so the fix reaches a repository only when its own `release.yml` changes shape. Two images from one release share base layers, and the registry inserts a blob by content hash. Pushing the same layer concurrently makes one insert lose a unique-constraint race (`UQE_package_blob_blake2b`) whose error path dereferences nil on the server; buildkit can only report `failed to push …: unknown`, so it reads as flaky CI. **The shape is one job with one step per image, not a matrix.** `max-parallel: 1` and job-level `concurrency` were both measured on this runner and both are ignored — three legs that should have serialised overlapped either way. Steps within one job are the only ordering this scheduler honours. `94b67b7` established this in the template; the analysis is in the comments below. Surveyed from the API — every repository the server lists, `release.yml` fetched through the contents API and its `publish` job parsed — rather than from local checkouts, which see only what happens to be cloned here. ### Repositories to change — six Each publishes more than one image from a matrix, so their pushes run concurrently today: | Repository | Images | Becomes | | --- | --- | --- | | `ahmad/namey` | 3 — `marketing`, `portal`, `api` | three steps | | `ahmad/imamah` | 2 — `api`, `web` | two steps | | `ahmad/portfolio` | 2 — `api`, `web` | two steps | | `ahmad/trip` | 2 — `app`, `portal` | two steps | | `Bayan/kidpoints` | 2 — `api`, `web` | two steps | | `Bayan/bayan-web` | 2 — into two different packages | two steps | The matrix currently supplies `matrix.service` (or a whole `include` entry) to the Dockerfile path, both tags and both cache refs; each becomes a literal per step. `Bayan/bayan-web` and `ahmad/namey` use `include` with a per-image package name, so their steps differ by more than a suffix. **`Bayan/bayan-web` publishes into two separate packages and is still affected** — the constraint is on the blob rather than the package, so two images sharing a base layer collide whatever they are named. Worth stating because "different packages, so no shared blob" is the natural and wrong assumption. ### Not affected `ahmad/larda`, `ahmad/expiro` and `suhba/suhba-web` already build two images from two sequential steps in one job — they are the worked example, not merely unaffected. `Bayan/stem`, `suhba/khitta` and `suhba/suhba-docs` publish a single image. ### Scope - In: converting the `publish` job of the six repositories above from a matrix to one job with one step per image, carrying the reasoning across rather than the bare shape; one pull request per repository. - Out: adding `max-parallel: 1` anywhere — it is inert on this runner. - Out: the Forgejo-side nil dereference (`amtronics/infrastructure-vps`); the repositories that already push serially; changing what any repository publishes. ### Acceptance criteria - [ ] Given each of the six repositories, when its `release.yml` is read, then the `publish` job declares no `strategy`/`matrix` and has one `docker/build-push-action` step per image. - [ ] Given each of them, when the `publish` job is compared against the matrix it replaced, then every image is still built from the same context and Dockerfile, with the same tags, build-args and cache refs. - [ ] Given a release in one of them, when it runs, then every image it declares is published under both the version tag and the commit tag. - [ ] Given a release in one of them, when the step timings are read, then the pushes did not overlap. - [ ] Given the three repositories that already push serially, then they are unchanged.
Author
Owner

Rewriting the body's premise: max-parallel: 1 does not work, so this issue is not "add a line" but "change a shape".

#66 originally serialised the template with max-parallel: 1. Review would not approve it because nothing on the fleet used the key and so nothing showed the scheduler honours it. It does not. Measured on a throwaway job, three legs each sleeping 25s:

max-parallel: 1          concurrency (cancel-in-progress: false)
one    07:14:00 → :27    one    07:16:14 → :41
three  07:14:20 → :47    three  07:16:32 → :59   ← both overlap
two    07:14:28 → :55    two    07:16:42 → 07:17:09

Serialised, three 25-second legs take ~81s; both runs took ~55. Forgejo 16.0.3+gitea-1.22.0 ignores both keys. Propagating max-parallel: 1 to six repositories would have shipped an inert line and left the next collision looking exactly like the last one.

What to do instead

Publish every image from one job, one step per image, with no matrix. Steps within a job run in order — that is what a job is, not a scheduler feature that may be unimplemented.

Three repositories already have this shape and are the worked example: ahmad/larda, ahmad/expiro and suhba/suhba-web each build two images from two sequential docker/build-push-action steps. The first version of this issue listed them as "not affected"; they are better than that — they are the answer.

Repositories to change — six

Repository Images Becomes
ahmad/namey 3 — marketing, portal, api three steps
ahmad/imamah 2 — api, web two steps
ahmad/portfolio 2 — api, web two steps
ahmad/trip 2 — app, portal two steps
Bayan/kidpoints 2 — api, web two steps
Bayan/bayan-web 2 — into two different packages two steps

The matrix currently supplies matrix.service (or a whole include entry) to the Dockerfile path, both tags and both cache refs; each becomes a literal per step. Bayan/bayan-web and ahmad/namey use include with a per-image package name, so their steps differ by more than a suffix.

Cost, stated plainly

The setup — checkout, Docker CLI install, buildx, two logins — currently runs once per matrix leg in parallel. As one job it runs once, and only the builds are serial. On a runner that was giving these legs two slots at best, that is not obviously slower and may be faster; it is worth timing one release rather than assuming either way.

Acceptance criteria

  • Given each of the six repositories, when its release.yml is read, then publish has no strategy.matrix and one build-and-push step per image.
  • Given a release in one of them, then every image is still published under both the version tag and the commit tag, with the same names as before.
  • Given a release in one of them, when GET /repos/{owner}/{repo}/actions/tasks is read, then there is one publish row rather than one per image — which is what makes the ordering unarguable.
  • Given the three repositories that already publish from sequential steps, then they are unchanged.

One warning for whoever counts occurrences

Review found ahmad/imamah run 307: two publish legs failing without overlapping, each dead in under ten seconds — the #65 shape (a vault fetch failing the job), not this one. A tally of red publish jobs is not a tally of this bug. #65 is merged, so that source should stop producing new cases.

**Rewriting the body's premise: `max-parallel: 1` does not work, so this issue is not "add a line" but "change a shape".** `#66` originally serialised the template with `max-parallel: 1`. Review would not approve it because nothing on the fleet used the key and so nothing showed the scheduler honours it. It does not. Measured on a throwaway job, three legs each sleeping 25s: ``` max-parallel: 1 concurrency (cancel-in-progress: false) one 07:14:00 → :27 one 07:16:14 → :41 three 07:14:20 → :47 three 07:16:32 → :59 ← both overlap two 07:14:28 → :55 two 07:16:42 → 07:17:09 ``` Serialised, three 25-second legs take ~81s; both runs took ~55. Forgejo `16.0.3+gitea-1.22.0` ignores both keys. **Propagating `max-parallel: 1` to six repositories would have shipped an inert line and left the next collision looking exactly like the last one.** ### What to do instead Publish every image from **one job, one step per image**, with no matrix. Steps within a job run in order — that is what a job is, not a scheduler feature that may be unimplemented. **Three repositories already have this shape and are the worked example:** `ahmad/larda`, `ahmad/expiro` and `suhba/suhba-web` each build two images from two sequential `docker/build-push-action` steps. The first version of this issue listed them as "not affected"; they are better than that — they are the answer. ### Repositories to change — six | Repository | Images | Becomes | | --- | --- | --- | | `ahmad/namey` | 3 — `marketing`, `portal`, `api` | three steps | | `ahmad/imamah` | 2 — `api`, `web` | two steps | | `ahmad/portfolio` | 2 — `api`, `web` | two steps | | `ahmad/trip` | 2 — `app`, `portal` | two steps | | `Bayan/kidpoints` | 2 — `api`, `web` | two steps | | `Bayan/bayan-web` | 2 — into two different packages | two steps | The matrix currently supplies `matrix.service` (or a whole `include` entry) to the Dockerfile path, both tags and both cache refs; each becomes a literal per step. `Bayan/bayan-web` and `ahmad/namey` use `include` with a per-image package name, so their steps differ by more than a suffix. ### Cost, stated plainly The setup — checkout, Docker CLI install, buildx, two logins — currently runs once per matrix leg in parallel. As one job it runs **once**, and only the builds are serial. On a runner that was giving these legs two slots at best, that is not obviously slower and may be faster; it is worth timing one release rather than assuming either way. ### Acceptance criteria - [ ] Given each of the six repositories, when its `release.yml` is read, then `publish` has no `strategy.matrix` and one build-and-push step per image. - [ ] Given a release in one of them, then every image is still published under both the version tag and the commit tag, with the same names as before. - [ ] Given a release in one of them, when `GET /repos/{owner}/{repo}/actions/tasks` is read, then there is **one** `publish` row rather than one per image — which is what makes the ordering unarguable. - [ ] Given the three repositories that already publish from sequential steps, then they are unchanged. ### One warning for whoever counts occurrences Review found `ahmad/imamah` run 307: two publish legs failing **without** overlapping, each dead in under ten seconds — the `#65` shape (a vault fetch failing the job), not this one. A tally of red `publish` jobs is not a tally of this bug. `#65` is merged, so that source should stop producing new cases.
ahmad changed title from chore: Serialise the publish matrix in the six multi-image repositories to chore: Publish images from one job in the six multi-image repositories 2026-09-09 07:20:27 +00:00
Author
Owner

Blocking this on #70 — the pull request that establishes the shape it propagates — and recording the dependency rather than leaving it as an ordering I happen to remember.

The first version of this issue would have propagated max-parallel: 1, which is ignored on this runner (measured; see the comment above). Had it gone out before #70 was reviewed, six repositories would have gained an inert line and the next collision would have looked exactly like the last. That is not a hypothetical: the issue said to do it and I filed it.

So the ordering is load-bearing rather than tidy. #70's shape — one job, one step per image — is what this copies, and it is not settled until that merges.

#70 is open, green and awaiting a verdict. When it closes, this returns to state/ready on its own.

Blocking this on `#70` — the pull request that establishes the shape it propagates — and recording the dependency rather than leaving it as an ordering I happen to remember. The first version of this issue would have propagated `max-parallel: 1`, which is **ignored on this runner** (measured; see the comment above). Had it gone out before `#70` was reviewed, six repositories would have gained an inert line and the next collision would have looked exactly like the last. That is not a hypothetical: the issue said to do it and I filed it. So the ordering is load-bearing rather than tidy. `#70`'s shape — one job, one step per image — is what this copies, and it is not settled until that merges. `#70` is open, green and awaiting a verdict. When it closes, this returns to `state/ready` on its own.
Author
Owner

Unblocked. #66 is closed — read back from GET /issues/69/dependencies, which reports it closed, rather than from the fact that I merged it a minute ago. 94b67b7 is on main, so the shape this propagates now exists in the template.

Carrying forward the sentence review asked for, because it is the one a quiet week will erase: this fixes a release racing its own pushes, not two repositories racing each other. The second needs the server-side nil dereference in amtronics/infrastructure-vps. If collisions continue after all six repositories are converted, that is the reason and not a failure of this work.

One measurement from the probes that belongs here rather than only on #66: this runner's capacity appears to be 2. Three 25-second legs took 55 seconds end to end, with legs starting exactly as earlier ones finished, two at a time. So the matrix these six repositories use was never getting more than two concurrent builds anyway — which means converting to sequential steps costs less wall-clock than the diff suggests, and is worth saying to whoever times the first converted release and expects it to double.

state/ready.

Unblocked. `#66` is closed — read back from `GET /issues/69/dependencies`, which reports it `closed`, rather than from the fact that I merged it a minute ago. `94b67b7` is on `main`, so the shape this propagates now exists in the template. Carrying forward the sentence review asked for, because it is the one a quiet week will erase: **this fixes a release racing its own pushes, not two repositories racing each other.** The second needs the server-side nil dereference in `amtronics/infrastructure-vps`. If collisions continue after all six repositories are converted, that is the reason and not a failure of this work. One measurement from the probes that belongs here rather than only on `#66`: **this runner's capacity appears to be 2.** Three 25-second legs took 55 seconds end to end, with legs starting exactly as earlier ones finished, two at a time. So the matrix these six repositories use was never getting more than two concurrent builds anyway — which means converting to sequential steps costs less wall-clock than the diff suggests, and is worth saying to whoever times the first converted release and expects it to double. `state/ready`.
ahmad self-assigned this 2026-09-09 08:45:01 +00:00
Author
Owner

Taking this. One pull request per repository, six in all, each touching
only .forgejo/workflows/release.yml.

Approach. Delete the strategy/matrix block and expand the single
Build and push step into one step per image, substituting the matrix
value into the Dockerfile path, both tags and both cache refs. The job's
setup — checkout, Docker CLI, buildx, both logins — is already shared and
does not move; only the build step multiplies. 94b67b7's comment on the
template job travels with it, so each repository carries why it has this
shape rather than a bare diff.

Two need more than a suffix substitution. Bayan/bayan-web and
ahmad/namey use include with a per-image package name, so each step
gets its own image literal. ahmad/trip needs care of a different
kind: its build-arg is conditional on the matrix value
(VITE_API_URL=${{ matrix.service == 'portal' && '/api' || '' }}), which
becomes a literal /api on the portal step and an empty value on the app
step — the conditional disappears rather than being carried across.

Ambiguity, and how I am reading it. The acceptance criterion "the
pushes did not overlap" cannot be checked from a pull request; it needs a
release to run. I am verifying the pre-merge half — that each converted
job declares no matrix, and that every image keeps the same context,
Dockerfile, tags, build-args and cache refs as the leg it replaced,
compared field by field rather than by reading the diff — and leaving the
timing check to the first release each repository cuts. Whether that is
enough to close the issue is the human's call; I will say so on each PR
rather than claim the criterion is met.

I have also corrected the issue body, which still specified
max-parallel: 1 in its scope and first acceptance criterion. The
comments had rewritten the premise but the body still said to ship the
inert line — and the body is what a reader starts from.

Taking this. One pull request per repository, six in all, each touching only `.forgejo/workflows/release.yml`. **Approach.** Delete the `strategy`/`matrix` block and expand the single `Build and push` step into one step per image, substituting the matrix value into the Dockerfile path, both tags and both cache refs. The job's setup — checkout, Docker CLI, buildx, both logins — is already shared and does not move; only the build step multiplies. `94b67b7`'s comment on the template job travels with it, so each repository carries why it has this shape rather than a bare diff. **Two need more than a suffix substitution.** `Bayan/bayan-web` and `ahmad/namey` use `include` with a per-image package name, so each step gets its own `image` literal. `ahmad/trip` needs care of a different kind: its build-arg is conditional on the matrix value (`VITE_API_URL=${{ matrix.service == 'portal' && '/api' || '' }}`), which becomes a literal `/api` on the portal step and an empty value on the app step — the conditional disappears rather than being carried across. **Ambiguity, and how I am reading it.** The acceptance criterion "the pushes did not overlap" cannot be checked from a pull request; it needs a release to run. I am verifying the pre-merge half — that each converted job declares no matrix, and that every image keeps the same context, Dockerfile, tags, build-args and cache refs as the leg it replaced, compared field by field rather than by reading the diff — and leaving the timing check to the first release each repository cuts. Whether that is enough to close the issue is the human's call; I will say so on each PR rather than claim the criterion is met. I have also corrected the issue body, which still specified `max-parallel: 1` in its scope and first acceptance criterion. The comments had rewritten the premise but the body still said to ship the inert line — and the body is what a reader starts from.
Author
Owner

All six pull requests are open, green and ready.

Repository PR Images Steps now
ahmad/imamah ahmad/imamah#159 2 api, web
ahmad/portfolio ahmad/portfolio#87 2 api, web
ahmad/trip ahmad/trip#139 2 app, portal
Bayan/kidpoints Bayan/kidpoints#212 2 api, web
Bayan/bayan-web Bayan/bayan-web#33 2 web, api (two packages)
ahmad/namey ahmad/namey#77 3 marketing, portal, api

Each carries its own equivalence check as a comment: main's matrix
expanded into its legs, and each leg's context, file, push, tags,
build-args, cache-from and cache-to compared against the new steps.
All thirteen images across the six repositories are identical to the legs
they replace.

Two carry an edit that is not part of the conversion, flagged on each
so it can be refused separately. Expanding a matrix strands its comments
on whichever image comes first, and in ahmad/trip and ahmad/portfolio
that put a note about the portal/web bundle directly over a build that
ignores every argument it names. Moved to the step it describes, with the
first step gaining a line saying why it still receives arguments it
ignores — because the matrix passed them to every leg, and dropping them
would be a behaviour change this issue does not cover.

The body needed correcting before I could work to it. Its scope and
first acceptance criterion still said to add max-parallel: 1; the
comments had rewritten the premise but not the part a reader starts from.
Rewritten to the shape actually agreed, with the measurement kept.

One criterion stays unmet by construction. "Given a release, when the
step timings are read, then the pushes did not overlap" needs a release
to run — no pull request can show it. Each PR says so rather than
claiming it. The first release each repository cuts is where to check it,
along with wall-clock: the per-leg setup now runs once, and this runner
was giving the legs about two slots anyway, so it should not be the
doubling the diff suggests.

**All six pull requests are open, green and ready.** | Repository | PR | Images | Steps now | |---|---|---|---| | `ahmad/imamah` | ahmad/imamah#159 | 2 | api, web | | `ahmad/portfolio` | ahmad/portfolio#87 | 2 | api, web | | `ahmad/trip` | ahmad/trip#139 | 2 | app, portal | | `Bayan/kidpoints` | Bayan/kidpoints#212 | 2 | api, web | | `Bayan/bayan-web` | Bayan/bayan-web#33 | 2 | web, api (two packages) | | `ahmad/namey` | ahmad/namey#77 | 3 | marketing, portal, api | Each carries its own equivalence check as a comment: `main`'s matrix expanded into its legs, and each leg's `context`, `file`, `push`, `tags`, `build-args`, `cache-from` and `cache-to` compared against the new steps. All thirteen images across the six repositories are identical to the legs they replace. **Two carry an edit that is not part of the conversion**, flagged on each so it can be refused separately. Expanding a matrix strands its comments on whichever image comes first, and in `ahmad/trip` and `ahmad/portfolio` that put a note about the portal/web bundle directly over a build that ignores every argument it names. Moved to the step it describes, with the first step gaining a line saying why it still receives arguments it ignores — because the matrix passed them to every leg, and dropping them would be a behaviour change this issue does not cover. **The body needed correcting before I could work to it.** Its scope and first acceptance criterion still said to add `max-parallel: 1`; the comments had rewritten the premise but not the part a reader starts from. Rewritten to the shape actually agreed, with the measurement kept. **One criterion stays unmet by construction.** "Given a release, when the step timings are read, then the pushes did not overlap" needs a release to run — no pull request can show it. Each PR says so rather than claiming it. The first release each repository cuts is where to check it, along with wall-clock: the per-leg setup now runs once, and this runner was giving the legs about two slots anyway, so it should not be the doubling the diff suggests.
Author
Owner

Done. All six repositories merged.

Repository PR Merge Images
ahmad/imamah ahmad/imamah#159 20d02b3 2
ahmad/portfolio ahmad/portfolio#87 c94f7ea 2
ahmad/trip ahmad/trip#139 94f6106 2
Bayan/kidpoints Bayan/kidpoints#212 9a2a271 2
Bayan/bayan-web Bayan/bayan-web#33 282def6 2
ahmad/namey ahmad/namey#77 0977932 3

Thirteen images across six repositories now build from one step each,
in order, rather than from a matrix that raced its own pushes. Together
with ahmad/larda, ahmad/expiro and suhba/suhba-web, which already
had this shape, no repository on the fleet publishes more than one
image concurrently.

Every conversion was checked mechanically rather than by reading the
diff.
For each repository, main's matrix was expanded into its legs by
substituting the matrix values into the build step, and each leg's
context, file, push, tags, build-args, cache-from and
cache-to compared field by field against the new steps. All thirteen
images are identical to the legs they replace; each PR carries its own
comparison as a comment.

Two things worth keeping.

The first version of this issue said to add max-parallel: 1. That key is
ignored on this runner, measured rather than assumed (#66), so the
issue would have propagated an inert line to six repositories and left the
next collision looking exactly like the last. The body was rewritten
before implementation because its scope and first acceptance criterion
still specified it.

Expanding a matrix silently relocates its comments. In ahmad/trip and
ahmad/portfolio a note about the portal and web bundles landed above a
build that ignores every argument it names. Both were moved and flagged on
their PRs as edits beyond the mechanical conversion.

One acceptance criterion is not met and cannot be met here. "Given a
release, when the step timings are read, then the pushes did not overlap."
That needs a release to run; no pull request can show it. The mechanism is
steps-within-a-job, which is the one ordering this runner was measured to
honour — but that is an argument, not an observation of these six
repositories. The first release each cuts is where to confirm it, along
with wall-clock: the setup that ran per matrix leg now runs once, and the
runner was giving these legs about two slots anyway, so it should not be
the doubling the diff suggests.

And the sentence this fixes is narrower than it looks: this removes a
release racing its own pushes. Two repositories releasing in the same
second can still collide, and that needs the server-side nil dereference
in amtronics/infrastructure-vps. If collisions continue, that is why —
not a failure of this work.

**Done. All six repositories merged.** | Repository | PR | Merge | Images | |---|---|---|---| | `ahmad/imamah` | ahmad/imamah#159 | `20d02b3` | 2 | | `ahmad/portfolio` | ahmad/portfolio#87 | `c94f7ea` | 2 | | `ahmad/trip` | ahmad/trip#139 | `94f6106` | 2 | | `Bayan/kidpoints` | Bayan/kidpoints#212 | `9a2a271` | 2 | | `Bayan/bayan-web` | Bayan/bayan-web#33 | `282def6` | 2 | | `ahmad/namey` | ahmad/namey#77 | `0977932` | 3 | Thirteen images across six repositories now build from one step each, in order, rather than from a matrix that raced its own pushes. Together with `ahmad/larda`, `ahmad/expiro` and `suhba/suhba-web`, which already had this shape, **no repository on the fleet publishes more than one image concurrently.** **Every conversion was checked mechanically rather than by reading the diff.** For each repository, `main`'s matrix was expanded into its legs by substituting the matrix values into the build step, and each leg's `context`, `file`, `push`, `tags`, `build-args`, `cache-from` and `cache-to` compared field by field against the new steps. All thirteen images are identical to the legs they replace; each PR carries its own comparison as a comment. **Two things worth keeping.** The first version of this issue said to add `max-parallel: 1`. That key is **ignored on this runner**, measured rather than assumed (#66), so the issue would have propagated an inert line to six repositories and left the next collision looking exactly like the last. The body was rewritten before implementation because its scope and first acceptance criterion still specified it. Expanding a matrix silently relocates its comments. In `ahmad/trip` and `ahmad/portfolio` a note about the portal and web bundles landed above a build that ignores every argument it names. Both were moved and flagged on their PRs as edits beyond the mechanical conversion. **One acceptance criterion is not met and cannot be met here.** "Given a release, when the step timings are read, then the pushes did not overlap." That needs a release to run; no pull request can show it. The mechanism is steps-within-a-job, which is the one ordering this runner was measured to honour — but that is an argument, not an observation of these six repositories. The first release each cuts is where to confirm it, along with wall-clock: the setup that ran per matrix leg now runs once, and the runner was giving these legs about two slots anyway, so it should not be the doubling the diff suggests. **And the sentence this fixes is narrower than it looks:** this removes a release racing *its own* pushes. Two repositories releasing in the same second can still collide, and that needs the server-side nil dereference in `amtronics/infrastructure-vps`. If collisions continue, that is why — not a failure of this work.
ahmad 2026-09-09 11:14:32 +00:00
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.

Reference
amtronics/platform-actions#69
No description provided.