mirror of
https://github.com/github/codeql-action.git
synced 2026-05-09 15:20:28 +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
|
||||
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
|
||||
if: always()
|
||||
run: npm test
|
||||
|
||||
+1
-1
@@ -12,4 +12,4 @@ eslint.sarif
|
||||
# for local incremental compilation
|
||||
tsconfig.tsbuildinfo
|
||||
# esbuild metadata file
|
||||
meta.json
|
||||
build-metadata.json
|
||||
|
||||
@@ -82,6 +82,6 @@ const context = await esbuild.context({
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"description": "CodeQL action",
|
||||
"scripts": {
|
||||
"_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-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",
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { ParseArgsConfig } from "node:util";
|
||||
|
||||
import * as githubUtils from "@actions/github/lib/utils";
|
||||
import { type Octokit } from "@octokit/core";
|
||||
import { type PaginateInterface } from "@octokit/plugin-paginate-rest";
|
||||
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. */
|
||||
export type ApiClient = Octokit & Api & { paginate: PaginateInterface };
|
||||
|
||||
@@ -11,3 +19,16 @@ export function getApiClient(token: string): ApiClient {
|
||||
const opts = githubUtils.getOctokitOptions(token);
|
||||
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
|
||||
|
||||
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 {
|
||||
bytesInOutput: number;
|
||||
@@ -23,21 +58,125 @@ function toMB(bytes: number): string {
|
||||
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() {
|
||||
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 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(
|
||||
metadata.outputs,
|
||||
).reverse()) {
|
||||
console.info(`${outputFile}: ${toMB(outputData.bytes)}`);
|
||||
const baselineOutputData = baselineMetadata.outputs[outputFile];
|
||||
|
||||
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;
|
||||
if (baselineOutputData === undefined) {
|
||||
console.info(`${outputFile}: New file (${toMB(outputData.bytes)})`);
|
||||
} else {
|
||||
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");
|
||||
|
||||
/** 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. */
|
||||
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 { type ApiClient, getApiClient } from "./api-client";
|
||||
import {
|
||||
type ApiClient,
|
||||
CODEQL_ACTION_REPO,
|
||||
getApiClient,
|
||||
TOKEN_OPTION_CONFIG,
|
||||
TokenOption,
|
||||
} from "./api-client";
|
||||
import {
|
||||
OLDEST_SUPPORTED_MAJOR_VERSION,
|
||||
PR_CHECK_EXCLUDED_FILE,
|
||||
} from "./config";
|
||||
|
||||
/** Represents the command-line options. */
|
||||
export interface Options {
|
||||
/** The token to use to authenticate to the GitHub API. */
|
||||
token?: string;
|
||||
export interface Options extends TokenOption {
|
||||
/** The git ref to use the checks for. */
|
||||
ref?: string;
|
||||
/** Whether to actually apply the changes or not. */
|
||||
@@ -25,12 +29,6 @@ export interface Options {
|
||||
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. */
|
||||
export interface Exclusions {
|
||||
/** 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(
|
||||
"GET /repos/{owner}/{repo}/commits/{ref}/check-runs",
|
||||
{
|
||||
...codeqlActionRepo,
|
||||
...CODEQL_ACTION_REPO,
|
||||
ref,
|
||||
},
|
||||
);
|
||||
@@ -133,7 +131,7 @@ async function getChecksFor(
|
||||
/** Gets the current list of release branches. */
|
||||
async function getReleaseBranches(client: ApiClient): Promise<string[]> {
|
||||
const refs = await client.rest.git.listMatchingRefs({
|
||||
...codeqlActionRepo,
|
||||
...CODEQL_ACTION_REPO,
|
||||
ref: "heads/releases/v",
|
||||
});
|
||||
return refs.data.map((ref) => ref.ref).sort();
|
||||
@@ -146,7 +144,7 @@ async function patchBranchProtectionRule(
|
||||
checks: Set<string>,
|
||||
) {
|
||||
await client.rest.repos.setStatusCheckContexts({
|
||||
...codeqlActionRepo,
|
||||
...CODEQL_ACTION_REPO,
|
||||
branch,
|
||||
contexts: Array.from(checks),
|
||||
});
|
||||
@@ -163,7 +161,7 @@ async function updateBranch(
|
||||
|
||||
// Query the current set of required checks for this branch.
|
||||
const currentContexts = await client.rest.repos.getAllStatusCheckContexts({
|
||||
...codeqlActionRepo,
|
||||
...CODEQL_ACTION_REPO,
|
||||
branch,
|
||||
});
|
||||
|
||||
@@ -205,10 +203,7 @@ async function updateBranch(
|
||||
async function main(): Promise<void> {
|
||||
const { values: options } = parseArgs({
|
||||
options: {
|
||||
// The token to use to authenticate to the API.
|
||||
token: {
|
||||
type: "string",
|
||||
},
|
||||
...TOKEN_OPTION_CONFIG,
|
||||
// The git ref for which to retrieve the check runs.
|
||||
ref: {
|
||||
type: "string",
|
||||
|
||||
Reference in New Issue
Block a user