diff --git a/.github/labeler.yml b/.github/labeler.yml index 488d96fbf..7ebddc12e 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -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/**/*' diff --git a/.github/scripts/validate-pr-title.test.ts b/.github/scripts/validate-pr-title.test.ts new file mode 100644 index 000000000..e06419596 --- /dev/null +++ b/.github/scripts/validate-pr-title.test.ts @@ -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 + }); +}); diff --git a/.github/scripts/validate-pr-title.ts b/.github/scripts/validate-pr-title.ts new file mode 100644 index 000000000..2462fec01 --- /dev/null +++ b/.github/scripts/validate-pr-title.ts @@ -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 +}): { 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); + } + } +} diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml index e1cf89c04..17c0cd632 100644 --- a/.github/workflows/android-ci.yml +++ b/.github/workflows/android-ci.yml @@ -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 diff --git a/.github/workflows/typecheck-scripts.yml b/.github/workflows/typecheck-scripts.yml deleted file mode 100644 index 610603f37..000000000 --- a/.github/workflows/typecheck-scripts.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/validate-pr-title.yml b/.github/workflows/validate-pr-title.yml new file mode 100644 index 000000000..39e4b69d2 --- /dev/null +++ b/.github/workflows/validate-pr-title.yml @@ -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 diff --git a/.github/workflows/validate-scripts.yml b/.github/workflows/validate-scripts.yml new file mode 100644 index 000000000..5bd69aab0 --- /dev/null +++ b/.github/workflows/validate-scripts.yml @@ -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 diff --git a/.gitignore b/.gitignore index b3130d0ae..dac847875 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ local.properties .DS_Store .cache compile_commands.json +.build /build /build-* **/.idea diff --git a/.nvmrc b/.nvmrc index 409940768..a45fd52cc 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -23 +24 diff --git a/package-lock.json b/package-lock.json index ed621e3dc..1caf9a9fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" }, diff --git a/package.json b/package.json index eb097bb48..7f6255a30 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/scripts/generate-changelog.mjs b/scripts/generate-changelog.mjs index c18ed1073..727400b56 100755 --- a/scripts/generate-changelog.mjs +++ b/scripts/generate-changelog.mjs @@ -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}[] }>} - */ -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)}.`); } diff --git a/scripts/update-ios-examples.mjs b/scripts/update-ios-examples.mjs index 47b83f8e1..203211121 100644 --- a/scripts/update-ios-examples.mjs +++ b/scripts/update-ios-examples.mjs @@ -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 diff --git a/tsconfig.json b/tsconfig.json index fcbcfa6f6..13798b3a4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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" ],