mirror of
https://github.com/actions/upload-artifact
synced 2024-12-22 22:42:46 +00:00
Implement follow-symlinks boolean action input
This commit implements a new boolean input to the action, `follow-symlinks`. This option configures whether the glob expansion will follow any symlinks it finds when determining the set of of files to be archived into the artifact. The default value of the option, which preserves the existing behaviour, is `true`. When set to true, symbolic links will be be followed and expanded If `false`, symbolic links will be included in the archived artifact verbatim. Users may wish to set this option to false if their artifact contains internally-referencing symlinks which would result in significant bloat (and semantic change!) in the source files when the artifact is created. Resolves: actions#93.
This commit is contained in:
parent
ee69f02b3d
commit
92b91569e6
7 changed files with 62 additions and 15 deletions
|
@ -2,7 +2,7 @@ import * as core from '@actions/core'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as io from '@actions/io'
|
import * as io from '@actions/io'
|
||||||
import {promises as fs} from 'fs'
|
import {promises as fs} from 'fs'
|
||||||
import {findFilesToUpload} from '../src/search'
|
import {findFilesToUpload, getDefaultGlobOptions} from '../src/search'
|
||||||
|
|
||||||
const root = path.join(__dirname, '_temp', 'search')
|
const root = path.join(__dirname, '_temp', 'search')
|
||||||
const searchItem1Path = path.join(
|
const searchItem1Path = path.join(
|
||||||
|
@ -110,6 +110,12 @@ describe('Search', () => {
|
||||||
await fs.writeFile(amazingFileInFolderHPath, 'amazing file')
|
await fs.writeFile(amazingFileInFolderHPath, 'amazing file')
|
||||||
|
|
||||||
await fs.writeFile(lonelyFilePath, 'all by itself')
|
await fs.writeFile(lonelyFilePath, 'all by itself')
|
||||||
|
|
||||||
|
await fs.symlink(
|
||||||
|
path.join(root, 'folder-d'),
|
||||||
|
path.join(root, 'symlink-to-folder-d')
|
||||||
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Directory structure of files that get created:
|
Directory structure of files that get created:
|
||||||
root/
|
root/
|
||||||
|
@ -136,6 +142,7 @@ describe('Search', () => {
|
||||||
folder-j/
|
folder-j/
|
||||||
folder-k/
|
folder-k/
|
||||||
lonely-file.txt
|
lonely-file.txt
|
||||||
|
symlink-to-folder-d/ -> ./folder-d/
|
||||||
search-item5.txt
|
search-item5.txt
|
||||||
*/
|
*/
|
||||||
})
|
})
|
||||||
|
@ -227,7 +234,8 @@ describe('Search', () => {
|
||||||
it('Wildcard search - Absolute Path', async () => {
|
it('Wildcard search - Absolute Path', async () => {
|
||||||
const searchPath = path.join(root, '**/*[Ss]earch*')
|
const searchPath = path.join(root, '**/*[Ss]earch*')
|
||||||
const searchResult = await findFilesToUpload(searchPath)
|
const searchResult = await findFilesToUpload(searchPath)
|
||||||
expect(searchResult.filesToUpload.length).toEqual(10)
|
// folder-d items included twice because symlink is followed by default
|
||||||
|
expect(searchResult.filesToUpload.length).toEqual(14)
|
||||||
|
|
||||||
expect(searchResult.filesToUpload.includes(searchItem1Path)).toEqual(true)
|
expect(searchResult.filesToUpload.includes(searchItem1Path)).toEqual(true)
|
||||||
expect(searchResult.filesToUpload.includes(searchItem2Path)).toEqual(true)
|
expect(searchResult.filesToUpload.includes(searchItem2Path)).toEqual(true)
|
||||||
|
@ -261,7 +269,8 @@ describe('Search', () => {
|
||||||
'**/*[Ss]earch*'
|
'**/*[Ss]earch*'
|
||||||
)
|
)
|
||||||
const searchResult = await findFilesToUpload(searchPath)
|
const searchResult = await findFilesToUpload(searchPath)
|
||||||
expect(searchResult.filesToUpload.length).toEqual(10)
|
// folder-d items included twice because symlink is followed by default
|
||||||
|
expect(searchResult.filesToUpload.length).toEqual(14)
|
||||||
|
|
||||||
expect(searchResult.filesToUpload.includes(searchItem1Path)).toEqual(true)
|
expect(searchResult.filesToUpload.includes(searchItem1Path)).toEqual(true)
|
||||||
expect(searchResult.filesToUpload.includes(searchItem2Path)).toEqual(true)
|
expect(searchResult.filesToUpload.includes(searchItem2Path)).toEqual(true)
|
||||||
|
@ -352,4 +361,15 @@ describe('Search', () => {
|
||||||
)
|
)
|
||||||
expect(searchResult.filesToUpload.includes(lonelyFilePath)).toEqual(true)
|
expect(searchResult.filesToUpload.includes(lonelyFilePath)).toEqual(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Declines to follow symlinks when requested', async () => {
|
||||||
|
const searchPath = path.join(root, 'symlink-to-folder-d')
|
||||||
|
const globOptions = {
|
||||||
|
...getDefaultGlobOptions(),
|
||||||
|
followSymbolicLinks: false
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchResult = await findFilesToUpload(searchPath, globOptions)
|
||||||
|
expect(searchResult.filesToUpload.length).toEqual(1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -23,6 +23,12 @@ inputs:
|
||||||
|
|
||||||
Minimum 1 day.
|
Minimum 1 day.
|
||||||
Maximum 90 days unless changed from the repository settings page.
|
Maximum 90 days unless changed from the repository settings page.
|
||||||
|
follow-symlinks:
|
||||||
|
description: >
|
||||||
|
Whether symbolic links should be followed and expanded when building the set of files to be
|
||||||
|
archived (true), or if symbolic links should be included in the archived artifact verbatim
|
||||||
|
(false).
|
||||||
|
default: true
|
||||||
runs:
|
runs:
|
||||||
using: 'node12'
|
using: 'node12'
|
||||||
main: 'dist/index.js'
|
main: 'dist/index.js'
|
||||||
|
|
|
@ -2,7 +2,8 @@ export enum Inputs {
|
||||||
Name = 'name',
|
Name = 'name',
|
||||||
Path = 'path',
|
Path = 'path',
|
||||||
IfNoFilesFound = 'if-no-files-found',
|
IfNoFilesFound = 'if-no-files-found',
|
||||||
RetentionDays = 'retention-days'
|
RetentionDays = 'retention-days',
|
||||||
|
FollowSymlinks = 'follow-symlinks'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum NoFileOptions {
|
export enum NoFileOptions {
|
||||||
|
|
|
@ -10,6 +10,11 @@ export function getInputs(): UploadInputs {
|
||||||
const path = core.getInput(Inputs.Path, {required: true})
|
const path = core.getInput(Inputs.Path, {required: true})
|
||||||
|
|
||||||
const ifNoFilesFound = core.getInput(Inputs.IfNoFilesFound)
|
const ifNoFilesFound = core.getInput(Inputs.IfNoFilesFound)
|
||||||
|
|
||||||
|
// getBooleanInput is not released yet :(
|
||||||
|
const followSymlinks =
|
||||||
|
core.getInput(Inputs.FollowSymlinks).toLowerCase() == 'true'
|
||||||
|
|
||||||
const noFileBehavior: NoFileOptions = NoFileOptions[ifNoFilesFound]
|
const noFileBehavior: NoFileOptions = NoFileOptions[ifNoFilesFound]
|
||||||
|
|
||||||
if (!noFileBehavior) {
|
if (!noFileBehavior) {
|
||||||
|
@ -25,7 +30,8 @@ export function getInputs(): UploadInputs {
|
||||||
const inputs = {
|
const inputs = {
|
||||||
artifactName: name,
|
artifactName: name,
|
||||||
searchPath: path,
|
searchPath: path,
|
||||||
ifNoFilesFound: noFileBehavior
|
ifNoFilesFound: noFileBehavior,
|
||||||
|
followSymlinks
|
||||||
} as UploadInputs
|
} as UploadInputs
|
||||||
|
|
||||||
const retentionDaysStr = core.getInput(Inputs.RetentionDays)
|
const retentionDaysStr = core.getInput(Inputs.RetentionDays)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as glob from '@actions/glob'
|
import * as glob from '@actions/glob'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import {debug, info} from '@actions/core'
|
import {debug, info} from '@actions/core'
|
||||||
import {stat} from 'fs'
|
import {promises as fsPromises, stat} from 'fs'
|
||||||
import {dirname} from 'path'
|
import {dirname} from 'path'
|
||||||
import {promisify} from 'util'
|
import {promisify} from 'util'
|
||||||
const stats = promisify(stat)
|
const stats = promisify(stat)
|
||||||
|
@ -11,7 +11,7 @@ export interface SearchResult {
|
||||||
rootDirectory: string
|
rootDirectory: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultGlobOptions(): glob.GlobOptions {
|
export function getDefaultGlobOptions(): glob.GlobOptions {
|
||||||
return {
|
return {
|
||||||
followSymbolicLinks: true,
|
followSymbolicLinks: true,
|
||||||
implicitDescendants: true,
|
implicitDescendants: true,
|
||||||
|
@ -83,10 +83,8 @@ export async function findFilesToUpload(
|
||||||
globOptions?: glob.GlobOptions
|
globOptions?: glob.GlobOptions
|
||||||
): Promise<SearchResult> {
|
): Promise<SearchResult> {
|
||||||
const searchResults: string[] = []
|
const searchResults: string[] = []
|
||||||
const globber = await glob.create(
|
const resolvedGlobOptions = globOptions || getDefaultGlobOptions()
|
||||||
searchPath,
|
const globber = await glob.create(searchPath, resolvedGlobOptions)
|
||||||
globOptions || getDefaultGlobOptions()
|
|
||||||
)
|
|
||||||
const rawSearchResults: string[] = await globber.glob()
|
const rawSearchResults: string[] = await globber.glob()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -100,8 +98,12 @@ export async function findFilesToUpload(
|
||||||
directories so filter any directories out from the raw search results
|
directories so filter any directories out from the raw search results
|
||||||
*/
|
*/
|
||||||
for (const searchResult of rawSearchResults) {
|
for (const searchResult of rawSearchResults) {
|
||||||
const fileStats = await stats(searchResult)
|
/* isDirectory() returns false for symlinks if using fs.lstat(), make sure to use fs.stat() instead
|
||||||
// isDirectory() returns false for symlinks if using fs.lstat(), make sure to use fs.stat() instead
|
* if we're following symlinks so that stat follows the symlink too */
|
||||||
|
const fileStats = resolvedGlobOptions.followSymbolicLinks
|
||||||
|
? await stats(searchResult)
|
||||||
|
: await fsPromises.lstat(searchResult)
|
||||||
|
|
||||||
if (!fileStats.isDirectory()) {
|
if (!fileStats.isDirectory()) {
|
||||||
debug(`File:${searchResult} was found using the provided searchPath`)
|
debug(`File:${searchResult} was found using the provided searchPath`)
|
||||||
searchResults.push(searchResult)
|
searchResults.push(searchResult)
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {create, UploadOptions} from '@actions/artifact'
|
import {create, UploadOptions} from '@actions/artifact'
|
||||||
import {findFilesToUpload} from './search'
|
import {findFilesToUpload, getDefaultGlobOptions} from './search'
|
||||||
import {getInputs} from './input-helper'
|
import {getInputs} from './input-helper'
|
||||||
import {NoFileOptions} from './constants'
|
import {NoFileOptions} from './constants'
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const inputs = getInputs()
|
const inputs = getInputs()
|
||||||
const searchResult = await findFilesToUpload(inputs.searchPath)
|
const globOptions = {
|
||||||
|
...getDefaultGlobOptions(),
|
||||||
|
followSymbolicLinks: inputs.followSymlinks
|
||||||
|
}
|
||||||
|
const searchResult = await findFilesToUpload(inputs.searchPath, globOptions)
|
||||||
|
|
||||||
if (searchResult.filesToUpload.length === 0) {
|
if (searchResult.filesToUpload.length === 0) {
|
||||||
// No files were found, different use cases warrant different types of behavior if nothing is found
|
// No files were found, different use cases warrant different types of behavior if nothing is found
|
||||||
switch (inputs.ifNoFilesFound) {
|
switch (inputs.ifNoFilesFound) {
|
||||||
|
|
|
@ -20,4 +20,11 @@ export interface UploadInputs {
|
||||||
* Duration after which artifact will expire in days
|
* Duration after which artifact will expire in days
|
||||||
*/
|
*/
|
||||||
retentionDays: number
|
retentionDays: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether symbolic links should be followed and expanded when building the set of files to be
|
||||||
|
* archived (true), or if symbolic links should be included in the archived artifact verbatim
|
||||||
|
* (false).
|
||||||
|
*/
|
||||||
|
followSymlinks: boolean
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue