Delegate Promptkit releases to ReleaseKit

This commit is contained in:
2026-10-08 16:40:34 +00:00
parent 05082c6fdf
commit b1866946d1
8 changed files with 136 additions and 302 deletions

View File

@@ -59,40 +59,43 @@ exist.
## Maintainer Validation
For ordinary work, select validation proportionate to the change. Do not repeat
successful checks for unchanged content merely to create a commit. The full
release-candidate checks have one executable entry point from the repository root:
successful checks for unchanged content merely to create a commit. During
development, run relevant Go tests and vet, format changed Go files, and execute
the maintained examples when they are affected:
```sh
./scripts/release-check.sh
go test ./...
go vet ./...
gofmt -w path/to/changed.go
python3 scripts/check-examples.py
git diff --check
```
The [script](../scripts/release-check.sh) owns the executable checks:
race-enabled tests, vet, the three offline consumer examples with semantic output
assertions, tracked Go formatting, local Markdown links, module/workspace
hygiene, whitespace, and the credential-pattern scan. It does not publish,
change files, validate release-note structure, or retire roadmaps. It requires Go,
Git, a POSIX shell, and Python 3. Tests and examples require no provider
credentials or live inference calls.
Release candidates use `./scripts/release.sh VERSION`, which runs the selected
profile checks and publishes the tag. The [release procedure](release.md)
describes its requirements and recovery. For an optional diagnostic of a
committed candidate without publication, run `./scripts/releasekit.sh check
VERSION`; this is not a required pre-publication step.
Run the consumer example checks independently with
`python3 scripts/check-examples.py`.
The standalone `python3 scripts/check-examples.py` command checks semantic
expectations for the three offline consumer examples. The focused
`python3 scripts/test-release.py` command exercises the release tooling against
disposable local repositories; use it when changing that tooling, not as a
recurring release gate. Tests and examples require no provider credentials or
live inference calls.
The script can run on intended uncommitted changes during development.
Formatting and documentation checks use tracked files; stage intended new
files before final validation so those checks include them. The release
procedure separately enforces a clean, committed candidate.
Ordinary validation can run on intended uncommitted work. Release validation
uses committed candidate contents and the selected local Go installation.
### Change Review
Inspect `git status --short --untracked-files=all` and the complete diff before
accepting a change. Reject unintended files, credentials, private data,
generated binaries, test output, downloaded assets, and template residue.
The automated credential scan does not replace review of the actual change.
For releases, follow the [release procedure](release.md). Its publication
command runs the full local checks once on the final committed candidate and
automates release-note structure and Git checks. Do not repeat ordinary change
review as a release sign-off. Roadmap retirement remains preparation housekeeping.
For releases, follow the [release procedure](release.md). Do not repeat its
candidate checks as a separate sign-off. Roadmap retirement remains preparation
housekeeping.
Follow the [agent commit policy](policy/agents.md#git-commit-policy) for local
commits. Ordinary commits may leave unrelated work in the workspace; preserve

View File

@@ -222,20 +222,26 @@ active throughout those operations. Commit the module update before validating
the final candidate, and describe a raised minimum Go requirement and its
compatibility impact in the release note under the versioning policy above.
The existing publication script requires Summary, Compatibility, Upgrade, and
Changes headings beneath a title of `# Promptkit vMAJOR.MINOR.PATCH`. State
“No action required” or “None” where appropriate. This retained local structure
matches the automated release-note guard; changing it is separate work.
Use a title containing `Promptkit` and the exact version, followed by a short
summary and any relevant compatibility impact or upgrade instructions. The
note is committed with the candidate and supplies the annotated tag message.
No fixed set of section headings is required.
The [publication script](../../scripts/release.sh) runs the
[local checks](../../scripts/release-check.sh) once for the final committed
candidate: race-enabled tests, vet, three offline consumer examples, tracked
Go formatting, Markdown links, module/workspace hygiene, whitespace, and a
credential-pattern scan. These protect concurrency, assembled library use,
source distribution, and documentation integrity. A separate ordinary test run
or standalone build is not an additional release gate. Candidate, tag, and
synchronized-main checks remain automated. No CI or module-availability wait
is required. Licensing footer URLs are preparation work, not a network gate.
The [publication command](../../scripts/release.sh) delegates to the released
ReleaseKit tool selected in `go.mod`. The committed
[profile](../../releasekit.json) selects the race-enabled Go suite, full vet,
committed Go formatting, local Markdown links, workspace and filesystem
replacement inspection, and the offline semantic checks for the three
maintained consumer examples. These are the selected checks for the final
candidate; a separate ordinary test run or standalone build is not an
additional release gate. The upstream tool owns candidate, remote, and tag
publication checks. It returns native status 0 for confirmed publication, 1
for a known validation or publication failure, 2 for invalid input or profile,
and 3 when the push outcome is unknown. For status 3, resolve the exact
captured publication endpoint before retrying; the repository-specific
[procedure](../release.md#failure-recovery) links to the upstream recovery
instructions. Do not wait for CI or Go module availability. Licensing footer
URLs are preparation work, not a network gate.
## Catalog Profile Compatibility

View File

@@ -58,17 +58,24 @@ and repository hygiene.
Introducing hosted CI later would supplement, not silently redefine, this
documented validation model.
Full release-candidate validation includes race-enabled package tests, explicit
static analysis, and execution with automated semantic checks of the three maintained
offline consumer examples. The test run builds the packages; the examples
also exercise ordinary compilation. Separate ordinary tests and a standalone
build are not additional release gates. Ordinary tests remain useful during
development. The preparation example protects assembled preparation and
The selected ReleaseKit profile runs the race-enabled package suite and full
vet, checks committed Go formatting, local Markdown links, and module
distribution hygiene, and executes semantic assertions for the three
maintained offline consumer examples. ReleaseKit owns this shared validation
machinery and publication workflow. The test run builds the packages; the
examples also exercise ordinary compilation. Separate ordinary tests and a
standalone build are not additional release gates. Ordinary tests remain useful
during development. The preparation example protects assembled preparation and
inspection behavior. The execution example separately protects assembled
`Run`, injected-client, validation, usage, and result behavior. The observation
example protects assembled built-in retries, structured proposal and assessment
runs, and application-owned interpretation of metadata.
The standalone `python3 scripts/test-release.py` command exercises Promptkit's
ReleaseKit wiring against disposable local repositories. It is focused
development validation when release tooling changes, not part of the default
Go test suite or a recurring release check.
Tests in the default suite must be deterministic, offline, and independent of
real credentials. They must not invoke paid APIs, use live network
dependencies, or depend on mutable external services. Tests that require live

View File

@@ -1,18 +1,19 @@
# Release Procedure
This runbook implements the [release policy](policy/release.md). A successful
push of the selected annotated tag to origin completes the release. Never wait
for CI to finish or succeed.
This runbook implements the [release policy](policy/release.md). Promptkit
publishes source-only Go module tags from `main` to `origin`. A successful push
of the selected annotated tag, after its source commit is available on `main`,
completes the release. Do not wait for CI or Go module availability.
## Prepare The Candidate
Required tools are Git, Go with race-detector support, POSIX shell, and Python
3. Publication requires push access to origin. Work from the repository root.
3. Work from the repository root with push access to `origin`.
Use the latest Go toolchain available in the development environment for
release preparation and validation. Make that installation the active `go`
command, then advance PromptKit's minimum Go requirement to that series' first
stable release before committing the candidate:
Use the latest Go installation available locally for the release tool, the
candidate validation, and the maintained examples. Make it the active `go`
command and record its version. Set Promptkit's consumer minimum to the initial
stable release of that Go series before committing the candidate:
```sh
export GOTOOLCHAIN=local
@@ -23,42 +24,36 @@ release_go_series=${release_go_series%.*}
go mod edit "-go=${release_go_series}.0"
```
For example, building with Go 1.26.8 normally records `go 1.26.0`, not
`go 1.26.8`. If a dependency or a specific correctness fix requires a higher
patch minimum, use that minimum instead of reducing it to `.0`.
For example, Go 1.26.8 normally results in `go 1.26.0`. Keep a higher patch
minimum if a dependency or correctness fix requires it. Prepare module caches
before validation; this may update `go.sum` and belongs before the candidate
commit:
Include the module change in the candidate commit. Describe a raised minimum
Go requirement in the release note and select the release version under the
[compatibility policy](policy/release.md#versioning-and-compatibility).
Keep this toolchain active through validation and publication. Toolchain
selection and the module update are preparation, not mutations of an already
validated candidate.
Select an unused version and create `docs/releases/<version>.md` using the
[required structure](policy/release.md#release-documentation). Describe
compatibility impact and consumer actions and update affected canonical docs.
For catalog updates, record selected catalog versions and any consumer action,
linking to the catalog policy rather than repeating its contracts.
Follow the [OSS policy](policy/oss.md) when preparing licensing and attribution
changes. Each new note must end with the following navigation, replacing
`vMAJOR.MINOR.PATCH` with the selected release tag:
```markdown
[COPYRIGHT](https://gitea.maximumdirect.net/eric/promptkit/src/tag/vMAJOR.MINOR.PATCH/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/promptkit/src/tag/vMAJOR.MINOR.PATCH/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/promptkit/src/tag/vMAJOR.MINOR.PATCH/docs/oss/attribution.md)
```sh
GOTOOLCHAIN=local go mod download
```
Check these file paths against the candidate; a future tag need not resolve
remotely during preparation. The annotated tag derives from this same note,
including its footer. Promptkit publishes no separate hosted release body or
download page. If such a channel is deliberately introduced, reuse the note
and preserve its footer. No extra network checks, artifacts, or approval gates
are introduced, and published notes are not retrofitted.
Choose an unused stable version and create `docs/releases/<version>.md` with a
title containing `Promptkit` and that exact version, a short summary, and any
relevant compatibility impact or upgrade instructions. No fixed section
headings are required. Describe a raised Go minimum and its compatibility
impact under the [versioning policy](policy/release.md#versioning-and-compatibility).
End each new note with the licensing footer below, using the selected version:
Retire completed roadmaps, preserving planned or deferred work and repairing
links. This is housekeeping, not a separate approval gate. Review changes
through the ordinary development workflow, commit them, and push main to
origin. No repeated release-specific diff review is required.
```markdown
[COPYRIGHT](https://gitea.maximumdirect.net/eric/promptkit/src/tag/vX.Y.Z/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/promptkit/src/tag/vX.Y.Z/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/promptkit/src/tag/vX.Y.Z/docs/oss/attribution.md)
```
Check the footer paths against the candidate; the future tag need not resolve
remotely during preparation. Follow the [OSS policy](policy/oss.md) for these
links. Promptkit publishes no binaries, hosted download page, or other
asynchronous deliverables.
Retire completed roadmaps while preserving deferred work and repairing incoming
links. Review the intended changes through the ordinary development workflow,
commit the candidate on `main`, and push `main` to `origin` before publication.
Preparation may update module metadata; the release bootstrap and validation
preserve the committed candidate.
## Validate And Publish
@@ -68,34 +63,29 @@ For an optional local diagnostic of a committed candidate, run:
./scripts/releasekit.sh check vMAJOR.MINOR.PATCH
```
The candidate must be committed on `main`, use a stable version, and include
its version-matched release note. See the upstream
[candidate check contract](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.2.0/docs/consumers/check.md)
for its requirements. This diagnostic is optional and is not required before
publication.
This is not a required step before publication. The upstream
[candidate-check contract](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.2.0/docs/consumers/check.md)
describes its requirements.
Run the following with the chosen unused version (replace the placeholder):
Run the single publication command with the chosen version:
```sh
./scripts/release.sh vMAJOR.MINOR.PATCH
```
The [publication script](../scripts/release.sh) checks a clean, synchronized
main candidate, the version and unused tag, and the release note's title and
required headings. It runs the [local checks](../scripts/release-check.sh)
once, rechecks candidate identity and publication state, generates the
annotation from the committed note plus the validated commit, and pushes only
the selected annotated tag.
The familiar wrapper delegates to the native ReleaseKit executable selected in
`go.mod`. The committed [profile](../releasekit.json) selects the race-enabled
Go suite, full vet, committed Go formatting, local Markdown links, workspace
and filesystem replacement inspection, and semantic checks for the three
maintained offline consumer examples. ReleaseKit also verifies the committed
candidate and publication state, then publishes only the selected tag. Its
checks run once for this candidate; do not repeat them as a separate sign-off.
There is no separate tag-message authoring, example-output inspection,
metadata inspection, or human sign-off. Compatibility and release-note content
remain ordinary editorial responsibilities. Development validation is useful
but does not require a second full run before invoking the publication script.
The script stops successfully after the tag push. CI completion, CI success,
remote read-back, and Go module availability are not release completion gates.
Do not wait or poll for them. Provider probes and inference calls are never
part of release validation.
The native command returns status 0 for confirmed publication, 1 for a known
validation or publication failure, 2 for invalid input or profile, and 3 when
the push outcome remains unknown. Successful publication needs no remote
read-back, CI wait, or installation check. Provider probes and inference calls
are not part of validation.
## Catalog Profile Compatibility
@@ -104,28 +94,32 @@ catalog compatibility boundary. This heading preserves older incoming links.
## Failure Recovery
If local checks fail, fix the candidate, commit and push changes, then rerun
the publication command. It never automatically removes a tag.
For status 1, use the diagnostic to distinguish a validation failure from a
known publication conflict. Correct and commit candidate defects, push `main`,
and rerun publication with an unused version when appropriate. Status 2 means
the invocation or profile must be corrected.
If a push fails or its result is ambiguous, set `RELEASE_VERSION` to the
attempted version and inspect origin before retrying:
For status 3, do not retry or remove the local tag until the remote outcome is
resolved. ReleaseKit reports the attempted version, tag object, and recovery
state, including the captured publication endpoint when available. Use that
endpoint—not merely the current `origin` URL—to query the exact tag; follow the
upstream
[failure-recovery procedure](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.2.0/docs/release.md#resolve-failures):
```sh
git ls-remote --tags origin "refs/tags/$RELEASE_VERSION" "refs/tags/$RELEASE_VERSION^{}"
git rev-parse "refs/tags/$RELEASE_VERSION" "refs/tags/$RELEASE_VERSION^{}"
releasekit_endpoint='COPY_CAPTURED_ENDPOINT'
releasekit_version='vX.Y.Z'
test "$(git ls-remote --get-url -- "$releasekit_endpoint")" = "$releasekit_endpoint" &&
git ls-remote --refs -- "$releasekit_endpoint" "refs/tags/$releasekit_version"
```
Matching remote tag and peeled commit values confirm publication, even if the
push command reported an error. If the remote query fails, publication status
is still unknown. Do not delete or recreate the local tag until absence on
origin is confirmed. Once confirmed unpublished, an unsuccessful attempt's
local tag can be removed and the publication command rerun. Never overwrite
a published tag.
CI or module-resolution problems may be investigated separately; temporary
service, network, or authentication failures do not require another release.
Only an actual defect requiring changes to published contents warrants a new
appropriate version. See the [recovery policy](policy/release.md#immutability-and-failure-recovery).
Compare the returned object ID with the reported tag object. An exact match
confirms publication. A missing ref confirms absence only when the endpoint
identity check and query both succeed. If either check fails, the outcome
remains unknown; correct the transport configuration and resolve it before
retrying. Only after authoritative absence is confirmed may an unpublished
local tag be removed before correcting the candidate or retrying. Never move,
overwrite, or delete a published tag.
## Policy Changes

View File

@@ -9,7 +9,8 @@ Status: Stage 1's tool selection and dependency preparation are complete in
requirement. Stage 2's example checker and shared validation profile are
complete in `0d163f8`. Stage 3's native tool bootstrap is complete in
`d594085`. Stage 4's successful-publication fixture is complete in `51b0729`.
Stage 5's failure-propagation fixture is complete. Resume adoption at Stage 6
Stage 5's failure-propagation fixture is complete in `05082c6`. Stage 6's
publication cutover and documentation are complete. Resume adoption at Stage 7
after this revision.
## Baseline And Working Rules

View File

@@ -1,126 +0,0 @@
#!/bin/sh
# Run local checks without publishing or changing files.
set -eu
cd "$(dirname "$0")/.."
go test -race -vet=off ./...
go vet ./...
python3 ./scripts/check-examples.py
unformatted=$(
git ls-files '*.go' |
while IFS= read -r go_file
do
gofmt -l "$go_file"
done
)
test -z "$unformatted"
python3 - <<'PY'
from pathlib import Path
import re
import subprocess
import sys
from urllib.parse import unquote
root = Path.cwd().resolve()
markdown_files = [
root / name
for name in subprocess.check_output(
["git", "ls-files", "*.md"], text=True
).splitlines()
]
link_pattern = re.compile(r"!?\[[^]]*\]\(([^)]+)\)")
heading_pattern = re.compile(r"^#{1,6}\s+(.+?)\s*#*\s*$")
scheme_pattern = re.compile(r"^[a-z][a-z0-9+.-]*:", re.IGNORECASE)
def markdown_lines(path):
in_fence = False
fence = ""
for line in path.read_text(encoding="utf-8").splitlines():
stripped = line.lstrip()
marker = stripped[:3]
if marker in {"```", "~~~"}:
if not in_fence:
in_fence = True
fence = marker
elif marker == fence:
in_fence = False
fence = ""
continue
if not in_fence:
yield line
anchor_cache = {}
def anchors(path):
if path in anchor_cache:
return anchor_cache[path]
found = set()
counts = {}
for line in markdown_lines(path):
match = heading_pattern.match(line)
if not match:
continue
heading = re.sub(r"<[^>]+>", "", match.group(1)).replace("`", "")
base = re.sub(r"[^\w\- ]", "", heading.lower()).replace(" ", "-")
count = counts.get(base, 0)
counts[base] = count + 1
found.add(base if count == 0 else f"{base}-{count}")
anchor_cache[path] = found
return found
failures = []
for source in markdown_files:
text = "\n".join(markdown_lines(source))
for match in link_pattern.finditer(text):
target = match.group(1).strip()
if target.startswith("<") and target.endswith(">"):
target = target[1:-1]
if scheme_pattern.match(target) or target.startswith("//"):
continue
path_text, separator, fragment = target.partition("#")
destination = source if not path_text else source.parent / unquote(path_text)
try:
destination = destination.resolve()
destination.relative_to(root)
except ValueError:
failures.append(f"{source.relative_to(root)}: escapes repository: {target}")
continue
if not destination.exists():
failures.append(f"{source.relative_to(root)}: missing target: {target}")
continue
if separator and destination.suffix.lower() == ".md":
fragment = unquote(fragment).lower()
if fragment not in anchors(destination):
failures.append(f"{source.relative_to(root)}: missing anchor: {target}")
if failures:
print("\n".join(failures), file=sys.stderr)
raise SystemExit(1)
PY
case "$(go env GOWORK)" in
''|off) ;;
*) printf '%s\n' 'an active Go workspace is not allowed' >&2; exit 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
exit 1
fi
git diff --check
git diff --cached --check
credential_pattern='-----BEGIN ([A-Z0-9]+ )?PRIV''ATE KEY-----|AKI''A[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{36,}|sk-[A-Za-z0-9]{32,}'
if git grep -nEI -e "$credential_pattern" -- .
then
printf '%s\n' 'possible credential found' >&2
exit 1
fi

View File

@@ -1,57 +1,5 @@
#!/bin/sh
# Validate one committed candidate and publish only its selected annotated tag.
set -eu
cd "$(dirname "$0")/.."
release_version=${1:?usage: scripts/release.sh vMAJOR.MINOR.PATCH}
printf '%s\n' "$release_version" |
grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'
release_commit=$(git rev-parse --verify 'HEAD^{commit}')
check_candidate() {
test "$(git branch --show-current)" = main
test -z "$(git status --porcelain)"
test "$(git rev-parse HEAD)" = "$release_commit"
git fetch origin main
test "$(git rev-parse refs/remotes/origin/main)" = "$release_commit"
if git show-ref --verify --quiet "refs/tags/$release_version"; then
printf '%s\n' "local tag already exists: $release_version" >&2
exit 1
fi
remote_refs=$(git ls-remote --tags origin "refs/tags/$release_version" "refs/tags/$release_version^{}")
test -z "$remote_refs"
}
check_candidate
python3 - "$release_version" <<'PY'
from pathlib import Path
import sys
version = sys.argv[1]
note = Path("docs/releases") / (version + ".md")
lines = note.read_text(encoding="utf-8").splitlines()
assert lines and lines[0] == "# Promptkit " + version, f"wrong release title: {note}"
headings = set()
fence = None
for line in lines:
marker = line.lstrip()[:3]
if marker in {"```", "~~~"}:
if fence is None:
fence = marker
elif fence == marker:
fence = None
elif fence is None:
headings.add(line)
for section in ("Summary", "Compatibility", "Upgrade", "Changes"):
assert "## " + section in headings, f"missing {section}: {note}"
PY
./scripts/release-check.sh
check_candidate
release_note=$(git show "$release_commit:docs/releases/$release_version.md")
printf '%s\n\nValidated commit: %s\n' "$release_note" "$release_commit" |
git tag --annotate --cleanup=verbatim "$release_version" --file - "$release_commit"
test "$(git cat-file -t "refs/tags/$release_version")" = tag
test "$(git rev-parse "refs/tags/$release_version^{commit}")" = "$release_commit"
git push origin "refs/tags/$release_version:refs/tags/$release_version"
printf '%s\n' "Released $release_version. No CI or module-availability wait is required."
exec ./scripts/releasekit.sh publish "$@"

View File

@@ -179,6 +179,7 @@ def prepare_repository(repository, remote, environment, module_bytes, version):
scripts = repository / "scripts"
scripts.mkdir(parents=True, exist_ok=True)
shutil.copy2(REPOSITORY_ROOT / "scripts/release.sh", scripts / "release.sh")
shutil.copy2(REPOSITORY_ROOT / "scripts/releasekit.sh", scripts / "releasekit.sh")
write_file(
scripts / "check-examples.py",
@@ -215,9 +216,9 @@ def prepare_repository(repository, remote, environment, module_bytes, version):
def verify_publication(repository, remote, temporary_root, environment, module_bytes, selected_go):
bootstrap = repository / "scripts/releasekit.sh"
publication = repository / "scripts/release.sh"
run(
[str(bootstrap), "publish", RELEASE_VERSION],
[str(publication), RELEASE_VERSION],
cwd=temporary_root,
environment=environment,
)
@@ -324,7 +325,7 @@ def verify_validation_failure(
)
result = execute(
[str(repository / "scripts/releasekit.sh"), "publish", FAILED_RELEASE_VERSION],
[str(repository / "scripts/release.sh"), FAILED_RELEASE_VERSION],
cwd=temporary_root,
environment=scenario_environment,
)