Skip to content

ci: normalize real CRLF in appcast description#254

Merged
ethanndickson merged 1 commit into
mainfrom
ethan/fix-appcast-description-crlf
May 28, 2026
Merged

ci: normalize real CRLF in appcast description#254
ethanndickson merged 1 commit into
mainfrom
ethan/fix-appcast-description-crlf

Conversation

@ethanndickson
Copy link
Copy Markdown
Member

Why does releases.coder.com/coder-desktop/mac/appcast.xml ship empty release notes for v0.8.1?

Live feed today:

<title>v0.8.1</title>
<description><![CDATA[]]></description>

VERSION_DESCRIPTION was populated correctly in the workflow run — the full release body was on stdin to update-appcast. The empty <description> is produced by update-appcast itself.

Root cause: two bugs lined up

1. The CRLF "normalization" is a no-op. In scripts/update-appcast/Sources/main.swift:

let description = description.replacingOccurrences(of: #"\r\n"#, with: "\n")

#"\r\n"# is a Swift raw string literal — it's the four characters \, r, \, n, not CR+LF. GitHub stores release bodies with CRLF line endings (gh release view v0.8.1 --jq .body | cat -A shows ^M$ on every line), so this replacement matches nothing and the CRs reach Parsley untouched.

2. Parsley + Foundation Scanner + CRLF = empty body. Parsley.parse does:

if scanner.scanString("# ") == "# " {
    title = scanner.scanUpToString("\n")
}
let body = String(scanner.string[scanner.currentIndex...])

Scanner.charactersToBeSkipped defaults to .whitespacesAndNewlines, which includes \n. When the target of scanUpToString is also in the skip set, Foundation walks past every newline to EOF, so currentIndex ends up at the end of the string and body is "".

The Scanner quirk only fires when the description starts with # (the H1 branch). v0.8.1 is the first release whose body begins with # Coder Desktop for macOS — v0.8.1; every prior release used GitHub's auto-generated notes starting with ## What's Changed, which never enters the H1 branch — so the broken raw-string replacement was silently cargo for months.

The preview entry in the same appcast still has a description because its VERSION_DESCRIPTION came from a push event (head_commit.message), which doesn't start with # .

Verification

Built update-appcast's exact deps (Parsley 0.9.1) in a Swift 5.9 container against a CRLF input starting with # Coder Desktop for macOS — v0.8.1:

=== input has CRLF: true ===
=== BEFORE (#"\r\n"# replace) ===   body length: 0
=== AFTER  ("\r\n"  replace) ===   body length: 409
<h2>Features</h2><ul>…</ul><h2>Fixes</h2>…

Same Parsley call, same input — only the raw-string delimiters differ.

Belt-and-suspenders alternative (not taken)

If you want to also defend against an upstream stage that ever encodes real CRLF as the literal escape sequence \r\n (e.g. a JSON-stringified value that wasn't unescaped), you can chain both:

let description = description
    .replacingOccurrences(of: "\r\n", with: "\n")    // real CRLF → LF
    .replacingOccurrences(of: #"\r\n"#, with: "\n")  // text "\\r\\n" → LF (paranoid)

There's no current code path that produces the literal-escape variant, so I left that out — happy to add it if you'd like.

The `#"\r\n"#` raw string literal matches the 4 characters \, r, \, n
— not CR+LF. GitHub stores release bodies with CRLF line endings, so the
replacement was a no-op and the CRs reached Parsley untouched.

Parsley's `parts()` calls `Scanner.scanUpToString("\n")` to grab the
H1 title. Foundation's Scanner skips newlines by default
(`charactersToBeSkipped` includes \n), so when the input has CRLF the
scanner walks past every newline to EOF, leaving the body empty.

Only releases whose notes start with `# ` hit the H1 branch, which is
why v0.8.1 was the first to publish an empty
<description><![CDATA[]]></description> to releases.coder.com.
@ethanndickson ethanndickson marked this pull request as ready for review May 27, 2026 05:23
@ethanndickson ethanndickson changed the title fix: normalize real CRLF in appcast description ci: normalize real CRLF in appcast description May 27, 2026
@ethanndickson ethanndickson requested a review from jakehwll May 27, 2026 14:05
@ethanndickson ethanndickson merged commit edb097f into main May 28, 2026
3 checks passed
@ethanndickson ethanndickson deleted the ethan/fix-appcast-description-crlf branch May 28, 2026 02:08
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.

2 participants