feat(ci): add reusable release-drafter and release-publish workflows#27
Conversation
yujiawei
left a comment
There was a problem hiding this comment.
Code Review — PR #27 (.github)
Summary
This PR adds two new reusable workflows — reusable-release-drafter.yml (rolling release-draft updater) and reusable-release-publish.yml (semver-gated publisher with optional CI-run evidence gate). The structural conventions (SHA-pinned actions, empty top-level permissions, scoped job-level grants, runs-on: ubuntu-24.04, workflow_call) match the patterns already established in reusable-pr-labeler.yml, and the surface area is additive only.
Happy paths work and the change is safe to land for the existing OSS consumers in this org. However, the optional evidence gate is the most-advertised feature in the PR body and design plan, and it has a real latent bug (missing actions: read scope) that will bite the first private-repo consumer silently. There are also a couple of release-lookup robustness issues worth tightening before this gets wired into business repos. None are merge-blocking for an additive, opt-in P1 batch, but they should be addressed in a quick follow-up.
Findings
P1 — Should fix soon (follow-up acceptable)
1. actions: read is missing on the publish job — the evidence gate will silently fail on private repos.
reusable-release-publish.yml:43-51 calls gh api repos/${{ github.repository }}/actions/runs/${RUN_ID}. That endpoint requires the actions permission (read) on the workflow token. The job only declares contents: write, and the top-level permissions: {} strips all defaults, so the token's actions scope ends up none.
- For public repos the call may succeed because the data is publicly readable, so this won't trip CI today.
- For any private/internal consumer (which the description implies is the target audience — "business repos"), the API call returns 403/404. Combined with the next finding, this means a
validate_run_idthat cannot be read looks identical to avalidate_run_idwhose CI failed, just with a confusingconclusion=(empty) error message.
Suggested fix:
jobs:
publish:
permissions:
contents: write
actions: read # required for actions/runs/{run_id} lookup2. 2>/dev/null swallows real failures from gh api, and set -e does not catch them inside command substitution.
reusable-release-publish.yml:48 and :66 both pipe stderr to /dev/null. Inside conclusion=$(gh api ... 2>/dev/null) and release_id=$(gh api ... 2>/dev/null | head -1), a non-zero exit from gh api does NOT propagate under set -euo pipefail (Bash explicitly excepts command substitution from errexit unless inherit_errexit is set). So a 403, network timeout, or rate-limit gets quietly converted to an empty string, and the user sees a misleading downstream error (or a duplicate release created — see finding 3).
Suggested fix: drop the 2>/dev/null, or check the exit code explicitly:
if ! conclusion=$(gh api "repos/${{ github.repository }}/actions/runs/${RUN_ID}" --jq '.conclusion'); then
echo "::error::Failed to query run ${RUN_ID} (token scope missing or run not found)"
exit 1
fi3. Existing-release lookup is not paginated; a draft outside the first page is treated as "not found" and a duplicate is created.
reusable-release-publish.yml:60-62:
release_id=$(gh api "repos/${{ github.repository }}/releases" \
--jq --arg tag "$TAG" '.[] | select(.tag_name == $tag) | .id' 2>/dev/null | head -1)gh api returns 30 releases per page by default. release-drafter typically keeps one rolling draft near the top of the list (sorted by created_at desc), so in practice this will usually hit. But:
- Repos that accumulate prerelease drafts, or use multiple per-branch drafters, can easily push the wanted draft off page 1.
- If the lookup silently misses the draft, the
elsebranch runs and creates a second release for the same tag, which leaves an orphan draft + a published release for the sametag_name. That's a manual-cleanup mess.
Suggested fix: use --paginate (and let --jq filter across pages), or use the dedicated endpoint gh api repos/.../releases/tags/$TAG (note: that endpoint only resolves published releases, not drafts, so it cannot fully replace the loop — --paginate is the safer choice here):
release_id=$(gh api --paginate "repos/${{ github.repository }}/releases" \
--jq --arg tag "$TAG" '.[] | select(.tag_name == $tag) | .id' | head -1)P2 — Nits / maintainability
4. The tag input docstring says "must be a tag that already exists" but the workflow doesn't enforce it, and no target_commitish is set on the create path.
reusable-release-publish.yml:8 documents tag-must-exist, but the create branch (:71-79) calls POST /releases without target_commitish. If the tag doesn't actually exist, GitHub will silently create it pointing at the default branch's HEAD — which may not be what the caller intended (consider a hotfix branch). Either enforce existence early:
if ! git rev-parse "refs/tags/$TAG" >/dev/null 2>&1; then
echo "::error::Tag $TAG does not exist in this repo"; exit 1
fi…or accept and document an optional target_commitish input. (Note: enforcing via git rev-parse requires actions/checkout first, since the workflow currently has no checkout step.)
5. The evidence gate validates "some run succeeded" but not "the run that built this tag's commit succeeded".
reusable-release-publish.yml:43-51 checks conclusion == success but doesn't cross-check head_sha against the tag's commit. A caller could pass any successful run_id from any branch and pass the gate. If the design intent is "publish only when CI for this exact ref succeeded", consider also asserting .head_sha == $tag_commit_sha. As-is, the gate is more of a sanity check than a security control — worth a comment in the input description, at minimum.
6. Semver error message omits the +build suffix that the regex actually allows.
reusable-release-publish.yml:30 regex includes (\+[0-9A-Za-z.-]+)?, but the error at :31 only says vMAJOR.MINOR.PATCH[-prerelease]. Trivial — update the message to vMAJOR.MINOR.PATCH[-prerelease][+build].
7. No workflow outputs: exposed.
Both workflows print html_url via --jq, but callers can't consume it as a workflow output for downstream jobs (changelog post, Slack notify, etc.). Consider adding:
outputs:
release_url:
description: 'URL of the created/updated release'
value: ${{ jobs.publish.outputs.release_url }}Not blocking, but a small DX improvement before consumers start chaining jobs.
8. PATCH path re-sends tag_name unnecessarily.
reusable-release-publish.yml:65-69: when patching an existing release, --field tag_name="${TAG}" is redundant (we already matched on it). Harmless but noisy.
Out-of-scope observations
add-to-projectcheck on this PR is failing (unrelated to the diff — it's the existingauto-add-to-project.ymlworkflow). Worth a separate look, but not this PR.- The PR description's caller example for
reusable-release-publish.ymldoesn't show how to obtainvalidate_run_id(typically via${{ github.event.workflow_run.id }}from aworkflow_runtrigger, or as a manualworkflow_dispatchinput). A README/example snippet in.github/would help adopters; can be its own follow-up.
Verdict
The structure and patterns are correct; the bugs above are localized to one job in reusable-release-publish.yml and don't block the additive rollout. Approving so the P1 batch can land, with a strong request to address findings 1–3 in a quick follow-up before any private-repo consumer wires up validate_run_id.
Jerry-Xin
left a comment
There was a problem hiding this comment.
Scope check passed; reusable GitHub Actions workflows are relevant to this .github shared workflow repository, but the publish workflow has blocking release-path bugs.
🔴 Blocking
-
🔴 Critical —
.github/workflows/reusable-release-publish.yml:63usesgh api --jq --arg tag "$TAG" ..., butgh apidoes not accept jq’s--argflag. This command exits before any release create/update path can run. Usegh api ... | jq -r --arg tag "$TAG" ..., or anotherghcommand that supports safe tag lookup. -
🔴 Critical —
.github/workflows/reusable-release-publish.yml:26only grantscontents: write, but line 48 calls the Actions workflow-run API. The optionalvalidate_run_idgate needsactions: read; otherwise callers using that advertised gate can fail with a permissions error. -
🔴 Critical —
.github/workflows/reusable-release-publish.yml:7says the tag must already exist, but lines 74-80 create a release without first verifying the tag ref exists. GitHub release creation can create the tag from the default branch when the tag is absent, so a typo likev1.4.00after regex acceptance could publish from the wrong commit. Add an explicit pre-write tag-ref check.
💬 Non-blocking
-
🟡 Warning —
.github/workflows/reusable-release-publish.yml:41validates only that the supplied run concludedsuccess; it does not bind that run to the release tag’s commit. For a stronger evidence gate, compare the workflow runhead_shawith the resolved tag target SHA. -
🟡 Warning —
.github/workflows/reusable-release-publish.yml:63lists releases without pagination. After the syntax bug is fixed, old releases outside the first page may be missed, causing duplicate-create attempts or avoidable failures. -
🔵 Suggestion —
.github/workflows/reusable-release-publish.yml:35is close to semver but still allows invalid prerelease/build identifiers such as empty dot-separated segments. Either tighten the regex or describe it as a release-tag pattern rather than strict semver.
✅ Highlights
- The release drafter workflow is appropriately scoped, SHA-pinned, and uses narrow job permissions.
- The publish workflow correctly attempts validation before writes and keeps default workflow permissions empty at the top level.
lml2468
left a comment
There was a problem hiding this comment.
Summary
Adds reusable-release-drafter.yml (rolling draft updater) and reusable-release-publish.yml (semver-gated publisher with optional CI evidence gate).
Findings
I concur with both prior reviewers. The drafter workflow is clean; the publish workflow has real issues that should be fixed before consumers wire it up:
P0 — gh api --jq --arg syntax is invalid (L63-64)
Jerry-Xin correctly flagged this. gh api does not support jq --arg — this command will fail at runtime on every invocation, making the release-lookup path completely broken. The existing-draft detection never works, so every publish creates a new release (duplicates). Must fix with | jq -r --arg tag "$TAG" ... or a different lookup approach.
P1 — Missing actions: read permission (L26-27)
The evidence gate (L48) calls the Actions API but the job only has contents: write. Will silently fail on private repos. Both prior reviewers noted this.
P1 — 2>/dev/null masks real API failures (L48, L64)
Stderr suppression turns 403/network errors into empty strings, producing misleading error messages downstream. Drop 2>/dev/null or check exit codes explicitly.
P1 — No pagination on release lookup (L63)
Default 30-per-page may miss drafts in repos with many releases, creating orphan duplicates.
P2 — Tag existence not enforced despite documented requirement (L7)
No checkout step means git rev-parse is unavailable. Either add checkout + tag check, or remove the "must exist" claim from the description.
P2 — Evidence gate does not bind run to tag commit (L48)
Any successful run_id from any branch passes. Worth a comment documenting this is a sanity check, not a cryptographic proof.
The drafter workflow (reusable-release-drafter.yml) is clean — correct SHA pin, appropriate permissions, minimal surface area.
Verdict
CHANGES_REQUESTED — the --jq --arg syntax bug (P0) means the publish workflow's core release-lookup path is broken at runtime. Combined with the missing permission and error masking, this needs a fix pass before merge. (Submitted as COMMENTED due to GitHub limitation, but the intent is CHANGES_REQUESTED.)
4b200d6 to
95914fc
Compare
Fix pushed (95914fc) — all 3 blocking issues addressed1. gh api .../releases --paginate | jq -rs --arg tag "$TAG" '[.[][] | select(.tag_name == $tag) | .id][0] // empty'( 2. Missing 3. No tag ref validation → added Additional hardening:
|
yujiawei
left a comment
There was a problem hiding this comment.
Code Review — PR #27 (.github)
Summary
Re-review of the fix commit 95914fc on top of the prior CHANGES_REQUESTED round. Both reusable workflows (reusable-release-drafter.yml, reusable-release-publish.yml) now look additive-only and the publish-path fixes match the points raised in the previous review. All P0 / P1 blockers from the earlier round are addressed; only minor nits remain.
Verification of prior findings
| Prior finding | Status | Evidence |
|---|---|---|
P0 — invalid gh api --jq --arg syntax (release lookup never ran) |
✅ Fixed | reusable-release-publish.yml:101-103 now uses gh api ... --paginate | jq -rs --arg tag "$TAG" '[.[][] | select(.tag_name == $tag) | .id][0] // empty'. gh api is no longer asked to forward jq flags; pagination is wired in at the same time. |
P1 — missing actions: read on the publish job |
✅ Fixed | reusable-release-publish.yml:63-65 now declares contents: write + actions: read. |
| P1 — release lookup not paginated (drafts off page 1 dropped) | ✅ Fixed | Same line as the P0 fix — --paginate plus jq -rs (slurp) correctly flattens multi-page output before filtering. |
| P2 — tag existence not enforced despite docstring | ✅ Fixed | New Verify tag ref exists step at reusable-release-publish.yml:46-58 calls repos/.../git/ref/tags/${TAG}, fails fast if missing, and exports TAG_SHA for downstream use. |
| P2 — evidence gate did not bind run to tag commit | ✅ Fixed | reusable-release-publish.yml:75-78 now compares the workflow run's head_sha against TAG_SHA and aborts on mismatch. Combined with making validate_run_id required: true (:11-14), the evidence gate is now actually enforced for every publish. |
| P2 — semver regex accepted leading zeros / empty prerelease segments | ✅ Fixed | :34-36 now uses (0|[1-9][0-9]*) for the numeric parts and rejects empty dot-segments inside prerelease. Verified with a small test matrix: v01.2.3, v1.2.3-, v1.2.3-. all rejected; v1.2.3, v1.2.3-alpha, v1.2.3-alpha.1 accepted. |
Remaining issues (non-blocking)
P2 — semver +build metadata is unreachable due to escape-level confusion
reusable-release-publish.yml:36 — the build-metadata branch is written as (\\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?. The pattern lives inside a YAML block scalar (no escapes) and is then passed to bash inside single quotes (no escapes), so grep -E actually receives \\+, which in ERE means "one or more literal backslashes" — not a literal +. Verified with a small test matrix:
v1.2.3 → MATCH
v1.2.3+build → REJECT (should be accepted per docstring)
v1.2.3+build.5 → REJECT
v1.2.3\+build → REJECT
Effect is minor — build metadata is rare in release tags and the segment is optional, so the primary vX.Y.Z[-prerelease] path is unaffected. Suggested fix is just (\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))? (single backslash, which in ERE escapes + to a literal).
P2 — 2>/dev/null still hides real gh api failures in two places
reusable-release-publish.yml:51 (Verify tag ref exists) and :103 (Validate CI run …) still pipe stderr to /dev/null. The downstream checks ([ -z "$tag_sha" ], conclusion != success, run_sha != TAG_SHA) all fail closed, so this is safe in practice — but a 403 / rate-limit / network error will surface as a confusing "tag does not exist" or conclusion=null message rather than the real cause. Dropping the 2>/dev/null (or guarding with if ! ... ; then echo "::error::Failed to query ..." ) would make on-call debugging easier without changing the safety properties.
P2 — workflow / job outputs still not declared
reusable-release-publish.yml:140 writes release_url=… to $GITHUB_OUTPUT, but the publish job has no outputs: map and the workflow_call has no top-level outputs:. Callers cannot consume the URL to chain a changelog post / Slack notify job. Adding:
on:
workflow_call:
outputs:
release_url:
description: 'URL of the created or updated release'
value: ${{ jobs.publish.outputs.release_url }}
jobs:
publish:
outputs:
release_url: ${{ steps.publish.outputs.release_url }}
steps:
- name: Publish GitHub Release
id: publish
...…would close the loop. Pure DX improvement, easy follow-up.
Nit — PATCH path re-sends tag_name
reusable-release-publish.yml:131-135: when updating an existing release, --field tag_name="${TAG}" is redundant because the release was matched on tag_name == $tag. Harmless, but noise.
Additional observations (no change needed)
reusable-release-drafter.ymlis unchanged from the previous round and remains correctly scoped: SHA-pinnedrelease-drafter/release-drafter@67e173ca…, empty top-levelpermissions, narrow job grants (contents: write,pull-requests: read),runs-on: ubuntu-24.04.TAG_SHAis exported viaGITHUB_ENVand consumed in the next step — that pattern works, but worth being aware that a force-pushed tag between the verify and publish steps is a theoretical TOCTOU. Tag immutability conventions make this acceptable for now.add-to-projectstatus-check failure on this PR is unrelated to the diff (existingauto-add-to-project.ymlissue, pre-existing across the org).
Verdict
All previously-blocking issues are resolved on 95914fc. The remaining items are P2 / nits and do not gate the rollout of this additive P1 batch. Approving so the workflows can land; recommend addressing the +build regex fix and outputs: declaration in a quick follow-up before business repos start chaining on the release URL.
Jerry-Xin
left a comment
There was a problem hiding this comment.
Summary: The PR is relevant to this .github repository, but the release publisher has correctness bugs that can block valid release flows.
🔴 Blocking
-
🔴 Critical: Annotated tags will fail CI evidence validation. In .github/workflows/reusable-release-publish.yml,
TAG_SHAis taken directly fromgit/ref/tags/${TAG}. For annotated tags, this SHA is the tag object SHA, not the target commit SHA, so the comparison againstactions/runs/${RUN_ID}.head_shaat line 74 incorrectly fails. The workflow should dereference tag objects via the Git tag API until it reaches the commit SHA before comparing. -
🔴 Critical: Build metadata in valid semver tags is rejected. The regex at .github/workflows/reusable-release-publish.yml uses
\\+inside the shell regex, causing tags likev1.2.3+buildandv1.2.3-alpha+buildto fail despite the error message and comment explicitly allowing[+build]. Use\+in the ERE pattern.
💬 Non-blocking
-
🟡 Warning: The PR description says
validate_run_idis optional, but the workflow requires it at .github/workflows/reusable-release-publish.yml. If the evidence gate is now mandatory, update the PR/user-facing docs. If it is intended to be optional, the workflow input and validation step need conditional handling. -
🔵 Suggestion: The publish step writes
release_urlto$GITHUB_OUTPUTat .github/workflows/reusable-release-publish.yml, but the step has noidand the reusable workflow declares no job/workflow outputs. Either expose it as a reusable workflow output or remove it.
✅ Highlights
- Permissions are scoped tightly with top-level
permissions: {}and job-level grants. - Release Drafter is SHA-pinned and follows the existing reusable workflow pattern in this repository.
- Inputs are passed through environment variables rather than interpolated directly into shell commands.
reusable-release-drafter.yml: - Delegates to release-drafter/release-drafter@v6 (SHA-pinned) - contents:write to update draft; pull-requests:read for PR metadata - config-name input allows per-repo drafter config customization reusable-release-publish.yml: - Inputs: tag (required), validate_run_id (optional CI evidence), draft flag - Validates semver tag format (strict: vMAJOR.MINOR.PATCH[-prerelease]) - Optional CI run validation: verifies a specific run_id succeeded before publishing (gates release on CI evidence, as per design plan) - Creates or updates GitHub Release (generate_release_notes=true as fallback)
1. Fix gh api jq injection: replace 'gh api --jq --arg' (invalid flag) with 'gh api ... --paginate | jq -rs --arg tag ...' — safe and handles pagination correctly without SIGPIPE risk from head -1 2. Add actions:read permission — required to call the workflow runs API when validate_run_id gate is used 3. Add explicit tag ref verification before any write — prevents accidental release creation from wrong commit when tag typo passes regex but does not exist 4. Make validate_run_id required (was optional) — optional gate defeats the 'gated release publish' design intent 5. Add head_sha check: validates CI run was triggered from the same commit as the tag SHA, preventing cross-commit evidence substitution 6. Tighten semver regex: reject leading zeros and empty prerelease segments
For annotated tags, /git/ref/tags/{TAG}.object.sha is the tag object SHA,
not the commit SHA. Comparing this directly against actions/runs.head_sha
(a commit SHA) incorrectly fails for all annotated tags.
Fix:
- Check .object.type from /git/ref/tags/{TAG}
- If type=tag (annotated): dereference /git/tags/{tag_sha}.object.sha
to get the actual commit SHA; validate target is a commit
- If type=commit (lightweight): use ref SHA directly
- Use jq -re everywhere to propagate errors on null/missing fields
95914fc to
c5a8271
Compare
Fix pushed (c5a8271) — annotated tag SHA resolutionNew blocking fixed: annotated tags fail CI evidence validation
Fix: ref_type=$(jq -re '.object.type') # 'tag' or 'commit'
if [ "$ref_type" = "tag" ]; then
# Annotated: dereference tag object → get actual commit SHA
tag_obj=$(gh api .../git/tags/{tag_object_sha})
target_type=$(jq -re '.object.type') # validate it's a commit
commit_sha=$(jq -re '.object.sha')
else
# Lightweight: ref is already the commit SHA
commit_sha="$ref_sha"
fiAdditional hardening:
|
yujiawei
left a comment
There was a problem hiding this comment.
Code Review — PR #27 (.github)
Summary
Re-review of c5a8271 on top of the prior CHANGES_REQUESTED round. The annotated-tag handling regression flagged in the last round is genuinely fixed — reusable-release-publish.yml:61-74 now dereferences annotated tag objects to a commit SHA before comparing against the CI run, which closes the most-critical correctness hole. The drafter workflow remains clean.
However, one of the P0/P1 items raised in the prior round on 95914fc is still present on c5a8271: the semver regex’s +build branch is escape-broken and rejects every tag with build metadata, even though the step’s own error message and the design intent advertise [+build] as supported. The fix is a single character. I’d like to see this addressed before the rollout — the workflows are advertised as the org’s shared release path, so the gate should not have a self-contradicting validation message in its very first step.
Verification of prior findings (against c5a8271)
| Prior finding | Status | Evidence |
|---|---|---|
| Annotated tag dereferenced before CI head_sha comparison | ✅ Fixed | reusable-release-publish.yml:61-74 calls gh api git/tags/${ref_sha} and exports the resolved commit SHA as TAG_SHA. |
actions: read on the publish job |
✅ Fixed | :27 declares actions: read with a comment explaining why. |
| Release lookup paginated | ✅ Fixed | :111-112 uses gh api ... --paginate with jq -rs slurp + [][] flatten. |
| Tag existence enforced before any write | ✅ Fixed | :45-77 verifies the ref via git/ref/tags/${TAG} and exits 1 if absent. |
| Semver rejects leading zeros / empty prerelease segments | ✅ Fixed | :39 uses `(0 |
Semver +build metadata accepted per docstring/error-message |
❌ Not fixed — see P1 below. |
Findings
P1 — Should fix before merge
1. Semver regex still rejects valid +build metadata tags (reusable-release-publish.yml:39).
The build-metadata branch is written as (\\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?. The pattern lives inside a YAML literal block scalar (no backslash escapes) wrapped in bash single quotes (no backslash escapes), so grep -E literally receives \\+. In ERE, \\ is an escaped backslash (one literal \) and + is the quantifier, so the branch matches “one or more literal backslashes,” not a literal +.
Verified locally against the exact pattern in the file:
ACCEPT: v1.2.3
ACCEPT: v1.2.3-alpha
ACCEPT: v1.2.3-alpha.1
REJECT: v1.2.3+build ← should ACCEPT per the error message
REJECT: v1.2.3-alpha+build ← should ACCEPT
REJECT: v1.2.3+build.5 ← should ACCEPT
REJECT: v01.2.3 ← correct
REJECT: v1.2.3- ← correct
REJECT: v1.2.3-. ← correct
This is a P1 because:
- It’s a contradiction inside the same step —
:40tells the uservMAJOR.MINOR.PATCH[-prerelease][+build]is the accepted shape, but the regex rejects every+buildexample. - It was flagged in the prior review round on
95914fc; the PR comment claiming “3 blocking issues addressed” covered the earlier round, but this regex point from the re-review was not folded in. - Fix is one character: change
\\+to\+. Re-verified the single-backslash form accepts the same valid set above and still rejects the invalid ones.
- '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$'
+ '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$'2. PR description still advertises validate_run_id as optional, but the workflow now requires it.
reusable-release-publish.yml:10-13 declares validate_run_id as required: true and the description begins with Required:. The PR body (Summary block) still says “Optional CI run validation: pass a validate_run_id to ensure …”, and the in-PR example comment reads validate_run_id: '12345678' # optional: CI run ID to validate. Anyone copy-pasting that example into a workflow_dispatch caller without supplying the input will get a hard failure at workflow startup with no idea why their working call broke.
Either tighten the workflow input back to required: false (with a conditional skip on the validate step when absent — the original design plan said the gate was opt-in), or update the PR body + caller example to match the new contract. Either fix is fine; the divergence is the issue.
P2 — Nits / maintainability
3. release_url is written to $GITHUB_OUTPUT but is unreachable to callers. reusable-release-publish.yml:133 writes the URL, but (a) the Publish GitHub Release step has no id:, (b) the publish job has no outputs: map, and (c) the workflow_call has no top-level outputs:. Callers cannot consume the URL for changelog posts / Slack notify chaining. This was raised twice now; small change, big DX win:
on:
workflow_call:
outputs:
release_url:
description: 'URL of the created or updated release'
value: ${{ jobs.publish.outputs.release_url }}
jobs:
publish:
outputs:
release_url: ${{ steps.publish.outputs.release_url }}
steps:
- name: Publish GitHub Release
id: publish
...4. 2>/dev/null still hides real gh api failures (:52, :85). Both downstream checks ([ -z "$ref_data" ], conclusion != success, run_sha != TAG_SHA) fail closed, so this is safe, but a 403 / rate-limit / token-scope error will surface as Tag ... does not exist or conclusion='' (need success) instead of the real cause. Dropping 2>/dev/null (or wrapping with if ! ...; then echo "::error::failed to query ..."; exit 1; fi) costs nothing and makes on-call debugging faster.
5. PATCH path re-sends tag_name unnecessarily (:119). We already matched the release on tag_name == $tag. Harmless but noise.
Highlights (no change needed)
reusable-release-drafter.ymlis unchanged from the prior round and remains correctly scoped: SHA-pinnedrelease-drafter/release-drafter@67e173cadb…, empty top-levelpermissions, narrow job grants (contents: write,pull-requests: read),runs-on: ubuntu-24.04.- All inputs in the publish workflow are funneled through
env:variables rather than interpolated directly into shell commands, which keeps shell-injection surface area minimal. - The annotated-tag dereferencing logic is correct and matches the GitHub Git refs API contract.
- TOCTOU between the verify-tag step and the publish step is theoretical only; tag-immutability convention makes it acceptable.
Verdict
Requesting changes for the +build regex (finding 1) — it was raised in the prior round and the latest commit does not address it, and the in-step error message actively misleads users about which tag shapes are accepted. The PR-body/required-input mismatch (finding 2) should also be reconciled before consumers wire the gate up. The remaining items (3-5) are non-blocking and can land as a follow-up. Once finding 1 is corrected (one-character change) and finding 2 is reconciled, this is ready to land — the broader pattern, permission scoping, and prior P0 fixes all check out.
Jerry-Xin
left a comment
There was a problem hiding this comment.
Project relevance gate: passed. These reusable release workflows fit the .github repository’s shared organization workflow purpose.
💬 Non-blocking
🟡 Warning — .github/workflows/reusable-release-publish.yml:39: The SemVer regex rejects valid build metadata tags such as v1.2.3+build.1 because \\+ matches a backslash sequence in ERE, not a literal plus. Use \+ in the grep pattern if +build support is intended.
🟡 Warning — .github/workflows/reusable-release-publish.yml:25: Callers must grant both contents: write and actions: read. If a caller follows the PR example with only contents: write, the called workflow cannot elevate token permissions enough to query the CI run.
🔵 Suggestion — .github/workflows/reusable-release-publish.yml:132: release_url is written to step output, but no step id, job output, or workflow_call.outputs exposes it to callers. Add those if downstream workflows are expected to consume the URL.
✅ Highlights
- Actions are SHA-pinned and use narrowly scoped job permissions.
- The publish workflow validates tag existence before writing a release.
- Annotated tags are correctly dereferenced to the target commit before CI evidence validation.
actionlintv1.7.12 completed successfully against the two new workflow files.
lml2468
left a comment
There was a problem hiding this comment.
Incremental Re-review (c5a8271 ← 4b200d6)
Previous review: CHANGES_REQUESTED with 3 blocking + 3 non-blocking findings. Verifying all are resolved.
Previously Blocking — All Fixed ✅
| Issue | Status | Evidence |
|---|---|---|
P0: gh api --jq --arg invalid syntax |
✅ Fixed | L115-116: now pipes through jq -rs --arg tag correctly |
P1: Missing actions: read |
✅ Fixed | L27: actions: read added with comment |
P1: 2>/dev/null masking errors |
✅ Fixed | Tag lookup uses || true with explicit emptiness check; CI validation dropped stderr suppression |
Previously Non-blocking — Addressed
| Issue | Status | Evidence |
|---|---|---|
| P1: No pagination on release lookup | ✅ Fixed | L115: --paginate added |
| P2: Tag existence not enforced | ✅ Fixed | New step "Verify tag ref exists" (L51-79) with annotated tag dereferencing |
| P2: Evidence gate not binding to tag commit | ✅ Fixed | L92: run_sha != TAG_SHA check added |
New Observations (non-blocking)
validate_run_idnow required (was optional) — correct design decision; an evidence gate that is optional by default defeats the purpose.- Annotated tag handling (L68-76) properly dereferences tag objects to commits — defense-in-depth that the prior version lacked.
- Semver regex build-metadata segment uses
\\\\+which in grep ERE matches a literal backslash, not+. Tags likev1.2.3+build.1would be rejected. Already flagged by prior reviewers as non-blocking.
Verdict
APPROVED — all blocking issues resolved, publish workflow is now functionally correct. (Submitted as COMMENTED due to GitHub self-approval limitation.)
1. Semver regex: replace \\+ with [+] for literal plus match In YAML block scalar + single-quoted shell string, \\+ becomes the literal characters \\+ which ERE interprets as escaped-backslash followed by +, matching \+ rather than +. Using [+] is unambiguous and matches a literal plus sign in all grep ERE implementations. Now validated: v1.2.3+build.1 accepted, v01.2.3 rejected. 2. Workflow name validation: validate_run_id must be from a workflow named 'CI'. Prevents bypass using successful non-CI runs (e.g. Release Drafter) on the same commit. Uses jq -re (exits on null/missing) throughout the step.
Fix pushed (b151630) — semver regex + workflow name validation1. Semver regex: In YAML block scalar + single-quoted shell string, Verified with bash test: 2. Workflow name validation (Jerry-Xin / lml2468 suggestion) Added a check that workflow_name=$(jq -re '.name')
if [ "$workflow_name" != "CI" ]; then
echo "::error::expected 'CI', got '${workflow_name}'"
exit 1
fiAlso switched all |
yujiawei
left a comment
There was a problem hiding this comment.
Code Review — PR #27 (.github)
Summary
Re-review of b151630 on top of the prior CHANGES_REQUESTED round on c5a8271. The two regressions flagged in the previous round are resolved:
- The semver regex's
+buildbranch now uses the unambiguous[+]character class, so tags likev1.2.3+build.1are accepted (matching what the error message and PR description advertise). - All
jq -rcalls in the publish workflow are nowjq -re(fail-fast on null/missing), and thegh api .../actions/runs/${RUN_ID}call no longer suppresses stderr.
In addition, this commit introduces a workflow-name pin (workflow_name != "CI" → fail), which closes a real bypass path I had not flagged before: without it, any successful run on the tag commit — e.g. a release-drafter run, an auxiliary stale-check, a docs-only build — would pass the validate_run_id gate even though it never executed the test suite. Hardcoding the name is a sharper gate than "any successful run on this SHA"; it's a net positive for the evidence-gate's stated purpose.
All previously-blocking findings (P0 gh api --jq --arg syntax, missing actions: read, unpaginated release lookup, annotated-tag SHA mismatch, +build regex) are now fixed. Remaining items are P2 nits and one maintainability concern worth folding into a follow-up.
Verification of prior findings (against b151630)
| Prior finding | Status | Evidence |
|---|---|---|
P1 (last round) — semver regex rejected valid +build metadata |
✅ Fixed | reusable-release-publish.yml:39 — build branch is now ([+][0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?. [+] inside a character class is a literal + and is escape-safe inside YAML block scalar + bash single quotes. Verified: v1.2.3+build, v1.2.3-alpha+build, v1.2.3+build.5 now ACCEPT; v01.2.3, v1.2.3-, v1.2.3-. still REJECT. |
P1 (last round) — jq -r did not fail on null fields, 2>/dev/null masked CI run errors |
✅ Fixed | :65, :66, :84, :85, :86, :117 all use jq -re. :83 (run_data=$(gh api ... actions/runs/${RUN_ID})) no longer pipes stderr to /dev/null — 403 / rate-limit / not-found will surface with the real cause. |
P0/P1 (earlier rounds) — gh api --jq --arg syntax, missing actions: read, unpaginated release lookup, annotated-tag SHA, evidence-binding |
✅ Still fixed | Confirmed unchanged: :27 (actions: read), :61-77 (annotated-tag dereferencing), :88-90 (run_sha != TAG_SHA check), :111-112 (--paginate + jq -rs). |
New observation (positive)
Workflow-name pin closes a real evidence-gate bypass.
reusable-release-publish.yml:93-96:
if [ "$workflow_name" != "CI" ]; then
echo "::error::CI run ${RUN_ID} is from workflow '${workflow_name}', expected 'CI'. Provide a run from the main CI workflow."
exit 1
fiWithout this, an attacker (or a sloppy caller) could pass the run_id of any successful workflow that happened to run on the tag's commit — release-drafter itself, auto-add-to-project, a docs lint job — and bypass the "did tests pass?" gate. The pin makes the gate mean what it says. Worth keeping.
Findings
P2 — Maintainability / follow-up
1. Workflow name "CI" is hardcoded with no input to override (:93).
This is the right default but the wrong abstraction for a reusable workflow that's meant to be adopted across the org. Repos that name their primary CI workflow anything other than literally CI (Continuous Integration, ci.yml → CI Pipeline, tests, build-and-test, etc.) cannot use this gate. The PR body / caller example do not document this requirement, so the first such adopter will discover it the hard way when their validate_run_id from a perfectly-good test run is rejected.
Suggested fix:
on:
workflow_call:
inputs:
...
ci_workflow_name:
description: 'Name of the CI workflow (as it appears in the Actions UI). The validate_run_id must come from this workflow.'
type: string
required: false
default: 'CI'…and reference ${{ inputs.ci_workflow_name }} in the comparison. Non-blocking — the current default is sane for the immediate consumers — but worth a follow-up before this is wired into business repos with non-standard naming.
2. PR body still describes validate_run_id as optional while the workflow declares it required.
reusable-release-publish.yml:11-14 declares validate_run_id as required: true. The PR body's Summary still reads:
Optional CI run validation: pass a
validate_run_idto ensure a specific CI run succeeded before publishing
…and the example block annotates the input as # optional: CI run ID to validate. The actual contract is now mandatory — making the gate non-bypassable is the right design call, but the user-facing docs need to match. Anyone copy-pasting the current example into a workflow_dispatch caller without supplying the input will see a hard failure at workflow startup with no obvious cause.
This is a doc-only fix on the PR body / future caller examples — no workflow change needed.
3. release_url is written to $GITHUB_OUTPUT but unreachable to callers (:132-133).
Still the case across the last three rounds:
- The
Publish GitHub Releasestep has noid:. - The
publishjob has nooutputs:map. - The
workflow_callhas no top-leveloutputs:.
Result: callers cannot chain a downstream changelog-post, Slack-notify, or release-announce job on the URL. Pure DX improvement, easy follow-up:
on:
workflow_call:
outputs:
release_url:
description: 'URL of the created or updated release'
value: ${{ jobs.publish.outputs.release_url }}
jobs:
publish:
outputs:
release_url: ${{ steps.publish.outputs.release_url }}
steps:
- name: Publish GitHub Release
id: publish
...4. 2>/dev/null still hides real gh api failures on the tag-existence check (:52).
ref_data=$(gh api ".../git/ref/tags/${TAG}" 2>/dev/null || true) is safe in the happy/missing-tag paths because the downstream [ -z "$ref_data" ] check fails closed, but a 403 / token-scope / rate-limit error will surface to on-call as Tag '${TAG}' does not exist — which is misleading. Consider:
if ! ref_data=$(gh api "repos/${{ github.repository }}/git/ref/tags/${TAG}"); then
if printf '%s' "$ref_data" | grep -q 'Not Found'; then
echo "::error::Tag '${TAG}' does not exist in this repository. Push the tag before running this workflow."
exit 1
fi
echo "::error::Failed to query tag ref for ${TAG} (check token scopes / GitHub API status)"
exit 1
fiCosts nothing, makes incident debugging cheaper.
Nits
5. PATCH path re-sends tag_name unnecessarily (:120). We already matched the release on tag_name == $tag; the field is redundant in the update payload. Harmless, just noise.
6. The hardcoded "CI" error message could include a hint about how to override (once #1 is addressed). When the workflow input lands, the error should suggest with: { ci_workflow_name: <yours> } so the failure is self-documenting.
Highlights (no change needed)
reusable-release-drafter.ymlis unchanged across rounds and remains correctly scoped: SHA-pinnedrelease-drafter/release-drafter@67e173cadb…(v6), empty top-levelpermissions, narrow job grants (contents: write,pull-requests: read),runs-on: ubuntu-24.04.- All
publishstep inputs are funneled throughenv:rather than interpolated into shell, keeping injection surface area minimal. - Annotated-tag dereferencing is correct against the GitHub Git refs API contract.
jq -rethroughout makes the script fail-fast on schema drift in the GitHub API responses.- The TOCTOU window between
Verify tag ref existsandPublish GitHub Releaseis theoretical; tag-immutability convention makes it acceptable here. actionlintvalidation on the prior round passed; structure has not changed in ways that would invalidate that.
Verdict
All previously-blocking findings (P0/P1 across three rounds) are resolved on b151630. The new workflow-name pin is a positive addition that hardens the evidence gate. Remaining items (1–6) are P2 maintainability/DX/docs concerns suited to a quick follow-up; none gate this additive P1 rollout.
Approving so the reusable release workflows can land. Recommend addressing #1 (ci_workflow_name input) and #2 (PR-body / caller-example docs) before business repos start adopting the publish workflow with non-standard CI workflow names.
Jerry-Xin
left a comment
There was a problem hiding this comment.
One blocking functional issue prevents this reusable workflow from working reliably across caller repositories.
🔴 Blocking
🔴 Critical — .github/workflows/reusable-release-publish.yml:100-104: the CI evidence gate hard-codes the workflow display name to exactly CI. This is a reusable org workflow, but caller repositories cannot configure that expected workflow name, so any repo whose primary workflow is named Build, Test, Node CI, Go CI, etc. will fail release publishing even when the provided run succeeded on the tag commit. The comment says “adjust if your repo uses a different name,” but consumers of a reusable workflow cannot adjust this without changing the central workflow. Add an input such as expected_workflow_name with a default of CI, accept a workflow ID, or remove this check and rely on validate_run_id + head_sha + conclusion.
💬 Non-blocking
🟡 Warning — .github/workflows/reusable-release-publish.yml:10-13: the PR description says validate_run_id is optional, but the workflow marks it as required. If the evidence gate is intentionally mandatory, update the PR/docs/caller examples to avoid confusion. If it is meant to be optional, the validation step needs an if: guard.
🔵 Suggestion — .github/workflows/reusable-release-publish.yml:140-141: the step writes release_url to $GITHUB_OUTPUT, but the step has no id, the job has no outputs, and workflow_call declares no outputs. If callers should consume this URL, expose it as a reusable workflow output.
✅ Highlights
The PR is in scope for this .github repository. The new workflows follow the repo’s existing reusable workflow style, use minimal top-level permissions, pin the third-party release-drafter action by SHA, validate the tag before write operations, and correctly handle annotated tag dereferencing before comparing the CI run commit.
lml2468
left a comment
There was a problem hiding this comment.
Incremental Re-review #2 (b151630 ← c5a8271)
Fixed since last review ✅
- Semver regex:
\\\\+→[+]— now correctly matches literal+in ERE.v1.2.3+build.1will pass. 2>/dev/nullfully removed: CI validation step no longer suppresses stderr. Usesjq -re(fail on null). Good.
New Blocking Issue
P1 — Hard-coded workflow name "CI" breaks reusability (L100-103)
if [ "$workflow_name" != "CI" ]; then
echo "::error::CI run ${RUN_ID} is from workflow '${workflow_name}', expected 'CI'..."
exit 1
fiThis is a reusable org workflow meant to be called by octo-server, octo-web, octo-adapters, etc. Each repo names its CI workflow differently (Build, Test, Go CI, Node CI, etc.). Hard-coding "CI" means most callers will fail at release time with a confusing error.
The commit-SHA binding (run_sha != TAG_SHA) already proves the run is from the right code. If you also need workflow identity, make it an input:
inputs:
ci_workflow_name:
description: 'Expected CI workflow name (must match the run's display name)'
type: string
required: false
default: 'CI'Then reference inputs.ci_workflow_name in the check. That preserves the guard without breaking reusability.
Alternatively, drop the workflow-name check entirely — run_id + commit SHA is already strong enough evidence for most use cases.
Verdict
CHANGES_REQUESTED — hard-coded workflow name is a functional blocker for downstream callers. (Submitted as COMMENTED due to GitHub limitation.)
1. Add ci_workflow_name input (string, optional, default='CI') so
callers with a differently-named primary CI workflow (Build, Go CI,
Node CI, etc.) can override the gate check without modifying the
reusable workflow.
2. Pass ci_workflow_name via env: EXPECTED_WORKFLOW instead of inline
'${{ inputs.ci_workflow_name }}' to avoid shell injection: GitHub
interpolates expressions before bash runs, so a value containing
single quotes would break the shell assignment. Using env: safely
passes the value as an environment variable with no quoting issues.
Fix pushed (cd1e6b9) — configurable workflow name + injection fixBlocking resolved: hard-coded 'CI' → configurable New optional input: ci_workflow_name:
description: 'Expected workflow name for the CI run (default: CI). Set to the exact name shown in your repo Actions tab if it differs.'
type: string
required: false
default: 'CI'Caller repos whose primary workflow is named with:
tag: v1.4.0
validate_run_id: '12345678'
ci_workflow_name: 'Go CI'Security fix: shell injection → env: pattern Previously: Now: passed via |
yujiawei
left a comment
There was a problem hiding this comment.
Code Review — PR #27 (.github)
Summary
Re-review of cd1e6b90 on top of the prior CHANGES_REQUESTED round on b1516305. The single blocking finding from that round — hard-coded "CI" workflow name in the evidence gate, which would break adoption in business repos whose primary workflow is called anything other than literally CI — is resolved in this commit. The fix introduces a ci_workflow_name input with a sensible default of 'CI', plumbs it through env: (not an inline expression, so no shell-injection surface), and rewrites the error message to point users at the override.
Everything else carried forward from earlier rounds remains correctly fixed: the gh api --jq --arg syntax bug, the missing actions: read scope, the unpaginated release lookup, the annotated-tag SHA mismatch, and the +build regex escape issue. The drafter workflow is unchanged from the previous additive baseline and remains correctly scoped.
No P0 / P1 blockers on the current head. A handful of P2 / DX nits remain (carried over from prior rounds); none gate the rollout of this additive P1 batch.
Verification of prior findings (against cd1e6b9)
| Prior finding | Status | Evidence |
|---|---|---|
P1 (last round) — hard-coded "CI" workflow name broke reusability across business repos |
✅ Fixed | New ci_workflow_name input at reusable-release-publish.yml:14-18 (default 'CI'). The check at :105-109 now compares against $EXPECTED_WORKFLOW, passed via env: at :88 — keeps the shell-injection guarantee intact. Error message at :107 includes a concrete remediation hint (Use ci_workflow_name input to configure.). |
P1 (rounds back) — semver regex rejected valid +build metadata |
✅ Still fixed | :39 retains ([+][0-9A-Za-z-]+...)? — character-class form is escape-safe inside YAML block scalar + bash single quotes. |
P0 — invalid gh api --jq --arg syntax (release lookup never ran) |
✅ Still fixed | :121-122 uses gh api ... --paginate | jq -rs --arg tag "$TAG" '[.[][] | select(.tag_name == $tag) | .id][0] // empty'. |
P1 — missing actions: read on publish job |
✅ Still fixed | :32 declares actions: read alongside contents: write, with an explanatory comment. |
| P1 — release lookup not paginated | ✅ Still fixed | --paginate + jq -rs slurp at :121-122. |
| P1 — annotated tag SHA not dereferenced before CI head_sha comparison | ✅ Still fixed | :66-77 dereferences via git/tags/${ref_sha} and exports TAG_SHA as the commit SHA. |
| P2 — tag existence enforced before any write | ✅ Still fixed | Verify tag ref exists and resolve commit SHA step at :51-79. |
| P2 — evidence gate binds run to tag commit | ✅ Still fixed | :98-101 checks run_sha != TAG_SHA. |
Review of the new commit's changes
The diff between b1516305 and cd1e6b90 is small and surgical:
-
New input declaration (
reusable-release-publish.yml:14-18):ci_workflow_name: description: 'Expected workflow name for the CI run (default: CI). Set to the exact name shown in your repo Actions tab if it differs.' type: string required: false default: 'CI'
Optional with a sane default — preserves the prior behavior for any existing caller and unblocks repos with non-standard CI workflow names. ✅
-
New env var on the validate step (
reusable-release-publish.yml:88):EXPECTED_WORKFLOW: ${{ inputs.ci_workflow_name }}
The value is funnelled through
env:rather than interpolated into the bash body — consistent with howRUN_IDandTAGare handled elsewhere in this workflow, and the in-step comment at:104(EXPECTED_WORKFLOW is passed via env (not inline expression) to avoid shell injection.) records the intent for future readers. ✅ -
Updated comparison & error message (
reusable-release-publish.yml:105-108):if [ "$workflow_name" != "$EXPECTED_WORKFLOW" ]; then echo "::error::CI run ${RUN_ID} is from workflow '${workflow_name}', expected '${EXPECTED_WORKFLOW}'. Use ci_workflow_name input to configure." exit 1 fi
Self-documenting failure — a caller hitting this for the first time gets the override knob name in the error itself. Small but high-leverage DX. ✅
The change has no impact on the drafter workflow.
Remaining issues (non-blocking, follow-up acceptable)
P2 — PR body still describes validate_run_id as optional while the workflow declares it required
reusable-release-publish.yml:10-13 marks validate_run_id as required: true and the description begins with Required:. The PR body's Summary still reads:
Optional CI run validation: pass a
validate_run_idto ensure a specific CI run succeeded before publishing
…and the example block annotates the input as # optional: CI run ID to validate. Anyone copy-pasting the current PR-body example into a workflow_dispatch caller without supplying validate_run_id will get a hard failure at workflow startup with no obvious cause. This is a doc-only fix on the PR body and on future caller examples in business repos.
P2 — release_url is written to $GITHUB_OUTPUT but unreachable to callers
Carried over across rounds. reusable-release-publish.yml:132-133 writes the URL, but:
- The
Publish GitHub Releasestep has noid:. - The
publishjob has nooutputs:map. - The
workflow_callhas no top-leveloutputs:.
Callers cannot chain a downstream changelog-post or Slack-notify job on the URL. Pure DX improvement, easy follow-up:
on:
workflow_call:
outputs:
release_url:
description: 'URL of the created or updated release'
value: ${{ jobs.publish.outputs.release_url }}
jobs:
publish:
outputs:
release_url: ${{ steps.publish.outputs.release_url }}
steps:
- name: Publish GitHub Release
id: publish
...P2 — 2>/dev/null still hides real gh api failures on the tag-existence check
reusable-release-publish.yml:56 — ref_data=$(gh api ".../git/ref/tags/${TAG}" 2>/dev/null || true). Safe in the happy/missing-tag paths because the downstream [ -z "$ref_data" ] check fails closed, but a 403 / token-scope / rate-limit error will surface as Tag '${TAG}' does not exist — misleading for on-call. Costs nothing to widen the error handling:
if ! ref_data=$(gh api "repos/${{ github.repository }}/git/ref/tags/${TAG}" 2>&1); then
if printf '%s' "$ref_data" | grep -q 'Not Found'; then
echo "::error::Tag '${TAG}' does not exist in this repository. Push the tag before running this workflow."
else
echo "::error::Failed to query tag ref for ${TAG}: ${ref_data}"
fi
exit 1
fiP2 — No concurrency: group on the publish job
Two simultaneous workflow_dispatch invocations for the same tag could race and create duplicate releases (the list-then-create check is not atomic against the GitHub API). Realistic, not theoretical, but very rare for a manually-triggered release publisher. Adding:
concurrency:
group: release-publish-${{ inputs.tag }}
cancel-in-progress: false…closes the race cheaply. Follow-up acceptable.
P2 — Prerelease tags are published as full releases
The semver regex correctly accepts v1.2.3-alpha, but the create branch (:141-148) does not pass prerelease=true, so a -alpha / -rc.1 tag ends up published as a stable release. Either auto-detect (prerelease: ${{ contains(inputs.tag, '-') }}) or expose an explicit prerelease input. Minor; not a blocker for the immediate consumers.
Nit — PATCH path re-sends tag_name unnecessarily (:131)
We already matched the release on tag_name == $tag; the field is redundant in the update payload. Harmless, just noise.
Highlights (no change needed)
reusable-release-drafter.ymlis unchanged across all rounds and remains correctly scoped: SHA-pinnedrelease-drafter/release-drafter@67e173cadb…(v6), empty top-levelpermissions: {}, narrow job grants (contents: write,pull-requests: read),runs-on: ubuntu-24.04.- All
publishstep inputs are funneled throughenv:rather than interpolated into shell, keeping injection surface area minimal — and the newci_workflow_nameplumbing follows the same convention, with an inline comment recording the intent. - Annotated-tag dereferencing is correct against the GitHub Git refs API contract.
jq -rethroughout makes the script fail-fast on schema drift in the GitHub API responses.- The evidence-gate is now genuinely binding:
validate_run_idis required, the run'shead_shamust equal the resolved tag commit, the run'sconclusionmust besuccess, and the run must come from the configuredci_workflow_name— closing the bypass paths that earlier rounds exposed. - The TOCTOU window between
Verify tag ref existsandPublish GitHub Releaseis theoretical; tag-immutability convention makes it acceptable here.
Verdict
All previously-blocking findings (across multiple rounds) are resolved on cd1e6b90. The ci_workflow_name fix lands the change cleanly: default behavior is preserved, business repos with non-standard CI workflow names are unblocked, and shell-injection safety is maintained. Remaining items (P2 / nits) are well-suited to a quick follow-up and do not gate this additive P1 rollout.
Approving so the reusable release workflows can land. Recommended follow-ups before business repos start adopting the publish workflow at scale: reconcile the PR body / caller-example docs against the now-required validate_run_id, expose release_url as a workflow output, and add a per-tag concurrency group.
Jerry-Xin
left a comment
There was a problem hiding this comment.
Summary: The PR is relevant to the .github repository and adds well-scoped reusable release workflows consistent with the existing workflow style.
💬 Non-blocking
- 🟡 Warning:
.github/workflows/reusable-release-publish.yml:10makesvalidate_run_idrequired, but the PR description and usage example describe it as optional. Either make the input optional and conditionally skip the CI validation step, or update the workflow contract/docs to say the evidence gate is mandatory. - 🔵 Suggestion:
.github/workflows/reusable-release-publish.yml:147writesrelease_urlto step output, but the step has noidand the reusable workflow defines noworkflow_call.outputs, so callers cannot consume it. Consider exposing it as a reusable workflow output or removing the unused output write. - 🔵 Suggestion:
.github/workflows/reusable-release-publish.yml:91could validateRUN_IDas numeric before building the API path. Current quoting avoids shell execution issues, but a clearer input check would improve failure messages.
✅ Highlights
- Uses least-privilege top-level
permissions: {}with job-scoped permissions. - Handles both lightweight and annotated tags before comparing CI evidence.
- Semver validation happens before release writes.
- Third-party action in the drafter workflow is SHA-pinned.
git diff --check, tab validation, andactionlint v1.7.12passed locally.
lml2468
left a comment
There was a problem hiding this comment.
Incremental Re-review #3 (cd1e6b9 ← b151630)
Fixed ✅
- Hard-coded workflow name:
"CI"→inputs.ci_workflow_namewithdefault: "CI". Callers can now configure via input. Injected through env var, not inline expression — correct shell injection prevention.
Previous findings status
All blocking issues from my original review are now resolved across three rounds of fixes:
✅ (round 1)gh api --jq --argsyntaxMissing✅ (round 1)actions: read✅ (round 2)2>/dev/nullerror maskingHard-coded workflow name✅ (round 3)
No new issues found.
Verdict
APPROVED — all blocking issues resolved. Clean reusable workflow pair ready for downstream adoption. (Submitted as COMMENTED due to GitHub limitation.)
Summary
P1 batch — adds two release-focused reusable workflows.
reusable-release-drafter.ymlKeeps a rolling GitHub Release draft updated as PRs merge to main.
release-drafter/release-drafter@v6(SHA-pinned:67e173ca)contents: write+pull-requests: readconfig-nameinput for per-repo drafter configCaller pattern in business repos:
reusable-release-publish.ymlGate-controlled release publisher.
validate_run_idto ensure a specific CI run succeeded before publishing (the 'evidence gate' from our design plan)draft: trueto keep as draft for human review before publishingUsage: