diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 97e95296..48dda39f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -219,6 +219,11 @@ jobs: --docker-tags-file "${RUNNER_TEMP}/docker-tags.txt" ) + notes_file="tools/release/notes/${{ steps.plan.outputs.tag }}.md" + if [[ -f "${notes_file}" ]]; then + args+=(--notes-file "${notes_file}") + fi + if [[ "${{ steps.plan.outputs.prerelease }}" == "true" ]]; then args+=(--prerelease) fi diff --git a/tools/release/create-github-release.mjs b/tools/release/create-github-release.mjs index dfaf2083..0e861e07 100644 --- a/tools/release/create-github-release.mjs +++ b/tools/release/create-github-release.mjs @@ -12,6 +12,7 @@ const valueArgumentNames = new Set([ "--image-name", "--image-digest", "--docker-tags-file", + "--notes-file", ]); const requiredArguments = [ "repository", @@ -106,6 +107,7 @@ function readDockerTags(filePath) { } export function composeReleaseBody({ + releaseNotes = "", generatedBody, imageName, imageDigest, @@ -132,7 +134,9 @@ export function composeReleaseBody({ dockerLines.push("", `Digest: \`${imageDigest}\``); } - return `${generatedBody.trim()}\n\n${dockerLines.join("\n")}\n`; + return `${[releaseNotes.trim(), generatedBody.trim(), dockerLines.join("\n")] + .filter(Boolean) + .join("\n\n")}\n`; } function writeGithubOutput(outputs) { @@ -159,6 +163,9 @@ export async function main(argv = process.argv.slice(2)) { } const dockerTags = readDockerTags(arguments_.dockerTagsFile); + const releaseNotes = arguments_.notesFile + ? readFileSync(arguments_.notesFile, "utf8") + : ""; const generatedNotes = await githubApi( `/repos/${arguments_.repository}/releases/generate-notes`, { @@ -172,6 +179,7 @@ export async function main(argv = process.argv.slice(2)) { }, ); const body = composeReleaseBody({ + releaseNotes, generatedBody: generatedNotes.body, imageName: arguments_.imageName, imageDigest: arguments_.imageDigest, diff --git a/tools/release/create-github-release.test.mjs b/tools/release/create-github-release.test.mjs index 958c8ec5..7ff99ffb 100644 --- a/tools/release/create-github-release.test.mjs +++ b/tools/release/create-github-release.test.mjs @@ -41,6 +41,38 @@ void test("rejects release arguments with missing values", () => { ); }); +void test("accepts a release notes file and rejects a missing path", () => { + const arguments_ = parseArguments([ + ...requiredArguments, + "--notes-file", + "tools/release/notes/v1.3.0.md", + ]); + + assert.equal(arguments_.notesFile, "tools/release/notes/v1.3.0.md"); + assert.throws( + () => parseArguments([...requiredArguments, "--notes-file"]), + /--notes-file requires a value\./u, + ); +}); + +void test("places upgrade notes before generated changes and preserves Docker details", () => { + const body = composeReleaseBody({ + releaseNotes: " ## Upgrade notes\n\nRemote URLs require sign-in.\n", + generatedBody: "## What's Changed\n\n- Improve the editor", + imageName: "ghcr.io/techsquidtv/cliparr", + imageDigest: "sha256:example", + dockerTags: ["ghcr.io/techsquidtv/cliparr:1.3.0"], + }); + + assert.ok( + body.startsWith( + "## Upgrade notes\n\nRemote URLs require sign-in.\n\n## What's Changed", + ), + ); + assert.match(body, /docker pull ghcr\.io\/techsquidtv\/cliparr:1\.3\.0/u); + assert.match(body, /Digest: `sha256:example`/u); +}); + void test("omits the digest line when no image digest exists", () => { const body = composeReleaseBody({ generatedBody: "## What's Changed\n\n- fix release dry runs", diff --git a/tools/release/notes/v1.3.0.md b/tools/release/notes/v1.3.0.md new file mode 100644 index 00000000..5876cd5b --- /dev/null +++ b/tools/release/notes/v1.3.0.md @@ -0,0 +1,13 @@ +## Highlights + +- Edit clip ranges directly on the media track, with improved timeline zoom, viewport controls, subtitle cue tracks, and mobile trimming controls. +- Undo and redo edits, discover keyboard shortcuts, and recover editor drafts saved on this device. +- Use simpler export settings, cancel exports in progress, and see size estimates based on the selected encoding plan. +- Get more reliable HLS trimming, video track selection, ProRes handling, and preview recovery. +- Connect providers more easily, including regular Jellyfin accounts, with improved Plex token selection. + +## Upgrade notes + +**Remote video URLs now require a signed-in provider session.** Sign in to Plex or Jellyfin before opening a remote URL. Previously, this workflow was available without signing in. Local files still work without an account. This access restriction is included in v1.3.0 as a security correction. + +Proxied content can no longer execute scripts, HLS playlists larger than 8 MB are rejected, and exports fail when the selected video would otherwise be silently discarded.