Skip to content

feat(ci): add a manual workflow_dispatch trigger to the hash sync - #45

Merged
mcfbytes merged 2 commits into
masterfrom
feat/hash-sync-manual-dispatch
Jul 19, 2026
Merged

feat(ci): add a manual workflow_dispatch trigger to the hash sync#45
mcfbytes merged 2 commits into
masterfrom
feat/hash-sync-manual-dispatch

Conversation

@mcfbytes

Copy link
Copy Markdown
Owner

Adds a workflow_dispatch trigger with a required branch input to renovate-hash-sync.yml.

Why this is necessary, not convenient

There is currently no way at all to re-drive this workflow after fixing a bug in it. Three things block every route, and all three held simultaneously on run 29669946883:

route why it fails
push the fix to the branch the paths: filter doesn't list the workflow file, so nothing triggers
a maintainer merges the fix in the actor == renovate[bot] gate skips the job
re-run the failed run a re-run replays the workflow definition from the commit the original run was created against — so it re-executes the old code

That run went through three attempts across two separate fixes and executed the same broken kernel-URL code every time. Confirmed by pulling attempt 3's log — it ran the pre-#42 unanchored grep, with none of #42's new comments present:

kver=$(grep -oE 'BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="[^"]+"' "$defconfig" \
"ANCHOR THIS GREP" present: 0
curl: (3) URL rejected: Malformed input to a URL function

Only Renovate moving the branch head, or a manual trigger, gets new code to run.

The input is the branch to repair, not the branch to run from

This distinction is the whole trap, so it's called out in the input description, the workflow header, and docs/renovate.md:

GitHub runs the workflow definition from the ref you dispatch on, while this workflow checks out and pushes to inputs.branch.

So: dispatch from the default branch (which has the fixed workflow) and name the broken branch — e.g. renovate/kernel-longterm-6.18-6.x — in the input. Dispatching on the stale branch would just run its stale copy again, reproducing the original problem.

Safety

  • Manual runs refuse to target the default branch. This workflow commits and pushes; an auto-generated hash commit must go through a PR. An empty input is rejected too.
  • The job gate doesn't widen. workflow_dispatch is already restricted to collaborators with write access — the same bar the renovate[bot] gate enforces on the automatic path.
  • Concurrency keys on the target branch for dispatch runs, so manual runs don't all share one empty-suffix group and cancel each other.

Fixes the adjacent pre-existing injection finding

I flagged this on #43 as out of scope, but this PR touches exactly those expressions, so it's fixed here. The push step interpolated github.event.pull_request.head.ref straight into the script body — a command-injection vector, since a branch name is attacker-chosen text on a PR from an untrusted contributor. Both use sites now route through a job-level TARGET_BRANCH env var, and the push uses -- so a branch named like an option isn't parsed as one. actionlint's potentially untrusted warning is gone (was 1, now 0).

Two stale header claims corrected

  • The header said this workflow "has never been exercised against a real Renovate PR". It has now — chore(deps): update dependency kernel-longterm-6.18 to v6.18.39 #41, kernel 6.18.38 → 6.18.39, end to end.
  • More importantly, it undersold the failure mode: the warn-and-skip paths can report SUCCESS while doing nothing, which is exactly how the stale linux.hash went unnoticed through three green runs. The header now says to read every warn-and-skip path that way.

The github-package loop and the lzma-sdk / sdcard-payload steps remain genuinely unproven, and still say so.

Verification

  • actionlint clean (apart from its stale runner-label list not knowing ubuntu-26.04, which every workflow in this repo trips).
  • shellcheck clean on all six run: blocks.
  • Guard logic exercised directly: a renovate branch passes; the default branch and an empty input are both rejected.
  • actionlint also caught a real self-inflicted bug during authoring — a literal expression-looking token inside a run: comment, which it parses as an empty expression. Fixed before commit.

Note I could not execute a real dispatch run, since the trigger only exists once this is on the default branch. First live use will be its own proof.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UFdgJXQKvrucXeKDami499

Without this there is NO way to re-drive renovate-hash-sync.yml after fixing a
bug in it. Three things block every other route, and all three held at once on
run 29669946883, which executed the same broken kernel-URL code three times
across two separate fixes:

  * pushing the fix to the branch triggers nothing — the paths: filter does
    not list the workflow file itself;
  * a maintainer merging the fix in is skipped by the actor == renovate[bot]
    gate;
  * and a re-run replays the workflow definition from the commit the ORIGINAL
    run was created against, so it faithfully re-executes the old code. Pulling
    attempt 3's log confirmed it ran the pre-#42 unanchored grep.

Only Renovate moving the branch head, or a manual trigger, gets new code to run.

The `branch` input is required and is NOT the ref you dispatch from: GitHub
runs the workflow definition from the dispatch ref while this workflow checks
out and pushes to inputs.branch. Dispatch from the default branch (fixed
workflow), name the broken branch in the input. Dispatching *on* the stale
branch would just re-run its stale copy — the exact trap this fixes.

Manual runs refuse to target the default branch: this workflow commits and
pushes, and an auto-generated hash commit must go through a PR.
workflow_dispatch is already restricted to collaborators with write access, so
the job gate does not widen who can drive this.

Also resolves the pre-existing actionlint finding it was adjacent to: the push
step interpolated github.event.pull_request.head.ref straight into the script
body, a command-injection vector via an attacker-chosen branch name on a PR
from an untrusted contributor. Both use sites now go through a job-level
TARGET_BRANCH env var, and the push uses `--` so a branch named like an option
is not parsed as one. actionlint's untrusted-expression warning is gone.

Corrected two stale header claims while in here: the workflow HAS now run
against a real Renovate PR (#41, 6.18.38 -> 6.18.39), and its warn-and-skip
paths can report SUCCESS while doing nothing — which is exactly how the stale
linux.hash went unnoticed. The github-package loop and the lzma-sdk and
sdcard-payload steps remain unproven.

actionlint and shellcheck clean. Guard logic tested: a renovate branch passes;
the default branch and an empty input are both rejected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UFdgJXQKvrucXeKDami499
Copilot AI review requested due to automatic review settings July 19, 2026 03:30

Copilot AI 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.

Pull request overview

Adds a manual workflow_dispatch escape hatch to the Renovate hash-sync workflow so maintainers can re-drive the hash refresh after workflow fixes land on the default branch, and updates the accompanying Renovate documentation to explain the “dispatch from vs. target branch” distinction.

Changes:

  • Add workflow_dispatch with required branch input to .github/workflows/renovate-hash-sync.yml, plus branch-keyed concurrency for dispatch runs.
  • Refactor target-branch handling into a single TARGET_BRANCH env var and use git push ... -- "HEAD:$TARGET_BRANCH" to avoid branch-name injection/option parsing.
  • Document the manual dispatch behavior and rationale in docs/renovate.md.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
docs/renovate.md Documents the new manual workflow_dispatch escape hatch and how to use the branch input safely.
.github/workflows/renovate-hash-sync.yml Adds manual dispatch trigger, dispatch-safe concurrency grouping, target-branch resolution, and safer push behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +200 to +208
if [ -z "$TARGET_BRANCH" ]; then
echo "::error::branch input resolved to empty -- nothing to check out." >&2
exit 1
fi
if [ "$TARGET_BRANCH" = "$DEFAULT_BRANCH" ]; then
echo "::error::refusing to run against the default branch ('$TARGET_BRANCH') -- this workflow commits and pushes hash fixes, which must go through a PR." >&2
exit 1
fi
echo "target branch: $TARGET_BRANCH"
From Copilot's review on #45.

The guard compared $TARGET_BRANCH to the default branch as a plain string, so
dispatching `refs/heads/master` slipped past it while actions/checkout and
`git push` still resolved it to master.

Verified with `git push --dry-run`, and the hole is wider than reported --
THREE spellings all reach master, two of which bypassed the check:

    master              ->  master     (caught)
    refs/heads/master   ->  master     (bypassed)
    heads/master        ->  master     (bypassed; not in the review)

So normalising away a `refs/heads/` prefix, as suggested, would still have left
`heads/master` open.

Rejecting all ref-namespace forms instead of normalising them, deliberately:
TARGET_BRANCH is consumed by BOTH actions/checkout and the final git push, so
the value validated here must be the exact value those steps use. Normalising
would mean re-exporting through $GITHUB_ENV and depending on it overriding the
job-level env: for subsequent steps -- a precedence subtlety not worth betting
a push to the default branch on.

Also added `git check-ref-format --branch` as a syntactic gate (rejects
embedded spaces, "..", a leading "-", empty), and dropped the
event_name == workflow_dispatch condition so the automatic path is validated
too. That costs nothing and means a PR head branch is checked as well.

Verified, 12 cases: legitimate names (renovate/..., feature/x) pass; master,
refs/heads/master, heads/master, remotes/..., tags/..., -x, "a..b", "x y" and
empty are all rejected, each with the message that explains which rule fired.

shellcheck clean; actionlint clean apart from its stale runner-label list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UFdgJXQKvrucXeKDami499
@mcfbytes

Copy link
Copy Markdown
Owner Author

Valid finding, fixed in 0246991 — and the hole is wider than reported.

Verified with git push --dry-run

Three spellings all reach master, two of which bypassed the guard:

input pushes to master old guard
master yes caught
refs/heads/master yes bypassed — the finding
heads/master yes bypassed — not in the review

So the suggested fix of normalising away a refs/heads/ prefix would still have left heads/master open. (remotes/... and tags/... don't reach master, but do create junk refs, so they're rejected too.)

Rejecting, not normalising — deliberately

TARGET_BRANCH is consumed by both actions/checkout and the final git push. The value validated has to be the exact value those steps use. Normalising in the guard step would mean re-exporting through $GITHUB_ENV and depending on that overriding the job-level env: for subsequent steps — a precedence subtlety I don't want to bet a push to the default branch on. Rejecting keeps one value end to end.

Also added git check-ref-format --branch as a syntactic gate, and dropped the event_name == 'workflow_dispatch' condition so the automatic path gets validated too. Costs nothing, and means a PR head branch is checked as well.

All 12 cases

'renovate/kernel-longterm-6.18-6.x'  ALLOW
'feature/x'                          ALLOW
'master'                             REJECT (default branch)
'refs/heads/master'                  REJECT (not a plain branch name)
'heads/master'                       REJECT (not a plain branch name)
'refs/heads/other'                   REJECT (not a plain branch name)
'remotes/origin/master'              REJECT (not a plain branch name)
'tags/v1'                            REJECT (not a plain branch name)
'-x'                                 REJECT (invalid branch name)
'a..b'                               REJECT (invalid branch name)
'x y'                                REJECT (invalid branch name)
''                                   REJECT (empty)

Each rejection reports which rule fired. shellcheck clean; actionlint clean apart from its stale runner-label list.

@mcfbytes
mcfbytes merged commit c112f82 into master Jul 19, 2026
@mcfbytes
mcfbytes deleted the feat/hash-sync-manual-dispatch branch July 19, 2026 04:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants