# Release Procedure ## Release Model Promptkit publishes a Go library through source commits and semantic Go module tags. It does not publish runnable binaries or binary packages and does not currently use hosted CI. The release maintainer performs and records the required validation. `v0.1.0` is the initial published release. Later releases use semantic `vMAJOR.MINOR.PATCH` tags. Before `v1`, minor releases may change the public API and patch releases preserve compatibility within their minor line. Every pre-`v1` release note must summarize compatibility, identify public API changes, and state any action required of consumers. Promptkit releases are source-only. The annotated tag message is the release note; there is no separate hosted release or binary packaging step. ## Establish The Candidate Choose a version that has not been published and export it as `RELEASE_VERSION`. Run every command in this procedure from the Promptkit repository root in the same POSIX shell. Do not reuse `v0.1.0` or another existing version. The following guard derives the release commit from `HEAD` and stops on a missing or malformed version, a checkout other than synchronized `main`, uncommitted changes, an active Go workspace, a module replacement, a vendor tree, or an existing local or remote tag: ```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 the guard completes successfully. In particular, push the intended commit through the normal `main` branch workflow before release; the tag procedure is not a substitute for publishing the source commit. ## Validate The Candidate Confirm the module and root package metadata: ```sh go list -m -f '{{.Path}} {{.GoVersion}}' go list -f '{{.Name}} {{.ImportPath}}' . ``` The output must be: ```text gitea.maximumdirect.net/eric/promptkit 1.25.5 promptkit gitea.maximumdirect.net/eric/promptkit ``` As a release prerequisite, run the complete [maintainer validation workflow](development.md#maintainer-validation) against the clean candidate. Do not substitute a partial command list: the development guide owns the tests, race checks, analysis, build, both offline examples, formatting, Markdown links, generated-output and credential review, and repository hygiene. Record the successful workflow result with the candidate. ## Write The Release Note Prepare a plain-text annotated-tag message outside the repository and export its path as `RELEASE_NOTES_FILE`. Use this form, replacing each summary with release-specific text; write `None.` when there are no public API changes or consumer actions: ```text Promptkit vMAJOR.MINOR.PATCH Validated commit: full commit ID Compatibility: compatibility summary Public API changes: changes or None. Consumer action: required action or None. ``` After writing it, require all release-note fields, the selected version, and the validated commit to be present: ```sh : "${RELEASE_NOTES_FILE:?export the path to the release-note file}" test -f "$RELEASE_NOTES_FILE" test -s "$RELEASE_NOTES_FILE" grep -F "Promptkit $RELEASE_VERSION" "$RELEASE_NOTES_FILE" grep -F "Validated commit: $RELEASE_COMMIT" "$RELEASE_NOTES_FILE" grep -F 'Compatibility:' "$RELEASE_NOTES_FILE" grep -F 'Public API changes:' "$RELEASE_NOTES_FILE" grep -F 'Consumer action:' "$RELEASE_NOTES_FILE" ``` Inspect the complete message and confirm that it accurately records the compatibility impact, public API changes, and required consumer action. ## Create And Inspect The Tag Run the candidate guard again immediately before tag creation. This ensures that validation or release-note preparation did not change the checkout and that the commit is still published and untagged: ```sh check_release_candidate ``` Create the annotated tag from the prepared release note and bind it explicitly to the validated commit: ```sh git tag --annotate "$RELEASE_VERSION" \ --file "$RELEASE_NOTES_FILE" \ "$RELEASE_COMMIT" ``` Inspect both the tag message and its source commit before publication: ```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 the unpublished local tag, correct the release note or candidate, and repeat the guards. Never move or recreate a tag that has been published. ## Publish The Selected Tag Push only the selected tag ref. Do not use `git push --tags`: ```sh git push origin \ "refs/tags/$RELEASE_VERSION:refs/tags/$RELEASE_VERSION" ``` ## Verify Publication Compare the remote annotated-tag object with the local object, then compare the remote peeled source commit with the validated commit: ```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" ``` Finally, resolve the version as an ordinary Go module in a temporary module outside this repository and without a workspace or replacement: ```sh resolution_dir=$(mktemp -d) ( trap 'rm -rf "$resolution_dir"' 0 1 2 15 cd "$resolution_dir" GOWORK=off go mod init example.com/promptkit-release-check GOWORK=off go mod download \ "gitea.maximumdirect.net/eric/promptkit@$RELEASE_VERSION" resolved_version=$( GOWORK=off go list -m -f '{{.Version}}' \ "gitea.maximumdirect.net/eric/promptkit@$RELEASE_VERSION" ) test "$resolved_version" = "$RELEASE_VERSION" ) ``` Promptkit must publish and verify the required version before Scriptorium or another consumer publishes a release that depends on it. This ordering does not replace the consumer project's own release procedure. Released consumers must select the published Promptkit tag through ordinary module resolution, without a workspace, replacement, vendored Promptkit source, or unpublished revision. ## Policy Changes Document and approve a durable policy change before introducing hosted automation, binary artifacts, or different release governance. Update this procedure in the same change so maintainers do not rely on hidden release requirements.