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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ their notes were not backfilled here rather than reconstructed after the fact.
- an unquoted shell comment is no longer read as arguments. `gh release create v1.2.3 # --verify-tag` offered a flag the shell discards, so the guard allowed the invocation and bash ran the bare create that can mint a lightweight tag; `gh release delete v1.2.3 # --help` passed the same way. The comment is now stripped with the quoted spans, before either check, and the notes-only test for `gh release edit` uses the same stripping. Found by CodeRabbit on the pull request that introduced the `--verify-tag` exemption, reproduced before the fix, and pinned by seven cases including two where a `#` is part of an argument rather than a comment
- `gh release create --help` and `gh release delete -h` are no longer blocked. Reading the help runs no release operation, and the blocked command was the one that would have explained `--verify-tag`
- a block message spanning several lines emitted its continuation lines at column 0, which ends the YAML block scalar and leaves the rest as stray text
- `release-status.sh` finds the tag's workflow run again once other runs have happened since the tag. It filtered the 12 newest runs of every workflow on its own side, so Renovate, a merge queue and CI pushed the release run out of that window within hours: `netresearch/raybeam` v1.2.0 and `netresearch/terraform-provider-ad` v0.5.3 both reported `workflow : none` while their release runs had succeeded, and a failed release run would have been missed the same way. The lookup now asks GitHub for the tag's runs with `--branch`, prefers the run whose name says release or publish over CI started by the same push, prints `unknown` rather than `none` when the lookup itself fails, and prints `in_progress/-` for a running run, where gh's empty `conclusion` used to leave `in_progress/`

### Added

- `release-status.sh --watch` waits while the tag's publishing workflow has not completed, prints each state change to stderr, and then gives the normal verdict. Until now the release side had no counterpart to `pr-status.sh --watch`, and every wait on a release run was a hand-written loop


## [1.0.4] - 2026-09-20
Expand Down
4 changes: 4 additions & 0 deletions commands/release-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ Run the manual inspection below only to explain a verdict it already gave, or
when the script is unavailable — reasoning the phase out by hand is how a step
gets skipped.

Right after pushing a tag, add `--watch`: the script then waits until the tag's
publishing workflow has completed and reports the verdict after it, instead of
answering `await-release-workflow` at once.

### 1. List recent releases

Run:
Expand Down
3 changes: 2 additions & 1 deletion skills/github-release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ Where the repository has a workflow that publishes the release, that workflow do
## Start Here

`scripts/release-status.sh -R owner/repo` reports the phase and a computed
`NEXT`, exiting 0 only when finished. `scripts/release-notes-status.sh`
`NEXT`, exiting 0 only when finished; after a tag push, `--watch` waits for
the tag's publishing workflow first. `scripts/release-notes-status.sh`
checks the published body.

## Release Flow
Expand Down
58 changes: 52 additions & 6 deletions skills/github-release/scripts/release-status.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,23 @@
#
# ./release-status.sh [-R owner/repo]
# ./release-status.sh -R owner/repo --json
# ./release-status.sh -R owner/repo --watch # wait for the tag's workflow, then report
#
# --watch waits only while the tag exists and its publishing workflow has not
# completed: it prints each state change to stderr and then gives the normal
# verdict. RELEASE_STATUS_WATCH_INTERVAL (seconds, default 20) and
# RELEASE_STATUS_WATCH_TIMEOUT (default 2700) tune it.
#
# Exit: 0 = ok, 1 = action needed, 2 = usage/lookup error.
set -euo pipefail

REPO=""; JSON=0
REPO=""; JSON=0; WATCH=0
while [ $# -gt 0 ]; do
case "$1" in
-R|--repo) REPO="$2"; shift 2 ;;
--json) JSON=1; shift ;;
-h|--help) sed -n '2,26p' "$0"; exit 0 ;;
--watch) WATCH=1; shift ;;
-h|--help) sed -n '2,29p' "$0"; exit 0 ;;
*) shift ;;
esac
done
Expand Down Expand Up @@ -166,11 +173,49 @@
fi

# --- phase 4: the publishing workflow ---------------------------------------
# Ask GitHub for the tag's runs by name. Filtering the 12 newest runs of EVERY
# workflow on this side found nothing once a dozen other runs had happened since
# the tag -- Renovate, a merge queue and CI get there in hours -- and reported
# "none" for a release whose run existed (netresearch/raybeam v1.2.0, run
# 32944806553, measured 2026-09-22). A failed release run falls out of that
# window just the same, and then nothing reports it.
#
# One tag push can start several workflows (CI beside Release). The publishing
# one is the run whose name says release or publish; only without such a run is
# the newest run on the tag taken. A failed lookup is "unknown", not "none":
# "the query failed" and "there is no run" must stay distinguishable. gh gives a
# run in progress `conclusion: ""`, which jq's `//` treats as present, so the
# empty string is tested for explicitly.
wf_query() {
gh run list --repo "$REPO" --branch "v$declared" --limit 20 \
--json status,conclusion,name \
--jq '([.[] | select(.name|test("release|publish";"i"))] + .) | .[0]
| if .==null then "none"
else "\(.status)/\(if (.conclusion // "") == "" then "-" else .conclusion end)" end' \
2>/dev/null || echo "unknown"
}

wf_state=""
if [ "$tag_state" = "annotated" ]; then
wf_state=$(gh run list --repo "$REPO" --limit 12 \
--json headBranch,status,conclusion,name \
--jq "[.[] | select(.headBranch==\"v$declared\")] | .[0] | if .==null then \"none\" else \"\(.status)/\(.conclusion // \"-\")\" end" 2>/dev/null || echo "none")
wf_state=$(wf_query)
if [ "$WATCH" = 1 ]; then

Check failure on line 201 in skills/github-release/scripts/release-status.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=netresearch_github-release-skill&issues=AaDKs0dnmiEzgr9Xmi-l&open=AaDKs0dnmiEzgr9Xmi-l&pullRequest=150
interval="${RELEASE_STATUS_WATCH_INTERVAL:-20}"
deadline=$(( $(date +%s) + ${RELEASE_STATUS_WATCH_TIMEOUT:-2700} ))
last=""
while [ "${wf_state%%/*}" != "completed" ]; do

Check failure on line 205 in skills/github-release/scripts/release-status.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=netresearch_github-release-skill&issues=AaDKs0dnmiEzgr9Xmi-m&open=AaDKs0dnmiEzgr9Xmi-m&pullRequest=150
if [ "$wf_state" != "$last" ]; then

Check failure on line 206 in skills/github-release/scripts/release-status.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=netresearch_github-release-skill&issues=AaDKs0dnmiEzgr9Xmi-n&open=AaDKs0dnmiEzgr9Xmi-n&pullRequest=150
printf 'watch: v%s workflow %s\n' "$declared" "$wf_state" >&2
last="$wf_state"
fi
if [ "$(date +%s)" -ge "$deadline" ]; then

Check failure on line 210 in skills/github-release/scripts/release-status.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=netresearch_github-release-skill&issues=AaDKs0dnmiEzgr9Xmi-o&open=AaDKs0dnmiEzgr9Xmi-o&pullRequest=150
add_note "watch timed out while the workflow was $wf_state"
break
fi
sleep "$interval"
wf_state=$(wf_query)
done
[ "${wf_state%%/*}" = "completed" ] && printf 'watch: v%s workflow %s\n' "$declared" "$wf_state" >&2

Check failure on line 217 in skills/github-release/scripts/release-status.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=netresearch_github-release-skill&issues=AaDKs0dnmiEzgr9Xmi-p&open=AaDKs0dnmiEzgr9Xmi-p&pullRequest=150
fi
fi

# --- phase 5: is the published body finished? --------------------------------
Expand Down Expand Up @@ -273,7 +318,8 @@
printf ' declared : %s%s\n' "${declared:-<none>}" "$([ "$version_source" = release ] && echo ' (from the latest release; no version file in the tree)')"
printf ' latest rel : %s\n' "${latest:-<none>}"
printf ' tag : %s\n' "${tag_state:-n/a}"
[ -n "$wf_state" ] && printf ' workflow : %s\n' "$wf_state"
[ -n "$wf_state" ] && printf ' workflow : %s%s\n' "$wf_state" \

Check failure on line 321 in skills/github-release/scripts/release-status.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=netresearch_github-release-skill&issues=AaDKs0dnmiEzgr9Xmi-q&open=AaDKs0dnmiEzgr9Xmi-q&pullRequest=150
"$([ "$wf_state" = none ] && echo " (no run found for v$declared)")"

Check failure on line 322 in skills/github-release/scripts/release-status.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=netresearch_github-release-skill&issues=AaDKs0dnmiEzgr9Xmi-r&open=AaDKs0dnmiEzgr9Xmi-r&pullRequest=150
[ -n "$notes_next" ] && printf ' notes : %s\n' "$notes_next"
[ -n "$reg_missing" ] && printf ' registries : missing on%s\n' "$reg_missing"
echo
Expand Down
85 changes: 85 additions & 0 deletions skills/github-release/scripts/tests/release-status.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,91 @@
check "versionless with no release still says so" "no version file found" "$norelease_out"
check "and still names the manifests" "herdr-plugin.toml" "$norelease_out"

# ---------------------------------------------------------------------------
# The tag's workflow run, behind a dozen newer runs of other workflows
# ---------------------------------------------------------------------------
# netresearch/raybeam v1.2.0 reported "workflow : none" on 2026-09-22 although
# its Release run 32944806553 had succeeded: the script filtered the 12 newest
# runs of every workflow, and Renovate, the merge queue and CI had long pushed
# the tag's run out of that window. This stub behaves like gh: `run list` honours
# --branch and applies the --jq filter with real jq, so a lookup that does not
# ask for the tag gets the twelve foreign runs and nothing else.

runs=$(mktemp -d)
trap 'rm -rf "$work" "$addon" "$herdr" "$tagonly" "$runs"' EXIT
mkdir -p "$runs/bin" "$runs/repo"
ln -sf "$jq_path" "$runs/bin/jq"
cat >"$runs/bin/gh" <<'STUB'
#!/usr/bin/env bash
# A repo released as v6.4.0 whose tag push started CI and Release. Twelve CI
# runs on main came after it. RUNS_MODE selects the state of the tag's runs.
case "$1 $2" in
"auth status") exit 0 ;;
"repo view") echo "acme/runs"; exit 0 ;;
"pr list") echo "null"; exit 0 ;;
"release view") exit 1 ;;
"run list")
[ "${RUNS_MODE:-}" = fail ] && exit 1
branch=""; filter="."; shift 2
while [ $# -gt 0 ]; do
case "$1" in
--branch) branch="$2"; shift 2 ;;
--jq) filter="$2"; shift 2 ;;
*) shift ;;
esac
done
if [ "$branch" = "v6.4.0" ]; then
n=0; [ -f "$RUNS_COUNTER" ] && n=$(cat "$RUNS_COUNTER"); echo $((n + 1)) >"$RUNS_COUNTER"
rel='{"name":"Release","status":"completed","conclusion":"success","headBranch":"v6.4.0"}'
if [ "${RUNS_MODE:-}" = watch ] && [ "$n" -lt 2 ]; then
rel='{"name":"Release","status":"in_progress","conclusion":"","headBranch":"v6.4.0"}'
fi
# CI on the same tag is newer and still running; it is not the publisher.
data="[{\"name\":\"CI\",\"status\":\"in_progress\",\"conclusion\":\"\",\"headBranch\":\"v6.4.0\"},$rel]"
elif [ -z "$branch" ]; then
# Cancelled, so a foreign run can never pass for the tag's successful one.
data=$(jq -nc '[range(12) | {name:"Release",status:"completed",conclusion:"cancelled",headBranch:"main"}]')
else
data='[]'
fi
printf '%s' "$data" | jq -r "$filter"
exit 0 ;;
esac
if [ "$1" = api ]; then
case "$2" in
*/releases/latest) echo "v6.4.0"; exit 0 ;;
*/git/ref/tags/v6.4.0) echo "tag"; exit 0 ;;
*/git/ref/tags/*) exit 1 ;;
*/contents/*) exit 1 ;;
esac
fi
exit 1
STUB
chmod +x "$runs/bin/gh"

runs_env() { env -i PATH="$runs/bin:/usr/bin:/bin" HOME="$runs" RUNS_COUNTER="$runs/count" "$@"; }

Check warning on line 246 in skills/github-release/scripts/tests/release-status.test.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=netresearch_github-release-skill&issues=AaDKs0YMmiEzgr9Xmi-k&open=AaDKs0YMmiEzgr9Xmi-k&pullRequest=150

rm -f "$runs/count"
runs_out=$(cd "$runs/repo" && runs_env bash --noprofile --norc "$SCRIPT" -R acme/runs 2>&1)
check "finds the tag's run behind twelve newer ones" "workflow : completed/success" "$runs_out"
refute "does not report the run as missing" "workflow : none" "$runs_out"

# The failed lookup must not read as "there is no run".
rm -f "$runs/count"
fail_out=$(cd "$runs/repo" && runs_env RUNS_MODE=fail bash --noprofile --norc "$SCRIPT" -R acme/runs 2>&1)
check "a failed lookup says unknown" "workflow : unknown" "$fail_out"

# --watch: the Release run is in progress for the first two lookups, then done.
# The timeout is short so that a broken watch fails this test in seconds rather
# than holding it for the default 45 minutes.
rm -f "$runs/count"
watch_out=$(cd "$runs/repo" && runs_env RUNS_MODE=watch RELEASE_STATUS_WATCH_INTERVAL=0 \
RELEASE_STATUS_WATCH_TIMEOUT=3 bash --noprofile --norc "$SCRIPT" -R acme/runs --watch 2>&1)
check "watch reports the state it waited through" "watch: v6.4.0 workflow in_progress/-" "$watch_out"
check "watch ends on the completed run" "workflow : completed/success" "$watch_out"
refute "watch did not run into its timeout" "watch timed out" "$watch_out"
check "watch polled exactly until the state changed" "lookups=3;" "lookups=$(cat "$runs/count");"

# The guard is a case pattern over the value gh returned; a JSON error body
# contains characters a tag cannot.
tagshaped() { case "$1" in *[!A-Za-z0-9._-]* | "" | null) echo no ;; *) echo yes ;; esac; }
Expand Down
Loading