Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
484a0b528f | ||
|
|
3e5f45b2cf | ||
|
|
e6d03f6737 | ||
|
|
70fc10c6e5 | ||
|
|
f258da9a50 | ||
|
|
ccc058e5fb | ||
|
|
bd7976ba57 | ||
|
|
ac21fcf45e | ||
|
|
15999bff51 | ||
|
|
974686ed50 | ||
|
|
fbe48b1d27 | ||
|
|
96bf374a61 | ||
|
|
b8c4819ef5 | ||
|
|
7127277c52 | ||
|
|
41a8b730f4 | ||
|
|
6533bb2df3 | ||
|
|
e6afc5e70e | ||
|
|
87db0e9f47 | ||
|
|
c9ff097e79 | ||
|
|
882c7072ac | ||
|
|
6d4a0de04f | ||
|
|
f45b79f03c | ||
|
|
1d636af56d | ||
|
|
43c2394920 | ||
|
|
3d65dc0e0b | ||
|
|
37930b1c2a | ||
|
|
72582b9e0a | ||
|
|
0d2ec9d4cb | ||
|
|
fd7ae8fda6 | ||
|
|
d484700543 | ||
|
|
03a808050e | ||
|
|
56fe6d904b | ||
|
|
8e3ebc4ab4 | ||
|
|
1e3c4b4d49 | ||
|
|
458627d354 | ||
|
|
623bd53c6d |
@@ -1,3 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
lib/
|
|
||||||
dist/
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
],
|
|
||||||
"plugins": ["@typescript-eslint"]
|
|
||||||
}
|
|
||||||
+191
-2
@@ -63,14 +63,14 @@ jobs:
|
|||||||
path: path/to/artifact-B
|
path: path/to/artifact-B
|
||||||
|
|
||||||
# Test downloading a single artifact
|
# Test downloading a single artifact
|
||||||
- name: Download artifact A
|
- name: Download artifact A (absolute path)
|
||||||
uses: ./
|
uses: ./
|
||||||
with:
|
with:
|
||||||
name: Artifact-A-${{ matrix.runs-on }}
|
name: Artifact-A-${{ matrix.runs-on }}
|
||||||
path: some/new/path
|
path: some/new/path
|
||||||
|
|
||||||
# Test downloading an artifact using tilde expansion
|
# Test downloading an artifact using tilde expansion
|
||||||
- name: Download artifact A
|
- name: Download artifact A (tilde expansion)
|
||||||
uses: ./
|
uses: ./
|
||||||
with:
|
with:
|
||||||
name: Artifact-A-${{ matrix.runs-on }}
|
name: Artifact-A-${{ matrix.runs-on }}
|
||||||
@@ -131,3 +131,192 @@ jobs:
|
|||||||
Write-Error "File contents of downloaded artifacts are incorrect"
|
Write-Error "File contents of downloaded artifacts are incorrect"
|
||||||
}
|
}
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
|
# Test downloading artifact without decompressing (skip-decompress)
|
||||||
|
- name: Download artifact A without decompressing
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
name: Artifact-A-${{ matrix.runs-on }}
|
||||||
|
path: skip-decompress-test
|
||||||
|
skip-decompress: true
|
||||||
|
|
||||||
|
- name: Verify skip-decompress download
|
||||||
|
run: |
|
||||||
|
$rawFile = "skip-decompress-test/Artifact-A-${{ matrix.runs-on }}.zip"
|
||||||
|
if(!(Test-Path -path $rawFile))
|
||||||
|
{
|
||||||
|
Write-Error "Expected raw artifact file does not exist at $rawFile"
|
||||||
|
}
|
||||||
|
$fileInfo = Get-Item $rawFile
|
||||||
|
if($fileInfo.Length -eq 0)
|
||||||
|
{
|
||||||
|
Write-Error "Downloaded artifact file is empty"
|
||||||
|
}
|
||||||
|
Write-Host "Successfully downloaded artifact without decompressing: $rawFile (size: $($fileInfo.Length) bytes)"
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
|
# Regression test for artifact filename vs content-type mismatch
|
||||||
|
# When an archived artifact has a name with a file extension that doesn't
|
||||||
|
# match the blob type (e.g. "report.txt" but blob is zip), the server
|
||||||
|
# should append .zip to the content-disposition filename.
|
||||||
|
- name: Create and upload archived artifact with misleading extension
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p path/to/extension-test
|
||||||
|
echo '{"key": "value"}' > path/to/extension-test/data.json
|
||||||
|
- uses: actions/upload-artifact@v4 # V4 is important here to ensure we're supporting older versions correctly
|
||||||
|
with:
|
||||||
|
name: report.txt-${{ matrix.runs-on }}.json
|
||||||
|
path: path/to/extension-test/data.json
|
||||||
|
|
||||||
|
- name: Download misleading-extension artifact without decompressing
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
name: report.txt-${{ matrix.runs-on }}.json
|
||||||
|
path: ext-test/raw
|
||||||
|
skip-decompress: true
|
||||||
|
|
||||||
|
- name: Verify downloaded file has .zip extension appended
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
expected="ext-test/raw/report.txt-${{ matrix.runs-on }}.json.zip"
|
||||||
|
if [ -f "$expected" ]; then
|
||||||
|
echo "PASS: Downloaded file has .zip appended: $expected"
|
||||||
|
else
|
||||||
|
echo "FAIL: Expected $expected but got:"
|
||||||
|
ls -al ext-test/raw/
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test uploading and downloading artifacts with CJK (Chinese, Japanese, Korean) characters
|
||||||
|
# Regression test: certain non-ASCII chars (e.g. U+571F 土) caused 400 errors from
|
||||||
|
# Azure Blob Storage due to encoding issues in the Content-Disposition / rscd parameter
|
||||||
|
- name: Create artifacts with CJK names
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p path/to/cjk-artifacts
|
||||||
|
# Chinese - 土 (U+571F) known to fail, 日 (U+65E5) known to work
|
||||||
|
echo "Content for 土" > "path/to/cjk-artifacts/file-土-${{ matrix.runs-on }}.txt"
|
||||||
|
echo "Content for 中文测试" > "path/to/cjk-artifacts/file-中文测试-${{ matrix.runs-on }}.txt"
|
||||||
|
# Japanese - katakana and kanji
|
||||||
|
echo "Content for テスト" > "path/to/cjk-artifacts/file-テスト-${{ matrix.runs-on }}.txt"
|
||||||
|
echo "Content for 東京タワー" > "path/to/cjk-artifacts/file-東京タワー-${{ matrix.runs-on }}.txt"
|
||||||
|
# Korean - Hangul
|
||||||
|
echo "Content for 테스트" > "path/to/cjk-artifacts/file-테스트-${{ matrix.runs-on }}.txt"
|
||||||
|
echo "Content for 서울시" > "path/to/cjk-artifacts/file-서울시-${{ matrix.runs-on }}.txt"
|
||||||
|
|
||||||
|
- name: Upload CJK artifact - Chinese 土
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
path: path/to/cjk-artifacts/file-土-${{ matrix.runs-on }}.txt
|
||||||
|
archive: false
|
||||||
|
|
||||||
|
- name: Upload CJK artifact - Chinese 中文测试
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
path: path/to/cjk-artifacts/file-中文测试-${{ matrix.runs-on }}.txt
|
||||||
|
archive: false
|
||||||
|
|
||||||
|
- name: Upload CJK artifact - Japanese テスト
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
path: path/to/cjk-artifacts/file-テスト-${{ matrix.runs-on }}.txt
|
||||||
|
archive: false
|
||||||
|
|
||||||
|
- name: Upload CJK artifact - Japanese 東京タワー
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
path: path/to/cjk-artifacts/file-東京タワー-${{ matrix.runs-on }}.txt
|
||||||
|
archive: false
|
||||||
|
|
||||||
|
- name: Upload CJK artifact - Korean 테스트
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
path: path/to/cjk-artifacts/file-테스트-${{ matrix.runs-on }}.txt
|
||||||
|
archive: false
|
||||||
|
|
||||||
|
- name: Upload CJK artifact - Korean 서울시
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
path: path/to/cjk-artifacts/file-서울시-${{ matrix.runs-on }}.txt
|
||||||
|
archive: false
|
||||||
|
|
||||||
|
- name: Download CJK artifact - Chinese 土
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
name: file-土-${{ matrix.runs-on }}.txt
|
||||||
|
path: cjk-download/土
|
||||||
|
|
||||||
|
- name: Download CJK artifact - Chinese 中文测试
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
name: file-中文测试-${{ matrix.runs-on }}.txt
|
||||||
|
path: cjk-download/中文测试
|
||||||
|
|
||||||
|
- name: Download CJK artifact - Japanese テスト
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
name: file-テスト-${{ matrix.runs-on }}.txt
|
||||||
|
path: cjk-download/テスト
|
||||||
|
|
||||||
|
- name: Download CJK artifact - Japanese 東京タワー
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
name: file-東京タワー-${{ matrix.runs-on }}.txt
|
||||||
|
path: cjk-download/東京タワー
|
||||||
|
|
||||||
|
- name: Download CJK artifact - Korean 테스트
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
name: file-테스트-${{ matrix.runs-on }}.txt
|
||||||
|
path: cjk-download/테스트
|
||||||
|
|
||||||
|
- name: Download CJK artifact - Korean 서울시
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
name: file-서울시-${{ matrix.runs-on }}.txt
|
||||||
|
path: cjk-download/서울시
|
||||||
|
|
||||||
|
- name: Verify CJK artifact downloads
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
check_file() {
|
||||||
|
local file="$1"
|
||||||
|
local expected="$2"
|
||||||
|
if [ ! -f "$file" ]; then
|
||||||
|
echo "FAIL: Missing file: $file"
|
||||||
|
fail=1
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
actual=$(cat "$file")
|
||||||
|
if [ "$actual" != "$expected" ]; then
|
||||||
|
echo "FAIL: Content mismatch in $file"
|
||||||
|
echo " Expected: '$expected'"
|
||||||
|
echo " Got: '$actual'"
|
||||||
|
fail=1
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo "PASS: $file"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== Chinese ==="
|
||||||
|
check_file "cjk-download/土/file-土-${{ matrix.runs-on }}.txt" "Content for 土"
|
||||||
|
check_file "cjk-download/中文测试/file-中文测试-${{ matrix.runs-on }}.txt" "Content for 中文测试"
|
||||||
|
|
||||||
|
echo "=== Japanese ==="
|
||||||
|
check_file "cjk-download/テスト/file-テスト-${{ matrix.runs-on }}.txt" "Content for テスト"
|
||||||
|
check_file "cjk-download/東京タワー/file-東京タワー-${{ matrix.runs-on }}.txt" "Content for 東京タワー"
|
||||||
|
|
||||||
|
echo "=== Korean ==="
|
||||||
|
check_file "cjk-download/테스트/file-테스트-${{ matrix.runs-on }}.txt" "Content for 테스트"
|
||||||
|
check_file "cjk-download/서울시/file-서울시-${{ matrix.runs-on }}.txt" "Content for 서울시"
|
||||||
|
|
||||||
|
if [ "$fail" -ne 0 ]; then
|
||||||
|
echo "Some CJK artifact checks failed"
|
||||||
|
ls -alR cjk-download/ || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "All CJK artifact downloads verified successfully"
|
||||||
+3
-1
@@ -2,4 +2,6 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
# Ignore js files that are transpiled from ts files in src/
|
# Ignore js files that are transpiled from ts files in src/
|
||||||
lib/
|
lib/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
BIN
Binary file not shown.
Generated
Generated
BIN
Binary file not shown.
Binary file not shown.
Generated
BIN
Binary file not shown.
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
@@ -4,62 +4,27 @@ Download [Actions Artifacts](https://docs.github.com/en/actions/using-workflows/
|
|||||||
|
|
||||||
See also [upload-artifact](https://github.com/actions/upload-artifact).
|
See also [upload-artifact](https://github.com/actions/upload-artifact).
|
||||||
|
|
||||||
- [`@actions/download-artifact`](#actionsdownload-artifact)
|
- [What's new](#whats-new)
|
||||||
- [v5 - What's new](#v5---whats-new)
|
- [Note](#note)
|
||||||
- [v4 - What's new](#v4---whats-new)
|
- [GHES Support](#ghes-support)
|
||||||
- [Improvements](#improvements)
|
- [Usage](#usage)
|
||||||
- [Breaking Changes](#breaking-changes)
|
- [Inputs](#inputs)
|
||||||
- [Note](#note)
|
- [Outputs](#outputs)
|
||||||
- [Usage](#usage)
|
- [Examples](#examples)
|
||||||
- [Inputs](#inputs)
|
- [Download Single Artifact](#download-single-artifact)
|
||||||
- [Outputs](#outputs)
|
- [Download Artifacts by ID](#download-artifacts-by-id)
|
||||||
- [Examples](#examples)
|
- [Download All Artifacts](#download-all-artifacts)
|
||||||
- [Download Single Artifact](#download-single-artifact)
|
- [Download multiple (filtered) Artifacts to the same directory](#download-multiple-filtered-artifacts-to-the-same-directory)
|
||||||
- [Download Artifacts by ID](#download-artifacts-by-id)
|
- [Download Artifacts from other Workflow Runs or Repositories](#download-artifacts-from-other-workflow-runs-or-repositories)
|
||||||
- [Download All Artifacts](#download-all-artifacts)
|
- [Maintaining File Permissions](#maintaining-file-permissions)
|
||||||
- [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)
|
|
||||||
- [Limitations](#limitations)
|
|
||||||
- [Permission Loss](#permission-loss)
|
|
||||||
|
|
||||||
## v5 - What's new
|
## What's new
|
||||||
|
|
||||||
Previously, **single artifact downloads** behaved differently depending on how you specified the artifact:
|
Check out the [releases page](https://github.com/actions/download-artifact/releases) for details on what's new.
|
||||||
|
|
||||||
- **By name**: `name: my-artifact` → extracted to `path/` (direct)
|
|
||||||
- **By ID**: `artifact-ids: 12345` → extracted to `path/my-artifact/` (nested)
|
|
||||||
|
|
||||||
Now both methods are consistent:
|
|
||||||
|
|
||||||
- **By name**: `name: my-artifact` → extracted to `path/` (unchanged)
|
|
||||||
- **By ID**: `artifact-ids: 12345` → extracted to `path/` (updated - now direct)
|
|
||||||
|
|
||||||
Note: This change also applies to patterns that only match a single artifact.
|
|
||||||
|
|
||||||
## v4 - What's new
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> download-artifact@v4+ is not currently supported on GitHub Enterprise Server (GHES) yet. If you are on GHES, you must use [v3](https://github.com/actions/download-artifact/releases/tag/v3) (Node 16) or [v3-node20](https://github.com/actions/download-artifact/releases/tag/v3-node20) (Node 20).
|
|
||||||
|
|
||||||
The release of upload-artifact@v4 and download-artifact@v4 are major changes to the backend architecture of Artifacts. They have numerous performance and behavioral improvements.
|
|
||||||
|
|
||||||
For more information, see the [`@actions/artifact`](https://github.com/actions/toolkit/tree/main/packages/artifact) documentation.
|
|
||||||
|
|
||||||
### Improvements
|
|
||||||
|
|
||||||
1. Downloads are significantly faster, upwards of 90% improvement in worst case scenarios.
|
|
||||||
2. Artifacts can be downloaded from other workflow runs and repositories when supplied with a PAT.
|
|
||||||
|
|
||||||
### Breaking Changes
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
For assistance with breaking changes, see [MIGRATION.md](docs/MIGRATION.md).
|
|
||||||
|
|
||||||
## Note
|
## Note
|
||||||
|
|
||||||
Thank you for your interest in this GitHub repo, however, right now we are not taking contributions.
|
Thank you for your interest in this GitHub repo, however, right now we are not taking contributions.
|
||||||
|
|
||||||
We continue to focus our resources on strategic areas that help our customers be successful while making developers' lives easier. While GitHub Actions remains a key part of this vision, we are allocating resources towards other areas of Actions and are not taking contributions to this repository at this time. The GitHub public roadmap is the best place to follow along for any updates on features we’re working on and what stage they’re in.
|
We continue to focus our resources on strategic areas that help our customers be successful while making developers' lives easier. While GitHub Actions remains a key part of this vision, we are allocating resources towards other areas of Actions and are not taking contributions to this repository at this time. The GitHub public roadmap is the best place to follow along for any updates on features we’re working on and what stage they’re in.
|
||||||
|
|
||||||
@@ -67,7 +32,7 @@ We are taking the following steps to better direct requests related to GitHub Ac
|
|||||||
|
|
||||||
1. We will be directing questions and support requests to our [Community Discussions area](https://github.com/orgs/community/discussions/categories/actions)
|
1. We will be directing questions and support requests to our [Community Discussions area](https://github.com/orgs/community/discussions/categories/actions)
|
||||||
|
|
||||||
2. High Priority bugs can be reported through Community Discussions or you can report these to our support team https://support.github.com/contact/bug-report.
|
2. High Priority bugs can be reported through Community Discussions or you can report these to our support team <https://support.github.com/contact/bug-report>.
|
||||||
|
|
||||||
3. Security Issues should be handled as per our [security.md](SECURITY.md).
|
3. Security Issues should be handled as per our [security.md](SECURITY.md).
|
||||||
|
|
||||||
@@ -75,12 +40,16 @@ We will still provide security updates for this project and fix major breaking c
|
|||||||
|
|
||||||
You are welcome to still raise bugs in this repo.
|
You are welcome to still raise bugs in this repo.
|
||||||
|
|
||||||
|
## GHES Support
|
||||||
|
|
||||||
|
`download-artifact@v4+` is not currently supported on GitHub Enterprise Server (GHES) yet. If you are on GHES, you must use [v3](https://github.com/actions/download-artifact/releases/tag/v3) (Node 16) or [v3-node20](https://github.com/actions/download-artifact/releases/tag/v3-node20) (Node 20).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Inputs
|
### Inputs
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/download-artifact@v5
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
# Name of the artifact to download.
|
# Name of the artifact to download.
|
||||||
# If unspecified, all artifacts for the run are downloaded.
|
# If unspecified, all artifacts for the run are downloaded.
|
||||||
@@ -122,6 +91,17 @@ You are welcome to still raise bugs in this repo.
|
|||||||
# If github-token is specified, this is the run that artifacts will be downloaded from.
|
# If github-token is specified, this is the run that artifacts will be downloaded from.
|
||||||
# Optional. Default is ${{ github.run_id }}
|
# Optional. Default is ${{ github.run_id }}
|
||||||
run-id:
|
run-id:
|
||||||
|
|
||||||
|
# Whether to skip decompressing a zip file (if detected).
|
||||||
|
# If true, the downloaded artifact will not be automatically extracted/decompressed.
|
||||||
|
# This is useful when you want to handle the artifact as-is without extraction.
|
||||||
|
# Optional. Default is `false`
|
||||||
|
skip-decompress:
|
||||||
|
|
||||||
|
# What to do if the action detects a mismatch between the downloaded hash and the expected hash from the server.
|
||||||
|
# Can be one of: `ignore`, `info`, `warn`, `error`
|
||||||
|
# Optional. Default is `error`
|
||||||
|
digest-mismatch:
|
||||||
```
|
```
|
||||||
|
|
||||||
### Outputs
|
### Outputs
|
||||||
@@ -138,7 +118,7 @@ Download to current working directory (`$GITHUB_WORKSPACE`):
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v5
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: my-artifact
|
name: my-artifact
|
||||||
- name: Display structure of downloaded files
|
- name: Display structure of downloaded files
|
||||||
@@ -149,7 +129,7 @@ Download to a specific directory (also supports `~` expansion):
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v5
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: my-artifact
|
name: my-artifact
|
||||||
path: your/destination/dir
|
path: your/destination/dir
|
||||||
@@ -157,15 +137,24 @@ steps:
|
|||||||
run: ls -R your/destination/dir
|
run: ls -R your/destination/dir
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Directly download a non-zipped file (only supports files uploaded with `actions/upload-artifact@v7` and `archive: false` set):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: my-artifact.json # corresponds to the uploaded file name
|
||||||
|
```
|
||||||
|
|
||||||
### Download Artifacts by ID
|
### Download Artifacts by ID
|
||||||
|
|
||||||
The `artifact-ids` input allows downloading artifacts using their unique ID rather than name. This is particularly useful when working with immutable artifacts from `actions/upload-artifact@v4` which assigns a unique ID to each artifact.
|
The `artifact-ids` input allows downloading artifacts using their unique ID rather than name. This is particularly useful when working with immutable artifacts from `actions/upload-artifact@v4+` which assigns a unique ID to each artifact.
|
||||||
|
|
||||||
Download a single artifact by ID to the current working directory (`$GITHUB_WORKSPACE`):
|
Download a single artifact by ID to the current working directory (`$GITHUB_WORKSPACE`):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v5
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
artifact-ids: 12345
|
artifact-ids: 12345
|
||||||
- name: Display structure of downloaded files
|
- name: Display structure of downloaded files
|
||||||
@@ -176,7 +165,7 @@ Download a single artifact by ID to a specific directory:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v5
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
artifact-ids: 12345
|
artifact-ids: 12345
|
||||||
path: your/destination/dir
|
path: your/destination/dir
|
||||||
@@ -190,7 +179,7 @@ Multiple artifacts can be downloaded by providing a comma-separated list of IDs:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v5
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
artifact-ids: 12345,67890
|
artifact-ids: 12345,67890
|
||||||
path: path/to/artifacts
|
path: path/to/artifacts
|
||||||
@@ -206,7 +195,7 @@ If the `name` input parameter is not provided, all artifacts will be downloaded.
|
|||||||
|
|
||||||
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:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
etc/usr/artifacts/
|
etc/usr/artifacts/
|
||||||
Artifact-A/
|
Artifact-A/
|
||||||
... contents of Artifact-A
|
... contents of Artifact-A
|
||||||
@@ -218,7 +207,7 @@ Download all artifacts to the current working directory:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v5
|
- uses: actions/download-artifact@v8
|
||||||
- name: Display structure of downloaded files
|
- name: Display structure of downloaded files
|
||||||
run: ls -R
|
run: ls -R
|
||||||
```
|
```
|
||||||
@@ -227,7 +216,7 @@ Download all artifacts to a specific directory:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v5
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
path: path/to/artifacts
|
path: path/to/artifacts
|
||||||
- name: Display structure of downloaded files
|
- name: Display structure of downloaded files
|
||||||
@@ -238,7 +227,7 @@ To download them to the _same_ directory:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v5
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
path: path/to/artifacts
|
path: path/to/artifacts
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -248,7 +237,7 @@ steps:
|
|||||||
|
|
||||||
Which will result in:
|
Which will result in:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
path/to/artifacts/
|
path/to/artifacts/
|
||||||
... contents of Artifact-A
|
... contents of Artifact-A
|
||||||
... contents of Artifact-B
|
... contents of Artifact-B
|
||||||
@@ -269,7 +258,7 @@ jobs:
|
|||||||
- name: Create a File
|
- name: Create a File
|
||||||
run: echo "hello from ${{ matrix.runs-on }}" > file-${{ matrix.runs-on }}.txt
|
run: echo "hello from ${{ matrix.runs-on }}" > file-${{ matrix.runs-on }}.txt
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: my-artifact-${{ matrix.runs-on }}
|
name: my-artifact-${{ matrix.runs-on }}
|
||||||
path: file-${{ matrix.runs-on }}.txt
|
path: file-${{ matrix.runs-on }}.txt
|
||||||
@@ -278,7 +267,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Download All Artifacts
|
- name: Download All Artifacts
|
||||||
uses: actions/download-artifact@v5
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
path: my-artifact
|
path: my-artifact
|
||||||
pattern: my-artifact-*
|
pattern: my-artifact-*
|
||||||
@@ -288,7 +277,7 @@ jobs:
|
|||||||
|
|
||||||
This results in a directory like so:
|
This results in a directory like so:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
my-artifact/
|
my-artifact/
|
||||||
file-macos-latest.txt
|
file-macos-latest.txt
|
||||||
file-ubuntu-latest.txt
|
file-ubuntu-latest.txt
|
||||||
@@ -301,7 +290,7 @@ It may be useful to download Artifacts from other workflow runs, or even other r
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v5
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: my-other-artifact
|
name: my-other-artifact
|
||||||
github-token: ${{ secrets.GH_PAT }} # token with actions:read permissions on target repo
|
github-token: ${{ secrets.GH_PAT }} # token with actions:read permissions on target repo
|
||||||
@@ -309,21 +298,30 @@ steps:
|
|||||||
run-id: 1234
|
run-id: 1234
|
||||||
```
|
```
|
||||||
|
|
||||||
## Limitations
|
### Maintaining File Permissions
|
||||||
|
|
||||||
### Permission Loss
|
Zipping files will remove file permissions during artifact upload. All directories will have `755` and all files will have `644`. For example, if you make a file executable using `chmod` and then upload that file as a zip file, post-download the file is no longer guaranteed to be set as an executable.
|
||||||
|
|
||||||
File permissions are not maintained during artifact upload. All directories will have `755` and all files will have `644`. 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.
|
If you must preserve permissions, you can `tar` all of your files together before artifact upload and upload it as a single file (using V7+ of `actions/upload-artifact`). Then download the file directly and unpack it manually:
|
||||||
|
|
||||||
If you must preserve permissions, you can `tar` all of your files together before artifact upload. Post download, the `tar` file will maintain file permissions and case sensitivity.
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|
||||||
- name: 'Tar files'
|
- name: 'Tar files'
|
||||||
run: tar -cvf my_files.tar /path/to/my/directory
|
run: tar -cvf my_files.tar /path/to/my/directory
|
||||||
|
|
||||||
- name: 'Upload Artifact'
|
- name: 'Upload Artifact'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: my-artifact
|
|
||||||
path: my_files.tar
|
path: my_files.tar
|
||||||
```
|
archive: false
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
# Later, download the file by name
|
||||||
|
|
||||||
|
- name: 'Download Artifact'
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: my_files.tar
|
||||||
|
|
||||||
|
```
|
||||||
+238
-41
@@ -1,10 +1,8 @@
|
|||||||
import * as core from '@actions/core'
|
import {jest, describe, test, expect, beforeEach} from '@jest/globals'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import artifact, {ArtifactNotFoundError} from '@actions/artifact'
|
|
||||||
import {run} from '../src/download-artifact'
|
|
||||||
import {Inputs} from '../src/constants'
|
|
||||||
|
|
||||||
jest.mock('@actions/github', () => ({
|
// Mock @actions/github before importing modules that use it
|
||||||
|
jest.unstable_mockModule('@actions/github', () => ({
|
||||||
context: {
|
context: {
|
||||||
repo: {
|
repo: {
|
||||||
owner: 'actions',
|
owner: 'actions',
|
||||||
@@ -12,14 +10,46 @@ jest.mock('@actions/github', () => ({
|
|||||||
},
|
},
|
||||||
runId: 123,
|
runId: 123,
|
||||||
serverUrl: 'https://github.com'
|
serverUrl: 'https://github.com'
|
||||||
}
|
},
|
||||||
|
getOctokit: jest.fn()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
jest.mock('@actions/core')
|
// Mock @actions/core
|
||||||
|
jest.unstable_mockModule('@actions/core', () => ({
|
||||||
|
getInput: jest.fn(),
|
||||||
|
getBooleanInput: jest.fn(),
|
||||||
|
setOutput: jest.fn(),
|
||||||
|
setFailed: jest.fn(),
|
||||||
|
setSecret: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
warning: jest.fn(),
|
||||||
|
debug: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
notice: jest.fn(),
|
||||||
|
startGroup: jest.fn(),
|
||||||
|
endGroup: jest.fn(),
|
||||||
|
isDebug: jest.fn(() => false),
|
||||||
|
getState: jest.fn(),
|
||||||
|
saveState: jest.fn(),
|
||||||
|
exportVariable: jest.fn(),
|
||||||
|
addPath: jest.fn(),
|
||||||
|
group: jest.fn((name: string, fn: () => Promise<unknown>) => fn()),
|
||||||
|
toPlatformPath: jest.fn(p => p),
|
||||||
|
toWin32Path: jest.fn(p => p),
|
||||||
|
toPosixPath: jest.fn(p => p)
|
||||||
|
}))
|
||||||
|
|
||||||
/* eslint-disable no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */
|
// Dynamic imports after mocking
|
||||||
const mockInputs = (overrides?: Partial<{[K in Inputs]?: any}>) => {
|
const core = await import('@actions/core')
|
||||||
const inputs = {
|
const artifact = await import('@actions/artifact')
|
||||||
|
const {run} = await import('../src/download-artifact.js')
|
||||||
|
const {Inputs} = await import('../src/constants.js')
|
||||||
|
const {ArtifactNotFoundError} = artifact
|
||||||
|
|
||||||
|
const mockInputs = (
|
||||||
|
overrides?: Partial<{[K in (typeof Inputs)[keyof typeof Inputs]]?: any}>
|
||||||
|
) => {
|
||||||
|
const inputs: Record<string, any> = {
|
||||||
[Inputs.Name]: 'artifact-name',
|
[Inputs.Name]: 'artifact-name',
|
||||||
[Inputs.Path]: '/some/artifact/path',
|
[Inputs.Path]: '/some/artifact/path',
|
||||||
[Inputs.GitHubToken]: 'warn',
|
[Inputs.GitHubToken]: 'warn',
|
||||||
@@ -29,10 +59,14 @@ const mockInputs = (overrides?: Partial<{[K in Inputs]?: any}>) => {
|
|||||||
...overrides
|
...overrides
|
||||||
}
|
}
|
||||||
|
|
||||||
;(core.getInput as jest.Mock).mockImplementation((name: string) => {
|
;(core.getInput as jest.Mock<typeof core.getInput>).mockImplementation(
|
||||||
return inputs[name]
|
(name: string) => {
|
||||||
})
|
return inputs[name]
|
||||||
;(core.getBooleanInput as jest.Mock).mockImplementation((name: string) => {
|
}
|
||||||
|
)
|
||||||
|
;(
|
||||||
|
core.getBooleanInput as jest.Mock<typeof core.getBooleanInput>
|
||||||
|
).mockImplementation((name: string) => {
|
||||||
return inputs[name]
|
return inputs[name]
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -46,13 +80,13 @@ describe('download', () => {
|
|||||||
|
|
||||||
// Mock artifact client methods
|
// Mock artifact client methods
|
||||||
jest
|
jest
|
||||||
.spyOn(artifact, 'listArtifacts')
|
.spyOn(artifact.default, 'listArtifacts')
|
||||||
.mockImplementation(() => Promise.resolve({artifacts: []}))
|
.mockImplementation(() => Promise.resolve({artifacts: []}))
|
||||||
jest.spyOn(artifact, 'getArtifact').mockImplementation(name => {
|
jest.spyOn(artifact.default, 'getArtifact').mockImplementation(name => {
|
||||||
throw new ArtifactNotFoundError(`Artifact '${name}' not found`)
|
throw new ArtifactNotFoundError(`Artifact '${name}' not found`)
|
||||||
})
|
})
|
||||||
jest
|
jest
|
||||||
.spyOn(artifact, 'downloadArtifact')
|
.spyOn(artifact.default, 'downloadArtifact')
|
||||||
.mockImplementation(() => Promise.resolve({digestMismatch: false}))
|
.mockImplementation(() => Promise.resolve({digestMismatch: false}))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -65,12 +99,12 @@ describe('download', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(artifact, 'getArtifact')
|
.spyOn(artifact.default, 'getArtifact')
|
||||||
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
||||||
|
|
||||||
await run()
|
await run()
|
||||||
|
|
||||||
expect(artifact.downloadArtifact).toHaveBeenCalledWith(
|
expect(artifact.default.downloadArtifact).toHaveBeenCalledWith(
|
||||||
mockArtifact.id,
|
mockArtifact.id,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
expectedHash: mockArtifact.digest
|
expectedHash: mockArtifact.digest
|
||||||
@@ -102,12 +136,12 @@ describe('download', () => {
|
|||||||
|
|
||||||
// Set up artifact mock after clearing mocks
|
// Set up artifact mock after clearing mocks
|
||||||
jest
|
jest
|
||||||
.spyOn(artifact, 'listArtifacts')
|
.spyOn(artifact.default, 'listArtifacts')
|
||||||
.mockImplementation(() => Promise.resolve({artifacts: mockArtifacts}))
|
.mockImplementation(() => Promise.resolve({artifacts: mockArtifacts}))
|
||||||
|
|
||||||
// Reset downloadArtifact mock as well
|
// Reset downloadArtifact mock as well
|
||||||
jest
|
jest
|
||||||
.spyOn(artifact, 'downloadArtifact')
|
.spyOn(artifact.default, 'downloadArtifact')
|
||||||
.mockImplementation(() => Promise.resolve({digestMismatch: false}))
|
.mockImplementation(() => Promise.resolve({digestMismatch: false}))
|
||||||
|
|
||||||
await run()
|
await run()
|
||||||
@@ -117,7 +151,7 @@ describe('download', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(core.info).toHaveBeenCalledWith('Total of 2 artifact(s) downloaded')
|
expect(core.info).toHaveBeenCalledWith('Total of 2 artifact(s) downloaded')
|
||||||
expect(artifact.downloadArtifact).toHaveBeenCalledTimes(2)
|
expect(artifact.default.downloadArtifact).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('sets download path output even when no artifacts are found', async () => {
|
test('sets download path output even when no artifacts are found', async () => {
|
||||||
@@ -144,7 +178,7 @@ describe('download', () => {
|
|||||||
]
|
]
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(artifact, 'listArtifacts')
|
.spyOn(artifact.default, 'listArtifacts')
|
||||||
.mockImplementation(() => Promise.resolve({artifacts: mockArtifacts}))
|
.mockImplementation(() => Promise.resolve({artifacts: mockArtifacts}))
|
||||||
|
|
||||||
mockInputs({
|
mockInputs({
|
||||||
@@ -154,8 +188,8 @@ describe('download', () => {
|
|||||||
|
|
||||||
await run()
|
await run()
|
||||||
|
|
||||||
expect(artifact.downloadArtifact).toHaveBeenCalledTimes(1)
|
expect(artifact.default.downloadArtifact).toHaveBeenCalledTimes(1)
|
||||||
expect(artifact.downloadArtifact).toHaveBeenCalledWith(
|
expect(artifact.default.downloadArtifact).toHaveBeenCalledWith(
|
||||||
123,
|
123,
|
||||||
expect.anything()
|
expect.anything()
|
||||||
)
|
)
|
||||||
@@ -172,12 +206,12 @@ describe('download', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(artifact, 'listArtifacts')
|
.spyOn(artifact.default, 'listArtifacts')
|
||||||
.mockImplementation(() => Promise.resolve({artifacts: []}))
|
.mockImplementation(() => Promise.resolve({artifacts: []}))
|
||||||
|
|
||||||
await run()
|
await run()
|
||||||
|
|
||||||
expect(artifact.listArtifacts).toHaveBeenCalledWith(
|
expect(artifact.default.listArtifacts).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
findBy: {
|
findBy: {
|
||||||
token,
|
token,
|
||||||
@@ -200,7 +234,7 @@ describe('download', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('warns when digest validation fails', async () => {
|
test('errors when digest validation fails (default behavior)', async () => {
|
||||||
const mockArtifact = {
|
const mockArtifact = {
|
||||||
id: 123,
|
id: 123,
|
||||||
name: 'corrupted-artifact',
|
name: 'corrupted-artifact',
|
||||||
@@ -209,11 +243,36 @@ describe('download', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(artifact, 'getArtifact')
|
.spyOn(artifact.default, 'getArtifact')
|
||||||
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(artifact, 'downloadArtifact')
|
.spyOn(artifact.default, 'downloadArtifact')
|
||||||
|
.mockImplementation(() => Promise.resolve({digestMismatch: true}))
|
||||||
|
|
||||||
|
await expect(run()).rejects.toThrow(
|
||||||
|
'Digest validation failed for artifact(s): corrupted-artifact'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('warns when digest validation fails with digest-mismatch set to warn', async () => {
|
||||||
|
const mockArtifact = {
|
||||||
|
id: 123,
|
||||||
|
name: 'corrupted-artifact',
|
||||||
|
size: 1024,
|
||||||
|
digest: 'abc123'
|
||||||
|
}
|
||||||
|
|
||||||
|
mockInputs({
|
||||||
|
[Inputs.DigestMismatch]: 'warn'
|
||||||
|
})
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(artifact.default, 'getArtifact')
|
||||||
|
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(artifact.default, 'downloadArtifact')
|
||||||
.mockImplementation(() => Promise.resolve({digestMismatch: true}))
|
.mockImplementation(() => Promise.resolve({digestMismatch: true}))
|
||||||
|
|
||||||
await run()
|
await run()
|
||||||
@@ -223,6 +282,61 @@ describe('download', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('logs info when digest validation fails with digest-mismatch set to info', async () => {
|
||||||
|
const mockArtifact = {
|
||||||
|
id: 123,
|
||||||
|
name: 'corrupted-artifact',
|
||||||
|
size: 1024,
|
||||||
|
digest: 'abc123'
|
||||||
|
}
|
||||||
|
|
||||||
|
mockInputs({
|
||||||
|
[Inputs.DigestMismatch]: 'info'
|
||||||
|
})
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(artifact.default, 'getArtifact')
|
||||||
|
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(artifact.default, 'downloadArtifact')
|
||||||
|
.mockImplementation(() => Promise.resolve({digestMismatch: true}))
|
||||||
|
|
||||||
|
await run()
|
||||||
|
|
||||||
|
expect(core.info).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('digest validation failed')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('silently continues when digest validation fails with digest-mismatch set to ignore', async () => {
|
||||||
|
const mockArtifact = {
|
||||||
|
id: 123,
|
||||||
|
name: 'corrupted-artifact',
|
||||||
|
size: 1024,
|
||||||
|
digest: 'abc123'
|
||||||
|
}
|
||||||
|
|
||||||
|
mockInputs({
|
||||||
|
[Inputs.DigestMismatch]: 'ignore'
|
||||||
|
})
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(artifact.default, 'getArtifact')
|
||||||
|
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(artifact.default, 'downloadArtifact')
|
||||||
|
.mockImplementation(() => Promise.resolve({digestMismatch: true}))
|
||||||
|
|
||||||
|
await run()
|
||||||
|
|
||||||
|
expect(core.warning).not.toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('digest validation failed')
|
||||||
|
)
|
||||||
|
expect(core.info).toHaveBeenCalledWith('Total of 1 artifact(s) downloaded')
|
||||||
|
})
|
||||||
|
|
||||||
test('downloads a single artifact by ID', async () => {
|
test('downloads a single artifact by ID', async () => {
|
||||||
const mockArtifact = {
|
const mockArtifact = {
|
||||||
id: 456,
|
id: 456,
|
||||||
@@ -237,7 +351,7 @@ describe('download', () => {
|
|||||||
[Inputs.ArtifactIds]: '456'
|
[Inputs.ArtifactIds]: '456'
|
||||||
})
|
})
|
||||||
|
|
||||||
jest.spyOn(artifact, 'listArtifacts').mockImplementation(() =>
|
jest.spyOn(artifact.default, 'listArtifacts').mockImplementation(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
artifacts: [mockArtifact]
|
artifacts: [mockArtifact]
|
||||||
})
|
})
|
||||||
@@ -247,8 +361,8 @@ describe('download', () => {
|
|||||||
|
|
||||||
expect(core.info).toHaveBeenCalledWith('Downloading artifacts by ID')
|
expect(core.info).toHaveBeenCalledWith('Downloading artifacts by ID')
|
||||||
expect(core.debug).toHaveBeenCalledWith('Parsed artifact IDs: ["456"]')
|
expect(core.debug).toHaveBeenCalledWith('Parsed artifact IDs: ["456"]')
|
||||||
expect(artifact.downloadArtifact).toHaveBeenCalledTimes(1)
|
expect(artifact.default.downloadArtifact).toHaveBeenCalledTimes(1)
|
||||||
expect(artifact.downloadArtifact).toHaveBeenCalledWith(
|
expect(artifact.default.downloadArtifact).toHaveBeenCalledWith(
|
||||||
456,
|
456,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
expectedHash: mockArtifact.digest
|
expectedHash: mockArtifact.digest
|
||||||
@@ -270,7 +384,7 @@ describe('download', () => {
|
|||||||
[Inputs.ArtifactIds]: '123, 456, 789'
|
[Inputs.ArtifactIds]: '123, 456, 789'
|
||||||
})
|
})
|
||||||
|
|
||||||
jest.spyOn(artifact, 'listArtifacts').mockImplementation(() =>
|
jest.spyOn(artifact.default, 'listArtifacts').mockImplementation(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
artifacts: mockArtifacts
|
artifacts: mockArtifacts
|
||||||
})
|
})
|
||||||
@@ -282,9 +396,9 @@ describe('download', () => {
|
|||||||
expect(core.debug).toHaveBeenCalledWith(
|
expect(core.debug).toHaveBeenCalledWith(
|
||||||
'Parsed artifact IDs: ["123","456","789"]'
|
'Parsed artifact IDs: ["123","456","789"]'
|
||||||
)
|
)
|
||||||
expect(artifact.downloadArtifact).toHaveBeenCalledTimes(3)
|
expect(artifact.default.downloadArtifact).toHaveBeenCalledTimes(3)
|
||||||
mockArtifacts.forEach(mockArtifact => {
|
mockArtifacts.forEach(mockArtifact => {
|
||||||
expect(artifact.downloadArtifact).toHaveBeenCalledWith(
|
expect(artifact.default.downloadArtifact).toHaveBeenCalledWith(
|
||||||
mockArtifact.id,
|
mockArtifact.id,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
expectedHash: mockArtifact.digest
|
expectedHash: mockArtifact.digest
|
||||||
@@ -305,7 +419,7 @@ describe('download', () => {
|
|||||||
[Inputs.ArtifactIds]: '123, 456, 789'
|
[Inputs.ArtifactIds]: '123, 456, 789'
|
||||||
})
|
})
|
||||||
|
|
||||||
jest.spyOn(artifact, 'listArtifacts').mockImplementation(() =>
|
jest.spyOn(artifact.default, 'listArtifacts').mockImplementation(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
artifacts: mockArtifacts
|
artifacts: mockArtifacts
|
||||||
})
|
})
|
||||||
@@ -317,7 +431,7 @@ describe('download', () => {
|
|||||||
'Could not find the following artifact IDs: 456, 789'
|
'Could not find the following artifact IDs: 456, 789'
|
||||||
)
|
)
|
||||||
expect(core.debug).toHaveBeenCalledWith('Found 1 artifacts by ID')
|
expect(core.debug).toHaveBeenCalledWith('Found 1 artifacts by ID')
|
||||||
expect(artifact.downloadArtifact).toHaveBeenCalledTimes(1)
|
expect(artifact.default.downloadArtifact).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('throws error when no artifacts with requested IDs are found', async () => {
|
test('throws error when no artifacts with requested IDs are found', async () => {
|
||||||
@@ -327,7 +441,7 @@ describe('download', () => {
|
|||||||
[Inputs.ArtifactIds]: '123, 456'
|
[Inputs.ArtifactIds]: '123, 456'
|
||||||
})
|
})
|
||||||
|
|
||||||
jest.spyOn(artifact, 'listArtifacts').mockImplementation(() =>
|
jest.spyOn(artifact.default, 'listArtifacts').mockImplementation(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
artifacts: []
|
artifacts: []
|
||||||
})
|
})
|
||||||
@@ -389,7 +503,7 @@ describe('download', () => {
|
|||||||
[Inputs.Path]: testPath
|
[Inputs.Path]: testPath
|
||||||
})
|
})
|
||||||
|
|
||||||
jest.spyOn(artifact, 'listArtifacts').mockImplementation(() =>
|
jest.spyOn(artifact.default, 'listArtifacts').mockImplementation(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
artifacts: [mockArtifact]
|
artifacts: [mockArtifact]
|
||||||
})
|
})
|
||||||
@@ -398,7 +512,7 @@ describe('download', () => {
|
|||||||
await run()
|
await run()
|
||||||
|
|
||||||
// Verify it downloads directly to the specified path (not nested in artifact name subdirectory)
|
// Verify it downloads directly to the specified path (not nested in artifact name subdirectory)
|
||||||
expect(artifact.downloadArtifact).toHaveBeenCalledWith(
|
expect(artifact.default.downloadArtifact).toHaveBeenCalledWith(
|
||||||
456,
|
456,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
path: path.resolve(testPath), // Should be the resolved path directly, not nested
|
path: path.resolve(testPath), // Should be the resolved path directly, not nested
|
||||||
@@ -406,4 +520,87 @@ describe('download', () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('passes skipDecompress option when skip-decompress input is true', async () => {
|
||||||
|
const mockArtifact = {
|
||||||
|
id: 123,
|
||||||
|
name: 'artifact-name',
|
||||||
|
size: 1024,
|
||||||
|
digest: 'abc123'
|
||||||
|
}
|
||||||
|
|
||||||
|
mockInputs({
|
||||||
|
[Inputs.SkipDecompress]: true
|
||||||
|
})
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(artifact.default, 'getArtifact')
|
||||||
|
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
||||||
|
|
||||||
|
await run()
|
||||||
|
|
||||||
|
expect(artifact.default.downloadArtifact).toHaveBeenCalledWith(
|
||||||
|
mockArtifact.id,
|
||||||
|
expect.objectContaining({
|
||||||
|
skipDecompress: true,
|
||||||
|
expectedHash: mockArtifact.digest
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not pass skipDecompress when skip-decompress input is false', async () => {
|
||||||
|
const mockArtifact = {
|
||||||
|
id: 123,
|
||||||
|
name: 'artifact-name',
|
||||||
|
size: 1024,
|
||||||
|
digest: 'abc123'
|
||||||
|
}
|
||||||
|
|
||||||
|
mockInputs({
|
||||||
|
[Inputs.SkipDecompress]: false
|
||||||
|
})
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(artifact.default, 'getArtifact')
|
||||||
|
.mockImplementation(() => Promise.resolve({artifact: mockArtifact}))
|
||||||
|
|
||||||
|
await run()
|
||||||
|
|
||||||
|
expect(artifact.default.downloadArtifact).toHaveBeenCalledWith(
|
||||||
|
mockArtifact.id,
|
||||||
|
expect.objectContaining({
|
||||||
|
skipDecompress: false,
|
||||||
|
expectedHash: mockArtifact.digest
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('passes skipDecompress for multiple artifact downloads', async () => {
|
||||||
|
mockInputs({
|
||||||
|
[Inputs.Name]: '',
|
||||||
|
[Inputs.Pattern]: '',
|
||||||
|
[Inputs.SkipDecompress]: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockArtifacts = [
|
||||||
|
{id: 123, name: 'artifact1', size: 1024, digest: 'abc123'},
|
||||||
|
{id: 456, name: 'artifact2', size: 2048, digest: 'def456'}
|
||||||
|
]
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(artifact.default, 'listArtifacts')
|
||||||
|
.mockImplementation(() => Promise.resolve({artifacts: mockArtifacts}))
|
||||||
|
|
||||||
|
await run()
|
||||||
|
|
||||||
|
expect(artifact.default.downloadArtifact).toHaveBeenCalledTimes(2)
|
||||||
|
expect(artifact.default.downloadArtifact).toHaveBeenCalledWith(
|
||||||
|
123,
|
||||||
|
expect.objectContaining({skipDecompress: true})
|
||||||
|
)
|
||||||
|
expect(artifact.default.downloadArtifact).toHaveBeenCalledWith(
|
||||||
|
456,
|
||||||
|
expect.objectContaining({skipDecompress: true})
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
+10
@@ -35,6 +35,16 @@ inputs:
|
|||||||
If github-token is specified, this is the run that artifacts will be downloaded from.'
|
If github-token is specified, this is the run that artifacts will be downloaded from.'
|
||||||
required: false
|
required: false
|
||||||
default: ${{ github.run_id }}
|
default: ${{ github.run_id }}
|
||||||
|
skip-decompress:
|
||||||
|
description: 'If true, the downloaded artifact will not be automatically extracted/decompressed.
|
||||||
|
This is useful when you want to handle the artifact as-is without extraction.'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
digest-mismatch:
|
||||||
|
description: 'The behavior when a downloaded artifact''s digest does not match the expected digest.
|
||||||
|
Options: ignore, info, warn, error. Default is error which will fail the action.'
|
||||||
|
required: false
|
||||||
|
default: 'error'
|
||||||
outputs:
|
outputs:
|
||||||
download-path:
|
download-path:
|
||||||
description: 'Path of artifact download'
|
description: 'Path of artifact download'
|
||||||
|
|||||||
Vendored
+68502
-66559
File diff suppressed because one or more lines are too long
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import github from 'eslint-plugin-github'
|
||||||
|
import jest from 'eslint-plugin-jest'
|
||||||
|
import prettier from 'eslint-plugin-prettier/recommended'
|
||||||
|
|
||||||
|
const githubConfigs = github.getFlatConfigs()
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
ignores: ['**/node_modules/**', '**/lib/**', '**/dist/**']
|
||||||
|
},
|
||||||
|
githubConfigs.recommended,
|
||||||
|
...githubConfigs.typescript,
|
||||||
|
prettier,
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.eslint.json'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// Prettier
|
||||||
|
'prettier/prettier': ['error', {endOfLine: 'auto'}],
|
||||||
|
|
||||||
|
// Disable rules that conflict with project style
|
||||||
|
'eslint-comments/no-use': 'off',
|
||||||
|
'github/no-then': 'off',
|
||||||
|
'github/filenames-match-regex': 'off',
|
||||||
|
'github/array-foreach': 'off',
|
||||||
|
'import/no-namespace': 'off',
|
||||||
|
'import/no-commonjs': 'off',
|
||||||
|
'import/named': 'off',
|
||||||
|
'import/no-unresolved': 'off',
|
||||||
|
'i18n-text/no-en': 'off',
|
||||||
|
'filenames/match-regex': 'off',
|
||||||
|
'no-shadow': 'off',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'no-undef': 'off',
|
||||||
|
camelcase: 'off',
|
||||||
|
|
||||||
|
// TypeScript rules
|
||||||
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-shadow': 'off',
|
||||||
|
'@typescript-eslint/array-type': 'off',
|
||||||
|
'@typescript-eslint/no-require-imports': 'off'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/__tests__/**/*.ts'],
|
||||||
|
...jest.configs['flat/recommended'],
|
||||||
|
rules: {
|
||||||
|
...jest.configs['flat/recommended'].rules,
|
||||||
|
'jest/expect-expect': 'off',
|
||||||
|
'jest/no-conditional-expect': 'off'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
+15
-3
@@ -1,12 +1,24 @@
|
|||||||
module.exports = {
|
export default {
|
||||||
clearMocks: true,
|
clearMocks: true,
|
||||||
moduleFileExtensions: ['js', 'ts'],
|
moduleFileExtensions: ['js', 'ts'],
|
||||||
roots: ['<rootDir>'],
|
roots: ['<rootDir>'],
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
testMatch: ['**/*.test.ts'],
|
testMatch: ['**/*.test.ts'],
|
||||||
testRunner: 'jest-circus/runner',
|
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.ts$': 'ts-jest'
|
'^.+\\.ts$': [
|
||||||
|
'ts-jest',
|
||||||
|
{
|
||||||
|
useESM: true,
|
||||||
|
diagnostics: {
|
||||||
|
ignoreCodes: [151002]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
extensionsToTreatAsEsm: ['.ts'],
|
||||||
|
transformIgnorePatterns: ['node_modules/(?!(@actions)/)'],
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^(\\.{1,2}/.*)\\.js$': '$1'
|
||||||
},
|
},
|
||||||
verbose: true
|
verbose: true
|
||||||
}
|
}
|
||||||
Generated
+1880
-4376
File diff suppressed because it is too large
Load Diff
+19
-16
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "download-artifact",
|
"name": "download-artifact",
|
||||||
"version": "6.0.0",
|
"version": "8.0.1",
|
||||||
"description": "Download an Actions Artifact from a workflow run",
|
"description": "Download an Actions Artifact from a workflow run",
|
||||||
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24"
|
"node": ">=24"
|
||||||
},
|
},
|
||||||
@@ -13,7 +14,7 @@
|
|||||||
"format": "prettier --write **/*.ts",
|
"format": "prettier --write **/*.ts",
|
||||||
"format-check": "prettier --check **/*.ts",
|
"format-check": "prettier --check **/*.ts",
|
||||||
"lint": "eslint **/*.ts",
|
"lint": "eslint **/*.ts",
|
||||||
"test": "jest"
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -32,22 +33,24 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/actions/download-artifact#readme",
|
"homepage": "https://github.com/actions/download-artifact#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/artifact": "^5.0.0",
|
"@actions/artifact": "^6.2.1",
|
||||||
"@actions/core": "^2.0.0",
|
"@actions/core": "^3.0.0",
|
||||||
"@actions/github": "^6.0.1",
|
"minimatch": "^10.1.1"
|
||||||
"minimatch": "^9.0.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.14",
|
"@actions/github": "^9.0.0",
|
||||||
"@types/node": "^24.1.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
"@types/node": "^25.1.0",
|
||||||
"@vercel/ncc": "^0.33.4",
|
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
||||||
"concurrently": "^5.2.0",
|
"@typescript-eslint/parser": "^8.54.0",
|
||||||
"eslint": "^8.55.0",
|
"@vercel/ncc": "^0.38.4",
|
||||||
"eslint-plugin-github": "^4.10.1",
|
"concurrently": "^9.2.1",
|
||||||
"eslint-plugin-prettier": "^5.0.1",
|
"eslint": "^9.39.2",
|
||||||
"jest": "^29.7.0",
|
"eslint-plugin-github": "^6.0.0",
|
||||||
"prettier": "^3.1.1",
|
"eslint-plugin-jest": "^29.12.1",
|
||||||
|
"eslint-plugin-prettier": "^5.5.5",
|
||||||
|
"jest": "^30.2.0",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
"ts-jest": "^29.2.6",
|
"ts-jest": "^29.2.6",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
|
|||||||
+10
-1
@@ -6,7 +6,16 @@ export enum Inputs {
|
|||||||
RunID = 'run-id',
|
RunID = 'run-id',
|
||||||
Pattern = 'pattern',
|
Pattern = 'pattern',
|
||||||
MergeMultiple = 'merge-multiple',
|
MergeMultiple = 'merge-multiple',
|
||||||
ArtifactIds = 'artifact-ids'
|
ArtifactIds = 'artifact-ids',
|
||||||
|
SkipDecompress = 'skip-decompress',
|
||||||
|
DigestMismatch = 'digest-mismatch'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DigestMismatchBehavior {
|
||||||
|
Ignore = 'ignore',
|
||||||
|
Info = 'info',
|
||||||
|
Warn = 'warn',
|
||||||
|
Error = 'error'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Outputs {
|
export enum Outputs {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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 {Minimatch} from 'minimatch'
|
||||||
import {Inputs, Outputs} from './constants'
|
import {Inputs, Outputs, DigestMismatchBehavior} from './constants.js'
|
||||||
|
|
||||||
const PARALLEL_DOWNLOADS = 5
|
const PARALLEL_DOWNLOADS = 5
|
||||||
|
|
||||||
@@ -26,7 +26,20 @@ export async function run(): Promise<void> {
|
|||||||
mergeMultiple: core.getBooleanInput(Inputs.MergeMultiple, {
|
mergeMultiple: core.getBooleanInput(Inputs.MergeMultiple, {
|
||||||
required: false
|
required: false
|
||||||
}),
|
}),
|
||||||
artifactIds: core.getInput(Inputs.ArtifactIds, {required: false})
|
artifactIds: core.getInput(Inputs.ArtifactIds, {required: false}),
|
||||||
|
skipDecompress: core.getBooleanInput(Inputs.SkipDecompress, {
|
||||||
|
required: false
|
||||||
|
}),
|
||||||
|
digestMismatch: (core.getInput(Inputs.DigestMismatch, {required: false}) ||
|
||||||
|
DigestMismatchBehavior.Error) as DigestMismatchBehavior
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate digest-mismatch input
|
||||||
|
const validBehaviors = Object.values(DigestMismatchBehavior)
|
||||||
|
if (!validBehaviors.includes(inputs.digestMismatch)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid value for 'digest-mismatch': '${inputs.digestMismatch}'. Valid options are: ${validBehaviors.join(', ')}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inputs.path) {
|
if (!inputs.path) {
|
||||||
@@ -179,11 +192,14 @@ export async function run(): Promise<void> {
|
|||||||
artifacts.length === 1
|
artifacts.length === 1
|
||||||
? resolvedPath
|
? resolvedPath
|
||||||
: path.join(resolvedPath, artifact.name),
|
: path.join(resolvedPath, artifact.name),
|
||||||
expectedHash: artifact.digest
|
expectedHash: artifact.digest,
|
||||||
|
skipDecompress: inputs.skipDecompress
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const chunkedPromises = chunk(downloadPromises, PARALLEL_DOWNLOADS)
|
const chunkedPromises = chunk(downloadPromises, PARALLEL_DOWNLOADS)
|
||||||
|
const digestMismatches: string[] = []
|
||||||
|
|
||||||
for (const chunk of chunkedPromises) {
|
for (const chunk of chunkedPromises) {
|
||||||
const chunkPromises = chunk.map(item => item.promise)
|
const chunkPromises = chunk.map(item => item.promise)
|
||||||
const results = await Promise.all(chunkPromises)
|
const results = await Promise.all(chunkPromises)
|
||||||
@@ -193,12 +209,38 @@ export async function run(): Promise<void> {
|
|||||||
const artifactName = chunk[i].name
|
const artifactName = chunk[i].name
|
||||||
|
|
||||||
if (outcome.digestMismatch) {
|
if (outcome.digestMismatch) {
|
||||||
core.warning(
|
digestMismatches.push(artifactName)
|
||||||
`Artifact '${artifactName}' digest validation failed. Please verify the integrity of the artifact.`
|
const message = `Artifact '${artifactName}' digest validation failed. Please verify the integrity of the artifact.`
|
||||||
)
|
|
||||||
|
switch (inputs.digestMismatch) {
|
||||||
|
case DigestMismatchBehavior.Ignore:
|
||||||
|
// Do nothing
|
||||||
|
break
|
||||||
|
case DigestMismatchBehavior.Info:
|
||||||
|
core.info(message)
|
||||||
|
break
|
||||||
|
case DigestMismatchBehavior.Warn:
|
||||||
|
core.warning(message)
|
||||||
|
break
|
||||||
|
case DigestMismatchBehavior.Error:
|
||||||
|
// Collect all errors and fail at the end
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there were digest mismatches and behavior is 'error', fail the action
|
||||||
|
if (
|
||||||
|
digestMismatches.length > 0 &&
|
||||||
|
inputs.digestMismatch === DigestMismatchBehavior.Error
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Digest validation failed for artifact(s): ${digestMismatches.join(', ')}. ` +
|
||||||
|
`Use 'digest-mismatch: warn' to continue on mismatch.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
core.info(`Total of ${artifacts.length} artifact(s) downloaded`)
|
core.info(`Total of ${artifacts.length} artifact(s) downloaded`)
|
||||||
core.setOutput(Outputs.DownloadPath, resolvedPath)
|
core.setOutput(Outputs.DownloadPath, resolvedPath)
|
||||||
core.info('Download artifact has finished successfully')
|
core.info('Download artifact has finished successfully')
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "."
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "__tests__/**/*.ts", "*.ts"],
|
||||||
|
"exclude": ["node_modules", "lib", "dist"]
|
||||||
|
}
|
||||||
+3
-3
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es6",
|
"target": "ES2022",
|
||||||
"module": "commonjs",
|
"module": "NodeNext",
|
||||||
"outDir": "./lib",
|
"outDir": "./lib",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "NodeNext",
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "**/*.test.ts", "jest.config.ts", "__tests__"]
|
"exclude": ["node_modules", "**/*.test.ts", "jest.config.ts", "__tests__"]
|
||||||
|
|||||||
Reference in New Issue
Block a user