Compare commits

...
Author SHA1 Message Date
github-actions[bot] d3ad3eeb7f Update Dependencies
Code Scanning - Action / CodeQL-Build (push) Has been cancelled
2021-03-28 18:13:37 +00:00
Sora MorimotoandGitHub 4dd900dde0 Replace find with which in io package description (#633)
Signed-off-by: Sora Morimoto <[email protected]>
2021-03-11 09:57:31 -05:00
Konrad PabjanandGitHub 383ec9fb03 Update deno.yml 2021-02-18 17:18:59 +01:00
Konrad PabjanandGitHub c3478210af Create deno.yml 2021-02-18 17:14:38 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
228a9534d1 Bump ini from 1.3.5 to 1.3.7 (#672)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-05 13:38:54 -05:00
Konrad PabjanandGitHub 2202465c69 actions/cache 1.0.6 release (#705) 2021-02-02 20:48:46 +01:00
Benedek KozmaandGitHub ccd1dd298f Use GNU tar on macOS if available (#701)
* use GNU tar on macOS

* remove unnecessary code

* formatting fix

* fix tests

* fix more tests

* typo fix

* fix test
2021-02-02 20:09:10 +01:00
Yang CaoandGitHub 825204968b Merge pull request #650 from rosik/main
Make caching more verbose
2021-01-19 15:43:18 -05:00
Robin NeatherwayandGitHub 85f6235ca9 Add on: pull_request trigger to CodeQL workflow (#689)
From February 2021, in order to provide feedback on pull requests, Code Scanning workflows must be configured with both `push` and `pull_request` triggers. This is because Code Scanning compares the results from a pull request against the results for the base branch to tell you only what has changed between the two.

Early in the beta period we supported displaying results on pull requests for workflows with only `push` triggers, but have discontinued support as this proved to be less robust.

See https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#scanning-pull-requests for more information on how best to configure your Code Scanning workflows.
2021-01-15 12:22:00 +01:00
Yaroslav Dynnikov bfdba95ece Make caching more verbose
- Print cache size when saving cache similarly to restoring
- Print restore success similarly to saving
- Print cached file list if debug logging is enabled

See also: https://github.com/actions/cache/issues/471
2021-01-07 23:51:43 +03:00
Konrad PabjanandGitHub c861dd8859 Retry all http calls for artifact upload and download (#675)
* Retry all http calls for artifact upload and download

* Extra debug information

* Fix lint

* Always read response body

* PR Feedback

* Change error message if patch call fails

* Add exponential backoff when retrying

* Rework tests and add diagnostic info if exception thrown

* Fix lint

* fix lint error for real this time

* PR cleanup

* 0.5.0 @actions/artifact release

* Display diagnostic info if non-retryable code is hit
2020-12-18 21:40:50 +01:00
Konrad PabjanandGitHub 73d5917a6b actions/artifact 0.4.2 release (#673)
* actions/artifact 0.4.2

* Update releases note
2020-12-11 17:43:12 +01:00
Aiqiao YanandGitHub 4e1ffd548b Merge pull request #670 from actions/aiqiaoy/path
Fix windows cache path
2020-12-10 16:00:39 -05:00
Aiqiao Yan 42b3ff04b2 Fix windows cache path 2020-12-10 10:59:04 -05:00
Josh GrossandGitHub 8d11ee5a8c Add CODEOWNERS (#662)
* Add CODEOWNERS

* Update owners of tool-cache
2020-12-07 15:48:38 -05:00
Chris SidiandGitHub 7ad5004f87 Merge pull request #661 from yacaovsnc/main
Retry artifact download when response stream is truncated (continued)
2020-12-04 13:10:40 -05:00
b593d1deb4 Close destination steam before reject
Co-authored-by: Chris Sidi <[email protected]>
2020-12-04 11:03:17 -05:00
Yang Cao 5be846b72d style change, refactor arg sequence 2020-12-04 09:41:00 -05:00
Yang Cao dc491a61ca pipeResponseToFile already throws when reading truncated stream 2020-12-04 09:19:58 -05:00
Yang Cao a600dd34a5 Try close stream before unlink 2020-12-03 13:34:59 -05:00
Yang Cao d4e990d92f Always print skip validation log 2020-12-03 13:09:43 -05:00
Yang Cao 05b1692026 Add some debugging statements 2020-12-03 12:40:37 -05:00
Yang Cao 8ed9455d68 More styling fix 2020-12-03 10:57:04 -05:00
Yang Cao b55731c11b Fix styling 2020-12-03 10:33:10 -05:00
Yang Cao 6b83f0554a Adding retry when we received 200
If we received 200, we will attempt to download the stream. If the
stream is gzipped, gzip should throw an error when trying to decompress
the stream; or if the stream is truncated, the received bytes should be
different from the value set in content-length header.
2020-12-03 10:15:12 -05:00
Chris SidiandYang Cao 990647a104 More error handling 2020-12-03 10:12:44 -05:00
Chris SidiandYang Cao 520206f818 Retry artifact download when response stream is truncated 2020-12-03 10:12:44 -05:00
Chris SidiandGitHub ff4308098f Remove spyon, use readable stream to test pipeResponseToFile (#651)
* Remove spyon, use readable stream to test pipeResponseToFile

* Check file contents
2020-11-30 12:12:12 -05:00
Thomas BoopandGitHub 2bf7365352 Update @actions/core in remaining toolkit packages (#636)
* Update to latest @actions/core version

* Bump release notes for new versions

* Add correct pr #
2020-11-13 16:51:15 -05:00
David HadkaandGitHub e7eb2c7418 Merge pull request #626 from edburns/patch-1
Explain to the ignorant reader where this `STATE_` prefix comes from.
2020-11-05 08:15:00 -06:00
Ed BurnsandGitHub 0b69311011 Explain to the ignorant reader where this STATE_ prefix comes from.
Explain to the ignorant reader where this `STATE_` prefix comes from.
2020-11-04 13:50:18 -05:00
David HadkaandGitHub 5e5e1b7aac Merge pull request #624 from actions/dhadka/upload-chunk-error
Handle non-successful responses from uploadChunk
2020-11-03 14:12:06 -06:00
Dave Hadka 5e8657cf12 Bump @actions/cache to 1.0.4 2020-11-03 13:12:34 -06:00
Dave Hadka 678f278caa Handle non-successful responses from uploadChunk 2020-11-03 09:53:44 -06:00
David HadkaandGitHub f1b118b2a9 Merge pull request #558 from dhadka/dhadka/fix-error-handling
Handle errors representing non-successful http responses in retry logic
2020-10-20 09:54:21 -05:00
Dave Hadka 464aebd43b Update package version and release notes 2020-10-12 13:47:24 -05:00
Dave Hadka c3c81d44c1 Clean up test code 2020-10-12 13:28:50 -05:00
Thomas BoopandGitHub af82147423 Add -Append flag to powershell workflow commands 2020-10-08 16:43:28 -04:00
Thomas BoopandGitHub 4f7fb6513a swap to file commands (#587)
* swap to file commands

* swap to require
2020-09-30 15:46:54 +02:00
Konrad PabjanandGitHub 2178f0baee Documentation around colored output in logs (#583) 2020-09-24 16:38:19 +02:00
Thomas BoopandGitHub a5e05630d8 Actions core 1.2.6 release (#582) 2020-09-23 12:10:51 -04:00
Thomas BoopandGitHub 0759cdc230 Add File Commands (#571)
* Add File Commands

* pr updates w/ feedback

* run format

* fix lint/format

* slight update with an example in the docs

* pr feedback
2020-09-23 11:19:20 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
da34bfb74d Bump node-fetch from 2.6.0 to 2.6.1 in /packages/github (#570)
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-21 09:57:23 -04:00
Yang CaoandGitHub bb290885a3 Release preparation of artifact 0.4.0 (#578) 2020-09-18 19:06:53 +02:00
ace7a82469 Add an option to specify retention days for artifacts (#575)
* Add an option to specify retention days for artifacts

* Validate against settings exposed as env var to give early feedback

* Fix lint

* Add tests and addressing feedback

* Update packages/artifact/__tests__/upload.test.ts

Co-authored-by: Konrad Pabjan <[email protected]>

* Update packages/artifact/README.md

Co-authored-by: Konrad Pabjan <[email protected]>

* Update packages/artifact/src/internal/utils.ts

Co-authored-by: Konrad Pabjan <[email protected]>

* Update packages/artifact/__tests__/util.test.ts

Co-authored-by: Konrad Pabjan <[email protected]>

Co-authored-by: Konrad Pabjan <[email protected]>
2020-09-18 11:30:00 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
71b19c1d65 Bump yargs-parser from 18.1.0 to 18.1.3 in /packages/github (#566)
Bumps [yargs-parser](https://github.com/yargs/yargs-parser) from 18.1.0 to 18.1.3.
- [Release notes](https://github.com/yargs/yargs-parser/releases)
- [Changelog](https://github.com/yargs/yargs-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/yargs-parser/compare/v18.1.0...v18.1.3)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-10 23:36:25 -04:00
Thomas BoopandGitHub c9819f79d2 Actions/core v1.2.5 release notes (#562) 2020-08-26 15:09:39 -04:00
Thomas BoopandGitHub c2bc747506 Add License.md to all npm packages (#548)
* Add License.md to all npm packages
2020-08-25 16:26:50 -04:00
ea69f13737 Update Dependencies (#561)
Co-authored-by: github-actions[bot] <[email protected]>
2020-08-25 13:44:55 -04:00
Ross BrodbeckandGitHub 7f7e22a940 zeit renamed to vercel 2020-08-21 07:32:09 -04:00
Dave Hadka de52c861c1 Add retry delay 2020-08-17 20:28:35 -05:00
Dave Hadka 1ef26b2390 Handle errors representing non-successful http responses in retry logic 2020-08-17 15:10:51 -05:00
Sora MorimotoandGitHub 9ad01e4fd3 Use posix archive format (#533)
* Use posix archive format

Signed-off-by: Sora Morimoto <[email protected]>

* Update package.json and RELEASES.md

Signed-off-by: Sora Morimoto <[email protected]>
2020-08-05 17:17:23 +02:00
Konrad Pabjan 6c5508d1fb @actions/artifact 0.3.5 2020-08-04 17:11:50 +02:00
Konrad PabjanandGitHub b2cba168a2 Retry on 413 (#540) 2020-08-04 16:57:38 +02:00
Thomas BoopandGitHub e3c6237940 Update audit.yml 2020-08-03 16:55:53 -04:00
fc28be88e4 Update Dependencies (#536)
Co-authored-by: github-actions[bot] <[email protected]>
2020-08-03 11:40:36 -04:00
Konrad Pabjan 905b2c7b06 @actions/artifact 0.3.3 update 2020-07-30 17:30:41 +02:00
Konrad PabjanandGitHub 3a9dc00629 Better Artifact Telemetry + Increase Upload chunk size (#535)
* Differentiate user-agents for better internal telemetry

* Bump chunk size from 4 to 8 MB

* Update User-Agent Strings
2020-07-30 17:23:28 +02:00
Thomas BoopandGitHub ccad19055e Set main as the default branch (#527)
* set main as the default branch

* revert change to tool-cache default branch

* use versions where applicable
2020-07-21 11:33:05 -04:00
Aiqiao YanandGitHub cb18a3df6b Merge pull request #528 from dhadka/dhadka/fix-download-calc
Fix bug downloading large files with the Azure SDK
2020-07-21 10:32:24 -04:00
Dave Hadka 781092b1d1 Fix bug downloading large files with the Azure SDK 2020-07-20 15:36:34 -05:00
81 changed files with 5760 additions and 491 deletions
+4 -3
View File
@@ -2,7 +2,7 @@ name: artifact-unit-tests
on: on:
push: push:
branches: branches:
- master - main
paths-ignore: paths-ignore:
- '**.md' - '**.md'
pull_request: pull_request:
@@ -46,9 +46,10 @@ jobs:
working-directory: packages/artifact working-directory: packages/artifact
- name: Set artifact file contents - name: Set artifact file contents
shell: bash
run: | run: |
echo "::set-env name=non-gzip-artifact-content::hello" echo "non-gzip-artifact-content=hello" >> $GITHUB_ENV
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 "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 - name: Create files that will be uploaded
run: | run: |
+3 -3
View File
@@ -2,7 +2,7 @@ name: toolkit-audit
on: on:
push: push:
branches: branches:
- master - main
paths-ignore: paths-ignore:
- '**.md' - '**.md'
pull_request: pull_request:
@@ -31,8 +31,8 @@ jobs:
- name: Bootstrap - name: Bootstrap
run: npm run bootstrap run: npm run bootstrap
- name: audit tools # - name: audit tools #disabled while we wait for https://github.com/actions/toolkit/issues/539
run: npm audit --audit-level=moderate # run: npm audit --audit-level=moderate
- name: audit packages - name: audit packages
run: npm run audit-all run: npm run audit-all
+1 -1
View File
@@ -2,7 +2,7 @@ name: cache-unit-tests
on: on:
push: push:
branches: branches:
- master - main
paths-ignore: paths-ignore:
- '**.md' - '**.md'
pull_request: pull_request:
+1
View File
@@ -2,6 +2,7 @@ name: "Code Scanning - Action"
on: on:
push: push:
pull_request:
schedule: schedule:
- cron: '0 0 * * 0' - cron: '0 0 * * 0'
+43
View File
@@ -0,0 +1,43 @@
name: Publish NPM
on:
workflow_dispatch:
inputs:
package:
required: true
description: 'core, artifact, cache, exec, github, glob, io, tool-cache'
version:
required: true
description: 'the version of the package to publish'
jobs:
test:
runs-on: macos-latest
steps:
- name: Setup repo
uses: actions/checkout@v2
- name: npm install
run: npm install
- name: bootstrap
run: npm run bootstrap
- name: build
run: npm run build
- name: Run tests
run: npm run test
- name: echo inputs
run: echo ${{ github.event.inputs.package }} ${{ github.event.inputs.version }}
publish:
runs-on: macos-latest
needs: test
environment: npm-publish
steps:
- name: Testing
run: echo 'this is where we publish'
+1 -1
View File
@@ -2,7 +2,7 @@ name: toolkit-unit-tests
on: on:
push: push:
branches: branches:
- master - main
paths-ignore: paths-ignore:
- '**.md' - '**.md'
pull_request: pull_request:
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
script: | script: |
github.pulls.create( github.pulls.create(
{ {
base: "master", base: "main",
owner: "${{github.repository_owner}}", owner: "${{github.repository_owner}}",
repo: "toolkit", repo: "toolkit",
title: "Update Octokit dependencies", title: "Update Octokit dependencies",
+5
View File
@@ -0,0 +1,5 @@
* @actions/actions-runtime
/packages/artifact/ @actions/actions-service
/packages/cache/ @actions/actions-service
/packages/tool-cache/ @actions/spark
+2
View File
@@ -1,3 +1,5 @@
The MIT License (MIT)
Copyright 2019 GitHub 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: 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:
+1 -1
View File
@@ -48,7 +48,7 @@ $ npm install @actions/glob
:pencil2: [@actions/io](packages/io) :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 ```bash
$ npm install @actions/io $ npm install @actions/io
+2 -2
View File
@@ -32,14 +32,14 @@ jobs:
os: [ubuntu-16.04, windows-2019] os: [ubuntu-16.04, windows-2019]
runs-on: ${{matrix.os}} runs-on: ${{matrix.os}}
actions: actions:
- uses: actions/setup-node@master - uses: actions/setup-node@v1
with: with:
version: ${{matrix.node}} version: ${{matrix.node}}
- run: | - run: |
npm install npm install
- run: | - run: |
npm test 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. 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.
+3 -3
View File
@@ -17,13 +17,13 @@ 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 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 `master` branch has the latest code and is unstable to bind to since changes get committed to master and released to the market place by creating a tag. In addition, a new major version carrying breaking changes will get implemented in master 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 `master` since that is the latest code and can be carrying breaking changes of the next major version. > Warning: do not reference `main` since that is the latest code and can be carrying breaking changes of the next major version.
```yaml ```yaml
steps: steps:
- uses: actions/javascript-action@master # do not do this - uses: actions/javascript-action@main # do not do this
``` ```
Binding to the immutable full sha1 may offer more reliability. However, note that the hosted images toolsets (e.g. ubuntu-latest) move forward and if there is a tool breaking issue, actions may react with a patch to a major version to compensate so binding to a specific SHA may prevent you from getting fixes. Binding to the immutable full sha1 may offer more reliability. However, note that the hosted images toolsets (e.g. ubuntu-latest) move forward and if there is a tool breaking issue, actions may react with a patch to a major version to compensate so binding to a specific SHA may prevent you from getting fixes.
+68 -34
View File
@@ -1,43 +1,12 @@
# :: Commands # :: 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 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. 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 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: 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 ### Set outputs
To set an output for the step, use `::set-output`: To set an output for the step, use `::set-output`:
@@ -120,7 +89,7 @@ Save a state to an environmental variable that can later be used in the main or
echo "::save-state name=FOO::foovalue" 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 ### Log Level
@@ -155,8 +124,73 @@ function setCommandEcho(enabled: boolean): void {}
The `add-mask`, `debug`, `warning` and `error` commands do not support echoing. 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 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 ```cmd
echo ::set-output name=FOO::BAR 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
```
+1 -1
View File
@@ -18,7 +18,7 @@ e.g. To use https://github.com/actions/setup-node, users will author:
```yaml ```yaml
steps: steps:
using: actions/setup-node@master using: actions/setup-node@v1
``` ```
# Define Metadata # Define Metadata
+4 -4
View File
@@ -111,10 +111,10 @@ Registering two problem-matchers with the same owner will result in only the pro
## Examples ## Examples
Some of the starter actions are already using problem matchers, for example: Some of the starter actions are already using problem matchers, for example:
- [setup-node](https://github.com/actions/setup-node/tree/master/.github) - [setup-node](https://github.com/actions/setup-node/tree/main/.github)
- [setup-python](https://github.com/actions/setup-python/tree/master/.github) - [setup-python](https://github.com/actions/setup-python/tree/main/.github)
- [setup-go](https://github.com/actions/setup-go/tree/master/.github) - [setup-go](https://github.com/actions/setup-go/tree/main/.github)
- [setup-dotnet](https://github.com/actions/setup-dotnet/tree/master/.github) - [setup-dotnet](https://github.com/actions/setup-dotnet/tree/main/.github)
## Troubleshooting ## Troubleshooting
+3 -9
View File
@@ -7586,12 +7586,6 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"ini": {
"version": "1.3.5",
"bundled": true,
"dev": true,
"optional": true
},
"is-fullwidth-code-point": { "is-fullwidth-code-point": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
@@ -8756,9 +8750,9 @@
"dev": true "dev": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.7",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz",
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==",
"dev": true "dev": true
}, },
"init-package-json": { "init-package-json": {
+2 -2
View File
@@ -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 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` 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. 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`) 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 5. Commit and push your local changes, you will then be able to test your changes with your forked action
+9
View File
@@ -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.
+6 -1
View File
@@ -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 `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 - 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 - 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 #### Example using Absolute File Paths
@@ -203,6 +208,6 @@ Check out [implementation-details](docs/implementation-details.md) for extra inf
## Contributions ## 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. For contributions related to this package, see [artifact contributions](CONTRIBUTIONS.md) for more information.
+26
View File
@@ -28,3 +28,29 @@
### 0.3.2 ### 0.3.2
- Fix to ensure readstreams get correctly reset in the event of a retry - 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
@@ -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 // 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 // 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}`) const fs = require('fs');
console.log(`::set-env name=ACTIONS_RUNTIME_TOKEN::${process.env.ACTIONS_RUNTIME_TOKEN}`) const os = require('os');
console.log(`::set-env name=GITHUB_RUN_ID::${process.env.GITHUB_RUN_ID}`) 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'
})
+184 -44
View File
@@ -12,8 +12,12 @@ import {
ListArtifactsResponse, ListArtifactsResponse,
QueryArtifactResponse QueryArtifactResponse
} from '../src/internal/contracts' } 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 root = path.join(__dirname, '_temp', 'artifact-download-tests')
const defaultEncoding = 'utf8'
jest.mock('../src/internal/config-variables') jest.mock('../src/internal/config-variables')
jest.mock('@actions/http-client') jest.mock('@actions/http-client')
@@ -67,7 +71,7 @@ describe('Download Tests', () => {
setupFailedResponse() setupFailedResponse()
const downloadHttpClient = new DownloadHttpClient() const downloadHttpClient = new DownloadHttpClient()
expect(downloadHttpClient.listArtifacts()).rejects.toThrow( 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() configVariables.getRuntimeUrl()
) )
).rejects.toThrow( ).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 () => { 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 downloadHttpClient = new DownloadHttpClient()
const items: DownloadItem[] = [] const items: DownloadItem[] = []
items.push({ items.push({
sourceLocation: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13?itemPath=my-artifact%2FFileA.txt`, sourceLocation: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13?itemPath=my-artifact%2FFileA.txt`,
targetPath: path.join(root, 'FileA.txt') targetPath
}) })
await expect( await expect(
downloadHttpClient.downloadSingleArtifact(items) downloadHttpClient.downloadSingleArtifact(items)
).resolves.not.toThrow() ).resolves.not.toThrow()
await checkDestinationFile(targetPath, fileContents)
}) })
it('Test downloading an individual artifact without gzip', async () => { 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 downloadHttpClient = new DownloadHttpClient()
const items: DownloadItem[] = [] const items: DownloadItem[] = []
items.push({ items.push({
sourceLocation: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13?itemPath=my-artifact%2FFileB.txt`, sourceLocation: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13?itemPath=my-artifact%2FFileB.txt`,
targetPath: path.join(root, 'FileB.txt') targetPath
}) })
await expect( await expect(
downloadHttpClient.downloadSingleArtifact(items) downloadHttpClient.downloadSingleArtifact(items)
).resolves.not.toThrow() ).resolves.not.toThrow()
await checkDestinationFile(targetPath, fileContents)
}) })
it('Test retryable status codes during artifact download', async () => { it('Test retryable status codes during artifact download', async () => {
@@ -148,21 +168,72 @@ describe('Download Tests', () => {
// the download should successfully finish // the download should successfully finish
const retryableStatusCodes = [429, 502, 503, 504] const retryableStatusCodes = [429, 502, 503, 504]
for (const statusCode of retryableStatusCodes) { 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 downloadHttpClient = new DownloadHttpClient()
const items: DownloadItem[] = [] const items: DownloadItem[] = []
items.push({ items.push({
sourceLocation: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13?itemPath=my-artifact%2FFileC.txt`, sourceLocation: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13?itemPath=my-artifact%2FFileC.txt`,
targetPath: path.join(root, 'FileC.txt') targetPath
}) })
await expect( await expect(
downloadHttpClient.downloadSingleArtifact(items) downloadHttpClient.downloadSingleArtifact(items)
).resolves.not.toThrow() ).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 * 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 * @param firstHttpResponseCode the http response code that should be returned
*/ */
function setupDownloadItemResponse( function setupDownloadItemResponse(
fileContents: Buffer,
isGzip: boolean, isGzip: boolean,
firstHttpResponseCode: number firstHttpResponseCode: number,
truncateFirstResponse: boolean,
retryExpected: boolean
): void { ): void {
jest const spyInstance = jest
.spyOn(DownloadHttpClient.prototype, 'pipeResponseToFile')
.mockImplementationOnce(async () => {
return new Promise<void>(resolve => {
resolve()
})
})
jest
.spyOn(HttpClient.prototype, 'get') .spyOn(HttpClient.prototype, 'get')
.mockImplementationOnce(async () => { .mockImplementationOnce(async () => {
const mockMessage = new http.IncomingMessage(new net.Socket()) if (firstHttpResponseCode === 200) {
mockMessage.statusCode = firstHttpResponseCode const fullResponse = await constructResponse(isGzip, fileContents)
if (isGzip) { const actualResponse = truncateFirstResponse
mockMessage.headers = { ? fullResponse.subarray(0, 3)
'content-type': 'gzip' : 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 // chained response, if the HTTP GET function gets called again, return a successful response
const mockMessage = new http.IncomingMessage(new net.Socket()) const fullResponse = await constructResponse(isGzip, fileContents)
mockMessage.statusCode = 200 return {
if (isGzip) { message: getDownloadResponseMessage(
mockMessage.headers = { 200,
'content-type': 'gzip' 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()
}
}) })
+114
View File
@@ -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'
})
})
+17 -3
View File
@@ -13,6 +13,7 @@ import {
} from '../src/internal/contracts' } from '../src/internal/contracts'
import {UploadSpecification} from '../src/internal/upload-specification' import {UploadSpecification} from '../src/internal/upload-specification'
import {getArtifactUrl} from '../src/internal/utils' import {getArtifactUrl} from '../src/internal/utils'
import {UploadOptions} from '../src/internal/upload-options'
const root = path.join(__dirname, '_temp', 'artifact-upload') const root = path.join(__dirname, '_temp', 'artifact-upload')
const file1Path = path.join(root, 'file1.txt') const file1Path = path.join(root, 'file1.txt')
@@ -101,11 +102,22 @@ describe('Upload Tests', () => {
uploadHttpClient.createArtifactInFileContainer(artifactName) uploadHttpClient.createArtifactInFileContainer(artifactName)
).rejects.toEqual( ).rejects.toEqual(
new Error( 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 () => { it('Create Artifact - Storage Quota Error', async () => {
const artifactName = 'storage-quota-hit' const artifactName = 'storage-quota-hit'
const uploadHttpClient = new UploadHttpClient() const uploadHttpClient = new UploadHttpClient()
@@ -113,7 +125,7 @@ describe('Upload Tests', () => {
uploadHttpClient.createArtifactInFileContainer(artifactName) uploadHttpClient.createArtifactInFileContainer(artifactName)
).rejects.toEqual( ).rejects.toEqual(
new Error( 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() const uploadHttpClient = new UploadHttpClient()
expect( expect(
uploadHttpClient.patchArtifactSize(-2, 'my-artifact') 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'
)
}) })
/** /**
+15
View File
@@ -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', () => { it('Test constructing artifact URL', () => {
const runtimeUrl = getRuntimeUrl() const runtimeUrl = getRuntimeUrl()
const runId = getWorkFlowRunId() const runId = getWorkFlowRunId()
@@ -192,6 +206,7 @@ describe('Utils', () => {
expect(utils.isRetryableStatusCode(HttpCodes.OK)).toEqual(false) expect(utils.isRetryableStatusCode(HttpCodes.OK)).toEqual(false)
expect(utils.isRetryableStatusCode(HttpCodes.NotFound)).toEqual(false) expect(utils.isRetryableStatusCode(HttpCodes.NotFound)).toEqual(false)
expect(utils.isRetryableStatusCode(HttpCodes.Forbidden)).toEqual(false) expect(utils.isRetryableStatusCode(HttpCodes.Forbidden)).toEqual(false)
expect(utils.isRetryableStatusCode(413)).toEqual(true) // Payload Too Large
}) })
it('Test Throttled Status Code', () => { it('Test Throttled Status Code', () => {
+4 -4
View File
@@ -1,13 +1,13 @@
{ {
"name": "@actions/artifact", "name": "@actions/artifact",
"version": "0.3.2", "version": "0.5.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@actions/core": { "@actions/core": {
"version": "1.2.3", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.3.tgz", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
"integrity": "sha512-Wp4xnyokakM45Uuj4WLUxdsa8fJjKVl1fDTsPbTEcTcuu0Nb26IPQbOtjmnfaCPGcaoPOOqId8H9NapZ8gii4w==" "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
}, },
"@actions/http-client": { "@actions/http-client": {
"version": "1.0.8", "version": "1.0.8",
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/artifact", "name": "@actions/artifact",
"version": "0.3.2", "version": "0.5.0",
"preview": true, "preview": true,
"description": "Actions artifact lib", "description": "Actions artifact lib",
"keywords": [ "keywords": [
@@ -8,7 +8,7 @@
"actions", "actions",
"artifact" "artifact"
], ],
"homepage": "https://github.com/actions/toolkit/tree/master/packages/artifact", "homepage": "https://github.com/actions/toolkit/tree/main/packages/artifact",
"license": "MIT", "license": "MIT",
"main": "lib/artifact-client.js", "main": "lib/artifact-client.js",
"types": "lib/artifact-client.d.ts", "types": "lib/artifact-client.d.ts",
@@ -37,7 +37,7 @@
"url": "https://github.com/actions/toolkit/issues" "url": "https://github.com/actions/toolkit/issues"
}, },
"dependencies": { "dependencies": {
"@actions/core": "^1.2.1", "@actions/core": "^1.2.6",
"@actions/http-client": "^1.0.7", "@actions/http-client": "^1.0.7",
"@types/tmp": "^0.1.0", "@types/tmp": "^0.1.0",
"tmp": "^0.1.0", "tmp": "^0.1.0",
@@ -45,3 +45,7 @@ export function getRuntimeUrl(): string {
export function getWorkFlowRunId(): string { export function getWorkFlowRunId(): string {
return '15' return '15'
} }
export function getRetentionDays(): string | undefined {
return '45'
}
@@ -94,7 +94,8 @@ export class DefaultArtifactClient implements ArtifactClient {
} else { } else {
// Create an entry for the artifact in the file container // Create an entry for the artifact in the file container
const response = await uploadHttpClient.createArtifactInFileContainer( const response = await uploadHttpClient.createArtifactInFileContainer(
name name,
options
) )
if (!response.fileContainerResourceUrl) { if (!response.fileContainerResourceUrl) {
core.debug(response.toString()) 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 // When uploading large files that can't be uploaded with a single http call, this controls
// the chunk size that is used during upload // the chunk size that is used during upload
export function getUploadChunkSize(): number { 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 // 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 return workspaceDirectory
} }
export function getRetentionDays(): string | undefined {
return process.env['GITHUB_RETENTION_DAYS']
}
@@ -11,6 +11,7 @@ export interface ArtifactResponse {
export interface CreateArtifactParameters { export interface CreateArtifactParameters {
Type: string Type: string
Name: string Name: string
RetentionDays?: number
} }
export interface PatchArtifactSize { export interface PatchArtifactSize {
@@ -9,7 +9,10 @@ import {
isThrottledStatusCode, isThrottledStatusCode,
getExponentialRetryTimeInMilliseconds, getExponentialRetryTimeInMilliseconds,
tryGetRetryAfterValueTimeInMilliseconds, tryGetRetryAfterValueTimeInMilliseconds,
displayHttpDiagnostics displayHttpDiagnostics,
getFileSize,
rmFile,
sleep
} from './utils' } from './utils'
import {URL} from 'url' import {URL} from 'url'
import {StatusReporter} from './status-reporter' import {StatusReporter} from './status-reporter'
@@ -20,6 +23,7 @@ import {HttpManager} from './http-manager'
import {DownloadItem} from './download-specification' import {DownloadItem} from './download-specification'
import {getDownloadFileConcurrency, getRetryLimit} from './config-variables' import {getDownloadFileConcurrency, getRetryLimit} from './config-variables'
import {IncomingHttpHeaders} from 'http' import {IncomingHttpHeaders} from 'http'
import {retryHttpClientRequest} from './requestUtils'
export class DownloadHttpClient { export class DownloadHttpClient {
// http manager is used for concurrent connections when downloading multiple files at once // http manager is used for concurrent connections when downloading multiple files at once
@@ -27,7 +31,10 @@ export class DownloadHttpClient {
private statusReporter: StatusReporter private statusReporter: StatusReporter
constructor() { 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 // downloads are usually significantly faster than uploads so display status information every second
this.statusReporter = new StatusReporter(1000) 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 // 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 client = this.downloadHttpManager.getClient(0)
const headers = getDownloadHeaders('application/json') const headers = getDownloadHeaders('application/json')
const response = await client.get(artifactUrl, headers) const response = await retryHttpClientRequest('List Artifacts', async () =>
const body: string = await response.readBody() client.get(artifactUrl, headers)
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 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 // 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 client = this.downloadHttpManager.getClient(0)
const headers = getDownloadHeaders('application/json') 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() const body: string = await response.readBody()
return JSON.parse(body)
if (isSuccessStatusCode(response.message.statusCode) && body) {
return JSON.parse(body)
}
displayHttpDiagnostics(response)
throw new Error(`Unable to get ContainersItems from ${resourceUrl}`)
} }
/** /**
@@ -148,7 +148,7 @@ export class DownloadHttpClient {
): Promise<void> { ): Promise<void> {
let retryCount = 0 let retryCount = 0
const retryLimit = getRetryLimit() const retryLimit = getRetryLimit()
const destinationStream = fs.createWriteStream(downloadPath) let destinationStream = fs.createWriteStream(downloadPath)
const headers = getDownloadHeaders('application/json', true, true) const headers = getDownloadHeaders('application/json', true, true)
// a single GET request is used to download a file // a single GET request is used to download a file
@@ -183,14 +183,14 @@ export class DownloadHttpClient {
core.info( core.info(
`Backoff due to too many requests, retry #${retryCount}. Waiting for ${retryAfterValue} milliseconds before continuing the download` `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 { } else {
// Back off using an exponential value that depends on the retry count // Back off using an exponential value that depends on the retry count
const backoffTime = getExponentialRetryTimeInMilliseconds(retryCount) const backoffTime = getExponentialRetryTimeInMilliseconds(retryCount)
core.info( core.info(
`Exponential backoff for retry #${retryCount}. Waiting for ${backoffTime} milliseconds before continuing the download` `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( core.info(
`Finished backoff for retry #${retryCount}, continuing with download` `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 // keep trying to download a file until a retry limit has been reached
while (retryCount <= retryLimit) { while (retryCount <= retryLimit) {
let response: IHttpClientResponse let response: IHttpClientResponse
try { try {
response = await makeDownloadRequest() response = await makeDownloadRequest()
if (core.isDebug()) {
displayHttpDiagnostics(response)
}
} catch (error) { } catch (error) {
// if an error is caught, it is usually indicative of a timeout so retry the download // 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') core.info('An error occurred while attempting to download a file')
@@ -214,19 +242,37 @@ export class DownloadHttpClient {
continue continue
} }
let forceRetry = false
if (isSuccessStatusCode(response.message.statusCode)) { 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 // 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 // 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 // Instead of using response.readBody(), response.message is a readableStream that can be directly used to get the raw body contents
return this.pipeResponseToFile( try {
response, const isGzipped = isGzip(response.message.headers)
destinationStream, await this.pipeResponseToFile(response, destinationStream, isGzipped)
isGzip(response.message.headers)
) if (
} else if (isRetryableStatusCode(response.message.statusCode)) { 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( core.info(
`A ${response.message.statusCode} response code has been received while attempting to download an artifact` `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 // if a throttled status code is received, try to get the retryAfter header value, else differ to standard exponential backoff
isThrottledStatusCode(response.message.statusCode) isThrottledStatusCode(response.message.statusCode)
? await backOff( ? await backOff(
@@ -260,26 +306,48 @@ export class DownloadHttpClient {
if (isGzip) { if (isGzip) {
const gunzip = zlib.createGunzip() const gunzip = zlib.createGunzip()
response.message 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) .pipe(gunzip)
.on('error', error => {
core.error(
`An error occurred while attempting to decompress the response stream`
)
destinationStream.close()
reject(error)
})
.pipe(destinationStream) .pipe(destinationStream)
.on('close', () => { .on('close', () => {
resolve() resolve()
}) })
.on('error', error => { .on('error', error => {
core.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) reject(error)
}) })
} else { } else {
response.message response.message
.on('error', error => {
core.error(
`An error occurred while attempting to read the response stream`
)
destinationStream.close()
reject(error)
})
.pipe(destinationStream) .pipe(destinationStream)
.on('close', () => { .on('close', () => {
resolve() resolve()
}) })
.on('error', error => { .on('error', error => {
core.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) reject(error)
}) })
@@ -6,12 +6,14 @@ import {createHttpClient} from './utils'
*/ */
export class HttpManager { export class HttpManager {
private clients: HttpClient[] private clients: HttpClient[]
private userAgent: string
constructor(clientCount: number) { constructor(clientCount: number, userAgent: string) {
if (clientCount < 1) { if (clientCount < 1) {
throw new Error('There must be at least one client') 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 { 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 // for more information see: https://github.com/actions/http-client/blob/04e5ad73cd3fd1f5610a32116b0759eddf6570d2/index.ts#L292
disposeAndReplaceClient(index: number): void { disposeAndReplaceClient(index: number): void {
this.clients[index].dispose() this.clients[index].dispose()
this.clients[index] = createHttpClient() this.clients[index] = createHttpClient(this.userAgent)
} }
disposeAndReplaceAllClients(): void { 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, isRetryableStatusCode,
isSuccessStatusCode, isSuccessStatusCode,
isThrottledStatusCode, isThrottledStatusCode,
isForbiddenStatusCode,
displayHttpDiagnostics, displayHttpDiagnostics,
getExponentialRetryTimeInMilliseconds, getExponentialRetryTimeInMilliseconds,
tryGetRetryAfterValueTimeInMilliseconds tryGetRetryAfterValueTimeInMilliseconds,
getProperRetention,
sleep
} from './utils' } from './utils'
import { import {
getUploadChunkSize, getUploadChunkSize,
getUploadFileConcurrency, getUploadFileConcurrency,
getRetryLimit getRetryLimit,
getRetentionDays
} from './config-variables' } from './config-variables'
import {promisify} from 'util' import {promisify} from 'util'
import {URL} from 'url' import {URL} from 'url'
import {performance} from 'perf_hooks' import {performance} from 'perf_hooks'
import {StatusReporter} from './status-reporter' 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 {IHttpClientResponse} from '@actions/http-client/interfaces'
import {HttpManager} from './http-manager' import {HttpManager} from './http-manager'
import {UploadSpecification} from './upload-specification' import {UploadSpecification} from './upload-specification'
import {UploadOptions} from './upload-options' import {UploadOptions} from './upload-options'
import {createGZipFileOnDisk, createGZipFileInBuffer} from './upload-gzip' import {createGZipFileOnDisk, createGZipFileInBuffer} from './upload-gzip'
import {retryHttpClientRequest} from './requestUtils'
const stat = promisify(fs.stat) const stat = promisify(fs.stat)
export class UploadHttpClient { export class UploadHttpClient {
@@ -42,7 +45,10 @@ export class UploadHttpClient {
private statusReporter: StatusReporter private statusReporter: StatusReporter
constructor() { constructor() {
this.uploadHttpManager = new HttpManager(getUploadFileConcurrency()) this.uploadHttpManager = new HttpManager(
getUploadFileConcurrency(),
'@actions/artifact-upload'
)
this.statusReporter = new StatusReporter(10000) 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 * @returns The response from the Artifact Service if the file container was successfully created
*/ */
async createArtifactInFileContainer( async createArtifactInFileContainer(
artifactName: string artifactName: string,
options?: UploadOptions | undefined
): Promise<ArtifactResponse> { ): Promise<ArtifactResponse> {
const parameters: CreateArtifactParameters = { const parameters: CreateArtifactParameters = {
Type: 'actions_storage', Type: 'actions_storage',
Name: artifactName 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 data: string = JSON.stringify(parameters, null, 2)
const artifactUrl = getArtifactUrl() const artifactUrl = getArtifactUrl()
// use the first client from the httpManager, `keep-alive` is not used so the connection will close immediately // 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 client = this.uploadHttpManager.getClient(0)
const headers = getUploadHeaders('application/json', false) 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) { // Extra information to display when a particular HTTP code is returned
return JSON.parse(body) // If a 403 is returned when trying to create a file container, the customer has exceeded
} else if (isForbiddenStatusCode(rawResponse.message.statusCode)) { // their storage quota so no new artifact containers can be created
// if a 403 is returned when trying to create a file container, the customer has exceeded const customErrorMessages: Map<number, string> = new Map([
// their storage quota so no new artifact containers can be created [
throw new Error( HttpCodes.Forbidden,
`Artifact storage quota has been hit. Unable to upload any new artifacts` 'Artifact storage quota has been hit. Unable to upload any new artifacts'
) ],
} else { [
displayHttpDiagnostics(rawResponse) HttpCodes.BadRequest,
throw new Error( `The artifact name ${artifactName} is not valid. Request URL ${artifactUrl}`
`Unable to create a container for the artifact ${artifactName} at ${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( core.info(
`Backoff due to too many requests, retry #${retryCount}. Waiting for ${retryAfterValue} milliseconds before continuing the upload` `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 { } else {
const backoffTime = getExponentialRetryTimeInMilliseconds(retryCount) const backoffTime = getExponentialRetryTimeInMilliseconds(retryCount)
core.info( core.info(
`Exponential backoff for retry #${retryCount}. Waiting for ${backoffTime} milliseconds before continuing the upload at offset ${start}` `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( core.info(
`Finished backoff for retry #${retryCount}, continuing with upload` `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 * Updating the size indicates that we are done uploading all the contents of the artifact
*/ */
async patchArtifactSize(size: number, artifactName: string): Promise<void> { async patchArtifactSize(size: number, artifactName: string): Promise<void> {
const headers = getUploadHeaders('application/json', false)
const resourceUrl = new URL(getArtifactUrl()) const resourceUrl = new URL(getArtifactUrl())
resourceUrl.searchParams.append('artifactName', artifactName) 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 // 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 client = this.uploadHttpManager.getClient(0)
const response: HttpClientResponse = await client.patch( const headers = getUploadHeaders('application/json', false)
resourceUrl.toString(),
data, // Extra information to display when a particular HTTP code is returned
headers 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 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
} }
+43 -5
View File
@@ -1,4 +1,4 @@
import {debug, info} from '@actions/core' import {debug, info, warning} from '@actions/core'
import {promises as fs} from 'fs' import {promises as fs} from 'fs'
import {HttpCodes, HttpClient} from '@actions/http-client' import {HttpCodes, HttpClient} from '@actions/http-client'
import {BearerCredentialHandler} from '@actions/http-client/auth' import {BearerCredentialHandler} from '@actions/http-client/auth'
@@ -65,7 +65,7 @@ export function isForbiddenStatusCode(statusCode?: number): boolean {
return statusCode === HttpCodes.Forbidden return statusCode === HttpCodes.Forbidden
} }
export function isRetryableStatusCode(statusCode?: number): boolean { export function isRetryableStatusCode(statusCode: number | undefined): boolean {
if (!statusCode) { if (!statusCode) {
return false return false
} }
@@ -74,7 +74,8 @@ export function isRetryableStatusCode(statusCode?: number): boolean {
HttpCodes.BadGateway, HttpCodes.BadGateway,
HttpCodes.ServiceUnavailable, HttpCodes.ServiceUnavailable,
HttpCodes.GatewayTimeout, HttpCodes.GatewayTimeout,
HttpCodes.TooManyRequests HttpCodes.TooManyRequests,
413 // Payload Too Large
] ]
return retryableStatusCodes.includes(statusCode) return retryableStatusCodes.includes(statusCode)
} }
@@ -204,8 +205,8 @@ export function getUploadHeaders(
return requestOptions return requestOptions
} }
export function createHttpClient(): HttpClient { export function createHttpClient(userAgent: string): HttpClient {
return new HttpClient('actions/artifact', [ return new HttpClient(userAgent, [
new BearerCredentialHandler(getRuntimeToken()) new BearerCredentialHandler(getRuntimeToken())
]) ])
} }
@@ -301,3 +302,40 @@ export async function createEmptyFilesForArtifact(
await (await fs.open(filePath, 'w')).close() 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))
}
+9
View File
@@ -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.
+23 -1
View File
@@ -14,4 +14,26 @@
- Downloads Azure-hosted caches using the Azure SDK for speed and reliability - Downloads Azure-hosted caches using the Azure SDK for speed and reliability
- Displays download progress - Displays download progress
- Includes changes that break compatibility with earlier versions, including: - 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)
+12 -3
View File
@@ -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 // 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 // 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}`) const fs = require('fs');
console.log(`::set-env name=ACTIONS_RUNTIME_TOKEN::${process.env.ACTIONS_RUNTIME_TOKEN}`) const os = require('os');
console.log(`::set-env name=GITHUB_RUN_ID::${process.env.GITHUB_RUN_ID}`) 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'
})
+78 -70
View File
@@ -1,27 +1,48 @@
import {retry} from '../src/internal/requestUtils' import {retry} from '../src/internal/requestUtils'
import {HttpClientError} from '@actions/http-client'
interface TestResponse { interface ITestResponse {
statusCode: number statusCode: number
result: string | null 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( async function handleResponse(
response: TestResponse | undefined response: ITestResponse | undefined
): Promise<TestResponse> { ): Promise<ITestResponse> {
if (!response) { if (!response) {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
fail('Retry method called too many times') fail('Retry method called too many times')
} }
if (response.statusCode === 999) { if (response.error) {
throw Error('Test Error') throw response.error
} else { } else {
return Promise.resolve(response) return Promise.resolve(response)
} }
} }
async function testRetryExpectingResult( async function testRetryExpectingResult(
responses: TestResponse[], responses: ITestResponse[],
expectedResult: string | null expectedResult: string | null
): Promise<void> { ): Promise<void> {
responses = responses.reverse() // Reverse responses since we pop from end responses = responses.reverse() // Reverse responses since we pop from end
@@ -29,14 +50,44 @@ async function testRetryExpectingResult(
const actualResult = await retry( const actualResult = await retry(
'test', 'test',
async () => handleResponse(responses.pop()), async () => handleResponse(responses.pop()),
(response: TestResponse) => response.statusCode (response: ITestResponse) => response.statusCode,
2, // maxAttempts
0 // delay
) )
expect(actualResult.result).toEqual(expectedResult) 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( async function testRetryExpectingError(
responses: TestResponse[] responses: ITestResponse[]
): Promise<void> { ): Promise<void> {
responses = responses.reverse() // Reverse responses since we pop from end responses = responses.reverse() // Reverse responses since we pop from end
@@ -44,97 +95,54 @@ async function testRetryExpectingError(
retry( retry(
'test', 'test',
async () => handleResponse(responses.pop()), async () => handleResponse(responses.pop()),
(response: TestResponse) => response.statusCode (response: ITestResponse) => response.statusCode,
2, // maxAttempts,
0 // delay
) )
).rejects.toBeInstanceOf(Error) ).rejects.toBeInstanceOf(Error)
} }
test('retry works on successful response', async () => { test('retry works on successful response', async () => {
await testRetryExpectingResult( await testRetryExpectingResult([TestResponse(200, 'Ok')], 'Ok')
[
{
statusCode: 200,
result: 'Ok'
}
],
'Ok'
)
}) })
test('retry works after retryable status code', async () => { test('retry works after retryable status code', async () => {
await testRetryExpectingResult( await testRetryExpectingResult(
[ [TestResponse(503), TestResponse(200, 'Ok')],
{
statusCode: 503,
result: null
},
{
statusCode: 200,
result: 'Ok'
}
],
'Ok' 'Ok'
) )
}) })
test('retry fails after exhausting retries', async () => { test('retry fails after exhausting retries', async () => {
await testRetryExpectingError([ await testRetryExpectingError([
{ TestResponse(503),
statusCode: 503, TestResponse(503),
result: null TestResponse(200, 'Ok')
},
{
statusCode: 503,
result: null
},
{
statusCode: 200,
result: 'Ok'
}
]) ])
}) })
test('retry fails after non-retryable status code', async () => { test('retry fails after non-retryable status code', async () => {
await testRetryExpectingError([ await testRetryExpectingError([TestResponse(500), TestResponse(200, 'Ok')])
{
statusCode: 500,
result: null
},
{
statusCode: 200,
result: 'Ok'
}
])
}) })
test('retry works after error', async () => { test('retry works after error', async () => {
await testRetryExpectingResult( await testRetryExpectingResult(
[ [TestResponse(new Error('Test error')), TestResponse(200, 'Ok')],
{
statusCode: 999,
result: null
},
{
statusCode: 200,
result: 'Ok'
}
],
'Ok' 'Ok'
) )
}) })
test('retry returns after client error', async () => { test('retry returns after client error', async () => {
await testRetryExpectingResult( await testRetryExpectingResult(
[ [TestResponse(400), TestResponse(200, 'Ok')],
{ null
statusCode: 400, )
result: null })
},
{ test('retry converts errors to response object', async () => {
statusCode: 200, await testRetryConvertingErrorToResult(
result: 'Ok' [TestResponse(new HttpClientError('Test error', 409))],
} 409,
],
null null
) )
}) })
+3 -3
View File
@@ -104,7 +104,7 @@ test('restore with gzip compressed cache found', async () => {
const cacheEntry: ArtifactCacheEntry = { const cacheEntry: ArtifactCacheEntry = {
cacheKey: key, cacheKey: key,
scope: 'refs/heads/master', scope: 'refs/heads/main',
archiveLocation: 'www.actionscache.test/download' archiveLocation: 'www.actionscache.test/download'
} }
const getCacheMock = jest.spyOn(cacheHttpClient, 'getCacheEntry') const getCacheMock = jest.spyOn(cacheHttpClient, 'getCacheEntry')
@@ -166,7 +166,7 @@ test('restore with zstd compressed cache found', async () => {
const cacheEntry: ArtifactCacheEntry = { const cacheEntry: ArtifactCacheEntry = {
cacheKey: key, cacheKey: key,
scope: 'refs/heads/master', scope: 'refs/heads/main',
archiveLocation: 'www.actionscache.test/download' archiveLocation: 'www.actionscache.test/download'
} }
const getCacheMock = jest.spyOn(cacheHttpClient, 'getCacheEntry') const getCacheMock = jest.spyOn(cacheHttpClient, 'getCacheEntry')
@@ -223,7 +223,7 @@ test('restore with cache found for restore key', async () => {
const cacheEntry: ArtifactCacheEntry = { const cacheEntry: ArtifactCacheEntry = {
cacheKey: restoreKey, cacheKey: restoreKey,
scope: 'refs/heads/master', scope: 'refs/heads/main',
archiveLocation: 'www.actionscache.test/download' archiveLocation: 'www.actionscache.test/download'
} }
const getCacheMock = jest.spyOn(cacheHttpClient, 'getCacheEntry') const getCacheMock = jest.spyOn(cacheHttpClient, 'getCacheEntry')
+78 -6
View File
@@ -12,6 +12,8 @@ jest.mock('@actions/io')
const IS_WINDOWS = process.platform === 'win32' const IS_WINDOWS = process.platform === 'win32'
const defaultTarPath = process.platform === 'darwin' ? 'gtar' : 'tar'
function getTempDir(): string { function getTempDir(): string {
return path.join(__dirname, '_temp', 'tar') return path.join(__dirname, '_temp', 'tar')
} }
@@ -38,14 +40,13 @@ test('zstd extract tar', async () => {
? `${process.env['windir']}\\fakepath\\cache.tar` ? `${process.env['windir']}\\fakepath\\cache.tar`
: 'cache.tar' : 'cache.tar'
const workspace = process.env['GITHUB_WORKSPACE'] const workspace = process.env['GITHUB_WORKSPACE']
const tarPath = 'tar'
await tar.extractTar(archivePath, CompressionMethod.Zstd) await tar.extractTar(archivePath, CompressionMethod.Zstd)
expect(mkdirMock).toHaveBeenCalledWith(workspace) expect(mkdirMock).toHaveBeenCalledWith(workspace)
expect(execMock).toHaveBeenCalledTimes(1) expect(execMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledWith( expect(execMock).toHaveBeenCalledWith(
`"${tarPath}"`, `"${defaultTarPath}"`,
[ [
'--use-compress-program', '--use-compress-program',
'zstd -d --long=30', 'zstd -d --long=30',
@@ -72,7 +73,7 @@ test('gzip extract tar', async () => {
expect(mkdirMock).toHaveBeenCalledWith(workspace) expect(mkdirMock).toHaveBeenCalledWith(workspace)
const tarPath = IS_WINDOWS const tarPath = IS_WINDOWS
? `${process.env['windir']}\\System32\\tar.exe` ? `${process.env['windir']}\\System32\\tar.exe`
: 'tar' : defaultTarPath
expect(execMock).toHaveBeenCalledTimes(1) expect(execMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledWith( expect(execMock).toHaveBeenCalledWith(
`"${tarPath}"`, `"${tarPath}"`,
@@ -125,7 +126,6 @@ test('zstd create tar', async () => {
const archiveFolder = getTempDir() const archiveFolder = getTempDir()
const workspace = process.env['GITHUB_WORKSPACE'] const workspace = process.env['GITHUB_WORKSPACE']
const sourceDirectories = ['~/.npm/cache', `${workspace}/dist`] const sourceDirectories = ['~/.npm/cache', `${workspace}/dist`]
const tarPath = 'tar'
await fs.promises.mkdir(archiveFolder, {recursive: true}) await fs.promises.mkdir(archiveFolder, {recursive: true})
@@ -133,8 +133,9 @@ test('zstd create tar', async () => {
expect(execMock).toHaveBeenCalledTimes(1) expect(execMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledWith( expect(execMock).toHaveBeenCalledWith(
`"${tarPath}"`, `"${defaultTarPath}"`,
[ [
'--posix',
'--use-compress-program', '--use-compress-program',
'zstd -T0 --long=30', 'zstd -T0 --long=30',
'-cf', '-cf',
@@ -164,12 +165,13 @@ test('gzip create tar', async () => {
const tarPath = IS_WINDOWS const tarPath = IS_WINDOWS
? `${process.env['windir']}\\System32\\tar.exe` ? `${process.env['windir']}\\System32\\tar.exe`
: 'tar' : defaultTarPath
expect(execMock).toHaveBeenCalledTimes(1) expect(execMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledWith( expect(execMock).toHaveBeenCalledWith(
`"${tarPath}"`, `"${tarPath}"`,
[ [
'--posix',
'-z', '-z',
'-cf', '-cf',
IS_WINDOWS ? CacheFilename.Gzip.replace(/\\/g, '/') : CacheFilename.Gzip, IS_WINDOWS ? CacheFilename.Gzip.replace(/\\/g, '/') : CacheFilename.Gzip,
@@ -184,3 +186,73 @@ test('gzip create tar', async () => {
} }
) )
}) })
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'] : []),
{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'] : []),
{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'
],
{cwd: undefined}
)
})
+4206 -34
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/cache", "name": "@actions/cache",
"version": "1.0.0", "version": "1.0.6",
"preview": true, "preview": true,
"description": "Actions cache lib", "description": "Actions cache lib",
"keywords": [ "keywords": [
@@ -8,7 +8,7 @@
"actions", "actions",
"cache" "cache"
], ],
"homepage": "https://github.com/actions/toolkit/tree/master/packages/cache", "homepage": "https://github.com/actions/toolkit/tree/main/packages/cache",
"license": "MIT", "license": "MIT",
"main": "lib/cache.js", "main": "lib/cache.js",
"types": "lib/cache.d.ts", "types": "lib/cache.d.ts",
@@ -37,10 +37,10 @@
"url": "https://github.com/actions/toolkit/issues" "url": "https://github.com/actions/toolkit/issues"
}, },
"dependencies": { "dependencies": {
"@actions/core": "^1.2.4", "@actions/core": "^1.2.6",
"@actions/exec": "^1.0.1", "@actions/exec": "^1.0.1",
"@actions/glob": "^0.1.0", "@actions/glob": "^0.1.0",
"@actions/http-client": "^1.0.8", "@actions/http-client": "^1.0.9",
"@actions/io": "^1.0.1", "@actions/io": "^1.0.1",
"@azure/ms-rest-js": "^2.0.7", "@azure/ms-rest-js": "^2.0.7",
"@azure/storage-blob": "^12.1.2", "@azure/storage-blob": "^12.1.2",
+9 -1
View File
@@ -2,7 +2,7 @@ import * as core from '@actions/core'
import * as path from 'path' import * as path from 'path'
import * as utils from './internal/cacheUtils' import * as utils from './internal/cacheUtils'
import * as cacheHttpClient from './internal/cacheHttpClient' import * as cacheHttpClient from './internal/cacheHttpClient'
import {createTar, extractTar} from './internal/tar' import {createTar, extractTar, listTar} from './internal/tar'
import {DownloadOptions, UploadOptions} from './options' import {DownloadOptions, UploadOptions} from './options'
export class ValidationError extends Error { export class ValidationError extends Error {
@@ -100,6 +100,10 @@ export async function restoreCache(
options options
) )
if (core.isDebug()) {
await listTar(archivePath, compressionMethod)
}
const archiveFileSize = utils.getArchiveFileSizeIsBytes(archivePath) const archiveFileSize = utils.getArchiveFileSizeIsBytes(archivePath)
core.info( core.info(
`Cache Size: ~${Math.round( `Cache Size: ~${Math.round(
@@ -108,6 +112,7 @@ export async function restoreCache(
) )
await extractTar(archivePath, compressionMethod) await extractTar(archivePath, compressionMethod)
core.info('Cache restored successfully')
} finally { } finally {
// Try to delete the archive to save space // Try to delete the archive to save space
try { try {
@@ -162,6 +167,9 @@ export async function saveCache(
core.debug(`Archive Path: ${archivePath}`) core.debug(`Archive Path: ${archivePath}`)
await createTar(archiveFolder, cachePaths, compressionMethod) await createTar(archiveFolder, cachePaths, compressionMethod)
if (core.isDebug()) {
await listTar(archivePath, compressionMethod)
}
const fileSizeLimit = 5 * 1024 * 1024 * 1024 // 5GB per repo limit const fileSizeLimit = 5 * 1024 * 1024 * 1024 // 5GB per repo limit
const archiveFileSize = utils.getArchiveFileSizeIsBytes(archivePath) const archiveFileSize = utils.getArchiveFileSizeIsBytes(archivePath)
+11 -1
View File
@@ -194,7 +194,7 @@ async function uploadChunk(
'Content-Range': getContentRange(start, end) 'Content-Range': getContentRange(start, end)
} }
await retryHttpClientResponse( const uploadChunkResponse = await retryHttpClientResponse(
`uploadChunk (start: ${start}, end: ${end})`, `uploadChunk (start: ${start}, end: ${end})`,
async () => async () =>
httpClient.sendStream( httpClient.sendStream(
@@ -204,6 +204,12 @@ async function uploadChunk(
additionalHeaders additionalHeaders
) )
) )
if (!isSuccessStatusCode(uploadChunkResponse.message.statusCode)) {
throw new Error(
`Cache service responded with ${uploadChunkResponse.message.statusCode} during upload chunk.`
)
}
} }
async function uploadFile( async function uploadFile(
@@ -295,6 +301,10 @@ export async function saveCache(
// Commit Cache // Commit Cache
core.debug('Commiting cache') core.debug('Commiting cache')
const cacheSize = utils.getArchiveFileSizeIsBytes(archivePath) const cacheSize = utils.getArchiveFileSizeIsBytes(archivePath)
core.info(
`Cache Size: ~${Math.round(cacheSize / (1024 * 1024))} MB (${cacheSize} B)`
)
const commitCacheResponse = await commitCache(httpClient, cacheId, cacheSize) const commitCacheResponse = await commitCache(httpClient, cacheId, cacheSize)
if (!isSuccessStatusCode(commitCacheResponse.statusCode)) { if (!isSuccessStatusCode(commitCacheResponse.statusCode)) {
throw new Error( throw new Error(
+4 -2
View File
@@ -9,7 +9,7 @@ import * as util from 'util'
import {v4 as uuidV4} from 'uuid' import {v4 as uuidV4} from 'uuid'
import {CacheFilename, CompressionMethod} from './constants' 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> { export async function createTempDirectory(): Promise<string> {
const IS_WINDOWS = process.platform === 'win32' const IS_WINDOWS = process.platform === 'win32'
@@ -47,7 +47,9 @@ export async function resolvePaths(patterns: string[]): Promise<string[]> {
}) })
for await (const file of globber.globGenerator()) { 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}`) core.debug(`Matched: ${relativeFile}`)
// Paths are made relative so the tar entries are all relative to the root of the workspace. // Paths are made relative so the tar entries are all relative to the root of the workspace.
paths.push(`${relativeFile}`) paths.push(`${relativeFile}`)
+6
View File
@@ -11,6 +11,12 @@ export enum CompressionMethod {
Zstd = 'zstd' 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 // Socket timeout in milliseconds during download. If no traffic is received
// over the socket during this period, the socket is destroyed and the download // over the socket during this period, the socket is destroyed and the download
// is aborted. // is aborted.
+5 -2
View File
@@ -249,15 +249,18 @@ export async function downloadCacheStorageSDK(
downloadProgress.startDisplayTimer() downloadProgress.startDisplayTimer()
while (!downloadProgress.isDone()) { while (!downloadProgress.isDone()) {
const segmentStart =
downloadProgress.segmentOffset + downloadProgress.segmentSize
const segmentSize = Math.min( const segmentSize = Math.min(
maxSegmentSize, maxSegmentSize,
contentLength - downloadProgress.segmentOffset contentLength - segmentStart
) )
downloadProgress.nextSegment(segmentSize) downloadProgress.nextSegment(segmentSize)
const result = await client.downloadToBuffer( const result = await client.downloadToBuffer(
downloadProgress.segmentOffset, segmentStart,
segmentSize, segmentSize,
{ {
concurrency: options.downloadConcurrency, concurrency: options.downloadConcurrency,
+47 -12
View File
@@ -1,9 +1,10 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {HttpCodes} from '@actions/http-client' import {HttpCodes, HttpClientError} from '@actions/http-client'
import { import {
IHttpClientResponse, IHttpClientResponse,
ITypedResponse ITypedResponse
} from '@actions/http-client/interfaces' } from '@actions/http-client/interfaces'
import {DefaultRetryDelay, DefaultRetryAttempts} from './constants'
export function isSuccessStatusCode(statusCode?: number): boolean { export function isSuccessStatusCode(statusCode?: number): boolean {
if (!statusCode) { if (!statusCode) {
@@ -31,32 +32,48 @@ export function isRetryableStatusCode(statusCode?: number): boolean {
return retryableStatusCodes.includes(statusCode) return retryableStatusCodes.includes(statusCode)
} }
async function sleep(milliseconds: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
export async function retry<T>( export async function retry<T>(
name: string, name: string,
method: () => Promise<T>, method: () => Promise<T>,
getStatusCode: (arg0: T) => number | undefined, getStatusCode: (arg0: T) => number | undefined,
maxAttempts = 2 maxAttempts = DefaultRetryAttempts,
delay = DefaultRetryDelay,
onError: ((arg0: Error) => T | undefined) | undefined = undefined
): Promise<T> { ): Promise<T> {
let response: T | undefined = undefined
let statusCode: number | undefined = undefined
let isRetryable = false
let errorMessage = '' let errorMessage = ''
let attempt = 1 let attempt = 1
while (attempt <= maxAttempts) { while (attempt <= maxAttempts) {
let response: T | undefined = undefined
let statusCode: number | undefined = undefined
let isRetryable = false
try { try {
response = await method() response = await method()
} catch (error) {
if (onError) {
response = onError(error)
}
isRetryable = true
errorMessage = error.message
}
if (response) {
statusCode = getStatusCode(response) statusCode = getStatusCode(response)
if (!isServerErrorStatusCode(statusCode)) { if (!isServerErrorStatusCode(statusCode)) {
return response return response
} }
}
if (statusCode) {
isRetryable = isRetryableStatusCode(statusCode) isRetryable = isRetryableStatusCode(statusCode)
errorMessage = `Cache service responded with ${statusCode}` errorMessage = `Cache service responded with ${statusCode}`
} catch (error) {
isRetryable = true
errorMessage = error.message
} }
core.debug( core.debug(
@@ -68,6 +85,7 @@ export async function retry<T>(
break break
} }
await sleep(delay)
attempt++ attempt++
} }
@@ -77,25 +95,42 @@ export async function retry<T>(
export async function retryTypedResponse<T>( export async function retryTypedResponse<T>(
name: string, name: string,
method: () => Promise<ITypedResponse<T>>, method: () => Promise<ITypedResponse<T>>,
maxAttempts = 2 maxAttempts = DefaultRetryAttempts,
delay = DefaultRetryDelay
): Promise<ITypedResponse<T>> { ): Promise<ITypedResponse<T>> {
return await retry( return await retry(
name, name,
method, method,
(response: ITypedResponse<T>) => response.statusCode, (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>( export async function retryHttpClientResponse<T>(
name: string, name: string,
method: () => Promise<IHttpClientResponse>, method: () => Promise<IHttpClientResponse>,
maxAttempts = 2 maxAttempts = DefaultRetryAttempts,
delay = DefaultRetryDelay
): Promise<IHttpClientResponse> { ): Promise<IHttpClientResponse> {
return await retry( return await retry(
name, name,
method, method,
(response: IHttpClientResponse) => response.message.statusCode, (response: IHttpClientResponse) => response.message.statusCode,
maxAttempts maxAttempts,
delay
) )
} }
+50 -11
View File
@@ -9,18 +9,29 @@ async function getTarPath(
args: string[], args: string[],
compressionMethod: CompressionMethod compressionMethod: CompressionMethod
): Promise<string> { ): Promise<string> {
const IS_WINDOWS = process.platform === 'win32' switch (process.platform) {
if (IS_WINDOWS) { case 'win32': {
const systemTar = `${process.env['windir']}\\System32\\tar.exe` const systemTar = `${process.env['windir']}\\System32\\tar.exe`
if (compressionMethod !== CompressionMethod.Gzip) { if (compressionMethod !== CompressionMethod.Gzip) {
// We only use zstandard compression on windows when gnu tar is installed due to // We only use zstandard compression on windows when gnu tar is installed due to
// a bug with compressing large files with bsdtar + zstd // a bug with compressing large files with bsdtar + zstd
args.push('--force-local') args.push('--force-local')
} else if (existsSync(systemTar)) { } else if (existsSync(systemTar)) {
return systemTar return systemTar
} else if (await utils.isGnuTarInstalled()) { } else if (await utils.isGnuTarInstalled()) {
args.push('--force-local') args.push('--force-local')
}
break
} }
case 'darwin': {
const gnuTar = await io.which('gtar', false)
if (gnuTar) {
return gnuTar
}
break
}
default:
break
} }
return await io.which('tar', true) return await io.which('tar', true)
} }
@@ -101,6 +112,7 @@ export async function createTar(
} }
} }
const args = [ const args = [
'--posix',
...getCompressionProgram(), ...getCompressionProgram(),
'-cf', '-cf',
cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'), cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
@@ -112,3 +124,30 @@ export async function createTar(
] ]
await execTar(args, compressionMethod, archiveFolder) 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)
}
+9
View File
@@ -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.
+55
View File
@@ -113,6 +113,61 @@ 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 #### 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:
+6
View File
@@ -1,5 +1,11 @@
# @actions/core Releases # @actions/core Releases
### 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 ### 1.2.4
- [Be more lenient in accepting non-string command inputs](https://github.com/actions/toolkit/pull/405) - [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) - [Add Echo commands](https://github.com/actions/toolkit/pull/411)
+79 -13
View File
@@ -1,3 +1,4 @@
import * as fs from 'fs'
import * as os from 'os' import * as os from 'os'
import * as path from 'path' import * as path from 'path'
import * as core from '../src/core' import * as core from '../src/core'
@@ -20,27 +21,34 @@ const testEnvVars = {
INPUT_MULTIPLE_SPACES_VARIABLE: 'I have multiple spaces', INPUT_MULTIPLE_SPACES_VARIABLE: 'I have multiple spaces',
// Save inputs // Save inputs
STATE_TEST_1: 'state_val' STATE_TEST_1: 'state_val',
// File Commands
GITHUB_PATH: '',
GITHUB_ENV: ''
} }
describe('@actions/core', () => { describe('@actions/core', () => {
beforeEach(() => { beforeAll(() => {
for (const key in testEnvVars) const filePath = path.join(__dirname, `test`)
process.env[key] = testEnvVars[key as keyof typeof testEnvVars] 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() process.stdout.write = jest.fn()
}) })
afterEach(() => { it('legacy exportVariable produces the correct command and sets the env', () => {
for (const key in testEnvVars) Reflect.deleteProperty(testEnvVars, key)
})
it('exportVariable produces the correct command and sets the env', () => {
core.exportVariable('my var', 'var val') core.exportVariable('my var', 'var val')
assertWriteCalls([`::set-env name=my var::var val${os.EOL}`]) 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') core.exportVariable('special char var \r\n,:', 'special val')
expect(process.env['special char var \r\n,:']).toBe('special val') expect(process.env['special char var \r\n,:']).toBe('special val')
assertWriteCalls([ assertWriteCalls([
@@ -48,28 +56,68 @@ describe('@actions/core', () => {
]) ])
}) })
it('exportVariable escapes variable values', () => { it('legacy exportVariable escapes variable values', () => {
core.exportVariable('my var2', 'var val\r\n') core.exportVariable('my var2', 'var val\r\n')
expect(process.env['my var2']).toBe('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}`]) 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) core.exportVariable('my var', true)
assertWriteCalls([`::set-env name=my var::true${os.EOL}`]) 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) core.exportVariable('my var', 5)
assertWriteCalls([`::set-env name=my var::5${os.EOL}`]) 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', () => { it('setSecret produces the correct command', () => {
core.setSecret('secret val') core.setSecret('secret val')
assertWriteCalls([`::add-mask::secret val${os.EOL}`]) assertWriteCalls([`::add-mask::secret val${os.EOL}`])
}) })
it('prependPath produces the correct commands and sets the env', () => { 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') core.addPath('myPath')
expect(process.env['PATH']).toBe( expect(process.env['PATH']).toBe(
`myPath${path.delimiter}path1${path.delimiter}path2` `myPath${path.delimiter}path1${path.delimiter}path2`
@@ -259,3 +307,21 @@ function assertWriteCalls(calls: string[]): void {
expect(process.stdout.write).toHaveBeenNthCalledWith(i + 1, calls[i]) 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)
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/core", "name": "@actions/core",
"version": "1.2.4", "version": "1.2.6",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
+2 -2
View File
@@ -1,13 +1,13 @@
{ {
"name": "@actions/core", "name": "@actions/core",
"version": "1.2.4", "version": "1.2.6",
"description": "Actions core lib", "description": "Actions core lib",
"keywords": [ "keywords": [
"github", "github",
"actions", "actions",
"core" "core"
], ],
"homepage": "https://github.com/actions/toolkit/tree/master/packages/core", "homepage": "https://github.com/actions/toolkit/tree/main/packages/core",
"license": "MIT", "license": "MIT",
"main": "lib/core.js", "main": "lib/core.js",
"types": "lib/core.d.ts", "types": "lib/core.d.ts",
+1 -13
View File
@@ -1,4 +1,5 @@
import * as os from 'os' import * as os from 'os'
import {toCommandValue} from './utils'
// For internal use, subject to change. // 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 { function escapeData(s: any): string {
return toCommandValue(s) return toCommandValue(s)
.replace(/%/g, '%25') .replace(/%/g, '%25')
+18 -3
View File
@@ -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 os from 'os'
import * as path from 'path' import * as path from 'path'
@@ -39,7 +41,15 @@ export enum ExitCode {
export function exportVariable(name: string, val: any): void { export function exportVariable(name: string, val: any): void {
const convertedVal = toCommandValue(val) const convertedVal = toCommandValue(val)
process.env[name] = convertedVal 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 * @param inputPath
*/ */
export function addPath(inputPath: string): void { 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']}` process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`
} }
+24
View File
@@ -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'
})
}
+15
View File
@@ -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)
}
+9
View File
@@ -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 -1
View File
@@ -7,7 +7,7 @@
"actions", "actions",
"exec" "exec"
], ],
"homepage": "https://github.com/actions/toolkit/tree/master/packages/exec", "homepage": "https://github.com/actions/toolkit/tree/main/packages/exec",
"license": "MIT", "license": "MIT",
"main": "lib/exec.js", "main": "lib/exec.js",
"types": "lib/exec.d.ts", "types": "lib/exec.d.ts",
+9
View File
@@ -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.
@@ -52,9 +52,9 @@ describe('@actions/github', () => {
const branch = await octokit.repos.getBranch({ const branch = await octokit.repos.getBranch({
owner: 'actions', owner: 'actions',
repo: 'toolkit', 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']) expect(proxyConnects).toEqual(['api.github.com:443'])
}) })
@@ -88,9 +88,9 @@ describe('@actions/github', () => {
const branch = await octokit.repos.getBranch({ const branch = await octokit.repos.getBranch({
owner: 'actions', owner: 'actions',
repo: 'toolkit', repo: 'toolkit',
branch: 'master' branch: 'main'
}) })
expect(branch.data.name).toBe('master') expect(branch.data.name).toBe('main')
expect(proxyConnects).toHaveLength(0) expect(proxyConnects).toHaveLength(0)
}) })
+7 -7
View File
@@ -48,9 +48,9 @@ describe('@actions/github', () => {
const branch = await octokit.repos.getBranch({ const branch = await octokit.repos.getBranch({
owner: 'actions', owner: 'actions',
repo: 'toolkit', repo: 'toolkit',
branch: 'master' branch: 'main'
}) })
expect(branch.data.name).toBe('master') expect(branch.data.name).toBe('main')
expect(proxyConnects).toHaveLength(0) expect(proxyConnects).toHaveLength(0)
}) })
@@ -63,9 +63,9 @@ describe('@actions/github', () => {
const branch = await octokit.repos.getBranch({ const branch = await octokit.repos.getBranch({
owner: 'actions', owner: 'actions',
repo: 'toolkit', repo: 'toolkit',
branch: 'master' branch: 'main'
}) })
expect(branch.data.name).toBe('master') expect(branch.data.name).toBe('main')
expect(proxyConnects).toHaveLength(0) expect(proxyConnects).toHaveLength(0)
}) })
@@ -80,9 +80,9 @@ describe('@actions/github', () => {
const branch = await octokit.repos.getBranch({ const branch = await octokit.repos.getBranch({
owner: 'actions', owner: 'actions',
repo: 'toolkit', repo: 'toolkit',
branch: 'master' branch: 'main'
}) })
expect(branch.data.name).toBe('master') expect(branch.data.name).toBe('main')
expect(proxyConnects).toHaveLength(0) expect(proxyConnects).toHaveLength(0)
// Invalid token // Invalid token
@@ -92,7 +92,7 @@ describe('@actions/github', () => {
await octokit.repos.getBranch({ await octokit.repos.getBranch({
owner: 'actions', owner: 'actions',
repo: 'toolkit', repo: 'toolkit',
branch: 'master' branch: 'main'
}) })
} catch (err) { } catch (err) {
failed = true failed = true
+64 -73
View File
@@ -5,9 +5,9 @@
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@actions/http-client": { "@actions/http-client": {
"version": "1.0.8", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-G4JjJ6f9Hb3Zvejj+ewLLKLf99ZC+9v+yCxoYf9vSyH+WkzPLB2LuUtRMGNkooMqdugGBFStIKXOuvH1W+EctA==", "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"requires": { "requires": {
"tunnel": "0.0.6" "tunnel": "0.0.6"
} }
@@ -457,104 +457,100 @@
} }
}, },
"@octokit/auth-token": { "@octokit/auth-token": {
"version": "2.4.2", "version": "2.4.5",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.2.tgz", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.5.tgz",
"integrity": "sha512-jE/lE/IKIz2v1+/P0u4fJqv0kYwXOTujKemJMFr6FeopsxlIK3+wKDCJGnysg81XID5TgZQbIfuJ5J0lnTiuyQ==", "integrity": "sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA==",
"requires": { "requires": {
"@octokit/types": "^5.0.0" "@octokit/types": "^6.0.3"
} }
}, },
"@octokit/core": { "@octokit/core": {
"version": "3.1.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.1.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.3.1.tgz",
"integrity": "sha512-cQ2HGrtyNJ1IBxpTP1U5m/FkMAJvgw7d2j1q3c9P0XUuYilEgF6e4naTpsgm4iVcQeOnccZlw7XHRIUBy0ymcg==", "integrity": "sha512-Dc5NNQOYjgZU5S1goN6A/E500yXOfDUFRGQB8/2Tl16AcfvS3H9PudyOe3ZNE/MaVyHPIfC0htReHMJb1tMrvw==",
"requires": { "requires": {
"@octokit/auth-token": "^2.4.0", "@octokit/auth-token": "^2.4.4",
"@octokit/graphql": "^4.3.1", "@octokit/graphql": "^4.5.8",
"@octokit/request": "^5.4.0", "@octokit/request": "^5.4.12",
"@octokit/types": "^5.0.0", "@octokit/request-error": "^2.0.5",
"before-after-hook": "^2.1.0", "@octokit/types": "^6.0.3",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0" "universal-user-agent": "^6.0.0"
} }
}, },
"@octokit/endpoint": { "@octokit/endpoint": {
"version": "6.0.4", "version": "6.0.11",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.4.tgz", "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.11.tgz",
"integrity": "sha512-ZJHIsvsClEE+6LaZXskDvWIqD3Ao7+2gc66pRG5Ov4MQtMvCU9wGu1TItw9aGNmRuU9x3Fei1yb+uqGaQnm0nw==", "integrity": "sha512-fUIPpx+pZyoLW4GCs3yMnlj2LfoXTWDUVPTC4V3MUEKZm48W+XYpeWSZCv+vYF1ZABUm2CqnDVf1sFtIYrj7KQ==",
"requires": { "requires": {
"@octokit/types": "^5.0.0", "@octokit/types": "^6.0.3",
"is-plain-object": "^3.0.0", "is-plain-object": "^5.0.0",
"universal-user-agent": "^6.0.0" "universal-user-agent": "^6.0.0"
} }
}, },
"@octokit/graphql": { "@octokit/graphql": {
"version": "4.5.2", "version": "4.6.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.5.2.tgz", "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.6.1.tgz",
"integrity": "sha512-SpB/JGdB7bxRj8qowwfAXjMpICUYSJqRDj26MKJAryRQBqp/ZzARsaO2LEFWzDaps0FLQoPYVGppS0HQXkBhdg==", "integrity": "sha512-2lYlvf4YTDgZCTXTW4+OX+9WTLFtEUc6hGm4qM1nlZjzxj+arizM4aHWzBVBCxY9glh7GIs0WEuiSgbVzv8cmA==",
"requires": { "requires": {
"@octokit/request": "^5.3.0", "@octokit/request": "^5.3.0",
"@octokit/types": "^5.0.0", "@octokit/types": "^6.0.3",
"universal-user-agent": "^6.0.0" "universal-user-agent": "^6.0.0"
} }
}, },
"@octokit/openapi-types": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-6.0.0.tgz",
"integrity": "sha512-CnDdK7ivHkBtJYzWzZm7gEkanA7gKH6a09Eguz7flHw//GacPJLmkHA3f3N++MJmlxD1Fl+mB7B32EEpSCwztQ=="
},
"@octokit/plugin-paginate-rest": { "@octokit/plugin-paginate-rest": {
"version": "2.2.3", "version": "2.13.3",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.2.3.tgz", "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.13.3.tgz",
"integrity": "sha512-eKTs91wXnJH8Yicwa30jz6DF50kAh7vkcqCQ9D7/tvBAP5KKkg6I2nNof8Mp/65G0Arjsb4QcOJcIEQY+rK1Rg==", "integrity": "sha512-46lptzM9lTeSmIBt/sVP/FLSTPGx6DCzAdSX3PfeJ3mTf4h9sGC26WpaQzMEq/Z44cOcmx8VsOhO+uEgE3cjYg==",
"requires": { "requires": {
"@octokit/types": "^5.0.0" "@octokit/types": "^6.11.0"
} }
}, },
"@octokit/plugin-rest-endpoint-methods": { "@octokit/plugin-rest-endpoint-methods": {
"version": "4.1.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.0.0.tgz",
"integrity": "sha512-zbRTjm+xplSNlixotTVMvLJe8aRogUXS+r37wZK5EjLsNYH4j02K5XLMOWyYaSS4AJEZtPmzCcOcui4VzVGq+A==", "integrity": "sha512-Jc7CLNUueIshXT+HWt6T+M0sySPjF32mSFQAK7UfAg8qGeRI6OM1GSBxDLwbXjkqy2NVdnqCedJcP1nC785JYg==",
"requires": { "requires": {
"@octokit/types": "^5.1.0", "@octokit/types": "^6.13.0",
"deprecation": "^2.3.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": { "@octokit/request": {
"version": "5.4.6", "version": "5.4.14",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.6.tgz", "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.14.tgz",
"integrity": "sha512-9r8Sn4CvqFI9LDLHl9P17EZHwj3ehwQnTpTE+LEneb0VBBqSiI/VS4rWIBfBhDrDs/aIGEGZRSB0QWAck8u+2g==", "integrity": "sha512-VkmtacOIQp9daSnBmDI92xNIeLuSRDOIuplp/CJomkvzt7M18NXgG044Cx/LFKLgjKt9T2tZR6AtJayba9GTSA==",
"requires": { "requires": {
"@octokit/endpoint": "^6.0.1", "@octokit/endpoint": "^6.0.1",
"@octokit/request-error": "^2.0.0", "@octokit/request-error": "^2.0.0",
"@octokit/types": "^5.0.0", "@octokit/types": "^6.7.1",
"deprecation": "^2.0.0", "deprecation": "^2.0.0",
"is-plain-object": "^3.0.0", "is-plain-object": "^5.0.0",
"node-fetch": "^2.3.0", "node-fetch": "^2.6.1",
"once": "^1.4.0", "once": "^1.4.0",
"universal-user-agent": "^6.0.0" "universal-user-agent": "^6.0.0"
} }
}, },
"@octokit/request-error": { "@octokit/request-error": {
"version": "2.0.2", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.2.tgz", "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.5.tgz",
"integrity": "sha512-2BrmnvVSV1MXQvEkrb9zwzP0wXFNbPJij922kYBTLIlIafukrGOb+ABBT2+c6wZiuyWDH1K1zmjGQ0toN/wMWw==", "integrity": "sha512-T/2wcCFyM7SkXzNoyVNWjyVlUwBvW3igM3Btr/eKYiPmucXTtkxt2RBsf6gn3LTzaLSLTQtNmvg+dGsOxQrjZg==",
"requires": { "requires": {
"@octokit/types": "^5.0.1", "@octokit/types": "^6.0.3",
"deprecation": "^2.0.0", "deprecation": "^2.0.0",
"once": "^1.4.0" "once": "^1.4.0"
} }
}, },
"@octokit/types": { "@octokit/types": {
"version": "5.0.1", "version": "6.13.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-5.0.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.13.0.tgz",
"integrity": "sha512-GorvORVwp244fGKEt3cgt/P+M0MGy4xEDbckw+K5ojEezxyMDgCaYPKVct+/eWQfZXOT7uq0xRpmrl/+hliabA==", "integrity": "sha512-W2J9qlVIU11jMwKHUp5/rbVUeErqelCsO5vW5PKNb7wAXQVUz87Rc+imjlEvpvbH8yUb+KHmv8NEjVZdsdpyxA==",
"requires": { "requires": {
"@types/node": ">= 8" "@octokit/openapi-types": "^6.0.0"
} }
}, },
"@sinonjs/commons": { "@sinonjs/commons": {
@@ -638,11 +634,6 @@
"@types/istanbul-lib-report": "*" "@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": { "@types/stack-utils": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
@@ -1029,9 +1020,9 @@
} }
}, },
"before-after-hook": { "before-after-hook": {
"version": "2.1.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.0.tgz",
"integrity": "sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A==" "integrity": "sha512-jH6rKQIfroBbhEXVmI7XmXe3ix5S/PgJqpzdDPnR8JGLHWNYLsYZ6tK5iWOF/Ra3oqEX0NobXGlzbiylIzVphQ=="
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
@@ -2214,9 +2205,9 @@
"dev": true "dev": true
}, },
"is-plain-object": { "is-plain-object": {
"version": "3.0.1", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==" "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
}, },
"is-regex": { "is-regex": {
"version": "1.0.5", "version": "1.0.5",
@@ -3183,9 +3174,9 @@
"dev": true "dev": true
}, },
"node-fetch": { "node-fetch": {
"version": "2.6.0", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
}, },
"node-int64": { "node-int64": {
"version": "0.4.0", "version": "0.4.0",
@@ -4817,9 +4808,9 @@
} }
}, },
"yargs-parser": { "yargs-parser": {
"version": "18.1.0", "version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.0.tgz", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o/Jr6JBOv6Yx3pL+5naWSoIA2jJ+ZkMYQG/ie9qFbukBe4uzmBatlXFOiu/tNKRWEtyf+n5w7jc/O16ufqOTdQ==", "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"camelcase": "^5.0.0", "camelcase": "^5.0.0",
+5 -5
View File
@@ -6,7 +6,7 @@
"github", "github",
"actions" "actions"
], ],
"homepage": "https://github.com/actions/toolkit/tree/master/packages/github", "homepage": "https://github.com/actions/toolkit/tree/main/packages/github",
"license": "MIT", "license": "MIT",
"main": "lib/github.js", "main": "lib/github.js",
"types": "lib/github.d.ts", "types": "lib/github.d.ts",
@@ -38,10 +38,10 @@
"url": "https://github.com/actions/toolkit/issues" "url": "https://github.com/actions/toolkit/issues"
}, },
"dependencies": { "dependencies": {
"@actions/http-client": "^1.0.8", "@actions/http-client": "^1.0.11",
"@octokit/core": "^3.1.1", "@octokit/core": "^3.3.1",
"@octokit/plugin-paginate-rest": "^2.2.3", "@octokit/plugin-paginate-rest": "^2.13.3",
"@octokit/plugin-rest-endpoint-methods": "^4.1.0" "@octokit/plugin-rest-endpoint-methods": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"jest": "^25.1.0", "jest": "^25.1.0",
+1 -1
View File
@@ -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 {WebhookPayload} from './interfaces'
import {readFileSync, existsSync} from 'fs' import {readFileSync, existsSync} from 'fs'
import {EOL} from 'os' import {EOL} from 'os'
+9
View File
@@ -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.
+4
View File
@@ -3,3 +3,7 @@
### 0.1.0 ### 0.1.0
- Initial release - Initial release
### 0.1.1
- Update @actions/core version
+6 -1
View File
@@ -1,9 +1,14 @@
{ {
"name": "@actions/glob", "name": "@actions/glob",
"version": "0.1.0", "version": "0.1.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "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": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/glob", "name": "@actions/glob",
"version": "0.1.0", "version": "0.1.1",
"preview": true, "preview": true,
"description": "Actions glob lib", "description": "Actions glob lib",
"keywords": [ "keywords": [
@@ -8,7 +8,7 @@
"actions", "actions",
"glob" "glob"
], ],
"homepage": "https://github.com/actions/toolkit/tree/master/packages/glob", "homepage": "https://github.com/actions/toolkit/tree/main/packages/glob",
"license": "MIT", "license": "MIT",
"main": "lib/glob.js", "main": "lib/glob.js",
"types": "lib/glob.d.ts", "types": "lib/glob.d.ts",
@@ -37,7 +37,7 @@
"url": "https://github.com/actions/toolkit/issues" "url": "https://github.com/actions/toolkit/issues"
}, },
"dependencies": { "dependencies": {
"@actions/core": "^1.2.0", "@actions/core": "^1.2.6",
"minimatch": "^3.0.4" "minimatch": "^3.0.4"
} }
} }
+9
View File
@@ -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 -1
View File
@@ -7,7 +7,7 @@
"actions", "actions",
"io" "io"
], ],
"homepage": "https://github.com/actions/toolkit/tree/master/packages/io", "homepage": "https://github.com/actions/toolkit/tree/main/packages/io",
"license": "MIT", "license": "MIT",
"main": "lib/io.js", "main": "lib/io.js",
"types": "lib/io.d.ts", "types": "lib/io.d.ts",
+9
View File
@@ -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
View File
@@ -1,5 +1,8 @@
# @actions/tool-cache Releases # @actions/tool-cache Releases
### 1.6.1
- [Update @actions/core version](https://github.com/actions/toolkit/pull/636)
### 1.6.0 ### 1.6.0
- [Add extractXar function to extract XAR files](https://github.com/actions/toolkit/pull/207) - [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 cp = require('child_process')
//import {coerce} from 'semver' //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 owner = 'actions'
const repo = 'some-tool' const repo = 'some-tool'
const fakeToken = 'notrealtoken' const fakeToken = 'notrealtoken'
+4 -4
View File
@@ -1,13 +1,13 @@
{ {
"name": "@actions/tool-cache", "name": "@actions/tool-cache",
"version": "1.6.0", "version": "1.6.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@actions/core": { "@actions/core": {
"version": "1.2.3", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.3.tgz", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
"integrity": "sha512-Wp4xnyokakM45Uuj4WLUxdsa8fJjKVl1fDTsPbTEcTcuu0Nb26IPQbOtjmnfaCPGcaoPOOqId8H9NapZ8gii4w==" "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
}, },
"@actions/exec": { "@actions/exec": {
"version": "1.0.3", "version": "1.0.3",
+3 -3
View File
@@ -1,13 +1,13 @@
{ {
"name": "@actions/tool-cache", "name": "@actions/tool-cache",
"version": "1.6.0", "version": "1.6.1",
"description": "Actions tool-cache lib", "description": "Actions tool-cache lib",
"keywords": [ "keywords": [
"github", "github",
"actions", "actions",
"exec" "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", "license": "MIT",
"main": "lib/tool-cache.js", "main": "lib/tool-cache.js",
"types": "lib/tool-cache.d.ts", "types": "lib/tool-cache.d.ts",
@@ -36,7 +36,7 @@
"url": "https://github.com/actions/toolkit/issues" "url": "https://github.com/actions/toolkit/issues"
}, },
"dependencies": { "dependencies": {
"@actions/core": "^1.2.3", "@actions/core": "^1.2.6",
"@actions/exec": "^1.0.0", "@actions/exec": "^1.0.0",
"@actions/http-client": "^1.0.8", "@actions/http-client": "^1.0.8",
"@actions/io": "^1.0.1", "@actions/io": "^1.0.1",