149149 registry-auths :
150150 description : " Raw authentication to registries, defined as YAML objects (for image output)"
151151 required : false
152+ build-secrets :
153+ description : " YAML object mapping BuildKit secret IDs, optionally target-scoped, to secret values"
154+ required : false
152155 github-token :
153156 description : " GitHub Token used to authenticate against the repository for Git context"
154157 required : false
@@ -215,6 +218,7 @@ jobs:
215218 metaImages : ${{ steps.set.outputs.metaImages }}
216219 sign : ${{ steps.set.outputs.sign }}
217220 privateRepo : ${{ steps.set.outputs.privateRepo }}
221+ targets : ${{ steps.set.outputs.targets }}
218222 ghaCacheSign : ${{ steps.set.outputs.ghaCacheSign }}
219223 proxyNetwork : ${{ steps.set.outputs.proxyNetwork }}
220224 steps :
@@ -490,7 +494,7 @@ jobs:
490494 }
491495 );
492496 await core.group(`Set envs`, async () => {
493- core.info(JSON.stringify(envs, null, 2));
497+ core.info(JSON.stringify(Object.keys( envs).sort() , null, 2));
494498 });
495499
496500 const metaImages = inpMetaImages.map(image => image.toLowerCase());
@@ -504,9 +508,15 @@ jobs:
504508 try {
505509 await core.group(`Validating definition`, async () => {
506510 const bake = new Bake();
511+ // Resolve the graph without local secret files. The build job validates
512+ // secrets after applying the workflow-provided source overrides.
513+ const validationOverrides = inpSet.filter(override => {
514+ const key = override.split('=', 1)[0].split('.')[1];
515+ return !['secret', 'secrets', 'secrets+'].includes(key);
516+ });
507517 def = await bake.getDefinition({
508518 files: inpFiles,
509- overrides: inpSet ,
519+ overrides: [...validationOverrides, '*.secrets='] ,
510520 sbom: inpSbom ? `generator=${inpSbomImage}` : 'false',
511521 source: bakeSource,
512522 targets: [inpTarget]
@@ -556,6 +566,7 @@ jobs:
556566 if (unsupportedTargets.length > 0) {
557567 throw new Error(`Only one target can be built at once, found unsupported targets: ${unsupportedTargets.join(', ')}`);
558568 }
569+ core.setOutput('targets', JSON.stringify([...allowedTargets]));
559570 });
560571 } catch (error) {
561572 core.setFailed(error);
@@ -838,6 +849,57 @@ jobs:
838849 cosignPath,
839850 `${containerName}:/usr/bin/cosign`
840851 ]);
852+ -
853+ name : Configure AWS credentials
854+ if : ${{ needs.registry-identities.outputs.aws-ecr-enabled == 'true' }}
855+ uses : aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
856+ with :
857+ role-to-assume : ${{ needs.registry-identities.outputs.aws-ecr-role-to-assume }}
858+ aws-region : ${{ needs.registry-identities.outputs.aws-ecr-region }}
859+ -
860+ name : Login to Amazon ECR
861+ if : ${{ needs.registry-identities.outputs.aws-ecr-enabled == 'true' }}
862+ uses : docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
863+ with :
864+ registry-auth : |
865+ - registry: ${{ needs.registry-identities.outputs.aws-ecr-registry }}
866+ -
867+ name : Authenticate to Google Cloud
868+ id : gcp-wif-auth
869+ if : ${{ needs.registry-identities.outputs.gcp-wif-enabled == 'true' }}
870+ uses : google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
871+ with :
872+ token_format : access_token
873+ workload_identity_provider : ${{ needs.registry-identities.outputs.gcp-wif-workload-identity-provider }}
874+ service_account : ${{ needs.registry-identities.outputs.gcp-wif-service-account }}
875+ project_id : ${{ needs.registry-identities.outputs.gcp-wif-project-id }}
876+ create_credentials_file : false
877+ export_environment_variables : false
878+ -
879+ name : Login to Google Artifact Registry
880+ if : ${{ needs.registry-identities.outputs.gcp-wif-enabled == 'true' }}
881+ uses : docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
882+ with :
883+ registry-auth : |
884+ - registry: ${{ needs.registry-identities.outputs.gcp-wif-registry }}
885+ username: oauth2accesstoken
886+ password: ${{ steps.gcp-wif-auth.outputs.access_token }}
887+ -
888+ name : Login to Docker Hub with OIDC
889+ if : ${{ needs.registry-identities.outputs.dockerhub-oidc-enabled == 'true' }}
890+ uses : docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
891+ env :
892+ DOCKERHUB_OIDC_CONNECTIONID : ${{ needs.registry-identities.outputs.dockerhub-oidc-connection-id }}
893+ with :
894+ registry-auth : |
895+ - registry: ${{ needs.registry-identities.outputs.dockerhub-oidc-registry }}
896+ username: ${{ needs.registry-identities.outputs.dockerhub-oidc-username }}
897+ -
898+ name : Login to registry
899+ if : ${{ inputs.push && inputs.output == 'image' && env.REGISTRY_AUTHS_PRESENT == 'true' }}
900+ uses : docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
901+ with :
902+ registry-auth : ${{ secrets.registry-auths }}
841903 -
842904 name : Prepare
843905 id : prepare
@@ -849,6 +911,8 @@ jobs:
849911 INPUT_CACHE : ${{ inputs.cache }}
850912 INPUT_CACHE-SCOPE : ${{ inputs.cache-scope }}
851913 INPUT_CACHE-MODE : ${{ inputs.cache-mode }}
914+ INPUT_BUILD-SECRETS : ${{ secrets.build-secrets }}
915+ INPUT_TARGETS : ${{ needs.prepare.outputs.targets }}
852916 INPUT_CONTEXT : ${{ inputs.context }}
853917 INPUT_FILES : ${{ inputs.files }}
854918 INPUT_OUTPUT : ${{ inputs.output }}
@@ -865,15 +929,23 @@ jobs:
865929 INPUT_BAKE-FILE-TAGS : ${{ steps.meta.outputs.bake-file-tags }}
866930 INPUT_BAKE-FILE-ANNOTATIONS : ${{ steps.meta.outputs.bake-file-annotations }}
867931 INPUT_BAKE-FILE-LABELS : ${{ steps.meta.outputs.bake-file-labels }}
868- INPUT_GITHUB-TOKEN : ${{ secrets.github-token || github.token }}
869932 INPUT_BUILDKIT-PROXY-NETWORK : ${{ inputs.buildkit-proxy-network }}
870933 with :
871934 script : |
935+ const fs = require('fs');
872936 const os = require('os');
937+ const path = require('path');
873938 const { Build } = require('@docker/github-builder-runtime/lib/buildx/build');
874939 const { GitHub } = require('@docker/github-builder-runtime/lib/github/github');
875940 const { Util } = require('@docker/github-builder-runtime/lib/util');
876-
941+
942+ let yaml;
943+ try {
944+ yaml = require('js-yaml');
945+ } catch {
946+ yaml = require('@docker/github-builder-runtime/node_modules/js-yaml');
947+ }
948+
877949 const inpPlatform = core.getInput('platform');
878950 const platformPairSuffix = inpPlatform ? `-${inpPlatform.replace(/\//g, '-')}` : '';
879951 core.setOutput('platform-pair-suffix', platformPairSuffix);
@@ -884,6 +956,8 @@ jobs:
884956 const inpCache = core.getBooleanInput('cache');
885957 const inpCacheScope = core.getInput('cache-scope');
886958 const inpCacheMode = core.getInput('cache-mode');
959+ const inpBuildSecrets = core.getInput('build-secrets');
960+ const inpTargets = core.getInput('targets');
887961 const inpContext = core.getInput('context');
888962 const inpFiles = Util.getInputList('files');
889963 const inpOutput = core.getInput('output');
@@ -892,7 +966,6 @@ jobs:
892966 const inpSet = Util.getInputList('set', {ignoreComma: true, quote: false});
893967 const inpTarget = core.getInput('target');
894968 const inpVars = Util.getInputList('vars');
895- const inpGitHubToken = core.getInput('github-token');
896969 const inpBuildkitProxyNetwork = core.getBooleanInput('buildkit-proxy-network');
897970
898971 const inpMetaImages = core.getMultilineInput('meta-images');
@@ -909,6 +982,42 @@ jobs:
909982 tags: inpMetaTags
910983 };
911984 const renderTemplate = value => Util.compileHandlebars(value, {noEscape: true}, {meta});
985+
986+ const isInputKeySafe = value => value && !/[\r\n=]/.test(value);
987+ const parseBuildSecretKey = key => {
988+ const separator = key.lastIndexOf('.');
989+ return separator === -1 ? {target: inpTarget, id: key} : {target: key.substring(0, separator), id: key.substring(separator + 1)};
990+ };
991+ const parseBuildSecrets = value => {
992+ const normalized = value.trim();
993+ if (!normalized) {
994+ return [];
995+ }
996+ let parsed;
997+ try {
998+ parsed = yaml.load(normalized, {schema: yaml.FAILSAFE_SCHEMA});
999+ } catch (err) {
1000+ const location = err.mark ? ` at line ${err.mark.line + 1}, column ${err.mark.column + 1}` : '';
1001+ throw new Error(`Failed to parse build-secrets YAML${location}`);
1002+ }
1003+ if (!parsed) {
1004+ return [];
1005+ }
1006+ if (Array.isArray(parsed) || typeof parsed !== 'object') {
1007+ throw new Error('build-secrets must be a YAML object');
1008+ }
1009+ return Object.entries(parsed).map(([key, secret]) => {
1010+ const {target, id} = parseBuildSecretKey(key);
1011+ if (!isInputKeySafe(target) || !isInputKeySafe(id)) {
1012+ throw new Error(`Invalid build-secrets key "${key}": use "secret_id" or "target.secret_id" without empty names, line breaks or "="`);
1013+ }
1014+ if (typeof secret !== 'string') {
1015+ throw new Error(`build-secrets value for "${key}" must be a string`);
1016+ }
1017+ core.setSecret(secret);
1018+ return {target, id, secret};
1019+ });
1020+ };
9121021
9131022 const gitContextAttrs = GitHub.context.ref.startsWith('refs/tags/') ? {checksum: GitHub.context.sha} : {'fetch-by-commit': 'true'};
9141023 const bakeSource = await new Build().gitContext({subdir: inpContext, attrs: gitContextAttrs});
@@ -927,7 +1036,29 @@ jobs:
9271036 core.info(sbom);
9281037 core.setOutput('sbom', sbom);
9291038 });
930-
1039+
1040+ let buildSecrets;
1041+ try {
1042+ buildSecrets = parseBuildSecrets(inpBuildSecrets);
1043+ } catch (err) {
1044+ core.setFailed(err.message);
1045+ return;
1046+ }
1047+
1048+ let targets;
1049+ try {
1050+ targets = JSON.parse(inpTargets || '[]');
1051+ const allowedTargets = new Set(targets);
1052+ for (const {target} of buildSecrets) {
1053+ if (!allowedTargets.has(target)) {
1054+ throw new Error(`Build secret target "${target}" is not part of the resolved Bake definition`);
1055+ }
1056+ }
1057+ } catch (err) {
1058+ core.setFailed(err.message);
1059+ return;
1060+ }
1061+
9311062 const envs = Object.assign({},
9321063 inpVars ? inpVars.reduce((acc, curr) => {
9331064 const idx = curr.indexOf('=');
@@ -937,12 +1068,26 @@ jobs:
9371068 return acc;
9381069 }, {}) : {},
9391070 {
940- BUILDKIT_MULTI_PLATFORM: '1',
941- BUILDX_BAKE_GIT_AUTH_TOKEN: inpGitHubToken
1071+ BUILDKIT_MULTI_PLATFORM: '1'
9421072 }
9431073 );
1074+
1075+ // Git authentication is supplied directly to the Bake action.
1076+ delete envs.BUILDX_BAKE_GIT_AUTH_TOKEN;
1077+
1078+ const secretOverrides = [];
1079+ if (buildSecrets.length > 0) {
1080+ const secretDir = fs.mkdtempSync(path.join(process.env.RUNNER_TEMP, 'build-secrets-'));
1081+ core.setOutput('secret-dir', secretDir);
1082+ buildSecrets.forEach(({target, id, secret}, index) => {
1083+ const secretFile = path.join(secretDir, String(index));
1084+ fs.writeFileSync(secretFile, secret, {mode: 0o600});
1085+ secretOverrides.push(`${target}.secret.${id}=src=${secretFile}`);
1086+ });
1087+ }
1088+
9441089 await core.group(`Set envs`, async () => {
945- core.info(JSON.stringify(envs, null, 2));
1090+ core.info(JSON.stringify(Object.keys( envs).sort() , null, 2));
9461091 core.setOutput('envs', JSON.stringify(envs));
9471092 });
9481093
@@ -1006,60 +1151,10 @@ jobs:
10061151 bakeOverrides.push(`*.cache-from=type=gha,scope=${inpCacheScope || inpTarget}${platformPairSuffix}${proxyNetworkSuffix}`);
10071152 bakeOverrides.push(`*.cache-to=type=gha,ignore-error=true,scope=${inpCacheScope || inpTarget}${platformPairSuffix}${proxyNetworkSuffix},mode=${inpCacheMode}`);
10081153 }
1154+ bakeOverrides.push(...secretOverrides);
10091155 core.info(JSON.stringify(bakeOverrides, null, 2));
10101156 core.setOutput('overrides', bakeOverrides.join(os.EOL));
10111157 });
1012- -
1013- name : Configure AWS credentials
1014- if : ${{ needs.registry-identities.outputs.aws-ecr-enabled == 'true' }}
1015- uses : aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
1016- with :
1017- role-to-assume : ${{ needs.registry-identities.outputs.aws-ecr-role-to-assume }}
1018- aws-region : ${{ needs.registry-identities.outputs.aws-ecr-region }}
1019- -
1020- name : Login to Amazon ECR
1021- if : ${{ needs.registry-identities.outputs.aws-ecr-enabled == 'true' }}
1022- uses : docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
1023- with :
1024- registry-auth : |
1025- - registry: ${{ needs.registry-identities.outputs.aws-ecr-registry }}
1026- -
1027- name : Authenticate to Google Cloud
1028- id : gcp-wif-auth
1029- if : ${{ needs.registry-identities.outputs.gcp-wif-enabled == 'true' }}
1030- uses : google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
1031- with :
1032- token_format : access_token
1033- workload_identity_provider : ${{ needs.registry-identities.outputs.gcp-wif-workload-identity-provider }}
1034- service_account : ${{ needs.registry-identities.outputs.gcp-wif-service-account }}
1035- project_id : ${{ needs.registry-identities.outputs.gcp-wif-project-id }}
1036- create_credentials_file : false
1037- export_environment_variables : false
1038- -
1039- name : Login to Google Artifact Registry
1040- if : ${{ needs.registry-identities.outputs.gcp-wif-enabled == 'true' }}
1041- uses : docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
1042- with :
1043- registry-auth : |
1044- - registry: ${{ needs.registry-identities.outputs.gcp-wif-registry }}
1045- username: oauth2accesstoken
1046- password: ${{ steps.gcp-wif-auth.outputs.access_token }}
1047- -
1048- name : Login to Docker Hub with OIDC
1049- if : ${{ needs.registry-identities.outputs.dockerhub-oidc-enabled == 'true' }}
1050- uses : docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
1051- env :
1052- DOCKERHUB_OIDC_CONNECTIONID : ${{ needs.registry-identities.outputs.dockerhub-oidc-connection-id }}
1053- with :
1054- registry-auth : |
1055- - registry: ${{ needs.registry-identities.outputs.dockerhub-oidc-registry }}
1056- username: ${{ needs.registry-identities.outputs.dockerhub-oidc-username }}
1057- -
1058- name : Login to registry
1059- if : ${{ inputs.push && inputs.output == 'image' && env.REGISTRY_AUTHS_PRESENT == 'true' }}
1060- uses : docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
1061- with :
1062- registry-auth : ${{ secrets.registry-auths }}
10631158 -
10641159 name : Build
10651160 id : bake
@@ -1070,7 +1165,18 @@ jobs:
10701165 targets : ${{ steps.prepare.outputs.target }}
10711166 sbom : ${{ steps.prepare.outputs.sbom }}
10721167 set : ${{ steps.prepare.outputs.overrides }}
1168+ github-token : ${{ secrets.github-token || github.token }}
10731169 env : ${{ fromJson(steps.prepare.outputs.envs || '{}') }}
1170+ -
1171+ name : Remove build secrets
1172+ if : ${{ always() && steps.prepare.outputs.secret-dir != '' }}
1173+ uses : actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
1174+ env :
1175+ INPUT_SECRET-DIR : ${{ steps.prepare.outputs.secret-dir }}
1176+ with :
1177+ script : |
1178+ const fs = require('fs');
1179+ fs.rmSync(core.getInput('secret-dir', {required: true}), {recursive: true, force: true});
10741180 -
10751181 name : Get image digest
10761182 id : get-image-digest
0 commit comments