一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
164 行
6.5 KiB
JavaScript
164 行
6.5 KiB
JavaScript
// Validates the PR title and acts on the result.
|
|
//
|
|
// - Renovate is filtered out at the workflow level (job `if:`).
|
|
// - Translation-only PRs (every changed file is a .po) are skipped.
|
|
// - Users in `softCheckUsers` get a relaxed check (issue number optional)
|
|
// and only ever receive a comment, never a close.
|
|
// - Everyone else must match the strict format. Invalid titles get a
|
|
// comment and the PR is closed. We also apply a marker label so that
|
|
// we can later tell our closures apart from any other closure.
|
|
// - If the title becomes valid and the marker label is present, the PR
|
|
// is reopened and the label is removed. Closures by humans (or by
|
|
// another workflow) lack the label and are never overturned.
|
|
//
|
|
// Invoked from .github/workflows/check-pr-title.yml via actions/github-script.
|
|
// Required inputs come from `env`: PR_AUTHOR, PR_NUMBER. The title is
|
|
// fetched from the API rather than passed via env to avoid YAML expansion
|
|
// silently stripping leading whitespace from `${{ ... }}`.
|
|
|
|
module.exports = async ({ github, context, core }) => {
|
|
const softCheckUsers = ['laurent22', 'personalizedrefrigerator', 'mrjo118', 'tessus', 'CalebJohn', 'Rygaa'];
|
|
const autoClosedLabel = 'auto-closed: invalid-title';
|
|
|
|
// Product-area prefixes are kept in sync with the list recognised by
|
|
// packages/tools/git-changelog.ts so that valid titles are picked up by
|
|
// the changelog generator. Non-product prefixes (Tools, Chore, CI, Doc,
|
|
// Plugin Repo) are administrative categories for changes that do not
|
|
// ship to users and so do not appear in the changelog.
|
|
const prefix = '(Desktop|Mobile|Android|iOS|Web|Windows|Linux|macOS|Cli|Clipper|Server|Transcribe|Plugins|Api|Cloud|Tools|Chore|CI|Doc)';
|
|
const prefixList = `${prefix}(,\\s*${prefix})*`;
|
|
const strictRegex = new RegExp(`^${prefixList}: (Fixes|Resolves) #[0-9]+: .+`);
|
|
const softRegex = new RegExp(`^${prefixList}: ((Fixes|Resolves) #[0-9]+: )?.+`);
|
|
|
|
const author = process.env.PR_AUTHOR;
|
|
const prNumber = Number(process.env.PR_NUMBER);
|
|
|
|
const { data: pr } = await github.rest.pulls.get({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: prNumber,
|
|
});
|
|
const title = pr.title;
|
|
core.info(`Title (length=${title.length}): ${JSON.stringify(title)}`);
|
|
|
|
const isSoft = softCheckUsers.includes(author);
|
|
|
|
// listFiles returns up to 30 files per page; a pure translation PR is
|
|
// small, so checking the first page is enough.
|
|
const { data: files } = await github.rest.pulls.listFiles({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: prNumber,
|
|
});
|
|
const isTranslationOnly = files.length > 0 && files.every(f => f.filename.endsWith('.po'));
|
|
if (isTranslationOnly) {
|
|
core.info('Translation-only PR — skipping title check.');
|
|
return;
|
|
}
|
|
|
|
// Doc-only PRs do not require an issue number.
|
|
const isDocOnly = /^Doc(,\s*Doc)*:/.test(title);
|
|
const regex = isSoft || isDocOnly ? softRegex : strictRegex;
|
|
if (regex.test(title)) {
|
|
core.info('Title is valid.');
|
|
|
|
// If we previously closed this PR for an invalid title and the
|
|
// title is now valid, reopen it. We only reopen if our marker
|
|
// label is present, so closures by humans (or other workflows)
|
|
// are never overturned. A maintainer can also remove the label
|
|
// by hand to lock a PR closed regardless of future fixes.
|
|
const wasAutoClosed = pr.state === 'closed' && pr.labels.some(l => l.name === autoClosedLabel);
|
|
if (wasAutoClosed) {
|
|
try {
|
|
await github.rest.pulls.update({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: prNumber,
|
|
state: 'open',
|
|
});
|
|
} catch (error) {
|
|
// GitHub refuses to reopen a PR when another open PR
|
|
// already exists from the same head→base branch pair.
|
|
// In that case the contributor has already opened a
|
|
// replacement, so leave this PR closed.
|
|
if (error.status === 422) {
|
|
core.info('Cannot reopen — another PR is already open from the same branch.');
|
|
return;
|
|
}
|
|
throw error;
|
|
}
|
|
await github.rest.issues.removeLabel({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: prNumber,
|
|
name: autoClosedLabel,
|
|
});
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: prNumber,
|
|
body: `@${author} thanks for fixing the title — this PR has been reopened.`,
|
|
});
|
|
core.info('PR reopened after title was fixed.');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Hidden marker so we can detect a previous nag and avoid reposting on
|
|
// every `synchronize` event (push to the PR branch).
|
|
const commentMarker = '<!-- joplin-bot:invalid-pr-title -->';
|
|
|
|
const helpMessage = [
|
|
commentMarker,
|
|
`@${author} the pull request title does not match the required format.`,
|
|
'',
|
|
'Please prefix the title with the area you are targeting, then add the issue you are addressing. If the change targets several areas, separate them with commas. For example:',
|
|
'',
|
|
'- `Desktop: Resolves #123: Added new setting to change font`',
|
|
'- `Mobile, Desktop: Fixes #456: Fixed config screen error`',
|
|
'- `Mobile, Desktop, Cli: Resolves #777: Improved note search performance`',
|
|
'',
|
|
'See the [pull request template](https://github.com/laurent22/joplin/blob/dev/.github/PULL_REQUEST_TEMPLATE) for the list of valid prefixes and the full specification.',
|
|
'',
|
|
isSoft
|
|
? '_This PR has been left open — please update the title when you have a moment._'
|
|
: '_This PR has been closed automatically. Once you update the title to match the format above, the PR will be reopened automatically._',
|
|
].join('\n');
|
|
|
|
const existingComments = await github.paginate(github.rest.issues.listComments, {
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: prNumber,
|
|
per_page: 100,
|
|
});
|
|
const alreadyCommented = existingComments.some(c => c.body && c.body.includes(commentMarker));
|
|
|
|
if (!alreadyCommented) {
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: prNumber,
|
|
body: helpMessage,
|
|
});
|
|
} else {
|
|
core.info('Invalid-title comment already posted — not commenting again.');
|
|
}
|
|
|
|
if (!isSoft) {
|
|
// Label first so the marker is set before the close event lands.
|
|
await github.rest.issues.addLabels({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: prNumber,
|
|
labels: [autoClosedLabel],
|
|
});
|
|
await github.rest.pulls.update({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: prNumber,
|
|
state: 'closed',
|
|
});
|
|
core.setFailed('Pull request title does not match the required format.');
|
|
}
|
|
};
|