mirror of
https://github.com/actions/toolkit.git
synced 2026-08-11 00:00:28 +02:00
* @actions/glob: extend hashFiles options * improve hashFiles symlink handling * Improve error handling and messaging in hashFiles function * apply relative exclude patterns across all roots and use named minimatch import * format error message
317 lines
11 KiB
TypeScript
317 lines
11 KiB
TypeScript
import * as io from '../../io/src/io.js'
|
|
import * as os from 'os'
|
|
import * as path from 'path'
|
|
import {hashFiles} from '../src/glob.js'
|
|
import {promises as fs} from 'fs'
|
|
|
|
const IS_WINDOWS = process.platform === 'win32'
|
|
const ORIGINAL_GITHUB_WORKSPACE = process.env['GITHUB_WORKSPACE']
|
|
|
|
/**
|
|
* These test focus on the ability of globber to find files
|
|
* and not on the pattern matching aspect
|
|
*/
|
|
describe('globber', () => {
|
|
beforeAll(async () => {
|
|
await io.rmRF(getTestTemp())
|
|
process.env['GITHUB_WORKSPACE'] = __dirname
|
|
})
|
|
|
|
afterAll(async () => {
|
|
if (ORIGINAL_GITHUB_WORKSPACE) {
|
|
process.env['GITHUB_WORKSPACE'] = ORIGINAL_GITHUB_WORKSPACE
|
|
} else {
|
|
delete process.env['GITHUB_WORKSPACE']
|
|
}
|
|
await io.rmRF(getTestTemp())
|
|
})
|
|
|
|
it('basic hashfiles test', async () => {
|
|
const root = path.join(getTestTemp(), 'basic-hashfiles')
|
|
await fs.mkdir(path.join(root), {recursive: true})
|
|
await fs.writeFile(path.join(root, 'test.txt'), 'test file content')
|
|
const hash = await hashFiles(`${root}/*`)
|
|
expect(hash).toEqual(
|
|
'd8a411e8f8643821bed189e627ff57151918aa554c00c10b31c693ab2dded273'
|
|
)
|
|
})
|
|
|
|
it('basic hashfiles no match should return empty string', async () => {
|
|
const root = path.join(getTestTemp(), 'empty-hashfiles')
|
|
const hash = await hashFiles(`${root}/*`)
|
|
expect(hash).toEqual('')
|
|
})
|
|
|
|
it('followSymbolicLinks defaults to true', async () => {
|
|
const root = path.join(
|
|
getTestTemp(),
|
|
'defaults-to-follow-symbolic-links-true'
|
|
)
|
|
await fs.mkdir(path.join(root, 'realdir'), {recursive: true})
|
|
await fs.writeFile(
|
|
path.join(root, 'realdir', 'file.txt'),
|
|
'test file content'
|
|
)
|
|
await createSymlinkDir(
|
|
path.join(root, 'realdir'),
|
|
path.join(root, 'symDir')
|
|
)
|
|
const testPath = path.join(root, `symDir`)
|
|
const hash = await hashFiles(testPath)
|
|
expect(hash).toEqual(
|
|
'd8a411e8f8643821bed189e627ff57151918aa554c00c10b31c693ab2dded273'
|
|
)
|
|
})
|
|
|
|
const emptyDirectory = ''
|
|
it('followSymbolicLinks set to true', async () => {
|
|
const root = path.join(getTestTemp(), 'set-to-true')
|
|
await fs.mkdir(path.join(root, 'realdir'), {recursive: true})
|
|
await fs.writeFile(path.join(root, 'realdir', 'file'), 'test file content')
|
|
await createSymlinkDir(
|
|
path.join(root, 'realdir'),
|
|
path.join(root, 'symDir')
|
|
)
|
|
const testPath = path.join(root, `symDir`)
|
|
const hash = await hashFiles(testPath, emptyDirectory, {
|
|
followSymbolicLinks: true
|
|
})
|
|
expect(hash).toEqual(
|
|
'd8a411e8f8643821bed189e627ff57151918aa554c00c10b31c693ab2dded273'
|
|
)
|
|
})
|
|
|
|
it('followSymbolicLinks set to false', async () => {
|
|
// Create the following layout:
|
|
// <root>
|
|
// <root>/folder-a
|
|
// <root>/folder-a/file
|
|
// <root>/symDir -> <root>/folder-a
|
|
const root = path.join(getTestTemp(), 'set-to-false')
|
|
await fs.mkdir(path.join(root, 'realdir'), {recursive: true})
|
|
await fs.writeFile(path.join(root, 'realdir', 'file'), 'test file content')
|
|
await createSymlinkDir(
|
|
path.join(root, 'realdir'),
|
|
path.join(root, 'symDir')
|
|
)
|
|
const testPath = path.join(root, 'symdir')
|
|
const hash = await hashFiles(testPath, emptyDirectory, {
|
|
followSymbolicLinks: false
|
|
})
|
|
expect(hash).toEqual('')
|
|
})
|
|
|
|
it('multipath test basic', async () => {
|
|
// Create the following layout:
|
|
// <root>
|
|
// <root>/folder-a
|
|
// <root>/folder-a/file
|
|
// <root>/symDir -> <root>/folder-a
|
|
const root = path.join(getTestTemp(), 'set-to-false')
|
|
await fs.mkdir(path.join(root, 'dir1'), {recursive: true})
|
|
await fs.mkdir(path.join(root, 'dir2'), {recursive: true})
|
|
await fs.writeFile(
|
|
path.join(root, 'dir1', 'testfile1.txt'),
|
|
'test file content'
|
|
)
|
|
await fs.writeFile(
|
|
path.join(root, 'dir2', 'testfile2.txt'),
|
|
'test file content'
|
|
)
|
|
const testPath = `${path.join(root, 'dir1')}\n${path.join(root, 'dir2')}`
|
|
const hash = await hashFiles(testPath)
|
|
expect(hash).toEqual(
|
|
'4e911ea5824830b6a3ec096c7833d5af8381c189ffaa825c3503a5333a73eadc'
|
|
)
|
|
})
|
|
|
|
it('hashes files in allowed roots only', async () => {
|
|
const root = path.join(getTestTemp(), 'roots-hashfiles')
|
|
const dir1 = path.join(root, 'dir1')
|
|
const dir2 = path.join(root, 'dir2')
|
|
await fs.mkdir(dir1, {recursive: true})
|
|
await fs.mkdir(dir2, {recursive: true})
|
|
await fs.writeFile(path.join(dir1, 'file1.txt'), 'test 1 file content')
|
|
await fs.writeFile(path.join(dir2, 'file2.txt'), 'test 2 file content')
|
|
|
|
const broadPattern = `${root}/**`
|
|
|
|
const hashDir1Only = await hashFiles(broadPattern, '', {roots: [dir1]})
|
|
expect(hashDir1Only).not.toEqual('')
|
|
|
|
const hashDir2Only = await hashFiles(broadPattern, '', {roots: [dir2]})
|
|
expect(hashDir2Only).not.toEqual('')
|
|
|
|
expect(hashDir1Only).not.toEqual(hashDir2Only)
|
|
|
|
const hashBoth = await hashFiles(broadPattern, '', {roots: [dir1, dir2]})
|
|
expect(hashBoth).not.toEqual(hashDir1Only)
|
|
expect(hashBoth).not.toEqual(hashDir2Only)
|
|
|
|
const hashDir1Again = await hashFiles(broadPattern, '', {roots: [dir1]})
|
|
expect(hashDir1Again).toEqual(hashDir1Only)
|
|
})
|
|
|
|
it('skips outside-root matches by default (hash unchanged)', async () => {
|
|
const root = path.join(getTestTemp(), 'default-skip-outside-roots')
|
|
const dir1 = path.join(root, 'dir1')
|
|
const outsideDir = path.join(root, 'outsideDir')
|
|
|
|
await fs.mkdir(dir1, {recursive: true})
|
|
await fs.mkdir(outsideDir, {recursive: true})
|
|
|
|
await fs.writeFile(path.join(dir1, 'file1.txt'), 'test 1 file content')
|
|
await fs.writeFile(
|
|
path.join(outsideDir, 'fileOut.txt'),
|
|
'test outside file content'
|
|
)
|
|
|
|
const insideOnly = await hashFiles(`${dir1}/*`, '', {roots: [dir1]})
|
|
expect(insideOnly).not.toEqual('')
|
|
|
|
const patterns = `${dir1}/*\n${outsideDir}/*`
|
|
const defaultSkip = await hashFiles(patterns, '', {roots: [dir1]})
|
|
|
|
expect(defaultSkip).toEqual(insideOnly)
|
|
})
|
|
|
|
it('allows files outside roots if opted-in (hash changes)', async () => {
|
|
const root = path.join(getTestTemp(), 'allow-outside-roots')
|
|
const dir1 = path.join(root, 'dir1')
|
|
const outsideDir = path.join(root, 'outsideDir')
|
|
await fs.mkdir(dir1, {recursive: true})
|
|
await fs.mkdir(outsideDir, {recursive: true})
|
|
await fs.writeFile(path.join(dir1, 'file1.txt'), 'test 1 file content')
|
|
await fs.writeFile(
|
|
path.join(outsideDir, 'fileOut.txt'),
|
|
'test outside file content'
|
|
)
|
|
|
|
const insideOnly = await hashFiles(`${dir1}/*`, '', {roots: [dir1]})
|
|
expect(insideOnly).not.toEqual('')
|
|
|
|
const patterns = `${dir1}/*\n${outsideDir}/*`
|
|
const withOptIn1 = await hashFiles(patterns, '', {
|
|
roots: [dir1],
|
|
allowFilesOutsideWorkspace: true
|
|
})
|
|
expect(withOptIn1).not.toEqual('')
|
|
expect(withOptIn1).not.toEqual(insideOnly)
|
|
|
|
const withOptIn2 = await hashFiles(patterns, '', {
|
|
roots: [dir1],
|
|
allowFilesOutsideWorkspace: true
|
|
})
|
|
expect(withOptIn2).toEqual(withOptIn1)
|
|
})
|
|
|
|
it('excludes files matching exclude patterns', async () => {
|
|
const root = path.join(getTestTemp(), 'exclude-hashfiles')
|
|
await fs.mkdir(root, {recursive: true})
|
|
await fs.writeFile(path.join(root, 'file1.txt'), 'test 1 file content')
|
|
await fs.writeFile(path.join(root, 'file2.log'), 'test 2 file content')
|
|
|
|
const all = await hashFiles(`${root}/*`, '', {roots: [root]})
|
|
expect(all).not.toEqual('')
|
|
|
|
// Exclude by exact filename and extension
|
|
const excluded = await hashFiles(`${root}/*`, '', {
|
|
roots: [root],
|
|
exclude: ['file2.log', '*.log']
|
|
})
|
|
expect(excluded).not.toEqual('')
|
|
|
|
const justIncluded = await hashFiles(
|
|
`${path.join(root, 'file1.txt')}`,
|
|
'',
|
|
{
|
|
roots: [root]
|
|
}
|
|
)
|
|
|
|
expect(excluded).toEqual(justIncluded)
|
|
expect(excluded).not.toEqual(all)
|
|
})
|
|
|
|
it('hashes files outside GITHUB_WORKSPACE only when opted-in', async () => {
|
|
// Files inside the workspace (GITHUB_WORKSPACE is set to __dirname).
|
|
const insideRoot = path.join(getTestTemp(), 'outside-workspace-inside')
|
|
await fs.mkdir(insideRoot, {recursive: true})
|
|
await fs.writeFile(path.join(insideRoot, 'inside.txt'), 'inside content')
|
|
|
|
// Files in a directory outside GITHUB_WORKSPACE (__dirname).
|
|
const outsideRoot = await fs.mkdtemp(
|
|
path.join(os.tmpdir(), 'hash-files-outside-')
|
|
)
|
|
try {
|
|
await fs.writeFile(
|
|
path.join(outsideRoot, 'outside.txt'),
|
|
'outside content'
|
|
)
|
|
|
|
const patterns = `${insideRoot}/*\n${outsideRoot}/*`
|
|
|
|
const insideOnly = await hashFiles(`${insideRoot}/*`)
|
|
expect(insideOnly).not.toEqual('')
|
|
|
|
// By default, files outside the workspace are skipped (hash unchanged).
|
|
const defaultSkip = await hashFiles(patterns)
|
|
expect(defaultSkip).toEqual(insideOnly)
|
|
|
|
// With opt-in, files outside the workspace are included (hash changes).
|
|
const withOptIn = await hashFiles(patterns, '', {
|
|
allowFilesOutsideWorkspace: true
|
|
})
|
|
expect(withOptIn).not.toEqual('')
|
|
expect(withOptIn).not.toEqual(insideOnly)
|
|
} finally {
|
|
await io.rmRF(outsideRoot)
|
|
}
|
|
})
|
|
|
|
it('applies relative exclude patterns across all allowed roots', async () => {
|
|
const root = path.join(getTestTemp(), 'exclude-across-roots')
|
|
const dir1 = path.join(root, 'dir1')
|
|
const dir2 = path.join(root, 'dir2')
|
|
await fs.mkdir(path.join(dir1, 'sub'), {recursive: true})
|
|
await fs.mkdir(path.join(dir2, 'sub'), {recursive: true})
|
|
await fs.writeFile(path.join(dir1, 'sub', 'secret.txt'), 'secret 1')
|
|
await fs.writeFile(path.join(dir2, 'sub', 'secret.txt'), 'secret 2')
|
|
await fs.writeFile(path.join(dir1, 'keep.txt'), 'keep 1')
|
|
await fs.writeFile(path.join(dir2, 'keep.txt'), 'keep 2')
|
|
|
|
const patterns = `${dir1}/**\n${dir2}/**`
|
|
|
|
// 'sub/secret.txt' must be excluded under both roots.
|
|
const excluded = await hashFiles(patterns, '', {
|
|
roots: [dir1, dir2],
|
|
exclude: ['sub/secret.txt']
|
|
})
|
|
expect(excluded).not.toEqual('')
|
|
|
|
// Hashing only the kept files should produce the same hash.
|
|
const keepOnly = await hashFiles(
|
|
`${path.join(dir1, 'keep.txt')}\n${path.join(dir2, 'keep.txt')}`,
|
|
'',
|
|
{roots: [dir1, dir2]}
|
|
)
|
|
expect(excluded).toEqual(keepOnly)
|
|
})
|
|
})
|
|
|
|
function getTestTemp(): string {
|
|
return path.join(__dirname, '_temp', 'hash_files')
|
|
}
|
|
|
|
/**
|
|
* Creates a symlink directory on OSX/Linux, and a junction point directory on Windows.
|
|
* A symlink directory is not created on Windows since it requires an elevated context.
|
|
*/
|
|
async function createSymlinkDir(real: string, link: string): Promise<void> {
|
|
if (IS_WINDOWS) {
|
|
await fs.symlink(real, link, 'junction')
|
|
} else {
|
|
await fs.symlink(real, link)
|
|
}
|
|
}
|