diff --git a/.github/workflows/actions-ci.yml b/.github/workflows/actions-ci.yml index 251423cd..9abc0bab 100644 --- a/.github/workflows/actions-ci.yml +++ b/.github/workflows/actions-ci.yml @@ -23,6 +23,8 @@ jobs: node: [24, 26] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ matrix.node }} diff --git a/actions/build/action.yml b/actions/build/action.yml index 128f4b1a..b2772a27 100644 --- a/actions/build/action.yml +++ b/actions/build/action.yml @@ -9,6 +9,10 @@ runs: with: node-version: 24 + - name: Check checkout credentials + run: node "$GITHUB_ACTION_PATH/scripts/validate-checkout/index.ts" --root "$GITHUB_WORKSPACE" + shell: bash + - name: Check required npm files and build script run: node "$GITHUB_ACTION_PATH/scripts/validate-repository/index.mjs" --root "$GITHUB_WORKSPACE" shell: bash diff --git a/actions/build/eslint.config.js b/actions/build/eslint.config.js index e395365d..cddc4f22 100644 --- a/actions/build/eslint.config.js +++ b/actions/build/eslint.config.js @@ -2,6 +2,7 @@ import js from "@eslint/js" import { defineConfig } from "eslint/config" import prettier from "eslint-config-prettier" import globals from "globals" +import tseslint from "typescript-eslint" export default defineConfig( { @@ -9,7 +10,11 @@ export default defineConfig( }, js.configs.recommended, { - files: ["**/*.mjs"], + files: ["**/*.ts"], + extends: [tseslint.configs.recommended], + }, + { + files: ["**/*.{mjs,ts}"], languageOptions: { globals: { ...globals.browser, ...globals.node }, }, diff --git a/actions/build/package-lock.json b/actions/build/package-lock.json index c4f2515c..f6bc0cc8 100644 --- a/actions/build/package-lock.json +++ b/actions/build/package-lock.json @@ -13,10 +13,14 @@ }, "devDependencies": { "@eslint/js": "10.0.1", + "@types/node": "26.6.5", + "@typescript/native": "npm:typescript@7.0.2", "eslint": "10.13.0", "eslint-config-prettier": "10.1.8", "globals": "17.13.0", - "prettier": "3.9.10" + "prettier": "3.9.10", + "typescript": "npm:@typescript/typescript6@6.0.2", + "typescript-eslint": "8.72.0" }, "engines": { "node": "^24.15.0 || ^26.0.0" @@ -285,6 +289,637 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.0.tgz", + "integrity": "sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.72.0", + "@typescript-eslint/type-utils": "8.72.0", + "@typescript-eslint/utils": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.72.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.13.tgz", + "integrity": "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.0.tgz", + "integrity": "sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.72.0", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.0.tgz", + "integrity": "sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.72.0", + "@typescript-eslint/types": "^8.72.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.0.tgz", + "integrity": "sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.0.tgz", + "integrity": "sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.0.tgz", + "integrity": "sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/utils": "8.72.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.0.tgz", + "integrity": "sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.0.tgz", + "integrity": "sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.72.0", + "@typescript-eslint/tsconfig-utils": "8.72.0", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.0.tgz", + "integrity": "sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.72.0", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.0.tgz", + "integrity": "sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript/native": { + "name": "typescript", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/@typescript/old": { + "name": "typescript", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/acorn": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", @@ -607,6 +1242,24 @@ "dev": true, "license": "MIT" }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "11.1.5", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", @@ -903,6 +1556,19 @@ "node": ">=8" } }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/playwright": { "version": "1.64.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz", @@ -995,6 +1661,19 @@ "dev": true, "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -1018,6 +1697,36 @@ "node": ">=8" } }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -1031,6 +1740,51 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "name": "@typescript/typescript6", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript6/-/typescript6-6.0.2.tgz", + "integrity": "sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@typescript/old": "npm:typescript@^6" + }, + "bin": { + "tsc6": "bin/tsc6" + } + }, + "node_modules/typescript-eslint": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.0.tgz", + "integrity": "sha512-r26Fkc8As+mZvqp7SLsMmNFyYUup/6CjK7/eHBHmhOw6GH6n6YFxHVFrG0jN1c5CMpDzelLPxvgeCOzx7BjN+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.72.0", + "@typescript-eslint/parser": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/utils": "8.72.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/actions/build/package.json b/actions/build/package.json index f0525ff0..fb92b11a 100644 --- a/actions/build/package.json +++ b/actions/build/package.json @@ -7,8 +7,8 @@ "node": "^24.15.0 || ^26.0.0" }, "scripts": { - "lint": "eslint . && prettier --check .", - "test": "node --test --test-concurrency=1 scripts/*/index.test.mjs" + "lint": "tsc --noEmit && eslint . && prettier --check .", + "test": "node --test --test-concurrency=1 scripts/*/index.test.mjs scripts/*/index.test.ts" }, "dependencies": { "playwright": "1.64.1", @@ -16,9 +16,13 @@ }, "devDependencies": { "@eslint/js": "10.0.1", + "@types/node": "26.6.5", + "@typescript/native": "npm:typescript@7.0.2", "eslint": "10.13.0", "eslint-config-prettier": "10.1.8", "globals": "17.13.0", - "prettier": "3.9.10" + "prettier": "3.9.10", + "typescript": "npm:@typescript/typescript6@6.0.2", + "typescript-eslint": "8.72.0" } } diff --git a/actions/build/scripts/validate-checkout/index.test.ts b/actions/build/scripts/validate-checkout/index.test.ts new file mode 100644 index 00000000..fa807ff3 --- /dev/null +++ b/actions/build/scripts/validate-checkout/index.test.ts @@ -0,0 +1,168 @@ +import assert from "node:assert/strict" +import { spawnSync } from "node:child_process" +import { mkdir, readFile, rm, writeFile } from "node:fs/promises" +import path from "node:path" +import { describe, it, type TestContext } from "node:test" +import { fileURLToPath } from "node:url" + +import { temporaryDirectory } from "../../test/temporary-directory/index.mjs" + +import { validateCheckout } from "./index.ts" + +const actionRoot = fileURLToPath(new URL("../../", import.meta.url)) +const script = fileURLToPath(new URL("./index.ts", import.meta.url)) +const secret = "dummy-checkout-credential-do-not-log" +const authorization = `[http]\nextraHeader = AUTHORIZATION: basic ${secret}\n` + +async function checkout(t: TestContext) { + const root = await temporaryDirectory() + t.after(() => rm(root, { recursive: true, force: true })) + const result = spawnSync("git", ["init", "--quiet", root], { encoding: "utf8" }) + assert.ifError(result.error) + assert.equal(result.status, 0, result.stderr) + return root +} + +describe("validateCheckout()", () => { + it("accepts an ordinary checkout with a credential-free internal submodule", async (t) => { + const root = await checkout(t) + await writeFile( + path.join(root, ".git", "config"), + '[remote "origin"]\nurl = https://github.com/example/prototype.git\n' + + '[branch "main"]\nremote = origin\nmerge = refs/heads/main\n', + ) + const metadata = path.join(root, ".git", "modules", "nested") + const submodule = path.join(root, "packages", "nested") + await mkdir(path.dirname(metadata), { recursive: true }) + const result = spawnSync( + "git", + ["init", "--quiet", "--separate-git-dir", metadata, submodule], + { encoding: "utf8" }, + ) + assert.ifError(result.error) + assert.equal(result.status, 0, result.stderr) + await writeFile(path.join(submodule, ".git"), "gitdir: ../../.git/modules/nested\n") + + await assert.doesNotReject(validateCheckout(root)) + }) + + it("runs the action guard before npm lifecycle and build scripts without leaking authorization", async (t) => { + const action = await readFile(path.join(actionRoot, "action.yml"), "utf8") + const guard = action.match(/run: (node [^\n]+validate-checkout[^\n]+)/) + assert.ok(guard, "The action must run checkout validation") + assert.ok(typeof guard.index === "number") + const guardCommand = guard[1] + const build = action.indexOf("npm ci && npm run build") + assert.ok(build > guard.index, "The guard must precede repository execution") + + const root = await checkout(t) + const name = "checkout-guard-fixture" + const version = "1.0.0" + await writeFile( + path.join(root, "package.json"), + JSON.stringify({ + name, + version, + scripts: { preinstall: "node marker.mjs installed", build: "node marker.mjs built" }, + }), + ) + await writeFile( + path.join(root, "package-lock.json"), + JSON.stringify({ + name, + version, + lockfileVersion: 3, + packages: { "": { name, version, hasInstallScript: true } }, + }), + ) + await writeFile( + path.join(root, "marker.mjs"), + 'import { writeFileSync } from "node:fs"; writeFileSync(process.argv[2], "executed")\n', + ) + function run() { + const result = spawnSync("bash", ["-e", "-c", `${guardCommand}\nnpm ci && npm run build`], { + cwd: root, + encoding: "utf8", + env: { + ...process.env, + GITHUB_ACTION_PATH: actionRoot, + GITHUB_WORKSPACE: root, + NODE_COMPILE_CACHE: path.join(root, ".node-cache"), + npm_config_audit: "false", + npm_config_fund: "false", + npm_config_cache: path.join(root, ".npm-cache"), + }, + }) + assert.ifError(result.error) + return result + } + + const clean = run() + assert.equal(clean.status, 0, clean.stderr) + for (const marker of ["installed", "built"]) { + assert.equal(await readFile(path.join(root, marker), "utf8"), "executed") + await rm(path.join(root, marker)) + } + await writeFile(path.join(root, ".git", "config"), authorization) + + const unsafe = run() + assert.equal(unsafe.status, 1) + assert.ok(!`${unsafe.stdout}${unsafe.stderr}`.includes(secret)) + for (const marker of ["installed", "built"]) { + await assert.rejects(readFile(path.join(root, marker)), { code: "ENOENT" }) + } + }) + + it("rejects authentication embedded in a remote URL", async (t) => { + const root = await checkout(t) + await writeFile( + path.join(root, ".git", "config"), + `[remote "origin"]\nurl = https://user:${secret}@github.com/example/prototype.git\n`, + ) + + await assert.rejects(validateCheckout(root)) + }) + + it("rejects authentication in recursive submodule metadata", async (t) => { + const root = await checkout(t) + const metadata = path.join(root, ".git", "modules", "parent", "modules", "child") + await mkdir(metadata, { recursive: true }) + await writeFile(path.join(metadata, "config"), authorization) + + await assert.rejects(validateCheckout(root)) + }) + + it("rejects authentication in worktree-specific config", async (t) => { + const root = await checkout(t) + await writeFile( + path.join(root, ".git", "config"), + "[core]\nrepositoryformatversion = 1\n[extensions]\nworktreeConfig = true\n", + ) + await writeFile(path.join(root, ".git", "config.worktree"), authorization) + + await assert.rejects(validateCheckout(root)) + }) + + it("fails closed when an include can hide additional configuration", async (t) => { + const root = await checkout(t) + await writeFile(path.join(root, ".git", "config"), '[include]\npath = "../hidden-config"\n') + await writeFile(path.join(root, "hidden-config"), "[core]\nbare = false\n") + + await assert.rejects(validateCheckout(root)) + }) + + it("fails the CLI for malformed Git config without exposing its diagnostic input", async (t) => { + const root = await checkout(t) + await writeFile(path.join(root, ".git", "config"), `${authorization}[broken\n`) + + const result = spawnSync(process.execPath, [script, "--root", root], { + encoding: "utf8", + cwd: root, + }) + + assert.ifError(result.error) + assert.equal(result.status, 1) + assert.ok(`${result.stdout}${result.stderr}`.trim().length > 0) + assert.ok(!`${result.stdout}${result.stderr}`.includes(secret)) + }) +}) diff --git a/actions/build/scripts/validate-checkout/index.ts b/actions/build/scripts/validate-checkout/index.ts new file mode 100644 index 00000000..1652aaa6 --- /dev/null +++ b/actions/build/scripts/validate-checkout/index.ts @@ -0,0 +1,111 @@ +import { execFile } from "node:child_process" +import { readFile, readdir, realpath } from "node:fs/promises" +import path from "node:path" +import { fileURLToPath } from "node:url" +import { promisify } from "node:util" + +import { requiredPathArgument } from "../required-path-argument/index.mjs" + +const run = promisify(execFile) +const unsafeCheckout = + "Checkout must not contain Git authentication settings or Git metadata outside the workspace. " + + "Use actions/checkout with persist-credentials: false, including nested checkouts and submodules." + +/** Rejects persisted Git authentication before repository-controlled scripts run. */ +export async function validateCheckout(root: string): Promise { + root = await realpath(root) + const pending = [{ directory: root, metadata: false }] + const visited = new Set() + + for (let current = pending.pop(); current; current = pending.pop()) { + const { directory, metadata } = current + const identity = `${metadata}:${directory}` + + if (visited.has(identity)) continue + visited.add(identity) + + for (const entry of await readdir(directory, { withFileTypes: true })) { + const location = path.join(directory, entry.name) + const gitMetadata = metadata || entry.name === ".git" + const config = metadata && ["config", "config.worktree"].includes(entry.name) + + if ((entry.name === ".git" && entry.isFile()) || (metadata && entry.name === "commondir")) { + if (!entry.isFile()) throw new Error(unsafeCheckout) + const content = await readFile(location, "utf8") + const reference = + entry.name === ".git" ? /^gitdir: (.+)\r?\n?$/.exec(content)?.[1] : content.trimEnd() + + if (!reference) throw new Error(unsafeCheckout) + let target + + try { + target = await realpath(path.resolve(directory, reference)) + } catch { + throw new Error("Cannot resolve checkout Git metadata. " + unsafeCheckout) + } + + const relative = path.relative(root, target) + if ( + relative === ".." || + relative.startsWith(`..${path.sep}`) || + path.isAbsolute(relative) + ) { + throw new Error(unsafeCheckout) + } + + pending.push({ directory: target, metadata: true }) + continue + } + + if ( + (entry.name === ".git" && !entry.isDirectory()) || + (metadata && entry.isSymbolicLink()) || + (config && !entry.isFile()) + ) { + throw new Error(unsafeCheckout) + } + + if (entry.isDirectory()) { + pending.push({ directory: location, metadata: gitMetadata }) + } else if (config) { + let output + + try { + const result = await run("git", [ + "config", + "--file", + location, + "--no-includes", + "--null", + "--list", + ]) + output = result.stdout + } catch { + // Git's parser errors can contain credentials from the config file. + throw new Error("Cannot safely read checkout Git configuration. " + unsafeCheckout) + } + + for (const setting of output.split("\0").filter(Boolean)) { + const separator = setting.indexOf("\n") + const key = (separator < 0 ? setting : setting.slice(0, separator)).toLowerCase() + + if ( + key.startsWith("credential.") || + /^http\.(.*\.)?(extraheader|cookiefile)$/.test(key) || + /^core\.(sshcommand|askpass)$/.test(key) || + key === "include.path" || + /^includeif\..*\.path$/.test(key) || + /[a-z][a-z0-9+.-]*:\/\/[^/\s]*@/i.test(setting) + ) { + throw new Error(unsafeCheckout) + } + } + } + } + } +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + await validateCheckout(requiredPathArgument("root")) + console.log("Checkout contains no persisted Git authentication settings.") +} diff --git a/actions/build/tsconfig.json b/actions/build/tsconfig.json new file mode 100644 index 00000000..d7225393 --- /dev/null +++ b/actions/build/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2024", + "module": "NodeNext", + "strict": true, + "erasableSyntaxOnly": true, + "allowJs": true, + "allowImportingTsExtensions": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["scripts/validate-checkout/**/*.ts"] +} diff --git a/hub/docs/github-actions-builds.md b/hub/docs/github-actions-builds.md index 65a0d790..452245e7 100644 --- a/hub/docs/github-actions-builds.md +++ b/hub/docs/github-actions-builds.md @@ -28,7 +28,7 @@ Installation and update retries reuse a branch only when GitHub confirms it star The action: -1. Checks for `package.json`, `package-lock.json`, and a build script. +1. Checks for persisted Git authentication settings, then for `package.json`, `package-lock.json`, and a build script. 2. Runs `npm ci` and `npm run build` in a pinned Node container. 3. Checks the generated `dist` files and their sizes. 4. Opens the prototype in Chromium. @@ -37,6 +37,14 @@ The action: The build container receives no Hub, GitHub App, or storage credentials. It receives no credentials for any other repository. +### Checkout credentials + +Use `persist-credentials: false` on every `actions/checkout` step that places files in the build workspace, including nested checkouts and submodules. Give the workflow only `contents: read` unless another step needs more access. Hub-generated workflows already use these settings. + +Before running repository scripts, the action rejects Git authentication settings in the workspace, including submodule configuration. Rejected settings include credential helpers, HTTP extra headers and cookie files, custom SSH and password-prompt commands, and URLs containing user information. Git configuration includes, symbolic links within Git metadata, and metadata references outside the workspace are also rejected so they cannot hide authentication settings from the check. Submodule metadata references within the workspace remain supported. Remove rejected settings or use a credential-free checkout with self-contained Git metadata before building. The action does not remove credentials or print their values. + +This check is not a general secret scanner. Do not place tokens, private keys, credential files, or other secrets in the build workspace. Repository-controlled install and build scripts can read that workspace and access the network. + ## How Hub follows the workflow Hub follows `.github/workflows/replica.yml` for connected repositories and matches each run to its default or prototype branch and exact commit.