From ebb5cf2cdbf7311635f8ccc7fd6a748c9fbbf1bf Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 22:54:04 +0000 Subject: [PATCH 1/4] feat: say what a sync carries, in a type the changelog keeps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This repository's changelog is generated by conventional-changelog with the angular preset, which drops `chore`. A sync committed as one is therefore invisible: 2.5.0 changed how every tool reports a failure, added retries and a request timeout, and annotated the whole catalog, and its changelog named a dependency bump. The manifest now carries the subjects of the monorepo commits behind the payload. The commit takes its type from them — `feat` if any is a feature, `fix` if any is a fix, `chore` only when neither — and lists them, as does the PR body. A carried subject marked breaking adds a BREAKING CHANGE footer, not a `!` in the header: the angular preset's header pattern has no `!` in it, so `feat!:` parses as no type at all and recommends a patch. They are artifact-controlled, so jq reads them to a file rather than through the shell, keeps only strings, strips them to printable characters, caps the count before transforming and the length after, and drops the `#` from a pull request number that belongs to a repository nobody reading this one can open. `[:print:]` rather than a \u range because jq matches with Oniguruma, which reads a backslash-u escape as the literal letters and silently ate most of every subject when I tried it that way. The list reaches the write job with the vetted tree, which is the only thing that crosses between them; it is written before anything from the artifact runs, and nothing copies it into the repository. This makes the sync appear with a meaningful subject and the detail in the body. It cannot make one commit into several changelog entries — conventional changelog emits one per commit — so a release that wants a line per change still expands them from the body. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UEJziXKWPKnF2WLHXF3iDU --- .github/workflows/sync-from-monorepo.yaml | 103 +++++++++++++++++++++- 1 file changed, 99 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sync-from-monorepo.yaml b/.github/workflows/sync-from-monorepo.yaml index e6c397f..69011d1 100644 --- a/.github/workflows/sync-from-monorepo.yaml +++ b/.github/workflows/sync-from-monorepo.yaml @@ -258,6 +258,44 @@ jobs: echo "previous=$PREVIOUS" } >> "$GITHUB_OUTPUT" + # The subjects of the monorepo commits this payload carries, so the + # commit `open-pr` writes can say what it brought rather than "sync". + # They are artifact-controlled and end up in a commit message and a + # PR body, so they never reach a shell as shell: jq reads them to a + # file, keeps only strings, strips them to printable characters, and + # caps the count before transforming and the length after. + # + # [:print:] rather than a \u range: jq matches with Oniguruma, which + # reads \u0020 as the literal characters and quietly eats most of + # the subject. + # + # `#1234` loses its `#`. A monorepo subject ends in the number of a + # pull request in a repository nobody reading this one can open, and + # left as `#1234` it becomes a link to this repository's issue 1234 + # in the changelog and a cross-reference on it here. + # + # A `.changes` that is missing, not an array, or not readable leaves + # this empty and warns. It decides only how the commit is worded, so + # failing a whole sync over it would be the wrong trade — but a + # silent empty list looks exactly like a sync that carries nothing. + : > /tmp/changes.txt + if jq -r '[(.changes? // []) | if type == "array" then .[0:50][] + else empty end + | select(type == "string") + | gsub("[^[:print:]]"; "") + | sub(" *\\(#[0-9]+\\) *$"; "") + | gsub("#(?[0-9]+)"; .n) + | .[0:120] + | sub("^ +"; "") | sub(" +$"; "") + | select(length > 0)][]' \ + /tmp/payload/manifest.json > /tmp/changes.new 2>/dev/null; then + mv /tmp/changes.new /tmp/changes.txt + else + rm -f /tmp/changes.new + echo "::warning::manifest .changes is unreadable" + fi + echo "Carrying $(wc -l < /tmp/changes.txt) change(s)." + - name: Apply it env: SHA: ${{ steps.manifest.outputs.sha }} @@ -362,6 +400,12 @@ jobs: rsync -a --relative \ mcp-server/src mcp-server/assets/logo.png mcp-server/.synced-from \ skills README.md /tmp/vetted/ + # Rides with the tree rather than through a job output: `open-pr` + # words its commit from these, and a step output would have to carry + # fifty artifact-controlled lines through `$GITHUB_OUTPUT`. Nothing + # copies it into the repository — `open-pr` names the paths it + # applies, and this is not one of them. + cp /tmp/changes.txt /tmp/vetted/changes.txt # Uploaded before the tests run, not after. Taking the snapshot early was # half the fix: `/tmp/vetted` stayed writable while artifact-derived tests @@ -461,11 +505,58 @@ jobs: git checkout -B "$SYNC_BRANCH" git add -A - cat > /tmp/message < /tmp/breaking + if [ -s "$CHANGES" ]; then + if grep -qE "${TICKET}feat(\([^)]*\))?!?:" "$CHANGES"; then + TYPE=feat + elif grep -qE "${TICKET}fix(\([^)]*\))?!?:" "$CHANGES"; then + TYPE=fix + fi + grep -E "${TICKET}[a-z]+(\([^)]*\))?!:" "$CHANGES" \ + > /tmp/breaking || true + fi - Published by ${SOURCE_REF}. Previous sync: ${PREVIOUS}. - EOF + # A breaking change goes in a footer, not as `feat!:` in the header. + # The angular preset's header pattern has no `!` in it, so `feat!:` + # parses as no type at all: against conventional-changelog-angular 8 + # it recommends a patch and names no breaking change, where this + # footer recommends a major and gives the changelog a BREAKING + # CHANGE section. + # + # The carried subjects are indented under both headings, which is + # also what stops one of them reaching the parser on its own: an + # indented `BREAKING CHANGE:` is not read as a footer, and an + # indented `#` is not stripped as a git comment. Unindent either + # list and an artifact-controlled line decides this package's next + # major version. + { + printf '%s: sync MCP source from the monorepo at %s\n\n' \ + "$TYPE" "$SHORT" + printf 'Published by %s. Previous sync: %s.\n' \ + "$SOURCE_REF" "$PREVIOUS" + if [ -s "$CHANGES" ]; then + printf '\nCarries:\n\n' + sed 's/^/ /' "$CHANGES" + fi + if [ -s /tmp/breaking ]; then + printf '\nBREAKING CHANGE: this sync carries breaking changes.' + printf '\n\n' + sed 's/^/ /' /tmp/breaking + fi + } > /tmp/message git commit --file /tmp/message { @@ -473,6 +564,10 @@ jobs: "$SOURCE_REF" "$SHA" printf 'Previous sync: `%s`.\n\n' "$PREVIOUS" printf 'src/host/ is this copy own and is not touched.\n' + if [ -s "$CHANGES" ]; then + printf '\n### Carries\n\n' + sed 's/^/- /' "$CHANGES" + fi if [ -n "$REMOVED" ]; then printf '\n**This withdraws:** `%s`\n\n' "$REMOVED" printf 'Already-released tools and skill:// resources disappear ' From 257fc5c7bca11838cca37330f27177c0faa59d1e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 23:09:56 +0000 Subject: [PATCH 2/4] fix: count the cap after the junk is dropped, and make carried text inert The cap ran on the raw array, so a manifest padded with nulls or numbers could push the subject that types this commit past the fiftieth position and have the sync committed as a `chore` with no breaking footer. There are now two caps: 500 bounds the work before any substitution runs, 50 is how many survive, counted after the junk is dropped. `@name` loses its `@`, like `#1234` already did. GitHub resolves a mention in a commit message whatever the surrounding markup, and `fix: handle @foo` is a subject someone writes by accident. The PR body indents the subjects four spaces instead of listing them, so each is a line of a code block rather than a link, an image or a mention in a public repository's pull request. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UEJziXKWPKnF2WLHXF3iDU --- .github/workflows/sync-from-monorepo.yaml | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sync-from-monorepo.yaml b/.github/workflows/sync-from-monorepo.yaml index 69011d1..bc78398 100644 --- a/.github/workflows/sync-from-monorepo.yaml +++ b/.github/workflows/sync-from-monorepo.yaml @@ -272,22 +272,31 @@ jobs: # `#1234` loses its `#`. A monorepo subject ends in the number of a # pull request in a repository nobody reading this one can open, and # left as `#1234` it becomes a link to this repository's issue 1234 - # in the changelog and a cross-reference on it here. + # in the changelog and a cross-reference on it here. `@name` loses + # its `@` for the same reason: GitHub resolves a mention in a commit + # message whatever the surrounding markup, and `fix: handle @foo` is + # a subject someone will write by accident. + # + # Two caps, not one. 500 bounds the work before any substitution + # runs; 50 is how many survive, counted after the junk is dropped, + # so a manifest full of nulls cannot push the subject that types + # this commit past the limit. # # A `.changes` that is missing, not an array, or not readable leaves # this empty and warns. It decides only how the commit is worded, so # failing a whole sync over it would be the wrong trade — but a # silent empty list looks exactly like a sync that carries nothing. : > /tmp/changes.txt - if jq -r '[(.changes? // []) | if type == "array" then .[0:50][] + if jq -r '[(.changes? // []) | if type == "array" then .[0:500][] else empty end | select(type == "string") | gsub("[^[:print:]]"; "") | sub(" *\\(#[0-9]+\\) *$"; "") | gsub("#(?[0-9]+)"; .n) + | gsub("@(?[A-Za-z0-9])"; .n) | .[0:120] | sub("^ +"; "") | sub(" +$"; "") - | select(length > 0)][]' \ + | select(length > 0)][0:50][]' \ /tmp/payload/manifest.json > /tmp/changes.new 2>/dev/null; then mv /tmp/changes.new /tmp/changes.txt else @@ -566,7 +575,11 @@ jobs: printf 'src/host/ is this copy own and is not touched.\n' if [ -s "$CHANGES" ]; then printf '\n### Carries\n\n' - sed 's/^/- /' "$CHANGES" + # Indented four spaces: a markdown code block, so a subject is + # text rather than a link, an image or a mention. The list marker + # it replaces left all three live in a public repository's PR + # body. + sed 's/^/ /' "$CHANGES" fi if [ -n "$REMOVED" ]; then printf '\n**This withdraws:** `%s`\n\n' "$REMOVED" From 9f043fec6b551b7df5ba09a9163bbdbf317d706c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 23:18:09 +0000 Subject: [PATCH 3/4] fix: replace @ rather than dropping it from a carried subject MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dropping the character let `@@foo` collapse to a live `@foo`, which is the mention the rule exists to stop, and let `@feat:` become a `feat:` prefix that the classifier then read — so a substitution meant to make text inert was choosing the commit type and, via `@chore!:`, the next major version. `(at)` cannot do either: it prepends to whatever followed the `@`, so no conventional-commit prefix can start a subject that did not already have one, and a second `@` becomes a second `(at)` rather than restoring the first. An npm scope reads `(at)modelcontextprotocol/sdk` and an address reads `user(at)example.com`. Both are live mentions as written — `@modelcontextprotocol` is an organization — so neutralizing them is the point, not a side effect. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UEJziXKWPKnF2WLHXF3iDU --- .github/workflows/sync-from-monorepo.yaml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sync-from-monorepo.yaml b/.github/workflows/sync-from-monorepo.yaml index bc78398..304db0c 100644 --- a/.github/workflows/sync-from-monorepo.yaml +++ b/.github/workflows/sync-from-monorepo.yaml @@ -272,11 +272,17 @@ jobs: # `#1234` loses its `#`. A monorepo subject ends in the number of a # pull request in a repository nobody reading this one can open, and # left as `#1234` it becomes a link to this repository's issue 1234 - # in the changelog and a cross-reference on it here. `@name` loses - # its `@` for the same reason: GitHub resolves a mention in a commit + # in the changelog and a cross-reference on it here. `@` becomes + # `(at)` for the same reason: GitHub resolves a mention in a commit # message whatever the surrounding markup, and `fix: handle @foo` is # a subject someone will write by accident. # + # Replaced rather than dropped. Dropping it let `@@foo` collapse to + # a live `@foo`, and let `@feat:` become a `feat:` prefix that the + # classifier below then read — so a substitution meant to make text + # inert was deciding this package's next version. Nothing this adds + # can start a subject with a conventional-commit prefix. + # # Two caps, not one. 500 bounds the work before any substitution # runs; 50 is how many survive, counted after the junk is dropped, # so a manifest full of nulls cannot push the subject that types @@ -293,7 +299,7 @@ jobs: | gsub("[^[:print:]]"; "") | sub(" *\\(#[0-9]+\\) *$"; "") | gsub("#(?[0-9]+)"; .n) - | gsub("@(?[A-Za-z0-9])"; .n) + | gsub("@"; "(at)") | .[0:120] | sub("^ +"; "") | sub(" +$"; "") | select(length > 0)][0:50][]' \ From ba39bd92dbe6ec4d48cd8de733283ca10ba4afbc Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 23:27:06 +0000 Subject: [PATCH 4/4] fix: read a commit scope up to the colon, not to the first paren MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `@` becomes `(at)` before the classifier sees a subject, so a scope that held one carried a paren the scope pattern read as its end: `feat(@currents/mcp)` arrived as `feat((at)currents/mcp)`, matched nothing, and typed the sync as a chore — and `fix(@api)!` lost its breaking footer with it. The scope is now anything up to the colon, which a conventional-commit scope cannot contain. `feat(api)` and `fix(mcp-server)!` parse as before, a message with a later colon still parses, and a subject beginning `(at)feat:` is still no prefix at all. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UEJziXKWPKnF2WLHXF3iDU --- .github/workflows/sync-from-monorepo.yaml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sync-from-monorepo.yaml b/.github/workflows/sync-from-monorepo.yaml index 304db0c..6b05d0d 100644 --- a/.github/workflows/sync-from-monorepo.yaml +++ b/.github/workflows/sync-from-monorepo.yaml @@ -530,17 +530,22 @@ jobs: # of its own line, so a `feat:` quoted inside someone's prose cannot # type the sync. A subject may lead with a ticket in brackets, as the # monorepo writes some of them. + # + # The scope is anything up to the colon rather than anything up to a + # closing paren, so a scope holding parens still parses: the `@` the + # step above rewrites to `(at)` would otherwise end the scope early + # and leave `feat((at)currents/mcp):` typed as a chore. CHANGES=/tmp/synced/changes.txt TICKET='^(\[[^]]+\][[:space:]]*)?' TYPE=chore : > /tmp/breaking if [ -s "$CHANGES" ]; then - if grep -qE "${TICKET}feat(\([^)]*\))?!?:" "$CHANGES"; then + if grep -qE "${TICKET}feat(\([^:]*\))?!?:" "$CHANGES"; then TYPE=feat - elif grep -qE "${TICKET}fix(\([^)]*\))?!?:" "$CHANGES"; then + elif grep -qE "${TICKET}fix(\([^:]*\))?!?:" "$CHANGES"; then TYPE=fix fi - grep -E "${TICKET}[a-z]+(\([^)]*\))?!:" "$CHANGES" \ + grep -E "${TICKET}[a-z]+(\([^:]*\))?!:" "$CHANGES" \ > /tmp/breaking || true fi