Merge 26dd037a41465ba689818a0736e5325f5fdbbaaa into ee69f02b3dfdecd58bb31b4d133da38ba6fe3700

This commit is contained in:
Peter Bridgman 2021-05-10 14:19:43 +00:00 committed by GitHub
commit 9f3071bd12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 12403 additions and 25 deletions

View File

@ -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)
})
}) })

View File

@ -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'

20
dist/index.js vendored
View File

@ -4028,7 +4028,8 @@ function run() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
try { try {
const inputs = input_helper_1.getInputs(); const inputs = input_helper_1.getInputs();
const searchResult = yield search_1.findFilesToUpload(inputs.searchPath); const globOptions = Object.assign(Object.assign({}, search_1.getDefaultGlobOptions()), { followSymbolicLinks: inputs.followSymlinks });
const searchResult = yield search_1.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) {
@ -6440,6 +6441,7 @@ function getDefaultGlobOptions() {
omitBrokenSymbolicLinks: true omitBrokenSymbolicLinks: true
}; };
} }
exports.getDefaultGlobOptions = getDefaultGlobOptions;
/** /**
* If multiple paths are specific, the least common ancestor (LCA) of the search paths is used as * If multiple paths are specific, the least common ancestor (LCA) of the search paths is used as
* the delimiter to control the directory structure for the artifact. This function returns the LCA * the delimiter to control the directory structure for the artifact. This function returns the LCA
@ -6494,7 +6496,8 @@ function getMultiPathLCA(searchPaths) {
function findFilesToUpload(searchPath, globOptions) { function findFilesToUpload(searchPath, globOptions) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const searchResults = []; const searchResults = [];
const globber = yield glob.create(searchPath, globOptions || getDefaultGlobOptions()); const resolvedGlobOptions = globOptions || getDefaultGlobOptions();
const globber = yield glob.create(searchPath, resolvedGlobOptions);
const rawSearchResults = yield globber.glob(); const rawSearchResults = yield globber.glob();
/* /*
Files are saved with case insensitivity. Uploading both a.txt and A.txt will files to be overwritten Files are saved with case insensitivity. Uploading both a.txt and A.txt will files to be overwritten
@ -6506,8 +6509,11 @@ function findFilesToUpload(searchPath, globOptions) {
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 = yield 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
? yield stats(searchResult)
: yield fs_1.promises.lstat(searchResult);
if (!fileStats.isDirectory()) { if (!fileStats.isDirectory()) {
core_1.debug(`File:${searchResult} was found using the provided searchPath`); core_1.debug(`File:${searchResult} was found using the provided searchPath`);
searchResults.push(searchResult); searchResults.push(searchResult);
@ -6577,6 +6583,8 @@ function getInputs() {
const name = core.getInput(constants_1.Inputs.Name); const name = core.getInput(constants_1.Inputs.Name);
const path = core.getInput(constants_1.Inputs.Path, { required: true }); const path = core.getInput(constants_1.Inputs.Path, { required: true });
const ifNoFilesFound = core.getInput(constants_1.Inputs.IfNoFilesFound); const ifNoFilesFound = core.getInput(constants_1.Inputs.IfNoFilesFound);
// getBooleanInput is not released yet :(
const followSymlinks = core.getInput(constants_1.Inputs.FollowSymlinks).toLowerCase() == 'true';
const noFileBehavior = constants_1.NoFileOptions[ifNoFilesFound]; const noFileBehavior = constants_1.NoFileOptions[ifNoFilesFound];
if (!noFileBehavior) { if (!noFileBehavior) {
core.setFailed(`Unrecognized ${constants_1.Inputs.IfNoFilesFound} input. Provided: ${ifNoFilesFound}. Available options: ${Object.keys(constants_1.NoFileOptions)}`); core.setFailed(`Unrecognized ${constants_1.Inputs.IfNoFilesFound} input. Provided: ${ifNoFilesFound}. Available options: ${Object.keys(constants_1.NoFileOptions)}`);
@ -6584,7 +6592,8 @@ function getInputs() {
const inputs = { const inputs = {
artifactName: name, artifactName: name,
searchPath: path, searchPath: path,
ifNoFilesFound: noFileBehavior ifNoFilesFound: noFileBehavior,
followSymlinks
}; };
const retentionDaysStr = core.getInput(constants_1.Inputs.RetentionDays); const retentionDaysStr = core.getInput(constants_1.Inputs.RetentionDays);
if (retentionDaysStr) { if (retentionDaysStr) {
@ -7521,6 +7530,7 @@ var Inputs;
Inputs["Path"] = "path"; Inputs["Path"] = "path";
Inputs["IfNoFilesFound"] = "if-no-files-found"; Inputs["IfNoFilesFound"] = "if-no-files-found";
Inputs["RetentionDays"] = "retention-days"; Inputs["RetentionDays"] = "retention-days";
Inputs["FollowSymlinks"] = "follow-symlinks";
})(Inputs = exports.Inputs || (exports.Inputs = {})); })(Inputs = exports.Inputs || (exports.Inputs = {}));
var NoFileOptions; var NoFileOptions;
(function (NoFileOptions) { (function (NoFileOptions) {

12329
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,7 @@
}, },
"homepage": "https://github.com/actions/upload-artifact#readme", "homepage": "https://github.com/actions/upload-artifact#readme",
"dependencies": { "dependencies": {
"@actions/artifact": "^0.5.1", "@actions/artifact": "https://github.com/fourieraudio/toolkit-artifact/tarball/main",
"@actions/core": "^1.2.6", "@actions/core": "^1.2.6",
"@actions/glob": "^0.1.0", "@actions/glob": "^0.1.0",
"@actions/io": "^1.0.2" "@actions/io": "^1.0.2"

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

@ -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) {

View File

@ -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
} }