Prepare the v1.6.0 release
All checks were successful
ci/woodpecker/push/verify Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful

This commit is contained in:
2026-08-30 20:40:04 +00:00
parent 98139f7e8b
commit 545aa6893b
5 changed files with 75 additions and 1513 deletions

View File

@@ -22,4 +22,5 @@ See the [release procedure](../release.md) for creating a candidate and tag.
When asynchronous publication succeeds, the corresponding Gitea release is the
canonical source for downloadable binaries and checksums.
- [v1.6.0](v1.6.0.md)
- [v1.5.0](v1.5.0.md)

74
docs/releases/v1.6.0.md Normal file
View File

@@ -0,0 +1,74 @@
# Narratio v1.6.0
Narratio v1.6.0 makes large pipeline configurations easier to organize,
inspect, and vary while adding character-oriented artifact generation and a
guarded, reproducible release procedure.
## Summary
Pipeline configuration can now be assembled from explicit additive imports and
a selected production or testing profile. Campaigns can own a canonical,
versioned party roster, and Scriptorium artifact families can expand one
definition into concrete per-character artifacts, dependencies, variables, and
publish rules.
New read-only configuration commands expose the fully resolved pipeline,
source provenance, semantic digest, and profile differences before a session is
run. Stage reuse now records configuration-sensitive semantic evidence so
profile or configuration changes cannot silently reuse incompatible work.
## Compatibility
This is a backward-compatible feature release. Existing monolithic pipeline
files, concrete Scriptorium artifacts, publish rules, and configurations
without profiles remain supported. When profiles are declared and no explicit
profile is selected, Narratio uses the configured production default.
An unversioned party file plus a separate players file remains available as an
isolated legacy compatibility path, but it cannot drive artifact families. New
campaigns and new party-oriented features should use the `narratio.party.v1`
schema. Existing manifests remain readable; missing legacy semantic evidence
is treated as stale rather than trusted.
Narratio continues to consume the documented Notarius D&D pipeline contract.
The release workflow still cross-compiles Linux, macOS, and Windows binaries
for `amd64` and `arm64`; cross-compilation is not native runtime evidence for
macOS or Windows.
## Upgrade
No special action is required for existing monolithic configurations that do
not adopt profiles or artifact families. On the first run after upgrading,
stages recorded by older manifests may regenerate once because those records do
not contain the new semantic configuration evidence.
To adopt the new configuration model, use the maintained
`examples/production-testing` bundle as a migration reference: split stable
settings into explicit imports, define a production default and optional
testing profile, convert campaign party data to `narratio.party.v1`, remove the
separate players file, and then introduce character artifact families. Review
the result with `narratio config validate`, `config show`, `config sources`, and
`config diff` before running a session.
Campaign, session, previous-session, and run identifiers must satisfy the
documented portable identity grammar. Existing manifests or remote state with
unsafe legacy identifiers must be migrated before use.
## Changes
- Added root-owned, non-recursive additive pipeline imports with strict,
source-aware conflict detection.
- Added named pipeline profiles with an explicit production default and
deliberate command-line selection.
- Added `config validate`, `config show`, `config sources`, and `config diff`
for read-only inspection of resolved configuration and provenance.
- Added the strict `narratio.party.v1` campaign roster, including stable
character IDs, player and character names, optional aliases, and classes.
- Added deterministic players derivation and canonical party delivery to
downstream integrations.
- Added character-oriented artifact families, corresponding member
dependencies, family selection, and generated publish policies.
- Added semantic configuration fingerprints and stage-specific resume checks
across transcript and artifact stages.
- Added shared release candidate, asset build, and guarded tag-publication
scripts, with tag-triggered publication remaining asynchronous.

View File

@@ -1,471 +0,0 @@
# 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 five stages were completed in numeric order. This document is retained as
implementation history; current release behavior is owned by the checked-in
scripts and canonical release documentation. This plan superseded 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: Completed**
### 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: Completed**
### 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.

View File

@@ -1,739 +0,0 @@
# Pipeline Configuration Ergonomics
## Status
Accepted target state. This document owns the intended feature scope, user
intent, policy choices, and target end state until the behavior is implemented.
## Goal
Make Narratio's pipeline configuration easier to organize, vary, inspect, and
reuse without weakening its strict configuration contract or turning it into a
general-purpose configuration or workflow language.
The feature should let operators:
- split a large pipeline configuration into explicitly imported partial files;
- maintain one production-default pipeline plus a selectable testing profile;
- keep stable settings separate from frequently edited models and artifacts;
- define a character-oriented artifact workflow once and expand it for every
character in the selected campaign;
- reuse one campaign-owned party roster across multiple artifact families and
the Notarius integration; and
- inspect and compare the fully resolved configuration before running a
session.
All composition and expansion must produce the same concrete, validated
`PipelineConfig` and concrete configured-artifact map consumed by the existing
stage, planning, manifest, artifact, and adapter boundaries.
## User Intent
Pipeline configuration currently combines long-lived platform and transcript
settings with model choices, prompt-backed artifact definitions, and publish
rules that change frequently during development. Keeping all of those concerns
in one file makes edits noisy and makes production/testing variation difficult
to review.
Character-specific artifacts introduce a second kind of repetition. Several
artifact families, including character meta-analysis and character-specific
item tracking, should execute once per campaign character. Their prompt,
inputs, dependencies, and output shape are shared, while player, character,
class, alias, and output identity vary by party member. Those campaign facts do
not belong in a global pipeline definition.
The desired model is therefore:
```text
pipeline.yml + explicit imports + one selected profile
campaign.yml + party.yml ─┤
resolved effective configuration
expand party-driven families
defaults and strict validation
existing concrete pipeline and artifact model
```
## Architectural Constraints
The feature must preserve the configuration and orchestration policies in
[`docs/policy/architecture.md`](../policy/architecture.md):
- YAML remains strict and rejects unknown fields.
- Defaults remain centralized and are applied exactly once.
- Composition and expansion are deterministic and testable.
- Session templating remains narrow; this feature does not introduce arbitrary
expressions, scripting, or a general template language.
- The canonical stage sequence and fixed invalidation relation remain
application-owned and non-configurable.
- Stages receive resolved Narratio-owned configuration and do not implement
import, profile, or family semantics independently.
- External tools continue to own their private runtime configuration and
defaults.
- Raw secrets remain indirect and are never introduced into composed YAML,
effective-configuration output, manifests, or diagnostics.
Configuration composition belongs to `internal/config`. CLI selection and
production loading belong to `internal/app`. Party parsing must be a
Narratio-owned campaign-input contract. Artifact-family expansion must finish
before existing configured-artifact validation, catalog construction,
planning, reconciliation, or execution begins.
## Configuration Imports
### Root document
Keep `pipeline.yml` as the selected pipeline entry point. Add an optional
top-level composition envelope with explicitly ordered imports:
```yaml
composition:
imports:
- conf.d/platform.yml
- conf.d/transcripts.yml
- conf.d/extraction.yml
- conf.d/artifacts.yml
- conf.d/publish.yml
default_profile: production
profiles:
production:
overlay: profiles/production.yml
testing:
overlay: profiles/testing.yml
```
An existing monolithic `pipeline.yml` without `composition` remains valid and
retains its current behavior. The root document may contain ordinary pipeline
fields alongside `composition`, allowing incremental migration.
`imports` is the public term. Imported files are partial pipeline
configuration documents rather than independently runnable pipelines.
### Import boundaries
- Only the root pipeline document may declare `composition`, imports, or
profiles. Imported and overlay documents cannot import other documents.
- Every import is explicit. Narratio does not automatically scan `conf.d` or
interpret unlisted files.
- Import paths are relative to the root pipeline file's directory, must remain
confined beneath that directory, and must resolve to supported regular YAML
files. Absolute paths, traversal, non-regular files, and cycles are rejected.
- All ordinary relative pipeline paths retain one stable base: the root
pipeline file's directory. Moving a field between imported files must not
silently change the meaning of its relative path.
- The declared import order is retained for diagnostics and provenance, but it
is not an implicit precedence mechanism between base imports.
- Composition metadata is removed before the effective mapping is decoded as
strict `PipelineConfig` data.
### Additive merge contract
Base configuration from the root document and its imports is additive:
- mappings merge recursively when their child fields are disjoint;
- two files may therefore contribute different entries beneath a shared map
such as `scriptorium.artifacts`;
- defining the same final field or keyed entry in more than one base source is
an error, even when the values are identical;
- lists are atomic values and cannot be contributed more than once at the same
base configuration path;
- mapping/scalar, mapping/list, and other incompatible type collisions are
errors; and
- duplicate YAML keys within one source remain errors.
Errors must identify the complete configuration path and every source file
that claims it. This contract prevents an import reorder or newly added file
from silently changing production behavior.
Composition must operate on a presence-aware YAML representation before Go
struct decoding. Merging already decoded structs is not sufficient because it
cannot reliably distinguish omission from explicitly configured `false`, zero,
an empty collection, or another meaningful zero value.
## Named Profiles
### Selection and precedence
Exactly one named profile may be active for an invocation. A composition root
may define `default_profile`; the maintained production-shaped configuration
uses `production`. Add `--profile <name>` to the common configuration flags for
every command that loads pipeline configuration:
```bash
narratio run SESSION
narratio run SESSION --profile testing
narratio session plan SESSION --profile testing
narratio regenerate-artifacts SESSION --profile testing
```
The explicit CLI value overrides `default_profile`. An unknown profile, a
profile selection against a configuration that defines no such profile, an
empty profile name, or a default naming an undefined profile is an error before
stage composition. Do not add an environment-variable selector in this
feature; profile selection should remain visible in the command or root
configuration.
Only one profile can be selected. Profiles cannot extend other profiles, and
callers cannot stack several profiles. A single overlay provides the required
production/testing variation without creating user-programmable precedence.
### Overlay semantics
The selected profile is the sole intentional override layer:
- mappings recursively merge with the assembled base mapping;
- scalar and boolean leaves replace base leaves;
- lists replace the complete base list rather than concatenate implicitly;
- keyed maps such as configured artifacts merge by key, allowing a testing
profile to add experimental artifacts;
- an existing artifact can be disabled explicitly with `enabled: false`;
- an overlay cannot change a value's YAML kind incompatibly; and
- the fully overlaid result undergoes the same strict decode, defaults, path
resolution, expansion, and validation as a monolithic configuration.
Generic deletion syntax is outside this feature. Shared definitions should
live in base imports, while production-only and testing-only definitions live
in their corresponding overlays. Existing enable/disable fields and complete
list replacement cover the required cases without a YAML patch language.
### Effective identity and provenance
Every loaded configuration must retain bounded, non-secret provenance:
- the root pipeline path;
- the selected profile name, including whether it came from the default or
CLI;
- the ordered imported and overlay source paths;
- a deterministic digest of the normalized effective pipeline configuration;
and
- enough source ownership information to explain composition errors and
effective values.
Run reporting and invocation state should identify the selected profile and
effective configuration digest. The profile name is provenance, not a blanket
cache key: changing profiles must invalidate only work whose result-affecting
effective configuration changed.
## Canonical Campaign Party Contract
### Ownership and schema
Make `party.yml` a strict, versioned Narratio campaign-input contract. A
canonical file has this shape:
```yaml
schema_version: narratio.party.v1
characters:
arannis:
player:
name: Eric
character:
name: Arannis
alias:
- Ari
- The Grey Owl
classes:
- name: wizard
level: 8
brenna:
player:
name: Jane
character:
name: Brenna
classes:
- name: paladin
level: 6
- name: warlock
level: 2
```
The contract has these semantics:
- `schema_version` is required and must equal `narratio.party.v1`.
- `characters` is a non-empty mapping.
- Each mapping key is the stable `character_id` and must satisfy the existing
configured-artifact key grammar.
- `player.name` and `character.name` are required, non-empty display strings.
- `character.alias` is optional and accepts a list of zero or more non-empty
alias strings. The singular field spelling `alias` is intentional.
- Leading or trailing whitespace and control characters in names, aliases, and
class names are rejected rather than silently normalized.
- A character alias cannot equal that character's primary name, another alias,
or another character's primary name or alias under case-insensitive
comparison. This keeps party-member grounding unambiguous. Player names may
repeat because one player may control more than one character.
- `character.classes` is required and non-empty. Every entry has a required
free-form `name` and an optional positive integer `level`.
- Class names are not restricted to a Narratio-owned D&D enumeration.
- Duplicate class names for one character are rejected case-insensitively.
- Class order and alias order are preserved.
- Narratio derives a deterministic class summary such as
`paladin 6 / warlock 2` and an alias summary that joins declared aliases with
`, ` in their declared order.
- Unknown fields and trailing YAML documents are rejected.
The stable `character_id`, rather than a display name, owns generated artifact
identity. Changing a display name, alias, class, or level retains that identity
and changes the relevant semantic inputs. Changing the mapping key is an
intentional remove-and-add operation.
### Load and prepare behavior
The selected campaign's effective `party_file` resolves relative to
`campaign.yml`. Narratio must load and validate canonical party data during
combined pipeline/campaign resolution so `session plan`, configuration
inspection, and artifact-family expansion see the same roster before stage
execution.
`prepare` continues to materialize the canonical document as
`inputs/party.yml`, record its source and content identity, and expose it as
`narratio.input.party`. The same canonical file is passed unchanged to
Notarius's `party` reference slot, which currently accepts YAML reference
material without imposing a competing roster schema.
### Players projection and compatibility
The canonical party document becomes the eventual single source of truth for
player-to-character relationships. Narratio derives a deterministic,
documented players-only YAML projection during prepare, materializes it at
`inputs/players.yml`, records its content identity, and exposes it through the
existing `narratio.input.players` source. This preserves the separate Notarius
`players` reference and existing Scriptorium source without requiring every
campaign to maintain duplicate facts.
The projection uses this versioned shape, with entries sorted by stable
character ID:
```yaml
schema_version: narratio.players.v1
players:
- name: Eric
character:
id: arannis
name: Arannis
alias:
- Ari
- The Grey Owl
```
There is one projection entry per character rather than one grouped entry per
display player name. This permits one player to control several characters and
does not conflate distinct players who happen to share a display name.
Backward compatibility must be narrow and removable:
- an unversioned legacy party file continues to be treated as opaque reference
material when the existing `players_file` is also configured;
- legacy mode preserves the current prepared `party` and `players` behavior
but cannot drive artifact-family expansion;
- configuring a party-driven artifact family with a legacy roster produces a
clear migration error;
- canonical `narratio.party.v1` mode derives players and rejects a separate
`players_file`, preventing contradictory authorities;
- parsing, validation, documentation, and tests for legacy mode live behind a
clearly identified compatibility boundary with comments stating that it is
intended for removal after migration; and
- the maintained examples migrate to canonical mode and demonstrate no
separate players file.
The derived players projection is a durable integration contract and must be
documented under `docs/integrations/` rather than left as incidental generated
YAML.
## Party-Driven Artifact Families
### Configuration model
Add `scriptorium.artifact_families`, separate from the existing concrete
`scriptorium.artifacts` map:
```yaml
scriptorium:
artifact_families:
character_meta:
enabled: true
for_each: party.characters
prompt_id: dnd.character_meta
profile_id: production
output_path_pattern: artifacts/characters/{character_id}/meta.md
inputs:
transcript:
source: narratio.transcript.final_trimmed
required: true
member_vars:
player_name: player.name
character_name: character.name
character_class: character.class_summary
character_aliases: character.alias_summary
character_items:
enabled: true
for_each: party.characters
prompt_id: dnd.character_items
profile_id: production
output_path_pattern: artifacts/characters/{character_id}/items.md
inputs:
transcript:
source: narratio.transcript.final_trimmed
required: true
item_occurrences:
source: narratio.extraction.item_occurrences
required: true
member_vars:
character_name: character.name
```
Families support the ordinary result-affecting fields shared by concrete
Scriptorium artifacts: enablement, prompt and profile IDs, timeout,
render-debug policy, ordinary dependencies, inputs, and static variables.
`output_path_pattern` replaces concrete `output_path` at the family level.
`for_each` is not an expression language. This feature accepts only the exact
source `party.characters`. `member_vars` maps a Scriptorium variable name to
one of a closed set of canonical values:
- `character_id`;
- `player.name`;
- `character.name`;
- `character.class_summary`; and
- `character.alias_summary`.
Static `vars` and resolved member variables merge deterministically. Duplicate
variable names across the two maps are rejected rather than assigned implicit
precedence. Narratio's existing reserved sticky-session variable remains
application-owned and is applied after expansion under its current rules.
### Narrow substitution
`{character_id}` is the only family substitution supported in this feature.
It is required in `output_path_pattern` and may appear exactly where documented
for family-owned publish destinations. Unknown, repeated in an invalid
position, unresolved, or malformed substitutions are errors. Names, aliases,
classes, environment values, and arbitrary YAML paths cannot be interpolated
into configuration strings.
### Concrete expansion
For each family and party character, resolution creates one ordinary concrete
artifact key by joining the normalized family key, an underscore, and the
stable character ID:
```text
character_meta_arannis
character_items_arannis
```
Expansion must be sorted, deterministic, and completed before existing
configured-artifact defaults and validation. Every generated artifact then
uses the existing catalog, dependency planner, fingerprinting, manifest,
reconciliation, execution, materialization, and publish boundaries.
Validation rejects:
- invalid family keys or character IDs;
- generated keys that fail the existing configured-artifact grammar;
- collisions between generated artifacts, explicit concrete artifacts, or
family names;
- duplicate or escaping output paths;
- a family with no canonical party source;
- invalid member-variable selectors;
- a selected profile that leaves an executable family incomplete; and
- any expanded artifact that fails ordinary concrete-artifact validation.
An added party character creates missing concrete artifacts. Removing a
character removes those artifacts from effective configuration so existing
analyze reconciliation classifies their old records as removed. A change to a
member field changes only fingerprints that actually consume its resolved
value or the party document as an input.
### Dependencies between families
Ordinary `depends_on` continues to name shared concrete artifacts such as
`session_recap`. Add a typed `member_dependencies` list for corresponding
members of another family:
```yaml
character_items:
member_dependencies:
- character_meta
```
For `arannis`, this expands to a dependency on
`character_meta_arannis`. The referenced family must use the same party source
and generate the same character ID. Cycles and missing members are rejected by
the existing concrete dependency validation after expansion.
When a family input consumes the corresponding member output, provide a narrow
pre-expansion member-artifact source form owned by configuration resolution.
The syntax is:
```yaml
inputs:
prior_meta:
source: narratio.member_artifact.character_meta
required: true
```
For the `arannis` member, this resolves to
`narratio.artifact.character_meta_arannis` before ordinary runtime validation.
The referenced family must also appear in `member_dependencies`. The
`narratio.member_artifact.*` form must not survive into stage or adapter
configuration as a new runtime artifact kind.
### Selection and reporting
Extend artifact selection so an exact family key selects all of that family's
concrete members. An exact generated key selects only that member. Explicitly
selecting a disabled family follows the existing rule for explicitly selected
disabled concrete artifacts: all generated targets must still have valid
executable fields. Mixed family and concrete selections are normalized and
deduplicated after expansion.
Planning, run summaries, artifact listing, and effective-configuration output
must show both family origin and concrete artifact identity. Execution remains
entirely concrete; a family is not a dynamic pipeline stage or runtime loop.
### Family publish rules
Allow a family to declare one typed publish policy so operators need not repeat
one publish output per character:
```yaml
publish:
enabled: true
required: false
dest_pattern: artifacts/characters/{character_id}/meta.md
```
Resolution expands this into ordinary concrete publish rules. When
`dest_pattern` is omitted, the generated artifact's output path is used where
the existing publish contract permits derivation. Conflicts with explicit
publish rules, duplicate destinations, unsafe paths, and generated sources
that are unavailable are rejected by the existing publish policy after
expansion. Family publish behavior must not introduce wildcard source matching
at runtime.
## Profile-Safe Resume And Invalidation
Named profiles are safe only if result-affecting model and configuration
changes cannot silently reuse incompatible successful stage results. Artifact
analysis already has a versioned semantic fingerprint that includes effective
prompt/profile identifiers, variables, dependencies, inputs, and output
identity. The broader pipeline requires the same principle at every applicable
stage boundary.
Audit each stage's resume contract and add a versioned semantic configuration
fingerprint wherever current evidence does not already cover all
Narratio-observable result-affecting settings. In particular, switching an
Audita model through a profile must not reuse polish output produced by the
testing model. Conversely, changing only an operational timeout, binary path,
workspace path, diagnostic location, or profile name must not invalidate
byte-equivalent semantic work unless that value genuinely affects the stage's
canonical result.
The selected profile name and complete effective-config digest are retained as
provenance. Resume decisions use stage-specific effective semantics rather
than the profile name as a blanket invalidator. This permits production and
testing profiles to share unaffected transcript work while correctly staling
changed stages and their fixed transitive dependents.
Private configuration, prompt, model, executable, and module contents loaded
inside an external tool remain outside Narratio's observable fingerprint
boundary unless an existing integration contract exposes their identity.
Changing such private inputs continues to require explicit force, and the
documentation must not claim otherwise.
## Configuration Inspection
Add a `config` command family that performs composition without running a
session:
```bash
narratio config validate [--config <pipeline.yml>] [--profile <name>]
narratio config show [--config <pipeline.yml>] [--profile <name>]
narratio config sources [--config <pipeline.yml>] [--profile <name>]
narratio config diff <left-profile> <right-profile> [--config <pipeline.yml>]
```
Required behavior:
- `validate` composes imports and the selected profile, resolves campaign data
when required for party-driven families, expands artifacts and publish rules,
applies defaults, and runs strict validation without creating session or run
state.
- `show` emits deterministic normalized effective YAML, including generated
concrete artifacts, without raw credentials or environment values.
- `sources` reports root, import, profile, campaign, and party provenance and
identifies which source owns effective configuration paths.
- `diff` compares normalized effective configurations rather than raw files
and clearly identifies additions, removals, and value changes.
- output order is deterministic and suitable for review, but output formatting
is not a substitute for the effective configuration digest.
If family expansion requires a campaign, these commands accept the existing
campaign selection mechanisms or report clearly that pipeline-only validation
cannot complete the party-dependent portion. They must not silently choose an
unintended campaign.
## Required Application Changes
The implemented feature will require coordinated changes across existing
owners rather than a parallel configuration subsystem:
- `internal/config`: presence-aware YAML composition, import confinement,
profile overlays, provenance, effective digesting, party parsing and
validation, players projection, family expansion, defaults, and final strict
validation;
- `internal/app`: `--profile` plumbing through every configuration-consuming
command, `config` inspection commands, campaign-aware inspection, profile
reporting, and one shared loading path;
- `internal/artifactpolicy` and `internal/artifacts`: generated concrete source
registration, family-aware selection metadata, derived-player identity, and
reuse of the existing configured-artifact grammar and path policy;
- `internal/stage`: consumption only of expanded concrete artifacts, party and
derived-player preparation, and stage-specific semantic resume evidence;
- `internal/manifest`: bounded profile/effective-config provenance and any
versioned stage semantic fingerprints required for safe reuse;
- publish planning: expansion of family publish declarations into existing
concrete rules before ordinary validation and execution;
- Notarius composition: continue passing prepared `party` and `players`
sources, with canonical mode sourcing both from the party-owned contract;
and
- Scriptorium composition: receive only resolved concrete artifacts and
string/bool variables under the existing adapter contract.
There must be one production configuration-loading path. Remote session
loading, restore, status, planning, helper commands, and execution must not
independently reimplement import, profile, campaign, or family selection.
## Documentation And Examples
When behavior is implemented, update the canonical owners in the same change:
- [`docs/config.md`](../config.md): import, merge, profile, party selection,
family fields, defaults, and validation contracts;
- [`docs/cli.md`](../cli.md): `--profile` and `config` command syntax and output
behavior;
- [`docs/operations.md`](../operations.md): recommended production/testing
bundle layout, migration, inspection, and profile-switching workflow;
- [`docs/policy/architecture.md`](../policy/architecture.md): only the durable
composition, campaign-data ownership, and semantic-resume invariants;
- `docs/integrations/`: the canonical party and derived players YAML contracts;
- [`docs/internal/overview.md`](../internal/overview.md) and focused internal
configuration, manifest, analyze, prepare, and publish documents: implemented
ownership and mechanics; and
- `examples/`: a validated split configuration bundle, production/testing
overlays, canonical party roster with aliases and multiclass data, at least
two party-driven artifact families, and representative family publishing.
Do not copy complete example bundles into reference prose. Maintained examples
must be secret-free and loaded by the repository's example validation test.
## Testing Expectations
Testing should protect the public contracts and high-risk composition and
state-reuse behavior without coupling to private merge helpers. Required
confidence includes:
- monolithic configuration compatibility;
- recursive mapping composition and all duplicate/type/list conflict classes;
- import confinement, missing files, non-regular files, unsupported nesting,
source-aware diagnostics, deterministic ordering, and duplicate YAML keys;
- explicit zero-value profile overrides, list replacement, keyed-map additions,
unknown profiles, invalid defaults, and absence of profile stacking;
- canonical party schema validation, aliases, alias/name ambiguity, multiclass
summaries, stable IDs, malformed data, and legacy/canonical mode separation;
- deterministic players projection and prepared-input provenance;
- deterministic multi-family expansion, generated identities and paths,
variable binding, same-member dependencies, collisions, additions,
removals, and family selection;
- expansion into ordinary analyze planning, fingerprints, manifests, partial
selection, failure handling, and publish rules;
- model/profile changes staling only stages whose semantic configuration
changed while unaffected transcript work remains reusable;
- representative CLI validation, show, sources, diff, plan, and run behavior;
and
- maintained monolithic, imported, production, and testing examples loading
and validating offline.
Use package-level behavioral tests through the narrowest stable owner. A small
number of assembled workflow tests should prove that composed profiles and
party-driven families reach the existing pipeline correctly; higher-level
tests should not repeat every parser and validation case.
## Compatibility And Migration
- Existing monolithic pipeline files remain supported.
- Omitting `--profile` retains existing behavior for configurations without
profiles and selects the declared production default for composed bundles.
- Existing explicit concrete Scriptorium artifacts and publish rules remain
valid and may coexist with non-colliding families.
- Legacy opaque party plus players files remain temporarily usable only through
the isolated compatibility mode and cannot power party-driven families.
- Migration consists of converting the campaign party document to
`narratio.party.v1`, removing the separate players file, and then enabling
party-driven families.
- Configuration inspection must make the migration result reviewable before a
session run.
- Compatibility code and documentation must clearly identify the eventual
removal boundary; new features must target canonical party mode rather than
extend legacy semantics.
## Non-Goals And Deferred Work
This feature does not introduce:
- automatic loading of every file in `conf.d`;
- recursive imports, imports in profile overlays, or profile inheritance;
- multiple simultaneously active profiles;
- environment-selected profiles;
- generic YAML patch/delete operations;
- arbitrary string interpolation, expressions, scripting, or loops;
- artifact expansion from arbitrary files, Notarius outputs, or runtime data;
- configurable pipeline stages or a workflow DAG;
- runtime wildcard artifact or publish sources;
- automatic hashing of private external-tool configuration, prompts, modules,
models, or executables; or
- immediate removal of the isolated legacy party/player compatibility path.
Named reusable parameter sets beyond the canonical party roster may be
considered later if a second non-party use case demonstrates a need. Additional
party-member types, campaign-domain fields, or family iteration sources should
be added only through a versioned schema decision rather than an untyped
extension map.
## Target End State
The feature is complete when:
- operators can organize a pipeline as a root file plus explicit additive
imports with deterministic, source-aware conflict errors;
- one production-default or explicitly selected testing profile applies a
deliberate override without duplicating stable configuration;
- every command uses the same resolved profile and reports its identity;
- a campaign owns one strict, versioned party roster containing stable
character IDs, players, character names, optional alias lists, and one or
more classes;
- Narratio derives the existing players input from that roster and supplies the
canonical campaign context to Notarius;
- multiple Scriptorium artifact families expand once per campaign character
into ordinary concrete artifacts, dependencies, variables, output paths, and
optional publish rules;
- adding, changing, or removing a character produces deterministic and
manifest-correct artifact reconciliation;
- profile model changes cannot silently reuse semantically incompatible stage
outputs and do not unnecessarily invalidate unaffected transcript work;
- operators can validate, display, trace, and compare effective configuration
without executing a session;
- existing monolithic and concrete-artifact configurations remain compatible;
and
- current user, operator, integration, architecture, internal, and example
documentation accurately owns the implemented contracts.

View File

@@ -1,303 +0,0 @@
# Release Procedure Upgrade
## Status
Implemented. This document records the accepted release-process scope,
maintainer policy, and delivered end state.
## Goal
Give Narratio one reproducible, guarded minor/patch release procedure that
validates a source candidate before publication, creates the existing binary
release inputs consistently, and makes the exact upstream tag push the clear
completion boundary.
The procedure should borrow WeatherReporter's binary-publication model and
Notarius's centralized source-candidate validation and immutable-tag guards,
while retaining Narratio's own repository-wide checks and six supported build
targets.
## Responsibility Boundary
A Narratio release is complete when the selected tag has been successfully
pushed to its exact tag ref in the upstream Git repository. The release command
must then return successfully without polling Woodpecker, waiting for a
pipeline, querying a Gitea release, downloading assets, or otherwise making its
result depend on CI availability.
The tag event may asynchronously cause Woodpecker to validate the tagged tree,
build binaries and checksums, and create a Gitea release. That work is outside
the synchronous release procedure. CI failure or delay does not change whether
the Git tag was successfully published, and release tooling must never wait
indefinitely for infrastructure outside the upstream Git push.
An operator may inspect CI or published assets later. Such inspection is an
optional asynchronous follow-up, not a release completion condition and not a
step that an automated coding agent must perform after pushing the tag.
## Release Model
- Releases use unused stable semantic-version tags in the form
`vMAJOR.MINOR.PATCH`; prereleases are outside this feature.
- Narratio is past `v1.0.0`, so a minor release adds backward-compatible
functionality, a patch release contains backward-compatible fixes, and an
intentional breaking change to a documented public contract requires a new
major version.
- Release tags remain lightweight, matching Narratio's established tag
history, and are bound explicitly to a guarded commit on `main`.
- Every new release has a nonempty checked-in note at
`docs/releases/<tag>.md` in the tagged tree.
- Published tags are immutable. They are never moved, reused, overwritten, or
deleted, even when asynchronous CI fails to create a complete binary
release.
- The Gitea release, when asynchronous publication succeeds, remains the
canonical source for downloadable binaries and checksums. The Git tag and
checked-in note remain valid source-release history independently of CI.
- Existing historical tags and notes are not retroactively rewritten or
backfilled to satisfy the new procedure.
## Canonical Documentation
Add `docs/release.md` as the canonical maintainer procedure. Update
`docs/development.md` to route release preparation, tagging, and publication to
it, and update the documentation policy so it assigns ownership as follows:
- `docs/release.md` owns version selection, candidate preparation, validation,
tag guards, tag publication, the responsibility boundary, and failure
recovery;
- `docs/releases/<tag>.md` owns the immutable historical summary for one
release;
- `docs/releases/README.md` owns the release-note index and points to the Gitea
release collection for downloadable assets; and
- current CLI, configuration, operation, integration, and internal contracts
remain in their existing canonical documents rather than being restated in
release notes.
The maintainer procedure must be written as a complete safe workflow suitable
for either a human maintainer or a coding agent. Commands must be explicit,
non-interactive, and scoped to the selected version and tag ref.
## Release Notes
Require each future note to use this minimum structure:
```markdown
# Narratio vMAJOR.MINOR.PATCH
This release ...
## Summary
## Compatibility
## Upgrade
## Changes
```
The compatibility section must identify changes to documented CLI,
configuration, durable artifact, integration, or operating contracts. The
upgrade section must state required operator actions or explicitly say that no
special action is required. When the Notarius consumer contract changes, the
note should identify the supported/tested contract version and any required
Notarius upgrade.
Release notes summarize a historical candidate and link to current canonical
documentation for exact behavior. They must not substitute for updating that
documentation, include credentials or private infrastructure details, or make
claims that have not been validated against the candidate.
## Shared Release Scripts
### Source-candidate checker
Add the POSIX-shell checker:
```text
scripts/check-release-candidate.sh vMAJOR.MINOR.PATCH
```
It must be deterministic, non-interactive, safe to run repeatedly, and usable
both by maintainers before tagging and by tag CI in a detached checkout. It
must not create or push Git tags, modify tracked source, contact CI, or require
live WhisperX, LLM, Notarius, Scriptorium, object-storage, or notification
services.
The checker must fail clearly unless all of the following hold:
- the argument is a stable semantic version;
- `go.mod` declares Narratio's expected module path;
- the matching release note exists, has the exact Narratio/version heading,
and contains the required sections;
- Go workspace use is disabled for candidate commands;
- tracked `go.work`/`go.work.sum`, vendoring, and `go.mod` replacement
directives are absent;
- uncached repository tests, race tests, vet, build, documentation checks, and
maintained-example validation succeed;
- `go mod tidy -diff` reports no module-file change;
- every tracked Go source file is `gofmt` clean;
- working-tree and cached whitespace checks succeed; and
- all supported release binaries cross-compile with the selected version
embedded, with the host-platform binary reporting exactly
`narratio <tag>` through `narratio version`.
The checker must preserve the distinction already documented by Narratio:
cross-compilation is compilation evidence, not native macOS or Windows runtime
evidence.
### Asset builder
Add `scripts/build-release-assets.sh`, used by both the candidate checker and
release CI. It must build exactly these CGO-disabled targets:
- Linux `amd64` and `arm64`;
- macOS `amd64` and `arm64`; and
- Windows `amd64` and `arm64`.
Asset names must remain deterministic and include the Narratio version,
operating system, architecture, and `.exe` suffix for Windows. Build output
must use a caller-supplied, narrowly validated staging directory and must not
perform broad or ambiguous deletion. Official assets use `-trimpath` and the
established release version linker override.
### Tag-publication command
Add the narrowly scoped release command:
```text
scripts/release.sh vMAJOR.MINOR.PATCH
```
The command owns local validation, publication guards, lightweight tag
creation, local tag verification, and pushing only the selected tag ref. It
must not commit candidate changes or push `main`; the candidate must already be
committed and present on upstream `main`.
Before creating a tag, the command must:
- run the shared candidate checker;
- require the current branch to be `main`;
- require a clean worktree and index;
- fetch upstream `main` and tags;
- record the exact candidate commit and require local `HEAD` to equal
`origin/main`;
- require the version-matched release note from that commit; and
- reject an existing local or upstream tag of the selected name.
It must create an explicitly lightweight tag against the recorded commit,
verify that the local tag resolves directly to that commit, and push only:
```text
refs/tags/<version>:refs/tags/<version>
```
It must never use `git push --tags`. If the tag push succeeds, the command must
report the version and commit and exit successfully immediately. In
particular, it must not invoke a CI client, poll a build status, wait for a
Gitea release, or verify release assets.
If a failure occurs before the upstream tag is published, no release has
completed. An unpublished local tag may be removed only after the maintainer
has inspected the failure. The script should not silently delete it. A
successful upstream push is final even if later asynchronous work fails.
## Woodpecker Release Workflow
Retain tag-triggered asynchronous release automation, but make it consume the
same checked-in contracts as local preparation:
- validate the tag syntax and matching release note before asset work;
- call the shared candidate checker rather than maintaining an independent
drifting validation list;
- use the shared asset builder for the six official binaries;
- pin the Go builder image to `golang:1.25.5`, an explicit patch version
compatible with the current `go.mod` language version;
- pin the release plugin to the reviewed explicit version
`woodpeckerci/plugin-release:0.3.1` rather than a floating image;
- set the Gitea release title explicitly to `Narratio <tag>`;
- use `docs/releases/<tag>.md` from the tagged tree as the release body;
- generate a flattened `SHA256SUMS` file for all official binaries;
- keep prerelease publication disabled and overwrite disabled; and
- keep validation, asset building, and publication dependency-ordered so a
failed CI validation cannot publish assets.
The workflow must not require a callback to, acknowledgement from, or status
update in the release command. The two sides have intentionally separate
responsibilities:
```text
local release command asynchronous infrastructure
--------------------- ---------------------------
validate candidate
guard main and exact commit
create lightweight tag
push exact upstream tag ────────► validate tagged tree
return success immediately build assets and checksums
create Gitea release
```
## Failure And Correction Policy
Failures before tag publication are corrected on `main`, followed by complete
candidate revalidation and fresh guards.
After a tag has been pushed, neither CI failure nor a defect permits changing
that tag. Correct the cause on `main`, choose a new patch version, write a new
release note, and repeat the complete release procedure. Do not move or delete
the published tag, manually overwrite generated assets, or make the release
command wait for repaired infrastructure.
A maintainer may later inspect the remote tag, Gitea note, assets, checksums,
and host binary. Documentation may provide these as optional asynchronous
inspection commands, clearly outside the release responsibility boundary. No
automated release instruction may require them after a successful tag push.
## Security And Operational Constraints
- Local scripts never read or print the Gitea release token; the token remains
a Woodpecker-owned secret supplied only to the release plugin.
- Candidate validation remains offline with respect to paid APIs, mutable
application services, and private user data.
- Temporary build output is confined to a validated temporary or staging
directory and cleaned safely.
- Scripts avoid environment dumps and never record credentials in release
notes, logs, repository files, or command output.
- The release command does not edit tracked files, create commits, or push
branches. Apart from fetching refs for its guards, its only release-state
mutations are creating the selected local tag and pushing that exact tag.
## Target End State
When this feature is complete:
1. A maintainer or coding agent prepares and commits a standardized release
note plus any final canonical-documentation updates.
2. The candidate is pushed to `main` through the ordinary development
workflow.
3. One shared checker proves the exact tagged source is clean, self-contained,
formatted, tested, race-tested, statically checked, documented, module
tidy, cross-buildable, and version-correct.
4. One guarded release command refuses ambiguous or unpublished candidates,
creates the established lightweight tag, and pushes only that tag.
5. The command completes successfully at the upstream tag push and performs no
CI or Gitea status wait.
6. Woodpecker independently reuses the shared checks and asset builder and may
publish six versioned binaries, release notes, and SHA-256 checksums.
7. Published tags remain immutable, and failures after tag publication are
corrected only through a new version.
8. The release procedure is documented in one canonical location, linked from
contributor guidance, enforced by scripts and CI, and clear about the
boundary between guaranteed Git publication and best-effort asynchronous
binary publication.
## Non-Goals
This feature does not:
- repair, replace, monitor, or improve Woodpecker reliability;
- wait for or synchronously verify CI or Gitea release completion;
- introduce prereleases, signed tags, containers, package-manager publication,
installers, auto-updaters, or native macOS/Windows test runners;
- publish `main` automatically or create release-note content automatically;
- run live external adapters or paid model calls during release validation; or
- rewrite historical tags or manufacture retrospective release notes.