Skip to content

fix(ci): reject unsafe release dispatch tags - #249

Open
MikeTomlin19 wants to merge 8 commits into
Gitlawb:mainfrom
MikeTomlin19:agent/harden-release-tag-239
Open

fix(ci): reject unsafe release dispatch tags#249
MikeTomlin19 wants to merge 8 commits into
Gitlawb:mainfrom
MikeTomlin19:agent/harden-release-tag-239

Conversation

@MikeTomlin19

@MikeTomlin19 MikeTomlin19 commented Jul 25, 2026

Copy link
Copy Markdown

Summary

Validate manual release tags before writing them to $GITHUB_OUTPUT, preventing malformed multiline input from creating additional step outputs.

Motivation & context

The existing v[0-9]* shell pattern is a prefix check, not a complete validation rule, and echo "tag=$TAG" writes embedded newlines verbatim. A malformed workflow_dispatch tag can therefore add unintended output keys consumed by later release steps.

This is input hygiene rather than a privilege boundary: manual dispatch inputs are limited to write-access collaborators, who also control the selected workflow ref. The separate workflow-trust boundary is tracked in #234 and remains out of scope.

Closes #239

Kind of change

  • Bug fix
  • Feature
  • Security fix
  • Docs
  • Tests / CI
  • Refactor (no behavior change)
  • Breaking or protocol change (issue required first)

What changed

  • Moved tag resolution into one shared, allowlist-validating helper used by all three release jobs.
  • Added three initial actions/checkout steps so each job can run the shared helper; docker-manifest now explicitly requests contents: read.
  • Rejects empty, multiline, whitespace-containing, path-containing, shell-metacharacter, Docker-incompatible +build, and bad-shape inputs before any output is written.
  • Preserves the existing v[0-9]* release-tag shape check.
  • Uses printf for validated tag and version outputs while preserving ::error:: annotations on stdout.
  • Added executable valid/newline/empty/invalid-shape regression cases, including explicit +build rejection.
  • Added a PR-check job that runs the resolver suite.
  • Added job-bound drift guards requiring exactly one active Resolve release tag step in each of docker, docker-manifest, and npm-publish; commented references do not count as resolver invocations.
  • Rejects same-step tag/version writes to $GITHUB_OUTPUT or $GITHUB_ENV regardless of output mechanism, with mutation regressions for decoy calls, commented calls, multiline brace groups, tee, heredocs, a renamed resolver step, and a renamed/reverted resolver step.

How a reviewer can verify

scripts/test-resolve-release-tag.sh

go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7 \
  -ignore 'label "macos-15-intel" is unknown' \
  .github/workflows/release.yml .github/workflows/pr-checks.yml

The regression script verifies exact outputs for v1.2.3. It verifies that empty, multiline, whitespace-containing, shell-metacharacter, path-containing, build-metadata, uppercase-prefix, and bad-shape inputs fail without writing output. It also binds the resolver invariant to all three release jobs and proves the guard rejects decoy calls, commented calls, multiline brace-group writes, tee and heredoc appends, a renamed resolver step, and a renamed/reverted resolver step.

Before you request review

  • Scope is one logical change; no unrelated churn
  • cargo test --workspace passes locally (Rust sources are unchanged)
  • New behavior is covered by validation cases
  • cargo fmt --all and cargo clippy --workspace --all-targets -- -D warnings are clean (Rust sources are unchanged)
  • Commit titles use Conventional Commits (feat(...), fix(...), docs(...))
  • Docs / .env.example updated if behavior or config changed (N/A)
  • Checked existing PRs so this isn't a duplicate

Protocol & signing impact

N/A — this does not change identity, signatures, UCANs, ref certificates, or P2P wire formats.

Notes for reviewers

The release tag validation job runs on pull_request and merge_group, but the current repository ruleset does not require status checks, so this PR does not claim that the job is branch-protection blocking.

The ignored Actionlint diagnostic is an existing repository baseline: Actionlint 1.7.7 does not recognize GitHub's macos-15-intel runner label. With that unrelated diagnostic ignored, both workflows are clean.

Summary by CodeRabbit

  • Reliability

    • Centralized release tag/backfill resolution for Docker and npm publishing into a shared resolver step.
    • Added stricter validation to ensure release tags match the expected format before publishing.
    • Added an additional pull request CI check to validate release tag resolution.
  • Tests

    • Added end-to-end automated tests for release tag resolution, including happy path and multiple invalid input scenarios.
    • Added safeguards that verify publishing workflows consistently use the resolver step and reject common output-injection bypass patterns.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a shared Bash script for validating and resolving release tags, updates Docker and npm release jobs to use it, and adds PR checks for valid and invalid inputs plus workflow wiring.

Changes

Release tag validation

Layer / File(s) Summary
Resolver validation and tests
scripts/resolve-release-tag.sh, scripts/test-resolve-release-tag.sh
The resolver validates allowed tag characters and v-prefixed format, writes tag and version outputs, and tests valid and invalid inputs plus workflow guard behavior.
Publishing workflow integration
.github/workflows/release.yml
Docker, Docker manifest, and npm publishing jobs check out the script and use it for release-tag resolution.
PR validation job
.github/workflows/pr-checks.yml
Adds a five-minute job that runs the release-tag resolver test harness.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • Gitlawb/node#233: Updates release workflow tag and version handling for the npm publishing path.
  • Gitlawb/node#235: Reworks Docker publishing and backfill tagging flow in the release workflow.

Suggested labels: kind:ci

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address #239 by validating release tags, blocking injection payloads, preserving shape checks, and writing outputs safely.
Out of Scope Changes check ✅ Passed The added workflow, helper script, and regression tests are all directly tied to the release-tag hardening objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title accurately summarizes the main change: hardening release dispatch tag validation in CI.
Description check ✅ Passed The description follows the template well and covers summary, motivation, change list, verification, and review notes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the workflow-change Fork PR edits CI workflows — mandatory human review label Jul 25, 2026
@MikeTomlin19
MikeTomlin19 marked this pull request as ready for review July 25, 2026 19:30

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found an issue that needs to be addressed before this is ready.

Findings

  • [P3] Add the claimed regression coverage, or correct the PR description
    .github/workflows/release.yml:86
    The PR says that the new validation cases are covered, but this change adds no executable check for any of the three Resolve release tag blocks; Actionlint only validates workflow syntax and does not execute the shell. A later edit can therefore restore the newline $GITHUB_OUTPUT injection while CI remains green. Add a focused probe that exercises a valid tag and a newline-containing tag for each resolver (or remove the coverage claim).

@MikeTomlin19

Copy link
Copy Markdown
Author

Addressed the requested executable coverage in 64e228f.

The three release jobs now call one shared scripts/resolve-release-tag.sh implementation. scripts/test-resolve-release-tag.sh executes that resolver with a valid tag, a newline-injection value, and an empty value; it verifies exact valid outputs, verifies invalid cases write nothing, and asserts that all three workflow steps use the tested helper.

The regression script is now a blocking release tag validation job in pr-checks.yml.

Validation:

  • scripts/test-resolve-release-tag.sh — passed
  • bash -n scripts/resolve-release-tag.sh scripts/test-resolve-release-tag.sh — passed
  • Actionlint 1.7.7 — passed with only the repository's pre-existing macos-15-intel runner-label diagnostic ignored

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/test-resolve-release-tag.sh`:
- Around line 16-28: Add rejected test cases in
scripts/test-resolve-release-tag.sh for release tags containing whitespace,
shell metacharacters, and values that do not match the v[0-9]* format. Follow
the existing newline and empty-tag assertions: invoke $resolver with each
invalid value, require failure, and verify the corresponding GITHUB_OUTPUT file
remains empty.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5ae66396-c00b-4045-a864-498dc8c16b79

📥 Commits

Reviewing files that changed from the base of the PR and between 111cff7 and 64e228f.

📒 Files selected for processing (4)
  • .github/workflows/pr-checks.yml
  • .github/workflows/release.yml
  • scripts/resolve-release-tag.sh
  • scripts/test-resolve-release-tag.sh

Comment thread scripts/test-resolve-release-tag.sh

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@beardthelion LGTM but needs your evaluation

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolver itself holds up. I reproduced the original injection against the pre-PR logic (v1\nversion=9.9.9\ntag=ghcr.io/attacker/evil passes the old glob, and last-wins resolution yields tag=ghcr.io/attacker/evil), confirmed the new script rejects that exact payload with a zero-byte $GITHUB_OUTPUT, and mutated every protection in it: the character allowlist, the shape check, and removing a call site each turn the suite red. actionlint 1.7.7 runs clean on both workflows.

What needs another round is the drift guard, not the fix.

Findings

  • [P2] Assert the invariant, not a line count

scripts/test-resolve-release-tag.sh:40

grep -c 'scripts/resolve-release-tag.sh' ... -ne 3 counts matching lines rather than resolver invocations, so it fails in both directions. I executed three reintroductions of the original bug that all leave the suite green: reverting one call site to inline while a comment still names the script, adding a fourth job with the old inline write, and keeping all three calls but wrapping one in || { echo "tag=$TAG" >> "$GITHUB_OUTPUT"; } so the rejection is swallowed. Meanwhile a benign comment naming the script flips it red with all three call sites intact.

Replacement, run against the current head plus all three bypasses plus the comment case, 5/5 as intended:

workflows="$repo_root/.github/workflows"
steps="$(grep -c 'name: Resolve release tag' "$workflows/release.yml" || true)"
calls="$(grep -cE '^[[:space:]]*scripts/resolve-release-tag\.sh ' "$workflows/release.yml" || true)"

if [[ "$steps" -lt 1 || "$steps" -ne "$calls" ]]; then
  printf '%s\n' "every 'Resolve release tag' step must call the tested resolver; steps=$steps calls=$calls" >&2
  exit 1
fi

if grep -rqE '(echo|printf)[^|]*(tag|version)=.*>>[[:space:]]*"?\$GITHUB_OUTPUT' "$workflows"; then
  printf '%s\n' "a workflow writes a tag/version output without the tested resolver:" >&2
  grep -rnE '(echo|printf)[^|]*(tag|version)=.*>>[[:space:]]*"?\$GITHUB_OUTPUT' "$workflows" >&2
  exit 1
fi

The || true earns its place separately: with all three call sites gone the count is 0, grep -c exits 1, and set -e kills the assignment before the intended "found 0" message ever prints. The second grep is clean on the current tree, where the only other $GITHUB_OUTPUT writers are image= and enabled=.

  • [P2] Cover the two resolver relaxations that survive the suite

scripts/test-resolve-release-tag.sh:31

Changing v[0-9]*) to v*) keeps the suite green and admits v-1, vlatest, and bare v. Widening the allowlist to [!A-Za-z0-9._/-] also stays green and admits v1.2.3/../../x. Extending the existing invalid loop to "v-1" "vlatest" "v" "v1.2.3/../../x" "V1.2.3" closes both: I ran all five against the current resolver (rejected, zero-byte output) and against each mutant (accepted, so every added case is load-bearing).

  • [P3] Fix three claims in the description

The body omits the three added actions/checkout steps and docker-manifest's new contents: read. It says the test asserts all three release jobs call the tested resolver, which the first finding disproves. It also calls the new job blocking: the job does run on pull_request and merge_group with no path filter and passes here in 5s, but the main ruleset carries no required-status-checks rule at all. Adding it there is on me rather than this PR, so the wording is the only thing to change.

  • [P3] Note two narrow behavior changes

scripts/resolve-release-tag.sh:7

The allowlist rejects +, so v1.2.3+build.5 now fails where the old glob accepted it. release-please emits plain vX.Y.Z, so this only reaches a hand-dispatched backfill, and it fails loudly rather than silently. Separately, the reject-path ::error:: annotations moved from stdout to stderr. The step still exits non-zero either way, but worth confirming the annotation still renders, since I did not test that against a runner.

Scope note on framing: the tag validation here is input hygiene rather than a privilege boundary. Only a write-access collaborator can supply docker_backfill_tag or npm_backfill_tag, and a dispatch runs the workflow from the selected ref, so that same actor controls both the invoking run: block and, since the new checkouts carry no ref:, the validator script itself. That is not an argument against the change, just a reason to keep the description modest. The gap tracked in #234 is what actually constrains that surface, and it is deliberately out of scope here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/test-resolve-release-tag.sh`:
- Around line 62-78: Update the workflow validation in the test script to
inspect each individual “Resolve release tag” step rather than comparing
aggregate counts. Extract each matching step’s run block, require that block to
invoke scripts/resolve-release-tag.sh, and reject tag= or version= writes to
GITHUB_OUTPUT even when the echo/printf command spans multiple lines; retain the
existing failure diagnostics and nonzero exit behavior.
- Around line 16-21: Update the resolver exercised by the test to prevent
Docker-incompatible versions containing “+” build metadata from being published:
either reject such tags through the existing allowlist validation or normalize
the metadata before producing the release version used for Docker tagging.
Adjust the test’s expected behavior so v1.2.3+build.5 is not accepted as a valid
release version, while preserving valid semver tag handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 580b5996-6788-468f-bc4a-be29195dee6d

📥 Commits

Reviewing files that changed from the base of the PR and between 64e228f and c00eaa2.

📒 Files selected for processing (2)
  • scripts/resolve-release-tag.sh
  • scripts/test-resolve-release-tag.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/resolve-release-tag.sh

Comment thread scripts/test-resolve-release-tag.sh Outdated
Comment thread scripts/test-resolve-release-tag.sh Outdated

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolver is right and I could not get anything past it: the original injection payload, shell metacharacters, whitespace, path traversal and case variants all reject with a zero-byte $GITHUB_OUTPUT, and the five invalid cases from last round are in and load-bearing. Two things need another pass, and the first one is a hole in the guard I handed you.

Findings

  • [P2] Bind the drift guard to each resolver step, not to file-wide counts
    scripts/test-resolve-release-tag.sh:62
    The guard I proposed compares a count of name: Resolve release tag lines against a count of resolver-call lines, and both greps are line-scoped. I rewrote the docker-manifest step to write tag/version through a { echo ...; echo ...; } >> "$GITHUB_OUTPUT" brace group and moved a decoy resolver call into a new step so the counts stayed 3 and 3. The suite exits 0 and prints "release tag resolver tests passed" while the original injection is fully back: that step resolves tag=ghcr.io/attacker/evil, version=9.9.9. The brace form is the idiom release.yml already uses elsewhere, so this is the shape drift takes by default, not a contrived edit. What worked when I ran it: extract each Resolve release tag step's own run block, flatten it to one line, require the resolver call in that block, and reject (tag|version)= followed by a $GITHUB_OUTPUT or $GITHUB_ENV redirect anywhere in it. Exit 0 on the current tree with steps=3, exit 1 on the bypass. Keep it per-step and key-scoped rather than a blanket ban, because echo "image=..." at release.yml:97 legitimately lives inside the first resolver step; my first attempt banned every redirect and false-positived on exactly that line. CodeRabbit raised the same class at line 78 and that thread is still unresolved.

  • [P2] Reject + again and assert the build-metadata tag fails
    scripts/resolve-release-tag.sh:8
    + entered the allowlist in c00eaa2; 64e228f did not have it. That came from my own P3 last round, which flagged the + rejection as a behavior change worth noting, and I should have been clearer that noting it was the whole ask. Accepting it is worse: version flows into docker buildx imagetools create -t "$IMAGE:$VERSION" at release.yml:222, and 1.2.3+build.5 is not a legal image tag, nor is the 1.2.3+build that MAJOR_MINOR="${VERSION%.*}" derives at :216. The suite at line 16 currently pins that broken value as correct. I dropped + from the allowlist and turned the build-metadata block into a rejection assertion: suite green, v0.7.0 still resolves, and putting + back turns it red, so the new case carries its weight. release-please emits plain vX.Y.Z, so nothing real regresses.

  • [P3] CI had never run on this head
    .github/workflows/pr-checks.yml:51
    PR Checks was sitting at action_required, so the release tag validation job had not executed once in the environment it exists for and the green rollup was triage jobs only. I approved the pending run, so it is going now. The job's own wiring reads correctly: pull_request and merge_group, no path filter, contents: read, persist-credentials: false.

  • [P3] Correct two claims in the description
    The body says the change "preserves safe +build metadata", which the finding above asks you to reverse, and that the drift guard requires every named Resolve release tag step to call the tested helper, which the bypass above disproves.

@beardthelion
beardthelion dismissed their stale review July 27, 2026 04:16

Superseded by my review on c00eaa2; dismissing so the state reflects the current head.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
scripts/test-resolve-release-tag.sh (1)

138-164: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Bypass fixture is coupled to release.yml's exact job order and step naming.

The resolver_calls == 2 check assumes the 2nd scripts/resolve-release-tag.sh occurrence is always the docker-manifest job, and the decoy insertion is anchored to a step literally named "Download digests". If jobs are reordered or that step is renamed, this fixture silently stops testing what it intends to (though check_resolver_steps would likely still fail correctly via the replaced-call detection alone). Consider anchoring on the docker-manifest: job header instead of an ordinal count for resilience against future release.yml restructuring.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-resolve-release-tag.sh` around lines 138 - 164, Make the bypass
fixture target the docker-manifest job explicitly instead of relying on
resolver_calls == 2 and the literal “Download digests” step name. Update the awk
logic around the release workflow transformation to track the docker-manifest:
job and inject the malicious resolver output and decoy call within that job,
preserving the existing check_resolver_steps assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@scripts/test-resolve-release-tag.sh`:
- Around line 138-164: Make the bypass fixture target the docker-manifest job
explicitly instead of relying on resolver_calls == 2 and the literal “Download
digests” step name. Update the awk logic around the release workflow
transformation to track the docker-manifest: job and inject the malicious
resolver output and decoy call within that job, preserving the existing
check_resolver_steps assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 02222c60-cae8-4263-ac30-a43f18051351

📥 Commits

Reviewing files that changed from the base of the PR and between c00eaa2 and 1975d29.

📒 Files selected for processing (2)
  • scripts/resolve-release-tag.sh
  • scripts/test-resolve-release-tag.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/resolve-release-tag.sh

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The #239 fix on the current tree looks correct. I still found gaps in the regression guard and CI evidence before I would call this fully ready.

What looks good

  • scripts/resolve-release-tag.sh rejects the original multiline workflow_dispatch payload (v1.2.3\nname=owned): non-zero exit, zero-byte $GITHUB_OUTPUT.
  • All three release jobs on head call the real script, not a comment or decoy.
  • beardthelion's earlier brace-group and +build follow-ups appear addressed on 73a6a0a.

Findings

  • [P2] Require a real resolver invocation, not a commented reference
    scripts/test-resolve-release-tag.sh:62
    This does not reopen #239 on the current diff, but it does leave the drift guard weaker than the PR description implies. resolver_call_re treats # scripts/resolve-release-tag.sh … as a call, so a step can comment out the script, keep the old inline path (or echo "$VAR" >> "$GITHUB_OUTPUT" with a multiline VAR), and scripts/test-resolve-release-tag.sh still exits 0. Please match only uncommented invocations.

  • [P2] Extend the per-step output guard beyond echo/printf
    scripts/test-resolve-release-tag.sh:63
    Same scope note: not a live bug in today's release.yml, but a real guard hole if someone regresses later. The brace-group bypass is fixed, yet a step that calls the resolver and then appends tag/version via tee -a "$GITHUB_OUTPUT" or a cat >> "$GITHUB_OUTPUT" heredoc still passes check_resolver_steps. Please reject any same-step tag=/version= write to $GITHUB_OUTPUT or $GITHUB_ENV, and add mutation cases for tee and heredoc appends.

  • [P2] Bind the drift guard to all three release jobs
    scripts/test-resolve-release-tag.sh:88
    Also a future-regression gap, not a current-tree failure. The guard only inspects steps literally named Resolve release tag and only fails when zero such steps exist. Renaming the docker job's resolver step and reverting that job to inline echo "tag=$TAG" >> "$GITHUB_OUTPUT" leaves two guarded steps and the suite still passes. Please assert all three call sites stay guarded, and add a rename/revert regression.

  • [P3] release tag validation has not reported on the current head
    .github/workflows/pr-checks.yml:51
    GitHub's status rollup for head 73a6a0ac6da814a09a1efea293c457c5d9a1c62f only shows Quality-signal triage and CodeRabbit; none of the PR Checks jobs completed on this SHA. This may be fork workflow-approval noise, but please get a green release tag validation run on the latest head before merge so the new suite is actually exercised in CI.

Merge posture

If the bar is only "fix #239 in today's workflow," the resolver half is there. If the bar is "make the new regression suite as load-bearing as described," the three guard items above still need another pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

workflow-change Fork PR edits CI workflows — mandatory human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci(release): unanchored tag validation glob lets a multiline dispatch input inject step outputs

3 participants