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
22 changes: 21 additions & 1 deletion .commitlintrc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
{
"extends": [
"@commitlint/config-conventional"
]
],
"rules": {
"type-enum": [
2,
"always",
[
"build",
"chore",
"ci",
"deps",
"docs",
"feat",
"fix",
"perf",
"refactor",
"revert",
"style",
"test"
]
]
}
}
71 changes: 62 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
- 'feature/**'
pull_request:
branches:
- main
- develop
- 'feature/**'
workflow_call:
Expand All @@ -20,19 +21,21 @@ on:
jobs:
compute-matrix:
name: Compute matrix
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
outputs:
matrix: ${{ steps.set.outputs.matrix }}
name_suffix: ${{ steps.set.outputs.name_suffix }}
steps:
- id: set
env:
FULL_MATRIX: ${{ inputs.full || (github.event_name == 'pull_request' && github.base_ref == 'main') }}
run: |
if [[ "${{ inputs.full }}" == "true" ]]; then
echo 'matrix={"os":["ubuntu-latest","windows-latest"],"node":[18,20,22]}' >> $GITHUB_OUTPUT
echo 'name_suffix=(full matrix)' >> $GITHUB_OUTPUT
if [[ "$FULL_MATRIX" == "true" ]]; then
echo 'matrix={"os":["ubuntu-24.04","windows-latest"],"node":[18,20,22]}' >> "$GITHUB_OUTPUT"
echo 'name_suffix=(full matrix)' >> "$GITHUB_OUTPUT"
else
echo 'matrix={"os":["ubuntu-latest"],"node":[20]}' >> $GITHUB_OUTPUT
echo 'name_suffix=' >> $GITHUB_OUTPUT
echo 'matrix={"os":["ubuntu-24.04"],"node":[20]}' >> "$GITHUB_OUTPUT"
echo 'name_suffix=' >> "$GITHUB_OUTPUT"
fi

build-and-test:
Expand All @@ -46,7 +49,7 @@ jobs:
matrix: ${{ fromJSON(needs.compute-matrix.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Configure git EOL (Windows)
if: runner.os == 'Windows'
Expand All @@ -55,7 +58,7 @@ jobs:
git config core.eol lf

- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ matrix.node }}
cache: npm
Expand All @@ -77,7 +80,57 @@ jobs:

- name: Upload coverage artifact
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: coverage-${{ matrix.os }}-node${{ matrix.node }}
path: coverage

release-tooling:
name: Validate release tooling
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Use Node.js for release tooling
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '22.23.3'
cache: npm

- name: Install dependencies
run: npm ci

- name: Test release tooling
run: npm run test:tooling

renovate-config:
name: Validate Renovate configuration
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Use Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24

- name: Validate configuration
run: npx --yes --package renovate@44.155.8 -- renovate-config-validator --strict renovate.json

ci-success:
name: CI passed
if: ${{ always() }}
needs: [compute-matrix, build-and-test, release-tooling, renovate-config]
runs-on: ubuntu-24.04
permissions: {}
steps:
- name: Require all checks to pass
env:
CHECKS_PASSED: ${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'skipped') }}
run: test "$CHECKS_PASSED" = true
14 changes: 8 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,25 @@ jobs:

release:
name: Release & Publish
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
needs: ci
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
fetch-tags: true

- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 22
node-version: '22.23.3'
cache: 'npm'

- name: Update npm
run: npm install -g npm@latest
run: |
npm install -g npm@11.7.0
npm -v

- name: Install dependencies
run: npm ci
Expand All @@ -61,7 +63,7 @@ jobs:

- name: Run release-it
env:
DEBUG: release-it:*,@release-it/*
NODE_DEBUG: release-it:*
HUSKY: 0
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
Expand Down
63 changes: 63 additions & 0 deletions .github/workflows/renovate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Update Dependencies

on:
schedule:
- cron: '15 2 * * 0'
timezone: America/Los_Angeles
workflow_dispatch:

permissions:
contents: read

concurrency:
group: renovate-dependencies
cancel-in-progress: false

jobs:
renovate:
name: Create or update dependency PR
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- name: Check GitHub App configuration
env:
APP_CLIENT_ID: ${{ vars.RENOVATE_APP_CLIENT_ID }}
APP_PRIVATE_KEY: ${{ secrets.RENOVATE_APP_PRIVATE_KEY }}
run: |
if [[ -z "$APP_CLIENT_ID" || -z "$APP_PRIVATE_KEY" ]]; then
echo '::error::Set RENOVATE_APP_CLIENT_ID (Actions variable) and RENOVATE_APP_PRIVATE_KEY (Actions secret). See CONTRIBUTING.md.'
exit 1
fi

- name: Create repository-scoped GitHub App token
id: app-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
# Omitting owner/repositories scopes the shared app's token to this repository.
client-id: ${{ vars.RENOVATE_APP_CLIENT_ID }}
private-key: ${{ secrets.RENOVATE_APP_PRIVATE_KEY }}

- name: Resolve bot identity
id: bot
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
run: |
BOT_LOGIN="${APP_SLUG}[bot]"
BOT_ID=$(gh api "/users/${BOT_LOGIN}" --jq .id)
printf 'username=%s\n' "$BOT_LOGIN" >> "$GITHUB_OUTPUT"
printf 'git-author=%s <%s+%s@users.noreply.github.com>\n' "$BOT_LOGIN" "$BOT_ID" "$BOT_LOGIN" >> "$GITHUB_OUTPUT"

- name: Run Renovate
uses: renovatebot/github-action@f0f41e5f2e706165f6814b1d0f0d0e319de7d1d9 # v46.3.8
with:
token: ${{ steps.app-token.outputs.token }}
renovate-version: 44.155.8
env:
RENOVATE_PLATFORM: github
RENOVATE_REPOSITORIES: ${{ github.repository }}
RENOVATE_AUTODISCOVER: 'false'
RENOVATE_ONBOARDING: 'false'
RENOVATE_REQUIRE_CONFIG: required
RENOVATE_USERNAME: ${{ steps.bot.outputs.username }}
RENOVATE_GIT_AUTHOR: ${{ steps.bot.outputs.git-author }}
1 change: 1 addition & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@

npm run typecheck || exit 1
npm run test:ci || exit 1
npm run test:tooling || exit 1
80 changes: 63 additions & 17 deletions .release-it.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ function getContributors() {
const types = new Map([
["feat", "✨ Features"],
["fix", "🐛 Bug Fixed"],
["deps", "📦 Dependencies"],
["perf", "⚡️ Performance Improvements"],
["refactor", "🛠️ Refactoring"],
["docs", "📝 Documentation"],
Expand Down Expand Up @@ -154,7 +155,7 @@ function whatBump(commits, currentVersion = pkg.version) {
isMinor = true;
}

if (["fix", "perf", "refactor", "ci"].includes(type)) {
if (["fix", "deps", "perf", "refactor"].includes(type)) {
isPatch = true;
}
}
Expand All @@ -171,6 +172,62 @@ function whatBump(commits, currentVersion = pkg.version) {
return null;
}

// Preserve the text escaping previously provided by Handlebars.
const escapeHtml = value =>
String(value ?? "").replace(/[&<>"'`=]/g, character => {
const entities = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#x27;",
"`": "&#x60;",
"=": "&#x3D;",
};
return entities[character];
});

function renderCommit(commit) {
const scope = commit.scope ? `**${escapeHtml(commit.scope)}:** ` : "";
const link = commit.href ? ` ([${escapeHtml(commit.shorthash)}](${escapeHtml(commit.href)}))` : "";
const body = commit.body ? `\n\n${commit.body}` : "";
return `* ${scope}${escapeHtml(commit.subject || commit.header)}${link}${body}`;
}

function renderContributor(contributor) {
const {name, login, email, url, count} = contributor;
let label = escapeHtml(name || email || "");
if (url) {
label = name
? `[${escapeHtml(name)}](${escapeHtml(url)})${login ? ` (@${escapeHtml(login)})` : ""}`
: `[@${escapeHtml(login)}](${escapeHtml(url)})`;
} else if (email && name) {
label = `[${escapeHtml(name)}](${escapeHtml(email)})`;
}
return `- ${label} — ${escapeHtml(count)} commits`;
}

function renderChangelog(context) {
const name = context.name || context.pkg?.name;
const packageName = name ? `\`${escapeHtml(name)}\` ` : "";
const sections = [`## 🚀 Release ${packageName}v${escapeHtml(context.version)} (${escapeHtml(context.date)})`];
const notes = (context.noteGroups || []).flatMap(group => group.notes || []);
if (notes.length) {
sections.push(`### 💥 Breaking Changes\n\n${notes.map(note => `* ${note.text}`).join("\n\n")}`);
}
if (context.commitGroups?.length) {
for (const group of context.commitGroups) {
sections.push(`### ${escapeHtml(group.title)}\n\n${group.commits.map(renderCommit).join("\n\n")}`);
}
} else if (context.commits?.length) {
sections.push(context.commits.map(renderCommit).join("\n\n"));
}
if (context.contributors?.length) {
sections.push(`### 🙌 Contributors\n\n${context.contributors.map(renderContributor).join("\n")}`);
}
return `${sections.join("\n\n")}\n`;
}

const createReleaseConfig = () => {
const contributors = getContributors();

Expand Down Expand Up @@ -213,18 +270,17 @@ const createReleaseConfig = () => {
plugins: {
"@release-it/conventional-changelog": {
infile: "CHANGELOG.md",
preset: "conventionalcommits",
preset: {
name: "conventionalcommits",
types: [...types.entries()].map(([type, section]) => ({type, section, effect: "changelog"})),
},

parserOpts: {
headerPattern: /^(\w+)(?:\(([^)]+)\))?(!)?:\s(.+?)(?:\s\(#\d+\))?$/,
headerCorrespondence: ["type", "scope", "breaking", "subject"],
noteKeywords: ["BREAKING CHANGE", "BREAKING-CHANGE"],
},

presetConfig: {
types: [...types.entries()].map(([type, section]) => ({type, section, hidden: false})),
},

context: {
name: pkg.name,
pkg: {name: pkg.name},
Expand All @@ -234,17 +290,7 @@ const createReleaseConfig = () => {

whatBump,
writerOpts: {
headerPartial:
"## 🚀 Release {{#if name}}`{{name}}` {{else}}{{#if @root.pkg}}`{{@root.pkg.name}}` {{/if}}{{/if}}v{{version}} ({{date}})\n\n",
footerPartial: `{{#if @root.contributors.length}}\n### 🙌 Contributors\n\n{{#each @root.contributors}}- {{#if url}}{{#if name}}[{{name}}]({{url}}){{#if login}} (@{{login}}){{/if}}{{else}}[@{{login}}]({{url}}){{/if}}{{else}}{{#if email}}{{#if name}}[{{name}}]({{email}}){{else}}{{email}}{{/if}}{{else}}{{name}}{{/if}}{{/if}} — {{count}} commits\n{{/each}}{{/if}}`,
mainTemplate:
"{{> header}}\n" +
"{{#if noteGroups}}\n### 💥 Breaking Changes\n\n{{#each noteGroups}}{{#each notes}}* {{{text}}}\n\n{{/each}}{{/each}}{{/if}}" +
"{{#each commitGroups}}\n### {{title}}\n\n{{#each commits}}{{> commit root=@root}}\n{{/each}}\n\n{{/each}}" +
"{{#unless commitGroups}}\n{{#each commits}}{{> commit root=@root}}\n{{/each}}{{/unless}}\n\n" +
"{{> footer}}",
commitPartial:
"{{#if type}}* {{#if scope}}**{{scope}}:** {{/if}}{{#if subject}}{{subject}}{{else}}{{header}}{{/if}}{{#if href}} ([{{shorthash}}]({{href}})){{/if}}\n\n{{#if body}}{{{body}}}\n{{/if}}{{/if}}",
template: renderChangelog,
groupBy: "type",
commitGroupsSort: "title",
commitsSort: ["scope", "subject"],
Expand Down
Loading