Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf5677ba88 | |||
| 830c827471 |
@@ -57,8 +57,3 @@ This will ask you some questions about the new package. Start with `0.0.0` as th
|
||||
```
|
||||
|
||||
3. Start developing 😄.
|
||||
|
||||
## Releasing Packages
|
||||
|
||||
For information on how packages are published to npm, including workflow inputs, dist-tags, and safety guards, see the [release documentation](../docs/release.md).
|
||||
|
||||
|
||||
@@ -71,143 +71,10 @@ jobs:
|
||||
} catch (err) {
|
||||
console.log('Successfully blocked second artifact upload')
|
||||
}
|
||||
|
||||
upload-single-file:
|
||||
name: Upload Single File (no zip)
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
runs-on: [ubuntu-latest, windows-latest, macos-latest]
|
||||
fail-fast: false
|
||||
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set Node.js 24.x
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 24.x
|
||||
|
||||
- name: Install root npm packages
|
||||
run: npm ci
|
||||
|
||||
- name: Compile artifact package
|
||||
run: |
|
||||
npm ci
|
||||
npm run tsc
|
||||
working-directory: packages/artifact
|
||||
|
||||
- name: Create file that will be uploaded
|
||||
run: |
|
||||
echo -n 'hello from single file upload' > single-file-${{ matrix.runs-on }}.txt
|
||||
|
||||
- name: Upload Single File Artifact (skipArchive)
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const {default: artifact} = require('./packages/artifact/lib/artifact')
|
||||
|
||||
const artifactName = 'my-single-file-${{ matrix.runs-on }}'
|
||||
console.log('artifactName: ' + artifactName)
|
||||
|
||||
const uploadResult = await artifact.uploadArtifact(
|
||||
artifactName,
|
||||
['single-file-${{ matrix.runs-on }}.txt'],
|
||||
'./',
|
||||
{skipArchive: true}
|
||||
)
|
||||
console.log(uploadResult)
|
||||
|
||||
const size = uploadResult.size
|
||||
const id = uploadResult.id
|
||||
|
||||
if (!id) {
|
||||
throw new Error('Artifact ID is missing from upload result')
|
||||
}
|
||||
|
||||
console.log(`Successfully uploaded single file artifact ${id}`)
|
||||
|
||||
upload-html-file:
|
||||
name: Upload HTML File (no zip)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set Node.js 24.x
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 24.x
|
||||
|
||||
- name: Install root npm packages
|
||||
run: npm ci
|
||||
|
||||
- name: Compile artifact package
|
||||
run: |
|
||||
npm ci
|
||||
npm run tsc
|
||||
working-directory: packages/artifact
|
||||
|
||||
- name: Create HTML file
|
||||
run: |
|
||||
cat > report.html << 'EOF'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Artifact Upload Test Report</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; color: #24292f; }
|
||||
h1 { border-bottom: 1px solid #d0d7de; padding-bottom: 8px; }
|
||||
.success { color: #1a7f37; }
|
||||
.info { background: #ddf4ff; border: 1px solid #54aeff; border-radius: 6px; padding: 12px 16px; margin: 16px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 16px 0; }
|
||||
th, td { border: 1px solid #d0d7de; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f6f8fa; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Artifact Upload Test Report</h1>
|
||||
<div class="info">
|
||||
<strong>This HTML file was uploaded as a single un-zipped artifact.</strong>
|
||||
If you can see this in the browser, the feature is working correctly!
|
||||
</div>
|
||||
<table>
|
||||
<tr><th>Property</th><th>Value</th></tr>
|
||||
<tr><td>Upload method</td><td><code>skipArchive: true</code></td></tr>
|
||||
<tr><td>Content-Type</td><td><code>text/html</code></td></tr>
|
||||
<tr><td>File</td><td><code>report.html</code></td></tr>
|
||||
</table>
|
||||
<p class="success">✔ Single file upload is working!</p>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
|
||||
- name: Upload HTML Artifact (skipArchive)
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const {default: artifact} = require('./packages/artifact/lib/artifact')
|
||||
|
||||
const uploadResult = await artifact.uploadArtifact(
|
||||
'test-report',
|
||||
['report.html'],
|
||||
'./',
|
||||
{skipArchive: true}
|
||||
)
|
||||
console.log(uploadResult)
|
||||
console.log(`Successfully uploaded HTML artifact ${uploadResult.id}`)
|
||||
console.log('This artifact is intentionally kept for manual browser verification')
|
||||
|
||||
verify:
|
||||
name: Verify and Delete
|
||||
runs-on: ubuntu-latest
|
||||
needs: [upload, upload-single-file, upload-html-file]
|
||||
needs: [upload]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
@@ -297,71 +164,6 @@ jobs:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- name: Download and Verify Single File Artifacts
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const {default: artifactClient} = require('./packages/artifact/lib/artifact')
|
||||
|
||||
const {readFile} = require('fs/promises')
|
||||
const path = require('path')
|
||||
|
||||
const findBy = {
|
||||
repositoryOwner: process.env.GITHUB_REPOSITORY.split('/')[0],
|
||||
repositoryName: process.env.GITHUB_REPOSITORY.split('/')[1],
|
||||
token: '${{ secrets.GITHUB_TOKEN }}',
|
||||
workflowRunId: process.env.GITHUB_RUN_ID
|
||||
}
|
||||
|
||||
const listResult = await artifactClient.listArtifacts({latest: true, findBy})
|
||||
const expectedSingleFiles = [
|
||||
'single-file-ubuntu-latest.txt',
|
||||
'single-file-windows-latest.txt',
|
||||
'single-file-macos-latest.txt'
|
||||
]
|
||||
|
||||
// Single file artifacts are named after the file basename
|
||||
const singleFileArtifacts = listResult.artifacts.filter(a =>
|
||||
expectedSingleFiles.includes(a.name)
|
||||
)
|
||||
|
||||
console.log('Found single file artifacts:', singleFileArtifacts.length)
|
||||
|
||||
if (singleFileArtifacts.length !== 3) {
|
||||
console.log('Unexpected single file artifacts:', singleFileArtifacts)
|
||||
throw new Error(
|
||||
`Expected 3 single-file artifacts but found ${singleFileArtifacts.length}`
|
||||
)
|
||||
}
|
||||
|
||||
for (const artifact of singleFileArtifacts) {
|
||||
const downloadDir = `single-file-download-${artifact.id}`
|
||||
const {downloadPath} = await artifactClient.downloadArtifact(artifact.id, {
|
||||
path: downloadDir,
|
||||
findBy
|
||||
})
|
||||
|
||||
console.log('Downloaded single file artifact to:', downloadPath)
|
||||
|
||||
const filePath = path.join(
|
||||
process.env.GITHUB_WORKSPACE,
|
||||
downloadPath,
|
||||
artifact.name
|
||||
)
|
||||
|
||||
console.log('Checking file:', filePath)
|
||||
|
||||
const content = await readFile(filePath, 'utf8')
|
||||
if (content.trim() !== 'hello from single file upload') {
|
||||
throw new Error(
|
||||
`Expected single file to contain 'hello from single file upload' but found '${content}'`
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`Successfully verified single file artifact ${artifact.id}`)
|
||||
}
|
||||
|
||||
- name: Delete Artifacts
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
@@ -371,19 +173,11 @@ jobs:
|
||||
const artifactsToDelete = [
|
||||
'my-artifact-ubuntu-latest',
|
||||
'my-artifact-windows-latest',
|
||||
'my-artifact-macos-latest',
|
||||
'single-file-ubuntu-latest.txt',
|
||||
'single-file-windows-latest.txt',
|
||||
'single-file-macos-latest.txt'
|
||||
'my-artifact-macos-latest'
|
||||
]
|
||||
|
||||
for (const artifactName of artifactsToDelete) {
|
||||
try {
|
||||
const {id} = await artifactClient.deleteArtifact(artifactName)
|
||||
console.log(`Deleted artifact '${artifactName}' (ID: ${id})`)
|
||||
} catch (err) {
|
||||
console.log(`Could not delete artifact '${artifactName}': ${err.message}`)
|
||||
}
|
||||
const {id} = await artifactClient.deleteArtifact(artifactName)
|
||||
}
|
||||
|
||||
const {artifacts} = await artifactClient.listArtifacts({latest: true})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: Publish NPM
|
||||
|
||||
run-name: Publish NPM - ${{ inputs.package }} from ${{ inputs.branch }}
|
||||
run-name: Publish NPM - ${{ github.event.inputs.package }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -20,37 +20,21 @@ on:
|
||||
- http-client
|
||||
- io
|
||||
- tool-cache
|
||||
branch:
|
||||
type: string
|
||||
required: false
|
||||
default: 'main'
|
||||
description: 'Branch to release from'
|
||||
npm-tag:
|
||||
type: string
|
||||
required: false
|
||||
default: 'latest'
|
||||
description: 'npm dist-tag for the release. Use "latest" for main branch releases. For non-main branches, use a non-semver tag like "v1-longlived". Semver values (e.g. "5.0.0") are not valid dist-tags and will be rejected by npm.'
|
||||
test-all:
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
description: 'Run tests for all packages instead of only the package being published'
|
||||
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: macos-latest-large
|
||||
|
||||
steps:
|
||||
- name: setup repo
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.branch }}
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: verify package exists
|
||||
run: ls packages/${{ github.event.inputs.package }}
|
||||
|
||||
- name: Set Node.js 24.x
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 24.x
|
||||
|
||||
@@ -64,25 +48,20 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: test
|
||||
run: |
|
||||
if [ "${{ inputs.test-all }}" = "true" ]; then
|
||||
npm run test
|
||||
else
|
||||
npm run test -- --testPathPattern="packages/${{ inputs.package }}"
|
||||
fi
|
||||
run: npm run test
|
||||
|
||||
- name: pack
|
||||
run: npm pack
|
||||
working-directory: packages/${{ github.event.inputs.package }}
|
||||
|
||||
- name: upload artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ github.event.inputs.package }}
|
||||
path: packages/${{ github.event.inputs.package }}/*.tgz
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-slim
|
||||
runs-on: macos-latest-large
|
||||
needs: test
|
||||
environment: npm-publish
|
||||
permissions:
|
||||
@@ -91,35 +70,29 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set Node.js 24.x
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 24.x
|
||||
|
||||
- name: download artifact
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ inputs.package }}
|
||||
|
||||
- name: guard against publishing latest from non-main branch
|
||||
if: inputs.branch != 'main' && inputs.npm-tag == 'latest'
|
||||
run: |
|
||||
echo "::error::Publishing with the 'latest' dist-tag from a non-main branch ('${{ inputs.branch }}') is not allowed. Use the package major version as the tag (e.g. v5)."
|
||||
exit 1
|
||||
name: ${{ github.event.inputs.package }}
|
||||
|
||||
- name: publish
|
||||
run: npm publish --provenance --tag "${{ inputs.npm-tag }}" *.tgz
|
||||
run: npm publish --provenance *.tgz
|
||||
|
||||
- name: notify slack on failure
|
||||
if: failure()
|
||||
run: |
|
||||
curl -X POST -H 'Content-type: application/json' --data '{"text":":pb__failed: Failed to publish a new version of ${{ inputs.package }} from ${{ inputs.branch }} (tag: ${{ inputs.npm-tag }})"}' $SLACK_WEBHOOK
|
||||
curl -X POST -H 'Content-type: application/json' --data '{"text":":pb__failed: Failed to publish a new version of ${{ github.event.inputs.package }}"}' $SLACK_WEBHOOK
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK }}
|
||||
|
||||
- name: notify slack on success
|
||||
if: success()
|
||||
run: |
|
||||
curl -X POST -H 'Content-type: application/json' --data '{"text":":dance: Successfully published a new version of ${{ inputs.package }} from ${{ inputs.branch }} (tag: ${{ inputs.npm-tag }})"}' $SLACK_WEBHOOK
|
||||
curl -X POST -H 'Content-type: application/json' --data '{"text":":dance: Successfully published a new version of ${{ github.event.inputs.package }}"}' $SLACK_WEBHOOK
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK }}
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 170 KiB |
@@ -1,82 +0,0 @@
|
||||
# Releasing Packages
|
||||
|
||||
Packages are published to npm via the **Publish NPM** workflow ([`.github/workflows/releases.yml`](../.github/workflows/releases.yml)). The workflow is triggered manually through `workflow_dispatch` from the GitHub Actions UI.
|
||||
|
||||
## How it works
|
||||
|
||||
The workflow has two jobs:
|
||||
|
||||
1. **test** — Checks out the specified branch, installs dependencies, bootstraps the monorepo, builds all packages, runs tests, then packs the target package into a `.tgz` archive and uploads it as a workflow artifact.
|
||||
2. **publish** — Downloads the packed artifact and publishes it to npm with `--provenance` (OIDC-based). Sends a Slack notification on success or failure. Requires the `npm-publish` environment.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Type | Required | Default | Description |
|
||||
|---|---|---|---|---|
|
||||
| `package` | choice | **yes** | — | Which package to release. One of: `artifact`, `attest`, `cache`, `core`, `exec`, `github`, `glob`, `http-client`, `io`, `tool-cache`. |
|
||||
| `branch` | string | no | `main` | The branch to check out and release from. |
|
||||
| `npm-tag` | string | no | `latest` | The npm dist-tag to publish under. See [Dist-tags](#dist-tags) below. |
|
||||
| `test-all` | boolean | no | `false` | When `false`, only tests for the selected package are run. Set to `true` to run the full test suite across all packages. |
|
||||
|
||||
## Dist-tags
|
||||
|
||||
npm dist-tags control which version users get when they `npm install @actions/<package>` (or `@<tag>`).
|
||||
|
||||
> **Important:** npm dist-tags **cannot** be valid semver strings. Values like `5.0.0` or `1.2.3` will be rejected by npm. Use a descriptive name instead.
|
||||
|
||||
- **`latest`** — The default tag. This is what users get with a plain `npm install`. Should only be used for releases from `main`.
|
||||
- **Custom tags** (e.g. `v1-longlived`) — Used for releases from long-lived or experimental branches.
|
||||
|
||||
Examples of **valid** dist-tags: `latest`, `next`, `beta`, `v1-longlived`
|
||||
Examples of **invalid** dist-tags: `5.0.0`, `1.2.3`, `6.0.0-rc.1` (these are semver and will be rejected)
|
||||
|
||||
|  |
|
||||
|---|
|
||||
| npm distribution tags |
|
||||
|
||||
### Safety guard
|
||||
|
||||
The workflow **blocks** publishing with the `latest` dist-tag from any branch other than `main`. This prevents accidentally overwriting `latest` with a version from an older or experimental branch. If you're releasing from a non-main branch, use the package's major version as the tag (e.g. `v5`).
|
||||
|
||||
## Examples
|
||||
|
||||
### Standard release from main
|
||||
|
||||
Use default inputs — just pick the package:
|
||||
|
||||
| Input | Value |
|
||||
|---|---|
|
||||
| `package` | `core` |
|
||||
| `branch` | `main` (default) |
|
||||
| `npm-tag` | `latest` (default) |
|
||||
| `test-all` | `false` (default) |
|
||||
|
||||
This publishes the version in `packages/core/package.json` on `main` as `@actions/core@latest`.
|
||||
|
||||
### Patch release from a long-lived branch
|
||||
|
||||
| Input | Value |
|
||||
|---|---|
|
||||
| `package` | `artifact` |
|
||||
| `branch` | `releases/v5` |
|
||||
| `npm-tag` | `v5` |
|
||||
| `test-all` | `false` (default) |
|
||||
|
||||
This publishes the version in `packages/artifact/package.json` on the `releases/v5` branch under the `v5` dist-tag. The `latest` tag remains untouched.
|
||||
|
||||
### Release with full test suite
|
||||
|
||||
| Input | Value |
|
||||
|---|---|
|
||||
| `package` | `cache` |
|
||||
| `branch` | `main` (default) |
|
||||
| `npm-tag` | `latest` (default) |
|
||||
| `test-all` | `true` |
|
||||
|
||||
Same as a standard release, but runs tests for all packages before publishing.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- You must have permission to trigger workflows on this repository.
|
||||
- The `npm-publish` environment must be configured with npm credentials and OIDC.
|
||||
- The `SLACK` secret must be set for Slack notifications to work.
|
||||
Generated
+121
-110
@@ -9,19 +9,19 @@
|
||||
"@types/jest": "^29.5.4",
|
||||
"@types/node": "^24.1.0",
|
||||
"@types/signale": "^1.4.1",
|
||||
"concurrently": "^6.1.0",
|
||||
"concurrently": "^9.0.0",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.9.0",
|
||||
"eslint-plugin-github": "^4.9.2",
|
||||
"eslint-plugin-jest": "^27.2.3",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-prettier": "^5.5.0",
|
||||
"flow-bin": "^0.115.0",
|
||||
"jest": "^29.6.4",
|
||||
"lerna": "^6.4.1",
|
||||
"nx": "16.6.0",
|
||||
"prettier": "^3.0.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.2.2"
|
||||
"prettier": "^3.8.0",
|
||||
"ts-jest": "^29.4.0",
|
||||
"typescript": "^5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -465,16 +465,6 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||
@@ -1706,16 +1696,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@lerna/legacy-package-management/node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lerna/legacy-package-management/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -4322,15 +4302,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.1.tgz",
|
||||
"integrity": "sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==",
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
@@ -5221,26 +5201,90 @@
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.5.1.tgz",
|
||||
"integrity": "sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag==",
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
|
||||
"integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
"date-fns": "^2.16.1",
|
||||
"lodash": "^4.17.21",
|
||||
"rxjs": "^6.6.3",
|
||||
"spawn-command": "^0.0.2-1",
|
||||
"supports-color": "^8.1.0",
|
||||
"tree-kill": "^1.2.2",
|
||||
"yargs": "^16.2.0"
|
||||
"chalk": "4.1.2",
|
||||
"rxjs": "7.8.2",
|
||||
"shell-quote": "1.8.3",
|
||||
"supports-color": "8.1.1",
|
||||
"tree-kill": "1.2.2",
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"concurrently": "bin/concurrently.js"
|
||||
"conc": "dist/bin/concurrently.js",
|
||||
"concurrently": "dist/bin/concurrently.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/config-chain": {
|
||||
@@ -5613,23 +5657,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/date-fns"
|
||||
}
|
||||
},
|
||||
"node_modules/dateformat": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz",
|
||||
@@ -7215,9 +7242,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -7235,9 +7262,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -8353,16 +8380,6 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inquirer/node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/internal-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||
@@ -13278,9 +13295,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -13417,9 +13434,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -13570,14 +13587,11 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pure-rand": {
|
||||
"version": "6.1.0",
|
||||
@@ -14206,25 +14220,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "6.6.7",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
|
||||
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=2.0.0"
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/safe-array-concat": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
|
||||
@@ -14413,6 +14417,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
@@ -14706,12 +14723,6 @@
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/spawn-command": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
|
||||
"integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/spdx-correct": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
|
||||
|
||||
+5
-5
@@ -19,19 +19,19 @@
|
||||
"@types/jest": "^29.5.4",
|
||||
"@types/node": "^24.1.0",
|
||||
"@types/signale": "^1.4.1",
|
||||
"concurrently": "^6.1.0",
|
||||
"concurrently": "^9.0.0",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.9.0",
|
||||
"eslint-plugin-github": "^4.9.2",
|
||||
"eslint-plugin-jest": "^27.2.3",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-prettier": "^5.5.0",
|
||||
"flow-bin": "^0.115.0",
|
||||
"jest": "^29.6.4",
|
||||
"lerna": "^6.4.1",
|
||||
"nx": "16.6.0",
|
||||
"prettier": "^3.0.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.2.2"
|
||||
"prettier": "^3.8.0",
|
||||
"ts-jest": "^29.4.0",
|
||||
"typescript": "^5.9.0"
|
||||
},
|
||||
"overrides": {
|
||||
"semver": "^7.6.0",
|
||||
|
||||
@@ -1,22 +1,5 @@
|
||||
# @actions/artifact Releases
|
||||
|
||||
## 6.2.1
|
||||
|
||||
- Support the RFC 5987 `filename*` field in the `content-disposition` header. This allows us to correctly download files and artifacts with Chinese/Japanese/Korean (among other) characters in their name.
|
||||
|
||||
## 6.2.0
|
||||
|
||||
- Support uploading single un-archived files (not zipped). Direct uploads are only supported for artifacts version 7+ (based on the major version of `actions/upload-artifact`). Callers must pass the `skipArchive` option to `uploadArtifact`. Only single files can be uploaded at a time right now. Default behavior should remain unchanged if `skipArchive = false`. When `skipArchive = true`, the name of the file is used as the name of the artifact for consistency with the downloads: you upload `artifact.txt`, you download `artifact.txt`.
|
||||
|
||||
## 6.1.0
|
||||
|
||||
- Support downloading non-zip artifacts. Zipped artifacts will be decompressed automatically (with an optional override). Un-zipped artifacts will be downloaded as-is.
|
||||
|
||||
## 6.0.0
|
||||
|
||||
- **Breaking change**: Package is now ESM-only
|
||||
- CommonJS consumers must use dynamic `import()` instead of `require()`
|
||||
|
||||
## 5.0.3
|
||||
|
||||
- Bump `@actions/http-client` to `3.0.2`
|
||||
|
||||
@@ -104,7 +104,6 @@ const cleanup = async (): Promise<void> => {
|
||||
const mockGetArtifactSuccess = jest.fn(() => {
|
||||
const message = new http.IncomingMessage(new net.Socket())
|
||||
message.statusCode = 200
|
||||
message.headers['content-type'] = 'application/zip'
|
||||
message.push(fs.readFileSync(fixtures.exampleArtifact.path))
|
||||
message.push(null)
|
||||
return {
|
||||
@@ -115,7 +114,6 @@ const mockGetArtifactSuccess = jest.fn(() => {
|
||||
const mockGetArtifactHung = jest.fn(() => {
|
||||
const message = new http.IncomingMessage(new net.Socket())
|
||||
message.statusCode = 200
|
||||
message.headers['content-type'] = 'application/zip'
|
||||
// Don't push any data or call push(null) to end the stream
|
||||
// This creates a stream that hangs and never completes
|
||||
return {
|
||||
@@ -136,7 +134,6 @@ const mockGetArtifactFailure = jest.fn(() => {
|
||||
const mockGetArtifactMalicious = jest.fn(() => {
|
||||
const message = new http.IncomingMessage(new net.Socket())
|
||||
message.statusCode = 200
|
||||
message.headers['content-type'] = 'application/zip'
|
||||
message.push(fs.readFileSync(path.join(__dirname, 'fixtures', 'evil.zip'))) // evil.zip contains files that are formatted x/../../etc/hosts
|
||||
message.push(null)
|
||||
return {
|
||||
@@ -622,17 +619,10 @@ describe('download-artifact', () => {
|
||||
...fixtures.backendIds,
|
||||
name: fixtures.artifactName
|
||||
})
|
||||
}, 38000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('streamExtractExternal', () => {
|
||||
beforeEach(async () => {
|
||||
await setup()
|
||||
// Create workspace directory for streamExtractExternal tests
|
||||
await fs.promises.mkdir(fixtures.workspaceDir, {recursive: true})
|
||||
})
|
||||
afterEach(cleanup)
|
||||
|
||||
it('should fail if the timeout is exceeded', async () => {
|
||||
const mockSlowGetArtifact = jest.fn(mockGetArtifactHung)
|
||||
|
||||
@@ -651,451 +641,12 @@ describe('download-artifact', () => {
|
||||
{timeout: 2}
|
||||
)
|
||||
expect(true).toBe(false) // should not be called
|
||||
} catch (error: unknown) {
|
||||
const e = error as Error
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(Error)
|
||||
expect(e.message).toContain('did not respond in 2ms')
|
||||
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
|
||||
expect(mockSlowGetArtifact).toHaveBeenCalledTimes(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('should extract zip file when content-type is application/zip', async () => {
|
||||
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
|
||||
() => {
|
||||
return {
|
||||
get: mockGetArtifactSuccess
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await streamExtractExternal(
|
||||
fixtures.blobStorageUrl,
|
||||
fixtures.workspaceDir
|
||||
)
|
||||
|
||||
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
|
||||
// Verify files were extracted (not saved as a single file)
|
||||
await expectExtractedArchive(fixtures.workspaceDir)
|
||||
})
|
||||
|
||||
it('should save raw file without extracting when content-type is not a zip', async () => {
|
||||
const rawFileContent = 'This is a raw text file, not a zip'
|
||||
const rawFileName = 'my-artifact.txt'
|
||||
|
||||
const mockGetRawFile = jest.fn(() => {
|
||||
const message = new http.IncomingMessage(new net.Socket())
|
||||
message.statusCode = 200
|
||||
message.headers['content-type'] = 'text/plain'
|
||||
message.headers['content-disposition'] =
|
||||
`attachment; filename="${rawFileName}"`
|
||||
message.push(Buffer.from(rawFileContent))
|
||||
message.push(null)
|
||||
return {
|
||||
message
|
||||
}
|
||||
})
|
||||
|
||||
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
|
||||
() => {
|
||||
return {
|
||||
get: mockGetRawFile
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await streamExtractExternal(
|
||||
fixtures.blobStorageUrl,
|
||||
fixtures.workspaceDir
|
||||
)
|
||||
|
||||
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
|
||||
// Verify file was saved as-is, not extracted
|
||||
const savedFilePath = path.join(fixtures.workspaceDir, rawFileName)
|
||||
expect(fs.existsSync(savedFilePath)).toBe(true)
|
||||
expect(fs.readFileSync(savedFilePath, 'utf8')).toBe(rawFileContent)
|
||||
})
|
||||
|
||||
it('should save raw file with default name when content-disposition is missing', async () => {
|
||||
const rawFileContent = 'Binary content here'
|
||||
|
||||
const mockGetRawFileNoDisposition = jest.fn(() => {
|
||||
const message = new http.IncomingMessage(new net.Socket())
|
||||
message.statusCode = 200
|
||||
message.headers['content-type'] = 'application/octet-stream'
|
||||
// No content-disposition header
|
||||
message.push(Buffer.from(rawFileContent))
|
||||
message.push(null)
|
||||
return {
|
||||
message
|
||||
}
|
||||
})
|
||||
|
||||
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
|
||||
() => {
|
||||
return {
|
||||
get: mockGetRawFileNoDisposition
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await streamExtractExternal(
|
||||
fixtures.blobStorageUrl,
|
||||
fixtures.workspaceDir
|
||||
)
|
||||
|
||||
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
|
||||
// Verify file was saved with default name 'artifact'
|
||||
const savedFilePath = path.join(fixtures.workspaceDir, 'artifact')
|
||||
expect(fs.existsSync(savedFilePath)).toBe(true)
|
||||
expect(fs.readFileSync(savedFilePath, 'utf8')).toBe(rawFileContent)
|
||||
})
|
||||
|
||||
it('should not attempt to unzip when content-type is image/png', async () => {
|
||||
const pngFileName = 'screenshot.png'
|
||||
// Simple PNG header bytes for testing
|
||||
const pngContent = Buffer.from([
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a
|
||||
])
|
||||
|
||||
const mockGetPngFile = jest.fn(() => {
|
||||
const message = new http.IncomingMessage(new net.Socket())
|
||||
message.statusCode = 200
|
||||
message.headers['content-type'] = 'image/png'
|
||||
message.headers['content-disposition'] =
|
||||
`attachment; filename="${pngFileName}"`
|
||||
message.push(pngContent)
|
||||
message.push(null)
|
||||
return {
|
||||
message
|
||||
}
|
||||
})
|
||||
|
||||
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
|
||||
() => {
|
||||
return {
|
||||
get: mockGetPngFile
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await streamExtractExternal(
|
||||
fixtures.blobStorageUrl,
|
||||
fixtures.workspaceDir
|
||||
)
|
||||
|
||||
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
|
||||
// Verify PNG was saved as-is
|
||||
const savedFilePath = path.join(fixtures.workspaceDir, pngFileName)
|
||||
expect(fs.existsSync(savedFilePath)).toBe(true)
|
||||
expect(fs.readFileSync(savedFilePath)).toEqual(pngContent)
|
||||
})
|
||||
|
||||
it('should extract when content-type is application/x-zip-compressed', async () => {
|
||||
const mockGetZipCompressed = jest.fn(() => {
|
||||
const message = new http.IncomingMessage(new net.Socket())
|
||||
message.statusCode = 200
|
||||
message.headers['content-type'] = 'application/x-zip-compressed'
|
||||
message.push(fs.readFileSync(fixtures.exampleArtifact.path))
|
||||
message.push(null)
|
||||
return {
|
||||
message
|
||||
}
|
||||
})
|
||||
|
||||
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
|
||||
() => {
|
||||
return {
|
||||
get: mockGetZipCompressed
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await streamExtractExternal(
|
||||
fixtures.blobStorageUrl,
|
||||
fixtures.workspaceDir
|
||||
)
|
||||
|
||||
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
|
||||
// Verify files were extracted
|
||||
await expectExtractedArchive(fixtures.workspaceDir)
|
||||
})
|
||||
|
||||
it('should extract zip when URL ends with .zip even if content-type is not application/zip', async () => {
|
||||
const blobUrlWithZipExtension =
|
||||
'https://blob-storage.local/artifact.zip?sig=abc123'
|
||||
|
||||
const mockGetZipByUrl = jest.fn(() => {
|
||||
const message = new http.IncomingMessage(new net.Socket())
|
||||
message.statusCode = 200
|
||||
// Azure Blob Storage may return a generic content-type
|
||||
message.headers['content-type'] = 'application/octet-stream'
|
||||
message.push(fs.readFileSync(fixtures.exampleArtifact.path))
|
||||
message.push(null)
|
||||
return {
|
||||
message
|
||||
}
|
||||
})
|
||||
|
||||
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
|
||||
() => {
|
||||
return {
|
||||
get: mockGetZipByUrl
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await streamExtractExternal(
|
||||
blobUrlWithZipExtension,
|
||||
fixtures.workspaceDir
|
||||
)
|
||||
|
||||
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
|
||||
// Verify files were extracted based on URL .zip extension
|
||||
await expectExtractedArchive(fixtures.workspaceDir)
|
||||
})
|
||||
|
||||
it('should skip decompression when skipDecompress option is true even for zip content-type', async () => {
|
||||
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
|
||||
() => {
|
||||
return {
|
||||
get: mockGetArtifactSuccess
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await streamExtractExternal(
|
||||
fixtures.blobStorageUrl,
|
||||
fixtures.workspaceDir,
|
||||
{skipDecompress: true}
|
||||
)
|
||||
|
||||
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
|
||||
// Verify zip was saved as-is, not extracted
|
||||
// When skipDecompress is true, the file should be saved with default name 'artifact'
|
||||
const savedFilePath = path.join(fixtures.workspaceDir, 'artifact')
|
||||
expect(fs.existsSync(savedFilePath)).toBe(true)
|
||||
// The saved file should be the raw zip content
|
||||
const savedContent = fs.readFileSync(savedFilePath)
|
||||
const originalZipContent = fs.readFileSync(fixtures.exampleArtifact.path)
|
||||
expect(savedContent).toEqual(originalZipContent)
|
||||
})
|
||||
|
||||
it('should sanitize path traversal attempts in Content-Disposition filename', async () => {
|
||||
const rawFileContent = 'malicious content'
|
||||
const maliciousFileName = '../../../etc/passwd'
|
||||
|
||||
const mockGetMaliciousFile = jest.fn(() => {
|
||||
const message = new http.IncomingMessage(new net.Socket())
|
||||
message.statusCode = 200
|
||||
message.headers['content-type'] = 'text/plain'
|
||||
message.headers['content-disposition'] =
|
||||
`attachment; filename="${maliciousFileName}"`
|
||||
message.push(Buffer.from(rawFileContent))
|
||||
message.push(null)
|
||||
return {
|
||||
message
|
||||
}
|
||||
})
|
||||
|
||||
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
|
||||
() => {
|
||||
return {
|
||||
get: mockGetMaliciousFile
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await streamExtractExternal(
|
||||
fixtures.blobStorageUrl,
|
||||
fixtures.workspaceDir
|
||||
)
|
||||
|
||||
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
|
||||
// Verify file was saved with sanitized name (just 'passwd', not the full path)
|
||||
const sanitizedFileName = 'passwd'
|
||||
const savedFilePath = path.join(fixtures.workspaceDir, sanitizedFileName)
|
||||
expect(fs.existsSync(savedFilePath)).toBe(true)
|
||||
expect(fs.readFileSync(savedFilePath, 'utf8')).toBe(rawFileContent)
|
||||
|
||||
// Verify the file was NOT written outside the workspace directory
|
||||
const maliciousPath = path.resolve(
|
||||
fixtures.workspaceDir,
|
||||
maliciousFileName
|
||||
)
|
||||
expect(fs.existsSync(maliciousPath)).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle encoded path traversal attempts in Content-Disposition filename', async () => {
|
||||
const rawFileContent = 'encoded malicious content'
|
||||
// URL encoded version of ../../../etc/passwd
|
||||
const encodedMaliciousFileName = '..%2F..%2F..%2Fetc%2Fpasswd'
|
||||
|
||||
const mockGetEncodedMaliciousFile = jest.fn(() => {
|
||||
const message = new http.IncomingMessage(new net.Socket())
|
||||
message.statusCode = 200
|
||||
message.headers['content-type'] = 'application/octet-stream'
|
||||
message.headers['content-disposition'] =
|
||||
`attachment; filename="${encodedMaliciousFileName}"`
|
||||
message.push(Buffer.from(rawFileContent))
|
||||
message.push(null)
|
||||
return {
|
||||
message
|
||||
}
|
||||
})
|
||||
|
||||
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
|
||||
() => {
|
||||
return {
|
||||
get: mockGetEncodedMaliciousFile
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await streamExtractExternal(
|
||||
fixtures.blobStorageUrl,
|
||||
fixtures.workspaceDir
|
||||
)
|
||||
|
||||
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
|
||||
// After decoding and sanitizing, should just be 'passwd'
|
||||
const sanitizedFileName = 'passwd'
|
||||
const savedFilePath = path.join(fixtures.workspaceDir, sanitizedFileName)
|
||||
expect(fs.existsSync(savedFilePath)).toBe(true)
|
||||
expect(fs.readFileSync(savedFilePath, 'utf8')).toBe(rawFileContent)
|
||||
|
||||
// Verify the file was NOT written outside the workspace directory
|
||||
const maliciousPathEncoded = path.resolve(
|
||||
fixtures.workspaceDir,
|
||||
encodedMaliciousFileName
|
||||
)
|
||||
expect(fs.existsSync(maliciousPathEncoded)).toBe(false)
|
||||
|
||||
const maliciousPath = path.resolve(
|
||||
fixtures.workspaceDir,
|
||||
'../../../etc/passwd'
|
||||
)
|
||||
expect(fs.existsSync(maliciousPath)).toBe(false)
|
||||
})
|
||||
|
||||
it('should correctly handle Content-Disposition with filename* parameter (RFC 5987)', async () => {
|
||||
const rawFileContent = 'content with rfc5987 encoding'
|
||||
const expectedFileName = '报告-土-x.txt'
|
||||
const asciiFileName = '__-_-x.txt'
|
||||
|
||||
const mockGetRfc5987File = jest.fn(() => {
|
||||
const message = new http.IncomingMessage(new net.Socket())
|
||||
message.statusCode = 200
|
||||
message.headers['content-type'] = 'text/plain'
|
||||
// Server sends both: filename with _ fallbacks, filename* with UTF-8 encoding
|
||||
message.headers['content-disposition'] =
|
||||
`attachment; filename="${asciiFileName}"; filename*=UTF-8''${encodeURIComponent(expectedFileName)}`
|
||||
message.push(Buffer.from(rawFileContent, 'utf8'))
|
||||
message.push(null)
|
||||
return {
|
||||
message
|
||||
}
|
||||
})
|
||||
|
||||
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
|
||||
() => {
|
||||
return {
|
||||
get: mockGetRfc5987File
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await streamExtractExternal(
|
||||
fixtures.blobStorageUrl,
|
||||
fixtures.workspaceDir
|
||||
)
|
||||
|
||||
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
|
||||
const savedFilePath = path.join(fixtures.workspaceDir, expectedFileName)
|
||||
expect(fs.existsSync(savedFilePath)).toBe(true)
|
||||
expect(fs.readFileSync(savedFilePath, 'utf8')).toBe(rawFileContent)
|
||||
})
|
||||
|
||||
it('should handle zip artifacts with Chinese characters in the artifact name', async () => {
|
||||
// Simulate Azure Blob Storage URL with rscd containing Chinese filename
|
||||
const chineseArtifactName = 'probe-土-x'
|
||||
const asciiArtifactName = 'probe-_-x'
|
||||
const blobUrlWithChineseName = `https://blob-storage.local/artifact.zip?rscd=${encodeURIComponent(`attachment; filename="${asciiArtifactName}.zip"; filename*=UTF-8''${encodeURIComponent(`${chineseArtifactName}.zip`)}`)}&rsct=application%2Fzip&sig=abc123`
|
||||
|
||||
const mockGetZip = jest.fn(() => {
|
||||
const message = new http.IncomingMessage(new net.Socket())
|
||||
message.statusCode = 200
|
||||
message.headers['content-type'] = 'application/zip'
|
||||
message.headers['content-disposition'] =
|
||||
`attachment; filename="${asciiArtifactName}.zip"; filename*=UTF-8''${encodeURIComponent(`${chineseArtifactName}.zip`)}`
|
||||
message.push(fs.readFileSync(fixtures.exampleArtifact.path))
|
||||
message.push(null)
|
||||
return {
|
||||
message
|
||||
}
|
||||
})
|
||||
|
||||
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
|
||||
() => {
|
||||
return {
|
||||
get: mockGetZip
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await streamExtractExternal(blobUrlWithChineseName, fixtures.workspaceDir)
|
||||
|
||||
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
|
||||
// Zip should be extracted normally regardless of Chinese artifact name
|
||||
await expectExtractedArchive(fixtures.workspaceDir)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['土', '_'], // U+571F - known to cause 400 errors
|
||||
['日', '_'], // U+65E5 - reported to work fine
|
||||
['中文测试', '____'], // multiple Chinese characters
|
||||
['文件-2026年', '__-2026_'], // mixed Chinese and numbers
|
||||
['データ', '___'], // Japanese katakana
|
||||
['테스트', '___'] // Korean characters
|
||||
])(
|
||||
'should prefer filename* over filename for non-ASCII character %s (%s)',
|
||||
async (chars, asciiReplacement) => {
|
||||
const rawFileContent = `content for ${chars}`
|
||||
const expectedFileName = `artifact-${chars}.txt`
|
||||
const asciiFileName = `artifact-${asciiReplacement}.txt`
|
||||
|
||||
const mockGetFile = jest.fn(() => {
|
||||
const message = new http.IncomingMessage(new net.Socket())
|
||||
message.statusCode = 200
|
||||
message.headers['content-type'] = 'text/plain'
|
||||
// Server sends filename with _ replacing non-ASCII, filename* with proper encoding
|
||||
message.headers['content-disposition'] =
|
||||
`attachment; filename="${asciiFileName}"; filename*=UTF-8''${encodeURIComponent(expectedFileName)}`
|
||||
message.push(Buffer.from(rawFileContent, 'utf8'))
|
||||
message.push(null)
|
||||
return {
|
||||
message
|
||||
}
|
||||
})
|
||||
|
||||
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
|
||||
() => {
|
||||
return {
|
||||
get: mockGetFile
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await streamExtractExternal(
|
||||
fixtures.blobStorageUrl,
|
||||
fixtures.workspaceDir
|
||||
)
|
||||
|
||||
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
|
||||
const savedFilePath = path.join(fixtures.workspaceDir, expectedFileName)
|
||||
expect(fs.existsSync(savedFilePath)).toBe(true)
|
||||
expect(fs.readFileSync(savedFilePath, 'utf8')).toBe(rawFileContent)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -230,9 +230,8 @@ describe('list-artifact', () => {
|
||||
jest.resetModules()
|
||||
|
||||
try {
|
||||
const {listArtifactsPublic: listArtifactsPublicReloaded} = await import(
|
||||
'../src/internal/find/list-artifacts'
|
||||
)
|
||||
const {listArtifactsPublic: listArtifactsPublicReloaded} =
|
||||
await import('../src/internal/find/list-artifacts')
|
||||
const githubReloaded = await import('@actions/github')
|
||||
|
||||
const mockRequest = (githubReloaded.getOctokit as jest.Mock)(
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import {createRawFileUploadStream} from '../src/internal/upload/stream.js'
|
||||
import {noopLogs} from './common.js'
|
||||
|
||||
const fixtures = {
|
||||
testDirectory: path.join(__dirname, '_temp', 'stream-test'),
|
||||
testFile: path.join(__dirname, '_temp', 'stream-test', 'test-file.txt'),
|
||||
testContent: 'hello stream test'
|
||||
}
|
||||
|
||||
describe('createRawFileUploadStream', () => {
|
||||
beforeAll(() => {
|
||||
fs.mkdirSync(fixtures.testDirectory, {recursive: true})
|
||||
fs.writeFileSync(fixtures.testFile, fixtures.testContent)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
noopLogs()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('should stream file contents through the upload stream', async () => {
|
||||
const uploadStream = await createRawFileUploadStream(fixtures.testFile)
|
||||
|
||||
const chunks: Buffer[] = []
|
||||
const result = await new Promise<string>((resolve, reject) => {
|
||||
uploadStream.on('data', chunk => chunks.push(Buffer.from(chunk)))
|
||||
uploadStream.on('end', () =>
|
||||
resolve(Buffer.concat(chunks).toString('utf-8'))
|
||||
)
|
||||
uploadStream.on('error', reject)
|
||||
})
|
||||
|
||||
expect(result).toBe(fixtures.testContent)
|
||||
})
|
||||
|
||||
it('should propagate file read errors through the upload stream', async () => {
|
||||
// Use a directory path — createReadStream on a directory fails cross-platform
|
||||
// Mock lstat to return a non-symlink result so we reach createReadStream
|
||||
const dirPath = fixtures.testDirectory
|
||||
jest
|
||||
.spyOn(fs.promises, 'lstat')
|
||||
.mockResolvedValue({isSymbolicLink: () => false} as fs.Stats)
|
||||
|
||||
const uploadStream = await createRawFileUploadStream(dirPath)
|
||||
|
||||
await expect(
|
||||
new Promise((resolve, reject) => {
|
||||
uploadStream.on('data', resolve)
|
||||
uploadStream.on('end', resolve)
|
||||
uploadStream.on('error', reject)
|
||||
})
|
||||
).rejects.toThrow('An error has occurred during file read for the artifact')
|
||||
})
|
||||
})
|
||||
@@ -7,7 +7,6 @@ import * as blobUpload from '../src/internal/upload/blob-upload.js'
|
||||
import {uploadArtifact} from '../src/internal/upload/upload-artifact.js'
|
||||
import {noopLogs} from './common.js'
|
||||
import {FilesNotFoundError} from '../src/internal/shared/errors.js'
|
||||
import * as stream from '../src/internal/upload/stream.js'
|
||||
import {BlockBlobUploadStreamOptions} from '@azure/storage-blob'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
@@ -151,7 +150,7 @@ describe('upload-artifact', () => {
|
||||
it('should return false if the creation request fails', async () => {
|
||||
jest
|
||||
.spyOn(zip, 'createZipUploadStream')
|
||||
.mockReturnValue(Promise.resolve(new stream.WaterMarkedUploadStream(1)))
|
||||
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
|
||||
jest
|
||||
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
||||
.mockReturnValue(Promise.resolve({ok: false, signedUploadUrl: ''}))
|
||||
@@ -168,7 +167,7 @@ describe('upload-artifact', () => {
|
||||
it('should return false if blob storage upload is unsuccessful', async () => {
|
||||
jest
|
||||
.spyOn(zip, 'createZipUploadStream')
|
||||
.mockReturnValue(Promise.resolve(new stream.WaterMarkedUploadStream(1)))
|
||||
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
|
||||
jest
|
||||
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
||||
.mockReturnValue(
|
||||
@@ -178,7 +177,7 @@ describe('upload-artifact', () => {
|
||||
})
|
||||
)
|
||||
jest
|
||||
.spyOn(blobUpload, 'uploadToBlobStorage')
|
||||
.spyOn(blobUpload, 'uploadZipToBlobStorage')
|
||||
.mockReturnValue(Promise.reject(new Error('boom')))
|
||||
|
||||
const uploadResp = uploadArtifact(
|
||||
@@ -193,7 +192,7 @@ describe('upload-artifact', () => {
|
||||
it('should reject if finalize artifact fails', async () => {
|
||||
jest
|
||||
.spyOn(zip, 'createZipUploadStream')
|
||||
.mockReturnValue(Promise.resolve(new stream.WaterMarkedUploadStream(1)))
|
||||
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
|
||||
jest
|
||||
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
||||
.mockReturnValue(
|
||||
@@ -202,7 +201,7 @@ describe('upload-artifact', () => {
|
||||
signedUploadUrl: 'https://signed-upload-url.com'
|
||||
})
|
||||
)
|
||||
jest.spyOn(blobUpload, 'uploadToBlobStorage').mockReturnValue(
|
||||
jest.spyOn(blobUpload, 'uploadZipToBlobStorage').mockReturnValue(
|
||||
Promise.resolve({
|
||||
uploadSize: 1234,
|
||||
sha256Hash: 'test-sha256-hash'
|
||||
@@ -371,284 +370,4 @@ describe('upload-artifact', () => {
|
||||
|
||||
await expect(uploadResp).rejects.toThrow('Upload progress stalled.')
|
||||
})
|
||||
|
||||
describe('skipArchive option', () => {
|
||||
it('should throw an error if skipArchive is true and files array is empty', async () => {
|
||||
const uploadResp = uploadArtifact(
|
||||
fixtures.inputs.artifactName,
|
||||
[],
|
||||
fixtures.inputs.rootDirectory,
|
||||
{skipArchive: true}
|
||||
)
|
||||
|
||||
await expect(uploadResp).rejects.toThrow(FilesNotFoundError)
|
||||
})
|
||||
|
||||
it('should throw an error if skipArchive is true and multiple files are provided', async () => {
|
||||
const uploadResp = uploadArtifact(
|
||||
fixtures.inputs.artifactName,
|
||||
fixtures.inputs.files,
|
||||
fixtures.inputs.rootDirectory,
|
||||
{skipArchive: true}
|
||||
)
|
||||
|
||||
await expect(uploadResp).rejects.toThrow(
|
||||
'skipArchive option is only supported when uploading a single file'
|
||||
)
|
||||
})
|
||||
|
||||
it('should upload a single file without archiving when skipArchive is true', async () => {
|
||||
jest
|
||||
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
|
||||
.mockRestore()
|
||||
|
||||
const singleFile = path.join(fixtures.uploadDirectory, 'file1.txt')
|
||||
const expectedContent = 'test 1 file content'
|
||||
|
||||
jest
|
||||
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
||||
.mockReturnValue(
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
signedUploadUrl: 'https://signed-upload-url.local'
|
||||
})
|
||||
)
|
||||
jest
|
||||
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
|
||||
.mockReturnValue(
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
artifactId: '1'
|
||||
})
|
||||
)
|
||||
|
||||
let uploadedContent = ''
|
||||
let loadedBytes = 0
|
||||
uploadStreamMock.mockImplementation(
|
||||
async (
|
||||
stream: NodeJS.ReadableStream,
|
||||
bufferSize?: number,
|
||||
maxConcurrency?: number,
|
||||
options?: BlockBlobUploadStreamOptions
|
||||
) => {
|
||||
const {onProgress} = options || {}
|
||||
|
||||
onProgress?.({loadedBytes: 0})
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on('data', chunk => {
|
||||
loadedBytes += chunk.length
|
||||
uploadedContent += chunk.toString()
|
||||
onProgress?.({loadedBytes})
|
||||
})
|
||||
stream.on('end', () => {
|
||||
onProgress?.({loadedBytes})
|
||||
resolve({})
|
||||
})
|
||||
stream.on('error', err => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const {id, size, digest} = await uploadArtifact(
|
||||
fixtures.inputs.artifactName,
|
||||
[singleFile],
|
||||
fixtures.uploadDirectory,
|
||||
{skipArchive: true}
|
||||
)
|
||||
|
||||
expect(id).toBe(1)
|
||||
expect(size).toBe(loadedBytes)
|
||||
expect(digest).toBeDefined()
|
||||
expect(digest).toHaveLength(64)
|
||||
// Verify the uploaded content is the raw file, not a zip
|
||||
expect(uploadedContent).toBe(expectedContent)
|
||||
})
|
||||
|
||||
it('should use the correct MIME type when skipArchive is true', async () => {
|
||||
jest
|
||||
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
|
||||
.mockRestore()
|
||||
|
||||
const singleFile = path.join(fixtures.uploadDirectory, 'file1.txt')
|
||||
|
||||
const createArtifactSpy = jest
|
||||
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
||||
.mockReturnValue(
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
signedUploadUrl: 'https://signed-upload-url.local'
|
||||
})
|
||||
)
|
||||
jest
|
||||
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
|
||||
.mockReturnValue(
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
artifactId: '1'
|
||||
})
|
||||
)
|
||||
|
||||
uploadStreamMock.mockImplementation(
|
||||
async (
|
||||
stream: NodeJS.ReadableStream,
|
||||
bufferSize?: number,
|
||||
maxConcurrency?: number,
|
||||
options?: BlockBlobUploadStreamOptions
|
||||
) => {
|
||||
const {onProgress} = options || {}
|
||||
onProgress?.({loadedBytes: 0})
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on('data', chunk => {
|
||||
onProgress?.({loadedBytes: chunk.length})
|
||||
})
|
||||
stream.on('end', () => {
|
||||
resolve({})
|
||||
})
|
||||
stream.on('error', err => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
await uploadArtifact(
|
||||
fixtures.inputs.artifactName,
|
||||
[singleFile],
|
||||
fixtures.uploadDirectory,
|
||||
{skipArchive: true}
|
||||
)
|
||||
|
||||
// Verify CreateArtifact was called with the correct MIME type for .txt file
|
||||
expect(createArtifactSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mimeType: expect.objectContaining({value: 'text/plain'})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should use application/zip MIME type when skipArchive is false', async () => {
|
||||
jest
|
||||
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
|
||||
.mockRestore()
|
||||
|
||||
const createArtifactSpy = jest
|
||||
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
||||
.mockReturnValue(
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
signedUploadUrl: 'https://signed-upload-url.local'
|
||||
})
|
||||
)
|
||||
jest
|
||||
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
|
||||
.mockReturnValue(
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
artifactId: '1'
|
||||
})
|
||||
)
|
||||
|
||||
uploadStreamMock.mockImplementation(
|
||||
async (
|
||||
stream: NodeJS.ReadableStream,
|
||||
bufferSize?: number,
|
||||
maxConcurrency?: number,
|
||||
options?: BlockBlobUploadStreamOptions
|
||||
) => {
|
||||
const {onProgress} = options || {}
|
||||
onProgress?.({loadedBytes: 0})
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on('data', chunk => {
|
||||
onProgress?.({loadedBytes: chunk.length})
|
||||
})
|
||||
stream.on('end', () => {
|
||||
resolve({})
|
||||
})
|
||||
stream.on('error', err => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
await uploadArtifact(
|
||||
fixtures.inputs.artifactName,
|
||||
fixtures.files.map(file =>
|
||||
path.join(fixtures.uploadDirectory, file.name)
|
||||
),
|
||||
fixtures.uploadDirectory
|
||||
)
|
||||
|
||||
// Verify CreateArtifact was called with application/zip MIME type
|
||||
expect(createArtifactSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mimeType: expect.objectContaining({value: 'application/zip'})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should use the file basename as artifact name when skipArchive is true', async () => {
|
||||
jest
|
||||
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
|
||||
.mockRestore()
|
||||
|
||||
const singleFile = path.join(fixtures.uploadDirectory, 'file1.txt')
|
||||
|
||||
const createArtifactSpy = jest
|
||||
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
||||
.mockReturnValue(
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
signedUploadUrl: 'https://signed-upload-url.local'
|
||||
})
|
||||
)
|
||||
jest
|
||||
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
|
||||
.mockReturnValue(
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
artifactId: '1'
|
||||
})
|
||||
)
|
||||
|
||||
uploadStreamMock.mockImplementation(
|
||||
async (
|
||||
stream: NodeJS.ReadableStream,
|
||||
bufferSize?: number,
|
||||
maxConcurrency?: number,
|
||||
options?: BlockBlobUploadStreamOptions
|
||||
) => {
|
||||
const {onProgress} = options || {}
|
||||
onProgress?.({loadedBytes: 0})
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on('data', chunk => {
|
||||
onProgress?.({loadedBytes: chunk.length})
|
||||
})
|
||||
stream.on('end', () => {
|
||||
resolve({})
|
||||
})
|
||||
stream.on('error', err => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
await uploadArtifact(
|
||||
'original-name',
|
||||
[singleFile],
|
||||
fixtures.uploadDirectory,
|
||||
{skipArchive: true}
|
||||
)
|
||||
|
||||
// Verify CreateArtifact was called with the file basename, not the original name
|
||||
expect(createArtifactSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'file1.txt'
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Generated
+9
-8
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@actions/artifact",
|
||||
"version": "6.2.1",
|
||||
"version": "6.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@actions/artifact",
|
||||
"version": "6.2.1",
|
||||
"version": "6.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^3.0.0",
|
||||
@@ -1112,9 +1112,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz",
|
||||
"integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==",
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.3.tgz",
|
||||
"integrity": "sha512-2O3dkPAAC6JavuMm8+4+pgTk+5hoAs+CjZ+sWcQLkX9+/tHRuTkQh/Oaifr8qDmZ8iEHb771Ea6G8CdwkrgvYA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -1795,6 +1795,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -1812,9 +1813,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.24.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.0.tgz",
|
||||
"integrity": "sha512-lVLNosgqo5EkGqh5XUDhGfsMSoO8K0BAN0TyJLvwNRSl4xWGZlCVYsAIpa/OpA3TvmnM01GWcoKmc3ZWo5wKKA==",
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/artifact",
|
||||
"version": "6.2.1",
|
||||
"version": "6.0.0",
|
||||
"preview": true,
|
||||
"description": "Actions artifact lib",
|
||||
"keywords": [
|
||||
|
||||
@@ -15,6 +15,66 @@ import { MessageType } from "@protobuf-ts/runtime";
|
||||
import { Int64Value } from "../../../google/protobuf/wrappers.js";
|
||||
import { StringValue } from "../../../google/protobuf/wrappers.js";
|
||||
import { Timestamp } from "../../../google/protobuf/timestamp.js";
|
||||
/**
|
||||
* @generated from protobuf message github.actions.results.api.v1.MigrateArtifactRequest
|
||||
*/
|
||||
export interface MigrateArtifactRequest {
|
||||
/**
|
||||
* @generated from protobuf field: string workflow_run_backend_id = 1;
|
||||
*/
|
||||
workflowRunBackendId: string;
|
||||
/**
|
||||
* @generated from protobuf field: string name = 2;
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @generated from protobuf field: google.protobuf.Timestamp expires_at = 3;
|
||||
*/
|
||||
expiresAt?: Timestamp;
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message github.actions.results.api.v1.MigrateArtifactResponse
|
||||
*/
|
||||
export interface MigrateArtifactResponse {
|
||||
/**
|
||||
* @generated from protobuf field: bool ok = 1;
|
||||
*/
|
||||
ok: boolean;
|
||||
/**
|
||||
* @generated from protobuf field: string signed_upload_url = 2;
|
||||
*/
|
||||
signedUploadUrl: string;
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message github.actions.results.api.v1.FinalizeMigratedArtifactRequest
|
||||
*/
|
||||
export interface FinalizeMigratedArtifactRequest {
|
||||
/**
|
||||
* @generated from protobuf field: string workflow_run_backend_id = 1;
|
||||
*/
|
||||
workflowRunBackendId: string;
|
||||
/**
|
||||
* @generated from protobuf field: string name = 2;
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @generated from protobuf field: int64 size = 3;
|
||||
*/
|
||||
size: string;
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message github.actions.results.api.v1.FinalizeMigratedArtifactResponse
|
||||
*/
|
||||
export interface FinalizeMigratedArtifactResponse {
|
||||
/**
|
||||
* @generated from protobuf field: bool ok = 1;
|
||||
*/
|
||||
ok: boolean;
|
||||
/**
|
||||
* @generated from protobuf field: int64 artifact_id = 2;
|
||||
*/
|
||||
artifactId: string;
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message github.actions.results.api.v1.CreateArtifactRequest
|
||||
*/
|
||||
@@ -39,10 +99,6 @@ export interface CreateArtifactRequest {
|
||||
* @generated from protobuf field: int32 version = 5;
|
||||
*/
|
||||
version: number;
|
||||
/**
|
||||
* @generated from protobuf field: google.protobuf.StringValue mime_type = 6;
|
||||
*/
|
||||
mimeType?: StringValue; // optional
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message github.actions.results.api.v1.CreateArtifactResponse
|
||||
@@ -237,6 +293,236 @@ export interface DeleteArtifactResponse {
|
||||
artifactId: string;
|
||||
}
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class MigrateArtifactRequest$Type extends MessageType<MigrateArtifactRequest> {
|
||||
constructor() {
|
||||
super("github.actions.results.api.v1.MigrateArtifactRequest", [
|
||||
{ no: 1, name: "workflow_run_backend_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||
{ no: 2, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||
{ no: 3, name: "expires_at", kind: "message", T: () => Timestamp }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<MigrateArtifactRequest>): MigrateArtifactRequest {
|
||||
const message = { workflowRunBackendId: "", name: "" };
|
||||
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||
if (value !== undefined)
|
||||
reflectionMergePartial<MigrateArtifactRequest>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: MigrateArtifactRequest): MigrateArtifactRequest {
|
||||
let message = target ?? this.create(), end = reader.pos + length;
|
||||
while (reader.pos < end) {
|
||||
let [fieldNo, wireType] = reader.tag();
|
||||
switch (fieldNo) {
|
||||
case /* string workflow_run_backend_id */ 1:
|
||||
message.workflowRunBackendId = reader.string();
|
||||
break;
|
||||
case /* string name */ 2:
|
||||
message.name = reader.string();
|
||||
break;
|
||||
case /* google.protobuf.Timestamp expires_at */ 3:
|
||||
message.expiresAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.expiresAt);
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === "throw")
|
||||
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||
let d = reader.skip(wireType);
|
||||
if (u !== false)
|
||||
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
internalBinaryWrite(message: MigrateArtifactRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||
/* string workflow_run_backend_id = 1; */
|
||||
if (message.workflowRunBackendId !== "")
|
||||
writer.tag(1, WireType.LengthDelimited).string(message.workflowRunBackendId);
|
||||
/* string name = 2; */
|
||||
if (message.name !== "")
|
||||
writer.tag(2, WireType.LengthDelimited).string(message.name);
|
||||
/* google.protobuf.Timestamp expires_at = 3; */
|
||||
if (message.expiresAt)
|
||||
Timestamp.internalBinaryWrite(message.expiresAt, writer.tag(3, WireType.LengthDelimited).fork(), options).join();
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false)
|
||||
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message github.actions.results.api.v1.MigrateArtifactRequest
|
||||
*/
|
||||
export const MigrateArtifactRequest = new MigrateArtifactRequest$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class MigrateArtifactResponse$Type extends MessageType<MigrateArtifactResponse> {
|
||||
constructor() {
|
||||
super("github.actions.results.api.v1.MigrateArtifactResponse", [
|
||||
{ no: 1, name: "ok", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
|
||||
{ no: 2, name: "signed_upload_url", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<MigrateArtifactResponse>): MigrateArtifactResponse {
|
||||
const message = { ok: false, signedUploadUrl: "" };
|
||||
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||
if (value !== undefined)
|
||||
reflectionMergePartial<MigrateArtifactResponse>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: MigrateArtifactResponse): MigrateArtifactResponse {
|
||||
let message = target ?? this.create(), end = reader.pos + length;
|
||||
while (reader.pos < end) {
|
||||
let [fieldNo, wireType] = reader.tag();
|
||||
switch (fieldNo) {
|
||||
case /* bool ok */ 1:
|
||||
message.ok = reader.bool();
|
||||
break;
|
||||
case /* string signed_upload_url */ 2:
|
||||
message.signedUploadUrl = reader.string();
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === "throw")
|
||||
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||
let d = reader.skip(wireType);
|
||||
if (u !== false)
|
||||
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
internalBinaryWrite(message: MigrateArtifactResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||
/* bool ok = 1; */
|
||||
if (message.ok !== false)
|
||||
writer.tag(1, WireType.Varint).bool(message.ok);
|
||||
/* string signed_upload_url = 2; */
|
||||
if (message.signedUploadUrl !== "")
|
||||
writer.tag(2, WireType.LengthDelimited).string(message.signedUploadUrl);
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false)
|
||||
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message github.actions.results.api.v1.MigrateArtifactResponse
|
||||
*/
|
||||
export const MigrateArtifactResponse = new MigrateArtifactResponse$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class FinalizeMigratedArtifactRequest$Type extends MessageType<FinalizeMigratedArtifactRequest> {
|
||||
constructor() {
|
||||
super("github.actions.results.api.v1.FinalizeMigratedArtifactRequest", [
|
||||
{ no: 1, name: "workflow_run_backend_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||
{ no: 2, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||
{ no: 3, name: "size", kind: "scalar", T: 3 /*ScalarType.INT64*/ }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<FinalizeMigratedArtifactRequest>): FinalizeMigratedArtifactRequest {
|
||||
const message = { workflowRunBackendId: "", name: "", size: "0" };
|
||||
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||
if (value !== undefined)
|
||||
reflectionMergePartial<FinalizeMigratedArtifactRequest>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FinalizeMigratedArtifactRequest): FinalizeMigratedArtifactRequest {
|
||||
let message = target ?? this.create(), end = reader.pos + length;
|
||||
while (reader.pos < end) {
|
||||
let [fieldNo, wireType] = reader.tag();
|
||||
switch (fieldNo) {
|
||||
case /* string workflow_run_backend_id */ 1:
|
||||
message.workflowRunBackendId = reader.string();
|
||||
break;
|
||||
case /* string name */ 2:
|
||||
message.name = reader.string();
|
||||
break;
|
||||
case /* int64 size */ 3:
|
||||
message.size = reader.int64().toString();
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === "throw")
|
||||
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||
let d = reader.skip(wireType);
|
||||
if (u !== false)
|
||||
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
internalBinaryWrite(message: FinalizeMigratedArtifactRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||
/* string workflow_run_backend_id = 1; */
|
||||
if (message.workflowRunBackendId !== "")
|
||||
writer.tag(1, WireType.LengthDelimited).string(message.workflowRunBackendId);
|
||||
/* string name = 2; */
|
||||
if (message.name !== "")
|
||||
writer.tag(2, WireType.LengthDelimited).string(message.name);
|
||||
/* int64 size = 3; */
|
||||
if (message.size !== "0")
|
||||
writer.tag(3, WireType.Varint).int64(message.size);
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false)
|
||||
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message github.actions.results.api.v1.FinalizeMigratedArtifactRequest
|
||||
*/
|
||||
export const FinalizeMigratedArtifactRequest = new FinalizeMigratedArtifactRequest$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class FinalizeMigratedArtifactResponse$Type extends MessageType<FinalizeMigratedArtifactResponse> {
|
||||
constructor() {
|
||||
super("github.actions.results.api.v1.FinalizeMigratedArtifactResponse", [
|
||||
{ no: 1, name: "ok", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
|
||||
{ no: 2, name: "artifact_id", kind: "scalar", T: 3 /*ScalarType.INT64*/ }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<FinalizeMigratedArtifactResponse>): FinalizeMigratedArtifactResponse {
|
||||
const message = { ok: false, artifactId: "0" };
|
||||
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||
if (value !== undefined)
|
||||
reflectionMergePartial<FinalizeMigratedArtifactResponse>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FinalizeMigratedArtifactResponse): FinalizeMigratedArtifactResponse {
|
||||
let message = target ?? this.create(), end = reader.pos + length;
|
||||
while (reader.pos < end) {
|
||||
let [fieldNo, wireType] = reader.tag();
|
||||
switch (fieldNo) {
|
||||
case /* bool ok */ 1:
|
||||
message.ok = reader.bool();
|
||||
break;
|
||||
case /* int64 artifact_id */ 2:
|
||||
message.artifactId = reader.int64().toString();
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === "throw")
|
||||
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||
let d = reader.skip(wireType);
|
||||
if (u !== false)
|
||||
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
internalBinaryWrite(message: FinalizeMigratedArtifactResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||
/* bool ok = 1; */
|
||||
if (message.ok !== false)
|
||||
writer.tag(1, WireType.Varint).bool(message.ok);
|
||||
/* int64 artifact_id = 2; */
|
||||
if (message.artifactId !== "0")
|
||||
writer.tag(2, WireType.Varint).int64(message.artifactId);
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false)
|
||||
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message github.actions.results.api.v1.FinalizeMigratedArtifactResponse
|
||||
*/
|
||||
export const FinalizeMigratedArtifactResponse = new FinalizeMigratedArtifactResponse$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class CreateArtifactRequest$Type extends MessageType<CreateArtifactRequest> {
|
||||
constructor() {
|
||||
super("github.actions.results.api.v1.CreateArtifactRequest", [
|
||||
@@ -244,8 +530,7 @@ class CreateArtifactRequest$Type extends MessageType<CreateArtifactRequest> {
|
||||
{ no: 2, name: "workflow_job_run_backend_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||
{ no: 3, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||
{ no: 4, name: "expires_at", kind: "message", T: () => Timestamp },
|
||||
{ no: 5, name: "version", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
|
||||
{ no: 6, name: "mime_type", kind: "message", T: () => StringValue }
|
||||
{ no: 5, name: "version", kind: "scalar", T: 5 /*ScalarType.INT32*/ }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<CreateArtifactRequest>): CreateArtifactRequest {
|
||||
@@ -275,9 +560,6 @@ class CreateArtifactRequest$Type extends MessageType<CreateArtifactRequest> {
|
||||
case /* int32 version */ 5:
|
||||
message.version = reader.int32();
|
||||
break;
|
||||
case /* google.protobuf.StringValue mime_type */ 6:
|
||||
message.mimeType = StringValue.internalBinaryRead(reader, reader.uint32(), options, message.mimeType);
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === "throw")
|
||||
@@ -305,9 +587,6 @@ class CreateArtifactRequest$Type extends MessageType<CreateArtifactRequest> {
|
||||
/* int32 version = 5; */
|
||||
if (message.version !== 0)
|
||||
writer.tag(5, WireType.Varint).int32(message.version);
|
||||
/* google.protobuf.StringValue mime_type = 6; */
|
||||
if (message.mimeType)
|
||||
StringValue.internalBinaryWrite(message.mimeType, writer.tag(6, WireType.LengthDelimited).fork(), options).join();
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false)
|
||||
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
@@ -573,7 +852,7 @@ export const ListArtifactsRequest = new ListArtifactsRequest$Type();
|
||||
class ListArtifactsResponse$Type extends MessageType<ListArtifactsResponse> {
|
||||
constructor() {
|
||||
super("github.actions.results.api.v1.ListArtifactsResponse", [
|
||||
{ no: 1, name: "artifacts", kind: "message", repeat: 2 /*RepeatType.UNPACKED*/, T: () => ListArtifactsResponse_MonolithArtifact }
|
||||
{ no: 1, name: "artifacts", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => ListArtifactsResponse_MonolithArtifact }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<ListArtifactsResponse>): ListArtifactsResponse {
|
||||
@@ -936,5 +1215,7 @@ export const ArtifactService = new ServiceType("github.actions.results.api.v1.Ar
|
||||
{ name: "FinalizeArtifact", options: {}, I: FinalizeArtifactRequest, O: FinalizeArtifactResponse },
|
||||
{ name: "ListArtifacts", options: {}, I: ListArtifactsRequest, O: ListArtifactsResponse },
|
||||
{ name: "GetSignedArtifactURL", options: {}, I: GetSignedArtifactURLRequest, O: GetSignedArtifactURLResponse },
|
||||
{ name: "DeleteArtifact", options: {}, I: DeleteArtifactRequest, O: DeleteArtifactResponse }
|
||||
{ name: "DeleteArtifact", options: {}, I: DeleteArtifactRequest, O: DeleteArtifactResponse },
|
||||
{ name: "MigrateArtifact", options: {}, I: MigrateArtifactRequest, O: MigrateArtifactResponse },
|
||||
{ name: "FinalizeMigratedArtifact", options: {}, I: FinalizeMigratedArtifactRequest, O: FinalizeMigratedArtifactResponse }
|
||||
]);
|
||||
@@ -229,4 +229,4 @@ export class ArtifactServiceClientProtobuf implements ArtifactServiceClient {
|
||||
DeleteArtifactResponse.fromBinary(data as Uint8Array)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import fs from 'fs/promises'
|
||||
import * as fsSync from 'fs'
|
||||
import * as crypto from 'crypto'
|
||||
import * as stream from 'stream'
|
||||
import * as path from 'path'
|
||||
|
||||
import * as github from '@actions/github'
|
||||
import * as core from '@actions/core'
|
||||
@@ -45,13 +43,12 @@ async function exists(path: string): Promise<boolean> {
|
||||
|
||||
async function streamExtract(
|
||||
url: string,
|
||||
directory: string,
|
||||
skipDecompress?: boolean
|
||||
directory: string
|
||||
): Promise<StreamExtractResponse> {
|
||||
let retryCount = 0
|
||||
while (retryCount < 5) {
|
||||
try {
|
||||
return await streamExtractExternal(url, directory, {skipDecompress})
|
||||
return await streamExtractExternal(url, directory)
|
||||
} catch (error) {
|
||||
retryCount++
|
||||
core.debug(
|
||||
@@ -68,9 +65,8 @@ async function streamExtract(
|
||||
export async function streamExtractExternal(
|
||||
url: string,
|
||||
directory: string,
|
||||
opts: {timeout?: number; skipDecompress?: boolean} = {}
|
||||
opts: {timeout: number} = {timeout: 30 * 1000}
|
||||
): Promise<StreamExtractResponse> {
|
||||
const {timeout = 30 * 1000, skipDecompress = false} = opts
|
||||
const client = new httpClient.HttpClient(getUserAgentString())
|
||||
const response = await client.get(url)
|
||||
if (response.message.statusCode !== 200) {
|
||||
@@ -79,97 +75,49 @@ export async function streamExtractExternal(
|
||||
)
|
||||
}
|
||||
|
||||
const contentType = response.message.headers['content-type'] || ''
|
||||
const mimeType = contentType.split(';', 1)[0].trim().toLowerCase()
|
||||
|
||||
// Check if the URL path ends with .zip (ignoring query parameters)
|
||||
const urlPath = new URL(url).pathname.toLowerCase()
|
||||
const urlEndsWithZip = urlPath.endsWith('.zip')
|
||||
|
||||
const isZip =
|
||||
mimeType === 'application/zip' ||
|
||||
mimeType === 'application/x-zip-compressed' ||
|
||||
mimeType === 'application/zip-compressed' ||
|
||||
urlEndsWithZip
|
||||
|
||||
// Extract filename from Content-Disposition header
|
||||
// Prefer filename* (RFC 5987) which supports UTF-8 encoded filenames,
|
||||
// fall back to filename which may contain ASCII-only replacements
|
||||
const contentDisposition =
|
||||
response.message.headers['content-disposition'] || ''
|
||||
let fileName = 'artifact'
|
||||
const filenameStar = contentDisposition.match(
|
||||
/filename\*\s*=\s*UTF-8''([^;\r\n]*)/i
|
||||
)
|
||||
const filenamePlain = contentDisposition.match(
|
||||
/(?<!\*)filename\s*=\s*['"]?([^;\r\n"']*)['"]?/i
|
||||
)
|
||||
const rawName = filenameStar?.[1] || filenamePlain?.[1]
|
||||
if (rawName) {
|
||||
// Sanitize fileName to prevent path traversal attacks
|
||||
// Use path.basename to extract only the filename component
|
||||
fileName = path.basename(decodeURIComponent(rawName.trim()))
|
||||
}
|
||||
|
||||
core.debug(
|
||||
`Content-Type: ${contentType}, mimeType: ${mimeType}, urlEndsWithZip: ${urlEndsWithZip}, isZip: ${isZip}, skipDecompress: ${skipDecompress}`
|
||||
)
|
||||
core.debug(
|
||||
`Content-Disposition: ${contentDisposition}, fileName: ${fileName}`
|
||||
)
|
||||
|
||||
let sha256Digest: string | undefined = undefined
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timerFn = (): void => {
|
||||
const timeoutError = new Error(
|
||||
`Blob storage chunk did not respond in ${timeout}ms`
|
||||
`Blob storage chunk did not respond in ${opts.timeout}ms`
|
||||
)
|
||||
response.message.destroy(timeoutError)
|
||||
reject(timeoutError)
|
||||
}
|
||||
const timer = setTimeout(timerFn, timeout)
|
||||
|
||||
const onError = (error: Error): void => {
|
||||
core.debug(`response.message: Artifact download failed: ${error.message}`)
|
||||
clearTimeout(timer)
|
||||
reject(error)
|
||||
}
|
||||
const timer = setTimeout(timerFn, opts.timeout)
|
||||
|
||||
const hashStream = crypto.createHash('sha256').setEncoding('hex')
|
||||
const passThrough = new stream.PassThrough()
|
||||
.on('data', () => {
|
||||
timer.refresh()
|
||||
})
|
||||
.on('error', onError)
|
||||
|
||||
response.message.pipe(passThrough)
|
||||
passThrough.pipe(hashStream)
|
||||
const extractStream = passThrough
|
||||
|
||||
const onClose = (): void => {
|
||||
clearTimeout(timer)
|
||||
if (hashStream) {
|
||||
hashStream.end()
|
||||
sha256Digest = hashStream.read() as string
|
||||
core.info(`SHA256 digest of downloaded artifact is ${sha256Digest}`)
|
||||
}
|
||||
resolve({sha256Digest: `sha256:${sha256Digest}`})
|
||||
}
|
||||
|
||||
if (isZip && !skipDecompress) {
|
||||
// Extract zip file
|
||||
passThrough
|
||||
.pipe(unzip.Extract({path: directory}))
|
||||
.on('close', onClose)
|
||||
.on('error', onError)
|
||||
} else {
|
||||
// Save raw file without extracting
|
||||
const filePath = path.join(directory, fileName)
|
||||
const writeStream = fsSync.createWriteStream(filePath)
|
||||
|
||||
core.info(`Downloading raw file (non-zip) to: ${filePath}`)
|
||||
passThrough.pipe(writeStream).on('close', onClose).on('error', onError)
|
||||
}
|
||||
extractStream
|
||||
.on('data', () => {
|
||||
timer.refresh()
|
||||
})
|
||||
.on('error', (error: Error) => {
|
||||
core.debug(
|
||||
`response.message: Artifact download failed: ${error.message}`
|
||||
)
|
||||
clearTimeout(timer)
|
||||
reject(error)
|
||||
})
|
||||
.pipe(unzip.Extract({path: directory}))
|
||||
.on('close', () => {
|
||||
clearTimeout(timer)
|
||||
if (hashStream) {
|
||||
hashStream.end()
|
||||
sha256Digest = hashStream.read() as string
|
||||
core.info(`SHA256 digest of downloaded artifact is ${sha256Digest}`)
|
||||
}
|
||||
resolve({sha256Digest: `sha256:${sha256Digest}`})
|
||||
})
|
||||
.on('error', (error: Error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -215,11 +163,7 @@ export async function downloadArtifactPublic(
|
||||
|
||||
try {
|
||||
core.info(`Starting download of artifact to: ${downloadPath}`)
|
||||
const extractResponse = await streamExtract(
|
||||
location,
|
||||
downloadPath,
|
||||
options?.skipDecompress
|
||||
)
|
||||
const extractResponse = await streamExtract(location, downloadPath)
|
||||
core.info(`Artifact download completed successfully.`)
|
||||
if (options?.expectedHash) {
|
||||
if (options?.expectedHash !== extractResponse.sha256Digest) {
|
||||
@@ -280,11 +224,7 @@ export async function downloadArtifactInternal(
|
||||
|
||||
try {
|
||||
core.info(`Starting download of artifact to: ${downloadPath}`)
|
||||
const extractResponse = await streamExtract(
|
||||
signedUrl,
|
||||
downloadPath,
|
||||
options?.skipDecompress
|
||||
)
|
||||
const extractResponse = await streamExtract(signedUrl, downloadPath)
|
||||
core.info(`Artifact download completed successfully.`)
|
||||
if (options?.expectedHash) {
|
||||
if (options?.expectedHash !== extractResponse.sha256Digest) {
|
||||
|
||||
@@ -60,7 +60,7 @@ export class NetworkError extends Error {
|
||||
|
||||
export class UsageError extends Error {
|
||||
constructor() {
|
||||
const message = `Artifact storage quota has been hit. Unable to upload any new artifacts.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending`
|
||||
const message = `Artifact storage quota has been hit. Unable to upload any new artifacts. Usage is recalculated every 6-12 hours.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending`
|
||||
super(message)
|
||||
this.name = 'UsageError'
|
||||
}
|
||||
|
||||
@@ -50,13 +50,6 @@ export interface UploadArtifactOptions {
|
||||
* For large files that are not easily compressed, a value of 0 is recommended for significantly faster uploads.
|
||||
*/
|
||||
compressionLevel?: number
|
||||
/**
|
||||
* If true, the artifact will be uploaded without being archived (zipped).
|
||||
* This is only supported when uploading a single file.
|
||||
* When using this option, the artifact will not be compressed.
|
||||
* When using this option, the name parameter passed to the upload is ignored. Instead, the name of the file is used as the name of the artifact.
|
||||
*/
|
||||
skipArchive?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,12 +113,6 @@ export interface DownloadArtifactOptions {
|
||||
* matches the expected hash.
|
||||
*/
|
||||
expectedHash?: string
|
||||
|
||||
/**
|
||||
* If true, the downloaded artifact will not be automatically extracted/decompressed.
|
||||
* The artifact will be saved as-is to the destination path.
|
||||
*/
|
||||
skipDecompress?: boolean
|
||||
}
|
||||
|
||||
export interface StreamExtractResponse {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {BlobClient, BlockBlobUploadStreamOptions} from '@azure/storage-blob'
|
||||
import {TransferProgressEvent} from '@azure/core-http-compat'
|
||||
import {WaterMarkedUploadStream} from './stream.js'
|
||||
import {ZipUploadStream} from './zip.js'
|
||||
import {
|
||||
getUploadChunkSize,
|
||||
getConcurrency,
|
||||
@@ -23,10 +23,9 @@ export interface BlobUploadResponse {
|
||||
sha256Hash?: string
|
||||
}
|
||||
|
||||
export async function uploadToBlobStorage(
|
||||
export async function uploadZipToBlobStorage(
|
||||
authenticatedUploadURL: string,
|
||||
uploadStream: WaterMarkedUploadStream,
|
||||
contentType: string
|
||||
zipUploadStream: ZipUploadStream
|
||||
): Promise<BlobUploadResponse> {
|
||||
let uploadByteCount = 0
|
||||
let lastProgressTime = Date.now()
|
||||
@@ -52,7 +51,7 @@ export async function uploadToBlobStorage(
|
||||
const blockBlobClient = blobClient.getBlockBlobClient()
|
||||
|
||||
core.debug(
|
||||
`Uploading artifact to blob storage with maxConcurrency: ${maxConcurrency}, bufferSize: ${bufferSize}, contentType: ${contentType}`
|
||||
`Uploading artifact zip to blob storage with maxConcurrency: ${maxConcurrency}, bufferSize: ${bufferSize}`
|
||||
)
|
||||
|
||||
const uploadCallback = (progress: TransferProgressEvent): void => {
|
||||
@@ -62,24 +61,24 @@ export async function uploadToBlobStorage(
|
||||
}
|
||||
|
||||
const options: BlockBlobUploadStreamOptions = {
|
||||
blobHTTPHeaders: {blobContentType: contentType},
|
||||
blobHTTPHeaders: {blobContentType: 'zip'},
|
||||
onProgress: uploadCallback,
|
||||
abortSignal: abortController.signal
|
||||
}
|
||||
|
||||
let sha256Hash: string | undefined = undefined
|
||||
const blobUploadStream = new stream.PassThrough()
|
||||
const uploadStream = new stream.PassThrough()
|
||||
const hashStream = crypto.createHash('sha256')
|
||||
|
||||
uploadStream.pipe(blobUploadStream) // This stream is used for the upload
|
||||
uploadStream.pipe(hashStream).setEncoding('hex') // This stream is used to compute a hash of the content for integrity check
|
||||
zipUploadStream.pipe(uploadStream) // This stream is used for the upload
|
||||
zipUploadStream.pipe(hashStream).setEncoding('hex') // This stream is used to compute a hash of the zip content that gets used. Integrity check
|
||||
|
||||
core.info('Beginning upload of artifact content to blob storage')
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
blockBlobClient.uploadStream(
|
||||
blobUploadStream,
|
||||
uploadStream,
|
||||
bufferSize,
|
||||
maxConcurrency,
|
||||
options
|
||||
@@ -99,7 +98,7 @@ export async function uploadToBlobStorage(
|
||||
|
||||
hashStream.end()
|
||||
sha256Hash = hashStream.read() as string
|
||||
core.info(`SHA256 digest of uploaded artifact is ${sha256Hash}`)
|
||||
core.info(`SHA256 digest of uploaded artifact zip is ${sha256Hash}`)
|
||||
|
||||
if (uploadByteCount === 0) {
|
||||
core.warning(
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import * as stream from 'stream'
|
||||
import * as fs from 'fs'
|
||||
import {realpath} from 'fs/promises'
|
||||
import * as core from '@actions/core'
|
||||
import {getUploadChunkSize} from '../shared/config.js'
|
||||
|
||||
// Custom stream transformer so we can set the highWaterMark property
|
||||
// See https://github.com/nodejs/node/issues/8855
|
||||
export class WaterMarkedUploadStream extends stream.Transform {
|
||||
constructor(bufferSize: number) {
|
||||
super({
|
||||
highWaterMark: bufferSize
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
_transform(chunk: any, enc: any, cb: any): void {
|
||||
cb(null, chunk)
|
||||
}
|
||||
}
|
||||
|
||||
export async function createRawFileUploadStream(
|
||||
filePath: string
|
||||
): Promise<WaterMarkedUploadStream> {
|
||||
core.debug(`Creating raw file upload stream for: ${filePath}`)
|
||||
|
||||
const bufferSize = getUploadChunkSize()
|
||||
const uploadStream = new WaterMarkedUploadStream(bufferSize)
|
||||
|
||||
// Check if symlink and resolve the source path
|
||||
let sourcePath = filePath
|
||||
const stats = await fs.promises.lstat(filePath)
|
||||
if (stats.isSymbolicLink()) {
|
||||
sourcePath = await realpath(filePath)
|
||||
}
|
||||
|
||||
// Create a read stream from the file and pipe it to the upload stream
|
||||
const fileStream = fs.createReadStream(sourcePath, {
|
||||
highWaterMark: bufferSize
|
||||
})
|
||||
|
||||
fileStream.on('error', error => {
|
||||
core.error('An error has occurred while reading the file for upload')
|
||||
core.error(String(error))
|
||||
uploadStream.destroy(
|
||||
new Error('An error has occurred during file read for the artifact')
|
||||
)
|
||||
})
|
||||
|
||||
fileStream.pipe(uploadStream)
|
||||
|
||||
return uploadStream
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import * as path from 'path'
|
||||
|
||||
/**
|
||||
* Maps file extensions to MIME types
|
||||
*/
|
||||
const mimeTypes: Record<string, string> = {
|
||||
// Text
|
||||
'.txt': 'text/plain',
|
||||
'.html': 'text/html',
|
||||
'.htm': 'text/html',
|
||||
'.css': 'text/css',
|
||||
'.csv': 'text/csv',
|
||||
'.xml': 'text/xml',
|
||||
'.md': 'text/markdown',
|
||||
|
||||
// JavaScript/JSON
|
||||
'.js': 'application/javascript',
|
||||
'.mjs': 'application/javascript',
|
||||
'.json': 'application/json',
|
||||
|
||||
// Images
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.webp': 'image/webp',
|
||||
'.ico': 'image/x-icon',
|
||||
'.bmp': 'image/bmp',
|
||||
'.tiff': 'image/tiff',
|
||||
'.tif': 'image/tiff',
|
||||
|
||||
// Audio
|
||||
'.mp3': 'audio/mpeg',
|
||||
'.wav': 'audio/wav',
|
||||
'.ogg': 'audio/ogg',
|
||||
'.flac': 'audio/flac',
|
||||
|
||||
// Video
|
||||
'.mp4': 'video/mp4',
|
||||
'.webm': 'video/webm',
|
||||
'.avi': 'video/x-msvideo',
|
||||
'.mov': 'video/quicktime',
|
||||
|
||||
// Documents
|
||||
'.pdf': 'application/pdf',
|
||||
'.doc': 'application/msword',
|
||||
'.docx':
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'.xls': 'application/vnd.ms-excel',
|
||||
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'.ppt': 'application/vnd.ms-powerpoint',
|
||||
'.pptx':
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
|
||||
// Archives
|
||||
'.zip': 'application/zip',
|
||||
'.tar': 'application/x-tar',
|
||||
'.gz': 'application/gzip',
|
||||
'.rar': 'application/vnd.rar',
|
||||
'.7z': 'application/x-7z-compressed',
|
||||
|
||||
// Code/Data
|
||||
'.wasm': 'application/wasm',
|
||||
'.yaml': 'application/x-yaml',
|
||||
'.yml': 'application/x-yaml',
|
||||
|
||||
// Fonts
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
'.ttf': 'font/ttf',
|
||||
'.otf': 'font/otf',
|
||||
'.eot': 'application/vnd.ms-fontobject'
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the MIME type for a file based on its extension
|
||||
*/
|
||||
export function getMimeType(filePath: string): string {
|
||||
const ext = path.extname(filePath).toLowerCase()
|
||||
return mimeTypes[ext] || 'application/octet-stream'
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import {
|
||||
UploadArtifactOptions,
|
||||
UploadArtifactResponse
|
||||
@@ -14,16 +12,14 @@ import {
|
||||
validateRootDirectory
|
||||
} from './upload-zip-specification.js'
|
||||
import {getBackendIdsFromToken} from '../shared/util.js'
|
||||
import {uploadToBlobStorage} from './blob-upload.js'
|
||||
import {uploadZipToBlobStorage} from './blob-upload.js'
|
||||
import {createZipUploadStream} from './zip.js'
|
||||
import {createRawFileUploadStream, WaterMarkedUploadStream} from './stream.js'
|
||||
import {
|
||||
CreateArtifactRequest,
|
||||
FinalizeArtifactRequest,
|
||||
StringValue
|
||||
} from '../../generated/index.js'
|
||||
import {FilesNotFoundError, InvalidResponseError} from '../shared/errors.js'
|
||||
import {getMimeType} from './types.js'
|
||||
|
||||
export async function uploadArtifact(
|
||||
name: string,
|
||||
@@ -31,43 +27,19 @@ export async function uploadArtifact(
|
||||
rootDirectory: string,
|
||||
options?: UploadArtifactOptions | undefined
|
||||
): Promise<UploadArtifactResponse> {
|
||||
let artifactFileName = `${name}.zip`
|
||||
if (options?.skipArchive) {
|
||||
if (files.length === 0) {
|
||||
throw new FilesNotFoundError([])
|
||||
}
|
||||
|
||||
if (files.length > 1) {
|
||||
throw new Error(
|
||||
'skipArchive option is only supported when uploading a single file'
|
||||
)
|
||||
}
|
||||
|
||||
if (!fs.existsSync(files[0])) {
|
||||
throw new FilesNotFoundError(files)
|
||||
}
|
||||
|
||||
artifactFileName = path.basename(files[0])
|
||||
name = artifactFileName
|
||||
}
|
||||
|
||||
validateArtifactName(name)
|
||||
validateRootDirectory(rootDirectory)
|
||||
|
||||
let zipSpecification: UploadZipSpecification[] = []
|
||||
|
||||
if (!options?.skipArchive) {
|
||||
zipSpecification = getUploadZipSpecification(files, rootDirectory)
|
||||
|
||||
if (zipSpecification.length === 0) {
|
||||
throw new FilesNotFoundError(
|
||||
zipSpecification.flatMap(s => (s.sourcePath ? [s.sourcePath] : []))
|
||||
)
|
||||
}
|
||||
const zipSpecification: UploadZipSpecification[] = getUploadZipSpecification(
|
||||
files,
|
||||
rootDirectory
|
||||
)
|
||||
if (zipSpecification.length === 0) {
|
||||
throw new FilesNotFoundError(
|
||||
zipSpecification.flatMap(s => (s.sourcePath ? [s.sourcePath] : []))
|
||||
)
|
||||
}
|
||||
|
||||
const contentType = getMimeType(artifactFileName)
|
||||
|
||||
// get the IDs needed for the artifact creation
|
||||
const backendIds = getBackendIdsFromToken()
|
||||
|
||||
@@ -79,8 +51,7 @@ export async function uploadArtifact(
|
||||
workflowRunBackendId: backendIds.workflowRunBackendId,
|
||||
workflowJobRunBackendId: backendIds.workflowJobRunBackendId,
|
||||
name,
|
||||
mimeType: StringValue.create({value: contentType}),
|
||||
version: 7
|
||||
version: 4
|
||||
}
|
||||
|
||||
// if there is a retention period, add it to the request
|
||||
@@ -97,24 +68,15 @@ export async function uploadArtifact(
|
||||
)
|
||||
}
|
||||
|
||||
let stream: WaterMarkedUploadStream
|
||||
const zipUploadStream = await createZipUploadStream(
|
||||
zipSpecification,
|
||||
options?.compressionLevel
|
||||
)
|
||||
|
||||
if (options?.skipArchive) {
|
||||
// Upload raw file without archiving
|
||||
stream = await createRawFileUploadStream(files[0])
|
||||
} else {
|
||||
// Create and upload zip archive
|
||||
stream = await createZipUploadStream(
|
||||
zipSpecification,
|
||||
options?.compressionLevel
|
||||
)
|
||||
}
|
||||
|
||||
core.info(`Uploading artifact: ${artifactFileName}`)
|
||||
const uploadResult = await uploadToBlobStorage(
|
||||
// Upload zip to blob storage
|
||||
const uploadResult = await uploadZipToBlobStorage(
|
||||
createArtifactResp.signedUploadUrl,
|
||||
stream,
|
||||
contentType
|
||||
zipUploadStream
|
||||
)
|
||||
|
||||
// finalize the artifact
|
||||
@@ -143,7 +105,7 @@ export async function uploadArtifact(
|
||||
|
||||
const artifactId = BigInt(finalizeArtifactResp.artifactId)
|
||||
core.info(
|
||||
`Artifact ${name} successfully finalized. Artifact ID ${artifactId}`
|
||||
`Artifact ${name}.zip successfully finalized. Artifact ID ${artifactId}`
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
import * as stream from 'stream'
|
||||
import {realpath} from 'fs/promises'
|
||||
import archiver from 'archiver'
|
||||
import * as core from '@actions/core'
|
||||
import {UploadZipSpecification} from './upload-zip-specification.js'
|
||||
import {getUploadChunkSize} from '../shared/config.js'
|
||||
import {WaterMarkedUploadStream} from './stream.js'
|
||||
|
||||
export const DEFAULT_COMPRESSION_LEVEL = 6
|
||||
|
||||
// Custom stream transformer so we can set the highWaterMark property
|
||||
// See https://github.com/nodejs/node/issues/8855
|
||||
export class ZipUploadStream extends stream.Transform {
|
||||
constructor(bufferSize: number) {
|
||||
super({
|
||||
highWaterMark: bufferSize
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
_transform(chunk: any, enc: any, cb: any): void {
|
||||
cb(null, chunk)
|
||||
}
|
||||
}
|
||||
|
||||
export async function createZipUploadStream(
|
||||
uploadSpecification: UploadZipSpecification[],
|
||||
compressionLevel: number = DEFAULT_COMPRESSION_LEVEL
|
||||
): Promise<WaterMarkedUploadStream> {
|
||||
): Promise<ZipUploadStream> {
|
||||
core.debug(
|
||||
`Creating Artifact archive with compressionLevel: ${compressionLevel}`
|
||||
)
|
||||
@@ -45,7 +60,7 @@ export async function createZipUploadStream(
|
||||
}
|
||||
|
||||
const bufferSize = getUploadChunkSize()
|
||||
const zipUploadStream = new WaterMarkedUploadStream(bufferSize)
|
||||
const zipUploadStream = new ZipUploadStream(bufferSize)
|
||||
|
||||
core.debug(
|
||||
`Zip write high watermark value ${zipUploadStream.writableHighWaterMark}`
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
# @actions/attest Releases
|
||||
|
||||
## 3.2.0
|
||||
|
||||
- Add custom user-agent for more API calls [#2321](https://github.com/actions/toolkit/pull/2321)
|
||||
|
||||
## 3.1.0
|
||||
|
||||
- Add support for `ACTIONS_ORCHESTRATION_ID` in user-agent [#2320](https://github.com/actions/toolkit/pull/2320)
|
||||
|
||||
## 3.0.0
|
||||
|
||||
- **Breaking change**: Package is now ESM-only
|
||||
|
||||
Generated
+5
-5
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@actions/attest",
|
||||
"version": "3.2.0",
|
||||
"version": "3.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@actions/attest",
|
||||
"version": "3.2.0",
|
||||
"version": "3.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^3.0.0",
|
||||
@@ -1529,9 +1529,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.10",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz",
|
||||
"integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==",
|
||||
"version": "7.5.7",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
|
||||
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/attest",
|
||||
"version": "3.2.0",
|
||||
"version": "3.0.0",
|
||||
"description": "Actions attestation lib",
|
||||
"keywords": [
|
||||
"github",
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: run tests from root\" && exit 1",
|
||||
"tsc": "tsc && cp src/internal/package-version.cjs lib/internal/"
|
||||
"tsc": "tsc"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/actions/toolkit/issues"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as github from '@actions/github'
|
||||
import {retry} from '@octokit/plugin-retry'
|
||||
import {RequestHeaders} from '@octokit/types'
|
||||
import {getUserAgent} from './internal/utils.js'
|
||||
|
||||
const CREATE_STORAGE_RECORD_REQUEST =
|
||||
'POST /orgs/{owner}/artifacts/metadata/storage-record'
|
||||
@@ -53,16 +52,10 @@ export async function createStorageRecord(
|
||||
): Promise<number[]> {
|
||||
const retries = retryAttempts ?? DEFAULT_RETRY_COUNT
|
||||
const octokit = github.getOctokit(token, {retry: {retries}}, retry)
|
||||
|
||||
const headersWithUserAgent = {
|
||||
'User-Agent': getUserAgent(),
|
||||
...headers
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await octokit.request(CREATE_STORAGE_RECORD_REQUEST, {
|
||||
owner: github.context.repo.owner,
|
||||
headers: headersWithUserAgent,
|
||||
headers,
|
||||
...buildRequestParams(artifactOptions, packageRegistryOptions)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// This file exists as a CommonJS module to read the version from package.json.
|
||||
// In an ESM package, using `require()` directly in .ts files requires disabling
|
||||
// ESLint rules and doesn't work reliably across all Node.js versions.
|
||||
// By keeping this as a .cjs file, we can use require() naturally and export
|
||||
// the version for the ESM modules to import.
|
||||
const packageJson = require('../../package.json')
|
||||
module.exports = {version: packageJson.version}
|
||||
@@ -1,15 +0,0 @@
|
||||
import {version} from './package-version.cjs'
|
||||
|
||||
export const getUserAgent = (): string => {
|
||||
const baseUserAgent = `@actions/attest-${version}`
|
||||
|
||||
const orchId = process.env['ACTIONS_ORCHESTRATION_ID']
|
||||
if (orchId) {
|
||||
// Sanitize the orchestration ID to ensure it contains only valid characters
|
||||
// Valid characters: 0-9, a-z, _, -, .
|
||||
const sanitizedId = orchId.replace(/[^a-z0-9_.-]/gi, '_')
|
||||
return `${baseUserAgent} actions_orchestration_id/${sanitizedId}`
|
||||
}
|
||||
|
||||
return baseUserAgent
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as github from '@actions/github'
|
||||
import {retry} from '@octokit/plugin-retry'
|
||||
import {RequestHeaders} from '@octokit/types'
|
||||
import {getUserAgent} from './internal/utils.js'
|
||||
|
||||
const CREATE_ATTESTATION_REQUEST = 'POST /repos/{owner}/{repo}/attestations'
|
||||
const DEFAULT_RETRY_COUNT = 5
|
||||
@@ -25,16 +24,11 @@ export const writeAttestation = async (
|
||||
const retries = options.retry ?? DEFAULT_RETRY_COUNT
|
||||
const octokit = github.getOctokit(token, {retry: {retries}}, retry)
|
||||
|
||||
const headers = {
|
||||
'User-Agent': getUserAgent(),
|
||||
...options.headers
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await octokit.request(CREATE_ATTESTATION_REQUEST, {
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
headers,
|
||||
headers: options.headers,
|
||||
bundle: attestation as {
|
||||
mediaType?: string
|
||||
verificationMaterial?: {[key: string]: unknown}
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ export class NetworkError extends Error {
|
||||
|
||||
export class UsageError extends Error {
|
||||
constructor() {
|
||||
const message = `Cache storage quota has been hit. Unable to upload any new cache entries.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending`
|
||||
const message = `Cache storage quota has been hit. Unable to upload any new cache entries. Usage is recalculated every 6-12 hours.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending`
|
||||
super(message)
|
||||
this.name = 'UsageError'
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# @actions/core Releases
|
||||
|
||||
## 3.0.1
|
||||
|
||||
- Bump `undici` from `6.23.0` to `6.24.1` [#2348](https://github.com/actions/toolkit/pull/2348)
|
||||
|
||||
## 3.0.0
|
||||
|
||||
- **Breaking change**: Package is now ESM-only
|
||||
|
||||
@@ -677,8 +677,5 @@ describe('oidc-client-tests', () => {
|
||||
const http = new HttpClient('actions/oidc-client')
|
||||
const res = await http.get(getTokenEndPoint())
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
// Consume the response to close the socket
|
||||
await res.readBody()
|
||||
res.message.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
Generated
+5
-5
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@actions/core",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@actions/core",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/exec": "^3.0.0",
|
||||
@@ -61,9 +61,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.24.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
|
||||
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/core",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.0",
|
||||
"description": "Actions core lib",
|
||||
"keywords": [
|
||||
"github",
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
# @actions/github Releases
|
||||
|
||||
### 9.1.1
|
||||
|
||||
- Bump `undici` from `6.23.0` to `6.24.0` [#2346](https://github.com/actions/toolkit/pull/2346)
|
||||
|
||||
### 9.1.0
|
||||
|
||||
- Append `actions_orchestration_id` to user-agent when the `ACTIONS_ORCHESTRATION_ID` environment variable is set [#2364](https://github.com/actions/toolkit/pull/2364)
|
||||
|
||||
### 9.0.0
|
||||
|
||||
- **Breaking change**: Package is now ESM-only
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
import {getOctokitOptions, getUserAgentWithOrchestrationId} from '../src/utils'
|
||||
import {getUserAgentWithOrchestrationId as internalGetUserAgentWithOrchestrationId} from '../src/internal/utils'
|
||||
|
||||
describe('orchestration ID support', () => {
|
||||
let originalOrchId: string | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
originalOrchId = process.env['ACTIONS_ORCHESTRATION_ID']
|
||||
delete process.env['ACTIONS_ORCHESTRATION_ID']
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalOrchId !== undefined) {
|
||||
process.env['ACTIONS_ORCHESTRATION_ID'] = originalOrchId
|
||||
} else {
|
||||
delete process.env['ACTIONS_ORCHESTRATION_ID']
|
||||
}
|
||||
})
|
||||
|
||||
describe('getUserAgentWithOrchestrationId', () => {
|
||||
it('returns undefined when env var is not set and no base user agent', () => {
|
||||
expect(getUserAgentWithOrchestrationId()).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns base user agent unchanged when env var is not set', () => {
|
||||
expect(getUserAgentWithOrchestrationId('my-app')).toBe('my-app')
|
||||
})
|
||||
|
||||
it('returns orchestration ID without base when env var is set and no base', () => {
|
||||
process.env['ACTIONS_ORCHESTRATION_ID'] = 'abc-123'
|
||||
expect(getUserAgentWithOrchestrationId()).toBe(
|
||||
'actions_orchestration_id/abc-123'
|
||||
)
|
||||
})
|
||||
|
||||
it('appends orchestration ID to base user agent', () => {
|
||||
process.env['ACTIONS_ORCHESTRATION_ID'] = 'abc-123'
|
||||
expect(getUserAgentWithOrchestrationId('my-app')).toBe(
|
||||
'my-app actions_orchestration_id/abc-123'
|
||||
)
|
||||
})
|
||||
|
||||
it('sanitizes special characters in orchestration ID', () => {
|
||||
process.env['ACTIONS_ORCHESTRATION_ID'] = 'id with spaces/and$pecial!'
|
||||
expect(getUserAgentWithOrchestrationId('my-app')).toBe(
|
||||
'my-app actions_orchestration_id/id_with_spaces_and_pecial_'
|
||||
)
|
||||
})
|
||||
|
||||
it('preserves allowed characters in orchestration ID', () => {
|
||||
process.env['ACTIONS_ORCHESTRATION_ID'] =
|
||||
'valid_id-with.allowed_chars.123'
|
||||
expect(getUserAgentWithOrchestrationId()).toBe(
|
||||
'actions_orchestration_id/valid_id-with.allowed_chars.123'
|
||||
)
|
||||
})
|
||||
|
||||
it('ignores whitespace-only orchestration ID', () => {
|
||||
process.env['ACTIONS_ORCHESTRATION_ID'] = ' '
|
||||
expect(getUserAgentWithOrchestrationId('my-app')).toBe('my-app')
|
||||
})
|
||||
|
||||
it('does not duplicate orchestration ID if already present in base', () => {
|
||||
process.env['ACTIONS_ORCHESTRATION_ID'] = 'abc-123'
|
||||
const alreadyTagged = 'my-app actions_orchestration_id/abc-123'
|
||||
expect(getUserAgentWithOrchestrationId(alreadyTagged)).toBe(alreadyTagged)
|
||||
})
|
||||
})
|
||||
|
||||
describe('public re-export', () => {
|
||||
it('exports getUserAgentWithOrchestrationId from utils (public API)', () => {
|
||||
expect(getUserAgentWithOrchestrationId).toBe(
|
||||
internalGetUserAgentWithOrchestrationId
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getOctokitOptions', () => {
|
||||
it('sets userAgent when ACTIONS_ORCHESTRATION_ID is set', () => {
|
||||
process.env['ACTIONS_ORCHESTRATION_ID'] = 'test-orch-id'
|
||||
const opts = getOctokitOptions('fake-token')
|
||||
expect(opts.userAgent).toBe('actions_orchestration_id/test-orch-id')
|
||||
})
|
||||
|
||||
it('does not set userAgent when ACTIONS_ORCHESTRATION_ID is not set', () => {
|
||||
const opts = getOctokitOptions('fake-token')
|
||||
expect(opts.userAgent).toBeUndefined()
|
||||
})
|
||||
|
||||
it('preserves and appends to caller-provided userAgent', () => {
|
||||
process.env['ACTIONS_ORCHESTRATION_ID'] = 'test-orch-id'
|
||||
const opts = getOctokitOptions('fake-token', {
|
||||
userAgent: 'custom-agent/1.0'
|
||||
})
|
||||
expect(opts.userAgent).toBe(
|
||||
'custom-agent/1.0 actions_orchestration_id/test-orch-id'
|
||||
)
|
||||
})
|
||||
|
||||
it('leaves caller-provided userAgent intact when env var is not set', () => {
|
||||
const opts = getOctokitOptions('fake-token', {
|
||||
userAgent: 'custom-agent/1.0'
|
||||
})
|
||||
expect(opts.userAgent).toBe('custom-agent/1.0')
|
||||
})
|
||||
|
||||
it('does not mutate the original options object', () => {
|
||||
process.env['ACTIONS_ORCHESTRATION_ID'] = 'test-orch-id'
|
||||
const original = {userAgent: 'original/1.0'}
|
||||
getOctokitOptions('fake-token', original)
|
||||
expect(original.userAgent).toBe('original/1.0')
|
||||
})
|
||||
|
||||
it('sanitizes special characters through getOctokitOptions', () => {
|
||||
process.env['ACTIONS_ORCHESTRATION_ID'] = 'bad chars here!'
|
||||
const opts = getOctokitOptions('fake-token')
|
||||
expect(opts.userAgent).toBe('actions_orchestration_id/bad_chars_here_')
|
||||
})
|
||||
|
||||
it('does not duplicate orchestration ID when caller already applied it', () => {
|
||||
process.env['ACTIONS_ORCHESTRATION_ID'] = 'test-orch-id'
|
||||
const opts = getOctokitOptions('fake-token', {
|
||||
userAgent: 'my-app actions_orchestration_id/test-orch-id'
|
||||
})
|
||||
expect(opts.userAgent).toBe(
|
||||
'my-app actions_orchestration_id/test-orch-id'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
Generated
+5
-5
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@actions/github",
|
||||
"version": "9.1.1",
|
||||
"version": "9.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@actions/github",
|
||||
"version": "9.1.1",
|
||||
"version": "9.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^3.0.2",
|
||||
@@ -363,9 +363,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.24.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.0.tgz",
|
||||
"integrity": "sha512-lVLNosgqo5EkGqh5XUDhGfsMSoO8K0BAN0TyJLvwNRSl4xWGZlCVYsAIpa/OpA3TvmnM01GWcoKmc3ZWo5wKKA==",
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/github",
|
||||
"version": "9.1.1",
|
||||
"version": "9.0.0",
|
||||
"description": "Actions github lib",
|
||||
"keywords": [
|
||||
"github",
|
||||
|
||||
@@ -42,17 +42,3 @@ export function getProxyFetch(destinationUrl): typeof fetch {
|
||||
export function getApiBaseUrl(): string {
|
||||
return process.env['GITHUB_API_URL'] || 'https://api.github.com'
|
||||
}
|
||||
|
||||
export function getUserAgentWithOrchestrationId(
|
||||
baseUserAgent?: string
|
||||
): string | undefined {
|
||||
const orchId = process.env['ACTIONS_ORCHESTRATION_ID']?.trim()
|
||||
if (orchId) {
|
||||
const sanitizedId = orchId.replace(/[^a-z0-9_.-]/gi, '_')
|
||||
const tag = `actions_orchestration_id/${sanitizedId}`
|
||||
if (baseUserAgent?.includes(tag)) return baseUserAgent
|
||||
const ua = baseUserAgent ? `${baseUserAgent} ` : ''
|
||||
return `${ua}${tag}`
|
||||
}
|
||||
return baseUserAgent
|
||||
}
|
||||
|
||||
@@ -23,8 +23,6 @@ export const GitHub = Octokit.plugin(
|
||||
paginateRest
|
||||
).defaults(defaults)
|
||||
|
||||
export {getUserAgentWithOrchestrationId} from './internal/utils.js'
|
||||
|
||||
/**
|
||||
* Convience function to correctly format Octokit Options to pass into the constructor.
|
||||
*
|
||||
@@ -43,13 +41,5 @@ export function getOctokitOptions(
|
||||
opts.auth = auth
|
||||
}
|
||||
|
||||
// Orchestration ID
|
||||
const userAgent = Utils.getUserAgentWithOrchestrationId(
|
||||
opts.userAgent as string | undefined
|
||||
)
|
||||
if (userAgent) {
|
||||
opts.userAgent = userAgent
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# @actions/glob Releases
|
||||
|
||||
## 0.7.0
|
||||
|
||||
- Bump `minimatch` from `^3.0.4` to `^10.2.5` [#2355](https://github.com/actions/toolkit/pull/2355)
|
||||
- Bump `undici` from `6.23.0` to `6.24.0` [#2345](https://github.com/actions/toolkit/pull/2345)
|
||||
- Bump `brace-expansion` in `/packages/glob` [#2369](https://github.com/actions/toolkit/pull/2369)
|
||||
|
||||
## 0.6.1
|
||||
|
||||
- Fix a bad import for `minimatch`
|
||||
|
||||
@@ -303,7 +303,7 @@ describe('pattern', () => {
|
||||
expect(pattern.match(`${root}foo/bar/baz`)).toBeFalsy()
|
||||
pattern = new Pattern(`${root}foo/b[!]r/b*`)
|
||||
expect(pattern.searchPath).toBe(`${root}foo${path.sep}b!r`)
|
||||
expect(pattern.match(`${root}foo/b!r/baz`)).toBeFalsy()
|
||||
expect(pattern.match(`${root}foo/b!r/baz`)).toBeTruthy()
|
||||
pattern = new Pattern(`${root}foo/b[[]ar/b*`)
|
||||
expect(pattern.searchPath).toBe(`${root}foo${path.sep}b[ar`)
|
||||
expect(pattern.match(`${root}foo/b[ar/baz`)).toBeTruthy()
|
||||
@@ -340,18 +340,9 @@ describe('pattern', () => {
|
||||
pattern = new Pattern('C:/foo/b\\[a]r/b*')
|
||||
expect(pattern.searchPath).toBe(`C:\\foo\\b\\ar`)
|
||||
expect(pattern.match('C:/foo/b/ar/baz')).toBeTruthy()
|
||||
|
||||
// Regression testing for minimatch v3
|
||||
// Historically, minimatch/glob had a bug when parsing a character class
|
||||
// containing an escaped '!' (e.g. `[\\!]`). In some cases, the internal
|
||||
// pattern construction would incorrectly insert the literal string
|
||||
// "undefined" into the generated pattern/segment, which could make a
|
||||
// pattern intended to match `b[\\!]r` also match a path segment like
|
||||
// `b[undefined/!]r`. This test ensures that a pattern with a literal
|
||||
// `[\\!]` in the directory name does *not* match such malformed paths.
|
||||
pattern = new Pattern('C:/foo/b[\\!]r/b*')
|
||||
expect(pattern.searchPath).toBe('C:\\foo\\b[\\!]r')
|
||||
expect(pattern.match('C:/foo/b[undefined/!]r/baz')).toBeFalsy()
|
||||
expect(pattern.match('C:/foo/b[undefined/!]r/baz')).toBeTruthy() // Note, "undefined" substr to accommodate a bug in Minimatch when nocase=true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Generated
+27
-29
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "@actions/glob",
|
||||
"version": "0.7.0",
|
||||
"version": "0.6.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@actions/glob",
|
||||
"version": "0.7.0",
|
||||
"version": "0.6.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^3.0.0",
|
||||
"minimatch": "^10.2.5"
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/core": {
|
||||
@@ -49,39 +49,37 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel": {
|
||||
@@ -94,9 +92,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.24.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.0.tgz",
|
||||
"integrity": "sha512-lVLNosgqo5EkGqh5XUDhGfsMSoO8K0BAN0TyJLvwNRSl4xWGZlCVYsAIpa/OpA3TvmnM01GWcoKmc3ZWo5wKKA==",
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/glob",
|
||||
"version": "0.7.0",
|
||||
"version": "0.6.1",
|
||||
"preview": true,
|
||||
"description": "Actions glob lib",
|
||||
"keywords": [
|
||||
@@ -45,6 +45,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^3.0.0",
|
||||
"minimatch": "^10.2.5"
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,14 @@ import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
import * as pathHelper from './internal-path-helper.js'
|
||||
import assert from 'assert'
|
||||
import {Minimatch, type MinimatchOptions} from 'minimatch'
|
||||
import minimatch from 'minimatch'
|
||||
import {MatchKind} from './internal-match-kind.js'
|
||||
import {Path} from './internal-path.js'
|
||||
|
||||
type IMinimatch = minimatch.IMinimatch
|
||||
type IMinimatchOptions = minimatch.IOptions
|
||||
const {Minimatch} = minimatch
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32'
|
||||
|
||||
export class Pattern {
|
||||
@@ -34,7 +38,7 @@ export class Pattern {
|
||||
/**
|
||||
* The Minimatch object used for matching
|
||||
*/
|
||||
private readonly minimatch: Minimatch
|
||||
private readonly minimatch: IMinimatch
|
||||
|
||||
/**
|
||||
* Used to workaround a limitation with Minimatch when determining a partial
|
||||
@@ -122,7 +126,7 @@ export class Pattern {
|
||||
this.isImplicitPattern = isImplicitPattern
|
||||
|
||||
// Create minimatch
|
||||
const minimatchOptions: MinimatchOptions = {
|
||||
const minimatchOptions: IMinimatchOptions = {
|
||||
dot: true,
|
||||
nobrace: true,
|
||||
nocase: IS_WINDOWS,
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# Releases
|
||||
|
||||
## 4.0.1
|
||||
|
||||
- Bump `undici` from `6.23.0` to `6.24.0` [#2347](https://github.com/actions/toolkit/pull/2347)
|
||||
|
||||
## 4.0.0
|
||||
|
||||
- **Breaking change**: Package is now ESM-only
|
||||
|
||||
@@ -238,8 +238,6 @@ describe('basics', () => {
|
||||
'https://postman-echo.com/get'
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
// Consume the response to close the socket
|
||||
res.message.destroy()
|
||||
})
|
||||
|
||||
it('does basic http delete request', async () => {
|
||||
|
||||
Generated
+5
-5
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@actions/http-client",
|
||||
"version": "4.0.1",
|
||||
"version": "4.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@actions/http-client",
|
||||
"version": "4.0.1",
|
||||
"version": "4.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tunnel": "^0.0.6",
|
||||
@@ -232,9 +232,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.24.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.0.tgz",
|
||||
"integrity": "sha512-lVLNosgqo5EkGqh5XUDhGfsMSoO8K0BAN0TyJLvwNRSl4xWGZlCVYsAIpa/OpA3TvmnM01GWcoKmc3ZWo5wKKA==",
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/http-client",
|
||||
"version": "4.0.1",
|
||||
"version": "4.0.0",
|
||||
"description": "Actions Http Client",
|
||||
"keywords": [
|
||||
"github",
|
||||
|
||||
Reference in New Issue
Block a user