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);