Adopt updated policies and simplify release publication
This commit is contained in:
461
docs/release.md
461
docs/release.md
@@ -1,377 +1,130 @@
|
||||
# Release Procedure
|
||||
|
||||
## Release Model And Status
|
||||
The [release policy](policy/release.md) owns versioning, compatibility,
|
||||
publication completion, and immutability. This runbook supplies Scriptorium's
|
||||
commands; release notes under `docs/releases/` are historical summaries.
|
||||
|
||||
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.
|
||||
## Prepare The Candidate
|
||||
|
||||
`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:
|
||||
Use a POSIX shell, Git with a configured tagger identity and access to
|
||||
`origin`, and a Go toolchain compatible with `go.mod`. The native host must
|
||||
support Go's race detector and have its required C compiler. If Git is
|
||||
configured to sign tags, its signing tool and key must be available. Run from the
|
||||
repository root. Provision the toolchain and module cache before offline
|
||||
validation; when dependencies are not cached:
|
||||
|
||||
```sh
|
||||
export RELEASE_VERSION=v0.12.1
|
||||
GOWORK=off go mod download
|
||||
```
|
||||
|
||||
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:
|
||||
Choose an unused stable version under the release policy. Commit its note as
|
||||
`docs/releases/<version>.md`, beginning with a title such as
|
||||
`# Scriptorium v0.14.0` (an example, not an instruction to select that version).
|
||||
Include a concise summary and relevant compatibility impact or upgrade action.
|
||||
Update affected canonical documentation with the change, and retire completed
|
||||
roadmap material when appropriate while preserving deferred work and links.
|
||||
No extra release-specific review signoff is required.
|
||||
|
||||
Finish and commit the intended candidate on `main`, then publish that branch:
|
||||
|
||||
```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
|
||||
git -c push.followTags=false push origin refs/heads/main:refs/heads/main
|
||||
```
|
||||
|
||||
Do not continue unless this guard succeeds. It deliberately requires the
|
||||
candidate to be the exact clean commit already published at `origin/main`.
|
||||
The publication command inspects the configured push endpoint of `origin`,
|
||||
not a potentially stale remote-tracking ref. If a separate push URL is
|
||||
configured, it is authoritative for both checks and publication. Configure one
|
||||
publication URL; multiple push endpoints cannot be validated as one remote.
|
||||
|
||||
## Verify Modules And Repository Boundaries
|
||||
## Publish
|
||||
|
||||
Confirm the module path and declared Go version:
|
||||
Invoke the single [publication command](../scripts/release.sh), substituting
|
||||
the selected version:
|
||||
|
||||
```sh
|
||||
test "$(
|
||||
GOWORK=off go list -m -f '{{.Path}} {{.GoVersion}}'
|
||||
)" = 'gitea.maximumdirect.net/eric/scriptorium 1.25.5'
|
||||
sh scripts/release.sh v0.14.0
|
||||
```
|
||||
|
||||
Require Promptkit `v0.9.0` as both the direct module-graph edge and the selected
|
||||
module version:
|
||||
The command validates a clean worktree and index on `main`, an unused local and
|
||||
remote version, exact agreement with authoritative remote `main`, and a
|
||||
checked-in note whose first-level title contains Scriptorium and the exact
|
||||
version. Ignored local files do not make the candidate dirty.
|
||||
|
||||
The existing source-boundary guards reject committed workspaces, vendor trees,
|
||||
and module replacements. No migration-specific exact Promptkit version audit
|
||||
is required.
|
||||
|
||||
It then runs the complete suite once with the race detector and builds Linux
|
||||
amd64 and arm64 binaries using `CGO_ENABLED=0`, `-trimpath`, and
|
||||
`-ldflags '-s -w'`. The native test run uses the host platform and enables cgo.
|
||||
Validation disables workspaces, uses read-only module mode, and disables module
|
||||
and toolchain downloads. Missing dependencies must be provisioned separately;
|
||||
fix the environment and rerun the command. No persistent validation cache is
|
||||
maintained by the release tool.
|
||||
|
||||
The command rechecks candidate and publication state, creates an annotated tag
|
||||
from the committed note, and pushes only that tag's exact object to its explicit
|
||||
ref. It does not push branches, follow other tags, force updates, or wait for
|
||||
CI. Temporary notes and validation builds are removed on exit. The successful
|
||||
summary identifies the source commit and tag object and reports binary
|
||||
delivery separately as asynchronous and unverified.
|
||||
|
||||
## Asynchronous Binary Delivery
|
||||
|
||||
The tag-triggered [hosted workflow](../.woodpecker/release.yml) builds and
|
||||
publishes:
|
||||
|
||||
- `scriptorium-<version>-linux-amd64`;
|
||||
- `scriptorium-<version>-linux-arm64`; and
|
||||
- `SHA256SUMS`.
|
||||
|
||||
The matching checked-in note supplies the hosted release body. The workflow
|
||||
owns image selection, packaging, checksum generation, and upload configuration.
|
||||
Its upload step skips existing files and does not overwrite the release.
|
||||
Source-publication success does not attest to workflow success or asset
|
||||
availability.
|
||||
|
||||
Workflow inspection, downloads, checksum verification, and a compatible-host
|
||||
smoke test are optional delivery diagnostics. They must not delay or redefine
|
||||
source-release completion. No polling or consumer-installation check is part
|
||||
of the publication command.
|
||||
|
||||
## Failure Recovery
|
||||
|
||||
Before tag creation, correct and commit a failed candidate, publish `main`,
|
||||
and invoke the publication command again. A changed candidate must pass its
|
||||
own validation.
|
||||
|
||||
After an unsuccessful push, the command queries the authoritative endpoint.
|
||||
If it confirms the exact tag object and candidate, it reports source success.
|
||||
Otherwise it exits unsuccessfully and preserves the local tag. It never retries
|
||||
a push or deletes a tag automatically. If the endpoint is unavailable, the
|
||||
publication status is unknown, not evidence that nothing was published.
|
||||
|
||||
For manual diagnosis, select the affected version and compare the local tag
|
||||
object and peeled commit with the authoritative remote:
|
||||
|
||||
```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.9.0'
|
||||
test "$(
|
||||
GOWORK=off go list -m -f '{{.Path}}@{{.Version}}' \
|
||||
gitea.maximumdirect.net/eric/promptkit
|
||||
)" = 'gitea.maximumdirect.net/eric/promptkit@v0.9.0'
|
||||
GOWORK=off go list -m all
|
||||
release_version=v0.14.0
|
||||
release_remote=$(git remote get-url --push origin)
|
||||
git rev-parse "refs/tags/$release_version" "refs/tags/$release_version^{commit}"
|
||||
git ls-remote "$release_remote" refs/heads/main \
|
||||
"refs/tags/$release_version" "refs/tags/$release_version^{}"
|
||||
```
|
||||
|
||||
Require tidy module metadata and recheck the repository exclusions:
|
||||
Do not retry while the outcome remains unknown. If the tag exists remotely,
|
||||
do not move, recreate, or delete it. If the tag was never published or used by
|
||||
any release channel, an explicitly authorized local cleanup may delete only
|
||||
that unpublished tag; then rerun the command to validate and publish the
|
||||
candidate. The command intentionally rejects an existing local tag rather than
|
||||
assuming that a previous process validated it.
|
||||
|
||||
```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 and Promptkit v0.9 feature scripts, then smoke-test
|
||||
both maintained configuration examples without a model call:
|
||||
|
||||
```sh
|
||||
GOWORK=off ./examples/render-markdown-summary.sh
|
||||
GOWORK=off ./examples/render-v0.9-features.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.
|
||||
For transient delivery failures, inspect what has already been uploaded before
|
||||
retrying the same immutable tag. The upload step's skip behavior alone does not
|
||||
prove checksum consistency. Add missing assets only if they match the tagged
|
||||
source and any already-published manifest, without replacing existing contents.
|
||||
Compare rebuilt bytes with published checksums when recovering a partial
|
||||
upload; do not assume toolchain changes reproduce identical binaries. If safe
|
||||
completion is impossible, or published source, notes, packaging, or artifact
|
||||
contents need correction, leave the original release intact and publish a new
|
||||
version from a corrected commit.
|
||||
|
||||
Reference in New Issue
Block a user