diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1e0512..7cee9e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,15 +15,15 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: run_install: false - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} @@ -32,7 +32,7 @@ jobs: run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - name: Cache pnpm store - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.pnpm-store.outputs.STORE_PATH }} key: ${{ runner.os }}-pnpm-${{ matrix.node }}-${{ hashFiles('pnpm-lock.yaml') }} diff --git a/.github/workflows/release_package.yml b/.github/workflows/release_package.yml index 157a2a7..2363691 100644 --- a/.github/workflows/release_package.yml +++ b/.github/workflows/release_package.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Ensure latest CI on default branch succeeded - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | const { data: repo } = await github.rest.repos.get({ @@ -53,16 +53,16 @@ jobs: steps: # Checkout project repository - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: run_install: false # Setup Node.js environment - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: registry-url: https://registry.npmjs.org/ node-version: "22" @@ -72,7 +72,7 @@ jobs: run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - name: Cache pnpm store - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.pnpm-store.outputs.STORE_PATH }} key: ${{ runner.os }}-pnpm-22-${{ hashFiles('pnpm-lock.yaml') }} @@ -116,11 +116,7 @@ jobs: # Update changelog unreleased section with new version - name: Update changelog - uses: superfaceai/release-changelog-action@v1 - with: - path-to-changelog: CHANGELOG.md - version: ${{ env.NEW_VERSION }} - operation: release + run: node scripts/release-changelog.mjs release "$NEW_VERSION" - name: Ensure tag does not already exist run: | @@ -156,15 +152,11 @@ jobs: # Read version changelog - id: get-changelog name: Get version changelog - uses: superfaceai/release-changelog-action@v1 - with: - path-to-changelog: CHANGELOG.md - version: ${{ env.NEW_VERSION }} - operation: read + run: node scripts/release-changelog.mjs read "$NEW_VERSION" # Update GitHub release with changelog - name: Update GitHub release documentation - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ env.NEW_VERSION }} body: ${{ steps.get-changelog.outputs.changelog }} diff --git a/scripts/release-changelog.mjs b/scripts/release-changelog.mjs new file mode 100644 index 0000000..af53ea8 --- /dev/null +++ b/scripts/release-changelog.mjs @@ -0,0 +1,59 @@ +import fs from "node:fs"; +import { randomUUID } from "node:crypto"; + +const [operation, version, changelogPath = "CHANGELOG.md"] = process.argv.slice(2); + +if (!version || !["read", "release"].includes(operation)) { + throw new Error( + "Usage: node scripts/release-changelog.mjs [changelog-path]", + ); +} + +const changelog = fs.readFileSync(changelogPath, "utf8"); + +function findNextHeading(content, startIndex) { + const heading = /^## /gm; + heading.lastIndex = startIndex; + return heading.exec(content)?.index ?? content.length; +} + +function findVersionHeading(content, targetVersion) { + const heading = /^## (v?\d+\.\d+\.\d+(?:-[^\s]+)?)\b.*$/gm; + let match; + + while ((match = heading.exec(content))) { + if (match[1] === targetVersion) { + return match; + } + } + + throw new Error(`Version ${targetVersion} was not found in ${changelogPath}`); +} + +if (operation === "release") { + const unreleased = /^## Unreleased[^\S\r\n]*$/m.exec(changelog); + + if (!unreleased) { + throw new Error(`An "Unreleased" heading was not found in ${changelogPath}`); + } + + const date = new Date().toISOString().slice(0, 10); + const insertionPoint = unreleased.index + unreleased[0].length; + const updatedChangelog = `${changelog.slice(0, insertionPoint)}\n\n## ${version} - ${date}${changelog.slice(insertionPoint)}`; + fs.writeFileSync(changelogPath, updatedChangelog); +} else { + const heading = findVersionHeading(changelog, version); + const changelogSection = changelog + .slice(heading.index, findNextHeading(changelog, heading.index + heading[0].length)) + .trimEnd(); + const delimiter = `CHANGELOG_${randomUUID()}`; + + if (!process.env.GITHUB_OUTPUT) { + throw new Error("GITHUB_OUTPUT must be set when reading a changelog section"); + } + + fs.appendFileSync( + process.env.GITHUB_OUTPUT, + `changelog<<${delimiter}\n${changelogSection}\n${delimiter}\n`, + ); +} diff --git a/tests/release-changelog.js b/tests/release-changelog.js new file mode 100644 index 0000000..38e62e5 --- /dev/null +++ b/tests/release-changelog.js @@ -0,0 +1,45 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const scriptPath = path.join(__dirname, "../scripts/release-changelog.mjs"); + +test("releases and reads the unreleased changelog section", (t) => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "release-changelog-")); + const changelogPath = path.join(tempDir, "CHANGELOG.md"); + const outputPath = path.join(tempDir, "github-output"); + const version = "v1.1.0"; + const unreleasedNotes = "### Added\n- Supports CSS aliases\n"; + + t.after(() => fs.rmSync(tempDir, { recursive: true, force: true })); + fs.writeFileSync( + changelogPath, + `# Changelog\n\n## Unreleased\n\n${unreleasedNotes}\n## v1.0.0 - 2026-01-01\n`, + ); + + execFileSync(process.execPath, [scriptPath, "release", version, changelogPath]); + + const releasedChangelog = fs.readFileSync(changelogPath, "utf8"); + assert.match( + releasedChangelog, + new RegExp(`## ${version} - \\d{4}-\\d{2}-\\d{2}\\n\\n${unreleasedNotes}`), + ); + assert.match(releasedChangelog, /## Unreleased\n\n## v1\.1\.0/); + + execFileSync(process.execPath, [scriptPath, "read", version, changelogPath], { + env: { ...process.env, GITHUB_OUTPUT: outputPath }, + }); + + const output = fs.readFileSync(outputPath, "utf8"); + const match = /^changelog<<([^\n]+)\n([\s\S]+)\n\1\n$/.exec(output); + assert.ok(match); + assert.match(match[2], new RegExp(`^## ${version} - \\d{4}-\\d{2}-\\d{2}`)); + assert.ok(match[2].includes(unreleasedNotes.trimEnd())); + assert.ok(!match[2].includes("v1.0.0")); +});