Align release publication policy and runbook

This commit is contained in:
2026-09-28 01:50:26 +00:00
parent 65502ce5b4
commit ce91f04974
9 changed files with 291 additions and 325 deletions

View File

@@ -78,8 +78,9 @@ go test ./internal/config -run '^TestExamplesLoadAndValidate$'
The documentation check verifies local Markdown links and the dependency graph
of the Woodpecker workflows. The configuration check loads every maintained
pipeline and session example. Tag CI reuses this validation path before its
asynchronous asset publication; the maintainer release boundary is documented
pipeline and session example. Tag CI independently revalidates the tagged source
before asynchronous asset delivery; it is not a second synchronous release
prerequisite. The normal maintainer entry point is the single publication command
in the [Release Procedure](release.md).
Woodpecker also runs `go test -race -shuffle=on -count=3 ./...` on its scheduled

View File

@@ -3,7 +3,7 @@
## Standard Repository Policies
Material: `docs/policy/agents.md`, `architecture.md`, `documentation.md`,
`testing.md`, `meta.md`, and `oss.md`; root `AGENTS.md` and the policy reading
`testing.md`, `release.md`, `meta.md`, and `oss.md`; root `AGENTS.md` and the policy reading
guide in `docs/development.md`, adapted for this repository.
Copyright (C) 2026 Eric Rakestraw

View File

@@ -213,6 +213,6 @@ Common ownership of projects is neither required nor a blanket exception.
Apply the formatting convention prospectively. Do not rewrite published tags,
notes, binaries, or checksums to retrofit it. Address actual omissions deliberately
under the applicable terms and the [release correction policy](release.md#immutability-and-correction).
under the applicable terms and the [release correction policy](release.md#immutability-and-failure-recovery).
Ordinary attribution maintenance and release links are preparation work, not new
release approval gates.

View File

@@ -1,54 +1,60 @@
# Release Policy
**Base Policy Version:** 1.0
**Base Policy Version:** 1.4
**Last Updated**: 2026-09-25
**Last Updated**: 2026-09-28
**Upstream Source**: https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/policy/release.md
## Purpose
This policy defines the minimum requirements for selecting, validating,
publishing, verifying, and correcting releases. Its goal is to make every
release intentional, reproducible from an exact source revision, documented
for consumers, and safe from retrospective replacement.
This policy defines a proportionate workflow for validating and publishing an
intended source revision under a fresh version. Releases should require little
routine intervention, avoid repeated work, and preserve published contents.
The repository-specific commands that implement these requirements belong in
`docs/release.md`. Historical release summaries belong in `docs/releases/`.
Neither document may weaken this policy, including its explicit repository
exceptions.
The repository-specific commands belong in `docs/release.md`. Historical release
summaries belong in `docs/releases/`. The procedure must agree with the adopted
policy and link to it rather than introduce additional release requirements.
## Release Model
## Release Model And Completion
A release consists of:
A source release identifies one exact committed source revision through an
immutable version tag and a version-matched, checked-in release note. The
primary release branch is `main` unless the adopted policy specifies otherwise.
The procedure must name the authoritative publication remote.
- one exact source commit reachable from the repository's primary branch;
- one immutable version tag identifying that commit;
- one version-matched, checked-in release note; and
- every artifact, image, package, registry entry, or other deliverable the
repository declares for that release.
A successful push of the intended tag, with its source commit available on the
authoritative primary branch, completes the release. The source commit may be
pushed before the tag or published together with it through an atomic push.
The procedure must choose and document the supported approach.
The repository's primary branch is `main` unless an adopted policy documents
an exception. A release candidate must be committed and pushed to that branch
before its release tag is published.
Release commands must finish at successful publication. They must not wait for
or poll CI, require successful CI status, inspect downloadable artifacts, or
check consumer installation or Go module availability as completion gates.
A failed or ambiguous push requires resolving the authoritative remote state
before declaring success or retrying; successful pushes do not require a
separate remote read-back gate.
The source tag is the authoritative release identity. Published deliverables
must be traceable to the tagged source and must agree with its version. A
repository may publish source only. When it publishes additional deliverables,
its release procedure must enumerate them and define how each is built,
identified, published, and verified.
Repositories may publish source only. Additional binaries, images, packages,
checksums, or other declared deliverables may be published asynchronously from
the tagged source. The procedure must identify those deliverables and the
responsibility of their automation. Published deliverables must be traceable
to that source and identify the same version.
Release automation may validate or publish a release only when its exact
responsibility is documented. Automation must not silently change the source
candidate, move a published tag, replace a published deliverable, or repair a
release in place.
Track source-release completion and artifact delivery separately. Report delivery
failures and optional investigations under artifact-delivery status.
## Versioning And Compatibility
Stable releases use Semantic Versioning in the form `vMAJOR.MINOR.PATCH`, with
no leading zeroes. Each version must be unused locally and in every configured
remote or publication channel before publication begins.
no leading zeroes. Prereleases require an explicit adopted policy defining
their format and use.
Unless an adopted policy explicitly supports and defines prereleases,
prerelease versions are not permitted.
The selected version must be unused locally and on the authoritative remote.
Do not require queries to every configured remote or publication service.
A version known to have already been published must not be reused, even if the
normal guards do not inspect that channel. Adding a distribution channel
requires a deliberate update to the policy and procedure where needed.
For releases at or after `v1.0.0`:
@@ -61,161 +67,154 @@ configuration, data, integration, or compatibility contract when the release
note explains the impact and required consumer action. A patch release must
not intentionally break those contracts within its minor line.
A repository with additional compatibility promises must define them in its
adopted policy or canonical contract documentation. A release note may explain
a compatibility change, but it must not redefine the current compatibility
contract.
Additional compatibility promises and deliberate exceptions belong in the
adopted policy or canonical contract documentation. Release notes explain
changes; they do not redefine current compatibility contracts.
## Release Documentation
## Release Documentation And Preparation
Every release made under this policy must include
`docs/releases/<version>.md` in the tagged source tree. The filename must use
the complete version tag, and the document must begin with a title containing
the product name and that exact version.
Every new release must include `docs/releases/<version>.md` in the tagged
source tree. Use the complete version tag as the filename and begin with a
title containing the product name and that exact version.
Every new release note must contain these sections:
Include a short summary and any relevant compatibility impact or upgrade
instructions. Choose sections suited to the release and keep the note
proportionate to the change.
```markdown
# <Product> vMAJOR.MINOR.PATCH
Keep exact current contracts in their canonical documents and update affected
documentation with the behavior change. Link to those documents from the note
where useful. If annotated tags are used, derive their summary from the
checked-in note.
This release ...
Include the release footer defined by the [OSS policy](oss.md#discoverability)
in each new release document and preserve it on the hosted download page. The
release procedure defines the concrete URLs and publication mechanics. Prepare
these links with the release note; they do not add a network check or approval
gate.
## Summary
Handle version selection, release-note content, and retirement of completed
roadmaps during preparation. Preserve planned and deferred work and repair
affected links when retiring roadmaps. Reuse ordinary change review for
unintended files, credentials, and private data. Scope release-content review
to the tagged source.
## Compatibility
## Candidate Validation
## Upgrade
Provide one publication command that validates the final committed candidate,
checks publication state, and publishes the intended refs. A validation-only
mode or command may be provided for diagnosis, but must not be a required step
before publication repeats the same checks.
## Changes
```
Run the complete deterministic, offline default test suite once for the final
candidate, following the [testing policy](testing.md). For repositories that
publish builds, verify the required build targets. Select those targets for
the application's actual support commitments; native testing on each platform
and access to particular hardware are not default release prerequisites.
Use an explicit statement such as "No action required" when a section has no
substantive content.
Additional gates must address a concrete risk that the existing checks do not
adequately cover. Do not automatically require linting, race testing, module
hygiene audits, documentation audits, credential scans, example validation, or
expanded platform matrices solely because they are possible. Retain such
checks when their distinct protection justifies their cost. A one-time migration
check must not silently become a permanent release requirement.
Release notes are immutable historical summaries. They should link to current
canonical documentation for exact behavior. Every affected canonical document
must be updated in the same candidate; a release note must not substitute for
current documentation or defer required documentation changes.
Release tests must not require real credentials, mutable application services,
paid APIs, provider probes, or inference calls. The procedure may describe dependency
preparation separately from offline validation.
## Candidate Preparation And Validation
Avoid duplicate validation across release phases. Successful checks for the
same final candidate need not be repeated solely to satisfy another phase;
any reuse must clearly apply to that exact candidate. Persistent validation
caches are not required. If the candidate changes, validate the new candidate.
Cheap publication-state checks may be repeated immediately before pushing
because local or remote state may have changed during validation.
Each repository must provide one documented release-check command or script
that exercises the complete set of automated source-candidate gates. The
repository-specific procedure must state all additional manual gates.
Automate mechanical guards. Human review gates are strongly disfavored: add one
only through an explicit project-specific policy decision explaining the
concrete risk and why ordinary change review or automation is insufficient.
Do not add routine confirmations for checks the command can perform itself.
The required gates are:
## Narratio Candidate Checks
- dependency or module hygiene appropriate to the language and ecosystem;
- the complete deterministic, offline default test suite;
- static analysis and linting used by the repository;
- formatting and whitespace checks;
- validation of maintained examples, configuration, schemas, generated
sources, and documentation commands where applicable;
- review of changed local documentation links;
- removal of obsolete roadmap documents under `docs/roadmap/` that describe
completed work included in the release; and
- review for unintended files, generated output, credentials, private data,
and unrelated changes.
Narratio's candidate checker adds the following checks to the one full,
race-enabled offline suite:
- `go mod tidy -diff` detects an incomplete checked-in module graph without
changing source.
- Tracked Go formatting and diff whitespace checks catch inexpensive source
hygiene defects before publication.
- Race instrumentation adds distinct coverage for concurrent I/O,
cancellation, and shared state.
- `go vet ./...` detects static issues not established by runtime tests.
- The asset builder verifies the declared Linux amd64, Linux arm64, and
best-effort macOS arm64 binaries and checks the embedded version on the host.
The checker uses `GOWORK=off` and readonly module mode for tests, vet, and
builds. Versioned module replacements are allowed; filesystem replacements
are not release inputs. Workspace and vendor files are not prohibited solely
by their presence.
## Publication Guards
Immediately before publication, the maintainer or release automation must
fetch the current primary branch and tags from the authoritative remote and
verify all of the following:
Automate the checks needed to publish the intended source under a fresh version:
- the version has the required format and is not already published;
- the current branch is the primary release branch;
- the worktree and index are clean;
- the current commit is the recorded, validated candidate;
- the authoritative remote branch points to that same commit;
- the matching release note exists in that commit and has the required title
and sections;
- all required approvals and automated gates succeeded for that commit; and
- no local or remote tag already uses the selected version.
- the version has the adopted format and is unused locally and on the
authoritative remote;
- the worktree and index are clean on the primary release branch;
- the source being published is the exact candidate that passed validation,
including its matching release note;
- the remote primary branch already identifies that candidate, or an atomic
source/tag push will advance it to that candidate without discarding remote
history; and
- the tag identifies the validated candidate and only the intended refs are
published.
Create the version tag against the guarded commit and verify its target before
publishing it. Publish only the exact intended ref and deliverables. Broad
operations such as `git push --tags` are discouraged because they can publish
unreviewed refs unrelated to the release.
Fetch the authoritative branch and tag state as needed for these checks.
Do not force branch or tag updates to bypass a failed guard. Publish explicit
refs rather than all local tags. Do not require CI success, publication-service
availability checks, or a separate manual confirmation for each guard.
The repository-specific procedure must make these guards executable or give
copyable commands for performing them.
## Immutability And Failure Recovery
## Immutability And Correction
Published version tags and published contents are immutable. Do not move,
overwrite, recreate, or delete a published tag, or replace a published artifact,
checksum, signature, provenance statement, or release note under that version.
The checked-in note at the immutable tag is that release's historical record.
Published version tags and published deliverables are immutable. Do not move,
overwrite, recreate, or delete a published tag. Do not replace an artifact,
image, package, checksum, signature, provenance statement, or release note
under an already published version.
If validation fails before publication, correct and commit the candidate and
rerun the publication command. If a push fails or its outcome is ambiguous,
inspect the authoritative remote before retrying. Remove an unpublished local
tag only after confirming it was never published or used by a release channel.
An unavailable remote leaves publication status unknown; do not overwrite a tag
to resolve uncertainty.
If validation fails before publication, correct the candidate, rerun the
complete gates, and repeat the publication guards. An unpublished local tag
may be removed after confirming that it was never published or used by a
release channel.
Transient network, authentication, CI, or consumer-availability failures do not
by themselves require a new version. Asynchronous delivery may be retried from
the same immutable tag. Missing artifacts may be published later, provided
existing published contents are not replaced and the additions remain consistent
with the tagged source and any already-published checksum manifest. If that
cannot be achieved, publish a new version.
If publication or post-publication verification reveals a defect, leave the
published release intact. Correct the defect on the primary branch, choose a
new patch version or other semantically appropriate new version, write a new
matching release note, and perform the complete release procedure again.
A distribution channel's supported yank, deprecation, or advisory mechanism
may warn consumers away from a defective release, but it must not replace that
release's contents or reuse its version.
A defect requiring changes to published source or contents requires a corrected
commit, a new semantically appropriate version, a matching release note, and
the release procedure again. Leave the original release intact. A channel's
yank, deprecation, or advisory mechanism may warn consumers away from a defective
release without replacing its contents or reusing its version.
## Repository Release Procedure
Every adopting repository must maintain `docs/release.md` as the canonical
maintainer runbook for implementing this policy. It must define, in execution
order:
Maintain `docs/release.md` as a short, executable maintainer runbook. It must
identify:
1. maintainer prerequisites and required access;
2. version selection and compatibility review;
3. release-note creation;
4. candidate preparation and the complete validation command;
5. candidate commit, push, and exact commit recording;
6. publication guards;
7. exact tag and deliverable publication commands;
8. authoritative remote and consumer-facing verification; and
9. failure handling and correction.
1. prerequisites, primary branch, and authoritative remote;
2. version and release-note preparation;
3. one publication command with its selected local checks and automated guards;
4. declared deliverables, required build targets, and automation responsibilities;
5. the successful-push completion point and separately reported delivery status;
and
6. failure handling, ambiguous-push recovery, and asynchronous delivery retries.
The procedure should use copyable, fail-fast commands where practical. It must
name the primary branch, authoritative remote, tag form, published
deliverables, supported build targets, release automation responsibilities,
and consumer verification path. It must link to this policy instead of
restating or redefining its requirements.
## Policy Exceptions
### Tag-push completion and asynchronous deliverables
Narratio's automated release responsibility ends when the exact intended tag is
successfully pushed to `origin`. It must not wait for, poll, or check CI/Gitea
results, download assets, or verify their checksums as a completion condition.
Binary and checksum publication is asynchronous; consumer-facing inspection is
optional follow-up, not a gate on the source release.
This deliberately qualifies the base policy's complete-deliverable and
post-publication verification requirements. CI reliability is outside the
release command's scope; an unavailable publication service must not cause an
automated releaser to wait indefinitely or claim to repair a published version.
Compensating controls are complete local candidate validation (including supported
cross-builds and a host version check), exact source/tag guards, immutable source
tags, asynchronous build validation, and non-overwriting asset publication.
Successful tag publication is not evidence that binary assets are available.
Optional inspection and corrective-release procedures belong in
[the release procedure](../release.md).
### Authoritative publication scope
Narratio's synchronous publication guards inspect the authoritative `origin`
remote and its `main` branch, not every configured Git remote or a Gitea release
entry. The command does not add release-service or registry availability checks.
This qualifies the base policy's all-channel unused-version requirement.
Other remotes are not Narratio release authorities, and Gitea asset publication
is asynchronous. The compensating controls are local and origin tag uniqueness,
exact candidate matching, and the publisher's non-overwrite settings. Any known
already-published version or conflicting deliverable remains ineligible for
reuse: choose a new version rather than bypassing guards. A future additional
distribution channel requires deliberate policy/procedure updates.
Use copyable commands where practical. Keep optional diagnostics and consumer
verification clearly optional. The procedure must not turn preparation work,
ordinary change review, or post-publication investigation into additional
release gates.

View File

@@ -1,167 +1,114 @@
# Releasing Narratio
This is the maintainer runbook for the [Release Policy](policy/release.md).
Its [explicit exceptions](policy/release.md#policy-exceptions) preserve the
successful tag push as the automated responsibility boundary. Binary publication
is asynchronous; later consumer-facing inspection is optional.
The source release completes when the intended tag is successfully pushed to
`origin`; binary delivery is a separate asynchronous status.
## Prerequisites and publication scope
## Prepare the candidate
Use a checkout on `main` with Git authentication permitting fetches and pushes
to `origin`, the authoritative Git remote. Install Git, a POSIX shell, standard
Unix build tools, and the Go toolchain below. Make required modules available
before offline validation. No model/provider credentials are needed. Gitea
publication credentials belong to CI's secret store, not the local command.
Use `main` and the authoritative Git remote `origin`. Install Git, a POSIX
shell, standard Unix build tools, and Go 1.26. Have the selected Go modules
available before offline validation. Git access must permit fetch and tag push.
Application-provider credentials are not required; Gitea delivery credentials
belong to CI.
Synchronous guards inspect local tags and `origin`, not every configured remote
or Gitea release entry. Do not select a version known to have been published in
any channel, even if these guards cannot see it.
Choose an unused stable `vMAJOR.MINOR.PATCH` under the
[compatibility rules](policy/release.md#versioning-and-compatibility). Narratio
is past v1.0.0, so intentional CLI or configuration breaks require a major
release. Update affected canonical documentation with the change. Write the
matching `docs/releases/vMAJOR.MINOR.PATCH.md` with a short summary and any
relevant compatibility impact or upgrade action; choose sections to suit the
release.
## Go prerequisite
End that checked-in note with these three links, replacing the version in
each URL with the intended tag. The tag need not exist remotely yet. Check that
`COPYRIGHT`, `LICENSE`, and `docs/oss/attribution.md` are present in the
candidate. The tag CI reuses the checked-in note as the hosted release body.
Release preparation requires the maintained Go 1.26 baseline. Tag CI currently
uses the explicitly pinned `golang:1.26.7` image for validation and asset builds.
That asynchronous work does not change the tag-push completion boundary.
```markdown
[COPYRIGHT](https://gitea.maximumdirect.net/eric/narratio/src/tag/vMAJOR.MINOR.PATCH/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/narratio/src/tag/vMAJOR.MINOR.PATCH/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/narratio/src/tag/vMAJOR.MINOR.PATCH/docs/oss/attribution.md)
```
## Choose a version and write its note
Select an unused stable version under the
[versioning rules](policy/release.md#versioning-and-compatibility). Narratio is
past v1.0.0; intentional CLI/configuration breaks require a major release.
Create `docs/releases/vMAJOR.MINOR.PATCH.md` using the
[required note structure](policy/release.md#release-documentation).
Describe CLI, configuration, artifact, integration, and operating-contract
changes, and the required operator action. Link to current canonical
documentation for exact behavior.
## Prepare and record the candidate
Before committing, perform these manual reviews in addition to automated checks:
- Confirm compatibility/version selection and update affected canonical
documentation and maintained examples.
- Remove completed roadmap documents covering work included in this release;
retain pending work. Their history remains in Git. Do not delete historical
release notes.
- Review the diff, staged changes, and untracked files for credentials, private
data, generated output, unintended files, and unrelated changes.
- Review changed documentation links and behavior-bearing commands. Automated
link checks supplement rather than replace review.
Commit the note and candidate changes, then push that commit to `main` through
the ordinary development workflow. Record the exact candidate:
Retire completed roadmaps included in the release while preserving planned or
deferred work and repairing affected links. Review the change through the
ordinary development workflow for unintended files, credentials, private data,
and documentation accuracy. Commit and push the candidate to `origin/main`
before tagging; record its exact commit if needed:
```sh
git rev-parse HEAD
```
Do not create a tag until `origin/main` points to that same commit. If the
candidate changes, repeat the reviews and validation.
## Publish
## Validate the candidate
From the repository root, run:
From a clean `main` checkout at the pushed candidate, run one command:
```sh
scripts/check-release-candidate.sh vMAJOR.MINOR.PATCH
scripts/release.sh vMAJOR.MINOR.PATCH
```
The script also works when invoked by its absolute path from another directory.
It validates the version and matching note, module hygiene, formatting,
whitespace, uncached tests, race tests, static checks, documentation, examples,
and three cross-build assets. It uses `GOWORK=off`, does not contact application
services, CI, or Gitea, and does not create tags or modify tracked source.
Fix failures on `main`, commit, push normally, and rerun validation.
It fetches `origin/main` and tags; requires local `HEAD` to equal
`origin/main`; checks the committed note and unused version; runs the
candidate checker **once**; then repeats cheap mutable-state guards before
creating an unsigned lightweight tag at the recorded commit. It verifies the
tag target and pushes only
`refs/tags/vMAJOR.MINOR.PATCH:refs/tags/vMAJOR.MINOR.PATCH`. It does not
commit, push `main`, force updates, or publish all tags.
The macOS cross-build proves compilation, not native runtime behavior.
The checker runs `go mod tidy -diff`, tracked Go formatting and diff whitespace
checks, one `go test -mod=readonly -race -count=1 ./...` full suite, and
`go vet -mod=readonly ./...`. Its asset builder cross-builds Linux amd64,
Linux arm64, and best-effort macOS arm64, then checks the embedded version
on a host binary. The macOS cross-build proves compilation, not native
runtime behavior. Owned Go commands run with `GOWORK=off`; tests, vet, and
builds use readonly module mode. The deterministic suite includes documentation
and maintained-example checks. The command does not contact application
services or Gitea and does not wait for CI.
## Declared deliverables and automation
A successful tag push is the source-release completion point. The command
prints the tag and commit and exits without a separate network read-back.
For optional diagnosis before publication, run
`scripts/check-release-candidate.sh vMAJOR.MINOR.PATCH`; publication will
validate the candidate again, so this is not a required preparation step.
The source release is the exact version tag at the validated commit.
The tag-triggered `.woodpecker/release.yml` validates the tagged source,
runs `scripts/build-release-assets.sh`, and publishes these Gitea assets:
## Artifact delivery
Tag-triggered `.woodpecker/release.yml` independently validates the tagged
source, builds the same three targets, and publishes these Gitea downloads:
- `narratio-vMAJOR.MINOR.PATCH-linux-amd64`
- `narratio-vMAJOR.MINOR.PATCH-linux-arm64`
- `narratio-vMAJOR.MINOR.PATCH-darwin-arm64` (best effort)
- `SHA256SUMS`
The builder embeds the version, disables ambient Go workspaces, and checks a
host binary's `version` output. The release publisher uses the matching
checked-in note and generates SHA-256 checksums. Existing files are skipped and
overwrite is disabled. No image or package-registry deliverable is declared.
CI does not choose versions, modify source candidates, or move tags.
The builder embeds the tag version and checks host `version` output. CI
generates checksums and uses non-overwriting publication settings. It does
not select versions, change the source, or move tags. No image or package
registry output is declared. Tag publication is not proof that these
asynchronous downloads are available.
## Publish the tag
## Failure and recovery
From a clean checkout on `main` whose `HEAD` equals `origin/main`, run:
If validation fails before tagging, correct and commit the candidate, push
it to `main`, and rerun the publication command. If the tag push reports
failure, its outcome may be ambiguous: keep the local tag and inspect the
exact `refs/tags/vMAJOR.MINOR.PATCH` at `origin` before declaring success
or retrying. A reachable matching remote tag at the recorded commit establishes
publication; a confirmed absent tag permits investigation of the transport
failure. If `origin` cannot be inspected, publication status is unknown.
Do not move, overwrite, or automatically retry the tag. Remove a local tag
only after confirming it was never published or used by a release channel.
```sh
scripts/release.sh vMAJOR.MINOR.PATCH
```
A transient CI or delivery failure can be retried from the same immutable
tag. Missing assets may be added later if they match the tagged source and
any published checksum manifest; do not overwrite existing binaries,
checksums, or release-note contents. A source/content defect, or delivery
that cannot be corrected without replacement, requires a new suitable
version and matching note. Leave the original release intact.
The command fetches and checks `origin/main`, re-runs candidate validation,
then fetches and checks again before creating an explicitly unsigned lightweight
tag for the originally recorded commit. It refuses dirty, divergent, changed,
or already-tagged candidates. It verifies the local tag target and pushes only:
```text
refs/tags/vMAJOR.MINOR.PATCH:refs/tags/vMAJOR.MINOR.PATCH
```
It never commits changes, pushes `main`, force-pushes, moves tags, or pushes all
tags. Successful publication of that ref completes the command; it prints the
tag and commit and returns without waiting for CI, querying Gitea, downloading
assets, or checking checksums.
## Failure handling
Before tag creation, correct the candidate on `main` and repeat validation.
If a push fails after local tag creation, the local tag is retained for inspection.
Do not retry blindly or assume the remote received nothing: a connection failure
can leave publication uncertain. Inspect the exact remote ref before taking any
local-tag cleanup action. This is recovery from a failed command, not a
post-success publication gate.
After publication, follow the
[immutability and correction rules](policy/release.md#immutability-and-correction).
Correct defects or failed asynchronous publication with a new semantically
appropriate version, matching note, and the complete procedure. Never replace
existing tagged assets in place.
## Optional asynchronous inspection
A human may later inspect the tag-triggered Woodpecker job and Gitea release.
This is optional follow-up, never part of the automated releaser's completion
criteria. Do not wait for or poll CI/Gitea from the release command.
For optional source verification, substitute the released version below and
compare the remote target to the recorded candidate hash:
```sh
git ls-remote --tags origin refs/tags/vMAJOR.MINOR.PATCH
git rev-parse 'refs/tags/vMAJOR.MINOR.PATCH^{commit}'
```
Download the three named binaries and `SHA256SUMS` from the matching Gitea release
into an empty inspection directory. In that directory, verify on Linux:
```sh
sha256sum --check SHA256SUMS
```
On macOS, use `shasum -a 256 --check SHA256SUMS`. After successful verification,
make the matching host binary executable and run its version command, for example
on Linux amd64 (replace the version in both commands):
```sh
chmod +x ./narratio-vMAJOR.MINOR.PATCH-linux-amd64
./narratio-vMAJOR.MINOR.PATCH-linux-amd64 version
```
Expected output is `narratio vMAJOR.MINOR.PATCH`. Use only the matching architecture
on other supported hosts. Checksums establish consistency with the downloaded
checksum file, not a new signature or independent authenticity guarantee.
Missing assets are an asynchronous publication problem, not grounds to move or
reuse the successfully published source tag.
Optional consumer inspection may compare the tag's remote commit with the
recorded candidate, check the Woodpecker delivery job and hosted Gitea note,
download the assets into an empty directory, run `sha256sum --check
SHA256SUMS` (or `shasum -a 256 --check SHA256SUMS` on macOS), and run
the matching host binary's `version` command. This is separate delivery
observation, never a source-release gate.

View File

@@ -1,22 +1,11 @@
# Release Notes
This directory contains immutable historical release notes for Narratio.
Future notes are created with the matching stable version and use this minimum
structure:
```markdown
# Narratio vMAJOR.MINOR.PATCH
This release ...
## Summary
## Compatibility
## Upgrade
## Changes
```
Future notes use the matching stable version and begin with
`# Narratio vMAJOR.MINOR.PATCH`. Include a short summary and relevant
compatibility or upgrade guidance; choose sections suited to the release.
End each new note with the three immutable-tag notice links specified in the
[release procedure](../release.md).
See the [release procedure](../release.md) for creating a candidate and tag.
When asynchronous publication succeeds, the corresponding Gitea release is the

View File

@@ -260,7 +260,18 @@ recursive real full-suite execution from within checker test fixtures.
## Stage 5: Align publication behavior, policy, and operator procedure
Status: Pending.
Status: Complete (2026-09-28).
Verification: Adopted pinned release policy v1.4 with only metadata and the
selected Narratio validation rationale added to common wording. Publication
reports failed/uncertain tag pushes without asserting nonpublication; offline
bare-remote tests cover one checker call, ignored files, retained local tag,
and the existing exact-ref/branch/candidate guards. The runbook now has one
normal publication command, immutable-tag footer URLs, separate asynchronous
delivery status, and ambiguity/recovery guidance. Updated attribution and
release navigation without changing historical notes. `go test
./internal/releasecheck ./internal/doccheck`, shell syntax checks, and
`git diff --check` passed. No tag, push, or release ran.
Read pinned release v1.4, current docs/policy/release.md and docs/release.md,
scripts/release.sh, and internal/releasecheck/publication_test.go.

View File

@@ -48,6 +48,7 @@ func TestReleaseCommandRejectsUnpublishedOrExistingCandidates(t *testing.T) {
fixture.extraEnv = append(fixture.extraEnv, "NARRATIO_RELEASE_CHECKER_MODE=fail")
fixture.mustFail(t, "release candidate validation failed")
fixture.requireNoTag(t)
assertCheckerCalls(t, fixture, 1)
})
t.Run("local tag exists", func(t *testing.T) {
fixture := newReleaseFixture(t)
@@ -90,6 +91,8 @@ git push -q origin main
func TestReleaseCommandPushesOnlyTheRecordedLightweightTag(t *testing.T) {
fixture := newReleaseFixture(t)
writeReleaseFixtureFile(t, filepath.Join(fixture.worktree, ".git", "info", "exclude"), "ignored.tmp\n", 0o644)
writeReleaseFixtureFile(t, filepath.Join(fixture.worktree, "ignored.tmp"), "local-only\n", 0o644)
candidate := fixture.gitOutput("rev-parse", "HEAD^{commit}")
output, err := fixture.run()
if err != nil {
@@ -113,13 +116,14 @@ func TestReleaseCommandPushesOnlyTheRecordedLightweightTag(t *testing.T) {
if got := strings.Fields(fixture.gitOutput("--git-dir", fixture.origin, "for-each-ref", "--format=%(refname)", "refs/heads", "refs/tags")); !slicesEqual(got, []string{"refs/heads/main", "refs/tags/v1.2.3"}) {
t.Fatalf("remote refs = %#v", got)
}
assertCheckerCalls(t, fixture, 1)
}
func TestReleaseCommandKeepsUnpublishedLocalTagAfterPushFailure(t *testing.T) {
func TestReleaseCommandRetainsLocalTagAfterUnconfirmedPush(t *testing.T) {
fixture := newReleaseFixture(t)
writeReleaseFixtureFile(t, filepath.Join(fixture.origin, "hooks", "pre-receive"), "#!/bin/sh\nexit 1\n", 0o755)
output, err := fixture.run()
if err == nil || !strings.Contains(output, "local tag v1.2.3 was created but not published") {
if err == nil || !strings.Contains(output, "tag push for v1.2.3 failed or is unconfirmed") || strings.Contains(output, "release: published") || strings.Contains(output, "not published") {
t.Fatalf("push failure = %v\n%s", err, output)
}
if got := fixture.gitOutput("cat-file", "-t", "refs/tags/v1.2.3"); got != "commit" {
@@ -128,12 +132,25 @@ func TestReleaseCommandKeepsUnpublishedLocalTagAfterPushFailure(t *testing.T) {
if output, err := runReleaseGit(fixture.worktree, "--git-dir", fixture.origin, "show-ref", "--verify", "refs/tags/v1.2.3"); err == nil {
t.Fatalf("remote tag exists after failed push: %s", output)
}
assertCheckerCalls(t, fixture, 1)
}
func assertCheckerCalls(t *testing.T, fixture *releaseFixture, want int) {
t.Helper()
data, err := os.ReadFile(fixture.checkerLog)
if err != nil {
t.Fatal(err)
}
if got := len(strings.Fields(string(data))); got != want {
t.Fatalf("candidate checker called %d times, want %d: %q", got, want, data)
}
}
type releaseFixture struct {
worktree string
origin string
extraEnv []string
worktree string
origin string
checkerLog string
extraEnv []string
}
func newReleaseFixture(t *testing.T) *releaseFixture {
@@ -154,7 +171,7 @@ func newReleaseFixture(t *testing.T) *releaseFixture {
writeReleaseFixtureFile(t, filepath.Join(worktree, "docs", "releases", "v1.2.3.md"), "# Narratio v1.2.3\n", 0o644)
writeReleaseFixtureFile(t, filepath.Join(worktree, "README.md"), "fixture\n", 0o644)
fixture := &releaseFixture{worktree: worktree, origin: origin}
fixture := &releaseFixture{worktree: worktree, origin: origin, checkerLog: filepath.Join(root, "checker.log")}
fixture.git("init", "-qb", "main")
fixture.git("config", "user.email", "releasecheck@example.test")
fixture.git("config", "user.name", "Release Check")
@@ -176,7 +193,8 @@ func (fixture *releaseFixture) runWith(args ...string) (string, error) {
commandArgs := append([]string{filepath.Join(fixture.worktree, "scripts", "release.sh")}, args...)
command := exec.Command("sh", commandArgs...)
command.Dir = fixture.worktree
command.Env = append(os.Environ(), fixture.extraEnv...)
command.Env = append(os.Environ(), "NARRATIO_RELEASE_CHECKER_LOG="+fixture.checkerLog)
command.Env = append(command.Env, fixture.extraEnv...)
output, err := command.CombinedOutput()
return string(output), err
}
@@ -238,6 +256,7 @@ func writeReleaseFixtureFile(t *testing.T, path, contents string, mode os.FileMo
const releaseCandidateStub = `#!/bin/sh
set -eu
printf 'called\n' >> "$NARRATIO_RELEASE_CHECKER_LOG"
case "${NARRATIO_RELEASE_CHECKER_MODE:-success}" in
success) ;;

View File

@@ -97,7 +97,7 @@ if [ "$(git rev-parse --verify "refs/tags/$release_version^{commit}")" != "$cand
fi
if ! git push origin "refs/tags/$release_version:refs/tags/$release_version"; then
printf 'release: local tag %s was created but not published; inspect it before retrying\n' "$release_version" >&2
printf 'release: tag push for %s failed or is unconfirmed; local tag retained; inspect origin before retrying\n' "$release_version" >&2
exit 1
fi
printf 'release: published %s at %s\n' "$release_version" "$candidate_commit"