From 367af5fecaca36aa3e4664f4bf5a8c2f763779ac Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 2 Nov 2026 14:10:37 +0000 Subject: [PATCH] Add hosted Gitea release delivery --- .gitignore | 2 +- docs/policy/oss.md | 11 +++---- docs/policy/release.md | 24 +++++++++++---- docs/release.md | 65 ++++++++++++++++++++++++++++++++++++++--- releasekit.json | 8 +++++ scripts/test-release.py | 33 ++++++++++++++++++++- 6 files changed, 126 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index f332b17..88395b5 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,7 @@ go.work go.work.sum -# Local ReleaseKit executable +# Local ReleaseKit executable and delivery credentials .releasekit/ # env file diff --git a/docs/policy/oss.md b/docs/policy/oss.md index 14bab6c..8bd6e3a 100644 --- a/docs/policy/oss.md +++ b/docs/policy/oss.md @@ -2,7 +2,7 @@ **Base Policy Version:** 1.8 -**Last Updated**: 2026-09-27 +**Last Updated**: 2026-11-02 **Upstream Source:** [oss.md](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/policy/oss.md) @@ -221,7 +221,8 @@ Promptkit retains its LGPL-3.0-or-later library grant in [COPYRIGHT](../../COPYRIGHT). Adapted policy documentation has the separate scope identified there and in [attribution](../oss/attribution.md). -Promptkit publishes source-only Go module tags on Gitea, with no binaries or -separate hosted download page. The [release procedure](../release.md) owns -publication and concrete footer URLs. The hosted-page guidance applies if such -a channel is deliberately introduced; it does not require creating one. +Promptkit publishes source-only Go module tags and Gitea release pages, with no +binaries. Each new hosted page reuses the complete checked-in release note, +including its licensing footer. The [release procedure](../release.md) owns +source publication, hosted delivery, and concrete footer URLs. Previously +published notes and tags retain their original records. diff --git a/docs/policy/release.md b/docs/policy/release.md index 4387c7b..b54d47e 100644 --- a/docs/policy/release.md +++ b/docs/policy/release.md @@ -2,7 +2,7 @@ **Base Policy Version:** 1.6 -**Last Updated**: 2026-10-08 +**Last Updated**: 2026-11-02 **Upstream Source:** [release.md](https://gitea.maximumdirect.net/eric/policy/src/commit/e833faddf120c789a9b45348edaf75720de16348/docs/policy/release.md) @@ -241,9 +241,13 @@ navigation on 2026-09-26. Apply them prospectively; previously published notes and tags retain their original records. Promptkit publishes source-only Go modules from main to origin on Gitea, using -annotated stable SemVer tags. It has no binaries, hosted download pages, or -asynchronous deliverables. The source commit is pushed through main before the -tag. The [release procedure](../release.md) owns commands and recovery. +annotated stable SemVer tags. The source commit is pushed through main before +the tag. After source publication, maintainers use ReleaseKit locally to deliver +a Gitea release page containing the complete checked-in note, including its +licensing footer. There are no binaries or other deliverables. Source publication +and hosted delivery have separate outcomes; delivery credentials and service +availability do not gate source-release completion. The +[release procedure](../release.md) owns commands and recovery. Release preparation uses the latest Go installation available in the development environment to build the maintainer release executable and validate the @@ -268,8 +272,10 @@ 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 +maintained consumer examples. It also rejects the configured local delivery +token path if committed; this narrowly scoped check is not a general credential +scan. 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, @@ -278,3 +284,9 @@ 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. + +Hosted delivery is a separate post-publication step for new releases. It reuses +the immutable tag and note without rerunning candidate validation or waiting +for CI. Retry failed delivery against that same version after resolving any +uncertain hosted write; preserve published tags and hosted content. Report +delivery status separately from the completed source release. diff --git a/docs/release.md b/docs/release.md index 7dfd8d2..1d4d4a2 100644 --- a/docs/release.md +++ b/docs/release.md @@ -4,6 +4,8 @@ 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. +After source publication, deliver the hosted Gitea release page as described +below. Report source publication and hosted delivery separately. ## Prepare The Candidate @@ -51,13 +53,22 @@ impact under the [versioning policy](policy/release.md#versioning-and-compatibil End each new note with the licensing footer below, using the selected version: ```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) +[COPYRIGHT](https://forge.maximumdirect.net/base-layer/promptkit/src/tag/vX.Y.Z/COPYRIGHT) | [LICENSE](https://forge.maximumdirect.net/base-layer/promptkit/src/tag/vX.Y.Z/LICENSE) | [ATTRIBUTION](https://forge.maximumdirect.net/base-layer/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. +links. Hosted delivery reuses this complete note without a separate summary. +Promptkit publishes no binaries, attachments, or checksum manifests. + +For hosted delivery, supply a Gitea API token with access to publish releases +through `RELEASEKIT_GITEA_TOKEN` or the ignored local file +`.releasekit/gitea-token`, with owner-only permissions for the file. A nonblank +environment value takes precedence. ReleaseKit trims surrounding whitespace +from file contents. Keep the token out of commits and shared logs. The profile +rejects that exact token path if +committed; it does not scan other files or Git history. Source publication uses +Git authentication and neither reads nor requires the delivery token. Retire completed roadmaps while preserving deferred work and repairing incoming links. Review the intended changes through the ordinary development workflow, @@ -87,7 +98,8 @@ 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 +maintained offline consumer examples. It also checks that the configured local +token path is not committed. ReleaseKit 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. @@ -97,8 +109,53 @@ 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. +## Deliver The Hosted Release + +After source publication succeeds, keep a clean checkout at the published tag's +commit and run: + +```sh +./scripts/releasekit.sh deliver vMAJOR.MINOR.PATCH +``` + +The profile selects Gitea at `https://forge.maximumdirect.net`, owner +`base-layer`, repository `promptkit`. With no build targets, ReleaseKit creates +or accepts a stable, non-draft release page containing the complete committed +note and licensing footer. There is no binary build, attachment upload, or +candidate revalidation. Delivery does not publish another tag or wait for CI. + +ReleaseKit performs its authenticated, read-only repository preflight during +delivery; no separate preflight is required before source publication. Missing +credentials or an unavailable hosted service leave the source release complete. + +Delivery returns status 0 for completion, 1 for an operational failure, +conflict, or partial delivery, 2 for invalid configuration or credential/access +errors, and 3 for an unresolved hosted write. Report the hosted URL on success +and retain the diagnostic and delivery state on failure. See the upstream +[delivery guide](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.7.0/docs/consumers/delivery.md) +for the full result contract. + +For a later retry, use a clean checkout of the original tag with its recorded +module files and make the token available locally. For example, in a separate +checkout with the tag available: + +```sh +git switch --detach vMAJOR.MINOR.PATCH +./scripts/releasekit.sh deliver vMAJOR.MINOR.PATCH +``` + +The launcher builds the tool selected by that checkout. Matching hosted content +is accepted; conflicting content is not overwritten. Resolve an unknown hosted +write before retrying. Retry delivery with the same immutable version rather +than rerunning `publish` or moving a tag. Corrections to published source or +content require a new appropriate version. These steps apply to tags containing +this delivery configuration; older releases retain their original workflows. + ## Failure Recovery +This section covers source publication. Hosted-delivery recovery is described +[above](#deliver-the-hosted-release); its failure does not undo source publication. + 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 diff --git a/releasekit.json b/releasekit.json index de23a7c..b87925a 100644 --- a/releasekit.json +++ b/releasekit.json @@ -2,6 +2,7 @@ "product": "Promptkit", "remote": "origin", "validation": { + "forbidden_files": [".releasekit/gitea-token"], "go": { "race": true, "full_vet": true @@ -13,6 +14,13 @@ "local_replacements": true } }, + "delivery": { + "forge": "gitea", + "base_url": "https://forge.maximumdirect.net", + "owner": "base-layer", + "repository": "promptkit", + "token_file": ".releasekit/gitea-token" + }, "checks": [ { "args": ["python3", "./scripts/check-examples.py"], diff --git a/scripts/test-release.py b/scripts/test-release.py index 71a705b..349d750 100644 --- a/scripts/test-release.py +++ b/scripts/test-release.py @@ -216,6 +216,10 @@ def prepare_repository(repository, remote, environment, module_bytes, version): def verify_publication(repository, remote, temporary_root, environment, module_bytes, selected_go): + profile = json.loads((repository / "releasekit.json").read_text(encoding="utf-8")) + token_path = repository / profile["delivery"]["token_file"] + if token_path.exists() or environment.get("RELEASEKIT_GITEA_TOKEN"): + raise RuntimeError("publication fixture must not have delivery credentials") selected_releasekit = run( ["go", "list", "-mod=readonly", "-m", "-f", "{{.Version}}", RELEASEKIT_MODULE], cwd=repository, @@ -387,6 +391,31 @@ def verify_validation_failure( raise RuntimeError(f"failure fixture checkout is not clean:\n{status}") +def verify_forbidden_token(repository, environment): + profile = json.loads((repository / "releasekit.json").read_text(encoding="utf-8")) + token_path = profile["delivery"]["token_file"] + write_file(repository / token_path, "disposable fixture placeholder\n") + note_path = f"docs/releases/{FAILED_RELEASE_VERSION}.md" + write_file(repository / note_path, f"# Promptkit {FAILED_RELEASE_VERSION}\n\nFixture.\n") + repo_git(repository, environment, "add", "--", note_path) + repo_git(repository, environment, "add", "-f", "--", token_path) + repo_git(repository, environment, "commit", "-m", "Add forbidden-path fixture") + + result = execute( + [str(repository / ".releasekit/bin/releasekit"), "check", FAILED_RELEASE_VERSION], + cwd=repository, + environment=environment, + ) + if ( + result.returncode != 1 + or "forbidden-files" not in result.stderr + or token_path not in result.stderr + ): + raise RuntimeError( + f"committed delivery token path was not rejected:\n{result.stdout}\n{result.stderr}" + ) + + def verify_usage_classification(binary, temporary_root, environment): result = execute( [str(binary), "unknown"], cwd=temporary_root, environment=environment @@ -404,6 +433,7 @@ def verify_usage_classification(binary, temporary_root, environment): def main(): environment = os.environ.copy() + environment.pop("RELEASEKIT_GITEA_TOKEN", None) environment.update( { "GOTOOLCHAIN": "local", @@ -464,11 +494,12 @@ def main(): module_bytes, selected_go, ) + verify_forbidden_token(repository, environment) except (RuntimeError, OSError, json.JSONDecodeError) as error: print(error, file=sys.stderr) return 1 - print("ReleaseKit publication success and failure fixtures passed") + print("ReleaseKit publication and committed-token-path fixtures passed") return 0