Merge pull request #259 from actions/robherley/glob-downloads

Support pattern matching to filter artifacts & merge to same directory
This commit is contained in:
Rob Herley 2023-12-18 17:22:01 -05:00 committed by GitHub
commit f44cd7b40b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 9511 additions and 4739 deletions

View file

@ -9,8 +9,7 @@
"plugin:import/errors", "plugin:import/errors",
"plugin:import/warnings", "plugin:import/warnings",
"plugin:import/typescript", "plugin:import/typescript",
"plugin:prettier/recommended", "plugin:prettier/recommended"
"prettier/@typescript-eslint"
], ],
"plugins": ["@typescript-eslint"] "plugins": ["@typescript-eslint"]
} }

View file

@ -106,3 +106,25 @@ jobs:
Write-Error "File contents of downloaded artifacts are incorrect" Write-Error "File contents of downloaded artifacts are incorrect"
} }
shell: pwsh shell: pwsh
# Test glob downloading both artifacts to same directory
- name: Download all Artifacts
uses: ./
with:
pattern: Artifact-*
path: single/directory
merge-multiple: true
- name: Verify successful download
run: |
$fileA = "single/directory/file-A.txt"
$fileB = "single/directory/file-B.txt"
if(!(Test-Path -path $fileA) -or !(Test-Path -path $fileB))
{
Write-Error "Expected files do not exist"
}
if(!((Get-Content $fileA) -ceq "Lorem ipsum dolor sit amet") -or !((Get-Content $fileB) -ceq "Hello world from file B"))
{
Write-Error "File contents of downloaded artifacts are incorrect"
}
shell: pwsh

26
.licenses/npm/minimatch-9.0.3.dep.yml generated Normal file
View file

@ -0,0 +1,26 @@
---
name: minimatch
version: 9.0.3
type: npm
summary: a glob matcher in javascript
homepage:
license: isc
licenses:
- sources: LICENSE
text: |
The ISC License
Copyright (c) 2011-2023 Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
notices: []

26
.licenses/npm/minimatch.dep.yml generated Normal file
View file

@ -0,0 +1,26 @@
---
name: minimatch
version: 9.0.3
type: npm
summary:
homepage:
license: isc
licenses:
- sources: LICENSE
text: |
The ISC License
Copyright (c) 2011-2023 Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
notices: []

View file

@ -14,6 +14,7 @@ See also [upload-artifact](https://github.com/actions/upload-artifact).
- [Examples](#examples) - [Examples](#examples)
- [Download Single Artifact](#download-single-artifact) - [Download Single Artifact](#download-single-artifact)
- [Download All Artifacts](#download-all-artifacts) - [Download All Artifacts](#download-all-artifacts)
- [Download multiple (filtered) Artifacts to the same directory](#download-multiple-filtered-artifacts-to-the-same-directory)
- [Download Artifacts from other Workflow Runs or Repositories](#download-artifacts-from-other-workflow-runs-or-repositories) - [Download Artifacts from other Workflow Runs or Repositories](#download-artifacts-from-other-workflow-runs-or-repositories)
- [Limitations](#limitations) - [Limitations](#limitations)
- [Permission Loss](#permission-loss) - [Permission Loss](#permission-loss)
@ -38,6 +39,8 @@ For more information, see the [`@actions/artifact`](https://github.com/actions/t
1. On self hosted runners, additional [firewall rules](https://github.com/actions/toolkit/tree/main/packages/artifact#breaking-changes) may be required. 1. On self hosted runners, additional [firewall rules](https://github.com/actions/toolkit/tree/main/packages/artifact#breaking-changes) may be required.
2. Downloading artifacts that were created from `action/upload-artifact@v3` and below are not supported. 2. Downloading artifacts that were created from `action/upload-artifact@v3` and below are not supported.
For assistance with breaking changes, see [MIGRATION.md](docs/MIGRATION.md).
## Usage ## Usage
### Inputs ### Inputs
@ -46,13 +49,25 @@ For more information, see the [`@actions/artifact`](https://github.com/actions/t
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
# Name of the artifact to download. # Name of the artifact to download.
# Optional. If unspecified, all artifacts for the run are downloaded. # If unspecified, all artifacts for the run are downloaded.
# Optional.
name: name:
# Destination path. Supports basic tilde expansion. # Destination path. Supports basic tilde expansion.
# Optional. Defaults is $GITHUB_WORKSPACE # Optional. Default is $GITHUB_WORKSPACE
path: path:
# A glob pattern to the artifacts that should be downloaded.
# Ignored if name is specified.
# Optional.
pattern:
# When multiple artifacts are matched, this changes the behavior of the destination directories.
# If true, the downloaded artifacts will be in the same directory specified by path.
# If false, the downloaded artifacts will be extracted into individual named directories within the specified path.
# Optional. Default is 'false'
merge-multiple:
# The GitHub token used to authenticate with the GitHub API. # The GitHub token used to authenticate with the GitHub API.
# This is required when downloading artifacts from a different repository or from a different workflow run. # This is required when downloading artifacts from a different repository or from a different workflow run.
# Optional. If unspecified, the action will download artifacts from the current repo and the current workflow run. # Optional. If unspecified, the action will download artifacts from the current repo and the current workflow run.
@ -105,7 +120,7 @@ steps:
### Download All Artifacts ### Download All Artifacts
If the `name` input parameter is not provided, all artifacts will be downloaded. **To differentiate between downloaded artifacts, a directory denoted by the artifacts name will be created for each individual artifact.** If the `name` input parameter is not provided, all artifacts will be downloaded. To differentiate between downloaded artifacts, by default a directory denoted by the artifacts name will be created for each individual artifact. This behavior can be changed with the `merge-multiple` input parameter.
Example, if there are two artifacts `Artifact-A` and `Artifact-B`, and the directory is `etc/usr/artifacts/`, the directory structure will look like this: Example, if there are two artifacts `Artifact-A` and `Artifact-B`, and the directory is `etc/usr/artifacts/`, the directory structure will look like this:
@ -137,6 +152,67 @@ steps:
run: ls -R path/to/artifacts run: ls -R path/to/artifacts
``` ```
To download them to the _same_ directory:
```yaml
steps:
- uses: actions/download-artifact@v4
with:
path: path/to/artifacts
merge-multiple: true
- name: Display structure of downloaded files
run: ls -R path/to/artifacts
```
Which will result in:
```
path/to/artifacts/
... contents of Artifact-A
... contents of Artifact-B
```
### Download multiple (filtered) Artifacts to the same directory
In multiple arch/os scenarios, you may have Artifacts built in different jobs. To download all Artifacts to the same directory (or matching a glob pattern), you can use the `pattern` and `merge-multiple` inputs.
```yaml
jobs:
upload:
strategy:
matrix:
runs-on: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.runs-on }}
steps:
- name: Create a File
run: echo "hello from ${{ matrix.runs-on }}" > file-${{ matrix.runs-on }}.txt
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: my-artifact-${{ matrix.runs-on }}
path: file-${{ matrix.runs-on }}.txt
download:
needs: upload
runs-on: ubuntu-latest
steps:
- name: Download All Artifacts
uses: actions/download-artifact@v4
with:
path: my-artifact
pattern: my-artifact-*
merge-multiple: true
- run: ls -R my-artifact
```
This results in a directory like so:
```
my-artifact/
file-macos-latest.txt
file-ubuntu-latest.txt
file-windows-latest.txt
```
### Download Artifacts from other Workflow Runs or Repositories ### Download Artifacts from other Workflow Runs or Repositories
It may be useful to download Artifacts from other workflow runs, or even other repositories. By default, the permissions are scoped so they can only download Artifacts within the current workflow run. To elevate permissions for this scenario, you can specify a `github-token` along with other repository and run identifiers: It may be useful to download Artifacts from other workflow runs, or even other repositories. By default, the permissions are scoped so they can only download Artifacts within the current workflow run. To elevate permissions for this scenario, you can specify a `github-token` along with other repository and run identifiers:

View file

@ -3,11 +3,20 @@ description: 'Download a build artifact that was previously uploaded in the work
author: 'GitHub' author: 'GitHub'
inputs: inputs:
name: name:
description: 'Name of the artifact to download. If unspecified, all artifacts for the run are downloaded' description: 'Name of the artifact to download. If unspecified, all artifacts for the run are downloaded.'
required: false required: false
path: path:
description: 'Destination path. Supports basic tilde expansion. Defaults to $GITHUB_WORKSPACE' description: 'Destination path. Supports basic tilde expansion. Defaults to $GITHUB_WORKSPACE'
required: false required: false
pattern:
description: 'A glob pattern matching the artifacts that should be downloaded. Ignored if name is specified.'
required: false
merge-multiple:
description: 'When multiple artifacts are matched, this changes the behavior of the destination directories.
If true, the downloaded artifacts will be in the same directory specified by path.
If false, the downloaded artifacts will be extracted into individual named directories within the specified path.'
required: false
default: 'false'
github-token: github-token:
description: 'The GitHub token used to authenticate with the GitHub API. description: 'The GitHub token used to authenticate with the GitHub API.
This is required when downloading artifacts from a different repository or from a different workflow run. This is required when downloading artifacts from a different repository or from a different workflow run.

7621
dist/index.js vendored

File diff suppressed because one or more lines are too long

80
docs/MIGRATION.md Normal file
View file

@ -0,0 +1,80 @@
# Migration
- [Migration](#migration)
- [Multiple uploads to the same named Artifact](#multiple-uploads-to-the-same-named-artifact)
Several behavioral differences exist between Artifact actions `v3` and below vs `v4`. This document outlines common scenarios in `v3`, and how they would be handled in `v4`.
## Multiple uploads to the same named Artifact
In `v3`, Artifacts are _mutable_ so it's possible to write workflow scenarios where multiple jobs upload to the same Artifact like so:
```yaml
jobs:
upload:
strategy:
matrix:
runs-on: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.runs-on }}
steps:
- name: Create a File
run: echo "hello from ${{ matrix.runs-on }}" > file-${{ matrix.runs-on }}.txt
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: my-artifact # NOTE: same artifact name
path: file-${{ matrix.runs-on }}.txt
download:
needs: upload
runs-on: ubuntu-latest
steps:
- name: Download All Artifacts
uses: actions/download-artifact@v3
with:
path: my-artifact
- run: ls -R my-artifact
```
This results in a directory like so:
```
my-artifact/
file-macos-latest.txt
file-ubuntu-latest.txt
file-windows-latest.txt
```
In v4, Artifacts are immutable (unless deleted). So you must change each of the uploaded Artifacts to have a different name and filter the downloads by name to achieve the same effect:
```diff
jobs:
upload:
strategy:
matrix:
runs-on: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.runs-on }}
steps:
- name: Create a File
run: echo "hello from ${{ matrix.runs-on }}" > file-${{ matrix.runs-on }}.txt
- name: Upload Artifact
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
- name: my-artifact
+ name: my-artifact-${{ matrix.runs-on }}
path: file-${{ matrix.runs-on }}.txt
download:
needs: upload
runs-on: ubuntu-latest
steps:
- name: Download All Artifacts
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
with:
path: my-artifact
+ pattern: my-artifact-*
+ merge-multiple: true
- run: ls -R my-artifact
```
In `v4`, the new `pattern:` input will filter the downloaded Artifacts to match the name specified. The new `merge-multiple:` input will support downloading multiple Artifacts to the same directory. If the files within the Artifacts have the same name, the last writer wins.

6183
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -30,16 +30,18 @@
"dependencies": { "dependencies": {
"@actions/artifact": "^2.0.0", "@actions/artifact": "^2.0.0",
"@actions/core": "^1.10.0", "@actions/core": "^1.10.0",
"@actions/github": "^5.1.1" "@actions/github": "^5.1.1",
"minimatch": "^9.0.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^12.12.6", "@types/node": "^12.12.6",
"@typescript-eslint/eslint-plugin": "^5.40.1", "@typescript-eslint/eslint-plugin": "^6.14.0",
"@vercel/ncc": "^0.33.4", "@vercel/ncc": "^0.33.4",
"concurrently": "^5.2.0", "concurrently": "^5.2.0",
"eslint": "^7.4.0", "eslint": "^8.55.0",
"eslint-plugin-github": "^4.1.1", "eslint-plugin-github": "^4.10.1",
"prettier": "^2.0.5", "eslint-plugin-prettier": "^5.0.1",
"typescript": "^3.8.3" "prettier": "^3.1.1",
"typescript": "^5.3.3"
} }
} }

View file

@ -3,7 +3,9 @@ export enum Inputs {
Path = 'path', Path = 'path',
GitHubToken = 'github-token', GitHubToken = 'github-token',
Repository = 'repository', Repository = 'repository',
RunID = 'run-id' RunID = 'run-id',
Pattern = 'pattern',
MergeMultiple = 'merge-multiple'
} }
export enum Outputs { export enum Outputs {

View file

@ -3,6 +3,7 @@ import * as path from 'path'
import * as core from '@actions/core' import * as core from '@actions/core'
import artifactClient from '@actions/artifact' import artifactClient from '@actions/artifact'
import type {Artifact, FindOptions} from '@actions/artifact' import type {Artifact, FindOptions} from '@actions/artifact'
import {Minimatch} from 'minimatch'
import {Inputs, Outputs} from './constants' import {Inputs, Outputs} from './constants'
const PARALLEL_DOWNLOADS = 5 const PARALLEL_DOWNLOADS = 5
@ -20,7 +21,9 @@ async function run(): Promise<void> {
path: core.getInput(Inputs.Path, {required: false}), path: core.getInput(Inputs.Path, {required: false}),
token: core.getInput(Inputs.GitHubToken, {required: false}), token: core.getInput(Inputs.GitHubToken, {required: false}),
repository: core.getInput(Inputs.Repository, {required: false}), repository: core.getInput(Inputs.Repository, {required: false}),
runID: parseInt(core.getInput(Inputs.RunID, {required: false})) runID: parseInt(core.getInput(Inputs.RunID, {required: false})),
pattern: core.getInput(Inputs.Pattern, {required: false}),
mergeMultiple: core.getBooleanInput(Inputs.MergeMultiple, {required: false})
} }
if (!inputs.path) { if (!inputs.path) {
@ -80,21 +83,34 @@ async function run(): Promise<void> {
latest: true, latest: true,
...options ...options
}) })
artifacts = listArtifactResponse.artifacts
if (listArtifactResponse.artifacts.length === 0) { core.debug(`Found ${artifacts.length} artifacts in run`)
throw new Error(
`No artifacts found for run '${inputs.runID}' in '${inputs.repository}'` if (inputs.pattern) {
core.info(`Filtering artifacts by pattern '${inputs.pattern}'`)
const matcher = new Minimatch(inputs.pattern)
artifacts = artifacts.filter(artifact => matcher.match(artifact.name))
core.debug(
`Filtered from ${listArtifactResponse.artifacts.length} to ${artifacts.length} artifacts`
) )
} }
}
core.debug(`Found ${listArtifactResponse.artifacts.length} artifacts`) if (artifacts.length) {
artifacts = listArtifactResponse.artifacts core.info(`Preparing to download the following artifacts:`)
artifacts.forEach(artifact => {
core.info(
`- ${artifact.name} (ID: ${artifact.id}, Size: ${artifact.size})`
)
})
} }
const downloadPromises = artifacts.map(artifact => const downloadPromises = artifacts.map(artifact =>
artifactClient.downloadArtifact(artifact.id, { artifactClient.downloadArtifact(artifact.id, {
...options, ...options,
path: isSingleArtifactDownload path:
isSingleArtifactDownload || inputs.mergeMultiple
? resolvedPath ? resolvedPath
: path.join(resolvedPath, artifact.name) : path.join(resolvedPath, artifact.name)
}) })