mirror of
https://github.com/actions/upload-artifact
synced 2024-12-22 06:22:45 +00:00
V2 Upload Artifact (#70)
* V2 Preview (#54) * V2 Upload Artifact * Improve logs * Update release * Update test.yml * Update test.yml * Update test.yml * @actions/artifact v0.2.0 package * Add extra YAML test * Extra Documentation * Update README.md * Update README.md * Update NPM packages * Update to @actions/artifact 0.3.1 * Update readme from v2 preview to v2 * Add .gitattributes * Misc Updates * macOS to macos * Update YAML so only push events trigger on the master branch
This commit is contained in:
parent
1283ca12b6
commit
e7eefc4917
18 changed files with 16404 additions and 12 deletions
3
.eslintignore
Normal file
3
.eslintignore
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules/
|
||||||
|
lib/
|
||||||
|
dist/
|
19
.eslintrc.json
Normal file
19
.eslintrc.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"env": { "node": true, "jest": true },
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": { "ecmaVersion": 9, "sourceType": "module" },
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:import/errors",
|
||||||
|
"plugin:import/warnings",
|
||||||
|
"plugin:import/typescript",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
"prettier/@typescript-eslint"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-empty-function": "off"
|
||||||
|
},
|
||||||
|
"plugins": ["@typescript-eslint", "jest"]
|
||||||
|
}
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
* text=auto eol=lf
|
140
.github/workflows/test.yml
vendored
Normal file
140
.github/workflows/test.yml
vendored
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
name: Test
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
runs-on: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
fail-fast: false
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.runs-on }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set Node.js 12.x
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 12.x
|
||||||
|
|
||||||
|
- name: npm install
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Compile
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: npm test
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Format
|
||||||
|
run: npm run format-check
|
||||||
|
|
||||||
|
# Test end-to-end by uploading two artifacts and then downloading them
|
||||||
|
- name: Create artifact files
|
||||||
|
run: |
|
||||||
|
mkdir -p path/to/dir-1
|
||||||
|
mkdir -p path/to/dir-2
|
||||||
|
mkdir -p path/to/dir-3
|
||||||
|
echo "Lorem ipsum dolor sit amet" > path/to/dir-1/file1.txt
|
||||||
|
echo "Hello world from file #2" > path/to/dir-2/file2.txt
|
||||||
|
echo "This is a going to be a test for a large enough file that should get compressed with GZip. The @actions/artifact package uses GZip to upload files. This text should have a compression ratio greater than 100% so it should get uploaded using GZip" > path/to/dir-3/gzip.txt
|
||||||
|
|
||||||
|
# Upload a single file artifact
|
||||||
|
- name: 'Upload artifact #1'
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
name: 'Artifact-A'
|
||||||
|
path: path/to/dir-1/file1.txt
|
||||||
|
|
||||||
|
# Upload using a wildcard pattern, name should default to 'artifact' if not provided
|
||||||
|
- name: 'Upload artifact #2'
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
path: path/**/dir*/
|
||||||
|
|
||||||
|
# Upload a directory that contains a file that will be uploaded with GZip
|
||||||
|
- name: 'Upload artifact #3'
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
name: 'GZip-Artifact'
|
||||||
|
path: path/to/dir-3/
|
||||||
|
|
||||||
|
# Verify artifacts. Switch to download-artifact@v2 once it's out of preview
|
||||||
|
|
||||||
|
# Download Artifact #1 and verify the correctness of the content
|
||||||
|
- name: 'Download artifact #1'
|
||||||
|
uses: actions/download-artifact@v1
|
||||||
|
with:
|
||||||
|
name: 'Artifact-A'
|
||||||
|
path: some/new/path
|
||||||
|
|
||||||
|
- name: 'Verify Artifact #1'
|
||||||
|
run: |
|
||||||
|
$file = "some/new/path/file1.txt"
|
||||||
|
if(!(Test-Path -path $file))
|
||||||
|
{
|
||||||
|
Write-Error "Expected file does not exist"
|
||||||
|
}
|
||||||
|
if(!((Get-Content $file) -ceq "Lorem ipsum dolor sit amet"))
|
||||||
|
{
|
||||||
|
Write-Error "File contents of downloaded artifact are incorrect"
|
||||||
|
}
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
|
# Download Artifact #2 and verify the correctness of the content
|
||||||
|
- name: 'Download artifact #2'
|
||||||
|
uses: actions/download-artifact@v1
|
||||||
|
with:
|
||||||
|
name: 'artifact'
|
||||||
|
path: some/other/path
|
||||||
|
|
||||||
|
- name: 'Verify Artifact #2'
|
||||||
|
run: |
|
||||||
|
$file1 = "some/other/path/to/dir-1/file1.txt"
|
||||||
|
$file2 = "some/other/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
|
||||||
|
|
||||||
|
# Download Artifact #3 and verify the correctness of the content
|
||||||
|
- name: 'Download artifact #3'
|
||||||
|
uses: actions/download-artifact@v1
|
||||||
|
with:
|
||||||
|
name: 'GZip-Artifact'
|
||||||
|
path: gzip/artifact/path
|
||||||
|
|
||||||
|
# Because a directory was used as input during the upload the parent directories, path/to/dir-3/, should not be included in the uploaded artifact
|
||||||
|
- name: 'Verify Artifact #3'
|
||||||
|
run: |
|
||||||
|
$gzipFile = "gzip/artifact/path/gzip.txt"
|
||||||
|
if(!(Test-Path -path $gzipFile))
|
||||||
|
{
|
||||||
|
Write-Error "Expected file do not exist"
|
||||||
|
}
|
||||||
|
if(!((Get-Content $gzipFile) -ceq "This is a going to be a test for a large enough file that should get compressed with GZip. The @actions/artifact package uses GZip to upload files. This text should have a compression ratio greater than 100% so it should get uploaded using GZip"))
|
||||||
|
{
|
||||||
|
Write-Error "File contents of downloaded artifact is incorrect"
|
||||||
|
}
|
||||||
|
shell: pwsh
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules/
|
||||||
|
lib/
|
||||||
|
__tests__/_temp/
|
3
.prettierignore
Normal file
3
.prettierignore
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
dist/
|
||||||
|
lib/
|
||||||
|
node_modules/
|
11
.prettierrc.json
Normal file
11
.prettierrc.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"bracketSpacing": false,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"parser": "typescript"
|
||||||
|
}
|
120
README.md
120
README.md
|
@ -1,38 +1,137 @@
|
||||||
# upload-artifact
|
# Upload-Artifact v2
|
||||||
|
|
||||||
This uploads artifacts from your workflow.
|
This uploads artifacts from your workflow allowing you to share data between jobs and store data once a workflow is complete.
|
||||||
|
|
||||||
See also [download-artifact](https://github.com/actions/download-artifact).
|
See also [download-artifact](https://github.com/actions/download-artifact).
|
||||||
|
|
||||||
|
# What's new
|
||||||
|
|
||||||
|
- Easier upload
|
||||||
|
- Specify a wildcard pattern
|
||||||
|
- Specify an individual file
|
||||||
|
- Specify a directory (previously you were limited to only this option)
|
||||||
|
- Upload an artifact without providing a name
|
||||||
|
- Fix for artifact uploads sometimes not working with containers
|
||||||
|
- Proxy support out of the box
|
||||||
|
- Port entire action to typescript from a runner plugin so it is easier to collaborate and accept contributions
|
||||||
|
|
||||||
|
Refer [here](https://github.com/actions/upload-artifact/tree/releases/v1) for the previous version
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
See [action.yml](action.yml)
|
See [action.yml](action.yml)
|
||||||
|
|
||||||
Basic:
|
### Upload an Individual File
|
||||||
```yaml
|
```yaml
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- run: mkdir -p path/to/artifact
|
- run: mkdir -p path/to/artifact
|
||||||
|
|
||||||
- run: echo hello > path/to/artifact/world.txt
|
- run: echo hello > path/to/artifact/world.txt
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v1
|
- uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: my-artifact
|
name: my-artifact
|
||||||
path: path/to/artifact
|
path: path/to/artifact/world.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Upload an Entire Directory
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: my-artifact
|
||||||
|
path: path/to/artifact/ # or path/to/artifact
|
||||||
|
```
|
||||||
|
|
||||||
|
### Upload using a Wildcard Pattern:
|
||||||
|
```yaml
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: my-artifact
|
||||||
|
path: path/**/[abc]rtifac?/*
|
||||||
|
```
|
||||||
|
|
||||||
|
For supported wildcards along with behavior and documentation, see [@actions/glob](https://github.com/actions/toolkit/tree/master/packages/glob) which is used internally to search for files.
|
||||||
|
|
||||||
|
Relative and absolute file paths are both allowed. Relative paths are rooted against the current working directory. Paths that begin with a wildcard character should be quoted to avoid being interpreted as YAML aliases.
|
||||||
|
|
||||||
|
The [@actions/artifact](https://github.com/actions/toolkit/tree/master/packages/artifact) package is also used internally to handle most of the logic around uploading an artifact. There is extra documentation around upload limitations and behavior in the toolkit repo that is worth checking out.
|
||||||
|
|
||||||
|
### Conditional Artifact Upload
|
||||||
|
|
||||||
To upload artifacts only when the previous step of a job failed, use [`if: failure()`](https://help.github.com/en/articles/contexts-and-expression-syntax-for-github-actions#job-status-check-functions):
|
To upload artifacts only when the previous step of a job failed, use [`if: failure()`](https://help.github.com/en/articles/contexts-and-expression-syntax-for-github-actions#job-status-check-functions):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/upload-artifact@v1
|
- uses: actions/upload-artifact@v2
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: my-artifact
|
name: my-artifact
|
||||||
path: path/to/artifact
|
path: path/to/artifact/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Uploading without an artifact name
|
||||||
|
|
||||||
|
You can upload an artifact without specifying a name
|
||||||
|
```yaml
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
path: path/to/artifact/world.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
If not provided, `artifact` will be used as the default name which will manifest itself in the UI after upload.
|
||||||
|
|
||||||
|
### Uploading to the same artifact
|
||||||
|
|
||||||
|
Each artifact behaves as a file share. Uploading to the same artifact multiple times in the same workflow can overwrite and append already uploaded files
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- run: echo hi > world.txt
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
path: world.txt
|
||||||
|
|
||||||
|
- run: echo howdy > extra-file.txt
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
path: extra-file.txt
|
||||||
|
|
||||||
|
- run: echo hello > world.txt
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
path: world.txt
|
||||||
|
```
|
||||||
|
With the following example, the available artifact (named `artifact` which is the default if no name is provided) would contain both `world.txt` (`hello`) and `extra-file.txt` (`howdy`).
|
||||||
|
|
||||||
|
### Environment Variables and Tilde Expansion
|
||||||
|
|
||||||
|
You can use `~` in the path input as a substitute for `$HOME`. Basic tilde expansion is supported.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- run: |
|
||||||
|
mkdir -p ~/new/artifact
|
||||||
|
echo hello > ~/new/artifact/world.txt
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: 'Artifacts-V2'
|
||||||
|
path: '~/new/**/*'
|
||||||
|
```
|
||||||
|
|
||||||
|
Environment variables along with context expressions can also be used for input. For documentation see [context and expression syntax](https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env:
|
||||||
|
name: my-artifact
|
||||||
|
steps:
|
||||||
|
- run: |
|
||||||
|
mkdir -p ${{ github.workspace }}/artifact
|
||||||
|
echo hello > ${{ github.workspace }}/artifact/world.txt
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: ${{ env.name }}-name
|
||||||
|
path: ${{ github.workspace }}/artifact/**/*
|
||||||
|
```
|
||||||
|
|
||||||
## Where does the upload go?
|
## Where does the upload go?
|
||||||
In the top right corner of a workflow run, once the run is over, if you used this action, there will be a `Artifacts` dropdown which you can download items from. Here's a screenshot of what it looks like<br/>
|
In the top right corner of a workflow run, once the run is over, if you used this action, there will be a `Artifacts` dropdown which you can download items from. Here's a screenshot of what it looks like<br/>
|
||||||
|
@ -40,6 +139,11 @@ 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.
|
||||||
|
|
||||||
|
## 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 extra documentation for the [@actions/artifact](https://github.com/actions/toolkit/blob/master/packages/artifact/docs/additional-information.md) package that is used internally regarding certain behaviors and limitations.
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
|
|
289
__tests__/search.test.ts
Normal file
289
__tests__/search.test.ts
Normal file
|
@ -0,0 +1,289 @@
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as io from '@actions/io'
|
||||||
|
import {promises as fs} from 'fs'
|
||||||
|
import {findFilesToUpload} from '../src/search'
|
||||||
|
|
||||||
|
const root = path.join(__dirname, '_temp', 'search')
|
||||||
|
const searchItem1Path = path.join(
|
||||||
|
root,
|
||||||
|
'folder-a',
|
||||||
|
'folder-b',
|
||||||
|
'folder-c',
|
||||||
|
'search-item1.txt'
|
||||||
|
)
|
||||||
|
const searchItem2Path = path.join(root, 'folder-d', 'search-item2.txt')
|
||||||
|
const searchItem3Path = path.join(root, 'folder-d', 'search-item3.txt')
|
||||||
|
const searchItem4Path = path.join(root, 'folder-d', 'search-item4.txt')
|
||||||
|
const searchItem5Path = path.join(root, 'search-item5.txt')
|
||||||
|
const extraSearchItem1Path = path.join(
|
||||||
|
root,
|
||||||
|
'folder-a',
|
||||||
|
'folder-b',
|
||||||
|
'folder-c',
|
||||||
|
'extraSearch-item1.txt'
|
||||||
|
)
|
||||||
|
const extraSearchItem2Path = path.join(
|
||||||
|
root,
|
||||||
|
'folder-d',
|
||||||
|
'extraSearch-item2.txt'
|
||||||
|
)
|
||||||
|
const extraSearchItem3Path = path.join(
|
||||||
|
root,
|
||||||
|
'folder-f',
|
||||||
|
'extraSearch-item3.txt'
|
||||||
|
)
|
||||||
|
const extraSearchItem4Path = path.join(
|
||||||
|
root,
|
||||||
|
'folder-h',
|
||||||
|
'folder-i',
|
||||||
|
'extraSearch-item4.txt'
|
||||||
|
)
|
||||||
|
const extraSearchItem5Path = path.join(
|
||||||
|
root,
|
||||||
|
'folder-h',
|
||||||
|
'folder-i',
|
||||||
|
'extraSearch-item5.txt'
|
||||||
|
)
|
||||||
|
const extraFileInFolderCPath = path.join(
|
||||||
|
root,
|
||||||
|
'folder-a',
|
||||||
|
'folder-b',
|
||||||
|
'folder-c',
|
||||||
|
'extra-file-in-folder-c.txt'
|
||||||
|
)
|
||||||
|
const amazingFileInFolderHPath = path.join(root, 'folder-h', 'amazing-item.txt')
|
||||||
|
const lonelyFilePath = path.join(
|
||||||
|
root,
|
||||||
|
'folder-h',
|
||||||
|
'folder-j',
|
||||||
|
'folder-k',
|
||||||
|
'lonely-file.txt'
|
||||||
|
)
|
||||||
|
|
||||||
|
describe('Search', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
// mock all output so that there is less noise when running tests
|
||||||
|
jest.spyOn(console, 'log').mockImplementation(() => {})
|
||||||
|
jest.spyOn(core, 'debug').mockImplementation(() => {})
|
||||||
|
jest.spyOn(core, 'info').mockImplementation(() => {})
|
||||||
|
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
||||||
|
|
||||||
|
// clear temp directory
|
||||||
|
await io.rmRF(root)
|
||||||
|
await fs.mkdir(path.join(root, 'folder-a', 'folder-b', 'folder-c'), {
|
||||||
|
recursive: true
|
||||||
|
})
|
||||||
|
await fs.mkdir(path.join(root, 'folder-a', 'folder-b', 'folder-e'), {
|
||||||
|
recursive: true
|
||||||
|
})
|
||||||
|
await fs.mkdir(path.join(root, 'folder-d'), {
|
||||||
|
recursive: true
|
||||||
|
})
|
||||||
|
await fs.mkdir(path.join(root, 'folder-f'), {
|
||||||
|
recursive: true
|
||||||
|
})
|
||||||
|
await fs.mkdir(path.join(root, 'folder-g'), {
|
||||||
|
recursive: true
|
||||||
|
})
|
||||||
|
await fs.mkdir(path.join(root, 'folder-h', 'folder-i'), {
|
||||||
|
recursive: true
|
||||||
|
})
|
||||||
|
await fs.mkdir(path.join(root, 'folder-h', 'folder-j', 'folder-k'), {
|
||||||
|
recursive: true
|
||||||
|
})
|
||||||
|
|
||||||
|
await fs.writeFile(searchItem1Path, 'search item1 file')
|
||||||
|
await fs.writeFile(searchItem2Path, 'search item2 file')
|
||||||
|
await fs.writeFile(searchItem3Path, 'search item3 file')
|
||||||
|
await fs.writeFile(searchItem4Path, 'search item4 file')
|
||||||
|
await fs.writeFile(searchItem5Path, 'search item5 file')
|
||||||
|
|
||||||
|
await fs.writeFile(extraSearchItem1Path, 'extraSearch item1 file')
|
||||||
|
await fs.writeFile(extraSearchItem2Path, 'extraSearch item2 file')
|
||||||
|
await fs.writeFile(extraSearchItem3Path, 'extraSearch item3 file')
|
||||||
|
await fs.writeFile(extraSearchItem4Path, 'extraSearch item4 file')
|
||||||
|
await fs.writeFile(extraSearchItem5Path, 'extraSearch item5 file')
|
||||||
|
|
||||||
|
await fs.writeFile(extraFileInFolderCPath, 'extra file')
|
||||||
|
|
||||||
|
await fs.writeFile(amazingFileInFolderHPath, 'amazing file')
|
||||||
|
|
||||||
|
await fs.writeFile(lonelyFilePath, 'all by itself')
|
||||||
|
/*
|
||||||
|
Directory structure of files that get created:
|
||||||
|
root/
|
||||||
|
folder-a/
|
||||||
|
folder-b/
|
||||||
|
folder-c/
|
||||||
|
search-item1.txt
|
||||||
|
extraSearch-item1.txt
|
||||||
|
extra-file-in-folder-c.txt
|
||||||
|
folder-e/
|
||||||
|
folder-d/
|
||||||
|
search-item2.txt
|
||||||
|
search-item3.txt
|
||||||
|
search-item4.txt
|
||||||
|
extraSearch-item2.txt
|
||||||
|
folder-f/
|
||||||
|
extraSearch-item3.txt
|
||||||
|
folder-g/
|
||||||
|
folder-h/
|
||||||
|
amazing-item.txt
|
||||||
|
folder-i/
|
||||||
|
extraSearch-item4.txt
|
||||||
|
extraSearch-item5.txt
|
||||||
|
folder-j/
|
||||||
|
folder-k/
|
||||||
|
lonely-file.txt
|
||||||
|
search-item5.txt
|
||||||
|
*/
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Single file search - Absolute Path', async () => {
|
||||||
|
const searchResult = await findFilesToUpload(extraFileInFolderCPath)
|
||||||
|
expect(searchResult.filesToUpload.length).toEqual(1)
|
||||||
|
expect(searchResult.filesToUpload[0]).toEqual(extraFileInFolderCPath)
|
||||||
|
expect(searchResult.rootDirectory).toEqual(
|
||||||
|
path.join(root, 'folder-a', 'folder-b', 'folder-c')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Single file search - Relative Path', async () => {
|
||||||
|
const relativePath = path.join(
|
||||||
|
'__tests__',
|
||||||
|
'_temp',
|
||||||
|
'search',
|
||||||
|
'folder-a',
|
||||||
|
'folder-b',
|
||||||
|
'folder-c',
|
||||||
|
'search-item1.txt'
|
||||||
|
)
|
||||||
|
|
||||||
|
const searchResult = await findFilesToUpload(relativePath)
|
||||||
|
expect(searchResult.filesToUpload.length).toEqual(1)
|
||||||
|
expect(searchResult.filesToUpload[0]).toEqual(searchItem1Path)
|
||||||
|
expect(searchResult.rootDirectory).toEqual(
|
||||||
|
path.join(root, 'folder-a', 'folder-b', 'folder-c')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Single file using wildcard', async () => {
|
||||||
|
const expectedRoot = path.join(root, 'folder-h')
|
||||||
|
const searchPath = path.join(root, 'folder-h', '**/*lonely*')
|
||||||
|
const searchResult = await findFilesToUpload(searchPath)
|
||||||
|
expect(searchResult.filesToUpload.length).toEqual(1)
|
||||||
|
expect(searchResult.filesToUpload[0]).toEqual(lonelyFilePath)
|
||||||
|
expect(searchResult.rootDirectory).toEqual(expectedRoot)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Single file using directory', async () => {
|
||||||
|
const searchPath = path.join(root, 'folder-h', 'folder-j')
|
||||||
|
const searchResult = await findFilesToUpload(searchPath)
|
||||||
|
expect(searchResult.filesToUpload.length).toEqual(1)
|
||||||
|
expect(searchResult.filesToUpload[0]).toEqual(lonelyFilePath)
|
||||||
|
expect(searchResult.rootDirectory).toEqual(searchPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Directory search - Absolute Path', async () => {
|
||||||
|
const searchPath = path.join(root, 'folder-h')
|
||||||
|
const searchResult = await findFilesToUpload(searchPath)
|
||||||
|
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)
|
||||||
|
|
||||||
|
expect(searchResult.rootDirectory).toEqual(searchPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Directory search - Relative Path', async () => {
|
||||||
|
const searchPath = path.join('__tests__', '_temp', 'search', 'folder-h')
|
||||||
|
const expectedRootDirectory = path.join(root, 'folder-h')
|
||||||
|
const searchResult = await findFilesToUpload(searchPath)
|
||||||
|
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)
|
||||||
|
|
||||||
|
expect(searchResult.rootDirectory).toEqual(expectedRootDirectory)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Wildcard search - Absolute Path', async () => {
|
||||||
|
const searchPath = path.join(root, '**/*[Ss]earch*')
|
||||||
|
const searchResult = await findFilesToUpload(searchPath)
|
||||||
|
expect(searchResult.filesToUpload.length).toEqual(10)
|
||||||
|
|
||||||
|
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(searchItem5Path)).toEqual(true)
|
||||||
|
expect(searchResult.filesToUpload.includes(extraSearchItem1Path)).toEqual(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
expect(searchResult.filesToUpload.includes(extraSearchItem2Path)).toEqual(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
expect(searchResult.filesToUpload.includes(extraSearchItem3Path)).toEqual(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
expect(searchResult.filesToUpload.includes(extraSearchItem4Path)).toEqual(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
expect(searchResult.filesToUpload.includes(extraSearchItem5Path)).toEqual(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(searchResult.rootDirectory).toEqual(root)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Wildcard search - Relative Path', async () => {
|
||||||
|
const searchPath = path.join(
|
||||||
|
'__tests__',
|
||||||
|
'_temp',
|
||||||
|
'search',
|
||||||
|
'**/*[Ss]earch*'
|
||||||
|
)
|
||||||
|
const searchResult = await findFilesToUpload(searchPath)
|
||||||
|
expect(searchResult.filesToUpload.length).toEqual(10)
|
||||||
|
|
||||||
|
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(searchItem5Path)).toEqual(true)
|
||||||
|
expect(searchResult.filesToUpload.includes(extraSearchItem1Path)).toEqual(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
expect(searchResult.filesToUpload.includes(extraSearchItem2Path)).toEqual(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
expect(searchResult.filesToUpload.includes(extraSearchItem3Path)).toEqual(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
expect(searchResult.filesToUpload.includes(extraSearchItem4Path)).toEqual(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
expect(searchResult.filesToUpload.includes(extraSearchItem5Path)).toEqual(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(searchResult.rootDirectory).toEqual(root)
|
||||||
|
})
|
||||||
|
})
|
|
@ -4,10 +4,10 @@ author: 'GitHub'
|
||||||
inputs:
|
inputs:
|
||||||
name:
|
name:
|
||||||
description: 'Artifact name'
|
description: 'Artifact name'
|
||||||
required: true
|
required: false
|
||||||
path:
|
path:
|
||||||
description: 'Directory containing files to upload'
|
description: 'A file, directory or wildcard pattern that describes what to upload'
|
||||||
required: true
|
required: true
|
||||||
runs:
|
runs:
|
||||||
# Plugins live on the runner and are only available to a certain set of first party actions.
|
using: 'node12'
|
||||||
plugin: 'publish'
|
main: 'dist/index.js'
|
8526
dist/index.js
vendored
Normal file
8526
dist/index.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
12
jest.config.js
Normal file
12
jest.config.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
module.exports = {
|
||||||
|
clearMocks: true,
|
||||||
|
moduleFileExtensions: ['js', 'ts'],
|
||||||
|
roots: ['<rootDir>'],
|
||||||
|
testEnvironment: 'node',
|
||||||
|
testMatch: ['**/*.test.ts'],
|
||||||
|
testRunner: 'jest-circus/runner',
|
||||||
|
transform: {
|
||||||
|
'^.+\\.ts$': 'ts-jest'
|
||||||
|
},
|
||||||
|
verbose: true
|
||||||
|
}
|
7096
package-lock.json
generated
Normal file
7096
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
51
package.json
Normal file
51
package.json
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
"name": "upload-artifact",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "Upload a build artifact that can be used by subsequent workflow steps",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"release": "ncc build src/upload-artifact.ts && git add -f dist/",
|
||||||
|
"check-all": "concurrently \"npm:format-check\" \"npm:lint\" \"npm:test\" \"npm:build\"",
|
||||||
|
"format": "prettier --write **/*.ts",
|
||||||
|
"format-check": "prettier --check **/*.ts",
|
||||||
|
"lint": "eslint **/*.ts",
|
||||||
|
"test": "jest --testTimeout 10000"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/actions/upload-artifact.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Actions",
|
||||||
|
"GitHub",
|
||||||
|
"Artifacts",
|
||||||
|
"Upload"
|
||||||
|
],
|
||||||
|
"author": "GitHub",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/actions/upload-artifact/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/actions/upload-artifact#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"@actions/artifact": "^0.3.1",
|
||||||
|
"@actions/core": "^1.2.3",
|
||||||
|
"@actions/glob": "^0.1.0",
|
||||||
|
"@actions/io": "^1.0.2",
|
||||||
|
"@types/jest": "^25.2.1",
|
||||||
|
"@types/node": "^13.11.1",
|
||||||
|
"@typescript-eslint/parser": "^2.27.0",
|
||||||
|
"@zeit/ncc": "^0.22.1",
|
||||||
|
"concurrently": "^5.1.0",
|
||||||
|
"eslint": "^6.8.0",
|
||||||
|
"eslint-plugin-github": "^3.4.1",
|
||||||
|
"eslint-plugin-jest": "^23.8.2",
|
||||||
|
"glob": "^7.1.6",
|
||||||
|
"jest": "^25.3.0",
|
||||||
|
"jest-circus": "^25.3.0",
|
||||||
|
"prettier": "^2.0.4",
|
||||||
|
"ts-jest": "^25.3.1",
|
||||||
|
"typescript": "^3.8.3"
|
||||||
|
}
|
||||||
|
}
|
8
src/constants.ts
Normal file
8
src/constants.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export enum Inputs {
|
||||||
|
Name = 'name',
|
||||||
|
Path = 'path'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDefaultArtifactName(): string {
|
||||||
|
return 'artifact'
|
||||||
|
}
|
69
src/search.ts
Normal file
69
src/search.ts
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import * as glob from '@actions/glob'
|
||||||
|
import {debug} from '@actions/core'
|
||||||
|
import {lstatSync} from 'fs'
|
||||||
|
import {dirname} from 'path'
|
||||||
|
|
||||||
|
export interface SearchResult {
|
||||||
|
filesToUpload: string[]
|
||||||
|
rootDirectory: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultGlobOptions(): glob.GlobOptions {
|
||||||
|
return {
|
||||||
|
followSymbolicLinks: true,
|
||||||
|
implicitDescendants: true,
|
||||||
|
omitBrokenSymbolicLinks: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findFilesToUpload(
|
||||||
|
searchPath: string,
|
||||||
|
globOptions?: glob.GlobOptions
|
||||||
|
): Promise<SearchResult> {
|
||||||
|
const searchResults: string[] = []
|
||||||
|
const globber = await glob.create(
|
||||||
|
searchPath,
|
||||||
|
globOptions || getDefaultGlobOptions()
|
||||||
|
)
|
||||||
|
const rawSearchResults: string[] = await globber.glob()
|
||||||
|
|
||||||
|
/*
|
||||||
|
Directories will be rejected if attempted to be uploaded. This includes just empty
|
||||||
|
directories so filter any directories out from the raw search results
|
||||||
|
*/
|
||||||
|
for (const searchResult of rawSearchResults) {
|
||||||
|
if (!lstatSync(searchResult).isDirectory()) {
|
||||||
|
debug(`File:${searchResult} was found using the provided searchPath`)
|
||||||
|
searchResults.push(searchResult)
|
||||||
|
} else {
|
||||||
|
debug(
|
||||||
|
`Removing ${searchResult} from rawSearchResults because it is a directory`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
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()
|
||||||
|
if (searchPaths.length > 1) {
|
||||||
|
throw new Error('Only 1 search path should be returned')
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Special case for a single file artifact that is uploaded without a directory or wildcard pattern. The directory structure is
|
||||||
|
not preserved and the root directory will be the single files parent directory
|
||||||
|
*/
|
||||||
|
if (searchResults.length === 1 && searchPaths[0] === searchResults[0]) {
|
||||||
|
return {
|
||||||
|
filesToUpload: searchResults,
|
||||||
|
rootDirectory: dirname(searchResults[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
filesToUpload: searchResults,
|
||||||
|
rootDirectory: searchPaths[0]
|
||||||
|
}
|
||||||
|
}
|
40
src/upload-artifact.ts
Normal file
40
src/upload-artifact.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import {create, UploadOptions} from '@actions/artifact'
|
||||||
|
import {Inputs, getDefaultArtifactName} from './constants'
|
||||||
|
import {findFilesToUpload} from './search'
|
||||||
|
|
||||||
|
async function run(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const name = core.getInput(Inputs.Name, {required: false})
|
||||||
|
const path = core.getInput(Inputs.Path, {required: true})
|
||||||
|
|
||||||
|
const searchResult = await findFilesToUpload(path)
|
||||||
|
if (searchResult.filesToUpload.length === 0) {
|
||||||
|
core.warning(
|
||||||
|
`No files were found for the provided path: ${path}. No artifacts will be uploaded.`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
core.info(
|
||||||
|
`With the provided path, there will be ${searchResult.filesToUpload.length} files uploaded`
|
||||||
|
)
|
||||||
|
core.debug(`Root artifact directory is ${searchResult.rootDirectory}`)
|
||||||
|
|
||||||
|
const artifactClient = create()
|
||||||
|
const options: UploadOptions = {
|
||||||
|
continueOnError: true
|
||||||
|
}
|
||||||
|
await artifactClient.uploadArtifact(
|
||||||
|
name || getDefaultArtifactName(),
|
||||||
|
searchResult.filesToUpload,
|
||||||
|
searchResult.rootDirectory,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
|
||||||
|
core.info('Artifact upload has finished successfully!')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
core.setFailed(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es6",
|
||||||
|
"module": "commonjs",
|
||||||
|
"outDir": "./lib",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"declaration": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"lib": ["es6"]
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "**/*.test.ts"]
|
||||||
|
}
|
Loading…
Reference in a new issue