From e314ca9bb539a1824edcce14d7845662ddc199db Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:04:45 +0100 Subject: [PATCH] buildx(build): support extensible git context attrs Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- __tests__/buildx/build.test.ts | 37 +++++++++++++++++++++------------- src/buildx/build.ts | 32 +++++++++++++++++------------ 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/__tests__/buildx/build.test.ts b/__tests__/buildx/build.test.ts index e56fa28e..df202b8a 100644 --- a/__tests__/buildx/build.test.ts +++ b/__tests__/buildx/build.test.ts @@ -60,13 +60,14 @@ describe('gitContext', () => { type GitContextTestCase = { ref: string; + checksum?: string; + subdir?: string; + attrs?: Record; format: GitContextFormat | undefined; + prHeadRef: boolean; sendGitQueryAsInput: boolean; buildxQuerySupport: boolean; - subdir?: string; - keepGitDir?: boolean; - submodules?: boolean; }; // prettier-ignore @@ -82,10 +83,15 @@ describe('gitContext', () => { [{ref: 'refs/pull/15/merge', format: undefined, prHeadRef: false, sendGitQueryAsInput: true, buildxQuerySupport: true}, 'https://github.com/docker/actions-toolkit.git?ref=refs/pull/15/merge&checksum=860c1904a1ce19322e91ac35af1ab07466440c37'], [{ref: 'refs/pull/15/merge', format: undefined, prHeadRef: true, sendGitQueryAsInput: true, buildxQuerySupport: true}, 'https://github.com/docker/actions-toolkit.git?ref=refs/pull/15/head&checksum=860c1904a1ce19322e91ac35af1ab07466440c37'], [{ref: 'refs/heads/master', format: undefined, prHeadRef: false, sendGitQueryAsInput: true, buildxQuerySupport: false}, 'https://github.com/docker/actions-toolkit.git#860c1904a1ce19322e91ac35af1ab07466440c37'], - [{ref: 'refs/heads/master', format: undefined, prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, keepGitDir: true}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37&keep-git-dir=true'], - [{ref: 'refs/heads/master', format: undefined, prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: false, keepGitDir: true}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37&keep-git-dir=true'], - [{ref: 'refs/heads/master', format: undefined, prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, submodules: false}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37&submodules=false'], - [{ref: 'refs/heads/master', format: undefined, prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: false, submodules: false}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37&submodules=false'], + [{ref: 'refs/heads/master', format: undefined, prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, attrs: {}}, 'https://github.com/docker/actions-toolkit.git#860c1904a1ce19322e91ac35af1ab07466440c37'], + [{ref: 'refs/heads/master', checksum: undefined, format: undefined, prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, attrs: {checksum: 'cafebabe'}}, 'https://github.com/docker/actions-toolkit.git#cafebabe'], + [{ref: 'refs/heads/master', format: undefined, prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, attrs: {subdir: 'subdir'}}, 'https://github.com/docker/actions-toolkit.git#860c1904a1ce19322e91ac35af1ab07466440c37:subdir'], + [{ref: 'refs/heads/master', format: undefined, prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, attrs: {ref: 'refs/tags/v1.0.0'}}, 'https://github.com/docker/actions-toolkit.git#860c1904a1ce19322e91ac35af1ab07466440c37'], + [{ref: 'refs/heads/master', format: undefined, prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, attrs: {'keep-git-dir': 'true'}}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37&keep-git-dir=true'], + [{ref: 'refs/heads/master', format: undefined, prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: false, attrs: {'keep-git-dir': 'true'}}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37&keep-git-dir=true'], + [{ref: 'refs/heads/master', checksum: undefined, format: undefined, prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, attrs: {checksum: 'cafebabe', 'keep-git-dir': 'true'}}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=cafebabe&keep-git-dir=true'], + [{ref: 'refs/heads/master', format: undefined, prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, attrs: {submodules: 'false'}}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37&submodules=false'], + [{ref: 'refs/heads/master', format: undefined, prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: false, attrs: {submodules: 'false'}}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37&submodules=false'], // query format [{ref: 'refs/heads/master', format: 'query', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37'], [{ref: 'master', format: 'query', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37'], @@ -94,22 +100,26 @@ describe('gitContext', () => { [{ref: 'refs/pull/15/merge', format: 'query', prHeadRef: true, sendGitQueryAsInput: false, buildxQuerySupport: true}, 'https://github.com/docker/actions-toolkit.git?ref=refs/pull/15/head&checksum=860c1904a1ce19322e91ac35af1ab07466440c37'], [{ref: 'refs/heads/master', format: 'query', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, subdir: 'subdir'}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37&subdir=subdir'], [{ref: 'refs/heads/master', format: 'query', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, subdir: '.'}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37'], - [{ref: 'refs/heads/master', format: 'query', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, subdir: 'subdir', keepGitDir: true}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37&subdir=subdir&keep-git-dir=true'], - [{ref: 'refs/heads/master', format: 'query', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, submodules: true}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37&submodules=true'], - [{ref: 'refs/heads/master', format: 'query', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, submodules: false}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37&submodules=false'], + [{ref: 'refs/heads/master', checksum: undefined, format: 'query', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, attrs: {ref: 'refs/tags/v1.0.0', checksum: 'cafebabe', subdir: 'subdir', submodules: 'false'}}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=cafebabe&subdir=subdir&submodules=false'], + [{ref: 'refs/heads/master', format: 'query', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, subdir: 'subdir', attrs: {'keep-git-dir': 'true'}}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37&subdir=subdir&keep-git-dir=true'], + [{ref: 'refs/heads/master', format: 'query', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, attrs: {submodules: 'true'}}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37&submodules=true'], + [{ref: 'refs/heads/master', format: 'query', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, attrs: {submodules: 'false'}}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37&submodules=false'], + [{ref: 'refs/heads/master', format: 'query', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, attrs: {'keep-git-dir': 'true', submodules: 'false'}}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37&keep-git-dir=true&submodules=false'], // fragment format [{ref: 'refs/heads/master', format: 'fragment', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true}, 'https://github.com/docker/actions-toolkit.git#860c1904a1ce19322e91ac35af1ab07466440c37'], [{ref: 'master', format: 'fragment', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true}, 'https://github.com/docker/actions-toolkit.git#860c1904a1ce19322e91ac35af1ab07466440c37'], [{ref: 'refs/pull/15/merge', format: 'fragment', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true}, 'https://github.com/docker/actions-toolkit.git#refs/pull/15/merge'], [{ref: 'refs/tags/v1.0.0', format: 'fragment', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true}, 'https://github.com/docker/actions-toolkit.git#860c1904a1ce19322e91ac35af1ab07466440c37'], [{ref: 'refs/pull/15/merge', format: 'fragment', prHeadRef: true, sendGitQueryAsInput: false, buildxQuerySupport: true}, 'https://github.com/docker/actions-toolkit.git#refs/pull/15/head'], + [{ref: 'refs/heads/master', checksum: undefined, format: 'fragment', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, attrs: {checksum: 'cafebabe', subdir: 'subdir', ref: 'refs/tags/v1.0.0'}}, 'https://github.com/docker/actions-toolkit.git#cafebabe:subdir'], + [{ref: 'refs/heads/master', format: 'fragment', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, attrs: {'keep-git-dir': 'true'}}, 'https://github.com/docker/actions-toolkit.git#860c1904a1ce19322e91ac35af1ab07466440c37'], [{ref: 'refs/heads/master', format: 'fragment', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, subdir: 'subdir'}, 'https://github.com/docker/actions-toolkit.git#860c1904a1ce19322e91ac35af1ab07466440c37:subdir'], [{ref: 'refs/heads/master', format: 'fragment', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, subdir: '.'}, 'https://github.com/docker/actions-toolkit.git#860c1904a1ce19322e91ac35af1ab07466440c37'], [{ref: 'refs/pull/15/merge', format: 'fragment', prHeadRef: true, sendGitQueryAsInput: false, buildxQuerySupport: true, subdir: 'subdir'}, 'https://github.com/docker/actions-toolkit.git#refs/pull/15/head:subdir'], ]; test.each(gitContextCases)('given %o should return %o', async (input: GitContextTestCase, expected: string) => { - const {ref, format, prHeadRef, sendGitQueryAsInput, buildxQuerySupport, subdir, keepGitDir, submodules} = input; + const {ref, checksum, format, prHeadRef, sendGitQueryAsInput, buildxQuerySupport, subdir, attrs} = input; process.env.DOCKER_DEFAULT_GIT_CONTEXT_PR_HEAD_REF = prHeadRef ? 'true' : ''; process.env.BUILDX_SEND_GIT_QUERY_AS_INPUT = sendGitQueryAsInput ? 'true' : ''; const buildx = new Buildx(); @@ -118,11 +128,10 @@ describe('gitContext', () => { expect( await build.gitContext({ ref, - checksum: '860c1904a1ce19322e91ac35af1ab07466440c37', + ...('checksum' in input ? {checksum} : {checksum: '860c1904a1ce19322e91ac35af1ab07466440c37'}), format, subdir, - keepGitDir, - submodules + attrs }) ).toEqual(expected); }); diff --git a/src/buildx/build.ts b/src/buildx/build.ts index 5e23f16c..01b70719 100644 --- a/src/buildx/build.ts +++ b/src/buildx/build.ts @@ -42,8 +42,7 @@ export interface GitContextOpts { ref?: string; checksum?: string; subdir?: string; - keepGitDir?: boolean; - submodules?: boolean; + attrs?: Record; format?: GitContextFormat; } @@ -59,19 +58,29 @@ export class Build { } public async gitContext(opts?: GitContextOpts): Promise { + const gitContextCommonAttrs = new Set(['ref', 'checksum', 'subdir']); const setPullRequestHeadRef = Util.parseBoolOrDefault(process.env.DOCKER_DEFAULT_GIT_CONTEXT_PR_HEAD_REF); - const gitChecksum = opts?.checksum || github.context.sha; - let ref = opts?.ref || github.context.ref; + const commonAttrs = { + ref: opts?.attrs?.ref, + checksum: opts?.attrs?.checksum, + subdir: opts?.attrs?.subdir + }; + + const gitChecksum = opts?.checksum || commonAttrs.checksum || github.context.sha; + let ref = opts?.ref || commonAttrs.ref || github.context.ref; + const subdir = opts?.subdir || commonAttrs.subdir; + const attrs = Object.entries(opts?.attrs || {}).filter(([name]) => !gitContextCommonAttrs.has(name)); if (!ref.startsWith('refs/')) { ref = `refs/heads/${ref}`; } else if (ref.startsWith(`refs/pull/`) && setPullRequestHeadRef) { ref = ref.replace(/\/merge$/g, '/head'); } + const baseURL = `${GitHub.serverURL}/${github.context.repo.owner}/${github.context.repo.repo}.git`; let format = opts?.format; if (!format) { const sendGitQueryAsInput = Util.parseBoolOrDefault(process.env.BUILDX_SEND_GIT_QUERY_AS_INPUT); - if (opts?.keepGitDir || typeof opts?.submodules !== 'undefined') { + if (attrs.length > 0) { format = 'query'; } else if (sendGitQueryAsInput && (await this.buildx.versionSatisfies('>=0.29.0'))) { format = 'query'; @@ -84,19 +93,16 @@ export class Build { if (gitChecksum) { query.push(`checksum=${gitChecksum}`); } - if (opts?.subdir && opts.subdir !== '.') { - query.push(`subdir=${opts.subdir}`); - } - if (typeof opts?.keepGitDir !== 'undefined') { - query.push(`keep-git-dir=${opts.keepGitDir}`); + if (subdir && subdir !== '.') { + query.push(`subdir=${subdir}`); } - if (typeof opts?.submodules !== 'undefined') { - query.push(`submodules=${opts.submodules}`); + for (const [name, value] of attrs) { + query.push(`${name}=${value}`); } return `${baseURL}?${query.join('&')}`; } const fragmentRef = gitChecksum && !ref.startsWith(`refs/pull/`) ? gitChecksum : ref; - return `${baseURL}#${fragmentRef}${opts?.subdir && opts.subdir !== '.' ? `:${opts.subdir}` : ''}`; + return `${baseURL}#${fragmentRef}${subdir && subdir !== '.' ? `:${subdir}` : ''}`; } public getImageIDFilePath(): string {