Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/backlog/COR-103-verification-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Each check fails first on one named past mistake.
| COR-103-T7 | One install and uninstall path | agent | - | done |
| COR-103-T8 | Update through the target updater | agent | COR-103-T7 | done |
| COR-103-T9 | Sign off an isolated checkout | agent | COR-103-T7 | done |
| COR-103-T10 | Release with a receipt | agent | COR-103-T7, COR-103-T9 | todo |
| COR-103-T10 | Release with a receipt | agent | COR-103-T7, COR-103-T9 | done |
| COR-103-T11 | macOS job in the template | agent | - | done |
| COR-103-T12 | Author path from the published tag | agent | - | done |
| COR-103-T13 | Rules and prose | agent | COR-103-T4, COR-103-T5, COR-103-T6, COR-103-T8, COR-103-T10, COR-103-T11, COR-103-T12 | todo |
Expand Down
6 changes: 4 additions & 2 deletions docs/north-star/contract/extension-repository.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ The template refuses `main` and each Go keyword as a domain, such as `type`. It
A push can skip Lefthook, so its GitHub workflow runs `just check` again on `ubuntu-24.04`.
Public repositories also run the same steps on `macos-15`, and both runners are free.

The repository tests itself through the [test kit](test-kit.md). Its `justfile` has seven recipes:
The repository tests itself through the [test kit](test-kit.md). Its `justfile` has eight recipes:

- `just check` runs every check: `go mod tidy -diff`, `gofmt`, `go vet`, and `go test`, whose test kit installs the extension into scratch folders
- `just signoff` checks the committed SHA in isolation, then signs that SHA, as [Local checks](../decisions/D016-local-ci.md) defines
- `just release <tag> [fixture]` publishes the signed SHA and proves the README install, as [Release and install](extensions.md#release-and-install) defines
- `just install` builds the executable and exports its sidecars from that build. It replaces the previous install of the slug in the extensions folder of [Discovery](discovery.md), the executable last
- `just uninstall` removes the install of the slug from that folder, the executable first. With nothing installed, it says so and exits 0
- `just acceptance [fixture]` runs `acceptance.sh` as an agent would, with `--json` and `--no-input`. It installs the extension and the `ncly` of the pinned Nicely version into scratch XDG folders. It works on a copy of the fixture and checks that the fixture keeps its checksum. Without a fixture, it makes an input
Expand All @@ -43,10 +44,11 @@ The repository tests itself through the [test kit](test-kit.md). Its `justfile`

`just install` grants no key. A human grants one after reviewing the extension, as [Grants](grants.md) defines.

The SDK package `extensionrepo` owns install, uninstall, and signoff.
The SDK package `extensionrepo` owns install, uninstall, signoff, and release.
The template's `internal/export/` program calls `extensionrepo.Main(Extension)`.
The recipes each call that program in one line: `go run ./internal/export install` or `go run ./internal/export uninstall`.
Signoff calls `go run ./internal/export signoff`.
Release calls `go run ./internal/export release <tag> [fixture]`.
Its `export <folder>` subcommand writes the embedded sidecars of the staged build.

Install stages both builds beside the destination with one `go build`.
Expand Down
29 changes: 28 additions & 1 deletion docs/north-star/contract/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,34 @@ An author outside Nicely keeps an extension in a Git repository of its own, whic

### Release and install

An author releases a version as one Git tag `vX.Y.Z` of Semantic Versioning, such as `v0.1.0`, on a pushed commit where `just check` passes.
An author releases a version with `just release vX.Y.Z [fixture]`.
The SDK requires a clean HEAD equal to the fetched default branch of `origin`.
A successful signoff receipt must name that SHA, with exit 0 for every check and signoff.
Release runs `just acceptance [fixture]`, then creates and pushes an annotated tag whose message holds that signoff receipt.
It executes the fenced `sh` block under `## Install` in `README.md`, with `TAG` replaced by the new tag.
The install uses scratch HOME and XDG folders, without the caller's keys or installed extensions.
The clone's HEAD must equal the tagged SHA, even when another README paragraph names the new tag.
Release then checks discovery, measures [Economy](../../qa/north-star.md#economy), uninstalls, and requires `NOT_FOUND`.
Economy runs literal manifest examples in both output formats, with a dry run when supported.
It selects these modes even when an example already sets them.
Each extension invocation extracts the files of `testdata/examples.txtar` from the tagged scratch clone.
It ignores archive setup commands and keeps extracted files off PATH.
An optional fixture replaces the first example argument that names an archive file, under that argument's filename.
Each measurement uses a fresh working folder, so previous output cannot change the next measurement.
Each measurement records the command, exit, elapsed milliseconds, and separate stdout and stderr byte counts.
Failed Economy calls and exceeded budgets are reservations, not release failures.
When available, `gh` reads the SHA's hosted check runs. Missing or failed hosted evidence becomes a note.
Release stores its receipt and command logs outside the repository, under the Nicely cache's `release/` folder.
It prints the receipt path, and its last success line names the tag and SHA.
A failed published install keeps the pushed tag and records the failure. Release never replaces an existing tag.

The receipt uses `tag`, `sha`, `os`, `signoff`, `pushed`, `cloned_sha`, `steps`, `economy`, `hosted_checks`, `notes`, `reservations`, and optional `failure`.
`signoff` holds the original signoff receipt. `pushed` records tag publication, and `cloned_sha` records the published clone's HEAD.
`hosted_checks` holds paginated check-run responses. `notes` records unavailable hosted evidence, and `reservations` records Economy findings.
`failure` holds the fatal diagnostic.
Each row in `steps` or `economy` holds `name`, `command`, `exit`, `elapsed_ms`, `stdout_bytes`, `stderr_bytes`, `stdout`, and `stderr`.
`stdout` and `stderr` name log files relative to the receipt. Scratch installs and measurement folders are removed after use.

A user installs a fixed version with `git clone --branch <tag> <repository>`, then `just install` in the clone, which need Go and `just`.
For an annotated tag, Git can warn that the tag `is not a commit!`, then note a detached HEAD: the clone holds the tagged commit.
`ncly describe <root> --json` then confirms the install.
Expand Down
19 changes: 19 additions & 0 deletions scripts/template-release-helpers.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Shared calls and receipt fixtures for the local release proof.

fct_release_attempt() {
local repo="${1}" scratch="${2}" tag="${3}" status=0
shift 3
(cd "${scratch}" && PATH="${scratch}/release-bin:${PATH}" XDG_CACHE_HOME="${scratch}/release-cache" just --justfile "${repo}/justfile" release "${tag}" "$@") >"${scratch}/release.out" 2>&1 || status=$?
printf 'release proof: just release %s, exit %s\n' "${tag}" "${status}"
cat "${scratch}/release.out"
return "${status}"
}

# Receipts are fixture facts. The release under test must read T9's format.
fct_release_receipt() {
local repo="${1}" scratch="${2}" sha
sha="$(git -C "${repo}" rev-parse HEAD)"
mkdir -p "${scratch}/release-cache/nicely/signoff/test-${sha}"
PATH="/usr/bin:${PATH}" jq -n --arg sha "${sha}" '{sha:$sha,os:"test",tools:{git:"test"},checks:[{command:["just","check"],exit:0}],signoff:{command:["gh","signoff","--commit",$sha],exit:0}}' >"${scratch}/release-cache/nicely/signoff/test-${sha}/receipt.json"
}
151 changes: 126 additions & 25 deletions scripts/template-release-test.sh
Original file line number Diff line number Diff line change
@@ -1,38 +1,139 @@
#!/usr/bin/env bash
# Sourced by template-test.sh to prove the release steps and the install
# commands of a rendered repository, as "Release and install" in
# docs/north-star/contract/extensions.md says.

# fct_test_release releases the rendered repository $1 as the tag v0.1.0, then
# installs it as a user does with the commands of its README.md, from a clone
# at that tag. The install goes to a new data folder, where ncly describe then
# finds the root $4 of the slug $3, so no earlier install can answer.
# Sourced by template-test.sh. Only local bare remotes receive release tags.

# shellcheck source=scripts/template-release-helpers.sh
source "${BASH_SOURCE[0]%/*}/template-release-helpers.sh"

fct_test_release() {
local repo="${1}" scratch="${2}" slug="${3}" extension_root="${4}" root_words data="${2}/release-data"
read -r -a root_words <<<"${extension_root}"
fct_has "${repo}/AGENTS.md" "## Release"
fct_has "${repo}/README.md" "git clone --branch TAG https://example.com/ncly-${slug} ncly-${slug}"
local repo="${1}" scratch="${2}" slug="${3}" extension_root="${4}" sha previous receipt dir status out_file out_bytes err_file err_bytes i examples command
local remote="${2}/release-origin.git" fixture="${2}/release-fixture/input.md"
fct_has "${repo}/AGENTS.md" '## Release'
fct_has "${repo}/README.md" "ncly describe ${extension_root} --json"
# An annotated tag makes Git warn that the tag is not a commit, which README.md names.
git -C "${repo}" tag -a v0.1.0 -m v0.1.0
git clone --quiet --branch v0.1.0 "${repo}" "${scratch}/ncly-${slug}-v0.1.0" 2>"${scratch}/clone.err"
(cd "${scratch}/ncly-${slug}-v0.1.0" && XDG_DATA_HOME="${data}" just install)
[[ -x "${data}/nicely/extensions/ncly-${slug}" ]] || fct_fail "the install of the tag wrote no ${data}/nicely/extensions/ncly-${slug}"
XDG_DATA_HOME="${data}" PATH="/usr/bin:${PATH}" "$(command -v go)" -C "${repo}" tool ncly describe "${root_words[@]}" --json >"${scratch}/describe.out"
grep -qE "\"path\":\"${extension_root}\",\"summary\":\"[^\"]*\",\"source\":\"extension\"" "${scratch}/describe.out" ||
fct_fail "ncly describe does not find ${extension_root} after the install of the tag: $(cat "${scratch}/describe.out")"
fct_has "${repo}/README.md" "git clone --branch TAG https://example.com/ncly-${slug} ncly-${slug}"
[[ "$(just --justfile "${repo}/justfile" --show release | grep -c '^ ')" == 1 ]] || fct_fail 'just release must have one command'
just --justfile "${repo}/justfile" --show release | grep -q '^ go run ./internal/export release ' || fct_fail 'just release must call the SDK program'
git init --bare -q "${remote}"
git -C "${repo}" remote add origin "${remote}"
git -C "${repo}" push -q origin HEAD:main
git -C "${remote}" symbolic-ref HEAD refs/heads/main
git config --file "${GIT_CONFIG_GLOBAL}" "url.${remote}.insteadOf" "https://example.com/ncly-${slug}"
mkdir -p "${scratch}/release-bin" "${scratch}/release-fixture"
printf '# Release input\nTwo lines\n' >"${fixture}"
cat >"${scratch}/release-bin/gh" <<'SCRIPT'
#!/usr/bin/env bash
set -Eeuo pipefail
[[ "$#" == 4 && "$1" == api && "$2" == --paginate && "$3" == --slurp && "$4" == "repos/{owner}/{repo}/commits/$(git rev-parse HEAD)/check-runs" ]]
printf '[{"total_count":1,"check_runs":[{"name":"local-stub","head_sha":"%s","conclusion":"success"}]}]\n' "$(git rev-parse HEAD)"
SCRIPT
chmod 755 "${scratch}/release-bin/gh"
status=0
fct_release_attempt "${repo}" "${scratch}" v0.1.0 || status=$?
if [[ "${status}" == 0 ]] || ! grep -qF 'just signoff' "${scratch}/release.out"; then
fct_fail 'release accepts a SHA without a receipt'
fi
[[ -z "$(git -C "${remote}" tag -l)" ]] || fct_fail 'receipt refusal pushed a tag'

# Invalid receipt exits cannot authorize a release.
fct_release_receipt "${repo}" "${scratch}"
sha="$(git -C "${repo}" rev-parse HEAD)"
receipt="${scratch}/release-cache/nicely/signoff/test-${sha}/receipt.json"
cp "${receipt}" "${scratch}/valid-signoff.json"
for previous in '.checks[0].exit = 1' '.signoff.exit = 1' '.sha = "previous"' 'del(.checks[0].exit)' '.signoff.exit = null'; do
PATH="/usr/bin:${PATH}" jq "${previous}" "${scratch}/valid-signoff.json" >"${receipt}"
status=0
fct_release_attempt "${repo}" "${scratch}" v0.1.0 || status=$?
if [[ "${status}" == 0 ]] || ! grep -qF 'just signoff' "${scratch}/release.out"; then
fct_fail 'release accepts an unsuccessful receipt'
fi
done
cp "${scratch}/valid-signoff.json" "${receipt}"

printf '\nDirty change\n' >>"${repo}/README.md"
status=0
fct_release_attempt "${repo}" "${scratch}" v0.1.0 || status=$?
if [[ "${status}" == 0 ]] || ! grep -qF 'clean HEAD' "${scratch}/release.out"; then
fct_fail 'release accepts a dirty tree'
fi
git -C "${repo}" restore README.md
status=0
fct_release_attempt "${repo}" "${scratch}" v0.1.0 missing-fixture.md || status=$?
if [[ "${status}" == 0 ]] || ! grep -qF 'acceptance exited' "${scratch}/release.out"; then
fct_fail 'release accepts failed acceptance'
fi
[[ -z "$(git -C "${remote}" tag -l)" ]] || fct_fail 'acceptance refusal pushed a tag'

# Economy selects modes even when the first example already sets them.
examples="$(grep -m1 '^examples = ' "${repo}/manifest.toml")"
command="$(sed -n 's/^examples = \["ncly \(.*\) notes.md".*$/\1/p' "${repo}/manifest.toml" | head -n 1)"
fct_edit "${repo}/manifest.toml" "${examples}" "examples = [\"ncly ${command} 'notes.md' --dry-run --json\", \"ncly ${command} notes.md\"]"
git -C "${repo}" add manifest.toml

# D129: exceeding a skill budget is a reported reservation, never a release block.
for ((i = 0; i < 100; i++)); do printf 'Release budget sample.\n' >>"${repo}/skill/SKILL.md"; done
git -C "${repo}" add skill/SKILL.md
printf '\nUnpushed change\n' >>"${repo}/README.md"
git -C "${repo}" add README.md
git -C "${repo}" commit -qm 'unpushed release input'
fct_release_receipt "${repo}" "${scratch}"
status=0
fct_release_attempt "${repo}" "${scratch}" v0.1.0 || status=$?
if [[ "${status}" == 0 ]] || ! grep -qF 'pushed default branch' "${scratch}/release.out"; then
fct_fail 'release accepts an unpushed SHA'
fi
git -C "${repo}" push -q origin HEAD:main
fct_release_attempt "${repo}" "${scratch}" v0.1.0 || fct_fail 'first local release failed'
receipt="$(sed -n 's/^release: receipt in //p' "${scratch}/release.out")"
PATH="/usr/bin:${PATH}" jq -r '.economy[] | select(.command[1:3] != ["skill", "view"] and .command[1] != "describe" and .command[1] != "doctor") | "default Economy: \(.command | join(" ")), exit \(.exit)"' "${receipt}"
PATH="/usr/bin:${PATH}" jq -e '[.economy[] | select(.command | index("notes.md"))] | length == 4 and all(.[]; .exit == 0)' "${receipt}" >/dev/null || fct_fail 'default release Economy did not run all four extension samples successfully'
previous="$(git -C "${repo}" rev-parse HEAD)"

# D095: a new tag in prose cannot repair an install block pinned to the previous tag.
fct_edit "${repo}/README.md" "git clone --branch TAG https://example.com/ncly-${slug} ncly-${slug}" "git clone --branch v0.1.0 https://example.com/ncly-${slug} ncly-${slug}"
printf '\nThe current release is v0.1.1.\n' >>"${repo}/README.md"
git -C "${repo}" add README.md
git -C "${repo}" commit -qm 'pin the README install to the previous tag'
git -C "${repo}" push -q origin HEAD:main
fct_release_receipt "${repo}" "${scratch}"
status=0
fct_release_attempt "${repo}" "${scratch}" v0.1.1 "${fixture}" || status=$?
if [[ "${status}" == 0 ]] || ! grep -qF "README install cloned SHA ${previous}, want tagged SHA" "${scratch}/release.out"; then
fct_fail 'D095: release accepts a README install of the previous tag'
fi
printf 'RED D095: previous-tag README rejected after publication, exit %s\n' "${status}"

fct_edit "${repo}/README.md" "git clone --branch v0.1.0 https://example.com/ncly-${slug} ncly-${slug}" "git clone --branch TAG https://example.com/ncly-${slug} ncly-${slug}"
git -C "${repo}" add README.md
git -C "${repo}" commit -qm 'install the selected release tag'
git -C "${repo}" push -q origin HEAD:main
fct_release_receipt "${repo}" "${scratch}"
sha="$(git -C "${repo}" rev-parse HEAD)"
fct_release_attempt "${repo}" "${scratch}" v0.1.2 release-fixture/input.md || fct_fail 'correct README release failed'
[[ "$(tail -n 1 "${scratch}/release.out")" == "Released v0.1.2 ${sha}" ]] || fct_fail 'release ends without the tag and SHA'
receipt="$(sed -n 's/^release: receipt in //p' "${scratch}/release.out")"
dir="${receipt%/*}"
PATH="/usr/bin:${PATH}" jq -e '.ok == true' "${dir}/economy-6-json.stdout" >/dev/null || fct_fail 'Economy JSON mode returned no answer'
PATH="/usr/bin:${PATH}" jq -e --argjson bytes "$(wc -c <"${fixture}" | tr -d ' ')" '.size_bytes == $bytes' "${dir}/economy-6-json.stdout" >/dev/null || fct_fail 'Economy did not replace the example input with the supplied fixture'
if PATH="/usr/bin:${PATH}" jq -e . "${dir}/economy-6-text.stdout" >/dev/null 2>&1; then fct_fail 'Economy text mode returned JSON'; fi
PATH="/usr/bin:${PATH}" jq -e --arg sha "${sha}" '.tag == "v0.1.2" and .sha == $sha and .cloned_sha == $sha and .pushed and (.failure == null) and .signoff.sha == $sha and .hosted_checks[0].check_runs[0].head_sha == $sha and any(.reservations[]; contains("exceeds a budget")) and (.economy | length == 14) and all(.economy[]; .exit == 0 and (.elapsed_ms | type) == "number") and ([.steps[] | select(.name == "not-found")][0].exit == 2)' "${receipt}" >/dev/null || fct_fail 'release receipt lacks published proof or Economy rows'
while IFS=$'\t' read -r out_file out_bytes err_file err_bytes; do
[[ "$(wc -c <"${dir}/${out_file}" | tr -d ' ')" == "${out_bytes}" ]] || fct_fail 'Economy stdout count differs from the actual output'
[[ "$(wc -c <"${dir}/${err_file}" | tr -d ' ')" == "${err_bytes}" ]] || fct_fail 'Economy stderr count differs from the actual output'
done < <(PATH="/usr/bin:${PATH}" jq -r '.economy[] | [.stdout, .stdout_bytes, .stderr, .stderr_bytes] | @tsv' "${receipt}")
grep -qF '"code":"NOT_FOUND"' "${dir}/not-found.stderr" || fct_fail 'uninstall gives no NOT_FOUND'
git -C "${remote}" for-each-ref --format='%(contents)' refs/tags/v0.1.2 >"${scratch}/tag-message.json"
PATH="/usr/bin:${PATH}" jq -e --arg sha "${sha}" '.sha == $sha and .signoff.exit == 0 and ([.checks[].exit] | all(. == 0))' "${scratch}/tag-message.json" >/dev/null || fct_fail 'annotated tag lacks its successful signoff receipt'
[[ -z "$(git -C "${repo}" status --porcelain)" ]] || fct_fail 'release wrote into tracked files'
printf 'GREEN release receipt: correct README, Economy, and NOT_FOUND, exit 0\n'
}

# fct_test_release_url renders the template source $1 for a module path with a
# major version suffix, such as /v2. Go adds that suffix to the module path, not
# to the repository URL, so the clone commands of README.md omit it.
# A Go module's /v2 suffix is not part of its published repository URL.
fct_test_release_url() {
local template_src="${1}" scratch="${2}" copier
shift 2
copier=("$@")
"${copier[@]}" copy --quiet --vcs-ref v0.0.1 --defaults --data domain=probe --data module=example.com/ncly-probe/v2 "${template_src}" "${scratch}/ncly-probe-v2"
fct_has "${scratch}/ncly-probe-v2/README.md" "git clone --branch TAG https://example.com/ncly-probe ncly-probe"
fct_has "${scratch}/ncly-probe-v2/README.md" 'git clone --branch TAG https://example.com/ncly-probe ncly-probe'
if grep -qF 'example.com/ncly-probe/v2' "${scratch}/ncly-probe-v2/README.md"; then
fct_fail "README.md gives the module path example.com/ncly-probe/v2 as a repository URL"
fct_fail 'README.md gives the module path example.com/ncly-probe/v2 as a repository URL'
fi
}
5 changes: 4 additions & 1 deletion sdk/extensionrepo/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ func Main(e extension.Extension) {
}

func run(e extension.Extension, args []string) int {
if len(args) > 0 && args[0] == "release" {
return report("release", release(e, args[1:]))
}
if len(args) > 0 && args[0] == "test" {
return Test(args[1:])
}
Expand All @@ -38,7 +41,7 @@ func run(e extension.Extension, args []string) int {
return report("export", extension.Export(e, args[1]))
}
if len(args) != 1 || (args[0] != "install" && args[0] != "uninstall") {
fmt.Fprintln(os.Stderr, "usage: export install | uninstall | signoff | check | contract | export <folder> | test [go test arguments] | update vX.Y.Z /absolute/repository [--continue]")
fmt.Fprintln(os.Stderr, "usage: export install | uninstall | signoff | release <tag> [fixture] | check | contract | export <folder> | test [go test arguments] | update vX.Y.Z /absolute/repository [--continue]")
return 2
}
action := args[0]
Expand Down
Loading
Loading