Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand All @@ -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') }}
Expand Down
24 changes: 8 additions & 16 deletions .github/workflows/release_package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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"
Expand All @@ -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') }}
Expand Down Expand Up @@ -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: |
Expand Down Expand Up @@ -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 }}
Expand Down
59 changes: 59 additions & 0 deletions scripts/release-changelog.mjs
Original file line number Diff line number Diff line change
@@ -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 <read|release> <version> [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`,
);
}
45 changes: 45 additions & 0 deletions tests/release-changelog.js
Original file line number Diff line number Diff line change
@@ -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"));
});