Document the published Promptkit release process
This commit is contained in:
@@ -31,4 +31,9 @@ Contributors should start with the [development guide](docs/development.md).
|
|||||||
The [architecture policy](docs/policy/architecture.md) defines the library
|
The [architecture policy](docs/policy/architecture.md) defines the library
|
||||||
boundary and constraints that framework work must preserve.
|
boundary and constraints that framework work must preserve.
|
||||||
|
|
||||||
|
## Related Project
|
||||||
|
|
||||||
|
[Scriptorium](https://gitea.maximumdirect.net/eric/scriptorium) is the CLI and
|
||||||
|
HTTP application built on Promptkit.
|
||||||
|
|
||||||
Promptkit is licensed under the [GNU General Public License version 3](LICENSE).
|
Promptkit is licensed under the [GNU General Public License version 3](LICENSE).
|
||||||
|
|||||||
254
docs/release.md
254
docs/release.md
@@ -7,27 +7,91 @@ tags. It does not publish runnable binaries or binary packages and does not
|
|||||||
currently use hosted CI. The release maintainer performs and records the
|
currently use hosted CI. The release maintainer performs and records the
|
||||||
required validation.
|
required validation.
|
||||||
|
|
||||||
The first planned release is `v0.1.0`. Do not create that tag until the
|
`v0.1.0` is the initial published release. Later releases use semantic
|
||||||
framework has been extracted and the resulting public library has passed this
|
`vMAJOR.MINOR.PATCH` tags. Before `v1`, minor releases may change the public
|
||||||
procedure. Later tags use the `vMAJOR.MINOR.PATCH` form. While Promptkit remains
|
API and patch releases preserve compatibility within their minor line. Every
|
||||||
pre-`v1`, release notes must identify intentional public API changes and any
|
pre-`v1` release note must summarize compatibility, identify public API
|
||||||
consumer migration required by them.
|
changes, and state any action required of consumers.
|
||||||
|
|
||||||
## Prepare The Release
|
Promptkit releases are source-only. The annotated tag message is the release
|
||||||
|
note; there is no separate hosted release or binary packaging step.
|
||||||
|
|
||||||
Work from a clean checkout of the intended release commit, outside any Go
|
## Establish The Candidate
|
||||||
workspace and without a local module replacement. Confirm the source commit is
|
|
||||||
already published through the normal branch workflow.
|
|
||||||
|
|
||||||
From the Promptkit repository root, verify the checkout:
|
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
|
```sh
|
||||||
gowork=$(go env GOWORK)
|
set -eu
|
||||||
test -z "$gowork" || test "$gowork" = off
|
|
||||||
test -z "$(git status --short)"
|
: "${RELEASE_VERSION:?export an unpublished vMAJOR.MINOR.PATCH version}"
|
||||||
git fetch --tags origin
|
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:
|
Confirm the module and root package metadata:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -42,7 +106,7 @@ gitea.maximumdirect.net/eric/promptkit 1.25.5
|
|||||||
promptkit gitea.maximumdirect.net/eric/promptkit
|
promptkit gitea.maximumdirect.net/eric/promptkit
|
||||||
```
|
```
|
||||||
|
|
||||||
Run the same default Go validation required by the
|
Run the complete maintainer validation required by the
|
||||||
[development guide](development.md):
|
[development guide](development.md):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -53,84 +117,158 @@ go build ./...
|
|||||||
go run ./examples/go-library/prepare
|
go run ./examples/go-library/prepare
|
||||||
```
|
```
|
||||||
|
|
||||||
Check every tracked Go file and repository whitespace:
|
Check every tracked Go file. This command must produce no output:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
gofmt -l $(git ls-files '*.go')
|
unformatted=$(
|
||||||
|
git ls-files '*.go' |
|
||||||
|
while IFS= read -r go_file
|
||||||
|
do
|
||||||
|
gofmt -l "$go_file"
|
||||||
|
done
|
||||||
|
)
|
||||||
|
test -z "$unformatted"
|
||||||
|
```
|
||||||
|
|
||||||
|
Follow every maintained Markdown link and confirm that its local or published
|
||||||
|
target exists. Review the repository for generated binaries, test or coverage
|
||||||
|
output, credentials, template residue, downloaded assets, and other files that
|
||||||
|
do not belong in source control.
|
||||||
|
|
||||||
|
Recheck module and repository hygiene, whitespace, and the clean checkout:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
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
|
||||||
git diff --check
|
git diff --check
|
||||||
|
test -z "$(git status --porcelain)"
|
||||||
```
|
```
|
||||||
|
|
||||||
The formatting command must produce no paths. Follow every maintained Markdown
|
## Write The Release Note
|
||||||
link and confirm its target exists. Review the repository for generated
|
|
||||||
binaries, test or coverage output, credentials, template residue, and other
|
|
||||||
files that do not belong in source control.
|
|
||||||
|
|
||||||
Confirm that no workspace override is tracked and that `go.mod` contains no
|
Prepare a plain-text annotated-tag message outside the repository and export
|
||||||
`replace` directive:
|
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:
|
||||||
|
|
||||||
```sh
|
```text
|
||||||
git ls-files go.work go.work.sum
|
Promptkit vMAJOR.MINOR.PATCH
|
||||||
rg -n '^replace\b' go.mod
|
|
||||||
|
Validated commit: full commit ID
|
||||||
|
Compatibility: compatibility summary
|
||||||
|
Public API changes: changes or None.
|
||||||
|
Consumer action: required action or None.
|
||||||
```
|
```
|
||||||
|
|
||||||
Both commands must produce no output. Re-run `git status --short` and require a
|
After writing it, require all release-note fields, the selected version, and
|
||||||
clean result after every validation and review check.
|
the validated commit to be present:
|
||||||
|
|
||||||
## Create And Publish The Tag
|
|
||||||
|
|
||||||
Choose the semantic version from the intended compatibility change. Record the
|
|
||||||
release commit before tagging:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
release_version=v0.1.0
|
: "${RELEASE_NOTES_FILE:?export the path to the release-note file}"
|
||||||
release_commit=$(git rev-parse HEAD)
|
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"
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace the example version for later releases and keep both values in the same
|
Inspect the complete message and confirm that it accurately records the
|
||||||
shell for the remaining commands. Confirm the tag does not already exist
|
compatibility impact, public API changes, and required consumer action.
|
||||||
locally or remotely:
|
|
||||||
|
## 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
|
```sh
|
||||||
test -z "$(git tag --list "$release_version")"
|
check_release_candidate
|
||||||
test -z "$(git ls-remote --tags origin "refs/tags/$release_version")"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Create an annotated tag whose message identifies the release and records that
|
Create the annotated tag from the prepared release note and bind it explicitly
|
||||||
the documented validation passed for the tagged commit:
|
to the validated commit:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git tag --annotate "$release_version" \
|
git tag --annotate "$RELEASE_VERSION" \
|
||||||
--message "Promptkit $release_version; documented validation passed for $release_commit"
|
--file "$RELEASE_NOTES_FILE" \
|
||||||
|
"$RELEASE_COMMIT"
|
||||||
```
|
```
|
||||||
|
|
||||||
Inspect the tag before publication:
|
Inspect both the tag message and its source commit before publication:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git show --no-patch --decorate "$release_version"
|
test "$(git cat-file -t "refs/tags/$RELEASE_VERSION")" = tag
|
||||||
test "$(git rev-list -n 1 "$release_version")" = "$release_commit"
|
git show --no-patch --decorate "refs/tags/$RELEASE_VERSION"
|
||||||
|
test "$(
|
||||||
|
git rev-parse --verify "refs/tags/$RELEASE_VERSION^{commit}"
|
||||||
|
)" = "$RELEASE_COMMIT"
|
||||||
```
|
```
|
||||||
|
|
||||||
Publish the tag without relying on a hosting-provider-specific release
|
If inspection finds an error, delete the unpublished local tag, correct the
|
||||||
interface:
|
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
|
```sh
|
||||||
git push origin "refs/tags/$release_version"
|
git push origin \
|
||||||
|
"refs/tags/$RELEASE_VERSION:refs/tags/$RELEASE_VERSION"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Verify Publication
|
## Verify Publication
|
||||||
|
|
||||||
Confirm that the remote tag object matches the local annotated tag and still
|
Compare the remote annotated-tag object with the local object, then compare the
|
||||||
resolves to the intended source commit:
|
remote peeled source commit with the validated commit:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
remote_tag=$(git ls-remote --tags origin "refs/tags/$release_version" | awk '{print $1}')
|
remote_tag=$(
|
||||||
test "$remote_tag" = "$(git rev-parse "refs/tags/$release_version")"
|
git ls-remote --tags origin "refs/tags/$RELEASE_VERSION" |
|
||||||
test "$(git rev-list -n 1 "refs/tags/$release_version")" = "$release_commit"
|
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"
|
||||||
```
|
```
|
||||||
|
|
||||||
Promptkit must publish the required tag before Scriptorium or another consumer
|
Finally, resolve the version as an ordinary Go module in a temporary module
|
||||||
publishes a release that depends on that version. Released consumer modules
|
outside this repository and without a workspace or replacement:
|
||||||
must not use a local replacement or unpublished Promptkit revision.
|
|
||||||
|
```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
|
## Policy Changes
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user