Files
narratio/docs/roadmap/implementation.md

472 lines
24 KiB
Markdown

# Release Procedure Upgrade Implementation Plan
## Purpose And Status
This document is the executable implementation plan for
[`release-procedure.md`](release-procedure.md). The feature roadmap owns the
accepted user intent, policy choices, required outcome, and responsibility
boundary. This plan translates that target into bounded implementation stages
suitable for one `gpt-5.6-terra` coding prompt apiece.
All stages are pending and must be implemented in numeric order. Each stage
must leave its owned behavior correct, tested at an appropriate stable
boundary, and ready for the next stage. This plan supersedes the completed
pipeline-configuration implementation plan that previously occupied this
path; that feature's implemented behavior is now owned by current canonical
documentation and code.
## Settled Implementation Decisions
The following decisions are final for this implementation:
- The synchronous release boundary is a successful push of the exact selected
tag ref to `origin`. Neither `scripts/release.sh` nor any instruction that
defines successful release completion may poll Woodpecker, query build
status, wait for a Gitea release, download assets, or verify asynchronous
publication.
- The supported command surfaces are exactly:
`scripts/build-release-assets.sh VERSION OUTPUT_DIR`,
`scripts/check-release-candidate.sh VERSION`, and
`scripts/release.sh VERSION`.
- All scripts are POSIX `sh`, use `set -eu`, derive the repository root from
their own checked-in location, and use narrowly named variables. They do not
rely on the caller's current directory and do not repurpose `HOME`,
`CODEX_HOME`, or common system-option variables.
- A private source-only `scripts/release-lib.sh` owns the stable semantic
version parser and shared fatal-error/reporting helpers. It has no behavior
merely from being sourced. Public scripts source it relative to their own
location so version syntax and error conventions do not drift.
- Stable versions match exactly
`v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)`. Prerelease and build
suffixes are rejected.
- `scripts/build-release-assets.sh` accepts exactly two arguments. Its output
path must be absolute, must not be `/` or the repository root, must not be a
symlink or non-directory, and must either not exist or be an empty directory.
The script creates the directory when absent, writes only its six named
assets and any narrowly scoped temporary smoke binary, and never recursively
clears a caller-supplied path.
- Official targets are CGO-disabled Linux, Darwin, and Windows builds for both
`amd64` and `arm64`. Filenames are
`narratio-VERSION-GOOS-GOARCH`, with `.exe` only on Windows. Builds use
`-trimpath`, `-s -w`, and
`-X gitea.maximumdirect.net/eric/narratio/internal/buildinfo.Version=VERSION`.
- The asset builder verifies `narratio version` on the current host. When the
host is one of the six official targets it executes that official asset;
otherwise it builds a separate temporary host smoke binary, executes it,
and removes only that file. Expected output is exactly
`narratio VERSION`.
- `scripts/check-release-candidate.sh` is read-only with respect to tracked
source and Git refs. It accepts exactly one version, validates the matching
note, performs repository and Go-module hygiene checks, runs every required
test/static/build/documentation/example check uncached where applicable,
verifies formatting and whitespace, and calls the shared asset builder in a
directory created with `mktemp -d`. Every Go command it owns runs with
`GOWORK=off`.
- The candidate checker deliberately does not require `main`, a clean checkout,
synchronized remote refs, or an unused tag. This allows it to validate a
candidate before commit and to run in tag CI's detached checkout. Those
mutable publication guards belong only to `scripts/release.sh`.
- `scripts/release.sh` accepts exactly one version, operates only against the
remote named `origin` and branch named `main`, and never accepts bypass,
force, alternate-remote, skip-check, or CI-wait flags.
- The publication script uses a two-phase guard. It checks cheap local and
upstream conditions before the potentially long candidate checker, then
fetches and rechecks the clean checkout, exact `HEAD == origin/main`, and
local/remote tag absence immediately before creating the tag. This prevents
a long validation run from publishing after the candidate or upstream tip
changed.
- Release tags remain explicitly unsigned lightweight tags. The script uses
`git -c tag.gpgSign=false tag VERSION COMMIT`, verifies that the ref type is
`commit` and resolves to the recorded commit, and pushes only
`refs/tags/VERSION:refs/tags/VERSION`. It never pushes `main` or uses
`git push --tags`.
- A failed push leaves the unpublished local tag intact for inspection. The
script never silently deletes or retries it. A successful push prints the
version and exact commit, returns zero immediately, and performs no later
network or CI action.
- Release-script behavioral tests use real temporary Git repositories and a
local bare `origin`, with a fixture-local stub candidate checker where a
full repository validation would add no confidence. They never touch the
developer's actual refs or any network service.
- Tag CI uses `golang:1.25.5` and
`woodpeckerci/plugin-release:0.3.1`. Its steps are named
`validate-release`, `build-release-assets`, and `publish-release` in that
dependency order.
- The release plugin explicitly sets title
`Narratio ${CI_COMMIT_TAG}`, note
`docs/releases/${CI_COMMIT_TAG}.md`, flattened SHA-256 checksums,
`file-exists: skip`, `overwrite: false`, and `prerelease: false`. Skipping an
existing remote release preserves idempotence and immutability; repairing a
failed published tag remains a new-patch-release operation.
- Release documentation describes optional later inspection but never makes
CI success, Gitea release creation, checksum download, or host asset execution
a required step after a successful tag push.
- This feature does not create the next release note, tag a version, push
`main`, or publish a release. Those actions occur only after this
implementation sprint is complete and a separate release candidate is
selected.
## Instructions For Every Stage
Before changing files in each stage:
1. Read `docs/development.md`, all documents under `docs/policy/`, this plan,
and the relevant portions of
[`release-procedure.md`](release-procedure.md).
2. Inspect the current scripts, Woodpecker workflows, release notes,
`internal/buildinfo`, `internal/app/version.go`, and focused repository
checks relevant to the stage. Prefer the codebase knowledge graph for Go
discovery and use text search for shell, YAML, and Markdown.
3. Confirm the worktree state and preserve unrelated changes.
4. Re-read the responsibility boundary before editing any publication command:
successful upstream tag push ends the synchronous release process.
During every stage:
- Keep public shell interfaces and file ownership exactly as settled above.
- Use the Go standard library and ordinary POSIX tools already available in the
builder images; do not add a shell framework, Go dependency, release SaaS,
or package-manager tool.
- Keep tests deterministic, offline, and free of real credentials and remote
infrastructure. Use `t.TempDir()` and local bare Git repositories for
publication behavior.
- Test consequential behavior at the narrowest stable boundary. Avoid tests
that merely snapshot complete script text or duplicate every shell branch.
- Make failures identify the responsible script, invalid value, or failed
command without printing environment dumps or secret values.
- Do not document future behavior outside `docs/roadmap/` until the stage that
makes that behavior usable. Update canonical current-behavior documentation
in the same stage that completes the release procedure.
- Format changed Go files, run `sh -n` on changed shell files, use focused
tests while iterating, and finish each stage with `git diff --check` plus the
stage's exit criteria.
- Do not create, delete, move, or push a real Narratio tag while implementing
or testing this feature.
## Stage 1 — Shared Release Library And Asset Builder
**Status: Completed**
### Goal
Create the single safe implementation of version validation and official
release-asset construction without changing Git refs, CI, or current release
documentation.
### Required Work
1. Create `scripts/release-lib.sh` as a source-only POSIX library. Give its
functions a Narratio-specific prefix. At minimum it must provide:
- a fatal-error helper that prefixes errors with the calling release tool;
- exact stable-semver validation; and
- a helper for locating the repository root from a public script's absolute
or relative path without depending on the caller's working directory.
Keep the library private to the scripts; it is not a supported user-facing
API.
2. Create executable `scripts/build-release-assets.sh` with the exact
`VERSION OUTPUT_DIR` interface and output-directory contract in the settled
decisions. Reject missing/extra arguments, malformed versions, relative or
dangerously broad output paths, symlink/non-directory output, and a
nonempty output directory before building anything.
3. Build the six official binaries in deterministic target order. Use the
current command package and build-info linker symbol; do not duplicate or
change application version semantics.
4. Verify exact host version output as settled above. Ensure a failed build or
version mismatch is fatal and leaves no false success message. Temporary
smoke output must be inside the validated staging directory and removed
narrowly.
5. Add focused tests under a new `internal/releasecheck` test package. Protect
the meaningful asset-builder risks without making the default Go suite
compile all six targets repeatedly:
- argument and semver rejection;
- refusal of unsafe, symlink, non-directory, and nonempty destinations;
- creation only beneath an accepted empty destination; and
- exact naming/version behavior through one controlled host build or a
command shim, whichever is simpler and remains behavior-focused.
The operational six-target build is exercised directly in the stage exit
criteria and later by candidate validation.
### Exit Criteria
- `sh -n scripts/release-lib.sh scripts/build-release-assets.sh` succeeds.
- Focused `internal/releasecheck` tests pass.
- Running the asset builder with a synthetic stable version and a fresh
temporary output directory produces exactly six official assets and the
host version check succeeds.
- Invalid and unsafe output examples fail before creating release assets.
- `go test ./...`, `go vet ./...`, `go build ./...`, and
`git diff --check` succeed.
## Stage 2 — Central Release-Candidate Checker
**Status: Completed**
### Goal
Create one read-only source-candidate validator that maintainers and tag CI can
run identically, eliminating drift between documented preparation and tagged
source validation.
### Required Work
1. Create executable `scripts/check-release-candidate.sh` with the exact
one-version interface. Source `release-lib.sh`, resolve and enter the
repository root, and validate the version before using it in a path.
2. Require `docs/releases/VERSION.md` to be nonempty, begin with the exact
`# Narratio VERSION` heading, and contain exact
`## Summary`, `## Compatibility`, `## Upgrade`, and `## Changes` headings.
Do not parse prose or impose historical-note requirements on other tags.
3. Enforce repository/module hygiene:
- exact Narratio module declaration;
- no tracked `go.work` or `go.work.sum`;
- no `vendor` directory;
- no single-line or block `replace` declaration in `go.mod`;
- `GOWORK=off go mod tidy -diff` leaves module files unchanged;
- all tracked `*.go` files are `gofmt` clean; and
- both working-tree and cached whitespace checks pass.
4. Run the complete Narratio candidate checks with `GOWORK=off`:
- `go test -count=1 ./...`;
- `go test -race -count=1 ./...`;
- `go vet ./...`;
- `go build ./...`;
- `go test -count=1 ./internal/doccheck`; and
- `go test -count=1 ./internal/config -run '^TestExamplesLoadAndValidate$'`.
5. Create one temporary directory with `mktemp -d`, install a trap that removes
only that verified directory, and invoke `build-release-assets.sh` with the
selected version. The candidate checker owns no independent target matrix,
filenames, linker flags, or version smoke logic.
6. Extend `internal/releasecheck` with lean behavioral coverage for the
candidate checker's guards. Use isolated fixture repositories and command
shims where needed so default tests do not recursively run the complete
Narratio suite. Cover malformed version/note, wrong module identity,
disallowed workspace/vendor/replace state, propagation of a failing owned
validation command, and evidence that owned Go commands receive
`GOWORK=off`.
7. Ensure the checker itself does not inspect the active branch, fetch a
remote, require a clean status, check tag availability, mutate refs, or
contact Woodpecker/Gitea. Those are deliberately outside this script's
contract.
### Exit Criteria
- `sh -n scripts/release-lib.sh scripts/build-release-assets.sh scripts/check-release-candidate.sh`
succeeds.
- Focused `internal/releasecheck` tests pass, including failure propagation.
- A disposable fixture with a compliant synthetic release note exercises the
candidate checker successfully; malformed fixtures fail with actionable
script-owned errors.
- The repository-wide test, race, vet, build, documentation, example,
formatting, module-tidiness, and whitespace checks all pass independently.
- No Git ref, tracked file, or external service is mutated by the checker.
## Stage 3 — Guarded Tag Publication Command
**Status: Completed**
### Goal
Provide one safe, non-interactive command that publishes exactly one guarded
lightweight tag and stops immediately when the upstream Git push succeeds.
### Required Work
1. Create executable `scripts/release.sh` with the exact one-version interface.
Source the shared library and reject invalid arguments before running Git
operations.
2. Implement the first cheap guard phase:
- require branch `main`;
- require an empty `git status --porcelain` result;
- fetch `origin main --tags`;
- record `HEAD^{commit}` as the immutable candidate for this attempt;
- require it to equal `origin/main^{commit}`;
- require the matching note to exist in that candidate; and
- reject an existing local or exact upstream tag.
3. Invoke `scripts/check-release-candidate.sh VERSION` without suppressing its
output or weakening any check.
4. Implement the second guard phase immediately after validation:
- refetch `origin main --tags`;
- require the checkout still to be clean and on `main`;
- require `HEAD` and `origin/main` still to equal the originally recorded
candidate commit; and
- recheck exact local and upstream tag absence.
Do not silently select a newer commit or restart validation.
5. Create the explicitly unsigned lightweight tag against the recorded commit,
verify its object type and resolved commit, show a concise local summary,
and push only its fully qualified tag ref to `origin`.
6. On push success, print one concise completion message containing the tag and
commit and exit zero immediately. There must be no commands after the push
that query CI, Gitea, a release API, assets, or checksums. Do not sleep or
retry waiting for asynchronous state.
7. On failure before tag creation, leave refs unchanged. On failure after local
tag creation, retain the local tag and return a clear instruction that it is
unpublished and requires inspection. Never automatically delete, move,
force-push, or reuse a tag.
8. Add behavioral tests in `internal/releasecheck` using temporary working
repositories and local bare `origin` repositories. Put a successful stub
candidate checker at the fixture's expected checked-in path so tests isolate
tag policy rather than rerun compilation. Cover at least:
- wrong branch and dirty checkout rejection;
- local `HEAD` not equal to `origin/main`;
- candidate-checker failure;
- existing local and upstream tag rejection;
- upstream `main` changing between the two guard phases;
- successful creation of a lightweight remote tag pointing to the exact
candidate commit;
- push failure retaining the local tag; and
- absence of any pushed branch or unrelated tag.
Tests must never use the real repository's `origin` or network.
### Exit Criteria
- All release scripts pass `sh -n`.
- Focused publication tests pass and prove the exact remote-tag outcome and
failure behavior.
- A success-path test demonstrates that the release command returns as soon as
the local bare-remote tag push succeeds; it performs no CI/Gitea follow-up.
- `go test ./...`, `go test -race ./...`, `go vet ./...`, `go build ./...`, and
`git diff --check` succeed.
- No real Narratio tag or remote ref is created or changed.
## Stage 4 — Asynchronous Woodpecker Release Integration
**Status: Pending**
### Goal
Make tag CI consume the shared candidate and asset contracts while preserving
strict validation-before-publication ordering and complete independence from
the synchronous tag command.
### Required Work
1. Rewrite `.woodpecker/release.yml` around three explicit steps:
- `validate-release` in `golang:1.25.5`, invoking
`./scripts/check-release-candidate.sh "$CI_COMMIT_TAG"`;
- `build-release-assets` in `golang:1.25.5`, depending on
`validate-release`, requiring a fresh absent/empty absolute `$PWD/dist`,
and invoking the shared asset builder; and
- `publish-release`, depending on `build-release-assets` and using
`woodpeckerci/plugin-release:0.3.1`.
Remove the redundant inline target matrix, validation command list, and
separate cross-build step from the release workflow.
2. Configure publication exactly as settled: the existing release-token
secret, `dist/narratio-*` files, explicit title and tagged note, flattened
`SHA256SUMS`, `file-exists: skip`, no overwrite, and no prerelease.
3. Preserve the tag-only event trigger. Do not add a pipeline callback, status
endpoint, wait command, or any coupling from `scripts/release.sh` to this
workflow.
4. Update `internal/doccheck/doccheck_test.go` so the dependency assertion names
`validate-release` and still proves transitively that publication cannot run
after failed validation. Extend its minimal YAML model only as needed to
protect consequential release invariants:
- the validator calls the shared checker;
- asset publication depends on validated shared construction;
- the release plugin image is pinned;
- title and note use the selected tag; and
- overwrite/prerelease remain disabled.
Avoid snapshotting the whole workflow or duplicating every plugin setting.
5. Confirm `.woodpecker/verify.yml` and `.woodpecker/shuffle.yml` remain normal
push/PR and scheduled validation. Do not make either wait for or invoke a
release operation.
### Exit Criteria
- The Woodpecker YAML parses and the focused `internal/doccheck` workflow
dependency/invariant tests pass.
- The release workflow has one target matrix owner and one candidate-check
owner, both under `scripts/`.
- `publish-release` has a dependency path to `validate-release`; no failure in
validation or asset building can publish.
- Static inspection confirms `scripts/release.sh` has no reference to
Woodpecker, Gitea release status, or CI polling.
- `go test ./...`, `go test -race ./...`, `go vet ./...`, `go build ./...`,
documentation/example checks, and `git diff --check` succeed.
## Stage 5 — Canonical Release Documentation And Final Audit
**Status: Pending**
### Goal
Document the now-implemented maintainer workflow in one canonical location,
make it discoverable, and perform a complete offline acceptance audit without
creating or publishing a real release.
### Required Work
1. Create `docs/release.md` as the canonical current-behavior procedure. It
must include:
- stable SemVer selection for a post-`v1.0.0` project;
- the exact future release-note structure;
- candidate preparation and ordinary `main` push expectations;
- direct use of `scripts/check-release-candidate.sh`;
- direct use and complete safety contract of `scripts/release.sh`;
- lightweight-tag immutability and correction through a new patch version;
- the explicit statement that successful upstream tag push completes the
release command; and
- a clearly optional asynchronous inspection section, separated from the
release steps and stating that a human may inspect CI/Gitea later but an
automated releaser must not wait for it.
Do not embed a second implementation of script guards as a long shell block;
the scripts own volatile mechanics and the document owns the maintainer
workflow and policy.
2. Update `docs/development.md` with a task-guide row pointing release work to
`docs/release.md`. Keep its validation summary concise and link to the
release procedure/checker rather than duplicating the candidate command
list.
3. Update `docs/policy/documentation.md` to assign canonical ownership for the
maintainer release procedure and version-matched historical notes. Preserve
the rule that release notes link to, rather than replace, current contract
documentation.
4. Update `docs/releases/README.md` to explain forward-looking note structure,
retain its existing `v1.5.0` entry, and identify the Gitea release collection
as the binary/checksum source when asynchronous publication succeeds. Do
not backfill or rewrite historical notes.
5. Review other current documentation for claims that release completion waits
on CI. Remove or link any conflicting duplication. Do not add release
mechanics to operations, CLI, architecture, or integration documents unless
they already own a directly affected current contract.
6. Mark `docs/roadmap/release-procedure.md` implemented only after scripts,
tests, CI, and canonical documentation match its target. Keep implementation
status in roadmap documents; current-behavior claims belong in
`docs/release.md` and the policy/developer routing documents.
7. Perform a final behavior and safety audit:
- inspect all scripts for unsafe path deletion, unresolved variables,
accidental secret output, broad ref pushes, force options, and commands
after the successful tag push;
- run all release-script behavioral tests against temporary local remotes;
- run the asset builder directly with a synthetic stable version and a
fresh temporary output directory, then verify the six filenames and host
version output;
- validate candidate-checker success in its isolated fixture and every
important failure class without creating a real note or tag;
- confirm the release workflow reads the note from the tagged tree and
cannot publish before validation; and
- confirm no test or documentation instruction requires live CI, Gitea,
credentials, or paid/external adapters.
### Exit Criteria
- All local Markdown links and Woodpecker dependency checks pass.
- `sh -n scripts/release-lib.sh scripts/build-release-assets.sh scripts/check-release-candidate.sh scripts/release.sh`
succeeds.
- Release-script tests and the direct six-target asset build pass without
touching real refs or external services.
- The full repository validation set from `docs/development.md` passes:
- `go test ./...`;
- `go test -race ./...`;
- `go vet ./...`;
- `go build ./...`;
- `go test ./internal/doccheck`; and
- `go test ./internal/config -run '^TestExamplesLoadAndValidate$'`.
- `go mod tidy -diff`, tracked-file `gofmt` inspection,
`git diff --check`, and `git diff --cached --check` succeed.
- The repository contains no synthetic release note, local test tag, build
asset, credential, temporary repository, or other generated test residue.
- No real release is tagged or pushed during this implementation plan.
## Open Questions
None. The feature roadmap and settled decisions above are sufficient to
implement the release procedure without additional product or policy choices.