mirror of
https://github.com/github/codeql-action.git
synced 2026-05-31 19:04:43 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a6fad803a6 | |||
| e09b8dbe8a | |||
| 6e368b7e89 | |||
| 2ab95cc5ab | |||
| af6b899441 | |||
| 989d6f4689 | |||
| dbfd510456 | |||
| 4acf6eb187 | |||
| 8b990db7d7 |
@@ -52,6 +52,14 @@ jobs:
|
|||||||
- name: Verify compiled JS up to date
|
- name: Verify compiled JS up to date
|
||||||
run: .github/workflows/script/check-js.sh
|
run: .github/workflows/script/check-js.sh
|
||||||
|
|
||||||
|
- name: Upload esbuild metadata
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: bundle-metadata-${{ matrix.os }}-${{ matrix.node-version }}
|
||||||
|
path: build-metadata.json
|
||||||
|
retention-days: ${{ (github.ref_name == github.event.repository.default_branch && 90) || 7 }}
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
if: always()
|
if: always()
|
||||||
run: npm test
|
run: npm test
|
||||||
|
|||||||
+1
-1
@@ -12,4 +12,4 @@ eslint.sarif
|
|||||||
# for local incremental compilation
|
# for local incremental compilation
|
||||||
tsconfig.tsbuildinfo
|
tsconfig.tsbuildinfo
|
||||||
# esbuild metadata file
|
# esbuild metadata file
|
||||||
meta.json
|
build-metadata.json
|
||||||
|
|||||||
@@ -82,6 +82,6 @@ const context = await esbuild.context({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = await context.rebuild();
|
const result = await context.rebuild();
|
||||||
await writeFile(join(__dirname, "meta.json"), JSON.stringify(result.metafile));
|
await writeFile(join(__dirname, "build-metadata.json"), JSON.stringify(result.metafile));
|
||||||
|
|
||||||
await context.dispose();
|
await context.dispose();
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
"description": "CodeQL action",
|
"description": "CodeQL action",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"_build_comment": "echo 'Run the full build so we typecheck the project and can reuse the transpiled files in npm test'",
|
"_build_comment": "echo 'Run the full build so we typecheck the project and can reuse the transpiled files in npm test'",
|
||||||
"build": "./scripts/check-node-modules.sh && npm run transpile && node build.mjs && npx tsx ./pr-checks/bundle-metadata.ts",
|
"build": "./scripts/check-node-modules.sh && npm run transpile && node build.mjs",
|
||||||
"lint": "eslint --report-unused-disable-directives --max-warnings=0 .",
|
"lint": "eslint --report-unused-disable-directives --max-warnings=0 .",
|
||||||
"lint-ci": "SARIF_ESLINT_IGNORE_SUPPRESSED=true eslint --report-unused-disable-directives --max-warnings=0 . --format @microsoft/eslint-formatter-sarif --output-file=eslint.sarif",
|
"lint-ci": "SARIF_ESLINT_IGNORE_SUPPRESSED=true eslint --report-unused-disable-directives --max-warnings=0 . --format @microsoft/eslint-formatter-sarif --output-file=eslint.sarif",
|
||||||
"lint-fix": "eslint --report-unused-disable-directives --max-warnings=0 . --fix",
|
"lint-fix": "eslint --report-unused-disable-directives --max-warnings=0 . --fix",
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
|
import { ParseArgsConfig } from "node:util";
|
||||||
|
|
||||||
import * as githubUtils from "@actions/github/lib/utils";
|
import * as githubUtils from "@actions/github/lib/utils";
|
||||||
import { type Octokit } from "@octokit/core";
|
import { type Octokit } from "@octokit/core";
|
||||||
import { type PaginateInterface } from "@octokit/plugin-paginate-rest";
|
import { type PaginateInterface } from "@octokit/plugin-paginate-rest";
|
||||||
import { type Api } from "@octokit/plugin-rest-endpoint-methods";
|
import { type Api } from "@octokit/plugin-rest-endpoint-methods";
|
||||||
|
|
||||||
|
/** Identifies the CodeQL Action repository. */
|
||||||
|
export const CODEQL_ACTION_REPO = {
|
||||||
|
owner: "github",
|
||||||
|
repo: "codeql-action",
|
||||||
|
};
|
||||||
|
|
||||||
/** The type of the Octokit client. */
|
/** The type of the Octokit client. */
|
||||||
export type ApiClient = Octokit & Api & { paginate: PaginateInterface };
|
export type ApiClient = Octokit & Api & { paginate: PaginateInterface };
|
||||||
|
|
||||||
@@ -11,3 +19,16 @@ export function getApiClient(token: string): ApiClient {
|
|||||||
const opts = githubUtils.getOctokitOptions(token);
|
const opts = githubUtils.getOctokitOptions(token);
|
||||||
return new githubUtils.GitHub(opts);
|
return new githubUtils.GitHub(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TokenOption {
|
||||||
|
/** The token to use to authenticate to the GitHub API. */
|
||||||
|
token?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Command-line argument parser settings for the token parameter. */
|
||||||
|
export const TOKEN_OPTION_CONFIG = {
|
||||||
|
// The token to use to authenticate to the API.
|
||||||
|
token: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
} satisfies ParseArgsConfig["options"];
|
||||||
|
|||||||
@@ -1,8 +1,43 @@
|
|||||||
#!/usr/bin/env npx tsx
|
#!/usr/bin/env npx tsx
|
||||||
|
|
||||||
import * as fs from "node:fs/promises";
|
import * as fs from "node:fs/promises";
|
||||||
|
import { parseArgs, ParseArgsConfig } from "node:util";
|
||||||
|
|
||||||
import { BUNDLE_METADATA_FILE } from "./config";
|
import * as exec from "@actions/exec";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ApiClient,
|
||||||
|
CODEQL_ACTION_REPO,
|
||||||
|
getApiClient,
|
||||||
|
TOKEN_OPTION_CONFIG,
|
||||||
|
} from "./api-client";
|
||||||
|
import { BASELINE_BUNDLE_METADATA_FILE, BUNDLE_METADATA_FILE } from "./config";
|
||||||
|
|
||||||
|
const optionsConfig = {
|
||||||
|
...TOKEN_OPTION_CONFIG,
|
||||||
|
branch: {
|
||||||
|
type: "string",
|
||||||
|
default: "main",
|
||||||
|
},
|
||||||
|
runner: {
|
||||||
|
type: "string",
|
||||||
|
default: "macos-latest",
|
||||||
|
},
|
||||||
|
"node-version": {
|
||||||
|
type: "string",
|
||||||
|
default: "24",
|
||||||
|
},
|
||||||
|
} satisfies ParseArgsConfig["options"];
|
||||||
|
|
||||||
|
function parseOptions() {
|
||||||
|
const { values: options } = parseArgs({
|
||||||
|
options: optionsConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options = ReturnType<typeof parseOptions>;
|
||||||
|
|
||||||
interface InputInfo {
|
interface InputInfo {
|
||||||
bytesInOutput: number;
|
bytesInOutput: number;
|
||||||
@@ -23,21 +58,125 @@ function toMB(bytes: number): string {
|
|||||||
return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
|
return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getBaselineFrom(client: ApiClient, options: Options) {
|
||||||
|
const workflowRun = await client.rest.actions.listWorkflowRuns({
|
||||||
|
...CODEQL_ACTION_REPO,
|
||||||
|
branch: options.branch,
|
||||||
|
workflow_id: "pr-checks.yml",
|
||||||
|
status: "success",
|
||||||
|
per_page: 1,
|
||||||
|
event: "push",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (workflowRun.data.total_count === 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Expected to find a 'pr-checks.yml' run for '${options.branch}', but found none.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedArtifactName = `bundle-metadata-${options.runner}-${options["node-version"]}`;
|
||||||
|
const artifacts = await client.rest.actions.listWorkflowRunArtifacts({
|
||||||
|
...CODEQL_ACTION_REPO,
|
||||||
|
run_id: workflowRun.data.workflow_runs[0].id,
|
||||||
|
name: expectedArtifactName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (artifacts.data.total_count === 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Expected to find an artifact named '${expectedArtifactName}', but found none.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadInfo = await client.rest.actions.downloadArtifact({
|
||||||
|
...CODEQL_ACTION_REPO,
|
||||||
|
artifact_id: artifacts.data.artifacts[0].id,
|
||||||
|
archive_format: "zip",
|
||||||
|
});
|
||||||
|
|
||||||
|
// This works fine for us with our version of Octokit, so we don't need to
|
||||||
|
// worry about over-complicating this script and handle other possibilities.
|
||||||
|
if (downloadInfo.data instanceof ArrayBuffer) {
|
||||||
|
const archivePath = `${expectedArtifactName}.zip`;
|
||||||
|
await fs.writeFile(archivePath, Buffer.from(downloadInfo.data));
|
||||||
|
|
||||||
|
console.info(`Extracting zip file: ${archivePath}`);
|
||||||
|
await exec.exec("unzip", ["-o", archivePath, "-d", "."]);
|
||||||
|
|
||||||
|
// We no longer need the archive after unzipping it.
|
||||||
|
await fs.rm(archivePath);
|
||||||
|
|
||||||
|
// Check that we have the expected file.
|
||||||
|
try {
|
||||||
|
await fs.stat(BASELINE_BUNDLE_METADATA_FILE);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(
|
||||||
|
`Expected '${BASELINE_BUNDLE_METADATA_FILE}' to have been extracted, but it does not exist: ${err}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baselineData = await fs.readFile(BASELINE_BUNDLE_METADATA_FILE);
|
||||||
|
return JSON.parse(String(baselineData)) as Metadata;
|
||||||
|
} else {
|
||||||
|
throw new Error("Expected to receive artifact data, but didn't.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
const options = parseOptions();
|
||||||
|
|
||||||
|
if (options.token === undefined) {
|
||||||
|
throw new Error("Missing --token");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialise the API client.
|
||||||
|
const client = getApiClient(options.token);
|
||||||
|
const baselineMetadata = await getBaselineFrom(client, options);
|
||||||
|
|
||||||
const fileContents = await fs.readFile(BUNDLE_METADATA_FILE);
|
const fileContents = await fs.readFile(BUNDLE_METADATA_FILE);
|
||||||
const metadata = JSON.parse(String(fileContents)) as Metadata;
|
const metadata = JSON.parse(String(fileContents)) as Metadata;
|
||||||
|
|
||||||
|
console.info("Comparing bundle metadata to baseline...");
|
||||||
|
|
||||||
|
const filesInBaseline = new Set(Object.keys(baselineMetadata.outputs));
|
||||||
|
const filesInCurrent = new Set(Object.keys(metadata.outputs));
|
||||||
|
|
||||||
|
const filesNotPresent = filesInBaseline.difference(filesInCurrent);
|
||||||
|
if (filesNotPresent.size > 0) {
|
||||||
|
console.info(`Found ${filesNotPresent.size} file(s) which were removed:`);
|
||||||
|
for (const removedFile of filesNotPresent) {
|
||||||
|
console.info(` - ${removedFile}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const [outputFile, outputData] of Object.entries(
|
for (const [outputFile, outputData] of Object.entries(
|
||||||
metadata.outputs,
|
metadata.outputs,
|
||||||
).reverse()) {
|
).reverse()) {
|
||||||
console.info(`${outputFile}: ${toMB(outputData.bytes)}`);
|
const baselineOutputData = baselineMetadata.outputs[outputFile];
|
||||||
|
|
||||||
for (const [inputName, inputData] of Object.entries(outputData.inputs)) {
|
if (baselineOutputData === undefined) {
|
||||||
// Ignore any inputs that make up less than 5% of the output.
|
console.info(`${outputFile}: New file (${toMB(outputData.bytes)})`);
|
||||||
const percentage = (inputData.bytesInOutput / outputData.bytes) * 100.0;
|
} else {
|
||||||
if (percentage < 5.0) continue;
|
const percentageDifference =
|
||||||
|
((outputData.bytes - baselineOutputData.bytes) /
|
||||||
|
baselineOutputData.bytes) *
|
||||||
|
100.0;
|
||||||
|
|
||||||
console.info(` ${inputName}: ${toMB(inputData.bytesInOutput)}`);
|
if (Math.abs(percentageDifference) >= 5) {
|
||||||
|
console.info(
|
||||||
|
`${outputFile}: ${toMB(outputData.bytes)} (${percentageDifference.toFixed(2)}%)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [inputName, inputData] of Object.entries(
|
||||||
|
outputData.inputs,
|
||||||
|
)) {
|
||||||
|
// Ignore any inputs that make up less than 5% of the output.
|
||||||
|
const percentage =
|
||||||
|
(inputData.bytesInOutput / outputData.bytes) * 100.0;
|
||||||
|
if (percentage < 5.0) continue;
|
||||||
|
|
||||||
|
console.info(` ${inputName}: ${toMB(inputData.bytesInOutput)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-1
@@ -10,7 +10,17 @@ export const PR_CHECKS_DIR = __dirname;
|
|||||||
export const PR_CHECK_EXCLUDED_FILE = path.join(PR_CHECKS_DIR, "excluded.yml");
|
export const PR_CHECK_EXCLUDED_FILE = path.join(PR_CHECKS_DIR, "excluded.yml");
|
||||||
|
|
||||||
/** The path to the esbuild metadata file. */
|
/** The path to the esbuild metadata file. */
|
||||||
export const BUNDLE_METADATA_FILE = path.join(PR_CHECKS_DIR, "..", "meta.json");
|
export const BUNDLE_METADATA_FILE = path.join(
|
||||||
|
PR_CHECKS_DIR,
|
||||||
|
"..",
|
||||||
|
"build-metadata.json",
|
||||||
|
);
|
||||||
|
|
||||||
|
/** The path of the baseline esbuild metadata file, once extracted from a workflow artifact. */
|
||||||
|
export const BASELINE_BUNDLE_METADATA_FILE = path.join(
|
||||||
|
PR_CHECKS_DIR,
|
||||||
|
"build-metadata.json",
|
||||||
|
);
|
||||||
|
|
||||||
/** The `src` directory. */
|
/** The `src` directory. */
|
||||||
const SOURCE_ROOT = path.join(PR_CHECKS_DIR, "..", "src");
|
const SOURCE_ROOT = path.join(PR_CHECKS_DIR, "..", "src");
|
||||||
|
|||||||
+13
-18
@@ -7,16 +7,20 @@ import { parseArgs } from "node:util";
|
|||||||
|
|
||||||
import * as yaml from "yaml";
|
import * as yaml from "yaml";
|
||||||
|
|
||||||
import { type ApiClient, getApiClient } from "./api-client";
|
import {
|
||||||
|
type ApiClient,
|
||||||
|
CODEQL_ACTION_REPO,
|
||||||
|
getApiClient,
|
||||||
|
TOKEN_OPTION_CONFIG,
|
||||||
|
TokenOption,
|
||||||
|
} from "./api-client";
|
||||||
import {
|
import {
|
||||||
OLDEST_SUPPORTED_MAJOR_VERSION,
|
OLDEST_SUPPORTED_MAJOR_VERSION,
|
||||||
PR_CHECK_EXCLUDED_FILE,
|
PR_CHECK_EXCLUDED_FILE,
|
||||||
} from "./config";
|
} from "./config";
|
||||||
|
|
||||||
/** Represents the command-line options. */
|
/** Represents the command-line options. */
|
||||||
export interface Options {
|
export interface Options extends TokenOption {
|
||||||
/** The token to use to authenticate to the GitHub API. */
|
|
||||||
token?: string;
|
|
||||||
/** The git ref to use the checks for. */
|
/** The git ref to use the checks for. */
|
||||||
ref?: string;
|
ref?: string;
|
||||||
/** Whether to actually apply the changes or not. */
|
/** Whether to actually apply the changes or not. */
|
||||||
@@ -25,12 +29,6 @@ export interface Options {
|
|||||||
verbose: boolean;
|
verbose: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Identifies the CodeQL Action repository. */
|
|
||||||
const codeqlActionRepo = {
|
|
||||||
owner: "github",
|
|
||||||
repo: "codeql-action",
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Represents a configuration of which checks should not be set up as required checks. */
|
/** Represents a configuration of which checks should not be set up as required checks. */
|
||||||
export interface Exclusions {
|
export interface Exclusions {
|
||||||
/** A list of strings that, if contained in a check name, are excluded. */
|
/** A list of strings that, if contained in a check name, are excluded. */
|
||||||
@@ -100,7 +98,7 @@ async function getChecksFor(
|
|||||||
const response = await client.paginate(
|
const response = await client.paginate(
|
||||||
"GET /repos/{owner}/{repo}/commits/{ref}/check-runs",
|
"GET /repos/{owner}/{repo}/commits/{ref}/check-runs",
|
||||||
{
|
{
|
||||||
...codeqlActionRepo,
|
...CODEQL_ACTION_REPO,
|
||||||
ref,
|
ref,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -133,7 +131,7 @@ async function getChecksFor(
|
|||||||
/** Gets the current list of release branches. */
|
/** Gets the current list of release branches. */
|
||||||
async function getReleaseBranches(client: ApiClient): Promise<string[]> {
|
async function getReleaseBranches(client: ApiClient): Promise<string[]> {
|
||||||
const refs = await client.rest.git.listMatchingRefs({
|
const refs = await client.rest.git.listMatchingRefs({
|
||||||
...codeqlActionRepo,
|
...CODEQL_ACTION_REPO,
|
||||||
ref: "heads/releases/v",
|
ref: "heads/releases/v",
|
||||||
});
|
});
|
||||||
return refs.data.map((ref) => ref.ref).sort();
|
return refs.data.map((ref) => ref.ref).sort();
|
||||||
@@ -146,7 +144,7 @@ async function patchBranchProtectionRule(
|
|||||||
checks: Set<string>,
|
checks: Set<string>,
|
||||||
) {
|
) {
|
||||||
await client.rest.repos.setStatusCheckContexts({
|
await client.rest.repos.setStatusCheckContexts({
|
||||||
...codeqlActionRepo,
|
...CODEQL_ACTION_REPO,
|
||||||
branch,
|
branch,
|
||||||
contexts: Array.from(checks),
|
contexts: Array.from(checks),
|
||||||
});
|
});
|
||||||
@@ -163,7 +161,7 @@ async function updateBranch(
|
|||||||
|
|
||||||
// Query the current set of required checks for this branch.
|
// Query the current set of required checks for this branch.
|
||||||
const currentContexts = await client.rest.repos.getAllStatusCheckContexts({
|
const currentContexts = await client.rest.repos.getAllStatusCheckContexts({
|
||||||
...codeqlActionRepo,
|
...CODEQL_ACTION_REPO,
|
||||||
branch,
|
branch,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -205,10 +203,7 @@ async function updateBranch(
|
|||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const { values: options } = parseArgs({
|
const { values: options } = parseArgs({
|
||||||
options: {
|
options: {
|
||||||
// The token to use to authenticate to the API.
|
...TOKEN_OPTION_CONFIG,
|
||||||
token: {
|
|
||||||
type: "string",
|
|
||||||
},
|
|
||||||
// The git ref for which to retrieve the check runs.
|
// The git ref for which to retrieve the check runs.
|
||||||
ref: {
|
ref: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
|||||||
Reference in New Issue
Block a user