What happens
ReadmeEditor pairs the last start marker in the document with the last end marker in the document. Neither is paired relative to the other. A README that repeats either marker after a real pair — in a fenced example, say — therefore has the wrong two markers paired, and the text between them rewritten.
Two shapes, both reproduced against the built CLI:
- A repeated end marker widens the span. Everything between the real end marker and the repeat is replaced by the generated content. User prose is deleted.
- A repeated start marker moves the start. The generated content lands at the repeat, and the tail of the README between the real pair and the repeat is duplicated.
Either way the tool rewrites content outside a marker pair, which docs/tool-contract.md calls a breaking change: content outside a pair is the user's.
Where
src/helpers.ts:
export function indexOfRegex(str: string, providedRegex: RegExp): number {
// ... walks every match and keeps the last one
}
The name says first; the body returns the last. ReadmeEditor.getTokenIndexes uses it for the end marker and lastIndexOfRegex for the start marker, so both ends of the span come from the last match anywhere in the file.
The (^|[^\])` guard does not help here. It excludes a marker quoted inline between backticks or escaped with a backslash. A marker alone on its own line inside a fence is preceded by a newline, so it matches like any other.
Reproduction
action.yml:
name: Decoy Action
description: An action whose README documents the markers
inputs:
token:
description: A token
required: true
runs:
using: node20
main: index.js
README.md, repeated end marker:
# Decoy
<!-- start inputs -->
| old | table |
|---|---|
<!-- end inputs -->
## USER PROSE THE TOOL MUST NOT TOUCH
* a bullet the user wrote
Closing marker looks like:
```text
<!-- end inputs -->
```
Run the generator. The heading, the bullet, the sentence and the fence opener are gone: the span now runs from the real start marker to the repeat inside the fence.
Swap that fenced line for <!-- start inputs --> and the mirror shape appears instead — the table, the end marker, the prose and the fence opener are all emitted twice.
Workarounds that do work
Both are tested against the built CLI and both leave the file correct:
- Put marker examples above the generated sections rather than below.
- Quote the markers inline between backticks, which the guard excludes.
This repository's own README uses the first form.
Why this is filed separately from #668
#668 is about formatter scope, and the fix for it (PR #690) leaves this untouched: the pairing rule is in getTokenIndexes, not in the formatter. PR #690's outside-content check in scripts/verify-readme-contract.mjs does catch both shapes, so the integration workflow reports them against a third-party repository rather than staying silent. That check deliberately does not reuse getTokenIndexes — a mask built on the editor's own pairing would hide the very bytes a mis-paired span destroyed.
Impact
This tool runs against arbitrary third-party repositories, and a README that documents the markers is an ordinary shape for a repository that uses this tool — the marker names are what a user copies from README.example.md. #644 covers markers that are absent; this covers markers that are present and paired wrongly.
Suggested direction
Pair the last start marker with the first end marker after it. Rename indexOfRegex to say what it returns, or give it a first-match sibling, so the next caller does not inherit the same surprise. Decide what to do when a section's markers are ambiguous rather than guessing silently — #644 is the neighbouring question.
What happens
ReadmeEditorpairs the last start marker in the document with the last end marker in the document. Neither is paired relative to the other. A README that repeats either marker after a real pair — in a fenced example, say — therefore has the wrong two markers paired, and the text between them rewritten.Two shapes, both reproduced against the built CLI:
Either way the tool rewrites content outside a marker pair, which
docs/tool-contract.mdcalls a breaking change: content outside a pair is the user's.Where
src/helpers.ts:The name says first; the body returns the last.
ReadmeEditor.getTokenIndexesuses it for the end marker andlastIndexOfRegexfor the start marker, so both ends of the span come from the last match anywhere in the file.The
(^|[^\])` guard does not help here. It excludes a marker quoted inline between backticks or escaped with a backslash. A marker alone on its own line inside a fence is preceded by a newline, so it matches like any other.Reproduction
action.yml:README.md, repeated end marker:Run the generator. The heading, the bullet, the sentence and the fence opener are gone: the span now runs from the real start marker to the repeat inside the fence.
Swap that fenced line for
<!-- start inputs -->and the mirror shape appears instead — the table, the end marker, the prose and the fence opener are all emitted twice.Workarounds that do work
Both are tested against the built CLI and both leave the file correct:
This repository's own README uses the first form.
Why this is filed separately from #668
#668 is about formatter scope, and the fix for it (PR #690) leaves this untouched: the pairing rule is in
getTokenIndexes, not in the formatter. PR #690's outside-content check inscripts/verify-readme-contract.mjsdoes catch both shapes, so the integration workflow reports them against a third-party repository rather than staying silent. That check deliberately does not reusegetTokenIndexes— a mask built on the editor's own pairing would hide the very bytes a mis-paired span destroyed.Impact
This tool runs against arbitrary third-party repositories, and a README that documents the markers is an ordinary shape for a repository that uses this tool — the marker names are what a user copies from
README.example.md. #644 covers markers that are absent; this covers markers that are present and paired wrongly.Suggested direction
Pair the last start marker with the first end marker after it. Rename
indexOfRegexto say what it returns, or give it a first-match sibling, so the next caller does not inherit the same surprise. Decide what to do when a section's markers are ambiguous rather than guessing silently — #644 is the neighbouring question.