Skip to content

docs: extract RELEASING.md, add approval tooling - #91

Merged
michen00 merged 6 commits into
mainfrom
docs/extract-releasing-md
Sep 16, 2026
Merged

michen00 merged 6 commits into
mainfrom
docs/extract-releasing-md

Conversation

@michen00

@michen00 michen00 commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Why

Merging #83 prepared v0.1.2, and then nothing happened for an hour. Nothing was broken — release-tag.yml was sitting in waiting on the protected release environment, which is exactly what CONTRIBUTING described. The step was documented; it just wasn't findable, and there was no reminder, no timeout, and no command to run.

What

RELEASING.md — the release material was lines 70–153 of CONTRIBUTING, about 45% of a contributor-facing doc given over to a maintainer-only process. Every substantive claim was verified against the workflows and the v0.1.0–v0.1.2 run history before the move; the ones that did not survive that check are listed under Doc accuracy fixes. Two changes while moving:

  • The approval is now its own numbered step rather than a consequence explained in prose after the list.
  • The flow names make release-status and make release-approve.

make release-approve / make release-statusrelease-approve finds the waiting run, prints the tag it would mint and the commit that tag will point at, and asks before POSTing. release-status reports where a release stands and changes nothing.

The approval gate itself is unchanged. It's what keeps the CI release key from being usable by anyone who can merge a release/* PR, so this makes the click easy to find and press rather than removing it.

Doc accuracy fixes

  • The GPG secrets claim was wrong, twice. The doc said both RELEASE_GPG_PRIVATE_KEY and RELEASE_GPG_PASSPHRASE were required and that both workflows fail without both. gh secret list shows only the private key, and v0.1.2 published anyway. The first correction then overstated the mechanism — it claimed signing distinguishes an omitted secret from an empty one. It does not: release-publish.yml passes the secret through unconditionally, an absent secret expands to an empty string, and sign-artifacts.sh branches on [ -n "${GPG_PASSPHRASE-}" ]. Absent and empty sign identically. The guidance to omit it survives with an accurate reason, and the setup snippet now guards its gh secret set instead of setting the secret unconditionally while telling the reader not to.
  • CONTRIBUTING's ToC was missing Creating a release and One-time release key setup — the two sections everything deep-links to.
  • The CODE_OF_CONDUCT.md link was relative, so it 404'd from this repo; the file is an account-level health file in michen00/.github.
  • .github/copilot-instructions.md linked CONTRIBUTING.md from inside .github/, which never resolved, and described the flow as Release PR → Release Publish with Release Tag left out.

CONTRIBUTING keeps a #### Creating a release stub, so the #creating-a-release anchor still resolves for any link not updated here.

Review-round fixes

Qodo raised nine findings across three passes; six were fixed here and three declined as style claims the repo's own conventions contradict (per-finding replies are on the threads).

  • The approval prompt named the wrong commit. approve.sh displayed the workflow run's headSha, but release-tag.yml checks out pull_request.merge_commit_sha, and this repo squash-merges, so the two always differ — v0.1.2's run reported 3179e83 while the tag landed on 62c098a. A confirmation whose only job is showing what you are approving was naming a commit the tag would never point at. It now resolves the merged PR's merge commit and refuses to approve when it cannot.
  • The test could reach the real GitHub CLI. Unchecked setup meant a failed mktemp or chmod left gh resolving to the caller's authenticated binary, so the --yes cases could have POSTed a real approval against a genuinely waiting release. Setup is now fail-fast and asserts the stub resolves first.
  • Status mode reported failures as facts. gh release view exits non-zero both when no release exists and when the call fails, so a lookup error printed as "no releases"; gh release list separates the two. The open-PR lookup used the default 30-item page, which could hide a pending release PR.

Verification

  • make test — all suites pass, including a new approve.sh suite driven by a stubbed gh (no network) covering status mode, the refusal paths, branch-name validation, and a regression asserting the summary names the merge commit rather than the branch tip.
  • shellcheck --shell=sh across scripts — clean.
  • make run-pre-commit — clean.
  • Every relative link and anchor across the touched Markdown files resolves.
  • make release-status smoke-tested against this repo: reports v0.1.2, no open release PR, nothing awaiting approval.

A docs: squash subject doesn't clear the worthiness gate, so this won't propose its own release — it'll ride along in the next one.

🤖 Generated with Claude Code


Closes #92 (review-convergence bulletin)

michen00 and others added 3 commits September 15, 2026 21:18
A merged release PR does not ship on its own: release-tag.yml is gated
on the protected `release` environment and sits in `waiting` until a
maintainer approves it. Nothing times out and nothing sends a reminder,
so a prepared release can sit indefinitely while Release PR's own guard
refuses to prepare the next one. v0.1.2 waited an hour for exactly this.

Finding that run in the Actions UI was the only step between a merged PR
and a signed tag. `make release-approve` finds it, prints the tag it
would mint and the commit that tag would point at, and asks before
POSTing the approval; `make release-status` reports where a release
stands without touching anything.

The gate itself is unchanged. It is what keeps the CI release key from
being usable by anyone who can merge a `release/*` PR, so this makes the
approval easy to find and press rather than automatic.

The branch name is parsed through the shared parse-version.sh for the
same reason the workflows use it: it decides what gets tagged, so
`release/v1.0.0; rm -rf /` has to be rejected rather than interpolated.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The release material was lines 70-153 of CONTRIBUTING, about 45% of a
contributor-facing document given over to a process only maintainers can
run. It moves out largely verbatim; every claim in it was checked
against the workflows and against the v0.1.0-v0.1.2 run history before
the move, and all of them held.

Two changes while moving it. The approval becomes its own numbered step
rather than a consequence explained in prose after the list, because
that is the one step a human has to take and burying it is how v0.1.2
came to wait an hour. And the flow now names `make release-status` and
`make release-approve`.

CONTRIBUTING keeps a `#### Creating a release` stub pointing here, so
the `#creating-a-release` anchor still resolves for any link not updated
below, and the heading is added to a table of contents that had been
missing it. README, AGENTS.md, CLAUDE.md and copilot-instructions.md all
now point at RELEASING; the copilot one had been linking
`CONTRIBUTING.md` from inside `.github/`, which never resolved, and
described the flow as Release PR then Release Publish with Release Tag
left out entirely.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The link was relative, so it resolved to a file in this repository that
does not exist. The code of conduct is an account-level community health
file in michen00/.github: GitHub surfaces it on the community profile,
but a relative link from here still 404s, so point at where it lives.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@codecov-commenter

codecov-commenter commented Sep 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.05970% with 107 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.85%. Comparing base (62c098a) to head (3a1cc5c).

Files with missing lines Patch % Lines
scripts/release/approve.sh 0.00% 94 Missing ⚠️
tests/test-release-approve.sh 94.58% 13 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #91      +/-   ##
==========================================
+ Coverage   51.57%   53.85%   +2.27%     
==========================================
  Files          31       33       +2     
  Lines        2092     2427     +335     
==========================================
+ Hits         1079     1307     +228     
- Misses       1013     1120     +107     
Flag Coverage Δ
integration 0.85% <0.00%> (-0.12%) ⬇️
unit 53.33% <68.05%> (+2.35%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Document release flow and add approval/status commands

✨ Enhancement 📝 Documentation 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Extract maintainer release guidance into a dedicated, discoverable document.
• Add safe commands to inspect and approve protected release runs.
• Cover approval, authorization failures, and malicious branch names with stubbed tests.
Diagram

sequenceDiagram
    actor Maintainer
    participant Make as Make Commands
    participant Tool as Approval Tool
    participant API as GitHub API
    participant Parser as Version Parser
    participant Tag as Release Tag
    participant Publish as Release Publish
    Maintainer->>Make: release-status or approve
    Make->>Tool: invoke requested mode
    Tool->>API: locate waiting run
    API-->>Tool: run and deployment data
    Tool->>Parser: validate release branch
    Parser-->>Tool: normalized tag
    Tool-->>Maintainer: preview and confirmation
    Tool->>API: POST approval
    API->>Tag: resume gated run
    Tag->>Publish: dispatch signed release
Loading
High-Level Assessment

The current approach is appropriate: it preserves the protected-environment security boundary while wrapping the existing GitHub deployment API with a small, auditable shell command. UI-only approval was dismissed because it caused the discoverability problem, while automatic approval or gate removal would weaken release-key protection.

Files changed (10) +486 / -88

Enhancement (2) +161 / -0
MakefileAdd release status and approval commands +12/-0

Add release status and approval commands

• Adds 'release-status' and 'release-approve' targets backed by the approval script. Supports non-interactive confirmation through 'YES=1'.

Makefile

approve.shAdd guarded GitHub release approval tooling +149/-0

Add guarded GitHub release approval tooling

• Adds read-only status reporting and interactive or explicit approval of the single waiting Release Tag run. It validates branch-derived tags with the shared parser, verifies approver authorization and pending deployments, previews the exact tag and commit, then submits the GitHub deployment approval.

scripts/release/approve.sh

Tests (2) +201 / -0
test-release-approve.shTest release status and approval safety paths +200/-0

Test release status and approval safety paths

• Adds network-isolated tests using a stubbed 'gh' command. Covers argument validation, idle and waiting status, non-interactive safeguards, denied authorization, missing deployments, malicious branch names, and the successful approval payload.

tests/test-release-approve.sh

test-unit.shRegister release approval tests in the unit suite +1/-0

Register release approval tests in the unit suite

• Adds the new release approval test script to the repository’s aggregate unit-test runner.

tests/test-unit.sh

Documentation (6) +124 / -88
copilot-instructions.mdCorrect the documented release workflow and guidance link +2/-2

Correct the documented release workflow and guidance link

• Documents the complete Release PR → Release Tag → Release Publish sequence, including the protected-environment approval. Fixes the guidance link to resolve from inside '.github/'.

.github/copilot-instructions.md

AGENTS.mdPoint release guidance to RELEASING.md +1/-1

Point release guidance to RELEASING.md

• Redirects agent-facing release process and verification guidance from CONTRIBUTING to the dedicated release document.

AGENTS.md

CLAUDE.mdReference the dedicated release documentation +1/-1

Reference the dedicated release documentation

• Updates repository guidance so release-process details and verification instructions point to 'RELEASING.md'.

CLAUDE.md

CONTRIBUTING.mdExtract maintainer release instructions from contributor guidance +3/-83

Extract maintainer release instructions from contributor guidance

• Replaces the lengthy maintainer-only release procedure with a short discoverable stub linking to 'RELEASING.md'. Adds the release anchor to the table of contents and fixes the account-level Code of Conduct link.

CONTRIBUTING.md

README.mdLink release information to RELEASING.md +1/-1

Link release information to RELEASING.md

• Redirects release-process and signature-verification guidance to the new maintainer release document.

README.md

RELEASING.mdCentralize maintainer release procedures +116/-0

Centralize maintainer release procedures

• Introduces dedicated documentation for the automated release lifecycle, explicit approval step, status commands, fallback process, signing model, key setup, and artifact verification.

RELEASING.md

RELEASING said both RELEASE_GPG_PRIVATE_KEY and RELEASE_GPG_PASSPHRASE
were required, and that Release Tag and Release Publish fail at the GPG
import step without both. `gh secret list` shows only the private key,
and v0.1.2 published forty minutes ago, so that was wrong in the
direction that costs the most: it invites someone to "fix" a missing
secret by setting it to an empty string.

The release key has no passphrase. sign-artifacts.sh already branches on
`[ -n "${GPG_PASSPHRASE-}" ]` and the import action accepts an empty
passphrase, so omitting the secret is the supported path, not an
oversight. Say which secret is actually required, say why the other is
absent, and keep the setup snippet's passphrase advice for anyone
generating a new key.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@qodo-code-review

qodo-code-review Bot commented Sep 16, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Maintainers see the wrong tag target 🐞
Description
approve.sh presents the workflow run's headSha as the commit to be tagged, while the tagging job
checks out pull_request.merge_commit_sha instead. Release pull requests are squash-merged, so
their head commit differs from the merge commit in normal operation and the confirmation can
authorize a target other than the one shown.
Code

scripts/release/approve.sh[96]

+head_sha="$(printf '%s' "$waiting" | jq -r '.[0].headSha')"
Evidence
The helper requests and prints the workflow run's headSha, which belongs to the release branch.
The release workflow instead checks out github.event.pull_request.merge_commit_sha and creates the
tag from that checked-out HEAD; repository policy requires squash merges, which create a distinct
merge commit.

scripts/release/approve.sh[64-65]
scripts/release/approve.sh[95-99]
scripts/release/approve.sh[116-123]
.github/workflows/release-tag.yml[32-38]
.github/workflows/release-tag.yml[63-78]
.github/workflows/release-tag.yml[97-99]
AGENTS.md[30-34]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The approval summary displays the workflow run's source-branch `headSha`, but the release workflow tags the pull request's merge commit. Resolve the merge commit associated with the selected workflow run before asking for approval.

## Fix Focus Areas
- scripts/release/approve.sh[64-65]
- scripts/release/approve.sh[95-123]
- tests/test-release-approve.sh[60-62]
- tests/test-release-approve.sh[172-196]

## Recommended Fix
Use the selected workflow run's associated pull request number to retrieve its `merge_commit_sha`, fail safely if that association or SHA cannot be resolved, and display that SHA in the confirmation. Extend the stub and tests so the branch head and merge commit differ and assert that the merge commit is shown.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Maintainers get false secret guidance 🐞
Description
RELEASING.md says omitting the passphrase secret is what sign-artifacts.sh tests for, but the
script only tests whether the resulting environment value is nonempty. The workflow always exports
that value and the setup block unconditionally sets the secret, so absent and explicitly empty
secrets follow the same signing path despite the documented distinction.
Code

RELEASING.md[69]

+This repository's release key has no passphrase, so `RELEASE_GPG_PASSPHRASE` is deliberately absent and `gh secret list` shows only the private key. `scripts/release/sign-artifacts.sh` branches on whether the passphrase is set, and the import action accepts an empty one, so this is a supported path rather than a misconfiguration — v0.1.0 through v0.1.2 were all signed this way. Do not "fix" the missing secret by setting it to an empty string; omitting it is what the branch tests for.
Evidence
The guide explicitly attributes behavior to omission, but the publishing workflow maps the secret
into GPG_PASSPHRASE and the signing script branches solely on whether that value is nonempty. The
setup instructions also say setting the secret is conditional immediately before an unconditional
gh secret set command.

RELEASING.md[69-69]
RELEASING.md[89-92]
.github/workflows/release-publish.yml[80-84]
scripts/release/sign-artifacts.sh[45-59]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The release guide incorrectly claims that signing distinguishes an omitted passphrase secret from an explicitly empty one, while the implementation only distinguishes empty from nonempty values.

## Fix Focus Areas
- RELEASING.md[69-69]
- RELEASING.md[89-92]

## Recommended Fix
Explain that absent and empty passphrase values behave identically during signing, while omission is preferred to avoid a meaningless secret. Make the setup command conditional so it does not create `RELEASE_GPG_PASSPHRASE` when the selected key has no passphrase.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Status command failures pass tests 📘
Description
test-release-approve.sh invokes run_approve "$log" --status but validates only the captured
output without saving or checking the command's exit status. A regression that emits the expected
version and URL before returning nonzero leaves this assertion counted as passed and can let the
aggregate suite succeed.
Code

tests/test-release-approve.sh[R168-169]

+out="$(GH_STUB_WAITING="$WAITING_RUN" GH_STUB_RELEASES='[]' \
+	run_approve "$log" --status)"
Evidence
Compliance rule 406873 requires shell commands whose success matters to have fail-fast behavior or
an explicit exit-status check. The cited test captures output from the status command but, unlike
adjacent tests, does not capture or inspect its status.

Rule 406873: Do not silently ignore exit codes in shell scripts
tests/test-release-approve.sh[168-174]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The pending-release status test ignores the exit status from `run_approve`, allowing a failing command to satisfy its output-only assertion.

## Fix Focus Areas
- tests/test-release-approve.sh[168-174]

## Recommended Fix
Capture `$?` immediately after the command substitution and require a zero status together with the existing version and URL checks.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Test setup failures reach the real API 📘
Description
test-release-approve.sh enables nounset and pipefail but not errexit, then leaves mktemp,
stub creation, and chmod without explicit checks. If any stub setup command fails, later --yes
cases continue with the caller's gh from PATH, so an authenticated checkout can send the
approval POST to the real repository.
Code

tests/test-release-approve.sh[9]

+set -uo pipefail
Evidence
Rules 406873 and 406886 require critical shell commands to be covered by set -e or explicit status
handling. The new test enables only set -uo pipefail, while its temporary directory, stub-file
creation, and executable-bit setup are prerequisites for preventing real network access and are left
unchecked.

Rule 406873: Do not silently ignore exit codes in shell scripts
Rule 406886: Shell scripts must not ignore exit codes and must handle errors explicitly
tests/test-release-approve.sh[9-9]
tests/test-release-approve.sh[35-58]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The release approval tests continue after failures while creating or enabling the `gh` stub, which can allow subsequent approval cases to invoke a real authenticated GitHub CLI.

## Fix Focus Areas
- tests/test-release-approve.sh[9-58]

## Recommended Fix
Make each setup operation fail the test immediately with a clear non-zero exit. If enabling `errexit`, restructure commands whose statuses are intentionally captured so expected failures remain explicitly handled.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Two scripts omit explicit success exits 📘
Description
approve.sh falls off the approval path after two echo commands, and test-release-approve.sh
ends with a bare [ "$FAILED" -eq 0 ] expression. Successful approval and aggregate test completion
therefore depend on the last utility's status rather than terminating each main path with an
explicit numeric exit.
Code

scripts/release/approve.sh[R148-149]

+echo "Approved. $tag will be tagged and published."
+echo "Watch it: gh run watch $run_id"
Evidence
Rule 406878 requires an explicit exit on every main success and error path. The approval path ends
after output, while the test suite delegates its process status to a final test expression instead
of issuing explicit exits.

Rule 406878: Shell scripts must use explicit exit codes
scripts/release/approve.sh[148-149]
tests/test-release-approve.sh[198-200]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new approval tool and its test both reach the end of a main execution path without an explicit numeric exit code.

## Fix Focus Areas
- scripts/release/approve.sh[148-149]
- tests/test-release-approve.sh[198-200]

## Recommended Fix
Add `exit 0` after the successful approval output. Replace the test's final bare comparison with an explicit conditional that calls `exit 0` when no tests failed and `exit 1` otherwise.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Release approval tests are not portable 📘
Description
test-release-approve.sh declares Bash and uses BASH_SOURCE, local, [[ ... ]], arithmetic
commands, and here-strings instead of the required /bin/sh dialect. Direct execution therefore
depends on Bash, and invoking it under a strict POSIX shell reaches syntax errors throughout the new
release-approval suite.
Code

tests/test-release-approve.sh[1]

+#!/usr/bin/env bash
Evidence
Rules 406876 and 406892 require shell scripts to use exactly #!/bin/sh and avoid Bash-specific
syntax. The new test instead selects Bash and relies on multiple constructs unavailable in POSIX
sh.

Rule 406876: Enforce POSIX-compliant shell scripts (no bashisms, use #!/bin/sh)
Rule 406892: Standardize shell script shebang to /bin/sh
tests/test-release-approve.sh[1-23]
tests/test-release-approve.sh[90-111]
tests/test-release-approve.sh[183-185]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new release approval test uses a Bash shebang and several Bash-only constructs even though reviewed shell scripts must use `/bin/sh` and POSIX syntax.

## Fix Focus Areas
- tests/test-release-approve.sh[1-200]

## Recommended Fix
Change the shebang to `#!/bin/sh` and replace `BASH_SOURCE`, `local`, `[[ ... ]]`, arithmetic commands, here-strings, and `echo -e` with POSIX-compatible equivalents while preserving all test cases.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (3)
7. Busy repositories hide release requests 🐞
Description
--status filters release branches only after gh pr list returns its default limited page,
because the command supplies no --limit or paginated query. When more open pull requests precede
the release pull request than fit on that page, the status command prints none even though a
release is pending.
Code

scripts/release/approve.sh[R74-76]

+	open_pr="$(gh pr list --state open --json number,title,headRefName \
+		--jq '[.[] | select(.headRefName | startswith("release/"))]
+		      | map("#\(.number) \(.title)") | join(", ") // empty')"
Evidence
The only pull-request lookup has no limit or pagination option, and branch filtering occurs locally
in the subsequent jq expression. The resulting empty value is rendered as none, despite the
documented promise to show any open release pull request.

scripts/release/approve.sh[74-77]
RELEASING.md[33-40]
Makefile[125-127]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The status query filters release pull requests after fetching only the GitHub CLI's default result page. A release pull request outside that page is incorrectly reported as absent.

## Fix Focus Areas
- scripts/release/approve.sh[74-77]
- tests/test-release-approve.sh[38-55]
- tests/test-release-approve.sh[98-121]

## Recommended Fix
Use a paginated pull-request query, or otherwise retrieve all open pull requests before selecting branches beginning with `release/`. Add a test where the release pull request appears beyond the first default-sized page and verify it remains visible.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Release lookup failures look like none 🐞
Description
--status converts every nonzero result from gh release view into the literal none and still
exits successfully. A transient API, authentication, or permission failure after run discovery
therefore produces an authoritative-looking empty release state instead of telling the maintainer
that status retrieval failed.
Code

scripts/release/approve.sh[71]

+	latest="$(gh release view --json tagName --jq .tagName 2>/dev/null || echo "none")"
Evidence
The run lookup immediately above preserves failures and reports them, but the release lookup
suppresses stderr and replaces every failure with none, which is then printed as the latest
release. The documentation presents this output as the repository's release status.

scripts/release/approve.sh[58-66]
scripts/release/approve.sh[70-72]
RELEASING.md[33-40]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Status mode treats every release lookup failure as proof that no release exists. It must distinguish an empty release list from an unsuccessful API request.

## Fix Focus Areas
- scripts/release/approve.sh[70-72]
- tests/test-release-approve.sh[38-55]
- tests/test-release-approve.sh[82-106]

## Recommended Fix
Query releases with an operation that returns an empty JSON list successfully when none exist, derive `none` from that empty result, and call `die` when the command itself fails. Add separate tests for an empty list and a failed release query.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Mutable test counters use capitals 📘
Description
PASSED and FAILED are assigned as mutable counters despite the required
lowercase-with-underscores style. Both names are incremented and read across every assertion and the
final summary, so later test additions inherit a second local-variable convention.
Code

tests/test-release-approve.sh[R18-19]

+PASSED=0
+FAILED=0
Evidence
Rule 406880 requires script-defined mutable variables to use lowercase letters, digits, and
underscores. These counters are declared in uppercase and subsequently incremented, proving they are
not constants.

Rule 406880: Use lowercase_with_underscores for shell variable names
tests/test-release-approve.sh[18-32]
tests/test-release-approve.sh[198-200]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new test uses uppercase names for mutable counters, which do not qualify for the constant or external-environment exceptions.

## Fix Focus Areas
- tests/test-release-approve.sh[18-32]
- tests/test-release-approve.sh[198-200]

## Recommended Fix
Rename `PASSED` and `FAILED` to `passed` and `failed`, then update every increment, comparison, and summary reference consistently.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 26 rules
Review mode: 🚀 Fast: This push makes a localized documentation clarification and adds a focused exit-status assertion to an existing shell test, with no production behavior or high-risk surface changed.

Grey Divider

Tip of the day
💡 Did you know, you can group findings by type and pick your Finding display, from Minimal to Full

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit 3a1cc5c

Results up to commit 7bc2f1c ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. Maintainers see the wrong tag target 🐞
Description
approve.sh presents the workflow run's headSha as the commit to be tagged, while the tagging job
checks out pull_request.merge_commit_sha instead. Release pull requests are squash-merged, so
their head commit differs from the merge commit in normal operation and the confirmation can
authorize a target other than the one shown.
Code

scripts/release/approve.sh[96]

+head_sha="$(printf '%s' "$waiting" | jq -r '.[0].headSha')"
Evidence
The helper requests and prints the workflow run's headSha, which belongs to the release branch.
The release workflow instead checks out github.event.pull_request.merge_commit_sha and creates the
tag from that checked-out HEAD; repository policy requires squash merges, which create a distinct
merge commit.

scripts/release/approve.sh[64-65]
scripts/release/approve.sh[95-99]
scripts/release/approve.sh[116-123]
.github/workflows/release-tag.yml[32-38]
.github/workflows/release-tag.yml[63-78]
.github/workflows/release-tag.yml[97-99]
AGENTS.md[30-34]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The approval summary displays the workflow run's source-branch `headSha`, but the release workflow tags the pull request's merge commit. Resolve the merge commit associated with the selected workflow run before asking for approval.

## Fix Focus Areas
- scripts/release/approve.sh[64-65]
- scripts/release/approve.sh[95-123]
- tests/test-release-approve.sh[60-62]
- tests/test-release-approve.sh[172-196]

## Recommended Fix
Use the selected workflow run's associated pull request number to retrieve its `merge_commit_sha`, fail safely if that association or SHA cannot be resolved, and display that SHA in the confirmation. Extend the stub and tests so the branch head and merge commit differ and assert that the merge commit is shown.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
2. Mutable test counters use capitals 📘
Description
PASSED and FAILED are assigned as mutable counters despite the required
lowercase-with-underscores style. Both names are incremented and read across every assertion and the
final summary, so later test additions inherit a second local-variable convention.
Code

tests/test-release-approve.sh[R18-19]

+PASSED=0
+FAILED=0
Evidence
Rule 406880 requires script-defined mutable variables to use lowercase letters, digits, and
underscores. These counters are declared in uppercase and subsequently incremented, proving they are
not constants.

Rule 406880: Use lowercase_with_underscores for shell variable names
tests/test-release-approve.sh[18-32]
tests/test-release-approve.sh[198-200]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new test uses uppercase names for mutable counters, which do not qualify for the constant or external-environment exceptions.

## Fix Focus Areas
- tests/test-release-approve.sh[18-32]
- tests/test-release-approve.sh[198-200]

## Recommended Fix
Rename `PASSED` and `FAILED` to `passed` and `failed`, then update every increment, comparison, and summary reference consistently.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Test setup failures reach the real API 📘
Description
test-release-approve.sh enables nounset and pipefail but not errexit, then leaves mktemp,
stub creation, and chmod without explicit checks. If any stub setup command fails, later --yes
cases continue with the caller's gh from PATH, so an authenticated checkout can send the
approval POST to the real repository.
Code

tests/test-release-approve.sh[9]

+set -uo pipefail
Evidence
Rules 406873 and 406886 require critical shell commands to be covered by set -e or explicit status
handling. The new test enables only set -uo pipefail, while its temporary directory, stub-file
creation, and executable-bit setup are prerequisites for preventing real network access and are left
unchecked.

Rule 406873: Do not silently ignore exit codes in shell scripts
Rule 406886: Shell scripts must not ignore exit codes and must handle errors explicitly
tests/test-release-approve.sh[9-9]
tests/test-release-approve.sh[35-58]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The release approval tests continue after failures while creating or enabling the `gh` stub, which can allow subsequent approval cases to invoke a real authenticated GitHub CLI.

## Fix Focus Areas
- tests/test-release-approve.sh[9-58]

## Recommended Fix
Make each setup operation fail the test immediately with a clear non-zero exit. If enabling `errexit`, restructure commands whose statuses are intentionally captured so expected failures remain explicitly handled.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Two scripts omit explicit success exits 📘
Description
approve.sh falls off the approval path after two echo commands, and test-release-approve.sh
ends with a bare [ "$FAILED" -eq 0 ] expression. Successful approval and aggregate test completion
therefore depend on the last utility's status rather than terminating each main path with an
explicit numeric exit.
Code

scripts/release/approve.sh[R148-149]

+echo "Approved. $tag will be tagged and published."
+echo "Watch it: gh run watch $run_id"
Evidence
Rule 406878 requires an explicit exit on every main success and error path. The approval path ends
after output, while the test suite delegates its process status to a final test expression instead
of issuing explicit exits.

Rule 406878: Shell scripts must use explicit exit codes
scripts/release/approve.sh[148-149]
tests/test-release-approve.sh[198-200]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new approval tool and its test both reach the end of a main execution path without an explicit numeric exit code.

## Fix Focus Areas
- scripts/release/approve.sh[148-149]
- tests/test-release-approve.sh[198-200]

## Recommended Fix
Add `exit 0` after the successful approval output. Replace the test's final bare comparison with an explicit conditional that calls `exit 0` when no tests failed and `exit 1` otherwise.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Release approval tests are not portable 📘
Description
test-release-approve.sh declares Bash and uses BASH_SOURCE, local, [[ ... ]], arithmetic
commands, and here-strings instead of the required /bin/sh dialect. Direct execution therefore
depends on Bash, and invoking it under a strict POSIX shell reaches syntax errors throughout the new
release-approval suite.
Code

tests/test-release-approve.sh[1]

+#!/usr/bin/env bash
Evidence
Rules 406876 and 406892 require shell scripts to use exactly #!/bin/sh and avoid Bash-specific
syntax. The new test instead selects Bash and relies on multiple constructs unavailable in POSIX
sh.

Rule 406876: Enforce POSIX-compliant shell scripts (no bashisms, use #!/bin/sh)
Rule 406892: Standardize shell script shebang to /bin/sh
tests/test-release-approve.sh[1-23]
tests/test-release-approve.sh[90-111]
tests/test-release-approve.sh[183-185]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new release approval test uses a Bash shebang and several Bash-only constructs even though reviewed shell scripts must use `/bin/sh` and POSIX syntax.

## Fix Focus Areas
- tests/test-release-approve.sh[1-200]

## Recommended Fix
Change the shebang to `#!/bin/sh` and replace `BASH_SOURCE`, `local`, `[[ ... ]]`, arithmetic commands, here-strings, and `echo -e` with POSIX-compatible equivalents while preserving all test cases.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Busy repositories hide release requests 🐞
Description
--status filters release branches only after gh pr list returns its default limited page,
because the command supplies no --limit or paginated query. When more open pull requests precede
the release pull request than fit on that page, the status command prints none even though a
release is pending.
Code

scripts/release/approve.sh[R74-76]

+	open_pr="$(gh pr list --state open --json number,title,headRefName \
+		--jq '[.[] | select(.headRefName | startswith("release/"))]
+		      | map("#\(.number) \(.title)") | join(", ") // empty')"
Evidence
The only pull-request lookup has no limit or pagination option, and branch filtering occurs locally
in the subsequent jq expression. The resulting empty value is rendered as none, despite the
documented promise to show any open release pull request.

scripts/release/approve.sh[74-77]
RELEASING.md[33-40]
Makefile[125-127]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The status query filters release pull requests after fetching only the GitHub CLI's default result page. A release pull request outside that page is incorrectly reported as absent.

## Fix Focus Areas
- scripts/release/approve.sh[74-77]
- tests/test-release-approve.sh[38-55]
- tests/test-release-approve.sh[98-121]

## Recommended Fix
Use a paginated pull-request query, or otherwise retrieve all open pull requests before selecting branches beginning with `release/`. Add a test where the release pull request appears beyond the first default-sized page and verify it remains visible.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (1)
7. Release lookup failures look like none 🐞
Description
--status converts every nonzero result from gh release view into the literal none and still
exits successfully. A transient API, authentication, or permission failure after run discovery
therefore produces an authoritative-looking empty release state instead of telling the maintainer
that status retrieval failed.
Code

scripts/release/approve.sh[71]

+	latest="$(gh release view --json tagName --jq .tagName 2>/dev/null || echo "none")"
Evidence
The run lookup immediately above preserves failures and reports them, but the release lookup
suppresses stderr and replaces every failure with none, which is then printed as the latest
release. The documentation presents this output as the repository's release status.

scripts/release/approve.sh[58-66]
scripts/release/approve.sh[70-72]
RELEASING.md[33-40]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Status mode treats every release lookup failure as proof that no release exists. It must distinguish an empty release list from an unsuccessful API request.

## Fix Focus Areas
- scripts/release/approve.sh[70-72]
- tests/test-release-approve.sh[38-55]
- tests/test-release-approve.sh[82-106]

## Recommended Fix
Query releases with an operation that returns an empty JSON list successfully when none exist, derive `none` from that empty result, and call `die` when the command itself fails. Add separate tests for an empty list and a failed release query.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread tests/test-release-approve.sh
Comment thread tests/test-release-approve.sh
Comment thread tests/test-release-approve.sh
Comment thread scripts/release/approve.sh
Comment thread scripts/release/approve.sh Outdated
Comment thread scripts/release/approve.sh Outdated
Comment thread scripts/release/approve.sh Outdated
Qodo reviewed 7bc2f1c and raised seven findings. Four are real.

The high one is a genuine bug. approve.sh printed the waiting run's
headSha as the commit to be tagged, but release-tag.yml checks out
`pull_request.merge_commit_sha`, and this repo squash-merges, so the
two always differ. Verified against the release that just shipped:
the run reported 3179e83 while v0.1.2 landed on 62c098a. A prompt
whose whole job is showing what you are approving was naming a commit
the tag never points at. It now resolves the merged PR for the
release branch and shows that merge commit, refusing to approve when
it cannot be resolved rather than falling back to the branch tip.

The test could reach the real GitHub CLI. Setup was unchecked, so a
failed mktemp or chmod would leave `gh` resolving to the caller's
authenticated binary and the --yes cases would POST an approval
against whatever release was actually waiting. Setup is now fail-fast
and ends by asserting the stub is what `gh` resolves to.

`gh release view` exits non-zero both when no release exists and when
the call fails, so `|| echo none` reported an outage as an empty
repository; `release list` separates them. `gh pr list` used the
default 30-item page, so a release PR behind thirty open PRs read as
absent. The approval path now exits 0 explicitly.

Three findings are declined, all style claims contradicted by the
repo's own conventions: the bash shebang, BASH_SOURCE, `local` and
`[[ ]]` match every other file in tests/; PASSED/FAILED match
tests/test-parse-version.sh; and that same file ends on a bare
`[ "$FAILED" -eq 0 ]`. CLAUDE.md's POSIX rule covers scripts/, which
shellcheck --shell=sh enforces separately.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@michen00

Copy link
Copy Markdown
Owner Author

/agentic_review

@michen00

Copy link
Copy Markdown
Owner Author

[Review-Convergence] Round 1: active

  • Head: 4d8edb7
  • Base ref: main
  • Base: 62c098a
  • CI: pending (new head)
  • Bot threads: 0 unresolved (7 replied and resolved)
  • Human threads: 0 unresolved
  • Clean signals: none yet
  • Pending reviewers: Qodo
  • Catch-up: none
  • Next action: wait for Qodo review of 4d8edb7
  • Next wakeup: ~4min (reviewer-pending first-tier floor)
  • Bulletin: Review convergence: PR #91 #92

Qodo raised 7 findings on 7bc2f1c. 4 fixed in 4d8edb7 — including a real one: the approval prompt showed the run's headSha while Release Tag tags merge_commit_sha, which differ on every squash-merged release (v0.1.2's run reported 3179e83; the tag landed on 62c098a). 3 declined as style claims contradicted by the repo's own conventions. Per-finding replies are on each thread.

Quorum for this PR is 1 clean Qodo pass, per operator instruction.

Comment thread tests/test-release-approve.sh
Comment thread RELEASING.md Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 4d8edb7

Qodo is right, and this is a claim I introduced one commit ago while
fixing a different wrong claim about the same secret.

RELEASING said omitting RELEASE_GPG_PASSPHRASE "is what the branch
tests for". It is not. Both workflows pass the secret through
unconditionally, an absent secret expands to an empty string, and
sign-artifacts.sh branches on `[ -n "${GPG_PASSPHRASE-}" ]` — empty
versus nonempty, never absent versus present. An empty secret and no
secret sign identically.

The advice to omit it still holds, but the reason had to change: not
that signing can tell the difference, but that a secret which exists
and means nothing invites someone to later fill it with a passphrase
the key does not have. The setup snippet now guards the `gh secret
set` on a non-empty passphrase instead of telling the reader to set
it conditionally while unconditionally setting it.

Also captured the exit status in the status-mode test that asserted
on output alone, so a regression that prints the right text and then
fails cannot pass.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@michen00

Copy link
Copy Markdown
Owner Author

/agentic_review

@michen00

Copy link
Copy Markdown
Owner Author

[Review-Convergence] Round 2: active

  • Head: 3a1cc5c
  • Base ref: main
  • Base: 62c098a
  • CI: pending (new head)
  • Bot threads: 0 unresolved (2 replied and resolved this round)
  • Human threads: 0 unresolved
  • Clean signals: none yet
  • Pending reviewers: Qodo
  • Catch-up: none
  • Next action: wait for Qodo review of 3a1cc5c
  • Next wakeup: ~4min (reviewer-pending first-tier floor)
  • Bulletin: Review convergence: PR #91 #92

Qodo's fresh pass on 4d8edb7 found 2 more, both real and both fixed in 3a1cc5c: a status-mode test that asserted on output while ignoring the exit status, and a false claim in RELEASING.md about how the passphrase secret is detected. The second was text I added last round while fixing a different wrong claim about the same secret — an absent secret and an empty one sign identically, so "omitting it is what the branch tests for" was not true.

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 3a1cc5c

@michen00

Copy link
Copy Markdown
Owner Author

[Review-Convergence] Round 3: converged-merge-blocked

  • Head: 3a1cc5c
  • Base ref: main
  • Base: 62c098a
  • CI: green (12 pass, 1 skipped, 0 failing)
  • Bot threads: 0 unresolved (9 total, all replied and resolved)
  • Human threads: 0 unresolved
  • Clean signals: Qodo (Bugs 0, Rule violations 0, Skill insights 0 at 3a1cc5c)
  • Pending reviewers: none
  • Catch-up: none — base is an ancestor of head
  • Bulletin: Review convergence: PR #91 #92

Bot-review convergence is complete. Qodo confirms it reviewed up to 3a1cc5c and reports zero findings, which meets the quorum of 1 clean Qodo pass set for this PR.

Remaining blocker is human, not bot: reviewDecision is REVIEW_REQUIRED, so the branch ruleset still wants an approving review. This runner does not merge or approve.

Three rounds, nine Qodo findings: six fixed, three declined as style claims the repo's own conventions contradict. The one worth a second look before merging is the GPG passphrase paragraph in RELEASING.md — it was corrected twice, in two different ways, and is the only part of this PR whose accuracy rests on my reading rather than on a test.

@michen00
michen00 merged commit 2b1365a into main Sep 16, 2026
14 checks passed
@michen00
michen00 deleted the docs/extract-releasing-md branch September 16, 2026 04:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Review convergence: PR #91

2 participants