Files
scriptorium/docs/release.md

11 KiB

Release Procedure

Release Model And Status

Scriptorium publishes annotated semantic tags and tag-triggered Linux binary releases. The hosted release workflow 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 selected version for the pending first application-only release. It remains an unreleased candidate until its annotated tag is published, the hosted workflow succeeds, and every published artifact is verified. Later releases select a new vMAJOR.MINOR.PATCH version according to the intended compatibility change.

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

For the pending application-only release, start a POSIX shell and select:

export RELEASE_VERSION=v0.12.0

For a later release, export its not-yet-published semantic version instead. Then run the following guard in that same shell:

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:

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:

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:

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:

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:

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:

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:

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:

git diff --check
test -z "$(git status --porcelain)"

Require Release Notes And Reproduce Packaging

The immutable release note must exist before tagging:

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:

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 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:

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:

git tag --annotate "$RELEASE_VERSION" \
    --file "$release_notes" \
    "$RELEASE_COMMIT"

Inspect the tag and require it to resolve to the validated source:

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:

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:

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:

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.