|  | 
|  | 1 | +const { spawnSync } = require('child_process'); | 
|  | 2 | +const fs = require('fs'); | 
|  | 3 | +const os = require('os'); | 
|  | 4 | +const path = require('path'); | 
|  | 5 | +const { isUtf8 } = require("buffer"); | 
|  | 6 | + | 
|  | 7 | +// Note that we are not using the `@actions/core` package as it is not available | 
|  | 8 | +// without either committing node_modules/ to the repository, or using something | 
|  | 9 | +// like ncc to bundle the code. | 
|  | 10 | + | 
|  | 11 | +// See https://github.com/actions/toolkit/blob/%40actions/core%401.1.0/packages/core/src/command.ts#L81-L87 | 
|  | 12 | +const escapeData = (s) => { | 
|  | 13 | +  return s | 
|  | 14 | +    .replace(/%/g, '%25') | 
|  | 15 | +    .replace(/\r/g, '%0D') | 
|  | 16 | +    .replace(/\n/g, '%0A') | 
|  | 17 | +} | 
|  | 18 | + | 
|  | 19 | +const stringify = (value) => { | 
|  | 20 | +  if (typeof value === 'string') return value; | 
|  | 21 | +  if (Buffer.isBuffer(value) && isUtf8(value)) return value.toString('utf-8'); | 
|  | 22 | +  return undefined; | 
|  | 23 | +} | 
|  | 24 | + | 
|  | 25 | +const trimEOL = (buf) => { | 
|  | 26 | +  let l = buf.length | 
|  | 27 | +  if (l > 0 && buf[l - 1] === 0x0a) { | 
|  | 28 | +    l -= l > 1 && buf[l - 2] === 0x0d ? 2 : 1 | 
|  | 29 | +  } | 
|  | 30 | +  return buf.slice(0, l) | 
|  | 31 | +} | 
|  | 32 | + | 
|  | 33 | +const writeBufToFile = (buf, file) => { | 
|  | 34 | +  out = fs.createWriteStream(file) | 
|  | 35 | +  out.write(buf) | 
|  | 36 | +  out.end() | 
|  | 37 | +} | 
|  | 38 | + | 
|  | 39 | +const logInfo = (message) => { | 
|  | 40 | +  process.stdout.write(`${message}${os.EOL}`); | 
|  | 41 | +} | 
|  | 42 | + | 
|  | 43 | +const setFailed = (error) => { | 
|  | 44 | +  process.stdout.write(`::error::${escapeData(error.message)}${os.EOL}`); | 
|  | 45 | +  process.exitCode = 1; | 
|  | 46 | +} | 
|  | 47 | + | 
|  | 48 | +const writeCommand = (file, name, value) => { | 
|  | 49 | +  // Unique delimiter to avoid conflicts with actual values | 
|  | 50 | +  let delim; | 
|  | 51 | +  for (let count = 0; ; count++) { | 
|  | 52 | +    delim = `XXXXXX${count}`; | 
|  | 53 | +    if (!name.includes(delim) && !value.includes(delim)) { | 
|  | 54 | +      break; | 
|  | 55 | +    } | 
|  | 56 | +  } | 
|  | 57 | + | 
|  | 58 | +  fs.appendFileSync(file, `${name}<<${delim}${os.EOL}${value}${os.EOL}${delim}${os.EOL}`); | 
|  | 59 | +} | 
|  | 60 | + | 
|  | 61 | +const setSecret = (value) => { | 
|  | 62 | +  value = stringify(value); | 
|  | 63 | + | 
|  | 64 | +  // Masking a secret that is not a valid UTF-8 string or buffer is not useful | 
|  | 65 | +  if (value === undefined) return; | 
|  | 66 | + | 
|  | 67 | +  process.stdout.write( | 
|  | 68 | +    value | 
|  | 69 | +      .split(/\r?\n/g) | 
|  | 70 | +      .filter(line => line.length > 0) // Cannot mask empty lines | 
|  | 71 | +      .map( | 
|  | 72 | +        value => `::add-mask::${escapeData(value)}${os.EOL}` | 
|  | 73 | +      ) | 
|  | 74 | +      .join('') | 
|  | 75 | +  ); | 
|  | 76 | +} | 
|  | 77 | + | 
|  | 78 | +const setOutput = (name, value) => { | 
|  | 79 | +  value = stringify(value); | 
|  | 80 | +  if (value === undefined) { | 
|  | 81 | +    throw new Error(`Output value '${name}' is not a valid UTF-8 string or buffer`); | 
|  | 82 | +  } | 
|  | 83 | + | 
|  | 84 | +  writeCommand(process.env.GITHUB_OUTPUT, name, value); | 
|  | 85 | +} | 
|  | 86 | + | 
|  | 87 | +const exportVariable = (name, value) => { | 
|  | 88 | +  value = stringify(value); | 
|  | 89 | +  if (value === undefined) { | 
|  | 90 | +    throw new Error(`Environment variable '${name}' is not a valid UTF-8 string or buffer`); | 
|  | 91 | +  } | 
|  | 92 | + | 
|  | 93 | +  writeCommand(process.env.GITHUB_ENV, name, value); | 
|  | 94 | +} | 
|  | 95 | + | 
|  | 96 | +(async () => { | 
|  | 97 | +  const vault = process.env.INPUT_VAULT; | 
|  | 98 | +  const secrets = process.env.INPUT_SECRETS; | 
|  | 99 | +  // Parse and normalize secret mappings | 
|  | 100 | +  const secretMappings = secrets | 
|  | 101 | +    .split(/[\n,]+/) | 
|  | 102 | +    .map((entry) => entry.trim()) | 
|  | 103 | +    .filter((entry) => entry) | 
|  | 104 | +    .map((entry) => { | 
|  | 105 | +      const [input, encoding, output] = entry.split(/(\S+)?>/).map((part) => part?.trim()); | 
|  | 106 | +      return { input, encoding, output: output || `\$output:${input}` }; // Default output to $output:input if not specified | 
|  | 107 | +    }); | 
|  | 108 | + | 
|  | 109 | +  if (secretMappings.length === 0) { | 
|  | 110 | +    throw new Error('No secrets provided.'); | 
|  | 111 | +  } | 
|  | 112 | + | 
|  | 113 | +  // Fetch secrets from Azure Key Vault | 
|  | 114 | +  for (const { input: secretName, encoding, output } of secretMappings) { | 
|  | 115 | +    let az = spawnSync('az', | 
|  | 116 | +      [ | 
|  | 117 | +        'keyvault', | 
|  | 118 | +        'secret', | 
|  | 119 | +        'show', | 
|  | 120 | +        '--vault-name', | 
|  | 121 | +        vault, | 
|  | 122 | +        '--name', | 
|  | 123 | +        secretName, | 
|  | 124 | +        '--query', | 
|  | 125 | +        'value', | 
|  | 126 | +        '--output', | 
|  | 127 | +        'tsv' | 
|  | 128 | +      ], | 
|  | 129 | +      { | 
|  | 130 | +        stdio: ['ignore', 'pipe', 'inherit'], | 
|  | 131 | +        shell: true // az is a batch script on Windows | 
|  | 132 | +      } | 
|  | 133 | +    ); | 
|  | 134 | + | 
|  | 135 | +    if (az.error) throw new Error(az.error, { cause: az.error }); | 
|  | 136 | +    if (az.status !== 0) throw new Error(`az failed with status ${az.status}`); | 
|  | 137 | + | 
|  | 138 | +    // az keyvault secret show --output tsv returns a buffer with trailing \n | 
|  | 139 | +    // (or \r\n on Windows), so we need to trim it specifically. | 
|  | 140 | +    let secretBuf = trimEOL(az.stdout); | 
|  | 141 | + | 
|  | 142 | +    // Mask the raw secret value in logs | 
|  | 143 | +    setSecret(secretBuf); | 
|  | 144 | + | 
|  | 145 | +    // Handle encoded values if specified | 
|  | 146 | +    // Sadly we cannot use the `--encoding` parameter of the `az keyvault | 
|  | 147 | +    // secret (show|download)` command as the former does not support it, and | 
|  | 148 | +    // the latter must be used with `--file` (we could use /dev/stdout on UNIX | 
|  | 149 | +    // but not on Windows). | 
|  | 150 | +    if (encoding) { | 
|  | 151 | +      switch (encoding.toLowerCase()) { | 
|  | 152 | +        case 'base64': | 
|  | 153 | +          secretBuf = Buffer.from(secretBuf.toString('utf-8'), 'base64'); | 
|  | 154 | +          break; | 
|  | 155 | +        case 'ascii': | 
|  | 156 | +        case 'utf8': | 
|  | 157 | +        case 'utf-8': | 
|  | 158 | +          // No need to decode the existing buffer from the az command | 
|  | 159 | +          break; | 
|  | 160 | +        default: | 
|  | 161 | +            throw new Error(`Unsupported encoding: ${encoding}`); | 
|  | 162 | +        } | 
|  | 163 | + | 
|  | 164 | +      // Mask the decoded value | 
|  | 165 | +      setSecret(secretBuf); | 
|  | 166 | +    } | 
|  | 167 | + | 
|  | 168 | +    const outputType = output.startsWith('$env:') | 
|  | 169 | +    ? 'env' | 
|  | 170 | +    : output.startsWith('$output:') | 
|  | 171 | +      ? 'output' | 
|  | 172 | +      : 'file'; | 
|  | 173 | + | 
|  | 174 | +    switch (outputType) { | 
|  | 175 | +      case 'env': | 
|  | 176 | +        const varName = output.replace('$env:', '').trim(); | 
|  | 177 | +        exportVariable(varName, secretBuf); | 
|  | 178 | +        logInfo(`Secret set as environment variable: ${varName}`); | 
|  | 179 | +        break; | 
|  | 180 | + | 
|  | 181 | +      case 'output': | 
|  | 182 | +        const outputName = output.replace('$output:', '').trim(); | 
|  | 183 | +        setOutput(outputName, secretBuf); | 
|  | 184 | +        logInfo(`Secret set as output variable: ${outputName}`); | 
|  | 185 | +        break; | 
|  | 186 | + | 
|  | 187 | +      case 'file': | 
|  | 188 | +        const filePath = output.trim(); | 
|  | 189 | +        fs.mkdirSync(path.dirname(filePath), { recursive: true }); | 
|  | 190 | +        writeBufToFile(secretBuf, filePath); | 
|  | 191 | +        logInfo(`Secret written to file: ${filePath}`); | 
|  | 192 | +        break; | 
|  | 193 | +    } | 
|  | 194 | +  } | 
|  | 195 | +})().catch(setFailed); | 
0 commit comments