Detect case insensitive uploads + Bump @actions/artifact to version 0.3.3 (#106)

* Detect case insensitive uploads

* PR feedback
This commit is contained in:
Konrad Pabjan 2020-07-31 19:22:08 +02:00 committed by GitHub
parent 5ba29a7d5b
commit c8879bf5ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 67 additions and 14 deletions

@ -205,6 +205,31 @@ In the top right corner of a workflow run, once the run is over, if you used thi
There is a trashcan icon that can be used to delete the artifact. This icon will only appear for users who have write permissions to the repository. There is a trashcan icon that can be used to delete the artifact. This icon will only appear for users who have write permissions to the repository.
# Limitations
### Permission Loss
:exclamation: File permissions are not maintained during artifact upload :exclamation: For example, if you make a file executable using `chmod` and then upload that file, post-download the file is no longer guaranteed to be set as an executable.
### Case Insensitive Uploads
:exclamation: File uploads are case insensitive :exclamation: If you upload `A.txt` and `a.txt` with the same root path, only a single file will be saved and available during download.
### Maintaining file permissions and case sensitive files
If file permissions and case sensitivity are required, you can `tar` all of your files together before artifact upload. Post download, the `tar` file will maintain file permissions and case sensitivity.
```yaml
- name: 'Tar files'
run: tar -cvf my_files.tar /path/to/my/directory
- name: 'Upload Artifact'
uses: actions/upload-artifact@v2
with:
name: my-artifact
path: my_files.tar
```
## Additional Documentation ## Additional Documentation
See [persisting workflow data using artifacts](https://help.github.com/en/actions/configuring-and-managing-workflows/persisting-workflow-data-using-artifacts) for additional examples and tips. See [persisting workflow data using artifacts](https://help.github.com/en/actions/configuring-and-managing-workflows/persisting-workflow-data-using-artifacts) for additional examples and tips.

29
dist/index.js vendored

@ -4992,11 +4992,12 @@ const utils_1 = __webpack_require__(870);
* Used for managing http clients during either upload or download * Used for managing http clients during either upload or download
*/ */
class HttpManager { class HttpManager {
constructor(clientCount) { constructor(clientCount, userAgent) {
if (clientCount < 1) { if (clientCount < 1) {
throw new Error('There must be at least one client'); throw new Error('There must be at least one client');
} }
this.clients = new Array(clientCount).fill(utils_1.createHttpClient()); this.userAgent = userAgent;
this.clients = new Array(clientCount).fill(utils_1.createHttpClient(userAgent));
} }
getClient(index) { getClient(index) {
return this.clients[index]; return this.clients[index];
@ -5005,7 +5006,7 @@ class HttpManager {
// for more information see: https://github.com/actions/http-client/blob/04e5ad73cd3fd1f5610a32116b0759eddf6570d2/index.ts#L292 // for more information see: https://github.com/actions/http-client/blob/04e5ad73cd3fd1f5610a32116b0759eddf6570d2/index.ts#L292
disposeAndReplaceClient(index) { disposeAndReplaceClient(index) {
this.clients[index].dispose(); this.clients[index].dispose();
this.clients[index] = utils_1.createHttpClient(); this.clients[index] = utils_1.createHttpClient(this.userAgent);
} }
disposeAndReplaceAllClients() { disposeAndReplaceAllClients() {
for (const [index] of this.clients.entries()) { for (const [index] of this.clients.entries()) {
@ -6288,7 +6289,7 @@ function getMultiPathLCA(searchPaths) {
} }
return true; return true;
} }
// Loop over all the search paths until there is a non-common ancestor or we go out of bounds // loop over all the search paths until there is a non-common ancestor or we go out of bounds
while (splitIndex < smallestPathLength) { while (splitIndex < smallestPathLength) {
if (!isPathTheSame()) { if (!isPathTheSame()) {
break; break;
@ -6304,6 +6305,11 @@ function findFilesToUpload(searchPath, globOptions) {
const searchResults = []; const searchResults = [];
const globber = yield glob.create(searchPath, globOptions || getDefaultGlobOptions()); const globber = yield glob.create(searchPath, globOptions || getDefaultGlobOptions());
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
Detect any files that could be overwritten for user awareness
*/
const set = new Set();
/* /*
Directories will be rejected if attempted to be uploaded. This includes just empty Directories will be rejected if attempted to be uploaded. This includes just empty
directories so filter any directories out from the raw search results directories so filter any directories out from the raw search results
@ -6314,6 +6320,13 @@ function findFilesToUpload(searchPath, globOptions) {
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);
// detect any files that would be overwritten because of case insensitivity
if (set.has(searchResult.toLowerCase())) {
core_1.info(`Uploads are case insensitive: ${searchResult} was detected that it will be overwritten by another file with the same path`);
}
else {
set.add(searchResult.toLowerCase());
}
} }
else { else {
core_1.debug(`Removing ${searchResult} from rawSearchResults because it is a directory`); core_1.debug(`Removing ${searchResult} from rawSearchResults because it is a directory`);
@ -6645,7 +6658,7 @@ const upload_gzip_1 = __webpack_require__(647);
const stat = util_1.promisify(fs.stat); const stat = util_1.promisify(fs.stat);
class UploadHttpClient { class UploadHttpClient {
constructor() { constructor() {
this.uploadHttpManager = new http_manager_1.HttpManager(config_variables_1.getUploadFileConcurrency()); this.uploadHttpManager = new http_manager_1.HttpManager(config_variables_1.getUploadFileConcurrency(), 'actions/upload-artifact');
this.statusReporter = new status_reporter_1.StatusReporter(10000); this.statusReporter = new status_reporter_1.StatusReporter(10000);
} }
/** /**
@ -7399,7 +7412,7 @@ const http_manager_1 = __webpack_require__(452);
const config_variables_1 = __webpack_require__(401); const config_variables_1 = __webpack_require__(401);
class DownloadHttpClient { class DownloadHttpClient {
constructor() { constructor() {
this.downloadHttpManager = new http_manager_1.HttpManager(config_variables_1.getDownloadFileConcurrency()); this.downloadHttpManager = new http_manager_1.HttpManager(config_variables_1.getDownloadFileConcurrency(), 'actions/download-artifact');
// downloads are usually significantly faster than uploads so display status information every second // downloads are usually significantly faster than uploads so display status information every second
this.statusReporter = new status_reporter_1.StatusReporter(1000); this.statusReporter = new status_reporter_1.StatusReporter(1000);
} }
@ -8034,8 +8047,8 @@ function getUploadHeaders(contentType, isKeepAlive, isGzip, uncompressedLength,
return requestOptions; return requestOptions;
} }
exports.getUploadHeaders = getUploadHeaders; exports.getUploadHeaders = getUploadHeaders;
function createHttpClient() { function createHttpClient(userAgent) {
return new http_client_1.HttpClient('actions/artifact', [ return new http_client_1.HttpClient(userAgent, [
new auth_1.BearerCredentialHandler(config_variables_1.getRuntimeToken()) new auth_1.BearerCredentialHandler(config_variables_1.getRuntimeToken())
]); ]);
} }

6
package-lock.json generated

@ -5,9 +5,9 @@
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@actions/artifact": { "@actions/artifact": {
"version": "0.3.2", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/@actions/artifact/-/artifact-0.3.2.tgz", "resolved": "https://registry.npmjs.org/@actions/artifact/-/artifact-0.3.3.tgz",
"integrity": "sha512-KzUe5DEeVXprAodxfGKtx9f7ukuVKE6V6pge6t5GDGk0cdkfiMEfahoq7HfBsOsmVy4J7rr1YZQPUTvXveYinw==", "integrity": "sha512-sKC1uA5p6064C6Qypmmt6O8iKlpDyMTfqqDlS4/zfJX1Hs8NbbzPLLN81RpewuJPWQNnroeF52w4VCWypbSNaA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@actions/core": "^1.2.1", "@actions/core": "^1.2.1",

@ -29,7 +29,7 @@
}, },
"homepage": "https://github.com/actions/upload-artifact#readme", "homepage": "https://github.com/actions/upload-artifact#readme",
"devDependencies": { "devDependencies": {
"@actions/artifact": "^0.3.2", "@actions/artifact": "^0.3.3",
"@actions/core": "^1.2.3", "@actions/core": "^1.2.3",
"@actions/glob": "^0.1.0", "@actions/glob": "^0.1.0",
"@actions/io": "^1.0.2", "@actions/io": "^1.0.2",

@ -66,7 +66,7 @@ function getMultiPathLCA(searchPaths: string[]): string {
return true return true
} }
// Loop over all the search paths until there is a non-common ancestor or we go out of bounds // loop over all the search paths until there is a non-common ancestor or we go out of bounds
while (splitIndex < smallestPathLength) { while (splitIndex < smallestPathLength) {
if (!isPathTheSame()) { if (!isPathTheSame()) {
break break
@ -89,6 +89,12 @@ export async function findFilesToUpload(
) )
const rawSearchResults: string[] = await globber.glob() const rawSearchResults: string[] = await globber.glob()
/*
Files are saved with case insensitivity. Uploading both a.txt and A.txt will files to be overwritten
Detect any files that could be overwritten for user awareness
*/
const set = new Set<string>()
/* /*
Directories will be rejected if attempted to be uploaded. This includes just empty Directories will be rejected if attempted to be uploaded. This includes just empty
directories so filter any directories out from the raw search results directories so filter any directories out from the raw search results
@ -99,6 +105,15 @@ export async function findFilesToUpload(
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)
// detect any files that would be overwritten because of case insensitivity
if (set.has(searchResult.toLowerCase())) {
info(
`Uploads are case insensitive: ${searchResult} was detected that it will be overwritten by another file with the same path`
)
} else {
set.add(searchResult.toLowerCase())
}
} else { } else {
debug( debug(
`Removing ${searchResult} from rawSearchResults because it is a directory` `Removing ${searchResult} from rawSearchResults because it is a directory`