* docs(227): create ADR for input-validation-shape-not-just-type Captures the architectural standard that defensive normalization at trust boundaries must validate both type and semantic shape, with silent coercion on failure. Concrete cases: parentTraceId UUID v4 fix in PR #225 and release-version validation in ADR 218. Closes #227 * docs(227): cross-reference new ADR from ADR 218 Appends a "See also" section at the end of ADR 218 pointing forward to ADR 227, which generalises the type+semantic-shape validation principle documented in ADR 218's narrower release-workflow context. * docs(227): add CONTRIBUTING pointer to new ADR Adds a "Code Review Lessons → Input validation" section after the Reviewer Standards block, linking to ADR 227 as the citable reference for the type+semantic-shape validation standard.
94 lines
4.8 KiB
Markdown
94 lines
4.8 KiB
Markdown
# ADR-0175: Harden release-workflow version validation — reject leading zeros and pre-check npm
|
|
|
|
- **Status:** Accepted (2026-05-24)
|
|
- **Date:** 2026-05-24
|
|
|
|
## Context
|
|
|
|
### The leading-zero normalization incident
|
|
|
|
The release workflow's `validate-version` job used the regex `^[0-9]+\.[0-9]+\.0$` to gate the `version` input. This regex accepts leading zeros in any segment (`1.01.0`, `01.0.0`, etc.) because `[0-9]+` matches one or more digits without anchoring against a leading zero.
|
|
|
|
A maintainer triggered the workflow with `version=1.01.0`. The validator accepted it. Downstream steps called `npm version 1.01.0`, which silently normalized the string to `1.1.0` per the semver specification (leading zeros are stripped). From that point the pipeline had irreconcilably divergent state:
|
|
|
|
| Artifact | Value |
|
|
|---|---|
|
|
| npm registry (`@opengsd/get-shit-done-redux`, `@opengsd/gsd-sdk`) | `1.1.0` — published, immutable |
|
|
| git tag | `v1.01.0` |
|
|
| GitHub release title | `v1.1.0` |
|
|
| GitHub release tag | `v1.01.0` |
|
|
| Release branch | `release/1.01.0` |
|
|
|
|
The workflow run ultimately failed downstream. A subsequent attempt to re-run finalize for `version=1.1.0` failed at the `Dry-run publish validation` step because npm refuses to republish an already-published version.
|
|
|
|
A second typo (`version=1.03.0`) produced a similar orphan tag and branch but never published to npm.
|
|
|
|
### The duplicate-version late-failure hole
|
|
|
|
The validator only checked format. It did not check whether the requested version was already live on npm. A duplicate-version request (e.g. re-running `finalize` for an already-published version, or mistyping a version that normalizes to one already published) ran through the full checkout → install → build → test cycle — roughly 10 minutes — before failing at the `Dry-run publish validation` step.
|
|
|
|
## Decision
|
|
|
|
### 1. Tighten the format regex to forbid leading zeros
|
|
|
|
Replace `^[0-9]+\.[0-9]+\.0$` with `^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.0$`.
|
|
|
|
Each segment now matches either `0` exactly or a string starting with a non-zero digit followed by zero or more digits. `1.01.0` fails because the minor segment `01` matches neither alternative. The error message includes the offending value so maintainers can correct it immediately.
|
|
|
|
The `IS_MAJOR` detection regex is updated in parallel: `^[0-9]+\.0\.0$` → `^(0|[1-9][0-9]*)\.0\.0$`.
|
|
|
|
### 2. Add a duplicate-version precheck against npm at validation time
|
|
|
|
A new `Reject already-published versions` step runs immediately after format validation in the `validate-version` job. It calls `npm view "$pkg@$VERSION" version` for both `@opengsd/get-shit-done-redux` and `@opengsd/gsd-sdk`. If either resolves, the job fails in under 5 seconds with a clear error. No build or test cycle is wasted.
|
|
|
|
## Recovery sequence
|
|
|
|
The following steps recover the production state left by the `1.01.0` and `1.03.0` incidents. Include them here so future maintainers do not need to re-derive them.
|
|
|
|
### 1. Cancel any failing in-flight runs
|
|
|
|
```sh
|
|
gh run cancel <run-id> --repo open-gsd/get-shit-done-redux
|
|
```
|
|
|
|
### 2. Fix the v1.1.0 divergence (npm published, tag/release/branch on wrong name)
|
|
|
|
```sh
|
|
# Fetch the release branch that holds the correct package.json at 1.1.0
|
|
git fetch origin release/1.1.0
|
|
|
|
# Create a proper v1.1.0 tag at the head of release/1.1.0
|
|
git tag v1.1.0 <sha-of-release/1.1.0-head>
|
|
git push origin v1.1.0
|
|
|
|
# Retarget the existing GitHub release from the typo tag to the correct one
|
|
gh release edit v1.01.0 --repo open-gsd/get-shit-done-redux --tag v1.1.0
|
|
gh release edit v1.1.0 --repo open-gsd/get-shit-done-redux --verify-tag
|
|
|
|
# Delete the typo tag and branch
|
|
git push origin :refs/tags/v1.01.0
|
|
git push origin --delete release/1.01.0
|
|
```
|
|
|
|
### 3. Clean the unpublished v1.03.0 orphan
|
|
|
|
```sh
|
|
git push origin :refs/tags/v1.03.0
|
|
git push origin --delete release/1.03.0
|
|
```
|
|
|
|
### 4. Resume releases at 1.2.0
|
|
|
|
`1.1.0` is consumed on npm and the workflow only allows `.0` patch-component versions. The next valid release is `1.2.0`.
|
|
|
|
## Consequences
|
|
|
|
- **Valid inputs are unaffected.** The new regex accepts every version that the old regex accepted minus leading-zero forms. All existing release runs used well-formed versions; no regression for normal use.
|
|
- **Leading-zero inputs fail in under 5 seconds** at the `validate-version` job before any branch, install, or publish operation runs.
|
|
- **Duplicate-version inputs fail in under 5 seconds** at `validate-version` rather than after a full install-and-test cycle.
|
|
- **The late dry-run check in `finalize` is unchanged.** It remains a belt-and-suspenders guard; the new precheck does not remove it.
|
|
|
|
## See also
|
|
|
|
- ADR 227 (`docs/adr/227-input-validation-shape-not-just-type.md`) generalises the principle this ADR documents in the narrower release-validation context: input validation at trust boundaries must check both type and semantic shape, with silent coercion on failure.
|