243 lines
7.7 KiB
Markdown
243 lines
7.7 KiB
Markdown
# 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.
|