24 KiB
Release Procedure Upgrade Implementation Plan
Purpose And Status
This document is the executable implementation plan for
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. Neitherscripts/release.shnor 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, andscripts/release.sh VERSION. - All scripts are POSIX
sh, useset -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 repurposeHOME,CODEX_HOME, or common system-option variables. - A private source-only
scripts/release-lib.showns 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.shaccepts 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
amd64andarm64. Filenames arenarratio-VERSION-GOOS-GOARCH, with.exeonly on Windows. Builds use-trimpath,-s -w, and-X gitea.maximumdirect.net/eric/narratio/internal/buildinfo.Version=VERSION. - The asset builder verifies
narratio versionon 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 exactlynarratio VERSION. scripts/check-release-candidate.shis 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 withmktemp -d. Every Go command it owns runs withGOWORK=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 toscripts/release.sh. scripts/release.shaccepts exactly one version, operates only against the remote namedoriginand branch namedmain, 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 iscommitand resolves to the recorded commit, and pushes onlyrefs/tags/VERSION:refs/tags/VERSION. It never pushesmainor usesgit 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.5andwoodpeckerci/plugin-release:0.3.1. Its steps are namedvalidate-release,build-release-assets, andpublish-releasein that dependency order. - The release plugin explicitly sets title
Narratio ${CI_COMMIT_TAG}, notedocs/releases/${CI_COMMIT_TAG}.md, flattened SHA-256 checksums,file-exists: skip,overwrite: false, andprerelease: 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:
- Read
docs/development.md, all documents underdocs/policy/, this plan, and the relevant portions ofrelease-procedure.md. - 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. - Confirm the worktree state and preserve unrelated changes.
- 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 -non changed shell files, use focused tests while iterating, and finish each stage withgit diff --checkplus 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
- Create
scripts/release-lib.shas 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.
- Create executable
scripts/build-release-assets.shwith the exactVERSION OUTPUT_DIRinterface 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. - 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.
- 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.
- Add focused tests under a new
internal/releasechecktest 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.shsucceeds.- Focused
internal/releasechecktests 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 ./..., andgit diff --checksucceed.
Stage 2 — Central Release-Candidate Checker
Status: Pending
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
- Create executable
scripts/check-release-candidate.shwith the exact one-version interface. Sourcerelease-lib.sh, resolve and enter the repository root, and validate the version before using it in a path. - Require
docs/releases/VERSION.mdto be nonempty, begin with the exact# Narratio VERSIONheading, and contain exact## Summary,## Compatibility,## Upgrade, and## Changesheadings. Do not parse prose or impose historical-note requirements on other tags. - Enforce repository/module hygiene:
- exact Narratio module declaration;
- no tracked
go.workorgo.work.sum; - no
vendordirectory; - no single-line or block
replacedeclaration ingo.mod; GOWORK=off go mod tidy -diffleaves module files unchanged;- all tracked
*.gofiles aregofmtclean; and - both working-tree and cached whitespace checks pass.
- 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; andgo test -count=1 ./internal/config -run '^TestExamplesLoadAndValidate$'.
- Create one temporary directory with
mktemp -d, install a trap that removes only that verified directory, and invokebuild-release-assets.shwith the selected version. The candidate checker owns no independent target matrix, filenames, linker flags, or version smoke logic. - Extend
internal/releasecheckwith 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 receiveGOWORK=off. - 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.shsucceeds.- Focused
internal/releasechecktests 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: Pending
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
- Create executable
scripts/release.shwith the exact one-version interface. Source the shared library and reject invalid arguments before running Git operations. - Implement the first cheap guard phase:
- require branch
main; - require an empty
git status --porcelainresult; - 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.
- require branch
- Invoke
scripts/check-release-candidate.sh VERSIONwithout suppressing its output or weakening any check. - Implement the second guard phase immediately after validation:
- refetch
origin main --tags; - require the checkout still to be clean and on
main; - require
HEADandorigin/mainstill 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.
- refetch
- 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. - 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.
- 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.
- Add behavioral tests in
internal/releasecheckusing temporary working repositories and local bareoriginrepositories. 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
HEADnot equal toorigin/main; - candidate-checker failure;
- existing local and upstream tag rejection;
- upstream
mainchanging 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
originor 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 ./..., andgit diff --checksucceed.- 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
- Rewrite
.woodpecker/release.ymlaround three explicit steps:validate-releaseingolang:1.25.5, invoking./scripts/check-release-candidate.sh "$CI_COMMIT_TAG";build-release-assetsingolang:1.25.5, depending onvalidate-release, requiring a fresh absent/empty absolute$PWD/dist, and invoking the shared asset builder; andpublish-release, depending onbuild-release-assetsand usingwoodpeckerci/plugin-release:0.3.1. Remove the redundant inline target matrix, validation command list, and separate cross-build step from the release workflow.
- Configure publication exactly as settled: the existing release-token
secret,
dist/narratio-*files, explicit title and tagged note, flattenedSHA256SUMS,file-exists: skip, no overwrite, and no prerelease. - Preserve the tag-only event trigger. Do not add a pipeline callback, status
endpoint, wait command, or any coupling from
scripts/release.shto this workflow. - Update
internal/doccheck/doccheck_test.goso the dependency assertion namesvalidate-releaseand 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.
- Confirm
.woodpecker/verify.ymland.woodpecker/shuffle.ymlremain 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/doccheckworkflow dependency/invariant tests pass. - The release workflow has one target matrix owner and one candidate-check
owner, both under
scripts/. publish-releasehas a dependency path tovalidate-release; no failure in validation or asset building can publish.- Static inspection confirms
scripts/release.shhas no reference to Woodpecker, Gitea release status, or CI polling. go test ./...,go test -race ./...,go vet ./...,go build ./..., documentation/example checks, andgit diff --checksucceed.
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
- Create
docs/release.mdas the canonical current-behavior procedure. It must include:- stable SemVer selection for a post-
v1.0.0project; - the exact future release-note structure;
- candidate preparation and ordinary
mainpush 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.
- stable SemVer selection for a post-
- Update
docs/development.mdwith a task-guide row pointing release work todocs/release.md. Keep its validation summary concise and link to the release procedure/checker rather than duplicating the candidate command list. - Update
docs/policy/documentation.mdto 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. - Update
docs/releases/README.mdto explain forward-looking note structure, retain its existingv1.5.0entry, and identify the Gitea release collection as the binary/checksum source when asynchronous publication succeeds. Do not backfill or rewrite historical notes. - 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.
- Mark
docs/roadmap/release-procedure.mdimplemented only after scripts, tests, CI, and canonical documentation match its target. Keep implementation status in roadmap documents; current-behavior claims belong indocs/release.mdand the policy/developer routing documents. - 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.shsucceeds.- 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.mdpasses:go test ./...;go test -race ./...;go vet ./...;go build ./...;go test ./internal/doccheck; andgo test ./internal/config -run '^TestExamplesLoadAndValidate$'.
go mod tidy -diff, tracked-filegofmtinspection,git diff --check, andgit diff --cached --checksucceed.- 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.