From 6dfd6bd71979f3f95412fb50f7c9e03c8797a2f1 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 23 Sep 2026 16:06:33 +0000 Subject: [PATCH] Align project policies and simplify the release workflow --- AGENTS.md | 34 +--- docs/consumers/pkg-promptkit.md | 2 +- docs/development.md | 209 ++++----------------- docs/formats.md | 4 +- docs/policy/agents.md | 88 +++++++++ docs/policy/architecture.md | 43 +---- docs/policy/documentation.md | 45 +++-- docs/policy/release.md | 146 +++++++++++++++ docs/policy/testing.md | 23 ++- docs/release.md | 320 +++++++------------------------- scripts/release-check.sh | 145 +++++++++++++++ scripts/release.sh | 57 ++++++ 12 files changed, 584 insertions(+), 532 deletions(-) create mode 100644 docs/policy/agents.md create mode 100644 docs/policy/release.md create mode 100755 scripts/release-check.sh create mode 100755 scripts/release.sh diff --git a/AGENTS.md b/AGENTS.md index 68a03c4..b9989e2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,35 +1,5 @@ Please review `docs/development.md` for initial orientation in this repository and follow its task-specific reading guide. -## Implementation Discipline - -Simplify the mechanism, not the user's choices. - -**Keep implementation machinery proportionate.** Use straightforward, -idiomatic code and introduce only the machinery needed to support the requested -behavior and flexibility. Optimize for conceptual simplicity and -maintainability, not for the fewest lines, fewest options, or narrowest product -surface. - -**Do not invent restrictions.** Do not reject, prohibit, constrain, cap, -harden, or otherwise narrow behavior unless required by the user, an explicit -repository contract, or an unavoidable correctness constraint. Preserve -operator and caller choice where policy has not been established. If an -additional restriction appears advisable, present it as a decision rather than -silently implementing it. - -A correctness constraint must follow from the requested behavior or an -established invariant, not from the convenience of the chosen implementation. -Explain the specific failure it prevents and why a less restrictive -implementation cannot satisfy the contract. - -**An implementation detail is not repository policy.** Tests and documentation -must not elevate incidental behavior into a durable guarantee or restriction -unless that behavior was expressly selected as part of the contract. Existing -explicit contracts remain authoritative; changing or weakening one requires a -deliberate decision rather than being inferred from this principle. - -Agent-authored plans, tests, documentation, and prior implementations are not -evidence that an additional restriction was deliberately approved. Identify the -authorizing requirement or decision; when that authority is unclear, surface -the ambiguity rather than treating repetition as approval. +Review and follow `docs/policy/agents.md` before recommending, planning, or +implementing any changes to this repository. diff --git a/docs/consumers/pkg-promptkit.md b/docs/consumers/pkg-promptkit.md index 07754f9..e723395 100644 --- a/docs/consumers/pkg-promptkit.md +++ b/docs/consumers/pkg-promptkit.md @@ -252,7 +252,7 @@ a catalog dependency can remove an inherited base and produce an untested combination. Pinning the resolved module graph preserves the embedded data for a build, but cannot preserve provider availability. Running binaries neither download catalog updates nor automatically replace withdrawn models. See the -[release policy](../release.md#catalog-profile-compatibility) for the exact +[release policy](../policy/release.md#catalog-profile-compatibility) for the exact cross-version guarantees. ### Alias A Built-In Profile diff --git a/docs/development.md b/docs/development.md index ff60124..87dd49d 100644 --- a/docs/development.md +++ b/docs/development.md @@ -2,7 +2,8 @@ This is the contributor entry point for Promptkit, a reusable Go library. All contributors must read the -[architecture policy](policy/architecture.md) before making changes. +[architecture policy](policy/architecture.md) before making changes. Agents must +also follow the [agent policy](policy/agents.md) before recommendations or work. ## Initial Orientation @@ -25,7 +26,8 @@ Start with: documentation; - the [testing policy](policy/testing.md) before adding, rewriting, or deleting tests; and -- the [release procedure](release.md) for version and publication work. +- the [release policy](policy/release.md) and [release procedure](release.md) + for version and publication work. ## Task-Specific Reading Guide @@ -40,7 +42,7 @@ Start with: | Tests or test fixtures | The [testing policy](policy/testing.md), owning package, and focused internal document listed by the component overview. | | Maintained example | The [example](../examples/go-library/prepare/main.go), [consumer guide](consumers/pkg-promptkit.md), [framework format reference](formats.md), and [documentation policy](policy/documentation.md). | | Documentation | The [documentation policy](policy/documentation.md) and canonical owner of every affected contract. | -| Release preparation or publication | The [release procedure](release.md). | +| Release preparation or publication | The [release policy](policy/release.md) and [release procedure](release.md). | For cross-cutting changes, follow every applicable row. Do not create placeholder documents for packages, APIs, or integrations that do not yet @@ -48,187 +50,40 @@ exist. ## Maintainer Validation -This section is the canonical local validation workflow for Promptkit. Run -every command from the repository root before accepting a change. The test -suite and maintained examples are deterministic, offline, and require no real -provider credentials. - -### Tests, Analysis, Build, And Examples - -Run the ordinary and race-enabled suites, static analysis, the build, and both -maintained consumer examples: +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: ```sh -go test ./... -go test -race ./... -go vet ./... -go build ./... -go run ./examples/go-library/prepare -go run ./examples/go-library/run +./scripts/release-check.sh ``` -Both examples must exit successfully. Review their JSON output: preparation -must report the selected offline prompt, profile, model, and message count; -execution must report the deterministic generated output, successful -validation, selected offline model, and usage. Neither command may contact a -provider or require credentials. +The [script](../scripts/release-check.sh) owns the executable checks: +race-enabled tests, vet, both 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. -### Go Formatting +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. -Check every tracked Go file. The final command must succeed and the captured -list must be empty: - -```sh -unformatted=$( - git ls-files '*.go' | - while IFS= read -r go_file - do - gofmt -l "$go_file" - done -) -test -z "$unformatted" -``` - -### Local Markdown Links - -Use the Python standard library to verify every repository-relative Markdown -target and local heading fragment. The check is offline and prints nothing on -success: - -```sh -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 -``` - -### Repository Hygiene And Review - -Reject an active Go workspace, tracked workspace files, a vendor tree, or a -module replacement: - -```sh -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 -``` - -Check whitespace in both unstaged and staged changes. List ignored files and -scan tracked content for common credential forms: - -```sh -git diff --check -git diff --cached --check -test -z "$(git ls-files --others --ignored --exclude-standard)" -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 -``` +### Change Review Inspect `git status --short --untracked-files=all` and the complete diff before -accepting a change. The status may contain only the intended source changes -during development. Reject credentials, private keys, environment files, -generated binaries, test or coverage output, downloaded assets, template -residue, and any other artifact that does not belong in source control. The -credential scan catches common forms but does not replace inspection of the -actual change. +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. -After committing the accepted change, require a clean candidate: +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. -```sh -test -z "$(git status --porcelain)" -``` +Follow the [agent commit policy](policy/agents.md#git-commit-policy) for local +commits. Ordinary commits may leave unrelated work in the workspace; preserve +and report it. A clean worktree and index are required for release candidates, +as enforced by the publication command, not for every ordinary commit. diff --git a/docs/formats.md b/docs/formats.md index 6fc07d9..a63daff 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -338,7 +338,7 @@ selections, not automatic defaults: Select either name through an existing request `profile_id` or prompt `default_profile`. Consumers can replace either definition or its inherited base through the ordinary profile-source precedence above. The -[release policy](release.md#catalog-profile-compatibility) defines the +[release policy](policy/release.md#catalog-profile-compatibility) defines the cross-version guarantee for these names. ## External Profile Inventories @@ -350,7 +350,7 @@ duplicated here. Browse the exact data selected by this Promptkit version: - [Rakestrawhome catalog v2.20260922.0 profiles](https://gitea.maximumdirect.net/eric/promptkit-backend-rakestrawhome/src/tag/v2.20260922.0/catalog/profiles) External profile entries are volatile across catalog releases. The -[release policy](release.md#catalog-profile-compatibility) owns the precise +[release policy](policy/release.md#catalog-profile-compatibility) owns the precise compatibility boundary; a selected module version identifies tested local data, not live provider availability. diff --git a/docs/policy/agents.md b/docs/policy/agents.md new file mode 100644 index 0000000..19fb6b6 --- /dev/null +++ b/docs/policy/agents.md @@ -0,0 +1,88 @@ +# Agent Policy + +**Base Policy Version:** 1.1 + +**Last Updated:** 2026-09-23 + +## Purpose + +Agents must follow these house rules before recommending, planning, or +implementing repository changes. + +## Implementation Discipline + +Write the simplest idiomatic code that satisfies the user's request. +Simplify the mechanism, not the user's choices. Do not implicitly strengthen +requirements, restrictions, or guarantees. + +**Keep implementation machinery proportionate.** Use straightforward, +idiomatic code and introduce only the machinery needed to support the requested +behavior and flexibility. Optimize for conceptual simplicity and +maintainability, not for the fewest lines, fewest options, or narrowest product +surface. + +**Do not invent restrictions.** Do not reject, prohibit, constrain, cap, +harden, or otherwise narrow behavior unless required by the user, an explicit +repository contract, or an unavoidable correctness constraint. Preserve +operator and caller choice where policy has not been established. If an +additional restriction appears advisable, present it as a decision rather than +silently implementing it. + +A correctness constraint must follow from the requested behavior or an +established invariant, not from the convenience of the chosen implementation. +Explain the specific failure it prevents and why a less restrictive +implementation cannot satisfy the contract. + +**An implementation detail is not repository policy.** Tests and documentation +must not elevate incidental behavior into a durable guarantee or restriction +unless that behavior was expressly selected as part of the contract. Existing +explicit contracts remain authoritative; changing or weakening one requires a +deliberate decision rather than being inferred from this principle. + +Agent-authored plans, tests, documentation, and prior implementations are not +evidence that an additional restriction was deliberately approved. Identify the +authorizing requirement or decision; when that authority is unclear, surface +the ambiguity rather than treating repetition as approval. + +**Do not invent guarantees.** Do not strengthen public contracts, guarantees, +or invariants beyond those expressly requested by the user. + +**Use the standard library.** Prefer simple, idiomatic standard-library +implementations when they satisfy the requested behavior. Do not add custom +power-loss protection or defenses against privileged concurrent filesystem +actors unless expressly required. Preserve existing repository contracts. + +## Git Commit Policy + +By default, create a commit after completing each coherent unit of work, using a +concise, plain-English message describing what changed. This policy is standing +authorization to create those local commits; a separate user request is not +required. An explicit user instruction to leave work uncommitted takes precedence. + +A unit of work is a focused, reviewable outcome, including its relevant tests and +documentation. Do not commit after every intermediate edit or create empty commits. +Complete the appropriate validation before committing. If work is incomplete or +validation is blocked, report that state rather than presenting it as completed. + +Before committing: + +1. Run `git status --short` and review the relevant diff, including any staged + changes. Identify changes that predate the current unit of work. +2. Exclude secrets, local environment files, generated junk, and unrelated changes. +3. Stage only the files or hunks belonging to the completed unit. Use `git add .` + only when every included change belongs to that unit. Do not disturb unrelated + staged or unstaged work. +4. Review `git diff --staged` and run appropriate checks for the change. Do not + repeat checks already passed for unchanged content merely to create a commit. +5. Review `git log --oneline -n 10` and use + `git commit -m ""`. +6. Report the commit identifier and any remaining uncommitted work. + +If the unit cannot be safely separated from pre-existing work, leave it uncommitted +and explain the overlap rather than silently including that work. If a commit +fails, preserve the changes and report the failure; do not bypass hooks. + +Default commit authorization does not authorize pushing, publishing, amending, +resetting, reverting, or rewriting history. Those actions require a separate user +instruction. Do not use `git commit -a` or `git commit --no-verify` to bypass the +staging and validation discipline above. diff --git a/docs/policy/architecture.md b/docs/policy/architecture.md index 72618fb..d9ce762 100644 --- a/docs/policy/architecture.md +++ b/docs/policy/architecture.md @@ -1,5 +1,9 @@ # Architecture Policy +**Base Policy Version:** 1.0 + +**Last Updated:** 2026-09-23 + ## Purpose This document defines Promptkit's current high-level architecture and the @@ -17,42 +21,9 @@ The module root contains package `promptkit`, which is the public facade. It provides the supported engine, configuration and source options, requests, results, public values, extension interfaces, profiles, and error sentinels. -The implemented internal components consist of: - -- `internal/domain`, which owns framework data values and source-neutral - invariants shared by later internal components; -- `internal/backend`, which owns validated immutable OpenAI-compatible backend - definitions; -- `internal/capacity`, which owns engine-local bounded run admission and - model-generation scheduling for limited backends; -- `internal/defaults`, which owns application-neutral framework defaults and - constructs the default execution target; -- `internal/filecatalog`, which discovers YAML files and provides source-path - helpers for filesystem and `fs.FS` consumers; -- `internal/catalog`, which validates immutable external maintained-catalog - assets and their reserved profile namespace before they are eligible for - engine assembly; -- `internal/jsonvalue`, which validates and defensively copies JSON-compatible - extra-parameter trees; -- `internal/promptdef`, which loads and validates prompt definitions from - filesystem and `fs.FS` sources; -- `internal/profile`, which loads, validates, and overlays execution profiles - from filesystem and `fs.FS` sources; -- `internal/prompt`, which renders prompt messages from Go templates; -- `internal/artifact`, which resolves ordinary inline and unrestricted - caller-selected file references; -- `internal/validate`, which validates basic, JSON, and JSON Schema output - using filesystem and `fs.FS` schema sources; -- `internal/llm`, which defines the provider-neutral generation boundary and - implements outbound OpenAI-compatible chat requests; and -- `internal/usecase`, which coordinates preparation and execution across the - internal framework components. -- `internal/engine`, which assembles those components into one engine and - delegates execution to the use-case runner. - -The `examples/go-library/prepare` and `examples/go-library/run` packages are -maintained downstream consumers of the root facade. They do not expose library -packages or participate in internal assembly. +The [internal component overview](../internal/overview.md) owns the inventory +of implemented packages and their concrete responsibilities. This policy owns +the boundaries and dependency rules those components must preserve. The internal engine assembles one immutable backend registry, one capacity manager, the internal repositories, renderer, validator, outbound client, and use-case diff --git a/docs/policy/documentation.md b/docs/policy/documentation.md index ae8fb40..bd79570 100644 --- a/docs/policy/documentation.md +++ b/docs/policy/documentation.md @@ -1,5 +1,9 @@ # Documentation Policy +**Base Policy Version:** 1.1 + +**Last Updated:** 2026-09-23 + ## Purpose This policy assigns each Promptkit documentation topic to one canonical owner. @@ -68,17 +72,19 @@ mechanisms, not secret values. | Topic | Canonical owner | Owned content | Content owned elsewhere | | --- | --- | --- | --- | -| Project orientation | `README.md` | What Promptkit is, its current usability, module identity, license summary, and links onward. | Exact API contracts, contributor procedures, architecture detail, and release steps. | +| Project orientation | `README.md` | What Promptkit is, its current usability, module identity, license summary, a minimal successful quickstart, and links onward. | Exact API contracts, contributor procedures, architecture detail, and release steps. | | Contributor workflow | `docs/development.md` | Task-oriented reading guide, local workflow, validation commands, and repository hygiene. | Architecture rules, API semantics, subsystem behavior, and release procedure. | | Current architecture | `docs/policy/architecture.md` | System shape, normative ownership, dependency direction, package boundaries, invariants, and non-goals. | Concrete component inventory, implementation mechanics, contributor procedures, decision history, and future work. | +| Agent policy | `docs/policy/agents.md` | Agent implementation discipline, proportionality, idiomatic simplicity, local commit authorization and discipline, and rules against inventing restrictions, guarantees, or repository policy. | Public contracts, architecture, testing, and contributor procedures. | | Documentation organization | `docs/policy/documentation.md` | Documentation ownership, audience boundaries, maintenance rules, and document lifecycle. | Library architecture or runtime behavior. | | Testing policy | `docs/policy/testing.md` | Test philosophy, risk-based sufficiency, test boundaries, doubles, coverage guidance, regression policy, and test maintenance. | Subsystem behavior, exact public contracts, subsystem-specific test inventories, and implementation plans. | -| Release procedure | `docs/release.md`, when present | Required release validation, version and tag procedure, release ordering, and post-publication checks. | General contributor workflow, public API semantics, and decision history. | +| Release policy | `docs/policy/release.md` | Versioning, compatibility, release-note requirements, validation obligations, immutability, recovery, and adoption scope. | Executable commands, publication mechanics, and historical release summaries. | +| Release procedure | `docs/release.md` | Maintainer prerequisites, candidate preparation, concrete publication guards and commands, verification, and recovery steps. | Release-policy requirements, public API semantics, and historical summaries. | | Exact exported Go API | Go declarations and GoDoc, as APIs are implemented | Exported names, signatures, types, values, errors, and exact behavioral contracts. | Task-oriented consumer walkthroughs, implementation details, and future API proposals. | | Framework file formats | `docs/formats.md` | Prompt-definition and profile YAML fields, schema references, defaults, validation modes, built-in profiles, credentials, and file-to-request precedence. | Exported Go declarations, outbound wire behavior, internal parsing mechanics, and application configuration. | | Consumer guidance | `docs/consumers/`, when consumer workflows require dedicated guidance | Task-oriented use of implemented public APIs, minimal examples, and consumer responsibilities. | Exact exported declarations and internal mechanics. | | Durable integration contracts | `docs/integrations/`, when integrations exist | External formats and protocols, compatibility behavior, and upstream or downstream responsibilities. | Internal transformations and public Go declarations. | -| Supplemental release guidance | None. `docs/releases/` may be used when a release benefits from a changelog or migration guide. | No canonical content. These files may briefly summarize release-specific changes, compatibility, and consumer migration paths, and may be corrected, consolidated, archived, or removed when no longer useful. | Public API and behavior contracts, formats, integrations, architecture, release procedure, and the authoritative annotated-tag release record. | +| Release-note history | `docs/releases/` | Mandatory version-matched historical summaries for releases made under the adopted release policy; the note at the immutable tag records that release. | Current APIs, behavior, formats, compatibility contracts, and release procedures. | | Implemented component inventory | `docs/internal/overview.md` | Current packages and components, their implemented responsibilities, and links to focused internal documents. | Normative architecture, contributor workflow, external contracts, and proposed components. | | Internal subsystem behavior | Other files under `docs/internal/`, when a subsystem needs durable detail | Implementation flow, internal collaborators and state transitions, package-local guarantees and failures, and relevant tests. | Global architecture invariants, public API definitions, and future package plans. | | Architectural decision history | `docs/adr/`, when repository-local decisions require records | Significant decisions, context, alternatives, rationale, consequences, and supersession history. | Current behavior reference, implementation status, and task sequencing. | @@ -110,22 +116,22 @@ but must link to its canonical definition rather than restate it. The [framework format reference](../formats.md) owns exact prompt, profile, and schema-file contracts. Integration documents own external wire formats. -### Supplemental Release Guidance +### Release Documentation -Files under `docs/releases/` may provide changelog-style summaries and -migration guidance for a particular release. They are navigation and -orientation aids, not canonical owners of public APIs, behavior, formats, -integrations, architecture, release procedure, or other durable facts. When a -reader needs detail beyond a short release-specific note, the release document -must link to the applicable canonical documentation rather than reproduce its -contract. +The [release policy](release.md) owns release requirements; the +[repository release procedure](../release.md) owns their concrete execution. +Version-matched checked-in notes are required for future releases under that +policy. The source tag is the release identity, and the note in its immutable +tree is the historical summary; annotated tag messages complement that record. -The annotated tag message required by the -[release procedure](../release.md#write-the-release-note) remains the -authoritative release record. Supplemental release documents may be corrected, -consolidated, archived, or removed at any time when they are no longer useful, -provided maintained documentation does not depend on them and the annotated -tag record remains intact. +Release notes do not own current API or behavior contracts. Link to the +canonical owners for details instead of reproducing those contracts. Published +notes are not disposable guidance and must not be replaced under the same +version. Corrections belong in a subsequent release. + +Adoption is prospective. Previously published tags and historical notes retain +their original records and are not reformatted or rewritten to satisfy the new +requirements. ### Security Topics @@ -178,9 +184,8 @@ durable owners, update incoming links, and archive or remove the roadmap according to repository practice. Do not preserve completed roadmaps as a second current-state reference. -Supplemental release documents may likewise be removed without preserving a -replacement. Before removal, update maintained incoming links so current -documentation does not depend on an optional historical guide. +Retain release notes as historical records under the release policy. Do not +apply temporary-roadmap retirement rules to published release notes. Before completing documentation work: diff --git a/docs/policy/release.md b/docs/policy/release.md new file mode 100644 index 0000000..faaa707 --- /dev/null +++ b/docs/policy/release.md @@ -0,0 +1,146 @@ +# Release Policy + +**Base Policy Version:** 1.0 + +**Last Updated:** 2026-09-23 + +## Purpose And Adoption Scope + +This policy owns Promptkit's release requirements and compatibility rules. +The [release procedure](../release.md) owns the commands used to satisfy them. +This scoped adoption applies to releases prepared after its adoption on +2026-09-23. Published tags and historical notes are not rewritten to retrofit +the new note format. + +## Release Model And Compatibility + +Promptkit publishes source-only Go module releases from committed, pushed +`main` candidates through annotated `vMAJOR.MINOR.PATCH` tags. It publishes +no binaries, hosted releases, or other artifacts and uses maintainer-run +validation rather than hosted CI. Versions must be unused locally and on +`origin`; the existing procedure accepts stable version tags only. + +Before v1, minor releases may change the public API and patches preserve +compatibility within their minor line, subject to the catalog-data provision +below. Release notes must identify compatibility impact, public API changes, +and required consumer action. The source tag identifies the exact release; +the checked-in note at that tag records its historical summary. + +## Catalog Profile Compatibility + +External catalog profile data may be added, removed, renamed, or modified +without deprecation or advance notice. This exception is limited to profile +data imported from the maintained catalog modules. It does not weaken ordinary +compatibility for Promptkit's public Go API, configuration and format semantics, +runtime behavior, catalog-module APIs, manifest schema, backend definitions, or +consumer-owned definitions. + +Within the catalog versions selected and tested by a Promptkit release, and +unless a consumer overrides the name or its base, Promptkit keeps the literal +profile IDs `promptkit-openrouter-standard` and +`promptkit-openrouter-auto` available. The names do not promise a permanent +model, backend, routing behavior, settings, cost, output, or upstream +availability. The [framework format reference](../formats.md#built-in-profile-catalog) +owns their current definitions. + +`v0.10.0` is the initial minor release for this compatibility transition. +Afterward, catalog-only updates and adjustments to the current selections may +ship in a patch release when all other compatibility obligations remain intact. +External profile removals can require consumer action; they are not automatic +migrations. + +Catalog adoption and release validation remain local and offline. They do not +probe provider endpoints, request model lists, or make inference calls. + +## Release Documentation + +Every new release must include `docs/releases/.md` in the tagged +source, using the complete tag as the filename. Its title contains Promptkit +and that exact version. Use this structure: + +```markdown +# Promptkit vMAJOR.MINOR.PATCH + +This release ... + +## Summary + +## Compatibility + +## Upgrade + +## Changes +``` + +State “No action required” or “None” where appropriate rather than omitting a +section. Keep exact current contracts in their canonical documents and link +to them. Update affected canonical documentation in the same candidate. + +These notes are historical records, not optional disposable guidance. Published +release-note contents must not be replaced under the same version. Corrections +belong in a subsequent release. Annotated-tag messages complement the checked-in +note: generate the annotation from that note and the validated commit rather +than maintaining a second summary. + +## Validation And Publication + +The [publication script](../../scripts/release.sh) runs the +[local checks](../../scripts/release-check.sh) once for the final candidate. +Retain clean-candidate, synchronized-main, unused-version, module-hygiene, +release-note structure, and tag-target checks. Publish only the selected tag +ref. Changing the candidate requires fresh validation. No provider endpoint +probes or inference calls are release checks. + +Release-note content and retirement of completed roadmaps are preparation +responsibilities, not separate human sign-off gates. Preserve planned and +deferred work and repair links. Ordinary change review covers unintended files, +secrets, and private data; do not repeat it as a release-specific approval. + +A successful tag push to origin completes the release. Never wait for CI to +finish or succeed. CI results, remote read-back, and Go module availability +are informational follow-ups, not completion gates. An ambiguous push result +requires confirming publication on origin, not waiting for CI. + +## Immutability And Failure Recovery + +Never move, replace, recreate, or delete a published tag or replace published +release contents. If validation fails before publication, correct the candidate, +rerun the complete checks, and repeat the publication guards. Remove an +unpublished local tag only after confirming it was not published. + +If publication or subsequent investigation reveals a defect, leave the +published release intact. Correct the defect on main, select a new patch or +other semantically appropriate version, write its matching release note, and +repeat the complete procedure. If publication status is uncertain, inspect +the authoritative remote before deciding whether a tag is unpublished; do not +overwrite it to retry. + +Transient network, authentication, CI, or module-availability failures do not +by themselves warrant a new version. Investigate separately and publish a +corrective release only when published contents actually need changing. + +## Policy Exceptions And Deliberate Scope + +This adoption follows the maintainer-approved lean workflow rather than every +base-policy guard: + +- Collision checks concern origin, the authoritative publication destination, + rather than every configured remote. Local and origin tag checks, synchronized + main, and an explicit selected-ref push protect the actual release channel. +- Automated local validation and publication-state checks replace manual + mechanical inspections and separate human sign-offs. Ordinary change review, + editorial responsibility for release notes, and roadmap housekeeping remain; + repeating them as approvals would add friction without distinct protection. +- Successful tag publication completes this source-only release. CI can be slow + or unreliable, and module availability depends on external services. Neither + is a completion gate. Local candidate validation and explicit tag-target + checks protect publication; ambiguous push results are resolved against origin. + Actual defects are corrected in a new release, never by replacing a tag. + +Ignored local files are not prohibited merely because they exist: they are not +included in the tagged commit. The clean-candidate guard and ordinary review of +tracked changes protect the published contents. + +The catalog profile provision preserves an already approved compatibility +boundary; it does not extend volatility to backend definitions or runtime +contracts. Further release-governance changes require deliberate approval. diff --git a/docs/policy/testing.md b/docs/policy/testing.md index ed62030..640cb7a 100644 --- a/docs/policy/testing.md +++ b/docs/policy/testing.md @@ -1,5 +1,9 @@ # Testing Policy +**Base Policy Version:** 1.0 + +**Last Updated:** 2026-09-23 + ## Purpose Our tests exist to make **incorrect changes expensive and correct changes cheap**. @@ -50,16 +54,21 @@ Examples of appropriate seams include clocks, randomness, subprocesses, remote A ## Test execution requirements Promptkit currently uses maintainer-run validation rather than hosted CI. -Maintainers run the complete local workflow in the -[development guide](../development.md#maintainer-validation) before accepting -changes. That guide is the canonical owner of exact commands, formatting, -documentation-link validation, and repository-hygiene checks. +Ordinary work requires validation proportionate to its scope and risk. Do not +repeat successful checks for unchanged content merely to create a commit. +Release candidates require the complete local workflow in the +[development guide](../development.md#maintainer-validation). That guide routes +contributors to the executable checks for formatting, documentation links, +and repository hygiene. Introducing hosted CI later would supplement, not silently redefine, this documented validation model. -Maintainer validation must include ordinary and race-enabled package tests, -static analysis, a complete build, and execution of both maintained offline -consumer examples. The preparation example protects assembled preparation and +Full release-candidate validation includes race-enabled package tests, explicit +static analysis, and execution with automated semantic checks of both 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 inspection behavior. The execution example separately protects assembled `Run`, injected-client, validation, usage, and result behavior. diff --git a/docs/release.md b/docs/release.md index 910508f..a86af7c 100644 --- a/docs/release.md +++ b/docs/release.md @@ -1,276 +1,82 @@ # Release Procedure -## Release Model +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. -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. +## Prepare The Candidate -`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. +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. -Promptkit releases are source-only. The annotated tag message is the release -note; there is no separate hosted release or binary packaging step. +Select an unused version and create `docs/releases/.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. + +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. + +## Validate And Publish + +Run the following with the chosen unused version (replace the placeholder): + +```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. + +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. ## Catalog Profile Compatibility -External catalog profile data may be added, removed, renamed, or modified -without deprecation or advance notice. This exception is limited to profile -data imported from the maintained catalog modules. It does not weaken ordinary -compatibility for Promptkit's public Go API, configuration and format semantics, -runtime behavior, catalog-module APIs, manifest schema, backend definitions, or -consumer-owned definitions. +The [release policy](policy/release.md#catalog-profile-compatibility) owns the +catalog compatibility boundary. This heading preserves older incoming links. -Within the catalog versions selected and tested by a Promptkit release, and -unless a consumer overrides the name or its base, Promptkit keeps the literal -profile IDs `promptkit-openrouter-standard` and -`promptkit-openrouter-auto` available. The names do not promise a permanent -model, backend, routing behavior, settings, cost, output, or upstream -availability. The [framework format reference](formats.md#built-in-profile-catalog) -owns their current definitions. +## Failure Recovery -`v0.10.0` is the initial minor release for this compatibility transition. -Afterward, catalog-only updates and adjustments to the current selections may -ship in a patch release when all other compatibility obligations remain intact. -External profile removals can require consumer action; they are not automatic -migrations. +If local checks fail, fix the candidate, commit and push changes, then rerun +the publication command. It never automatically removes a tag. -Catalog adoption and release validation remain local and offline. They do not -probe provider endpoints, request model lists, or make inference calls. - -## 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: +If a push fails or its result is ambiguous, set `RELEASE_VERSION` to the +attempted version and inspect origin before retrying: ```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 +git ls-remote --tags origin "refs/tags/$RELEASE_VERSION" "refs/tags/$RELEASE_VERSION^{}" +git rev-parse "refs/tags/$RELEASE_VERSION" "refs/tags/$RELEASE_VERSION^{}" ``` -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. +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. -## 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. - -When a release introduces or updates the maintained external catalogs, state -the two independently versioned selected data dependencies and any resulting -consumer action. State that external profile additions, removals, renames, and -selection changes are covered by the catalog-profile policy, while the ordinary -public API and configuration contracts remain in force. Link to the current -format and consumer documentation rather than duplicating catalog inventories -or contracts in the release note. - -## 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. +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). ## 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. +Update the policy and this procedure together when changing release governance. +Consumers own their dependency-availability and release checks; their readiness +does not delay completion of a Promptkit release. diff --git a/scripts/release-check.sh b/scripts/release-check.sh new file mode 100755 index 0000000..2ff321c --- /dev/null +++ b/scripts/release-check.sh @@ -0,0 +1,145 @@ +#!/bin/sh +# Run local checks without publishing or changing files. +set -eu +cd "$(dirname "$0")/.." + +go test -race -vet=off ./... +go vet ./... +python3 - <<'PY' +import json +import subprocess + +def example(name): + return json.loads(subprocess.check_output( + ["go", "run", "./examples/go-library/" + name], text=True)) + +prepared = example("prepare") +assert prepared["prompt_id"] == "example.prepare", prepared +assert prepared["selected_profile"] == "offline-example", prepared +assert prepared["model"] == "offline-model", prepared +assert prepared["message_count"] > 0, prepared +result = example("run") +assert result["output"] == "Ada finished the migration review.", result +assert result["is_valid"] is True, result +assert result["validation_status"] == "passed", result +assert result["model"] == "offline-model", result +assert result["total_tokens"] == 18, result +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 diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..a4b7d2e --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,57 @@ +#!/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."