From 2e27f81f3ff42b613c1232b92de9ab0e82e142c3 Mon Sep 17 00:00:00 2001 From: Kang Jaehong Date: Sat, 31 Oct 2026 17:12:04 +0900 Subject: [PATCH] buildx(build): percent-encode git context query values BuildKit parses the query-format git context with Go's url.Query(), which decodes "+" as a space and treats "&", "=", "#", "%" and ";" as delimiters. Since tag refs are now passed verbatim (ref=refs/tags/...), a SemVer tag with build metadata such as v1.0.0+build.1 was resolved as "refs/tags/v1.0.0 build.1" and the build failed to load the source. Encode each query value with encodeURIComponent per path segment so "/" stays readable while every other reserved character round-trips. The fragment format is left untouched because BuildKit reads it without decoding. Fixes docker/github-builder#330 Signed-off-by: Kang Jaehong --- __tests__/buildx/build.test.ts | 5 +++++ src/buildx/build.ts | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/__tests__/buildx/build.test.ts b/__tests__/buildx/build.test.ts index 18084cfa..d25883a7 100644 --- a/__tests__/buildx/build.test.ts +++ b/__tests__/buildx/build.test.ts @@ -120,6 +120,11 @@ describe('gitContext', () => { [{ref: 'refs/heads/master', checksum: undefined, format: 'query', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, attrs: {'fetch-by-commit': 'true'}}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/master&checksum=860c1904a1ce19322e91ac35af1ab07466440c37&fetch-by-commit=true'], [{ref: 'refs/pull/15/merge', checksum: undefined, format: 'query', prHeadRef: true, sendGitQueryAsInput: false, buildxQuerySupport: true, attrs: {'fetch-by-commit': 'true'}}, 'https://github.com/docker/actions-toolkit.git?ref=refs/pull/15/head&checksum=f11797113e5a9b86bd976329c5dbb8a8bfdfadfa&fetch-by-commit=true'], [{ref: 'refs/heads/master', checksum: undefined, format: 'query', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, attrs: {'fetch-by-commit': 'false'}}, 'https://github.com/docker/actions-toolkit.git?ref=860c1904a1ce19322e91ac35af1ab07466440c37&fetch-by-commit=false'], + // query values must be percent-encoded: BuildKit decodes "+" (SemVer build metadata separator) as a space (https://github.com/docker/github-builder/issues/330) + [{ref: 'refs/tags/v1.0.0-beta+exp.sha.5114f85', checksum: undefined, format: 'query', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, attrs: {checksum: 'cafebabe'}}, 'https://github.com/docker/actions-toolkit.git?ref=refs/tags/v1.0.0-beta%2Bexp.sha.5114f85&checksum=cafebabe'], + [{ref: 'refs/tags/v1.0.0-beta+exp.sha.5114f85', checksum: undefined, format: 'query', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, attrs: {'fetch-by-commit': 'true'}}, 'https://github.com/docker/actions-toolkit.git?ref=refs/tags/v1.0.0-beta%2Bexp.sha.5114f85&checksum=860c1904a1ce19322e91ac35af1ab07466440c37&fetch-by-commit=true'], + [{ref: 'refs/heads/feature/a&b=c#d%e', checksum: undefined, format: 'query', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, attrs: {checksum: 'cafebabe'}}, 'https://github.com/docker/actions-toolkit.git?ref=refs/heads/feature/a%26b%3Dc%23d%25e&checksum=cafebabe'], + [{ref: 'refs/heads/master', checksum: undefined, format: 'query', prHeadRef: false, sendGitQueryAsInput: false, buildxQuerySupport: true, subdir: 'sub dir/with+plus'}, 'https://github.com/docker/actions-toolkit.git?ref=860c1904a1ce19322e91ac35af1ab07466440c37&subdir=sub%20dir/with%2Bplus'], // 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'], diff --git a/src/buildx/build.ts b/src/buildx/build.ts index ae08666c..91b30582 100644 --- a/src/buildx/build.ts +++ b/src/buildx/build.ts @@ -102,16 +102,16 @@ export class Build { // Only use fetch-by-commit when the caller opts in; older BuildKit // versions do not know this query key. Without it, use the pinned commit // as the ref to keep existing compatibility. - const query = [`ref=${fetchByCommit || explicitChecksum ? ref : pinnedRef}`]; + const query = [`ref=${Build.encodeGitQueryValue(fetchByCommit || explicitChecksum ? ref : pinnedRef)}`]; const queryChecksum = fetchByCommit ? pinnedRef : explicitChecksum; if (queryChecksum) { - query.push(`checksum=${queryChecksum}`); + query.push(`checksum=${Build.encodeGitQueryValue(queryChecksum)}`); } if (subdir && subdir !== '.') { - query.push(`subdir=${subdir}`); + query.push(`subdir=${Build.encodeGitQueryValue(subdir)}`); } for (const [name, value] of extraAttrs) { - query.push(`${name}=${value}`); + query.push(`${Build.encodeGitQueryValue(name)}=${Build.encodeGitQueryValue(value)}`); } return `${baseURL}?${query.join('&')}`; } @@ -119,6 +119,14 @@ export class Build { return `${baseURL}#${pinnedRef}${subdir && subdir !== '.' ? `:${subdir}` : ''}`; } + // BuildKit parses the git context query with Go's url.Query(), which + // decodes "+" as a space and treats "&", "=", "#", "%" and ";" as + // delimiters. Percent-encode each value but keep "/" readable since it has + // no special meaning in a query component (e.g. refs/tags/v1.0.0+build). + private static encodeGitQueryValue(value: string): string { + return value.split('/').map(encodeURIComponent).join('/'); + } + public getImageIDFilePath(): string { return path.join(Context.tmpDir(), this.iidFilename); }