From 4450b955ca08ef4cdc965341e83391b5a63cda2f Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Fri, 26 Jun 2026 12:17:11 +0200 Subject: [PATCH] feat: add a comment to merged pull requests on successful deployment Adds a comment to new pull requests when there is a successful build and deploy. Runs on TeamCity. Test-bot: skip --- resources/build/version/.gitignore | 5 + resources/build/version/README.md | 6 + resources/build/version/build.sh | 33 + resources/build/version/package-lock.json | 1530 +++++++++++++++++ resources/build/version/package.json | 38 + .../find-and-report-on-new-pull-requests.ts | 70 + .../get-list-of-keyboards-for-pull-request.ts | 40 + .../get-pull-requests-from-commit-range.ts | 99 ++ .../build/version/src/graphql/queries.ts | 43 + resources/build/version/src/index.ts | 120 ++ .../build/version/src/prepare-pull-comment.ts | 45 + .../src/send-comment-to-pull-request.ts | 22 + .../version/src/teamcity-get-build-changes.ts | 33 + .../build/version/src/util/spawnAwait.ts | 31 + .../fixtures/pull-requests/3860.comment.txt | 5 + .../tests/fixtures/pull-requests/3860.json | 23 + .../fixtures/pull-requests/3932.comment.txt | 7 + .../tests/fixtures/pull-requests/3932.json | 30 + .../tests/fixtures/pull-requests/3967.json | 43 + .../tests/fixtures/pull-requests/4023.json | 21 + .../tests/fixtures/pull-requests/4029.json | 7 + .../version/tests/fixtures/teamcity.json | 124 ++ ...ist-of-keyboards-for-pull-request.tests.ts | 37 + resources/build/version/tests/helpers.ts | 24 + .../tests/prepare-pull-comment.tests.ts | 35 + .../tests/teamcity-get-build-changes.tests.ts | 27 + resources/build/version/tests/tsconfig.json | 17 + resources/build/version/tsconfig.json | 23 + .../keyboards/keyboards-build-deploy.sh | 9 + 29 files changed, 2547 insertions(+) create mode 100644 resources/build/version/.gitignore create mode 100644 resources/build/version/README.md create mode 100755 resources/build/version/build.sh create mode 100644 resources/build/version/package-lock.json create mode 100644 resources/build/version/package.json create mode 100644 resources/build/version/src/find-and-report-on-new-pull-requests.ts create mode 100644 resources/build/version/src/get-list-of-keyboards-for-pull-request.ts create mode 100644 resources/build/version/src/get-pull-requests-from-commit-range.ts create mode 100644 resources/build/version/src/graphql/queries.ts create mode 100644 resources/build/version/src/index.ts create mode 100644 resources/build/version/src/prepare-pull-comment.ts create mode 100644 resources/build/version/src/send-comment-to-pull-request.ts create mode 100644 resources/build/version/src/teamcity-get-build-changes.ts create mode 100644 resources/build/version/src/util/spawnAwait.ts create mode 100644 resources/build/version/tests/fixtures/pull-requests/3860.comment.txt create mode 100644 resources/build/version/tests/fixtures/pull-requests/3860.json create mode 100644 resources/build/version/tests/fixtures/pull-requests/3932.comment.txt create mode 100644 resources/build/version/tests/fixtures/pull-requests/3932.json create mode 100644 resources/build/version/tests/fixtures/pull-requests/3967.json create mode 100644 resources/build/version/tests/fixtures/pull-requests/4023.json create mode 100644 resources/build/version/tests/fixtures/pull-requests/4029.json create mode 100644 resources/build/version/tests/fixtures/teamcity.json create mode 100644 resources/build/version/tests/get-list-of-keyboards-for-pull-request.tests.ts create mode 100644 resources/build/version/tests/helpers.ts create mode 100644 resources/build/version/tests/prepare-pull-comment.tests.ts create mode 100644 resources/build/version/tests/teamcity-get-build-changes.tests.ts create mode 100644 resources/build/version/tests/tsconfig.json create mode 100644 resources/build/version/tsconfig.json diff --git a/resources/build/version/.gitignore b/resources/build/version/.gitignore new file mode 100644 index 0000000000..e3698a3a86 --- /dev/null +++ b/resources/build/version/.gitignore @@ -0,0 +1,5 @@ +# Dependency directories +node_modules + +# Build output +build diff --git a/resources/build/version/README.md b/resources/build/version/README.md new file mode 100644 index 0000000000..e9cfb1bef3 --- /dev/null +++ b/resources/build/version/README.md @@ -0,0 +1,6 @@ +# Announce publication of merged pull requests + +This node module automatically adds a comment to each merged pull request since +the last successful deployment. + +It is wrappered by ../../teamcity/keyboards-build-deploy.sh for normal CI usage. diff --git a/resources/build/version/build.sh b/resources/build/version/build.sh new file mode 100755 index 0000000000..94fb183d1c --- /dev/null +++ b/resources/build/version/build.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Keyman is copyright (C) SIL Global. MIT License. + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../resources/builder.inc.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +cd "$THIS_SCRIPT_PATH" + +################################ Main script ################################ + +builder_describe "Build version tooling" clean configure build test + +builder_describe_outputs \ + configure /resources/build/version/node_modules \ + build /resources/build/version/build/src/index.js + +builder_describe_internal_dependency \ + report-stable-changes:project build:project + +builder_parse "$@" + +do_test() { + node_modules/.bin/tsc --build tests/ + node_modules/.bin/mocha +} + +builder_run_action clean rm -rf build/ node_modules/ +builder_run_action configure npm ci +builder_run_action build node_modules/.bin/tsc --build +builder_run_action test do_test diff --git a/resources/build/version/package-lock.json b/resources/build/version/package-lock.json new file mode 100644 index 0000000000..ce0879182e --- /dev/null +++ b/resources/build/version/package-lock.json @@ -0,0 +1,1530 @@ +{ + "name": "@keymanapp/auto-history-action", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@keymanapp/auto-history-action", + "license": "MIT", + "dependencies": { + "@actions/core": "^1.11.1", + "@actions/github": "^6.0.1", + "yargs": "^17.7.2" + }, + "devDependencies": { + "@types/chai": "^5.2.3", + "@types/mocha": "^10.0.10", + "@types/node": "^25.7.0", + "@types/semver": "^7.1.0", + "@types/sinon": "^21.0.1", + "@types/yargs": "^17.0.26", + "chai": "^6.2.2", + "mocha": "^11.7.6", + "sinon": "^22.0.0", + "typescript": "^5.4.5" + } + }, + "node_modules/@actions/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "license": "MIT", + "dependencies": { + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "license": "MIT", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/github": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.1.tgz", + "integrity": "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==", + "license": "MIT", + "dependencies": { + "@actions/http-client": "^2.2.0", + "@octokit/core": "^5.0.1", + "@octokit/plugin-paginate-rest": "^9.2.2", + "@octokit/plugin-rest-endpoint-methods": "^10.4.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "undici": "^5.28.5" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "license": "MIT" + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", + "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", + "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-10.0.2.tgz", + "integrity": "sha512-8lVwD1Df1BmzoaOLhMcGGcz/Jyr5QY2KSB75/YK1QgKzoabTeLdIVyhXNZK9ojfSKSdirbXqdbsXXqP9/Ve8+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.4.tgz", + "integrity": "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sinon": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-21.0.1.tgz", + "integrity": "sha512-5yoJSqLbjH8T9V2bksgRayuhpZy+723/z6wBOR+Soe4ZlXC0eW8Na71TeaZPUWDQvM7LYKa9UGFc6LRqxiR5fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz", + "integrity": "sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "license": "Apache-2.0" + }, + "node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "license": "ISC" + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mocha": { + "version": "11.7.6", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.6.tgz", + "integrity": "sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sinon": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-22.0.0.tgz", + "integrity": "sha512-sq/6DpdXOrLyfbKlXLg/Usc7xu8YXPeLkOFZRvA3bNUSA2lhbrZ06yuXbH1fkzBPCbz9O10+7hznzUsjaYNm0Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^15.4.0", + "@sinonjs/samsam": "^10.0.2", + "diff": "^9.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "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" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "license": "ISC" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/resources/build/version/package.json b/resources/build/version/package.json new file mode 100644 index 0000000000..f384d8fb1c --- /dev/null +++ b/resources/build/version/package.json @@ -0,0 +1,38 @@ +{ + "description": "Automatically updates HISTORY.md based on pull requests", + "type": "module", + "dependencies": { + "@actions/core": "^1.11.1", + "@actions/github": "^6.0.1", + "yargs": "^17.7.2" + }, + "devDependencies": { + "@types/chai": "^5.2.3", + "@types/mocha": "^10.0.10", + "@types/node": "^25.7.0", + "@types/semver": "^7.1.0", + "@types/sinon": "^21.0.1", + "@types/yargs": "^17.0.26", + "chai": "^6.2.2", + "mocha": "^11.7.6", + "sinon": "^22.0.0", + "typescript": "^5.4.5" + }, + "files": [ + "src" + ], + "license": "MIT", + "main": "build/src/index.js", + "name": "@keymanapp/auto-history-action", + "private": true, + "scripts": { + "build:ts": "tsc --project tsconfig.production.json", + "build:ts:watch": "tsc --project tsconfig.production.json --watch", + "clean": "run-p clean:*", + "clean:lib": "rm -rf lib", + "types": "tsc --noEmit" + }, + "mocha": { + "spec": "build/tests/**/*.tests.js" + } +} diff --git a/resources/build/version/src/find-and-report-on-new-pull-requests.ts b/resources/build/version/src/find-and-report-on-new-pull-requests.ts new file mode 100644 index 0000000000..a49dcd0e95 --- /dev/null +++ b/resources/build/version/src/find-and-report-on-new-pull-requests.ts @@ -0,0 +1,70 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import { getOctokit } from '@actions/github'; +import { warning as logWarning } from '@actions/core'; +export type GitHub = ReturnType; + +import { getPullRequestsFromCommitRange, PRInformation } from './get-pull-requests-from-commit-range.js'; +import { sendCommentToPullRequest } from './send-comment-to-pull-request.js'; +import { preparePullComment } from './prepare-pull-comment.js'; + +export interface PullComment { + number: number; + title: string; + comment: string; +}; + +/** + * Adds any outstanding pull request titles to HISTORY.md for the current + * version. Retrieves pull request details from GitHub. + * @returns number of history entries for the current version, + * 0 if no pulls associated with the current version, or + * -1 on error. + */ + +export const findAndReportOnNewPullRequests = async ( + octokit: GitHub, from: string, to?: string, omitFirstCommit?: boolean +): Promise => { + + // + // Get pull request details + // + + let pulls: PRInformation[] = []; + + try { + pulls = await getPullRequestsFromCommitRange(octokit, from, to, omitFirstCommit); + } catch(e) { + logWarning(String(e)); + return -1; + } + + if(pulls == undefined) { + return -1; + } + + // + // Build a comment for each pull + // + let pullComments = await Promise.all(pulls.map(async pull => await preparePullComment(octokit, pull))); + pullComments = pullComments.filter(pull => pull !== null); + + // + // Write a comment to GitHub for each of the pulls + // + + for(let pull of pullComments) { + await sendCommentToPullRequest(octokit, pull!); + + if(pullComments.length > 1) { + // Potentially creating many comments, we need to respect GitHub rate limits + // https://docs.github.com/en/rest/guides/best-practices-for-integrators#dealing-with-abuse-rate-limits + // At least 1 second delay between requests + await new Promise(resolve => setTimeout(() => resolve(null), 1000)); + } + } + + return pulls.length; +}; diff --git a/resources/build/version/src/get-list-of-keyboards-for-pull-request.ts b/resources/build/version/src/get-list-of-keyboards-for-pull-request.ts new file mode 100644 index 0000000000..9eae4ec9e8 --- /dev/null +++ b/resources/build/version/src/get-list-of-keyboards-for-pull-request.ts @@ -0,0 +1,40 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import { GitHub } from './find-and-report-on-new-pull-requests.js'; +import { graphql, getPullRequestFiles } from './graphql/queries.js'; + +export const getListOfKeyboardsForPullRequest = async (octokit: GitHub, number: number): Promise => { + const response = await graphql.call(octokit, + getPullRequestFiles, + { pr: number } + ); + + if (response === null || (response).repository.pullRequest === null) { + return []; + } + + const { + repository: { + pullRequest: { + files: { + nodes: nodes + } + } + } + }: any = response; + + const ids = [ + // unique list of ids + ...new Set(nodes + // remove non-keyboard-related files + .filter((node:{path:string}) => node.path.match(/^(?:release|experimental|legacy)\/[^/]+\/[^/]+/) && !node.path.match(/^([^/]+)\/(shared|template)\//)) + // get the ids + .map((node:{path:string}) => node.path.match(/^(?:release|experimental|legacy)\/(?:[^/]+)\/([^/]+)/)![1])) + ]; + + return ids; + //const node = nodes.find((node:any) => node.state == 'MERGED'); + //return node ? { title: node.title, number: node.number } : undefined; +}; diff --git a/resources/build/version/src/get-pull-requests-from-commit-range.ts b/resources/build/version/src/get-pull-requests-from-commit-range.ts new file mode 100644 index 0000000000..b6a5cd7ea6 --- /dev/null +++ b/resources/build/version/src/get-pull-requests-from-commit-range.ts @@ -0,0 +1,99 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import { warning as logWarning, info as logInfo } from '@actions/core'; + +import { getOctokit } from '@actions/github'; +type GitHub = ReturnType; + +import { getAssociatedPR } from './graphql/queries.js'; +import { spawnChild } from './util/spawnAwait.js'; + +export interface PRInformation { + title: string; + number: number; +} + +const getAssociatedPRInformation = async ( + octokit: GitHub, + commit_id: string +): Promise => { + const response = await octokit.graphql( + getAssociatedPR, + { sha: commit_id } + ); + + if (response === null || (response).repository.commit === null) { + return undefined; + } + + const { + repository: { + commit: { + parents: { + nodes: [ + { + associatedPullRequests: { + nodes: nodes + } + } + ] + } + } + } + }: any = response; + + const node = nodes.find((node:any) => node.state == 'MERGED'); + return node ? { title: node.title, number: node.number } : undefined; +}; + +/** + * Retrieves any outstanding pull request titles to HISTORY.md for the + * current version. Retrieves pull request details from GitHub. + * @returns array of pull requests + */ + +export const getPullRequestsFromCommitRange = async ( + octokit: GitHub, fromVersion: string, toVersion?: string, + omitFirstCommit?: boolean +): Promise => { + + // + // Use git log to retrieve list of merge commit refs in from -- to + // + + const args = ['log', '--merges', '--first-parent', '--format=%H', fromVersion + '..' + (toVersion ?? 'HEAD')]; + const gitResult = (await spawnChild('git', args)).trim(); + if(gitResult.length == 0) { + // We won't throw on this + logWarning('No pull requests found since previous increment'); + return []; + } + + logInfo('Found commits for '+JSON.stringify(args)+': \n'+gitResult); + + let newCommits = gitResult.split(/\r?\n/g); + + if(omitFirstCommit) { + // We exclude the first commit, because it comes from the previous build in TeamCity + newCommits = newCommits.filter(commit => commit != fromVersion); + } + + // + // Retrieve the pull requests associated with each merge + // + let pulls: PRInformation[] = []; + + for(const commit of newCommits) { + const pr = await getAssociatedPRInformation(octokit, commit); + if(pr === undefined) { + logWarning(`commit ref ${commit} has no associated pull request.`); + continue; + } + if(pulls.find(p => p.number == pr.number) == undefined) { + pulls.push(pr); + } + } + return pulls; +}; diff --git a/resources/build/version/src/graphql/queries.ts b/resources/build/version/src/graphql/queries.ts new file mode 100644 index 0000000000..162c4ec587 --- /dev/null +++ b/resources/build/version/src/graphql/queries.ts @@ -0,0 +1,43 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +export const graphql = { + call: (octokit: any, query: any, parameters: any) => octokit.graphql(query, parameters) +} + +export const getAssociatedPR = ` + query associatedPRs($sha: String!) { + repository(name: "keyboards", owner: "keymanapp") { + commit: object(expression: $sha) { + ... on Commit { + parents(last: 1) { + nodes { + associatedPullRequests(last: 10) { + nodes { + state + title + number + } + } + } + } + } + } + } + } +`; + +export const getPullRequestFiles = ` + query pullRequstFiles($pr: Int!) { + repository(name: "keyboards", owner: "keymanapp") { + pullRequest(number: $pr) { + files(first: 100) { + nodes { + path + } + } + } + } + } +`; diff --git a/resources/build/version/src/index.ts b/resources/build/version/src/index.ts new file mode 100644 index 0000000000..f64cbcbe61 --- /dev/null +++ b/resources/build/version/src/index.ts @@ -0,0 +1,120 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import { info as logInfo } from '@actions/core'; +import { getOctokit } from '@actions/github'; +type GitHub = ReturnType; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; + +import { findAndReportOnNewPullRequests } from './find-and-report-on-new-pull-requests.js'; +import { getPullRequestsFromCommitRange } from './get-pull-requests-from-commit-range.js'; +import { teamcityGetBuildChanges } from './teamcity-get-build-changes.js'; + +const argv = await yargs(hideBin(process.argv)) + .command(['last-commit'], 'Get last commit hash from the last successful build') + .command(['report'], 'Print list of outstanding PRs waiting for the next release') + .command(['pr'], 'Get list of outstanding PRs and add a comment to each one') + .demandCommand(1, 1) + .options({ + 'github-token': { + description: 'GitHub oauth token', + type: 'string' + }, + + 'teamcity-token': { + description: 'Teamcity oauth token', + type: 'string' + }, + + 'build-id': { + description: 'Teamcity build id to retrieve, defaults to last successful build', + type: 'number' + }, + + 'from': { + description: 'Version tag or commit for starting version to report after (excludes this commit)', + type: 'string' + }, + + 'to': { + description: 'Version tag or commit for finishing version to report to, defaults to HEAD', + type: 'string' + } + }) + .help() + .alias('help', 'h') + .argv; + +function require_arg(arg: string): string|never { + if(!argv[arg]) { + console.error(`--${arg} is required for this command`); + process.exit(1); + } + return argv[arg] as string; +} + +function is_command(cmd: string): boolean { + return argv._.includes(cmd); +} + +const main = async (): Promise => { + + if(is_command('run')) { + const shas = await teamcityGetBuildChanges(require_arg('teamcity-token'), argv['build-id']); + if(!shas) { + console.error('Could not retrieve SHAs from TeamCity'); + process.exit(1); + } + + const octokit: GitHub = getOctokit(require_arg('github-token')); + let changeCount = await findAndReportOnNewPullRequests(octokit, shas[0], shas[1], false); + logInfo(`# ${changeCount} change(s)\n`); + + process.exit(0); + } + + // Teamcity commands + + if(is_command('last-commit')) { + const shas = await teamcityGetBuildChanges(require_arg('teamcity-token'), argv['build-id']); + if(!shas) { + console.error('Could not retrieve SHAs'); + process.exit(1); + } + console.log(shas[0]); + console.log(shas[1]); + process.exit(0); + } + + // GitHub commands + + if(is_command('report')) { + logInfo(`# Reporting on merged pull requests`); + const octokit: GitHub = getOctokit(require_arg('github-token')); + let pulls = await getPullRequestsFromCommitRange(octokit, argv.from!, argv.to); + for(const pull of pulls) { + logInfo(`* ${pull.title} (#${pull.number})`); + } + process.exit(0); + } + + if(is_command('pr')) { + logInfo(`# Writing announcements to pull requests`); + const octokit: GitHub = getOctokit(require_arg('github-token')); + let changeCount = await findAndReportOnNewPullRequests(octokit, argv.from!, argv.to); + logInfo(`# ${changeCount} change(s)\n`); + process.exit(0); + } + + process.exit(50); // No command found +}; + +try { + await main(); + console.log('Unexpected exit'); +} catch(error: any) { + console.error(`An unexpected error occurred: ${error.message}, ${error.stack ?? 'no stack trace'}.`); + process.exit(1); +}; diff --git a/resources/build/version/src/prepare-pull-comment.ts b/resources/build/version/src/prepare-pull-comment.ts new file mode 100644 index 0000000000..90e3a3aaf9 --- /dev/null +++ b/resources/build/version/src/prepare-pull-comment.ts @@ -0,0 +1,45 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import { GitHub, PullComment } from './find-and-report-on-new-pull-requests.js'; +import { getListOfKeyboardsForPullRequest } from './get-list-of-keyboards-for-pull-request.js'; +import { PRInformation } from './get-pull-requests-from-commit-range.js'; + +export const preparePullComment = async (octokit: GitHub, pull: PRInformation): Promise => { + const ids = await getListOfKeyboardsForPullRequest(octokit, pull.number); + if (ids.length == 0) { + // skip comment + return null; + } + + return preparePullCommentWithIds(pull, ids); +} + +export const preparePullCommentWithIds = (pull: PRInformation, ids: string[]): PullComment => { + let comment: string; + + if (ids.length == 1) { + comment = ` +Thank you for your contribution! + +This keyboard has now been published and will shortly be available at: https://keyman.com/keyboards/${ids[0]}. + +(There may be a delay of a few minutes for deployment of the keyboard to run across multiple services.) + `; + } else { + comment = ` +Thank you for your contribution! + +The following keyboards have been published: +${ids.map(id => `* https://keyman.com/keyboards/${id}\n`).join('')} +(There may be a delay of a few minutes for deployment of the keyboard to run across multiple services.) + `; + } + + return { + number: pull.number, + title: pull.title, + comment + }; +}; diff --git a/resources/build/version/src/send-comment-to-pull-request.ts b/resources/build/version/src/send-comment-to-pull-request.ts new file mode 100644 index 0000000000..c88546df44 --- /dev/null +++ b/resources/build/version/src/send-comment-to-pull-request.ts @@ -0,0 +1,22 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import { GitHub, PullComment } from './find-and-report-on-new-pull-requests.js'; + +/** + * Creates a comment on each pull request found to point users to a relevant + * build. +*/ +export const sendCommentToPullRequest = async ( + octokit: GitHub, + pull: PullComment +): Promise => { + console.log(`Creating comment on Pull Request #${pull.number}: ${pull.title}`); + await octokit.rest.issues.createComment({ + owner: 'keymanapp', + repo: 'keyboards', + issue_number: pull.number, + body: pull.comment, + }); +}; diff --git a/resources/build/version/src/teamcity-get-build-changes.ts b/resources/build/version/src/teamcity-get-build-changes.ts new file mode 100644 index 0000000000..03a79a0e7f --- /dev/null +++ b/resources/build/version/src/teamcity-get-build-changes.ts @@ -0,0 +1,33 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +let teamcityToken: string = null; + +export const teamcity = { + fetch: async (path: string) => + await (await fetch(path, { + headers: [ + ['Accept', 'application/json'], + ['Authorization', `Bearer ${teamcityToken}`], + ], + })).json(), +}; + +const FETCH_LATEST_BUILD_CHANGES = + 'https://build.palaso.org/app/rest/builds'+ + '?locator=buildType:(Keyboards_Upload),state:finished,count:1,status:SUCCESS'+ + '&fields=build(id,status,state,number,finishOnAgentDate,webUrl,changes(change(version,vcsRootInstance)))'; +const FETCH_BUILD_CHANGES = (id: number) => + 'https://build.palaso.org/app/rest/builds'+ + `?locator=buildId:${id}`+ + '&fields=build(id,status,state,number,finishOnAgentDate,webUrl,changes(change(version,vcsRootInstance)))'; +const REPO = 'keymanapp/keyboards'; + +export const teamcityGetBuildChanges = async (token: string, buildId?: number): Promise => { + teamcityToken = token; + const data = await teamcity.fetch(buildId ? FETCH_BUILD_CHANGES(buildId) : FETCH_LATEST_BUILD_CHANGES); + const changes = data?.build?.[0]?.changes?.change?.filter((change:any) => change?.vcsRootInstance?.name == REPO); + return changes && changes.length > 0 ? + [changes[changes.length-1].version, changes[0].version] : null; +} diff --git a/resources/build/version/src/util/spawnAwait.ts b/resources/build/version/src/util/spawnAwait.ts new file mode 100644 index 0000000000..f6cd43dbe7 --- /dev/null +++ b/resources/build/version/src/util/spawnAwait.ts @@ -0,0 +1,31 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import { spawn } from 'child_process'; + +export const spawnChild = async function(cmd: string, params: string[]) { + const child = spawn(cmd, params); + + let data = ""; + for await (const chunk of child.stdout) { + //console.log('stdout chunk: '+chunk); + data += chunk; + } + + let error = ""; + for await (const chunk of child.stderr) { + //console.error('stderr chunk: '+chunk); + error += chunk; + } + + const exitCode = await new Promise( (resolve) => { + child.on('close', resolve); + }); + + if(exitCode) { + throw new Error(`subprocess error exit ${exitCode}, ${error}`); + } + + return data; +} diff --git a/resources/build/version/tests/fixtures/pull-requests/3860.comment.txt b/resources/build/version/tests/fixtures/pull-requests/3860.comment.txt new file mode 100644 index 0000000000..a38c624a72 --- /dev/null +++ b/resources/build/version/tests/fixtures/pull-requests/3860.comment.txt @@ -0,0 +1,5 @@ +Thank you for your contribution! + +This keyboard has now been published and will shortly be available at: https://keyman.com/keyboards/ladino. + +(There may be a delay of a few minutes for deployment of the keyboard to run across multiple services.) diff --git a/resources/build/version/tests/fixtures/pull-requests/3860.json b/resources/build/version/tests/fixtures/pull-requests/3860.json new file mode 100644 index 0000000000..dffbb23021 --- /dev/null +++ b/resources/build/version/tests/fixtures/pull-requests/3860.json @@ -0,0 +1,23 @@ +{ + "repository": { + "pullRequest": { + "files": { + "nodes": [ + { "path": "release/l/ladino/HISTORY.md" }, + { "path": "release/l/ladino/LICENSE.md" }, + { "path": "release/l/ladino/README.md" }, + { "path": "release/l/ladino/ladino.kpj" }, + { "path": "release/l/ladino/source/help/ladino.php" }, + { "path": "release/l/ladino/source/ladino.ico" }, + { "path": "release/l/ladino/source/ladino.keyman-touch-layout" }, + { "path": "release/l/ladino/source/ladino.kmn" }, + { "path": "release/l/ladino/source/ladino.kps" }, + { "path": "release/l/ladino/source/ladino.kvks" }, + { "path": "release/l/ladino/source/ladino.png" }, + { "path": "release/l/ladino/source/readme.htm" }, + { "path": "release/l/ladino/source/welcome/welcome.htm" } + ] + } + } + } +} diff --git a/resources/build/version/tests/fixtures/pull-requests/3932.comment.txt b/resources/build/version/tests/fixtures/pull-requests/3932.comment.txt new file mode 100644 index 0000000000..63e2c3c8c7 --- /dev/null +++ b/resources/build/version/tests/fixtures/pull-requests/3932.comment.txt @@ -0,0 +1,7 @@ +Thank you for your contribution! + +The following keyboards have been published: +* https://keyman.com/keyboards/baybayin +* https://keyman.com/keyboards/slp1_deva + +(There may be a delay of a few minutes for deployment of the keyboard to run across multiple services.) diff --git a/resources/build/version/tests/fixtures/pull-requests/3932.json b/resources/build/version/tests/fixtures/pull-requests/3932.json new file mode 100644 index 0000000000..ac4829baf1 --- /dev/null +++ b/resources/build/version/tests/fixtures/pull-requests/3932.json @@ -0,0 +1,30 @@ +{ + "repository": { + "pullRequest": { + "files": { + "nodes": [ + { "path": "release/b/baybayin/HISTORY.md" }, + { "path": "release/b/baybayin/LICENSE.md" }, + { "path": "release/b/baybayin/README.md" }, + { "path": "release/b/baybayin/source/baybayin.keyman-touch-layout" }, + { "path": "release/b/baybayin/source/baybayin.kmn" }, + { "path": "release/b/baybayin/source/baybayin.kps" }, + { "path": "release/b/baybayin/source/baybayin.kvks" }, + { "path": "release/b/baybayin/source/welcome/LayoutU_.png" }, + { "path": "release/b/baybayin/source/welcome/LayoutU_S.png" }, + { "path": "release/b/baybayin/source/welcome/kbd1.png" }, + { "path": "release/b/baybayin/source/welcome/kbd2.png" }, + { "path": "release/b/baybayin/source/welcome/welcome.htm" }, + { "path": "release/s/slp1_deva/HISTORY.md" }, + { "path": "release/s/slp1_deva/LICENSE.md" }, + { "path": "release/s/slp1_deva/README.md" }, + { + "path": "release/s/slp1_deva/source/slp1_deva.keyman-touch-layout" + }, + { "path": "release/s/slp1_deva/source/slp1_deva.kmn" }, + { "path": "release/s/slp1_deva/source/slp1_deva.kvks" } + ] + } + } + } +} diff --git a/resources/build/version/tests/fixtures/pull-requests/3967.json b/resources/build/version/tests/fixtures/pull-requests/3967.json new file mode 100644 index 0000000000..d21f7d84e0 --- /dev/null +++ b/resources/build/version/tests/fixtures/pull-requests/3967.json @@ -0,0 +1,43 @@ +{ + "repository": { + "pullRequest": { + "files": { + "nodes": [ + { "path": "release/shared/fonts/khmer/khmerbusra/Busra-B.ttf" }, + { "path": "release/shared/fonts/khmer/khmerbusra/Busra-BI.ttf" }, + { "path": "release/shared/fonts/khmer/khmerbusra/Busra-I.ttf" }, + { "path": "release/shared/fonts/khmer/khmerbusra/Busra-R.ttf" }, + { "path": "release/shared/fonts/khmer/khmerbusra/OFL.txt" }, + { "path": "release/shared/fonts/khmer/mondulkiri/FONTLOG.txt" }, + { "path": "release/shared/fonts/khmer/mondulkiri/Mondulkiri-B.ttf" }, + { "path": "release/shared/fonts/khmer/mondulkiri/Mondulkiri-BI.ttf" }, + { "path": "release/shared/fonts/khmer/mondulkiri/Mondulkiri-I.ttf" }, + { "path": "release/shared/fonts/khmer/mondulkiri/Mondulkiri-R.ttf" }, + { "path": "release/shared/fonts/khmer/mondulkiri/OFL-FAQ.txt" }, + { "path": "release/shared/fonts/khmer/mondulkiri/OFL.txt" }, + { + "path": "release/shared/fonts/khmer/mondulkiri/documentation/Differences in the Implementations of Khmer Unicode.pdf" + }, + { + "path": "release/shared/fonts/khmer/mondulkiri/documentation/Quick Guide to Stylistic Sets and Typography Options.pdf" + }, + { + "path": "release/shared/fonts/khmer/mondulkiri/documentation/Quick guide to typing Khmer.pdf" + }, + { + "path": "release/shared/fonts/khmer/mondulkiri/documentation/Stylistic sets in Mondulkiri fonts.pdf" + }, + { + "path": "release/shared/fonts/khmer/mondulkiri/documentation/The Mondulkiri Font Family.pdf" + }, + { + "path": "release/shared/fonts/khmer/mondulkiri/documentation/The orthographical syllable in Khmer and rules for the rendering of register shifters.pdf" + }, + { + "path": "release/shared/fonts/khmer/mondulkiri/documentation/Typography options in Mondulkiri fonts.pdf" + } + ] + } + } + } +} diff --git a/resources/build/version/tests/fixtures/pull-requests/4023.json b/resources/build/version/tests/fixtures/pull-requests/4023.json new file mode 100644 index 0000000000..c6a8b09dae --- /dev/null +++ b/resources/build/version/tests/fixtures/pull-requests/4023.json @@ -0,0 +1,21 @@ +{ + "repository": { + "pullRequest": { + "files": { + "nodes": [ + { "path": "release/s/shughni/HISTORY.md" }, + { "path": "release/s/shughni/LICENSE.md" }, + { "path": "release/s/shughni/README.md" }, + { "path": "release/s/shughni/shughni.kpj" }, + { "path": "release/s/shughni/source/readme.htm" }, + { "path": "release/s/shughni/source/shughni.ico" }, + { "path": "release/s/shughni/source/shughni.keyman-touch-layout" }, + { "path": "release/s/shughni/source/shughni.kmn" }, + { "path": "release/s/shughni/source/shughni.kps" }, + { "path": "release/s/shughni/source/shughni.kvks" }, + { "path": "release/s/shughni/source/welcome.htm" } + ] + } + } + } +} diff --git a/resources/build/version/tests/fixtures/pull-requests/4029.json b/resources/build/version/tests/fixtures/pull-requests/4029.json new file mode 100644 index 0000000000..578ac46d08 --- /dev/null +++ b/resources/build/version/tests/fixtures/pull-requests/4029.json @@ -0,0 +1,7 @@ +{ + "repository": { + "pullRequest": { + "files": { "nodes": [{ "path": "resources/build_targets.inc.sh" }] } + } + } +} diff --git a/resources/build/version/tests/fixtures/teamcity.json b/resources/build/version/tests/fixtures/teamcity.json new file mode 100644 index 0000000000..d6a2c96558 --- /dev/null +++ b/resources/build/version/tests/fixtures/teamcity.json @@ -0,0 +1,124 @@ +{ + "build": [ + { + "id": 630399, + "number": "2568", + "status": "SUCCESS", + "state": "finished", + "webUrl": "https://build.palaso.org/buildConfiguration/Keyboards_Upload/630399", + "changes": { + "change": [ + { + "version": "68318eb289e0a3b43ceaadd69e930679b429f723", + "vcsRootInstance": { + "id": "2238", + "vcs-root-id": "Keyboards_HttpsGithubComKeymanappKeyboards", + "name": "keymanapp/keyboards", + "href": "/app/rest/vcs-root-instances/id:2238" + } + }, + { + "version": "842f010c37b9c19ffc062a0f8c56ae2661ac1b7f", + "vcsRootInstance": { + "id": "2238", + "vcs-root-id": "Keyboards_HttpsGithubComKeymanappKeyboards", + "name": "keymanapp/keyboards", + "href": "/app/rest/vcs-root-instances/id:2238" + } + }, + { + "version": "75cd125ccc3956480292a2ed43f653126515dc6c", + "vcsRootInstance": { + "id": "2238", + "vcs-root-id": "Keyboards_HttpsGithubComKeymanappKeyboards", + "name": "keymanapp/keyboards", + "href": "/app/rest/vcs-root-instances/id:2238" + } + }, + { + "version": "0dcf816dd359ebd18f30eaa6ac3a471e5367cec2", + "vcsRootInstance": { + "id": "2238", + "vcs-root-id": "Keyboards_HttpsGithubComKeymanappKeyboards", + "name": "keymanapp/keyboards", + "href": "/app/rest/vcs-root-instances/id:2238" + } + }, + { + "version": "d02976106c58913103b9e6321dd303a389447c03", + "vcsRootInstance": { + "id": "2183", + "vcs-root-id": "Keyboards_HelpKeymanCom", + "name": "keymanapp/help.keyman.com", + "href": "/app/rest/vcs-root-instances/id:2183" + } + }, + { + "version": "9f7fb2a44fb18af9a13983eaac9c198a89bc8c38", + "vcsRootInstance": { + "id": "2183", + "vcs-root-id": "Keyboards_HelpKeymanCom", + "name": "keymanapp/help.keyman.com", + "href": "/app/rest/vcs-root-instances/id:2183" + } + }, + { + "version": "b2c4ee0763c3e6030fb734b48eacd874c3adbe86", + "vcsRootInstance": { + "id": "2189", + "vcs-root-id": "Keymanweb_SKeymanCom", + "name": "keymanapp/s.keyman.com", + "href": "/app/rest/vcs-root-instances/id:2189" + } + }, + { + "version": "88ada4025a33f7d01a1dca72ea2ec46dbf84a992", + "vcsRootInstance": { + "id": "2189", + "vcs-root-id": "Keymanweb_SKeymanCom", + "name": "keymanapp/s.keyman.com", + "href": "/app/rest/vcs-root-instances/id:2189" + } + }, + { + "version": "abf7cf018a13d8266dda87728d8bfad26d4b26d8", + "vcsRootInstance": { + "id": "2183", + "vcs-root-id": "Keyboards_HelpKeymanCom", + "name": "keymanapp/help.keyman.com", + "href": "/app/rest/vcs-root-instances/id:2183" + } + }, + { + "version": "7f9560136b4dd05e43ba3db51c6dfd25aaf58a81", + "vcsRootInstance": { + "id": "2183", + "vcs-root-id": "Keyboards_HelpKeymanCom", + "name": "keymanapp/help.keyman.com", + "href": "/app/rest/vcs-root-instances/id:2183" + } + }, + { + "version": "ee562cabacb7bdc5a4a79beaa528a5a34c5960e7", + "vcsRootInstance": { + "id": "2189", + "vcs-root-id": "Keymanweb_SKeymanCom", + "name": "keymanapp/s.keyman.com", + "href": "/app/rest/vcs-root-instances/id:2189" + } + }, + { + "version": "37892c832718b66783100763e1f4907abd035282", + "vcsRootInstance": { + "id": "2189", + "vcs-root-id": "Keymanweb_SKeymanCom", + "name": "keymanapp/s.keyman.com", + "href": "/app/rest/vcs-root-instances/id:2189" + } + } + ] + }, + "finishOnAgentDate": "20260624T162707+0000" + } + ] +} diff --git a/resources/build/version/tests/get-list-of-keyboards-for-pull-request.tests.ts b/resources/build/version/tests/get-list-of-keyboards-for-pull-request.tests.ts new file mode 100644 index 0000000000..9efe791f57 --- /dev/null +++ b/resources/build/version/tests/get-list-of-keyboards-for-pull-request.tests.ts @@ -0,0 +1,37 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import * as fs from 'fs'; +import 'mocha'; +import sinon from 'sinon'; +import { assert } from 'chai'; +import { getListOfKeyboardsForPullRequest } from '../src/get-list-of-keyboards-for-pull-request.js'; +import { makePathToFixture, octokit } from "./helpers.js"; +import { graphql } from '../src/graphql/queries.js'; + +const pull_responses = new Map([ + [3860, ['ladino'] ], + [4029, []], + [4023, ['shughni']], + [3967, []], + [3932, ['baybayin','slp1_deva']], +]); + +describe('getListOfKeyboardsForPullRequest', function() { + + afterEach(function() { + sinon.restore(); + }); + + pull_responses.forEach( (value, id) => + it(`should report a list of keyboards IDs for pull request ${id}`, async function() { + + const data = JSON.parse(fs.readFileSync(makePathToFixture('pull-requests', `${id}.json`), 'utf-8')); + sinon.replace(graphql, 'call', sinon.fake.returns(data)); + + const ids = await getListOfKeyboardsForPullRequest(octokit, id); + assert.deepEqual(ids, value); + }) + ); +}); diff --git a/resources/build/version/tests/helpers.ts b/resources/build/version/tests/helpers.ts new file mode 100644 index 0000000000..ac751032a7 --- /dev/null +++ b/resources/build/version/tests/helpers.ts @@ -0,0 +1,24 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { getOctokit } from '@actions/github'; +type GitHub = ReturnType; + +export const octokit: GitHub = getOctokit(process.env['GITHUB_TOKEN']!); + + +/** + * Builds a path to the fixture with the given path components. + * + * e.g., makePathToFixture('example.qaa.trivial') + * e.g., makePathToFixture('example.qaa.trivial', 'model.ts') + * + * @param components One or more path components. + */ + export function makePathToFixture(...components: string[]): string { + return fileURLToPath(new URL(path.join('..', '..', 'tests', 'fixtures', ...components), import.meta.url)); +} diff --git a/resources/build/version/tests/prepare-pull-comment.tests.ts b/resources/build/version/tests/prepare-pull-comment.tests.ts new file mode 100644 index 0000000000..afd4e5109c --- /dev/null +++ b/resources/build/version/tests/prepare-pull-comment.tests.ts @@ -0,0 +1,35 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import * as fs from 'fs'; +import 'mocha'; +import sinon from 'sinon'; +import { assert } from 'chai'; +import { makePathToFixture, octokit } from "./helpers.js"; +import { graphql } from '../src/graphql/queries.js'; +import { preparePullComment } from '../src/prepare-pull-comment.js'; + +const pull_comments = [3860, 3932]; + +describe('preparePullComment', function() { + + afterEach(function() { + sinon.restore(); + }); + + pull_comments.forEach( id => + it(`should generate a comment for pull request ${id}`, async function() { + + const data = JSON.parse(fs.readFileSync(makePathToFixture('pull-requests', `${id}.json`), 'utf-8')); + sinon.replace(graphql, 'call', sinon.fake.returns(data)); + + const comment = fs.readFileSync(makePathToFixture('pull-requests', `${id}.comment.txt`), 'utf-8').replace(/\r\n/g, '\n').trim(); + const result = await preparePullComment(octokit, {number: id, title: ''}); + assert.isObject(result); + assert.equal(result?.title, ''); + assert.equal(result?.number, id); + assert.equal(result?.comment.trim(), comment); + }) + ); +}); diff --git a/resources/build/version/tests/teamcity-get-build-changes.tests.ts b/resources/build/version/tests/teamcity-get-build-changes.tests.ts new file mode 100644 index 0000000000..c24458195c --- /dev/null +++ b/resources/build/version/tests/teamcity-get-build-changes.tests.ts @@ -0,0 +1,27 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import * as fs from 'fs'; +import 'mocha'; +import sinon from 'sinon'; +import { assert } from 'chai'; +import { makePathToFixture } from "./helpers.js"; +import { teamcity, teamcityGetBuildChanges } from '../src/teamcity-get-build-changes.js'; + +describe('teamcityGetBuildChanges', function() { + + afterEach(function() { + sinon.restore(); + }); + + it(`should return a set of changes for a given build`, async function() { + const data = JSON.parse(fs.readFileSync(makePathToFixture(`teamcity.json`), 'utf-8')); + sinon.replace(teamcity, 'fetch', sinon.fake.returns(data)); + const sha = await teamcityGetBuildChanges(''); + assert.deepEqual(sha, [ + "0dcf816dd359ebd18f30eaa6ac3a471e5367cec2", + "68318eb289e0a3b43ceaadd69e930679b429f723" + ]); + }); +}); diff --git a/resources/build/version/tests/tsconfig.json b/resources/build/version/tests/tsconfig.json new file mode 100644 index 0000000000..ad8638f3a4 --- /dev/null +++ b/resources/build/version/tests/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../tsconfig.json", + + "compilerOptions": { + "rootDir": ".", + "outDir": "../build/tests", + "baseUrl": ".", + "allowSyntheticDefaultImports": true, // for chai + }, + "include": [ + "**/*.tests.ts", + "helpers.ts", + ], + "references": [ + { "path": "../" } + ] +} diff --git a/resources/build/version/tsconfig.json b/resources/build/version/tsconfig.json new file mode 100644 index 0000000000..fb8723ffd1 --- /dev/null +++ b/resources/build/version/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "rootDir": ".", + "outDir": "build/", + "module": "node16", + "target": "es2022", + "moduleResolution": "node16", + "forceConsistentCasingInFileNames": true, + "sourceMap": true, + "alwaysStrict": true, + "noImplicitThis": true, + "noImplicitReturns": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "noUnusedLocals": true, + "allowJs": true, + "composite": true, + "declaration": true, + "declarationMap": true, + }, + "include": ["./**/*.ts"] +} diff --git a/resources/teamcity/keyboards/keyboards-build-deploy.sh b/resources/teamcity/keyboards/keyboards-build-deploy.sh index 32ccb7eaac..97dbc5bf29 100755 --- a/resources/teamcity/keyboards/keyboards-build-deploy.sh +++ b/resources/teamcity/keyboards/keyboards-build-deploy.sh @@ -17,6 +17,7 @@ builder_describe \ "all run all actions" \ "build build keyboards" \ "publish deploy keyboards" \ + "report report changes to affected pull requests" \ "--downloads-keyman-com=DOWNLOADS_KEYMAN_COM_URL URL of downloads server" \ "--help-keyman-com=HELP_KEYMAN_COM Path to help.keyman.com repository" \ "--s-keyman-com=S_KEYMAN_COM Path to s.keyman.com repository" \ @@ -79,12 +80,20 @@ function do_publish() { _upload_to_help_keyman_com } +function do_report() { + "${REPO_ROOT}/resources/build/version/build.sh" configure build + local BUILDID="${BUILD_URL##*/}" + node resources/build/version run --teamcity-token "$TEAMCITY_TOKEN" --github-token "$GITHUB_TOKEN" --build-id "$BUILDID" +} + cd "${REPO_ROOT}" if builder_has_action all; then do_build do_publish + do_report else builder_run_action build do_build builder_run_action publish do_publish + builder_run_action report do_report fi