mirror of
https://github.com/github/codeql-action.git
synced 2026-10-21 02:01:27 +00:00
Compare commits
8 Commits
mbg/use-gi
...
mbg/ts/rem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e5cfb2878 | ||
|
|
7fbadd1a21 | ||
|
|
7e36708483 | ||
|
|
fccc166683 | ||
|
|
f8f91fb336 | ||
|
|
58f39a7d42 | ||
|
|
40b3f0c1bc | ||
|
|
6e32e882a9 |
@@ -54,16 +54,15 @@ runs:
|
|||||||
env:
|
env:
|
||||||
CODEQL_ACTION_TEST_MODE: 'true'
|
CODEQL_ACTION_TEST_MODE: 'true'
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
shell: bash
|
|
||||||
run: npm install --location=global ts-node js-yaml
|
|
||||||
|
|
||||||
- name: Check config
|
- name: Check config
|
||||||
working-directory: ${{ github.action_path }}
|
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
EXPECTED_CONFIG_FILE_CONTENTS: '${{ inputs.expected-config-file-contents }}'
|
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
|
- name: Clean up
|
||||||
shell: bash
|
shell: bash
|
||||||
if: always()
|
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.
|
Comma separated list of query ids that should NOT be included in this SARIF file.
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: node24
|
using: "composite"
|
||||||
main: index.js
|
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
|
npm ci
|
||||||
|
|
||||||
- name: Verify compiled JS up to date
|
- 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
|
- name: Run unit tests
|
||||||
if: always()
|
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
|
run: npm ci
|
||||||
|
|
||||||
- name: Update bundle
|
- name: Update bundle
|
||||||
uses: ./.github/actions/update-bundle
|
run: npx tsx pr-checks/update-bundle.ts
|
||||||
|
|
||||||
- name: Set up CodeQL CLI from new bundle
|
- name: Set up CodeQL CLI from new bundle
|
||||||
id: setup-codeql
|
id: setup-codeql
|
||||||
|
|||||||
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");
|
export const BUNDLE_METADATA_FILE = path.join(REPO_ROOT, "meta.json");
|
||||||
|
|
||||||
/** The `src` directory. */
|
/** 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. */
|
/** The path to the built-in languages file. */
|
||||||
export const BUILTIN_LANGUAGES_FILE = path.join(
|
export const BUILTIN_LANGUAGES_FILE = path.join(
|
||||||
|
|||||||
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,12 +18,12 @@
|
|||||||
* [--dry-run]
|
* [--dry-run]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execFileSync, type ExecFileSyncOptions } from "node:child_process";
|
import { execFileSync } from "node:child_process";
|
||||||
import { parseArgs } from "node:util";
|
import { parseArgs } from "node:util";
|
||||||
|
|
||||||
import { type ApiClient, getApiClient } from "./api-client";
|
import { type ApiClient, getApiClient } from "./api-client";
|
||||||
import * as changelog from "./changelog";
|
import * as changelog from "./changelog";
|
||||||
import { DryRunOption, REPO_ROOT } from "./config";
|
import { runCommand, runGit } from "./command";
|
||||||
import {
|
import {
|
||||||
getCurrentVersion,
|
getCurrentVersion,
|
||||||
replaceVersionInPackageJson,
|
replaceVersionInPackageJson,
|
||||||
@@ -65,84 +65,6 @@ export function getGitHubToken(): string {
|
|||||||
throw new Error("Missing GitHub token. Set GITHUB_TOKEN or GH_TOKEN.");
|
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. */
|
/** Returns true if the given branch exists on the origin remote. */
|
||||||
export function branchExistsOnRemote(branchName: string): boolean {
|
export function branchExistsOnRemote(branchName: string): boolean {
|
||||||
const result = runGit(["ls-remote", "--heads", ORIGIN, branchName]);
|
const result = runGit(["ls-remote", "--heads", ORIGIN, branchName]);
|
||||||
|
|||||||
Reference in New Issue
Block a user