Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fad1bf5141 | |||
| 3491e2eeea | |||
| 208fa83feb | |||
| 393feda10a | |||
| d972090333 | |||
| fbdf27470c | |||
| ff45a53422 | |||
| 15fef78171 | |||
| dd046652c3 | |||
| cf3d93512b | |||
| 3512925c1c | |||
| e76decaf8a | |||
| 8afb976445 | |||
| b05573d945 | |||
| 634dc61da2 | |||
| aad34ab0bc | |||
| fac664b5d0 | |||
| 74236358e6 | |||
| ac7b0e436e | |||
| 440a06ef56 | |||
| 547a77cf75 | |||
| a6966e3148 | |||
| 92488b8ab2 | |||
| f628f161c4 | |||
| ea2465fe63 | |||
| 770dc3a982 | |||
| 418978c2e0 | |||
| fc00528337 | |||
| bd9017e99f | |||
| de122731f3 | |||
| 4dd900dde0 | |||
| 383ec9fb03 | |||
| c3478210af | |||
| 228a9534d1 | |||
| 2202465c69 | |||
| ccd1dd298f | |||
| 825204968b | |||
| 85f6235ca9 | |||
| bfdba95ece | |||
| c861dd8859 | |||
| 73d5917a6b | |||
| 4e1ffd548b | |||
| 42b3ff04b2 | |||
| 8d11ee5a8c | |||
| 7ad5004f87 | |||
| b593d1deb4 | |||
| 5be846b72d | |||
| dc491a61ca | |||
| a600dd34a5 | |||
| d4e990d92f | |||
| 05b1692026 | |||
| 8ed9455d68 | |||
| b55731c11b | |||
| 6b83f0554a | |||
| 990647a104 | |||
| 520206f818 | |||
| ff4308098f | |||
| 2bf7365352 | |||
| e7eb2c7418 | |||
| 0b69311011 | |||
| 5e5e1b7aac | |||
| 5e8657cf12 | |||
| 678f278caa | |||
| f1b118b2a9 | |||
| 464aebd43b | |||
| c3c81d44c1 | |||
| af82147423 | |||
| 4f7fb6513a | |||
| 2178f0baee | |||
| a5e05630d8 | |||
| 0759cdc230 | |||
| da34bfb74d | |||
| bb290885a3 | |||
| ace7a82469 | |||
| 71b19c1d65 | |||
| c9819f79d2 | |||
| c2bc747506 | |||
| ea69f13737 | |||
| 7f7e22a940 | |||
| de52c861c1 | |||
| 1ef26b2390 | |||
| 9ad01e4fd3 | |||
| 6c5508d1fb | |||
| b2cba168a2 | |||
| e3c6237940 | |||
| fc28be88e4 | |||
| 905b2c7b06 | |||
| 3a9dc00629 | |||
| ccad19055e | |||
| cb18a3df6b | |||
| 781092b1d1 |
@@ -2,7 +2,7 @@ name: artifact-unit-tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
pull_request:
|
||||
@@ -46,9 +46,10 @@ jobs:
|
||||
working-directory: packages/artifact
|
||||
|
||||
- name: Set artifact file contents
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::set-env name=non-gzip-artifact-content::hello"
|
||||
echo "::set-env name=gzip-artifact-content::Some large amount of text that has a compression ratio that is greater than 100%. If greater than 100%, gzip is used to upload the file"
|
||||
echo "non-gzip-artifact-content=hello" >> $GITHUB_ENV
|
||||
echo "gzip-artifact-content=Some large amount of text that has a compression ratio that is greater than 100%. If greater than 100%, gzip is used to upload the file" >> $GITHUB_ENV
|
||||
|
||||
- name: Create files that will be uploaded
|
||||
run: |
|
||||
|
||||
@@ -2,7 +2,7 @@ name: toolkit-audit
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
pull_request:
|
||||
@@ -31,8 +31,8 @@ jobs:
|
||||
- name: Bootstrap
|
||||
run: npm run bootstrap
|
||||
|
||||
- name: audit tools
|
||||
run: npm audit --audit-level=moderate
|
||||
# - name: audit tools #disabled while we wait for https://github.com/actions/toolkit/issues/539
|
||||
# run: npm audit --audit-level=moderate
|
||||
|
||||
- name: audit packages
|
||||
run: npm run audit-all
|
||||
|
||||
@@ -2,7 +2,7 @@ name: cache-unit-tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
pull_request:
|
||||
|
||||
@@ -2,6 +2,7 @@ name: "Code Scanning - Action"
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
name: Publish NPM
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
required: true
|
||||
description: 'core, artifact, cache, exec, github, glob, io, tool-cache'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: setup repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: verify package exists
|
||||
run: ls packages/${{ github.event.inputs.package }}
|
||||
|
||||
- name: npm install
|
||||
run: npm install
|
||||
|
||||
- name: bootstrap
|
||||
run: npm run bootstrap
|
||||
|
||||
- name: build
|
||||
run: npm run build
|
||||
|
||||
- name: test
|
||||
run: npm run test
|
||||
|
||||
- name: pack
|
||||
run: npm pack
|
||||
working-directory: packages/${{ github.event.inputs.package }}
|
||||
|
||||
- name: upload artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: ${{ github.event.inputs.package }}
|
||||
path: packages/${{ github.event.inputs.package }}/*.tgz
|
||||
|
||||
publish:
|
||||
runs-on: macos-latest
|
||||
needs: test
|
||||
environment: npm-publish
|
||||
steps:
|
||||
|
||||
- name: download artifact
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: ${{ github.event.inputs.package }}
|
||||
|
||||
- name: setup authentication
|
||||
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.TOKEN }}
|
||||
|
||||
- name: publish
|
||||
run: npm publish *.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 ${{ 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 ${{ github.event.inputs.package }}"}' $SLACK_WEBHOOK
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK }}
|
||||
|
||||
@@ -2,7 +2,7 @@ name: toolkit-unit-tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
pull_request:
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
name: "UpdateOctokit"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 18 * * 0' # sunday at 18 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
UpdateOctokit:
|
||||
@@ -37,7 +36,7 @@ jobs:
|
||||
script: |
|
||||
github.pulls.create(
|
||||
{
|
||||
base: "master",
|
||||
base: "main",
|
||||
owner: "${{github.repository_owner}}",
|
||||
repo: "toolkit",
|
||||
title: "Update Octokit dependencies",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
* @actions/actions-runtime
|
||||
|
||||
/packages/artifact/ @actions/actions-service
|
||||
/packages/cache/ @actions/actions-service
|
||||
@@ -1,3 +1,5 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2019 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
@@ -48,7 +48,7 @@ $ npm install @actions/glob
|
||||
|
||||
:pencil2: [@actions/io](packages/io)
|
||||
|
||||
Provides disk i/o functions like cp, mv, rmRF, find etc. Read more [here](packages/io)
|
||||
Provides disk i/o functions like cp, mv, rmRF, which etc. Read more [here](packages/io)
|
||||
|
||||
```bash
|
||||
$ npm install @actions/io
|
||||
|
||||
@@ -32,14 +32,14 @@ jobs:
|
||||
os: [ubuntu-16.04, windows-2019]
|
||||
runs-on: ${{matrix.os}}
|
||||
actions:
|
||||
- uses: actions/setup-node@master
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
version: ${{matrix.node}}
|
||||
- run: |
|
||||
npm install
|
||||
- run: |
|
||||
npm test
|
||||
- uses: actions/custom-action@master
|
||||
- uses: actions/custom-action@v1
|
||||
```
|
||||
|
||||
JavaScript actions work on any environment that host action runtime is supported on which is currently node 12. However, a host action that runs a toolset expects the environment that it's running on to have that toolset in its PATH or using a setup-* action to acquire it on demand.
|
||||
|
||||
@@ -17,7 +17,7 @@ Binding to a major version is the latest of that major version ( e.g. `v1` == "1
|
||||
|
||||
Major versions should guarantee compatibility. A major version can add net new capabilities but should not break existing input compatibility or break existing workflows.
|
||||
|
||||
Major version binding allows you to take advantage of bug fixes and critical functionality and security fixes. The `main` branch has the latest code and is unstable to bind to since changes get committed to `main` and released to the market place by creating a tag. In addition, a new major version carrying breaking changes will get implemented in `main` after branching off the previous major version.
|
||||
Major version binding allows you to take advantage of bug fixes and critical functionality and security fixes. The `main` branch has the latest code and is unstable to bind to since changes get committed to main and released to the market place by creating a tag. In addition, a new major version carrying breaking changes will get implemented in main after branching off the previous major version.
|
||||
|
||||
> Warning: do not reference `main` since that is the latest code and can be carrying breaking changes of the next major version.
|
||||
|
||||
|
||||
+76
-38
@@ -1,43 +1,12 @@
|
||||
# :: Commands
|
||||
|
||||
The [core toolkit package](https://github.com/actions/toolkit/tree/master/packages/core) offers a number of convenience functions for
|
||||
The [core toolkit package](https://github.com/actions/toolkit/tree/main/packages/core) offers a number of convenience functions for
|
||||
setting results, logging, registering secrets and exporting variables across actions. Sometimes, however, its useful to be able to do
|
||||
these things in a script or other tool.
|
||||
|
||||
To allow this, we provide a special `::` syntax which, if logged to `stdout` on a new line, will allow the runner to perform special behavior on
|
||||
your commands. The following commands are all supported:
|
||||
|
||||
### Set an environment variable
|
||||
|
||||
To set an environment variable for future out of process steps, use `::set-env`:
|
||||
|
||||
```sh
|
||||
echo "::set-env name=FOO::BAR"
|
||||
```
|
||||
|
||||
Running `$FOO` in a future step will now return `BAR`
|
||||
|
||||
This is wrapped by the core exportVariable method which sets for future steps but also updates the variable for this step
|
||||
|
||||
```javascript
|
||||
export function exportVariable(name: string, val: string): void {}
|
||||
```
|
||||
|
||||
### PATH Manipulation
|
||||
|
||||
To prepend a string to PATH, use `::addPath`:
|
||||
|
||||
```sh
|
||||
echo "::add-path::BAR"
|
||||
```
|
||||
|
||||
Running `$PATH` in a future step will now return `BAR:{Previous Path}`;
|
||||
|
||||
This is wrapped by the core addPath method:
|
||||
```javascript
|
||||
export function addPath(inputPath: string): void {}
|
||||
```
|
||||
|
||||
### Set outputs
|
||||
|
||||
To set an output for the step, use `::set-output`:
|
||||
@@ -88,7 +57,7 @@ For example, if you mask the letter `l`, running `echo "Hello FOO BAR World"` wi
|
||||
|
||||
### Group and Ungroup Log Lines
|
||||
|
||||
Emitting a group with a title will instruct the logs to create a collapsable region up to the next ungroup command.
|
||||
Emitting a group with a title will instruct the logs to create a collapsible region up to the next endgroup command.
|
||||
|
||||
```bash
|
||||
echo "::group::my title"
|
||||
@@ -103,6 +72,7 @@ function endGroup(): void {}
|
||||
```
|
||||
|
||||
### Problem Matchers
|
||||
|
||||
Problems matchers can be used to scan a build's output to automatically surface lines to the user that matches the provided pattern. A file path to a .json Problem Matcher must be provided. See [Problem Matchers](problem-matchers.md) for more information on how to define a Problem Matcher.
|
||||
|
||||
```bash
|
||||
@@ -112,6 +82,7 @@ echo "::remove-matcher owner=eslint-compact::"
|
||||
|
||||
`add-matcher` takes a path to a Problem Matcher file
|
||||
`remove-matcher` removes a Problem Matcher by owner
|
||||
|
||||
### Save State
|
||||
|
||||
Save a state to an environmental variable that can later be used in the main or post action.
|
||||
@@ -120,7 +91,7 @@ Save a state to an environmental variable that can later be used in the main or
|
||||
echo "::save-state name=FOO::foovalue"
|
||||
```
|
||||
|
||||
An environmental variable named `STATE_FOO` will be available to use in the post or main action. See [Sending Values to the pre and post actions](https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#sending-values-to-the-pre-and-post-actions) for more information.
|
||||
Because `save-state` prepends the string `STATE_` to the name, the environment variable `STATE_FOO` will be available to use in the post or main action. See [Sending Values to the pre and post actions](https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#sending-values-to-the-pre-and-post-actions) for more information.
|
||||
|
||||
### Log Level
|
||||
|
||||
@@ -129,13 +100,11 @@ There are several commands to emit different levels of log output:
|
||||
| log level | example usage |
|
||||
|---|---|
|
||||
| [debug](action-debugging.md) | `echo "::debug::My debug message"` |
|
||||
| notice | `echo "::notice::My notice message"` |
|
||||
| warning | `echo "::warning::My warning message"` |
|
||||
| error | `echo "::error::My error message"` |
|
||||
|
||||
Additional syntax options are described at [the workflow command documentation](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-a-debug-message).
|
||||
|
||||
### Command Echoing
|
||||
|
||||
By default, the echoing of commands to stdout only occurs if [Step Debugging is enabled](./action-debugging.md#How-to-Access-Step-Debug-Logs)
|
||||
|
||||
You can enable or disable this for the current step by using the `echo` command.
|
||||
@@ -158,8 +127,77 @@ function setCommandEcho(enabled: boolean): void {}
|
||||
|
||||
The `add-mask`, `debug`, `warning` and `error` commands do not support echoing.
|
||||
|
||||
### Command Prompt
|
||||
### Command Prompt
|
||||
|
||||
CMD processes the `"` character differently from other shells when echoing. In CMD, the above snippets should have the `"` characters removed in order to correctly process. For example, the set output command would be:
|
||||
|
||||
```cmd
|
||||
echo ::set-output name=FOO::BAR
|
||||
```
|
||||
|
||||
## Environment files
|
||||
|
||||
During the execution of a workflow, the runner generates temporary files that can be used to perform certain actions. The path to these files are exposed via environment variables. You will need to use the `utf-8` encoding when writing to these files to ensure proper processing of the commands. Multiple commands can be written to the same file, separated by newlines.
|
||||
|
||||
### Set an environment variable
|
||||
|
||||
To set an environment variable for future out of process steps, write to the file located at `GITHUB_ENV` or use the equivalent `actions/core` function
|
||||
|
||||
```sh
|
||||
echo "FOO=BAR" >> $GITHUB_ENV
|
||||
```
|
||||
|
||||
Running `$FOO` in a future step will now return `BAR`
|
||||
|
||||
For multiline strings, you may use a heredoc style syntax with your choice of delimeter. In the below example, we use `EOF`.
|
||||
|
||||
```
|
||||
steps:
|
||||
- name: Set the value
|
||||
id: step_one
|
||||
run: |
|
||||
echo 'JSON_RESPONSE<<EOF' >> $GITHUB_ENV
|
||||
curl https://httpbin.org/json >> $GITHUB_ENV
|
||||
echo 'EOF' >> $GITHUB_ENV
|
||||
```
|
||||
|
||||
This would set the value of the `JSON_RESPONSE` env variable to the value of the curl response.
|
||||
|
||||
The expected syntax for the heredoc style is:
|
||||
|
||||
```
|
||||
{VARIABLE_NAME}<<{DELIMETER}
|
||||
{VARIABLE_VALUE}
|
||||
{DELIMETER}
|
||||
```
|
||||
|
||||
This is wrapped by the core `exportVariable` method which sets for future steps but also updates the variable for this step.
|
||||
|
||||
```javascript
|
||||
export function exportVariable(name: string, val: string): void {}
|
||||
```
|
||||
|
||||
### PATH Manipulation
|
||||
|
||||
To prepend a string to PATH write to the file located at `GITHUB_PATH` or use the equivalent `actions/core` function
|
||||
|
||||
```sh
|
||||
echo "/Users/test/.nvm/versions/node/v12.18.3/bin" >> $GITHUB_PATH
|
||||
```
|
||||
|
||||
Running `$PATH` in a future step will now return `/Users/test/.nvm/versions/node/v12.18.3/bin:{Previous Path}`;
|
||||
|
||||
This is wrapped by the core addPath method:
|
||||
|
||||
```javascript
|
||||
export function addPath(inputPath: string): void {}
|
||||
```
|
||||
|
||||
### Powershell
|
||||
|
||||
Powershell does not use UTF8 by default. You will want to make sure you write in the correct encoding. For example, to set the path:
|
||||
|
||||
```
|
||||
steps:
|
||||
- run: echo "mypath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
```
|
||||
|
||||
@@ -18,7 +18,7 @@ e.g. To use https://github.com/actions/setup-node, users will author:
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
using: actions/setup-node@master
|
||||
using: actions/setup-node@v1
|
||||
```
|
||||
|
||||
# Define Metadata
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
|
||||
Problem Matchers are a way to scan the output of actions for a specified regex pattern and surface that information prominently in the UI. Both [GitHub Annotations](https://developer.github.com/v3/checks/runs/#annotations-object-1) and log file decorations are created when a match is detected.
|
||||
|
||||
## Limitations
|
||||
|
||||
Currently, GitHub Actions limit the annotation count in a workflow run.
|
||||
|
||||
- 10 warning annotations and 10 error annotations per step
|
||||
- 50 annotations per job (sum of annotations from all the steps)
|
||||
- 50 annotations per run (separate from the job annotations, these annotations aren’t created by users)
|
||||
|
||||
If your workflow may exceed these annotation counts, consider filtering of the log messages which the Problem Matcher is exposed to (e.g. by PR touched files, lines, or other).
|
||||
|
||||
## Single Line Matchers
|
||||
|
||||
Let's consider the ESLint compact output:
|
||||
@@ -100,6 +110,16 @@ The eslint-stylish problem matcher defined below catches that output, and create
|
||||
The first pattern matches the `test.js` line and records the file information. This line is not decorated in the UI.
|
||||
The second pattern loops through the remaining lines with `loop: true` until it fails to find a match, and surfaces these lines prominently in the UI.
|
||||
|
||||
Note that the pattern matches must be on consecutive lines. The following would not result in any match findings.
|
||||
|
||||
```
|
||||
test.js
|
||||
extraneous log line of no interest
|
||||
1:0 error Missing "use strict" statement strict
|
||||
5:10 error 'addOne' is defined but never used no-unused-vars
|
||||
✖ 2 problems (2 errors, 0 warnings)
|
||||
```
|
||||
|
||||
## Adding and Removing Problem Matchers
|
||||
|
||||
Problem Matchers are enabled and removed via the toolkit [commands](commands.md#problem-matchers).
|
||||
@@ -111,10 +131,10 @@ Registering two problem-matchers with the same owner will result in only the pro
|
||||
## Examples
|
||||
|
||||
Some of the starter actions are already using problem matchers, for example:
|
||||
- [setup-node](https://github.com/actions/setup-node/tree/master/.github)
|
||||
- [setup-python](https://github.com/actions/setup-python/tree/master/.github)
|
||||
- [setup-go](https://github.com/actions/setup-go/tree/master/.github)
|
||||
- [setup-dotnet](https://github.com/actions/setup-dotnet/tree/master/.github)
|
||||
- [setup-node](https://github.com/actions/setup-node/tree/main/.github)
|
||||
- [setup-python](https://github.com/actions/setup-python/tree/main/.github)
|
||||
- [setup-go](https://github.com/actions/setup-go/tree/main/.github)
|
||||
- [setup-dotnet](https://github.com/actions/setup-dotnet/tree/main/.github)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -124,6 +144,6 @@ Use ECMAScript regular expression syntax when testing patterns.
|
||||
|
||||
### File property getting dropped
|
||||
|
||||
[Enable debug logging](https://docs.github.com/en/actions/managing-workflow-runs/enabling-debug-logging) to determine why the file is getting dropped.
|
||||
[Enable debug logging](https://help.github.com/en/actions/configuring-and-managing-workflows/managing-a-workflow-run#enabling-debug-logging) to determine why the file is getting dropped.
|
||||
|
||||
This usually happens when the file does not exist or is not under the workflow repo.
|
||||
|
||||
Generated
+12
-18
@@ -7586,12 +7586,6 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
@@ -8756,9 +8750,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
||||
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz",
|
||||
"integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==",
|
||||
"dev": true
|
||||
},
|
||||
"init-package-json": {
|
||||
@@ -18690,9 +18684,9 @@
|
||||
}
|
||||
},
|
||||
"ssri": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
|
||||
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
|
||||
"integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"figgy-pudding": "^3.5.1"
|
||||
@@ -19342,9 +19336,9 @@
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "3.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.4.tgz",
|
||||
"integrity": "sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw==",
|
||||
"version": "3.9.9",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
|
||||
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==",
|
||||
"dev": true
|
||||
},
|
||||
"uglify-js": {
|
||||
@@ -19865,9 +19859,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
|
||||
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
|
||||
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
|
||||
+1
-1
@@ -27,6 +27,6 @@
|
||||
"lerna": "^3.18.4",
|
||||
"prettier": "^1.19.1",
|
||||
"ts-jest": "^25.4.0",
|
||||
"typescript": "^3.7.4"
|
||||
"typescript": "^3.9.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,5 +26,5 @@ Any easy way to test changes is to fork the artifact actions and to use `npm lin
|
||||
2. Clone the forks locally
|
||||
3. With your local changes to the toolkit repo, type `npm link` after ensuring there are no errors when running `tsc`
|
||||
4. In the locally cloned fork, type `npm link @actions/artifact`
|
||||
4. Create a new release for your local fork using `tsc` and `npm run release` (this will create a new `dist/index.js` file using `@zeit/ncc`)
|
||||
5. Commit and push your local changes, you will then be able to test your changes with your forked action
|
||||
4. Create a new release for your local fork using `tsc` and `npm run release` (this will create a new `dist/index.js` file using `@vercel/ncc`)
|
||||
5. Commit and push your local changes, you will then be able to test your changes with your forked action
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2019 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -39,6 +39,11 @@ Method Name: `uploadArtifact`
|
||||
- If set to `false`, and an error is encountered, all other uploads will stop and any files that were queued will not be attempted to be uploaded. The partial artifact available will only include files up until the failure.
|
||||
- If set to `true` and an error is encountered, the failed file will be skipped and ignored and all other queued files will be attempted to be uploaded. There will be an artifact available for download at the end with everything excluding the file that failed to upload
|
||||
- Optional, defaults to `true` if not specified
|
||||
- `retentionDays`
|
||||
- Duration after which artifact will expire in days
|
||||
- Minimum value: 1
|
||||
- Maximum value: 90 unless changed by repository setting
|
||||
- If this is set to a greater value than the retention settings allowed, the retention on artifacts will be reduced to match the max value allowed on the server, and the upload process will continue. An input of 0 assumes default retention value.
|
||||
|
||||
#### Example using Absolute File Paths
|
||||
|
||||
@@ -203,6 +208,6 @@ Check out [implementation-details](docs/implementation-details.md) for extra inf
|
||||
|
||||
## Contributions
|
||||
|
||||
See [contributor guidelines](https://github.com/actions/toolkit/blob/master/.github/CONTRIBUTING.md) for general guidelines and information about toolkit contributions.
|
||||
See [contributor guidelines](https://github.com/actions/toolkit/blob/main/.github/CONTRIBUTING.md) for general guidelines and information about toolkit contributions.
|
||||
|
||||
For contributions related to this package, see [artifact contributions](CONTRIBUTIONS.md) for more information.
|
||||
|
||||
@@ -28,3 +28,33 @@
|
||||
### 0.3.2
|
||||
|
||||
- Fix to ensure readstreams get correctly reset in the event of a retry
|
||||
|
||||
### 0.3.3
|
||||
|
||||
- Increase chunk size during upload from 4MB to 8MB
|
||||
- Improve user-agent strings during API calls to help internally diagnose issues
|
||||
|
||||
### 0.3.5
|
||||
|
||||
- Retry in the event of a 413 response
|
||||
|
||||
### 0.4.0
|
||||
|
||||
- Add option to specify custom retentions on artifacts
|
||||
|
||||
### 0.4.1
|
||||
|
||||
- Update to latest @actions/core version
|
||||
|
||||
### 0.4.2
|
||||
|
||||
- Improved retry-ability when a partial artifact download is encountered
|
||||
|
||||
### 0.5.0
|
||||
|
||||
- Improved retry-ability for all http calls during artifact upload and download if an error is encountered
|
||||
|
||||
### 0.5.1
|
||||
|
||||
- Bump @actions/http-client to version 1.0.11 to fix proxy related issues during artifact upload and download
|
||||
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
// Certain env variables are not set by default in a shell context and are only available in a node context from a running action
|
||||
// In order to be able to upload and download artifacts e2e in a shell when running CI tests, we need these env variables set
|
||||
console.log(`::set-env name=ACTIONS_RUNTIME_URL::${process.env.ACTIONS_RUNTIME_URL}`)
|
||||
console.log(`::set-env name=ACTIONS_RUNTIME_TOKEN::${process.env.ACTIONS_RUNTIME_TOKEN}`)
|
||||
console.log(`::set-env name=GITHUB_RUN_ID::${process.env.GITHUB_RUN_ID}`)
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const filePath = process.env[`GITHUB_ENV`]
|
||||
fs.appendFileSync(filePath, `ACTIONS_RUNTIME_URL=${process.env.ACTIONS_RUNTIME_URL}${os.EOL}`, {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
fs.appendFileSync(filePath, `ACTIONS_RUNTIME_TOKEN=${process.env.ACTIONS_RUNTIME_TOKEN}${os.EOL}`, {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
fs.appendFileSync(filePath, `GITHUB_RUN_ID=${process.env.GITHUB_RUN_ID}${os.EOL}`, {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
@@ -12,8 +12,12 @@ import {
|
||||
ListArtifactsResponse,
|
||||
QueryArtifactResponse
|
||||
} from '../src/internal/contracts'
|
||||
import * as stream from 'stream'
|
||||
import {gzip} from 'zlib'
|
||||
import {promisify} from 'util'
|
||||
|
||||
const root = path.join(__dirname, '_temp', 'artifact-download-tests')
|
||||
const defaultEncoding = 'utf8'
|
||||
|
||||
jest.mock('../src/internal/config-variables')
|
||||
jest.mock('@actions/http-client')
|
||||
@@ -67,7 +71,7 @@ describe('Download Tests', () => {
|
||||
setupFailedResponse()
|
||||
const downloadHttpClient = new DownloadHttpClient()
|
||||
expect(downloadHttpClient.listArtifacts()).rejects.toThrow(
|
||||
'Unable to list artifacts for the run'
|
||||
'List Artifacts failed: Artifact service responded with 500'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -109,38 +113,54 @@ describe('Download Tests', () => {
|
||||
configVariables.getRuntimeUrl()
|
||||
)
|
||||
).rejects.toThrow(
|
||||
`Unable to get ContainersItems from ${configVariables.getRuntimeUrl()}`
|
||||
`Get Container Items failed: Artifact service responded with 500`
|
||||
)
|
||||
})
|
||||
|
||||
it('Test downloading an individual artifact with gzip', async () => {
|
||||
setupDownloadItemResponse(true, 200)
|
||||
const fileContents = Buffer.from(
|
||||
'gzip worked on the first try\n',
|
||||
defaultEncoding
|
||||
)
|
||||
const targetPath = path.join(root, 'FileA.txt')
|
||||
|
||||
setupDownloadItemResponse(fileContents, true, 200, false, false)
|
||||
const downloadHttpClient = new DownloadHttpClient()
|
||||
|
||||
const items: DownloadItem[] = []
|
||||
items.push({
|
||||
sourceLocation: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13?itemPath=my-artifact%2FFileA.txt`,
|
||||
targetPath: path.join(root, 'FileA.txt')
|
||||
targetPath
|
||||
})
|
||||
|
||||
await expect(
|
||||
downloadHttpClient.downloadSingleArtifact(items)
|
||||
).resolves.not.toThrow()
|
||||
|
||||
await checkDestinationFile(targetPath, fileContents)
|
||||
})
|
||||
|
||||
it('Test downloading an individual artifact without gzip', async () => {
|
||||
setupDownloadItemResponse(false, 200)
|
||||
const fileContents = Buffer.from(
|
||||
'plaintext worked on the first try\n',
|
||||
defaultEncoding
|
||||
)
|
||||
const targetPath = path.join(root, 'FileB.txt')
|
||||
|
||||
setupDownloadItemResponse(fileContents, false, 200, false, false)
|
||||
const downloadHttpClient = new DownloadHttpClient()
|
||||
|
||||
const items: DownloadItem[] = []
|
||||
items.push({
|
||||
sourceLocation: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13?itemPath=my-artifact%2FFileB.txt`,
|
||||
targetPath: path.join(root, 'FileB.txt')
|
||||
targetPath
|
||||
})
|
||||
|
||||
await expect(
|
||||
downloadHttpClient.downloadSingleArtifact(items)
|
||||
).resolves.not.toThrow()
|
||||
|
||||
await checkDestinationFile(targetPath, fileContents)
|
||||
})
|
||||
|
||||
it('Test retryable status codes during artifact download', async () => {
|
||||
@@ -148,21 +168,72 @@ describe('Download Tests', () => {
|
||||
// the download should successfully finish
|
||||
const retryableStatusCodes = [429, 502, 503, 504]
|
||||
for (const statusCode of retryableStatusCodes) {
|
||||
setupDownloadItemResponse(false, statusCode)
|
||||
const fileContents = Buffer.from('try, try again\n', defaultEncoding)
|
||||
const targetPath = path.join(root, `FileC-${statusCode}.txt`)
|
||||
|
||||
setupDownloadItemResponse(fileContents, false, statusCode, false, true)
|
||||
const downloadHttpClient = new DownloadHttpClient()
|
||||
|
||||
const items: DownloadItem[] = []
|
||||
items.push({
|
||||
sourceLocation: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13?itemPath=my-artifact%2FFileC.txt`,
|
||||
targetPath: path.join(root, 'FileC.txt')
|
||||
targetPath
|
||||
})
|
||||
|
||||
await expect(
|
||||
downloadHttpClient.downloadSingleArtifact(items)
|
||||
).resolves.not.toThrow()
|
||||
|
||||
await checkDestinationFile(targetPath, fileContents)
|
||||
}
|
||||
})
|
||||
|
||||
it('Test retry on truncated response with gzip', async () => {
|
||||
const fileContents = Buffer.from(
|
||||
'Sometimes gunzip fails on the first try\n',
|
||||
defaultEncoding
|
||||
)
|
||||
const targetPath = path.join(root, 'FileD.txt')
|
||||
|
||||
setupDownloadItemResponse(fileContents, true, 200, true, true)
|
||||
const downloadHttpClient = new DownloadHttpClient()
|
||||
|
||||
const items: DownloadItem[] = []
|
||||
items.push({
|
||||
sourceLocation: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13?itemPath=my-artifact%2FFileD.txt`,
|
||||
targetPath
|
||||
})
|
||||
|
||||
await expect(
|
||||
downloadHttpClient.downloadSingleArtifact(items)
|
||||
).resolves.not.toThrow()
|
||||
|
||||
await checkDestinationFile(targetPath, fileContents)
|
||||
})
|
||||
|
||||
it('Test retry on truncated response without gzip', async () => {
|
||||
const fileContents = Buffer.from(
|
||||
'You have to inspect the content-length header to know if you got everything\n',
|
||||
defaultEncoding
|
||||
)
|
||||
const targetPath = path.join(root, 'FileE.txt')
|
||||
|
||||
setupDownloadItemResponse(fileContents, false, 200, true, true)
|
||||
const downloadHttpClient = new DownloadHttpClient()
|
||||
|
||||
const items: DownloadItem[] = []
|
||||
items.push({
|
||||
sourceLocation: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13?itemPath=my-artifact%2FFileD.txt`,
|
||||
targetPath
|
||||
})
|
||||
|
||||
await expect(
|
||||
downloadHttpClient.downloadSingleArtifact(items)
|
||||
).resolves.not.toThrow()
|
||||
|
||||
await checkDestinationFile(targetPath, fileContents)
|
||||
})
|
||||
|
||||
/**
|
||||
* Helper used to setup mocking for the HttpClient
|
||||
*/
|
||||
@@ -226,52 +297,111 @@ describe('Download Tests', () => {
|
||||
* @param firstHttpResponseCode the http response code that should be returned
|
||||
*/
|
||||
function setupDownloadItemResponse(
|
||||
fileContents: Buffer,
|
||||
isGzip: boolean,
|
||||
firstHttpResponseCode: number
|
||||
firstHttpResponseCode: number,
|
||||
truncateFirstResponse: boolean,
|
||||
retryExpected: boolean
|
||||
): void {
|
||||
jest
|
||||
.spyOn(DownloadHttpClient.prototype, 'pipeResponseToFile')
|
||||
.mockImplementationOnce(async () => {
|
||||
return new Promise<void>(resolve => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
jest
|
||||
const spyInstance = jest
|
||||
.spyOn(HttpClient.prototype, 'get')
|
||||
.mockImplementationOnce(async () => {
|
||||
const mockMessage = new http.IncomingMessage(new net.Socket())
|
||||
mockMessage.statusCode = firstHttpResponseCode
|
||||
if (isGzip) {
|
||||
mockMessage.headers = {
|
||||
'content-type': 'gzip'
|
||||
if (firstHttpResponseCode === 200) {
|
||||
const fullResponse = await constructResponse(isGzip, fileContents)
|
||||
const actualResponse = truncateFirstResponse
|
||||
? fullResponse.subarray(0, 3)
|
||||
: fullResponse
|
||||
|
||||
return {
|
||||
message: getDownloadResponseMessage(
|
||||
firstHttpResponseCode,
|
||||
isGzip,
|
||||
fullResponse.length,
|
||||
actualResponse
|
||||
),
|
||||
readBody: emptyMockReadBody
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
message: getDownloadResponseMessage(
|
||||
firstHttpResponseCode,
|
||||
false,
|
||||
0,
|
||||
null
|
||||
),
|
||||
readBody: emptyMockReadBody
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise<HttpClientResponse>(resolve => {
|
||||
resolve({
|
||||
message: mockMessage,
|
||||
readBody: emptyMockReadBody
|
||||
})
|
||||
})
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
|
||||
// set up a second mock only if we expect a retry. Otherwise this mock will affect other tests.
|
||||
if (retryExpected) {
|
||||
spyInstance.mockImplementationOnce(async () => {
|
||||
// chained response, if the HTTP GET function gets called again, return a successful response
|
||||
const mockMessage = new http.IncomingMessage(new net.Socket())
|
||||
mockMessage.statusCode = 200
|
||||
if (isGzip) {
|
||||
mockMessage.headers = {
|
||||
'content-type': 'gzip'
|
||||
}
|
||||
const fullResponse = await constructResponse(isGzip, fileContents)
|
||||
return {
|
||||
message: getDownloadResponseMessage(
|
||||
200,
|
||||
isGzip,
|
||||
fullResponse.length,
|
||||
fullResponse
|
||||
),
|
||||
readBody: emptyMockReadBody
|
||||
}
|
||||
|
||||
return new Promise<HttpClientResponse>(resolve => {
|
||||
resolve({
|
||||
message: mockMessage,
|
||||
readBody: emptyMockReadBody
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function constructResponse(
|
||||
isGzip: boolean,
|
||||
plaintext: Buffer | string
|
||||
): Promise<Buffer> {
|
||||
if (isGzip) {
|
||||
return <Buffer>await promisify(gzip)(plaintext)
|
||||
} else if (typeof plaintext === 'string') {
|
||||
return Buffer.from(plaintext, defaultEncoding)
|
||||
} else {
|
||||
return plaintext
|
||||
}
|
||||
}
|
||||
|
||||
function getDownloadResponseMessage(
|
||||
httpResponseCode: number,
|
||||
isGzip: boolean,
|
||||
contentLength: number,
|
||||
response: Buffer | null
|
||||
): http.IncomingMessage {
|
||||
let readCallCount = 0
|
||||
const mockMessage = <http.IncomingMessage>new stream.Readable({
|
||||
read(size) {
|
||||
switch (readCallCount++) {
|
||||
case 0:
|
||||
if (!!response && response.byteLength > size) {
|
||||
throw new Error(
|
||||
`test response larger than requested size (${size})`
|
||||
)
|
||||
}
|
||||
this.push(response)
|
||||
break
|
||||
|
||||
default:
|
||||
// end the stream
|
||||
this.push(null)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
mockMessage.statusCode = httpResponseCode
|
||||
mockMessage.headers = {
|
||||
'content-length': contentLength.toString()
|
||||
}
|
||||
|
||||
if (isGzip) {
|
||||
mockMessage.headers['content-encoding'] = 'gzip'
|
||||
}
|
||||
|
||||
return mockMessage
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -347,4 +477,14 @@ describe('Download Tests', () => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function checkDestinationFile(
|
||||
targetPath: string,
|
||||
expectedContents: Buffer
|
||||
): Promise<void> {
|
||||
const fileContents = await fs.readFile(targetPath)
|
||||
|
||||
expect(fileContents.byteLength).toEqual(expectedContents.byteLength)
|
||||
expect(fileContents.equals(expectedContents)).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import * as http from 'http'
|
||||
import * as net from 'net'
|
||||
import * as core from '@actions/core'
|
||||
import * as configVariables from '../src/internal/config-variables'
|
||||
import {retry} from '../src/internal/requestUtils'
|
||||
import {IHttpClientResponse} from '@actions/http-client/interfaces'
|
||||
import {HttpClientResponse} from '@actions/http-client'
|
||||
|
||||
jest.mock('../src/internal/config-variables')
|
||||
|
||||
interface ITestResult {
|
||||
responseCode: number
|
||||
errorMessage: string | null
|
||||
}
|
||||
|
||||
async function testRetry(
|
||||
responseCodes: number[],
|
||||
expectedResult: ITestResult
|
||||
): Promise<void> {
|
||||
const reverse = responseCodes.reverse() // Reverse responses since we pop from end
|
||||
if (expectedResult.errorMessage) {
|
||||
// we expect some exception to be thrown
|
||||
expect(
|
||||
retry(
|
||||
'test',
|
||||
async () => handleResponse(reverse.pop()),
|
||||
new Map(), // extra error message for any particular http codes
|
||||
configVariables.getRetryLimit()
|
||||
)
|
||||
).rejects.toThrow(expectedResult.errorMessage)
|
||||
} else {
|
||||
// we expect a correct status code to be returned
|
||||
const actualResult = await retry(
|
||||
'test',
|
||||
async () => handleResponse(reverse.pop()),
|
||||
new Map(), // extra error message for any particular http codes
|
||||
configVariables.getRetryLimit()
|
||||
)
|
||||
expect(actualResult.message.statusCode).toEqual(expectedResult.responseCode)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResponse(
|
||||
testResponseCode: number | undefined
|
||||
): Promise<IHttpClientResponse> {
|
||||
if (!testResponseCode) {
|
||||
throw new Error(
|
||||
'Test incorrectly set up. reverse.pop() was called too many times so not enough test response codes were supplied'
|
||||
)
|
||||
}
|
||||
|
||||
return setupSingleMockResponse(testResponseCode)
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
// mock all output so that there is less noise when running tests
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {})
|
||||
jest.spyOn(core, 'debug').mockImplementation(() => {})
|
||||
jest.spyOn(core, 'info').mockImplementation(() => {})
|
||||
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
||||
jest.spyOn(core, 'error').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
/**
|
||||
* Helpers used to setup mocking for the HttpClient
|
||||
*/
|
||||
async function emptyMockReadBody(): Promise<string> {
|
||||
return new Promise(resolve => {
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
|
||||
async function setupSingleMockResponse(
|
||||
statusCode: number
|
||||
): Promise<IHttpClientResponse> {
|
||||
const mockMessage = new http.IncomingMessage(new net.Socket())
|
||||
const mockReadBody = emptyMockReadBody
|
||||
mockMessage.statusCode = statusCode
|
||||
return new Promise<HttpClientResponse>(resolve => {
|
||||
resolve({
|
||||
message: mockMessage,
|
||||
readBody: mockReadBody
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
test('retry works on successful response', async () => {
|
||||
await testRetry([200], {
|
||||
responseCode: 200,
|
||||
errorMessage: null
|
||||
})
|
||||
})
|
||||
|
||||
test('retry works after retryable status code', async () => {
|
||||
await testRetry([503, 200], {
|
||||
responseCode: 200,
|
||||
errorMessage: null
|
||||
})
|
||||
})
|
||||
|
||||
test('retry fails after exhausting retries', async () => {
|
||||
// __mocks__/config-variables caps the max retry count in tests to 2
|
||||
await testRetry([503, 503, 200], {
|
||||
responseCode: 200,
|
||||
errorMessage: 'test failed: Artifact service responded with 503'
|
||||
})
|
||||
})
|
||||
|
||||
test('retry fails after non-retryable status code', async () => {
|
||||
await testRetry([500, 200], {
|
||||
responseCode: 500,
|
||||
errorMessage: 'test failed: Artifact service responded with 500'
|
||||
})
|
||||
})
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '../src/internal/contracts'
|
||||
import {UploadSpecification} from '../src/internal/upload-specification'
|
||||
import {getArtifactUrl} from '../src/internal/utils'
|
||||
import {UploadOptions} from '../src/internal/upload-options'
|
||||
|
||||
const root = path.join(__dirname, '_temp', 'artifact-upload')
|
||||
const file1Path = path.join(root, 'file1.txt')
|
||||
@@ -101,11 +102,22 @@ describe('Upload Tests', () => {
|
||||
uploadHttpClient.createArtifactInFileContainer(artifactName)
|
||||
).rejects.toEqual(
|
||||
new Error(
|
||||
`Unable to create a container for the artifact invalid-artifact-name at ${getArtifactUrl()}`
|
||||
`Create Artifact Container failed: The artifact name invalid-artifact-name is not valid. Request URL ${getArtifactUrl()}`
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('Create Artifact - Retention Less Than Min Value Error', async () => {
|
||||
const artifactName = 'valid-artifact-name'
|
||||
const options: UploadOptions = {
|
||||
retentionDays: -1
|
||||
}
|
||||
const uploadHttpClient = new UploadHttpClient()
|
||||
expect(
|
||||
uploadHttpClient.createArtifactInFileContainer(artifactName, options)
|
||||
).rejects.toEqual(new Error('Invalid retention, minimum value is 1.'))
|
||||
})
|
||||
|
||||
it('Create Artifact - Storage Quota Error', async () => {
|
||||
const artifactName = 'storage-quota-hit'
|
||||
const uploadHttpClient = new UploadHttpClient()
|
||||
@@ -113,7 +125,7 @@ describe('Upload Tests', () => {
|
||||
uploadHttpClient.createArtifactInFileContainer(artifactName)
|
||||
).rejects.toEqual(
|
||||
new Error(
|
||||
'Artifact storage quota has been hit. Unable to upload any new artifacts'
|
||||
'Create Artifact Container failed: Artifact storage quota has been hit. Unable to upload any new artifacts'
|
||||
)
|
||||
)
|
||||
})
|
||||
@@ -350,7 +362,9 @@ describe('Upload Tests', () => {
|
||||
const uploadHttpClient = new UploadHttpClient()
|
||||
expect(
|
||||
uploadHttpClient.patchArtifactSize(-2, 'my-artifact')
|
||||
).rejects.toThrow('Unable to finish uploading artifact my-artifact')
|
||||
).rejects.toThrow(
|
||||
'Finalize artifact upload failed: Artifact service responded with 400'
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -106,6 +106,20 @@ describe('Utils', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('Test negative artifact retention throws', () => {
|
||||
expect(() => {
|
||||
utils.getProperRetention(-1, undefined)
|
||||
}).toThrow()
|
||||
})
|
||||
|
||||
it('Test no setting specified takes artifact retention input', () => {
|
||||
expect(utils.getProperRetention(180, undefined)).toEqual(180)
|
||||
})
|
||||
|
||||
it('Test artifact retention must conform to max allowed', () => {
|
||||
expect(utils.getProperRetention(180, '45')).toEqual(45)
|
||||
})
|
||||
|
||||
it('Test constructing artifact URL', () => {
|
||||
const runtimeUrl = getRuntimeUrl()
|
||||
const runId = getWorkFlowRunId()
|
||||
@@ -192,6 +206,7 @@ describe('Utils', () => {
|
||||
expect(utils.isRetryableStatusCode(HttpCodes.OK)).toEqual(false)
|
||||
expect(utils.isRetryableStatusCode(HttpCodes.NotFound)).toEqual(false)
|
||||
expect(utils.isRetryableStatusCode(HttpCodes.Forbidden)).toEqual(false)
|
||||
expect(utils.isRetryableStatusCode(413)).toEqual(true) // Payload Too Large
|
||||
})
|
||||
|
||||
it('Test Throttled Status Code', () => {
|
||||
|
||||
Generated
+7
-7
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@actions/artifact",
|
||||
"version": "0.3.2",
|
||||
"version": "0.5.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@actions/core": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.3.tgz",
|
||||
"integrity": "sha512-Wp4xnyokakM45Uuj4WLUxdsa8fJjKVl1fDTsPbTEcTcuu0Nb26IPQbOtjmnfaCPGcaoPOOqId8H9NapZ8gii4w=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
|
||||
"integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
|
||||
},
|
||||
"@actions/http-client": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.8.tgz",
|
||||
"integrity": "sha512-G4JjJ6f9Hb3Zvejj+ewLLKLf99ZC+9v+yCxoYf9vSyH+WkzPLB2LuUtRMGNkooMqdugGBFStIKXOuvH1W+EctA==",
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
|
||||
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
|
||||
"requires": {
|
||||
"tunnel": "0.0.6"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/artifact",
|
||||
"version": "0.3.2",
|
||||
"version": "0.5.1",
|
||||
"preview": true,
|
||||
"description": "Actions artifact lib",
|
||||
"keywords": [
|
||||
@@ -8,7 +8,7 @@
|
||||
"actions",
|
||||
"artifact"
|
||||
],
|
||||
"homepage": "https://github.com/actions/toolkit/tree/master/packages/artifact",
|
||||
"homepage": "https://github.com/actions/toolkit/tree/main/packages/artifact",
|
||||
"license": "MIT",
|
||||
"main": "lib/artifact-client.js",
|
||||
"types": "lib/artifact-client.d.ts",
|
||||
@@ -37,8 +37,8 @@
|
||||
"url": "https://github.com/actions/toolkit/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.1",
|
||||
"@actions/http-client": "^1.0.7",
|
||||
"@actions/core": "^1.2.6",
|
||||
"@actions/http-client": "^1.0.11",
|
||||
"@types/tmp": "^0.1.0",
|
||||
"tmp": "^0.1.0",
|
||||
"tmp-promise": "^2.0.2"
|
||||
|
||||
@@ -45,3 +45,7 @@ export function getRuntimeUrl(): string {
|
||||
export function getWorkFlowRunId(): string {
|
||||
return '15'
|
||||
}
|
||||
|
||||
export function getRetentionDays(): string | undefined {
|
||||
return '45'
|
||||
}
|
||||
|
||||
@@ -94,7 +94,8 @@ export class DefaultArtifactClient implements ArtifactClient {
|
||||
} else {
|
||||
// Create an entry for the artifact in the file container
|
||||
const response = await uploadHttpClient.createArtifactInFileContainer(
|
||||
name
|
||||
name,
|
||||
options
|
||||
)
|
||||
if (!response.fileContainerResourceUrl) {
|
||||
core.debug(response.toString())
|
||||
|
||||
@@ -6,7 +6,7 @@ export function getUploadFileConcurrency(): number {
|
||||
// When uploading large files that can't be uploaded with a single http call, this controls
|
||||
// the chunk size that is used during upload
|
||||
export function getUploadChunkSize(): number {
|
||||
return 4 * 1024 * 1024 // 4 MB Chunks
|
||||
return 8 * 1024 * 1024 // 8 MB Chunks
|
||||
}
|
||||
|
||||
// The maximum number of retries that can be attempted before an upload or download fails
|
||||
@@ -61,3 +61,7 @@ export function getWorkSpaceDirectory(): string {
|
||||
}
|
||||
return workspaceDirectory
|
||||
}
|
||||
|
||||
export function getRetentionDays(): string | undefined {
|
||||
return process.env['GITHUB_RETENTION_DAYS']
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface ArtifactResponse {
|
||||
export interface CreateArtifactParameters {
|
||||
Type: string
|
||||
Name: string
|
||||
RetentionDays?: number
|
||||
}
|
||||
|
||||
export interface PatchArtifactSize {
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
isThrottledStatusCode,
|
||||
getExponentialRetryTimeInMilliseconds,
|
||||
tryGetRetryAfterValueTimeInMilliseconds,
|
||||
displayHttpDiagnostics
|
||||
displayHttpDiagnostics,
|
||||
getFileSize,
|
||||
rmFile,
|
||||
sleep
|
||||
} from './utils'
|
||||
import {URL} from 'url'
|
||||
import {StatusReporter} from './status-reporter'
|
||||
@@ -20,6 +23,7 @@ import {HttpManager} from './http-manager'
|
||||
import {DownloadItem} from './download-specification'
|
||||
import {getDownloadFileConcurrency, getRetryLimit} from './config-variables'
|
||||
import {IncomingHttpHeaders} from 'http'
|
||||
import {retryHttpClientRequest} from './requestUtils'
|
||||
|
||||
export class DownloadHttpClient {
|
||||
// http manager is used for concurrent connections when downloading multiple files at once
|
||||
@@ -27,7 +31,10 @@ export class DownloadHttpClient {
|
||||
private statusReporter: StatusReporter
|
||||
|
||||
constructor() {
|
||||
this.downloadHttpManager = new HttpManager(getDownloadFileConcurrency())
|
||||
this.downloadHttpManager = new HttpManager(
|
||||
getDownloadFileConcurrency(),
|
||||
'@actions/artifact-download'
|
||||
)
|
||||
// downloads are usually significantly faster than uploads so display status information every second
|
||||
this.statusReporter = new StatusReporter(1000)
|
||||
}
|
||||
@@ -41,16 +48,11 @@ export class DownloadHttpClient {
|
||||
// use the first client from the httpManager, `keep-alive` is not used so the connection will close immediately
|
||||
const client = this.downloadHttpManager.getClient(0)
|
||||
const headers = getDownloadHeaders('application/json')
|
||||
const response = await client.get(artifactUrl, headers)
|
||||
const body: string = await response.readBody()
|
||||
|
||||
if (isSuccessStatusCode(response.message.statusCode) && body) {
|
||||
return JSON.parse(body)
|
||||
}
|
||||
displayHttpDiagnostics(response)
|
||||
throw new Error(
|
||||
`Unable to list artifacts for the run. Resource Url ${artifactUrl}`
|
||||
const response = await retryHttpClientRequest('List Artifacts', async () =>
|
||||
client.get(artifactUrl, headers)
|
||||
)
|
||||
const body: string = await response.readBody()
|
||||
return JSON.parse(body)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,14 +71,12 @@ export class DownloadHttpClient {
|
||||
// use the first client from the httpManager, `keep-alive` is not used so the connection will close immediately
|
||||
const client = this.downloadHttpManager.getClient(0)
|
||||
const headers = getDownloadHeaders('application/json')
|
||||
const response = await client.get(resourceUrl.toString(), headers)
|
||||
const response = await retryHttpClientRequest(
|
||||
'Get Container Items',
|
||||
async () => client.get(resourceUrl.toString(), headers)
|
||||
)
|
||||
const body: string = await response.readBody()
|
||||
|
||||
if (isSuccessStatusCode(response.message.statusCode) && body) {
|
||||
return JSON.parse(body)
|
||||
}
|
||||
displayHttpDiagnostics(response)
|
||||
throw new Error(`Unable to get ContainersItems from ${resourceUrl}`)
|
||||
return JSON.parse(body)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,7 +148,7 @@ export class DownloadHttpClient {
|
||||
): Promise<void> {
|
||||
let retryCount = 0
|
||||
const retryLimit = getRetryLimit()
|
||||
const destinationStream = fs.createWriteStream(downloadPath)
|
||||
let destinationStream = fs.createWriteStream(downloadPath)
|
||||
const headers = getDownloadHeaders('application/json', true, true)
|
||||
|
||||
// a single GET request is used to download a file
|
||||
@@ -183,14 +183,14 @@ export class DownloadHttpClient {
|
||||
core.info(
|
||||
`Backoff due to too many requests, retry #${retryCount}. Waiting for ${retryAfterValue} milliseconds before continuing the download`
|
||||
)
|
||||
await new Promise(resolve => setTimeout(resolve, retryAfterValue))
|
||||
await sleep(retryAfterValue)
|
||||
} else {
|
||||
// Back off using an exponential value that depends on the retry count
|
||||
const backoffTime = getExponentialRetryTimeInMilliseconds(retryCount)
|
||||
core.info(
|
||||
`Exponential backoff for retry #${retryCount}. Waiting for ${backoffTime} milliseconds before continuing the download`
|
||||
)
|
||||
await new Promise(resolve => setTimeout(resolve, backoffTime))
|
||||
await sleep(backoffTime)
|
||||
}
|
||||
core.info(
|
||||
`Finished backoff for retry #${retryCount}, continuing with download`
|
||||
@@ -198,11 +198,39 @@ export class DownloadHttpClient {
|
||||
}
|
||||
}
|
||||
|
||||
const isAllBytesReceived = (
|
||||
expected?: string,
|
||||
received?: number
|
||||
): boolean => {
|
||||
// be lenient, if any input is missing, assume success, i.e. not truncated
|
||||
if (
|
||||
!expected ||
|
||||
!received ||
|
||||
process.env['ACTIONS_ARTIFACT_SKIP_DOWNLOAD_VALIDATION']
|
||||
) {
|
||||
core.info('Skipping download validation.')
|
||||
return true
|
||||
}
|
||||
|
||||
return parseInt(expected) === received
|
||||
}
|
||||
|
||||
const resetDestinationStream = async (
|
||||
fileDownloadPath: string
|
||||
): Promise<void> => {
|
||||
destinationStream.close()
|
||||
await rmFile(fileDownloadPath)
|
||||
destinationStream = fs.createWriteStream(fileDownloadPath)
|
||||
}
|
||||
|
||||
// keep trying to download a file until a retry limit has been reached
|
||||
while (retryCount <= retryLimit) {
|
||||
let response: IHttpClientResponse
|
||||
try {
|
||||
response = await makeDownloadRequest()
|
||||
if (core.isDebug()) {
|
||||
displayHttpDiagnostics(response)
|
||||
}
|
||||
} catch (error) {
|
||||
// if an error is caught, it is usually indicative of a timeout so retry the download
|
||||
core.info('An error occurred while attempting to download a file')
|
||||
@@ -214,19 +242,37 @@ export class DownloadHttpClient {
|
||||
continue
|
||||
}
|
||||
|
||||
let forceRetry = false
|
||||
if (isSuccessStatusCode(response.message.statusCode)) {
|
||||
// The body contains the contents of the file however calling response.readBody() causes all the content to be converted to a string
|
||||
// which can cause some gzip encoded data to be lost
|
||||
// Instead of using response.readBody(), response.message is a readableStream that can be directly used to get the raw body contents
|
||||
return this.pipeResponseToFile(
|
||||
response,
|
||||
destinationStream,
|
||||
isGzip(response.message.headers)
|
||||
)
|
||||
} else if (isRetryableStatusCode(response.message.statusCode)) {
|
||||
try {
|
||||
const isGzipped = isGzip(response.message.headers)
|
||||
await this.pipeResponseToFile(response, destinationStream, isGzipped)
|
||||
|
||||
if (
|
||||
isGzipped ||
|
||||
isAllBytesReceived(
|
||||
response.message.headers['content-length'],
|
||||
await getFileSize(downloadPath)
|
||||
)
|
||||
) {
|
||||
return
|
||||
} else {
|
||||
forceRetry = true
|
||||
}
|
||||
} catch (error) {
|
||||
// retry on error, most likely streams were corrupted
|
||||
forceRetry = true
|
||||
}
|
||||
}
|
||||
|
||||
if (forceRetry || isRetryableStatusCode(response.message.statusCode)) {
|
||||
core.info(
|
||||
`A ${response.message.statusCode} response code has been received while attempting to download an artifact`
|
||||
)
|
||||
resetDestinationStream(downloadPath)
|
||||
// if a throttled status code is received, try to get the retryAfter header value, else differ to standard exponential backoff
|
||||
isThrottledStatusCode(response.message.statusCode)
|
||||
? await backOff(
|
||||
@@ -260,26 +306,48 @@ export class DownloadHttpClient {
|
||||
if (isGzip) {
|
||||
const gunzip = zlib.createGunzip()
|
||||
response.message
|
||||
.on('error', error => {
|
||||
core.error(
|
||||
`An error occurred while attempting to read the response stream`
|
||||
)
|
||||
gunzip.close()
|
||||
destinationStream.close()
|
||||
reject(error)
|
||||
})
|
||||
.pipe(gunzip)
|
||||
.on('error', error => {
|
||||
core.error(
|
||||
`An error occurred while attempting to decompress the response stream`
|
||||
)
|
||||
destinationStream.close()
|
||||
reject(error)
|
||||
})
|
||||
.pipe(destinationStream)
|
||||
.on('close', () => {
|
||||
resolve()
|
||||
})
|
||||
.on('error', error => {
|
||||
core.error(
|
||||
`An error has been encountered while decompressing and writing a downloaded file to ${destinationStream.path}`
|
||||
`An error occurred while writing a downloaded file to ${destinationStream.path}`
|
||||
)
|
||||
reject(error)
|
||||
})
|
||||
} else {
|
||||
response.message
|
||||
.on('error', error => {
|
||||
core.error(
|
||||
`An error occurred while attempting to read the response stream`
|
||||
)
|
||||
destinationStream.close()
|
||||
reject(error)
|
||||
})
|
||||
.pipe(destinationStream)
|
||||
.on('close', () => {
|
||||
resolve()
|
||||
})
|
||||
.on('error', error => {
|
||||
core.error(
|
||||
`An error has been encountered while writing a downloaded file to ${destinationStream.path}`
|
||||
`An error occurred while writing a downloaded file to ${destinationStream.path}`
|
||||
)
|
||||
reject(error)
|
||||
})
|
||||
|
||||
@@ -6,12 +6,14 @@ import {createHttpClient} from './utils'
|
||||
*/
|
||||
export class HttpManager {
|
||||
private clients: HttpClient[]
|
||||
private userAgent: string
|
||||
|
||||
constructor(clientCount: number) {
|
||||
constructor(clientCount: number, userAgent: string) {
|
||||
if (clientCount < 1) {
|
||||
throw new Error('There must be at least one client')
|
||||
}
|
||||
this.clients = new Array(clientCount).fill(createHttpClient())
|
||||
this.userAgent = userAgent
|
||||
this.clients = new Array(clientCount).fill(createHttpClient(userAgent))
|
||||
}
|
||||
|
||||
getClient(index: number): HttpClient {
|
||||
@@ -22,7 +24,7 @@ export class HttpManager {
|
||||
// for more information see: https://github.com/actions/http-client/blob/04e5ad73cd3fd1f5610a32116b0759eddf6570d2/index.ts#L292
|
||||
disposeAndReplaceClient(index: number): void {
|
||||
this.clients[index].dispose()
|
||||
this.clients[index] = createHttpClient()
|
||||
this.clients[index] = createHttpClient(this.userAgent)
|
||||
}
|
||||
|
||||
disposeAndReplaceAllClients(): void {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import {IHttpClientResponse} from '@actions/http-client/interfaces'
|
||||
import {
|
||||
isRetryableStatusCode,
|
||||
isSuccessStatusCode,
|
||||
sleep,
|
||||
getExponentialRetryTimeInMilliseconds,
|
||||
displayHttpDiagnostics
|
||||
} from './utils'
|
||||
import * as core from '@actions/core'
|
||||
import {getRetryLimit} from './config-variables'
|
||||
|
||||
export async function retry(
|
||||
name: string,
|
||||
operation: () => Promise<IHttpClientResponse>,
|
||||
customErrorMessages: Map<number, string>,
|
||||
maxAttempts: number
|
||||
): Promise<IHttpClientResponse> {
|
||||
let response: IHttpClientResponse | undefined = undefined
|
||||
let statusCode: number | undefined = undefined
|
||||
let isRetryable = false
|
||||
let errorMessage = ''
|
||||
let customErrorInformation: string | undefined = undefined
|
||||
let attempt = 1
|
||||
|
||||
while (attempt <= maxAttempts) {
|
||||
try {
|
||||
response = await operation()
|
||||
statusCode = response.message.statusCode
|
||||
|
||||
if (isSuccessStatusCode(statusCode)) {
|
||||
return response
|
||||
}
|
||||
|
||||
// Extra error information that we want to display if a particular response code is hit
|
||||
if (statusCode) {
|
||||
customErrorInformation = customErrorMessages.get(statusCode)
|
||||
}
|
||||
|
||||
isRetryable = isRetryableStatusCode(statusCode)
|
||||
errorMessage = `Artifact service responded with ${statusCode}`
|
||||
} catch (error) {
|
||||
isRetryable = true
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
if (!isRetryable) {
|
||||
core.info(`${name} - Error is not retryable`)
|
||||
if (response) {
|
||||
displayHttpDiagnostics(response)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
core.info(
|
||||
`${name} - Attempt ${attempt} of ${maxAttempts} failed with error: ${errorMessage}`
|
||||
)
|
||||
|
||||
await sleep(getExponentialRetryTimeInMilliseconds(attempt))
|
||||
attempt++
|
||||
}
|
||||
|
||||
if (response) {
|
||||
displayHttpDiagnostics(response)
|
||||
}
|
||||
|
||||
if (customErrorInformation) {
|
||||
throw Error(`${name} failed: ${customErrorInformation}`)
|
||||
}
|
||||
throw Error(`${name} failed: ${errorMessage}`)
|
||||
}
|
||||
|
||||
export async function retryHttpClientRequest<T>(
|
||||
name: string,
|
||||
method: () => Promise<IHttpClientResponse>,
|
||||
customErrorMessages: Map<number, string> = new Map(),
|
||||
maxAttempts = getRetryLimit()
|
||||
): Promise<IHttpClientResponse> {
|
||||
return await retry(name, method, customErrorMessages, maxAttempts)
|
||||
}
|
||||
@@ -15,26 +15,29 @@ import {
|
||||
isRetryableStatusCode,
|
||||
isSuccessStatusCode,
|
||||
isThrottledStatusCode,
|
||||
isForbiddenStatusCode,
|
||||
displayHttpDiagnostics,
|
||||
getExponentialRetryTimeInMilliseconds,
|
||||
tryGetRetryAfterValueTimeInMilliseconds
|
||||
tryGetRetryAfterValueTimeInMilliseconds,
|
||||
getProperRetention,
|
||||
sleep
|
||||
} from './utils'
|
||||
import {
|
||||
getUploadChunkSize,
|
||||
getUploadFileConcurrency,
|
||||
getRetryLimit
|
||||
getRetryLimit,
|
||||
getRetentionDays
|
||||
} from './config-variables'
|
||||
import {promisify} from 'util'
|
||||
import {URL} from 'url'
|
||||
import {performance} from 'perf_hooks'
|
||||
import {StatusReporter} from './status-reporter'
|
||||
import {HttpClientResponse} from '@actions/http-client/index'
|
||||
import {HttpCodes} from '@actions/http-client'
|
||||
import {IHttpClientResponse} from '@actions/http-client/interfaces'
|
||||
import {HttpManager} from './http-manager'
|
||||
import {UploadSpecification} from './upload-specification'
|
||||
import {UploadOptions} from './upload-options'
|
||||
import {createGZipFileOnDisk, createGZipFileInBuffer} from './upload-gzip'
|
||||
import {retryHttpClientRequest} from './requestUtils'
|
||||
const stat = promisify(fs.stat)
|
||||
|
||||
export class UploadHttpClient {
|
||||
@@ -42,7 +45,10 @@ export class UploadHttpClient {
|
||||
private statusReporter: StatusReporter
|
||||
|
||||
constructor() {
|
||||
this.uploadHttpManager = new HttpManager(getUploadFileConcurrency())
|
||||
this.uploadHttpManager = new HttpManager(
|
||||
getUploadFileConcurrency(),
|
||||
'@actions/artifact-upload'
|
||||
)
|
||||
this.statusReporter = new StatusReporter(10000)
|
||||
}
|
||||
|
||||
@@ -52,35 +58,51 @@ export class UploadHttpClient {
|
||||
* @returns The response from the Artifact Service if the file container was successfully created
|
||||
*/
|
||||
async createArtifactInFileContainer(
|
||||
artifactName: string
|
||||
artifactName: string,
|
||||
options?: UploadOptions | undefined
|
||||
): Promise<ArtifactResponse> {
|
||||
const parameters: CreateArtifactParameters = {
|
||||
Type: 'actions_storage',
|
||||
Name: artifactName
|
||||
}
|
||||
|
||||
// calculate retention period
|
||||
if (options && options.retentionDays) {
|
||||
const maxRetentionStr = getRetentionDays()
|
||||
parameters.RetentionDays = getProperRetention(
|
||||
options.retentionDays,
|
||||
maxRetentionStr
|
||||
)
|
||||
}
|
||||
|
||||
const data: string = JSON.stringify(parameters, null, 2)
|
||||
const artifactUrl = getArtifactUrl()
|
||||
|
||||
// use the first client from the httpManager, `keep-alive` is not used so the connection will close immediately
|
||||
const client = this.uploadHttpManager.getClient(0)
|
||||
const headers = getUploadHeaders('application/json', false)
|
||||
const rawResponse = await client.post(artifactUrl, data, headers)
|
||||
const body: string = await rawResponse.readBody()
|
||||
|
||||
if (isSuccessStatusCode(rawResponse.message.statusCode) && body) {
|
||||
return JSON.parse(body)
|
||||
} else if (isForbiddenStatusCode(rawResponse.message.statusCode)) {
|
||||
// if a 403 is returned when trying to create a file container, the customer has exceeded
|
||||
// their storage quota so no new artifact containers can be created
|
||||
throw new Error(
|
||||
`Artifact storage quota has been hit. Unable to upload any new artifacts`
|
||||
)
|
||||
} else {
|
||||
displayHttpDiagnostics(rawResponse)
|
||||
throw new Error(
|
||||
`Unable to create a container for the artifact ${artifactName} at ${artifactUrl}`
|
||||
)
|
||||
}
|
||||
// Extra information to display when a particular HTTP code is returned
|
||||
// If a 403 is returned when trying to create a file container, the customer has exceeded
|
||||
// their storage quota so no new artifact containers can be created
|
||||
const customErrorMessages: Map<number, string> = new Map([
|
||||
[
|
||||
HttpCodes.Forbidden,
|
||||
'Artifact storage quota has been hit. Unable to upload any new artifacts'
|
||||
],
|
||||
[
|
||||
HttpCodes.BadRequest,
|
||||
`The artifact name ${artifactName} is not valid. Request URL ${artifactUrl}`
|
||||
]
|
||||
])
|
||||
|
||||
const response = await retryHttpClientRequest(
|
||||
'Create Artifact Container',
|
||||
async () => client.post(artifactUrl, data, headers),
|
||||
customErrorMessages
|
||||
)
|
||||
const body: string = await response.readBody()
|
||||
return JSON.parse(body)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -401,13 +423,13 @@ export class UploadHttpClient {
|
||||
core.info(
|
||||
`Backoff due to too many requests, retry #${retryCount}. Waiting for ${retryAfterValue} milliseconds before continuing the upload`
|
||||
)
|
||||
await new Promise(resolve => setTimeout(resolve, retryAfterValue))
|
||||
await sleep(retryAfterValue)
|
||||
} else {
|
||||
const backoffTime = getExponentialRetryTimeInMilliseconds(retryCount)
|
||||
core.info(
|
||||
`Exponential backoff for retry #${retryCount}. Waiting for ${backoffTime} milliseconds before continuing the upload at offset ${start}`
|
||||
)
|
||||
await new Promise(resolve => setTimeout(resolve, backoffTime))
|
||||
await sleep(backoffTime)
|
||||
}
|
||||
core.info(
|
||||
`Finished backoff for retry #${retryCount}, continuing with upload`
|
||||
@@ -470,7 +492,6 @@ export class UploadHttpClient {
|
||||
* Updating the size indicates that we are done uploading all the contents of the artifact
|
||||
*/
|
||||
async patchArtifactSize(size: number, artifactName: string): Promise<void> {
|
||||
const headers = getUploadHeaders('application/json', false)
|
||||
const resourceUrl = new URL(getArtifactUrl())
|
||||
resourceUrl.searchParams.append('artifactName', artifactName)
|
||||
|
||||
@@ -480,25 +501,26 @@ export class UploadHttpClient {
|
||||
|
||||
// use the first client from the httpManager, `keep-alive` is not used so the connection will close immediately
|
||||
const client = this.uploadHttpManager.getClient(0)
|
||||
const response: HttpClientResponse = await client.patch(
|
||||
resourceUrl.toString(),
|
||||
data,
|
||||
headers
|
||||
const headers = getUploadHeaders('application/json', false)
|
||||
|
||||
// Extra information to display when a particular HTTP code is returned
|
||||
const customErrorMessages: Map<number, string> = new Map([
|
||||
[
|
||||
HttpCodes.NotFound,
|
||||
`An Artifact with the name ${artifactName} was not found`
|
||||
]
|
||||
])
|
||||
|
||||
// TODO retry for all possible response codes, the artifact upload is pretty much complete so it at all costs we should try to finish this
|
||||
const response = await retryHttpClientRequest(
|
||||
'Finalize artifact upload',
|
||||
async () => client.patch(resourceUrl.toString(), data, headers),
|
||||
customErrorMessages
|
||||
)
|
||||
await response.readBody()
|
||||
core.debug(
|
||||
`Artifact ${artifactName} has been successfully uploaded, total size in bytes: ${size}`
|
||||
)
|
||||
const body: string = await response.readBody()
|
||||
if (isSuccessStatusCode(response.message.statusCode)) {
|
||||
core.debug(
|
||||
`Artifact ${artifactName} has been successfully uploaded, total size in bytes: ${size}`
|
||||
)
|
||||
} else if (response.message.statusCode === 404) {
|
||||
throw new Error(`An Artifact with the name ${artifactName} was not found`)
|
||||
} else {
|
||||
displayHttpDiagnostics(response)
|
||||
core.info(body)
|
||||
throw new Error(
|
||||
`Unable to finish uploading artifact ${artifactName} to ${resourceUrl}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,4 +15,21 @@ export interface UploadOptions {
|
||||
*
|
||||
*/
|
||||
continueOnError?: boolean
|
||||
|
||||
/**
|
||||
* Duration after which artifact will expire in days.
|
||||
*
|
||||
* By default artifact expires after 90 days:
|
||||
* https://docs.github.com/en/actions/configuring-and-managing-workflows/persisting-workflow-data-using-artifacts#downloading-and-deleting-artifacts-after-a-workflow-run-is-complete
|
||||
*
|
||||
* Use this option to override the default expiry.
|
||||
*
|
||||
* Min value: 1
|
||||
* Max value: 90 unless changed by repository setting
|
||||
*
|
||||
* If this is set to a greater value than the retention settings allowed, the retention on artifacts
|
||||
* will be reduced to match the max value allowed on server, and the upload process will continue. An
|
||||
* input of 0 assumes default retention setting.
|
||||
*/
|
||||
retentionDays?: number
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {debug, info} from '@actions/core'
|
||||
import {debug, info, warning} from '@actions/core'
|
||||
import {promises as fs} from 'fs'
|
||||
import {HttpCodes, HttpClient} from '@actions/http-client'
|
||||
import {BearerCredentialHandler} from '@actions/http-client/auth'
|
||||
@@ -65,7 +65,7 @@ export function isForbiddenStatusCode(statusCode?: number): boolean {
|
||||
return statusCode === HttpCodes.Forbidden
|
||||
}
|
||||
|
||||
export function isRetryableStatusCode(statusCode?: number): boolean {
|
||||
export function isRetryableStatusCode(statusCode: number | undefined): boolean {
|
||||
if (!statusCode) {
|
||||
return false
|
||||
}
|
||||
@@ -74,7 +74,8 @@ export function isRetryableStatusCode(statusCode?: number): boolean {
|
||||
HttpCodes.BadGateway,
|
||||
HttpCodes.ServiceUnavailable,
|
||||
HttpCodes.GatewayTimeout,
|
||||
HttpCodes.TooManyRequests
|
||||
HttpCodes.TooManyRequests,
|
||||
413 // Payload Too Large
|
||||
]
|
||||
return retryableStatusCodes.includes(statusCode)
|
||||
}
|
||||
@@ -204,8 +205,8 @@ export function getUploadHeaders(
|
||||
return requestOptions
|
||||
}
|
||||
|
||||
export function createHttpClient(): HttpClient {
|
||||
return new HttpClient('actions/artifact', [
|
||||
export function createHttpClient(userAgent: string): HttpClient {
|
||||
return new HttpClient(userAgent, [
|
||||
new BearerCredentialHandler(getRuntimeToken())
|
||||
])
|
||||
}
|
||||
@@ -301,3 +302,40 @@ export async function createEmptyFilesForArtifact(
|
||||
await (await fs.open(filePath, 'w')).close()
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFileSize(filePath: string): Promise<number> {
|
||||
const stats = await fs.stat(filePath)
|
||||
debug(
|
||||
`${filePath} size:(${stats.size}) blksize:(${stats.blksize}) blocks:(${stats.blocks})`
|
||||
)
|
||||
return stats.size
|
||||
}
|
||||
|
||||
export async function rmFile(filePath: string): Promise<void> {
|
||||
await fs.unlink(filePath)
|
||||
}
|
||||
|
||||
export function getProperRetention(
|
||||
retentionInput: number,
|
||||
retentionSetting: string | undefined
|
||||
): number {
|
||||
if (retentionInput < 0) {
|
||||
throw new Error('Invalid retention, minimum value is 1.')
|
||||
}
|
||||
|
||||
let retention = retentionInput
|
||||
if (retentionSetting) {
|
||||
const maxRetention = parseInt(retentionSetting)
|
||||
if (!isNaN(maxRetention) && maxRetention < retention) {
|
||||
warning(
|
||||
`Retention days is greater than the max value allowed by the repository setting, reduce retention to ${maxRetention} days`
|
||||
)
|
||||
retention = maxRetention
|
||||
}
|
||||
}
|
||||
return retention
|
||||
}
|
||||
|
||||
export async function sleep(milliseconds: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, milliseconds))
|
||||
}
|
||||
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2019 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
Vendored
+14
-14
@@ -10,6 +10,20 @@ Note that GitHub will remove any cache entries that have not been accessed in ov
|
||||
|
||||
This package is used by the v2+ versions of our first party cache action. You can find an example implementation in the cache repo [here](https://github.com/actions/cache).
|
||||
|
||||
#### Save Cache
|
||||
|
||||
Saves a cache containing the files in `paths` using the `key` provided. The files would be compressed using zstandard compression algorithm if zstd is installed, otherwise gzip is used. Function returns the cache id if the cache was saved succesfully and throws an error if cache upload fails.
|
||||
|
||||
```js
|
||||
const cache = require('@actions/cache');
|
||||
const paths = [
|
||||
'node_modules',
|
||||
'packages/*/node_modules/'
|
||||
]
|
||||
const key = 'npm-foobar-d5ea0750'
|
||||
const cacheId = await cache.saveCache(paths, key)
|
||||
```
|
||||
|
||||
#### Restore Cache
|
||||
|
||||
Restores a cache based on `key` and `restoreKeys` to the `paths` provided. Function returns the cache key for cache hit and returns undefined if cache not found.
|
||||
@@ -28,17 +42,3 @@ const restoreKeys = [
|
||||
const cacheKey = await cache.restoreCache(paths, key, restoreKeys)
|
||||
```
|
||||
|
||||
#### Save Cache
|
||||
|
||||
Saves a cache containing the files in `paths` using the `key` provided. The files would be compressed using zstandard compression algorithm if zstd is installed, otherwise gzip is used. Function returns the cache id if the cache was saved succesfully and throws an error if cache upload fails.
|
||||
|
||||
```js
|
||||
const cache = require('@actions/cache');
|
||||
const paths = [
|
||||
'node_modules',
|
||||
'packages/*/node_modules/'
|
||||
]
|
||||
const key = 'npm-foobar-d5ea0750'
|
||||
const cacheId = await cache.saveCache(paths, key)
|
||||
```
|
||||
|
||||
|
||||
Vendored
+26
-1
@@ -14,4 +14,29 @@
|
||||
- Downloads Azure-hosted caches using the Azure SDK for speed and reliability
|
||||
- Displays download progress
|
||||
- Includes changes that break compatibility with earlier versions, including:
|
||||
- `retry`, `retryTypedResponse`, and `retryHttpClientResponse` moved from `cacheHttpClient` to `requestUtils`
|
||||
- `retry`, `retryTypedResponse`, and `retryHttpClientResponse` moved from `cacheHttpClient` to `requestUtils`
|
||||
|
||||
### 1.0.1
|
||||
- Fix bug in downloading large files (> 2 GBs) with the Azure SDK
|
||||
|
||||
### 1.0.2
|
||||
- Use posix archive format to add support for some tools
|
||||
|
||||
### 1.0.3
|
||||
- Use http-client v1.0.9
|
||||
- Fixes error handling so retries are not attempted on non-retryable errors (409 Conflict, for example)
|
||||
- Adds 5 second delay between retry attempts
|
||||
|
||||
### 1.0.4
|
||||
- Use @actions/core v1.2.6
|
||||
- Fixes uploadChunk to throw an error if any unsuccessful response code is received
|
||||
|
||||
### 1.0.5
|
||||
- Fix to ensure Windows cache paths get resolved correctly
|
||||
|
||||
### 1.0.6
|
||||
- Make caching more verbose [#650](https://github.com/actions/toolkit/pull/650)
|
||||
- Use GNU tar on macOS if available [#701](https://github.com/actions/toolkit/pull/701)
|
||||
|
||||
### 1.0.7
|
||||
- Fixes permissions issue extracting archives with GNU tar on macOS ([issue](https://github.com/actions/cache/issues/527))
|
||||
|
||||
+12
-3
@@ -1,5 +1,14 @@
|
||||
// Certain env variables are not set by default in a shell context and are only available in a node context from a running action
|
||||
// In order to be able to restore and save cache e2e in a shell when running CI tests, we need these env variables set
|
||||
console.log(`::set-env name=ACTIONS_RUNTIME_URL::${process.env.ACTIONS_RUNTIME_URL}`)
|
||||
console.log(`::set-env name=ACTIONS_RUNTIME_TOKEN::${process.env.ACTIONS_RUNTIME_TOKEN}`)
|
||||
console.log(`::set-env name=GITHUB_RUN_ID::${process.env.GITHUB_RUN_ID}`)
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const filePath = process.env[`GITHUB_ENV`]
|
||||
fs.appendFileSync(filePath, `ACTIONS_RUNTIME_URL=${process.env.ACTIONS_RUNTIME_URL}${os.EOL}`, {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
fs.appendFileSync(filePath, `ACTIONS_RUNTIME_TOKEN=${process.env.ACTIONS_RUNTIME_TOKEN}${os.EOL}`, {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
fs.appendFileSync(filePath, `GITHUB_RUN_ID=${process.env.GITHUB_RUN_ID}${os.EOL}`, {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
+2
-2
@@ -2,10 +2,10 @@ import {promises as fs} from 'fs'
|
||||
import * as path from 'path'
|
||||
import * as cacheUtils from '../src/internal/cacheUtils'
|
||||
|
||||
test('getArchiveFileSizeIsBytes returns file size', () => {
|
||||
test('getArchiveFileSizeInBytes returns file size', () => {
|
||||
const filePath = path.join(__dirname, '__fixtures__', 'helloWorld.txt')
|
||||
|
||||
const size = cacheUtils.getArchiveFileSizeIsBytes(filePath)
|
||||
const size = cacheUtils.getArchiveFileSizeInBytes(filePath)
|
||||
|
||||
expect(size).toBe(11)
|
||||
})
|
||||
|
||||
+78
-70
@@ -1,27 +1,48 @@
|
||||
import {retry} from '../src/internal/requestUtils'
|
||||
import {HttpClientError} from '@actions/http-client'
|
||||
|
||||
interface TestResponse {
|
||||
interface ITestResponse {
|
||||
statusCode: number
|
||||
result: string | null
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
function TestResponse(
|
||||
action: number | Error,
|
||||
result: string | null = null
|
||||
): ITestResponse {
|
||||
if (action instanceof Error) {
|
||||
return {
|
||||
statusCode: -1,
|
||||
result,
|
||||
error: action
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
statusCode: action,
|
||||
result,
|
||||
error: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResponse(
|
||||
response: TestResponse | undefined
|
||||
): Promise<TestResponse> {
|
||||
response: ITestResponse | undefined
|
||||
): Promise<ITestResponse> {
|
||||
if (!response) {
|
||||
// eslint-disable-next-line no-undef
|
||||
fail('Retry method called too many times')
|
||||
}
|
||||
|
||||
if (response.statusCode === 999) {
|
||||
throw Error('Test Error')
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
} else {
|
||||
return Promise.resolve(response)
|
||||
}
|
||||
}
|
||||
|
||||
async function testRetryExpectingResult(
|
||||
responses: TestResponse[],
|
||||
responses: ITestResponse[],
|
||||
expectedResult: string | null
|
||||
): Promise<void> {
|
||||
responses = responses.reverse() // Reverse responses since we pop from end
|
||||
@@ -29,14 +50,44 @@ async function testRetryExpectingResult(
|
||||
const actualResult = await retry(
|
||||
'test',
|
||||
async () => handleResponse(responses.pop()),
|
||||
(response: TestResponse) => response.statusCode
|
||||
(response: ITestResponse) => response.statusCode,
|
||||
2, // maxAttempts
|
||||
0 // delay
|
||||
)
|
||||
|
||||
expect(actualResult.result).toEqual(expectedResult)
|
||||
}
|
||||
|
||||
async function testRetryConvertingErrorToResult(
|
||||
responses: ITestResponse[],
|
||||
expectedStatus: number,
|
||||
expectedResult: string | null
|
||||
): Promise<void> {
|
||||
responses = responses.reverse() // Reverse responses since we pop from end
|
||||
|
||||
const actualResult = await retry(
|
||||
'test',
|
||||
async () => handleResponse(responses.pop()),
|
||||
(response: ITestResponse) => response.statusCode,
|
||||
2, // maxAttempts
|
||||
0, // delay
|
||||
(e: Error) => {
|
||||
if (e instanceof HttpClientError) {
|
||||
return {
|
||||
statusCode: e.statusCode,
|
||||
result: null,
|
||||
error: null
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(actualResult.statusCode).toEqual(expectedStatus)
|
||||
expect(actualResult.result).toEqual(expectedResult)
|
||||
}
|
||||
|
||||
async function testRetryExpectingError(
|
||||
responses: TestResponse[]
|
||||
responses: ITestResponse[]
|
||||
): Promise<void> {
|
||||
responses = responses.reverse() // Reverse responses since we pop from end
|
||||
|
||||
@@ -44,97 +95,54 @@ async function testRetryExpectingError(
|
||||
retry(
|
||||
'test',
|
||||
async () => handleResponse(responses.pop()),
|
||||
(response: TestResponse) => response.statusCode
|
||||
(response: ITestResponse) => response.statusCode,
|
||||
2, // maxAttempts,
|
||||
0 // delay
|
||||
)
|
||||
).rejects.toBeInstanceOf(Error)
|
||||
}
|
||||
|
||||
test('retry works on successful response', async () => {
|
||||
await testRetryExpectingResult(
|
||||
[
|
||||
{
|
||||
statusCode: 200,
|
||||
result: 'Ok'
|
||||
}
|
||||
],
|
||||
'Ok'
|
||||
)
|
||||
await testRetryExpectingResult([TestResponse(200, 'Ok')], 'Ok')
|
||||
})
|
||||
|
||||
test('retry works after retryable status code', async () => {
|
||||
await testRetryExpectingResult(
|
||||
[
|
||||
{
|
||||
statusCode: 503,
|
||||
result: null
|
||||
},
|
||||
{
|
||||
statusCode: 200,
|
||||
result: 'Ok'
|
||||
}
|
||||
],
|
||||
[TestResponse(503), TestResponse(200, 'Ok')],
|
||||
'Ok'
|
||||
)
|
||||
})
|
||||
|
||||
test('retry fails after exhausting retries', async () => {
|
||||
await testRetryExpectingError([
|
||||
{
|
||||
statusCode: 503,
|
||||
result: null
|
||||
},
|
||||
{
|
||||
statusCode: 503,
|
||||
result: null
|
||||
},
|
||||
{
|
||||
statusCode: 200,
|
||||
result: 'Ok'
|
||||
}
|
||||
TestResponse(503),
|
||||
TestResponse(503),
|
||||
TestResponse(200, 'Ok')
|
||||
])
|
||||
})
|
||||
|
||||
test('retry fails after non-retryable status code', async () => {
|
||||
await testRetryExpectingError([
|
||||
{
|
||||
statusCode: 500,
|
||||
result: null
|
||||
},
|
||||
{
|
||||
statusCode: 200,
|
||||
result: 'Ok'
|
||||
}
|
||||
])
|
||||
await testRetryExpectingError([TestResponse(500), TestResponse(200, 'Ok')])
|
||||
})
|
||||
|
||||
test('retry works after error', async () => {
|
||||
await testRetryExpectingResult(
|
||||
[
|
||||
{
|
||||
statusCode: 999,
|
||||
result: null
|
||||
},
|
||||
{
|
||||
statusCode: 200,
|
||||
result: 'Ok'
|
||||
}
|
||||
],
|
||||
[TestResponse(new Error('Test error')), TestResponse(200, 'Ok')],
|
||||
'Ok'
|
||||
)
|
||||
})
|
||||
|
||||
test('retry returns after client error', async () => {
|
||||
await testRetryExpectingResult(
|
||||
[
|
||||
{
|
||||
statusCode: 400,
|
||||
result: null
|
||||
},
|
||||
{
|
||||
statusCode: 200,
|
||||
result: 'Ok'
|
||||
}
|
||||
],
|
||||
[TestResponse(400), TestResponse(200, 'Ok')],
|
||||
null
|
||||
)
|
||||
})
|
||||
|
||||
test('retry converts errors to response object', async () => {
|
||||
await testRetryConvertingErrorToResult(
|
||||
[TestResponse(new HttpClientError('Test error', 409))],
|
||||
409,
|
||||
null
|
||||
)
|
||||
})
|
||||
|
||||
+12
-12
@@ -104,7 +104,7 @@ test('restore with gzip compressed cache found', async () => {
|
||||
|
||||
const cacheEntry: ArtifactCacheEntry = {
|
||||
cacheKey: key,
|
||||
scope: 'refs/heads/master',
|
||||
scope: 'refs/heads/main',
|
||||
archiveLocation: 'www.actionscache.test/download'
|
||||
}
|
||||
const getCacheMock = jest.spyOn(cacheHttpClient, 'getCacheEntry')
|
||||
@@ -123,8 +123,8 @@ test('restore with gzip compressed cache found', async () => {
|
||||
const downloadCacheMock = jest.spyOn(cacheHttpClient, 'downloadCache')
|
||||
|
||||
const fileSize = 142
|
||||
const getArchiveFileSizeIsBytesMock = jest
|
||||
.spyOn(cacheUtils, 'getArchiveFileSizeIsBytes')
|
||||
const getArchiveFileSizeInBytesMock = jest
|
||||
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
||||
.mockReturnValue(fileSize)
|
||||
|
||||
const extractTarMock = jest.spyOn(tar, 'extractTar')
|
||||
@@ -147,7 +147,7 @@ test('restore with gzip compressed cache found', async () => {
|
||||
archivePath,
|
||||
undefined
|
||||
)
|
||||
expect(getArchiveFileSizeIsBytesMock).toHaveBeenCalledWith(archivePath)
|
||||
expect(getArchiveFileSizeInBytesMock).toHaveBeenCalledWith(archivePath)
|
||||
|
||||
expect(extractTarMock).toHaveBeenCalledTimes(1)
|
||||
expect(extractTarMock).toHaveBeenCalledWith(archivePath, compression)
|
||||
@@ -166,7 +166,7 @@ test('restore with zstd compressed cache found', async () => {
|
||||
|
||||
const cacheEntry: ArtifactCacheEntry = {
|
||||
cacheKey: key,
|
||||
scope: 'refs/heads/master',
|
||||
scope: 'refs/heads/main',
|
||||
archiveLocation: 'www.actionscache.test/download'
|
||||
}
|
||||
const getCacheMock = jest.spyOn(cacheHttpClient, 'getCacheEntry')
|
||||
@@ -184,8 +184,8 @@ test('restore with zstd compressed cache found', async () => {
|
||||
const downloadCacheMock = jest.spyOn(cacheHttpClient, 'downloadCache')
|
||||
|
||||
const fileSize = 62915000
|
||||
const getArchiveFileSizeIsBytesMock = jest
|
||||
.spyOn(cacheUtils, 'getArchiveFileSizeIsBytes')
|
||||
const getArchiveFileSizeInBytesMock = jest
|
||||
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
||||
.mockReturnValue(fileSize)
|
||||
|
||||
const extractTarMock = jest.spyOn(tar, 'extractTar')
|
||||
@@ -206,7 +206,7 @@ test('restore with zstd compressed cache found', async () => {
|
||||
archivePath,
|
||||
undefined
|
||||
)
|
||||
expect(getArchiveFileSizeIsBytesMock).toHaveBeenCalledWith(archivePath)
|
||||
expect(getArchiveFileSizeInBytesMock).toHaveBeenCalledWith(archivePath)
|
||||
expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~60 MB (62915000 B)`)
|
||||
|
||||
expect(extractTarMock).toHaveBeenCalledTimes(1)
|
||||
@@ -223,7 +223,7 @@ test('restore with cache found for restore key', async () => {
|
||||
|
||||
const cacheEntry: ArtifactCacheEntry = {
|
||||
cacheKey: restoreKey,
|
||||
scope: 'refs/heads/master',
|
||||
scope: 'refs/heads/main',
|
||||
archiveLocation: 'www.actionscache.test/download'
|
||||
}
|
||||
const getCacheMock = jest.spyOn(cacheHttpClient, 'getCacheEntry')
|
||||
@@ -241,8 +241,8 @@ test('restore with cache found for restore key', async () => {
|
||||
const downloadCacheMock = jest.spyOn(cacheHttpClient, 'downloadCache')
|
||||
|
||||
const fileSize = 142
|
||||
const getArchiveFileSizeIsBytesMock = jest
|
||||
.spyOn(cacheUtils, 'getArchiveFileSizeIsBytes')
|
||||
const getArchiveFileSizeInBytesMock = jest
|
||||
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
||||
.mockReturnValue(fileSize)
|
||||
|
||||
const extractTarMock = jest.spyOn(tar, 'extractTar')
|
||||
@@ -263,7 +263,7 @@ test('restore with cache found for restore key', async () => {
|
||||
archivePath,
|
||||
undefined
|
||||
)
|
||||
expect(getArchiveFileSizeIsBytesMock).toHaveBeenCalledWith(archivePath)
|
||||
expect(getArchiveFileSizeInBytesMock).toHaveBeenCalledWith(archivePath)
|
||||
expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~0 MB (142 B)`)
|
||||
|
||||
expect(extractTarMock).toHaveBeenCalledTimes(1)
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ test('save with large cache outputs should fail', async () => {
|
||||
|
||||
const cacheSize = 6 * 1024 * 1024 * 1024 //~6GB, over the 5GB limit
|
||||
jest
|
||||
.spyOn(cacheUtils, 'getArchiveFileSizeIsBytes')
|
||||
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
||||
.mockReturnValueOnce(cacheSize)
|
||||
const compression = CompressionMethod.Gzip
|
||||
const getCompressionMock = jest
|
||||
|
||||
Vendored
+91
-10
@@ -11,6 +11,9 @@ jest.mock('@actions/exec')
|
||||
jest.mock('@actions/io')
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32'
|
||||
const IS_MAC = process.platform === 'darwin'
|
||||
|
||||
const defaultTarPath = process.platform === 'darwin' ? 'gtar' : 'tar'
|
||||
|
||||
function getTempDir(): string {
|
||||
return path.join(__dirname, '_temp', 'tar')
|
||||
@@ -38,14 +41,13 @@ test('zstd extract tar', async () => {
|
||||
? `${process.env['windir']}\\fakepath\\cache.tar`
|
||||
: 'cache.tar'
|
||||
const workspace = process.env['GITHUB_WORKSPACE']
|
||||
const tarPath = 'tar'
|
||||
|
||||
await tar.extractTar(archivePath, CompressionMethod.Zstd)
|
||||
|
||||
expect(mkdirMock).toHaveBeenCalledWith(workspace)
|
||||
expect(execMock).toHaveBeenCalledTimes(1)
|
||||
expect(execMock).toHaveBeenCalledWith(
|
||||
`"${tarPath}"`,
|
||||
`"${defaultTarPath}"`,
|
||||
[
|
||||
'--use-compress-program',
|
||||
'zstd -d --long=30',
|
||||
@@ -54,7 +56,9 @@ test('zstd extract tar', async () => {
|
||||
'-P',
|
||||
'-C',
|
||||
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace
|
||||
].concat(IS_WINDOWS ? ['--force-local'] : []),
|
||||
]
|
||||
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
||||
.concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
||||
{cwd: undefined}
|
||||
)
|
||||
})
|
||||
@@ -72,7 +76,7 @@ test('gzip extract tar', async () => {
|
||||
expect(mkdirMock).toHaveBeenCalledWith(workspace)
|
||||
const tarPath = IS_WINDOWS
|
||||
? `${process.env['windir']}\\System32\\tar.exe`
|
||||
: 'tar'
|
||||
: defaultTarPath
|
||||
expect(execMock).toHaveBeenCalledTimes(1)
|
||||
expect(execMock).toHaveBeenCalledWith(
|
||||
`"${tarPath}"`,
|
||||
@@ -83,7 +87,7 @@ test('gzip extract tar', async () => {
|
||||
'-P',
|
||||
'-C',
|
||||
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace
|
||||
],
|
||||
].concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
||||
{cwd: undefined}
|
||||
)
|
||||
})
|
||||
@@ -125,7 +129,6 @@ test('zstd create tar', async () => {
|
||||
const archiveFolder = getTempDir()
|
||||
const workspace = process.env['GITHUB_WORKSPACE']
|
||||
const sourceDirectories = ['~/.npm/cache', `${workspace}/dist`]
|
||||
const tarPath = 'tar'
|
||||
|
||||
await fs.promises.mkdir(archiveFolder, {recursive: true})
|
||||
|
||||
@@ -133,8 +136,9 @@ test('zstd create tar', async () => {
|
||||
|
||||
expect(execMock).toHaveBeenCalledTimes(1)
|
||||
expect(execMock).toHaveBeenCalledWith(
|
||||
`"${tarPath}"`,
|
||||
`"${defaultTarPath}"`,
|
||||
[
|
||||
'--posix',
|
||||
'--use-compress-program',
|
||||
'zstd -T0 --long=30',
|
||||
'-cf',
|
||||
@@ -144,7 +148,9 @@ test('zstd create tar', async () => {
|
||||
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace,
|
||||
'--files-from',
|
||||
'manifest.txt'
|
||||
].concat(IS_WINDOWS ? ['--force-local'] : []),
|
||||
]
|
||||
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
||||
.concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
||||
{
|
||||
cwd: archiveFolder
|
||||
}
|
||||
@@ -164,12 +170,13 @@ test('gzip create tar', async () => {
|
||||
|
||||
const tarPath = IS_WINDOWS
|
||||
? `${process.env['windir']}\\System32\\tar.exe`
|
||||
: 'tar'
|
||||
: defaultTarPath
|
||||
|
||||
expect(execMock).toHaveBeenCalledTimes(1)
|
||||
expect(execMock).toHaveBeenCalledWith(
|
||||
`"${tarPath}"`,
|
||||
[
|
||||
'--posix',
|
||||
'-z',
|
||||
'-cf',
|
||||
IS_WINDOWS ? CacheFilename.Gzip.replace(/\\/g, '/') : CacheFilename.Gzip,
|
||||
@@ -178,9 +185,83 @@ test('gzip create tar', async () => {
|
||||
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace,
|
||||
'--files-from',
|
||||
'manifest.txt'
|
||||
],
|
||||
].concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
||||
{
|
||||
cwd: archiveFolder
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test('zstd list tar', async () => {
|
||||
const execMock = jest.spyOn(exec, 'exec')
|
||||
|
||||
const archivePath = IS_WINDOWS
|
||||
? `${process.env['windir']}\\fakepath\\cache.tar`
|
||||
: 'cache.tar'
|
||||
|
||||
await tar.listTar(archivePath, CompressionMethod.Zstd)
|
||||
|
||||
expect(execMock).toHaveBeenCalledTimes(1)
|
||||
expect(execMock).toHaveBeenCalledWith(
|
||||
`"${defaultTarPath}"`,
|
||||
[
|
||||
'--use-compress-program',
|
||||
'zstd -d --long=30',
|
||||
'-tf',
|
||||
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
||||
'-P'
|
||||
]
|
||||
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
||||
.concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
||||
{cwd: undefined}
|
||||
)
|
||||
})
|
||||
|
||||
test('zstdWithoutLong list tar', async () => {
|
||||
const execMock = jest.spyOn(exec, 'exec')
|
||||
|
||||
const archivePath = IS_WINDOWS
|
||||
? `${process.env['windir']}\\fakepath\\cache.tar`
|
||||
: 'cache.tar'
|
||||
|
||||
await tar.listTar(archivePath, CompressionMethod.ZstdWithoutLong)
|
||||
|
||||
expect(execMock).toHaveBeenCalledTimes(1)
|
||||
expect(execMock).toHaveBeenCalledWith(
|
||||
`"${defaultTarPath}"`,
|
||||
[
|
||||
'--use-compress-program',
|
||||
'zstd -d',
|
||||
'-tf',
|
||||
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
||||
'-P'
|
||||
]
|
||||
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
||||
.concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
||||
{cwd: undefined}
|
||||
)
|
||||
})
|
||||
|
||||
test('gzip list tar', async () => {
|
||||
const execMock = jest.spyOn(exec, 'exec')
|
||||
const archivePath = IS_WINDOWS
|
||||
? `${process.env['windir']}\\fakepath\\cache.tar`
|
||||
: 'cache.tar'
|
||||
|
||||
await tar.listTar(archivePath, CompressionMethod.Gzip)
|
||||
|
||||
const tarPath = IS_WINDOWS
|
||||
? `${process.env['windir']}\\System32\\tar.exe`
|
||||
: defaultTarPath
|
||||
expect(execMock).toHaveBeenCalledTimes(1)
|
||||
expect(execMock).toHaveBeenCalledWith(
|
||||
`"${tarPath}"`,
|
||||
[
|
||||
'-z',
|
||||
'-tf',
|
||||
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
||||
'-P'
|
||||
].concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
||||
{cwd: undefined}
|
||||
)
|
||||
})
|
||||
|
||||
+4200
-34
File diff suppressed because it is too large
Load Diff
Vendored
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/cache",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.7",
|
||||
"preview": true,
|
||||
"description": "Actions cache lib",
|
||||
"keywords": [
|
||||
@@ -8,7 +8,7 @@
|
||||
"actions",
|
||||
"cache"
|
||||
],
|
||||
"homepage": "https://github.com/actions/toolkit/tree/master/packages/cache",
|
||||
"homepage": "https://github.com/actions/toolkit/tree/main/packages/cache",
|
||||
"license": "MIT",
|
||||
"main": "lib/cache.js",
|
||||
"types": "lib/cache.d.ts",
|
||||
@@ -37,10 +37,10 @@
|
||||
"url": "https://github.com/actions/toolkit/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.4",
|
||||
"@actions/core": "^1.2.6",
|
||||
"@actions/exec": "^1.0.1",
|
||||
"@actions/glob": "^0.1.0",
|
||||
"@actions/http-client": "^1.0.8",
|
||||
"@actions/http-client": "^1.0.9",
|
||||
"@actions/io": "^1.0.1",
|
||||
"@azure/ms-rest-js": "^2.0.7",
|
||||
"@azure/storage-blob": "^12.1.2",
|
||||
|
||||
Vendored
+11
-3
@@ -2,7 +2,7 @@ import * as core from '@actions/core'
|
||||
import * as path from 'path'
|
||||
import * as utils from './internal/cacheUtils'
|
||||
import * as cacheHttpClient from './internal/cacheHttpClient'
|
||||
import {createTar, extractTar} from './internal/tar'
|
||||
import {createTar, extractTar, listTar} from './internal/tar'
|
||||
import {DownloadOptions, UploadOptions} from './options'
|
||||
|
||||
export class ValidationError extends Error {
|
||||
@@ -100,7 +100,11 @@ export async function restoreCache(
|
||||
options
|
||||
)
|
||||
|
||||
const archiveFileSize = utils.getArchiveFileSizeIsBytes(archivePath)
|
||||
if (core.isDebug()) {
|
||||
await listTar(archivePath, compressionMethod)
|
||||
}
|
||||
|
||||
const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath)
|
||||
core.info(
|
||||
`Cache Size: ~${Math.round(
|
||||
archiveFileSize / (1024 * 1024)
|
||||
@@ -108,6 +112,7 @@ export async function restoreCache(
|
||||
)
|
||||
|
||||
await extractTar(archivePath, compressionMethod)
|
||||
core.info('Cache restored successfully')
|
||||
} finally {
|
||||
// Try to delete the archive to save space
|
||||
try {
|
||||
@@ -162,9 +167,12 @@ export async function saveCache(
|
||||
core.debug(`Archive Path: ${archivePath}`)
|
||||
|
||||
await createTar(archiveFolder, cachePaths, compressionMethod)
|
||||
if (core.isDebug()) {
|
||||
await listTar(archivePath, compressionMethod)
|
||||
}
|
||||
|
||||
const fileSizeLimit = 5 * 1024 * 1024 * 1024 // 5GB per repo limit
|
||||
const archiveFileSize = utils.getArchiveFileSizeIsBytes(archivePath)
|
||||
const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath)
|
||||
core.debug(`File Size: ${archiveFileSize}`)
|
||||
if (archiveFileSize > fileSizeLimit) {
|
||||
throw new Error(
|
||||
|
||||
+13
-3
@@ -194,7 +194,7 @@ async function uploadChunk(
|
||||
'Content-Range': getContentRange(start, end)
|
||||
}
|
||||
|
||||
await retryHttpClientResponse(
|
||||
const uploadChunkResponse = await retryHttpClientResponse(
|
||||
`uploadChunk (start: ${start}, end: ${end})`,
|
||||
async () =>
|
||||
httpClient.sendStream(
|
||||
@@ -204,6 +204,12 @@ async function uploadChunk(
|
||||
additionalHeaders
|
||||
)
|
||||
)
|
||||
|
||||
if (!isSuccessStatusCode(uploadChunkResponse.message.statusCode)) {
|
||||
throw new Error(
|
||||
`Cache service responded with ${uploadChunkResponse.message.statusCode} during upload chunk.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadFile(
|
||||
@@ -213,7 +219,7 @@ async function uploadFile(
|
||||
options?: UploadOptions
|
||||
): Promise<void> {
|
||||
// Upload Chunks
|
||||
const fileSize = fs.statSync(archivePath).size
|
||||
const fileSize = utils.getArchiveFileSizeInBytes(archivePath)
|
||||
const resourceUrl = getCacheApiUrl(`caches/${cacheId.toString()}`)
|
||||
const fd = fs.openSync(archivePath, 'r')
|
||||
const uploadOptions = getUploadOptions(options)
|
||||
@@ -294,7 +300,11 @@ export async function saveCache(
|
||||
|
||||
// Commit Cache
|
||||
core.debug('Commiting cache')
|
||||
const cacheSize = utils.getArchiveFileSizeIsBytes(archivePath)
|
||||
const cacheSize = utils.getArchiveFileSizeInBytes(archivePath)
|
||||
core.info(
|
||||
`Cache Size: ~${Math.round(cacheSize / (1024 * 1024))} MB (${cacheSize} B)`
|
||||
)
|
||||
|
||||
const commitCacheResponse = await commitCache(httpClient, cacheId, cacheSize)
|
||||
if (!isSuccessStatusCode(commitCacheResponse.statusCode)) {
|
||||
throw new Error(
|
||||
|
||||
+5
-3
@@ -9,7 +9,7 @@ import * as util from 'util'
|
||||
import {v4 as uuidV4} from 'uuid'
|
||||
import {CacheFilename, CompressionMethod} from './constants'
|
||||
|
||||
// From https://github.com/actions/toolkit/blob/master/packages/tool-cache/src/tool-cache.ts#L23
|
||||
// From https://github.com/actions/toolkit/blob/main/packages/tool-cache/src/tool-cache.ts#L23
|
||||
export async function createTempDirectory(): Promise<string> {
|
||||
const IS_WINDOWS = process.platform === 'win32'
|
||||
|
||||
@@ -35,7 +35,7 @@ export async function createTempDirectory(): Promise<string> {
|
||||
return dest
|
||||
}
|
||||
|
||||
export function getArchiveFileSizeIsBytes(filePath: string): number {
|
||||
export function getArchiveFileSizeInBytes(filePath: string): number {
|
||||
return fs.statSync(filePath).size
|
||||
}
|
||||
|
||||
@@ -47,7 +47,9 @@ export async function resolvePaths(patterns: string[]): Promise<string[]> {
|
||||
})
|
||||
|
||||
for await (const file of globber.globGenerator()) {
|
||||
const relativeFile = path.relative(workspace, file)
|
||||
const relativeFile = path
|
||||
.relative(workspace, file)
|
||||
.replace(new RegExp(`\\${path.sep}`, 'g'), '/')
|
||||
core.debug(`Matched: ${relativeFile}`)
|
||||
// Paths are made relative so the tar entries are all relative to the root of the workspace.
|
||||
paths.push(`${relativeFile}`)
|
||||
|
||||
+6
@@ -11,6 +11,12 @@ export enum CompressionMethod {
|
||||
Zstd = 'zstd'
|
||||
}
|
||||
|
||||
// The default number of retry attempts.
|
||||
export const DefaultRetryAttempts = 2
|
||||
|
||||
// The default delay in milliseconds between retry attempts.
|
||||
export const DefaultRetryDelay = 5000
|
||||
|
||||
// Socket timeout in milliseconds during download. If no traffic is received
|
||||
// over the socket during this period, the socket is destroyed and the download
|
||||
// is aborted.
|
||||
|
||||
+6
-3
@@ -190,7 +190,7 @@ export async function downloadCacheHttpClient(
|
||||
|
||||
if (contentLengthHeader) {
|
||||
const expectedLength = parseInt(contentLengthHeader)
|
||||
const actualLength = utils.getArchiveFileSizeIsBytes(archivePath)
|
||||
const actualLength = utils.getArchiveFileSizeInBytes(archivePath)
|
||||
|
||||
if (actualLength !== expectedLength) {
|
||||
throw new Error(
|
||||
@@ -249,15 +249,18 @@ export async function downloadCacheStorageSDK(
|
||||
downloadProgress.startDisplayTimer()
|
||||
|
||||
while (!downloadProgress.isDone()) {
|
||||
const segmentStart =
|
||||
downloadProgress.segmentOffset + downloadProgress.segmentSize
|
||||
|
||||
const segmentSize = Math.min(
|
||||
maxSegmentSize,
|
||||
contentLength - downloadProgress.segmentOffset
|
||||
contentLength - segmentStart
|
||||
)
|
||||
|
||||
downloadProgress.nextSegment(segmentSize)
|
||||
|
||||
const result = await client.downloadToBuffer(
|
||||
downloadProgress.segmentOffset,
|
||||
segmentStart,
|
||||
segmentSize,
|
||||
{
|
||||
concurrency: options.downloadConcurrency,
|
||||
|
||||
+47
-12
@@ -1,9 +1,10 @@
|
||||
import * as core from '@actions/core'
|
||||
import {HttpCodes} from '@actions/http-client'
|
||||
import {HttpCodes, HttpClientError} from '@actions/http-client'
|
||||
import {
|
||||
IHttpClientResponse,
|
||||
ITypedResponse
|
||||
} from '@actions/http-client/interfaces'
|
||||
import {DefaultRetryDelay, DefaultRetryAttempts} from './constants'
|
||||
|
||||
export function isSuccessStatusCode(statusCode?: number): boolean {
|
||||
if (!statusCode) {
|
||||
@@ -31,32 +32,48 @@ export function isRetryableStatusCode(statusCode?: number): boolean {
|
||||
return retryableStatusCodes.includes(statusCode)
|
||||
}
|
||||
|
||||
async function sleep(milliseconds: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, milliseconds))
|
||||
}
|
||||
|
||||
export async function retry<T>(
|
||||
name: string,
|
||||
method: () => Promise<T>,
|
||||
getStatusCode: (arg0: T) => number | undefined,
|
||||
maxAttempts = 2
|
||||
maxAttempts = DefaultRetryAttempts,
|
||||
delay = DefaultRetryDelay,
|
||||
onError: ((arg0: Error) => T | undefined) | undefined = undefined
|
||||
): Promise<T> {
|
||||
let response: T | undefined = undefined
|
||||
let statusCode: number | undefined = undefined
|
||||
let isRetryable = false
|
||||
let errorMessage = ''
|
||||
let attempt = 1
|
||||
|
||||
while (attempt <= maxAttempts) {
|
||||
let response: T | undefined = undefined
|
||||
let statusCode: number | undefined = undefined
|
||||
let isRetryable = false
|
||||
|
||||
try {
|
||||
response = await method()
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
response = onError(error)
|
||||
}
|
||||
|
||||
isRetryable = true
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
if (response) {
|
||||
statusCode = getStatusCode(response)
|
||||
|
||||
if (!isServerErrorStatusCode(statusCode)) {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
if (statusCode) {
|
||||
isRetryable = isRetryableStatusCode(statusCode)
|
||||
errorMessage = `Cache service responded with ${statusCode}`
|
||||
} catch (error) {
|
||||
isRetryable = true
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
core.debug(
|
||||
@@ -68,6 +85,7 @@ export async function retry<T>(
|
||||
break
|
||||
}
|
||||
|
||||
await sleep(delay)
|
||||
attempt++
|
||||
}
|
||||
|
||||
@@ -77,25 +95,42 @@ export async function retry<T>(
|
||||
export async function retryTypedResponse<T>(
|
||||
name: string,
|
||||
method: () => Promise<ITypedResponse<T>>,
|
||||
maxAttempts = 2
|
||||
maxAttempts = DefaultRetryAttempts,
|
||||
delay = DefaultRetryDelay
|
||||
): Promise<ITypedResponse<T>> {
|
||||
return await retry(
|
||||
name,
|
||||
method,
|
||||
(response: ITypedResponse<T>) => response.statusCode,
|
||||
maxAttempts
|
||||
maxAttempts,
|
||||
delay,
|
||||
// If the error object contains the statusCode property, extract it and return
|
||||
// an ITypedResponse<T> so it can be processed by the retry logic.
|
||||
(error: Error) => {
|
||||
if (error instanceof HttpClientError) {
|
||||
return {
|
||||
statusCode: error.statusCode,
|
||||
result: null,
|
||||
headers: {}
|
||||
}
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export async function retryHttpClientResponse<T>(
|
||||
name: string,
|
||||
method: () => Promise<IHttpClientResponse>,
|
||||
maxAttempts = 2
|
||||
maxAttempts = DefaultRetryAttempts,
|
||||
delay = DefaultRetryDelay
|
||||
): Promise<IHttpClientResponse> {
|
||||
return await retry(
|
||||
name,
|
||||
method,
|
||||
(response: IHttpClientResponse) => response.message.statusCode,
|
||||
maxAttempts
|
||||
maxAttempts,
|
||||
delay
|
||||
)
|
||||
}
|
||||
|
||||
Vendored
+52
-11
@@ -9,18 +9,31 @@ async function getTarPath(
|
||||
args: string[],
|
||||
compressionMethod: CompressionMethod
|
||||
): Promise<string> {
|
||||
const IS_WINDOWS = process.platform === 'win32'
|
||||
if (IS_WINDOWS) {
|
||||
const systemTar = `${process.env['windir']}\\System32\\tar.exe`
|
||||
if (compressionMethod !== CompressionMethod.Gzip) {
|
||||
// We only use zstandard compression on windows when gnu tar is installed due to
|
||||
// a bug with compressing large files with bsdtar + zstd
|
||||
args.push('--force-local')
|
||||
} else if (existsSync(systemTar)) {
|
||||
return systemTar
|
||||
} else if (await utils.isGnuTarInstalled()) {
|
||||
args.push('--force-local')
|
||||
switch (process.platform) {
|
||||
case 'win32': {
|
||||
const systemTar = `${process.env['windir']}\\System32\\tar.exe`
|
||||
if (compressionMethod !== CompressionMethod.Gzip) {
|
||||
// We only use zstandard compression on windows when gnu tar is installed due to
|
||||
// a bug with compressing large files with bsdtar + zstd
|
||||
args.push('--force-local')
|
||||
} else if (existsSync(systemTar)) {
|
||||
return systemTar
|
||||
} else if (await utils.isGnuTarInstalled()) {
|
||||
args.push('--force-local')
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'darwin': {
|
||||
const gnuTar = await io.which('gtar', false)
|
||||
if (gnuTar) {
|
||||
// fix permission denied errors when extracting BSD tar archive with GNU tar - https://github.com/actions/cache/issues/527
|
||||
args.push('--delay-directory-restore')
|
||||
return gnuTar
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return await io.which('tar', true)
|
||||
}
|
||||
@@ -101,6 +114,7 @@ export async function createTar(
|
||||
}
|
||||
}
|
||||
const args = [
|
||||
'--posix',
|
||||
...getCompressionProgram(),
|
||||
'-cf',
|
||||
cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
|
||||
@@ -112,3 +126,30 @@ export async function createTar(
|
||||
]
|
||||
await execTar(args, compressionMethod, archiveFolder)
|
||||
}
|
||||
|
||||
export async function listTar(
|
||||
archivePath: string,
|
||||
compressionMethod: CompressionMethod
|
||||
): Promise<void> {
|
||||
// --d: Decompress.
|
||||
// --long=#: Enables long distance matching with # bits.
|
||||
// Maximum is 30 (1GB) on 32-bit OS and 31 (2GB) on 64-bit.
|
||||
// Using 30 here because we also support 32-bit self-hosted runners.
|
||||
function getCompressionProgram(): string[] {
|
||||
switch (compressionMethod) {
|
||||
case CompressionMethod.Zstd:
|
||||
return ['--use-compress-program', 'zstd -d --long=30']
|
||||
case CompressionMethod.ZstdWithoutLong:
|
||||
return ['--use-compress-program', 'zstd -d']
|
||||
default:
|
||||
return ['-z']
|
||||
}
|
||||
}
|
||||
const args = [
|
||||
...getCompressionProgram(),
|
||||
'-tf',
|
||||
archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
|
||||
'-P'
|
||||
]
|
||||
await execTar(args, compressionMethod)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2019 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+66
-4
@@ -16,11 +16,13 @@ import * as core from '@actions/core';
|
||||
|
||||
#### Inputs/Outputs
|
||||
|
||||
Action inputs can be read with `getInput`. Outputs can be set with `setOutput` which makes them available to be mapped into inputs of other actions to ensure they are decoupled.
|
||||
Action inputs can be read with `getInput` which returns a `string` or `getBooleanInput` which parses a boolean based on the [yaml 1.2 specification](https://yaml.org/spec/1.2/spec.html#id2804923). If `required` set to be false, the input should have a default value in `action.yml`.
|
||||
|
||||
Outputs can be set with `setOutput` which makes them available to be mapped into inputs of other actions to ensure they are decoupled.
|
||||
|
||||
```js
|
||||
const myInput = core.getInput('inputName', { required: true });
|
||||
|
||||
const myBooleanInput = core.getBooleanInput('booleanInputName', { required: true });
|
||||
core.setOutput('outputKey', 'outputVal');
|
||||
```
|
||||
|
||||
@@ -112,11 +114,70 @@ const result = await core.group('Do something async', async () => {
|
||||
})
|
||||
```
|
||||
|
||||
#### Styling output
|
||||
|
||||
Colored output is supported in the Action logs via standard [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code). 3/4 bit, 8 bit and 24 bit colors are all supported.
|
||||
|
||||
Foreground colors:
|
||||
|
||||
```js
|
||||
// 3/4 bit
|
||||
core.info('\u001b[35mThis foreground will be magenta')
|
||||
|
||||
// 8 bit
|
||||
core.info('\u001b[38;5;6mThis foreground will be cyan')
|
||||
|
||||
// 24 bit
|
||||
core.info('\u001b[38;2;255;0;0mThis foreground will be bright red')
|
||||
```
|
||||
|
||||
Background colors:
|
||||
|
||||
```js
|
||||
// 3/4 bit
|
||||
core.info('\u001b[43mThis background will be yellow');
|
||||
|
||||
// 8 bit
|
||||
core.info('\u001b[48;5;6mThis background will be cyan')
|
||||
|
||||
// 24 bit
|
||||
core.info('\u001b[48;2;255;0;0mThis background will be bright red')
|
||||
```
|
||||
|
||||
Special styles:
|
||||
|
||||
```js
|
||||
core.info('\u001b[1mBold text')
|
||||
core.info('\u001b[3mItalic text')
|
||||
core.info('\u001b[4mUnderlined text')
|
||||
```
|
||||
|
||||
ANSI escape codes can be combined with one another:
|
||||
|
||||
```js
|
||||
core.info('\u001b[31;46mRed foreground with a cyan background and \u001b[1mbold text at the end');
|
||||
```
|
||||
|
||||
> Note: Escape codes reset at the start of each line
|
||||
|
||||
```js
|
||||
core.info('\u001b[35mThis foreground will be magenta')
|
||||
core.info('This foreground will reset to the default')
|
||||
```
|
||||
|
||||
Manually typing escape codes can be a little difficult, but you can use third party modules such as [ansi-styles](https://github.com/chalk/ansi-styles).
|
||||
|
||||
```js
|
||||
const style = require('ansi-styles');
|
||||
core.info(style.color.ansi16m.hex('#abcdef') + 'Hello world!')
|
||||
```
|
||||
|
||||
#### Action state
|
||||
|
||||
You can use this library to save state and get state for sharing information between a given wrapper action:
|
||||
You can use this library to save state and get state for sharing information between a given wrapper action:
|
||||
|
||||
**action.yml**:
|
||||
|
||||
**action.yml**
|
||||
```yaml
|
||||
name: 'Wrapper action sample'
|
||||
inputs:
|
||||
@@ -137,6 +198,7 @@ core.saveState("pidToKill", 12345);
|
||||
```
|
||||
|
||||
In action's `cleanup.js`:
|
||||
|
||||
```js
|
||||
const core = require('@actions/core');
|
||||
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# @actions/core Releases
|
||||
|
||||
### 1.2.7
|
||||
- [Prepend newline for set-output](https://github.com/actions/toolkit/pull/772)
|
||||
|
||||
### 1.2.6
|
||||
- [Update `exportVariable` and `addPath` to use environment files](https://github.com/actions/toolkit/pull/571)
|
||||
|
||||
### 1.2.5
|
||||
- [Correctly bundle License File with package](https://github.com/actions/toolkit/pull/548)
|
||||
|
||||
### 1.2.4
|
||||
- [Be more lenient in accepting non-string command inputs](https://github.com/actions/toolkit/pull/405)
|
||||
- [Add Echo commands](https://github.com/actions/toolkit/pull/411)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
import * as core from '../src/core'
|
||||
@@ -18,29 +19,44 @@ const testEnvVars = {
|
||||
INPUT_MISSING: '',
|
||||
'INPUT_SPECIAL_CHARS_\'\t"\\': '\'\t"\\ response ',
|
||||
INPUT_MULTIPLE_SPACES_VARIABLE: 'I have multiple spaces',
|
||||
INPUT_BOOLEAN_INPUT: 'true',
|
||||
INPUT_BOOLEAN_INPUT_TRUE1: 'true',
|
||||
INPUT_BOOLEAN_INPUT_TRUE2: 'True',
|
||||
INPUT_BOOLEAN_INPUT_TRUE3: 'TRUE',
|
||||
INPUT_BOOLEAN_INPUT_FALSE1: 'false',
|
||||
INPUT_BOOLEAN_INPUT_FALSE2: 'False',
|
||||
INPUT_BOOLEAN_INPUT_FALSE3: 'FALSE',
|
||||
INPUT_WRONG_BOOLEAN_INPUT: 'wrong',
|
||||
|
||||
// Save inputs
|
||||
STATE_TEST_1: 'state_val'
|
||||
STATE_TEST_1: 'state_val',
|
||||
|
||||
// File Commands
|
||||
GITHUB_PATH: '',
|
||||
GITHUB_ENV: ''
|
||||
}
|
||||
|
||||
describe('@actions/core', () => {
|
||||
beforeEach(() => {
|
||||
for (const key in testEnvVars)
|
||||
process.env[key] = testEnvVars[key as keyof typeof testEnvVars]
|
||||
beforeAll(() => {
|
||||
const filePath = path.join(__dirname, `test`)
|
||||
if (!fs.existsSync(filePath)) {
|
||||
fs.mkdirSync(filePath)
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
for (const key in testEnvVars) {
|
||||
process.env[key] = testEnvVars[key as keyof typeof testEnvVars]
|
||||
}
|
||||
process.stdout.write = jest.fn()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
for (const key in testEnvVars) Reflect.deleteProperty(testEnvVars, key)
|
||||
})
|
||||
|
||||
it('exportVariable produces the correct command and sets the env', () => {
|
||||
it('legacy exportVariable produces the correct command and sets the env', () => {
|
||||
core.exportVariable('my var', 'var val')
|
||||
assertWriteCalls([`::set-env name=my var::var val${os.EOL}`])
|
||||
})
|
||||
|
||||
it('exportVariable escapes variable names', () => {
|
||||
it('legacy exportVariable escapes variable names', () => {
|
||||
core.exportVariable('special char var \r\n,:', 'special val')
|
||||
expect(process.env['special char var \r\n,:']).toBe('special val')
|
||||
assertWriteCalls([
|
||||
@@ -48,28 +64,68 @@ describe('@actions/core', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('exportVariable escapes variable values', () => {
|
||||
it('legacy exportVariable escapes variable values', () => {
|
||||
core.exportVariable('my var2', 'var val\r\n')
|
||||
expect(process.env['my var2']).toBe('var val\r\n')
|
||||
assertWriteCalls([`::set-env name=my var2::var val%0D%0A${os.EOL}`])
|
||||
})
|
||||
|
||||
it('exportVariable handles boolean inputs', () => {
|
||||
it('legacy exportVariable handles boolean inputs', () => {
|
||||
core.exportVariable('my var', true)
|
||||
assertWriteCalls([`::set-env name=my var::true${os.EOL}`])
|
||||
})
|
||||
|
||||
it('exportVariable handles number inputs', () => {
|
||||
it('legacy exportVariable handles number inputs', () => {
|
||||
core.exportVariable('my var', 5)
|
||||
assertWriteCalls([`::set-env name=my var::5${os.EOL}`])
|
||||
})
|
||||
|
||||
it('exportVariable produces the correct command and sets the env', () => {
|
||||
const command = 'ENV'
|
||||
createFileCommandFile(command)
|
||||
core.exportVariable('my var', 'var val')
|
||||
verifyFileCommand(
|
||||
command,
|
||||
`my var<<_GitHubActionsFileCommandDelimeter_${os.EOL}var val${os.EOL}_GitHubActionsFileCommandDelimeter_${os.EOL}`
|
||||
)
|
||||
})
|
||||
|
||||
it('exportVariable handles boolean inputs', () => {
|
||||
const command = 'ENV'
|
||||
createFileCommandFile(command)
|
||||
core.exportVariable('my var', true)
|
||||
verifyFileCommand(
|
||||
command,
|
||||
`my var<<_GitHubActionsFileCommandDelimeter_${os.EOL}true${os.EOL}_GitHubActionsFileCommandDelimeter_${os.EOL}`
|
||||
)
|
||||
})
|
||||
|
||||
it('exportVariable handles number inputs', () => {
|
||||
const command = 'ENV'
|
||||
createFileCommandFile(command)
|
||||
core.exportVariable('my var', 5)
|
||||
verifyFileCommand(
|
||||
command,
|
||||
`my var<<_GitHubActionsFileCommandDelimeter_${os.EOL}5${os.EOL}_GitHubActionsFileCommandDelimeter_${os.EOL}`
|
||||
)
|
||||
})
|
||||
|
||||
it('setSecret produces the correct command', () => {
|
||||
core.setSecret('secret val')
|
||||
assertWriteCalls([`::add-mask::secret val${os.EOL}`])
|
||||
})
|
||||
|
||||
it('prependPath produces the correct commands and sets the env', () => {
|
||||
const command = 'PATH'
|
||||
createFileCommandFile(command)
|
||||
core.addPath('myPath')
|
||||
expect(process.env['PATH']).toBe(
|
||||
`myPath${path.delimiter}path1${path.delimiter}path2`
|
||||
)
|
||||
verifyFileCommand(command, `myPath${os.EOL}`)
|
||||
})
|
||||
|
||||
it('legacy prependPath produces the correct commands and sets the env', () => {
|
||||
core.addPath('myPath')
|
||||
expect(process.env['PATH']).toBe(
|
||||
`myPath${path.delimiter}path1${path.delimiter}path2`
|
||||
@@ -109,19 +165,46 @@ describe('@actions/core', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('getInput gets non-required boolean input', () => {
|
||||
expect(core.getBooleanInput('boolean input')).toBe(true)
|
||||
})
|
||||
|
||||
it('getInput gets required input', () => {
|
||||
expect(core.getBooleanInput('boolean input', {required: true})).toBe(true)
|
||||
})
|
||||
|
||||
it('getBooleanInput handles boolean input', () => {
|
||||
expect(core.getBooleanInput('boolean input true1')).toBe(true)
|
||||
expect(core.getBooleanInput('boolean input true2')).toBe(true)
|
||||
expect(core.getBooleanInput('boolean input true3')).toBe(true)
|
||||
expect(core.getBooleanInput('boolean input false1')).toBe(false)
|
||||
expect(core.getBooleanInput('boolean input false2')).toBe(false)
|
||||
expect(core.getBooleanInput('boolean input false3')).toBe(false)
|
||||
})
|
||||
|
||||
it('getBooleanInput handles wrong boolean input', () => {
|
||||
expect(() => core.getBooleanInput('wrong boolean input')).toThrow(
|
||||
'Input does not meet YAML 1.2 "Core Schema" specification: wrong boolean input\n' +
|
||||
`Support boolean input list: \`true | True | TRUE | false | False | FALSE\``
|
||||
)
|
||||
})
|
||||
|
||||
it('setOutput produces the correct command', () => {
|
||||
core.setOutput('some output', 'some value')
|
||||
assertWriteCalls([`::set-output name=some output::some value${os.EOL}`])
|
||||
assertWriteCalls([
|
||||
os.EOL,
|
||||
`::set-output name=some output::some value${os.EOL}`
|
||||
])
|
||||
})
|
||||
|
||||
it('setOutput handles bools', () => {
|
||||
core.setOutput('some output', false)
|
||||
assertWriteCalls([`::set-output name=some output::false${os.EOL}`])
|
||||
assertWriteCalls([os.EOL, `::set-output name=some output::false${os.EOL}`])
|
||||
})
|
||||
|
||||
it('setOutput handles numbers', () => {
|
||||
core.setOutput('some output', 1.01)
|
||||
assertWriteCalls([`::set-output name=some output::1.01${os.EOL}`])
|
||||
assertWriteCalls([os.EOL, `::set-output name=some output::1.01${os.EOL}`])
|
||||
})
|
||||
|
||||
it('setFailed sets the correct exit code and failure message', () => {
|
||||
@@ -259,3 +342,21 @@ function assertWriteCalls(calls: string[]): void {
|
||||
expect(process.stdout.write).toHaveBeenNthCalledWith(i + 1, calls[i])
|
||||
}
|
||||
}
|
||||
|
||||
function createFileCommandFile(command: string): void {
|
||||
const filePath = path.join(__dirname, `test/${command}`)
|
||||
process.env[`GITHUB_${command}`] = filePath
|
||||
fs.appendFileSync(filePath, '', {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
}
|
||||
|
||||
function verifyFileCommand(command: string, expectedContents: string): void {
|
||||
const filePath = path.join(__dirname, `test/${command}`)
|
||||
const contents = fs.readFileSync(filePath, 'utf8')
|
||||
try {
|
||||
expect(contents).toEqual(expectedContents)
|
||||
} finally {
|
||||
fs.unlinkSync(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/core",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.7",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@actions/core",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.7",
|
||||
"description": "Actions core lib",
|
||||
"keywords": [
|
||||
"github",
|
||||
"actions",
|
||||
"core"
|
||||
],
|
||||
"homepage": "https://github.com/actions/toolkit/tree/master/packages/core",
|
||||
"homepage": "https://github.com/actions/toolkit/tree/main/packages/core",
|
||||
"license": "MIT",
|
||||
"main": "lib/core.js",
|
||||
"types": "lib/core.d.ts",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as os from 'os'
|
||||
import {toCommandValue} from './utils'
|
||||
|
||||
// For internal use, subject to change.
|
||||
|
||||
@@ -76,19 +77,6 @@ class Command {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes an input into a string so it can be passed into issueCommand safely
|
||||
* @param input input to sanitize into a string
|
||||
*/
|
||||
export function toCommandValue(input: any): string {
|
||||
if (input === null || input === undefined) {
|
||||
return ''
|
||||
} else if (typeof input === 'string' || input instanceof String) {
|
||||
return input as string
|
||||
}
|
||||
return JSON.stringify(input)
|
||||
}
|
||||
|
||||
function escapeData(s: any): string {
|
||||
return toCommandValue(s)
|
||||
.replace(/%/g, '%25')
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import {issue, issueCommand, toCommandValue} from './command'
|
||||
import {issue, issueCommand} from './command'
|
||||
import {issueCommand as issueFileCommand} from './file-command'
|
||||
import {toCommandValue} from './utils'
|
||||
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
@@ -39,7 +41,15 @@ export enum ExitCode {
|
||||
export function exportVariable(name: string, val: any): void {
|
||||
const convertedVal = toCommandValue(val)
|
||||
process.env[name] = convertedVal
|
||||
issueCommand('set-env', {name}, convertedVal)
|
||||
|
||||
const filePath = process.env['GITHUB_ENV'] || ''
|
||||
if (filePath) {
|
||||
const delimiter = '_GitHubActionsFileCommandDelimeter_'
|
||||
const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`
|
||||
issueFileCommand('ENV', commandValue)
|
||||
} else {
|
||||
issueCommand('set-env', {name}, convertedVal)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,7 +65,12 @@ export function setSecret(secret: string): void {
|
||||
* @param inputPath
|
||||
*/
|
||||
export function addPath(inputPath: string): void {
|
||||
issueCommand('add-path', {}, inputPath)
|
||||
const filePath = process.env['GITHUB_PATH'] || ''
|
||||
if (filePath) {
|
||||
issueFileCommand('PATH', inputPath)
|
||||
} else {
|
||||
issueCommand('add-path', {}, inputPath)
|
||||
}
|
||||
process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`
|
||||
}
|
||||
|
||||
@@ -76,6 +91,28 @@ export function getInput(name: string, options?: InputOptions): string {
|
||||
return val.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the input value of the boolean type in the YAML 1.2 "core schema" specification.
|
||||
* Support boolean input list: `true | True | TRUE | false | False | FALSE` .
|
||||
* The return value is also in boolean type.
|
||||
* ref: https://yaml.org/spec/1.2/spec.html#id2804923
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
* @returns boolean
|
||||
*/
|
||||
export function getBooleanInput(name: string, options?: InputOptions): boolean {
|
||||
const trueValue = ['true', 'True', 'TRUE']
|
||||
const falseValue = ['false', 'False', 'FALSE']
|
||||
const val = getInput(name, options)
|
||||
if (trueValue.includes(val)) return true
|
||||
if (falseValue.includes(val)) return false
|
||||
throw new TypeError(
|
||||
`Input does not meet YAML 1.2 "Core Schema" specification: ${name}\n` +
|
||||
`Support boolean input list: \`true | True | TRUE | false | False | FALSE\``
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of an output.
|
||||
*
|
||||
@@ -84,6 +121,7 @@ export function getInput(name: string, options?: InputOptions): string {
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function setOutput(name: string, value: any): void {
|
||||
process.stdout.write(os.EOL)
|
||||
issueCommand('set-output', {name}, value)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// For internal use, subject to change.
|
||||
|
||||
// We use any as a valid input type
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import {toCommandValue} from './utils'
|
||||
|
||||
export function issueCommand(command: string, message: any): void {
|
||||
const filePath = process.env[`GITHUB_${command}`]
|
||||
if (!filePath) {
|
||||
throw new Error(
|
||||
`Unable to find environment variable for file command ${command}`
|
||||
)
|
||||
}
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`Missing file at path: ${filePath}`)
|
||||
}
|
||||
|
||||
fs.appendFileSync(filePath, `${toCommandValue(message)}${os.EOL}`, {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// We use any as a valid input type
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/**
|
||||
* Sanitizes an input into a string so it can be passed into issueCommand safely
|
||||
* @param input input to sanitize into a string
|
||||
*/
|
||||
export function toCommandValue(input: any): string {
|
||||
if (input === null || input === undefined) {
|
||||
return ''
|
||||
} else if (typeof input === 'string' || input instanceof String) {
|
||||
return input as string
|
||||
}
|
||||
return JSON.stringify(input)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2019 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as exec from '../src/exec'
|
||||
import * as tr from '../src/toolrunner'
|
||||
import * as im from '../src/interfaces'
|
||||
|
||||
import * as childProcess from 'child_process'
|
||||
@@ -620,6 +621,14 @@ describe('@actions/exec', () => {
|
||||
expect(output.trim()).toBe(`args[0]: "hello"${os.EOL}args[1]: "world"`)
|
||||
})
|
||||
|
||||
it('tool runner strips INPUT_ params from environment for child process', () => {
|
||||
const env = {INPUT_TEST: 'input value', SOME_OTHER_ENV: 'some other value'}
|
||||
const sanitizedEnv = tr.stripInputEnvironmentVariables(env)
|
||||
|
||||
expect(sanitizedEnv).not.toHaveProperty('INPUT_TEST')
|
||||
expect(sanitizedEnv).toHaveProperty('SOME_OTHER_ENV')
|
||||
})
|
||||
|
||||
if (IS_WINDOWS) {
|
||||
it('Exec roots relative tool path using process.cwd (Windows path separator)', async () => {
|
||||
let exitCode: number
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"actions",
|
||||
"exec"
|
||||
],
|
||||
"homepage": "https://github.com/actions/toolkit/tree/master/packages/exec",
|
||||
"homepage": "https://github.com/actions/toolkit/tree/main/packages/exec",
|
||||
"license": "MIT",
|
||||
"main": "lib/exec.js",
|
||||
"types": "lib/exec.d.ts",
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface ExecOptions {
|
||||
/** optional working directory. defaults to current */
|
||||
cwd?: string
|
||||
|
||||
/** optional envvar dictionary. defaults to current process's env */
|
||||
/** optional envvar dictionary. defaults to current process's env with `INPUT_*` variables removed */
|
||||
env?: {[key: string]: string}
|
||||
|
||||
/** optional. defaults to false */
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as stream from 'stream'
|
||||
import * as im from './interfaces'
|
||||
import * as io from '@actions/io'
|
||||
import * as ioUtil from '@actions/io/lib/io-util'
|
||||
import {setTimeout} from 'timers'
|
||||
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
|
||||
@@ -376,7 +377,7 @@ export class ToolRunner extends events.EventEmitter {
|
||||
options = options || <im.ExecOptions>{}
|
||||
const result = <child.SpawnOptions>{}
|
||||
result.cwd = options.cwd
|
||||
result.env = options.env
|
||||
result.env = options.env || stripInputEnvironmentVariables(process.env)
|
||||
result['windowsVerbatimArguments'] =
|
||||
options.windowsVerbatimArguments || this._isCmdFile()
|
||||
if (options.windowsVerbatimArguments) {
|
||||
@@ -599,6 +600,16 @@ export function argStringToArray(argString: string): string[] {
|
||||
return args
|
||||
}
|
||||
|
||||
// Strips INPUT_ environment variables to prevent them leaking to child processes
|
||||
export function stripInputEnvironmentVariables(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||
return Object.entries(env).filter(([key, value]) => {
|
||||
return !key.startsWith('INPUT_')
|
||||
}).reduce((obj: NodeJS.ProcessEnv, [key, value]) => {
|
||||
obj[key] = value
|
||||
return obj
|
||||
}, {})
|
||||
}
|
||||
|
||||
class ExecState extends events.EventEmitter {
|
||||
constructor(options: im.ExecOptions, toolPath: string) {
|
||||
super()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2019 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -22,7 +22,7 @@ async function run() {
|
||||
// You can also pass in additional options as a second parameter to getOctokit
|
||||
// const octokit = github.getOctokit(myToken, {userAgent: "MyActionVersion1"});
|
||||
|
||||
const { data: pullRequest } = await octokit.pulls.get({
|
||||
const { data: pullRequest } = await octokit.rest.pulls.get({
|
||||
owner: 'octokit',
|
||||
repo: 'rest.js',
|
||||
pull_number: 123,
|
||||
@@ -50,7 +50,7 @@ const github = require('@actions/github');
|
||||
|
||||
const context = github.context;
|
||||
|
||||
const newIssue = await octokit.issues.create({
|
||||
const newIssue = await octokit.rest.issues.create({
|
||||
...context.repo,
|
||||
title: 'New issue!',
|
||||
body: 'Hello Universe!'
|
||||
@@ -90,7 +90,7 @@ const octokit = GitHub.plugin(enterpriseServer220Admin)
|
||||
const myToken = core.getInput('myToken');
|
||||
const myOctokit = new octokit(getOctokitOptions(token))
|
||||
// Create a new user
|
||||
myOctokit.enterpriseAdmin.createUser({
|
||||
myOctokit.rest.enterpriseAdmin.createUser({
|
||||
login: "testuser",
|
||||
email: "testuser@test.com",
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# @actions/github Releases
|
||||
|
||||
### 5.0.0
|
||||
- [Update @actions/github to include latest octokit definitions](https://github.com/actions/toolkit/pull/783)
|
||||
|
||||
### 4.0.0
|
||||
- [Add execution state information to context](https://github.com/actions/toolkit/pull/499)
|
||||
- [Update Octokit Dependencies with some api breaking changes](https://github.com/actions/toolkit/pull/498)
|
||||
|
||||
@@ -49,12 +49,12 @@ describe('@actions/github', () => {
|
||||
}
|
||||
|
||||
const octokit = getOctokit(token)
|
||||
const branch = await octokit.repos.getBranch({
|
||||
const branch = await octokit.rest.repos.getBranch({
|
||||
owner: 'actions',
|
||||
repo: 'toolkit',
|
||||
branch: 'master'
|
||||
branch: 'main'
|
||||
})
|
||||
expect(branch.data.name).toBe('master')
|
||||
expect(branch.data.name).toBe('main')
|
||||
expect(proxyConnects).toEqual(['api.github.com:443'])
|
||||
})
|
||||
|
||||
@@ -85,12 +85,12 @@ describe('@actions/github', () => {
|
||||
agent: new https.Agent()
|
||||
}
|
||||
})
|
||||
const branch = await octokit.repos.getBranch({
|
||||
const branch = await octokit.rest.repos.getBranch({
|
||||
owner: 'actions',
|
||||
repo: 'toolkit',
|
||||
branch: 'master'
|
||||
branch: 'main'
|
||||
})
|
||||
expect(branch.data.name).toBe('master')
|
||||
expect(branch.data.name).toBe('main')
|
||||
expect(proxyConnects).toHaveLength(0)
|
||||
})
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('@actions/github', () => {
|
||||
proxyServer = proxy()
|
||||
await new Promise(resolve => {
|
||||
const port = Number(proxyUrl.split(':')[2])
|
||||
proxyServer.listen(port, () => resolve())
|
||||
proxyServer.listen(port, () => resolve(null))
|
||||
})
|
||||
proxyServer.on('connect', req => {
|
||||
proxyConnects.push(req.url)
|
||||
@@ -30,7 +30,7 @@ describe('@actions/github', () => {
|
||||
afterAll(async () => {
|
||||
// Stop proxy server
|
||||
await new Promise(resolve => {
|
||||
proxyServer.once('close', () => resolve())
|
||||
proxyServer.once('close', () => resolve(null))
|
||||
proxyServer.close()
|
||||
})
|
||||
|
||||
@@ -45,12 +45,12 @@ describe('@actions/github', () => {
|
||||
return
|
||||
}
|
||||
const octokit = new GitHub(getOctokitOptions(token))
|
||||
const branch = await octokit.repos.getBranch({
|
||||
const branch = await octokit.rest.repos.getBranch({
|
||||
owner: 'actions',
|
||||
repo: 'toolkit',
|
||||
branch: 'master'
|
||||
branch: 'main'
|
||||
})
|
||||
expect(branch.data.name).toBe('master')
|
||||
expect(branch.data.name).toBe('main')
|
||||
expect(proxyConnects).toHaveLength(0)
|
||||
})
|
||||
|
||||
@@ -60,12 +60,12 @@ describe('@actions/github', () => {
|
||||
return
|
||||
}
|
||||
const octokit = getOctokit(token)
|
||||
const branch = await octokit.repos.getBranch({
|
||||
const branch = await octokit.rest.repos.getBranch({
|
||||
owner: 'actions',
|
||||
repo: 'toolkit',
|
||||
branch: 'master'
|
||||
branch: 'main'
|
||||
})
|
||||
expect(branch.data.name).toBe('master')
|
||||
expect(branch.data.name).toBe('main')
|
||||
expect(proxyConnects).toHaveLength(0)
|
||||
})
|
||||
|
||||
@@ -77,22 +77,22 @@ describe('@actions/github', () => {
|
||||
|
||||
// Valid token
|
||||
let octokit = new GitHub({auth: `token ${token}`})
|
||||
const branch = await octokit.repos.getBranch({
|
||||
const branch = await octokit.rest.repos.getBranch({
|
||||
owner: 'actions',
|
||||
repo: 'toolkit',
|
||||
branch: 'master'
|
||||
branch: 'main'
|
||||
})
|
||||
expect(branch.data.name).toBe('master')
|
||||
expect(branch.data.name).toBe('main')
|
||||
expect(proxyConnects).toHaveLength(0)
|
||||
|
||||
// Invalid token
|
||||
octokit = new GitHub({auth: `token asdf`})
|
||||
let failed = false
|
||||
try {
|
||||
await octokit.repos.getBranch({
|
||||
await octokit.rest.repos.getBranch({
|
||||
owner: 'actions',
|
||||
repo: 'toolkit',
|
||||
branch: 'master'
|
||||
branch: 'main'
|
||||
})
|
||||
} catch (err) {
|
||||
failed = true
|
||||
|
||||
Generated
+68
-79
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@actions/github",
|
||||
"version": "4.0.0",
|
||||
"version": "5.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@actions/http-client": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.8.tgz",
|
||||
"integrity": "sha512-G4JjJ6f9Hb3Zvejj+ewLLKLf99ZC+9v+yCxoYf9vSyH+WkzPLB2LuUtRMGNkooMqdugGBFStIKXOuvH1W+EctA==",
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
|
||||
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
|
||||
"requires": {
|
||||
"tunnel": "0.0.6"
|
||||
}
|
||||
@@ -457,104 +457,98 @@
|
||||
}
|
||||
},
|
||||
"@octokit/auth-token": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.2.tgz",
|
||||
"integrity": "sha512-jE/lE/IKIz2v1+/P0u4fJqv0kYwXOTujKemJMFr6FeopsxlIK3+wKDCJGnysg81XID5TgZQbIfuJ5J0lnTiuyQ==",
|
||||
"version": "2.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.5.tgz",
|
||||
"integrity": "sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA==",
|
||||
"requires": {
|
||||
"@octokit/types": "^5.0.0"
|
||||
"@octokit/types": "^6.0.3"
|
||||
}
|
||||
},
|
||||
"@octokit/core": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.1.1.tgz",
|
||||
"integrity": "sha512-cQ2HGrtyNJ1IBxpTP1U5m/FkMAJvgw7d2j1q3c9P0XUuYilEgF6e4naTpsgm4iVcQeOnccZlw7XHRIUBy0ymcg==",
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.4.0.tgz",
|
||||
"integrity": "sha512-6/vlKPP8NF17cgYXqucdshWqmMZGXkuvtcrWCgU5NOI0Pl2GjlmZyWgBMrU8zJ3v2MJlM6++CiB45VKYmhiWWg==",
|
||||
"requires": {
|
||||
"@octokit/auth-token": "^2.4.0",
|
||||
"@octokit/graphql": "^4.3.1",
|
||||
"@octokit/request": "^5.4.0",
|
||||
"@octokit/types": "^5.0.0",
|
||||
"before-after-hook": "^2.1.0",
|
||||
"@octokit/auth-token": "^2.4.4",
|
||||
"@octokit/graphql": "^4.5.8",
|
||||
"@octokit/request": "^5.4.12",
|
||||
"@octokit/request-error": "^2.0.5",
|
||||
"@octokit/types": "^6.0.3",
|
||||
"before-after-hook": "^2.2.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"@octokit/endpoint": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.4.tgz",
|
||||
"integrity": "sha512-ZJHIsvsClEE+6LaZXskDvWIqD3Ao7+2gc66pRG5Ov4MQtMvCU9wGu1TItw9aGNmRuU9x3Fei1yb+uqGaQnm0nw==",
|
||||
"version": "6.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.11.tgz",
|
||||
"integrity": "sha512-fUIPpx+pZyoLW4GCs3yMnlj2LfoXTWDUVPTC4V3MUEKZm48W+XYpeWSZCv+vYF1ZABUm2CqnDVf1sFtIYrj7KQ==",
|
||||
"requires": {
|
||||
"@octokit/types": "^5.0.0",
|
||||
"is-plain-object": "^3.0.0",
|
||||
"@octokit/types": "^6.0.3",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"@octokit/graphql": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.5.2.tgz",
|
||||
"integrity": "sha512-SpB/JGdB7bxRj8qowwfAXjMpICUYSJqRDj26MKJAryRQBqp/ZzARsaO2LEFWzDaps0FLQoPYVGppS0HQXkBhdg==",
|
||||
"version": "4.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.6.1.tgz",
|
||||
"integrity": "sha512-2lYlvf4YTDgZCTXTW4+OX+9WTLFtEUc6hGm4qM1nlZjzxj+arizM4aHWzBVBCxY9glh7GIs0WEuiSgbVzv8cmA==",
|
||||
"requires": {
|
||||
"@octokit/request": "^5.3.0",
|
||||
"@octokit/types": "^5.0.0",
|
||||
"@octokit/types": "^6.0.3",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"@octokit/openapi-types": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-7.0.0.tgz",
|
||||
"integrity": "sha512-gV/8DJhAL/04zjTI95a7FhQwS6jlEE0W/7xeYAzuArD0KVAVWDLP2f3vi98hs3HLTczxXdRK/mF0tRoQPpolEw=="
|
||||
},
|
||||
"@octokit/plugin-paginate-rest": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.2.3.tgz",
|
||||
"integrity": "sha512-eKTs91wXnJH8Yicwa30jz6DF50kAh7vkcqCQ9D7/tvBAP5KKkg6I2nNof8Mp/65G0Arjsb4QcOJcIEQY+rK1Rg==",
|
||||
"version": "2.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.13.3.tgz",
|
||||
"integrity": "sha512-46lptzM9lTeSmIBt/sVP/FLSTPGx6DCzAdSX3PfeJ3mTf4h9sGC26WpaQzMEq/Z44cOcmx8VsOhO+uEgE3cjYg==",
|
||||
"requires": {
|
||||
"@octokit/types": "^5.0.0"
|
||||
"@octokit/types": "^6.11.0"
|
||||
}
|
||||
},
|
||||
"@octokit/plugin-rest-endpoint-methods": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.1.0.tgz",
|
||||
"integrity": "sha512-zbRTjm+xplSNlixotTVMvLJe8aRogUXS+r37wZK5EjLsNYH4j02K5XLMOWyYaSS4AJEZtPmzCcOcui4VzVGq+A==",
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.1.1.tgz",
|
||||
"integrity": "sha512-u4zy0rVA8darm/AYsIeWkRalhQR99qPL1D/EXHejV2yaECMdHfxXiTXtba8NMBSajOJe8+C9g+EqMKSvysx0dg==",
|
||||
"requires": {
|
||||
"@octokit/types": "^5.1.0",
|
||||
"@octokit/types": "^6.14.1",
|
||||
"deprecation": "^2.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/types": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-5.1.0.tgz",
|
||||
"integrity": "sha512-OFxUBgrEllAbdEmWp/wNmKIu5EuumKHG4sgy56vjZ8lXPgMhF05c76hmulfOdFHHYRpPj49ygOZJ8wgVsPecuA==",
|
||||
"requires": {
|
||||
"@types/node": ">= 8"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@octokit/request": {
|
||||
"version": "5.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.6.tgz",
|
||||
"integrity": "sha512-9r8Sn4CvqFI9LDLHl9P17EZHwj3ehwQnTpTE+LEneb0VBBqSiI/VS4rWIBfBhDrDs/aIGEGZRSB0QWAck8u+2g==",
|
||||
"version": "5.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.15.tgz",
|
||||
"integrity": "sha512-6UnZfZzLwNhdLRreOtTkT9n57ZwulCve8q3IT/Z477vThu6snfdkBuhxnChpOKNGxcQ71ow561Qoa6uqLdPtag==",
|
||||
"requires": {
|
||||
"@octokit/endpoint": "^6.0.1",
|
||||
"@octokit/request-error": "^2.0.0",
|
||||
"@octokit/types": "^5.0.0",
|
||||
"deprecation": "^2.0.0",
|
||||
"is-plain-object": "^3.0.0",
|
||||
"node-fetch": "^2.3.0",
|
||||
"once": "^1.4.0",
|
||||
"@octokit/types": "^6.7.1",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"@octokit/request-error": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.2.tgz",
|
||||
"integrity": "sha512-2BrmnvVSV1MXQvEkrb9zwzP0wXFNbPJij922kYBTLIlIafukrGOb+ABBT2+c6wZiuyWDH1K1zmjGQ0toN/wMWw==",
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.5.tgz",
|
||||
"integrity": "sha512-T/2wcCFyM7SkXzNoyVNWjyVlUwBvW3igM3Btr/eKYiPmucXTtkxt2RBsf6gn3LTzaLSLTQtNmvg+dGsOxQrjZg==",
|
||||
"requires": {
|
||||
"@octokit/types": "^5.0.1",
|
||||
"@octokit/types": "^6.0.3",
|
||||
"deprecation": "^2.0.0",
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"@octokit/types": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-5.0.1.tgz",
|
||||
"integrity": "sha512-GorvORVwp244fGKEt3cgt/P+M0MGy4xEDbckw+K5ojEezxyMDgCaYPKVct+/eWQfZXOT7uq0xRpmrl/+hliabA==",
|
||||
"version": "6.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.14.2.tgz",
|
||||
"integrity": "sha512-wiQtW9ZSy4OvgQ09iQOdyXYNN60GqjCL/UdMsepDr1Gr0QzpW6irIKbH3REuAHXAhxkEk9/F2a3Gcs1P6kW5jA==",
|
||||
"requires": {
|
||||
"@types/node": ">= 8"
|
||||
"@octokit/openapi-types": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"@sinonjs/commons": {
|
||||
@@ -638,11 +632,6 @@
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "14.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.13.tgz",
|
||||
"integrity": "sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA=="
|
||||
},
|
||||
"@types/stack-utils": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
|
||||
@@ -1029,9 +1018,9 @@
|
||||
}
|
||||
},
|
||||
"before-after-hook": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz",
|
||||
"integrity": "sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A=="
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.1.tgz",
|
||||
"integrity": "sha512-/6FKxSTWoJdbsLDF8tdIjaRiFXiE6UHsEHE3OPI/cwPURCVi1ukP0gmLn7XWEiFk5TcwQjjY5PWsU+j+tgXgmw=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
@@ -2214,9 +2203,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"is-plain-object": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz",
|
||||
"integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g=="
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
|
||||
},
|
||||
"is-regex": {
|
||||
"version": "1.0.5",
|
||||
@@ -3183,9 +3172,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
|
||||
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
|
||||
},
|
||||
"node-int64": {
|
||||
"version": "0.4.0",
|
||||
@@ -4792,9 +4781,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
|
||||
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
|
||||
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
|
||||
"dev": true
|
||||
},
|
||||
"yargs": {
|
||||
@@ -4817,9 +4806,9 @@
|
||||
}
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "18.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.0.tgz",
|
||||
"integrity": "sha512-o/Jr6JBOv6Yx3pL+5naWSoIA2jJ+ZkMYQG/ie9qFbukBe4uzmBatlXFOiu/tNKRWEtyf+n5w7jc/O16ufqOTdQ==",
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"camelcase": "^5.0.0",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@actions/github",
|
||||
"version": "4.0.0",
|
||||
"version": "5.0.0",
|
||||
"description": "Actions github lib",
|
||||
"keywords": [
|
||||
"github",
|
||||
"actions"
|
||||
],
|
||||
"homepage": "https://github.com/actions/toolkit/tree/master/packages/github",
|
||||
"homepage": "https://github.com/actions/toolkit/tree/main/packages/github",
|
||||
"license": "MIT",
|
||||
"main": "lib/github.js",
|
||||
"types": "lib/github.d.ts",
|
||||
@@ -38,10 +38,10 @@
|
||||
"url": "https://github.com/actions/toolkit/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^1.0.8",
|
||||
"@octokit/core": "^3.1.1",
|
||||
"@octokit/plugin-paginate-rest": "^2.2.3",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^4.1.0"
|
||||
"@actions/http-client": "^1.0.11",
|
||||
"@octokit/core": "^3.4.0",
|
||||
"@octokit/plugin-paginate-rest": "^2.13.3",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^5.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^25.1.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Originally pulled from https://github.com/JasonEtco/actions-toolkit/blob/master/src/context.ts
|
||||
// Originally pulled from https://github.com/JasonEtco/actions-toolkit/blob/main/src/context.ts
|
||||
import {WebhookPayload} from './interfaces'
|
||||
import {readFileSync, existsSync} from 'fs'
|
||||
import {EOL} from 'os'
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2019 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -3,3 +3,7 @@
|
||||
### 0.1.0
|
||||
|
||||
- Initial release
|
||||
|
||||
### 0.1.1
|
||||
|
||||
- Update @actions/core version
|
||||
Generated
+6
-1
@@ -1,9 +1,14 @@
|
||||
{
|
||||
"name": "@actions/glob",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@actions/core": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
|
||||
"integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/glob",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"preview": true,
|
||||
"description": "Actions glob lib",
|
||||
"keywords": [
|
||||
@@ -8,7 +8,7 @@
|
||||
"actions",
|
||||
"glob"
|
||||
],
|
||||
"homepage": "https://github.com/actions/toolkit/tree/master/packages/glob",
|
||||
"homepage": "https://github.com/actions/toolkit/tree/main/packages/glob",
|
||||
"license": "MIT",
|
||||
"main": "lib/glob.js",
|
||||
"types": "lib/glob.d.ts",
|
||||
@@ -37,7 +37,7 @@
|
||||
"url": "https://github.com/actions/toolkit/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.0",
|
||||
"@actions/core": "^1.2.6",
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2019 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,9 +1,13 @@
|
||||
# @actions/io Releases
|
||||
|
||||
### 1.1.0
|
||||
|
||||
- Add `findInPath` method to locate all matching executables in the system path
|
||||
|
||||
### 1.0.2
|
||||
|
||||
- [Add \"types\" to package.json](https://github.com/actions/toolkit/pull/221)
|
||||
|
||||
### 1.0.0
|
||||
|
||||
- Initial release
|
||||
- Initial release
|
||||
|
||||
@@ -5,6 +5,10 @@ import * as path from 'path'
|
||||
import * as io from '../src/io'
|
||||
|
||||
describe('cp', () => {
|
||||
beforeAll(async () => {
|
||||
await io.rmRF(getTestTemp())
|
||||
})
|
||||
|
||||
it('copies file with no flags', async () => {
|
||||
const root = path.join(getTestTemp(), 'cp_with_no_flags')
|
||||
const sourceFile = path.join(root, 'cp_source')
|
||||
@@ -86,6 +90,29 @@ describe('cp', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('copies directory into existing destination with -r without copying source directory', async () => {
|
||||
const root: string = path.join(
|
||||
getTestTemp(),
|
||||
'cp_with_-r_existing_dest_no_source_dir'
|
||||
)
|
||||
const sourceFolder: string = path.join(root, 'cp_source')
|
||||
const sourceFile: string = path.join(sourceFolder, 'cp_source_file')
|
||||
|
||||
const targetFolder: string = path.join(root, 'cp_target')
|
||||
const targetFile: string = path.join(targetFolder, 'cp_source_file')
|
||||
await io.mkdirP(sourceFolder)
|
||||
await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'})
|
||||
await io.mkdirP(targetFolder)
|
||||
await io.cp(sourceFolder, targetFolder, {
|
||||
recursive: true,
|
||||
copySourceDirectory: false
|
||||
})
|
||||
|
||||
expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe(
|
||||
'test file content'
|
||||
)
|
||||
})
|
||||
|
||||
it('copies directory into non-existing destination with -r', async () => {
|
||||
const root: string = path.join(getTestTemp(), 'cp_with_-r_nonexistent_dest')
|
||||
const sourceFolder: string = path.join(root, 'cp_source')
|
||||
@@ -165,6 +192,10 @@ describe('cp', () => {
|
||||
})
|
||||
|
||||
describe('mv', () => {
|
||||
beforeAll(async () => {
|
||||
await io.rmRF(getTestTemp())
|
||||
})
|
||||
|
||||
it('moves file with no flags', async () => {
|
||||
const root = path.join(getTestTemp(), ' mv_with_no_flags')
|
||||
const sourceFile = path.join(root, ' mv_source')
|
||||
@@ -263,6 +294,10 @@ describe('mv', () => {
|
||||
})
|
||||
|
||||
describe('rmRF', () => {
|
||||
beforeAll(async () => {
|
||||
await io.rmRF(getTestTemp())
|
||||
})
|
||||
|
||||
it('removes single folder with rmRF', async () => {
|
||||
const testPath = path.join(getTestTemp(), 'testFolder')
|
||||
|
||||
@@ -815,6 +850,10 @@ describe('mkdirP', () => {
|
||||
})
|
||||
|
||||
describe('which', () => {
|
||||
beforeAll(async () => {
|
||||
await io.rmRF(getTestTemp())
|
||||
})
|
||||
|
||||
it('which() finds file name', async () => {
|
||||
// create a executable file
|
||||
const testPath = path.join(getTestTemp(), 'which-finds-file-name')
|
||||
@@ -1347,6 +1386,53 @@ describe('which', () => {
|
||||
}
|
||||
})
|
||||
|
||||
describe('findInPath', () => {
|
||||
beforeAll(async () => {
|
||||
await io.rmRF(getTestTemp())
|
||||
})
|
||||
|
||||
it('findInPath() not found', async () => {
|
||||
expect(await io.findInPath('findInPath-test-no-such-file')).toEqual([])
|
||||
})
|
||||
|
||||
it('findInPath() finds file names', async () => {
|
||||
// create executable files
|
||||
let fileName = 'FindInPath-Test-File'
|
||||
if (process.platform === 'win32') {
|
||||
fileName += '.exe'
|
||||
}
|
||||
|
||||
const testPaths = ['1', '2', '3'].map(count =>
|
||||
path.join(getTestTemp(), `findInPath-finds-file-names-${count}`)
|
||||
)
|
||||
for (const testPath of testPaths) {
|
||||
await io.mkdirP(testPath)
|
||||
}
|
||||
|
||||
const filePaths = testPaths.map(testPath => path.join(testPath, fileName))
|
||||
for (const filePath of filePaths) {
|
||||
await fs.writeFile(filePath, '')
|
||||
if (process.platform !== 'win32') {
|
||||
chmod(filePath, '+x')
|
||||
}
|
||||
}
|
||||
|
||||
const originalPath = process.env['PATH']
|
||||
try {
|
||||
// update the PATH
|
||||
for (const testPath of testPaths) {
|
||||
process.env[
|
||||
'PATH'
|
||||
] = `${process.env['PATH']}${path.delimiter}${testPath}`
|
||||
}
|
||||
// exact file names
|
||||
expect(await io.findInPath(fileName)).toEqual(filePaths)
|
||||
} finally {
|
||||
process.env['PATH'] = originalPath
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
async function findsExecutableWithScopedPermissions(
|
||||
chmodOptions: string
|
||||
): Promise<void> {
|
||||
|
||||
Generated
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@actions/io",
|
||||
"version": "1.0.2",
|
||||
"version": "1.1.0",
|
||||
"lockfileVersion": 1
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@actions/io",
|
||||
"version": "1.0.2",
|
||||
"version": "1.1.0",
|
||||
"description": "Actions io lib",
|
||||
"keywords": [
|
||||
"github",
|
||||
"actions",
|
||||
"io"
|
||||
],
|
||||
"homepage": "https://github.com/actions/toolkit/tree/master/packages/io",
|
||||
"homepage": "https://github.com/actions/toolkit/tree/main/packages/io",
|
||||
"license": "MIT",
|
||||
"main": "lib/io.js",
|
||||
"types": "lib/io.d.ts",
|
||||
|
||||
+84
-62
@@ -14,6 +14,8 @@ export interface CopyOptions {
|
||||
recursive?: boolean
|
||||
/** Optional. Whether to overwrite existing files in the destination. Defaults to true */
|
||||
force?: boolean
|
||||
/** Optional. Whether to copy the source directory along with all the files. Only takes effect when recursive=true and copying a directory. Default is true*/
|
||||
copySourceDirectory?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,7 +39,7 @@ export async function cp(
|
||||
dest: string,
|
||||
options: CopyOptions = {}
|
||||
): Promise<void> {
|
||||
const {force, recursive} = readCopyOptions(options)
|
||||
const {force, recursive, copySourceDirectory} = readCopyOptions(options)
|
||||
|
||||
const destStat = (await ioUtil.exists(dest)) ? await ioUtil.stat(dest) : null
|
||||
// Dest is an existing file, but not forcing
|
||||
@@ -47,7 +49,7 @@ export async function cp(
|
||||
|
||||
// If dest is an existing directory, should copy inside.
|
||||
const newDest: string =
|
||||
destStat && destStat.isDirectory()
|
||||
destStat && destStat.isDirectory() && copySourceDirectory
|
||||
? path.join(dest, path.basename(source))
|
||||
: dest
|
||||
|
||||
@@ -194,75 +196,95 @@ export async function which(tool: string, check?: boolean): Promise<string> {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
try {
|
||||
// build the list of extensions to try
|
||||
const extensions: string[] = []
|
||||
if (ioUtil.IS_WINDOWS && process.env.PATHEXT) {
|
||||
for (const extension of process.env.PATHEXT.split(path.delimiter)) {
|
||||
if (extension) {
|
||||
extensions.push(extension)
|
||||
}
|
||||
}
|
||||
}
|
||||
const matches: string[] = await findInPath(tool)
|
||||
|
||||
// if it's rooted, return it if exists. otherwise return empty.
|
||||
if (ioUtil.isRooted(tool)) {
|
||||
const filePath: string = await ioUtil.tryGetExecutablePath(
|
||||
tool,
|
||||
extensions
|
||||
)
|
||||
|
||||
if (filePath) {
|
||||
return filePath
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
// if any path separators, return empty
|
||||
if (tool.includes('/') || (ioUtil.IS_WINDOWS && tool.includes('\\'))) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// build the list of directories
|
||||
//
|
||||
// Note, technically "where" checks the current directory on Windows. From a toolkit perspective,
|
||||
// it feels like we should not do this. Checking the current directory seems like more of a use
|
||||
// case of a shell, and the which() function exposed by the toolkit should strive for consistency
|
||||
// across platforms.
|
||||
const directories: string[] = []
|
||||
|
||||
if (process.env.PATH) {
|
||||
for (const p of process.env.PATH.split(path.delimiter)) {
|
||||
if (p) {
|
||||
directories.push(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return the first match
|
||||
for (const directory of directories) {
|
||||
const filePath = await ioUtil.tryGetExecutablePath(
|
||||
directory + path.sep + tool,
|
||||
extensions
|
||||
)
|
||||
if (filePath) {
|
||||
return filePath
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
} catch (err) {
|
||||
throw new Error(`which failed with message ${err.message}`)
|
||||
if (matches && matches.length > 0) {
|
||||
return matches[0]
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all occurrences of the given tool on the system path.
|
||||
*
|
||||
* @returns Promise<string[]> the paths of the tool
|
||||
*/
|
||||
export async function findInPath(tool: string): Promise<string[]> {
|
||||
if (!tool) {
|
||||
throw new Error("parameter 'tool' is required")
|
||||
}
|
||||
|
||||
// build the list of extensions to try
|
||||
const extensions: string[] = []
|
||||
if (ioUtil.IS_WINDOWS && process.env['PATHEXT']) {
|
||||
for (const extension of process.env['PATHEXT'].split(path.delimiter)) {
|
||||
if (extension) {
|
||||
extensions.push(extension)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if it's rooted, return it if exists. otherwise return empty.
|
||||
if (ioUtil.isRooted(tool)) {
|
||||
const filePath: string = await ioUtil.tryGetExecutablePath(tool, extensions)
|
||||
|
||||
if (filePath) {
|
||||
return [filePath]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
// if any path separators, return empty
|
||||
if (tool.includes(path.sep)) {
|
||||
return []
|
||||
}
|
||||
|
||||
// build the list of directories
|
||||
//
|
||||
// Note, technically "where" checks the current directory on Windows. From a toolkit perspective,
|
||||
// it feels like we should not do this. Checking the current directory seems like more of a use
|
||||
// case of a shell, and the which() function exposed by the toolkit should strive for consistency
|
||||
// across platforms.
|
||||
const directories: string[] = []
|
||||
|
||||
if (process.env.PATH) {
|
||||
for (const p of process.env.PATH.split(path.delimiter)) {
|
||||
if (p) {
|
||||
directories.push(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// find all matches
|
||||
const matches: string[] = []
|
||||
|
||||
for (const directory of directories) {
|
||||
const filePath = await ioUtil.tryGetExecutablePath(
|
||||
path.join(directory, tool),
|
||||
extensions
|
||||
)
|
||||
if (filePath) {
|
||||
matches.push(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
function readCopyOptions(options: CopyOptions): Required<CopyOptions> {
|
||||
const force = options.force == null ? true : options.force
|
||||
const recursive = Boolean(options.recursive)
|
||||
return {force, recursive}
|
||||
const copySourceDirectory =
|
||||
options.copySourceDirectory == null
|
||||
? true
|
||||
: Boolean(options.copySourceDirectory)
|
||||
return {force, recursive, copySourceDirectory}
|
||||
}
|
||||
|
||||
async function cpDirRecursive(
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2019 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,5 +1,8 @@
|
||||
# @actions/tool-cache Releases
|
||||
|
||||
### 1.6.1
|
||||
- [Update @actions/core version](https://github.com/actions/toolkit/pull/636)
|
||||
|
||||
### 1.6.0
|
||||
- [Add extractXar function to extract XAR files](https://github.com/actions/toolkit/pull/207)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import osm = require('os')
|
||||
import cp = require('child_process')
|
||||
//import {coerce} from 'semver'
|
||||
|
||||
// we fetch the manifest file from master of a repo
|
||||
// we fetch the manifest file from main of a repo
|
||||
const owner = 'actions'
|
||||
const repo = 'some-tool'
|
||||
const fakeToken = 'notrealtoken'
|
||||
|
||||
@@ -763,6 +763,61 @@ describe('@actions/tool-cache', function() {
|
||||
expect(err.toString()).toContain('404')
|
||||
}
|
||||
})
|
||||
|
||||
it('supports authorization headers', async function() {
|
||||
nock('http://example.com', {
|
||||
reqheaders: {
|
||||
authorization: 'token abc123'
|
||||
}
|
||||
})
|
||||
.get('/some-file-that-needs-authorization')
|
||||
.reply(200, undefined)
|
||||
|
||||
await tc.downloadTool(
|
||||
'http://example.com/some-file-that-needs-authorization',
|
||||
undefined,
|
||||
'token abc123'
|
||||
)
|
||||
})
|
||||
|
||||
it('supports custom headers', async function() {
|
||||
nock('http://example.com', {
|
||||
reqheaders: {
|
||||
accept: 'application/octet-stream'
|
||||
}
|
||||
})
|
||||
.get('/some-file-that-needs-headers')
|
||||
.reply(200, undefined)
|
||||
|
||||
await tc.downloadTool(
|
||||
'http://example.com/some-file-that-needs-headers',
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
accept: 'application/octet-stream'
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('supports authorization and custom headers', async function() {
|
||||
nock('http://example.com', {
|
||||
reqheaders: {
|
||||
accept: 'application/octet-stream',
|
||||
authorization: 'token abc123'
|
||||
}
|
||||
})
|
||||
.get('/some-file-that-needs-authorization-and-headers')
|
||||
.reply(200, undefined)
|
||||
|
||||
await tc.downloadTool(
|
||||
'http://example.com/some-file-that-needs-authorization-and-headers',
|
||||
undefined,
|
||||
'token abc123',
|
||||
{
|
||||
accept: 'application/octet-stream'
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
Generated
+4
-4
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@actions/tool-cache",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@actions/core": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.3.tgz",
|
||||
"integrity": "sha512-Wp4xnyokakM45Uuj4WLUxdsa8fJjKVl1fDTsPbTEcTcuu0Nb26IPQbOtjmnfaCPGcaoPOOqId8H9NapZ8gii4w=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
|
||||
"integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
|
||||
},
|
||||
"@actions/exec": {
|
||||
"version": "1.0.3",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@actions/tool-cache",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1",
|
||||
"description": "Actions tool-cache lib",
|
||||
"keywords": [
|
||||
"github",
|
||||
"actions",
|
||||
"exec"
|
||||
],
|
||||
"homepage": "https://github.com/actions/toolkit/tree/master/packages/tool-cache",
|
||||
"homepage": "https://github.com/actions/toolkit/tree/main/packages/tool-cache",
|
||||
"license": "MIT",
|
||||
"main": "lib/tool-cache.js",
|
||||
"types": "lib/tool-cache.d.ts",
|
||||
@@ -36,7 +36,7 @@
|
||||
"url": "https://github.com/actions/toolkit/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.3",
|
||||
"@actions/core": "^1.2.6",
|
||||
"@actions/exec": "^1.0.0",
|
||||
"@actions/http-client": "^1.0.8",
|
||||
"@actions/io": "^1.0.1",
|
||||
|
||||
@@ -32,12 +32,14 @@ const userAgent = 'actions/tool-cache'
|
||||
* @param url url of tool to download
|
||||
* @param dest path to download tool
|
||||
* @param auth authorization header
|
||||
* @param headers other headers
|
||||
* @returns path to downloaded tool
|
||||
*/
|
||||
export async function downloadTool(
|
||||
url: string,
|
||||
dest?: string,
|
||||
auth?: string
|
||||
auth?: string,
|
||||
headers?: IHeaders
|
||||
): Promise<string> {
|
||||
dest = dest || path.join(_getTempDirectory(), uuidV4())
|
||||
await io.mkdirP(path.dirname(dest))
|
||||
@@ -56,7 +58,7 @@ export async function downloadTool(
|
||||
const retryHelper = new RetryHelper(maxAttempts, minSeconds, maxSeconds)
|
||||
return await retryHelper.execute(
|
||||
async () => {
|
||||
return await downloadToolAttempt(url, dest || '', auth)
|
||||
return await downloadToolAttempt(url, dest || '', auth, headers)
|
||||
},
|
||||
(err: Error) => {
|
||||
if (err instanceof HTTPError && err.httpStatusCode) {
|
||||
@@ -79,7 +81,8 @@ export async function downloadTool(
|
||||
async function downloadToolAttempt(
|
||||
url: string,
|
||||
dest: string,
|
||||
auth?: string
|
||||
auth?: string,
|
||||
headers?: IHeaders
|
||||
): Promise<string> {
|
||||
if (fs.existsSync(dest)) {
|
||||
throw new Error(`Destination file path ${dest} already exists`)
|
||||
@@ -90,12 +93,12 @@ async function downloadToolAttempt(
|
||||
allowRetries: false
|
||||
})
|
||||
|
||||
let headers: IHeaders | undefined
|
||||
if (auth) {
|
||||
core.debug('set auth')
|
||||
headers = {
|
||||
authorization: auth
|
||||
if (headers === undefined) {
|
||||
headers = {}
|
||||
}
|
||||
headers.authorization = auth
|
||||
}
|
||||
|
||||
const response: httpm.HttpClientResponse = await http.get(url, headers)
|
||||
|
||||
+1
-2
@@ -5,8 +5,7 @@
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"target": "es6",
|
||||
"sourceMap": true,
|
||||
"lib": ["es6"]
|
||||
"sourceMap": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
||||
Reference in New Issue
Block a user