mirror of
https://github.com/actualbudget/actual.git
synced 2026-05-07 20:38:54 -05:00
migrate actualbudget/actions to the main repo (#7406)
* migrate release note actions * move workflows to use the local actions * note * fix failing cleanup in release notes action * fix codeQL violation
This commit is contained in:
17
.github/actions/release-notes/check/action.yml
vendored
Normal file
17
.github/actions/release-notes/check/action.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Check release notes
|
||||
description: Validate that a PR includes a properly formatted release note file
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: yarn --immutable
|
||||
- name: Check release notes
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
shell: bash
|
||||
run: node packages/ci-actions/bin/release-notes-check.mjs
|
||||
37
.github/actions/release-notes/generate/action.yml
vendored
Normal file
37
.github/actions/release-notes/generate/action.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Generate release notes
|
||||
description: Aggregate individual release note files into a formatted PR comment
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: yarn --immutable
|
||||
- name: Generate release notes
|
||||
id: generate
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: node packages/ci-actions/bin/release-notes-generate.mjs
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@v2
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ steps.generate.outputs.pr_number }}
|
||||
body-includes: auto-generated-release-notes
|
||||
- name: Create Comment
|
||||
if: ${{ steps.fc.outputs.comment-id == 0 }}
|
||||
uses: peter-evans/create-or-update-comment@v2
|
||||
with:
|
||||
issue-number: ${{ steps.generate.outputs.pr_number }}
|
||||
body: ${{ steps.generate.outputs.comment }}
|
||||
- name: Update Comment
|
||||
if: ${{ steps.fc.outputs.comment-id != 0 }}
|
||||
uses: peter-evans/create-or-update-comment@v2
|
||||
with:
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
edit-mode: replace
|
||||
body: ${{ steps.generate.outputs.comment }}
|
||||
4
.github/workflows/release-notes.yml
vendored
4
.github/workflows/release-notes.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
fi
|
||||
- name: Check release notes
|
||||
if: startsWith(github.head_ref, 'release/') == false && steps.changed-files.outputs.only_docs != 'true'
|
||||
uses: actualbudget/actions/release-notes/check@main
|
||||
uses: ./.github/actions/release-notes/check
|
||||
- name: Generate release notes
|
||||
if: startsWith(github.head_ref, 'release/') == true
|
||||
uses: actualbudget/actions/release-notes/generate@main
|
||||
uses: ./.github/actions/release-notes/generate
|
||||
|
||||
68
packages/ci-actions/bin/release-notes-check.mjs
Normal file
68
packages/ci-actions/bin/release-notes-check.mjs
Normal file
@@ -0,0 +1,68 @@
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
import matter from 'gray-matter';
|
||||
|
||||
import {
|
||||
categoryAutocorrections,
|
||||
categoryOrder,
|
||||
} from '../src/release-notes/util.mjs';
|
||||
|
||||
console.log('Looking in ' + fs.realpathSync('upcoming-release-notes'));
|
||||
|
||||
const expectedPath = `upcoming-release-notes/${process.env.PR_NUMBER}.md`;
|
||||
|
||||
function reportError(message) {
|
||||
console.log(`::error::${message}`);
|
||||
|
||||
process.stdout.write('::notice::');
|
||||
fs.createReadStream('upcoming-release-notes/README.md').pipe(process.stdout);
|
||||
|
||||
fs.createReadStream('upcoming-release-notes/README.md')
|
||||
.pipe(fs.createWriteStream(process.env.GITHUB_STEP_SUMMARY))
|
||||
.on('close', () => {
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
(() => {
|
||||
if (!fs.existsSync(expectedPath)) {
|
||||
reportError(`Release note file ${expectedPath} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data, content } = matter(fs.readFileSync(expectedPath, 'utf-8'));
|
||||
|
||||
if (!data.category) {
|
||||
reportError(`Release note is missing a category.`);
|
||||
return;
|
||||
}
|
||||
if (categoryAutocorrections[data.category]) {
|
||||
data.category = categoryAutocorrections[data.category];
|
||||
}
|
||||
if (!categoryOrder.includes(data.category)) {
|
||||
reportError(
|
||||
`Release note category "${data.category}" is not one of ${categoryOrder
|
||||
.map(JSON.stringify)
|
||||
.join(', ')}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.authors) {
|
||||
reportError(`Release note is missing authors.`);
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(data.authors)) {
|
||||
reportError(`Release note authors should be a list.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.trim().split('\n').length !== 1) {
|
||||
reportError(
|
||||
`Release note file ${expectedPath} body should contain exactly one line`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Everything looks good! \u{1f389}');
|
||||
})();
|
||||
184
packages/ci-actions/bin/release-notes-generate.mjs
Normal file
184
packages/ci-actions/bin/release-notes-generate.mjs
Normal file
@@ -0,0 +1,184 @@
|
||||
import * as childProcess from 'node:child_process';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { inspect, promisify } from 'node:util';
|
||||
|
||||
import matter from 'gray-matter';
|
||||
import listify from 'listify';
|
||||
|
||||
import {
|
||||
categoryAutocorrections,
|
||||
categoryOrder,
|
||||
} from '../src/release-notes/util.mjs';
|
||||
|
||||
const exec = promisify(childProcess.exec);
|
||||
|
||||
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');
|
||||
|
||||
const apiResult = await fetch('https://api.github.com/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `bearer ${process.env.GITHUB_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: /* GraphQL */ `
|
||||
query GetPRMetadata(
|
||||
$name: String!
|
||||
$owner: String!
|
||||
$headRefName: String!
|
||||
) {
|
||||
repository(name: $name, owner: $owner) {
|
||||
pullRequests(headRefName: $headRefName, first: 1) {
|
||||
edges {
|
||||
node {
|
||||
number
|
||||
baseRef {
|
||||
target {
|
||||
oid
|
||||
}
|
||||
}
|
||||
headRefName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
name: repo,
|
||||
owner,
|
||||
headRefName: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME,
|
||||
},
|
||||
}),
|
||||
}).then(res => res.json());
|
||||
|
||||
await collapsedLog('API Response', apiResult);
|
||||
|
||||
const prData = apiResult.data.repository.pullRequests.edges[0].node;
|
||||
|
||||
await setOutput('pr_number', prData.number);
|
||||
const version = prData.headRefName.split('/')[1];
|
||||
const baseRef = prData.baseRef.target.oid;
|
||||
|
||||
await group('Checkout base ref in worktree', async () => {
|
||||
await exec(`git fetch origin ${baseRef}`, { stdio: 'inherit' });
|
||||
await exec(`git worktree add ../../tmp ${baseRef}`, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
});
|
||||
|
||||
const notes = printNotes(
|
||||
await parseReleaseNotes('../../tmp/upcoming-release-notes'),
|
||||
);
|
||||
|
||||
await collapsedLog('Release Notes', notes);
|
||||
|
||||
await setOutput(
|
||||
'comment',
|
||||
`<!-- auto-generated-release-notes -->\nHere are the automatically generated release notes!\n\n~~~markdown\n${notes}\n~~~`,
|
||||
);
|
||||
|
||||
const releaseNotes = await fs
|
||||
.readdir('upcoming-release-notes')
|
||||
.then(contents =>
|
||||
contents.filter(f => f.endsWith('.md') && f !== 'README.md'),
|
||||
);
|
||||
|
||||
if (releaseNotes.length === 0) {
|
||||
console.log('No release notes found, no cleanup needed');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
await group('Remove used release notes', async () => {
|
||||
if (process.env.GITHUB_HEAD_REF) {
|
||||
await exec(`git fetch origin ${process.env.GITHUB_HEAD_REF}`, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
await exec(`git checkout ${process.env.GITHUB_HEAD_REF}`, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}
|
||||
await exec('rm -r upcoming-release-notes/*.md', { stdio: 'inherit' });
|
||||
await exec('git checkout upcoming-release-notes/README.md', {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
});
|
||||
|
||||
await group('Commit and push', async () => {
|
||||
await exec('git add upcoming-release-notes', { stdio: 'inherit' });
|
||||
const name = 'github-actions[bot]';
|
||||
const email = '41898282+github-actions[bot]@users.noreply.github.com';
|
||||
await exec("git commit -m 'Remove used release notes'", {
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
GIT_AUTHOR_NAME: name,
|
||||
GIT_COMMITTER_NAME: name,
|
||||
GIT_AUTHOR_EMAIL: email,
|
||||
GIT_COMMITTER_EMAIL: email,
|
||||
},
|
||||
});
|
||||
await exec('git push origin', { stdio: 'inherit' });
|
||||
});
|
||||
|
||||
async function parseReleaseNotes(dir) {
|
||||
const notes = (await fs.readdir(dir))
|
||||
.filter(f => f.match(/^\d+\.md$/))
|
||||
.map(async name => {
|
||||
const content = await fs.readFile(join(dir, name), 'utf-8');
|
||||
const { data, content: body } = matter(content);
|
||||
const number = name.replace('.md', '');
|
||||
const authors = listify(
|
||||
data.authors.map(a => `@${a}`),
|
||||
{ finalWord: '&' },
|
||||
);
|
||||
return {
|
||||
category: categoryAutocorrections[data.category] ?? data.category,
|
||||
value: `- [#${number}](https://github.com/actualbudget/${repo}/pull/${number}) ${body.trim()} — thanks ${authors}`,
|
||||
};
|
||||
});
|
||||
|
||||
return (await Promise.all(notes)).reduce(
|
||||
(acc, note) => {
|
||||
if (!acc[note.category]) {
|
||||
console.log(`WARNING: Unrecognized category "${note.category}"`);
|
||||
acc[note.category] = [];
|
||||
}
|
||||
acc[note.category].push(note.value);
|
||||
return acc;
|
||||
},
|
||||
Object.fromEntries(categoryOrder.map(c => [c, []])),
|
||||
);
|
||||
}
|
||||
|
||||
function printNotes(notes) {
|
||||
const printedNotes = Object.entries(notes)
|
||||
.filter(([_, values]) => values.length > 0)
|
||||
.map(([category, values]) => `#### ${category}\n\n${values.join('\n')}`);
|
||||
return `Version: ${version}\n\n${printedNotes.join('\n\n')}`;
|
||||
}
|
||||
|
||||
async function collapsedLog(name, value) {
|
||||
await group(name, () => {
|
||||
if (typeof value === 'string') {
|
||||
console.log(value);
|
||||
} else {
|
||||
console.log(inspect(value, { depth: null }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function group(name, cb) {
|
||||
console.log(`::group::${name}`);
|
||||
await cb();
|
||||
console.log('::endgroup::');
|
||||
}
|
||||
|
||||
async function setOutput(name, value) {
|
||||
const delimiter = Math.random().toString(36).slice(2);
|
||||
await fs.appendFile(
|
||||
process.env.GITHUB_OUTPUT,
|
||||
`\n${name}<<${delimiter}\n${value}\n${delimiter}\n`,
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,8 @@
|
||||
"devDependencies": {
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
|
||||
"extensionless": "^2.0.6",
|
||||
"gray-matter": "^4.0.3",
|
||||
"listify": "^1.0.3",
|
||||
"vitest": "^4.1.0"
|
||||
},
|
||||
"extensionless": {
|
||||
|
||||
12
packages/ci-actions/src/release-notes/util.mjs
Normal file
12
packages/ci-actions/src/release-notes/util.mjs
Normal file
@@ -0,0 +1,12 @@
|
||||
export const categoryAutocorrections = {
|
||||
Feature: 'Features',
|
||||
Enhancement: 'Enhancements',
|
||||
Bugfix: 'Bugfixes',
|
||||
};
|
||||
|
||||
export const categoryOrder = [
|
||||
'Features',
|
||||
'Enhancements',
|
||||
'Bugfixes',
|
||||
'Maintenance',
|
||||
];
|
||||
6
upcoming-release-notes/7406.md
Normal file
6
upcoming-release-notes/7406.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [matt-fidd]
|
||||
---
|
||||
|
||||
Consolidate all GitHub actions into the main repository
|
||||
@@ -44,6 +44,8 @@ __metadata:
|
||||
dependencies:
|
||||
"@typescript/native-preview": "npm:^7.0.0-dev.20260309.1"
|
||||
extensionless: "npm:^2.0.6"
|
||||
gray-matter: "npm:^4.0.3"
|
||||
listify: "npm:^1.0.3"
|
||||
vitest: "npm:^4.1.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -20068,6 +20070,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"listify@npm:^1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "listify@npm:1.0.3"
|
||||
checksum: 10/1f9756ac1ad7641b1eb4552348c663419e6df98a3c2486d6a5b579ac20f12f2ac9bb634a966d34da61b0357fec2baf09b5c346d089d4c0a7c48bef904cdc5a42
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"listr2@npm:^9.0.5":
|
||||
version: 9.0.5
|
||||
resolution: "listr2@npm:9.0.5"
|
||||
|
||||
Reference in New Issue
Block a user