Merge branch 'main' into next
Some checks are pending
Build Docker Image / build-and-push (push) Waiting to run
Build Pro Image / app-token (push) Waiting to run
Build Pro Image / build-and-push (push) Blocked by required conditions
E2E / Build (push) Waiting to run
E2E / Core and plugins (push) Blocked by required conditions
E2E / plugin-workflow (push) Blocked by required conditions
E2E / plugin-workflow-approval (push) Blocked by required conditions
E2E / plugin-data-source-main (push) Blocked by required conditions
E2E / Comment on PR (push) Blocked by required conditions
NocoBase FrontEnd Test / frontend-test (18) (push) Waiting to run

This commit is contained in:
GitHub Actions Bot 2024-09-02 06:39:23 +00:00
commit bf386c4b1a
4 changed files with 416 additions and 3 deletions

View File

@ -0,0 +1,72 @@
name: Write changelog and create release
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
workflow_dispatch:
push:
tags:
- 'v*-beta'
jobs:
app-token:
uses: nocobase/nocobase/.github/workflows/get-nocobase-app-token.yml@main
secrets: inherit
write-changelog-and-release:
needs:
- app-token
runs-on: ubuntu-latest
steps:
- name: Decrypt app token
id: app-token
shell: bash
run: |
ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }};
APP_TOKEN=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@v4
with:
repository: nocobase/nocobase
token: ${{ steps.app-token.outputs.token }}
persist-credentials: true
fetch-depth: 0
- name: Checkout pro-plugins
uses: actions/checkout@v4
with:
repository: nocobase/pro-plugins
path: packages/pro-plugins
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
persist-credentials: true
- name: Clone pro repos
shell: bash
run: |
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }}
do
git clone -b main https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo
done
- name: Set user
run: |
git config --global user.name '${{ needs.app-token.outputs.app-slug }}[bot]'
git config --global user.email '${{ needs.app-token.outputs.user-id }}+${{ needs.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>'
- name: Set Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run script
shell: bash
run: |
node scripts/release/changelogAndRelease.js
env:
PRO_PLUGIN_REPOS: ${{ vars.PRO_PLUGIN_REPOS }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Commit and push
run: |
git add .
git commit -m "docs: update changelogs"
git push origin main

0
CHANGELOG.zh-CN.md Normal file
View File

View File

@ -28,7 +28,4 @@ cd ../../
git add .
git commit -m "chore(versions): 😊 publish v$(jq -r '.version' lerna.json)"
git tag v$(jq -r '.version' lerna.json)
yarn changelog --breaking-pattern "BREAKING CHANGE:"
git add .
git commit -m "chore: update changelog"
# git push --atomic origin main v$(jq -r '.version' lerna.json)

View File

@ -0,0 +1,344 @@
const execa = require('execa');
const fs = require('fs/promises');
const path = require('path');
const { Command } = require('commander');
const program = new Command();
program.option('-f, --from [from]').option('-t, --to [to]').option('-v, --ver [ver]');
program.parse(process.argv);
const header = {
en: `# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
`,
cn: `# 更新日志
本项目的所有重要更改都将记录在此文件中
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
`,
};
function parsePRBody(body, language) {
const regExp = new RegExp(`${language}[ ]?\\|[ ]?([^\\|]*)\\|`, 'g');
const match = [...body.matchAll(regExp)];
if (!match.length) {
return {
description: '',
docTitle: '',
docLink: '',
};
}
const description = match[0]?.[1].trim() || '';
const docLink = match[1]?.[1].trim();
if (!docLink || docLink.startsWith('<!--')) {
return {
description,
docTitle: '',
docLink: '',
};
}
const docMatch = docLink.match(/\[(.*)\]\((.*)\)/);
return {
description,
docTitle: docMatch[1],
docLink: docMatch[2],
};
}
// pkgType: oss, pro
async function getPackageJson(pkg, pkgType) {
let dir;
if (pkgType === 'oss') {
dir = pkg.includes('plugin-') ? `plugins/@nocobase/${pkg}` : `core/${pkg}`;
} else {
dir = `pro-plugins/@nocobase/${pkg}`;
}
const pkgJSON = await fs.readFile(path.join(__dirname, '../../packages', dir, 'package.json'), 'utf8');
return JSON.parse(pkgJSON);
}
async function parsePackage(files, pkgType, pkg) {
if (!pkg) {
const maxChanges = { pkg: '', changes: 0 };
for (const file of files) {
const { path, additions, deletions } = file;
const match = path.match(/(?:@nocobase|core)\/([^\/]+)/);
const name = match && match[1];
if (!name) {
continue;
}
const changes = additions + deletions;
if (changes > maxChanges.changes) {
maxChanges.pkg = name;
maxChanges.changes = changes;
}
}
pkg = maxChanges.pkg;
}
if (!pkg) {
return { displayName: '', cnDisplayName: '' };
}
const pkgJSON = await getPackageJson(pkg, pkgType);
const { displayName, 'displayName.zh-CN': cnDisplayName, name } = pkgJSON;
return { displayName, cnDisplayName, name };
}
async function parsePR(number, pkgType, cwd, pkg, retries = 10) {
// gh pr view 5112 --json author,body,files
let res;
try {
const { stdout } = await execa('gh', ['pr', 'view', number, '--json', 'author,body,files'], { cwd });
res = stdout;
} catch (error) {
console.error(`Get PR #${number} failed, error: ${error.message}`);
if (retries > 0) {
console.log(`Retrying... ${retries}`);
retries -= 1;
return parsePR(number, pkgType, cwd, pkg, retries);
}
return { number };
}
const { author, body, files } = JSON.parse(res);
const typeRegExp = /\[x\] ([^(\\\r)]+)/;
const typeMatch = body.match(typeRegExp);
const prType = typeMatch ? typeMatch[1] : '';
if (!prType) {
return { number };
}
const { description, docTitle, docLink } = parsePRBody(body, 'English');
const { description: cnDescription, docTitle: cnDocTitle, docLink: cnDocLink } = parsePRBody(body, 'Chinese');
const { displayName, cnDisplayName, name } = await parsePackage(files, pkgType, pkg);
const pkgName = name.split('/').pop();
const changelog = {
prType,
number,
author: author.login,
moduleType: name?.includes('plugin-') ? 'plugin' : 'core',
module: name,
en: {
module: displayName || pkgName,
description,
docTitle,
docLink,
},
cn: {
module: cnDisplayName || pkgName,
description: cnDescription,
docTitle: cnDocTitle,
docLink: cnDocLink,
},
};
return changelog;
}
async function getPRList(from, to, cwd) {
// git log v1.3.9-beta..HEAD --pretty=format:"%s"
const { stdout: logs } = await execa('git', ['log', `${from}..${to}`, '--pretty=format:%s'], { cwd });
const prs = [];
for (const log of logs.split('\n')) {
const match = log.match(/\(#(\d+)\)/);
if (match) {
prs.push(match[1]);
}
}
return prs;
}
function arrangeChangelogs(changelogs) {
const result = {
'New feature': {
core: {},
plugin: {},
},
Improvement: {
core: {},
plugin: {},
},
'Bug fix': {
core: {},
plugin: {},
},
};
for (const changelog of changelogs) {
const { prType, module, moduleType } = changelog;
if (!result[prType]?.[moduleType]) {
continue;
}
if (!result[prType][moduleType][module]) {
result[prType][moduleType][module] = [];
}
result[prType][moduleType][module].push(changelog);
}
return result;
}
async function collect() {
let { from, to, ver = 'beta' } = program.opts();
if (!from || !to) {
// git describe --tags $(git rev-list --tags --max-count=2) --abbrev=0
const tagPattern = `v*-${ver}`;
const { stdout: tags } = await execa(
'git',
['describe', '--tags', `$(git rev-list --tags=${tagPattern} --max-count=2)`, '--abbrev=0'],
{ shell: true },
);
[from, to] = tags.split('\n').reverse();
}
console.log(`From: ${from}, To: ${to}`);
const changelogs = [];
const get = async (changelogs, pkgType, cwd, pkg) => {
const prs = await getPRList(from, to, cwd);
await Promise.all(
prs.map(async (pr) => {
console.log(`Parsing PR #${pr}`);
const changelog = await parsePR(pr, pkgType, cwd, pkg);
if (pkgType !== 'oss') {
changelog.pro = true;
}
changelogs.push(changelog);
}),
);
};
console.log('===nocobase/nocobase===');
await get(changelogs, 'oss');
console.log('===nocobase/pro-plugins===');
await get(changelogs, 'pro', path.join(__dirname, '../../packages/pro-plugins/'));
if (process.env.PRO_PLUGIN_REPOS) {
const repos = JSON.parse(process.env.PRO_PLUGIN_REPOS);
for (const repo of repos) {
console.log(`===nocobase/${repo}===`);
try {
await get(changelogs, 'pro', path.join(__dirname, '../../packages/pro-plugins/@nocobase', repo), repo);
} catch (error) {
console.error(`Generate changelog for ${repo} failed, error: ${error.message}`);
}
}
}
return { changelogs: arrangeChangelogs(changelogs), from, to };
}
async function generateChangelog() {
const { changelogs, from, to } = await collect();
const prTypeLocale = {
'New feature': {
en: '🎉 New Features',
cn: '🎉 新特性',
},
Improvement: {
en: '🚀 Improvements',
cn: '🚀 优化',
},
'Bug fix': {
en: '🐛 Bug Fixes',
cn: '🐛 修复',
},
};
const moduleTypeLocale = {
core: {
en: 'Core',
cn: '内核',
},
plugin: {
en: 'Plugins',
cn: '插件',
},
};
const generate = (changelogs, lang) => {
let result = '';
for (const [prType, moduleTypes] of Object.entries(changelogs)) {
const prTypeResults = [];
for (const [moduleType, modules] of Object.entries(moduleTypes)) {
const moduleTypeResults = [];
for (const [_, moduleChangelogs] of Object.entries(modules)) {
const moduleResults = [];
const lists = [];
for (const changelog of moduleChangelogs) {
const { number, author, pro } = changelog;
const { description, docTitle, docLink } = changelog[lang];
if (!description) {
console.warn(`PR #${number} has no ${lang} changelog`);
continue;
}
const pr = pro ? '' : ` ([#${number}](https://github.com/nocobase/nocobase/pull/${number}))`;
const doc = docTitle && docLink ? `Reference: [${docTitle}](${docLink})` : '';
lists.push(`${description}${pr} by @${author}\n${doc}`);
}
if (!lists.length) {
continue;
}
if (lists.length > 1) {
moduleResults.push(`\n - ${lists.join('\n - ')}\n`);
} else {
moduleResults.push(` ${lists[0]}\n`);
}
if (moduleResults.length) {
const moduleName = moduleChangelogs[0][lang].module;
moduleTypeResults.push(`- **[${moduleName}]**${moduleResults.join('')}`);
}
}
if (moduleTypeResults.length) {
const moduleTypeTitle = moduleTypeLocale[moduleType][lang];
prTypeResults.push(`${moduleTypeResults.join('')}`);
}
}
if (prTypeResults.length) {
const prTypeTitle = prTypeLocale[prType][lang];
result += `### ${prTypeTitle}\n\n`;
result += prTypeResults.join('');
}
}
return result;
};
const cn = generate(changelogs, 'cn');
const en = generate(changelogs, 'en');
return { cn, en, from, to };
}
async function writeChangelog(cn, en, from, to) {
const date = new Date().toISOString().split('T')[0];
const title = `## [${to}](https://github.com/nocobase/nocobase/compare/${from}...${to}) - ${date}`;
const write = async (lang) => {
const file = lang === 'cn' ? 'CHANGELOG.zh-CN.md' : 'CHANGELOG.md';
const oldChangelog = await fs.readFile(path.join(__dirname, `../../${file}`), 'utf8');
if (oldChangelog.includes(`## [${to}]`)) {
return;
}
const fromIndex = oldChangelog.indexOf(`## [`);
const newChangelog = `${header[lang]}\n${title}\n\n${lang === 'cn' ? cn : en}${oldChangelog.slice(fromIndex)}`;
await fs.writeFile(path.join(__dirname, `../../${file}`), newChangelog);
};
write('cn');
write('en');
}
async function createRelease(cn, en, to) {
let { ver = 'beta' } = program.opts();
// gh release create -t title -n note
if (ver === 'alpha') {
await execa('gh', ['release', 'create', to, '-t', to, '-n', `${en}\n---\n${cn}`, '-p']);
return;
}
await execa('gh', ['release', 'create', to, '-t', to, '-n', `${en}\n---\n${cn}`]);
}
async function writeChangelogAndCreateRelease() {
let { ver = 'beta' } = program.opts();
const { cn, en, from, to } = await generateChangelog();
if (!cn && !en) {
throw new Error('No changelog generated');
}
if (ver === 'beta') {
await writeChangelog(cn, en, from, to);
}
await createRelease(cn, en, to);
}
writeChangelogAndCreateRelease();