diff --git a/packages/glob/__tests__/hash-files.test.ts b/packages/glob/__tests__/hash-files.test.ts index 1ab4c524cf..d5d3d687c0 100644 --- a/packages/glob/__tests__/hash-files.test.ts +++ b/packages/glob/__tests__/hash-files.test.ts @@ -269,6 +269,46 @@ describe('globber', () => { } }) + it('honors an explicit root outside the workspace without the opt-in', async () => { + // Mirrors the documented GITHUB_ACTION_PATH usage: an action passes its own + // directory as an allowed root, and that directory is not under the workspace. + const insideRoot = path.join(getTestTemp(), 'explicit-root-inside') + await fs.mkdir(insideRoot, {recursive: true}) + await fs.writeFile(path.join(insideRoot, 'inside.txt'), 'inside content') + + // Outside GITHUB_WORKSPACE (which the suite pins to __dirname). + const actionRoot = await fs.mkdtemp( + path.join(os.tmpdir(), 'hash-files-explicit-root-') + ) + try { + await fs.writeFile(path.join(actionRoot, 'action.txt'), 'action content') + + const patterns = `${insideRoot}/*\n${actionRoot}/*` + + const insideOnly = await hashFiles(`${insideRoot}/*`, '', { + roots: [insideRoot] + }) + expect(insideOnly).not.toEqual('') + + // The explicit roots list is the allowlist, so files under actionRoot + // must be hashed even though allowFilesOutsideWorkspace is not set. + const both = await hashFiles(patterns, '', { + roots: [insideRoot, actionRoot] + }) + expect(both).not.toEqual('') + expect(both).not.toEqual(insideOnly) + + // And it hashes exactly those two files, no more. + const expected = await hashFiles(patterns, '', { + roots: [insideRoot, actionRoot], + allowFilesOutsideWorkspace: true + }) + expect(both).toEqual(expected) + } finally { + await io.rmRF(actionRoot) + } + }) + it('applies relative exclude patterns across all allowed roots', async () => { const root = path.join(getTestTemp(), 'exclude-across-roots') const dir1 = path.join(root, 'dir1') diff --git a/packages/glob/src/internal-hash-files.ts b/packages/glob/src/internal-hash-files.ts index 3ffaf811cd..888bc49486 100644 --- a/packages/glob/src/internal-hash-files.ts +++ b/packages/glob/src/internal-hash-files.ts @@ -113,7 +113,12 @@ export async function hashFiles( const excludeMatchers = buildExcludeMatchers(options?.exclude ?? []) // Resolve roots up front; warn and skip any that fail to resolve. - // If allowFilesOutsideWorkspace is not enabled, roots are restricted to the resolved workspace. + // When the caller did not specify roots, the workspace is the only allowed + // root and `allowFilesOutsideWorkspace` is the opt-in that widens it. + // An explicit `roots` list is itself the allowlist, so its entries are + // honored as given - otherwise a root outside the workspace would be + // dropped here and the files under it silently skipped. + const explicitRoots = options?.roots !== undefined const resolvedRootsSet = new Set() const roots = options?.roots ?? [resolvedWorkspace] @@ -123,6 +128,7 @@ export async function hashFiles( root === resolvedWorkspace ? root : fs.realpathSync(root) if ( + !explicitRoots && !allowOutside && !isInResolvedRoots(resolvedRoot, [resolvedWorkspace]) ) {