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:
|
- any-glob-to-any-file:
|
||||||
- 'platform/ios/**/*'
|
- 'platform/ios/**/*'
|
||||||
- 'platform/darwin/**/*'
|
- 'platform/darwin/**/*'
|
||||||
cpp-core:
|
core:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file:
|
||||||
- 'src/**/*'
|
- '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"
|
node-version-file: ".nvmrc"
|
||||||
|
|
||||||
- name: npm install
|
- name: npm install
|
||||||
run: npm install --ignore-scripts
|
run: npm ci --ignore-scripts
|
||||||
working-directory: .
|
working-directory: .
|
||||||
shell: bash
|
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
|
.DS_Store
|
||||||
.cache
|
.cache
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
|
.build
|
||||||
/build
|
/build
|
||||||
/build-*
|
/build-*
|
||||||
**/.idea
|
**/.idea
|
||||||
|
|||||||
25
package-lock.json
generated
25
package-lock.json
generated
@@ -23,13 +23,13 @@
|
|||||||
"@mapbox/mvt-fixtures": "3.10.0",
|
"@mapbox/mvt-fixtures": "3.10.0",
|
||||||
"@octokit/rest": "^20.1.0",
|
"@octokit/rest": "^20.1.0",
|
||||||
"@types/ejs": "^3.1.5",
|
"@types/ejs": "^3.1.5",
|
||||||
"@types/node": "^22.9.0",
|
"@types/node": "^24.1.0",
|
||||||
"csscolorparser": "^1.0.3",
|
"csscolorparser": "^1.0.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lodash.template": "4.5.0",
|
"lodash.template": "4.5.0",
|
||||||
"typescript": "^5.4.5"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/core": {
|
"node_modules/@actions/core": {
|
||||||
@@ -2069,13 +2069,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.9.0",
|
"version": "24.10.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.8.tgz",
|
||||||
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
|
"integrity": "sha512-r0bBaXu5Swb05doFYO2kTWHMovJnNVbCsII0fhesM8bNRlLhXIuckley4a2DaD+vOdmm5G+zGkQZAPZsF80+YQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.8"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
@@ -4600,10 +4600,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.4.5",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -4654,9 +4655,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.19.8",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,14 +20,14 @@
|
|||||||
"@mapbox/mvt-fixtures": "3.10.0",
|
"@mapbox/mvt-fixtures": "3.10.0",
|
||||||
"@octokit/rest": "^20.1.0",
|
"@octokit/rest": "^20.1.0",
|
||||||
"@types/ejs": "^3.1.5",
|
"@types/ejs": "^3.1.5",
|
||||||
"@types/node": "^22.9.0",
|
"@types/node": "^24.1.0",
|
||||||
"csscolorparser": "^1.0.3",
|
"csscolorparser": "^1.0.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lodash.template": "4.5.0",
|
"lodash.template": "4.5.0",
|
||||||
"@mapbox/mapbox-gl-styles": "2.1.1",
|
"@mapbox/mapbox-gl-styles": "2.1.1",
|
||||||
"typescript": "^5.4.5"
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"copy-style-spec": "cp node_modules/@maplibre/maplibre-gl-style-spec/dist/latest.json scripts/style-spec-reference/v8.json"
|
"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 { simpleGit } from "simple-git";
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import { Octokit } from "@octokit/rest";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {never}
|
* @returns {never}
|
||||||
@@ -27,12 +26,7 @@ function getTagLastVersion() {
|
|||||||
|
|
||||||
const tagLastVersion = getTagLastVersion();
|
const tagLastVersion = getTagLastVersion();
|
||||||
|
|
||||||
if (!process.env.GITHUB_ACCESS_TOKEN) throw new Error("!process.env.GITHUB_ACCESS_TOKEN");
|
|
||||||
|
|
||||||
const git = simpleGit();
|
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);
|
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 });
|
const logs = await git.log({ from: commitLastVersion });
|
||||||
|
|
||||||
for await (const logEntry of logs.all.toReversed()) {
|
for await (const logEntry of logs.all.toReversed()) {
|
||||||
const pr = pullRequestsMap.get(logEntry.hash);
|
console.log(`- ${formatMessageToMarkdownLink(logEntry.message)}.`);
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { join } from "path";
|
|||||||
*/
|
*/
|
||||||
async function getFilePaths(baseDir, predicate) {
|
async function getFilePaths(baseDir, predicate) {
|
||||||
const entries = (await fs.readdir(baseDir, { withFileTypes: true }))
|
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
|
// documentation files that will be updated and source files that contain examples
|
||||||
|
|||||||
@@ -3,14 +3,16 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
"module": "NodeNext",
|
"module": "nodenext",
|
||||||
|
"target": "esnext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"maxNodeModuleJsDepth": 0,
|
"maxNodeModuleJsDepth": 0,
|
||||||
"noEmit": true
|
"noEmit": true,
|
||||||
|
"allowImportingTsExtensions": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
".github/scripts/**/*.ts",
|
".github/scripts/*.ts",
|
||||||
"scripts/**/*.mjs",
|
"scripts/**/*.mjs",
|
||||||
"scripts/**/*.d.ts"
|
"scripts/**/*.d.ts"
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user