From edeaa2e3010aab5bafbe75dd5011ed04319d22b8 Mon Sep 17 00:00:00 2001 From: Jeffrey Aven Date: Mon, 20 Jul 2026 18:41:34 +1000 Subject: [PATCH] Scope auth and command to step outputs, not GITHUB_ENV setupAuth and getStackqlCommand previously wrote AUTH and STACKQL_COMMAND via core.exportVariable, which persists through GITHUB_ENV to every later step in a job. That let auth/args from one action invocation carry into the next (e.g. github bearer auth being applied to a later cloudflare query). Emit both as step outputs (core.setOutput) and wire them through action.yml via steps..outputs, so they are scoped to this invocation and cannot leak across steps or actions. Tests updated to the setOutput contract with added setupAuth coverage. Co-Authored-By: Claude Opus 4.8 (1M context) --- action.yml | 10 ++++++++-- lib/tests/utils.test.js | 41 ++++++++++++++++++++++++++++++++--------- lib/utils.js | 18 +++++++++++++----- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/action.yml b/action.yml index 9164ece..aa8aa8d 100644 --- a/action.yml +++ b/action.yml @@ -84,6 +84,7 @@ runs: AUTH_STR: ${{inputs.auth_str}} - name: get stackql command + id: get-command uses: actions/github-script@v8 with: script: | @@ -98,7 +99,10 @@ runs: DRY_RUN: ${{inputs.dry_run}} OUTPUT: ${{inputs.query_output}} VARS: ${{inputs.vars}} - + # Scoped to this invocation via the setup-auth step output - not GITHUB_ENV - + # so auth never carries over from another step in the job. + AUTH: ${{ steps.setup-auth.outputs.auth }} + - name: execute stackql command uses: actions/github-script@v8 id: exec-stackql @@ -110,9 +114,11 @@ runs: const onFailure = process.env.ON_FAILURE || 'exit'; // default to 'exit' if not specified await execStackQLQuery(core, process.env.STACKQL_COMMAND, process.env.IS_COMMAND === 'true', onFailure, process.env.DRY_RUN === 'true'); env: + # Consumed from the get-command step output, not GITHUB_ENV. + STACKQL_COMMAND: ${{ steps.get-command.outputs.stackql_command }} IS_COMMAND: ${{ inputs.is_command }} DRY_RUN: ${{ inputs.dry_run }} - ON_FAILURE: ${{ inputs.on_failure }} + ON_FAILURE: ${{ inputs.on_failure }} branding: icon: 'terminal' diff --git a/lib/tests/utils.test.js b/lib/tests/utils.test.js index 759495c..02ed6e0 100644 --- a/lib/tests/utils.test.js +++ b/lib/tests/utils.test.js @@ -1,4 +1,4 @@ -const { getStackqlCommand } = require("../utils"); +const { getStackqlCommand, setupAuth } = require("../utils"); describe("Utils Functions Tests", () => { let core; @@ -10,9 +10,10 @@ describe("Utils Functions Tests", () => { setFailed: jest.fn().mockImplementation((message) => console.error(message)), info: jest.fn().mockImplementation((message) => console.log(message)), exportVariable: jest.fn(), + setOutput: jest.fn(), error: jest.fn().mockImplementation((message) => console.error(message)), }; - jest.spyOn(core, 'exportVariable').mockImplementation((name, value) => { + jest.spyOn(core, 'setOutput').mockImplementation((name, value) => { core[name] = value; }); // set defaults for env vars @@ -39,7 +40,7 @@ describe("Utils Functions Tests", () => { const expectedCommand = 'stackql exec "SELECT status, count(*) as num_instances FROM google.compute.instances WHERE project = \'stackql-demo\' GROUP BY status" --output json'; // Verify that the full command is set correctly - expect(core.exportVariable).toHaveBeenCalledWith('STACKQL_COMMAND', expectedCommand); + expect(core.setOutput).toHaveBeenCalledWith('stackql_command', expectedCommand); // Optionally, check for no errors expect(core.error).not.toHaveBeenCalled(); @@ -52,16 +53,16 @@ describe("Utils Functions Tests", () => { getStackqlCommand(core); // Use RegExp for case insensitive comparison expect(core.setFailed).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`query file path does not exist: ${process.env.QUERY_FILE_PATH}`, 'i'))); - expect(core.exportVariable).not.toHaveBeenCalledWith('STACKQL_COMMAND', expect.any(String)); + expect(core.setOutput).not.toHaveBeenCalledWith('stackql_command', expect.any(String)); }); - + // Test for unsupported output formats it('should handle unsupported output formats', () => { process.env.OUTPUT = 'unsupported_format'; getStackqlCommand(core); // Use RegExp for case insensitive comparison expect(core.setFailed).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`output format not supported: ${process.env.OUTPUT}`, 'i'))); - expect(core.exportVariable).not.toHaveBeenCalled(); + expect(core.setOutput).not.toHaveBeenCalled(); }); it("should handle query execution from an existing file", () => { @@ -75,7 +76,7 @@ describe("Utils Functions Tests", () => { const expectedCommand = 'stackql exec -i "stackql_scripts/google-instances-by-status.iql" --output json'; // Verify that the command is set correctly - expect(core.exportVariable).toHaveBeenCalledWith('STACKQL_COMMAND', expectedCommand); + expect(core.setOutput).toHaveBeenCalledWith('stackql_command', expectedCommand); // Optionally, check for no errors expect(core.error).not.toHaveBeenCalled(); @@ -94,12 +95,34 @@ describe("Utils Functions Tests", () => { const expectedCommand = 'stackql exec -i "stackql_scripts/google-instances-by-status-with-external-data-file.iql" --iqldata "stackql_scripts/google-instances-by-status-with-external-data-file.jsonnet" --output json'; // Verify that the command is set correctly - expect(core.exportVariable).toHaveBeenCalledWith('STACKQL_COMMAND', expectedCommand); + expect(core.setOutput).toHaveBeenCalledWith('stackql_command', expectedCommand); // Optionally, check for no errors expect(core.error).not.toHaveBeenCalled(); expect(core.setFailed).not.toHaveBeenCalled(); }); - + + + // auth is emitted as a scoped step output, never exported to GITHUB_ENV, + // so it cannot carry over between steps/actions in a job. + describe("setupAuth", () => { + afterEach(() => { + delete process.env.AUTH_STR; + delete process.env.AUTH_FILE_PATH; + }); + + it("emits the provided auth string as the 'auth' output", () => { + process.env.AUTH_STR = '{ "cloudflare": { "type": "bearer", "credentialsenvvar": "CLOUDFLARE_API_TOKEN" } }'; + setupAuth(core); + expect(core.setOutput).toHaveBeenCalledWith('auth', process.env.AUTH_STR); + expect(core.exportVariable).not.toHaveBeenCalled(); + }); + + it("emits an empty 'auth' output when no auth is supplied (no carry-over)", () => { + setupAuth(core); + expect(core.setOutput).toHaveBeenCalledWith('auth', ''); + expect(core.exportVariable).not.toHaveBeenCalled(); + }); + }); }); diff --git a/lib/utils.js b/lib/utils.js index 3b4a422..51ecdda 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -4,8 +4,10 @@ const execAsync = promisify(require('child_process').exec); /** * Sets up authentication by reading from either a file or a string. - * The AUTH environment variable is set with the authentication details. - * @param {*} core - GitHub core library for setting outputs and environment variables. + * The resolved auth is emitted as this step's `auth` output (NOT exported to + * GITHUB_ENV), so it is scoped to this action invocation and cannot leak into + * later steps/actions in the same job. + * @param {*} core - GitHub core library for setting outputs. */ function setupAuth(core) { let auth; @@ -15,6 +17,9 @@ function setupAuth(core) { // Check if any authentication method is provided if (!checkEnvVarValid(fileName) && !checkEnvVarValid(authStr)) { core.info("using default provider environment variable variables as AUTH_FILE_PATH or AUTH_STR are not set..."); + // Emit an empty auth output so downstream steps receive a clean value for + // this invocation rather than anything carried over from a previous one. + core.setOutput("auth", ""); return; } @@ -32,8 +37,8 @@ function setupAuth(core) { auth = authStr; } - core.info("setting AUTH environment variable..."); - core.exportVariable("AUTH", auth); + core.info("setting auth output..."); + core.setOutput("auth", auth); } async function getStackqlCommand(core) { @@ -113,7 +118,10 @@ async function getStackqlCommand(core) { try { const stackQLCommand = `${stackQLExecutable} ${args.join(" ")}`; - core.exportVariable('STACKQL_COMMAND', `${stackQLCommand}`); + // Emit as a step output (scoped to this invocation) rather than exporting to + // GITHUB_ENV, so the assembled command - which may embed auth/secrets - does + // not persist into later steps or actions in the same job. + core.setOutput('stackql_command', `${stackQLCommand}`); core.debug(`STACKQL_COMMAND: ${stackQLCommand}`); } catch (error) { core.error(error);