Visitar URL original
Render changelog subjects as literal text by metcalfc · Pull Request #475 · metcalfc/changelog-generator · GitHub
Skip to content

Render changelog subjects as literal text - #475

Merged
metcalfc merged 3 commits into
mainfrom
security/escape-release-markdown
Aug 26, 2026
Merged

metcalfc merged 3 commits into
mainfrom
security/escape-release-markdown

Conversation

@metcalfc

Copy link
Copy Markdown
Owner

Summary

  • parse Git history as NUL-delimited records instead of rendering subjects directly into Markdown
  • normalize control, line-separator, and bidirectional-control characters
  • render each untrusted subject inside a dynamically sized inline-code span
  • rebuild the checked-in action bundle and make the release workflow use the checked-out action
  • add source, built-bundle, active-Markdown, control-character, and release-workflow regression tests

Security invariant

Commit subjects remain visible as literal text but cannot create links, images, mentions, issue/commit references, emoji, raw HTML, or additional changelog entries. The generated commit link remains active.

Verification

  • npm test — 32/32
  • npm run lint
  • npm run format-check
  • reproducible npm run build
  • actionlint (excluding the existing major-tag quoting warning handled in a separate branch)
  • git diff --check

metcalfc and others added 2 commits August 25, 2026 21:02
Rendering subjects as literal Markdown moved the work into node, and the
script reached for it on PATH. That holds on hosted runners but not in a
container job: the runner mounts its own node at /__e/node<version>/bin/node
and never adds it to the container's PATH, so images without a system node
failed the whole action with a bare exit 127.

index.js now passes process.execPath as ACTION_NODE -- the interpreter
already running the bundle -- and the script falls back to PATH when invoked
standalone, as the tests do. The regression test pins ACTION_NODE to an
unusable path, so a script that quietly went back to PATH would fail it;
asserting only that a stripped PATH works would pass on any image carrying a
system node in /usr/bin.

Document the output change while we are here. Wrapping subjects in code spans
is what stops a commit message from contributing links, mentions, or extra
entries to a release, and it costs #123 and bare-SHA autolinking inside the
subject. That is worth stating in the README rather than leaving users to
discover it from their next release notes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EmepbnR2nSdk8q8DBsrsD
@metcalfc
metcalfc force-pushed the security/escape-release-markdown branch from 4bb0001 to 1d64103 Compare August 26, 2026 04:04
The existing assertions compare changelog.sh against literalMarkdownSubject
in the test helper, which is a second copy of the same logic. That catches a
bug in the script alone, but not one written into both. Add a table of
hand-derived input/output pairs that never touches the helper, covering the
fence-widening cases and control-character normalization.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EmepbnR2nSdk8q8DBsrsD
@metcalfc
metcalfc merged commit 91ac6bb into main Aug 26, 2026
6 checks passed
@metcalfc
metcalfc deleted the security/escape-release-markdown branch August 26, 2026 04:10
metcalfc added a commit that referenced this pull request Aug 26, 2026
…#477)

bump:workflow rewrote every vX.Y.Z in .github/workflows to this project's new
version. Its one real target was the self-reference in release.yml, and #475
replaced that with `uses: ./` -- so the script now has no legitimate target
and only collateral damage.

Cutting a release today would rewrite

  actions/setup-node@8207627... # was: actions/setup-node@v7.0.0

to `# was: actions/setup-node@v4.9.0`, a release of setup-node that does not
exist. gh-refme maintains those comments so a reviewer can tell what upstream
release a pinned SHA corresponds to; corrupting them defeats the pinning the
README recommends. It is cosmetic to the runtime and misleading to a human,
which is the worse half.

Anchor the substitution to the self-reference. It is a no-op while the
workflows use `uses: ./`, and correct again if the pin ever returns. The test
covered the old behaviour, so it asserted the corruption; it now proves a
third-party provenance comment survives a bump.


Claude-Session: https://claude.ai/code/session_015EmepbnR2nSdk8q8DBsrsD

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
metcalfc added a commit that referenced this pull request Aug 26, 2026
actionlint flagged SC2086 on the unquoted $GITHUB_OUTPUT. Quoting it alone
would have polished a snippet that does not work.

The example percent-encodes newlines as %0A, which the removed ::set-output
command used to decode. $GITHUB_OUTPUT does not, so the step has been emitting
one line with literal %0A in it. The latest end-to-end run shows exactly that:

  %0A- [c74bcf9](...) - ` Fix README example spacing. `%0A- [8ea6bc3](...)

Use the heredoc form, which is how a multiline value is returned now. The
delimiter is random rather than fixed because the value is built from commit
subjects: a fixed marker is a string a commit subject could contain in order
to write further keys into $GITHUB_OUTPUT, which is the same class of problem
as the Markdown injection fixed in #475.

README carried the same broken snippet, so it is fixed there too, along with
the prose explaining the escaping and its dead github.community link.


Claude-Session: https://claude.ai/code/session_015EmepbnR2nSdk8q8DBsrsD

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
metcalfc added a commit that referenced this pull request Aug 26, 2026
actionlint flagged SC2086 on the unquoted $GITHUB_OUTPUT. Quoting it alone
would have polished a snippet that does not work.

The example percent-encodes newlines as %0A, which the removed ::set-output
command used to decode. $GITHUB_OUTPUT does not, so the step has been emitting
one line with literal %0A in it. The latest end-to-end run shows exactly that:

  %0A- [c74bcf9](...) - ` Fix README example spacing. `%0A- [8ea6bc3](...)

Use the heredoc form, which is how a multiline value is returned now. The
delimiter is random rather than fixed because the value is built from commit
subjects: a fixed marker is a string a commit subject could contain in order
to write further keys into $GITHUB_OUTPUT, which is the same class of problem
as the Markdown injection fixed in #475.

README carried the same broken snippet, so it is fixed there too, along with
the prose explaining the escaping and its dead github.community link.

Claude-Session: https://claude.ai/code/session_015EmepbnR2nSdk8q8DBsrsD

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 0bee5ad)
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.

1 participant