mirror of
https://github.com/github/codeql-action.git
synced 2026-10-17 08:11:24 +00:00
Compare commits
8 Commits
v4.39.0
...
mbg/ts/rem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e5cfb2878 | ||
|
|
7fbadd1a21 | ||
|
|
7e36708483 | ||
|
|
fccc166683 | ||
|
|
f8f91fb336 | ||
|
|
58f39a7d42 | ||
|
|
40b3f0c1bc | ||
|
|
6e32e882a9 |
@@ -54,16 +54,15 @@ runs:
|
||||
env:
|
||||
CODEQL_ACTION_TEST_MODE: 'true'
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: npm install --location=global ts-node js-yaml
|
||||
|
||||
- name: Check config
|
||||
working-directory: ${{ github.action_path }}
|
||||
shell: bash
|
||||
env:
|
||||
EXPECTED_CONFIG_FILE_CONTENTS: '${{ inputs.expected-config-file-contents }}'
|
||||
run: ts-node ./index.ts "$RUNNER_TEMP/user-config.yaml" "$EXPECTED_CONFIG_FILE_CONTENTS"
|
||||
run: |
|
||||
npx tsx ../action/pr-checks/check-cs-config.ts \
|
||||
--file "$RUNNER_TEMP/user-config.yaml" \
|
||||
--expected-contents "$EXPECTED_CONFIG_FILE_CONTENTS"
|
||||
|
||||
- name: Clean up
|
||||
shell: bash
|
||||
if: always()
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
|
||||
import * as core from '@actions/core'
|
||||
import * as yaml from 'js-yaml'
|
||||
import * as fs from 'fs'
|
||||
import * as assert from 'assert'
|
||||
|
||||
const actualConfig = loadActualConfig()
|
||||
|
||||
function sortConfigArrays(config) {
|
||||
for (const key of Object.keys(config)) {
|
||||
const value = config[key];
|
||||
if (key === 'queries' && Array.isArray(value)) {
|
||||
config[key] = value.sort();
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
const rawExpectedConfig = process.argv[3].trim()
|
||||
if (!rawExpectedConfig) {
|
||||
core.setFailed('No expected configuration provided')
|
||||
} else {
|
||||
core.startGroup('Expected generated user config')
|
||||
core.info(yaml.dump(JSON.parse(rawExpectedConfig)))
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
const expectedConfig = rawExpectedConfig ? JSON.parse(rawExpectedConfig) : undefined;
|
||||
|
||||
assert.deepStrictEqual(
|
||||
sortConfigArrays(actualConfig),
|
||||
sortConfigArrays(expectedConfig),
|
||||
'Expected configuration does not match actual configuration'
|
||||
);
|
||||
|
||||
|
||||
function loadActualConfig() {
|
||||
if (!fs.existsSync(process.argv[2])) {
|
||||
core.info('No configuration file found')
|
||||
return undefined
|
||||
} else {
|
||||
const rawActualConfig = fs.readFileSync(process.argv[2], 'utf8')
|
||||
core.startGroup('Actual generated user config')
|
||||
core.info(rawActualConfig)
|
||||
core.endGroup()
|
||||
|
||||
return yaml.load(rawActualConfig)
|
||||
}
|
||||
}
|
||||
22
.github/actions/check-sarif/action.yml
vendored
22
.github/actions/check-sarif/action.yml
vendored
@@ -16,5 +16,23 @@ inputs:
|
||||
Comma separated list of query ids that should NOT be included in this SARIF file.
|
||||
|
||||
runs:
|
||||
using: node24
|
||||
main: index.js
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Run `check-sarif.ts`
|
||||
shell: bash
|
||||
env:
|
||||
SARIF_FILE: ${{ inputs.sarif-file }}
|
||||
QUERIES_RUN: ${{ inputs.queries-run }}
|
||||
QUERIES_NOT_RUN: ${{ inputs.queries-not-run }}
|
||||
run: |
|
||||
if [[ -d pr-checks ]]; then
|
||||
npx tsx ./pr-checks/check-sarif.ts \
|
||||
--sarif-file "$SARIF_FILE" \
|
||||
--queries-run "$QUERIES_RUN" \
|
||||
--queries-not-run "$QUERIES_NOT_RUN"
|
||||
else
|
||||
npx tsx ../action/pr-checks/check-sarif.ts \
|
||||
--sarif-file "$SARIF_FILE" \
|
||||
--queries-run "$QUERIES_RUN" \
|
||||
--queries-not-run "$QUERIES_NOT_RUN"
|
||||
fi
|
||||
|
||||
43
.github/actions/check-sarif/index.js
vendored
43
.github/actions/check-sarif/index.js
vendored
@@ -1,43 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const core = require('@actions/core')
|
||||
const fs = require('fs')
|
||||
|
||||
const sarif = JSON.parse(fs.readFileSync(core.getInput('sarif-file'), 'utf8'))
|
||||
const rules = sarif.runs[0].tool.extensions.flatMap(ext => ext.rules || [])
|
||||
const ruleIds = rules.map(rule => rule.id)
|
||||
|
||||
// Check that all the expected queries ran
|
||||
const expectedQueriesRun = getQueryIdsInput('queries-run')
|
||||
const queriesThatShouldHaveRunButDidNot = expectedQueriesRun.filter(queryId => !ruleIds.includes(queryId))
|
||||
|
||||
if (queriesThatShouldHaveRunButDidNot.length > 0) {
|
||||
core.setFailed(`The following queries were expected to run but did not: ${queriesThatShouldHaveRunButDidNot.join(', ')}`)
|
||||
}
|
||||
|
||||
// Check that all the unexpected queries did not run
|
||||
const expectedQueriesNotRun = getQueryIdsInput('queries-not-run')
|
||||
|
||||
const queriesThatShouldNotHaveRunButDid = expectedQueriesNotRun.filter(queryId => ruleIds.includes(queryId))
|
||||
|
||||
if (queriesThatShouldNotHaveRunButDid.length > 0) {
|
||||
core.setFailed(`The following queries were NOT expected to have run but did: ${queriesThatShouldNotHaveRunButDid.join(', ')}`)
|
||||
}
|
||||
|
||||
|
||||
core.startGroup('All queries run')
|
||||
rules.forEach(rule => {
|
||||
core.info(`${rule.id}: ${(rule.properties && rule.properties.name) || rule.name}`)
|
||||
})
|
||||
core.endGroup()
|
||||
|
||||
core.startGroup('Full SARIF')
|
||||
core.info(JSON.stringify(sarif, null, 2))
|
||||
core.endGroup()
|
||||
|
||||
function getQueryIdsInput(name) {
|
||||
return core.getInput(name)
|
||||
.split(',')
|
||||
.map(q => q.trim())
|
||||
.filter(q => q.length > 0)
|
||||
}
|
||||
14
.github/actions/update-bundle/action.yml
vendored
14
.github/actions/update-bundle/action.yml
vendored
@@ -1,14 +0,0 @@
|
||||
name: Update default CodeQL bundle
|
||||
description: Updates 'src/defaults.json' to point to a new CodeQL bundle release.
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install ts-node
|
||||
shell: bash
|
||||
run: npm install -g ts-node
|
||||
|
||||
- name: Run update script
|
||||
working-directory: ${{ github.action_path }}
|
||||
shell: bash
|
||||
run: ts-node ./index.ts
|
||||
67
.github/actions/update-bundle/index.ts
vendored
67
.github/actions/update-bundle/index.ts
vendored
@@ -1,67 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as github from '@actions/github';
|
||||
|
||||
interface BundleInfo {
|
||||
bundleVersion: string;
|
||||
cliVersion: string;
|
||||
}
|
||||
|
||||
interface Defaults {
|
||||
bundleVersion: string;
|
||||
cliVersion: string;
|
||||
priorBundleVersion: string;
|
||||
priorCliVersion: string;
|
||||
}
|
||||
|
||||
function getCodeQLCliVersionForRelease(release): string {
|
||||
// We do not currently tag CodeQL bundles based on the CLI version they contain.
|
||||
// Instead, we use a marker file `cli-version-<version>.txt` to record the CLI version.
|
||||
// This marker file is uploaded as a release asset for all new CodeQL bundles.
|
||||
const cliVersionsFromMarkerFiles = release.assets
|
||||
.map((asset) => asset.name.match(/cli-version-(.*)\.txt/)?.[1])
|
||||
.filter((v) => v)
|
||||
.map((v) => v as string);
|
||||
if (cliVersionsFromMarkerFiles.length > 1) {
|
||||
throw new Error(
|
||||
`Release ${release.tag_name} has multiple CLI version marker files.`
|
||||
);
|
||||
} else if (cliVersionsFromMarkerFiles.length === 0) {
|
||||
throw new Error(
|
||||
`Failed to find the CodeQL CLI version for release ${release.tag_name}.`
|
||||
);
|
||||
}
|
||||
return cliVersionsFromMarkerFiles[0];
|
||||
}
|
||||
|
||||
async function getBundleInfoFromRelease(release): Promise<BundleInfo> {
|
||||
return {
|
||||
bundleVersion: release.tag_name,
|
||||
cliVersion: getCodeQLCliVersionForRelease(release)
|
||||
};
|
||||
}
|
||||
|
||||
async function getNewDefaults(currentDefaults: Defaults): Promise<Defaults> {
|
||||
const release = github.context.payload.release;
|
||||
console.log('Updating default bundle as a result of the following release: ' +
|
||||
`${JSON.stringify(release)}.`)
|
||||
|
||||
const bundleInfo = await getBundleInfoFromRelease(release);
|
||||
return {
|
||||
bundleVersion: bundleInfo.bundleVersion,
|
||||
cliVersion: bundleInfo.cliVersion,
|
||||
priorBundleVersion: currentDefaults.bundleVersion,
|
||||
priorCliVersion: currentDefaults.cliVersion
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const previousDefaults: Defaults = JSON.parse(fs.readFileSync('../../../src/defaults.json', 'utf8'));
|
||||
const newDefaults = await getNewDefaults(previousDefaults);
|
||||
// Update the source file in the repository. Calling workflows should subsequently rebuild
|
||||
// the Action to update `lib/defaults.json`.
|
||||
fs.writeFileSync('../../../src/defaults.json', JSON.stringify(newDefaults, null, 2) + "\n");
|
||||
}
|
||||
|
||||
// Ideally, we'd await main() here, but that doesn't work well with `ts-node`.
|
||||
// So instead we rely on the fact that Node won't exit until the event loop is empty.
|
||||
main();
|
||||
2
.github/workflows/pr-checks.yml
vendored
2
.github/workflows/pr-checks.yml
vendored
@@ -61,7 +61,7 @@ jobs:
|
||||
npm ci
|
||||
|
||||
- name: Verify compiled JS up to date
|
||||
run: .github/workflows/script/check-js.sh
|
||||
run: npx tsx pr-checks/check-js.ts
|
||||
|
||||
- name: Run unit tests
|
||||
if: always()
|
||||
|
||||
33
.github/workflows/script/check-js.sh
vendored
33
.github/workflows/script/check-js.sh
vendored
@@ -1,33 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
# Sanity check that repo is clean to start with
|
||||
if [ ! -z "$(git status --porcelain)" ]; then
|
||||
# If we get a fail here then this workflow needs attention...
|
||||
>&2 echo "Failed: Repo should be clean before testing!"
|
||||
exit 1
|
||||
fi
|
||||
# Wipe the lib directory in case there are extra unnecessary files in there
|
||||
rm -rf lib
|
||||
# Generate the JavaScript files
|
||||
npm run-script build
|
||||
# Check that repo is still clean
|
||||
if [ ! -z "$(git status --porcelain)" ]; then
|
||||
# If we get a fail here then the PR needs attention
|
||||
>&2 echo "Failed: JavaScript files are not up to date. Run 'rm -rf lib && npm run-script build' to update"
|
||||
git status
|
||||
|
||||
echo "### Transpiled JS diff" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```diff' >> $GITHUB_STEP_SUMMARY
|
||||
git diff --output="$RUNNER_TEMP/js.diff"
|
||||
cat "$RUNNER_TEMP/js.diff" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Reset bundled files to allow other checks to test for changes
|
||||
git checkout lib
|
||||
|
||||
# Fail this check
|
||||
exit 1
|
||||
fi
|
||||
echo "Success: JavaScript files are up to date"
|
||||
2
.github/workflows/update-bundle.yml
vendored
2
.github/workflows/update-bundle.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
||||
run: npm ci
|
||||
|
||||
- name: Update bundle
|
||||
uses: ./.github/actions/update-bundle
|
||||
run: npx tsx pr-checks/update-bundle.ts
|
||||
|
||||
- name: Set up CodeQL CLI from new bundle
|
||||
id: setup-codeql
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
|
||||
See the [releases page](https://github.com/github/codeql-action/releases) for the relevant changes to the CodeQL CLI and language packs.
|
||||
|
||||
## 4.39.0 - 15 Oct 2026
|
||||
## [UNRELEASED]
|
||||
|
||||
- _Breaking change_: Bump the minimum required CodeQL bundle version to 2.20.7. [#4205](https://github.com/github/codeql-action/pull/4205)
|
||||
- The `codeql-action/analyze` step now validates that a correct argument for the `checkout_path` input has been provided. The `checkout_path` input must be set to the path where the repository that is being analyzed is checked out at on the runner, if that path is something other than the workspace root. If the path does not point to a repository checkout or is different from the path that was used by `codeql-action/init`, then the CodeQL Action now produces a warning. [#4148](https://github.com/github/codeql-action/pull/4148)
|
||||
- Resolved an issue with third-party caching implementations that prevented caches from being restored correctly. [#4206](https://github.com/github/codeql-action/pull/4206)
|
||||
- The `codeql-action/start-proxy` step that is used in Code Scanning Default Setup now finds CodeQL releases using the same logic as `codeql-action/init`. This primarily benefits customers who are using GitHub instances other than `github.com` where the `codeql-action/start-proxy` step may previously have been unable to locate recent releases and ended up using outdated versions of the private registry authentication proxy as a result. [#4198](https://github.com/github/codeql-action/pull/4198)
|
||||
|
||||
## 4.38.3 - 08 Oct 2026
|
||||
|
||||
|
||||
1762
lib/entry-points.js
generated
1762
lib/entry-points.js
generated
File diff suppressed because it is too large
Load Diff
207
package-lock.json
generated
207
package-lock.json
generated
@@ -14,7 +14,7 @@
|
||||
"dependencies": {
|
||||
"@actions/artifact": "^5.0.3",
|
||||
"@actions/artifact-legacy": "npm:@actions/artifact@^1.1.2",
|
||||
"@actions/cache": "^5.3.0",
|
||||
"@actions/cache": "^5.2.0",
|
||||
"@actions/core": "^2.0.3",
|
||||
"@actions/exec": "^2.0.0",
|
||||
"@actions/github": "^8.0.1",
|
||||
@@ -23,14 +23,15 @@
|
||||
"@actions/io": "^2.0.0",
|
||||
"@actions/tool-cache": "^3.0.1",
|
||||
"@octokit/core": "^7.0.8",
|
||||
"@octokit/plugin-paginate-rest": "^16.0.0",
|
||||
"@octokit/plugin-paginate-rest": "^15.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^18.0.0",
|
||||
"@octokit/plugin-retry": "^8.1.1",
|
||||
"archiver": "^8.0.0",
|
||||
"follow-redirects": "^1.16.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"follow-redirects": "^1.16.0",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"js-yaml": "^5.4.3",
|
||||
"js-yaml": "^5.4.2",
|
||||
"jsonschema": "1.5.0",
|
||||
"long": "^5.3.2",
|
||||
"node-forge": "^1.4.0",
|
||||
@@ -57,14 +58,14 @@
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
"eslint-plugin-github": "^6.1.2",
|
||||
"eslint-plugin-import-x": "^4.17.1",
|
||||
"eslint-plugin-jsdoc": "^65.2.0",
|
||||
"eslint-plugin-jsdoc": "^65.0.1",
|
||||
"eslint-plugin-no-async-foreach": "^0.1.1",
|
||||
"glob": "^13.0.6",
|
||||
"globals": "^17.13.0",
|
||||
"globals": "^17.12.0",
|
||||
"nock": "^14.0.17",
|
||||
"sinon": "^22.1.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.71.1"
|
||||
"typescript-eslint": "^8.70.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
@@ -458,19 +459,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/cache": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/cache/-/cache-5.3.0.tgz",
|
||||
"integrity": "sha512-VqJBlQFt1lpp7op7o/Z0syWywX9cN8TxjFCPjHyGHmXCiczU5NII5ZdEpGpTelUnOGFG2bO7viUx9q3q7WR/Gg==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/cache/-/cache-5.2.0.tgz",
|
||||
"integrity": "sha512-1R1Oc8cuDNCygsIP7gLiKLGCymOw/k5FkGQkXZFcLz6/RWyMImkfP0dZX6kjA9SRAmANcKNocI2XrsIaZ1it8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^2.0.3",
|
||||
"@actions/core": "^2.0.0",
|
||||
"@actions/exec": "^2.0.0",
|
||||
"@actions/glob": "^0.5.1",
|
||||
"@actions/http-client": "^3.0.2",
|
||||
"@actions/io": "^2.0.0",
|
||||
"@azure/abort-controller": "^1.1.0",
|
||||
"@azure/core-rest-pipeline": "^1.25.0",
|
||||
"@azure/storage-blob": "^12.34.0",
|
||||
"@azure/core-rest-pipeline": "^1.22.0",
|
||||
"@azure/storage-blob": "^12.29.1",
|
||||
"@protobuf-ts/runtime-rpc": "^2.11.1",
|
||||
"semver": "^6.3.1"
|
||||
}
|
||||
@@ -2204,12 +2205,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/plugin-paginate-rest": {
|
||||
"version": "16.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-16.0.0.tgz",
|
||||
"integrity": "sha512-7RtUeGZypAK4EA4LrjTCnsLALNOoUcYh5vOnCNXpdIQVTQCPh4OFu7324iPqAls3KGgVnZktKUnNKZCOKyHrlA==",
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-15.0.0.tgz",
|
||||
"integrity": "sha512-lw9A9YL5s4VPj+VEx8uMxaxQm2YTgrPDkrLEuZuu18R8TK3oPcXF4K6t52nx6xn1RcogZC9i188sAr/4XYJaQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^18.0.0"
|
||||
"@octokit/types": "^17.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
@@ -2218,6 +2219,21 @@
|
||||
"@octokit/core": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": {
|
||||
"version": "28.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz",
|
||||
"integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": {
|
||||
"version": "17.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz",
|
||||
"integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^28.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/plugin-request-log": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz",
|
||||
@@ -2702,17 +2718,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.71.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.1.tgz",
|
||||
"integrity": "sha512-nNlBf6HuotqMgZvSkWD5hNdemiBIxQ2NmrlgfcUIeZRs+BbakjscCQRq/Hb74EFVnFiSWWdL12JruqwBy3ERDw==",
|
||||
"version": "8.70.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.1.tgz",
|
||||
"integrity": "sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.71.1",
|
||||
"@typescript-eslint/type-utils": "8.71.1",
|
||||
"@typescript-eslint/utils": "8.71.1",
|
||||
"@typescript-eslint/visitor-keys": "8.71.1",
|
||||
"@typescript-eslint/scope-manager": "8.70.1",
|
||||
"@typescript-eslint/type-utils": "8.70.1",
|
||||
"@typescript-eslint/utils": "8.70.1",
|
||||
"@typescript-eslint/visitor-keys": "8.70.1",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -2725,7 +2741,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.71.1",
|
||||
"@typescript-eslint/parser": "^8.70.1",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
@@ -2741,16 +2757,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.71.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.1.tgz",
|
||||
"integrity": "sha512-oM/5sAqz/l1v/zYK1qTOPrn2+JIrBGC1V6uNYucRO5gh6eT9vWJ/RhPbGE59byvZEUQ4VP6XkLBaS5Jti5jpvw==",
|
||||
"version": "8.70.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.1.tgz",
|
||||
"integrity": "sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.71.1",
|
||||
"@typescript-eslint/types": "8.71.1",
|
||||
"@typescript-eslint/typescript-estree": "8.71.1",
|
||||
"@typescript-eslint/visitor-keys": "8.71.1",
|
||||
"@typescript-eslint/scope-manager": "8.70.1",
|
||||
"@typescript-eslint/types": "8.70.1",
|
||||
"@typescript-eslint/typescript-estree": "8.70.1",
|
||||
"@typescript-eslint/visitor-keys": "8.70.1",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2784,14 +2800,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.71.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.1.tgz",
|
||||
"integrity": "sha512-Oijc9RUsohHjncg6iGi7gCtjnEjYt9FWv9u6ygK8uKcHK5eLjLAkqERQ9BaytbpceOFdk6kifPCS/zAZE+A99g==",
|
||||
"version": "8.70.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.1.tgz",
|
||||
"integrity": "sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.71.1",
|
||||
"@typescript-eslint/types": "^8.71.1",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.70.1",
|
||||
"@typescript-eslint/types": "^8.70.1",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2824,14 +2840,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.71.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.1.tgz",
|
||||
"integrity": "sha512-1YUDZXdTnXLlob03SFHBipdyMBsDVm7zoSz0ytTJ39dk5J1TgsTK03VlR/dLXG/RLLUrPcYbD4rp23BmHxHP0w==",
|
||||
"version": "8.70.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.1.tgz",
|
||||
"integrity": "sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.71.1",
|
||||
"@typescript-eslint/visitor-keys": "8.71.1"
|
||||
"@typescript-eslint/types": "8.70.1",
|
||||
"@typescript-eslint/visitor-keys": "8.70.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2842,9 +2858,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.71.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.1.tgz",
|
||||
"integrity": "sha512-oTvkml5SxXhgg+WWKhod5qWQICDXtOB+/Hi9VdPKFAANJ2b7YOgMffgvzXuqog7R6A/JtrzRf128dsxummyWhg==",
|
||||
"version": "8.70.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.1.tgz",
|
||||
"integrity": "sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2859,15 +2875,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.71.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.1.tgz",
|
||||
"integrity": "sha512-kH3t3jZCYv2PZSBEWPKTariLrCaoF1CLoPitYFBJOhAGEwWnSRAtGEifrfrpBa4K9JlwWNZotgA5w3/wLSk/FQ==",
|
||||
"version": "8.70.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.1.tgz",
|
||||
"integrity": "sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.71.1",
|
||||
"@typescript-eslint/typescript-estree": "8.71.1",
|
||||
"@typescript-eslint/utils": "8.71.1",
|
||||
"@typescript-eslint/types": "8.70.1",
|
||||
"@typescript-eslint/typescript-estree": "8.70.1",
|
||||
"@typescript-eslint/utils": "8.70.1",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -2902,9 +2918,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.71.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.1.tgz",
|
||||
"integrity": "sha512-ybYnPTUwg3VQQOFEiBPLs8Jxl1ry2vXdn1kVKguomBbvxpetdu3MKTNDqFNUrlG8ylP9MpZosmXMFYlGQHk1oQ==",
|
||||
"version": "8.70.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.1.tgz",
|
||||
"integrity": "sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2916,16 +2932,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.71.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.1.tgz",
|
||||
"integrity": "sha512-pSKSEK1JJpHs6KiKVtABCE4iJK5d7FLFzDXQNBQ2oRY7LEd3UwOt3TZemTgu9syTIf56dJWlTn/pdmrO9oKUXQ==",
|
||||
"version": "8.70.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.1.tgz",
|
||||
"integrity": "sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.71.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.71.1",
|
||||
"@typescript-eslint/types": "8.71.1",
|
||||
"@typescript-eslint/visitor-keys": "8.71.1",
|
||||
"@typescript-eslint/project-service": "8.70.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.70.1",
|
||||
"@typescript-eslint/types": "8.70.1",
|
||||
"@typescript-eslint/visitor-keys": "8.70.1",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -3001,16 +3017,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.71.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.1.tgz",
|
||||
"integrity": "sha512-CBjT6gfAz3DW2j9Q4moFnqt208Aq+506sVGXfpRcyAeqn0EpayL0mjdWoCO2t3RC7GewNrFxdeehuEaPg6DMzQ==",
|
||||
"version": "8.70.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.1.tgz",
|
||||
"integrity": "sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.71.1",
|
||||
"@typescript-eslint/types": "8.71.1",
|
||||
"@typescript-eslint/typescript-estree": "8.71.1"
|
||||
"@typescript-eslint/scope-manager": "8.70.1",
|
||||
"@typescript-eslint/types": "8.70.1",
|
||||
"@typescript-eslint/typescript-estree": "8.70.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -3025,13 +3041,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.71.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.1.tgz",
|
||||
"integrity": "sha512-0GxiUGqMU0qXJt58cBsiG3DP8lUmBaFo2vChtUX+CT7PnR3v/VGLLrzVP4K9r1lNiZXjHYefMBbPunplr09v6w==",
|
||||
"version": "8.70.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.1.tgz",
|
||||
"integrity": "sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.71.1",
|
||||
"@typescript-eslint/types": "8.70.1",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -5365,9 +5381,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-jsdoc": {
|
||||
"version": "65.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-65.2.0.tgz",
|
||||
"integrity": "sha512-IMZaNwdDa5otUQYGWN1pzywZLsGKswitvFFNhcO8IOsBfaWpWIDXYuZh2sYhe9MNyKN9hz0YryOFaKdpbTP5Bw==",
|
||||
"version": "65.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-65.0.1.tgz",
|
||||
"integrity": "sha512-7IvB+ZS71WCw5tGKh6f6D5k55qtXKdrGTnIpQTBKWdpyZ5IfhGHfh0i6ArWlCJ4JEtz22YRk5Bqu1MvbOlpMKQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
@@ -5801,7 +5817,6 @@
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-diff": {
|
||||
@@ -5992,9 +6007,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz",
|
||||
"integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -6278,9 +6293,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "17.13.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz",
|
||||
"integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==",
|
||||
"version": "17.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz",
|
||||
"integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -7121,9 +7136,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "5.4.3",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.3.tgz",
|
||||
"integrity": "sha512-yzzNtgczwgoVHNigCyKTXcQJU4Y8t/ikU4UxXW0BvrLfHTGWZhsNcOT2s/a7vn81/4KqVnNmHjaUEPlvWnNwVw==",
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.2.tgz",
|
||||
"integrity": "sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -7479,9 +7494,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-from-markdown": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.1.0.tgz",
|
||||
"integrity": "sha512-KRLgmPaP2H1ul2bpoJkOS+9pAcDAAj9Nu1mqLnh8NLeJG5tlvj42TH80PIGBpROWlRXLk0lDrWNuZCFy/tsB4A==",
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
|
||||
"integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
@@ -9981,16 +9996,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.71.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.1.tgz",
|
||||
"integrity": "sha512-oiNVPC3/NbV1FpaHU07l8O6ynqG9v4PAiW2WvTrAmZdR0f2HI/gE668ElMLCK7Xbsu5k2fK5C4fXNiq05j7/nQ==",
|
||||
"version": "8.70.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.1.tgz",
|
||||
"integrity": "sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.71.1",
|
||||
"@typescript-eslint/parser": "8.71.1",
|
||||
"@typescript-eslint/typescript-estree": "8.71.1",
|
||||
"@typescript-eslint/utils": "8.71.1"
|
||||
"@typescript-eslint/eslint-plugin": "8.70.1",
|
||||
"@typescript-eslint/parser": "8.70.1",
|
||||
"@typescript-eslint/typescript-estree": "8.70.1",
|
||||
"@typescript-eslint/utils": "8.70.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -10485,10 +10500,10 @@
|
||||
"@actions/core": "^2.0.3",
|
||||
"@actions/github": "^8.0.1",
|
||||
"@octokit/core": "^7.0.8",
|
||||
"@octokit/plugin-paginate-rest": ">=16.0.0",
|
||||
"@octokit/plugin-paginate-rest": ">=15.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^18.0.0",
|
||||
"lite-matter": "^0.1.2",
|
||||
"mdast-util-from-markdown": "^2.1.0",
|
||||
"mdast-util-from-markdown": "^2.0.3",
|
||||
"semver": "^7.8.5",
|
||||
"yaml": "^2.9.1"
|
||||
},
|
||||
|
||||
15
package.json
15
package.json
@@ -22,7 +22,7 @@
|
||||
"dependencies": {
|
||||
"@actions/artifact": "^5.0.3",
|
||||
"@actions/artifact-legacy": "npm:@actions/artifact@^1.1.2",
|
||||
"@actions/cache": "^5.3.0",
|
||||
"@actions/cache": "^5.2.0",
|
||||
"@actions/core": "^2.0.3",
|
||||
"@actions/exec": "^2.0.0",
|
||||
"@actions/github": "^8.0.1",
|
||||
@@ -31,14 +31,15 @@
|
||||
"@actions/io": "^2.0.0",
|
||||
"@actions/tool-cache": "^3.0.1",
|
||||
"@octokit/core": "^7.0.8",
|
||||
"@octokit/plugin-paginate-rest": "^16.0.0",
|
||||
"@octokit/plugin-paginate-rest": "^15.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^18.0.0",
|
||||
"@octokit/plugin-retry": "^8.1.1",
|
||||
"archiver": "^8.0.0",
|
||||
"follow-redirects": "^1.16.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"follow-redirects": "^1.16.0",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"js-yaml": "^5.4.3",
|
||||
"js-yaml": "^5.4.2",
|
||||
"jsonschema": "1.5.0",
|
||||
"long": "^5.3.2",
|
||||
"node-forge": "^1.4.0",
|
||||
@@ -65,14 +66,14 @@
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
"eslint-plugin-github": "^6.1.2",
|
||||
"eslint-plugin-import-x": "^4.17.1",
|
||||
"eslint-plugin-jsdoc": "^65.2.0",
|
||||
"eslint-plugin-jsdoc": "^65.0.1",
|
||||
"eslint-plugin-no-async-foreach": "^0.1.1",
|
||||
"glob": "^13.0.6",
|
||||
"globals": "^17.13.0",
|
||||
"globals": "^17.12.0",
|
||||
"nock": "^14.0.17",
|
||||
"sinon": "^22.1.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.71.1"
|
||||
"typescript-eslint": "^8.70.1"
|
||||
},
|
||||
"overrides": {
|
||||
"@actions/tool-cache": {
|
||||
|
||||
@@ -50,28 +50,6 @@ function getChangenotes(): ChangenoteFile[] {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the '[UNRELEASED]' section of CHANGELOG.md with the contents
|
||||
* of the changenotes in {@link CHANGENOTES_DIR} and then deletes those changenotes.
|
||||
* @param contents The contents of CHANGELOG.md
|
||||
* @returns The updated contents of CHANGELOG.md
|
||||
*/
|
||||
export function transferChangenotesToChangelog(contents: string): string {
|
||||
const changenotes = getChangenotes();
|
||||
const changenoteBodies = changenotes.map((c) => c.content.trim());
|
||||
const changenotePaths = changenotes.map((c) => c.absolutePath);
|
||||
|
||||
const changelog = parseChangelog(contents);
|
||||
addBodyLinesToUnreleasedSection(changelog, changenoteBodies);
|
||||
const updatedChangelog = renderChangelog(changelog);
|
||||
|
||||
for (const p of changenotePaths) {
|
||||
fs.unlinkSync(p);
|
||||
}
|
||||
|
||||
return updatedChangelog;
|
||||
}
|
||||
|
||||
const entryPoint = process.argv[1];
|
||||
if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) {
|
||||
try {
|
||||
@@ -113,13 +91,26 @@ function usage(): ExitCode {
|
||||
|
||||
function assemble(): ExitCode {
|
||||
try {
|
||||
const changenotes = getChangenotes();
|
||||
const changenoteBodies = changenotes.map((c) => c.content);
|
||||
const changenotePaths = changenotes.map((c) => c.absolutePath);
|
||||
|
||||
withChangelog((contents) => {
|
||||
return transferChangenotesToChangelog(contents);
|
||||
const changelog = parseChangelog(contents);
|
||||
addBodyLinesToUnreleasedSection(changelog, changenoteBodies);
|
||||
return renderChangelog(changelog);
|
||||
}, {});
|
||||
|
||||
// Delete changenotes only after successful processing.
|
||||
for (const p of changenotePaths) {
|
||||
fs.unlinkSync(p);
|
||||
}
|
||||
|
||||
return ExitCode.Success;
|
||||
} catch (e) {
|
||||
console.error("Failed to assemble changenotes to 'CHANGELOG.md'", e);
|
||||
}
|
||||
|
||||
return ExitCode.Failure;
|
||||
}
|
||||
|
||||
|
||||
68
pr-checks/check-cs-config.test.ts
Normal file
68
pr-checks/check-cs-config.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Tests for `check-cs-config.ts`.
|
||||
*/
|
||||
|
||||
import * as assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import type { UserConfig } from "../src/config/db-config";
|
||||
|
||||
import { checkConfiguration } from "./check-cs-config";
|
||||
|
||||
describe("checkConfiguration", async () => {
|
||||
await it("passes when actual and expected configs match", () => {
|
||||
const actual: UserConfig = { name: "test-config", paths: ["src"] };
|
||||
const expected = JSON.stringify(actual);
|
||||
assert.doesNotThrow(() => checkConfiguration(actual, expected));
|
||||
});
|
||||
|
||||
await it("passes when queries arrays match after sorting", () => {
|
||||
const actual: UserConfig = { paths: ["b", "a", "c"] };
|
||||
const expected = JSON.stringify(actual);
|
||||
assert.doesNotThrow(() => checkConfiguration(actual, expected));
|
||||
});
|
||||
|
||||
await it("throws when actual config does not match expected", () => {
|
||||
const actual: UserConfig = { name: "actual-name" };
|
||||
const expected = JSON.stringify({
|
||||
name: "expected-name",
|
||||
} satisfies UserConfig);
|
||||
assert.throws(() => checkConfiguration(actual, expected), {
|
||||
message: /Expected configuration does not match actual configuration/,
|
||||
});
|
||||
});
|
||||
|
||||
await it("throws when expected contents are empty", () => {
|
||||
assert.throws(() => checkConfiguration({}, ""), {
|
||||
message: /No expected configuration provided/,
|
||||
});
|
||||
});
|
||||
|
||||
await it("throws when expected contents are only whitespace", () => {
|
||||
assert.throws(() => checkConfiguration({}, " "), {
|
||||
message: /No expected configuration provided/,
|
||||
});
|
||||
});
|
||||
|
||||
await it("passes with complex config", () => {
|
||||
const actual: UserConfig = {
|
||||
name: "complex",
|
||||
"disable-default-queries": true,
|
||||
paths: ["src", "lib"],
|
||||
"paths-ignore": ["test"],
|
||||
"threat-models": ["remote"],
|
||||
};
|
||||
const expected = JSON.stringify(actual);
|
||||
assert.doesNotThrow(() => checkConfiguration(actual, expected));
|
||||
});
|
||||
|
||||
await it("trims whitespace from expected contents before parsing", () => {
|
||||
const actual: UserConfig = { name: "trimmed" };
|
||||
const expected = ` ${JSON.stringify(actual)} `;
|
||||
assert.doesNotThrow(() => checkConfiguration(actual, expected));
|
||||
});
|
||||
|
||||
await it("passes when both configs are empty objects", () => {
|
||||
assert.doesNotThrow(() => checkConfiguration({}, "{}"));
|
||||
});
|
||||
});
|
||||
100
pr-checks/check-cs-config.ts
Executable file
100
pr-checks/check-cs-config.ts
Executable file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Checks the code scanning configuration file generated by the
|
||||
* action to ensure it contains the expected contents
|
||||
*/
|
||||
|
||||
import * as assert from "node:assert";
|
||||
import * as fs from "node:fs";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import * as yaml from "yaml";
|
||||
|
||||
import type { UserConfig } from "../src/config/db-config";
|
||||
|
||||
import { getErrorMessage } from "./util";
|
||||
|
||||
function sortConfigArrays(config: UserConfig) {
|
||||
for (const key of Object.keys(config)) {
|
||||
const value = config[key];
|
||||
if (key === "queries" && Array.isArray(value)) {
|
||||
config[key] = value.sort();
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
function loadActualConfig(configPath: string) {
|
||||
if (!fs.existsSync(configPath)) {
|
||||
throw new Error("No configuration file found");
|
||||
} else {
|
||||
const rawActualConfig = fs.readFileSync(configPath, "utf8");
|
||||
core.startGroup("Actual generated user config");
|
||||
core.info(rawActualConfig);
|
||||
core.endGroup();
|
||||
|
||||
return yaml.parse(rawActualConfig) as UserConfig;
|
||||
}
|
||||
}
|
||||
|
||||
export function checkConfiguration(
|
||||
actualConfig: UserConfig,
|
||||
expectedContents: string,
|
||||
) {
|
||||
const rawExpectedConfig = expectedContents.trim();
|
||||
if (!rawExpectedConfig) {
|
||||
throw new Error("No expected configuration provided");
|
||||
}
|
||||
|
||||
const expectedConfig = JSON.parse(rawExpectedConfig) as UserConfig;
|
||||
|
||||
core.startGroup("Expected generated user config");
|
||||
core.info(yaml.stringify(expectedConfig));
|
||||
core.endGroup();
|
||||
|
||||
assert.deepStrictEqual(
|
||||
sortConfigArrays(actualConfig),
|
||||
sortConfigArrays(expectedConfig),
|
||||
"Expected configuration does not match actual configuration",
|
||||
);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
// The path of the configuration file to check.
|
||||
file: {
|
||||
type: "string",
|
||||
},
|
||||
// The expected contents of the file.
|
||||
"expected-contents": {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
strict: true,
|
||||
});
|
||||
|
||||
if (values.file === undefined) {
|
||||
throw new Error("The '--file' input is required.");
|
||||
}
|
||||
if (values["expected-contents"] === undefined) {
|
||||
throw new Error("The '--expected-contents' input is required.");
|
||||
}
|
||||
|
||||
const actualConfig = loadActualConfig(values.file);
|
||||
|
||||
try {
|
||||
checkConfiguration(actualConfig, values["expected-contents"]);
|
||||
} catch (err) {
|
||||
core.error(getErrorMessage(err));
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
process.exit(main());
|
||||
}
|
||||
67
pr-checks/check-js.ts
Executable file
67
pr-checks/check-js.ts
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { runCommand, runGit } from "./command";
|
||||
import { LIB_ROOT, PR_CHECKS_DIR, REPO_ROOT } from "./config";
|
||||
import { getErrorMessage } from "./util";
|
||||
|
||||
function main() {
|
||||
// Sanity check that repo is clean to start with
|
||||
try {
|
||||
runGit(["diff", "--exit-code"], { allowNonZeroExitCode: false });
|
||||
console.info("Repository is clean.");
|
||||
} catch (err) {
|
||||
// If we get a fail here then this workflow needs attention...
|
||||
console.error(getErrorMessage(err));
|
||||
console.error("Failed: Repo should be clean before testing!");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Wipe the lib directory in case there are extra unnecessary files in there
|
||||
console.info(`Removing ${LIB_ROOT}...`);
|
||||
fs.rmSync(LIB_ROOT, { recursive: true, force: true });
|
||||
|
||||
// Generate the JavaScript files
|
||||
runCommand("npm", ["run", "build"], { execOptions: { shell: true } });
|
||||
|
||||
// Check that repo is still clean
|
||||
try {
|
||||
runGit(["diff", "--exit-code"], { allowNonZeroExitCode: false });
|
||||
console.info("Repository is clean.");
|
||||
} catch (err) {
|
||||
// If we get a fail here then the PR needs attention
|
||||
console.error(getErrorMessage(err));
|
||||
console.error("Failed: JavaScript files are not up to date.");
|
||||
console.error("Run 'rm -rf lib && npm run build' to update.");
|
||||
|
||||
const diffFile = path.join(
|
||||
process.env["RUNNER_TEMP"] ?? PR_CHECKS_DIR,
|
||||
"js.diff",
|
||||
);
|
||||
runCommand("git", ["status"]);
|
||||
runCommand("git", ["diff", `--output=${diffFile}`], {
|
||||
execOptions: { cwd: REPO_ROOT },
|
||||
});
|
||||
|
||||
core.summary.addHeading("Transpiled JS diff", 3);
|
||||
core.summary.addCodeBlock(fs.readFileSync(diffFile, "utf-8"), "diff");
|
||||
|
||||
fs.rmSync(diffFile);
|
||||
|
||||
// Reset bundled files to allow other checks to test for changes
|
||||
runCommand("git", ["checkout", "lib"]);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
console.info("Success: JavaScript files are up to date");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
process.exit(main());
|
||||
}
|
||||
159
pr-checks/check-sarif.test.ts
Normal file
159
pr-checks/check-sarif.test.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Tests for `check-sarif.ts`.
|
||||
*/
|
||||
|
||||
import * as assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import type { Log } from "sarif";
|
||||
|
||||
import { checkSarif } from "./check-sarif";
|
||||
|
||||
/** Builds a minimal SARIF Log with the given rule IDs spread across extensions. */
|
||||
function buildSarifLog(ruleIds: string[]): Log {
|
||||
return {
|
||||
version: "2.1.0",
|
||||
$schema:
|
||||
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
|
||||
runs: [
|
||||
{
|
||||
tool: {
|
||||
driver: { name: "CodeQL" },
|
||||
extensions: [
|
||||
{
|
||||
name: "test-pack",
|
||||
rules: ruleIds.map((id) => ({ id })),
|
||||
},
|
||||
],
|
||||
},
|
||||
results: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe("checkSarif", async () => {
|
||||
await it("returns 0 when all expected queries ran and no unexpected queries ran", () => {
|
||||
const sarif = buildSarifLog(["js/sql-injection", "js/xss"]);
|
||||
const exitCode = checkSarif(sarif, {
|
||||
sarifFile: "test.sarif",
|
||||
queriesRun: "js/sql-injection, js/xss",
|
||||
queriesNotRun: "js/hardcoded-credentials",
|
||||
});
|
||||
assert.equal(exitCode, 0);
|
||||
});
|
||||
|
||||
await it("returns -2 when an expected query did not run", () => {
|
||||
const sarif = buildSarifLog(["js/sql-injection"]);
|
||||
const exitCode = checkSarif(sarif, {
|
||||
sarifFile: "test.sarif",
|
||||
queriesRun: "js/sql-injection, js/xss",
|
||||
queriesNotRun: "",
|
||||
});
|
||||
assert.equal(exitCode, -2);
|
||||
});
|
||||
|
||||
await it("returns -2 when an unexpected query ran", () => {
|
||||
const sarif = buildSarifLog(["js/sql-injection", "js/xss"]);
|
||||
const exitCode = checkSarif(sarif, {
|
||||
sarifFile: "test.sarif",
|
||||
queriesRun: "js/sql-injection",
|
||||
queriesNotRun: "js/xss",
|
||||
});
|
||||
assert.equal(exitCode, -2);
|
||||
});
|
||||
|
||||
await it("handles empty queries-run and queries-not-run inputs", () => {
|
||||
const sarif = buildSarifLog(["js/sql-injection"]);
|
||||
const exitCode = checkSarif(sarif, {
|
||||
sarifFile: "test.sarif",
|
||||
queriesRun: "",
|
||||
queriesNotRun: "",
|
||||
});
|
||||
assert.equal(exitCode, 0);
|
||||
});
|
||||
|
||||
await it("handles multiple extensions with rules", () => {
|
||||
const sarif: Log = {
|
||||
version: "2.1.0",
|
||||
$schema:
|
||||
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
|
||||
runs: [
|
||||
{
|
||||
tool: {
|
||||
driver: { name: "CodeQL" },
|
||||
extensions: [
|
||||
{
|
||||
name: "pack-a",
|
||||
rules: [{ id: "js/sql-injection" }],
|
||||
},
|
||||
{
|
||||
name: "pack-b",
|
||||
rules: [{ id: "js/xss" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
results: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
const exitCode = checkSarif(sarif, {
|
||||
sarifFile: "test.sarif",
|
||||
queriesRun: "js/sql-injection, js/xss",
|
||||
queriesNotRun: "",
|
||||
});
|
||||
assert.equal(exitCode, 0);
|
||||
});
|
||||
|
||||
await it("handles extensions with no rules", () => {
|
||||
const sarif: Log = {
|
||||
version: "2.1.0",
|
||||
$schema:
|
||||
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
|
||||
runs: [
|
||||
{
|
||||
tool: {
|
||||
driver: { name: "CodeQL" },
|
||||
extensions: [
|
||||
{ name: "empty-pack" },
|
||||
{
|
||||
name: "pack-with-rules",
|
||||
rules: [{ id: "js/xss" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
results: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
const exitCode = checkSarif(sarif, {
|
||||
sarifFile: "test.sarif",
|
||||
queriesRun: "js/xss",
|
||||
queriesNotRun: "js/sql-injection",
|
||||
});
|
||||
assert.equal(exitCode, 0);
|
||||
});
|
||||
|
||||
await it("throws when tool extensions are undefined", () => {
|
||||
const sarif: Log = {
|
||||
version: "2.1.0",
|
||||
$schema:
|
||||
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
|
||||
runs: [
|
||||
{
|
||||
tool: { driver: { name: "CodeQL" } },
|
||||
results: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
assert.throws(
|
||||
() =>
|
||||
checkSarif(sarif, {
|
||||
sarifFile: "test.sarif",
|
||||
queriesRun: "js/xss",
|
||||
queriesNotRun: "",
|
||||
}),
|
||||
{ message: /Couldn't find tool extensions/ },
|
||||
);
|
||||
});
|
||||
});
|
||||
125
pr-checks/check-sarif.ts
Normal file
125
pr-checks/check-sarif.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/** Checks a SARIF file to see if certain queries were run and others were not run. */
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import type { ReportingDescriptor, Log } from "sarif";
|
||||
|
||||
import { getErrorMessage } from "./util";
|
||||
|
||||
type Options = { sarifFile: string; queriesRun: string; queriesNotRun: string };
|
||||
|
||||
function getOptions(): Options {
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
// The path of the SARIF file to check.
|
||||
"sarif-file": {
|
||||
type: "string",
|
||||
},
|
||||
// The query ids to check are present.
|
||||
"queries-run": {
|
||||
type: "string",
|
||||
},
|
||||
// The query ids to check are absent.
|
||||
"queries-not-run": {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
strict: true,
|
||||
});
|
||||
|
||||
if (values["sarif-file"] === undefined) {
|
||||
throw new Error("The '--sarif-file' input is required.");
|
||||
}
|
||||
if (values["queries-run"] === undefined) {
|
||||
throw new Error("The '--queries-run' input is required.");
|
||||
}
|
||||
if (values["queries-not-run"] === undefined) {
|
||||
throw new Error("The '--queries-not-run' input is required.");
|
||||
}
|
||||
|
||||
return {
|
||||
sarifFile: values["sarif-file"],
|
||||
queriesRun: values["queries-run"],
|
||||
queriesNotRun: values["queries-not-run"],
|
||||
};
|
||||
}
|
||||
|
||||
function parseQueryIdsInput(queriesRun: string): string[] {
|
||||
return queriesRun
|
||||
.split(",")
|
||||
.map((q) => q.trim())
|
||||
.filter((q) => q.length > 0);
|
||||
}
|
||||
|
||||
export function checkSarif(sarif: Log, options: Options) {
|
||||
if (sarif.runs[0].tool.extensions === undefined) {
|
||||
throw new Error(`Couldn't find tool extensions in the SARIF file.`);
|
||||
}
|
||||
|
||||
let exitCode = 0;
|
||||
|
||||
// Extract the rule ids from the SARIF file.
|
||||
const rules: ReportingDescriptor[] = sarif.runs[0].tool.extensions.flatMap(
|
||||
(ext) => ext.rules || [],
|
||||
);
|
||||
const ruleIds: string[] = rules.map((rule) => rule.id);
|
||||
|
||||
// Check that all the expected queries ran
|
||||
const expectedQueriesRun = parseQueryIdsInput(options.queriesRun);
|
||||
const queriesThatShouldHaveRunButDidNot = expectedQueriesRun.filter(
|
||||
(queryId) => !ruleIds.includes(queryId),
|
||||
);
|
||||
|
||||
if (queriesThatShouldHaveRunButDidNot.length > 0) {
|
||||
core.error(
|
||||
`The following queries were expected to run but did not: ${queriesThatShouldHaveRunButDidNot.join(", ")}`,
|
||||
);
|
||||
exitCode = -2;
|
||||
}
|
||||
|
||||
// Check that all the unexpected queries did not run
|
||||
const expectedQueriesNotRun = parseQueryIdsInput(options.queriesNotRun);
|
||||
|
||||
const queriesThatShouldNotHaveRunButDid = expectedQueriesNotRun.filter(
|
||||
(queryId) => ruleIds.includes(queryId),
|
||||
);
|
||||
|
||||
if (queriesThatShouldNotHaveRunButDid.length > 0) {
|
||||
core.error(
|
||||
`The following queries were NOT expected to have run but did: ${queriesThatShouldNotHaveRunButDid.join(", ")}`,
|
||||
);
|
||||
exitCode = -2;
|
||||
}
|
||||
|
||||
core.startGroup("All queries that ran");
|
||||
for (const rule of rules) {
|
||||
core.info(`${rule.id}: ${rule.properties?.name || rule.name}`);
|
||||
}
|
||||
core.endGroup();
|
||||
|
||||
core.startGroup("Full SARIF");
|
||||
core.info(JSON.stringify(sarif, null, 2));
|
||||
core.endGroup();
|
||||
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
function main() {
|
||||
try {
|
||||
const options = getOptions();
|
||||
const sarif: Log = JSON.parse(fs.readFileSync(options.sarifFile, "utf8"));
|
||||
|
||||
return checkSarif(sarif, options);
|
||||
} catch (err) {
|
||||
core.error(`Failed to check SARIF file: ${getErrorMessage(err)}`);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
process.exit(main());
|
||||
}
|
||||
81
pr-checks/command.ts
Normal file
81
pr-checks/command.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { execFileSync, ExecFileSyncOptions } from "node:child_process";
|
||||
|
||||
import { DryRunOption, REPO_ROOT } from "./config";
|
||||
|
||||
/** Options for {@link runCommand}. */
|
||||
export interface RunCommandOptions extends DryRunOption {
|
||||
/** Options for `execFileSync`. */
|
||||
execOptions?: ExecFileSyncOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a command, streaming output to the console by default.
|
||||
*
|
||||
* @param command The name of the command to run.
|
||||
* @param args The arguments for the command.
|
||||
* @throws When the process exits with a non-zero exit code.
|
||||
* @param options How to run the command.
|
||||
*/
|
||||
export function runCommand(
|
||||
command: string,
|
||||
args: string[],
|
||||
options?: RunCommandOptions,
|
||||
) {
|
||||
if (!options?.dryRun) {
|
||||
console.log(`Running \`${command} ${args.join(" ")}\`.`);
|
||||
return execFileSync(command, args, {
|
||||
stdio: "inherit",
|
||||
cwd: REPO_ROOT,
|
||||
...options?.execOptions,
|
||||
});
|
||||
} else {
|
||||
console.info(
|
||||
`[DRY RUN] Would have executed '${command} ${args.join(" ")}'`,
|
||||
);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/** Options for {@link runGit}. */
|
||||
export interface RunGitOptions extends DryRunOption {
|
||||
/** When true, non-zero exit codes will not throw. */
|
||||
allowNonZeroExitCode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs `git` with the given `args` and returns the stdout.
|
||||
*
|
||||
* @param args - Arguments to pass to `git`.
|
||||
* @param options - Optional settings.
|
||||
* @throws If `git` does not exit successfully, unless
|
||||
* `options.allowNonZeroExitCode` is `true`.
|
||||
* @returns The trimmed stdout output.
|
||||
*/
|
||||
export function runGit(args: string[], options?: RunGitOptions): string {
|
||||
const execOptions: ExecFileSyncOptions = {
|
||||
encoding: "utf8",
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
};
|
||||
|
||||
try {
|
||||
const result = runCommand("git", args, {
|
||||
dryRun: options?.dryRun,
|
||||
execOptions,
|
||||
}) as string;
|
||||
return result.trimEnd();
|
||||
} catch (error: unknown) {
|
||||
if (options?.allowNonZeroExitCode) {
|
||||
// execFileSync throws an object with `stdout` when the process exits
|
||||
// with a non-zero code.
|
||||
const execError = error as { stdout?: Buffer | string };
|
||||
if (typeof execError.stdout === "string") {
|
||||
return execError.stdout.trimEnd();
|
||||
}
|
||||
if (Buffer.isBuffer(execError.stdout)) {
|
||||
return execError.stdout.toString("utf8").trimEnd();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,13 @@ export const CHANGENOTES_DIR = path.join(REPO_ROOT, "unreleased-change-notes");
|
||||
export const BUNDLE_METADATA_FILE = path.join(REPO_ROOT, "meta.json");
|
||||
|
||||
/** The `src` directory. */
|
||||
const SOURCE_ROOT = path.join(REPO_ROOT, "src");
|
||||
export const SOURCE_ROOT = path.join(REPO_ROOT, "src");
|
||||
|
||||
/** The `src` directory. */
|
||||
export const LIB_ROOT = path.join(REPO_ROOT, "lib");
|
||||
|
||||
/** The path to `defaults.json`. */
|
||||
export const DEFAULTS_FILE = path.join(SOURCE_ROOT, "defaults.json");
|
||||
|
||||
/** The path to the built-in languages file. */
|
||||
export const BUILTIN_LANGUAGES_FILE = path.join(
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
"@actions/core": "^2.0.3",
|
||||
"@actions/github": "^8.0.1",
|
||||
"@octokit/core": "^7.0.8",
|
||||
"@octokit/plugin-paginate-rest": ">=16.0.0",
|
||||
"@octokit/plugin-paginate-rest": ">=15.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^18.0.0",
|
||||
"lite-matter": "^0.1.2",
|
||||
"mdast-util-from-markdown": "^2.1.0",
|
||||
"mdast-util-from-markdown": "^2.0.3",
|
||||
"semver": "^7.8.5",
|
||||
"yaml": "^2.9.1"
|
||||
},
|
||||
|
||||
58
pr-checks/update-bundle.test.ts
Normal file
58
pr-checks/update-bundle.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Tests for the update-bundle.ts script.
|
||||
*/
|
||||
|
||||
import * as assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { Defaults, getNewDefaults } from "./update-bundle";
|
||||
|
||||
const testDefaults: Defaults = {
|
||||
bundleVersion: "codeql-bundle-v2.26.2",
|
||||
cliVersion: "2.26.2",
|
||||
priorBundleVersion: "codeql-bundle-v2.26.1",
|
||||
priorCliVersion: "2.26.1",
|
||||
};
|
||||
|
||||
describe("getNewDefaults", async () => {
|
||||
await it("throws if there is no cli-version-*.txt asset", async () => {
|
||||
assert.throws(
|
||||
() => getNewDefaults({ tag_name: "foo", assets: [] }, testDefaults),
|
||||
{ message: "Failed to find the CodeQL CLI version for release foo." },
|
||||
);
|
||||
});
|
||||
|
||||
await it("throws if there are multiple cli-version-*.txt assets", async () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
getNewDefaults(
|
||||
{
|
||||
tag_name: "foo",
|
||||
assets: [
|
||||
{ name: "cli-version-foo.txt" },
|
||||
{ name: "cli-version-bar.txt" },
|
||||
],
|
||||
},
|
||||
testDefaults,
|
||||
),
|
||||
{ message: "Release foo has multiple CLI version marker files." },
|
||||
);
|
||||
});
|
||||
|
||||
await it("finds the new bundle info", async () => {
|
||||
const newDefaults = getNewDefaults(
|
||||
{
|
||||
tag_name: "foo",
|
||||
assets: [{ name: "cli-version-1.2.3.txt" }],
|
||||
},
|
||||
testDefaults,
|
||||
);
|
||||
|
||||
assert.deepEqual(newDefaults, {
|
||||
bundleVersion: "foo",
|
||||
cliVersion: "1.2.3",
|
||||
priorBundleVersion: testDefaults.bundleVersion,
|
||||
priorCliVersion: testDefaults.cliVersion,
|
||||
} satisfies Defaults);
|
||||
});
|
||||
});
|
||||
93
pr-checks/update-bundle.ts
Executable file
93
pr-checks/update-bundle.ts
Executable file
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/** Updates 'src/defaults.json' to point to a new CodeQL bundle release. */
|
||||
|
||||
import * as fs from "fs";
|
||||
|
||||
import * as github from "@actions/github";
|
||||
|
||||
import * as defaults from "../src/defaults.json";
|
||||
|
||||
import { DEFAULTS_FILE } from "./config";
|
||||
|
||||
interface BundleInfo {
|
||||
bundleVersion: string;
|
||||
cliVersion: string;
|
||||
}
|
||||
|
||||
export type Defaults = typeof defaults;
|
||||
|
||||
interface Release {
|
||||
tag_name: string;
|
||||
assets: Array<{
|
||||
name: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
function getCodeQLCliVersionForRelease(release: Release): string {
|
||||
// We do not currently tag CodeQL bundles based on the CLI version they contain.
|
||||
// Instead, we use a marker file `cli-version-<version>.txt` to record the CLI version.
|
||||
// This marker file is uploaded as a release asset for all new CodeQL bundles.
|
||||
const cliVersionsFromMarkerFiles = release.assets
|
||||
.map((asset) => asset.name.match(/cli-version-(.*)\.txt/)?.[1])
|
||||
.filter((v) => v)
|
||||
.map((v) => v as string);
|
||||
if (cliVersionsFromMarkerFiles.length > 1) {
|
||||
throw new Error(
|
||||
`Release ${release.tag_name} has multiple CLI version marker files.`,
|
||||
);
|
||||
} else if (cliVersionsFromMarkerFiles.length === 0) {
|
||||
throw new Error(
|
||||
`Failed to find the CodeQL CLI version for release ${release.tag_name}.`,
|
||||
);
|
||||
}
|
||||
return cliVersionsFromMarkerFiles[0];
|
||||
}
|
||||
|
||||
function getBundleInfoFromRelease(release: Release): BundleInfo {
|
||||
return {
|
||||
bundleVersion: release.tag_name,
|
||||
cliVersion: getCodeQLCliVersionForRelease(release),
|
||||
};
|
||||
}
|
||||
|
||||
export function getNewDefaults(
|
||||
release: Release,
|
||||
currentDefaults: Defaults,
|
||||
): Defaults {
|
||||
console.log(
|
||||
"Updating default bundle as a result of the following release: " +
|
||||
`${JSON.stringify(release)}.`,
|
||||
);
|
||||
|
||||
const bundleInfo = getBundleInfoFromRelease(release);
|
||||
|
||||
return {
|
||||
bundleVersion: bundleInfo.bundleVersion,
|
||||
cliVersion: bundleInfo.cliVersion,
|
||||
priorBundleVersion: currentDefaults.bundleVersion,
|
||||
priorCliVersion: currentDefaults.cliVersion,
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
const release: Release = github.context.payload.release;
|
||||
|
||||
if (release === undefined) {
|
||||
console.error(`Release payload is undefined.`);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const previousDefaults = defaults;
|
||||
const newDefaults = getNewDefaults(release, previousDefaults);
|
||||
|
||||
// Update the source file in the repository. Calling workflows should subsequently rebuild
|
||||
// the Action to update `lib/defaults.json`.
|
||||
fs.writeFileSync(DEFAULTS_FILE, `${JSON.stringify(newDefaults, null, 2)}\n`);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
process.exit(main());
|
||||
}
|
||||
@@ -18,18 +18,12 @@
|
||||
* [--dry-run]
|
||||
*/
|
||||
|
||||
import { execFileSync, type ExecFileSyncOptions } from "node:child_process";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
import { type ApiClient, getApiClient } from "./api-client";
|
||||
import * as changelog from "./changelog";
|
||||
import { transferChangenotesToChangelog } from "./changenotes";
|
||||
import {
|
||||
CHANGELOG_FILE,
|
||||
CHANGENOTES_DIR,
|
||||
DryRunOption,
|
||||
REPO_ROOT,
|
||||
} from "./config";
|
||||
import { runCommand, runGit } from "./command";
|
||||
import {
|
||||
getCurrentVersion,
|
||||
replaceVersionInPackageJson,
|
||||
@@ -71,84 +65,6 @@ export function getGitHubToken(): string {
|
||||
throw new Error("Missing GitHub token. Set GITHUB_TOKEN or GH_TOKEN.");
|
||||
}
|
||||
|
||||
/** Options for {@link runCommand}. */
|
||||
export interface RunCommandOptions extends DryRunOption {
|
||||
/** Options for `execFileSync`. */
|
||||
execOptions?: ExecFileSyncOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a command, streaming output to the console by default.
|
||||
*
|
||||
* @param command The name of the command to run.
|
||||
* @param args The arguments for the command.
|
||||
* @throws When the process exits with a non-zero exit code.
|
||||
* @param options How to run the command.
|
||||
*/
|
||||
export function runCommand(
|
||||
command: string,
|
||||
args: string[],
|
||||
options?: RunCommandOptions,
|
||||
) {
|
||||
if (!options?.dryRun) {
|
||||
console.log(`Running \`${command} ${args.join(" ")}\`.`);
|
||||
return execFileSync(command, args, {
|
||||
stdio: "inherit",
|
||||
cwd: REPO_ROOT,
|
||||
...options?.execOptions,
|
||||
});
|
||||
} else {
|
||||
console.info(
|
||||
`[DRY RUN] Would have executed '${command} ${args.join(" ")}'`,
|
||||
);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/** Options for {@link runGit}. */
|
||||
export interface RunGitOptions extends DryRunOption {
|
||||
/** When true, non-zero exit codes will not throw. */
|
||||
allowNonZeroExitCode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs `git` with the given `args` and returns the stdout.
|
||||
*
|
||||
* @param args - Arguments to pass to `git`.
|
||||
* @param options - Optional settings.
|
||||
* @throws If `git` does not exit successfully, unless
|
||||
* `options.allowNonZeroExitCode` is `true`.
|
||||
* @returns The trimmed stdout output.
|
||||
*/
|
||||
export function runGit(args: string[], options?: RunGitOptions): string {
|
||||
const execOptions: ExecFileSyncOptions = {
|
||||
encoding: "utf8",
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
};
|
||||
|
||||
try {
|
||||
const result = runCommand("git", args, {
|
||||
dryRun: options?.dryRun,
|
||||
execOptions,
|
||||
}) as string;
|
||||
return result.trimEnd();
|
||||
} catch (error: unknown) {
|
||||
if (options?.allowNonZeroExitCode) {
|
||||
// execFileSync throws an object with `stdout` when the process exits
|
||||
// with a non-zero code.
|
||||
const execError = error as { stdout?: Buffer | string };
|
||||
if (typeof execError.stdout === "string") {
|
||||
return execError.stdout.trimEnd();
|
||||
}
|
||||
if (Buffer.isBuffer(execError.stdout)) {
|
||||
return execError.stdout.toString("utf8").trimEnd();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true if the given branch exists on the origin remote. */
|
||||
export function branchExistsOnRemote(branchName: string): boolean {
|
||||
const result = runGit(["ls-remote", "--heads", ORIGIN, branchName]);
|
||||
@@ -699,14 +615,11 @@ export async function prepareNewBranch(
|
||||
|
||||
console.log("Updating changelog");
|
||||
changelog.withChangelog(
|
||||
(contents) => {
|
||||
const populatedContents = transferChangenotesToChangelog(contents);
|
||||
return changelog.setVersionAndDate(version, populatedContents);
|
||||
},
|
||||
(contents) => changelog.setVersionAndDate(version, contents),
|
||||
{ ...options, initChangelog: true },
|
||||
);
|
||||
|
||||
runGit(["add", CHANGELOG_FILE, CHANGENOTES_DIR], {
|
||||
runGit(["add", "CHANGELOG.md"], {
|
||||
dryRun: options.dryRun,
|
||||
});
|
||||
runGit(["commit", "-m", `Update changelog for v${version}`], {
|
||||
|
||||
@@ -93,7 +93,7 @@ export function getActionVersion(): string {
|
||||
*
|
||||
* This will be "dynamic" for default setup workflow runs.
|
||||
*/
|
||||
export function getWorkflowEventName(env: ReadOnlyEnv = getEnv()) {
|
||||
export function getWorkflowEventName(env: Env = getEnv()) {
|
||||
return env.getRequired(ActionsEnvVars.GITHUB_EVENT_NAME);
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ export function getWorkflowEventName(env: ReadOnlyEnv = getEnv()) {
|
||||
* Returns whether the current workflow is executing a local copy of the Action, e.g. we're running
|
||||
* a workflow on the codeql-action repo itself.
|
||||
*/
|
||||
export function isRunningLocalAction(env: ReadOnlyEnv = getEnv()): boolean {
|
||||
export function isRunningLocalAction(env: Env = getEnv()): boolean {
|
||||
const relativeScriptPath = getRelativeScriptPath(env);
|
||||
return (
|
||||
relativeScriptPath.startsWith("..") || path.isAbsolute(relativeScriptPath)
|
||||
@@ -113,14 +113,14 @@ export function isRunningLocalAction(env: ReadOnlyEnv = getEnv()): boolean {
|
||||
*
|
||||
* This can be used to get the Action's name or tell if we're running a local Action.
|
||||
*/
|
||||
function getRelativeScriptPath(env: ReadOnlyEnv): string {
|
||||
function getRelativeScriptPath(env: Env): string {
|
||||
const runnerTemp = env.getRequired(ActionsEnvVars.RUNNER_TEMP);
|
||||
const actionsDirectory = path.join(path.dirname(runnerTemp), "_actions");
|
||||
return path.relative(actionsDirectory, __filename);
|
||||
}
|
||||
|
||||
/** Returns the contents of `GITHUB_EVENT_PATH` as a JSON object. */
|
||||
export function getWorkflowEvent(env: ReadOnlyEnv = getEnv()): any {
|
||||
export function getWorkflowEvent(env: Env = getEnv()): any {
|
||||
const eventJsonFile = env.getRequired(ActionsEnvVars.GITHUB_EVENT_PATH);
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(eventJsonFile, "utf-8"));
|
||||
|
||||
@@ -45,7 +45,6 @@ test.serial(
|
||||
requiredInputStub.withArgs("token").returns("fake-token");
|
||||
requiredInputStub.withArgs("upload-database").returns("false");
|
||||
requiredInputStub.withArgs("output").returns("out");
|
||||
requiredInputStub.withArgs("checkout_path").returns("");
|
||||
const optionalInputStub = sinon.stub(actionsUtil, "getOptionalInput");
|
||||
optionalInputStub.withArgs("expect-error").returns("false");
|
||||
sinon.stub(api, "getGitHubVersion").resolves(gitHubVersion);
|
||||
@@ -105,7 +104,6 @@ test.serial(
|
||||
requiredInputStub.withArgs("token").returns("fake-token");
|
||||
requiredInputStub.withArgs("upload-database").returns("false");
|
||||
requiredInputStub.withArgs("output").returns("out");
|
||||
requiredInputStub.withArgs("checkout_path").returns("");
|
||||
const optionalInputStub = sinon.stub(actionsUtil, "getOptionalInput");
|
||||
optionalInputStub.withArgs("expect-error").returns("false");
|
||||
sinon.stub(api, "getGitHubVersion").resolves(gitHubVersion);
|
||||
|
||||
@@ -4,18 +4,12 @@ import { performance } from "perf_hooks";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import {
|
||||
Action,
|
||||
ActionState,
|
||||
extendActionState,
|
||||
runInActions,
|
||||
} from "./action-common";
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import * as analyses from "./analyses";
|
||||
import {
|
||||
CodeQLAnalysisError,
|
||||
dbIsFinalized,
|
||||
determineCheckoutPath,
|
||||
QueriesStatusReport,
|
||||
runFinalize,
|
||||
runQueries,
|
||||
@@ -218,11 +212,14 @@ async function runAutobuildIfLegacyGoWorkflow(config: Config, logger: Logger) {
|
||||
await runAutobuild(config, BuiltInLanguage.go, logger);
|
||||
}
|
||||
|
||||
async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
|
||||
async function run({
|
||||
startedAt,
|
||||
env,
|
||||
logger,
|
||||
actions,
|
||||
}: ActionState<["Base", "Env", "Logger", "Actions"]>) {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
const startedAt = action.startedAt;
|
||||
const logger = action.logger;
|
||||
|
||||
let uploadResults:
|
||||
| Partial<Record<analyses.AnalysisKind, UploadResult>>
|
||||
@@ -280,7 +277,7 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
|
||||
|
||||
const apiDetails = getApiDetails();
|
||||
const outputDir = actionsUtil.getRequiredInput("output");
|
||||
action.env.export(EnvVar.SARIF_RESULTS_OUTPUT_DIR, outputDir);
|
||||
env.export(EnvVar.SARIF_RESULTS_OUTPUT_DIR, outputDir);
|
||||
const threads = util.getThreadsFlag(
|
||||
actionsUtil.getOptionalInput("threads") || process.env["CODEQL_THREADS"],
|
||||
logger,
|
||||
@@ -304,7 +301,7 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
|
||||
logger,
|
||||
);
|
||||
|
||||
const checkoutPath = await determineCheckoutPath(action, config);
|
||||
const checkoutPath = actions.getRequiredInput("checkout_path");
|
||||
|
||||
// Setup diff informed analysis if needed (based on whether init created the file)
|
||||
const diffRangePackDir = await setupDiffInformedQueryRun(
|
||||
@@ -396,13 +393,11 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
|
||||
checkoutPath,
|
||||
);
|
||||
|
||||
const actionWithFeatures = extendActionState(action, { features });
|
||||
|
||||
// Possibly upload the database bundles for remote queries.
|
||||
// Note: Take care with the ordering of this call since databases may be cleaned up
|
||||
// at the `overlay` or `clear` level.
|
||||
databaseUploadResults = await cleanupAndUploadDatabases(
|
||||
actionWithFeatures,
|
||||
{ logger, features },
|
||||
repositoryNwo,
|
||||
codeql,
|
||||
config,
|
||||
@@ -451,7 +446,7 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
|
||||
`expect-error input was set to true but no error was thrown.`,
|
||||
);
|
||||
}
|
||||
action.env.export(EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY, "true");
|
||||
env.export(EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY, "true");
|
||||
} catch (unwrappedError) {
|
||||
const error = util.wrapError(unwrappedError);
|
||||
if (
|
||||
|
||||
@@ -10,12 +10,10 @@ import {
|
||||
resolveQuerySuiteAlias,
|
||||
addSarifExtension,
|
||||
diffRangeExtensionPackContents,
|
||||
determineCheckoutPath,
|
||||
} from "./analyze";
|
||||
import { createStubCodeQL } from "./codeql";
|
||||
import { defaultSuites } from "./config/db-config";
|
||||
import { Feature } from "./feature-flags";
|
||||
import * as gitUtils from "./git-utils";
|
||||
import { BuiltInLanguage } from "./languages";
|
||||
import { getRunnerLogger } from "./logging";
|
||||
import {
|
||||
@@ -23,98 +21,12 @@ import {
|
||||
setupActionsVars,
|
||||
createFeatures,
|
||||
createTestConfig,
|
||||
callee,
|
||||
} from "./testing-utils";
|
||||
import * as uploadLib from "./upload-lib";
|
||||
import * as util from "./util";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
test.serial(
|
||||
"determineCheckoutPath - logs when checkout_path is not in a work tree",
|
||||
async (t) => {
|
||||
const expectedPath = path.resolve("/checkout/path");
|
||||
const target = callee(determineCheckoutPath)
|
||||
.withActions((actions) => {
|
||||
sinon
|
||||
.stub(actions, "getRequiredInput")
|
||||
.withArgs("checkout_path")
|
||||
.returns(expectedPath);
|
||||
})
|
||||
.withArgs(createTestConfig({}));
|
||||
sinon.stub(gitUtils, "getGitRoot").resolves(undefined);
|
||||
|
||||
await target
|
||||
.logs(t, "is not in the work tree of a git repository")
|
||||
.passes(t.is, expectedPath);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"determineCheckoutPath - logs when checkout_path is not a repo root",
|
||||
async (t) => {
|
||||
const expectedPath = path.resolve("/checkout/path");
|
||||
const target = callee(determineCheckoutPath)
|
||||
.withActions((actions) => {
|
||||
sinon
|
||||
.stub(actions, "getRequiredInput")
|
||||
.withArgs("checkout_path")
|
||||
.returns(expectedPath);
|
||||
})
|
||||
.withArgs(createTestConfig({}));
|
||||
sinon.stub(gitUtils, "getGitRoot").resolves("/checkout");
|
||||
|
||||
await target
|
||||
.logs(t, "is not the root of the repository")
|
||||
.passes(t.is, expectedPath);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"determineCheckoutPath - logs when checkout_path is not the same as repo root in config",
|
||||
async (t) => {
|
||||
const expectedPath = path.resolve("/checkout/path");
|
||||
const target = callee(determineCheckoutPath)
|
||||
.withActions((actions) => {
|
||||
sinon
|
||||
.stub(actions, "getRequiredInput")
|
||||
.withArgs("checkout_path")
|
||||
.returns(expectedPath);
|
||||
})
|
||||
.withArgs(createTestConfig({ repositoryRoot: "/some/other/path" }));
|
||||
sinon.stub(gitUtils, "getGitRoot").resolves(expectedPath);
|
||||
|
||||
await target
|
||||
.logs(t, "does not match that found by the 'codeql-action/init' step")
|
||||
.passes(t.is, expectedPath);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"determineCheckoutPath - doesn't log any of the messages when all is as expected",
|
||||
async (t) => {
|
||||
const expectedPath = path.resolve("/checkout/path");
|
||||
const target = callee(determineCheckoutPath)
|
||||
.withActions((actions) => {
|
||||
sinon
|
||||
.stub(actions, "getRequiredInput")
|
||||
.withArgs("checkout_path")
|
||||
.returns(expectedPath);
|
||||
})
|
||||
.withArgs(createTestConfig({ repositoryRoot: expectedPath }));
|
||||
sinon.stub(gitUtils, "getGitRoot").resolves(expectedPath);
|
||||
|
||||
await target
|
||||
.notLogs(
|
||||
t,
|
||||
"is not in the work tree of a git repository",
|
||||
"is not the root of the repository",
|
||||
"does not match that found by the 'codeql-action/init' step",
|
||||
)
|
||||
.passes(t.is, expectedPath);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks the status report produced by the analyze Action.
|
||||
*
|
||||
|
||||
@@ -5,7 +5,6 @@ import { performance } from "perf_hooks";
|
||||
import * as io from "@actions/io";
|
||||
import * as yaml from "js-yaml";
|
||||
|
||||
import type { ActionState } from "./action-common";
|
||||
import { getTemporaryDirectory } from "./actions-util";
|
||||
import * as analyses from "./analyses";
|
||||
import { setupCppAutobuild } from "./autobuild";
|
||||
@@ -24,7 +23,6 @@ import {
|
||||
} from "./diff-informed-analysis-utils";
|
||||
import { EnvVar } from "./environment";
|
||||
import { FeatureEnablement, Feature } from "./feature-flags";
|
||||
import { getGitRoot } from "./git-utils";
|
||||
import { BuiltInLanguage, Language } from "./languages";
|
||||
import { Logger, withGroupAsync } from "./logging";
|
||||
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
|
||||
@@ -89,58 +87,6 @@ export interface QueriesStatusReport
|
||||
event_reports?: EventReport[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the path at which the repository being analysed is checked out at.
|
||||
* Returns the value of the required `checkout_path` input and validates that it
|
||||
* refers to the root of a repository.
|
||||
*
|
||||
* @param action The action state.
|
||||
* @param config The CodeQL Action configuration state.
|
||||
*/
|
||||
export async function determineCheckoutPath(
|
||||
action: ActionState<["Logger", "Actions"]>,
|
||||
config: configUtils.Config,
|
||||
) {
|
||||
const checkoutPathInput = action.actions.getRequiredInput("checkout_path");
|
||||
|
||||
// Try to obtain the root path of the repository and validate that it matches the input.
|
||||
const absCheckoutPathInput = path.resolve(checkoutPathInput);
|
||||
const repositoryRoot = await getGitRoot(absCheckoutPathInput);
|
||||
|
||||
if (repositoryRoot === undefined) {
|
||||
action.logger.warning(
|
||||
[
|
||||
`The directory at '${absCheckoutPathInput}' is not in the work tree of a git repository.`,
|
||||
"If the repository being analyzed is checked out elsewhere,",
|
||||
"you must explicitly set the 'checkout_path' input for the 'codeql-action/analyze' step to",
|
||||
"the checkout path.",
|
||||
].join(" "),
|
||||
);
|
||||
} else if (repositoryRoot !== absCheckoutPathInput) {
|
||||
action.logger.warning(
|
||||
[
|
||||
`The directory at '${absCheckoutPathInput}' is not the root of the repository ('${repositoryRoot}').`,
|
||||
"Set the 'checkout_path' input for the 'codeql-action/analyze' step to the root path of the checkout.",
|
||||
].join(" "),
|
||||
);
|
||||
} else if (
|
||||
config.repositoryRoot !== undefined &&
|
||||
repositoryRoot !== config.repositoryRoot
|
||||
) {
|
||||
// The repository root that was persisted by the `init` step doesn't match the one we have found here.
|
||||
action.logger.warning(
|
||||
[
|
||||
`The repository path at '${repositoryRoot}' does not match that found by the 'codeql-action/init' step: '${config.repositoryRoot}'.`,
|
||||
"Ensure that the 'checkout_path' input for the 'codeql-action/analyze' step is set to the path of the same repository that",
|
||||
"the 'codeql-action/init' step determined. This is either the GitHub Actions workspace or the repository root corresponding to",
|
||||
"the 'source-root' input if that was provided.",
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
return absCheckoutPathInput;
|
||||
}
|
||||
|
||||
async function setupPythonExtractor(logger: Logger) {
|
||||
const codeqlPython = process.env["CODEQL_PYTHON"];
|
||||
if (codeqlPython === undefined || codeqlPython.length === 0) {
|
||||
|
||||
@@ -1225,7 +1225,7 @@ export async function getTrapCachingExtractorConfigArgsForLang(
|
||||
): Promise<string[]> {
|
||||
const cacheDir = config.trapCaches[language];
|
||||
if (cacheDir === undefined) return [];
|
||||
const write = await isAnalyzingDefaultBranch(getEnv(), config.repositoryRoot);
|
||||
const write = await isAnalyzingDefaultBranch();
|
||||
return [
|
||||
`-O=${language}.trap.cache.dir=${cacheDir}`,
|
||||
`-O=${language}.trap.cache.bound=${TRAP_CACHE_SIZE_MB}`,
|
||||
|
||||
@@ -172,7 +172,6 @@ test.serial("load empty config", async (t) => {
|
||||
createTestInitConfigInputs({
|
||||
languagesInput: languages,
|
||||
repository: { owner: "github", repo: "example" },
|
||||
sourceRoot: tempDir,
|
||||
tempDir,
|
||||
codeql,
|
||||
logger,
|
||||
@@ -187,7 +186,6 @@ test.serial("load empty config", async (t) => {
|
||||
logger,
|
||||
}),
|
||||
{},
|
||||
undefined,
|
||||
);
|
||||
|
||||
t.deepEqual(config, expectedConfig);
|
||||
@@ -218,7 +216,6 @@ test.serial("load code quality config", async (t) => {
|
||||
analysisKinds: [AnalysisKind.CodeQuality],
|
||||
languagesInput: languages,
|
||||
repository: { owner: "github", repo: "example" },
|
||||
sourceRoot: tempDir,
|
||||
tempDir,
|
||||
codeql,
|
||||
logger,
|
||||
@@ -299,7 +296,6 @@ test.serial(
|
||||
analysisKinds: [AnalysisKind.CodeQuality],
|
||||
languagesInput: languages,
|
||||
repository: { owner: "github", repo: "example" },
|
||||
sourceRoot: tempDir,
|
||||
tempDir,
|
||||
codeql,
|
||||
repositoryProperties,
|
||||
@@ -522,7 +518,6 @@ test.serial("load non-empty input", async (t) => {
|
||||
// And the config we expect it to parse to
|
||||
const expectedConfig = createTestConfig({
|
||||
languages: [BuiltInLanguage.javascript],
|
||||
repositoryRoot: undefined,
|
||||
buildMode: BuildMode.None,
|
||||
originalUserInput: userConfig,
|
||||
computedConfig: userConfig,
|
||||
@@ -543,7 +538,6 @@ test.serial("load non-empty input", async (t) => {
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput,
|
||||
sourceRoot: tempDir,
|
||||
buildModeInput: "none",
|
||||
configFile: configFilePath,
|
||||
debugArtifactName: "my-artifact",
|
||||
@@ -1111,6 +1105,11 @@ const checkOverlayEnablementMacro = makeMacro({
|
||||
return lang === BuiltInLanguage.java;
|
||||
});
|
||||
|
||||
// Mock git root detection
|
||||
if (setup.gitRoot !== undefined) {
|
||||
sinon.stub(gitUtils, "getGitRoot").resolves(setup.gitRoot);
|
||||
}
|
||||
|
||||
// Mock submodule detection
|
||||
sinon.stub(gitUtils, "hasSubmodules").returns(setup.hasSubmodules);
|
||||
|
||||
@@ -1123,7 +1122,6 @@ const checkOverlayEnablementMacro = makeMacro({
|
||||
codeql,
|
||||
features,
|
||||
setup.languages,
|
||||
setup.gitRoot, // repositoryRoot
|
||||
tempDir, // sourceRoot
|
||||
setup.buildMode,
|
||||
undefined,
|
||||
|
||||
@@ -49,7 +49,7 @@ import {
|
||||
makeTelemetryDiagnostic,
|
||||
} from "./diagnostics";
|
||||
import { prepareDiffInformedAnalysis } from "./diff-informed-analysis-utils";
|
||||
import { EnvVar, exportEnvVar, getEnv } from "./environment";
|
||||
import { EnvVar, exportEnvVar } from "./environment";
|
||||
import * as errorMessages from "./error-messages";
|
||||
import { Feature, FeatureEnablement, FeatureWithoutCLI } from "./feature-flags";
|
||||
import {
|
||||
@@ -390,7 +390,6 @@ export async function initActionState(
|
||||
enableFileCoverageInformation,
|
||||
}: InitConfigInputs,
|
||||
userConfig: UserConfig,
|
||||
repositoryRoot: string | undefined,
|
||||
): Promise<Config> {
|
||||
const languages = await getLanguages(
|
||||
codeql,
|
||||
@@ -442,7 +441,6 @@ export async function initActionState(
|
||||
|
||||
return {
|
||||
version: getActionVersion(),
|
||||
repositoryRoot,
|
||||
analysisKinds,
|
||||
languages,
|
||||
buildMode,
|
||||
@@ -472,18 +470,12 @@ async function downloadCacheWithTime(
|
||||
codeQL: CodeQL,
|
||||
languages: Language[],
|
||||
logger: Logger,
|
||||
repositoryRoot: string | undefined,
|
||||
): Promise<{
|
||||
trapCaches: { [language: string]: string };
|
||||
trapCacheDownloadTime: number;
|
||||
}> {
|
||||
const start = performance.now();
|
||||
const trapCaches = await downloadTrapCaches(
|
||||
codeQL,
|
||||
languages,
|
||||
logger,
|
||||
repositoryRoot,
|
||||
);
|
||||
const trapCaches = await downloadTrapCaches(codeQL, languages, logger);
|
||||
const trapCacheDownloadTime = performance.now() - start;
|
||||
return { trapCaches, trapCacheDownloadTime };
|
||||
}
|
||||
@@ -731,7 +723,6 @@ export async function checkOverlayEnablement(
|
||||
codeql: CodeQL,
|
||||
features: FeatureEnablement,
|
||||
languages: Language[],
|
||||
repositoryRoot: string | undefined,
|
||||
sourceRoot: string,
|
||||
buildMode: BuildMode | undefined,
|
||||
ramInput: string | undefined,
|
||||
@@ -761,7 +752,6 @@ export async function checkOverlayEnablement(
|
||||
true,
|
||||
codeql,
|
||||
languages,
|
||||
repositoryRoot,
|
||||
sourceRoot,
|
||||
buildMode,
|
||||
gitVersion,
|
||||
@@ -835,7 +825,7 @@ export async function checkOverlayEnablement(
|
||||
`Setting overlay database mode to ${overlayDatabaseMode} ` +
|
||||
"with caching because we are analyzing a pull request.",
|
||||
);
|
||||
} else if (await isAnalyzingDefaultBranch(getEnv(), repositoryRoot)) {
|
||||
} else if (await isAnalyzingDefaultBranch()) {
|
||||
overlayDatabaseMode = OverlayDatabaseMode.OverlayBase;
|
||||
logger.info(
|
||||
`Setting overlay database mode to ${overlayDatabaseMode} ` +
|
||||
@@ -851,7 +841,6 @@ export async function checkOverlayEnablement(
|
||||
false,
|
||||
codeql,
|
||||
languages,
|
||||
repositoryRoot,
|
||||
sourceRoot,
|
||||
buildMode,
|
||||
gitVersion,
|
||||
@@ -871,7 +860,6 @@ async function validateOverlayDatabaseMode(
|
||||
overlayModeSetExplicitly: boolean,
|
||||
codeql: CodeQL,
|
||||
languages: Language[],
|
||||
repositoryRoot: string | undefined,
|
||||
sourceRoot: string,
|
||||
buildMode: BuildMode | undefined,
|
||||
gitVersion: GitVersionInfo | undefined,
|
||||
@@ -907,7 +895,8 @@ async function validateOverlayDatabaseMode(
|
||||
);
|
||||
return new Failure(OverlayDisabledReason.IncompatibleCodeQl);
|
||||
}
|
||||
if (repositoryRoot === undefined) {
|
||||
const gitRoot = await getGitRoot(sourceRoot);
|
||||
if (gitRoot === undefined) {
|
||||
logger.warning(
|
||||
`Cannot build an ${overlayDatabaseMode} database because ` +
|
||||
`the source root "${sourceRoot}" is not inside a git repository. ` +
|
||||
@@ -915,7 +904,7 @@ async function validateOverlayDatabaseMode(
|
||||
);
|
||||
return new Failure(OverlayDisabledReason.NoGitRoot);
|
||||
}
|
||||
if (hasSubmodules(repositoryRoot)) {
|
||||
if (hasSubmodules(gitRoot)) {
|
||||
if (gitVersion === undefined) {
|
||||
logger.warning(
|
||||
`Cannot build an ${overlayDatabaseMode} database because ` +
|
||||
@@ -1167,11 +1156,9 @@ export async function initConfig(
|
||||
const { logger, features } = actionState;
|
||||
const { tempDir } = inputs;
|
||||
|
||||
const repositoryRoot = await getGitRoot(inputs.sourceRoot);
|
||||
|
||||
const userConfig = await determineUserConfig(actionState, tempDir, inputs);
|
||||
|
||||
const config = await initActionState(inputs, userConfig, repositoryRoot);
|
||||
const config = await initActionState(inputs, userConfig);
|
||||
|
||||
// If Code Quality analysis is the only enabled analysis kind, then we will initialise
|
||||
// the database for Code Quality. That entails disabling the default queries and only
|
||||
@@ -1253,7 +1240,6 @@ export async function initConfig(
|
||||
inputs.codeql,
|
||||
inputs.features,
|
||||
config.languages,
|
||||
repositoryRoot,
|
||||
inputs.sourceRoot,
|
||||
config.buildMode,
|
||||
inputs.ramInput,
|
||||
@@ -1307,7 +1293,6 @@ export async function initConfig(
|
||||
inputs.codeql,
|
||||
config.languages,
|
||||
logger,
|
||||
repositoryRoot,
|
||||
);
|
||||
config.trapCaches = trapCaches;
|
||||
config.trapCacheDownloadTime = trapCacheDownloadTime;
|
||||
|
||||
@@ -16,11 +16,6 @@ export interface Config {
|
||||
* The version of the CodeQL Action that the configuration is for.
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* The path at which the repository being analysed is checked out at, if available.
|
||||
* Persisted in the CodeQL Action configuration state, so that we can consult it in later workflow steps.
|
||||
*/
|
||||
repositoryRoot: string | undefined;
|
||||
/**
|
||||
* Set of analysis kinds that are enabled.
|
||||
*/
|
||||
|
||||
@@ -46,7 +46,7 @@ export interface DatabaseUploadResult {
|
||||
}
|
||||
|
||||
export async function cleanupAndUploadDatabases(
|
||||
action: ActionState<["ReadOnlyEnv", "Logger", "FeatureFlags"]>,
|
||||
action: ActionState<["Logger", "FeatureFlags"]>,
|
||||
repositoryNwo: RepositoryNwo,
|
||||
codeql: CodeQL,
|
||||
config: Config,
|
||||
@@ -81,7 +81,7 @@ export async function cleanupAndUploadDatabases(
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!(await gitUtils.isAnalyzingDefaultBranch(action.env, checkoutPath))) {
|
||||
if (!(await gitUtils.isAnalyzingDefaultBranch())) {
|
||||
// We only want to upload a database if we are analyzing the default branch.
|
||||
logger.debug("Not analyzing default branch. Skipping upload.");
|
||||
return [];
|
||||
@@ -113,7 +113,7 @@ export async function cleanupAndUploadDatabases(
|
||||
includeDiagnostics: false,
|
||||
});
|
||||
bundledDbSize = fs.statSync(bundledDb).size;
|
||||
const commitOid = await gitUtils.getCommitOid(action.env, checkoutPath);
|
||||
const commitOid = await gitUtils.getCommitOid(checkoutPath);
|
||||
// Upload with manual retry logic. We disable Octokit's built-in retries
|
||||
// because the request body is a ReadStream, which can only be consumed
|
||||
// once.
|
||||
|
||||
@@ -313,7 +313,6 @@ export async function downloadDependencyCaches(
|
||||
|
||||
const primaryKey = await cacheKey(codeql, features, language, patterns);
|
||||
const restoreKeys: string[] = [
|
||||
primaryKey,
|
||||
await cachePrefix(codeql, features, language),
|
||||
];
|
||||
|
||||
|
||||
@@ -346,13 +346,6 @@ export class Env<
|
||||
this.changed = true;
|
||||
}
|
||||
|
||||
/** Sets all environment variables given by `vars`. */
|
||||
public setAll(vars: Record<string, T>): void {
|
||||
for (const [key, val] of Object.entries(vars)) {
|
||||
this.set(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around `core.exportVariable` which does not call `core.exportVariable`
|
||||
* when running unit tests. This is important, because otherwise `core.exportVariable`
|
||||
|
||||
@@ -7,42 +7,35 @@ import test from "ava";
|
||||
import * as sinon from "sinon";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { ActionsEnvVars, EnvVar } from "./environment";
|
||||
import * as gitUtils from "./git-utils";
|
||||
import { getTestEnv, setupActionsVars, setupTests } from "./testing-utils";
|
||||
import { setupActionsVars, setupTests } from "./testing-utils";
|
||||
import { withTmpDir } from "./util";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
test("getRef() throws on the empty string", async (t) => {
|
||||
const env = getTestEnv({ [ActionsEnvVars.GITHUB_REF]: "" });
|
||||
await t.throwsAsync(() => gitUtils.getRef(env, ""));
|
||||
test.serial("getRef() throws on the empty string", async (t) => {
|
||||
process.env["GITHUB_REF"] = "";
|
||||
await t.throwsAsync(gitUtils.getRef);
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"getRef() returns merge PR ref if GITHUB_SHA still checked out",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir: string) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const expectedRef = "refs/pull/1/merge";
|
||||
const currentSha = "a".repeat(40);
|
||||
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
env.setAll({
|
||||
[ActionsEnvVars.GITHUB_REF]: expectedRef,
|
||||
[ActionsEnvVars.GITHUB_SHA]: currentSha,
|
||||
});
|
||||
process.env["GITHUB_REF"] = expectedRef;
|
||||
process.env["GITHUB_SHA"] = currentSha;
|
||||
|
||||
const callback = sinon.stub(gitUtils, "getCommitOid");
|
||||
callback
|
||||
.withArgs(sinon.match.any, sinon.match.string, "HEAD")
|
||||
.resolves(currentSha);
|
||||
callback.withArgs(sinon.match.string, "HEAD").resolves(currentSha);
|
||||
|
||||
const actualRef = await gitUtils.getRef(env, tmpDir);
|
||||
const actualRef = await gitUtils.getRef();
|
||||
t.deepEqual(actualRef, expectedRef);
|
||||
|
||||
t.is(callback.callCount, 1);
|
||||
t.true(callback.calledOnceWith(env, tmpDir, "HEAD"));
|
||||
t.true(callback.calledOnceWith(tmpDir, "HEAD"));
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -51,33 +44,24 @@ test.serial(
|
||||
"getRef() returns merge PR ref if GITHUB_REF still checked out but sha has changed (actions checkout@v1)",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir: string) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const expectedRef = "refs/pull/1/merge";
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
env.setAll({
|
||||
[ActionsEnvVars.GITHUB_REF]: expectedRef,
|
||||
[ActionsEnvVars.GITHUB_SHA]: "b".repeat(40),
|
||||
});
|
||||
process.env["GITHUB_REF"] = expectedRef;
|
||||
process.env["GITHUB_SHA"] = "b".repeat(40);
|
||||
const sha = "a".repeat(40);
|
||||
|
||||
const callback = sinon.stub(gitUtils, "getCommitOid");
|
||||
callback
|
||||
.withArgs(
|
||||
sinon.match.any,
|
||||
sinon.match.string,
|
||||
"refs/remotes/pull/1/merge",
|
||||
)
|
||||
.resolves(sha);
|
||||
callback
|
||||
.withArgs(sinon.match.any, sinon.match.string, "HEAD")
|
||||
.withArgs(sinon.match.string, "refs/remotes/pull/1/merge")
|
||||
.resolves(sha);
|
||||
callback.withArgs(sinon.match.any, "HEAD").resolves(sha);
|
||||
|
||||
const actualRef = await gitUtils.getRef(env, tmpDir);
|
||||
const actualRef = await gitUtils.getRef();
|
||||
t.deepEqual(actualRef, expectedRef);
|
||||
|
||||
t.is(callback.callCount, 2);
|
||||
t.true(callback.calledWith(env, tmpDir, "HEAD"));
|
||||
t.true(callback.calledWith(env, tmpDir, "refs/remotes/pull/1/merge"));
|
||||
t.true(callback.calledWith(tmpDir, "HEAD"));
|
||||
t.true(callback.calledWith(tmpDir, "refs/remotes/pull/1/merge"));
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -86,28 +70,23 @@ test.serial(
|
||||
"getRef() returns head PR ref if GITHUB_REF no longer checked out",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir: string) => {
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
env.setAll({
|
||||
[ActionsEnvVars.GITHUB_REF]: "refs/pull/1/merge",
|
||||
[ActionsEnvVars.GITHUB_SHA]: "a".repeat(40),
|
||||
});
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
process.env["GITHUB_REF"] = "refs/pull/1/merge";
|
||||
process.env["GITHUB_SHA"] = "a".repeat(40);
|
||||
|
||||
const callback = sinon.stub(gitUtils, "getCommitOid");
|
||||
callback
|
||||
.withArgs(sinon.match.any, tmpDir, "refs/remotes/pull/1/merge")
|
||||
.withArgs(tmpDir, "refs/remotes/pull/1/merge")
|
||||
.resolves("a".repeat(40));
|
||||
callback
|
||||
.withArgs(sinon.match.any, tmpDir, "HEAD")
|
||||
.resolves("b".repeat(40));
|
||||
callback.withArgs(tmpDir, "HEAD").resolves("b".repeat(40));
|
||||
callback.throws(new Error("Unexpected getCommitOid call in test."));
|
||||
|
||||
const actualRef = await gitUtils.getRef(env, tmpDir);
|
||||
const actualRef = await gitUtils.getRef();
|
||||
t.deepEqual(actualRef, "refs/pull/1/head");
|
||||
|
||||
t.is(callback.callCount, 2);
|
||||
t.true(callback.calledWith(env, tmpDir, "refs/remotes/pull/1/merge"));
|
||||
t.true(callback.calledWith(env, tmpDir, "HEAD"));
|
||||
t.true(callback.calledWith(tmpDir, "refs/remotes/pull/1/merge"));
|
||||
t.true(callback.calledWith(tmpDir, "HEAD"));
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -116,6 +95,7 @@ test.serial(
|
||||
"getRef() returns ref provided as an input and ignores current HEAD",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir: string) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const getAdditionalInputStub = sinon.stub(
|
||||
actionsUtil,
|
||||
"getOptionalInput",
|
||||
@@ -124,22 +104,14 @@ test.serial(
|
||||
getAdditionalInputStub.withArgs("sha").resolves("b".repeat(40));
|
||||
|
||||
// These values are be ignored
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
env.setAll({
|
||||
[ActionsEnvVars.GITHUB_REF]: "refs/pull/1/merge",
|
||||
[ActionsEnvVars.GITHUB_SHA]: "a".repeat(40),
|
||||
});
|
||||
process.env["GITHUB_REF"] = "refs/pull/1/merge";
|
||||
process.env["GITHUB_SHA"] = "a".repeat(40);
|
||||
|
||||
const callback = sinon.stub(gitUtils, "getCommitOid");
|
||||
callback
|
||||
.withArgs(sinon.match.any, tmpDir, "refs/pull/1/merge")
|
||||
.resolves("b".repeat(40));
|
||||
callback
|
||||
.withArgs(sinon.match.any, sinon.match.string, "HEAD")
|
||||
.resolves("b".repeat(40));
|
||||
callback.withArgs(tmpDir, "refs/pull/1/merge").resolves("b".repeat(40));
|
||||
callback.withArgs(sinon.match.any, "HEAD").resolves("b".repeat(40));
|
||||
|
||||
const actualRef = await gitUtils.getRef(env, tmpDir);
|
||||
const actualRef = await gitUtils.getRef();
|
||||
t.deepEqual(actualRef, "refs/pull/2/merge");
|
||||
|
||||
// getCommitOid shouldn't be called, because the ref should be taken from the input
|
||||
@@ -152,17 +124,14 @@ test.serial(
|
||||
"getRef() returns CODE_SCANNING_REF as a fallback for GITHUB_REF",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir: string) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const expectedRef = "refs/pull/1/HEAD";
|
||||
const currentSha = "a".repeat(40);
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
env.setAll({
|
||||
[EnvVar.CODE_SCANNING_REF]: expectedRef,
|
||||
[ActionsEnvVars.GITHUB_REF]: "",
|
||||
[ActionsEnvVars.GITHUB_SHA]: currentSha,
|
||||
});
|
||||
process.env["CODE_SCANNING_REF"] = expectedRef;
|
||||
process.env["GITHUB_REF"] = "";
|
||||
process.env["GITHUB_SHA"] = currentSha;
|
||||
|
||||
const actualRef = await gitUtils.getRef(env, tmpDir);
|
||||
const actualRef = await gitUtils.getRef();
|
||||
t.deepEqual(actualRef, expectedRef);
|
||||
});
|
||||
},
|
||||
@@ -172,17 +141,14 @@ test.serial(
|
||||
"getRef() returns GITHUB_REF over CODE_SCANNING_REF if both are provided",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir: string) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const expectedRef = "refs/pull/1/merge";
|
||||
const currentSha = "a".repeat(40);
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
env.setAll({
|
||||
[EnvVar.CODE_SCANNING_REF]: "refs/pull/1/HEAD",
|
||||
[ActionsEnvVars.GITHUB_REF]: expectedRef,
|
||||
[ActionsEnvVars.GITHUB_SHA]: currentSha,
|
||||
});
|
||||
process.env["CODE_SCANNING_REF"] = "refs/pull/1/HEAD";
|
||||
process.env["GITHUB_REF"] = expectedRef;
|
||||
process.env["GITHUB_SHA"] = currentSha;
|
||||
|
||||
const actualRef = await gitUtils.getRef(env, tmpDir);
|
||||
const actualRef = await gitUtils.getRef();
|
||||
t.deepEqual(actualRef, expectedRef);
|
||||
});
|
||||
},
|
||||
@@ -192,9 +158,7 @@ test.serial(
|
||||
"getRef() throws an error if only `ref` is provided as an input",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir: string) => {
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const getAdditionalInputStub = sinon.stub(
|
||||
actionsUtil,
|
||||
"getOptionalInput",
|
||||
@@ -203,7 +167,7 @@ test.serial(
|
||||
|
||||
await t.throwsAsync(
|
||||
async () => {
|
||||
await gitUtils.getRef(env, tmpDir);
|
||||
await gitUtils.getRef();
|
||||
},
|
||||
{
|
||||
instanceOf: Error,
|
||||
@@ -219,12 +183,8 @@ test.serial(
|
||||
"getRef() throws an error if only `sha` is provided as an input",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir: string) => {
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
env.setAll({
|
||||
[ActionsEnvVars.GITHUB_WORKSPACE]: "/tmp",
|
||||
});
|
||||
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
process.env["GITHUB_WORKSPACE"] = "/tmp";
|
||||
const getAdditionalInputStub = sinon.stub(
|
||||
actionsUtil,
|
||||
"getOptionalInput",
|
||||
@@ -233,7 +193,7 @@ test.serial(
|
||||
|
||||
await t.throwsAsync(
|
||||
async () => {
|
||||
await gitUtils.getRef(env, tmpDir);
|
||||
await gitUtils.getRef();
|
||||
},
|
||||
{
|
||||
instanceOf: Error,
|
||||
@@ -246,16 +206,13 @@ test.serial(
|
||||
);
|
||||
|
||||
test.serial("isAnalyzingDefaultBranch()", async (t) => {
|
||||
const env = getTestEnv({
|
||||
[ActionsEnvVars.GITHUB_EVENT_NAME]: "push",
|
||||
CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH: "true",
|
||||
});
|
||||
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(env, ""), true);
|
||||
|
||||
env.set("CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH", "false");
|
||||
process.env["GITHUB_EVENT_NAME"] = "push";
|
||||
process.env["CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH"] = "true";
|
||||
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), true);
|
||||
process.env["CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH"] = "false";
|
||||
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const envFile = path.join(tmpDir, "event.json");
|
||||
fs.writeFileSync(
|
||||
envFile,
|
||||
@@ -265,17 +222,17 @@ test.serial("isAnalyzingDefaultBranch()", async (t) => {
|
||||
},
|
||||
}),
|
||||
);
|
||||
env.set(ActionsEnvVars.GITHUB_EVENT_PATH, envFile);
|
||||
process.env["GITHUB_EVENT_PATH"] = envFile;
|
||||
|
||||
env.set(ActionsEnvVars.GITHUB_REF, "main");
|
||||
env.set(ActionsEnvVars.GITHUB_SHA, "1234");
|
||||
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(env, tmpDir), true);
|
||||
process.env["GITHUB_REF"] = "main";
|
||||
process.env["GITHUB_SHA"] = "1234";
|
||||
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), true);
|
||||
|
||||
env.set(ActionsEnvVars.GITHUB_REF, "refs/heads/main");
|
||||
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(env, tmpDir), true);
|
||||
process.env["GITHUB_REF"] = "refs/heads/main";
|
||||
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), true);
|
||||
|
||||
env.set(ActionsEnvVars.GITHUB_REF, "feature");
|
||||
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(env, tmpDir), false);
|
||||
process.env["GITHUB_REF"] = "feature";
|
||||
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), false);
|
||||
|
||||
fs.writeFileSync(
|
||||
envFile,
|
||||
@@ -283,9 +240,9 @@ test.serial("isAnalyzingDefaultBranch()", async (t) => {
|
||||
schedule: "0 0 * * *",
|
||||
}),
|
||||
);
|
||||
env.set(ActionsEnvVars.GITHUB_EVENT_NAME, "schedule");
|
||||
env.set(ActionsEnvVars.GITHUB_REF, "refs/heads/main");
|
||||
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(env, tmpDir), true);
|
||||
process.env["GITHUB_EVENT_NAME"] = "schedule";
|
||||
process.env["GITHUB_REF"] = "refs/heads/main";
|
||||
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), true);
|
||||
|
||||
const getAdditionalInputStub = sinon.stub(actionsUtil, "getOptionalInput");
|
||||
getAdditionalInputStub
|
||||
@@ -294,9 +251,9 @@ test.serial("isAnalyzingDefaultBranch()", async (t) => {
|
||||
getAdditionalInputStub
|
||||
.withArgs("sha")
|
||||
.resolves("0000000000000000000000000000000000000000");
|
||||
env.set(ActionsEnvVars.GITHUB_EVENT_NAME, "schedule");
|
||||
env.set(ActionsEnvVars.GITHUB_REF, "refs/heads/main");
|
||||
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(env, tmpDir), false);
|
||||
process.env["GITHUB_EVENT_NAME"] = "schedule";
|
||||
process.env["GITHUB_REF"] = "refs/heads/main";
|
||||
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
getWorkflowEvent,
|
||||
getWorkflowEventName,
|
||||
} from "./actions-util";
|
||||
import { ActionsEnvVars, EnvVar, type ReadOnlyEnv } from "./environment";
|
||||
import { ConfigurationError, getRequiredEnvParam } from "./util";
|
||||
|
||||
/**
|
||||
@@ -102,7 +101,6 @@ export const runGitCommand = async function (
|
||||
* Gets the SHA of the commit that is currently checked out.
|
||||
*/
|
||||
export const getCommitOid = async function (
|
||||
env: ReadOnlyEnv,
|
||||
checkoutPath: string,
|
||||
ref = "HEAD",
|
||||
): Promise<string> {
|
||||
@@ -121,9 +119,7 @@ export const getCommitOid = async function (
|
||||
);
|
||||
return stdout.trim();
|
||||
} catch {
|
||||
return (
|
||||
getOptionalInput("sha") || env.getRequired(ActionsEnvVars.GITHUB_SHA)
|
||||
);
|
||||
return getOptionalInput("sha") || getRequiredEnvParam("GITHUB_SHA");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -245,7 +241,7 @@ export const getGitRoot = async function (
|
||||
["rev-parse", "--show-toplevel"],
|
||||
`Cannot find Git repository root from the source root ${sourceRoot}.`,
|
||||
);
|
||||
return path.resolve(stdout.trim());
|
||||
return stdout.trim();
|
||||
} catch {
|
||||
// Errors are already logged by runGitCommand()
|
||||
return undefined;
|
||||
@@ -318,18 +314,18 @@ export const getFileOidsUnderPath = async function (
|
||||
return fileOidMap;
|
||||
};
|
||||
|
||||
function getRefFromEnv(env: ReadOnlyEnv): string {
|
||||
function getRefFromEnv(): string {
|
||||
// To workaround a limitation of Actions dynamic workflows not setting
|
||||
// the GITHUB_REF in some cases, we accept also the ref within the
|
||||
// CODE_SCANNING_REF variable. When possible, however, we prefer to use
|
||||
// the GITHUB_REF as that is a protected variable and cannot be overwritten.
|
||||
let refEnv: string;
|
||||
try {
|
||||
refEnv = env.getRequired(ActionsEnvVars.GITHUB_REF);
|
||||
refEnv = getRequiredEnvParam("GITHUB_REF");
|
||||
} catch (e) {
|
||||
// If the GITHUB_REF is not set, we try to rescue by getting the
|
||||
// CODE_SCANNING_REF.
|
||||
const maybeRef = env.getOptional(EnvVar.CODE_SCANNING_REF);
|
||||
const maybeRef = process.env["CODE_SCANNING_REF"];
|
||||
if (maybeRef === undefined || maybeRef.length === 0) {
|
||||
throw e;
|
||||
}
|
||||
@@ -341,16 +337,15 @@ function getRefFromEnv(env: ReadOnlyEnv): string {
|
||||
/**
|
||||
* Get the ref currently being analyzed.
|
||||
*/
|
||||
export async function getRef(
|
||||
env: ReadOnlyEnv,
|
||||
checkoutPath: string | undefined,
|
||||
): Promise<string> {
|
||||
export async function getRef(): Promise<string> {
|
||||
// Will be in the form "refs/heads/master" on a push event
|
||||
// or in the form "refs/pull/N/merge" on a pull_request event
|
||||
const refInput = getOptionalInput("ref");
|
||||
const shaInput = getOptionalInput("sha");
|
||||
checkoutPath =
|
||||
checkoutPath ?? env.getRequired(ActionsEnvVars.GITHUB_WORKSPACE);
|
||||
const checkoutPath =
|
||||
getOptionalInput("checkout_path") ||
|
||||
getOptionalInput("source-root") ||
|
||||
getRequiredEnvParam("GITHUB_WORKSPACE");
|
||||
|
||||
const hasRefInput = !!refInput;
|
||||
const hasShaInput = !!shaInput;
|
||||
@@ -361,8 +356,8 @@ export async function getRef(
|
||||
);
|
||||
}
|
||||
|
||||
const ref = refInput || getRefFromEnv(env);
|
||||
const sha = shaInput || env.getRequired(ActionsEnvVars.GITHUB_SHA);
|
||||
const ref = refInput || getRefFromEnv();
|
||||
const sha = shaInput || getRequiredEnvParam("GITHUB_SHA");
|
||||
|
||||
// If the ref is a user-provided input, we have to skip logic
|
||||
// and assume that it is really where they want to upload the results.
|
||||
@@ -379,7 +374,7 @@ export async function getRef(
|
||||
return ref;
|
||||
}
|
||||
|
||||
const head = await getCommitOid(env, checkoutPath, "HEAD");
|
||||
const head = await getCommitOid(checkoutPath, "HEAD");
|
||||
|
||||
// in actions/checkout@v2+ we can check if git rev-parse HEAD == GITHUB_SHA
|
||||
// in actions/checkout@v1 this may not be true as it checks out the repository
|
||||
@@ -389,7 +384,6 @@ export async function getRef(
|
||||
const hasChangedRef =
|
||||
sha !== head &&
|
||||
(await getCommitOid(
|
||||
env,
|
||||
checkoutPath,
|
||||
ref.replace(/^refs\/pull\//, "refs/remotes/pull/"),
|
||||
)) !== head;
|
||||
@@ -416,23 +410,20 @@ function removeRefsHeadsPrefix(ref: string): string {
|
||||
* environment variable can be set in cases where repository information might not be available, for
|
||||
* example dynamic workflows.
|
||||
*/
|
||||
export async function isAnalyzingDefaultBranch(
|
||||
env: ReadOnlyEnv,
|
||||
checkoutPath: string | undefined,
|
||||
): Promise<boolean> {
|
||||
if (env.getOptional("CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH") === "true") {
|
||||
export async function isAnalyzingDefaultBranch(): Promise<boolean> {
|
||||
if (process.env.CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH === "true") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get the current ref and trim and refs/heads/ prefix
|
||||
let currentRef = await getRef(env, checkoutPath);
|
||||
let currentRef = await getRef();
|
||||
currentRef = removeRefsHeadsPrefix(currentRef);
|
||||
|
||||
const event = getWorkflowEvent(env);
|
||||
const event = getWorkflowEvent();
|
||||
let defaultBranch = event?.repository?.default_branch;
|
||||
|
||||
if (getWorkflowEventName(env) === "schedule") {
|
||||
defaultBranch = removeRefsHeadsPrefix(getRefFromEnv(env));
|
||||
if (getWorkflowEventName() === "schedule") {
|
||||
defaultBranch = removeRefsHeadsPrefix(getRefFromEnv());
|
||||
}
|
||||
|
||||
return currentRef === defaultBranch;
|
||||
|
||||
@@ -50,7 +50,7 @@ import {
|
||||
} from "./util";
|
||||
import {
|
||||
getCategoryInputOrThrow,
|
||||
getRepositoryRootOrThrow,
|
||||
getCheckoutPathInputOrThrow,
|
||||
getUploadInputOrThrow,
|
||||
getWorkflow,
|
||||
} from "./workflow";
|
||||
@@ -155,15 +155,7 @@ async function prepareFailedSarif(
|
||||
});
|
||||
}
|
||||
const category = getCategoryInputOrThrow(workflow, jobName, matrix);
|
||||
|
||||
// Try to determine the path at which the repository that we failed to analyse is checked out at.
|
||||
// We need this to relativise the paths in the SARIF.
|
||||
const checkoutPath = getRepositoryRootOrThrow(
|
||||
workflow,
|
||||
jobName,
|
||||
matrix,
|
||||
config,
|
||||
);
|
||||
const checkoutPath = getCheckoutPathInputOrThrow(workflow, jobName, matrix);
|
||||
|
||||
const result = await generateFailedSarif(
|
||||
logger,
|
||||
|
||||
@@ -114,10 +114,7 @@ async function run(action: ActionState<["Base", "Logger", "Env"]>) {
|
||||
// do this under these circumstances to avoid slowing down analyses for PRs
|
||||
// and where caching may not be enabled.
|
||||
if (
|
||||
(await gitUtils.isAnalyzingDefaultBranch(
|
||||
action.env,
|
||||
config.repositoryRoot,
|
||||
)) &&
|
||||
(await gitUtils.isAnalyzingDefaultBranch()) &&
|
||||
hasConfigKey(config, "dependencyCachingEnabled") &&
|
||||
config.dependencyCachingEnabled !== CachingKind.None
|
||||
) {
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
CleanupLevel,
|
||||
getBaseDatabaseOidsFilePath,
|
||||
getCodeQLDatabasePath,
|
||||
getEnv,
|
||||
getErrorMessage,
|
||||
isInTestMode,
|
||||
tryGetFolderBytes,
|
||||
@@ -291,7 +290,7 @@ export async function downloadOverlayBaseDatabaseFromCache(
|
||||
actionsCache.restoreCache(
|
||||
[dbLocation],
|
||||
cacheRestoreKeyPrefix,
|
||||
[cacheRestoreKeyPrefix],
|
||||
undefined,
|
||||
{
|
||||
// Azure SDK download (which is the default) uses 128MB segments; see
|
||||
// https://github.com/actions/toolkit/blob/main/packages/cache/README.md.
|
||||
@@ -378,7 +377,7 @@ export async function getCacheSaveKey(
|
||||
`Failed to get workflow run ID or attempt ID. Reason: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
const sha = await getCommitOid(getEnv(), checkoutPath);
|
||||
const sha = await getCommitOid(checkoutPath);
|
||||
const restoreKeyPrefix = await getCacheRestoreKeyPrefix(
|
||||
config,
|
||||
codeQlVersion,
|
||||
|
||||
@@ -130,7 +130,7 @@ export async function getOverlayStatus(
|
||||
await fs.promises.mkdir(path.dirname(statusFile), { recursive: true });
|
||||
const foundKey = await waitForResultWithTimeLimit(
|
||||
MAX_CACHE_OPERATION_MS,
|
||||
actionsCache.restoreCache([statusFile], cacheKey, [cacheKey]),
|
||||
actionsCache.restoreCache([statusFile], cacheKey),
|
||||
() => {
|
||||
logger.warning("Timed out restoring overlay status from cache.");
|
||||
},
|
||||
|
||||
@@ -6,14 +6,6 @@ export interface RepositoryNwo {
|
||||
repo: string;
|
||||
}
|
||||
|
||||
/** An approximation of what a repository NWO string looks like. */
|
||||
export type RepositoryNwoStr = `${string}/${string}`;
|
||||
|
||||
/** A basic check that `val` looks like a `RepositoryNwoStr`. */
|
||||
export function isRepositoryNwoStr(val: string): val is RepositoryNwoStr {
|
||||
return val.includes("/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the repository name with owner from the environment variable
|
||||
* `GITHUB_REPOSITORY`.
|
||||
|
||||
@@ -126,6 +126,24 @@ test.serial("convert to semver", (t) => {
|
||||
}
|
||||
});
|
||||
|
||||
test.serial("getCodeQLActionRepository", (t) => {
|
||||
const logger = getRunnerLogger(true);
|
||||
|
||||
initializeEnvironment("1.2.3");
|
||||
|
||||
// isRunningLocalAction() === true
|
||||
delete process.env["GITHUB_ACTION_REPOSITORY"];
|
||||
process.env["RUNNER_TEMP"] = path.dirname(__dirname);
|
||||
const repoLocalRunner = setupCodeql.getCodeQLActionRepository(logger);
|
||||
t.deepEqual(repoLocalRunner, "github/codeql-action");
|
||||
|
||||
// isRunningLocalAction() === false
|
||||
sinon.stub(actionsUtil, "isRunningLocalAction").returns(false);
|
||||
process.env["GITHUB_ACTION_REPOSITORY"] = "xxx/yyy";
|
||||
const repoEnv = setupCodeql.getCodeQLActionRepository(logger);
|
||||
t.deepEqual(repoEnv, "xxx/yyy");
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"getCodeQLSource sets CLI version for a semver tagged bundle",
|
||||
async (t) => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as path from "path";
|
||||
import { performance } from "perf_hooks";
|
||||
|
||||
import * as toolcache from "@actions/tool-cache";
|
||||
import { default as deepEqual } from "fast-deep-equal";
|
||||
import * as semver from "semver";
|
||||
import { v4 as uuidV4 } from "uuid";
|
||||
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
isAnalyzingPullRequest,
|
||||
isDynamicWorkflow,
|
||||
isGitHubHostedRunner,
|
||||
isRunningLocalAction,
|
||||
} from "./actions-util";
|
||||
import * as api from "./api-client";
|
||||
import {
|
||||
@@ -42,7 +44,6 @@ import {
|
||||
logPerLanguageBundleFallback,
|
||||
} from "./per-language-bundles";
|
||||
import { getBundlePlatform } from "./platform";
|
||||
import { getCodeQLAssetDownloadURL } from "./setup/repository";
|
||||
import * as tar from "./tar";
|
||||
import {
|
||||
deleteToolcacheBundles,
|
||||
@@ -64,6 +65,7 @@ export enum ToolsSource {
|
||||
Download = "DOWNLOAD",
|
||||
}
|
||||
|
||||
const CODEQL_DEFAULT_ACTION_REPOSITORY = "github/codeql-action";
|
||||
const CODEQL_NIGHTLIES_REPOSITORY_OWNER = "dsp-testing";
|
||||
const CODEQL_NIGHTLIES_REPOSITORY_NAME = "codeql-cli-nightlies";
|
||||
|
||||
@@ -71,6 +73,75 @@ const CODEQL_BUNDLE_VERSION_ALIAS: string[] = ["linked", "latest"];
|
||||
const CODEQL_NIGHTLY_TOOLS_INPUTS = ["nightly", "nightly-latest"];
|
||||
const CODEQL_TOOLCACHE_INPUT = "toolcache";
|
||||
|
||||
export function getCodeQLActionRepository(logger: Logger): string {
|
||||
if (isRunningLocalAction()) {
|
||||
// This handles the case where the Action does not come from an Action repository,
|
||||
// e.g. our integration tests which use the Action code from the current checkout.
|
||||
// In these cases, the GITHUB_ACTION_REPOSITORY environment variable is not set.
|
||||
logger.info(
|
||||
"The CodeQL Action is checked out locally. Using the default CodeQL Action repository.",
|
||||
);
|
||||
return CODEQL_DEFAULT_ACTION_REPOSITORY;
|
||||
}
|
||||
|
||||
return util.getRequiredEnvParam("GITHUB_ACTION_REPOSITORY");
|
||||
}
|
||||
|
||||
async function getCodeQLBundleDownloadURL(
|
||||
tagName: string,
|
||||
apiDetails: api.GitHubApiDetails,
|
||||
codeQLBundleName: string,
|
||||
logger: Logger,
|
||||
): Promise<string> {
|
||||
const codeQLActionRepository = getCodeQLActionRepository(logger);
|
||||
const potentialDownloadSources = [
|
||||
// This GitHub instance, and this Action.
|
||||
[apiDetails.url, codeQLActionRepository],
|
||||
// This GitHub instance, and the canonical Action.
|
||||
[apiDetails.url, CODEQL_DEFAULT_ACTION_REPOSITORY],
|
||||
// GitHub.com, and the canonical Action.
|
||||
[util.GITHUB_DOTCOM_URL, CODEQL_DEFAULT_ACTION_REPOSITORY],
|
||||
];
|
||||
// We now filter out any duplicates.
|
||||
// Duplicates will happen either because the GitHub instance is GitHub.com, or because the Action is not a fork.
|
||||
const uniqueDownloadSources = potentialDownloadSources.filter(
|
||||
(source, index, self) => {
|
||||
return !self.slice(0, index).some((other) => deepEqual(source, other));
|
||||
},
|
||||
);
|
||||
for (const downloadSource of uniqueDownloadSources) {
|
||||
const [apiURL, repository] = downloadSource;
|
||||
// If we've reached the final case, short-circuit the API check since we know the bundle exists and is public.
|
||||
if (
|
||||
apiURL === util.GITHUB_DOTCOM_URL &&
|
||||
repository === CODEQL_DEFAULT_ACTION_REPOSITORY
|
||||
) {
|
||||
break;
|
||||
}
|
||||
const [repositoryOwner, repositoryName] = repository.split("/");
|
||||
try {
|
||||
const release = await api.getApiClient().rest.repos.getReleaseByTag({
|
||||
owner: repositoryOwner,
|
||||
repo: repositoryName,
|
||||
tag: tagName,
|
||||
});
|
||||
for (const asset of release.data.assets) {
|
||||
if (asset.name === codeQLBundleName) {
|
||||
logger.info(
|
||||
`Found CodeQL bundle ${codeQLBundleName} in ${repository} on ${apiURL} with URL ${asset.url}.`,
|
||||
);
|
||||
return asset.url;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.info(
|
||||
`Looked for CodeQL bundle ${codeQLBundleName} in ${repository} on ${apiURL} but got error ${e}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return `https://github.com/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${codeQLBundleName}`;
|
||||
}
|
||||
|
||||
function tryGetBundleVersionFromTagName(
|
||||
tagName: string,
|
||||
logger: Logger,
|
||||
@@ -649,10 +720,9 @@ export async function getCodeQLSource(
|
||||
? "zstd"
|
||||
: "gzip";
|
||||
|
||||
const action = { env: getEnv(), logger };
|
||||
const platform = getBundlePlatform();
|
||||
const perLanguageBundleLanguage = await getPerLanguageBundleLanguage(
|
||||
{ ...action, features },
|
||||
{ env: getEnv(), features, logger },
|
||||
{
|
||||
rawLanguages,
|
||||
otherLanguagePacksReason,
|
||||
@@ -665,11 +735,11 @@ export async function getCodeQLSource(
|
||||
|
||||
// Resolves the combined or per-language bundle URL for the requested release.
|
||||
const resolveBundleURL = (language?: BuiltInLanguage) =>
|
||||
getCodeQLAssetDownloadURL(
|
||||
action,
|
||||
apiDetails,
|
||||
getCodeQLBundleDownloadURL(
|
||||
bundleTagName,
|
||||
apiDetails,
|
||||
getCodeQLBundleName(compressionMethod, platform, language),
|
||||
logger,
|
||||
);
|
||||
|
||||
const combinedBundleURL = await resolveBundleURL();
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import * as path from "path";
|
||||
|
||||
import test from "ava";
|
||||
import * as sinon from "sinon";
|
||||
|
||||
import * as actionsUtil from "../actions-util";
|
||||
import { ActionsEnvVars } from "../environment";
|
||||
import { callee, getTestEnv, setupTests } from "../testing-utils";
|
||||
import { initializeEnvironment } from "../util";
|
||||
|
||||
import { getCodeQLActionRepository } from "./repository";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
test.serial("getCodeQLActionRepository", async (t) => {
|
||||
const initialEnv = getTestEnv({
|
||||
[ActionsEnvVars.RUNNER_TEMP]: path.dirname(__dirname),
|
||||
});
|
||||
initializeEnvironment("1.2.3", initialEnv);
|
||||
|
||||
const target = callee(getCodeQLActionRepository)
|
||||
.withArgs()
|
||||
.withEnv(initialEnv);
|
||||
|
||||
// isRunningLocalAction() === true
|
||||
await target.passes(t.deepEqual, "github/codeql-action");
|
||||
|
||||
// isRunningLocalAction() === false
|
||||
sinon.stub(actionsUtil, "isRunningLocalAction").returns(false);
|
||||
await target
|
||||
.withEnv((env) => {
|
||||
env.set(ActionsEnvVars.GITHUB_ACTION_REPOSITORY, "xxx/yyy");
|
||||
})
|
||||
.passes(t.deepEqual, "xxx/yyy");
|
||||
});
|
||||
@@ -1,158 +0,0 @@
|
||||
import { ActionState } from "../action-common";
|
||||
import { isRunningLocalAction } from "../actions-util";
|
||||
import * as api from "../api-client";
|
||||
import { ActionsEnvVars } from "../environment";
|
||||
import {
|
||||
isRepositoryNwoStr,
|
||||
parseRepositoryNwo,
|
||||
type RepositoryNwoStr,
|
||||
} from "../repository";
|
||||
import { GITHUB_DOTCOM_URL, type InstanceURL } from "../util";
|
||||
|
||||
/** The NWO of the standard CodeQL Action repository. */
|
||||
export const CODEQL_DEFAULT_ACTION_REPOSITORY = "github/codeql-action";
|
||||
|
||||
/**
|
||||
* Retrieves the NWO of the repository that the CodeQL Action is loaded from.
|
||||
* Typically, this will be `github/codeql-action`, but it may be something else for
|
||||
* forks of the Action or in environments other than GitHub.com.
|
||||
*
|
||||
* If this function detects that we are running the Action locally (e.g. in tests),
|
||||
* then we default to `CODEQL_DEFAULT_ACTION_REPOSITORY`.
|
||||
*/
|
||||
export function getCodeQLActionRepository(
|
||||
action: ActionState<["ReadOnlyEnv", "Logger"]>,
|
||||
): RepositoryNwoStr {
|
||||
if (isRunningLocalAction(action.env)) {
|
||||
// This handles the case where the Action does not come from an Action repository,
|
||||
// e.g. our integration tests which use the Action code from the current checkout.
|
||||
// In these cases, the GITHUB_ACTION_REPOSITORY environment variable is not set.
|
||||
action.logger.info(
|
||||
"The CodeQL Action is checked out locally. Using the default CodeQL Action repository.",
|
||||
);
|
||||
return CODEQL_DEFAULT_ACTION_REPOSITORY;
|
||||
}
|
||||
|
||||
const actionRepository = action.env.getRequired(
|
||||
ActionsEnvVars.GITHUB_ACTION_REPOSITORY,
|
||||
);
|
||||
|
||||
if (isRepositoryNwoStr(actionRepository)) {
|
||||
return actionRepository;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`The value of '${ActionsEnvVars.GITHUB_ACTION_REPOSITORY}' is '${actionRepository}', which does not match the expected 'owner/repo' format.`,
|
||||
);
|
||||
}
|
||||
|
||||
/**A download source is a pair of strings. */
|
||||
export type DownloadSource = [InstanceURL, RepositoryNwoStr];
|
||||
|
||||
/** GitHub.com, and the canonical Action. */
|
||||
export const DEFAULT_DOWNLOAD_SOURCE: DownloadSource = [
|
||||
GITHUB_DOTCOM_URL,
|
||||
CODEQL_DEFAULT_ACTION_REPOSITORY,
|
||||
];
|
||||
|
||||
/** Decides if the two provided download sources are the same. */
|
||||
const isSameDownloadSource = (
|
||||
[srcUrl, srcRepo]: DownloadSource,
|
||||
[otherUrl, otherRepo]: DownloadSource,
|
||||
) => {
|
||||
return srcUrl === otherUrl && srcRepo === otherRepo;
|
||||
};
|
||||
|
||||
/** A download URL is represented as a string. */
|
||||
export type DownloadURL = string;
|
||||
|
||||
/** Gets the default GitHub.com download URL for `assetName` in a release tagged `tagName`. */
|
||||
export function getDefaultDotcomDownloadURL(
|
||||
tagName: string,
|
||||
assetName: string,
|
||||
): DownloadURL {
|
||||
return `${GITHUB_DOTCOM_URL}/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${assetName}`;
|
||||
}
|
||||
|
||||
/** Enumerates log-friendly descriptions of different kinds of assets we might look for in releases. */
|
||||
export type ReleaseAssetKind = "CodeQL bundle" | "private registry proxy";
|
||||
|
||||
/**
|
||||
* Tries to find a download URL for `assetName` in a release tagged with `tagName`.
|
||||
*
|
||||
* Depending on where and how the CodeQL Action is running, we may be using different `apiDetails` and there may be
|
||||
* different options for where to source CodeQL releases from.
|
||||
*
|
||||
* This function either returns the download URL for the asset for the first release we find, or
|
||||
* defaults to the assumed download URL for the asset on the default CodeQL Action repository on GitHub.com.
|
||||
* In the latter case, this function does not guarantee that the asset actually exists.
|
||||
*
|
||||
* @param action The Action state.
|
||||
* @param apiDetails The details of the GitHub API in use.
|
||||
* @param tagName The name of the release tag we want to obtain the asset from.
|
||||
* @param assetName The name of the asset we should look for in the release.
|
||||
* @param [assetKind="CodeQL bundle"] The kind of asset we are looking for to show in log messages.
|
||||
* @returns A URL that we can use to download the asset.
|
||||
*/
|
||||
export async function getCodeQLAssetDownloadURL(
|
||||
action: ActionState<["ReadOnlyEnv", "Logger"]>,
|
||||
apiDetails: api.GitHubApiDetails,
|
||||
tagName: string,
|
||||
assetName: string,
|
||||
assetKind: ReleaseAssetKind = "CodeQL bundle",
|
||||
): Promise<DownloadURL> {
|
||||
const codeQLActionRepository = getCodeQLActionRepository(action);
|
||||
|
||||
const potentialDownloadSources: DownloadSource[] = [
|
||||
// This GitHub instance, and this Action.
|
||||
[apiDetails.url, codeQLActionRepository],
|
||||
// This GitHub instance, and the canonical Action.
|
||||
[apiDetails.url, CODEQL_DEFAULT_ACTION_REPOSITORY],
|
||||
// GitHub.com, and the canonical Action.
|
||||
DEFAULT_DOWNLOAD_SOURCE,
|
||||
];
|
||||
|
||||
// We now filter out any duplicates.
|
||||
// Duplicates will happen either because the GitHub instance is GitHub.com, or because the Action is not a fork.
|
||||
const uniqueDownloadSources = potentialDownloadSources.filter(
|
||||
(source, index, self) => {
|
||||
return !self
|
||||
.slice(0, index)
|
||||
.some((other) => isSameDownloadSource(source, other));
|
||||
},
|
||||
);
|
||||
|
||||
for (const [instanceURL, repository] of uniqueDownloadSources) {
|
||||
// If we've reached the final case, short-circuit the API check since we know the asset exists and is public.
|
||||
if (
|
||||
isSameDownloadSource(DEFAULT_DOWNLOAD_SOURCE, [instanceURL, repository])
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
const { owner: repositoryOwner, repo: repositoryName } =
|
||||
parseRepositoryNwo(repository);
|
||||
try {
|
||||
const release = await api.getApiClient().rest.repos.getReleaseByTag({
|
||||
owner: repositoryOwner,
|
||||
repo: repositoryName,
|
||||
tag: tagName,
|
||||
});
|
||||
|
||||
for (const asset of release.data.assets) {
|
||||
if (asset.name === assetName) {
|
||||
action.logger.info(
|
||||
`Found ${assetKind} ${assetName} in ${repository} on ${instanceURL} with URL ${asset.url}.`,
|
||||
);
|
||||
return asset.url;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
action.logger.info(
|
||||
`Looked for ${assetKind} ${assetName} in ${repository} on ${instanceURL} but got error ${e}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return getDefaultDotcomDownloadURL(tagName, assetName);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
runInActions,
|
||||
} from "./action-common";
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { getApiDetails, getGitHubVersion } from "./api-client";
|
||||
import { getGitHubVersion } from "./api-client";
|
||||
import { FeatureEnablement, initFeatures } from "./feature-flags";
|
||||
import { BuiltInLanguage, parseBuiltInLanguage } from "./languages";
|
||||
import { Logger } from "./logging";
|
||||
@@ -104,12 +104,9 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
|
||||
|
||||
// Start the Proxy
|
||||
const actionWithFeatures = extendActionState(action, { features });
|
||||
const proxyBin = await getProxyBinaryPath(
|
||||
actionWithFeatures,
|
||||
getApiDetails(action.env),
|
||||
);
|
||||
const proxyBin = await getProxyBinaryPath(actionWithFeatures);
|
||||
const proxyInfo = await startProxy(
|
||||
proxyBin.path,
|
||||
proxyBin,
|
||||
proxyConfig,
|
||||
proxyLogFilePath,
|
||||
logger,
|
||||
@@ -125,7 +122,6 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
|
||||
languages: language === undefined ? undefined : [language],
|
||||
},
|
||||
proxyConfig.all_credentials.map((c) => c.type),
|
||||
{ proxy_version: proxyBin.version },
|
||||
logger,
|
||||
);
|
||||
} catch (unwrappedError) {
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import * as filepath from "path";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import * as toolcache from "@actions/tool-cache";
|
||||
import test, { ExecutionContext } from "ava";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { ActionState } from "./action-common";
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import * as apiClient from "./api-client";
|
||||
import * as defaults from "./defaults.json";
|
||||
import { ActionsEnvVars } from "./environment";
|
||||
import { setUpFeatureFlagTests } from "./feature-flags/testing-util";
|
||||
import { UnvalidatedObject, validateSchema } from "./json";
|
||||
import { makeFromSchema } from "./json/testing-util";
|
||||
import { BuiltInLanguage } from "./languages";
|
||||
import { getRunnerLogger, Logger } from "./logging";
|
||||
import { BundlePlatform, getBundlePlatform } from "./platform";
|
||||
import * as repository from "./setup/repository";
|
||||
import * as startProxyExports from "./start-proxy";
|
||||
import * as statusReport from "./status-report";
|
||||
import {
|
||||
addSinonAssertions,
|
||||
assertNotLogged,
|
||||
checkExpectedLogMessages,
|
||||
createFeatures,
|
||||
@@ -26,14 +24,97 @@ import {
|
||||
makeMacro,
|
||||
makeTestToken,
|
||||
RecordingLogger,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
setupTests,
|
||||
withRecordingLoggerAsync,
|
||||
} from "./testing-utils";
|
||||
import { GitHubVariant, GitHubVersion, withTmpDir } from "./util";
|
||||
import {
|
||||
ConfigurationError,
|
||||
GitHubVariant,
|
||||
GitHubVersion,
|
||||
withTmpDir,
|
||||
} from "./util";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
const sendFailedStatusReportTest = makeMacro({
|
||||
exec: async (
|
||||
t: ExecutionContext<unknown>,
|
||||
err: Error,
|
||||
expectedMessage: string,
|
||||
expectedStatus: statusReport.ActionStatus = "failure",
|
||||
) => {
|
||||
const now = new Date();
|
||||
|
||||
// Override core.setFailed to avoid it setting the program's exit code
|
||||
sinon.stub(core, "setFailed").returns();
|
||||
|
||||
const createStatusReportBase = sinon.stub(
|
||||
statusReport,
|
||||
"createStatusReportBase",
|
||||
);
|
||||
createStatusReportBase.resolves(undefined);
|
||||
|
||||
await withRecordingLoggerAsync(async (logger) => {
|
||||
await startProxyExports.sendFailedStatusReport(
|
||||
logger,
|
||||
now,
|
||||
undefined,
|
||||
err,
|
||||
);
|
||||
|
||||
// Check that the stub has been called exactly once, with the expected arguments,
|
||||
// but not with the message from the error.
|
||||
sinon.assert.calledOnceWithExactly(
|
||||
createStatusReportBase,
|
||||
statusReport.ActionName.StartProxy,
|
||||
expectedStatus,
|
||||
now,
|
||||
sinon.match.any,
|
||||
sinon.match.any,
|
||||
sinon.match.any,
|
||||
expectedMessage,
|
||||
);
|
||||
t.false(
|
||||
createStatusReportBase.calledWith(
|
||||
statusReport.ActionName.StartProxy,
|
||||
expectedStatus,
|
||||
now,
|
||||
sinon.match.any,
|
||||
sinon.match.any,
|
||||
sinon.match.any,
|
||||
sinon.match((msg: string) => msg.includes(err.message)),
|
||||
),
|
||||
"createStatusReportBase was called with the error message",
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
title: (providedTitle = "") => `sendFailedStatusReport - ${providedTitle}`,
|
||||
});
|
||||
|
||||
sendFailedStatusReportTest.serial(
|
||||
"reports generic error message for non-StartProxyError error",
|
||||
new Error("Something went wrong today"),
|
||||
"Error from start-proxy Action omitted (Error).",
|
||||
);
|
||||
|
||||
sendFailedStatusReportTest.serial(
|
||||
"reports generic error message for non-StartProxyError error with safe error message",
|
||||
new Error(
|
||||
startProxyExports.getStartProxyErrorMessage(
|
||||
startProxyExports.StartProxyErrorType.DownloadFailed,
|
||||
),
|
||||
),
|
||||
"Error from start-proxy Action omitted (Error).",
|
||||
);
|
||||
|
||||
sendFailedStatusReportTest.serial(
|
||||
"reports generic error message for ConfigurationError error",
|
||||
new ConfigurationError("Something went wrong today"),
|
||||
"Error from start-proxy Action omitted (ConfigurationError).",
|
||||
"user-error",
|
||||
);
|
||||
|
||||
const toEncodedJSON = (data: any) =>
|
||||
Buffer.from(JSON.stringify(data)).toString("base64");
|
||||
|
||||
@@ -624,29 +705,18 @@ function mockGetApiClient(endpoints: any) {
|
||||
|
||||
type ReleaseAssets = Array<{ name: string; url?: string }>;
|
||||
|
||||
function mockGetReleaseByTag(expectedTag?: string, assets?: ReleaseAssets) {
|
||||
function mockGetReleaseByTag(assets?: ReleaseAssets) {
|
||||
const getReleaseByTag =
|
||||
assets === undefined
|
||||
? sinon.stub().rejects()
|
||||
: sinon.stub().callsFake(({ tag }) => {
|
||||
if (tag === expectedTag) {
|
||||
return {
|
||||
status: 200,
|
||||
data: { assets },
|
||||
headers: {},
|
||||
url: "GET /repos/:owner/:repo/releases/tags/:tag",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 404,
|
||||
};
|
||||
}
|
||||
: sinon.stub().resolves({
|
||||
status: 200,
|
||||
data: { assets },
|
||||
headers: {},
|
||||
url: "GET /repos/:owner/:repo/releases/tags/:tag",
|
||||
});
|
||||
|
||||
return {
|
||||
getReleaseByTag,
|
||||
getApiClient: mockGetApiClient({ repos: { getReleaseByTag } }),
|
||||
};
|
||||
return mockGetApiClient({ repos: { getReleaseByTag } });
|
||||
}
|
||||
|
||||
function mockOfflineFeatures(tempDir: string, logger: Logger) {
|
||||
@@ -681,33 +751,25 @@ test.serial(
|
||||
logger,
|
||||
features,
|
||||
});
|
||||
const info = await startProxyExports.getDownloadUrl(state);
|
||||
|
||||
// Stub this to always return `true` so we get the default repo for the tests.
|
||||
sinon.stub(actionsUtil, "isRunningLocalAction").returns(true);
|
||||
|
||||
const info = await startProxyExports.getDownloadUrl(
|
||||
state,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION);
|
||||
t.is(
|
||||
info.url,
|
||||
startProxyExports.getFallbackUrl(
|
||||
startProxyExports.getProxyPackage(BundlePlatform.Linux64),
|
||||
),
|
||||
);
|
||||
|
||||
t.true(
|
||||
logger.hasMessage(`Unsupported platform android on architecture ppc`),
|
||||
);
|
||||
|
||||
t.is(info.version, defaults.cliVersion);
|
||||
t.is(
|
||||
info.url,
|
||||
repository.getDefaultDotcomDownloadURL(
|
||||
defaults.bundleVersion,
|
||||
startProxyExports.getProxyPackage(BundlePlatform.Linux64),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getDownloadUrl returns default bundle URL for linked version",
|
||||
"getDownloadUrl returns fallback when `getReleaseByVersion` rejects",
|
||||
async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
mockGetReleaseByTag();
|
||||
@@ -715,20 +777,12 @@ test.serial(
|
||||
await withTmpDir(async (tempDir) => {
|
||||
const features = mockOfflineFeatures(tempDir, logger);
|
||||
const state = initAllState({ logger, features });
|
||||
const info = await startProxyExports.getDownloadUrl(state);
|
||||
|
||||
// Stub this to always return `true` so we get the default repo for the tests.
|
||||
sinon.stub(actionsUtil, "isRunningLocalAction").returns(true);
|
||||
|
||||
const info = await startProxyExports.getDownloadUrl(
|
||||
state,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
);
|
||||
|
||||
t.is(info.version, defaults.cliVersion);
|
||||
t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION);
|
||||
t.is(
|
||||
info.url,
|
||||
repository.getDefaultDotcomDownloadURL(
|
||||
defaults.bundleVersion,
|
||||
startProxyExports.getFallbackUrl(
|
||||
startProxyExports.getProxyPackage(getTestPlatform(state)),
|
||||
),
|
||||
);
|
||||
@@ -737,7 +791,7 @@ test.serial(
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getDownloadUrl doesn't check that the default asset exists",
|
||||
"getDownloadUrl returns fallback when there's no matching release asset",
|
||||
async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const testAssets = [[], [{ name: "foo" }]];
|
||||
@@ -746,28 +800,19 @@ test.serial(
|
||||
const features = mockOfflineFeatures(tempDir, logger);
|
||||
const state = initAllState({ logger, features });
|
||||
|
||||
// Stub this to always return `true` so we get the default repo for the tests.
|
||||
sinon.stub(actionsUtil, "isRunningLocalAction").returns(true);
|
||||
|
||||
for (const assets of testAssets) {
|
||||
const stubs = mockGetReleaseByTag(defaults.bundleVersion, assets);
|
||||
const info = await startProxyExports.getDownloadUrl(
|
||||
state,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
);
|
||||
const stub = mockGetReleaseByTag(assets);
|
||||
const info = await startProxyExports.getDownloadUrl(state);
|
||||
|
||||
t.is(info.version, defaults.cliVersion);
|
||||
t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION);
|
||||
t.is(
|
||||
info.url,
|
||||
repository.getDefaultDotcomDownloadURL(
|
||||
defaults.bundleVersion,
|
||||
startProxyExports.getFallbackUrl(
|
||||
startProxyExports.getProxyPackage(getTestPlatform(state)),
|
||||
),
|
||||
);
|
||||
|
||||
// mockGetReleaseByTag stubs the global getApiClient; since we are calling
|
||||
// that in a loop, we need to manually restore it here
|
||||
stubs.getApiClient.restore();
|
||||
stub.restore();
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -783,22 +828,11 @@ test.serial("getDownloadUrl returns matching release asset", async (t) => {
|
||||
url: "url-we-want",
|
||||
},
|
||||
];
|
||||
const stubs = mockGetReleaseByTag(defaults.bundleVersion, assets);
|
||||
mockGetReleaseByTag(assets);
|
||||
|
||||
await withTmpDir(async (tempDir) => {
|
||||
state.env.set(ActionsEnvVars.RUNNER_TEMP, tempDir);
|
||||
const features = mockOfflineFeatures(tempDir, logger);
|
||||
|
||||
// Stub this to always return `false` so we force `getCodeQLAssetDownloadURL` to use the (mocked) API.
|
||||
sinon.stub(actionsUtil, "isRunningLocalAction").returns(false);
|
||||
state.env.set(ActionsEnvVars.GITHUB_ACTION_REPOSITORY, "test/repo");
|
||||
|
||||
const info = await startProxyExports.getDownloadUrl(
|
||||
{ ...state, features },
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
);
|
||||
|
||||
t.is(stubs.getReleaseByTag.callCount, 1);
|
||||
const info = await startProxyExports.getDownloadUrl({ ...state, features });
|
||||
|
||||
t.is(info.version, defaults.cliVersion);
|
||||
t.is(info.url, "url-we-want");
|
||||
@@ -931,21 +965,16 @@ test.serial(
|
||||
const toolcachePath = "/path/to/proxy/dir";
|
||||
sinon.stub(toolcache, "find").returns(toolcachePath);
|
||||
|
||||
// Stub this to always return `true` so we get the default repo for the tests.
|
||||
sinon.stub(actionsUtil, "isRunningLocalAction").returns(true);
|
||||
|
||||
const features = mockOfflineFeatures(tempDir, logger);
|
||||
const result = await startProxyExports.getProxyBinaryPath(
|
||||
const path = await startProxyExports.getProxyBinaryPath(
|
||||
initAllState({ logger, features }),
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
);
|
||||
|
||||
t.assert(result);
|
||||
t.assert(path);
|
||||
t.is(
|
||||
result.path,
|
||||
path,
|
||||
filepath.join(toolcachePath, startProxyExports.getProxyFilename()),
|
||||
);
|
||||
t.is(result.version, defaults.cliVersion);
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -956,7 +985,7 @@ test.serial(
|
||||
const logger = new RecordingLogger();
|
||||
const state = initAllState({ logger });
|
||||
const downloadUrl = "url-we-want";
|
||||
const stubs = mockGetReleaseByTag(defaults.bundleVersion, [
|
||||
mockGetReleaseByTag([
|
||||
{
|
||||
name: startProxyExports.getProxyPackage(getTestPlatform(state)),
|
||||
url: downloadUrl,
|
||||
@@ -965,6 +994,11 @@ test.serial(
|
||||
|
||||
const toolcachePath = "/path/to/proxy/dir";
|
||||
const find = sinon.stub(toolcache, "find").returns("");
|
||||
const getApiDetails = sinon.stub(apiClient, "getApiDetails").returns({
|
||||
auth: "",
|
||||
url: "",
|
||||
apiURL: "",
|
||||
});
|
||||
const getAuthorizationHeaderFor = sinon
|
||||
.stub(apiClient, "getAuthorizationHeaderFor")
|
||||
.returns(undefined);
|
||||
@@ -978,36 +1012,25 @@ test.serial(
|
||||
.resolves(extractedPath);
|
||||
const cacheDir = sinon.stub(toolcache, "cacheDir").resolves(toolcachePath);
|
||||
|
||||
// Stub this to always return `false` so we force `getCodeQLAssetDownloadURL` to use the (mocked) API.
|
||||
sinon.stub(actionsUtil, "isRunningLocalAction").returns(false);
|
||||
state.env.set(ActionsEnvVars.GITHUB_ACTION_REPOSITORY, "test/repo");
|
||||
const path = await startProxyExports.getProxyBinaryPath({
|
||||
...state,
|
||||
features: createFeatures([]),
|
||||
});
|
||||
|
||||
const result = await startProxyExports.getProxyBinaryPath(
|
||||
{
|
||||
...state,
|
||||
features: createFeatures([]),
|
||||
},
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
);
|
||||
|
||||
t.is(stubs.getReleaseByTag.callCount, 1);
|
||||
t.assert(find.calledOnce);
|
||||
t.assert(getApiDetails.calledOnce);
|
||||
t.assert(getAuthorizationHeaderFor.calledOnce);
|
||||
|
||||
const st = addSinonAssertions(t);
|
||||
st.onceWith(downloadTool, downloadUrl);
|
||||
st.onceWith(extractTar, archivePath);
|
||||
st.onceWith(cacheDir, extractedPath);
|
||||
|
||||
t.assert(result);
|
||||
t.assert(downloadTool.calledOnceWith(downloadUrl));
|
||||
t.assert(extractTar.calledOnceWith(archivePath));
|
||||
t.assert(cacheDir.calledOnceWith(extractedPath));
|
||||
t.assert(path);
|
||||
t.is(
|
||||
result.path,
|
||||
path,
|
||||
filepath.join(toolcachePath, startProxyExports.getProxyFilename()),
|
||||
);
|
||||
t.is(result.version, defaults.cliVersion);
|
||||
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
`Found private registry proxy ${startProxyExports.getProxyPackage(getTestPlatform(state))} in test/repo on https://github.com with URL ${downloadUrl}`,
|
||||
`Found '${startProxyExports.getProxyPackage(getTestPlatform(state))}' in release '${defaults.bundleVersion}' at '${downloadUrl}'`,
|
||||
]);
|
||||
},
|
||||
);
|
||||
@@ -1017,11 +1040,10 @@ test.serial(
|
||||
async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const state = initAllState({ logger });
|
||||
const expectedVersion = "2.20.1";
|
||||
const expectedTag = `codeql-bundle-v${expectedVersion}`;
|
||||
const expectedTag = "codeql-bundle-v2.20.1";
|
||||
const expectedParams = {
|
||||
owner: "test",
|
||||
repo: "repo",
|
||||
owner: "github",
|
||||
repo: "codeql-action",
|
||||
tag: expectedTag,
|
||||
};
|
||||
const downloadUrl = "url-we-want";
|
||||
@@ -1044,6 +1066,11 @@ test.serial(
|
||||
await withTmpDir(async (tempDir) => {
|
||||
const toolcachePath = "/path/to/proxy/dir";
|
||||
const find = sinon.stub(toolcache, "find").returns("");
|
||||
const getApiDetails = sinon.stub(apiClient, "getApiDetails").returns({
|
||||
auth: "",
|
||||
url: "",
|
||||
apiURL: "",
|
||||
});
|
||||
const getAuthorizationHeaderFor = sinon
|
||||
.stub(apiClient, "getAuthorizationHeaderFor")
|
||||
.returns(undefined);
|
||||
@@ -1071,43 +1098,34 @@ test.serial(
|
||||
const getDefaultCliVersion = sinon
|
||||
.stub(features, "getEnabledDefaultCliVersions")
|
||||
.resolves({
|
||||
enabledVersions: [
|
||||
{ cliVersion: expectedVersion, tagName: expectedTag },
|
||||
],
|
||||
enabledVersions: [{ cliVersion: "2.20.1", tagName: expectedTag }],
|
||||
});
|
||||
|
||||
// Stub this to always return `false` so we force `getCodeQLAssetDownloadURL` to use the (mocked) API.
|
||||
sinon.stub(actionsUtil, "isRunningLocalAction").returns(false);
|
||||
state.env.set(ActionsEnvVars.GITHUB_ACTION_REPOSITORY, "test/repo");
|
||||
|
||||
const result = await startProxyExports.getProxyBinaryPath(
|
||||
{
|
||||
...state,
|
||||
features,
|
||||
},
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
);
|
||||
const path = await startProxyExports.getProxyBinaryPath({
|
||||
...state,
|
||||
features,
|
||||
});
|
||||
|
||||
t.assert(getDefaultCliVersion.calledOnce);
|
||||
|
||||
const st = addSinonAssertions(t);
|
||||
st.onceWith(getReleaseByTag, expectedParams);
|
||||
sinon.assert.calledOnceWithMatch(
|
||||
getReleaseByTag,
|
||||
sinon.match(expectedParams),
|
||||
);
|
||||
t.assert(find.calledOnce);
|
||||
t.assert(getApiDetails.calledOnce);
|
||||
t.assert(getAuthorizationHeaderFor.calledOnce);
|
||||
st.onceWith(downloadTool, downloadUrl);
|
||||
st.onceWith(extractTar, archivePath);
|
||||
st.onceWith(cacheDir, extractedPath);
|
||||
t.assert(downloadTool.calledOnceWith(downloadUrl));
|
||||
t.assert(extractTar.calledOnceWith(archivePath));
|
||||
t.assert(cacheDir.calledOnceWith(extractedPath));
|
||||
|
||||
t.assert(result);
|
||||
t.assert(path);
|
||||
t.is(
|
||||
result.path,
|
||||
path,
|
||||
filepath.join(toolcachePath, startProxyExports.getProxyFilename()),
|
||||
);
|
||||
t.is(result.version, expectedVersion);
|
||||
});
|
||||
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
`Found private registry proxy ${startProxyExports.getProxyPackage(getTestPlatform(state))} in test/repo on https://github.com with URL url-we-want.`,
|
||||
`Found '${startProxyExports.getProxyPackage(getTestPlatform(state))}' in release '${expectedTag}' at '${downloadUrl}'`,
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import * as path from "path";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import * as toolcache from "@actions/tool-cache";
|
||||
|
||||
import { ActionState } from "./action-common";
|
||||
import {
|
||||
getApiClient,
|
||||
getApiDetails,
|
||||
getAuthorizationHeaderFor,
|
||||
getGitHubVersion,
|
||||
GitHubApiDetails,
|
||||
} from "./api-client";
|
||||
import * as artifactScanner from "./artifact-scanner";
|
||||
import { Config } from "./config-utils";
|
||||
import * as defaults from "./defaults.json";
|
||||
import {
|
||||
CodeQLDefaultVersionInfo,
|
||||
@@ -19,11 +22,6 @@ import * as json from "./json";
|
||||
import { BuiltInLanguage } from "./languages";
|
||||
import { Logger } from "./logging";
|
||||
import { BundlePlatform, getBundlePlatform } from "./platform";
|
||||
import { getCodeQLAssetDownloadURL } from "./setup/repository";
|
||||
import {
|
||||
StartProxyError,
|
||||
StartProxyErrorType,
|
||||
} from "./start-proxy/status-report";
|
||||
import {
|
||||
Address,
|
||||
Registry,
|
||||
@@ -34,13 +32,149 @@ import {
|
||||
RawCredential,
|
||||
} from "./start-proxy/types";
|
||||
import { getAuthConfig } from "./start-proxy/validation";
|
||||
import {
|
||||
ActionName,
|
||||
createStatusReportBase,
|
||||
getActionsStatus,
|
||||
sendStatusReport,
|
||||
StatusReportBase,
|
||||
} from "./status-report";
|
||||
import * as util from "./util";
|
||||
import { ConfigurationError, getErrorMessage, isDefined } from "./util";
|
||||
|
||||
export * from "./start-proxy/types";
|
||||
export * from "./start-proxy/status-report";
|
||||
|
||||
/** The basename of the proxy artifacts. */
|
||||
/**
|
||||
* Enumerates specific error types for which we have corresponding error messages that
|
||||
* are safe to include in status reports.
|
||||
*/
|
||||
export enum StartProxyErrorType {
|
||||
DownloadFailed,
|
||||
ExtractionFailed,
|
||||
CacheFailed,
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The error message corresponding to the error type.
|
||||
*/
|
||||
export function getStartProxyErrorMessage(
|
||||
errorType: StartProxyErrorType,
|
||||
): string {
|
||||
switch (errorType) {
|
||||
case StartProxyErrorType.DownloadFailed:
|
||||
return "Failed to download proxy archive.";
|
||||
case StartProxyErrorType.ExtractionFailed:
|
||||
return "Failed to extract proxy archive.";
|
||||
case StartProxyErrorType.CacheFailed:
|
||||
return "Failed to add proxy to toolcache";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We want to avoid accidentally leaking secrets that may be contained in exception
|
||||
* messages in the `start-proxy` action. Consequently, we don't report the messages
|
||||
* of arbitrary exceptions. This type of error ensures that the message is one from
|
||||
* `StartProxyErrorType` and therefore safe to include in a status report.
|
||||
*/
|
||||
export class StartProxyError extends Error {
|
||||
public readonly errorType: StartProxyErrorType;
|
||||
|
||||
constructor(errorType: StartProxyErrorType) {
|
||||
super();
|
||||
this.errorType = errorType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a status report for the `start-proxy` action indicating a successful outcome.
|
||||
*
|
||||
* @param startedAt When the action was started.
|
||||
* @param config The configuration used.
|
||||
* @param registry_types The types of registries that are configured.
|
||||
* @param logger The logger to use.
|
||||
*/
|
||||
export async function sendSuccessStatusReport(
|
||||
startedAt: Date,
|
||||
config: Partial<Config>,
|
||||
registry_types: string[],
|
||||
logger: Logger,
|
||||
) {
|
||||
const statusReportBase = await createStatusReportBase(
|
||||
ActionName.StartProxy,
|
||||
"success",
|
||||
startedAt,
|
||||
config,
|
||||
await util.checkDiskUsage(logger),
|
||||
logger,
|
||||
);
|
||||
if (statusReportBase !== undefined) {
|
||||
const statusReport: StatusReportBase = {
|
||||
...statusReportBase,
|
||||
registry_types: registry_types.join(","),
|
||||
};
|
||||
await sendStatusReport(statusReport);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an error message for `error` that can safely be reported in a status report,
|
||||
* i.e. that does not contain sensitive information.
|
||||
*
|
||||
* @param error The error for which to get an error message.
|
||||
*/
|
||||
export function getSafeErrorMessage(error: Error): string {
|
||||
// If the error is a `StartProxyError`, resolve the error type to the corresponding
|
||||
// error message.
|
||||
if (error instanceof StartProxyError) {
|
||||
return getStartProxyErrorMessage(error.errorType);
|
||||
}
|
||||
|
||||
// Otherwise, omit the actual error message.
|
||||
return `Error from start-proxy Action omitted (${error.constructor.name}).`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a status report for the `start-proxy` action indicating a failure.
|
||||
*
|
||||
* @param logger The logger to use.
|
||||
* @param startedAt When the action was started.
|
||||
* @param language The language provided as input, if any.
|
||||
* @param unwrappedError The exception that was thrown.
|
||||
*/
|
||||
export async function sendFailedStatusReport(
|
||||
logger: Logger,
|
||||
startedAt: Date,
|
||||
language: BuiltInLanguage | undefined,
|
||||
unwrappedError: unknown,
|
||||
) {
|
||||
const error = util.wrapError(unwrappedError);
|
||||
core.setFailed(`start-proxy action failed: ${error.message}`);
|
||||
|
||||
// To avoid the possibility of leaking sensitive information into the telemetry,
|
||||
// we don't include arbitrary error messages. Instead, `getSafeErrorMessage` will
|
||||
// return a generic message that includes the type of the error, unless it can decide
|
||||
// that the message is safe to include.
|
||||
const statusReportMessage = getSafeErrorMessage(error);
|
||||
const errorStatusReportBase = await createStatusReportBase(
|
||||
ActionName.StartProxy,
|
||||
getActionsStatus(error),
|
||||
startedAt,
|
||||
{
|
||||
languages: language === undefined ? undefined : [language],
|
||||
},
|
||||
await util.checkDiskUsage(logger),
|
||||
logger,
|
||||
statusReportMessage,
|
||||
);
|
||||
if (errorStatusReportBase !== undefined) {
|
||||
await sendStatusReport(errorStatusReportBase);
|
||||
}
|
||||
}
|
||||
|
||||
export const UPDATEJOB_PROXY = "update-job-proxy";
|
||||
export const UPDATEJOB_PROXY_VERSION = "v2.0.20250624110901";
|
||||
const UPDATEJOB_PROXY_URL_PREFIX =
|
||||
"https://github.com/github/codeql-action/releases/download/codeql-bundle-v2.22.0/";
|
||||
|
||||
function isPAT(value: string) {
|
||||
return artifactScanner.isAuthToken(value, [
|
||||
@@ -245,6 +379,30 @@ export function getProxyPackage(platform: BundlePlatform): string {
|
||||
return `${UPDATEJOB_PROXY}-${platform}.tar.gz`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the fallback URL for downloading the proxy release asset.
|
||||
*
|
||||
* @param proxyPackage The asset name.
|
||||
* @returns The full URL to download the specified asset from the fallback release.
|
||||
*/
|
||||
export function getFallbackUrl(proxyPackage: string): string {
|
||||
return `${UPDATEJOB_PROXY_URL_PREFIX}${proxyPackage}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the GitHub API to obtain information about the CodeQL CLI bundle release
|
||||
* that is tagged by `version`.
|
||||
*
|
||||
* @returns The response from the GitHub API.
|
||||
*/
|
||||
async function getReleaseByVersion(version: string) {
|
||||
return getApiClient().rest.repos.getReleaseByTag({
|
||||
owner: "github",
|
||||
repo: "codeql-action",
|
||||
tag: version,
|
||||
});
|
||||
}
|
||||
|
||||
/** Uses `features` to determine the default CLI version. */
|
||||
async function getCliVersionFromFeatures(
|
||||
features: FeatureEnablement,
|
||||
@@ -261,8 +419,7 @@ async function getCliVersionFromFeatures(
|
||||
* @returns Returns the download URL and version of the proxy package we plan to use.
|
||||
*/
|
||||
export async function getDownloadUrl(
|
||||
action: ActionState<["Base", "ReadOnlyEnv", "Logger", "FeatureFlags"]>,
|
||||
apiDetails: GitHubApiDetails,
|
||||
action: ActionState<["Base", "Logger", "FeatureFlags"]>,
|
||||
): Promise<{ url: string; version: string }> {
|
||||
// Default to linux64 if we don't recognise the platform+arch pair.
|
||||
// This maintains the behaviour we had before switching to `getBundlePlatform` here.
|
||||
@@ -291,26 +448,37 @@ export async function getDownloadUrl(
|
||||
};
|
||||
|
||||
// Try to retrieve information about the CLI bundle release identified by `versionInfo`.
|
||||
const proxyAssetUrl = await getCodeQLAssetDownloadURL(
|
||||
action,
|
||||
apiDetails,
|
||||
versionInfo.tagName,
|
||||
proxyPackage,
|
||||
"private registry proxy",
|
||||
);
|
||||
const cliRelease = await getReleaseByVersion(versionInfo.tagName);
|
||||
|
||||
return {
|
||||
url: proxyAssetUrl,
|
||||
// The `update-job-proxy` doesn't have a version as such. Since we now bundle it
|
||||
// with CodeQL CLI bundle releases, we use the corresponding CLI version to
|
||||
// differentiate between (potentially) different versions of `update-job-proxy`.
|
||||
version: versionInfo.cliVersion,
|
||||
};
|
||||
// Search the release's assets to find the one we are looking for.
|
||||
for (const asset of cliRelease.data.assets) {
|
||||
if (asset.name === proxyPackage) {
|
||||
action.logger.info(
|
||||
`Found '${proxyPackage}' in release '${versionInfo.tagName}' at '${asset.url}'`,
|
||||
);
|
||||
return {
|
||||
url: asset.url,
|
||||
// The `update-job-proxy` doesn't have a version as such. Since we now bundle it
|
||||
// with CodeQL CLI bundle releases, we use the corresponding CLI version to
|
||||
// differentiate between (potentially) different versions of `update-job-proxy`.
|
||||
version: versionInfo.cliVersion,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
throw new Error(
|
||||
action.logger.warning(
|
||||
`Failed to retrieve information about the linked release: ${getErrorMessage(ex)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback to the hard-coded URL.
|
||||
action.logger.info(
|
||||
`Did not find '${proxyPackage}' in the linked release, falling back to hard-coded version.`,
|
||||
);
|
||||
return {
|
||||
url: getFallbackUrl(proxyPackage),
|
||||
version: UPDATEJOB_PROXY_VERSION,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -329,14 +497,9 @@ export async function downloadProxy(
|
||||
try {
|
||||
// Download the proxy archive from `url`. We let `downloadTool` choose where
|
||||
// to store it. The path to the downloaded file will be returned if successful.
|
||||
return await toolcache.downloadTool(
|
||||
url,
|
||||
/* dest: */ undefined,
|
||||
authorization,
|
||||
{
|
||||
accept: "application/octet-stream",
|
||||
},
|
||||
);
|
||||
return toolcache.downloadTool(url, /* dest: */ undefined, authorization, {
|
||||
accept: "application/octet-stream",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to download proxy archive from ${url}: ${getErrorMessage(error)}`,
|
||||
@@ -397,70 +560,37 @@ export function getProxyFilename() {
|
||||
: UPDATEJOB_PROXY;
|
||||
}
|
||||
|
||||
export type ProxyBinary = {
|
||||
/** The path to the proxy binary to run. */
|
||||
path: string;
|
||||
/**
|
||||
* The version associated with the proxy binary.
|
||||
* This usually corresponds to the CodeQL CLI release that the binary was obtained from.
|
||||
*/
|
||||
version: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a path to the proxy binary. If possible, this function will find the proxy in the
|
||||
* runner's tool cache. Otherwise, it downloads and extracts the proxy binary,
|
||||
* and stores it in the tool cache.
|
||||
*
|
||||
* @param action The action state.
|
||||
* @returns The path to the proxy binary and its version.
|
||||
* @returns The path to the proxy binary.
|
||||
*/
|
||||
export async function getProxyBinaryPath(
|
||||
action: ActionState<["Base", "ReadOnlyEnv", "Logger", "FeatureFlags"]>,
|
||||
apiDetails: GitHubApiDetails,
|
||||
): Promise<ProxyBinary> {
|
||||
action: ActionState<["Base", "Logger", "FeatureFlags"]>,
|
||||
): Promise<string> {
|
||||
const logger = action.logger;
|
||||
logger.startGroup("Setting up authentication proxy");
|
||||
const proxyFileName = getProxyFilename();
|
||||
const proxyInfo = await getDownloadUrl(action);
|
||||
|
||||
try {
|
||||
const proxyFileName = getProxyFilename();
|
||||
const proxyInfo = await getDownloadUrl(action, apiDetails);
|
||||
|
||||
logger.info(
|
||||
`Using authentication proxy from CodeQL release ${proxyInfo.version}.`,
|
||||
let proxyBin = toolcache.find(proxyFileName, proxyInfo.version);
|
||||
if (!proxyBin) {
|
||||
const apiDetails = getApiDetails();
|
||||
const authorization = getAuthorizationHeaderFor(
|
||||
logger,
|
||||
apiDetails,
|
||||
proxyInfo.url,
|
||||
);
|
||||
const temp = await downloadProxy(logger, proxyInfo.url, authorization);
|
||||
const extracted = await extractProxy(logger, temp);
|
||||
proxyBin = await cacheProxy(
|
||||
logger,
|
||||
extracted,
|
||||
proxyFileName,
|
||||
proxyInfo.version,
|
||||
);
|
||||
|
||||
let proxyDir = toolcache.find(proxyFileName, proxyInfo.version);
|
||||
|
||||
if (!proxyDir) {
|
||||
logger.info(
|
||||
`Authentication proxy is not in the toolcache. Will try to download it from '${proxyInfo.url}'.`,
|
||||
);
|
||||
|
||||
const authorization = getAuthorizationHeaderFor(
|
||||
logger,
|
||||
apiDetails,
|
||||
proxyInfo.url,
|
||||
);
|
||||
const temp = await downloadProxy(logger, proxyInfo.url, authorization);
|
||||
|
||||
logger.info(`Downloaded the authentication proxy asset to '${temp}'.`);
|
||||
|
||||
const extracted = await extractProxy(logger, temp);
|
||||
proxyDir = await cacheProxy(
|
||||
logger,
|
||||
extracted,
|
||||
proxyFileName,
|
||||
proxyInfo.version,
|
||||
);
|
||||
|
||||
logger.info(`Extracted the authentication proxy to '${proxyDir}'.`);
|
||||
}
|
||||
return {
|
||||
path: path.join(proxyDir, proxyFileName),
|
||||
version: proxyInfo.version,
|
||||
};
|
||||
} finally {
|
||||
logger.endGroup();
|
||||
}
|
||||
return path.join(proxyBin, proxyFileName);
|
||||
}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import * as core from "@actions/core";
|
||||
import test, { ExecutionContext } from "ava";
|
||||
import sinon from "sinon";
|
||||
|
||||
import * as statusReport from "../status-report";
|
||||
import {
|
||||
createTestConfig,
|
||||
makeMacro,
|
||||
RecordingLogger,
|
||||
setupTests,
|
||||
withRecordingLoggerAsync,
|
||||
} from "../testing-utils";
|
||||
import { ConfigurationError } from "../util";
|
||||
|
||||
import * as startProxyExports from "./status-report";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
test.serial("sendSuccessStatusReport", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const now = new Date();
|
||||
const config = createTestConfig({});
|
||||
const proxy_version = "1.2.3";
|
||||
const registry_types = ["git_source", "docker_registry"];
|
||||
|
||||
const createStatusReportBase = sinon.stub(
|
||||
statusReport,
|
||||
"createStatusReportBase",
|
||||
);
|
||||
createStatusReportBase.resolves({
|
||||
action_name: statusReport.ActionName.StartProxy,
|
||||
} as statusReport.StatusReportBase);
|
||||
|
||||
const sendStatusReport = sinon
|
||||
.stub(statusReport, "sendStatusReport")
|
||||
.resolves();
|
||||
|
||||
await startProxyExports.sendSuccessStatusReport(
|
||||
now,
|
||||
config,
|
||||
registry_types,
|
||||
{ proxy_version },
|
||||
logger,
|
||||
);
|
||||
|
||||
t.is(createStatusReportBase.callCount, 1);
|
||||
|
||||
t.is(sendStatusReport.callCount, 1);
|
||||
t.is(sendStatusReport.args[0].length, 1);
|
||||
|
||||
const arg = sendStatusReport.args[0][0];
|
||||
t.like(arg, {
|
||||
action_name: statusReport.ActionName.StartProxy,
|
||||
registry_types: "docker_registry,git_source",
|
||||
start_proxy: { proxy_version },
|
||||
});
|
||||
});
|
||||
|
||||
const sendFailedStatusReportTest = makeMacro({
|
||||
exec: async (
|
||||
t: ExecutionContext<unknown>,
|
||||
err: Error,
|
||||
expectedMessage: string,
|
||||
expectedStatus: statusReport.ActionStatus = "failure",
|
||||
) => {
|
||||
const now = new Date();
|
||||
|
||||
// Override core.setFailed to avoid it setting the program's exit code
|
||||
sinon.stub(core, "setFailed").returns();
|
||||
|
||||
const createStatusReportBase = sinon.stub(
|
||||
statusReport,
|
||||
"createStatusReportBase",
|
||||
);
|
||||
createStatusReportBase.resolves(undefined);
|
||||
|
||||
await withRecordingLoggerAsync(async (logger) => {
|
||||
await startProxyExports.sendFailedStatusReport(
|
||||
logger,
|
||||
now,
|
||||
undefined,
|
||||
err,
|
||||
);
|
||||
|
||||
// Check that the stub has been called exactly once, with the expected arguments,
|
||||
// but not with the message from the error.
|
||||
sinon.assert.calledOnceWithExactly(
|
||||
createStatusReportBase,
|
||||
statusReport.ActionName.StartProxy,
|
||||
expectedStatus,
|
||||
now,
|
||||
sinon.match.any,
|
||||
sinon.match.any,
|
||||
sinon.match.any,
|
||||
expectedMessage,
|
||||
);
|
||||
t.false(
|
||||
createStatusReportBase.calledWith(
|
||||
statusReport.ActionName.StartProxy,
|
||||
expectedStatus,
|
||||
now,
|
||||
sinon.match.any,
|
||||
sinon.match.any,
|
||||
sinon.match.any,
|
||||
sinon.match((msg: string) => msg.includes(err.message)),
|
||||
),
|
||||
"createStatusReportBase was called with the error message",
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
title: (providedTitle = "") => `sendFailedStatusReport - ${providedTitle}`,
|
||||
});
|
||||
|
||||
sendFailedStatusReportTest.serial(
|
||||
"reports generic error message for non-StartProxyError error",
|
||||
new Error("Something went wrong today"),
|
||||
"Error from start-proxy Action omitted (Error).",
|
||||
);
|
||||
|
||||
sendFailedStatusReportTest.serial(
|
||||
"reports generic error message for non-StartProxyError error with safe error message",
|
||||
new Error(
|
||||
startProxyExports.getStartProxyErrorMessage(
|
||||
startProxyExports.StartProxyErrorType.DownloadFailed,
|
||||
),
|
||||
),
|
||||
"Error from start-proxy Action omitted (Error).",
|
||||
);
|
||||
|
||||
sendFailedStatusReportTest.serial(
|
||||
"reports generic error message for ConfigurationError error",
|
||||
new ConfigurationError("Something went wrong today"),
|
||||
"Error from start-proxy Action omitted (ConfigurationError).",
|
||||
"user-error",
|
||||
);
|
||||
@@ -1,153 +0,0 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import type { Config } from "../config-utils";
|
||||
import type { BuiltInLanguage } from "../languages";
|
||||
import type { Logger } from "../logging";
|
||||
import {
|
||||
ActionName,
|
||||
createStatusReportBase,
|
||||
getActionsStatus,
|
||||
sendStatusReport,
|
||||
type StatusReportBase,
|
||||
} from "../status-report";
|
||||
import * as util from "../util";
|
||||
|
||||
/** Additional status report data from `start-proxy`. */
|
||||
export interface StartProxyStatus {
|
||||
proxy_version: string;
|
||||
}
|
||||
|
||||
/** The type of successful status reports from `start-proxy`. */
|
||||
type StartProxySuccessReport = StatusReportBase & {
|
||||
start_proxy: StartProxyStatus;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a status report for the `start-proxy` action indicating a successful outcome.
|
||||
*
|
||||
* @param startedAt When the action was started.
|
||||
* @param config The configuration used.
|
||||
* @param registry_types The types of registries that are configured.
|
||||
* @param status Status information.
|
||||
* @param logger The logger to use.
|
||||
*/
|
||||
export async function sendSuccessStatusReport(
|
||||
startedAt: Date,
|
||||
config: Partial<Config>,
|
||||
registry_types: string[],
|
||||
status: StartProxyStatus,
|
||||
logger: Logger,
|
||||
) {
|
||||
const statusReportBase = await createStatusReportBase(
|
||||
ActionName.StartProxy,
|
||||
"success",
|
||||
startedAt,
|
||||
config,
|
||||
await util.checkDiskUsage(logger),
|
||||
logger,
|
||||
);
|
||||
if (statusReportBase !== undefined) {
|
||||
const statusReport: StartProxySuccessReport = {
|
||||
...statusReportBase,
|
||||
registry_types: registry_types.sort().join(","),
|
||||
start_proxy: status,
|
||||
};
|
||||
await sendStatusReport(statusReport);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerates specific error types for which we have corresponding error messages that
|
||||
* are safe to include in status reports.
|
||||
*/
|
||||
export enum StartProxyErrorType {
|
||||
DownloadFailed,
|
||||
ExtractionFailed,
|
||||
CacheFailed,
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The error message corresponding to the error type.
|
||||
*/
|
||||
export function getStartProxyErrorMessage(
|
||||
errorType: StartProxyErrorType,
|
||||
): string {
|
||||
switch (errorType) {
|
||||
case StartProxyErrorType.DownloadFailed:
|
||||
return "Failed to download proxy archive.";
|
||||
case StartProxyErrorType.ExtractionFailed:
|
||||
return "Failed to extract proxy archive.";
|
||||
case StartProxyErrorType.CacheFailed:
|
||||
return "Failed to add proxy to toolcache";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We want to avoid accidentally leaking secrets that may be contained in exception
|
||||
* messages in the `start-proxy` action. Consequently, we don't report the messages
|
||||
* of arbitrary exceptions. This type of error ensures that the message is one from
|
||||
* `StartProxyErrorType` and therefore safe to include in a status report.
|
||||
*/
|
||||
export class StartProxyError extends Error {
|
||||
public readonly errorType: StartProxyErrorType;
|
||||
|
||||
constructor(errorType: StartProxyErrorType) {
|
||||
super();
|
||||
this.errorType = errorType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an error message for `error` that can safely be reported in a status report,
|
||||
* i.e. that does not contain sensitive information.
|
||||
*
|
||||
* @param error The error for which to get an error message.
|
||||
*/
|
||||
export function getSafeErrorMessage(error: Error): string {
|
||||
// If the error is a `StartProxyError`, resolve the error type to the corresponding
|
||||
// error message.
|
||||
if (error instanceof StartProxyError) {
|
||||
return getStartProxyErrorMessage(error.errorType);
|
||||
}
|
||||
|
||||
// Otherwise, omit the actual error message.
|
||||
return `Error from start-proxy Action omitted (${error.constructor.name}).`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a status report for the `start-proxy` action indicating a failure.
|
||||
*
|
||||
* @param logger The logger to use.
|
||||
* @param startedAt When the action was started.
|
||||
* @param language The language provided as input, if any.
|
||||
* @param unwrappedError The exception that was thrown.
|
||||
*/
|
||||
export async function sendFailedStatusReport(
|
||||
logger: Logger,
|
||||
startedAt: Date,
|
||||
language: BuiltInLanguage | undefined,
|
||||
unwrappedError: unknown,
|
||||
) {
|
||||
const error = util.wrapError(unwrappedError);
|
||||
core.setFailed(`start-proxy action failed: ${error.message}`);
|
||||
|
||||
// To avoid the possibility of leaking sensitive information into the telemetry,
|
||||
// we don't include arbitrary error messages. Instead, `getSafeErrorMessage` will
|
||||
// return a generic message that includes the type of the error, unless it can decide
|
||||
// that the message is safe to include.
|
||||
const statusReportMessage = getSafeErrorMessage(error);
|
||||
const errorStatusReportBase = await createStatusReportBase(
|
||||
ActionName.StartProxy,
|
||||
getActionsStatus(error),
|
||||
startedAt,
|
||||
{
|
||||
languages: language === undefined ? undefined : [language],
|
||||
},
|
||||
await util.checkDiskUsage(logger),
|
||||
logger,
|
||||
statusReportMessage,
|
||||
);
|
||||
if (errorStatusReportBase !== undefined) {
|
||||
await sendStatusReport(errorStatusReportBase);
|
||||
}
|
||||
}
|
||||
@@ -378,7 +378,7 @@ export async function createStatusReportBase(
|
||||
try {
|
||||
const commitOid =
|
||||
getOptionalInput("sha") || process.env["GITHUB_SHA"] || "";
|
||||
const ref = await getRef(getEnv(), config?.repositoryRoot);
|
||||
const ref = await getRef();
|
||||
const jobRunUUID = process.env[EnvVar.JOB_RUN_UUID] || "";
|
||||
const workflowRunID = getWorkflowRunID();
|
||||
const workflowRunAttempt = getWorkflowRunAttempt();
|
||||
|
||||
@@ -163,46 +163,6 @@ export function setupTests(testFn: TestFn<any>) {
|
||||
});
|
||||
}
|
||||
|
||||
export interface AvaSinonAssertions {
|
||||
/**
|
||||
* Asserts that `stub` must have been called once with at least the `expected` arguments.
|
||||
*/
|
||||
onceWith: <TArgs extends readonly any[]>(
|
||||
stub: sinon.SinonStub<TArgs>,
|
||||
...expected: sinon.MatchPartialArguments<TArgs>
|
||||
) => boolean;
|
||||
}
|
||||
|
||||
/** Adds additional assertions for use with `sinon` to `t`. */
|
||||
export function addSinonAssertions(
|
||||
t: ExecutionContext<unknown>,
|
||||
): ExecutionContext<unknown> & AvaSinonAssertions {
|
||||
return {
|
||||
...t,
|
||||
onceWith: <TArgs extends readonly any[]>(
|
||||
stub: sinon.SinonStub<TArgs>,
|
||||
...expected: sinon.MatchPartialArguments<TArgs>
|
||||
) => {
|
||||
// Fail if the stub hasn't been called at all so that we can safely compare
|
||||
// the arguments of the first call in the else branch.
|
||||
if (stub.callCount === 0) {
|
||||
t.fail("The stub wasn't called.");
|
||||
} else {
|
||||
// Reduce the arguments of each to the same number that was provided,
|
||||
// so that extra arguments don't lead to a failure.
|
||||
const actual = stub.args.map<sinon.MatchPartialArguments<TArgs>>(
|
||||
(args: TArgs) =>
|
||||
args.slice(
|
||||
0,
|
||||
expected.length,
|
||||
) as sinon.MatchPartialArguments<TArgs>,
|
||||
);
|
||||
return t.deepEqual(actual, [expected]);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare a reusable test implementation, with better type safety than `test.macro`.
|
||||
*/
|
||||
@@ -1006,7 +966,6 @@ export function createTestConfig(overrides: Partial<Config>): Config {
|
||||
{},
|
||||
{
|
||||
version: getActionVersion(),
|
||||
repositoryRoot: undefined,
|
||||
analysisKinds: [AnalysisKind.CodeScanning],
|
||||
languages: [],
|
||||
buildMode: undefined,
|
||||
|
||||
@@ -182,7 +182,6 @@ test.serial(
|
||||
stubCodeql,
|
||||
[BuiltInLanguage.javascript, BuiltInLanguage.cpp],
|
||||
logger,
|
||||
undefined,
|
||||
);
|
||||
t.assert(
|
||||
stubRestore.calledOnceWith(
|
||||
|
||||
@@ -14,7 +14,6 @@ import { Language } from "./languages";
|
||||
import { Logger } from "./logging";
|
||||
import {
|
||||
asHTTPError,
|
||||
getEnv,
|
||||
getErrorMessage,
|
||||
tryGetFolderBytes,
|
||||
waitForResultWithTimeLimit,
|
||||
@@ -44,7 +43,6 @@ const MAX_CACHE_OPERATION_MS = 120_000; // Two minutes
|
||||
* @param codeql The CodeQL instance to use.
|
||||
* @param languages The languages being analyzed.
|
||||
* @param logger A logger to record some informational messages to.
|
||||
* @param repositoryRoot The path at which the repository is checked out at.
|
||||
* @returns A partial map from languages to TRAP cache paths on disk, with
|
||||
* languages for which we shouldn't use TRAP caching omitted.
|
||||
*/
|
||||
@@ -52,7 +50,6 @@ export async function downloadTrapCaches(
|
||||
codeql: CodeQL,
|
||||
languages: Language[],
|
||||
logger: Logger,
|
||||
repositoryRoot: string | undefined,
|
||||
): Promise<{ [language: string]: string }> {
|
||||
const result: { [language: string]: string } = {};
|
||||
const languagesSupportingCaching = await getLanguagesSupportingCaching(
|
||||
@@ -75,7 +72,7 @@ export async function downloadTrapCaches(
|
||||
result[language] = cacheDir;
|
||||
}
|
||||
|
||||
if (await gitUtils.isAnalyzingDefaultBranch(getEnv(), repositoryRoot)) {
|
||||
if (await gitUtils.isAnalyzingDefaultBranch()) {
|
||||
logger.info(
|
||||
"Analyzing default branch. Skipping downloading of TRAP caches.",
|
||||
);
|
||||
@@ -102,7 +99,6 @@ export async function downloadTrapCaches(
|
||||
const found = await waitForResultWithTimeLimit(
|
||||
MAX_CACHE_OPERATION_MS,
|
||||
actionsCache.restoreCache([cacheDir], preferredKey, [
|
||||
preferredKey,
|
||||
// Fall back to any cache with the right key prefix
|
||||
await cachePrefix(codeql, language),
|
||||
]),
|
||||
@@ -136,12 +132,7 @@ export async function uploadTrapCaches(
|
||||
config: Config,
|
||||
logger: Logger,
|
||||
): Promise<boolean> {
|
||||
// Only upload caches from the default branch
|
||||
if (
|
||||
!(await gitUtils.isAnalyzingDefaultBranch(getEnv(), config.repositoryRoot))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!(await gitUtils.isAnalyzingDefaultBranch())) return false; // Only upload caches from the default branch
|
||||
|
||||
for (const language of config.languages) {
|
||||
const cacheDir = config.trapCaches[language];
|
||||
@@ -189,8 +180,6 @@ export async function cleanupTrapCaches(
|
||||
features: FeatureEnablement,
|
||||
logger: Logger,
|
||||
): Promise<TrapCacheCleanupStatusReport> {
|
||||
const env = getEnv();
|
||||
|
||||
if (!(await features.getValue(Feature.CleanupTrapCaches))) {
|
||||
return {
|
||||
trap_cache_cleanup_skipped_because: "feature disabled",
|
||||
@@ -200,7 +189,7 @@ export async function cleanupTrapCaches(
|
||||
"TRAP cache cleanup is deprecated and will be removed in May 2026. " +
|
||||
"We recommend instead disabling TRAP caching by passing the `trap-caching: false` input to the `init` Action.",
|
||||
);
|
||||
if (!(await gitUtils.isAnalyzingDefaultBranch(env, config.repositoryRoot))) {
|
||||
if (!(await gitUtils.isAnalyzingDefaultBranch())) {
|
||||
return {
|
||||
trap_cache_cleanup_skipped_because: "not analyzing default branch",
|
||||
};
|
||||
@@ -211,7 +200,7 @@ export async function cleanupTrapCaches(
|
||||
|
||||
const allCaches = await apiClient.listActionsCaches(
|
||||
CODEQL_TRAP_CACHE_PREFIX,
|
||||
await gitUtils.getRef(env, config.repositoryRoot),
|
||||
await gitUtils.getRef(),
|
||||
);
|
||||
|
||||
for (const language of config.languages) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { getGitHubVersion, wrapApiConfigurationError } from "./api-client";
|
||||
import { CodeQL, getCodeQL } from "./codeql";
|
||||
import { getConfig } from "./config-utils";
|
||||
import { readDiffRangesJsonFile } from "./diff-informed-analysis-utils";
|
||||
import { ActionsEnvVars, EnvVar, exportEnvVar } from "./environment";
|
||||
import { EnvVar, exportEnvVar } from "./environment";
|
||||
import { FeatureEnablement } from "./feature-flags";
|
||||
import * as fingerprints from "./fingerprints";
|
||||
import * as gitUtils from "./git-utils";
|
||||
@@ -762,13 +762,12 @@ export async function uploadPostProcessedFiles(
|
||||
const zippedSarif = zlib.gzipSync(sarifPayload).toString("base64");
|
||||
const checkoutURI = url.pathToFileURL(checkoutPath).href;
|
||||
|
||||
const env = util.getEnv();
|
||||
const payload = uploadTarget.transformPayload(
|
||||
buildPayload(
|
||||
await gitUtils.getCommitOid(env, checkoutPath),
|
||||
await gitUtils.getRef(env, checkoutPath),
|
||||
await gitUtils.getCommitOid(checkoutPath),
|
||||
await gitUtils.getRef(),
|
||||
postProcessingResults.analysisKey,
|
||||
env.getRequired(ActionsEnvVars.GITHUB_WORKFLOW),
|
||||
util.getRequiredEnvParam("GITHUB_WORKFLOW"),
|
||||
zippedSarif,
|
||||
actionsUtil.getWorkflowRunID(),
|
||||
actionsUtil.getWorkflowRunAttempt(),
|
||||
|
||||
19
src/util.ts
19
src/util.ts
@@ -20,8 +20,6 @@ import {
|
||||
getRequiredEnvParam,
|
||||
isInTestMode,
|
||||
exportEnvVar,
|
||||
getEnv,
|
||||
type Env,
|
||||
} from "./environment";
|
||||
import * as json from "./json";
|
||||
import { Language } from "./languages";
|
||||
@@ -47,13 +45,10 @@ const BASE_DATABASE_OIDS_FILE_NAME = "base-database-oids.json";
|
||||
*/
|
||||
const BROKEN_VERSIONS = ["0.0.0-20211207"];
|
||||
|
||||
/** A string representing a GitHub instance. */
|
||||
export type InstanceURL = string;
|
||||
|
||||
/**
|
||||
* The URL for github.com.
|
||||
*/
|
||||
export const GITHUB_DOTCOM_URL: InstanceURL = "https://github.com";
|
||||
export const GITHUB_DOTCOM_URL = "https://github.com";
|
||||
|
||||
/**
|
||||
* Default name of the debugging artifact.
|
||||
@@ -604,12 +599,12 @@ export function assertNever(value: never): never {
|
||||
* Set some initial environment variables that we can set even without
|
||||
* knowing what version of CodeQL we're running.
|
||||
*/
|
||||
export function initializeEnvironment(version: string, env: Env = getEnv()) {
|
||||
env.export(EnvVar.FEATURE_MULTI_LANGUAGE, "false");
|
||||
env.export(EnvVar.FEATURE_SANDWICH, "false");
|
||||
env.export(EnvVar.FEATURE_SARIF_COMBINE, "true");
|
||||
env.export(EnvVar.FEATURE_WILL_UPLOAD, "true");
|
||||
env.export(EnvVar.VERSION, version);
|
||||
export function initializeEnvironment(version: string) {
|
||||
exportEnvVar(EnvVar.FEATURE_MULTI_LANGUAGE, "false");
|
||||
exportEnvVar(EnvVar.FEATURE_SANDWICH, "false");
|
||||
exportEnvVar(EnvVar.FEATURE_SARIF_COMBINE, "true");
|
||||
exportEnvVar(EnvVar.FEATURE_WILL_UPLOAD, "true");
|
||||
exportEnvVar(EnvVar.VERSION, version);
|
||||
}
|
||||
|
||||
export class HTTPError extends Error {
|
||||
|
||||
@@ -4,12 +4,10 @@ import * as sinon from "sinon";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { createStubCodeQL, getCodeQLForTesting } from "./codeql";
|
||||
import { ActionsEnvVars, EnvVar } from "./environment";
|
||||
import { EnvVar } from "./environment";
|
||||
import {
|
||||
checkExpectedLogMessages,
|
||||
createTestConfig,
|
||||
getRecordingLogger,
|
||||
getTestEnv,
|
||||
LoggedMessage,
|
||||
setupTests,
|
||||
} from "./testing-utils";
|
||||
@@ -1004,124 +1002,3 @@ test.serial(
|
||||
t.is(messages.length, 0);
|
||||
},
|
||||
);
|
||||
|
||||
test("getRepositoryRootOrThrow - gets root from config", async (t) => {
|
||||
const expectedRoot = "/path/to/root";
|
||||
const repositoryRoot = workflow.getRepositoryRootOrThrow(
|
||||
{},
|
||||
"testJob",
|
||||
{},
|
||||
createTestConfig({ repositoryRoot: expectedRoot }),
|
||||
getTestEnv(),
|
||||
);
|
||||
|
||||
t.is(repositoryRoot, expectedRoot);
|
||||
});
|
||||
|
||||
test("getRepositoryRootOrThrow - gets root from workflow", async (t) => {
|
||||
const expectedRoot = "/path/to/root";
|
||||
const repositoryRoot = workflow.getRepositoryRootOrThrow(
|
||||
{
|
||||
jobs: {
|
||||
testJob: {
|
||||
steps: [
|
||||
{
|
||||
uses: "github/codeql-action/analyze",
|
||||
with: { checkout_path: expectedRoot },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"testJob",
|
||||
{},
|
||||
createTestConfig({}),
|
||||
getTestEnv(),
|
||||
);
|
||||
|
||||
t.is(repositoryRoot, expectedRoot);
|
||||
});
|
||||
|
||||
test("getRepositoryRootOrThrow - gets root from environment", async (t) => {
|
||||
const expectedRoot = "/path/to/root";
|
||||
const repositoryRoot = workflow.getRepositoryRootOrThrow(
|
||||
{
|
||||
jobs: { testJob: { steps: [{ uses: "github/codeql-action/analyze" }] } },
|
||||
},
|
||||
"testJob",
|
||||
{},
|
||||
createTestConfig({}),
|
||||
getTestEnv({ [ActionsEnvVars.GITHUB_WORKSPACE]: expectedRoot }),
|
||||
);
|
||||
|
||||
t.is(repositoryRoot, expectedRoot);
|
||||
});
|
||||
|
||||
test("getRepositoryRootOrThrow - throws if there's no matching job", async (t) => {
|
||||
t.throws(
|
||||
() =>
|
||||
workflow.getRepositoryRootOrThrow(
|
||||
{
|
||||
jobs: {
|
||||
otherJob: {
|
||||
steps: [
|
||||
{
|
||||
uses: "github/codeql-action/analyze",
|
||||
with: { checkout_path: "/some/path" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"testJob",
|
||||
{},
|
||||
createTestConfig({}),
|
||||
getTestEnv(),
|
||||
),
|
||||
{ message: /since the workflow has no job named testJob./ },
|
||||
);
|
||||
});
|
||||
|
||||
test("getRepositoryRootOrThrow - throws if there's no analyze step", async (t) => {
|
||||
t.throws(
|
||||
() =>
|
||||
workflow.getRepositoryRootOrThrow(
|
||||
{
|
||||
jobs: {
|
||||
testJob: { steps: [] },
|
||||
},
|
||||
},
|
||||
"testJob",
|
||||
{},
|
||||
createTestConfig({}),
|
||||
getTestEnv(),
|
||||
),
|
||||
{ message: /since the testJob job does not call/ },
|
||||
);
|
||||
});
|
||||
|
||||
test("getRepositoryRootOrThrow - throws if the env var is not set", async (t) => {
|
||||
t.throws(
|
||||
() =>
|
||||
workflow.getRepositoryRootOrThrow(
|
||||
{
|
||||
jobs: {
|
||||
testJob: {
|
||||
steps: [
|
||||
{
|
||||
uses: "github/codeql-action/analyze",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"testJob",
|
||||
{},
|
||||
createTestConfig({}),
|
||||
getTestEnv(),
|
||||
),
|
||||
{
|
||||
message: `${ActionsEnvVars.GITHUB_WORKSPACE} environment variable must be set`,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,8 +8,7 @@ import * as yaml from "js-yaml";
|
||||
import { isDynamicWorkflow } from "./actions-util";
|
||||
import * as api from "./api-client";
|
||||
import { CodeQL } from "./codeql";
|
||||
import type { Config } from "./config-utils";
|
||||
import { ActionsEnvVars, EnvVar, getEnv, ReadOnlyEnv } from "./environment";
|
||||
import { EnvVar } from "./environment";
|
||||
import { Logger } from "./logging";
|
||||
import {
|
||||
getRequiredEnvParam,
|
||||
@@ -442,45 +441,27 @@ export function getUploadInputOrThrow(
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a best effort attempt to determine the root path of the repository that the analysis
|
||||
* relates to. We need that to make paths in SARIF files relative.
|
||||
*
|
||||
* - If available, we take the `repositoryRoot` from the `config`.
|
||||
* - If it isn't, we fall back to trying to extract a `checkout_path` input from the `workflow`.
|
||||
* - Finally, we fall back to the value of `GITHUB_WORKSPACE`.
|
||||
* Makes a best effort attempt to retrieve the checkout_path input for the
|
||||
* particular job, given a set of matrix variables.
|
||||
*
|
||||
* Typically you'll want to wrap this function in a try/catch block and handle the error.
|
||||
*
|
||||
* @param workflow The workflow specification of the currently running workflow.
|
||||
* @param jobName The name of the job that is currently running.
|
||||
* @param matrixVars The matrix variables, if any.
|
||||
* @param config The CodeQL Action configuration state.
|
||||
* @param env The environment variables.
|
||||
*
|
||||
* @returns The repository root path, or its best approximation.
|
||||
* @throws `Error` if the repository root could not be determined.
|
||||
* @returns the checkout_path input
|
||||
* @throws an error if the checkout_path input could not be determined
|
||||
*/
|
||||
export function getRepositoryRootOrThrow(
|
||||
export function getCheckoutPathInputOrThrow(
|
||||
workflow: Workflow,
|
||||
jobName: string,
|
||||
matrixVars: { [key: string]: string } | undefined,
|
||||
config: Partial<Config>,
|
||||
env: ReadOnlyEnv = getEnv(),
|
||||
): string {
|
||||
return (
|
||||
// If the CodeQL Action already has a persisted repository root, then we can just use that.
|
||||
config.repositoryRoot ??
|
||||
// Otherwise, try to retrieve it from a `checkout_path` input in the workflow specification.
|
||||
getInputOrThrow(
|
||||
workflow,
|
||||
jobName,
|
||||
getAnalyzeActionName(),
|
||||
"checkout_path",
|
||||
matrixVars,
|
||||
) ??
|
||||
// Finally, if all of the above fail, just use the value of `GITHUB_WORKSPACE` since that
|
||||
// is what is used by default.
|
||||
env.getRequired(ActionsEnvVars.GITHUB_WORKSPACE)
|
||||
) || getRequiredEnvParam("GITHUB_WORKSPACE") // if unspecified, checkout_path defaults to ${{ github.workspace }}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user