From bb98ff4838ad0b291a318f58cf744d74ff43a391 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 10 Sep 2025 12:18:50 +0100 Subject: [PATCH 1/2] Validate workflow to check that all `codeql-action` versions are the same --- lib/init-action-post.js | 3 ++- lib/init-action.js | 19 ++++++++++++- src/workflow.test.ts | 59 +++++++++++++++++++++++++++++++++++++++++ src/workflow.ts | 27 +++++++++++++++++++ 4 files changed, 106 insertions(+), 2 deletions(-) diff --git a/lib/init-action-post.js b/lib/init-action-post.js index 8f90107276..14f1d6ca8a 100644 --- a/lib/init-action-post.js +++ b/lib/init-action-post.js @@ -133255,7 +133255,8 @@ function toCodedErrors(errors) { } var WorkflowErrors = toCodedErrors({ MissingPushHook: `Please specify an on.push hook to analyze and see code scanning alerts from the default branch on the Security tab.`, - CheckoutWrongHead: `git checkout HEAD^2 is no longer necessary. Please remove this step as Code Scanning recommends analyzing the merge commit for best results.` + CheckoutWrongHead: `git checkout HEAD^2 is no longer necessary. Please remove this step as Code Scanning recommends analyzing the merge commit for best results.`, + InconsistentActionVersion: `Not all workflow steps that use \`github/codeql-action\` actions use the same version. Please ensure that all such steps use the same version to avoid compatibility issues.` }); async function getWorkflow(logger) { const maybeWorkflow = process.env["CODE_SCANNING_WORKFLOW_FILE"]; diff --git a/lib/init-action.js b/lib/init-action.js index 0f8fc3ec9a..23b807806a 100644 --- a/lib/init-action.js +++ b/lib/init-action.js @@ -90109,7 +90109,8 @@ function toCodedErrors(errors) { } var WorkflowErrors = toCodedErrors({ MissingPushHook: `Please specify an on.push hook to analyze and see code scanning alerts from the default branch on the Security tab.`, - CheckoutWrongHead: `git checkout HEAD^2 is no longer necessary. Please remove this step as Code Scanning recommends analyzing the merge commit for best results.` + CheckoutWrongHead: `git checkout HEAD^2 is no longer necessary. Please remove this step as Code Scanning recommends analyzing the merge commit for best results.`, + InconsistentActionVersion: `Not all workflow steps that use \`github/codeql-action\` actions use the same version. Please ensure that all such steps use the same version to avoid compatibility issues.` }); async function groupLanguagesByExtractor(languages, codeql) { const resolveResult = await codeql.betterResolveLanguages(); @@ -90163,6 +90164,22 @@ async function getWorkflowErrors(doc, codeql) { } } } + const codeqlStepRefs = []; + for (const job of Object.values(doc?.jobs || {})) { + if (Array.isArray(job.steps)) { + for (const step of job.steps) { + if (step.uses !== void 0 && step.uses.startsWith("github/codeql-action/")) { + const parts = step.uses.split("@"); + if (parts.length >= 2) { + codeqlStepRefs.push(parts[parts.length - 1]); + } + } + } + } + } + if (codeqlStepRefs.length > 0 && !codeqlStepRefs.every((ref) => ref === codeqlStepRefs[0])) { + errors.push(WorkflowErrors.InconsistentActionVersion); + } const hasPushTrigger = hasWorkflowTrigger("push", doc); const hasPullRequestTrigger = hasWorkflowTrigger("pull_request", doc); const hasWorkflowCallTrigger = hasWorkflowTrigger("workflow_call", doc); diff --git a/src/workflow.test.ts b/src/workflow.test.ts index 21a5874f02..9af81459ef 100644 --- a/src/workflow.test.ts +++ b/src/workflow.test.ts @@ -655,6 +655,65 @@ test("getWorkflowErrors() should not report a warning if there is a workflow_cal t.deepEqual(...errorCodes(errors, [])); }); +test("getWorkflowErrors() should report a warning if different versions of the CodeQL Action are used", async (t) => { + const errors = await getWorkflowErrors( + yaml.load(` + name: "CodeQL" + on: + push: + branches: [main] + jobs: + analyze: + steps: + - uses: github/codeql-action/init@v2 + - uses: github/codeql-action/analyze@v3 + `) as Workflow, + await getCodeQLForTesting(), + ); + + t.deepEqual( + ...errorCodes(errors, [WorkflowErrors.InconsistentActionVersion]), + ); +}); + +test("getWorkflowErrors() should not report a warning if the same versions of the CodeQL Action are used", async (t) => { + const errors = await getWorkflowErrors( + yaml.load(` + name: "CodeQL" + on: + push: + branches: [main] + jobs: + analyze: + steps: + - uses: github/codeql-action/init@v3 + - uses: github/codeql-action/analyze@v3 + `) as Workflow, + await getCodeQLForTesting(), + ); + + t.deepEqual(...errorCodes(errors, [])); +}); + +test("getWorkflowErrors() should not report a warning involving versions of other actions", async (t) => { + const errors = await getWorkflowErrors( + yaml.load(` + name: "CodeQL" + on: + push: + branches: [main] + jobs: + analyze: + steps: + - uses: actions/checkout@v5 + - uses: github/codeql-action/init@v3 + `) as Workflow, + await getCodeQLForTesting(), + ); + + t.deepEqual(...errorCodes(errors, [])); +}); + test("getCategoryInputOrThrow returns category for simple workflow with category", (t) => { process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository"; t.is( diff --git a/src/workflow.ts b/src/workflow.ts index 37094154e1..330e00c2bd 100644 --- a/src/workflow.ts +++ b/src/workflow.ts @@ -72,6 +72,7 @@ function toCodedErrors(errors: { export const WorkflowErrors = toCodedErrors({ MissingPushHook: `Please specify an on.push hook to analyze and see code scanning alerts from the default branch on the Security tab.`, CheckoutWrongHead: `git checkout HEAD^2 is no longer necessary. Please remove this step as Code Scanning recommends analyzing the merge commit for best results.`, + InconsistentActionVersion: `Not all workflow steps that use \`github/codeql-action\` actions use the same version. Please ensure that all such steps use the same version to avoid compatibility issues.`, }); /** @@ -161,6 +162,32 @@ export async function getWorkflowErrors( } } + // Check that all `github/codeql-action` steps use the same ref, i.e. the same version. + // Mixing different versions of the actions can lead to unpredictable behaviour. + const codeqlStepRefs: string[] = []; + for (const job of Object.values(doc?.jobs || {})) { + if (Array.isArray(job.steps)) { + for (const step of job.steps) { + if ( + step.uses !== undefined && + step.uses.startsWith("github/codeql-action/") + ) { + const parts = step.uses.split("@"); + if (parts.length >= 2) { + codeqlStepRefs.push(parts[parts.length - 1]); + } + } + } + } + } + + if ( + codeqlStepRefs.length > 0 && + !codeqlStepRefs.every((ref) => ref === codeqlStepRefs[0]) + ) { + errors.push(WorkflowErrors.InconsistentActionVersion); + } + // If there is no push trigger, we will not be able to analyze the default branch. // So add a warning to the user to add a push trigger. // If there is a workflow_call trigger, we don't need a push trigger since we assume From 754f2e184f461607b45a1d55105127a48df3a7a9 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 10 Sep 2025 13:35:39 +0100 Subject: [PATCH 2/2] Simplify `step.uses` condition --- lib/init-action.js | 2 +- src/workflow.ts | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/init-action.js b/lib/init-action.js index 23b807806a..c852c75294 100644 --- a/lib/init-action.js +++ b/lib/init-action.js @@ -90168,7 +90168,7 @@ async function getWorkflowErrors(doc, codeql) { for (const job of Object.values(doc?.jobs || {})) { if (Array.isArray(job.steps)) { for (const step of job.steps) { - if (step.uses !== void 0 && step.uses.startsWith("github/codeql-action/")) { + if (step.uses?.startsWith("github/codeql-action/")) { const parts = step.uses.split("@"); if (parts.length >= 2) { codeqlStepRefs.push(parts[parts.length - 1]); diff --git a/src/workflow.ts b/src/workflow.ts index 330e00c2bd..ee95c337f5 100644 --- a/src/workflow.ts +++ b/src/workflow.ts @@ -168,10 +168,7 @@ export async function getWorkflowErrors( for (const job of Object.values(doc?.jobs || {})) { if (Array.isArray(job.steps)) { for (const step of job.steps) { - if ( - step.uses !== undefined && - step.uses.startsWith("github/codeql-action/") - ) { + if (step.uses?.startsWith("github/codeql-action/")) { const parts = step.uses.split("@"); if (parts.length >= 2) { codeqlStepRefs.push(parts[parts.length - 1]);