Add script for validating PR title (#4050)
This commit is contained in:
2
.github/labeler.yml
vendored
2
.github/labeler.yml
vendored
@@ -6,7 +6,7 @@ iOS:
|
||||
- any-glob-to-any-file:
|
||||
- 'platform/ios/**/*'
|
||||
- 'platform/darwin/**/*'
|
||||
cpp-core:
|
||||
core:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'src/**/*'
|
||||
|
||||
30
.github/scripts/validate-pr-title.test.ts
vendored
Normal file
30
.github/scripts/validate-pr-title.test.ts
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
import test from "node:test";
|
||||
import { validateTitle } from "./validate-pr-title.ts";
|
||||
|
||||
test("validateTitle - missing Android label", (t) => {
|
||||
t.assert.deepEqual(validateTitle({
|
||||
labels: new Set(["android"]),
|
||||
title: "make a change"
|
||||
}), {
|
||||
mismatches: ["android"],
|
||||
ok: false
|
||||
});
|
||||
});
|
||||
|
||||
test("validateTitle - android required and title contains Android", (t) => {
|
||||
t.assert.deepEqual(validateTitle({
|
||||
labels: new Set(["android"]),
|
||||
title: "make change to Android"
|
||||
}), {
|
||||
ok: true
|
||||
});
|
||||
});
|
||||
|
||||
test("validateTitle - only core required when label contains core", (t) => {
|
||||
t.assert.deepEqual(validateTitle({
|
||||
labels: new Set(["core", "android"]),
|
||||
title: "core: make some change"
|
||||
}), {
|
||||
ok: true
|
||||
});
|
||||
});
|
||||
68
.github/scripts/validate-pr-title.ts
vendored
Normal file
68
.github/scripts/validate-pr-title.ts
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// This script validates that a PR title also contains its labels
|
||||
// in the title. This is a requirement, because this makes it easier
|
||||
// to parse commit messages. For example, for generating a changelog
|
||||
// for a particular platform.
|
||||
// Usage:
|
||||
// PR_TITLE="Make some changes to Android" PR_LABELS="android" node validate-pr-title.ts
|
||||
|
||||
export function validateTitle({
|
||||
title,
|
||||
labels
|
||||
}: {
|
||||
title: string,
|
||||
labels: Set<string>
|
||||
}): { ok: true } | { ok: false, mismatches: string[] } {
|
||||
// these labels are the ones we care about
|
||||
const keywords = new Set(['node', 'core', 'ios', 'android', 'qt']);
|
||||
|
||||
// when labels includes core, only 'core' is required
|
||||
const requiredKeyWords = labels.has('core')
|
||||
? new Set(['core'])
|
||||
: labels.intersection(keywords);
|
||||
|
||||
title = title.toLocaleLowerCase();
|
||||
|
||||
const actualKeywords = new Set();
|
||||
for (const keyword of requiredKeyWords) {
|
||||
if (title.includes(keyword)) actualKeywords.add(keyword);
|
||||
}
|
||||
|
||||
const mismatches = requiredKeyWords.difference(actualKeywords);
|
||||
|
||||
if (mismatches.size === 0) return { ok: true };
|
||||
return { ok: false, mismatches: [...mismatches] };
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
if (!process.env.PR_TITLE) {
|
||||
console.log('::error::PR_TITLE environment variable is not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!process.env.PR_LABELS) {
|
||||
console.log('::error::PR_LABELS environment variable is not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const title = process.env.PR_TITLE;
|
||||
const labelsString = process.env.PR_LABELS.toLowerCase();
|
||||
const labels = new Set(labelsString.split(',').map(l => l.trim()).filter(l => l));
|
||||
|
||||
const result = validateTitle({
|
||||
title,
|
||||
labels
|
||||
});
|
||||
if (result.ok) {
|
||||
console.log('✅ PR labels match the title');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!result.ok) {
|
||||
if (result.mismatches.length > 0) {
|
||||
console.log(`::error::PR title does not match PR labels. Mismatched: ${result.mismatches.join(', ')}. Title: "${process.env.PR_TITLE}". Labels: "${process.env.PR_LABELS}"`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
.github/workflows/android-ci.yml
vendored
2
.github/workflows/android-ci.yml
vendored
@@ -69,7 +69,7 @@ jobs:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: npm install
|
||||
run: npm install --ignore-scripts
|
||||
run: npm ci --ignore-scripts
|
||||
working-directory: .
|
||||
shell: bash
|
||||
|
||||
|
||||
18
.github/workflows/typecheck-scripts.yml
vendored
18
.github/workflows/typecheck-scripts.yml
vendored
@@ -1,18 +0,0 @@
|
||||
name: typecheck-scripts
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
typecheck-scripts:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- run: |
|
||||
npm install
|
||||
npx tsc -p tsconfig.json
|
||||
22
.github/workflows/validate-pr-title.yml
vendored
Normal file
22
.github/workflows/validate-pr-title.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: Validate PR title
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled, unlabeled, opened, edited]
|
||||
|
||||
jobs:
|
||||
validate-tags-match-title:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Check if labels match PR title
|
||||
env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
|
||||
run: node .github/scripts/validate-pr-title.ts
|
||||
26
.github/workflows/validate-scripts.yml
vendored
Normal file
26
.github/workflows/validate-scripts.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: validate-scripts
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
validate-scripts:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Typecheck
|
||||
run: |
|
||||
npm install
|
||||
npx tsc -p tsconfig.json
|
||||
|
||||
- name: Run tests
|
||||
run: node --test .github/scripts/**.test.ts
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,6 +20,7 @@ local.properties
|
||||
.DS_Store
|
||||
.cache
|
||||
compile_commands.json
|
||||
.build
|
||||
/build
|
||||
/build-*
|
||||
**/.idea
|
||||
|
||||
25
package-lock.json
generated
25
package-lock.json
generated
@@ -23,13 +23,13 @@
|
||||
"@mapbox/mvt-fixtures": "3.10.0",
|
||||
"@octokit/rest": "^20.1.0",
|
||||
"@types/ejs": "^3.1.5",
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/node": "^24.1.0",
|
||||
"csscolorparser": "^1.0.3",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.19.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash.template": "4.5.0",
|
||||
"typescript": "^5.4.5"
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/core": {
|
||||
@@ -2069,13 +2069,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
|
||||
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
|
||||
"version": "24.10.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.8.tgz",
|
||||
"integrity": "sha512-r0bBaXu5Swb05doFYO2kTWHMovJnNVbCsII0fhesM8bNRlLhXIuckley4a2DaD+vOdmm5G+zGkQZAPZsF80+YQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.8"
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
@@ -4600,10 +4600,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -4654,9 +4655,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
@@ -20,14 +20,14 @@
|
||||
"@mapbox/mvt-fixtures": "3.10.0",
|
||||
"@octokit/rest": "^20.1.0",
|
||||
"@types/ejs": "^3.1.5",
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/node": "^24.1.0",
|
||||
"csscolorparser": "^1.0.3",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.19.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash.template": "4.5.0",
|
||||
"@mapbox/mapbox-gl-styles": "2.1.1",
|
||||
"typescript": "^5.4.5"
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"scripts": {
|
||||
"copy-style-spec": "cp node_modules/@maplibre/maplibre-gl-style-spec/dist/latest.json scripts/style-spec-reference/v8.json"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { simpleGit } from "simple-git";
|
||||
import * as fs from "node:fs";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
|
||||
/**
|
||||
* @returns {never}
|
||||
@@ -27,12 +26,7 @@ function getTagLastVersion() {
|
||||
|
||||
const tagLastVersion = getTagLastVersion();
|
||||
|
||||
if (!process.env.GITHUB_ACCESS_TOKEN) throw new Error("!process.env.GITHUB_ACCESS_TOKEN");
|
||||
|
||||
const git = simpleGit();
|
||||
const octokit = new Octokit({
|
||||
auth: process.env.GITHUB_ACCESS_TOKEN
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -71,41 +65,8 @@ function formatMessageToMarkdownLink(message) {
|
||||
|
||||
const commitLastVersion = await getCommit(tagLastVersion);
|
||||
|
||||
// Load all closed pull requests
|
||||
const pulls = await octokit.paginate(octokit.pulls.list, {
|
||||
owner: "maplibre",
|
||||
repo: "maplibre-native",
|
||||
state: "closed",
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
/**
|
||||
* @type {Map<string, typeof pulls[0]>}[] }>}
|
||||
*/
|
||||
const pullRequestsMap = new Map();
|
||||
pulls.forEach(pr => {
|
||||
if (pr.merge_commit_sha) {
|
||||
pullRequestsMap.set(pr.merge_commit_sha, pr);
|
||||
}
|
||||
});
|
||||
|
||||
const logs = await git.log({ from: commitLastVersion });
|
||||
|
||||
for await (const logEntry of logs.all.toReversed()) {
|
||||
const pr = pullRequestsMap.get(logEntry.hash);
|
||||
|
||||
const log = () => console.log(`- ${formatMessageToMarkdownLink(logEntry.message)}.`);
|
||||
|
||||
|
||||
if (!pr) {
|
||||
log();
|
||||
continue;
|
||||
}
|
||||
|
||||
// only log changelog entry when PR has corresponding platform label (or none)
|
||||
const hasLabel = (/** @type {string} **/ label) => pr.labels.some(({name}) => name === label);
|
||||
|
||||
if (!hasLabel("ios") && !hasLabel("android") && !hasLabel("node")) log();
|
||||
else if (platform === "android" && hasLabel("android")) log();
|
||||
else if (platform === "ios" && hasLabel("ios")) log();
|
||||
console.log(`- ${formatMessageToMarkdownLink(logEntry.message)}.`);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { join } from "path";
|
||||
*/
|
||||
async function getFilePaths(baseDir, predicate) {
|
||||
const entries = (await fs.readdir(baseDir, { withFileTypes: true }))
|
||||
return entries.map((entry) => join(entry.path, entry.name)).filter(predicate);
|
||||
return entries.map((entry) => join(baseDir, entry.name)).filter(predicate);
|
||||
}
|
||||
|
||||
// documentation files that will be updated and source files that contain examples
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"module": "NodeNext",
|
||||
"module": "nodenext",
|
||||
"target": "esnext",
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"maxNodeModuleJsDepth": 0,
|
||||
"noEmit": true
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true
|
||||
},
|
||||
"include": [
|
||||
".github/scripts/**/*.ts",
|
||||
".github/scripts/*.ts",
|
||||
"scripts/**/*.mjs",
|
||||
"scripts/**/*.d.ts"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user