fix(release): reject zero-time release timestamps, not just Go's spelling #42

Merged
ryangr0 merged 1 commit from fix/reject-zero-timestamps into development 2026-08-26 09:32:40 +00:00
Owner

Found while verifying rc.29. Independent of the OCI-annotation work — this is a live defect on development.

What happened

rc.29 shipped with org.opencontainers.image.created=1970-01-01T00:00:00Z — the exact label the Stamp image creation timestamp step exists to protect, reached by a route it did not guard.

Forgejo returns a zero time rather than an empty string for a release published through the API, which is how semantic-release cuts every rc. The guard rejected Go's zero value and nothing else:

published_at=0001-01-01T00:00:00Z  ->  falls back to build time  OK
published_at=1970-01-01T00:00:00Z  ->  passes straight through   BUG

The epoch is ISO-8601 shaped, so it cleared the format assertion and was baked into the image. The downstream label gate then verified it as correct, because that gate compares the image against this value — a wrong value here is confirmed, not caught.

Worth noting this is probably the real root cause of the rc.21 incident the step's own comment describes as "root cause unconfirmed". That was diagnosed as a build-arg failing to reach BuildKit; it may simply have been this.

The fix

Enumerating sentinels is a losing game, so the test is plausibility: nothing this project publishes was created before the project existed, so anything earlier is a sentinel whatever it spells itself. One predicate serves both the candidate choice and the final assertion — a value good enough to pick is exactly a value good enough to ship.

Two deliberate behaviour changes

  • published_at and created_at are each tested, so an unusable published_at now falls through to a usable created_at. The old code consulted created_at only when published_at was empty.
  • An unusable timestamp no longer fails the build. It cannot produce a wrong label any more, so refusing the release buys nothing; it degrades to build time, which is the OCI-conventional meaning anyway. Every rejection is logged, so the forge's behaviour stays visible rather than inferred.

Verified

Eight inputs, each exercised against the real step body: dispatch, real published_at, Go zero, Unix epoch, epoch-with-real-created_at, both-sentinels, garbage, and a pre-2020 year.

Unix epoch  <-- rc.29          NOTE: ... is a zero time or implausible — ignoring it.
                               created=2026-08-26T09:16:16Z
epoch published, REAL created  created=2026-08-26T11:00:00Z   <- now used, previously skipped
Found while verifying rc.29. Independent of the OCI-annotation work — this is a live defect on `development`. ## What happened rc.29 shipped with `org.opencontainers.image.created=1970-01-01T00:00:00Z` — the exact label the `Stamp image creation timestamp` step exists to protect, reached by a route it did not guard. Forgejo returns a **zero time** rather than an empty string for a release published through the API, which is how semantic-release cuts every rc. The guard rejected Go's zero value and nothing else: ``` published_at=0001-01-01T00:00:00Z -> falls back to build time OK published_at=1970-01-01T00:00:00Z -> passes straight through BUG ``` The epoch is ISO-8601 shaped, so it cleared the format assertion and was baked into the image. **The downstream label gate then verified it as correct**, because that gate compares the image against this value — a wrong value here is confirmed, not caught. Worth noting this is probably the real root cause of the rc.21 incident the step's own comment describes as "root cause unconfirmed". That was diagnosed as a build-arg failing to reach BuildKit; it may simply have been this. ## The fix Enumerating sentinels is a losing game, so the test is **plausibility**: nothing this project publishes was created before the project existed, so anything earlier is a sentinel whatever it spells itself. One predicate serves both the candidate choice and the final assertion — a value good enough to pick is exactly a value good enough to ship. ## Two deliberate behaviour changes - `published_at` and `created_at` are each tested, so an unusable `published_at` now falls through to a usable `created_at`. The old code consulted `created_at` only when `published_at` was **empty**. - An unusable timestamp **no longer fails the build**. It cannot produce a wrong label any more, so refusing the release buys nothing; it degrades to build time, which is the OCI-conventional meaning anyway. Every rejection is logged, so the forge's behaviour stays visible rather than inferred. ## Verified Eight inputs, each exercised against the real step body: dispatch, real `published_at`, Go zero, Unix epoch, epoch-with-real-`created_at`, both-sentinels, garbage, and a pre-2020 year. ``` Unix epoch <-- rc.29 NOTE: ... is a zero time or implausible — ignoring it. created=2026-08-26T09:16:16Z epoch published, REAL created created=2026-08-26T11:00:00Z <- now used, previously skipped ```
fix(release): reject zero-time release timestamps, not just Go's spelling
All checks were successful
On Pull Request / checks (pull_request) Successful in 1m18s
08dfd2ff15
rc.29 shipped with org.opencontainers.image.created=1970-01-01T00:00:00Z — the
exact label this step exists to protect, reached by a route it did not guard.

Forgejo returns a ZERO TIME rather than an empty string for a release published
through the API, which is how semantic-release cuts every rc. The guard rejected
Go's zero value (0001-01-01) and nothing else, so the Unix epoch went straight
through, was ISO-8601 shaped, passed the format assertion, and was baked into
the image. The downstream label gate then verified it as correct, because that
gate compares the image against this value — a wrong value here is confirmed,
not caught.

Enumerating sentinels is a losing game, so the test is now plausibility: nothing
this project publishes was created before the project existed, so anything
earlier is a sentinel whatever it spells itself. One predicate serves both the
candidate choice and the final assertion, because a value good enough to pick is
exactly a value good enough to ship.

Two behaviour changes, both deliberate:

- published_at and created_at are each tested, so an unusable published_at now
  falls through to a usable created_at instead of skipping to build time. The
  old code only consulted created_at when published_at was EMPTY.
- an unusable timestamp no longer fails the build. It cannot produce a wrong
  label any more, so refusing the release buys nothing; it degrades to build
  time, which is the OCI-conventional meaning anyway. Every rejection is logged
  so the forge's behaviour stays visible rather than inferred.

Verified across eight inputs: dispatch, real published_at, Go zero, Unix epoch,
epoch-with-real-created_at, both-sentinels, garbage, and a pre-2020 year.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ryangr0 merged commit 1fdd02a705 into development 2026-08-26 09:32:40 +00:00
Commenting is not possible because the repository is archived.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
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
webgrip/ploeg!42
No description provided.