|
| 1 | +name: registry-identities |
| 2 | + |
| 3 | +on: |
| 4 | + workflow_call: |
| 5 | + inputs: |
| 6 | + registry-identities: |
| 7 | + type: string |
| 8 | + description: "Keyless registry identity configuration as YAML objects" |
| 9 | + required: false |
| 10 | + outputs: |
| 11 | + aws-ecr-enabled: |
| 12 | + description: "Whether an AWS ECR registry identity was configured" |
| 13 | + value: ${{ jobs.validate.outputs.aws-ecr-enabled }} |
| 14 | + aws-ecr-role-to-assume: |
| 15 | + description: "AWS IAM role ARN to assume for ECR authentication" |
| 16 | + value: ${{ jobs.validate.outputs.aws-ecr-role-to-assume }} |
| 17 | + aws-ecr-region: |
| 18 | + description: "AWS region to use for ECR authentication" |
| 19 | + value: ${{ jobs.validate.outputs.aws-ecr-region }} |
| 20 | + aws-ecr-registry-auth: |
| 21 | + description: "Registry auth YAML for Docker login" |
| 22 | + value: ${{ jobs.validate.outputs.aws-ecr-registry-auth }} |
| 23 | + |
| 24 | +env: |
| 25 | + RUNTIME_MODULE: "@docker/github-builder-runtime@0.91.0" |
| 26 | + RUNTIME_INSTALL_ARGS: | |
| 27 | + --loglevel=error |
| 28 | + --no-save |
| 29 | + --package-lock=false |
| 30 | + --ignore-scripts |
| 31 | + --omit=dev |
| 32 | + --prefer-offline |
| 33 | + --fund=false |
| 34 | + --audit=false |
| 35 | + NPM_CONFIG_FETCH_RETRIES: "5" |
| 36 | + |
| 37 | +jobs: |
| 38 | + setup-registry-identities: |
| 39 | + runs-on: ubuntu-24.04 |
| 40 | + outputs: |
| 41 | + aws-ecr-enabled: ${{ steps.validate.outputs.aws-ecr-enabled }} |
| 42 | + aws-ecr-role-to-assume: ${{ steps.validate.outputs.aws-ecr-role-to-assume }} |
| 43 | + aws-ecr-region: ${{ steps.validate.outputs.aws-ecr-region }} |
| 44 | + aws-ecr-registry-auth: ${{ steps.validate.outputs.aws-ecr-registry-auth }} |
| 45 | + steps: |
| 46 | + - |
| 47 | + name: Install dependencies |
| 48 | + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 |
| 49 | + env: |
| 50 | + INPUT_RUNTIME-MODULE: ${{ env.RUNTIME_MODULE }} |
| 51 | + INPUT_RUNTIME-INSTALL-ARGS: ${{ env.RUNTIME_INSTALL_ARGS }} |
| 52 | + with: |
| 53 | + script: | |
| 54 | + const npmArgs = ['install', ...core.getMultilineInput('runtime-install-args'), core.getInput('runtime-module')]; |
| 55 | + const maxAttempts = 3; |
| 56 | + for (let attempt = 1; attempt <= maxAttempts; attempt++) { |
| 57 | + const exitCode = await exec.exec('npm', npmArgs, {ignoreReturnCode: true}); |
| 58 | + if (exitCode === 0) { |
| 59 | + return; |
| 60 | + } |
| 61 | + if (attempt === maxAttempts) { |
| 62 | + core.setFailed(`npm install failed after ${maxAttempts} attempts`); |
| 63 | + return; |
| 64 | + } |
| 65 | + const retryDelayMs = attempt * 50; |
| 66 | + core.info(`npm install failed with exit code ${exitCode}; retrying in ${retryDelayMs}ms`); |
| 67 | + await new Promise(resolve => setTimeout(resolve, retryDelayMs)); |
| 68 | + } |
| 69 | + - |
| 70 | + name: Validate registry identities |
| 71 | + id: validate |
| 72 | + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 |
| 73 | + env: |
| 74 | + INPUT_REGISTRY-IDENTITIES: ${{ inputs.registry-identities }} |
| 75 | + with: |
| 76 | + script: | |
| 77 | + let yaml; |
| 78 | + try { |
| 79 | + yaml = require('js-yaml'); |
| 80 | + } catch { |
| 81 | + yaml = require('@docker/github-builder-runtime/node_modules/js-yaml'); |
| 82 | + } |
| 83 | +
|
| 84 | + const setEmptyOutputs = () => { |
| 85 | + core.setOutput('aws-ecr-enabled', 'false'); |
| 86 | + core.setOutput('aws-ecr-role-to-assume', ''); |
| 87 | + core.setOutput('aws-ecr-region', ''); |
| 88 | + core.setOutput('aws-ecr-registry-auth', ''); |
| 89 | + }; |
| 90 | +
|
| 91 | + const input = core.getInput('registry-identities', {trimWhitespace: false}); |
| 92 | + if (!input.trim()) { |
| 93 | + setEmptyOutputs(); |
| 94 | + return; |
| 95 | + } |
| 96 | +
|
| 97 | + const fail = message => { |
| 98 | + throw new Error(`Invalid registry-identities input: ${message}`); |
| 99 | + }; |
| 100 | + const ensureObject = (value, path) => { |
| 101 | + if (!value || typeof value !== 'object' || Array.isArray(value)) { |
| 102 | + fail(`${path} must be an object`); |
| 103 | + } |
| 104 | + }; |
| 105 | + const requireString = (entry, key, path) => { |
| 106 | + const value = entry[key]; |
| 107 | + if (typeof value !== 'string' || !value.trim()) { |
| 108 | + fail(`${path}.${key} must be a non-empty string`); |
| 109 | + } |
| 110 | + return value.trim(); |
| 111 | + }; |
| 112 | + const requireStringAny = (entry, keys, path) => { |
| 113 | + for (const key of keys) { |
| 114 | + if (entry[key] !== undefined) { |
| 115 | + return requireString(entry, key, path); |
| 116 | + } |
| 117 | + } |
| 118 | + fail(`${path}.${keys[0]} must be a non-empty string`); |
| 119 | + }; |
| 120 | + const normalizeRegistries = (value, path) => { |
| 121 | + if (value === undefined || value === null || value === '') { |
| 122 | + fail(`${path}.registries must be a non-empty string or list`); |
| 123 | + } |
| 124 | + const values = Array.isArray(value) ? value : String(value).split(/[,\r\n]+/); |
| 125 | + const registries = values.map(item => String(item).trim()).filter(Boolean); |
| 126 | + if (registries.length === 0) { |
| 127 | + fail(`${path}.registries must contain at least one registry`); |
| 128 | + } |
| 129 | + return [...new Set(registries)]; |
| 130 | + }; |
| 131 | + const toAwsEcrLoginRegistry = (registry, region) => { |
| 132 | + if (registry.includes('.')) { |
| 133 | + return registry.split('/')[0]; |
| 134 | + } |
| 135 | + return `${registry}.dkr.ecr.${region}.amazonaws.com`; |
| 136 | + }; |
| 137 | +
|
| 138 | + let parsed; |
| 139 | + try { |
| 140 | + parsed = yaml.load(input); |
| 141 | + } catch (error) { |
| 142 | + fail(error.message); |
| 143 | + } |
| 144 | + if (parsed === null || parsed === undefined) { |
| 145 | + setEmptyOutputs(); |
| 146 | + return; |
| 147 | + } |
| 148 | +
|
| 149 | + const entries = Array.isArray(parsed) ? parsed : [parsed]; |
| 150 | + if (entries.length === 0) { |
| 151 | + setEmptyOutputs(); |
| 152 | + return; |
| 153 | + } |
| 154 | +
|
| 155 | + let awsEcr; |
| 156 | + entries.forEach((entry, index) => { |
| 157 | + const path = `registry-identities[${index}]`; |
| 158 | + ensureObject(entry, path); |
| 159 | + const type = requireString(entry, 'type', path); |
| 160 | + switch (type) { |
| 161 | + case 'aws-ecr': { |
| 162 | + const allowedKeys = new Set(['type', 'aws-region', 'aws_region', 'role-to-assume', 'role_to_assume', 'registries']); |
| 163 | + for (const key of Object.keys(entry)) { |
| 164 | + if (!allowedKeys.has(key)) { |
| 165 | + fail(`${path}.${key} is not supported for aws-ecr`); |
| 166 | + } |
| 167 | + } |
| 168 | + if (awsEcr) { |
| 169 | + fail('only one aws-ecr registry identity is supported'); |
| 170 | + } |
| 171 | + const awsRegion = requireStringAny(entry, ['aws-region', 'aws_region'], path); |
| 172 | + const roleToAssume = requireStringAny(entry, ['role-to-assume', 'role_to_assume'], path); |
| 173 | + const registries = normalizeRegistries(entry.registries, path); |
| 174 | + awsEcr = { |
| 175 | + awsRegion, |
| 176 | + roleToAssume, |
| 177 | + registryAuth: yaml.dump(registries.map(registry => ({ |
| 178 | + registry: toAwsEcrLoginRegistry(registry, awsRegion) |
| 179 | + })), {lineWidth: -1}) |
| 180 | + }; |
| 181 | + break; |
| 182 | + } |
| 183 | + default: |
| 184 | + fail(`${path}.type has unsupported provider ${type}`); |
| 185 | + } |
| 186 | + }); |
| 187 | +
|
| 188 | + core.setOutput('aws-ecr-enabled', awsEcr ? 'true' : 'false'); |
| 189 | + core.setOutput('aws-ecr-role-to-assume', awsEcr?.roleToAssume || ''); |
| 190 | + core.setOutput('aws-ecr-region', awsEcr?.awsRegion || ''); |
| 191 | + core.setOutput('aws-ecr-registry-auth', awsEcr?.registryAuth || ''); |
0 commit comments