Skip to content

Publishing a superseded tag still walks catalog.json backwards #9

Description

@amondnet

Context

Found while fixing a regression the GPT-5.6 review engine raised on the ADR-0003 implementation (PR for issue #6, commit bd65732).

That fix closes the register path: re-running an interrupted +rebuild.1 after +rebuild.2 had completed both phases no longer opens a catalog pull request walking consumers backwards. The same hazard remains on the publish path, and is left as it was found — recorded under ADR-0003 § Consequences → Negative.

The gap

classifyRelease (scripts/lib/release-state.ts) checks supersession only after establishing that the release exists:

if (!state.releaseExists) {
  // ... refuse when the catalog names this tag
  return 'publish'
}

if (registered)
  return 'complete'

if (state.catalogTag !== null && isSupersededBy(state.tag, state.catalogTag))
  return 'complete'

return 'register'

A tag whose release does not exist returns publish regardless of what the catalog names. release.yml gates registration on mode != 'complete', so publish runs both phases — publication and the catalog write.

Reachable sequence:

  1. boot-4.1.1+rebuild.1 is tagged; its run fails during build, so no release is ever created
  2. The operator cuts boot-4.1.1+rebuild.2 instead; it publishes and registers cleanly
  3. Someone re-runs the failed rebuild.1 job (or re-pushes the tag)
  4. releaseExists === falsepublishrebuild.1 is published, then update-catalog.ts repoints the entry from rebuild.2 back to rebuild.1

applyEntry (scripts/lib/catalog-update.ts:47-60) permits this by design — it records "the rebuild last published, not the highest one" and deliberately does not enforce suffix ordering. Step 4 also drags released_at backwards.

Publication itself is harmless: rebuild.1 is a tag of its own and no published asset is replaced. The catalog write is the damaging half.

Not a regression

The guard this replaced keyed on the release existing:

- name: Refuse to overwrite an existing release
  run: |
    if gh release view "$TAG" >/dev/null 2>&1; then ... exit 1; fi

With no release present it never fired either, so this path behaved the same way before ADR-0003. It is pre-existing, not introduced by that change.

The decision to make

Not obviously a defect repair — it turns on a question ADR-0003 did not need to answer: should a superseded tag still be publishable at all?

  1. Extend supersession to publish — a tag the catalog has moved past returns complete whether or not its release exists. Simplest, and consistent with "the modes name what a run still owes". Costs the ability to publish a back-filled older rebuild as an archived artifact.
  2. Publish but do not register — a fourth mode that runs phase 1 and skips phase 2. The tag becomes downloadable without disturbing the index. More workflow surface, and a mode whose name has to explain itself.
  3. Refuse — treat it as an operator error and fail with a message naming the newer rebuild. Loudest, but makes a re-run of a stale failed job an error rather than a no-op, which cuts against the idempotence ADR-0003 chose.

Option 1 folds into the rule already implemented and needs no new mode. Option 2 is the only one that preserves publishing a superseded tag, if that turns out to matter.

Mitigations today

  • The catalog write lands as a pull request, not a direct push — a human merge gate stands between this and consumers. The PR title (chore(catalog): record boot-4.1.1+rebuild.1) looks routine, so the gate is weaker than it sounds.
  • The sequence needs a failed-before-publication tag and a later superseding rebuild and a re-run of the stale job.

Acceptance

  • classifyRelease handles the superseded-and-unpublished case per whichever option is chosen
  • A unit test in tests/unit/release-state.test.ts covers { releaseExists: false, catalogTag: <newer rebuild> }
  • ADR-0003's decision table loses its publish* row and the matching Negative bullet, or a superseding ADR records the change

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    type:bugSomething isn't workingtype:ciCI/CD configuration changes

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions