# Release Procedure ## Release Model And Status Scriptorium publishes annotated semantic tags and tag-triggered Linux binary releases. The hosted [release workflow](../.woodpecker/release.yml) builds `amd64` and `arm64` executables, publishes their SHA-256 checksums, and uses the matching file under `docs/releases/` as the hosted release body. `v0.12.0` is the first published application-only release. For each later release, select a new `vMAJOR.MINOR.PATCH` version according to the intended compatibility change. A selected version remains an unreleased candidate until its annotated tag is published, the hosted workflow succeeds, and every published artifact is verified. Run this procedure from the Scriptorium repository root. A release must not depend on a Go workspace, module replacement, vendor tree, sibling checkout, unpublished dependency, or unpushed source commit. ## Establish The Candidate Start a POSIX shell, choose a semantic version that has not been published, and export it as `RELEASE_VERSION`. For example, if `v0.12.1` is the intended next version and remains unpublished, select: ```sh export RELEASE_VERSION=v0.12.1 ``` Use the version appropriate to the actual compatibility change rather than assuming that the example is the next release. Then run the following guard in that same shell: ```sh set -eu : "${RELEASE_VERSION:?export an unpublished vMAJOR.MINOR.PATCH version}" if ! printf '%s\n' "$RELEASE_VERSION" | grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' then printf '%s\n' "invalid release version: $RELEASE_VERSION" >&2 exit 1 fi RELEASE_COMMIT=$(git rev-parse --verify 'HEAD^{commit}') export RELEASE_COMMIT check_release_candidate() { test "$(git branch --show-current)" = main test -z "$(git status --porcelain)" gowork_value=$(go env GOWORK) case "$gowork_value" in ''|off) ;; *) printf '%s\n' "active Go workspace: $gowork_value" >&2 return 1 ;; esac test -z "$(git ls-files go.work go.work.sum)" test ! -e vendor if grep -Eq '^[[:space:]]*replace([[:space:]]|\()' go.mod then printf '%s\n' 'go.mod contains a replacement' >&2 return 1 fi git fetch origin main --tags test "$RELEASE_COMMIT" = \ "$(git rev-parse --verify 'refs/remotes/origin/main^{commit}')" if git show-ref --verify --quiet "refs/tags/$RELEASE_VERSION" then printf '%s\n' "local tag already exists: $RELEASE_VERSION" >&2 return 1 fi if test -n "$( git ls-remote --tags origin \ "refs/tags/$RELEASE_VERSION" \ "refs/tags/$RELEASE_VERSION^{}" )" then printf '%s\n' "remote tag already exists: $RELEASE_VERSION" >&2 return 1 fi } check_release_candidate ``` Do not continue unless this guard succeeds. It deliberately requires the candidate to be the exact clean commit already published at `origin/main`. ## Verify Modules And Repository Boundaries Confirm the module path and declared Go version: ```sh test "$( GOWORK=off go list -m -f '{{.Path}} {{.GoVersion}}' )" = 'gitea.maximumdirect.net/eric/scriptorium 1.25.5' ``` Require Promptkit `v0.1.0` as both the direct module-graph edge and the selected module version: ```sh direct_promptkit=$( GOWORK=off go mod graph | awk ' $1 == "gitea.maximumdirect.net/eric/scriptorium" && $2 ~ /^gitea\.maximumdirect\.net\/eric\/promptkit@/ { print $2 } ' ) test "$direct_promptkit" = \ 'gitea.maximumdirect.net/eric/promptkit@v0.1.0' test "$( GOWORK=off go list -m -f '{{.Path}}@{{.Version}}' \ gitea.maximumdirect.net/eric/promptkit )" = 'gitea.maximumdirect.net/eric/promptkit@v0.1.0' GOWORK=off go list -m all ``` Require tidy module metadata and recheck the repository exclusions: ```sh GOWORK=off go mod tidy -diff test -z "$(git ls-files go.work go.work.sum)" test ! -e vendor if grep -Eq '^[[:space:]]*replace([[:space:]]|\()' go.mod then printf '%s\n' 'go.mod contains a replacement' >&2 exit 1 fi test -z "$(git status --porcelain)" ``` ## Validate The Application Run the complete application validation: ```sh GOWORK=off go test ./... GOWORK=off go test -race ./... GOWORK=off go vet ./... validation_build_dir=$(mktemp -d) GOWORK=off go build \ -o "$validation_build_dir/scriptorium" \ ./cmd/scriptorium ``` The ordinary test run includes the architecture guard that rejects a root Go package, former framework package families, and imports of Promptkit internal packages. Inspect the repository for generated binaries, credentials, temporary output, sibling paths, and other files that do not belong in the tracked release source. Check every tracked Go file. This command must produce no output: ```sh unformatted=$( git ls-files '*.go' | while IFS= read -r go_file do gofmt -l "$go_file" done ) test -z "$unformatted" ``` Run the maintained render script and smoke-test both maintained configuration examples without a model call: ```sh GOWORK=off ./examples/render-markdown-summary.sh for config_file in examples/config.yml examples/config.full.yml do GOWORK=off go run ./cmd/scriptorium render \ --config "$config_file" \ --prompt generic.markdown_summary \ --input transcript=./examples/fixtures/transcript.md \ --input glossary=./examples/fixtures/glossary.yml \ --format json >/dev/null done ``` Exercise usage output and offline rendering with the temporary native executable: ```sh usage_output="$validation_build_dir/usage.txt" if "$validation_build_dir/scriptorium" >"$usage_output" 2>&1 then printf '%s\n' 'expected an invocation without a command to fail' >&2 exit 1 fi grep -F 'usage: scriptorium' "$usage_output" "$validation_build_dir/scriptorium" render \ --config ./examples/config.yml \ --prompt generic.markdown_summary \ --input transcript=./examples/fixtures/transcript.md \ --input glossary=./examples/fixtures/glossary.yml \ --format text >/dev/null ``` Follow every maintained local, Promptkit-tagged, and other external Markdown link. Confirm that all repository-relative link targets exist. Finish the application checks with: ```sh git diff --check test -z "$(git status --porcelain)" ``` ## Require Release Notes And Reproduce Packaging The immutable release note must exist before tagging: ```sh release_notes="docs/releases/$RELEASE_VERSION.md" test -f "$release_notes" test -s "$release_notes" ``` Validate every local and currently published link in the note. For links pinned to the candidate Scriptorium tag, confirm that the corresponding repository-relative path exists even though its tag URL is not live yet. Reproduce the hosted build flags, targets, and filenames in a temporary directory: ```sh release_dist=$(mktemp -d) release_package='gitea.maximumdirect.net/eric/scriptorium/cmd/scriptorium' build_release_binary() { target_os="$1" target_arch="$2" output="$release_dist/scriptorium-$RELEASE_VERSION-$target_os-$target_arch" CGO_ENABLED=0 GOOS="$target_os" GOARCH="$target_arch" GOWORK=off \ go build -trimpath -ldflags '-s -w' \ -o "$output" "$release_package" } build_release_binary linux amd64 build_release_binary linux arm64 test -s "$release_dist/scriptorium-$RELEASE_VERSION-linux-amd64" test -s "$release_dist/scriptorium-$RELEASE_VERSION-linux-arm64" file "$release_dist/scriptorium-$RELEASE_VERSION-linux-amd64" file "$release_dist/scriptorium-$RELEASE_VERSION-linux-arm64" ``` Require `file` to identify Linux executables for `x86-64` and `ARM aarch64`, respectively. Inspect the [hosted workflow](../.woodpecker/release.yml) and confirm that it uses the same build flags and names, copies the selected release note to `dist/RELEASE_NOTES.md`, publishes only `dist/scriptorium-*`, and keeps checksum generation enabled. ## Create And Publish The Tag Run the candidate guard again immediately before creating the tag: ```sh check_release_candidate test -f "$release_notes" test -s "$release_notes" ``` Create an annotated tag explicitly bound to the validated commit, using the version-specific release note as its message: ```sh git tag --annotate "$RELEASE_VERSION" \ --file "$release_notes" \ "$RELEASE_COMMIT" ``` Inspect the tag and require it to resolve to the validated source: ```sh test "$(git cat-file -t "refs/tags/$RELEASE_VERSION")" = tag git show --no-patch --decorate "refs/tags/$RELEASE_VERSION" test "$( git rev-parse --verify "refs/tags/$RELEASE_VERSION^{commit}" )" = "$RELEASE_COMMIT" ``` If inspection finds an error, delete only the unpublished local tag, correct the candidate, and repeat the complete validation. Never move or recreate a published tag. Push only the selected tag ref: ```sh git push origin \ "refs/tags/$RELEASE_VERSION:refs/tags/$RELEASE_VERSION" ``` ## Observe And Verify Publication Open the hosted workflow run for the selected tag. Require `build-release-assets` to succeed before `publish-release`, then require the publication step and hosted release to succeed. A queued, running, failed, or partially published workflow is not a verified release. Compare the local and remote annotated-tag objects and their source commits: ```sh remote_tag=$( git ls-remote --tags origin "refs/tags/$RELEASE_VERSION" | awk 'NR == 1 { print $1 }' ) remote_commit=$( git ls-remote --tags origin "refs/tags/$RELEASE_VERSION^{}" | awk 'NR == 1 { print $1 }' ) test -n "$remote_tag" test "$remote_tag" = \ "$(git rev-parse --verify "refs/tags/$RELEASE_VERSION")" test "$remote_commit" = "$RELEASE_COMMIT" ``` Download the hosted binaries and checksum file into a temporary directory: ```sh release_base="https://gitea.maximumdirect.net/eric/scriptorium/releases/download/$RELEASE_VERSION" download_dir=$(mktemp -d) ( cd "$download_dir" for asset in \ "scriptorium-$RELEASE_VERSION-linux-amd64" \ "scriptorium-$RELEASE_VERSION-linux-arm64" \ SHA256SUMS do curl --fail --location --remote-name "$release_base/$asset" done test -s "scriptorium-$RELEASE_VERSION-linux-amd64" test -s "scriptorium-$RELEASE_VERSION-linux-arm64" test -s SHA256SUMS sha256sum --check SHA256SUMS test "$(wc -l < SHA256SUMS | tr -d ' ')" = 2 file "scriptorium-$RELEASE_VERSION-linux-amd64" file "scriptorium-$RELEASE_VERSION-linux-arm64" ) ``` Require the same Linux architectures observed in the local packaging check and confirm that the hosted release contains no unexpected asset. On a compatible Linux host, make the matching downloaded binary executable and repeat the usage-output and offline-render smoke checks against it. Only after the tag, workflow, release body, binaries, architectures, and checksums all pass verification is the candidate a verified published release. ## Handle Failures Before tag publication, correct the release commit or note and restart the complete procedure. After tag publication, never delete, move, overwrite, or recreate the tag. A transient hosted failure may be retried only against the same immutable tag and commit and only when doing so cannot overwrite or silently retain partial assets. A source, packaging, note, or artifact defect requires a new corrective semantic version from a new validated commit. Record the selected version, validated commit, tag object, workflow result, artifact names, checksum result, and smoke-check outcome in the release checkpoint. Keep temporary builds and downloaded assets outside the repository, and require a clean `main` synchronized with `origin/main` when verification is complete.