Check this receipt without trusting this site

gh attestation verify run-record-M-009.tar.gz --repo northset-oss/verification-pilot --signer-workflow northset-oss/verification-pilot/.github/workflows/attest-bundle.yml

Expected output includes Verification succeeded!

NORTHSET

Proof-of-Pass Receipt

Receipt ID M-009

ISSUE / WORKIdempotency bug in escaped semicolon in Markdown link · PR #19611

CONTRIBUTOR SELF-RUN — NOT MAINTAINER VERIFICATION

RUN1m 14s

01 / TECHNICAL RESULT

Technical result

1/1

declared command passed

1/1 declared command returned exit 0 in the recorded environment

Upstream
CLOSED UNMERGEDmutable external state
Environment
node:22-bookwormnetwork phaseA:bridge,phaseB:none
Signature
verified Northset signing workflow provenance

02 / DECLARED CHECKS

Command evidence

Execution summary
1/1 declared command returned exit 0 in the recorded environment

  1. node .yarn/releases/yarn-4.17.1.cjs jest tests/format/markdown

    exit 0 · 11.7s

unclassified executor time (derived residual) 1m02s

run wall (derived from recorded timestamps) 1m14s

Every command listed returned exit 0 in the declared environment. Only the listed commands are in scope. Unlisted test, lint, typecheck, build, coverage, compiler, full-suite, and CI gates are not implied or recorded.

03 / RECORDED IDENTITIES

Compact hashes

Patch diff SHA-256
sha256:c29d38…ac0a714
Container image digest
node@sha256:a25c99…127c365
Bundle contents digest
sha256:3655d6…fc2a0f5
Signed asset SHA-256
sha256:83000c…74dc9a8

04 / PUBLIC SCOPE INTERPRETATION

Public scope interpretation

The declared network-off check runs Prettier's full Markdown Jest suite (tests/format/markdown, 1476 tests including the new escaped-entity-semicolon regression fixture) on Node 22 via the repository's vendored Yarn, after a disclosed online phase-A install. It does not run the full repository test/lint matrix; upstream CI additionally runs multiple Node versions, ESLint, TypeScript typecheck, changelog lint, and production/browser tests.

05 / CLAIMS BOUNDARY

NOT INCLUDED

  • Does not prove code quality
  • Does not prove security
  • Contributor self-run record of Northset's own contribution; not the maintainer's verification.
06 / Evidence annexTechnical · provenance · full recorded values
01

Technical evidence

Code, full environment, patch, and redacted outputs

Project

prettier/prettier

Work

Idempotency bug in escaped semicolon in Markdown link · PR #19611

Verification execution

runtime: northset-oss executor v0
human operator: aeziz

Code

base
41a8b99bb8d7d68c00d90d5b8fbaed20511d4cfd
recorded patch commit
296019b33e287334febc9882b204c9501725116d
declared metadata; not execution-bound
patch diff SHA-256
sha256:c29d38…ac0a714
bound to executed patch bytes

Environment

image reference
node:22-bookworm
repository digest
node@sha256:a25c99…127c365
network
phaseA:bridge,phaseB:none
Committed patch.diff
diff --git a/changelog_unreleased/markdown/19611.md b/changelog_unreleased/markdown/19611.md
new file mode 100644
index 0000000000000000000000000000000000000000..9999818122d0ae50304e2ac651630fba2e0cf3dd
--- /dev/null
+++ b/changelog_unreleased/markdown/19611.md
@@ -0,0 +1,18 @@
+#### Preserve escaped semicolons that guard character references in Markdown link URLs (#19611 by @AysajanE)
+
+An escaped semicolon terminating an HTML character reference in a link/image URL
+(e.g. `&quot\;`) previously lost its backslash on the first format, producing a
+real entity that a second format then resolved — breaking idempotency. Prettier
+now keeps the escape so the URL round-trips.
+
+<!-- prettier-ignore -->
+```markdown
+<!-- Input -->
+[test](/hello?foo&quot\;bar)
+
+<!-- Prettier stable -->
+[test](/hello?foo&quot;bar)
+
+<!-- Prettier main -->
+[test](/hello?foo&quot\;bar)
+```
diff --git a/src/language-markdown/print/mdast.js b/src/language-markdown/print/mdast.js
index ca3a5991bfd3f8bac52b1a6e58f297c5a453ca33..7674f66ed0a451b7dca0cfe99cc65faec9b36239 100644
--- a/src/language-markdown/print/mdast.js
+++ b/src/language-markdown/print/mdast.js
@@ -531,11 +531,18 @@ function printUrl(url, dangerousCharOrChars = []) {
       : [dangerousCharOrChars]),
   ];
 
-  return new RegExp(
+  const printedUrl = new RegExp(
     dangerousChars.map((x) => escapeStringRegexp(x)).join("|"),
   ).test(url)
     ? `<${encodeUrl(url, "<>")}>`
     : url;
+
+  // An escaped semicolon is decoded by the parser. Escape it again when it
+  // would otherwise turn the preceding text into a character reference.
+  return printedUrl.replaceAll(
+    /&(?:#\d{1,7}|#x[0-9A-F]{1,6}|[A-Z][A-Z0-9]{1,30});/gi,
+    (reference) => String.raw`${reference.slice(0, -1)}\;`,
+  );
 }
 
 function printTitle(title, options, printSpace = true) {
diff --git a/tests/format/markdown/link/__snapshots__/format.test.js.snap b/tests/format/markdown/link/__snapshots__/format.test.js.snap
index f896dc4143af354c5856f6a20dfc481bb8e4ff40..eea6d5d9f24578d23827bd39944708b7f1929829 100644
--- a/tests/format/markdown/link/__snapshots__/format.test.js.snap
+++ b/tests/format/markdown/link/__snapshots__/format.test.js.snap
@@ -129,6 +129,56 @@ proseWrap: "always"
 ================================================================================
 `;
 
+exports[`escaped-entity-semicolon.md - {"proseWrap":"always"} format 1`] = `
+====================================options=====================================
+parsers: ["markdown"]
+proseWrap: "always"
+                                                      printWidth: 80 (default) |
+=====================================input======================================
+[test](/hello?foo&quot\\;bar)
+
+[test](/hello?foo&quot;bar)
+
+![image](/hello?foo&amp\\;bar)
+
+[decimal](/hello?foo&#123\\;bar)
+
+[hex-lowercase](/hello?foo&#xAB\\;bar)
+
+[hex-uppercase](/hello?foo&#XAB\\;bar)
+
+[non-entity](/hello?foo;a;b)
+
+[wrapped](</hello?foo=&quot\\;bar baz>)
+
+[reference][entity]
+
+[entity]: /hello?foo&#123\\;bar
+
+=====================================output=====================================
+[test](/hello?foo&quot\\;bar)
+
+[test](/hello?foo"bar)
+
+![image](/hello?foo&amp\\;bar)
+
+[decimal](/hello?foo&#123\\;bar)
+
+[hex-lowercase](/hello?foo&#xAB\\;bar)
+
+[hex-uppercase](/hello?foo&#XAB\\;bar)
+
+[non-entity](/hello?foo;a;b)
+
+[wrapped](</hello?foo=&quot\\;bar baz>)
+
+[reference][entity]
+
+[entity]: /hello?foo&#123\\;bar
+
+================================================================================
+`;
+
 exports[`long.md - {"proseWrap":"always"} format 1`] = `
 ====================================options=====================================
 parsers: ["markdown"]
diff --git a/tests/format/markdown/link/escaped-entity-semicolon.md b/tests/format/markdown/link/escaped-entity-semicolon.md
new file mode 100644
index 0000000000000000000000000000000000000000..89b76c23968ce1f628dc68b9cb1f24d254f3b999
--- /dev/null
+++ b/tests/format/markdown/link/escaped-entity-semicolon.md
@@ -0,0 +1,19 @@
+[test](/hello?foo&quot\;bar)
+
+[test](/hello?foo&quot;bar)
+
+![image](/hello?foo&amp\;bar)
+
+[decimal](/hello?foo&#123\;bar)
+
+[hex-lowercase](/hello?foo&#xAB\;bar)
+
+[hex-uppercase](/hello?foo&#XAB\;bar)
+
+[non-entity](/hello?foo;a;b)
+
+[wrapped](</hello?foo=&quot\;bar baz>)
+
+[reference][entity]
+
+[entity]: /hello?foo&#123\;bar
Redacted stdout
=== cmd 1: node .yarn/releases/yarn-4.17.1.cjs jest tests/format/markdown ===
Redacted stderr
=== cmd 1: node .yarn/releases/yarn-4.17.1.cjs jest tests/format/markdown ===
PASS tests/format/markdown/trim-space/format.test.js
PASS tests/format/markdown/splitCjkText/format.test.js
PASS tests/format/markdown/commonmark-test-suite-legacy/format.test.js
PASS tests/format/markdown/gfm-test-suite/format.test.js
PASS tests/format/markdown/auto-link/format.test.js
PASS tests/format/markdown/splitCjkText/non-bmp/format.test.js
PASS tests/format/markdown/list/empty-list-items/format.test.js
PASS tests/format/markdown/heading/setext/format.test.js
PASS tests/format/markdown/wiki-link/format.test.js
PASS tests/format/markdown/list/indent/format.test.js
PASS tests/format/markdown/broken-plugins/format.test.js
PASS tests/format/markdown/footnoteDefinition/format.test.js
PASS tests/format/markdown/table/empty-table/format.test.js
PASS tests/format/markdown/inlineCode/format.test.js
PASS tests/format/markdown/html/format.test.js
PASS tests/format/markdown/paragraph/format.test.js
PASS tests/format/markdown/commonmark-test-suite/format.test.js
PASS tests/format/markdown/link/quotes/format.test.js
PASS tests/format/markdown/jsx-semi/format.test.js
PASS tests/format/markdown/fenced-code-block/format.test.js
PASS tests/format/markdown/list/huge-first-number/format.test.js
PASS tests/format/markdown/linkReference/format.test.js
PASS tests/format/markdown/blockquote/format.test.js
PASS tests/format/markdown/code/lwc/format.test.js
PASS tests/format/markdown/break/list-item/format.test.js
PASS tests/format/markdown/code/angular/format.test.js
PASS tests/format/markdown/liquid/format.test.js
PASS tests/format/markdown/long-table/format.test.js
PASS tests/format/markdown/indentation/format.test.js
PASS tests/format/markdown/multiparser-json/format.test.js
PASS tests/format/markdown/list/parser-regression/format.test.js
PASS tests/format/markdown/multiparser-js/format.test.js
PASS tests/format/markdown/link/parser-regression/format.test.js
PASS tests/format/markdown/word/format.test.js
PASS tests/format/markdown/thematicBreak/format.test.js
PASS tests/format/markdown/toml/format.test.js
PASS tests/format/markdown/table/format.test.js
PASS tests/format/markdown/strong/format.test.js
PASS tests/format/markdown/spec-legacy/format.test.js
PASS tests/format/markdown/yaml/format.test.js
PASS tests/format/markdown/link/format.test.js
PASS tests/format/markdown/image/format.test.js
PASS tests/format/markdown/imageReference/format.test.js
PASS tests/format/markdown/ignore/format.test.js
PASS tests/format/markdown/heading/format.test.js
PASS tests/format/markdown/footnote/format.test.js
PASS tests/format/markdown/footnoteReference/format.test.js
PASS tests/format/markdown/emphasis/format.test.js
PASS tests/format/markdown/list/format.test.js
PASS tests/format/markdown/delete/format.test.js
PASS tests/format/markdown/definition/format.test.js
PASS tests/format/markdown/break/format.test.js
PASS tests/format/markdown/list/blank-lines/format.test.js
PASS tests/format/markdown/wiki-link/alias/format.test.js
PASS tests/format/markdown/list/task-list/format.test.js
PASS tests/format/markdown/linkReference/case-and-space/format.test.js
PASS tests/format/markdown/html/html/format.test.js
PASS tests/format/markdown/markdown/format.test.js
PASS tests/format/markdown/math/format.test.js
PASS tests/format/markdown/front-matter/format.test.js
PASS tests/format/markdown/multiparser-css/format.test.js
PASS tests/format/markdown/cursor/format.test.js
PASS tests/format/markdown/code/format.test.js

Test Suites: 63 passed, 63 total
Tests:       1476 passed, 1476 total
Snapshots:   1465 passed, 1465 total
Time:        7.23 s
Ran all test suites matching tests/format/markdown.
02

Provenance & record

Full hashes, bundle identity, attestation, and record details

Record details

payment
none · not merge-contingent
redactions
none recorded
Bundle contents digest
sha256:3655d6…fc2a0f5
Signed asset SHA-256
sha256:83000c…74dc9a8
Signed provenance recorded
verified

Full cryptographic values

Full recorded values. The compact receipt above shortens these for legibility only.

Patch diff SHA-256
sha256:c29d387a39ad255b7b8e506095b4281e5b9a704b6de656b55377629dcac0a714
Container image digest
node@sha256:a25c9934ff6382cd4f08b6bc26c82bf4ea69b1e6f8dabfb2ead457374127c365
Bundle contents digest
sha256:3655d695fef0b43a3af656fbbbc5d260a84f69eebcdcc4fe76e435174fc2a0f5
Signed asset SHA-256
sha256:83000c6ca030d502b4b3f1fc6afd16168bb1148c3699d17dc618792c874dc9a8

Signed bundle

Download signed bundle

Verify this receipt

gh attestation verify run-record-M-009.tar.gz --repo northset-oss/verification-pilot --signer-workflow northset-oss/verification-pilot/.github/workflows/attest-bundle.yml

Attestation confirms that Northset's signing workflow produced this exact bundle. The signer does not witness the recorded run, and verification does not turn it into maintainer verification.

QR → receipt page
Northset contributed this fix and ran its declared checks. Contributor self-run. Not maintainer verification.

Evidence of what ran — not a verdict that the code is good.

SELF-FUNDED FIELD-TESTING.

- - - detach here - - -

External status

Mutable upstream observation; unattested and separate from the signed run record.

PR state
CLOSED UNMERGED
GitHub review decision
NONE
Upstream updated
Observed
CLOSED UNMERGED

Recorded upstream outcome · open linked record

All Northset work in prettier/prettier →

FOR MAINTAINERS

Maintain prettier/prettier?

Get this same run for any PR in your queue — private, free during the pilot, nothing published without your approval.

The issue form is public. Do not include secrets or private repository details there; use email instead.

Already onboarded? Add northset-verify to a PR to request a run on that PR.

Claims boundary

This page reports scoped proof-of-pass receipt evidence. It does not prove code quality, security, full CI coverage, production readiness, or maintainer approval. An attestation confirms bundle provenance; it does not broaden the receipt's claim.

Read the full Claims Boundary policy.