14 Commits

23 changed files with 1254 additions and 810 deletions

View File

@@ -1,26 +1,21 @@
matrix:
include:
- GO_IMAGE: golang:1.25.5
RELEASEKIT_VERSION: v0.1.0
when:
- event: tag
ref: refs/tags/v*
steps:
- name: build-release-assets
image: golang:1.25.5
commands:
- bash scripts/build-release.sh "$CI_COMMIT_TAG" dist
- name: publish-release
image: woodpeckerci/plugin-release
depends_on:
- build-release-assets
settings:
api_key:
- name: deliver
image: ${GO_IMAGE}
environment:
RELEASEKIT_GITEA_TOKEN:
from_secret: GITEA_RELEASE_TOKEN
files:
- dist/audita-*-linux-amd64
- dist/audita-*-linux-arm64
note: docs/releases/${CI_COMMIT_TAG}.md
checksum: sha256
checksum-file: SHA256SUMS
checksum-flatten: true
file-exists: skip
overwrite: false
prerelease: false
commands:
- |
go run gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit@${RELEASEKIT_VERSION} ci-deliver \
--repo "$${CI_WORKSPACE}" \
--commit "$${CI_COMMIT_SHA}" \
"$${CI_COMMIT_TAG}"

View File

@@ -0,0 +1,45 @@
# ADR 0001: Adopt ReleaseKit for release management
## Status
Accepted.
## Date
2026-10-07
## Context
Audita maintains custom release scripts, build tooling, delivery configuration,
and tests for mechanics shared across applications in the same suite.
[ReleaseKit](https://gitea.maximumdirect.net/eric/ReleaseKit) provides a common implementation and standard workflow for
those responsibilities.
## Decision
Adopt ReleaseKit for shared release validation, source publication, binary
builds, version information, and hosted delivery. Audita will follow its standard
workflow and supply product identity, delivery targets, release notes, and
necessary product-specific checks.
Better maintainability and less locally owned release-focused code outweigh any
small advantage from retaining Audita's custom release procedure. Changes to
that procedure are an accepted consequence of standardization.
## Alternatives considered
Retaining the custom procedure would preserve its exact behavior and local
control. It would also retain responsibility for duplicated mechanics, tests,
and maintenance across projects. The shared implementation offers greater
long-term value.
## Consequences
Audita's release integration becomes smaller, with common mechanics and their
tests maintained upstream. Audita benefits from shared fixes and takes on
ReleaseKit dependency maintenance.
Adoption requires updates to release tooling, CI wiring, documentation, and
runtime version reporting. The [release policy](../policy/release.md) continues
to govern compatibility and release completion; the
[release procedure](../release.md) will describe the adopted workflow.

View File

@@ -1,6 +1,11 @@
# Architectural Decision Records
No ADRs have been recorded here. Use the
## Accepted
- [ADR 0001: Adopt ReleaseKit for release management](0001-releasekit.md) —
accepted 2026-10-07.
Use the
[documentation policy's ADR lifecycle](../policy/documentation.md#architecture-decision-records)
for significant decisions. Current architecture belongs in the
[architecture policy](../policy/architecture.md); implementation status belongs in

View File

@@ -11,6 +11,7 @@ the [migration guide](migrations/v2.md) maps retired inputs.
audita process <transcript.json> --glossary <glossary.yml> [flags]
audita config validate --config <config.yml> [flags]
audita config print-effective [flags]
audita version
```
Root invocation, `--help`, `-h`, and command help succeed without loading a config
@@ -26,6 +27,16 @@ Missing system candidates are skipped; inaccessible selected files are errors.
No file means a configuration error. `AUDITA_CONFIG` is ignored. The selected
overlay is loaded beside the main file, not used to locate profile files.
`audita version` prints `audita <version>` and a newline to stdout, with no
application configuration or provider setup. It returns `0` on success with
empty stderr. `help version`, `version -h`, and `version --help` print command
usage to stdout and return `0`. Help follows standard flag parsing and can
short-circuit trailing arguments. Other invalid invocations return `2` with a
usage error on stderr. The command uses ReleaseKit's public
`buildinfo.Version()` accessor. It reports an injected release identifier when
present, otherwise the executable's main-module version when available and not
`(devel)`, and `dev` when neither source provides a release identifier.
## Setting overrides
Only flags actually supplied override file or overlay settings. `--modules ''`

View File

@@ -27,6 +27,11 @@ CLI wiring, and maintained examples. Definitions of behavior belong to the
for the changed behavior.
4. Update the CLI reference and affected integration guidance.
The `version` route uses ReleaseKit's public `buildinfo.Version()` accessor and
stays independent of application runtime assembly. Preserve its behavior from
the [CLI contract](../cli.md#commands-and-help) when changing the command; the
adoption decision is recorded in [ADR 0001](../adr/0001-releasekit.md).
## Validating Examples
Keep complete copyable files in [examples](../../examples/) and link to them from

View File

@@ -28,6 +28,20 @@ License and notices: [LGPL v3](licenses/LGPL-3.0.txt) |
[Source](https://gitea.maximumdirect.net/eric/ConfigKit/src/commit/dc09959cca4e105f49ec316c6d4ba585bd2340bd).
## ReleaseKit
Selected module: `gitea.maximumdirect.net/eric/ReleaseKit` at `v0.1.0`.
Copyright (C) 2026 Eric Rakestraw
SPDX-License-Identifier: LGPL-3.0-or-later
License and notices: [LGPL v3](licenses/LGPL-3.0.txt) |
[GPL v3](licenses/GPL-3.0.txt) |
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ReleaseKit/src/commit/5e60fe3113c69f95c42fbbe32a8ca4b51f5b83c1/COPYRIGHT).
[Source](https://gitea.maximumdirect.net/eric/ReleaseKit/src/commit/5e60fe3113c69f95c42fbbe32a8ca4b51f5b83c1).
## Go runtime and standard library
Selected toolchain: Go 1.25.5 for the distributed Linux binaries.

View File

@@ -2,7 +2,7 @@
**Base Policy Version:** 1.4
**Last Updated**: 2026-09-28
**Last Updated**: 2026-10-07
**Upstream Source:** [Standard Repository Policies release](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/policy/release.md)
## Purpose
@@ -49,12 +49,17 @@ failures and optional investigations under artifact-delivery status.
| --- | --- |
| Linux amd64 | Required production target |
| Linux arm64 | Required production target |
| macOS arm64 | Best effort for local development; failures do not block a release or Linux artifact publication |
| macOS arm64 | Best effort for local development; development failures do not block a release or Linux artifact publication |
Windows and macOS amd64 are unsupported. Neither their builds nor tests are
release requirements. Native macOS testing and access to a Mac are not release
prerequisites.
Linux amd64 and arm64 remain the required production targets. Publication from
a host outside the selected target set must pass ReleaseKit's temporary native
version check. This check does not require access to a Mac or native tests on
every delivery target.
## Versioning And Compatibility
Stable releases use Semantic Versioning in the form `vMAJOR.MINOR.PATCH`, with

View File

@@ -1,144 +1,120 @@
# Releasing Audita
The [release policy](policy/release.md) defines versioning, supported platforms,
immutability, and success. This runbook implements it with
[scripts/release.sh](../scripts/release.sh). Publication ends after a successful
source/tag push; binary delivery runs asynchronously.
The [release policy](policy/release.md) owns release requirements. This runbook
uses the native commands in [scripts/release.sh](../scripts/release.sh), which
builds and invokes ReleaseKit. A confirmed source tag completes the release;
Woodpecker reports binary delivery separately.
## Prerequisites
Use Bash, Git, and Go 1.25.5 or newer (CI uses Go 1.25.5), with dependencies
available locally or downloadable. Tests themselves are offline. Work from a
checkout of `main` with permission to push that branch and tags to `origin`.
For this repository, `origin` is `https://gitea.maximumdirect.net/eric/audita.git`.
The script uses the checkout's configured `origin`; set it correctly before use.
The remote must support atomic pushes. No CI credentials or Mac are needed locally.
Use Bash, Git, and Go 1.25.5 or newer. The authoritative remote is `origin`,
which must point to `https://gitea.maximumdirect.net/eric/audita.git`; the
primary branch is `main`. Source publication needs no delivery credential.
Woodpecker uses the existing `GITEA_RELEASE_TOKEN` secret for delivery.
CI uses the existing `GITEA_RELEASE_TOKEN` secret for asynchronous asset delivery.
macOS arm64 is a best-effort development environment; no macOS binary is published.
Prepare dependencies before offline candidate validation:
## Prepare the Candidate
```sh
GOWORK=off GOTOOLCHAIN=local go mod download
```
Choose an unused version following the policy. Write
`docs/releases/<version>.md`, beginning with `# Audita <version>`, followed by a
short summary and any compatibility or upgrade instructions relevant to the
release. There are no mandatory boilerplate sections. Update affected canonical
documentation with behavior changes.
If dependency preparation changes `go.mod` or `go.sum`, include those changes in
the committed candidate. The release wrapper separately bootstraps the pinned
ReleaseKit command into ignored `bin/releasekit`; that build is tool preparation,
not candidate validation.
End each new note with these links, substituting the selected release version for
`<version>`:
## Prepare and publish the candidate
Choose an unused version under the [release policy](policy/release.md), write
`docs/releases/<version>.md`, and begin it with `# Audita <version>`. Include a
short summary, relevant compatibility or upgrade instructions, and the required
`COPYRIGHT | LICENSE | ATTRIBUTION` footer with links for the selected version.
Update affected canonical documentation with behavior changes.
End each note with this footer, substituting its complete tag for `<version>`:
```markdown
[COPYRIGHT](https://gitea.maximumdirect.net/eric/audita/src/tag/<version>/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/audita/src/tag/<version>/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/audita/src/tag/<version>/docs/oss/attribution.md)
```
Check that all three paths exist in the candidate source. Retire completed
roadmaps when useful during preparation, preserving planned and deferred work
and repairing affected links. This is ordinary preparation, not a script gate.
Commit the intended changes, including the note, on `main`. The release script
requires a clean index and worktree and does not create commits. There is no
separate approval round. Published notes describe their tagged release; existing
release history does not need to be reconstructed.
## Validate and Publish
From the repository root, substitute the chosen version:
Commit the complete candidate on `main`, including the release note, then push
the branch first:
```sh
bash scripts/release.sh --publish v0.2.0
git push origin main
```
This single invocation:
Record the candidate commit. Before publication, remote `main` must identify
that exact commit. ReleaseKit checks this along with the clean candidate, note,
version, and remote tag state.
1. Checks the version, branch, clean candidate, matching note title, and tag absence.
2. Runs the default offline test suite once, then builds both required Linux targets
in a temporary directory using the same build script as CI.
3. Checks that the candidate and worktree are unchanged and that it includes the
current `origin/main` history.
4. Creates the version tag at that commit and atomically pushes the exact source
commit to `main` and the exact tag to `origin`.
5. Reports release success and exits without polling CI or checking asset availability.
To validate a prepared candidate without remote access, creating a tag, or pushing:
Publish from the repository root, substituting the selected version:
```sh
bash scripts/release.sh --check v0.2.0
bash scripts/release.sh publish VERSION
```
This is an optional diagnostic command, not a required step before `--publish`.
Publication validates within its own invocation; it does not use persistent cached
results. Both modes require the candidate note and a clean `main` checkout.
The command runs `go test -mod=readonly -count=1 ./...` once with module and
toolchain network access disabled, builds the profile's Linux amd64 and arm64
targets, and checks the native version output. It creates an annotated tag whose
message comes from the committed note, then pushes the exact tag object. A
successful publication completes the source release without waiting for CI or
checking artifact availability.
## Artifact Delivery
`bash scripts/release.sh check VERSION` is an optional local diagnostic. It
does not inspect remote availability or publish a tag, and it is not a required
step before `publish`, which performs its own candidate validation.
[Woodpecker](../.woodpecker/release.yml) runs on tag events. It invokes
[scripts/build-release.sh](../scripts/build-release.sh), then publishes:
The wrapper preserves ReleaseKit's exit status: `0` means confirmed publication;
`1` means a known validation or publication failure/conflict; `2` means usage,
profile, or version input error; and `3` means the push outcome remains unknown.
## Binary delivery
Woodpecker runs the pinned ReleaseKit `ci-deliver` command for tags under
`refs/tags/v*`. It verifies the triggering commit against the tag, prepares the
tagged source, and uses `releasekit.json` to build and deliver:
- `audita-<version>-linux-amd64`
- `audita-<version>-linux-arm64`
- `SHA256SUMS`
Builds disable CGO and use the shared script's release flags. The version is in the
asset name and source tag; Audita has no CLI version command. Cross-compilation
checks compilation, not native execution on each architecture.
The CI job status and logs show delivery progress or failure. Assets appear on
the matching Gitea release page when delivery succeeds. A successful tag push
must not be reported as confirmation that these files are available.
For a new hosted release, the release plugin uses the checked-in note at the
tag as its body, including the footer. On delivery retries it preserves
existing release information and skips files that already exist.
Consumers can download the binary for their Linux architecture and `SHA256SUMS`
from that page, check its listed SHA-256 digest with `sha256sum`, and make it
executable. Optional consumer checks are not maintainer release gates. Digests
detect file mismatches; they are not an independent signature of authenticity.
## Failure and Retry
**Local validation fails:** correct the issue, commit any changes, and rerun the
publication command. A failure before tag creation publishes nothing. Temporary
local build files are removed on exit.
**Push fails or the connection is interrupted:** the script retains the local tag
and reports failure rather than guessing whether publication succeeded. Inspect
remote state with the actual version:
The workflow invokes:
```sh
git rev-parse 'v0.2.0^{commit}'
git ls-remote origin refs/heads/main refs/tags/v0.2.0
go run gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit@${RELEASEKIT_VERSION} ci-deliver \
--repo "$${CI_WORKSPACE}" \
--commit "$${CI_COMMIT_SHA}" \
"$${CI_COMMIT_TAG}"
```
If the remote tag identifies the intended candidate and its source is on remote
`main`, the release succeeded; no CI waiting is needed. An atomic push normally
updates both refs or neither. If `main` has advanced since publication, fetch it
and check candidate ancestry rather than requiring tip equality.
Woodpecker sets `RELEASEKIT_VERSION` to `v0.1.0` for this workflow.
If the tag is absent remotely and was never published or used by a release
channel, remove only that unpublished local tag (`git tag -d v0.2.0`), resolve
any rejected branch update, and rerun the command. If the remote tag has a
different target, do not overwrite it; choose a fresh version. Never force a
branch or tag update to bypass the guards.
The complete checked-in release note, including its licensing footer, supplies
the hosted release body. The workflow maps the existing `GITEA_RELEASE_TOKEN`
secret to `RELEASEKIT_GITEA_TOKEN`. Delivery runs independently after source
publication; its status and logs report delivery progress or failure.
**CI delivery fails:** the release remains successful. Inspect the failed tag
job's logs and retry that same tagged job for a transient failure. Publication is
configured to skip existing filenames and not overwrite assets. Keep the same
source and build environment when retrying; inspect any existing assets/checksum
manifest before filling gaps. Do not regenerate and replace a published manifest
or binary. If an existing manifest cannot describe the missing rebuilt assets,
use a new version instead. A pipeline or source defect also requires a corrected
commit and new version. An outage does not require moving the tag.
ReleaseKit handles content-aware retries and retained-artifact recovery. See
the pinned [publication recovery guide](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.1.0/docs/consumers/publish.md)
and [delivery guide](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.1.0/docs/consumers/delivery.md)
for those mechanics. A delivery failure does not undo a confirmed source
release. Retry the same immutable tag after resolving the reported state; do not
replace published files or a checksum manifest.
**A released source defect is discovered:** fix it on `main` and publish a new
appropriate version with a note explaining the correction. Leave the old tag and
published files intact.
## Failure and recovery
## Maintaining the Release Workflow
Fix validation failures in a new committed candidate, then rerun `publish`.
After a failed push, ReleaseKit retains the local annotated tag and reports the
captured authoritative endpoint and publication state. Resolve the result
against that endpoint before retrying. An exact remote tag-object match, with
its source available on remote `main`, can confirm publication. If the remote
state cannot be read, the result remains unknown; do not remove or replace the
tag while uncertain. Follow the pinned [publication recovery guide](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.1.0/docs/consumers/publish.md)
for object-level recovery details.
The shared build script keeps local and CI build settings aligned. Tests in
[scripts/release_test.go](../scripts/release_test.go) exercise publication against
temporary local Git remotes, including guard failures and rejected pushes, with
Go execution stubbed to avoid recursive builds/tests. They run in `go test ./...`.
Changes to CI delivery should preserve explicit asset names, checksums, and
non-overwriting publication. CI delivery is separate from the local success path.
A source-release success and artifact-delivery status are separate. For a
partial or uncertain delivery, inspect the job output and follow the pinned
[delivery recovery guidance](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.1.0/docs/consumers/delivery.md).
A source or published-content correction requires a new appropriate version;
leave the original tag and its contents intact.

View File

@@ -1,5 +1,6 @@
# Release Notes
- [Audita v3.0.1](v3.0.1.md)
- [Audita v3.0.0](v3.0.0.md)
- [Audita v2.0.1](v2.0.1.md)
- [Audita v2.0.0](v2.0.0.md)

14
docs/releases/v3.0.1.md Normal file
View File

@@ -0,0 +1,14 @@
# Audita v3.0.1
This patch release adds a configuration-free version command and adopts
ReleaseKit for source publication and Linux binary delivery. Transcript
processing, configuration, and LLM behavior are unchanged.
## Changes
- `audita version` prints the application version without loading application
configuration or contacting an LLM provider.
- Release publication and Linux artifact delivery now use ReleaseKit v0.1.0.
The release runbook describes the maintainer workflow and delivery recovery.
[COPYRIGHT](https://gitea.maximumdirect.net/eric/audita/src/tag/v3.0.1/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/audita/src/tag/v3.0.1/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/audita/src/tag/v3.0.1/docs/oss/attribution.md)

View File

@@ -5,8 +5,8 @@
**Status:** Complete. The implementation and tests are committed through
`8175ed8`. Final documentation and example changes were included in the full
build, test, example-validation, and local-link checks before the documentation
completion commit. The [implementation plan](implementation.md) records the
ordered work and verification.
completion commit. The [historical implementation plan](https://gitea.maximumdirect.net/eric/audita/src/commit/b31955fc97adec707b4e5f9a1b0c43c4cbc66750/docs/roadmap/implementation.md)
records the ordered work and verification.
## Purpose and Baseline

View File

@@ -1,523 +1,327 @@
# Implementation Plan: PromptKit v0.12.0 Adoption
# Implementation Plan: ReleaseKit Adoption
**Last updated:** 2026-10-04
**Status:** Complete; Stages 1–9 completed.
**Last updated:** 2026-10-07
**Status:** Complete; Stages 1–5 complete.
**Audience:** Luna coding agent.
## Scope and Execution
Implement the accepted target in the [PromptKit roadmap](promptkit.md). That
document owns feature intent and policy choices; this plan owns the ordered
implementation work. The planning baseline is Audita commit
`b31955fc97adec707b4e5f9a1b0c43c4cbc66750`: ConfigKit v0.5.0 is adopted,
PromptKit is v0.11.2, and the finalized roadmap includes the selected-profile,
operator-override, example-layout, and major-release clarifications.
Implement the target in the [ReleaseKit roadmap](releasekit.md), following
[ADR 0001](../adr/0001-releasekit.md). The roadmap owns intent and policy
choices; this document owns execution. The planning baseline is Audita commit
`418da456d75a68d3bb5a43ab823c5b2b434b90cc`: v3.0.0 is released, the ADR is
accepted, and release tooling still uses the custom scripts and delivery plugin.
This replaces the completed ConfigKit plan at this path. Its
[completed implementation record](https://gitea.maximumdirect.net/eric/audita/src/commit/b31955fc97adec707b4e5f9a1b0c43c4cbc66750/docs/roadmap/implementation.md)
remains available for readers following the ConfigKit completion roadmap.
This replaces the completed PromptKit plan. Its
[completion record](https://gitea.maximumdirect.net/eric/audita/src/commit/418da456d75a68d3bb5a43ab823c5b2b434b90cc/docs/roadmap/implementation.md)
remains available for historical adoption references.
Execute Stage 1 through Stage 9 in order. Each is sized as one bounded Luna
implementation prompt. Commit completed units with short, plain English messages
under the [agent policy](../policy/agents.md); no approval round is needed between
stages. Record actual checks and any remaining expected migration failures.
Stages 1 and 2 change formats before Stage 3 changes their consumer. Intermediate
commits need not be runnable or release-ready. Do not add temporary readers,
compatibility shims, fallback engines for execution, or scaffolding to make them
so. Later stages complete the same migration rather than separate product modes.
Execute Stage 1 through Stage 5 in order. Each is a bounded Luna implementation
unit. Commit completed units with plain English messages under the
[agent policy](../policy/agents.md), recording the relevant checks. Intermediate
commits need not be release-ready; complete the migration directly without
compatibility wrappers for the old release arguments or temporary release modes.
Start with [development guidance](../development.md) and the applicable agent,
[architecture](../policy/architecture.md), [testing](../policy/testing.md),
[documentation](../policy/documentation.md), and [OSS](../policy/oss.md)
policies. Use graph-first discovery. Read the current
[configuration](../config.md), [LLM runtime](../internal/llm-runtime.md),
[prompt registry](../internal/prompts.md), and
[LLM integration](../integrations/openai-compatible-llm.md) contracts for the
established behavior this migration preserves.
Start with [development guidance](../development.md), use graph-first discovery,
and follow the applicable [architecture](../policy/architecture.md),
[testing](../policy/testing.md), [documentation](../policy/documentation.md),
[release](../policy/release.md), and [OSS](../policy/oss.md) policies. Preserve
transcript processing and its current configuration, reporting, and LLM behavior.
Target the released PromptKit v0.12.0 at commit
`4b8169da286917fde7875425bb32b799b12c6af3`, ConfigKit v0.5.0, and both
maintained `/v3` catalogs at `v3.20260930.0`. Read the pinned
[format reference](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.12.0/docs/formats.md)
and [migration guide](https://gitea.maximumdirect.net/eric/promptkit/src/tag/v0.12.0/docs/consumers/pkg-promptkit.md#migrate-from-the-previous-configuration-api).
The sibling checkout is reference material, not a dependency replacement.
Use ReleaseKit v0.1.0, commit
`5e60fe3113c69f95c42fbbe32a8ca4b51f5b83c1`. The sibling checkout is reference
material. Its pinned [configuration](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.1.0/docs/config.md),
[CLI](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.1.0/docs/cli.md),
[build](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.1.0/docs/consumers/build.md),
and [delivery](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.1.0/docs/consumers/delivery.md)
contracts own generic mechanics. Keep Go 1.25.5 and unrelated dependency
selections. Use released modules without a production local replacement.
### Selected Implementation Decisions
- Keep Go 1.25.5, application `version: 2`, current flags, role selection,
application overlays, and main-file-relative path rules. Retain the
`profiles_dir` name and default; broaden its accepted documents as agreed.
- Use PromptKit's catalog and owned defaults, then caller underlays for embedded
Audita prompts and operator directory inputs, then `Sources.Main` for
application backend settings. Directory inputs compose strictly within their
group; discovery order supplies no field precedence. Keep imports disabled.
- Use one ConfigKit mapping for the final application source. For each
application backend, supply its endpoint, `concurrency_limit`,
`queue_capacity`, and explicit `api_key_env: null`. The null suppresses
lower-tier credential lookup. Leave other supported provider fields available
to native layering. Do not put resolved credential values in this mapping.
- Keep selected-role registration, endpoint-match, competing-credential, and
required-key checks. Add explicit nonblank effective model and endpoint checks
for both selected roles. Valid unselected partial profiles remain permitted.
- Preserve the adapter's complete per-call schemas, configured repairs/retries,
operation context, shared variables, observation, and prepare/check/run flow.
Operator messages can omit shared-variable references; do not append the
shared text unconditionally or require particular message counts or roles
beyond PromptKit's own format rules.
- Use captured source inspection for consumed host configuration and message
paths. Keep source-relative resource resolution with PromptKit and ConfigKit;
Audita projects paths for its existing output collision checks.
- Replace the old profile-reload test expectation with snapshot behavior.
Successful construction uses only final effective definitions. A failed
construction may make an embedded-only diagnostic construction to distinguish
asset defects from operator errors; it never executes or substitutes for the
requested configuration.
- Maintained example roots are `examples/profiles/local/` and
`examples/profiles/service/`. Service testing profiles stay in the service
root. Keep the literal `temperature: 0` in examples as an explicit outbound
setting, and use native presence semantics without a compatibility translator.
- Audita imports `ReleaseKit/buildinfo` for runtime version information. The
upstream executable owns release orchestration; no application release engine
or private linker variable is needed.
- `releasekit.json` owns product facts. Local tooling builds the executable from
the module graph; CI pins the same v0.1.0 release explicitly.
- Keep `scripts/release.sh` as a thin entry point with native ReleaseKit
arguments, such as `publish VERSION` and `check VERSION`. Compile its tool into
ignored `bin/releasekit` and invoke it with `exec` to preserve native statuses.
- Main-first preparation, annotated tags, the offline standard suite, Linux
builds, and the native version smoke check are accepted. On a nonselected
host, the shared builder may compile a temporary native executable and require
its check to succeed. Keep only Linux amd64 and arm64 delivery targets.
- CI uses managed `ci-deliver` and the existing `GITEA_RELEASE_TOKEN` secret,
exposed to the command as `RELEASEKIT_GITEA_TOKEN`. Retained prebuilt artifacts
remain an upstream recovery option rather than a second ordinary CI sequence.
- Additional profile checks are omitted. Do not repeat the standard suite,
maintain a local Git/publication guard layer, or copy upstream delivery logic.
Focus tests on Audita wiring and reuse existing protection for common mechanics.
This unit is an intentional major-release migration. Exact version selection,
release notes, tag/push, and publication timing remain release preparation under
the [release policy](../policy/release.md). Guided recovery, new report formats,
hot reload, extra configuration commands, and scheduling changes remain outside
this plan. Tests should protect changed integration behavior and established
contracts, not reproduce the libraries' entire validation matrices.
This implementation sprint does not choose an Audita release version, write a
new versioned release note, tag, push to the authoritative repository, contact
Gitea with delivery credentials, or wait for CI. Deployment observations belong
to the first adopting release and its separate delivery status. No extra release
gates or platform guarantees are introduced by these adoption checks.
## Stage 1 — Convert Embedded Prompt Definitions
## Stage 1 — Add the Dependency and Version Command [x]
**Primary files:** The seven YAML files under
`internal/prompts/assets/prompts/`.
**Primary files:** `go.mod`, `go.sum`, `internal/cli/run.go`, and focused tests in
`internal/cli`.
1. Wrap each existing definition beneath `prompts`, its exact current ID, and
the string version `v1`. Remove the old inner `id` and `version` fields.
Keep filenames and Audita registry identifiers unchanged; filenames are
asset locations rather than PromptKit identities.
2. Preserve messages, block-scalar text, variable references, task examples,
output settings, and schema paths. Preserve the applicable references to
`HardeningPolicy`, `ProposalContract`, and `ValidationContract`. Do not edit
shared fragments, response schemas, or domain policies to accommodate the
new container format.
3. Review each before/after definition structurally, including YAML indentation
inside block scalars. Do not introduce default profile selections or new
repair counts. The adapter continues to own the runtime output contract.
**Verification and completion:** Check all seven migrated identities and
version keys against `RegisteredMetadata`, and run `git diff --check`. Registry
preparation tests become runnable with the new constructor in Stage 3. Commit
the asset conversion; do not introduce an old-format reader to test it here.
**Completion marker:** Stage 1 complete. All seven registered prompt assets
use keyed prompt/version mappings; parsed definitions were structurally checked
against their prior content, and `git diff --check` passed.
## Stage 2 — Migrate Operator Profiles, Example Roots, and YAML Fixtures
**Primary files:** `examples/profiles/`, the three maintained configuration
examples, and profile-producing test fixtures in
`internal/framework/llm/{runtime_inspection,adapter,adapter_capacity}_test.go`,
`internal/framework/proposal_generation/adapter_integration_test.go`,
`internal/framework/validators/llm_integration_test.go`,
`internal/framework/runner/cancellation_test.go`, and
`cmd/audita/main_integration_test.go`.
1. Move local proposal/validation examples beneath `profiles/local/`; move the
four service proposal/validation and testing files beneath
`profiles/service/`. Convert each to `profiles: {<ID>: <fields>}` in readable
YAML. Keep IDs, models, positive timeouts, and controls, including explicit
zero temperature. No example needs a real credential or live model.
2. Set `profiles_dir: profiles/local` in `examples/minimal-config.yml` and
`examples/configkit-inheritance.yml`; set `profiles_dir: profiles/service`
in `examples/production-config.yml`. Preserve the testing overlay's role IDs
and the existing ConfigKit inheritance example. Select the leaf root, since
recursive discovery at their common parent would still load both groups.
3. Convert profile YAML emitted by the listed fixtures to keyed definitions.
Search for remaining old profile construction strings and removed
`base_profile` syntax, and migrate any genuine profile fixture found.
Do not rewrite transcript/proposal `id` fields or domain schema fixtures.
Keep test source names independent of profile IDs where already exercised.
4. Keep negative tests negative for their selected behavioral reason. Give
cases clean temporary layouts or replace their contribution completely
rather than leaving an unrelated invalid definition behind for later cases.
Eager validation now reads unselected final definitions too. Do not remove
such definitions merely to simulate the old lazy loader where the test is
intended to exercise eager validation.
**Verification and completion:** Review the six profiles, explicit root
selections, and changed fixture text; run `git diff --check` and format touched
Go test files. These formats become consumable in Stage 3. Commit this unit.
Current documentation links and recipes are updated together in Stage 8.
**Completion marker:** Stage 2 complete. Six maintained profile documents are
keyed and split into separately selected local/service roots; the listed Go
fixtures use keyed definitions. Touched Go files were formatted and
`git diff --check` passed. Runtime tests are deferred until Stage 3 replaces
the current consumer, as the plan allows intermediate commits not to run.
## Stage 3 — Adopt the Released Source API and Selected-Role Checks
**Primary files:** `go.mod`, `go.sum`,
`internal/framework/llm/runtime.go`, and `internal/prompts/registry_test.go`.
1. Upgrade with `go get gitea.maximumdirect.net/eric/promptkit@v0.12.0` and
reconcile the module files. Verify ConfigKit stays v0.5.0, the catalog
dependencies become `/v3` at `v3.20260930.0`, and obsolete `/v2` requirements
are removed when unused. Retain the Go minimum and unrelated selections;
introduce no local replacements.
2. Resolve `profiles_dir` exactly as the current runtime does. Obtain embedded
inputs with `pk.FilesystemInputs` and operator inputs with `pk.DirectoryInputs`.
An unavailable selected root is an operator error; an empty directory can
contribute no definitions. Do not require one profile per file, IDs matching
filenames, or profile-only top-level content.
3. Construct the agreed `pk.ConfigurationSources`: two ordered caller underlays
for embedded and operator groups, and one highest-priority application
`ck.Input` whose source is the mapping specified above. Retain
`pk.WithSchemaFS`, the supplied HTTP client, and configured transient retries.
Remove old prompt/profile/backend registration options.
4. Inspect registered prompt ID/version selections, then both selected roles.
Reject a selected role whose effective model or endpoint is empty or
whitespace-only. Retain backend registration, normalized endpoint equality,
nonblank effective `APIKeyEnv` rejection, and required-credential checks.
Preserve literal IDs; do not trim/re-key identities or require all profiles
to be complete. Populate the same role inspection map for both clients.
5. Migrate the registry test engine to `FilesystemInputs` and a programmatic
`ProfileSource` fixture in its own tier, retaining its schema filesystem.
A complete endpoint/model fixture without an application backend is valid
for this direct library test; Audita's selected-role guard belongs in its
runtime tests. Keep the embedded prompt preparation assertions scoped to
embedded prompts.
6. Remove the old `profileSourceFS` wrapper once unused. Source accounting is
completed in Stage 5. Preserve the adapter boundary; change other production
callers only to resolve actual API incompatibilities. Search for remaining
uses of removed source options and old `pk.Profile`/`pk.Backend` values.
Run `go mod tidy` after migrating construction callers and review its diff.
**Verification and completion:** Run `go test ./internal/prompts` and the
existing offline profile assembly, unsafe-selection, and absolute-directory
tests in `internal/framework/llm`. Run `git diff --check` and review the module
diff. The obsolete file-reload assertion is intentionally replaced in Stage 6;
do not restore reload behavior to make that assertion pass. Commit the runtime
API migration.
**Completion marker:** Stage 3 complete. Runtime construction now uses the
released PromptKit source API with the selected application mapping; module
selections match PromptKit v0.12.0 and both `/v3` catalogs at
`v3.20260930.0`. `go mod tidy`, prompt registry tests, offline profile
assembly, unsafe-selection, absolute-directory tests, and `git diff --check`
passed. Captured source accounting remains in Stage 5.
## Stage 4 — Preserve Startup Failure Classification and Validate Assembly
**Primary files:** `internal/framework/llm/runtime.go`,
`internal/framework/llm/runtime_inspection_test.go`, and configuration-command
tests in `internal/cli`. Update `internal/cli/run.go` only where the changed
startup path needs existing exit behavior preserved.
1. Treat embedded discovery failures as `AssetError`; treat operator-directory
discovery failures as configuration input errors. Preserve wrapped causes
for `errors.Is`/`errors.As` and retain configured-secret redaction at CLI
boundaries. Caller cancellation remains a runtime exit, not an operator
mistake, and should not trigger further diagnostic loading.
2. PromptKit wraps both definition-loading and template/content failures in
`ErrInvalidConfig`; some materialization errors have no public source/path
type. Do not classify by parsing error strings or by that sentinel alone.
On final engine-construction failure, perform an embedded-only diagnostic
construction with the same embedded prompt/schema sources and library
defaults, excluding operator inputs and application backend mappings. If
that also fails, surface an `AssetError` with the asset cause; otherwise
return the original final-construction error as an operator/configuration
error. The diagnostic engine does no provider work and is never retained
for execution. Keep a small unexported assembly seam accepting an embedded
filesystem so tests can exercise asset failures without modifying shipped
files or creating a global test hook.
3. Do not run this diagnostic construction as a successful-path preflight.
A valid final snapshot remains accepted even if an operator contribution
repairs an otherwise invalid lower-tier prompt. Verify that property with
the same small assembly test seam. This avoids strengthening upstream
final-value validation into mandatory validation of displaced definitions.
4. Cover incomplete selected profiles using omitted, empty, and null model
contributions and a cleared selected endpoint. Exercise proposal and
validation selection, including omission-derived validation selection, with
a rejecting HTTP transport. Confirm valid unselected partial profiles are
accepted; malformed unselected definitions, undefined backend references,
and conflicting directory contributions still fail under native rules.
5. Add focused boundary coverage that invalid operator YAML/template/resource
input produces configuration exit `2`, and test the asset classification
directly at the runtime seam. Reuse existing CLI handling of `AssetError`
for exit `1`; no damaged-binary subprocess fixture is needed. Cover the
shared `NewRuntime` validation route and preserve cancellation exit `1` in
configuration commands if the new error path would otherwise classify it
as `2`. Avoid exact complete diagnostic wording assertions.
**Verification and completion:** Run targeted startup/inspection tests and
`go test ./internal/cli`; check whitespace and commit. Preserve the original
failure cause where available rather than flattening it to a generic error.
**Completion marker:** Stage 4 complete. Embedded discovery and construction
failures are distinguished from operator inputs through an embedded-only
diagnostic construction; cancellation skips that diagnostic. Selected-profile
completeness and native directory validation are covered, and both config
commands preserve exit 1 for cancellation/assets and exit 2 for invalid
operator YAML, templates, and resources. Targeted runtime tests,
`go test ./internal/cli`, and `git diff --check` passed.
## Stage 5 — Protect Consumed Configuration and Message Files
**Primary files:** `internal/framework/llm/runtime.go` or a focused source-path
helper in that package, `internal/cli/process.go`, runtime source-accounting
tests, and `cmd/audita/main_integration_test.go`.
1. Rename the internal `Runtime.ProfilePaths` field to `SourcePaths` and update
its process caller. It now covers consumed host YAML and message resources,
rather than profiles alone. Populate it from the successfully captured final
engine; do not expose source metadata as a new public output.
2. Call `InspectConfiguration`. Index `Sources()` by source ID, and collect
paths with `Kind == ck.SourceKindFile`; embedded filesystem and synthetic
sources are not host paths. All loaded host YAML counts, including unused
definitions and files whose contributions were later overlaid. Deduplicate
paths and sort for a stable internal projection.
3. For message resources, enumerate `Metadata().HistoricalPaths()` and retain
effective, nonnull paths shaped as
`/prompts/<id>/<version>/messages/<index>/content_file`. Use `Lookup` and the
effective field's origin, not an inherited destination's guessed source.
PromptKit successfully captures these fields only with a unique usable
source context. Obtain the effective scalar from `Explain(path).Effective`
and decode that scalar YAML into a string with the existing YAML dependency.
This decodes captured inspection data; it is not a second configuration
loader. When the contributing source is a host file, join the untrimmed
resource name against that source file's directory and add it to the ledger.
Ignore displaced references and non-host resources. Do not reread operator
YAML, infer IDs from filenames, or reimplement resource-root validation.
4. Pass `SourcePaths` through existing `diagnostics.PrepareOutput` protection.
Leave canonicalization, symlink/hardlink checks, permitted in-place transcript
handling, and output initialization with their current owner. Do not protect
every unrelated file in the selected directory or add filesystem mutation
guarantees beyond the existing collision contract.
5. Extend `TestSubprocessProfileSources` and a focused runtime case to cover an
arbitrarily named YAML contribution, a consumed source-relative message
file, and conflicting transcript/report destinations. Use a non-YAML message
filename, such as `instruction.md`, so directory discovery does not mistake
plain prompt text for a configuration document. Include an inherited message
reference from a different directory and a prompt ID/version with a JSON
Pointer escape character so source provenance and literal keys determine
the path. Check rejection before provider dispatch and unchanged input bytes.
Preserve acceptance of an unused alternate output filename; do not turn the
whole directory into a forbidden output location.
**Verification and completion:** Run focused source-ledger tests and subprocess
collision cases, plus `git diff --check`, then commit. Check that resource
accounting uses the frozen snapshot and never changes the engine's source or
resource-resolution behavior to facilitate tracking.
**Completion marker:** Stage 5 complete. `Runtime.SourcePaths` now projects
host YAML and effective source-relative message files from PromptKit's captured
configuration inspection, using field provenance for inherited references.
Process output protection consumes that ledger. Focused source-accounting and
subprocess collision tests passed, including unchanged input bytes, no provider
dispatch, JSON Pointer-escaped keys, and acceptance of an unused alternate
output; `git diff --check` passed.
## Stage 6 — Verify Operator Prompt Overrides and Captured Execution
**Primary files:** `internal/framework/llm/adapter_test.go` and focused runtime
tests. Change `adapter.go` only for an actual integration defect.
1. Add a native keyed operator contribution overriding messages for an active
prompt/version. Use the real runtime and controlled HTTP responses to prove
the operator text reaches both the prepared snapshot and dispatched request.
One case deliberately omits shared-variable references and still succeeds;
do not require Audita to inject their text. A reference-bearing case renders
the adapter-supplied value and retains existing protection against caller
variables replacing the reserved shared variables.
2. Override the prompt's definition-level output contract with another valid
upstream contract. Prove that Audita's per-call JSON schema and configured
repair budget still govern execution: a structurally invalid response is
repaired or exhausted as the controlled budget dictates. Domain validators
remain downstream. Reuse existing validator/proposal integration coverage
for their rules rather than weakening them to fit an operator prompt.
3. Replace the reload portion of
`TestPreparedAdapterRejectsTargetDriftAndPreparationErrors`. After successful
construction, change a profile or message file and verify the existing
runtime still uses its captured settings/text. Construct a new runtime to
observe a valid edit or reject a newly mismatched endpoint. Keep the missing
prompt preparation failure case.
4. Retain a focused test of the prepared-target check independently of reload:
deliberately change the test runtime's application backend endpoint after
construction and verify the inconsistency is blocked before dispatch. This
tests the existing guard, not a supported live mutation/reload feature.
Continue executing the same prepared handle whose details were inspected;
do not add another preparation or an application-side retry loop.
**Verification and completion:** Run the prompt override, snapshot, and
prepared-adapter tests. Compare semantic messages and selected targets, not old
hash values or exact YAML formatting. Check whitespace and commit.
**Completion marker:** Stage 6 complete. Native operator messages reach both
the prepared snapshot and provider request, with and without shared-variable
references. The adapter's per-call schema and repair budget remain effective
under a valid operator output-contract override. Existing runtimes retain
captured prompt/profile values after source edits, and a new runtime observes
valid edits; the target guard and preparation failure remain covered. The
`internal/framework/llm` tests and `git diff --check` passed.
## Stage 7 — Verify Native Profiles, Backend Authority, and Resilience
**Primary files:** Runtime inspection and adapter tests,
`internal/framework/llm/adapter_capacity_test.go`, and existing framework/CLI
integration tests where their established behavior is affected.
1. Add a compact native profile inheritance fixture with literal keyed IDs
unrelated to filenames, multiple definitions in one file, a partial parent,
and a complete selected child. Verify inherited controls survive a child
model change and child overrides win. Reuse the metadata-facing literal-key
coverage from Stage 5 rather than repeating it in profile tests.
2. Use controlled HTTP request bodies to distinguish explicit zero temperature
from omission and inherited-control suppression by null. Check a
representative recursively overlaid provider parameter and null clearing;
do not promise that all providers accept every valid configured zero.
Reuse existing deadline/cancellation coverage for unchanged outer deadlines;
explicit zero profile timeout must not disable the operation context.
3. Verify an operator backend contribution can add supported provider fields,
while a conflicting endpoint/concurrency/queue/environment contribution
cannot displace the final application fields. Preserve an explicit profile
environment lookup as a selected-role error rather than hiding it with an
adapter override. Include an application backend using a catalog ID to show
it is allowed and its catalog credential lookup is suppressed. Use synthetic
environment values and local HTTP fixtures, not provider catalog endpoints.
4. Reuse `TestRolesShareBackendCapacityAndReleaseAfterCancellation` and
`TestDistinctBackendsHaveIndependentCapacity`; verify the final mapped
limits, including zero queue capacity, still coordinate both roles and
remain backend-local. Strengthen a fixture with conflicting lower-tier
capacity only where needed to prove the new mapping wins. Keep existing
synchronization; do not infer admission with sleeps or add another limiter.
5. Preserve offline selected-profile inspection and value-free application
explanations. Check configured secrets, endpoint strings, arbitrary provider
fields, and prompt text remain outside the current safe profile summary.
Do not replace it with a full upstream dump. Its numeric projection is not
proof that an omitted provider setting is sent; wire presence belongs to
the HTTP coverage above.
6. Run the existing transient retry, structural repair, attempt-count,
observation, partial-validation, and all-model-failure subprocess cases
after fixture migration. Preserve their error/recovery/debug classifications,
cancellation, and continued output. Fix proven integration regressions;
do not rewrite domain outcomes or reduce budgets to obtain a passing suite.
1. Add `gitea.maximumdirect.net/eric/ReleaseKit@v0.1.0` with `go get`. Import its
public `buildinfo` package in the CLI and run `go mod tidy` after the import
is present. Review the dependency diff for unrelated upgrades or replacements.
2. Add `version` dispatch to the existing CLI. With no arguments, print
`audita ` followed by `buildinfo.Version()` and a newline to stdout, returning
zero. Run this route independently of application configuration and runtime
assembly. Use the existing output streams and standard-library parsing style.
3. Add the command to root help. Support `help version` and the usual
`version -h`/`version --help` help behavior with stdout and status zero.
Unexpected positional arguments or unknown flags use the existing usage-error
status two. No new version flags are needed at the root level.
4. Add compact CLI coverage for successful version output, help, and malformed
invocation. Derive the expected version from the public accessor rather than
hardcoding `dev`. Exercise the version route from a temporary working directory
without application setup and verify stderr stays empty on success. Preserve
existing commands and help behavior.
**Verification and completion:** Run
`go test ./internal/framework/llm ./internal/framework/proposal_generation ./internal/framework/validators ./internal/framework/runner ./internal/cli ./cmd/audita`
and `git diff --check`. Review whether each new test addresses a distinct changed
boundary. Commit the native-profile and runtime verification unit.
`go test ./internal/cli ./cmd/audita`, build the application to ignored
`bin/audita`, and run its `version` command. Run `git diff --check`, review the
module diff, and commit. Real release-version injection is verified in Stage 5;
no test should hardcode an upstream private linker symbol.
**Completion marker:** Stage 7 complete. Native profile inheritance, control
presence, provider-map overlays, application backend authority, selected
credential rejection, catalog-ID handling, shared backend capacity, and safe
inspection behavior are covered. The six targeted packages passed, along with
focused wire, timeout, capacity, credential, and safe-view tests; `git diff
--check` passed.
**Completed 2026-10-07:** Added ReleaseKit v0.1.0 and the configuration-free
`audita version` command with root/help routing and usage-error handling.
Checks passed: `go test ./internal/cli ./cmd/audita`,
`go build -o ./bin/audita ./cmd/audita`, `./bin/audita version`, and
`git diff --check`.
## Stage 8 — Document the Migration and Update Attribution
## Stage 2 — Introduce the Product Profile and Thin Local Wrapper [x]
**Primary files:** `docs/config.md`, `docs/internal/llm-runtime.md`,
`docs/internal/prompts.md`, `docs/integrations/openai-compatible-llm.md`,
`docs/operations.md`, `docs/internal/cli-config.md`, `README.md`, and
`docs/oss/attribution.md`.
**Primary files:** new `releasekit.json`, `scripts/release.sh`, and
`scripts/release_test.go`.
1. Update the configuration contract for unified `profiles_dir` content, exact
keyed identities, directory composition, agreed tier precedence, profile
inheritance/presence, selected-role completeness, eager validation of all
final definitions, selected-root failures, and construction-time capture.
Replace the obsolete statement that PromptKit profiles do not gain ConfigKit
inheritance. Retain application overlays, defaults, and credential ownership.
2. Include concise migration instructions in the profile section: wrap old
profile fields beneath `profiles/<id>`, remove inner `id`, change
`base_profile` to `extends`, review explicit values/nulls, and select roots
whose final definitions have valid backend references. Show a small keyed
inheritance/override snippet and link complete maintained examples. Explain
explicit zero temperature versus omitted or null controls and link upstream
for the full field table. State that this ships as an Audita major migration;
do not invent an exact release version or automatic file conversion.
3. Document intentional prompt overrides and optional shared-variable references.
Supplying shared variables does not inject omitted text, while per-call
schemas and domain validators remain enforced. Internal prompt documentation
distinguishes shipped asset instructions from operator-effective messages;
no prompt-quality rewrite or new global prompt-policy requirement is added.
4. Describe layered runtime assembly, source accounting, snapshot use, and
startup classification in their internal owner. Update the pinned PromptKit
consumer link in the LLM integration contract. Expand operations' loaded
source description to include consumed operator message files; keep physical
output/collision rules with operations and diagnostics.
5. Repair example navigation for separate local/service roots, especially the
README's local-profile link. Update validation recipes to use the maintained
config files' explicit roots and keep the service testing overlay workflow.
Review other affected links without rewriting historical release notes.
6. Update the PromptKit and both catalog attribution entries to selected versions
and exact commit-pinned source/notice URLs. Resolve catalog tag commits from
the selected module origins or authoritative upstream refs, not annotated
tag object IDs. Check the releases' COPYRIGHT/LICENSE files and retain the
applicable unchanged local LGPL/GPL and MIT texts. Keep the existing House
Format; no new license selection, policy rewrite, or attribution inventory
is needed. Review `go.mod`/`go.sum` against those entries.
1. Write a JSON profile with these exact product settings:
**Verification and completion:** Check affected local links/anchors, format
examples against the implemented loader, verify source revisions/notices, and
run `git diff --check`. Commit the documentation and attribution unit.
| Field | Value |
| --- | --- |
| `product` | `Audita` |
| `remote` | `origin` |
| `build.main_package` | `./cmd/audita` |
| `build.binary_name` | `audita` |
| `build.targets` | `linux/amd64`, `linux/arm64` |
| `delivery.base_url` | `https://gitea.maximumdirect.net` |
| `delivery.owner` | `eric` |
| `delivery.repository` | `audita` |
| `delivery.token_env` | `RELEASEKIT_GITEA_TOKEN` |
**Completion marker:** Stage 8 complete. Configuration, runtime, prompt,
integration, operations, contributor, and README guidance now reflects native
PromptKit layering, migration, presence, snapshot, and separate example-root
behavior. Attribution matches PromptKit v0.12.0 and both v3 catalog module
origins and notices. Local links/anchors and `git diff --check` passed.
## Stage 9 — Validate the Integrated End State and Close the Plan
**Primary files:** This plan and the feature roadmap; correct an implementation
file only if the final checks reveal an actual gap.
1. Verify the full roadmap acceptance criteria against the landed code and the
distinct tests from these stages. Review for unintended requirements:
profile-only filtering, mandatory shared text in operator messages,
filename-derived IDs, complete unselected profiles, catalog ID bans,
successful-path asset preflights, compatibility readers, and new output
guarantees are not part of the accepted target.
2. Run `go build -o ./bin/audita ./cmd/audita` and `go test ./...`. Keep the
generated binary out of commits. Reuse successful checks on unchanged
content; rerun checks affected by any corrective code changes.
3. Validate the maintained examples offline using the built binary:
Omit `checks`. Store no token value and use the upstream profile format
directly; Audita does not need another profile parser in production.
2. Replace the wrapper body with straightforward Bash using `set -euo pipefail`:
select the repository directory relative to the script, create `bin/`, build
the selected CLI, and execute it with the original argument vector. The
build command is:
```sh
./bin/audita config validate --config examples/minimal-config.yml
./bin/audita config validate --config examples/configkit-inheritance.yml
AUDITA_SERVICE_KEY=synthetic-example ./bin/audita config validate --config examples/production-config.yml
AUDITA_SERVICE_KEY=synthetic-example ./bin/audita config validate --config examples/production-config.yml --overlay testing
./bin/audita config print-effective --config examples/minimal-config.yml --view profiles
./bin/audita config print-effective --config examples/configkit-inheritance.yml --view explain
GOWORK=off GOTOOLCHAIN=local go build -mod=readonly \
-o ./bin/releasekit gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit
```
Confirm local/service roots and testing selections work without provider
dispatch. Check safe views exclude resolved secrets and preserve the
established explanation fields. Do not call a model to validate examples.
4. Check the selected dependency graph, attribution, affected links, and final
diff. Run `git diff --check`. Confirm only the released modules are used and
no obsolete `/v2` catalog or removed construction API remains in active code.
Historical release documents and historical roadmap links may retain old
versions as accurate records.
5. Mark this plan and the feature roadmap complete only after the full target
passes. Record the tested implementation revision and checks accurately;
distinguish a subsequent documentation-only completion commit. Commit the
completion documentation and report remaining work, if any. Do not select
a release number, publish, tag, push, poll CI, or add permanent release gates.
Finish with `exec ./bin/releasekit "$@"`. Preserve argument boundaries and
build failures. Existing `bin/` ignore rules suffice. Let ReleaseKit parse
its commands and report errors; remove all local note, version, Git, test,
build-target, and publication mechanics from the wrapper.
3. Replace `TestReleaseWorkflow`'s old fourteen-scenario matrix. Use a temporary
layout with the real wrapper and a stub at the Go/tool boundary to verify
repository selection, forwarded native arguments (including an argument with
spaces), and returned tool status. Include uncertain publication status three
and a failed bootstrap that never executes the tool. The stub writes a small
executable at the requested output path; assert effects and arguments without
tying tests to harmless build-flag ordering or a particular shell layout.
4. Add a focused check of the committed JSON profile's product/build/delivery
facts and absence of additional checks or credential values. Standard JSON
decoding in test code is sufficient. Keep this coverage alongside wrapper
tests; compare target membership rather than incidental list order. Avoid
reproducing upstream profile validation or remote guard tests.
5. Keep `scripts/build-release.sh` until Stage 3 removes its CI caller. The new
wrapper already uses ReleaseKit's selected build specification.
**Completion marker:** Stage 9 complete. The tested implementation revision is
`5525fe8`; the preceding documentation migration commit is `9bcf183`, and this
roadmap closure is documentation-only. At `9bcf183`, the application built,
`go test ./...` passed, all six maintained example commands passed offline,
`go mod verify` passed, the selected module graph contained PromptKit v0.12.0
and both `/v3` catalogs at `v3.20260930.0` with no `/v2` catalogs, and
`git diff --check` passed. The active Go/module scan found no removed PromptKit
construction options.
**Verification and completion:** Run `go test ./scripts`, then
`bash scripts/release.sh version` to verify real tool bootstrap and execution.
This prints the tool's version, not Audita's. Check ignored build output and
`git diff --check`, then commit. No publication or delivery invocation against
an external service is needed.
**End state:** Native PromptKit configuration and catalogs are adopted,
operator overrides and captured resources work through the real runtime,
application backend/domain ownership and resilience remain effective, and
maintained examples and migration documentation match the implementation.
No implementation-blocking decisions remain; exact major-release numbering
and publication timing remain outside this implementation unit.
**Completed 2026-10-07:** Added the Audita profile and replaced the local
publication workflow with a wrapper that builds and execs ReleaseKit. Replaced
the publication test matrix with focused wrapper and profile checks. Checks
passed: `go test ./scripts`, `bash scripts/release.sh version`, ignored-output
verification for `bin/releasekit`, and `git diff --check`.
## Stage 3 — Migrate Woodpecker Delivery and Retire the Builder [x]
**Primary files:** `.woodpecker/release.yml` and deletion of
`scripts/build-release.sh`.
1. Adapt the maintained upstream
[tag-delivery template](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.1.0/examples/woodpecker/tag-delivery.yml).
Use its matrix and single `deliver` step with `GO_IMAGE: golang:1.25.5` and
`RELEASEKIT_VERSION: v0.1.0`. Select tag events under `refs/tags/v*`.
2. Map the command environment variable `RELEASEKIT_GITEA_TOKEN` from the
existing Woodpecker secret `GITEA_RELEASE_TOKEN`. Use the existing credential;
no new secret name or credential provisioning is selected by this migration.
3. Use the template's command, preserving Woodpecker interpolation escaping:
```sh
go run gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit@${RELEASEKIT_VERSION} ci-deliver \
--repo "$${CI_WORKSPACE}" \
--commit "$${CI_COMMIT_SHA}" \
"$${CI_COMMIT_TAG}"
```
ReleaseKit owns fetch/checkout verification, dependency preparation, builds,
note-page creation, manifests, and uploads. Remove the separate build step,
release plugin, wildcard asset settings, and local checksum configuration.
Managed delivery gets its targets and destination from `releasekit.json`.
4. Delete `scripts/build-release.sh` after confirming its active caller is gone.
Search remaining script/CI references and remove only obsolete active
invocations. Historical release records retain their original descriptions.
5. Review the parsed workflow against the template: selected image/pin, tag
trigger, token mapping, and event arguments. Use the existing YAML dependency
or available tooling for local syntax inspection. Confirm the tool pin matches
the selected module. This is adoption validation; it does not require a new
permanent workflow-text snapshot test or a running Woodpecker instance.
**Verification and completion:** Run `go test ./scripts`, review the workflow
and profile together, and run `git diff --check`. Commit the delivery migration.
The existing Go image must provide compatible Go and Git; real runner execution
is observed during subsequent delivery, independently of source completion.
**Completed 2026-10-07:** Migrated Woodpecker to the pinned managed
`ci-deliver` step using the existing secret, then removed the obsolete binary
builder. Verified the parsed workflow settings and command against the pinned
template, confirmed the module/workflow pins match, ran `go test ./scripts`,
confirmed no active CI or script caller remained, and passed `git diff --check`.
## Stage 4 — Align Policy, Documentation, and Attribution [x]
**Primary files:** `docs/release.md`, `docs/policy/release.md`, `docs/cli.md`,
`docs/internal/cli-config.md`, `docs/adr/README.md`, and
`docs/oss/attribution.md`. Update other current references only where affected.
1. Rewrite the runbook around dependency preparation, a committed candidate,
`git push origin main`, and `bash scripts/release.sh publish VERSION`.
Explain that remote `main` must identify the captured candidate exactly.
`bash scripts/release.sh check VERSION` is an optional local diagnostic,
never a required pre-publication step. Document preparation with
`GOWORK=off GOTOOLCHAIN=local go mod download`; if it changes module files,
those changes belong in the candidate before publication.
2. Describe annotated notes-derived tags, native publication statuses, and
recovery against the captured authoritative endpoint. State that the tool
retains a local tag after push failure. An exact-object recovery can confirm
success; an unreadable result remains unknown. Link to pinned upstream
recovery guidance instead of implementing or repeating its full algorithm.
Remove the old atomic-push prerequisite and procedure.
3. Describe the shared tagged-source delivery command, the existing secret
mapping, Linux artifacts, note body/footer, and separate delivery status.
Explain content-aware retries and retained-artifact recovery by linking to
upstream. Remove references to the retired builder/plugin and the assertion
that Audita has no version command. Keep the release script's bootstrap
preparation distinct from its offline candidate validation.
4. In the release policy, retain upstream wording wherever it already permits
the selected approach. Add a concise project-specific native-host check
provision under supported platforms/candidate validation: Linux delivery
targets remain required, macOS development remains best effort, and running
publication on a nonselected host requires ReleaseKit's temporary native
version check to succeed. Qualify the existing statement that macOS failures
never block release accordingly. Mac access and native tests on every target
remain unnecessary. Date the local policy update and preserve its upstream
source attribution. Avoid unrelated policy changes or additional gates.
5. Document `audita version`, its stdout/help/usage behavior, and public version
fallback in `docs/cli.md`. Add a short implementation/change-recipe note in
`docs/internal/cli-config.md` linking to that contract and the accepted ADR.
Update the ADR index to list ADR 0001; its current assertion that no ADR exists
is obsolete. Preserve the accepted ADR's content.
6. Add a House Format ReleaseKit entry with selected module v0.1.0,
`Copyright (C) 2026 Eric Rakestraw`, and `LGPL-3.0-or-later`. Link existing
`licenses/LGPL-3.0.txt` and `licenses/GPL-3.0.txt`, and the upstream COPYRIGHT
and source at commit `5e60fe3113c69f95c42fbbe32a8ca4b51f5b83c1`. Check the
selected module's notices. Keep the application grant and unchanged license
texts intact; no policy-template material is copied in this feature.
7. Repair current navigation affected by replacing this implementation plan.
Completed PromptKit/ConfigKit roadmap references that intend to identify
their former plan should link to the corresponding historical completion
records, not this ReleaseKit plan. Preserve published release notes and tags.
The next release note is deferred to release preparation.
**Verification and completion:** Check commands against the new wrapper/CLI,
local links and anchors, pinned upstream references, source attribution, and
`git diff --check`. Use the prior successful code checks for unchanged code.
Commit the documentation unit.
**Completed 2026-10-07:** Updated the ReleaseKit runbook, release policy,
version contract and contributor guidance, ADR index, and component attribution.
Repaired PromptKit and ConfigKit links to their historical implementation
records. Verified local links and anchors, release CLI and version commands,
pinned module/notices, source attribution, and `git diff --check`.
## Stage 5 — Verify Adoption and Close the Plan [x]
**Primary files:** This plan and the ReleaseKit feature roadmap; fix an
implementation file only if the final checks expose a real integration defect.
1. Audit the roadmap acceptance criteria against the landed files. Confirm the
runtime uses the public version accessor, the profile selects only the two
Linux targets, pins agree, the wrapper owns no common release mechanics, and
the old builder and repeated publication tests have been retired.
2. Prepare dependencies if necessary, then run `go test ./...` and
`go build -o ./bin/audita ./cmd/audita`. Run the normal binary's `version`
command without application setup. These tests remain offline; preparation
may use the module network separately.
3. On the committed implementation, verify the real shared builder once using
temporary output outside the repository:
```sh
releasekit_check_dir=$(mktemp -d)
bash scripts/release.sh build --output "$releasekit_check_dir" v0.0.0
```
`v0.0.0` is solely a build-test identifier, not an Audita release selection.
`build` requires no release note or remote publication. Confirm both
`audita-v0.0.0-linux-amd64` and `audita-v0.0.0-linux-arm64` are produced;
ReleaseKit's own mandatory native smoke check must report success. It checks
`audita v0.0.0` with a newline on a host-runnable executable, building a
temporary native executable itself when needed. Do not execute a foreign
architecture binary. Remove the temporary output afterward. The CLI's private
build receipt is an upstream artifact and should not enter Git.
4. Review the dependency/attribution diff, wrapper/profile wiring evidence,
workflow syntax and pin, and current documentation. Run `git diff --check`
and confirm no generated output, credentials, or unrelated work is staged.
Fix proven defects and rerun only affected checks. No real tag publication,
hosted upload, paid inference, extra platform matrix, or CI polling is needed
to establish this implementation's completion.
5. Mark the plan and feature roadmap complete after these requirements pass.
Record the tested implementation revision and checks; distinguish a later
documentation-only closure commit. Summarize the local infrastructure removed
and any remaining deployment observations accurately. Commit the closure.
The first adopting release and live delivery remain separate authorized work.
**End state:** Audita supplies a product profile, native version command, thin
local entry point, and shared CI invocation. ReleaseKit owns the common release
mechanics and their tests. Documentation and attribution match the integration.
No implementation-blocking open questions remain.
**Completed 2026-10-07:** Audited the adoption criteria against implementation
revision `5ac704c4c42fd23ed94b1f7351491ad580726db1`. Checks passed:
`go test ./...`; `go build -o ./bin/audita ./cmd/audita` followed by the binary's
`version` and `--help` commands; ReleaseKit's real `build` command produced
`audita-v0.0.0-linux-amd64` and `audita-v0.0.0-linux-arm64` and passed its
native version smoke check; profile, module/tool pins, wrapper, and parsed
Woodpecker workflow matched the selected integration; and `git diff --check`.
The closure record is a documentation-only commit after the tested
implementation revision. Audita's local build and publication mechanics and
their repeated-mechanics test matrix have been removed in favor of ReleaseKit.
The first adopting release and live Woodpecker/Gitea delivery observation
remain separate work.

View File

@@ -262,8 +262,8 @@ No live provider calls, new release gates, or CI-success dependency are needed
to establish adoption. Guided domain recovery and the other work recorded in
[future.md](future.md) remain separate features. Do not expand this upgrade into
new report formats, pipeline scheduling, or general configuration-inspection
commands. The [implementation plan](implementation.md) owns execution steps
and their verification.
commands. The [historical implementation plan](https://gitea.maximumdirect.net/eric/audita/src/commit/418da456d75a68d3bb5a43ab823c5b2b434b90cc/docs/roadmap/implementation.md)
records that work's execution steps and verification.
## Completion Record

216
docs/roadmap/releasekit.md Normal file
View File

@@ -0,0 +1,216 @@
# ReleaseKit Adoption Roadmap
**Last updated:** 2026-10-07
**Status:** Implementation complete; first adopting release and live delivery
observation pending.
## Purpose and Original Baseline
Implement the decision in [ADR 0001](../adr/0001-releasekit.md): standardize
Audita's release process on ReleaseKit to improve maintainability and reduce
locally owned release-focused code. Those benefits outweigh the small advantages
of retaining Audita's custom procedure. The workflow changes required by the
shared standard are accepted.
At Audita revision `6de470b53d185b10327ec4deba23f5a84e606e1f`, v3.0.0 is
released. Audita owns `scripts/release.sh`, `scripts/build-release.sh`, and
`scripts/release_test.go`, totaling 207 lines, plus the 26-line Woodpecker delivery
workflow. Publication validates a clean candidate, cross-builds Linux binaries,
and atomically pushes `main` and a lightweight tag. Woodpecker builds artifacts
and uses a release plugin to publish the note, binaries, and checksums. Audita
has no runtime version command.
The adoption baseline is ReleaseKit v0.1.0, commit
`5e60fe3113c69f95c42fbbe32a8ca4b51f5b83c1`. Its public library, executable,
`buildinfo` package, and maintained Woodpecker integration cover the required
mechanics. The upstream test suite passes; its documentation records live
Woodpecker/Gitea verification as remaining deployment work.
## Current Baseline
The adoption implementation is now present: `go.mod` selects ReleaseKit
v0.1.0, `releasekit.json` supplies Audita's product and delivery facts,
`scripts/release.sh` builds and forwards arguments to its executable, and
`.woodpecker/release.yml` invokes the same pinned version for tag delivery.
The application exposes `audita version` through `buildinfo`; the custom
release builder has been removed. The sections below retain the intended
adoption scope rather than describing work that still needs implementation.
The [adoption review](remediation.md) records focused CLI and wiring-test
findings and their resolution status. Remaining implementation corrections
belong there. First-release delivery observation remains deployment work,
separate from source-release completion.
## Target End State
- ReleaseKit owns candidate validation, source publication, version injection,
selected-target builds, checksum generation, and hosted delivery. Audita owns
its product facts, compatibility decisions, release notes, and necessary
product-specific checks.
- A small `releasekit.json` declares product `Audita`, remote `origin`, main
package `./cmd/audita`, binary name `audita`, and delivery targets
`linux/amd64` and `linux/arm64`. Its Gitea destination identifies
`https://gitea.maximumdirect.net`, owner `eric`, and repository `audita`.
- Maintainers commit the candidate and publish it to authoritative `main`
before invoking ReleaseKit publication. ReleaseKit validates that candidate,
creates an annotated tag from the committed note, and pushes its exact tag
object. Confirmed source publication completes the release independently of
CI and artifact delivery.
- One captured candidate receives ReleaseKit's standard offline test suite and
selected build verification. Audita's profile introduces no additional checks
without a concrete product need. The optional local check remains a diagnostic
operation rather than a prerequisite that duplicates publication validation.
- `audita version` reports `audita <version>` through ReleaseKit's public
`buildinfo.Version()` accessor. Ordinary development builds use its native
fallback behavior, including `dev` when release information is unavailable.
- Woodpecker invokes the pinned shared `ci-deliver` command. Hosted notes,
Linux assets, and `SHA256SUMS` retain their current identities; delivery
compares existing content, adds missing assets, and reports conflicts and
uncertainty separately from source publication.
- Audita's release code consists of thin tool invocation and product wiring,
with focused integration coverage. Common mechanics and their repeated tests
reside upstream.
## Accepted Standardization Choices
Adopt ReleaseKit's main-first publication and annotated tags, replacing the
atomic branch/tag push and lightweight tags for new releases. Existing released
tags, notes, pages, and assets retain their historical contents.
Adopt its standard build settings and mandatory native version smoke check.
The check verifies that version injection reaches the executable. On a host
outside the selected Linux targets, ReleaseKit can build a temporary native
binary; publication from that host depends on that check succeeding. This is an
accepted workflow change, including for macOS invocations. Linux remains the
production delivery target, and macOS remains a best-effort development platform.
Mac access and macOS artifact delivery are not release prerequisites.
Use the shared managed build-and-deliver operation for ordinary CI delivery.
ReleaseKit's retained-artifact workflow remains available when a delivery retry
needs the original bytes. Follow its content-aware recovery behavior rather
than reproducing that machinery in Audita.
## Adoption Scope
### Dependency, Profile, and Tool Entry Point
Select the released v0.1.0 module in `go.mod` and `go.sum`. The application uses
the lightweight `buildinfo` facade; release orchestration is exercised through
the upstream executable. The sibling checkout is review material, not a
production dependency replacement.
Add the product profile described above. Leave additional checks empty and keep
delivery tokens out of the profile. Use `RELEASEKIT_GITEA_TOKEN` as the token
environment variable, supplied from the existing Woodpecker
`GITEA_RELEASE_TOKEN` secret.
Reduce `scripts/release.sh` to repository selection, building the CLI selected
by Audita's module graph into ignored `bin/` output, and invoking it directly.
Forward ReleaseKit's native commands and arguments and preserve its exit status,
including uncertain publication status. Use the same released tool version in
the module graph and CI pin. Dependency/tool preparation belongs before offline
candidate validation.
### Runtime Version Reporting
Add `version` to the existing CLI dispatch and help, with output
`audita <version>` followed by a newline and exit status zero. This operation
works without loading application configuration, resolving LLM credentials, or
contacting a provider. Use the public accessor and let ReleaseKit own injection
mechanics. Keep the release orchestration dependency outside transcript
processing.
### Publication and Binary Delivery
Replace the local publication and cross-build mechanics with ReleaseKit's
commands and profile. Retire `scripts/build-release.sh` when its callers use
the shared builder.
Replace the current Woodpecker build/plugin sequence with the maintained
tag-delivery integration, pinned to v0.1.0. Retain a compatible Go 1.25.5 image
with Git, the existing secret, and only the selected Linux targets. Pass the
workspace, event commit, and version to `ci-deliver`; the shared command owns
tagged-checkout verification, dependency preparation, build, and delivery.
The declared artifacts remain `audita-<version>-linux-amd64`,
`audita-<version>-linux-arm64`, and `SHA256SUMS`. The committed release note
supplies the hosted body and required licensing footer. Delivery status and
recovery follow the upstream operation; source publication does not poll CI or
wait for assets.
### Tests and Removal of Duplicated Mechanics
Replace the repeated release-mechanics matrix in `scripts/release_test.go`
with proportionate coverage of Audita's wiring. Protect the new version command
and wrapper/profile integration where these introduce distinct consumer risks.
Verify actual version injection and the selected Linux builds through the real
builder during adoption validation. Use local Git/HTTP fixtures and controlled
command boundaries where needed; keep the default suite offline.
Reuse ReleaseKit's coverage for common Git guards, push reconciliation,
checksums, and delivery retries. Evaluate the reduction in local machinery from
the resulting implementation rather than imposing a numerical deletion target.
### Documentation and Attribution
Update `docs/release.md` for main-first preparation, native ReleaseKit commands,
annotated tags, dependency preparation, delivery status, and failure recovery.
Align `docs/policy/release.md` where necessary to describe the deliberately
accepted native-host check while retaining the existing production targets and
independent source-completion rule.
Update `docs/cli.md`, relevant internal CLI guidance, and the ADR index for the
version command and accepted decision. Keep the ADR's decision content intact.
Record the adoption and maintainer workflow change in the next release note
when that release is prepared.
Add ReleaseKit's selected source and LGPL-3.0-or-later notices to
`docs/oss/attribution.md` under the existing OSS policy. Reuse applicable license
texts already present. Incorporating `buildinfo` does not change Audita's
application license.
## Verification and Completion
Adoption is complete when:
- The released dependency and pinned tool are selected consistently, and the
application builds with its default offline test suite passing.
- Version output works without LLM setup and reports the injected version from
a ReleaseKit-built executable.
- The native profile and thin wrapper drive candidate checks and publication
without Audita retaining common publication or build mechanics.
- Both required Linux targets build through ReleaseKit, and the CI workflow
declares the intended destination, artifacts, note body, and secret mapping.
- Focused consumer coverage exercises the changed boundaries, and redundant
mechanics tests and the old builder have been retired.
- Current documentation and attribution describe the adopted behavior.
## Remaining Release and Deployment Work
Prepare the first adopting release under the [release procedure](../release.md),
including a release note explaining the maintainer workflow change. Resolve
outstanding adoption-review findings through the linked remediation record.
Version selection and publication timing remain release-preparation decisions.
Offline integration evidence and live delivery observations are distinct.
Observe the shared workflow during the first adopting release and address any
deployment failures through ReleaseKit's delivery recovery. Check that the
tagged commit, hosted note with its licensing footer, selected Linux assets,
and checksums reach the intended destination. This observation is asynchronous;
CI success or asset availability does not become a source-release completion
gate or require the publication command to wait.
The accepted ADR settles the architectural choice and standard workflow
trade-offs. No significant feature decisions remain open. The next Audita
version and publication timing belong to release preparation. The
[implementation plan](implementation.md) owns the ordered execution steps and
their verification; this roadmap owns the desired end state and adoption intent.
Implementation completed 2026-10-07 at revision
`5ac704c4c42fd23ed94b1f7351491ad580726db1`. The full Go suite and executable
version path passed, and ReleaseKit built both selected Linux artifacts with
its native version smoke check passing. The profile, dependency and CI pins,
wrapper, and parsed workflow were verified together. Audita's local release
builder and publication mechanics are retired. This completion record is a
later documentation-only commit; live delivery remains to be observed during
the first adopting release and is separate from source implementation.

151
docs/roadmap/remediation.md Normal file
View File

@@ -0,0 +1,151 @@
# ReleaseKit Adoption Review: Findings and Recommended Changes
**Reviewed:** 2026-10-07, source commit
`3c405f0524ec6c098d00d4e371abbace7648d446`.
**Scope:** ReleaseKit adoption commits after `721365e`, including CLI version
reporting, local release wiring, CI delivery, tests, and documentation.
**Status:** R1–R3 addressed. The findings below describe the reviewed
baseline; completed work is recorded under Resolution Record.
## Architecture Assessment
The core adoption matches [ADR 0001](../adr/0001-releasekit.md) and the
[feature roadmap](releasekit.md). Audita supplies product facts in
`releasekit.json`, a thin executable wrapper, a configuration-free version
command using the public `buildinfo` accessor, and a pinned Woodpecker
`ci-deliver` invocation. ReleaseKit owns candidate validation, annotated-tag
publication, version injection, Linux builds, checksums, and hosted delivery.
The custom builder and local publication mechanics have been retired.
No core ownership or delivery-target gap was found. The findings concern one
CLI behavior defect, an ineffective target-selection assertion, and unnecessary
test restrictions. Main-first publication and the native-host version smoke
check are deliberately accepted workflow changes, not accidental strengthening.
## R1 — P2: Simplify Version Help Handling and Correct Its Streams
**Category:** CLI behavior defect and unnecessary argument restriction.
**Evidence:** [runVersion](../../internal/cli/run.go) handles standalone `-h`
and `--help` before parsing, then rejects either spelling when combined with
other arguments. This adds a special prohibition beyond the requested usual
help behavior. For example, `audita version -h --help` returns status `2`.
The parser's help path also writes usage twice: `FlagSet.Usage` writes to stderr,
and the `flag.ErrHelp` branch writes it again to stdout. Reproduced with
`audita version --h` and `audita version -help`: both return status `0` and emit
the same usage on both streams. The standard library recognizes these help
spellings, but the preliminary special cases do not handle them.
**Recommended change:** Use one standard parsing path. Let help short-circuit
according to the parser's ordinary behavior, and emit successful help once to
stdout with empty stderr. Keep non-help unknown flags and unexpected positional
arguments as usage errors. Remove the bespoke mixed-help prohibition and the
test that promotes it into a requirement. Update the CLI wording if needed so
it describes ordinary help handling without promising validation of trailing
arguments after a help request.
**Acceptance:** Normal version output remains configuration-free and unchanged.
Representative parser help paths produce status `0`, stdout usage, and empty
stderr. Non-help invalid invocations still return status `2`. Tests protect
streams and meaningful behavior without establishing a new argument-combination
policy or a comprehensive help-alias contract.
## R2 — P2: Make the Delivery-Target Check Compare Actual Membership
**Category:** Test correctness gap.
**Evidence:** [sameMembers](../../scripts/release_test.go) compares slice
lengths and checks only that each observed item belongs to the expected slice.
It never checks that every required target is present. Calling the actual helper
with `got = [linux/amd64, linux/amd64]` and
`want = [linux/amd64, linux/arm64]` returns true. A profile missing arm64 can
therefore pass the selected-target assertion. ReleaseKit deduplicates repeated
targets, so that configuration would build only amd64.
The reverse problem also occurs: a list containing both required targets plus
a repeated amd64 entry returns false despite having the same effective target
membership. The current committed profile is correct; the defect is in its
regression protection.
**Recommended change:** Compare the sets of configured and required targets,
including coverage of every required member and rejection of an unintended
member. Reuse straightforward standard-library data structures. Duplicate
normalization already belongs to ReleaseKit; Audita needs no duplicate ban or
order requirement.
**Acceptance:** A repeated single target cannot conceal an omitted production
target. Either omitted required target and an additional unintended target fail
the profile check. Reordered or repeated entries with the same effective target
set remain equivalent. Focused coverage catches the demonstrated false positive.
## R3 — P3: Remove Incidental Wiring-Test Requirements
**Category:** Tests strengthen representation choices into contracts.
**Evidence:** [release wrapper/profile tests](../../scripts/release_test.go)
require the exact output argument spelling `-o ./bin/releasekit` and reject the
presence of a `checks` key regardless of its value. In disposable copies of the
tests, changing the output path to the equivalent `bin/releasekit` fails the
wrapper assertion even though execution still reaches the intended tool.
Adding `"checks": []` fails the profile assertion despite configuring no
additional commands.
The accepted requirements concern the executable destination, native argument
and status forwarding, and avoiding redundant additional checks. They do not
require one path spelling or one serialization of an empty list. The plan's
instruction to omit `checks` selects an initial file representation rather than
an enduring prohibition on equivalent configuration.
**Recommended change:** Check the resolved output destination and observed
wrapper behavior instead of the literal relative-path spelling. Check that the
effective additional-check list is empty, accepting omission or an empty list.
Keep the existing product, target, destination, argument-boundary, failure, and
exit-status protection. Simplify fixture helpers where that reduces maintenance
without adding another parser or test framework.
**Acceptance:** The two demonstrated equivalent edits preserve passing wiring
tests, while a wrong executable destination, changed product facts, nonempty
additional checks, lost argument boundaries, or lost tool status still fail.
The tests remain offline and do not duplicate ReleaseKit's release-mechanics
matrix.
## Review Verification and Limits
- `go test ./...` passed on the reviewed source.
- A normal application build succeeded. Direct executable invocations reproduced
the version-help stream defect and mixed-help rejection.
- The actual membership helper reproduced the missing-target false positive.
Disposable test copies reproduced both equivalent-configuration failures;
the repository's code and tests were left unchanged.
- The real ReleaseKit builder produced both configured Linux artifacts with
build-test identifier `v0.0.0` and passed its injected-version smoke check.
This used the available Go 1.26.8 Linux amd64 toolchain; CI remains configured
for Go 1.25.5. Temporary build artifacts were removed.
- Module selection, source attribution, product profile, secret mapping, and
CI command match the intended integration. Live Woodpecker/Gitea delivery
remains unobserved, as already recorded in the feature roadmap; it is separate
deployment work rather than an implementation finding or source-release gate.
- No tags, pushes, hosted uploads, or LLM calls were made. Remediation should
remain bounded to the findings above and add no new release gates.
## Resolution Record
- **R1:** The version command now uses one standard flag-parsing path. Successful
help is written once to stdout, with no stderr usage, and help can short-circuit
trailing arguments. Non-help usage errors retain status `2`. Regression tests
reproduced both defects before the fix; `go test ./internal/cli ./cmd/audita`
passed afterward. The CLI contract reflects ordinary help behavior.
- **R2:** Delivery targets are compared as sets using the standard library.
Regression cases reject either missing Linux target concealed by duplicates
and an unintended target, while accepting equivalent reordered/repeated lists.
The cases reproduced the faulty membership behavior before the fix;
`go test ./scripts` passed afterward.
- **R3:** Wrapper coverage now checks the executable's actual destination, and
profile coverage checks the effective additional-check list. Equivalent output
paths and an explicit empty list are accepted. Fixture helpers use standard
library operations where appropriate. Disposable mutation checks accepted
`-o bin/releasekit`, `-o=bin/releasekit`, and `"checks": []`, while detecting a
wrong executable destination, changed product, nonempty checks, lost argument
boundaries, and lost tool status. The complete `go test ./...` suite passed
with all three remediations in place. No new release gates were added.

1
go.mod
View File

@@ -4,6 +4,7 @@ go 1.25.5
require (
gitea.maximumdirect.net/eric/ConfigKit v0.5.0
gitea.maximumdirect.net/eric/ReleaseKit v0.1.0
gitea.maximumdirect.net/eric/promptkit v0.12.0
gopkg.in/yaml.v3 v3.0.1
)

2
go.sum
View File

@@ -1,5 +1,7 @@
gitea.maximumdirect.net/eric/ConfigKit v0.5.0 h1:2KwG10+kApWM3avOoYA+p768xcbHUp7tQ2rqizBNpyg=
gitea.maximumdirect.net/eric/ConfigKit v0.5.0/go.mod h1:tYWpsrbLyu05r8+1MapWp5fN6ZrX9I8hU2xEPedYmYk=
gitea.maximumdirect.net/eric/ReleaseKit v0.1.0 h1:Ejk85+9Of+A8M7Mod91iB3KxrrQYgnJcz9hPA6fL1a4=
gitea.maximumdirect.net/eric/ReleaseKit v0.1.0/go.mod h1:KmH607Vc/RK/zKrCB96cWh0QZYvkbxBFvtqV+BkKaxs=
gitea.maximumdirect.net/eric/promptkit v0.12.0 h1:oIFWEg6GPxtmX3Plqv0euP+FnK1LCk/t4xDy6oKCpP4=
gitea.maximumdirect.net/eric/promptkit v0.12.0/go.mod h1:pFOOy5v1zH17stEi9rmHYmfbuLjnfVMcUhfmk+4VtJY=
gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v3 v3.20260930.0 h1:lzfCDbzJQRGzIlRPRprnAV9hVVL5zUQy80VMQ0kbYa0=

View File

@@ -2,9 +2,11 @@ package cli
import (
"context"
"flag"
"fmt"
"io"
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
"gitea.maximumdirect.net/eric/audita/internal/framework/llm"
)
@@ -42,6 +44,9 @@ func Run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
case "config":
writeConfigUsage(stdout)
return 0
case "version":
writeVersionUsage(stdout)
return 0
}
}
@@ -56,12 +61,36 @@ func Run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
if args[0] == "config" {
return runConfig(ctx, args[1:], stdout, stderr)
}
if args[0] == "version" {
return runVersion(args[1:], stdout, stderr)
}
fmt.Fprintf(stderr, "audita: unknown command %q\n\n", args[0])
writeRootUsage(stderr)
return 2
}
func runVersion(args []string, stdout, stderr io.Writer) int {
flags := flag.NewFlagSet("version", flag.ContinueOnError)
flags.SetOutput(stderr)
flags.Usage = func() {} // Route usage after parsing so help stays on stdout.
if err := flags.Parse(args); err != nil {
if err == flag.ErrHelp {
writeVersionUsage(stdout)
return 0
}
writeVersionUsage(stderr)
return 2
}
if positional := flags.Args(); len(positional) > 0 {
fmt.Fprintf(stderr, "audita version: unexpected argument %q\n\n", positional[0])
writeVersionUsage(stderr)
return 2
}
fmt.Fprintf(stdout, "audita %s\n", buildinfo.Version())
return 0
}
func runConfig(ctx context.Context, args []string, stdout, stderr io.Writer) int {
if len(args) == 0 || isHelpCommand(args) || args[0] == "--help" || args[0] == "-h" {
writeConfigUsage(stdout)
@@ -177,11 +206,19 @@ func writeRootUsage(w io.Writer) {
fmt.Fprintln(w, "Commands:")
fmt.Fprintln(w, " process Process a transcript JSON file")
fmt.Fprintln(w, " config Validate and inspect config")
fmt.Fprintln(w, " version Print the Audita version")
fmt.Fprintln(w)
fmt.Fprintln(w, "Example:")
fmt.Fprintln(w, " audita process transcript.json --glossary glossary.yaml --output corrected.json")
}
func writeVersionUsage(w io.Writer) {
fmt.Fprintln(w, "Print the Audita version.")
fmt.Fprintln(w)
fmt.Fprintln(w, "Usage:")
fmt.Fprintln(w, " audita version")
}
func writeConfigUsage(w io.Writer) {
fmt.Fprintln(w, "Validate and inspect Audita config.")
fmt.Fprintln(w)

View File

@@ -0,0 +1,73 @@
package cli
import (
"bytes"
"context"
"fmt"
"strings"
"testing"
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
)
func TestVersionCommandRunsWithoutApplicationSetup(t *testing.T) {
t.Chdir(t.TempDir())
var stdout, stderr bytes.Buffer
if code := Run(context.Background(), []string{"version"}, &stdout, &stderr); code != 0 {
t.Fatalf("exit=%d, stdout=%q, stderr=%q", code, stdout.String(), stderr.String())
}
if got, want := stdout.String(), fmt.Sprintf("audita %s\n", buildinfo.Version()); got != want {
t.Fatalf("stdout=%q, want %q", got, want)
}
if got := stderr.String(); got != "" {
t.Fatalf("stderr=%q, want empty", got)
}
}
func TestVersionCommandHelp(t *testing.T) {
for _, test := range []struct {
name string
args []string
want string
}{
{name: "root help", args: []string{"--help"}, want: "version Print the Audita version"},
{name: "help version", args: []string{"help", "version"}, want: "audita version"},
{name: "short flag", args: []string{"version", "-h"}, want: "audita version"},
{name: "long flag", args: []string{"version", "--help"}, want: "audita version"},
{name: "parser help", args: []string{"version", "--h"}, want: "audita version"},
{name: "help short circuits", args: []string{"version", "-h", "--help", "extra"}, want: "audita version"},
} {
t.Run(test.name, func(t *testing.T) {
var stdout, stderr bytes.Buffer
if code := Run(context.Background(), test.args, &stdout, &stderr); code != 0 {
t.Fatalf("exit=%d, stdout=%q, stderr=%q", code, stdout.String(), stderr.String())
}
if !strings.Contains(stdout.String(), test.want) {
t.Fatalf("stdout=%q, want %q", stdout.String(), test.want)
}
if got := stderr.String(); got != "" {
t.Fatalf("stderr=%q, want empty", got)
}
})
}
}
func TestVersionCommandRejectsUnexpectedArguments(t *testing.T) {
for _, args := range [][]string{
{"version", "extra"},
{"version", "--unknown"},
} {
t.Run(strings.Join(args, " "), func(t *testing.T) {
var stdout, stderr bytes.Buffer
if code := Run(context.Background(), args, &stdout, &stderr); code != 2 {
t.Fatalf("exit=%d, stdout=%q, stderr=%q", code, stdout.String(), stderr.String())
}
if got := stdout.String(); got != "" {
t.Fatalf("stdout=%q, want empty", got)
}
if got := stderr.String(); got == "" {
t.Fatal("expected usage error on stderr")
}
})
}
}

15
releasekit.json Normal file
View File

@@ -0,0 +1,15 @@
{
"product": "Audita",
"remote": "origin",
"build": {
"main_package": "./cmd/audita",
"binary_name": "audita",
"targets": ["linux/amd64", "linux/arm64"]
},
"delivery": {
"base_url": "https://gitea.maximumdirect.net",
"owner": "eric",
"repository": "audita",
"token_env": "RELEASEKIT_GITEA_TOKEN"
}
}

View File

@@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# != 2 || ! $1 =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
echo 'Usage: scripts/build-release.sh vMAJOR.MINOR.PATCH OUTPUT_DIRECTORY' >&2
exit 2
fi
version=$1
output=$2
mkdir -p "$output"
output=$(cd "$output" && pwd)
cd "$(dirname "$0")/.."
for arch in amd64 arm64; do
CGO_ENABLED=0 GOOS=linux GOARCH=$arch GOWORK=off \
go build -mod=readonly -trimpath -ldflags '-s -w' \
-o "$output/audita-$version-linux-$arch" ./cmd/audita
done

View File

@@ -1,48 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
fail() { echo "Release stopped: $*" >&2; exit 1; }
if [[ $# != 2 || ( $1 != --check && $1 != --publish ) || ! $2 =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
echo 'Usage: scripts/release.sh --check|--publish vMAJOR.MINOR.PATCH' >&2
exit 2
fi
mode=$1
version=$2
cd "$(dirname "$0")/.."
[[ $(git symbolic-ref --quiet --short HEAD) == main ]] || fail 'switch to main'
[[ -z $(git status --porcelain) ]] || fail 'commit or remove outstanding changes first'
candidate=$(git rev-parse HEAD)
note="docs/releases/$version.md"
[[ -s $note ]] || fail "missing release note: $note"
IFS= read -r title < "$note" || true
[[ $title == "# Audita $version" ]] || fail "note must begin with # Audita $version"
git show-ref --verify --quiet "refs/tags/$version" && fail 'version already exists locally'
# Check mode is local and never contacts a remote or creates a tag.
if [[ $mode == --publish ]]; then
existing=$(git ls-remote --tags origin "refs/tags/$version")
[[ -z $existing ]] || fail 'version already exists on origin'
fi
build_dir=$(mktemp -d)
trap 'rm -rf "$build_dir"' EXIT
GOWORK=off go test -mod=readonly ./...
bash scripts/build-release.sh "$version" "$build_dir"
[[ $(git rev-parse HEAD) == "$candidate" ]] || fail 'HEAD changed during validation'
[[ -z $(git status --porcelain) ]] || fail 'worktree changed during validation'
[[ $(git symbolic-ref --quiet --short HEAD) == main ]] || fail 'branch changed during validation'
if [[ $mode == --check ]]; then
echo "Validated $version at $candidate; nothing published."
exit 0
fi
# Fetch the authoritative branch before checking that this is a forward update.
git fetch --no-tags origin refs/heads/main
git merge-base --is-ancestor FETCH_HEAD "$candidate" || fail 'candidate does not include origin/main'
existing=$(git ls-remote --tags origin "refs/tags/$version")
[[ -z $existing ]] || fail 'version already exists on origin'
git tag "$version" "$candidate"
[[ $(git rev-parse "refs/tags/$version") == "$candidate" ]] || fail 'tag target changed'
# Atomic publication prevents a rejected tag/branch update from publishing only half.
if ! git push --atomic origin "$candidate:refs/heads/main" "refs/tags/$version:refs/tags/$version"; then
fail "push failed or its outcome is uncertain; inspect origin/main and refs/tags/$version before retrying (local tag retained)"
fi
echo "Released $version at $candidate. Binary publication runs asynchronously; CI is not awaited."
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
cd "$script_dir/.."
mkdir -p bin
GOWORK=off GOTOOLCHAIN=local go build -mod=readonly \
-o ./bin/releasekit gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit
exec ./bin/releasekit "$@"

View File

@@ -1,142 +1,272 @@
package scripts
import (
"encoding/json"
"maps"
"os"
"os/exec"
"path/filepath"
"reflect"
"slices"
"strconv"
"strings"
"testing"
)
// Real local Git repositories exercise publication; Go is stubbed at the slow
// build/test boundary so these tests remain offline and cannot recurse.
func TestReleaseWorkflow(t *testing.T) {
for _, scenario := range []string{"check", "publish", "dirty", "tests-fail", "build-fail", "existing-remote-tag", "remote-ahead", "rejected-push", "source-changed", "missing-note", "invalid-version", "existing-local-tag", "wrong-branch", "head-changed"} {
t.Run(scenario, func(t *testing.T) {
root := t.TempDir()
repo, remote := filepath.Join(root, "repo"), filepath.Join(root, "remote.git")
if err := os.MkdirAll(repo, 0755); err != nil {
t.Fatal(err)
const goStub = `#!/usr/bin/env bash
set -euo pipefail
printf '%s\0' "$PWD" "GOWORK=$GOWORK" "GOTOOLCHAIN=$GOTOOLCHAIN" "$@" > "$RELEASEKIT_BUILD_LOG"
if [[ ${RELEASEKIT_BOOTSTRAP_STATUS:-0} != 0 ]]; then
exit "$RELEASEKIT_BOOTSTRAP_STATUS"
fi
output=
while (($#)); do
case $1 in
-o)
shift
output=${1:?missing output path}
;;
-o=*) output=${1#-o=} ;;
esac
shift
done
[[ -n $output ]]
cat > "$output" <<'RELEASEKIT_TOOL'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\0' "$PWD" "$@" > "$RELEASEKIT_ARGS_LOG"
exit "${RELEASEKIT_TOOL_STATUS:-0}"
RELEASEKIT_TOOL
chmod +x "$output"
`
func TestReleaseWrapperForwardsNativeArgumentsAndStatus(t *testing.T) {
for _, toolStatus := range []int{0, 3} {
t.Run("status "+strconv.Itoa(toolStatus), func(t *testing.T) {
layout := newReleaseTestLayout(t)
args := []string{"publish", "v4.2.0", "argument with spaces"}
cmd := exec.Command("bash", append([]string{layout.wrapper}, args...)...)
cmd.Dir = layout.caller
cmd.Env = layout.environment(map[string]string{
"RELEASEKIT_ARGS_LOG": layout.argsLog,
"RELEASEKIT_BUILD_LOG": layout.buildLog,
"RELEASEKIT_TOOL_STATUS": strconv.Itoa(toolStatus),
})
output, err := cmd.CombinedOutput()
if got := commandStatus(err); got != toolStatus {
t.Fatalf("exit=%d, want %d; output=%q, error=%v", got, toolStatus, output, err)
}
env := append(os.Environ(), "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL="+os.DevNull, "GIT_AUTHOR_NAME=Release Test", "GIT_AUTHOR_EMAIL=test@example.invalid", "GIT_COMMITTER_NAME=Release Test", "GIT_COMMITTER_EMAIL=test@example.invalid", "GIT_TERMINAL_PROMPT=0")
run := func(dir string, args ...string) string {
t.Helper()
c := exec.Command("git", args...)
c.Dir = dir
c.Env = env
b, e := c.CombinedOutput()
if e != nil {
t.Fatalf("git %v: %v\n%s", args, e, b)
}
return strings.TrimSpace(string(b))
if len(output) != 0 {
t.Fatalf("unexpected output: %q", output)
}
write := func(path, data string, mode os.FileMode) {
t.Helper()
if e := os.MkdirAll(filepath.Dir(path), 0755); e != nil {
t.Fatal(e)
}
if e := os.WriteFile(path, []byte(data), mode); e != nil {
t.Fatal(e)
}
buildInvocation := readNULArguments(t, layout.buildLog)
if len(buildInvocation) < 4 || buildInvocation[0] != layout.repo {
t.Fatalf("build invocation=%q, want repository %q", buildInvocation, layout.repo)
}
run(root, "init", "--bare", "--initial-branch=main", remote)
run(repo, "init", "--initial-branch=main")
for _, name := range []string{"release.sh", "build-release.sh"} {
b, e := os.ReadFile(name)
if e != nil {
t.Fatal(e)
}
write(filepath.Join(repo, "scripts", name), string(b), 0755)
if buildInvocation[1] != "GOWORK=off" || buildInvocation[2] != "GOTOOLCHAIN=local" {
t.Fatalf("build environment=%q", buildInvocation[1:3])
}
write(filepath.Join(repo, "docs/releases/v1.2.3.md"), "# Audita v1.2.3\n\nA test release.\n", 0644)
run(repo, "add", ".")
run(repo, "commit", "-m", "Initial fixture")
run(repo, "remote", "add", "origin", remote)
run(repo, "push", "origin", "main")
write(filepath.Join(repo, "change.txt"), "candidate\n", 0644)
run(repo, "add", ".")
run(repo, "commit", "-m", "Candidate fixture")
candidate := run(repo, "rev-parse", "HEAD")
log := filepath.Join(root, "go.log")
stub := "#!/bin/sh\necho \"$* GOOS=$GOOS GOARCH=$GOARCH CGO_ENABLED=$CGO_ENABLED GOWORK=$GOWORK\" >> \"$GO_LOG\"\n"
switch scenario {
case "existing-local-tag":
run(repo, "tag", "v1.2.3")
case "wrong-branch":
run(repo, "checkout", "-b", "feature")
case "head-changed":
stub += "if [ \"$1\" = test ]; then echo changed > change.txt; git add change.txt; git commit -m Changed; fi\n"
case "dirty":
write(filepath.Join(repo, "untracked"), "dirty", 0644)
case "tests-fail":
stub += "[ \"$1\" != test ] || exit 1\n"
case "build-fail":
stub += "[ \"$GOARCH\" != arm64 ] || exit 1\n"
case "existing-remote-tag":
run(remote, "tag", "v1.2.3", "main")
case "remote-ahead":
other := filepath.Join(root, "other")
run(root, "clone", remote, other)
write(filepath.Join(other, "other.txt"), "new remote commit", 0644)
run(other, "add", ".")
run(other, "commit", "-m", "Remote fixture")
run(other, "push", "origin", "main")
case "rejected-push":
write(filepath.Join(remote, "hooks/pre-receive"), "#!/bin/sh\nexit 1\n", 0755)
case "source-changed":
stub += "[ \"$1\" != test ] || echo changed > change.txt\n"
case "missing-note":
run(repo, "rm", "docs/releases/v1.2.3.md")
run(repo, "commit", "-m", "Remove fixture note")
buildArgs := buildInvocation[3:]
if buildArgs[0] != "build" || !slices.Contains(buildArgs, "-mod=readonly") || !slices.Contains(buildArgs, "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit") {
t.Fatalf("unexpected build arguments: %q", buildArgs)
}
stub += "exit 0\n"
write(filepath.Join(root, "bin/go"), stub, 0755)
mode, version := "--publish", "v1.2.3"
if scenario == "check" {
mode = "--check"
run(repo, "remote", "remove", "origin")
if info, err := os.Stat(filepath.Join(layout.repo, "bin", "releasekit")); err != nil || !info.Mode().IsRegular() {
t.Fatalf("ReleaseKit executable was not built in bin: %v", err)
}
if scenario == "invalid-version" {
version = "v01.2.3"
}
c := exec.Command("bash", "scripts/release.sh", mode, version)
c.Dir = repo
c.Env = append(env, "PATH="+filepath.Join(root, "bin")+":"+os.Getenv("PATH"), "GO_LOG="+log)
b, e := c.CombinedOutput()
success := scenario == "check" || scenario == "publish"
if (e == nil) != success {
t.Fatalf("success=%v: %v\n%s", success, e, b)
}
tags := run(remote, "tag", "--list")
if scenario == "publish" {
if run(remote, "rev-parse", "refs/tags/v1.2.3") != candidate || run(remote, "rev-parse", "main") != candidate {
t.Fatal("wrong source published")
}
if !strings.Contains(string(b), "CI is not awaited") {
t.Fatal("missing asynchronous completion message")
}
} else if scenario != "existing-remote-tag" && tags != "" {
t.Fatalf("unexpected remote tags: %s", tags)
}
if scenario == "rejected-push" && run(remote, "rev-parse", "main") == candidate {
t.Fatal("rejected push published candidate")
}
if scenario == "check" && run(repo, "tag", "--list") != "" {
t.Fatal("check created a tag")
}
if success {
calls, e := os.ReadFile(log)
if e != nil {
t.Fatal(e)
}
lines := strings.Split(strings.TrimSpace(string(calls)), "\n")
if len(lines) != 3 {
t.Fatalf("expected one test and two builds: %s", calls)
}
if !strings.HasPrefix(lines[0], "test -mod=readonly ./...") || !strings.Contains(lines[1], "GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GOWORK=off") || !strings.Contains(lines[2], "GOOS=linux GOARCH=arm64 CGO_ENABLED=0 GOWORK=off") {
t.Fatalf("unexpected validation: %s", calls)
}
toolInvocation := readNULArguments(t, layout.argsLog)
wantToolInvocation := append([]string{layout.repo}, args...)
if !reflect.DeepEqual(toolInvocation, wantToolInvocation) {
t.Fatalf("tool invocation=%q, want %q", toolInvocation, wantToolInvocation)
}
})
}
}
func TestReleaseWrapperStopsWhenBootstrapFails(t *testing.T) {
layout := newReleaseTestLayout(t)
cmd := exec.Command("bash", layout.wrapper, "publish", "v4.2.0")
cmd.Dir = layout.caller
cmd.Env = layout.environment(map[string]string{
"RELEASEKIT_ARGS_LOG": layout.argsLog,
"RELEASEKIT_BUILD_LOG": layout.buildLog,
"RELEASEKIT_BOOTSTRAP_STATUS": "42",
})
output, err := cmd.CombinedOutput()
if got := commandStatus(err); got != 42 {
t.Fatalf("exit=%d, want 42; output=%q, error=%v", got, output, err)
}
if _, err := os.Stat(layout.argsLog); !os.IsNotExist(err) {
t.Fatalf("ReleaseKit executed after failed bootstrap; stat error=%v", err)
}
}
func TestReleaseKitProfileDeclaresProductAndDeliverySettings(t *testing.T) {
data, err := os.ReadFile("../releasekit.json")
if err != nil {
t.Fatal(err)
}
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
t.Fatal(err)
}
var deliveryFields map[string]json.RawMessage
if err := json.Unmarshal(raw["delivery"], &deliveryFields); err != nil {
t.Fatal(err)
}
if _, exists := deliveryFields["token"]; exists {
t.Fatal("profile must not contain a credential value")
}
var profile struct {
Product string `json:"product"`
Remote string `json:"remote"`
Checks []json.RawMessage `json:"checks"`
Build struct {
MainPackage string `json:"main_package"`
BinaryName string `json:"binary_name"`
Targets []string `json:"targets"`
} `json:"build"`
Delivery struct {
BaseURL string `json:"base_url"`
Owner string `json:"owner"`
Repository string `json:"repository"`
TokenEnv string `json:"token_env"`
} `json:"delivery"`
}
if err := json.Unmarshal(data, &profile); err != nil {
t.Fatal(err)
}
if len(profile.Checks) != 0 {
t.Fatal("profile must not define additional checks")
}
if profile.Product != "Audita" || profile.Remote != "origin" {
t.Fatalf("product=%q remote=%q", profile.Product, profile.Remote)
}
if profile.Build.MainPackage != "./cmd/audita" || profile.Build.BinaryName != "audita" {
t.Fatalf("build=%+v", profile.Build)
}
if !sameMembers(profile.Build.Targets, []string{"linux/amd64", "linux/arm64"}) {
t.Fatalf("build targets=%q", profile.Build.Targets)
}
if profile.Delivery.BaseURL != "https://gitea.maximumdirect.net" || profile.Delivery.Owner != "eric" || profile.Delivery.Repository != "audita" || profile.Delivery.TokenEnv != "RELEASEKIT_GITEA_TOKEN" {
t.Fatalf("delivery=%+v", profile.Delivery)
}
}
func TestReleaseKitTargetMembership(t *testing.T) {
want := []string{"linux/amd64", "linux/arm64"}
for _, test := range []struct {
name string
targets []string
match bool
}{
{"missing amd64", []string{"linux/arm64", "linux/arm64"}, false},
{"missing arm64", []string{"linux/amd64", "linux/amd64"}, false},
{"additional target", []string{"linux/amd64", "linux/arm64", "darwin/arm64"}, false},
{"reordered", []string{"linux/arm64", "linux/amd64"}, true},
{"repeated", []string{"linux/amd64", "linux/arm64", "linux/amd64"}, true},
} {
t.Run(test.name, func(t *testing.T) {
if got := sameMembers(test.targets, want); got != test.match {
t.Fatalf("target membership for %q = %t, want %t", test.targets, got, test.match)
}
})
}
}
type releaseTestLayout struct {
repo string
caller string
wrapper string
argsLog string
buildLog string
path string
}
func newReleaseTestLayout(t *testing.T) releaseTestLayout {
t.Helper()
root := t.TempDir()
layout := releaseTestLayout{
repo: filepath.Join(root, "repository"),
caller: filepath.Join(root, "caller"),
argsLog: filepath.Join(root, "tool-arguments"),
buildLog: filepath.Join(root, "build-arguments"),
path: filepath.Join(root, "bin"),
}
if err := os.MkdirAll(filepath.Join(layout.repo, "scripts"), 0755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(layout.caller, 0755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(layout.path, 0755); err != nil {
t.Fatal(err)
}
wrapper, err := os.ReadFile("release.sh")
if err != nil {
t.Fatal(err)
}
layout.wrapper = filepath.Join(layout.repo, "scripts", "release.sh")
writeReleaseTestFile(t, layout.wrapper, wrapper, 0755)
writeReleaseTestFile(t, filepath.Join(layout.path, "go"), []byte(goStub), 0755)
return layout
}
func (layout releaseTestLayout) environment(values map[string]string) []string {
filtered := make([]string, 0, len(os.Environ())+len(values)+1)
for _, value := range os.Environ() {
key, _, _ := strings.Cut(value, "=")
if _, overridden := values[key]; key == "PATH" || overridden {
continue
}
filtered = append(filtered, value)
}
filtered = append(filtered, "PATH="+layout.path+string(os.PathListSeparator)+os.Getenv("PATH"))
for key, value := range values {
filtered = append(filtered, key+"="+value)
}
return filtered
}
func writeReleaseTestFile(t *testing.T, path string, data []byte, mode os.FileMode) {
t.Helper()
if err := os.WriteFile(path, data, mode); err != nil {
t.Fatal(err)
}
}
func readNULArguments(t *testing.T, path string) []string {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
parts := strings.Split(string(data), "\x00")
if len(parts) > 0 && parts[len(parts)-1] == "" {
parts = parts[:len(parts)-1]
}
return parts
}
func commandStatus(err error) int {
if err == nil {
return 0
}
if exitErr, ok := err.(*exec.ExitError); ok {
return exitErr.ExitCode()
}
return -1
}
func sameMembers(got, want []string) bool {
gotSet := make(map[string]struct{}, len(got))
for _, value := range got {
gotSet[value] = struct{}{}
}
wantSet := make(map[string]struct{}, len(want))
for _, value := range want {
wantSet[value] = struct{}{}
}
return maps.Equal(gotSet, wantSet)
}