diff --git a/.github/actions/fetch-secrets/action.yml b/.github/actions/fetch-secrets/action.yml index ec75cfe..fb3af68 100644 --- a/.github/actions/fetch-secrets/action.yml +++ b/.github/actions/fetch-secrets/action.yml @@ -32,8 +32,14 @@ runs: using: composite steps: - name: Register role ARN mask - shell: bash - run: echo "::add-mask::${{ inputs.role-arn }}" + shell: node {0} + env: + ACTION_PATH: ${{ github.action_path }} + ROLE_ARN: ${{ inputs.role-arn }} + run: | + const path = require("node:path"); + const { registerMask } = require(path.join(process.env.ACTION_PATH, "build-secret-mappings.js")); + registerMask(process.env.ROLE_ARN); - name: Configure AWS credentials (central secrets reader) uses: aws-actions/configure-aws-credentials@v6 @@ -42,44 +48,21 @@ runs: aws-region: ${{ inputs.aws-region }} mask-aws-account-id: true - - name: Build secret-ids list + - name: Build secret mappings id: build - shell: bash + shell: node {0} env: + ACTION_PATH: ${{ github.action_path }} SHARED_NAMES: ${{ inputs.shared }} REPO_NAMES: ${{ inputs.repo }} CALLER_REPO: ${{ github.repository }} run: | - set -euo pipefail - # Normalize commas/spaces to newlines, drop blanks; build ",/". - # Alias = the bare NAME, so each secret lands in an env var of that exact name. - : > /tmp/secret-ids.txt - emit() { - local prefix="$1" names="$2" - echo "$names" | tr ', ' '\n\n' | while IFS= read -r n; do - n="$(echo "$n" | xargs)" # trim surrounding whitespace - [ -z "$n" ] && continue - echo "${n},${prefix}/${n}" >> /tmp/secret-ids.txt - done - } - emit "shared" "$SHARED_NAMES" - emit "$CALLER_REPO" "$REPO_NAMES" - - if [ ! -s /tmp/secret-ids.txt ]; then - echo "::error::fetch-secrets: neither 'shared' nor 'repo' provided any secret names." - exit 1 - fi - - echo "Resolved secret-ids:" - sed 's/^/ /' /tmp/secret-ids.txt - { - echo "ids<> "$GITHUB_OUTPUT" + const path = require("node:path"); + const { main } = require(path.join(process.env.ACTION_PATH, "build-secret-mappings.js")); + main(); - name: Fetch secrets uses: aws-actions/aws-secretsmanager-get-secrets@v2 with: - secret-ids: ${{ steps.build.outputs.ids }} + secret-ids: ${{ steps.build.outputs.mappings }} parse-json-secrets: false diff --git a/.github/actions/fetch-secrets/build-secret-mappings.js b/.github/actions/fetch-secrets/build-secret-mappings.js new file mode 100644 index 0000000..23fc63c --- /dev/null +++ b/.github/actions/fetch-secrets/build-secret-mappings.js @@ -0,0 +1,61 @@ +const fs = require("node:fs"); + +const OUTPUT_DELIMITER = "SECRET_MAPPINGS_EOF"; + +/** + * Registers a value for redaction from subsequent GitHub Actions logs. + * + * @param value - Sensitive value to mask. + * @see {@link https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#masking-a-value-in-a-log} + */ +function registerMask(value) { + console.log(`::add-mask::${value}`); +} + +/** + * Resolves action inputs and appends the multiline secret ID step output. + * + * @param environment - GitHub Actions environment variables. + * @see {@link https://github.com/aws-actions/aws-secretsmanager-get-secrets} + * @see {@link https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter} + */ +function main(environment = process.env) { + // Format each name as ",/". + const formatMappings = (names = "", prefix) => + names + .split(/[,\s]+/) + .map((name) => name.trim()) + .filter(Boolean) + .map((name) => `${name},${prefix}/${name}`); + + const secretMappings = [ + ...formatMappings(environment.SHARED_NAMES, "shared"), + ...formatMappings(environment.REPO_NAMES, environment.CALLER_REPO), + ]; + + if (secretMappings.length === 0) { + console.error( + "::error::fetch-secrets: neither 'shared' nor 'repo' provided any secret names.", + ); + process.exitCode = 1; + return; + } + + console.log( + `Resolved secret mappings:\n${secretMappings.map((mapping) => ` ${mapping}`).join("\n")}`, + ); + + fs.appendFileSync( + environment.GITHUB_OUTPUT, + `mappings<<${OUTPUT_DELIMITER}\n${secretMappings.join("\n")}\n${OUTPUT_DELIMITER}\n`, + ); +} + +if (require.main === module) { + main(); +} + +module.exports = { + main, + registerMask, +};