mirror of
https://github.com/actions/toolkit.git
synced 2026-08-22 00:00:21 +02:00
@actions/glob: extend hashFiles options (#2357)
* @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
This commit is contained in:
@@ -76,6 +76,36 @@ for await (const file of globber.globGenerator()) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Hashing files (`hashFiles`)
|
||||||
|
|
||||||
|
`hashFiles` computes a hash of files matched by glob patterns.
|
||||||
|
|
||||||
|
By default, only files under the workspace (`GITHUB_WORKSPACE`) are eligible to be hashed.
|
||||||
|
|
||||||
|
To improve security, file eligibility is evaluated using each file's resolved (real) path to prevent symbolic link traversal outside the allowed root path(s).
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
- `roots?: string[]` — Allowlist of root paths. Only files that resolve under (or equal) one of these roots are hashed. Defaults to `[GITHUB_WORKSPACE]` (or `currentWorkspace` if provided).
|
||||||
|
- `allowFilesOutsideWorkspace?: boolean` — Explicit opt-in to include files outside the specified root path(s). Defaults to `false`.
|
||||||
|
- `exclude?: string[]` — Glob patterns to exclude from hashing. Defaults to `[]`.
|
||||||
|
|
||||||
|
If files match your patterns but are outside the allowed roots and `allowFilesOutsideWorkspace` is not enabled, those files are skipped and a warning is emitted. If no eligible files remain after filtering, `hashFiles` returns an empty string (`''`).
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
const glob = require('@actions/glob')
|
||||||
|
|
||||||
|
const hash = await glob.hashFiles('**/*.json', process.env.GITHUB_WORKSPACE || '', {
|
||||||
|
roots: [process.env.GITHUB_WORKSPACE, process.env.GITHUB_ACTION_PATH].filter(Boolean),
|
||||||
|
allowFilesOutsideWorkspace: true,
|
||||||
|
exclude: ['**/node_modules/**']
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(hash)
|
||||||
|
```
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
### Glob behavior
|
### Glob behavior
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as io from '../../io/src/io.js'
|
import * as io from '../../io/src/io.js'
|
||||||
|
import * as os from 'os'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import {hashFiles} from '../src/glob.js'
|
import {hashFiles} from '../src/glob.js'
|
||||||
import {promises as fs} from 'fs'
|
import {promises as fs} from 'fs'
|
||||||
@@ -123,6 +124,179 @@ describe('globber', () => {
|
|||||||
'4e911ea5824830b6a3ec096c7833d5af8381c189ffaa825c3503a5333a73eadc'
|
'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 {
|
function getTestTemp(): string {
|
||||||
|
|||||||
@@ -37,5 +37,5 @@ export async function hashFiles(
|
|||||||
followSymbolicLinks = options.followSymbolicLinks
|
followSymbolicLinks = options.followSymbolicLinks
|
||||||
}
|
}
|
||||||
const globber = await create(patterns, {followSymbolicLinks})
|
const globber = await create(patterns, {followSymbolicLinks})
|
||||||
return _hashFiles(globber, currentWorkspace, verbose)
|
return _hashFiles(globber, currentWorkspace, options, verbose)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,27 @@ export interface HashFileOptions {
|
|||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
followSymbolicLinks?: boolean
|
followSymbolicLinks?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of allowed root directories. Only files that resolve under one of
|
||||||
|
* these roots will be included in the hash.
|
||||||
|
*
|
||||||
|
* @default [GITHUB_WORKSPACE]
|
||||||
|
*/
|
||||||
|
roots?: string[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether files outside the allowed roots should be included.
|
||||||
|
* If false, outside-root files are skipped with a warning.
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
allowFilesOutsideWorkspace?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of glob patterns for files to exclude from hashing.
|
||||||
|
*
|
||||||
|
* @default []
|
||||||
|
*/
|
||||||
|
exclude?: string[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,41 +4,224 @@ import * as fs from 'fs'
|
|||||||
import * as stream from 'stream'
|
import * as stream from 'stream'
|
||||||
import * as util from 'util'
|
import * as util from 'util'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
import {Minimatch, type MinimatchOptions} from 'minimatch'
|
||||||
import {Globber} from './glob.js'
|
import {Globber} from './glob.js'
|
||||||
|
import {HashFileOptions} from './internal-hash-file-options.js'
|
||||||
|
|
||||||
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
|
const MAX_WARNED_FILES = 10
|
||||||
|
|
||||||
|
const MINIMATCH_OPTIONS: MinimatchOptions = {
|
||||||
|
dot: true,
|
||||||
|
nobrace: true,
|
||||||
|
nocase: IS_WINDOWS,
|
||||||
|
nocomment: true,
|
||||||
|
noext: true,
|
||||||
|
nonegate: true
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExcludeMatcher = {
|
||||||
|
absolutePathMatcher: Minimatch
|
||||||
|
relativePathMatcher: Minimatch
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutsideRootFile = {
|
||||||
|
matched: string
|
||||||
|
resolved: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if resolvedFile is inside any of resolvedRoots.
|
||||||
|
function isInResolvedRoots(
|
||||||
|
resolvedFile: string,
|
||||||
|
resolvedRoots: string[]
|
||||||
|
): boolean {
|
||||||
|
const normalizedFile = IS_WINDOWS ? resolvedFile.toLowerCase() : resolvedFile
|
||||||
|
return resolvedRoots.some(root => {
|
||||||
|
const normalizedRoot = IS_WINDOWS ? root.toLowerCase() : root
|
||||||
|
if (normalizedFile === normalizedRoot) return true
|
||||||
|
const rel = path.relative(normalizedRoot, normalizedFile)
|
||||||
|
return (
|
||||||
|
!path.isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${path.sep}`)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeForMatch(p: string): string {
|
||||||
|
return p.split(path.sep).join('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildExcludeMatchers(excludePatterns: string[]): ExcludeMatcher[] {
|
||||||
|
return excludePatterns.map(pattern => {
|
||||||
|
const normalizedPattern = normalizeForMatch(pattern)
|
||||||
|
// basename-only pattern (no "/") uses matchBase so "*.log" matches anywhere
|
||||||
|
const isBasenamePattern = !normalizedPattern.includes('/')
|
||||||
|
return {
|
||||||
|
absolutePathMatcher: new Minimatch(normalizedPattern, {
|
||||||
|
...MINIMATCH_OPTIONS,
|
||||||
|
matchBase: false
|
||||||
|
}),
|
||||||
|
relativePathMatcher: new Minimatch(normalizedPattern, {
|
||||||
|
...MINIMATCH_OPTIONS,
|
||||||
|
matchBase: isBasenamePattern
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExcluded(
|
||||||
|
resolvedFile: string,
|
||||||
|
excludeMatchers: ExcludeMatcher[],
|
||||||
|
rootsForRelativeMatch: string[]
|
||||||
|
): boolean {
|
||||||
|
if (excludeMatchers.length === 0) return false
|
||||||
|
const absolutePath = path.resolve(resolvedFile)
|
||||||
|
const absolutePathForMatch = normalizeForMatch(absolutePath)
|
||||||
|
// Match relative patterns against every allowed root (and the workspace).
|
||||||
|
const relativePathsForMatch = rootsForRelativeMatch.map(root =>
|
||||||
|
normalizeForMatch(path.relative(root, absolutePath))
|
||||||
|
)
|
||||||
|
return excludeMatchers.some(
|
||||||
|
m =>
|
||||||
|
m.absolutePathMatcher.match(absolutePathForMatch) ||
|
||||||
|
relativePathsForMatch.some(rel => m.relativePathMatcher.match(rel))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export async function hashFiles(
|
export async function hashFiles(
|
||||||
globber: Globber,
|
globber: Globber,
|
||||||
currentWorkspace: string,
|
currentWorkspace: string,
|
||||||
|
options?: HashFileOptions,
|
||||||
verbose: Boolean = false
|
verbose: Boolean = false
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const writeDelegate = verbose ? core.info : core.debug
|
const writeDelegate = verbose ? core.info : core.debug
|
||||||
let hasMatch = false
|
|
||||||
const githubWorkspace = currentWorkspace
|
const githubWorkspace = currentWorkspace
|
||||||
? currentWorkspace
|
? currentWorkspace
|
||||||
: (process.env['GITHUB_WORKSPACE'] ?? process.cwd())
|
: (process.env['GITHUB_WORKSPACE'] ?? process.cwd())
|
||||||
|
|
||||||
|
// Resolve the workspace so workspace-relative exclude matching is consistent.
|
||||||
|
// This avoids mismatches when resolvedFile is a realpath but the workspace path contains symlinks.
|
||||||
|
let resolvedWorkspace = githubWorkspace
|
||||||
|
try {
|
||||||
|
resolvedWorkspace = fs.realpathSync(githubWorkspace)
|
||||||
|
} catch (err) {
|
||||||
|
writeDelegate(
|
||||||
|
`Could not resolve workspace '${githubWorkspace}', falling back to original path. Details: ${err.message}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowOutside = options?.allowFilesOutsideWorkspace ?? false
|
||||||
|
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.
|
||||||
|
const resolvedRootsSet = new Set<string>()
|
||||||
|
const roots = options?.roots ?? [resolvedWorkspace]
|
||||||
|
|
||||||
|
for (const root of roots) {
|
||||||
|
try {
|
||||||
|
const resolvedRoot =
|
||||||
|
root === resolvedWorkspace ? root : fs.realpathSync(root)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!allowOutside &&
|
||||||
|
!isInResolvedRoots(resolvedRoot, [resolvedWorkspace])
|
||||||
|
) {
|
||||||
|
writeDelegate(`Skipping root outside workspace: ${resolvedRoot}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedRootsSet.add(resolvedRoot)
|
||||||
|
} catch (err) {
|
||||||
|
writeDelegate(
|
||||||
|
`Skipping unresolved root '${root}'. Details: ${err.message}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedRoots = Array.from(resolvedRootsSet)
|
||||||
|
if (resolvedRoots.length === 0) {
|
||||||
|
core.warning(
|
||||||
|
`Could not resolve any allowed root(s); no files will be considered for hashing.`
|
||||||
|
)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workspace + every allowed root, used to evaluate relative exclude patterns.
|
||||||
|
const rootsForRelativeMatch = Array.from(
|
||||||
|
new Set([resolvedWorkspace, ...resolvedRoots])
|
||||||
|
)
|
||||||
|
|
||||||
|
const outsideRootFiles: OutsideRootFile[] = []
|
||||||
const result = crypto.createHash('sha256')
|
const result = crypto.createHash('sha256')
|
||||||
|
const pipeline = util.promisify(stream.pipeline)
|
||||||
|
let hasMatch = false
|
||||||
let count = 0
|
let count = 0
|
||||||
|
|
||||||
for await (const file of globber.globGenerator()) {
|
for await (const file of globber.globGenerator()) {
|
||||||
writeDelegate(file)
|
writeDelegate(file)
|
||||||
if (!file.startsWith(`${githubWorkspace}${path.sep}`)) {
|
|
||||||
writeDelegate(`Ignore '${file}' since it is not under GITHUB_WORKSPACE.`)
|
// Resolve real path of the file for symlink-safe exclude + root checking
|
||||||
|
let resolvedFile: string
|
||||||
|
try {
|
||||||
|
resolvedFile = fs.realpathSync(file)
|
||||||
|
} catch (err) {
|
||||||
|
core.warning(
|
||||||
|
`Could not read "${file}". Please check symlinks and file access. Details: ${err.message}`
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (fs.statSync(file).isDirectory()) {
|
|
||||||
|
// Exclude matching patterns (apply to resolved path for symlink-safety)
|
||||||
|
if (isExcluded(resolvedFile, excludeMatchers, rootsForRelativeMatch)) {
|
||||||
|
writeDelegate(`Exclude '${file}' (exclude pattern match).`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if in resolved roots
|
||||||
|
if (!isInResolvedRoots(resolvedFile, resolvedRoots)) {
|
||||||
|
outsideRootFiles.push({matched: file, resolved: resolvedFile})
|
||||||
|
if (allowOutside) {
|
||||||
|
writeDelegate(
|
||||||
|
`Including '${file}' since it is outside the allowed root(s) and 'allowFilesOutsideWorkspace' is enabled.`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
writeDelegate(`Skip '${file}' since it is not under allowed root(s).`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.statSync(resolvedFile).isDirectory()) {
|
||||||
writeDelegate(`Skip directory '${file}'.`)
|
writeDelegate(`Skip directory '${file}'.`)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const hash = crypto.createHash('sha256')
|
const hash = crypto.createHash('sha256')
|
||||||
const pipeline = util.promisify(stream.pipeline)
|
await pipeline(fs.createReadStream(resolvedFile), hash)
|
||||||
await pipeline(fs.createReadStream(file), hash)
|
|
||||||
result.write(hash.digest())
|
result.write(hash.digest())
|
||||||
count++
|
count++
|
||||||
if (!hasMatch) {
|
hasMatch = true
|
||||||
hasMatch = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
result.end()
|
result.end()
|
||||||
|
|
||||||
|
// Warn if any files outside root were found without opt-in.
|
||||||
|
if (!allowOutside && outsideRootFiles.length > 0) {
|
||||||
|
const shown = outsideRootFiles.slice(0, MAX_WARNED_FILES)
|
||||||
|
const remaining = outsideRootFiles.length - shown.length
|
||||||
|
const fileList = shown
|
||||||
|
.map(f => `- ${f.matched} -> ${f.resolved}`)
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
const suffix =
|
||||||
|
remaining > 0
|
||||||
|
? `\n ...and ${remaining} more file(s). Enable debug logging to see all.`
|
||||||
|
: ''
|
||||||
|
|
||||||
|
core.warning(
|
||||||
|
`Some matched files are outside the allowed root(s) and were skipped:\n${fileList}${suffix}\n` +
|
||||||
|
`To include them, set 'allowFilesOutsideWorkspace: true' in your options.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (hasMatch) {
|
if (hasMatch) {
|
||||||
writeDelegate(`Found ${count} files to hash.`)
|
writeDelegate(`Found ${count} files to hash.`)
|
||||||
return result.digest('hex')
|
return result.digest('hex')
|
||||||
|
|||||||
Reference in New Issue
Block a user