Skip to content

fix(ci): write git-refs base/head through the heredoc writer - #1787

Merged
mergify[bot] merged 3 commits into
mainfrom
devs/sileht/mrgfy-8845-git-refs-base-validation/write-git-refs-base-head-heredoc-writer--2f7b6c72
Aug 26, 2026
Merged

fix(ci): write git-refs base/head through the heredoc writer#1787
mergify[bot] merged 3 commits into
mainfrom
devs/sileht/mrgfy-8845-git-refs-base-validation/write-git-refs-base-head-heredoc-writer--2f7b6c72

Conversation

@sileht

@sileht sileht commented Aug 25, 2026

Copy link
Copy Markdown
Member

write_github_output emitted base=<value> and head=<value> as bare
lines. GitHub Actions has no escape in that form: a newline in the
value ends the assignment and every following line becomes another
step output, so whatever chose the value also chose how many outputs
the step declares. head was the only one that could not be
overwritten, because the genuine head= line follows and the last
assignment wins.

Route both through github_output::append, which writes the runner's
heredoc form under a random delimiter. Consuming workflows see the same
values, GitHub parses both forms identically.

Validating checking_base_sha already keeps a newline out of base
today. This is the layer under that: it holds whatever reaches these
outputs next, without the next author having to notice.

Fixes MRGFY-8845

Depends-On: #1786

sileht added 3 commits August 25, 2026 12:37
`ci git-refs` took the merge-queue `checking_base_sha` verbatim out of
the merge-queue draft pull request body and emitted it as `base`.
`queue_metadata::extract_from_event` admits a body on the title prefix
`merge queue: ` and nothing else, so whoever opens the pull request
writes that value, fork PRs included.

`base` then leaves the CLI through several sinks that read a leading
`-` as an option: `$GITHUB_OUTPUT` and the `--format=shell` eval hand
it to the caller's workflow, `ci scopes` puts it in git invocations,
and both commands echo it to stderr, which GitHub Actions also scans
for `::` workflow commands. The reporter's proof run put
`checking_base_sha: --output=/home/runner/.gitconfig` in a fork PR body
and truncated that file inside the job.

Require a full hex object name, 40 digits or 64, before the value can
become a base. Abbreviations are out on purpose:
`scopes_detect::changed_files::is_sha` routes only full SHAs as
revisions, so a short one is fetched as a branch name and fails the
run. Nothing honest is lost, the engine types the field
`github_types.SHAType` and writes a full SHA.

The gate is on the pull request body alone, not on the git note that
carries the same field. That asymmetry is deliberate: the note comes
from the engine over a push to `origin`, and a note the check rejected
would fall through to the body path, handing the untrusted payload the
precedence the note is meant to hold, silently, since
`real_notes_reader` has no `Output` to warn through.

A rejected value is not metadata, so it falls through to the pull
request's own base with a warning, the shape already used when the key
is missing. The warning renders the value escaped and cut to a bounded
length, so it can neither start a workflow command of its own nor fill
the log.

Reported as HackerOne #3965784, closed Informative: the blast radius is
the ephemeral runner the pull request itself triggered.

Fixes MRGFY-8845

Change-Id: Ie24ad032ac9c96b1231e76bc30ece50cd905d8d2
`ci queue-info` and `ci scopes` each carried their own copy of the
"open $GITHUB_OUTPUT, draw a random ghadelimiter_ suffix, write the
heredoc" sequence, including a byte-for-byte duplicate of
`random_delimiter_suffix`. `ci git-refs` needs the same thing next,
which would have made three.

Move it to a `github_output` module that takes the `(name, value)`
pairs and does the rest, so a call site cannot pick the bare
`name=value` form by accident: the heredoc is the only form the module
emits, and its delimiter comes fresh from the OS RNG per output.

The name half of the pair is `&'static str` rather than `&str`. A
newline in a name, or an `=` ahead of the `<<`, lets the runner read
the block as something else, and the type keeps a name derived from a
payload out by construction.

The block is assembled and written once. Three `writeln!` calls on an
unbuffered `File` are several `write_all` syscalls each, and a
sequence cut in the middle leaves an unterminated heredoc, which fails
the step outright rather than losing a line.

`junit_process` keeps its own bare-form writer; the module says why.

What is written does not change. GitHub parses both forms into the
same output value, and both migrated call sites already wrote the
heredoc. Each now builds its payload before the `$GITHUB_OUTPUT` check
rather than after, so off a GitHub Actions runner one small string is
built and dropped.

Fixes MRGFY-8845

Change-Id: Ie32fc586e5e6313a5f46f63b6da728ddf867f850
`write_github_output` emitted `base=<value>` and `head=<value>` as bare
lines. GitHub Actions has no escape in that form: a newline in the
value ends the assignment and every following line becomes another
step output, so whatever chose the value also chose how many outputs
the step declares. `head` was the only one that could not be
overwritten, because the genuine `head=` line follows and the last
assignment wins.

Route both through `github_output::append`, which writes the runner's
heredoc form under a random delimiter. Consuming workflows see the same
values, GitHub parses both forms identically.

Validating `checking_base_sha` already keeps a newline out of `base`
today. This is the layer under that: it holds whatever reaches these
outputs next, without the next author having to notice.

Fixes MRGFY-8845

Change-Id: I2f7b6c72f55767b7ea90762cfced8965a8342d94
@mergify
mergify Bot deployed to Mergify Merge Protections August 25, 2026 10:42 Active
@sileht

sileht commented Aug 25, 2026

Copy link
Copy Markdown
Member Author

This pull request is part of a Mergify stack:

# Pull Request Link
1 fix(ci): reject a checking_base_sha that is not a git object name #1785
2 refactor(ci): share one $GITHUB_OUTPUT heredoc writer #1786
3 fix(ci): write git-refs base/head through the heredoc writer #1787 👈

@mergify

mergify Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 All 7 merge protections satisfied — ready to merge.

Show 7 satisfied protections

🟢 ⛓️ Depends-On Requirements

Requirement based on the presence of Depends-On in the body of the pull request

🟢 🤖 Continuous Integration

  • all of:
    • check-success=ci-gate

🟢 👀 Review Requirements

  • any of:
    • #approved-reviews-by>=2
    • author = dependabot[bot]
    • author = mergify-ci-bot
    • author = renovate[bot]

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|internal|docs|style|refactor|perf|test|build|ci|chore|revert|ui)(?:\(.+\))?!?:

🟢 🔎 Reviews

  • #changes-requested-reviews-by = 0
  • #review-requested = 0
  • #review-threads-unresolved = 0

🟢 📕 PR description

  • body ~= (?ms:.{48,})

🟢 🚦 Auto-queue

When all merge protections are satisfied, this pull request will be queued automatically.

@mergify
mergify Bot requested a review from a team August 25, 2026 10:48
@sileht
sileht marked this pull request as ready for review August 25, 2026 11:46
Base automatically changed from devs/sileht/mrgfy-8845-git-refs-base-validation/share-one-github-output-heredoc-writer--e32fc586 to main August 25, 2026 19:46
@mergify
mergify Bot requested a review from a team August 26, 2026 06:53
@mergify

mergify Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Merge Queue Status

This pull request spent 8 minutes 33 seconds in the queue, including 7 minutes 43 seconds running CI.

Required conditions to merge

@mergify mergify Bot added the queued label Aug 26, 2026
@mergify
mergify Bot merged commit 5c5c405 into main Aug 26, 2026
22 checks passed
@mergify
mergify Bot deleted the devs/sileht/mrgfy-8845-git-refs-base-validation/write-git-refs-base-head-heredoc-writer--2f7b6c72 branch August 26, 2026 13:24
@mergify mergify Bot removed the queued label Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants