From e2bd4a9000dce0768fd6298a5a3d250c88baca6e Mon Sep 17 00:00:00 2001 From: Pandelis Zembashis
Date: Sat, 17 Oct 2026 21:32:43 -0700 Subject: [PATCH 1/2] Validate cached refs in one Git transaction --- __test__/composal-cache.test.ts | 6 ++++++ dist/index.js | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/composal-cache.ts | 2 +- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/__test__/composal-cache.test.ts b/__test__/composal-cache.test.ts index 0236fd5..c782f8b 100644 --- a/__test__/composal-cache.test.ts +++ b/__test__/composal-cache.test.ts @@ -233,6 +233,12 @@ test('full history, annotated tags and sparse checkout keep standard Git behavio await expect(fs.access(path.join(workspace, 'root-file'))).rejects.toThrow() }) +test('bulk ref creation rejects invalid names without publishing local refs', async () => { + await expect( + cache({'refs/heads/main': second, 'refs/heads/invalid..name': first}) + ).rejects.toThrow('Local checkout cache operation failed') +}) + test('dissociate makes the checkout usable after the mounted cache disappears', async () => { const checkout = await cache() expect( diff --git a/dist/index.js b/dist/index.js index def80ec..a3ff300 100644 --- a/dist/index.js +++ b/dist/index.js @@ -42329,9 +42329,9 @@ class CheckoutCache { for (const [name, value] of Object.entries(refs)) { if (!oid.test(value) || !name.startsWith('refs/')) throw new Error('Invalid checkout cache ref'); - run(['check-ref-format', name], localOrigin); commands.push(`create ${name} ${value}`); } + // update-ref validates the complete batch, including every ref name. run(['update-ref', '--stdin'], localOrigin, commands.join('\n') + '\n'); const alternateFile = external_node_path_namespaceObject.join(workspace, '.git', 'objects', 'info', 'alternates'); let previous = ''; diff --git a/package-lock.json b/package-lock.json index acd9175..f13d01f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "composal-checkout", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "composal-checkout", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@actions/core": "^3.0.1", diff --git a/package.json b/package.json index ec8de79..a2df9f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "composal-checkout", - "version": "1.0.0", + "version": "1.0.1", "description": "Composal Git checkout action", "type": "module", "main": "lib/main.js", diff --git a/src/composal-cache.ts b/src/composal-cache.ts index 12482d6..c558fbc 100644 --- a/src/composal-cache.ts +++ b/src/composal-cache.ts @@ -185,9 +185,9 @@ export class CheckoutCache { for (const [name, value] of Object.entries(refs)) { if (!oid.test(value) || !name.startsWith('refs/')) throw new Error('Invalid checkout cache ref') - run(['check-ref-format', name], localOrigin) commands.push(`create ${name} ${value}`) } + // update-ref validates the complete batch, including every ref name. run(['update-ref', '--stdin'], localOrigin, commands.join('\n') + '\n') const alternateFile = path.join( workspace, From eda86db83397b7b8ba2d88fead63c9cd85e644a6 Mon Sep 17 00:00:00 2001 From: Pandelis Zembashis
Date: Sat, 17 Oct 2026 21:33:34 -0700 Subject: [PATCH 2/2] Reject command delimiters before bulk ref validation --- __test__/composal-cache.test.ts | 2 +- dist/index.js | 4 +++- src/composal-cache.ts | 6 +++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/__test__/composal-cache.test.ts b/__test__/composal-cache.test.ts index c782f8b..5ed2080 100644 --- a/__test__/composal-cache.test.ts +++ b/__test__/composal-cache.test.ts @@ -277,7 +277,7 @@ test('cache fetch failure removes its alternate and allows normal GitHub fetchin test('malformed cached ref metadata cannot write outside the temporary ref database', async () => { await expect( - cache({'refs/heads/main\ncreate refs/heads/injected': second}) + cache({[`refs/heads/main ${second}\ncreate refs/heads/injected`]: second}) ).rejects.toThrow() await expect( fs.access(path.join(workspace, '.git', 'objects', 'info', 'alternates')) diff --git a/dist/index.js b/dist/index.js index a3ff300..e399124 100644 --- a/dist/index.js +++ b/dist/index.js @@ -42327,7 +42327,9 @@ class CheckoutCache { const refs = { ...manifest.refs, 'refs/composal/target': manifest.object_oid }; const commands = []; for (const [name, value] of Object.entries(refs)) { - if (!oid.test(value) || !name.startsWith('refs/')) + if (!oid.test(value) || + !name.startsWith('refs/') || + /[\x00-\x20\x7f]/.test(name)) throw new Error('Invalid checkout cache ref'); commands.push(`create ${name} ${value}`); } diff --git a/src/composal-cache.ts b/src/composal-cache.ts index c558fbc..e63522a 100644 --- a/src/composal-cache.ts +++ b/src/composal-cache.ts @@ -183,7 +183,11 @@ export class CheckoutCache { const refs = {...manifest.refs, 'refs/composal/target': manifest.object_oid} const commands: string[] = [] for (const [name, value] of Object.entries(refs)) { - if (!oid.test(value) || !name.startsWith('refs/')) + if ( + !oid.test(value) || + !name.startsWith('refs/') || + /[\x00-\x20\x7f]/.test(name) + ) throw new Error('Invalid checkout cache ref') commands.push(`create ${name} ${value}`) }