Support for multi path upload

This commit is contained in:
Konrad Pabjan 2020-07-09 11:43:31 +02:00
parent 4347a0d55a
commit 5db5708164
4 changed files with 231 additions and 11 deletions

View File

@ -75,6 +75,16 @@ jobs:
name: 'GZip-Artifact' name: 'GZip-Artifact'
path: path/to/dir-3/ path: path/to/dir-3/
# Upload a directory that contains a file that will be uploaded with GZip
- name: 'Upload artifact #4'
uses: ./
with:
name: 'Multi-Path-Artifact'
path: |
path/to/dir-1/*
path/to/dir-[23]/*
!path/to/dir-3/*.txt
# Verify artifacts. Switch to download-artifact@v2 once it's out of preview # Verify artifacts. Switch to download-artifact@v2 once it's out of preview
# Download Artifact #1 and verify the correctness of the content # Download Artifact #1 and verify the correctness of the content
@ -138,3 +148,23 @@ jobs:
Write-Error "File contents of downloaded artifact is incorrect" Write-Error "File contents of downloaded artifact is incorrect"
} }
shell: pwsh shell: pwsh
- name: 'Download artifact #4'
uses: actions/download-artifact@v1
with:
name: 'Multi-Path-Artifact'
path: multi/artifact/path
- name: 'Verify Artifact #4'
run: |
$file1 = "multi/artifact/path/to/dir-1/file1.txt"
$file2 = "multi/artifact/path/to/dir-2/file2.txt"
if(!(Test-Path -path $file1) -or !(Test-Path -path $file2))
{
Write-Error "Expected files do not exist"
}
if(!((Get-Content $file1) -ceq "Lorem ipsum dolor sit amet") -or !((Get-Content $file2) -ceq "Hello world from file #2"))
{
Write-Error "File contents of downloaded artifacts are incorrect"
}
shell: pwsh

View File

@ -286,4 +286,70 @@ describe('Search', () => {
expect(searchResult.rootDirectory).toEqual(root) expect(searchResult.rootDirectory).toEqual(root)
}) })
it('Multi path search - root directory', async () => {
const searchPath1 = path.join(root, 'folder-a')
const searchPath2 = path.join(root, 'folder-d')
const searchPaths = searchPath1 + '\n' + searchPath2
const searchResult = await findFilesToUpload(searchPaths)
expect(searchResult.rootDirectory).toEqual(root)
expect(searchResult.filesToUpload.length).toEqual(7)
expect(searchResult.filesToUpload.includes(searchItem1Path)).toEqual(true)
expect(searchResult.filesToUpload.includes(searchItem2Path)).toEqual(true)
expect(searchResult.filesToUpload.includes(searchItem2Path)).toEqual(true)
expect(searchResult.filesToUpload.includes(searchItem4Path)).toEqual(true)
expect(searchResult.filesToUpload.includes(extraSearchItem1Path)).toEqual(
true
)
expect(searchResult.filesToUpload.includes(extraSearchItem2Path)).toEqual(
true
)
expect(searchResult.filesToUpload.includes(extraFileInFolderCPath)).toEqual(
true
)
})
it('Multi path search - with exclude character', async () => {
const searchPath1 = path.join(root, 'folder-a')
const searchPath2 = path.join(root, 'folder-d')
const searchPath3 = path.join(root, 'folder-a', 'folder-b', '**/extra*.txt')
// negating the third search path
const searchPaths = searchPath1 + '\n' + searchPath2 + '\n!' + searchPath3
const searchResult = await findFilesToUpload(searchPaths)
expect(searchResult.rootDirectory).toEqual(root)
expect(searchResult.filesToUpload.length).toEqual(5)
expect(searchResult.filesToUpload.includes(searchItem1Path)).toEqual(true)
expect(searchResult.filesToUpload.includes(searchItem2Path)).toEqual(true)
expect(searchResult.filesToUpload.includes(searchItem2Path)).toEqual(true)
expect(searchResult.filesToUpload.includes(searchItem4Path)).toEqual(true)
expect(searchResult.filesToUpload.includes(extraSearchItem2Path)).toEqual(
true
)
})
it('Multi path search - non root directory', async () => {
const searchPath1 = path.join(root, 'folder-h', 'folder-i')
const searchPath2 = path.join(root, 'folder-h', 'folder-j', 'folder-k')
const searchPath3 = amazingFileInFolderHPath
const searchPaths = searchPath1 + '\n' + searchPath2 + '\n' + searchPath3
const searchResult = await findFilesToUpload(searchPaths)
expect(searchResult.rootDirectory).toEqual(path.join(root, 'folder-h'))
expect(searchResult.filesToUpload.length).toEqual(4)
expect(
searchResult.filesToUpload.includes(amazingFileInFolderHPath)
).toEqual(true)
expect(searchResult.filesToUpload.includes(extraSearchItem4Path)).toEqual(
true
)
expect(searchResult.filesToUpload.includes(extraSearchItem5Path)).toEqual(
true
)
expect(searchResult.filesToUpload.includes(lonelyFilePath)).toEqual(true)
})
}) })

65
dist/index.js vendored
View File

@ -6221,6 +6221,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
const glob = __importStar(__webpack_require__(281)); const glob = __importStar(__webpack_require__(281));
const path = __importStar(__webpack_require__(622));
const core_1 = __webpack_require__(470); const core_1 = __webpack_require__(470);
const fs_1 = __webpack_require__(747); const fs_1 = __webpack_require__(747);
const path_1 = __webpack_require__(622); const path_1 = __webpack_require__(622);
@ -6231,6 +6232,57 @@ function getDefaultGlobOptions() {
omitBrokenSymbolicLinks: true omitBrokenSymbolicLinks: true
}; };
} }
/**
* 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
* when given an array of search paths
*
* Example 1: The patterns `/foo/` and `/bar/` returns `/`
*
* Example 2: The patterns `~/foo/bar/*` and `~/foo/voo/two/*` and `~/foo/mo/` returns `~/foo`
*/
function getMultiPathLCA(searchPaths) {
if (searchPaths.length < 2) {
throw new Error('At least two search paths must be provided');
}
const commonPaths = new Array();
const splitPaths = new Array();
let smallestPathLength = Number.MAX_SAFE_INTEGER;
// split each of the search paths using the platform specific separator
for (const searchPath of searchPaths) {
core_1.debug(`Using search path ${searchPath}`);
const splitSearchPath = searchPath.split(path.sep);
// keep track of the smallest path length so that we don't accidentally later go out of bounds
smallestPathLength = Math.min(smallestPathLength, splitSearchPath.length);
splitPaths.push(splitSearchPath);
}
// on Unix-like file systems, the file separator exists at the beginning of the file path, make sure to preserve it
if (searchPaths[0].startsWith(path.sep)) {
commonPaths.push(path.sep);
}
let splitIndex = 0;
// function to check if the paths are the same at a specific index
function isPathTheSame() {
const common = splitPaths[0][splitIndex];
for (let i = 1; i < splitPaths.length; i++) {
if (common !== splitPaths[i][splitIndex]) {
// a non-common index has been reached
return false;
}
}
// if all are the same, add to the end result & increment the index
commonPaths.push(common);
splitIndex++;
return true;
}
// Loop over all the search paths until there is a non-common ancestor or we go out of bounds
while (splitIndex < smallestPathLength) {
if (!isPathTheSame()) {
break;
}
}
return path.join(...commonPaths);
}
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 = [];
@ -6249,13 +6301,16 @@ function findFilesToUpload(searchPath, globOptions) {
core_1.debug(`Removing ${searchResult} from rawSearchResults because it is a directory`); core_1.debug(`Removing ${searchResult} from rawSearchResults because it is a directory`);
} }
} }
/* // Calculate the root directory for the artifact using the search paths that were utilized
Only a single search pattern is being included so only 1 searchResult is expected. In the future if multiple search patterns are
simultaneously supported this will change
*/
const searchPaths = globber.getSearchPaths(); const searchPaths = globber.getSearchPaths();
if (searchPaths.length > 1) { if (searchPaths.length > 1) {
throw new Error('Only 1 search path should be returned'); core_1.info(`Multiple search paths detected. Calculating the least common ancestor of all paths`);
const lcaSearchPath = getMultiPathLCA(searchPaths);
core_1.info(`The least common ancestor is ${lcaSearchPath} This will be the root directory of the artifact`);
return {
filesToUpload: searchResults,
rootDirectory: lcaSearchPath
};
} }
/* /*
Special case for a single file artifact that is uploaded without a directory or wildcard pattern. The directory structure is Special case for a single file artifact that is uploaded without a directory or wildcard pattern. The directory structure is

View File

@ -1,5 +1,6 @@
import * as glob from '@actions/glob' import * as glob from '@actions/glob'
import {debug} from '@actions/core' import * as path from 'path'
import {debug, info} from '@actions/core'
import {lstatSync} from 'fs' import {lstatSync} from 'fs'
import {dirname} from 'path' import {dirname} from 'path'
@ -16,6 +17,65 @@ function getDefaultGlobOptions(): glob.GlobOptions {
} }
} }
/**
* 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
* when given an array of search paths
*
* Example 1: The patterns `/foo/` and `/bar/` returns `/`
*
* Example 2: The patterns `~/foo/bar/*` and `~/foo/voo/two/*` and `~/foo/mo/` returns `~/foo`
*/
function getMultiPathLCA(searchPaths: string[]): string {
if (searchPaths.length < 2) {
throw new Error('At least two search paths must be provided')
}
const commonPaths = new Array<string>()
const splitPaths = new Array<string[]>()
let smallestPathLength = Number.MAX_SAFE_INTEGER
// split each of the search paths using the platform specific separator
for (const searchPath of searchPaths) {
debug(`Using search path ${searchPath}`)
const splitSearchPath = searchPath.split(path.sep)
// keep track of the smallest path length so that we don't accidentally later go out of bounds
smallestPathLength = Math.min(smallestPathLength, splitSearchPath.length)
splitPaths.push(splitSearchPath)
}
// on Unix-like file systems, the file separator exists at the beginning of the file path, make sure to preserve it
if (searchPaths[0].startsWith(path.sep)) {
commonPaths.push(path.sep)
}
let splitIndex = 0
// function to check if the paths are the same at a specific index
function isPathTheSame(): boolean {
const common = splitPaths[0][splitIndex]
for (let i = 1; i < splitPaths.length; i++) {
if (common !== splitPaths[i][splitIndex]) {
// a non-common index has been reached
return false
}
}
// if all are the same, add to the end result & increment the index
commonPaths.push(common)
splitIndex++
return true
}
// Loop over all the search paths until there is a non-common ancestor or we go out of bounds
while (splitIndex < smallestPathLength) {
if (!isPathTheSame()) {
break
}
}
return path.join(...commonPaths)
}
export async function findFilesToUpload( export async function findFilesToUpload(
searchPath: string, searchPath: string,
globOptions?: glob.GlobOptions globOptions?: glob.GlobOptions
@ -42,13 +102,22 @@ export async function findFilesToUpload(
} }
} }
/* // Calculate the root directory for the artifact using the search paths that were utilized
Only a single search pattern is being included so only 1 searchResult is expected. In the future if multiple search patterns are
simultaneously supported this will change
*/
const searchPaths: string[] = globber.getSearchPaths() const searchPaths: string[] = globber.getSearchPaths()
if (searchPaths.length > 1) { if (searchPaths.length > 1) {
throw new Error('Only 1 search path should be returned') info(
`Multiple search paths detected. Calculating the least common ancestor of all paths`
)
const lcaSearchPath = getMultiPathLCA(searchPaths)
info(
`The least common ancestor is ${lcaSearchPath} This will be the root directory of the artifact`
)
return {
filesToUpload: searchResults,
rootDirectory: lcaSearchPath
}
} }
/* /*