diff --git a/.github/workflows/__linux-arm64.yml b/.github/workflows/__linux-arm64.yml index a1764426d..de045d895 100644 --- a/.github/workflows/__linux-arm64.yml +++ b/.github/workflows/__linux-arm64.yml @@ -20,6 +20,11 @@ on: - cron: '0 5 * * *' workflow_dispatch: inputs: + dotnet-version: + type: string + description: The version of .NET to install + required: false + default: 9.x go-version: type: string description: The version of Go to install @@ -27,6 +32,11 @@ on: default: '>=1.21.0' workflow_call: inputs: + dotnet-version: + type: string + description: The version of .NET to install + required: false + default: 9.x go-version: type: string description: The version of Go to install @@ -37,7 +47,7 @@ defaults: shell: bash concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' || false }} - group: linux-arm64-${{github.ref}}-${{inputs.go-version}} + group: linux-arm64-${{github.ref}}-${{inputs.dotnet-version}}-${{inputs.go-version}} jobs: linux-arm64: strategy: @@ -56,6 +66,10 @@ jobs: steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: ${{ inputs.dotnet-version || '9.x' }} - name: Install Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: @@ -70,21 +84,23 @@ jobs: setup-kotlin: 'true' - uses: ./../action/init with: - languages: javascript,python,go + languages: ${{ env.LANGUAGES }} tools: ${{ steps.prepare-test.outputs.tools-url }} - - name: Build Go code - run: go build main.go + - name: Build code + run: ./build.sh - uses: ./../action/analyze with: upload-database: false - name: Assert databases exist run: | cd "$RUNNER_TEMP/codeql_databases" - for lang in javascript python go; do + for lang in ${LANGUAGES//,/ }; do if [[ ! -d "$lang" ]]; then echo "Did not find a database for $lang" exit 1 fi + echo "Found database for $lang" done env: + LANGUAGES: cpp,csharp,go,java,javascript,python,ruby CODEQL_ACTION_TEST_MODE: true diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index b1e131425..3915fddf5 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -67,7 +67,12 @@ jobs: - name: Upload sarif uses: ./upload-sarif - if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 + # The merge queue deletes its `gh-readonly-queue` ref as soon as the queue entry resolves, + # so uploading against it races with that deletion. Both the `merge_group` run and the + # paired `push` run that the queue branch creates use that ref, so gate on the ref itself + # rather than the event. The same results are uploaded by the `pull_request` run and again + # by the `push` run on `main`. + if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 && !startsWith(github.ref, 'refs/heads/gh-readonly-queue/') with: sarif_file: eslint.sarif category: eslint diff --git a/CHANGELOG.md b/CHANGELOG.md index 58268df8d..c2542a414 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th - The CodeQL Action now supports CodeQL releases that are compatible with Linux Arm64 and download the native `linux-arm64` CodeQL bundle when available. [#4072](https://github.com/github/codeql-action/pull/4072) - On GitHub-hosted runners, the CodeQL Action now deletes unused CodeQL bundles from the toolcache before downloading a different bundle, which frees up disk space for the analysis. We expect to roll this change out to everyone in September. [#4124](https://github.com/github/codeql-action/pull/4124) +- The CodeQL Action now supports CodeQL releases that are compatible with Linux Arm64 and downloads the native `linux-arm64` CodeQL bundle when available. [#4072](https://github.com/github/codeql-action/pull/4072) ## 4.37.9 - 26 Aug 2026 diff --git a/init/action.yml b/init/action.yml index 1b64e8d2a..7787a0a07 100644 --- a/init/action.yml +++ b/init/action.yml @@ -164,6 +164,13 @@ inputs: [Internal] The ID of the check run, as provided by the Actions runtime environment. Do not set this value manually. default: ${{ job.check_run_id }} required: false + job-status: + description: >- + [Internal] The status of the job, as provided by the Actions runtime environment. This is how the + post step learns whether the job as a whole succeeded, failed, or was cancelled. Do not set this + value manually. + default: ${{ job.status }} + required: false outputs: codeql-path: description: The path of the CodeQL binary used for analysis diff --git a/lib/entry-points.js b/lib/entry-points.js index 249db17be..b282834ef 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -23510,18 +23510,18 @@ var init_dist_src2 = __esm({ } }); -// node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/version.js +// node_modules/@actions/github/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/version.js var VERSION5; var init_version2 = __esm({ - "node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/version.js"() { + "node_modules/@actions/github/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/version.js"() { VERSION5 = "17.0.0"; } }); -// node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/generated/endpoints.js +// node_modules/@actions/github/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/generated/endpoints.js var Endpoints, endpoints_default; var init_endpoints = __esm({ - "node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/generated/endpoints.js"() { + "node_modules/@actions/github/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/generated/endpoints.js"() { Endpoints = { actions: { addCustomLabelsToSelfHostedRunnerForOrg: [ @@ -25815,7 +25815,7 @@ var init_endpoints = __esm({ } }); -// node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/endpoints-to-methods.js +// node_modules/@actions/github/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/endpoints-to-methods.js function endpointsToMethods(octokit) { const newMethods = {}; for (const scope of endpointMethodsMap.keys()) { @@ -25866,7 +25866,7 @@ function decorate(octokit, scope, methodName, defaults3, decorations) { } var endpointMethodsMap, handler; var init_endpoints_to_methods = __esm({ - "node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/endpoints-to-methods.js"() { + "node_modules/@actions/github/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/endpoints-to-methods.js"() { init_endpoints(); endpointMethodsMap = /* @__PURE__ */ new Map(); for (const [scope, endpoints] of Object.entries(endpoints_default)) { @@ -25944,7 +25944,7 @@ var init_endpoints_to_methods = __esm({ } }); -// node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/index.js +// node_modules/@actions/github/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/index.js var dist_src_exports2 = {}; __export(dist_src_exports2, { legacyRestEndpointMethods: () => legacyRestEndpointMethods, @@ -25964,7 +25964,7 @@ function legacyRestEndpointMethods(octokit) { }; } var init_dist_src3 = __esm({ - "node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/index.js"() { + "node_modules/@actions/github/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/index.js"() { init_version2(); init_endpoints_to_methods(); restEndpointMethods.VERSION = VERSION5; @@ -162582,8 +162582,8 @@ async function tryUploadSarifIfRunFailed(config, repositoryNwo, features, logger return createFailedUploadFailedSarifResult(e); } } -async function uploadFailureInfo(uploadAllAvailableDebugArtifacts, printDebugLogs2, codeql, config, repositoryNwo, features, logger) { - await recordOverlayStatus(codeql, config, features, logger); +async function uploadFailureInfo(uploadAllAvailableDebugArtifacts, printDebugLogs2, codeql, config, repositoryNwo, features, jobStatus, env, logger) { + await recordOverlayStatus(codeql, config, features, jobStatus, env, logger); const uploadFailedSarifResult = await tryUploadSarifIfRunFailed( config, repositoryNwo, @@ -162645,8 +162645,27 @@ async function uploadFailureInfo(uploadAllAvailableDebugArtifacts, printDebugLog } return uploadFailedSarifResult; } -async function recordOverlayStatus(codeql, config, features, logger) { - if (config.overlayDatabaseMode !== "overlay-base" /* OverlayBase */ || process.env["CODEQL_ACTION_ANALYZE_DID_COMPLETE_SUCCESSFULLY" /* ANALYZE_DID_COMPLETE_SUCCESSFULLY */] === "true" || !await features.getValue("overlay_analysis_status_save" /* OverlayAnalysisStatusSave */)) { +function didCodeQlReportError(env) { + const jobStatus = env.getOptional("CODEQL_ACTION_JOB_STATUS" /* JOB_STATUS */); + return jobStatus === "JOB_STATUS_FAILURE" /* FailureStatus */ || jobStatus === "JOB_STATUS_CONFIGURATION_ERROR" /* ConfigErrorStatus */; +} +function isConclusiveJobStatus(jobStatus) { + switch (jobStatus?.trim().toLowerCase()) { + case "failure": + case "success": + return true; + default: + return false; + } +} +async function recordOverlayStatus(codeql, config, features, jobStatus, env, logger) { + if (config.overlayDatabaseMode !== "overlay-base" /* OverlayBase */ || env.getOptional("CODEQL_ACTION_ANALYZE_DID_COMPLETE_SUCCESSFULLY" /* ANALYZE_DID_COMPLETE_SUCCESSFULLY */) === "true" || !await features.getValue("overlay_analysis_status_save" /* OverlayAnalysisStatusSave */)) { + return; + } + if (!isConclusiveJobStatus(jobStatus) && !didCodeQlReportError(env)) { + logger.info( + `Not recording an improved incremental analysis failure for this job because the job status (${jobStatus ?? "unset"}) does not tell us whether the analysis itself failed.` + ); return; } const checkRunIdInput = getOptionalInput("check-run-id"); @@ -162750,6 +162769,7 @@ async function run4(startedAt) { let uploadFailedSarifResult; let dependencyCachingUsage; try { + const jobStatus2 = getOptionalInput("job-status"); restoreInputs(); const gitHubVersion = await getGitHubVersion(); checkGitHubVersionInRange(gitHubVersion, logger); @@ -162774,6 +162794,8 @@ async function run4(startedAt) { config, repositoryNwo, features, + jobStatus2, + getEnv(), logger ); if (await isAnalyzingDefaultBranch() && config.dependencyCachingEnabled !== "none" /* None */) { diff --git a/package-lock.json b/package-lock.json index 110a6d4fd..da14fd0ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "@actions/tool-cache": "^3.0.1", "@octokit/core": "^7.0.7", "@octokit/plugin-paginate-rest": "^15.0.0", - "@octokit/plugin-rest-endpoint-methods": "^17.0.0", + "@octokit/plugin-rest-endpoint-methods": "^18.0.0", "@octokit/plugin-retry": "^8.1.1", "archiver": "^8.0.0", "fast-deep-equal": "^3.1.3", @@ -539,6 +539,21 @@ "@octokit/core": ">=6" } }, + "node_modules/@actions/github/node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, "node_modules/@actions/github/node_modules/@octokit/types": { "version": "16.0.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", @@ -2198,12 +2213,12 @@ } }, "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", - "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-18.0.0.tgz", + "integrity": "sha512-1wM02pQTHEarWYij0Bb4gu8X8fBy2FJdI4HMTSFDxtYLbOkJErGVx5ywmM6jeS8tOmC0iCiRLrn6YsSIecUeOQ==", "license": "MIT", "dependencies": { - "@octokit/types": "^16.0.0" + "@octokit/types": "^17.0.0" }, "engines": { "node": ">= 20" @@ -2212,21 +2227,6 @@ "@octokit/core": ">=6" } }, - "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { - "version": "27.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", - "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", - "license": "MIT" - }, - "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", - "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^27.0.0" - } - }, "node_modules/@octokit/plugin-retry": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-8.1.1.tgz", @@ -10723,7 +10723,7 @@ "@actions/github": "^8.0.1", "@octokit/core": "^7.0.7", "@octokit/plugin-paginate-rest": ">=15.0.0", - "@octokit/plugin-rest-endpoint-methods": "^17.0.0", + "@octokit/plugin-rest-endpoint-methods": "^18.0.0", "semver": "^7.8.5", "yaml": "^2.9.0" }, diff --git a/package.json b/package.json index cd16ff344..0e8fc4bb2 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@actions/tool-cache": "^3.0.1", "@octokit/core": "^7.0.7", "@octokit/plugin-paginate-rest": "^15.0.0", - "@octokit/plugin-rest-endpoint-methods": "^17.0.0", + "@octokit/plugin-rest-endpoint-methods": "^18.0.0", "@octokit/plugin-retry": "^8.1.1", "archiver": "^8.0.0", "fast-deep-equal": "^3.1.3", diff --git a/pr-checks/api-client.ts b/pr-checks/api-client.ts index 93675dba7..b8f914d81 100644 --- a/pr-checks/api-client.ts +++ b/pr-checks/api-client.ts @@ -1,10 +1,7 @@ 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"; /** The type of the Octokit client. */ -export type ApiClient = Octokit & Api & { paginate: PaginateInterface }; +export type ApiClient = InstanceType; /** Constructs an `ApiClient` using `token` for authentication. */ export function getApiClient(token: string): ApiClient { diff --git a/pr-checks/checks/linux-arm64.yml b/pr-checks/checks/linux-arm64.yml index 94e03da17..29d3eea41 100644 --- a/pr-checks/checks/linux-arm64.yml +++ b/pr-checks/checks/linux-arm64.yml @@ -9,22 +9,27 @@ operatingSystems: versions: - nightly-latest installGo: true +installDotNet: true +# The set of languages CodeQL supports on this platform, excluding Swift (macOS only). +env: + LANGUAGES: cpp,csharp,go,java,javascript,python,ruby steps: - uses: ./../action/init with: - languages: javascript,python,go + languages: ${{ env.LANGUAGES }} tools: ${{ steps.prepare-test.outputs.tools-url }} - - name: Build Go code - run: go build main.go + - name: Build code + run: ./build.sh - uses: ./../action/analyze with: upload-database: false - name: Assert databases exist run: | cd "$RUNNER_TEMP/codeql_databases" - for lang in javascript python go; do + for lang in ${LANGUAGES//,/ }; do if [[ ! -d "$lang" ]]; then echo "Did not find a database for $lang" exit 1 fi + echo "Found database for $lang" done diff --git a/pr-checks/package.json b/pr-checks/package.json index a29253523..00f4b813c 100644 --- a/pr-checks/package.json +++ b/pr-checks/package.json @@ -6,7 +6,7 @@ "@actions/github": "^8.0.1", "@octokit/core": "^7.0.7", "@octokit/plugin-paginate-rest": ">=15.0.0", - "@octokit/plugin-rest-endpoint-methods": "^17.0.0", + "@octokit/plugin-rest-endpoint-methods": "^18.0.0", "semver": "^7.8.5", "yaml": "^2.9.0" }, diff --git a/src/api-client.ts b/src/api-client.ts index ba800a258..bc6018877 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -1,8 +1,5 @@ import * as core from "@actions/core"; 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"; import * as retry from "@octokit/plugin-retry"; import { RequestRequestOptions } from "@octokit/types"; import { @@ -128,7 +125,7 @@ export function makeProxyRequestOptions( } /** The type of GitHub API client we use. */ -export type ApiClient = Octokit & Api & { paginate: PaginateInterface }; +export type ApiClient = InstanceType; /** Options for `createApiClientWithDetails`. */ interface CreateApiClientOptions { diff --git a/src/init-action-post-helper.test.ts b/src/init-action-post-helper.test.ts index f24cc5e4e..fd1f489f7 100644 --- a/src/init-action-post-helper.test.ts +++ b/src/init-action-post-helper.test.ts @@ -15,10 +15,12 @@ import { getRunnerLogger } from "./logging"; import { OverlayDatabaseMode } from "./overlay/overlay-database-mode"; import * as overlayStatus from "./overlay/status"; import { parseRepositoryNwo } from "./repository"; +import { JobStatus } from "./status-report"; import { createFeatures, createTestConfig, DEFAULT_ACTIONS_VARS, + getTestEnv, makeMacro, makeVersionInfo, RecordingLogger, @@ -58,6 +60,8 @@ test.serial("init-post action with debug mode off", async (t) => { createTestConfig({ debugMode: false }), parseRepositoryNwo("github/codeql-action"), createFeatures([]), + "success", + getTestEnv(), getRunnerLogger(true), ); @@ -80,6 +84,8 @@ test.serial("init-post action with debug mode on", async (t) => { createTestConfig({ debugMode: true }), parseRepositoryNwo("github/codeql-action"), createFeatures([]), + "success", + getTestEnv(), getRunnerLogger(true), ); @@ -375,6 +381,8 @@ test.serial( }), parseRepositoryNwo("github/codeql-action"), createFeatures([Feature.OverlayAnalysisStatusSave]), + "success", + getTestEnv(), getRunnerLogger(true), ); @@ -443,6 +451,8 @@ test.serial( }), parseRepositoryNwo("github/codeql-action"), createFeatures([]), + "success", + getTestEnv(), getRunnerLogger(true), ); @@ -457,8 +467,13 @@ test.serial( test.serial("does not save overlay status when build successful", async (t) => { return await util.withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); - // Mark analyze as having completed successfully. + // Mark analyze as having completed successfully. `tryUploadSarifIfRunFailed` reads this from + // the process environment, while `recordOverlayStatus` reads it from the environment it is + // given. process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY] = "true"; + const env = getTestEnv({ + [EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY]: "true", + }); sinon.stub(util, "checkDiskUsage").resolves({ numAvailableBytes: 100 * NUM_BYTES_PER_GIB, @@ -480,6 +495,8 @@ test.serial("does not save overlay status when build successful", async (t) => { }), parseRepositoryNwo("github/codeql-action"), createFeatures([Feature.OverlayAnalysisStatusSave]), + "success", + env, getRunnerLogger(true), ); @@ -517,6 +534,8 @@ test.serial( }), parseRepositoryNwo("github/codeql-action"), createFeatures([]), + "success", + getTestEnv(), getRunnerLogger(true), ); @@ -528,6 +547,137 @@ test.serial( }, ); +/** + * Runs `uploadFailureInfo` for an overlay-base job that did not complete successfully, with the + * given job status from the Actions runtime environment. + */ +async function runOverlayPostStep({ + jobStatus, + codeQlReportedError = false, +}: { + jobStatus: string | undefined; + codeQlReportedError?: boolean; +}) { + return await util.withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + delete process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY]; + const env = getTestEnv( + codeQlReportedError + ? { [EnvVar.JOB_STATUS]: JobStatus.FailureStatus } + : {}, + ); + + sinon.stub(util, "checkDiskUsage").resolves({ + numAvailableBytes: 100 * NUM_BYTES_PER_GIB, + numTotalBytes: 200 * NUM_BYTES_PER_GIB, + }); + + const saveOverlayStatusStub = sinon + .stub(overlayStatus, "saveOverlayStatus") + .resolves(true); + + await initActionPostHelper.uploadFailureInfo( + sinon.spy(), + sinon.spy(), + codeql.createStubCodeQL({}), + createTestConfig({ + debugMode: false, + languages: ["javascript"], + overlayDatabaseMode: OverlayDatabaseMode.OverlayBase, + }), + parseRepositoryNwo("github/codeql-action"), + createFeatures([Feature.OverlayAnalysisStatusSave]), + jobStatus, + env, + getRunnerLogger(true), + ); + + return { saveOverlayStatusStub }; + }); +} + +test.serial( + "does not save overlay status when the job was cancelled", + async (t) => { + const { saveOverlayStatusStub } = await runOverlayPostStep({ + jobStatus: "cancelled", + }); + + t.true( + saveOverlayStatusStub.notCalled, + "a cancellation tells us nothing about whether the analysis would have succeeded", + ); + }, +); + +test.serial( + "does not save overlay status when the job status is not recognised", + async (t) => { + const { saveOverlayStatusStub } = await runOverlayPostStep({ + jobStatus: "some-new-status", + }); + + t.true( + saveOverlayStatusStub.notCalled, + "a status we do not recognise tells us nothing about whether the analysis would have succeeded", + ); + }, +); + +test.serial( + "does not save overlay status when the job status is unavailable", + async (t) => { + const { saveOverlayStatusStub } = await runOverlayPostStep({ + jobStatus: undefined, + }); + + t.true( + saveOverlayStatusStub.notCalled, + "without a job status we cannot tell whether the analysis would have succeeded", + ); + }, +); + +test.serial( + "saves overlay status when the job failed rather than being cancelled", + async (t) => { + const { saveOverlayStatusStub } = await runOverlayPostStep({ + jobStatus: "failure", + }); + + t.true( + saveOverlayStatusStub.calledOnce, + "a failed job indicates that the analysis itself failed", + ); + }, +); + +test.serial("saves overlay status when the job succeeded", async (t) => { + const { saveOverlayStatusStub } = await runOverlayPostStep({ + jobStatus: "success", + }); + + t.true( + saveOverlayStatusStub.calledOnce, + "the analysis did not complete successfully even though the job as a whole succeeded", + ); +}); + +test.serial( + "saves overlay status when a CodeQL Action reported an error before the run was cancelled", + async (t) => { + const { saveOverlayStatusStub } = await runOverlayPostStep({ + jobStatus: "cancelled", + codeQlReportedError: true, + }); + + t.true( + saveOverlayStatusStub.calledOnce, + "the analysis genuinely failed, even though the run was later cancelled", + ); + }, +); + function createTestWorkflow( steps: workflow.WorkflowJobStep[], ): workflow.Workflow { diff --git a/src/init-action-post-helper.ts b/src/init-action-post-helper.ts index 7b7b056a1..0e6dae13a 100644 --- a/src/init-action-post-helper.ts +++ b/src/init-action-post-helper.ts @@ -18,7 +18,7 @@ import { sanitizeArtifactName, } from "./debug-artifacts"; import * as dependencyCaching from "./dependency-caching"; -import { EnvVar } from "./environment"; +import { EnvVar, ReadOnlyEnv } from "./environment"; import { Feature, FeatureEnablement } from "./feature-flags"; import { Logger } from "./logging"; import { OverlayDatabaseMode } from "./overlay/overlay-database-mode"; @@ -316,6 +316,8 @@ export async function tryUploadSarifIfRunFailed( * @param config The CodeQL Action configuration. * @param repositoryNwo The name and owner of the repository. * @param features Information about enabled features. + * @param jobStatus The status of the job, as reported by the Actions runtime environment. + * @param env The environment to read variables from. * @param logger The logger to use. * @returns The results of uploading the SARIF file for the failure. */ @@ -331,9 +333,11 @@ export async function uploadFailureInfo( config: Config, repositoryNwo: RepositoryNwo, features: FeatureEnablement, + jobStatus: string | undefined, + env: ReadOnlyEnv, logger: Logger, ): Promise { - await recordOverlayStatus(codeql, config, features, logger); + await recordOverlayStatus(codeql, config, features, jobStatus, env, logger); const uploadFailedSarifResult = await tryUploadSarifIfRunFailed( config, @@ -412,6 +416,37 @@ export async function uploadFailureInfo( return uploadFailedSarifResult; } +/** + * Whether one of the CodeQL Actions reported an error for this job, which means the analysis + * genuinely failed. + * + * Note that the converse does not hold: an Action that is terminated abruptly, or that fails before + * it can gather telemetry, does not get to report anything. + */ +function didCodeQlReportError(env: ReadOnlyEnv): boolean { + const jobStatus = env.getOptional(EnvVar.JOB_STATUS); + return ( + jobStatus === JobStatus.FailureStatus || + jobStatus === JobStatus.ConfigErrorStatus + ); +} + +/** + * Whether the job status tells us anything about whether the analysis itself would have succeeded. + * + * We check for the statuses we know to be meaningful rather than excluding the ones that are not, + * so that a status we do not recognise is treated as inconclusive. + */ +function isConclusiveJobStatus(jobStatus: string | undefined): boolean { + switch (jobStatus?.trim().toLowerCase()) { + case "failure": + case "success": + return true; + default: + return false; + } +} + /** * If overlay base database creation was attempted but the analysis did not complete * successfully, save the failure status to the Actions cache so that subsequent runs @@ -421,16 +456,30 @@ async function recordOverlayStatus( codeql: CodeQL, config: Config, features: FeatureEnablement, + jobStatus: string | undefined, + env: ReadOnlyEnv, logger: Logger, ) { if ( config.overlayDatabaseMode !== OverlayDatabaseMode.OverlayBase || - process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY] === "true" || + env.getOptional(EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY) === "true" || !(await features.getValue(Feature.OverlayAnalysisStatusSave)) ) { return; } + // Only record a failure when the job outcome tells us something about the analysis. A cancelled + // job, or a status we do not recognise, says nothing about whether the analysis would have + // succeeded, so recording a failure would disable overlay analysis needlessly. We still record + // one if a CodeQL Action reported an error before the job ended. + if (!isConclusiveJobStatus(jobStatus) && !didCodeQlReportError(env)) { + logger.info( + "Not recording an improved incremental analysis failure for this job because the job " + + `status (${jobStatus ?? "unset"}) does not tell us whether the analysis itself failed.`, + ); + return; + } + const checkRunIdInput = actionsUtil.getOptionalInput("check-run-id"); const checkRunId = checkRunIdInput !== undefined ? parseInt(checkRunIdInput, 10) : undefined; diff --git a/src/init-action-post.ts b/src/init-action-post.ts index 2261b56ea..749020ac6 100644 --- a/src/init-action-post.ts +++ b/src/init-action-post.ts @@ -8,6 +8,7 @@ import * as core from "@actions/core"; import { restoreInputs, + getOptionalInput, getTemporaryDirectory, printDebugLogs, } from "./actions-util"; @@ -20,7 +21,7 @@ import { DependencyCachingUsageReport, getDependencyCacheUsage, } from "./dependency-caching"; -import { EnvVar } from "./environment"; +import { EnvVar, getEnv } from "./environment"; import { initFeatures } from "./feature-flags"; import * as gitUtils from "./git-utils"; import * as initActionPostHelper from "./init-action-post-helper"; @@ -55,6 +56,11 @@ async function run(startedAt: Date) { | undefined; let dependencyCachingUsage: DependencyCachingUsageReport | undefined; try { + // Read the job status before restoring inputs, since it is provided by the Actions runtime + // environment for this step and would otherwise be overwritten by the value that the `init` + // Action saw, which is always a success. + const jobStatus = getOptionalInput("job-status"); + // Restore inputs from `init` Action. restoreInputs(); @@ -84,6 +90,8 @@ async function run(startedAt: Date) { config, repositoryNwo, features, + jobStatus, + getEnv(), logger, );