377 lines
12 KiB
Markdown
377 lines
12 KiB
Markdown
# 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.
|