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
91 changed files with 14792 additions and 35990 deletions
-1
View File
@@ -1,4 +1,3 @@
node_modules/ node_modules/
packages/*/node_modules/ packages/*/node_modules/
packages/*/lib/ packages/*/lib/
packages/glob/__tests__/_temp
+6 -18
View File
@@ -1,6 +1,6 @@
{ {
"plugins": ["jest", "@typescript-eslint"], "plugins": ["jest", "@typescript-eslint"],
"extends": ["plugin:github/recommended"], "extends": ["plugin:github/es6"],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {
"ecmaVersion": 9, "ecmaVersion": 9,
@@ -9,34 +9,20 @@
}, },
"rules": { "rules": {
"eslint-comments/no-use": "off", "eslint-comments/no-use": "off",
"github/no-then": "off",
"import/no-namespace": "off", "import/no-namespace": "off",
"no-shadow": "off",
"no-unused-vars": "off", "no-unused-vars": "off",
"no-undef": "off",
"@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
"@typescript-eslint/no-require-imports": "error", "@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/array-type": "error", "@typescript-eslint/array-type": "error",
"@typescript-eslint/await-thenable": "error", "@typescript-eslint/await-thenable": "error",
"@typescript-eslint/ban-ts-comment": "error", "@typescript-eslint/ban-ts-ignore": "error",
"camelcase": "off", "camelcase": "off",
"@typescript-eslint/camelcase": "off", "@typescript-eslint/camelcase": "off",
"@typescript-eslint/consistent-type-assertions": "off", "@typescript-eslint/class-name-casing": "error",
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}], "@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
"@typescript-eslint/func-call-spacing": ["error", "never"], "@typescript-eslint/func-call-spacing": ["error", "never"],
"@typescript-eslint/naming-convention": [ "@typescript-eslint/generic-type-naming": ["error", "^[A-Z][A-Za-z]*$"],
"error",
{
"format": null,
"filter": {
// you can expand this regex as you find more cases that require quoting that you want to allow
"regex": "^[A-Z][A-Za-z]*$",
"match": true
},
"selector": "memberLike"
}
],
"@typescript-eslint/no-array-constructor": "error", "@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-empty-interface": "error", "@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-explicit-any": "error",
@@ -46,6 +32,7 @@
"@typescript-eslint/no-misused-new": "error", "@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error", "@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "warn", "@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-object-literal-type-assertion": "error",
"@typescript-eslint/no-unnecessary-qualifier": "error", "@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error", "@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-useless-constructor": "error", "@typescript-eslint/no-useless-constructor": "error",
@@ -53,6 +40,7 @@
"@typescript-eslint/prefer-for-of": "warn", "@typescript-eslint/prefer-for-of": "warn",
"@typescript-eslint/prefer-function-type": "warn", "@typescript-eslint/prefer-function-type": "warn",
"@typescript-eslint/prefer-includes": "error", "@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-interface": "error",
"@typescript-eslint/prefer-string-starts-ends-with": "error", "@typescript-eslint/prefer-string-starts-ends-with": "error",
"@typescript-eslint/promise-function-async": "error", "@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/require-array-sort-compare": "error", "@typescript-eslint/require-array-sort-compare": "error",
+2 -3
View File
@@ -31,9 +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
# `|| npm audit` to pretty-print the output if vulnerabilies are found after filtering. # run: npm audit --audit-level=moderate
run: npm audit --audit-level=moderate --json | scripts/audit-allow-list || npm audit --audit-level=moderate
- name: audit packages - name: audit packages
run: npm run audit-all run: npm run audit-all
-2
View File
@@ -2,8 +2,6 @@ name: "Code Scanning - Action"
on: on:
push: push:
branches:
- main
pull_request: 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'
-75
View File
@@ -1,75 +0,0 @@
name: Publish NPM
on:
workflow_dispatch:
inputs:
package:
required: true
description: 'core, artifact, cache, exec, github, glob, io, tool-cache'
jobs:
test:
runs-on: macos-latest
steps:
- name: setup repo
uses: actions/checkout@v2
- name: verify package exists
run: ls packages/${{ github.event.inputs.package }}
- name: npm install
run: npm install
- name: bootstrap
run: npm run bootstrap
- name: build
run: npm run build
- name: test
run: npm run test
- name: pack
run: npm pack
working-directory: packages/${{ github.event.inputs.package }}
- name: upload artifact
uses: actions/upload-artifact@v2
with:
name: ${{ github.event.inputs.package }}
path: packages/${{ github.event.inputs.package }}/*.tgz
publish:
runs-on: macos-latest
needs: test
environment: npm-publish
steps:
- name: download artifact
uses: actions/download-artifact@v2
with:
name: ${{ github.event.inputs.package }}
- name: setup authentication
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc
env:
NPM_TOKEN: ${{ secrets.TOKEN }}
- name: publish
run: npm publish *.tgz
- name: notify slack on failure
if: failure()
run: |
curl -X POST -H 'Content-type: application/json' --data '{"text":":pb__failed: Failed to publish a new version of ${{ github.event.inputs.package }}"}' $SLACK_WEBHOOK
env:
SLACK_WEBHOOK: ${{ secrets.SLACK }}
- name: notify slack on success
if: success()
run: |
curl -X POST -H 'Content-type: application/json' --data '{"text":":dance: Successfully published a new version of ${{ github.event.inputs.package }}"}' $SLACK_WEBHOOK
env:
SLACK_WEBHOOK: ${{ secrets.SLACK }}
+2 -1
View File
@@ -1,7 +1,8 @@
name: "UpdateOctokit" name: "UpdateOctokit"
on: on:
workflow_dispatch: schedule:
- cron: '0 18 * * 0' # sunday at 18 UTC
jobs: jobs:
UpdateOctokit: UpdateOctokit:
+1
View File
@@ -2,3 +2,4 @@
/packages/artifact/ @actions/actions-service /packages/artifact/ @actions/actions-service
/packages/cache/ @actions/actions-service /packages/cache/ @actions/actions-service
/packages/tool-cache/ @actions/spark
Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

+4 -11
View File
@@ -57,7 +57,7 @@ For example, if you mask the letter `l`, running `echo "Hello FOO BAR World"` wi
### Group and Ungroup Log Lines ### Group and Ungroup Log Lines
Emitting a group with a title will instruct the logs to create a collapsible region up to the next endgroup command. Emitting a group with a title will instruct the logs to create a collapsable region up to the next ungroup command.
```bash ```bash
echo "::group::my title" echo "::group::my title"
@@ -72,7 +72,6 @@ function endGroup(): void {}
``` ```
### Problem Matchers ### Problem Matchers
Problems matchers can be used to scan a build's output to automatically surface lines to the user that matches the provided pattern. A file path to a .json Problem Matcher must be provided. See [Problem Matchers](problem-matchers.md) for more information on how to define a Problem Matcher. Problems matchers can be used to scan a build's output to automatically surface lines to the user that matches the provided pattern. A file path to a .json Problem Matcher must be provided. See [Problem Matchers](problem-matchers.md) for more information on how to define a Problem Matcher.
```bash ```bash
@@ -82,7 +81,6 @@ echo "::remove-matcher owner=eslint-compact::"
`add-matcher` takes a path to a Problem Matcher file `add-matcher` takes a path to a Problem Matcher file
`remove-matcher` removes a Problem Matcher by owner `remove-matcher` removes a Problem Matcher by owner
### Save State ### Save State
Save a state to an environmental variable that can later be used in the main or post action. Save a state to an environmental variable that can later be used in the main or post action.
@@ -104,7 +102,6 @@ There are several commands to emit different levels of log output:
| error | `echo "::error::My error message"` | | error | `echo "::error::My error message"` |
### Command Echoing ### Command Echoing
By default, the echoing of commands to stdout only occurs if [Step Debugging is enabled](./action-debugging.md#How-to-Access-Step-Debug-Logs) By default, the echoing of commands to stdout only occurs if [Step Debugging is enabled](./action-debugging.md#How-to-Access-Step-Debug-Logs)
You can enable or disable this for the current step by using the `echo` command. You can enable or disable this for the current step by using the `echo` command.
@@ -130,12 +127,12 @@ 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
# 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. 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.
@@ -149,8 +146,7 @@ echo "FOO=BAR" >> $GITHUB_ENV
Running `$FOO` in a future step will now return `BAR` 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`. For multiline strings, you may use a heredoc style syntax with your choice of delimeter. In the below example, we use `EOF`
``` ```
steps: steps:
- name: Set the value - name: Set the value
@@ -164,7 +160,6 @@ steps:
This would set the value of the `JSON_RESPONSE` env variable to the value of the curl response. 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: The expected syntax for the heredoc style is:
``` ```
{VARIABLE_NAME}<<{DELIMETER} {VARIABLE_NAME}<<{DELIMETER}
{VARIABLE_VALUE} {VARIABLE_VALUE}
@@ -188,7 +183,6 @@ 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}`; 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: This is wrapped by the core addPath method:
```javascript ```javascript
export function addPath(inputPath: string): void {} export function addPath(inputPath: string): void {}
``` ```
@@ -196,7 +190,6 @@ export function addPath(inputPath: string): void {}
### Powershell ### 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: 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: steps:
- run: echo "mypath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - run: echo "mypath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
+1 -21
View File
@@ -2,16 +2,6 @@
Problem Matchers are a way to scan the output of actions for a specified regex pattern and surface that information prominently in the UI. Both [GitHub Annotations](https://developer.github.com/v3/checks/runs/#annotations-object-1) and log file decorations are created when a match is detected. Problem Matchers are a way to scan the output of actions for a specified regex pattern and surface that information prominently in the UI. Both [GitHub Annotations](https://developer.github.com/v3/checks/runs/#annotations-object-1) and log file decorations are created when a match is detected.
## Limitations
Currently, GitHub Actions limit the annotation count in a workflow run.
- 10 warning annotations, 10 error annotations, and 10 notice annotations per step
- 50 annotations per job (sum of annotations from all the steps)
- 50 annotations per run (separate from the job annotations, these annotations arent created by users)
If your workflow may exceed these annotation counts, consider filtering of the log messages which the Problem Matcher is exposed to (e.g. by PR touched files, lines, or other).
## Single Line Matchers ## Single Line Matchers
Let's consider the ESLint compact output: Let's consider the ESLint compact output:
@@ -110,16 +100,6 @@ The eslint-stylish problem matcher defined below catches that output, and create
The first pattern matches the `test.js` line and records the file information. This line is not decorated in the UI. The first pattern matches the `test.js` line and records the file information. This line is not decorated in the UI.
The second pattern loops through the remaining lines with `loop: true` until it fails to find a match, and surfaces these lines prominently in the UI. The second pattern loops through the remaining lines with `loop: true` until it fails to find a match, and surfaces these lines prominently in the UI.
Note that the pattern matches must be on consecutive lines. The following would not result in any match findings.
```
test.js
extraneous log line of no interest
1:0 error Missing "use strict" statement strict
5:10 error 'addOne' is defined but never used no-unused-vars
✖ 2 problems (2 errors, 0 warnings)
```
## Adding and Removing Problem Matchers ## Adding and Removing Problem Matchers
Problem Matchers are enabled and removed via the toolkit [commands](commands.md#problem-matchers). Problem Matchers are enabled and removed via the toolkit [commands](commands.md#problem-matchers).
@@ -144,6 +124,6 @@ Use ECMAScript regular expression syntax when testing patterns.
### File property getting dropped ### File property getting dropped
[Enable debug logging](https://docs.github.com/en/actions/managing-workflow-runs/enabling-debug-logging) to determine why the file is getting dropped. [Enable debug logging](https://help.github.com/en/actions/configuring-and-managing-workflows/managing-a-workflow-run#enabling-debug-logging) to determine why the file is getting dropped.
This usually happens when the file does not exist or is not under the workflow repo. This usually happens when the file does not exist or is not under the workflow repo.
+13043 -32347
View File
File diff suppressed because it is too large Load Diff
+13 -14
View File
@@ -9,25 +9,24 @@
"format": "prettier --write packages/**/*.ts", "format": "prettier --write packages/**/*.ts",
"format-check": "prettier --check packages/**/*.ts", "format-check": "prettier --check packages/**/*.ts",
"lint": "eslint packages/**/*.ts", "lint": "eslint packages/**/*.ts",
"lint-fix": "eslint packages/**/*.ts --fix",
"new-package": "scripts/create-package", "new-package": "scripts/create-package",
"test": "jest --testTimeout 10000" "test": "jest --testTimeout 10000"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^24.9.1", "@types/jest": "^24.0.11",
"@types/node": "^12.20.13", "@types/node": "^12.12.47",
"@types/signale": "^1.4.1", "@types/signale": "^1.2.1",
"@typescript-eslint/parser": "^4.0.0", "@typescript-eslint/parser": "^2.2.7",
"concurrently": "^6.1.0", "concurrently": "^4.1.0",
"eslint": "^7.23.0", "eslint": "^5.16.0",
"eslint-plugin-github": "^4.1.3", "eslint-plugin-github": "^2.0.0",
"eslint-plugin-jest": "^22.21.0", "eslint-plugin-jest": "^22.5.1",
"flow-bin": "^0.115.0", "flow-bin": "^0.115.0",
"jest": "^26.6.3", "jest": "^25.1.0",
"jest-circus": "^24.9.0", "jest-circus": "^24.7.1",
"lerna": "^4.0.0", "lerna": "^3.18.4",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"ts-jest": "^26.5.6", "ts-jest": "^25.4.0",
"typescript": "^3.9.9" "typescript": "^3.7.4"
} }
} }
-7
View File
@@ -54,10 +54,3 @@
- Improved retry-ability for all http calls during artifact upload and download if an error is encountered - Improved retry-ability for all http calls during artifact upload and download if an error is encountered
### 0.5.1
- Bump @actions/http-client to version 1.0.11 to fix proxy related issues during artifact upload and download
### 0.5.2
- Add HTTP 500 as a retryable status code for artifact upload and download.
+5 -5
View File
@@ -71,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(
'List Artifacts failed: Artifact service responded with 400' 'List Artifacts failed: Artifact service responded with 500'
) )
}) })
@@ -113,7 +113,7 @@ describe('Download Tests', () => {
configVariables.getRuntimeUrl() configVariables.getRuntimeUrl()
) )
).rejects.toThrow( ).rejects.toThrow(
`Get Container Items failed: Artifact service responded with 400` `Get Container Items failed: Artifact service responded with 500`
) )
}) })
@@ -166,7 +166,7 @@ describe('Download Tests', () => {
it('Test retryable status codes during artifact download', async () => { it('Test retryable status codes during artifact download', async () => {
// The first http response should return a retryable status call while the subsequent call should return a 200 so // The first http response should return a retryable status call while the subsequent call should return a 200 so
// the download should successfully finish // the download should successfully finish
const retryableStatusCodes = [429, 500, 502, 503, 504] const retryableStatusCodes = [429, 502, 503, 504]
for (const statusCode of retryableStatusCodes) { for (const statusCode of retryableStatusCodes) {
const fileContents = Buffer.from('try, try again\n', defaultEncoding) const fileContents = Buffer.from('try, try again\n', defaultEncoding)
const targetPath = path.join(root, `FileC-${statusCode}.txt`) const targetPath = path.join(root, `FileC-${statusCode}.txt`)
@@ -357,7 +357,7 @@ describe('Download Tests', () => {
plaintext: Buffer | string plaintext: Buffer | string
): Promise<Buffer> { ): Promise<Buffer> {
if (isGzip) { if (isGzip) {
return await promisify(gzip)(plaintext) return <Buffer>await promisify(gzip)(plaintext)
} else if (typeof plaintext === 'string') { } else if (typeof plaintext === 'string') {
return Buffer.from(plaintext, defaultEncoding) return Buffer.from(plaintext, defaultEncoding)
} else { } else {
@@ -468,7 +468,7 @@ describe('Download Tests', () => {
function setupFailedResponse(): void { function setupFailedResponse(): void {
jest.spyOn(HttpClient.prototype, 'get').mockImplementationOnce(async () => { jest.spyOn(HttpClient.prototype, 'get').mockImplementationOnce(async () => {
const mockMessage = new http.IncomingMessage(new net.Socket()) const mockMessage = new http.IncomingMessage(new net.Socket())
mockMessage.statusCode = 400 mockMessage.statusCode = 500
return new Promise<HttpClientResponse>(resolve => { return new Promise<HttpClientResponse>(resolve => {
resolve({ resolve({
message: mockMessage, message: mockMessage,
+3 -3
View File
@@ -107,8 +107,8 @@ test('retry fails after exhausting retries', async () => {
}) })
test('retry fails after non-retryable status code', async () => { test('retry fails after non-retryable status code', async () => {
await testRetry([400, 200], { await testRetry([500, 200], {
responseCode: 400, responseCode: 500,
errorMessage: 'test failed: Artifact service responded with 400' errorMessage: 'test failed: Artifact service responded with 500'
}) })
}) })
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/artifact", "name": "@actions/artifact",
"version": "0.5.2", "version": "0.5.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -10,9 +10,9 @@
"integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==" "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
}, },
"@actions/http-client": { "@actions/http-client": {
"version": "1.0.11", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.8.tgz",
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", "integrity": "sha512-G4JjJ6f9Hb3Zvejj+ewLLKLf99ZC+9v+yCxoYf9vSyH+WkzPLB2LuUtRMGNkooMqdugGBFStIKXOuvH1W+EctA==",
"requires": { "requires": {
"tunnel": "0.0.6" "tunnel": "0.0.6"
} }
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/artifact", "name": "@actions/artifact",
"version": "0.5.2", "version": "0.5.0",
"preview": true, "preview": true,
"description": "Actions artifact lib", "description": "Actions artifact lib",
"keywords": [ "keywords": [
@@ -38,7 +38,7 @@
}, },
"dependencies": { "dependencies": {
"@actions/core": "^1.2.6", "@actions/core": "^1.2.6",
"@actions/http-client": "^1.0.11", "@actions/http-client": "^1.0.7",
"@types/tmp": "^0.1.0", "@types/tmp": "^0.1.0",
"tmp": "^0.1.0", "tmp": "^0.1.0",
"tmp-promise": "^2.0.2" "tmp-promise": "^2.0.2"
@@ -69,7 +69,7 @@ export async function retry(
throw Error(`${name} failed: ${errorMessage}`) throw Error(`${name} failed: ${errorMessage}`)
} }
export async function retryHttpClientRequest( export async function retryHttpClientRequest<T>(
name: string, name: string,
method: () => Promise<IHttpClientResponse>, method: () => Promise<IHttpClientResponse>,
customErrorMessages: Map<number, string> = new Map(), customErrorMessages: Map<number, string> = new Map(),
+1 -2
View File
@@ -72,9 +72,8 @@ export function isRetryableStatusCode(statusCode: number | undefined): boolean {
const retryableStatusCodes = [ const retryableStatusCodes = [
HttpCodes.BadGateway, HttpCodes.BadGateway,
HttpCodes.GatewayTimeout,
HttpCodes.InternalServerError,
HttpCodes.ServiceUnavailable, HttpCodes.ServiceUnavailable,
HttpCodes.GatewayTimeout,
HttpCodes.TooManyRequests, HttpCodes.TooManyRequests,
413 // Payload Too Large 413 // Payload Too Large
] ]
+14 -14
View File
@@ -10,20 +10,6 @@ Note that GitHub will remove any cache entries that have not been accessed in ov
This package is used by the v2+ versions of our first party cache action. You can find an example implementation in the cache repo [here](https://github.com/actions/cache). This package is used by the v2+ versions of our first party cache action. You can find an example implementation in the cache repo [here](https://github.com/actions/cache).
#### Save Cache
Saves a cache containing the files in `paths` using the `key` provided. The files would be compressed using zstandard compression algorithm if zstd is installed, otherwise gzip is used. Function returns the cache id if the cache was saved succesfully and throws an error if cache upload fails.
```js
const cache = require('@actions/cache');
const paths = [
'node_modules',
'packages/*/node_modules/'
]
const key = 'npm-foobar-d5ea0750'
const cacheId = await cache.saveCache(paths, key)
```
#### Restore Cache #### Restore Cache
Restores a cache based on `key` and `restoreKeys` to the `paths` provided. Function returns the cache key for cache hit and returns undefined if cache not found. Restores a cache based on `key` and `restoreKeys` to the `paths` provided. Function returns the cache key for cache hit and returns undefined if cache not found.
@@ -42,3 +28,17 @@ const restoreKeys = [
const cacheKey = await cache.restoreCache(paths, key, restoreKeys) const cacheKey = await cache.restoreCache(paths, key, restoreKeys)
``` ```
#### Save Cache
Saves a cache containing the files in `paths` using the `key` provided. The files would be compressed using zstandard compression algorithm if zstd is installed, otherwise gzip is used. Function returns the cache id if the cache was saved succesfully and throws an error if cache upload fails.
```js
const cache = require('@actions/cache');
const paths = [
'node_modules',
'packages/*/node_modules/'
]
const key = 'npm-foobar-d5ea0750'
const cacheId = await cache.saveCache(paths, key)
```
-3
View File
@@ -37,6 +37,3 @@
### 1.0.6 ### 1.0.6
- Make caching more verbose [#650](https://github.com/actions/toolkit/pull/650) - 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) - Use GNU tar on macOS if available [#701](https://github.com/actions/toolkit/pull/701)
### 1.0.7
- Fixes permissions issue extracting archives with GNU tar on macOS ([issue](https://github.com/actions/cache/issues/527))
+2 -2
View File
@@ -2,10 +2,10 @@ import {promises as fs} from 'fs'
import * as path from 'path' import * as path from 'path'
import * as cacheUtils from '../src/internal/cacheUtils' import * as cacheUtils from '../src/internal/cacheUtils'
test('getArchiveFileSizeInBytes returns file size', () => { test('getArchiveFileSizeIsBytes returns file size', () => {
const filePath = path.join(__dirname, '__fixtures__', 'helloWorld.txt') const filePath = path.join(__dirname, '__fixtures__', 'helloWorld.txt')
const size = cacheUtils.getArchiveFileSizeInBytes(filePath) const size = cacheUtils.getArchiveFileSizeIsBytes(filePath)
expect(size).toBe(11) expect(size).toBe(11)
}) })
+3 -4
View File
@@ -87,7 +87,7 @@ test('download progress tracked correctly', () => {
expect(progress.isDone()).toBe(true) expect(progress.isDone()).toBe(true)
}) })
test('display timer works correctly', done => { test('display timer works correctly', () => {
const progress = new DownloadProgress(1000) const progress = new DownloadProgress(1000)
const infoMock = jest.spyOn(core, 'info') const infoMock = jest.spyOn(core, 'info')
@@ -103,7 +103,6 @@ test('display timer works correctly', done => {
const test2 = (): void => { const test2 = (): void => {
check() check()
expect(progress.timeoutHandle).toBeUndefined() expect(progress.timeoutHandle).toBeUndefined()
done()
} }
// Validate the progress is displayed, stop the timer, and call test2. // Validate the progress is displayed, stop the timer, and call test2.
@@ -113,7 +112,7 @@ test('display timer works correctly', done => {
progress.stopDisplayTimer() progress.stopDisplayTimer()
progress.setReceivedBytes(1000) progress.setReceivedBytes(1000)
setTimeout(() => test2(), 500) setTimeout(() => test2(), 100)
} }
// Start the timer, update the received bytes, and call test1. // Start the timer, update the received bytes, and call test1.
@@ -123,7 +122,7 @@ test('display timer works correctly', done => {
progress.setReceivedBytes(500) progress.setReceivedBytes(500)
setTimeout(() => test1(), 500) setTimeout(() => test1(), 100)
} }
start() start()
+1
View File
@@ -30,6 +30,7 @@ async function handleResponse(
response: ITestResponse | undefined response: ITestResponse | undefined
): Promise<ITestResponse> { ): Promise<ITestResponse> {
if (!response) { if (!response) {
// eslint-disable-next-line no-undef
fail('Retry method called too many times') fail('Retry method called too many times')
} }
+10 -9
View File
@@ -18,6 +18,7 @@ beforeAll(() => {
jest.spyOn(core, 'warning').mockImplementation(() => {}) jest.spyOn(core, 'warning').mockImplementation(() => {})
jest.spyOn(core, 'error').mockImplementation(() => {}) jest.spyOn(core, 'error').mockImplementation(() => {})
// eslint-disable-next-line @typescript-eslint/promise-function-async
jest.spyOn(cacheUtils, 'getCacheFileName').mockImplementation(cm => { jest.spyOn(cacheUtils, 'getCacheFileName').mockImplementation(cm => {
const actualUtils = jest.requireActual('../src/internal/cacheUtils') const actualUtils = jest.requireActual('../src/internal/cacheUtils')
return actualUtils.getCacheFileName(cm) return actualUtils.getCacheFileName(cm)
@@ -122,8 +123,8 @@ test('restore with gzip compressed cache found', async () => {
const downloadCacheMock = jest.spyOn(cacheHttpClient, 'downloadCache') const downloadCacheMock = jest.spyOn(cacheHttpClient, 'downloadCache')
const fileSize = 142 const fileSize = 142
const getArchiveFileSizeInBytesMock = jest const getArchiveFileSizeIsBytesMock = jest
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes') .spyOn(cacheUtils, 'getArchiveFileSizeIsBytes')
.mockReturnValue(fileSize) .mockReturnValue(fileSize)
const extractTarMock = jest.spyOn(tar, 'extractTar') const extractTarMock = jest.spyOn(tar, 'extractTar')
@@ -146,7 +147,7 @@ test('restore with gzip compressed cache found', async () => {
archivePath, archivePath,
undefined undefined
) )
expect(getArchiveFileSizeInBytesMock).toHaveBeenCalledWith(archivePath) expect(getArchiveFileSizeIsBytesMock).toHaveBeenCalledWith(archivePath)
expect(extractTarMock).toHaveBeenCalledTimes(1) expect(extractTarMock).toHaveBeenCalledTimes(1)
expect(extractTarMock).toHaveBeenCalledWith(archivePath, compression) expect(extractTarMock).toHaveBeenCalledWith(archivePath, compression)
@@ -183,8 +184,8 @@ test('restore with zstd compressed cache found', async () => {
const downloadCacheMock = jest.spyOn(cacheHttpClient, 'downloadCache') const downloadCacheMock = jest.spyOn(cacheHttpClient, 'downloadCache')
const fileSize = 62915000 const fileSize = 62915000
const getArchiveFileSizeInBytesMock = jest const getArchiveFileSizeIsBytesMock = jest
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes') .spyOn(cacheUtils, 'getArchiveFileSizeIsBytes')
.mockReturnValue(fileSize) .mockReturnValue(fileSize)
const extractTarMock = jest.spyOn(tar, 'extractTar') const extractTarMock = jest.spyOn(tar, 'extractTar')
@@ -205,7 +206,7 @@ test('restore with zstd compressed cache found', async () => {
archivePath, archivePath,
undefined undefined
) )
expect(getArchiveFileSizeInBytesMock).toHaveBeenCalledWith(archivePath) expect(getArchiveFileSizeIsBytesMock).toHaveBeenCalledWith(archivePath)
expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~60 MB (62915000 B)`) expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~60 MB (62915000 B)`)
expect(extractTarMock).toHaveBeenCalledTimes(1) expect(extractTarMock).toHaveBeenCalledTimes(1)
@@ -240,8 +241,8 @@ test('restore with cache found for restore key', async () => {
const downloadCacheMock = jest.spyOn(cacheHttpClient, 'downloadCache') const downloadCacheMock = jest.spyOn(cacheHttpClient, 'downloadCache')
const fileSize = 142 const fileSize = 142
const getArchiveFileSizeInBytesMock = jest const getArchiveFileSizeIsBytesMock = jest
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes') .spyOn(cacheUtils, 'getArchiveFileSizeIsBytes')
.mockReturnValue(fileSize) .mockReturnValue(fileSize)
const extractTarMock = jest.spyOn(tar, 'extractTar') const extractTarMock = jest.spyOn(tar, 'extractTar')
@@ -262,7 +263,7 @@ test('restore with cache found for restore key', async () => {
archivePath, archivePath,
undefined undefined
) )
expect(getArchiveFileSizeInBytesMock).toHaveBeenCalledWith(archivePath) expect(getArchiveFileSizeIsBytesMock).toHaveBeenCalledWith(archivePath)
expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~0 MB (142 B)`) expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~0 MB (142 B)`)
expect(extractTarMock).toHaveBeenCalledTimes(1) expect(extractTarMock).toHaveBeenCalledTimes(1)
+2 -1
View File
@@ -17,6 +17,7 @@ beforeAll(() => {
jest.spyOn(core, 'warning').mockImplementation(() => {}) jest.spyOn(core, 'warning').mockImplementation(() => {})
jest.spyOn(core, 'error').mockImplementation(() => {}) jest.spyOn(core, 'error').mockImplementation(() => {})
// eslint-disable-next-line @typescript-eslint/promise-function-async
jest.spyOn(cacheUtils, 'getCacheFileName').mockImplementation(cm => { jest.spyOn(cacheUtils, 'getCacheFileName').mockImplementation(cm => {
const actualUtils = jest.requireActual('../src/internal/cacheUtils') const actualUtils = jest.requireActual('../src/internal/cacheUtils')
return actualUtils.getCacheFileName(cm) return actualUtils.getCacheFileName(cm)
@@ -48,7 +49,7 @@ test('save with large cache outputs should fail', async () => {
const cacheSize = 6 * 1024 * 1024 * 1024 //~6GB, over the 5GB limit const cacheSize = 6 * 1024 * 1024 * 1024 //~6GB, over the 5GB limit
jest jest
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes') .spyOn(cacheUtils, 'getArchiveFileSizeIsBytes')
.mockReturnValueOnce(cacheSize) .mockReturnValueOnce(cacheSize)
const compression = CompressionMethod.Gzip const compression = CompressionMethod.Gzip
const getCompressionMock = jest const getCompressionMock = jest
+7 -16
View File
@@ -11,7 +11,6 @@ jest.mock('@actions/exec')
jest.mock('@actions/io') jest.mock('@actions/io')
const IS_WINDOWS = process.platform === 'win32' const IS_WINDOWS = process.platform === 'win32'
const IS_MAC = process.platform === 'darwin'
const defaultTarPath = process.platform === 'darwin' ? 'gtar' : 'tar' const defaultTarPath = process.platform === 'darwin' ? 'gtar' : 'tar'
@@ -56,9 +55,7 @@ test('zstd extract tar', async () => {
'-P', '-P',
'-C', '-C',
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace
] ].concat(IS_WINDOWS ? ['--force-local'] : []),
.concat(IS_WINDOWS ? ['--force-local'] : [])
.concat(IS_MAC ? ['--delay-directory-restore'] : []),
{cwd: undefined} {cwd: undefined}
) )
}) })
@@ -87,7 +84,7 @@ test('gzip extract tar', async () => {
'-P', '-P',
'-C', '-C',
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace
].concat(IS_MAC ? ['--delay-directory-restore'] : []), ],
{cwd: undefined} {cwd: undefined}
) )
}) })
@@ -148,9 +145,7 @@ test('zstd create tar', async () => {
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace, IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace,
'--files-from', '--files-from',
'manifest.txt' 'manifest.txt'
] ].concat(IS_WINDOWS ? ['--force-local'] : []),
.concat(IS_WINDOWS ? ['--force-local'] : [])
.concat(IS_MAC ? ['--delay-directory-restore'] : []),
{ {
cwd: archiveFolder cwd: archiveFolder
} }
@@ -185,7 +180,7 @@ test('gzip create tar', async () => {
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace, IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace,
'--files-from', '--files-from',
'manifest.txt' 'manifest.txt'
].concat(IS_MAC ? ['--delay-directory-restore'] : []), ],
{ {
cwd: archiveFolder cwd: archiveFolder
} }
@@ -210,9 +205,7 @@ test('zstd list tar', async () => {
'-tf', '-tf',
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath, IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
'-P' '-P'
] ].concat(IS_WINDOWS ? ['--force-local'] : []),
.concat(IS_WINDOWS ? ['--force-local'] : [])
.concat(IS_MAC ? ['--delay-directory-restore'] : []),
{cwd: undefined} {cwd: undefined}
) )
}) })
@@ -235,9 +228,7 @@ test('zstdWithoutLong list tar', async () => {
'-tf', '-tf',
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath, IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
'-P' '-P'
] ].concat(IS_WINDOWS ? ['--force-local'] : []),
.concat(IS_WINDOWS ? ['--force-local'] : [])
.concat(IS_MAC ? ['--delay-directory-restore'] : []),
{cwd: undefined} {cwd: undefined}
) )
}) })
@@ -261,7 +252,7 @@ test('gzip list tar', async () => {
'-tf', '-tf',
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath, IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
'-P' '-P'
].concat(IS_MAC ? ['--delay-directory-restore'] : []), ],
{cwd: undefined} {cwd: undefined}
) )
}) })
+33 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/cache", "name": "@actions/cache",
"version": "1.0.7", "version": "1.0.6",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -59,6 +59,7 @@
"debug": "^4.1.0", "debug": "^4.1.0",
"gensync": "^1.0.0-beta.1", "gensync": "^1.0.0-beta.1",
"json5": "^2.1.2", "json5": "^2.1.2",
"lodash": "^4.17.13",
"resolve": "^1.3.2", "resolve": "^1.3.2",
"semver": "^5.4.1", "semver": "^5.4.1",
"source-map": "^0.5.0" "source-map": "^0.5.0"
@@ -78,6 +79,7 @@
"requires": { "requires": {
"@babel/types": "^7.9.0", "@babel/types": "^7.9.0",
"jsesc": "^2.5.1", "jsesc": "^2.5.1",
"lodash": "^4.17.13",
"source-map": "^0.5.0" "source-map": "^0.5.0"
}, },
"dependencies": { "dependencies": {
@@ -132,7 +134,8 @@
"@babel/helper-simple-access": "^7.8.3", "@babel/helper-simple-access": "^7.8.3",
"@babel/helper-split-export-declaration": "^7.8.3", "@babel/helper-split-export-declaration": "^7.8.3",
"@babel/template": "^7.8.6", "@babel/template": "^7.8.6",
"@babel/types": "^7.9.0" "@babel/types": "^7.9.0",
"lodash": "^4.17.13"
} }
}, },
"@babel/helper-optimise-call-expression": { "@babel/helper-optimise-call-expression": {
@@ -290,7 +293,8 @@
"@babel/parser": "^7.9.0", "@babel/parser": "^7.9.0",
"@babel/types": "^7.9.0", "@babel/types": "^7.9.0",
"debug": "^4.1.0", "debug": "^4.1.0",
"globals": "^11.1.0" "globals": "^11.1.0",
"lodash": "^4.17.13"
} }
}, },
"@babel/types": { "@babel/types": {
@@ -299,6 +303,7 @@
"integrity": "sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng==", "integrity": "sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng==",
"requires": { "requires": {
"@babel/helper-validator-identifier": "^7.9.0", "@babel/helper-validator-identifier": "^7.9.0",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0" "to-fast-properties": "^2.0.0"
} }
}, },
@@ -2552,6 +2557,7 @@
"whatwg-encoding": "^1.0.5", "whatwg-encoding": "^1.0.5",
"whatwg-mimetype": "^2.3.0", "whatwg-mimetype": "^2.3.0",
"whatwg-url": "^7.0.0", "whatwg-url": "^7.0.0",
"ws": "^7.0.0",
"xml-name-validator": "^3.0.0" "xml-name-validator": "^3.0.0"
} }
}, },
@@ -2626,6 +2632,11 @@
"p-locate": "^4.1.0" "p-locate": "^4.1.0"
} }
}, },
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"lodash.memoize": { "lodash.memoize": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -3191,7 +3202,10 @@
"request-promise-core": { "request-promise-core": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz",
"integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==" "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==",
"requires": {
"lodash": "^4.17.15"
}
}, },
"request-promise-native": { "request-promise-native": {
"version": "1.0.8", "version": "1.0.8",
@@ -3720,7 +3734,10 @@
"string-similarity": { "string-similarity": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-1.1.0.tgz", "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-1.1.0.tgz",
"integrity": "sha1-PGZJiFikZex8QMfYFzm72ZWQSRQ=" "integrity": "sha1-PGZJiFikZex8QMfYFzm72ZWQSRQ=",
"requires": {
"lodash": "^4.13.1"
}
}, },
"string-width": { "string-width": {
"version": "4.2.0", "version": "4.2.0",
@@ -4167,6 +4184,11 @@
"typedarray-to-buffer": "^3.1.5" "typedarray-to-buffer": "^3.1.5"
} }
}, },
"ws": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.2.3.tgz",
"integrity": "sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ=="
},
"xml-name-validator": { "xml-name-validator": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
@@ -4177,6 +4199,11 @@
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
}, },
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w=="
},
"yargs": { "yargs": {
"version": "15.3.1", "version": "15.3.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz",
@@ -4191,6 +4218,7 @@
"set-blocking": "^2.0.0", "set-blocking": "^2.0.0",
"string-width": "^4.2.0", "string-width": "^4.2.0",
"which-module": "^2.0.0", "which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.1" "yargs-parser": "^18.1.1"
} }
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/cache", "name": "@actions/cache",
"version": "1.0.7", "version": "1.0.6",
"preview": true, "preview": true,
"description": "Actions cache lib", "description": "Actions cache lib",
"keywords": [ "keywords": [
+18 -27
View File
@@ -104,7 +104,7 @@ export async function restoreCache(
await listTar(archivePath, compressionMethod) await listTar(archivePath, compressionMethod)
} }
const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath) const archiveFileSize = utils.getArchiveFileSizeIsBytes(archivePath)
core.info( core.info(
`Cache Size: ~${Math.round( `Cache Size: ~${Math.round(
archiveFileSize / (1024 * 1024) archiveFileSize / (1024 * 1024)
@@ -166,33 +166,24 @@ export async function saveCache(
core.debug(`Archive Path: ${archivePath}`) core.debug(`Archive Path: ${archivePath}`)
try { await createTar(archiveFolder, cachePaths, compressionMethod)
await createTar(archiveFolder, cachePaths, compressionMethod) if (core.isDebug()) {
if (core.isDebug()) { await listTar(archivePath, compressionMethod)
await listTar(archivePath, compressionMethod)
}
const fileSizeLimit = 5 * 1024 * 1024 * 1024 // 5GB per repo limit
const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath)
core.debug(`File Size: ${archiveFileSize}`)
if (archiveFileSize > fileSizeLimit) {
throw new Error(
`Cache size of ~${Math.round(
archiveFileSize / (1024 * 1024)
)} MB (${archiveFileSize} B) is over the 5GB limit, not saving cache.`
)
}
core.debug(`Saving Cache (ID: ${cacheId})`)
await cacheHttpClient.saveCache(cacheId, archivePath, options)
} finally {
// Try to delete the archive to save space
try {
await utils.unlinkFile(archivePath)
} catch (error) {
core.debug(`Failed to delete archive: ${error}`)
}
} }
const fileSizeLimit = 5 * 1024 * 1024 * 1024 // 5GB per repo limit
const archiveFileSize = utils.getArchiveFileSizeIsBytes(archivePath)
core.debug(`File Size: ${archiveFileSize}`)
if (archiveFileSize > fileSizeLimit) {
throw new Error(
`Cache size of ~${Math.round(
archiveFileSize / (1024 * 1024)
)} MB (${archiveFileSize} B) is over the 5GB limit, not saving cache.`
)
}
core.debug(`Saving Cache (ID: ${cacheId})`)
await cacheHttpClient.saveCache(cacheId, archivePath, options)
return cacheId return cacheId
} }
+2 -2
View File
@@ -219,7 +219,7 @@ async function uploadFile(
options?: UploadOptions options?: UploadOptions
): Promise<void> { ): Promise<void> {
// Upload Chunks // Upload Chunks
const fileSize = utils.getArchiveFileSizeInBytes(archivePath) const fileSize = fs.statSync(archivePath).size
const resourceUrl = getCacheApiUrl(`caches/${cacheId.toString()}`) const resourceUrl = getCacheApiUrl(`caches/${cacheId.toString()}`)
const fd = fs.openSync(archivePath, 'r') const fd = fs.openSync(archivePath, 'r')
const uploadOptions = getUploadOptions(options) const uploadOptions = getUploadOptions(options)
@@ -300,7 +300,7 @@ export async function saveCache(
// Commit Cache // Commit Cache
core.debug('Commiting cache') core.debug('Commiting cache')
const cacheSize = utils.getArchiveFileSizeInBytes(archivePath) const cacheSize = utils.getArchiveFileSizeIsBytes(archivePath)
core.info( core.info(
`Cache Size: ~${Math.round(cacheSize / (1024 * 1024))} MB (${cacheSize} B)` `Cache Size: ~${Math.round(cacheSize / (1024 * 1024))} MB (${cacheSize} B)`
) )
+1 -1
View File
@@ -35,7 +35,7 @@ export async function createTempDirectory(): Promise<string> {
return dest return dest
} }
export function getArchiveFileSizeInBytes(filePath: string): number { export function getArchiveFileSizeIsBytes(filePath: string): number {
return fs.statSync(filePath).size return fs.statSync(filePath).size
} }
+2 -2
View File
@@ -133,7 +133,7 @@ export class DownloadProgress {
* *
* @param delayInMs the delay between each write * @param delayInMs the delay between each write
*/ */
startDisplayTimer(delayInMs = 1000): void { startDisplayTimer(delayInMs: number = 1000): void {
const displayCallback = (): void => { const displayCallback = (): void => {
this.display() this.display()
@@ -190,7 +190,7 @@ export async function downloadCacheHttpClient(
if (contentLengthHeader) { if (contentLengthHeader) {
const expectedLength = parseInt(contentLengthHeader) const expectedLength = parseInt(contentLengthHeader)
const actualLength = utils.getArchiveFileSizeInBytes(archivePath) const actualLength = utils.getArchiveFileSizeIsBytes(archivePath)
if (actualLength !== expectedLength) { if (actualLength !== expectedLength) {
throw new Error( throw new Error(
+1 -1
View File
@@ -120,7 +120,7 @@ export async function retryTypedResponse<T>(
) )
} }
export async function retryHttpClientResponse( export async function retryHttpClientResponse<T>(
name: string, name: string,
method: () => Promise<IHttpClientResponse>, method: () => Promise<IHttpClientResponse>,
maxAttempts = DefaultRetryAttempts, maxAttempts = DefaultRetryAttempts,
-2
View File
@@ -26,8 +26,6 @@ async function getTarPath(
case 'darwin': { case 'darwin': {
const gnuTar = await io.which('gtar', false) const gnuTar = await io.which('gtar', false)
if (gnuTar) { if (gnuTar) {
// fix permission denied errors when extracting BSD tar archive with GNU tar - https://github.com/actions/cache/issues/527
args.push('--delay-directory-restore')
return gnuTar return gnuTar
} }
break break
+5 -110
View File
@@ -16,14 +16,11 @@ import * as core from '@actions/core';
#### Inputs/Outputs #### Inputs/Outputs
Action inputs can be read with `getInput` which returns a `string` or `getBooleanInput` which parses a boolean based on the [yaml 1.2 specification](https://yaml.org/spec/1.2/spec.html#id2804923). If `required` set to be false, the input should have a default value in `action.yml`. Action inputs can be read with `getInput`. Outputs can be set with `setOutput` which makes them available to be mapped into inputs of other actions to ensure they are decoupled.
Outputs can be set with `setOutput` which makes them available to be mapped into inputs of other actions to ensure they are decoupled.
```js ```js
const myInput = core.getInput('inputName', { required: true }); const myInput = core.getInput('inputName', { required: true });
const myBooleanInput = core.getBooleanInput('booleanInputName', { required: true });
const myMultilineInput = core.getMultilineInput('multilineInputName', { required: true });
core.setOutput('outputKey', 'outputVal'); core.setOutput('outputKey', 'outputVal');
``` ```
@@ -65,10 +62,11 @@ catch (err) {
// setFailed logs the message and sets a failing exit code // setFailed logs the message and sets a failing exit code
core.setFailed(`Action failed with error ${err}`); core.setFailed(`Action failed with error ${err}`);
} }
```
Note that `setNeutral` is not yet implemented in actions V2 but equivalent functionality is being planned. Note that `setNeutral` is not yet implemented in actions V2 but equivalent functionality is being planned.
```
#### Logging #### Logging
Finally, this library provides some utilities for logging. Note that debug logging is hidden from the logs by default. This behavior can be toggled by enabling the [Step Debug Logs](../../docs/action-debugging.md#step-debug-logs). Finally, this library provides some utilities for logging. Note that debug logging is hidden from the logs by default. This behavior can be toggled by enabling the [Step Debug Logs](../../docs/action-debugging.md#step-debug-logs).
@@ -92,8 +90,6 @@ try {
// Do stuff // Do stuff
core.info('Output to the actions build log') core.info('Output to the actions build log')
core.notice('This is a message that will also emit an annotation')
} }
catch (err) { catch (err) {
core.error(`Error ${err}, action may still succeed though`); core.error(`Error ${err}, action may still succeed though`);
@@ -117,60 +113,11 @@ const result = await core.group('Do something async', async () => {
}) })
``` ```
#### Annotations
This library has 3 methods that will produce [annotations](https://docs.github.com/en/rest/reference/checks#create-a-check-run).
```js
core.error('This is a bad error. This will also fail the build.')
core.warning('Something went wrong, but it\'s not bad enough to fail the build.')
core.notice('Something happened that you might want to know about.')
```
These will surface to the UI in the Actions page and on Pull Requests. They look something like this:
![Annotations Image](../../docs/assets/annotations.png)
These annotations can also be attached to particular lines and columns of your source files to show exactly where a problem is occuring.
These options are:
```typescript
export interface AnnotationProperties {
/**
* A title for the annotation.
*/
title?: string
/**
* The start line for the annotation.
*/
startLine?: number
/**
* The end line for the annotation. Defaults to `startLine` when `startLine` is provided.
*/
endLine?: number
/**
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
*/
startColumn?: number
/**
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
* Defaults to `startColumn` when `startColumn` is provided.
*/
endColumn?: number
}
```
#### Styling output #### 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. 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: Foreground colors:
```js ```js
// 3/4 bit // 3/4 bit
core.info('\u001b[35mThis foreground will be magenta') core.info('\u001b[35mThis foreground will be magenta')
@@ -183,7 +130,6 @@ core.info('\u001b[38;2;255;0;0mThis foreground will be bright red')
``` ```
Background colors: Background colors:
```js ```js
// 3/4 bit // 3/4 bit
core.info('\u001b[43mThis background will be yellow'); core.info('\u001b[43mThis background will be yellow');
@@ -210,7 +156,6 @@ core.info('\u001b[31;46mRed foreground with a cyan background and \u001b[1mbold
``` ```
> Note: Escape codes reset at the start of each line > Note: Escape codes reset at the start of each line
```js ```js
core.info('\u001b[35mThis foreground will be magenta') core.info('\u001b[35mThis foreground will be magenta')
core.info('This foreground will reset to the default') core.info('This foreground will reset to the default')
@@ -227,8 +172,7 @@ core.info(style.color.ansi16m.hex('#abcdef') + 'Hello world!')
You can use this library to save state and get state for sharing information between a given wrapper action: You can use this library to save state and get state for sharing information between a given wrapper action:
**action.yml**: **action.yml**
```yaml ```yaml
name: 'Wrapper action sample' name: 'Wrapper action sample'
inputs: inputs:
@@ -249,7 +193,6 @@ core.saveState("pidToKill", 12345);
``` ```
In action's `cleanup.js`: In action's `cleanup.js`:
```js ```js
const core = require('@actions/core'); const core = require('@actions/core');
@@ -257,51 +200,3 @@ var pid = core.getState("pidToKill");
process.kill(pid); process.kill(pid);
``` ```
#### OIDC Token
You can use these methods to interact with the GitHub OIDC provider and get a JWT ID token which would help to get access token from third party cloud providers.
**Method Name**: getIDToken()
**Inputs**
audience : optional
**Outputs**
A [JWT](https://jwt.io/) ID Token
In action's `main.ts`:
```js
const core = require('@actions/core');
async function getIDTokenAction(): Promise<void> {
const audience = core.getInput('audience', {required: false})
const id_token1 = await core.getIDToken() // ID Token with default audience
const id_token2 = await core.getIDToken(audience) // ID token with custom audience
// this id_token can be used to get access token from third party cloud providers
}
getIDTokenAction()
```
In action's `actions.yml`:
```yaml
name: 'GetIDToken'
description: 'Get ID token from Github OIDC provider'
inputs:
audience:
description: 'Audience for which the ID token is intended for'
required: false
outputs:
id_token1:
description: 'ID token obtained from OIDC provider'
id_token2:
description: 'ID token obtained from OIDC provider'
runs:
using: 'node12'
main: 'dist/index.js'
```
-17
View File
@@ -1,22 +1,5 @@
# @actions/core Releases # @actions/core Releases
### 1.6.0
- [Added OIDC Client function `getIDToken`](https://github.com/actions/toolkit/pull/919)
- [Added `file` parameter to `AnnotationProperties`](https://github.com/actions/toolkit/pull/896)
### 1.5.0
- [Added support for notice annotations and more annotation fields](https://github.com/actions/toolkit/pull/855)
### 1.4.0
- [Added the `getMultilineInput` function](https://github.com/actions/toolkit/pull/829)
### 1.3.0
- [Added the trimWhitespace option to getInput](https://github.com/actions/toolkit/pull/802)
- [Added the getBooleanInput function](https://github.com/actions/toolkit/pull/725)
### 1.2.7
- [Prepend newline for set-output](https://github.com/actions/toolkit/pull/772)
### 1.2.6 ### 1.2.6
- [Update `exportVariable` and `addPath` to use environment files](https://github.com/actions/toolkit/pull/571) - [Update `exportVariable` and `addPath` to use environment files](https://github.com/actions/toolkit/pull/571)
+3 -130
View File
@@ -2,8 +2,6 @@ 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'
import {HttpClient} from '@actions/http-client'
import {toCommandProperties} from '../src/utils'
/* eslint-disable @typescript-eslint/unbound-method */ /* eslint-disable @typescript-eslint/unbound-method */
@@ -21,17 +19,6 @@ const testEnvVars = {
INPUT_MISSING: '', INPUT_MISSING: '',
'INPUT_SPECIAL_CHARS_\'\t"\\': '\'\t"\\ response ', 'INPUT_SPECIAL_CHARS_\'\t"\\': '\'\t"\\ response ',
INPUT_MULTIPLE_SPACES_VARIABLE: 'I have multiple spaces', INPUT_MULTIPLE_SPACES_VARIABLE: 'I have multiple spaces',
INPUT_BOOLEAN_INPUT: 'true',
INPUT_BOOLEAN_INPUT_TRUE1: 'true',
INPUT_BOOLEAN_INPUT_TRUE2: 'True',
INPUT_BOOLEAN_INPUT_TRUE3: 'TRUE',
INPUT_BOOLEAN_INPUT_FALSE1: 'false',
INPUT_BOOLEAN_INPUT_FALSE2: 'False',
INPUT_BOOLEAN_INPUT_FALSE3: 'FALSE',
INPUT_WRONG_BOOLEAN_INPUT: 'wrong',
INPUT_WITH_TRAILING_WHITESPACE: ' some val ',
INPUT_MY_INPUT_LIST: 'val1\nval2\nval3',
// Save inputs // Save inputs
STATE_TEST_1: 'state_val', STATE_TEST_1: 'state_val',
@@ -170,70 +157,19 @@ describe('@actions/core', () => {
) )
}) })
it('getMultilineInput works', () => {
expect(core.getMultilineInput('my input list')).toEqual([
'val1',
'val2',
'val3'
])
})
it('getInput trims whitespace by default', () => {
expect(core.getInput('with trailing whitespace')).toBe('some val')
})
it('getInput trims whitespace when option is explicitly true', () => {
expect(
core.getInput('with trailing whitespace', {trimWhitespace: true})
).toBe('some val')
})
it('getInput does not trim whitespace when option is false', () => {
expect(
core.getInput('with trailing whitespace', {trimWhitespace: false})
).toBe(' some val ')
})
it('getInput gets non-required boolean input', () => {
expect(core.getBooleanInput('boolean input')).toBe(true)
})
it('getInput gets required input', () => {
expect(core.getBooleanInput('boolean input', {required: true})).toBe(true)
})
it('getBooleanInput handles boolean input', () => {
expect(core.getBooleanInput('boolean input true1')).toBe(true)
expect(core.getBooleanInput('boolean input true2')).toBe(true)
expect(core.getBooleanInput('boolean input true3')).toBe(true)
expect(core.getBooleanInput('boolean input false1')).toBe(false)
expect(core.getBooleanInput('boolean input false2')).toBe(false)
expect(core.getBooleanInput('boolean input false3')).toBe(false)
})
it('getBooleanInput handles wrong boolean input', () => {
expect(() => core.getBooleanInput('wrong boolean input')).toThrow(
'Input does not meet YAML 1.2 "Core Schema" specification: wrong boolean input\n' +
`Support boolean input list: \`true | True | TRUE | false | False | FALSE\``
)
})
it('setOutput produces the correct command', () => { it('setOutput produces the correct command', () => {
core.setOutput('some output', 'some value') core.setOutput('some output', 'some value')
assertWriteCalls([ assertWriteCalls([`::set-output name=some output::some value${os.EOL}`])
os.EOL,
`::set-output name=some output::some value${os.EOL}`
])
}) })
it('setOutput handles bools', () => { it('setOutput handles bools', () => {
core.setOutput('some output', false) core.setOutput('some output', false)
assertWriteCalls([os.EOL, `::set-output name=some output::false${os.EOL}`]) assertWriteCalls([`::set-output name=some output::false${os.EOL}`])
}) })
it('setOutput handles numbers', () => { it('setOutput handles numbers', () => {
core.setOutput('some output', 1.01) core.setOutput('some output', 1.01)
assertWriteCalls([os.EOL, `::set-output name=some output::1.01${os.EOL}`]) assertWriteCalls([`::set-output name=some output::1.01${os.EOL}`])
}) })
it('setFailed sets the correct exit code and failure message', () => { it('setFailed sets the correct exit code and failure message', () => {
@@ -271,20 +207,6 @@ describe('@actions/core', () => {
assertWriteCalls([`::error::Error: ${message}${os.EOL}`]) assertWriteCalls([`::error::Error: ${message}${os.EOL}`])
}) })
it('error handles parameters correctly', () => {
const message = 'this is my error message'
core.error(new Error(message), {
title: 'A title',
startColumn: 1,
endColumn: 2,
startLine: 5,
endLine: 5
})
assertWriteCalls([
`::error title=A title,line=5,endLine=5,col=1,endColumn=2::Error: ${message}${os.EOL}`
])
})
it('warning sets the correct message', () => { it('warning sets the correct message', () => {
core.warning('Warning') core.warning('Warning')
assertWriteCalls([`::warning::Warning${os.EOL}`]) assertWriteCalls([`::warning::Warning${os.EOL}`])
@@ -301,38 +223,6 @@ describe('@actions/core', () => {
assertWriteCalls([`::warning::Error: ${message}${os.EOL}`]) assertWriteCalls([`::warning::Error: ${message}${os.EOL}`])
}) })
it('warning handles parameters correctly', () => {
const message = 'this is my error message'
core.warning(new Error(message), {
title: 'A title',
startColumn: 1,
endColumn: 2,
startLine: 5,
endLine: 5
})
assertWriteCalls([
`::warning title=A title,line=5,endLine=5,col=1,endColumn=2::Error: ${message}${os.EOL}`
])
})
it('annotations map field names correctly', () => {
const commandProperties = toCommandProperties({
title: 'A title',
startColumn: 1,
endColumn: 2,
startLine: 5,
endLine: 5
})
expect(commandProperties.title).toBe('A title')
expect(commandProperties.col).toBe(1)
expect(commandProperties.endColumn).toBe(2)
expect(commandProperties.line).toBe(5)
expect(commandProperties.endLine).toBe(5)
expect(commandProperties.startColumn).toBeUndefined()
expect(commandProperties.startLine).toBeUndefined()
})
it('startGroup starts a new group', () => { it('startGroup starts a new group', () => {
core.startGroup('my-group') core.startGroup('my-group')
assertWriteCalls([`::group::my-group${os.EOL}`]) assertWriteCalls([`::group::my-group${os.EOL}`])
@@ -435,20 +325,3 @@ function verifyFileCommand(command: string, expectedContents: string): void {
fs.unlinkSync(filePath) fs.unlinkSync(filePath)
} }
} }
function getTokenEndPoint(): string {
return 'https://vstoken.actions.githubusercontent.com/.well-known/openid-configuration'
}
describe('oidc-client-tests', () => {
it('Get Http Client', async () => {
const http = new HttpClient('actions/oidc-client')
expect(http).toBeDefined()
})
it('HTTP get request to get token endpoint', async () => {
const http = new HttpClient('actions/oidc-client')
const res = await http.get(getTokenEndPoint())
expect(res.message.statusCode).toBe(200)
})
})
+2 -50
View File
@@ -1,62 +1,14 @@
{ {
"name": "@actions/core", "name": "@actions/core",
"version": "1.6.0", "version": "1.2.6",
"lockfileVersion": 2, "lockfileVersion": 1,
"requires": true, "requires": true,
"packages": {
"": {
"name": "@actions/core",
"version": "1.6.0",
"license": "MIT",
"dependencies": {
"@actions/http-client": "^1.0.11"
},
"devDependencies": {
"@types/node": "^12.0.2"
}
},
"node_modules/@actions/http-client": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"dependencies": {
"tunnel": "0.0.6"
}
},
"node_modules/@types/node": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz",
"integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==",
"dev": true
},
"node_modules/tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
"engines": {
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
}
}
},
"dependencies": { "dependencies": {
"@actions/http-client": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"requires": {
"tunnel": "0.0.6"
}
},
"@types/node": { "@types/node": {
"version": "12.0.2", "version": "12.0.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz",
"integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==", "integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==",
"dev": true "dev": true
},
"tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
} }
} }
} }
+1 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/core", "name": "@actions/core",
"version": "1.6.0", "version": "1.2.6",
"description": "Actions core lib", "description": "Actions core lib",
"keywords": [ "keywords": [
"github", "github",
@@ -35,9 +35,6 @@
"bugs": { "bugs": {
"url": "https://github.com/actions/toolkit/issues" "url": "https://github.com/actions/toolkit/issues"
}, },
"dependencies": {
"@actions/http-client": "^1.0.11"
},
"devDependencies": { "devDependencies": {
"@types/node": "^12.0.2" "@types/node": "^12.0.2"
} }
+2 -2
View File
@@ -6,7 +6,7 @@ import {toCommandValue} from './utils'
// We use any as a valid input type // We use any as a valid input type
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
export interface CommandProperties { interface CommandProperties {
[key: string]: any [key: string]: any
} }
@@ -29,7 +29,7 @@ export function issueCommand(
process.stdout.write(cmd.toString() + os.EOL) process.stdout.write(cmd.toString() + os.EOL)
} }
export function issue(name: string, message = ''): void { export function issue(name: string, message: string = ''): void {
issueCommand(name, {}, message) issueCommand(name, {}, message)
} }
+7 -128
View File
@@ -1,21 +1,16 @@
import {issue, issueCommand} from './command' import {issue, issueCommand} from './command'
import {issueCommand as issueFileCommand} from './file-command' import {issueCommand as issueFileCommand} from './file-command'
import {toCommandProperties, toCommandValue} from './utils' import {toCommandValue} from './utils'
import * as os from 'os' import * as os from 'os'
import * as path from 'path' import * as path from 'path'
import {OidcClient} from './oidc-utils'
/** /**
* Interface for getInput options * Interface for getInput options
*/ */
export interface InputOptions { export interface InputOptions {
/** Optional. Whether the input is required. If required and not present, will throw. Defaults to false */ /** Optional. Whether the input is required. If required and not present, will throw. Defaults to false */
required?: boolean required?: boolean
/** Optional. Whether leading/trailing whitespace will be trimmed for the input. Defaults to true */
trimWhitespace?: boolean
} }
/** /**
@@ -33,38 +28,6 @@ export enum ExitCode {
Failure = 1 Failure = 1
} }
/**
* Optional properties that can be sent with annotatation commands (notice, error, and warning)
* See: https://docs.github.com/en/rest/reference/checks#create-a-check-run for more information about annotations.
*/
export interface AnnotationProperties {
/**
* A title for the annotation.
*/
title?: string
/**
* The start line for the annotation.
*/
startLine?: number
/**
* The end line for the annotation. Defaults to `startLine` when `startLine` is provided.
*/
endLine?: number
/**
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
*/
startColumn?: number
/**
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
* Defaults to `startColumn` when `startColumn` is provided.
*/
endColumn?: number
}
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
// Variables // Variables
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
@@ -112,9 +75,7 @@ export function addPath(inputPath: string): void {
} }
/** /**
* Gets the value of an input. * Gets the value of an input. The value is also trimmed.
* Unless trimWhitespace is set to false in InputOptions, the value is also trimmed.
* Returns an empty string if the value is not defined.
* *
* @param name name of the input to get * @param name name of the input to get
* @param options optional. See InputOptions. * @param options optional. See InputOptions.
@@ -127,54 +88,9 @@ export function getInput(name: string, options?: InputOptions): string {
throw new Error(`Input required and not supplied: ${name}`) throw new Error(`Input required and not supplied: ${name}`)
} }
if (options && options.trimWhitespace === false) {
return val
}
return val.trim() return val.trim()
} }
/**
* Gets the values of an multiline input. Each value is also trimmed.
*
* @param name name of the input to get
* @param options optional. See InputOptions.
* @returns string[]
*
*/
export function getMultilineInput(
name: string,
options?: InputOptions
): string[] {
const inputs: string[] = getInput(name, options)
.split('\n')
.filter(x => x !== '')
return inputs
}
/**
* Gets the input value of the boolean type in the YAML 1.2 "core schema" specification.
* Support boolean input list: `true | True | TRUE | false | False | FALSE` .
* The return value is also in boolean type.
* ref: https://yaml.org/spec/1.2/spec.html#id2804923
*
* @param name name of the input to get
* @param options optional. See InputOptions.
* @returns boolean
*/
export function getBooleanInput(name: string, options?: InputOptions): boolean {
const trueValue = ['true', 'True', 'TRUE']
const falseValue = ['false', 'False', 'FALSE']
const val = getInput(name, options)
if (trueValue.includes(val)) return true
if (falseValue.includes(val)) return false
throw new TypeError(
`Input does not meet YAML 1.2 "Core Schema" specification: ${name}\n` +
`Support boolean input list: \`true | True | TRUE | false | False | FALSE\``
)
}
/** /**
* Sets the value of an output. * Sets the value of an output.
* *
@@ -183,7 +99,6 @@ export function getBooleanInput(name: string, options?: InputOptions): boolean {
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function setOutput(name: string, value: any): void { export function setOutput(name: string, value: any): void {
process.stdout.write(os.EOL)
issueCommand('set-output', {name}, value) issueCommand('set-output', {name}, value)
} }
@@ -233,49 +148,17 @@ export function debug(message: string): void {
/** /**
* Adds an error issue * Adds an error issue
* @param message error issue message. Errors will be converted to string via toString() * @param message error issue message. Errors will be converted to string via toString()
* @param properties optional properties to add to the annotation.
*/ */
export function error( export function error(message: string | Error): void {
message: string | Error, issue('error', message instanceof Error ? message.toString() : message)
properties: AnnotationProperties = {}
): void {
issueCommand(
'error',
toCommandProperties(properties),
message instanceof Error ? message.toString() : message
)
} }
/** /**
* Adds a warning issue * Adds an warning issue
* @param message warning issue message. Errors will be converted to string via toString() * @param message warning issue message. Errors will be converted to string via toString()
* @param properties optional properties to add to the annotation.
*/ */
export function warning( export function warning(message: string | Error): void {
message: string | Error, issue('warning', message instanceof Error ? message.toString() : message)
properties: AnnotationProperties = {}
): void {
issueCommand(
'warning',
toCommandProperties(properties),
message instanceof Error ? message.toString() : message
)
}
/**
* Adds a notice issue
* @param message notice issue message. Errors will be converted to string via toString()
* @param properties optional properties to add to the annotation.
*/
export function notice(
message: string | Error,
properties: AnnotationProperties = {}
): void {
issueCommand(
'notice',
toCommandProperties(properties),
message instanceof Error ? message.toString() : message
)
} }
/** /**
@@ -350,7 +233,3 @@ export function saveState(name: string, value: any): void {
export function getState(name: string): string { export function getState(name: string): string {
return process.env[`STATE_${name}`] || '' return process.env[`STATE_${name}`] || ''
} }
export async function getIDToken(aud?: string): Promise<string> {
return await OidcClient.getIDToken(aud)
}
-84
View File
@@ -1,84 +0,0 @@
/* eslint-disable @typescript-eslint/no-extraneous-class */
import * as actions_http_client from '@actions/http-client'
import {IRequestOptions} from '@actions/http-client/interfaces'
import {HttpClient} from '@actions/http-client'
import {BearerCredentialHandler} from '@actions/http-client/auth'
import {debug, setSecret} from './core'
interface TokenResponse {
value?: string
}
export class OidcClient {
private static createHttpClient(
allowRetry = true,
maxRetry = 10
): actions_http_client.HttpClient {
const requestOptions: IRequestOptions = {
allowRetries: allowRetry,
maxRetries: maxRetry
}
return new HttpClient(
'actions/oidc-client',
[new BearerCredentialHandler(OidcClient.getRequestToken())],
requestOptions
)
}
private static getRequestToken(): string {
const token = process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN']
if (!token) {
throw new Error(
'Unable to get ACTIONS_ID_TOKEN_REQUEST_TOKEN env variable'
)
}
return token
}
private static getIDTokenUrl(): string {
const runtimeUrl = process.env['ACTIONS_ID_TOKEN_REQUEST_URL']
if (!runtimeUrl) {
throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable')
}
return runtimeUrl
}
private static async getCall(id_token_url: string): Promise<string> {
const httpclient = OidcClient.createHttpClient()
const res = await httpclient
.getJson<TokenResponse>(id_token_url)
.catch(error => {
throw new Error(
`Failed to get ID Token. \n
Error Code : ${error.statusCode}\n
Error Message: ${error.result.message}`
)
})
const id_token = res.result?.value
if (!id_token) {
throw new Error('Response json body do not have ID Token field')
}
return id_token
}
static async getIDToken(audience?: string): Promise<string> {
try {
// New ID Token is requested from action service
let id_token_url: string = OidcClient.getIDTokenUrl()
if (audience) {
const encodedAudience = encodeURIComponent(audience)
id_token_url = `${id_token_url}&audience=${encodedAudience}`
}
debug(`ID token url is ${id_token_url}`)
const id_token = await OidcClient.getCall(id_token_url)
setSecret(id_token)
return id_token
} catch (error) {
throw new Error(`Error message: ${error.message}`)
}
}
}
-25
View File
@@ -1,9 +1,6 @@
// We use any as a valid input type // We use any as a valid input type
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import {AnnotationProperties} from './core'
import {CommandProperties} from './command'
/** /**
* Sanitizes an input into a string so it can be passed into issueCommand safely * Sanitizes an input into a string so it can be passed into issueCommand safely
* @param input input to sanitize into a string * @param input input to sanitize into a string
@@ -16,25 +13,3 @@ export function toCommandValue(input: any): string {
} }
return JSON.stringify(input) return JSON.stringify(input)
} }
/**
*
* @param annotationProperties
* @returns The command properties to send with the actual annotation command
* See IssueCommandProperties: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs#L646
*/
export function toCommandProperties(
annotationProperties: AnnotationProperties
): CommandProperties {
if (!Object.keys(annotationProperties).length) {
return {}
}
return {
title: annotationProperties.title,
line: annotationProperties.startLine,
endLine: annotationProperties.endLine,
col: annotationProperties.startColumn,
endColumn: annotationProperties.endColumn
}
}
-6
View File
@@ -1,11 +1,5 @@
# @actions/exec Releases # @actions/exec Releases
### 1.1.0
- [Fix stdline dropping large output](https://github.com/actions/toolkit/pull/773)
- [Add getExecOutput function](https://github.com/actions/toolkit/pull/814)
- [Better error for bad cwd](https://github.com/actions/toolkit/pull/793)
### 1.0.3 ### 1.0.3
- [Add \"types\" to package.json](https://github.com/actions/toolkit/pull/221) - [Add \"types\" to package.json](https://github.com/actions/toolkit/pull/221)
-200
View File
@@ -286,31 +286,6 @@ describe('@actions/exec', () => {
expect(stderrCalled).toBeTruthy() expect(stderrCalled).toBeTruthy()
}) })
it('Handles large stdline', async () => {
const stdlinePath: string = path.join(
__dirname,
'scripts',
'stdlineoutput.js'
)
const nodePath: string = await io.which('node', true)
const _testExecOptions = getExecOptions()
let largeLine = ''
_testExecOptions.listeners = {
stdline: (line: string) => {
largeLine = line
}
}
const exitCode = await exec.exec(
`"${nodePath}"`,
[stdlinePath],
_testExecOptions
)
expect(exitCode).toBe(0)
expect(Buffer.byteLength(largeLine)).toEqual(2 ** 16 + 1)
})
it('Handles stdin shell', async () => { it('Handles stdin shell', async () => {
let command: string let command: string
if (IS_WINDOWS) { if (IS_WINDOWS) {
@@ -563,22 +538,6 @@ describe('@actions/exec', () => {
expect(output.trim()).toBe(`args[0]: "hello"${os.EOL}args[1]: "world"`) expect(output.trim()).toBe(`args[0]: "hello"${os.EOL}args[1]: "world"`)
}) })
it('Exec roots throws friendly error when bad cwd is specified', async () => {
const execOptions = getExecOptions()
execOptions.cwd = 'nonexistent/path'
await expect(exec.exec('ls', ['-all'], execOptions)).rejects.toThrowError(
`The cwd: ${execOptions.cwd} does not exist!`
)
})
it('Exec roots does not throw when valid cwd is provided', async () => {
const execOptions = getExecOptions()
execOptions.cwd = './'
await expect(exec.exec('ls', ['-all'], execOptions)).resolves.toBe(0)
})
it('Exec roots relative tool path using rooted options.cwd', async () => { it('Exec roots relative tool path using rooted options.cwd', async () => {
let command: string let command: string
if (IS_WINDOWS) { if (IS_WINDOWS) {
@@ -661,165 +620,6 @@ describe('@actions/exec', () => {
expect(output.trim()).toBe(`args[0]: "hello"${os.EOL}args[1]: "world"`) expect(output.trim()).toBe(`args[0]: "hello"${os.EOL}args[1]: "world"`)
}) })
it('correctly outputs for getExecOutput', async () => {
const stdErrPath: string = path.join(
__dirname,
'scripts',
'stderroutput.js'
)
const stdOutPath: string = path.join(
__dirname,
'scripts',
'stdoutoutput.js'
)
const nodePath: string = await io.which('node', true)
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
`"${nodePath}"`,
[stdOutPath],
getExecOptions()
)
expect(exitCodeOut).toBe(0)
expect(stdout).toBe('this is output to stdout')
const {exitCode: exitCodeErr, stderr} = await exec.getExecOutput(
`"${nodePath}"`,
[stdErrPath],
getExecOptions()
)
expect(exitCodeErr).toBe(0)
expect(stderr).toBe('this is output to stderr')
})
it('correctly outputs for getExecOutput with additional listeners', async () => {
const stdErrPath: string = path.join(
__dirname,
'scripts',
'stderroutput.js'
)
const stdOutPath: string = path.join(
__dirname,
'scripts',
'stdoutoutput.js'
)
const nodePath: string = await io.which('node', true)
let listenerOut = ''
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
`"${nodePath}"`,
[stdOutPath],
{
...getExecOptions(),
listeners: {
stdout: data => {
listenerOut = data.toString()
}
}
}
)
expect(exitCodeOut).toBe(0)
expect(stdout).toBe('this is output to stdout')
expect(listenerOut).toBe('this is output to stdout')
let listenerErr = ''
const {exitCode: exitCodeErr, stderr} = await exec.getExecOutput(
`"${nodePath}"`,
[stdErrPath],
{
...getExecOptions(),
listeners: {
stderr: data => {
listenerErr = data.toString()
}
}
}
)
expect(exitCodeErr).toBe(0)
expect(stderr).toBe('this is output to stderr')
expect(listenerErr).toBe('this is output to stderr')
})
it('correctly outputs for getExecOutput when total size exceeds buffer size', async () => {
const stdErrPath: string = path.join(
__dirname,
'scripts',
'stderroutput.js'
)
const stdOutPath: string = path.join(
__dirname,
'scripts',
'stdoutoutputlarge.js'
)
const nodePath: string = await io.which('node', true)
let listenerOut = ''
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
`"${nodePath}"`,
[stdOutPath],
{
...getExecOptions(),
listeners: {
stdout: data => {
listenerOut += data.toString()
}
}
}
)
expect(exitCodeOut).toBe(0)
expect(Buffer.byteLength(stdout || '', 'utf8')).toBe(2 ** 25)
expect(Buffer.byteLength(listenerOut, 'utf8')).toBe(2 ** 25)
let listenerErr = ''
const {exitCode: exitCodeErr, stderr} = await exec.getExecOutput(
`"${nodePath}"`,
[stdErrPath],
{
...getExecOptions(),
listeners: {
stderr: data => {
listenerErr = data.toString()
}
}
}
)
expect(exitCodeErr).toBe(0)
expect(stderr).toBe('this is output to stderr')
expect(listenerErr).toBe('this is output to stderr')
})
it('correctly outputs for getExecOutput with multi-byte characters', async () => {
const stdOutPath: string = path.join(
__dirname,
'scripts',
'stdoutputspecial.js'
)
const nodePath: string = await io.which('node', true)
let numStdOutBufferCalls = 0
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
`"${nodePath}"`,
[stdOutPath],
{
...getExecOptions(),
listeners: {
stdout: () => {
numStdOutBufferCalls += 1
}
}
}
)
expect(exitCodeOut).toBe(0)
//one call for each half of the © character, ensuring it was actually split and not sent together
expect(numStdOutBufferCalls).toBe(2)
expect(stdout).toBe('©')
})
if (IS_WINDOWS) { if (IS_WINDOWS) {
it('Exec roots relative tool path using process.cwd (Windows path separator)', async () => { it('Exec roots relative tool path using process.cwd (Windows path separator)', async () => {
let exitCode: number let exitCode: number
@@ -1,5 +0,0 @@
//Default highWaterMark for readable stream buffers us 64K (2^16)
//so we go over that to get more than a buffer's worth
const os = require('os')
process.stdout.write('a'.repeat(2**16 + 1) + os.EOL);
@@ -1,3 +0,0 @@
//Default highWaterMark for readable stream buffers us 64K (2^16)
//so we go over that to get more than a buffer's worth
process.stdout.write('a\n'.repeat(2**24));
@@ -1,8 +0,0 @@
//first half of © character
process.stdout.write(Buffer.from([0xC2]), (err) => {
//write in the callback so that the second byte is sent separately
setTimeout(() => {
process.stdout.write(Buffer.from([0xA9])) //second half of © character
}, 5000)
})
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/exec", "name": "@actions/exec",
"version": "1.1.0", "version": "1.0.4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/exec", "name": "@actions/exec",
"version": "1.1.0", "version": "1.0.4",
"description": "Actions exec lib", "description": "Actions exec lib",
"keywords": [ "keywords": [
"github", "github",
+2 -62
View File
@@ -1,8 +1,7 @@
import {StringDecoder} from 'string_decoder' import {ExecOptions} from './interfaces'
import {ExecOptions, ExecOutput, ExecListeners} from './interfaces'
import * as tr from './toolrunner' import * as tr from './toolrunner'
export {ExecOptions, ExecOutput, ExecListeners} export {ExecOptions}
/** /**
* Exec a command. * Exec a command.
@@ -29,62 +28,3 @@ export async function exec(
const runner: tr.ToolRunner = new tr.ToolRunner(toolPath, args, options) const runner: tr.ToolRunner = new tr.ToolRunner(toolPath, args, options)
return runner.exec() return runner.exec()
} }
/**
* Exec a command and get the output.
* Output will be streamed to the live console.
* Returns promise with the exit code and collected stdout and stderr
*
* @param commandLine command to execute (can include additional args). Must be correctly escaped.
* @param args optional arguments for tool. Escaping is handled by the lib.
* @param options optional exec options. See ExecOptions
* @returns Promise<ExecOutput> exit code, stdout, and stderr
*/
export async function getExecOutput(
commandLine: string,
args?: string[],
options?: ExecOptions
): Promise<ExecOutput> {
let stdout = ''
let stderr = ''
//Using string decoder covers the case where a mult-byte character is split
const stdoutDecoder = new StringDecoder('utf8')
const stderrDecoder = new StringDecoder('utf8')
const originalStdoutListener = options?.listeners?.stdout
const originalStdErrListener = options?.listeners?.stderr
const stdErrListener = (data: Buffer): void => {
stderr += stderrDecoder.write(data)
if (originalStdErrListener) {
originalStdErrListener(data)
}
}
const stdOutListener = (data: Buffer): void => {
stdout += stdoutDecoder.write(data)
if (originalStdoutListener) {
originalStdoutListener(data)
}
}
const listeners: ExecListeners = {
...options?.listeners,
stdout: stdOutListener,
stderr: stdErrListener
}
const exitCode = await exec(commandLine, args, {...options, listeners})
//flush any remaining characters
stdout += stdoutDecoder.end()
stderr += stderrDecoder.end()
return {
exitCode,
stdout,
stderr
}
}
+7 -31
View File
@@ -34,39 +34,15 @@ export interface ExecOptions {
input?: Buffer input?: Buffer
/** optional. Listeners for output. Callback functions that will be called on these events */ /** optional. Listeners for output. Callback functions that will be called on these events */
listeners?: ExecListeners listeners?: {
} stdout?: (data: Buffer) => void
/** stderr?: (data: Buffer) => void
* Interface for the output of getExecOutput()
*/
export interface ExecOutput {
/**The exit code of the process */
exitCode: number
/**The entire stdout of the process as a string */ stdline?: (data: string) => void
stdout: string
/**The entire stderr of the process as a string */ errline?: (data: string) => void
stderr: string
}
/** debug?: (data: string) => void
* The user defined listeners for an exec call }
*/
export interface ExecListeners {
/** A call back for each buffer of stdout */
stdout?: (data: Buffer) => void
/** A call back for each buffer of stderr */
stderr?: (data: Buffer) => void
/** A call back for each line of stdout */
stdline?: (data: string) => void
/** A call back for each line of stderr */
errline?: (data: string) => void
/** A call back for each debug log */
debug?: (data: string) => void
} }
+19 -34
View File
@@ -6,7 +6,6 @@ import * as stream from 'stream'
import * as im from './interfaces' import * as im from './interfaces'
import * as io from '@actions/io' import * as io from '@actions/io'
import * as ioUtil from '@actions/io/lib/io-util' import * as ioUtil from '@actions/io/lib/io-util'
import {setTimeout} from 'timers'
/* eslint-disable @typescript-eslint/unbound-method */ /* eslint-disable @typescript-eslint/unbound-method */
@@ -84,7 +83,7 @@ export class ToolRunner extends events.EventEmitter {
data: Buffer, data: Buffer,
strBuffer: string, strBuffer: string,
onLine: (line: string) => void onLine: (line: string) => void
): string { ): void {
try { try {
let s = strBuffer + data.toString() let s = strBuffer + data.toString()
let n = s.indexOf(os.EOL) let n = s.indexOf(os.EOL)
@@ -98,12 +97,10 @@ export class ToolRunner extends events.EventEmitter {
n = s.indexOf(os.EOL) n = s.indexOf(os.EOL)
} }
return s strBuffer = s
} catch (err) { } catch (err) {
// streaming lines to console is best effort. Don't fail a build. // streaming lines to console is best effort. Don't fail a build.
this._debug(`error processing line. Failed with error ${err}`) this._debug(`error processing line. Failed with error ${err}`)
return ''
} }
} }
@@ -416,7 +413,7 @@ export class ToolRunner extends events.EventEmitter {
// otherwise verify it exists (add extension on Windows if necessary) // otherwise verify it exists (add extension on Windows if necessary)
this.toolPath = await io.which(this.toolPath, true) this.toolPath = await io.which(this.toolPath, true)
return new Promise<number>(async (resolve, reject) => { return new Promise<number>((resolve, reject) => {
this._debug(`exec tool: ${this.toolPath}`) this._debug(`exec tool: ${this.toolPath}`)
this._debug('arguments:') this._debug('arguments:')
for (const arg of this.args) { for (const arg of this.args) {
@@ -435,10 +432,6 @@ export class ToolRunner extends events.EventEmitter {
this._debug(message) this._debug(message)
}) })
if (this.options.cwd && !(await ioUtil.exists(this.options.cwd))) {
return reject(new Error(`The cwd: ${this.options.cwd} does not exist!`))
}
const fileName = this._getSpawnFileName() const fileName = this._getSpawnFileName()
const cp = child.spawn( const cp = child.spawn(
fileName, fileName,
@@ -446,7 +439,7 @@ export class ToolRunner extends events.EventEmitter {
this._getSpawnOptions(this.options, fileName) this._getSpawnOptions(this.options, fileName)
) )
let stdbuffer = '' const stdbuffer = ''
if (cp.stdout) { if (cp.stdout) {
cp.stdout.on('data', (data: Buffer) => { cp.stdout.on('data', (data: Buffer) => {
if (this.options.listeners && this.options.listeners.stdout) { if (this.options.listeners && this.options.listeners.stdout) {
@@ -457,19 +450,15 @@ export class ToolRunner extends events.EventEmitter {
optionsNonNull.outStream.write(data) optionsNonNull.outStream.write(data)
} }
stdbuffer = this._processLineBuffer( this._processLineBuffer(data, stdbuffer, (line: string) => {
data, if (this.options.listeners && this.options.listeners.stdline) {
stdbuffer, this.options.listeners.stdline(line)
(line: string) => {
if (this.options.listeners && this.options.listeners.stdline) {
this.options.listeners.stdline(line)
}
} }
) })
}) })
} }
let errbuffer = '' const errbuffer = ''
if (cp.stderr) { if (cp.stderr) {
cp.stderr.on('data', (data: Buffer) => { cp.stderr.on('data', (data: Buffer) => {
state.processStderr = true state.processStderr = true
@@ -488,15 +477,11 @@ export class ToolRunner extends events.EventEmitter {
s.write(data) s.write(data)
} }
errbuffer = this._processLineBuffer( this._processLineBuffer(data, errbuffer, (line: string) => {
data, if (this.options.listeners && this.options.listeners.errline) {
errbuffer, this.options.listeners.errline(line)
(line: string) => {
if (this.options.listeners && this.options.listeners.errline) {
this.options.listeners.errline(line)
}
} }
) })
}) })
} }
@@ -629,13 +614,13 @@ class ExecState extends events.EventEmitter {
} }
} }
processClosed = false // tracks whether the process has exited and stdio is closed processClosed: boolean = false // tracks whether the process has exited and stdio is closed
processError = '' processError: string = ''
processExitCode = 0 processExitCode: number = 0
processExited = false // tracks whether the process has exited processExited: boolean = false // tracks whether the process has exited
processStderr = false // tracks whether stderr was written to processStderr: boolean = false // tracks whether stderr was written to
private delay = 10000 // 10 seconds private delay = 10000 // 10 seconds
private done = false private done: boolean = false
private options: im.ExecOptions private options: im.ExecOptions
private timeout: NodeJS.Timer | null = null private timeout: NodeJS.Timer | null = null
private toolPath: string private toolPath: string
+8 -9
View File
@@ -22,7 +22,7 @@ async function run() {
// You can also pass in additional options as a second parameter to getOctokit // You can also pass in additional options as a second parameter to getOctokit
// const octokit = github.getOctokit(myToken, {userAgent: "MyActionVersion1"}); // const octokit = github.getOctokit(myToken, {userAgent: "MyActionVersion1"});
const { data: pullRequest } = await octokit.rest.pulls.get({ const { data: pullRequest } = await octokit.pulls.get({
owner: 'octokit', owner: 'octokit',
repo: 'rest.js', repo: 'rest.js',
pull_number: 123, pull_number: 123,
@@ -50,7 +50,7 @@ const github = require('@actions/github');
const context = github.context; const context = github.context;
const newIssue = await octokit.rest.issues.create({ const newIssue = await octokit.issues.create({
...context.repo, ...context.repo,
title: 'New issue!', title: 'New issue!',
body: 'Hello Universe!' body: 'Hello Universe!'
@@ -59,19 +59,18 @@ const newIssue = await octokit.rest.issues.create({
## Webhook payload typescript definitions ## Webhook payload typescript definitions
The npm module `@octokit/webhooks-definitions` provides type definitions for the response payloads. You can cast the payload to these types for better type information. The npm module `@octokit/webhooks` provides type definitions for the response payloads. You can cast the payload to these types for better type information.
First, install the npm module `npm install @octokit/webhooks-definitions` First, install the npm module `npm install @octokit/webhooks`
Then, assert the type based on the eventName Then, assert the type based on the eventName
```ts ```ts
import * as core from '@actions/core' import * as core from '@actions/core'
import * as github from '@actions/github' import * as github from '@actions/github'
import {PushEvent} from '@octokit/webhooks-definitions/schema' import * as Webhooks from '@octokit/webhooks'
if (github.context.eventName === 'push') { if (github.context.eventName === 'push') {
const pushPayload = github.context.payload as PushEvent const pushPayload = github.context.payload as Webhooks.WebhookPayloadPush
core.info(`The head commit is: ${pushPayload.head_commit}`) core.info(`The head commit is: ${pushPayload.head}`)
} }
``` ```
@@ -91,7 +90,7 @@ const octokit = GitHub.plugin(enterpriseServer220Admin)
const myToken = core.getInput('myToken'); const myToken = core.getInput('myToken');
const myOctokit = new octokit(getOctokitOptions(token)) const myOctokit = new octokit(getOctokitOptions(token))
// Create a new user // Create a new user
myOctokit.rest.enterpriseAdmin.createUser({ myOctokit.enterpriseAdmin.createUser({
login: "testuser", login: "testuser",
email: "[email protected]", email: "[email protected]",
}); });
-4
View File
@@ -1,9 +1,5 @@
# @actions/github Releases # @actions/github Releases
### 5.0.0
- [Update @actions/github to include latest octokit definitions](https://github.com/actions/toolkit/pull/783)
- [Add urls to context](https://github.com/actions/toolkit/pull/794)
### 4.0.0 ### 4.0.0
- [Add execution state information to context](https://github.com/actions/toolkit/pull/499) - [Add execution state information to context](https://github.com/actions/toolkit/pull/499)
- [Update Octokit Dependencies with some api breaking changes](https://github.com/actions/toolkit/pull/498) - [Update Octokit Dependencies with some api breaking changes](https://github.com/actions/toolkit/pull/498)
@@ -49,7 +49,7 @@ describe('@actions/github', () => {
} }
const octokit = getOctokit(token) const octokit = getOctokit(token)
const branch = await octokit.rest.repos.getBranch({ const branch = await octokit.repos.getBranch({
owner: 'actions', owner: 'actions',
repo: 'toolkit', repo: 'toolkit',
branch: 'main' branch: 'main'
@@ -85,7 +85,7 @@ describe('@actions/github', () => {
agent: new https.Agent() agent: new https.Agent()
} }
}) })
const branch = await octokit.rest.repos.getBranch({ const branch = await octokit.repos.getBranch({
owner: 'actions', owner: 'actions',
repo: 'toolkit', repo: 'toolkit',
branch: 'main' branch: 'main'
+6 -6
View File
@@ -15,7 +15,7 @@ describe('@actions/github', () => {
proxyServer = proxy() proxyServer = proxy()
await new Promise(resolve => { await new Promise(resolve => {
const port = Number(proxyUrl.split(':')[2]) const port = Number(proxyUrl.split(':')[2])
proxyServer.listen(port, () => resolve(null)) proxyServer.listen(port, () => resolve())
}) })
proxyServer.on('connect', req => { proxyServer.on('connect', req => {
proxyConnects.push(req.url) proxyConnects.push(req.url)
@@ -30,7 +30,7 @@ describe('@actions/github', () => {
afterAll(async () => { afterAll(async () => {
// Stop proxy server // Stop proxy server
await new Promise(resolve => { await new Promise(resolve => {
proxyServer.once('close', () => resolve(null)) proxyServer.once('close', () => resolve())
proxyServer.close() proxyServer.close()
}) })
@@ -45,7 +45,7 @@ describe('@actions/github', () => {
return return
} }
const octokit = new GitHub(getOctokitOptions(token)) const octokit = new GitHub(getOctokitOptions(token))
const branch = await octokit.rest.repos.getBranch({ const branch = await octokit.repos.getBranch({
owner: 'actions', owner: 'actions',
repo: 'toolkit', repo: 'toolkit',
branch: 'main' branch: 'main'
@@ -60,7 +60,7 @@ describe('@actions/github', () => {
return return
} }
const octokit = getOctokit(token) const octokit = getOctokit(token)
const branch = await octokit.rest.repos.getBranch({ const branch = await octokit.repos.getBranch({
owner: 'actions', owner: 'actions',
repo: 'toolkit', repo: 'toolkit',
branch: 'main' branch: 'main'
@@ -77,7 +77,7 @@ describe('@actions/github', () => {
// Valid token // Valid token
let octokit = new GitHub({auth: `token ${token}`}) let octokit = new GitHub({auth: `token ${token}`})
const branch = await octokit.rest.repos.getBranch({ const branch = await octokit.repos.getBranch({
owner: 'actions', owner: 'actions',
repo: 'toolkit', repo: 'toolkit',
branch: 'main' branch: 'main'
@@ -89,7 +89,7 @@ describe('@actions/github', () => {
octokit = new GitHub({auth: `token asdf`}) octokit = new GitHub({auth: `token asdf`})
let failed = false let failed = false
try { try {
await octokit.rest.repos.getBranch({ await octokit.repos.getBranch({
owner: 'actions', owner: 'actions',
repo: 'toolkit', repo: 'toolkit',
branch: 'main' branch: 'main'
-1
View File
@@ -2,7 +2,6 @@ import * as path from 'path'
import {Context} from '../src/context' import {Context} from '../src/context'
/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable @typescript-eslint/no-var-requires */
describe('@actions/context', () => { describe('@actions/context', () => {
let context: Context let context: Context
+1237 -1463
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/github", "name": "@actions/github",
"version": "5.0.0", "version": "4.0.0",
"description": "Actions github lib", "description": "Actions github lib",
"keywords": [ "keywords": [
"github", "github",
@@ -39,12 +39,12 @@
}, },
"dependencies": { "dependencies": {
"@actions/http-client": "^1.0.11", "@actions/http-client": "^1.0.11",
"@octokit/core": "^3.4.0", "@octokit/core": "^3.3.1",
"@octokit/plugin-paginate-rest": "^2.13.3", "@octokit/plugin-paginate-rest": "^2.13.3",
"@octokit/plugin-rest-endpoint-methods": "^5.1.1" "@octokit/plugin-rest-endpoint-methods": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"jest": "^26.6.3", "jest": "^25.1.0",
"proxy": "^1.0.2" "proxy": "^1.0.1"
} }
} }
-7
View File
@@ -18,9 +18,6 @@ export class Context {
job: string job: string
runNumber: number runNumber: number
runId: number runId: number
apiUrl: string
serverUrl: string
graphqlUrl: string
/** /**
* Hydrate the context from the environment * Hydrate the context from the environment
@@ -46,10 +43,6 @@ export class Context {
this.job = process.env.GITHUB_JOB as string this.job = process.env.GITHUB_JOB as string
this.runNumber = parseInt(process.env.GITHUB_RUN_NUMBER as string, 10) this.runNumber = parseInt(process.env.GITHUB_RUN_NUMBER as string, 10)
this.runId = parseInt(process.env.GITHUB_RUN_ID as string, 10) this.runId = parseInt(process.env.GITHUB_RUN_ID as string, 10)
this.apiUrl = process.env.GITHUB_API_URL ?? `https://api.github.com`
this.serverUrl = process.env.GITHUB_SERVER_URL ?? `https://github.com`
this.graphqlUrl =
process.env.GITHUB_GRAPHQL_URL ?? `https://api.github.com/graphql`
} }
get issue(): {owner: string; repo: string; number: number} { get issue(): {owner: string; repo: string; number: number} {
+3 -11
View File
@@ -1,17 +1,9 @@
# @actions/glob Releases # @actions/glob Releases
### 0.2.0
- [Added the hashFiles function to Glob](https://github.com/actions/toolkit/pull/830)
- [Added an option to filter out directories](https://github.com/actions/toolkit/pull/728)
### 0.1.2
- [Fix bug where files were matched incorrectly](https://github.com/actions/toolkit/pull/805)
### 0.1.1
- Update @actions/core version
### 0.1.0 ### 0.1.0
- Initial release - Initial release
### 0.1.1
- Update @actions/core version
-126
View File
@@ -1,126 +0,0 @@
import * as io from '../../io/src/io'
import * as path from 'path'
import {hashFiles} from '../src/glob'
import {promises as fs} from 'fs'
const IS_WINDOWS = process.platform === 'win32'
/**
* These test focus on the ability of globber to find files
* and not on the pattern matching aspect
*/
describe('globber', () => {
beforeAll(async () => {
await io.rmRF(getTestTemp())
})
it('basic hashfiles test', async () => {
const root = path.join(getTestTemp(), 'basic-hashfiles')
await fs.mkdir(path.join(root), {recursive: true})
await fs.writeFile(path.join(root, 'test.txt'), 'test file content')
const hash = await hashFiles(`${root}/*`)
expect(hash).toEqual(
'd8a411e8f8643821bed189e627ff57151918aa554c00c10b31c693ab2dded273'
)
})
it('basic hashfiles no match should return empty string', async () => {
const root = path.join(getTestTemp(), 'empty-hashfiles')
const hash = await hashFiles(`${root}/*`)
expect(hash).toEqual('')
})
it('followSymbolicLinks defaults to true', async () => {
const root = path.join(
getTestTemp(),
'defaults-to-follow-symbolic-links-true'
)
await fs.mkdir(path.join(root, 'realdir'), {recursive: true})
await fs.writeFile(
path.join(root, 'realdir', 'file.txt'),
'test file content'
)
await createSymlinkDir(
path.join(root, 'realdir'),
path.join(root, 'symDir')
)
const testPath = path.join(root, `symDir`)
const hash = await hashFiles(testPath)
expect(hash).toEqual(
'd8a411e8f8643821bed189e627ff57151918aa554c00c10b31c693ab2dded273'
)
})
it('followSymbolicLinks set to true', async () => {
const root = path.join(getTestTemp(), 'set-to-true')
await fs.mkdir(path.join(root, 'realdir'), {recursive: true})
await fs.writeFile(path.join(root, 'realdir', 'file'), 'test file content')
await createSymlinkDir(
path.join(root, 'realdir'),
path.join(root, 'symDir')
)
const testPath = path.join(root, `symDir`)
const hash = await hashFiles(testPath, {followSymbolicLinks: true})
expect(hash).toEqual(
'd8a411e8f8643821bed189e627ff57151918aa554c00c10b31c693ab2dded273'
)
})
it('followSymbolicLinks set to false', async () => {
// Create the following layout:
// <root>
// <root>/folder-a
// <root>/folder-a/file
// <root>/symDir -> <root>/folder-a
const root = path.join(getTestTemp(), 'set-to-false')
await fs.mkdir(path.join(root, 'realdir'), {recursive: true})
await fs.writeFile(path.join(root, 'realdir', 'file'), 'test file content')
await createSymlinkDir(
path.join(root, 'realdir'),
path.join(root, 'symDir')
)
const testPath = path.join(root, 'symdir')
const hash = await hashFiles(testPath, {followSymbolicLinks: false})
expect(hash).toEqual('')
})
it('multipath test basic', async () => {
// Create the following layout:
// <root>
// <root>/folder-a
// <root>/folder-a/file
// <root>/symDir -> <root>/folder-a
const root = path.join(getTestTemp(), 'set-to-false')
await fs.mkdir(path.join(root, 'dir1'), {recursive: true})
await fs.mkdir(path.join(root, 'dir2'), {recursive: true})
await fs.writeFile(
path.join(root, 'dir1', 'testfile1.txt'),
'test file content'
)
await fs.writeFile(
path.join(root, 'dir2', 'testfile2.txt'),
'test file content'
)
const testPath = `${path.join(root, 'dir1')}\n${path.join(root, 'dir2')}`
const hash = await hashFiles(testPath)
expect(hash).toEqual(
'4e911ea5824830b6a3ec096c7833d5af8381c189ffaa825c3503a5333a73eadc'
)
})
})
function getTestTemp(): string {
return path.join(__dirname, '_temp', 'hash_files')
}
/**
* Creates a symlink directory on OSX/Linux, and a junction point directory on Windows.
* A symlink directory is not created on Windows since it requires an elevated context.
*/
async function createSymlinkDir(real: string, link: string): Promise<void> {
if (IS_WINDOWS) {
await fs.symlink(real, link, 'junction')
} else {
await fs.symlink(real, link)
}
}
@@ -97,41 +97,6 @@ describe('globber', () => {
]) ])
}) })
it('defaults to matchDirectories=true', async () => {
// Create the following layout:
// <root>
// <root>/folder-a
// <root>/folder-a/file
const root = path.join(getTestTemp(), 'defaults-to-match-directories-true')
await fs.mkdir(path.join(root, 'folder-a'), {recursive: true})
await fs.writeFile(path.join(root, 'folder-a', 'file'), 'test file content')
const itemPaths = await glob(root, {})
expect(itemPaths).toEqual([
root,
path.join(root, 'folder-a'),
path.join(root, 'folder-a', 'file')
])
})
it('does not match file with trailing slash when implicitDescendants=true', async () => {
// Create the following layout:
// <root>
// <root>/file
const root = path.join(
getTestTemp(),
'defaults-to-implicit-descendants-true'
)
const filePath = path.join(root, 'file')
await fs.mkdir(root, {recursive: true})
await fs.writeFile(filePath, 'test file content')
const itemPaths = await glob(`${filePath}/`, {})
expect(itemPaths).toEqual([])
})
it('defaults to omitBrokenSymbolicLinks=true', async () => { it('defaults to omitBrokenSymbolicLinks=true', async () => {
// Create the following layout: // Create the following layout:
// <root> // <root>
@@ -378,34 +343,6 @@ describe('globber', () => {
expect(itemPaths).toEqual([]) expect(itemPaths).toEqual([])
}) })
it('does not return directories when match directories false', async () => {
// Create the following layout:
// <root>/file-1
// <root>/dir-1
// <root>/dir-1/file-2
// <root>/dir-1/dir-2
// <root>/dir-1/dir-2/file-3
const root = path.join(
getTestTemp(),
'does-not-return-directories-when-match-directories-false'
)
await fs.mkdir(path.join(root, 'dir-1', 'dir-2'), {recursive: true})
await fs.writeFile(path.join(root, 'file-1'), '')
await fs.writeFile(path.join(root, 'dir-1', 'file-2'), '')
await fs.writeFile(path.join(root, 'dir-1', 'dir-2', 'file-3'), '')
const pattern = `${root}${path.sep}**`
expect(
await glob(pattern, {
matchDirectories: false
})
).toEqual([
path.join(root, 'dir-1', 'dir-2', 'file-3'),
path.join(root, 'dir-1', 'file-2'),
path.join(root, 'file-1')
])
})
it('does not search paths that are not partial matches', async () => { it('does not search paths that are not partial matches', async () => {
// Create the following layout: // Create the following layout:
// <root> // <root>
@@ -26,7 +26,7 @@ describe('pattern', () => {
it('escapes homedir', async () => { it('escapes homedir', async () => {
const home = path.join(getTestTemp(), 'home-with-[and]') const home = path.join(getTestTemp(), 'home-with-[and]')
await fs.mkdir(home, {recursive: true}) await fs.mkdir(home, {recursive: true})
const pattern = new Pattern('~/m*', false, undefined, home) const pattern = new Pattern('~/m*', undefined, home)
expect(pattern.searchPath).toBe(home) expect(pattern.searchPath).toBe(home)
expect(pattern.match(path.join(home, 'match'))).toBeTruthy() expect(pattern.match(path.join(home, 'match'))).toBeTruthy()
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/glob", "name": "@actions/glob",
"version": "0.2.0", "version": "0.1.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/glob", "name": "@actions/glob",
"version": "0.2.0", "version": "0.1.1",
"preview": true, "preview": true,
"description": "Actions glob lib", "description": "Actions glob lib",
"keywords": [ "keywords": [
-20
View File
@@ -1,7 +1,5 @@
import {Globber, DefaultGlobber} from './internal-globber' import {Globber, DefaultGlobber} from './internal-globber'
import {GlobOptions} from './internal-glob-options' import {GlobOptions} from './internal-glob-options'
import {HashFileOptions} from './internal-hash-file-options'
import {hashFiles as _hashFiles} from './internal-hash-files'
export {Globber, GlobOptions} export {Globber, GlobOptions}
@@ -17,21 +15,3 @@ export async function create(
): Promise<Globber> { ): Promise<Globber> {
return await DefaultGlobber.create(patterns, options) return await DefaultGlobber.create(patterns, options)
} }
/**
* Computes the sha256 hash of a glob
*
* @param patterns Patterns separated by newlines
* @param options Glob options
*/
export async function hashFiles(
patterns: string,
options?: HashFileOptions
): Promise<string> {
let followSymbolicLinks = true
if (options && typeof options.followSymbolicLinks === 'boolean') {
followSymbolicLinks = options.followSymbolicLinks
}
const globber = await create(patterns, {followSymbolicLinks})
return _hashFiles(globber)
}
@@ -8,7 +8,6 @@ export function getOptions(copy?: GlobOptions): GlobOptions {
const result: GlobOptions = { const result: GlobOptions = {
followSymbolicLinks: true, followSymbolicLinks: true,
implicitDescendants: true, implicitDescendants: true,
matchDirectories: true,
omitBrokenSymbolicLinks: true omitBrokenSymbolicLinks: true
} }
@@ -23,11 +22,6 @@ export function getOptions(copy?: GlobOptions): GlobOptions {
core.debug(`implicitDescendants '${result.implicitDescendants}'`) core.debug(`implicitDescendants '${result.implicitDescendants}'`)
} }
if (typeof copy.matchDirectories === 'boolean') {
result.matchDirectories = copy.matchDirectories
core.debug(`matchDirectories '${result.matchDirectories}'`)
}
if (typeof copy.omitBrokenSymbolicLinks === 'boolean') { if (typeof copy.omitBrokenSymbolicLinks === 'boolean') {
result.omitBrokenSymbolicLinks = copy.omitBrokenSymbolicLinks result.omitBrokenSymbolicLinks = copy.omitBrokenSymbolicLinks
core.debug(`omitBrokenSymbolicLinks '${result.omitBrokenSymbolicLinks}'`) core.debug(`omitBrokenSymbolicLinks '${result.omitBrokenSymbolicLinks}'`)
@@ -21,14 +21,6 @@ export interface GlobOptions {
*/ */
implicitDescendants?: boolean implicitDescendants?: boolean
/**
* Indicates whether matching directories should be included in the
* result set.
*
* @default true
*/
matchDirectories?: boolean
/** /**
* Indicates whether broken symbolic should be ignored and omitted from the * Indicates whether broken symbolic should be ignored and omitted from the
* result set. Otherwise an error will be thrown. * result set. Otherwise an error will be thrown.
+3 -4
View File
@@ -66,6 +66,7 @@ export class DefaultGlobber implements Globber {
async *globGenerator(): AsyncGenerator<string, void> { async *globGenerator(): AsyncGenerator<string, void> {
// Fill in defaults options // Fill in defaults options
const options = globOptionsHelper.getOptions(this.options) const options = globOptionsHelper.getOptions(this.options)
// Implicit descendants? // Implicit descendants?
const patterns: Pattern[] = [] const patterns: Pattern[] = []
for (const pattern of this.patterns) { for (const pattern of this.patterns) {
@@ -76,13 +77,12 @@ export class DefaultGlobber implements Globber {
pattern.segments[pattern.segments.length - 1] !== '**') pattern.segments[pattern.segments.length - 1] !== '**')
) { ) {
patterns.push( patterns.push(
new Pattern(pattern.negate, true, pattern.segments.concat('**')) new Pattern(pattern.negate, pattern.segments.concat('**'))
) )
} }
} }
// Push the search paths // Push the search paths
const stack: SearchState[] = [] const stack: SearchState[] = []
for (const searchPath of patternHelper.getSearchPaths(patterns)) { for (const searchPath of patternHelper.getSearchPaths(patterns)) {
core.debug(`Search path '${searchPath}'`) core.debug(`Search path '${searchPath}'`)
@@ -131,7 +131,7 @@ export class DefaultGlobber implements Globber {
// Directory // Directory
if (stats.isDirectory()) { if (stats.isDirectory()) {
// Matched // Matched
if (match & MatchKind.Directory && options.matchDirectories) { if (match & MatchKind.Directory) {
yield item.path yield item.path
} }
// Descend? // Descend?
@@ -180,7 +180,6 @@ export class DefaultGlobber implements Globber {
} }
result.searchPaths.push(...patternHelper.getSearchPaths(result.patterns)) result.searchPaths.push(...patternHelper.getSearchPaths(result.patterns))
return result return result
} }
@@ -1,12 +0,0 @@
/**
* Options to control globbing behavior
*/
export interface HashFileOptions {
/**
* Indicates whether to follow symbolic links. Generally should set to false
* when deleting files.
*
* @default true
*/
followSymbolicLinks?: boolean
}
-42
View File
@@ -1,42 +0,0 @@
import * as crypto from 'crypto'
import * as core from '@actions/core'
import * as fs from 'fs'
import * as stream from 'stream'
import * as util from 'util'
import * as path from 'path'
import {Globber} from './glob'
export async function hashFiles(globber: Globber): Promise<string> {
let hasMatch = false
const githubWorkspace = process.env['GITHUB_WORKSPACE'] ?? process.cwd()
const result = crypto.createHash('sha256')
let count = 0
for await (const file of globber.globGenerator()) {
core.debug(file)
if (!file.startsWith(`${githubWorkspace}${path.sep}`)) {
core.debug(`Ignore '${file}' since it is not under GITHUB_WORKSPACE.`)
continue
}
if (fs.statSync(file).isDirectory()) {
core.debug(`Skip directory '${file}'.`)
continue
}
const hash = crypto.createHash('sha256')
const pipeline = util.promisify(stream.pipeline)
await pipeline(fs.createReadStream(file), hash)
result.write(hash.digest())
count++
if (!hasMatch) {
hasMatch = true
}
}
result.end()
if (hasMatch) {
core.debug(`Found ${count} files to hash.`)
return result.digest('hex')
} else {
core.debug(`No matches found for glob`)
return ''
}
}
+6 -20
View File
@@ -43,27 +43,15 @@ export class Pattern {
*/ */
private readonly rootRegExp: RegExp private readonly rootRegExp: RegExp
/** /* eslint-disable no-dupe-class-members */
* Indicates that the pattern is implicitly added as opposed to user specified. // Disable no-dupe-class-members due to false positive for method overload
*/ // https://github.com/typescript-eslint/typescript-eslint/issues/291
private readonly isImplicitPattern: boolean
constructor(pattern: string) constructor(pattern: string)
constructor( constructor(pattern: string, segments: undefined, homedir: string)
pattern: string, constructor(negate: boolean, segments: string[])
isImplicitPattern: boolean,
segments: undefined,
homedir: string
)
constructor(
negate: boolean,
isImplicitPattern: boolean,
segments: string[],
homedir?: string
)
constructor( constructor(
patternOrNegate: string | boolean, patternOrNegate: string | boolean,
isImplicitPattern = false,
segments?: string[], segments?: string[],
homedir?: string homedir?: string
) { ) {
@@ -119,8 +107,6 @@ export class Pattern {
IS_WINDOWS ? 'i' : '' IS_WINDOWS ? 'i' : ''
) )
this.isImplicitPattern = isImplicitPattern
// Create minimatch // Create minimatch
const minimatchOptions: IMinimatchOptions = { const minimatchOptions: IMinimatchOptions = {
dot: true, dot: true,
@@ -146,7 +132,7 @@ export class Pattern {
// Append a trailing slash. Otherwise Minimatch will not match the directory immediately // Append a trailing slash. Otherwise Minimatch will not match the directory immediately
// preceding the globstar. For example, given the pattern `/foo/**`, Minimatch returns // preceding the globstar. For example, given the pattern `/foo/**`, Minimatch returns
// false for `/foo` but returns true for `/foo/`. Append a trailing slash to handle that quirk. // false for `/foo` but returns true for `/foo/`. Append a trailing slash to handle that quirk.
if (!itemPath.endsWith(path.sep) && this.isImplicitPattern === false) { if (!itemPath.endsWith(path.sep)) {
// Note, this is safe because the constructor ensures the pattern has an absolute root. // Note, this is safe because the constructor ensures the pattern has an absolute root.
// For example, formats like C: and C:foo on Windows are resolved to an absolute root. // For example, formats like C: and C:foo on Windows are resolved to an absolute root.
itemPath = `${itemPath}${path.sep}` itemPath = `${itemPath}${path.sep}`
-7
View File
@@ -1,12 +1,5 @@
# @actions/io Releases # @actions/io Releases
### 1.1.1
- [Fixed a bug where we incorrectly escaped paths for rmrf](https://github.com/actions/toolkit/pull/828)
### 1.1.0
- Add `findInPath` method to locate all matching executables in the system path
### 1.0.2 ### 1.0.2
- [Add \"types\" to package.json](https://github.com/actions/toolkit/pull/221) - [Add \"types\" to package.json](https://github.com/actions/toolkit/pull/221)
+26 -125
View File
@@ -3,12 +3,9 @@ import {promises 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 io from '../src/io' import * as io from '../src/io'
import * as ioUtil from '../src/io-util'
describe('cp', () => { describe('cp', () => {
beforeAll(async () => {
await io.rmRF(getTestTemp())
})
it('copies file with no flags', async () => { it('copies file with no flags', async () => {
const root = path.join(getTestTemp(), 'cp_with_no_flags') const root = path.join(getTestTemp(), 'cp_with_no_flags')
const sourceFile = path.join(root, 'cp_source') const sourceFile = path.join(root, 'cp_source')
@@ -90,29 +87,6 @@ describe('cp', () => {
) )
}) })
it('copies directory into existing destination with -r without copying source directory', async () => {
const root: string = path.join(
getTestTemp(),
'cp_with_-r_existing_dest_no_source_dir'
)
const sourceFolder: string = path.join(root, 'cp_source')
const sourceFile: string = path.join(sourceFolder, 'cp_source_file')
const targetFolder: string = path.join(root, 'cp_target')
const targetFile: string = path.join(targetFolder, 'cp_source_file')
await io.mkdirP(sourceFolder)
await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'})
await io.mkdirP(targetFolder)
await io.cp(sourceFolder, targetFolder, {
recursive: true,
copySourceDirectory: false
})
expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe(
'test file content'
)
})
it('copies directory into non-existing destination with -r', async () => { it('copies directory into non-existing destination with -r', async () => {
const root: string = path.join(getTestTemp(), 'cp_with_-r_nonexistent_dest') const root: string = path.join(getTestTemp(), 'cp_with_-r_nonexistent_dest')
const sourceFolder: string = path.join(root, 'cp_source') const sourceFolder: string = path.join(root, 'cp_source')
@@ -192,10 +166,6 @@ describe('cp', () => {
}) })
describe('mv', () => { describe('mv', () => {
beforeAll(async () => {
await io.rmRF(getTestTemp())
})
it('moves file with no flags', async () => { it('moves file with no flags', async () => {
const root = path.join(getTestTemp(), ' mv_with_no_flags') const root = path.join(getTestTemp(), ' mv_with_no_flags')
const sourceFile = path.join(root, ' mv_source') const sourceFile = path.join(root, ' mv_source')
@@ -294,10 +264,6 @@ describe('mv', () => {
}) })
describe('rmRF', () => { describe('rmRF', () => {
beforeAll(async () => {
await io.rmRF(getTestTemp())
})
it('removes single folder with rmRF', async () => { it('removes single folder with rmRF', async () => {
const testPath = path.join(getTestTemp(), 'testFolder') const testPath = path.join(getTestTemp(), 'testFolder')
@@ -556,45 +522,6 @@ describe('rmRF', () => {
await assertNotExists(symlinkFile) await assertNotExists(symlinkFile)
await assertNotExists(outerDirectory) await assertNotExists(outerDirectory)
}) })
} else {
it('correctly escapes % on windows', async () => {
const root: string = path.join(getTestTemp(), 'rmRF_escape_test_win')
const directory: string = path.join(root, '%test%')
await io.mkdirP(root)
await io.mkdirP(directory)
const oldEnv = process.env['test']
process.env['test'] = 'thisshouldnotresolve'
await io.rmRF(directory)
await assertNotExists(directory)
process.env['test'] = oldEnv
})
it('Should throw for invalid characters', async () => {
const root: string = path.join(getTestTemp(), 'rmRF_invalidChar_Windows')
const errorString =
'File path must not contain `*`, `"`, `<`, `>` or `|` on Windows'
await expect(io.rmRF(path.join(root, '"'))).rejects.toHaveProperty(
'message',
errorString
)
await expect(io.rmRF(path.join(root, '<'))).rejects.toHaveProperty(
'message',
errorString
)
await expect(io.rmRF(path.join(root, '>'))).rejects.toHaveProperty(
'message',
errorString
)
await expect(io.rmRF(path.join(root, '|'))).rejects.toHaveProperty(
'message',
errorString
)
await expect(io.rmRF(path.join(root, '*'))).rejects.toHaveProperty(
'message',
errorString
)
})
} }
it('removes symlink folder with missing source using rmRF', async () => { it('removes symlink folder with missing source using rmRF', async () => {
@@ -886,13 +813,34 @@ describe('mkdirP', () => {
(await fs.lstat(path.join(realDirPath, 'sub_dir'))).isDirectory() (await fs.lstat(path.join(realDirPath, 'sub_dir'))).isDirectory()
).toBe(true) ).toBe(true)
}) })
it('breaks if mkdirP loop out of control', async () => {
const testPath = path.join(
getTestTemp(),
'mkdirP_failsafe',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'10'
)
expect.assertions(1)
try {
await ioUtil.mkdirP(testPath, 10)
} catch (err) {
expect(err.code).toBe('ENOENT')
}
})
}) })
describe('which', () => { describe('which', () => {
beforeAll(async () => {
await io.rmRF(getTestTemp())
})
it('which() finds file name', async () => { it('which() finds file name', async () => {
// create a executable file // create a executable file
const testPath = path.join(getTestTemp(), 'which-finds-file-name') const testPath = path.join(getTestTemp(), 'which-finds-file-name')
@@ -1425,53 +1373,6 @@ describe('which', () => {
} }
}) })
describe('findInPath', () => {
beforeAll(async () => {
await io.rmRF(getTestTemp())
})
it('findInPath() not found', async () => {
expect(await io.findInPath('findInPath-test-no-such-file')).toEqual([])
})
it('findInPath() finds file names', async () => {
// create executable files
let fileName = 'FindInPath-Test-File'
if (process.platform === 'win32') {
fileName += '.exe'
}
const testPaths = ['1', '2', '3'].map(count =>
path.join(getTestTemp(), `findInPath-finds-file-names-${count}`)
)
for (const testPath of testPaths) {
await io.mkdirP(testPath)
}
const filePaths = testPaths.map(testPath => path.join(testPath, fileName))
for (const filePath of filePaths) {
await fs.writeFile(filePath, '')
if (process.platform !== 'win32') {
chmod(filePath, '+x')
}
}
const originalPath = process.env['PATH']
try {
// update the PATH
for (const testPath of testPaths) {
process.env[
'PATH'
] = `${process.env['PATH']}${path.delimiter}${testPath}`
}
// exact file names
expect(await io.findInPath(fileName)).toEqual(filePaths)
} finally {
process.env['PATH'] = originalPath
}
})
})
async function findsExecutableWithScopedPermissions( async function findsExecutableWithScopedPermissions(
chmodOptions: string chmodOptions: string
): Promise<void> { ): Promise<void> {
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "@actions/io", "name": "@actions/io",
"version": "1.1.1", "version": "1.0.2",
"lockfileVersion": 1 "lockfileVersion": 1
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/io", "name": "@actions/io",
"version": "1.1.1", "version": "1.0.2",
"description": "Actions io lib", "description": "Actions io lib",
"keywords": [ "keywords": [
"github", "github",
+48 -6
View File
@@ -1,3 +1,4 @@
import {ok} from 'assert'
import * as fs from 'fs' import * as fs from 'fs'
import * as path from 'path' import * as path from 'path'
@@ -33,7 +34,7 @@ export async function exists(fsPath: string): Promise<boolean> {
export async function isDirectory( export async function isDirectory(
fsPath: string, fsPath: string,
useStat = false useStat: boolean = false
): Promise<boolean> { ): Promise<boolean> {
const stats = useStat ? await stat(fsPath) : await lstat(fsPath) const stats = useStat ? await stat(fsPath) : await lstat(fsPath)
return stats.isDirectory() return stats.isDirectory()
@@ -58,6 +59,52 @@ export function isRooted(p: string): boolean {
return p.startsWith('/') return p.startsWith('/')
} }
/**
* Recursively create a directory at `fsPath`.
*
* This implementation is optimistic, meaning it attempts to create the full
* path first, and backs up the path stack from there.
*
* @param fsPath The path to create
* @param maxDepth The maximum recursion depth
* @param depth The current recursion depth
*/
export async function mkdirP(
fsPath: string,
maxDepth: number = 1000,
depth: number = 1
): Promise<void> {
ok(fsPath, 'a path argument must be provided')
fsPath = path.resolve(fsPath)
if (depth >= maxDepth) return mkdir(fsPath)
try {
await mkdir(fsPath)
return
} catch (err) {
switch (err.code) {
case 'ENOENT': {
await mkdirP(path.dirname(fsPath), maxDepth, depth + 1)
await mkdir(fsPath)
return
}
default: {
let stats: fs.Stats
try {
stats = await stat(fsPath)
} catch (err2) {
throw err
}
if (!stats.isDirectory()) throw err
}
}
}
}
/** /**
* Best effort attempt to determine whether a file exists and is executable. * Best effort attempt to determine whether a file exists and is executable.
* @param filePath file path to check * @param filePath file path to check
@@ -166,8 +213,3 @@ function isUnixExecutable(stats: fs.Stats): boolean {
((stats.mode & 64) > 0 && stats.uid === process.getuid()) ((stats.mode & 64) > 0 && stats.uid === process.getuid())
) )
} }
// Get the path of cmd.exe in windows
export function getCmdPath(): string {
return process.env['COMSPEC'] ?? `cmd.exe`
}
+55 -93
View File
@@ -1,11 +1,9 @@
import {ok} from 'assert'
import * as childProcess from 'child_process' import * as childProcess from 'child_process'
import * as path from 'path' import * as path from 'path'
import {promisify} from 'util' import {promisify} from 'util'
import * as ioUtil from './io-util' import * as ioUtil from './io-util'
const exec = promisify(childProcess.exec) const exec = promisify(childProcess.exec)
const execFile = promisify(childProcess.execFile)
/** /**
* Interface for cp/mv options * Interface for cp/mv options
@@ -15,8 +13,6 @@ export interface CopyOptions {
recursive?: boolean recursive?: boolean
/** Optional. Whether to overwrite existing files in the destination. Defaults to true */ /** Optional. Whether to overwrite existing files in the destination. Defaults to true */
force?: boolean force?: boolean
/** Optional. Whether to copy the source directory along with all the files. Only takes effect when recursive=true and copying a directory. Default is true*/
copySourceDirectory?: boolean
} }
/** /**
@@ -40,7 +36,7 @@ export async function cp(
dest: string, dest: string,
options: CopyOptions = {} options: CopyOptions = {}
): Promise<void> { ): Promise<void> {
const {force, recursive, copySourceDirectory} = readCopyOptions(options) const {force, recursive} = readCopyOptions(options)
const destStat = (await ioUtil.exists(dest)) ? await ioUtil.stat(dest) : null const destStat = (await ioUtil.exists(dest)) ? await ioUtil.stat(dest) : null
// Dest is an existing file, but not forcing // Dest is an existing file, but not forcing
@@ -50,7 +46,7 @@ export async function cp(
// If dest is an existing directory, should copy inside. // If dest is an existing directory, should copy inside.
const newDest: string = const newDest: string =
destStat && destStat.isDirectory() && copySourceDirectory destStat && destStat.isDirectory()
? path.join(dest, path.basename(source)) ? path.join(dest, path.basename(source))
: dest : dest
@@ -118,24 +114,11 @@ export async function rmRF(inputPath: string): Promise<void> {
if (ioUtil.IS_WINDOWS) { if (ioUtil.IS_WINDOWS) {
// Node doesn't provide a delete operation, only an unlink function. This means that if the file is being used by another // Node doesn't provide a delete operation, only an unlink function. This means that if the file is being used by another
// program (e.g. antivirus), it won't be deleted. To address this, we shell out the work to rd/del. // program (e.g. antivirus), it won't be deleted. To address this, we shell out the work to rd/del.
// Check for invalid characters
// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
if (/[*"<>|]/.test(inputPath)) {
throw new Error(
'File path must not contain `*`, `"`, `<`, `>` or `|` on Windows'
)
}
try { try {
const cmdPath = ioUtil.getCmdPath()
if (await ioUtil.isDirectory(inputPath, true)) { if (await ioUtil.isDirectory(inputPath, true)) {
await exec(`${cmdPath} /s /c "rd /s /q "%inputPath%""`, { await exec(`rd /s /q "${inputPath}"`)
env: {inputPath}
})
} else { } else {
await exec(`${cmdPath} /s /c "del /f /a "%inputPath%""`, { await exec(`del /f /a "${inputPath}"`)
env: {inputPath}
})
} }
} catch (err) { } catch (err) {
// if you try to delete a file that doesn't exist, desired result is achieved // if you try to delete a file that doesn't exist, desired result is achieved
@@ -163,7 +146,7 @@ export async function rmRF(inputPath: string): Promise<void> {
} }
if (isDir) { if (isDir) {
await execFile(`rm`, [`-rf`, `${inputPath}`]) await exec(`rm -rf "${inputPath}"`)
} else { } else {
await ioUtil.unlink(inputPath) await ioUtil.unlink(inputPath)
} }
@@ -178,8 +161,7 @@ export async function rmRF(inputPath: string): Promise<void> {
* @returns Promise<void> * @returns Promise<void>
*/ */
export async function mkdirP(fsPath: string): Promise<void> { export async function mkdirP(fsPath: string): Promise<void> {
ok(fsPath, 'a path argument must be provided') await ioUtil.mkdirP(fsPath)
await ioUtil.mkdir(fsPath, {recursive: true})
} }
/** /**
@@ -210,95 +192,75 @@ export async function which(tool: string, check?: boolean): Promise<string> {
) )
} }
} }
return result
} }
const matches: string[] = await findInPath(tool) try {
// build the list of extensions to try
if (matches && matches.length > 0) { const extensions: string[] = []
return matches[0] if (ioUtil.IS_WINDOWS && process.env.PATHEXT) {
} for (const extension of process.env.PATHEXT.split(path.delimiter)) {
if (extension) {
return '' extensions.push(extension)
} }
/**
* Returns a list of all occurrences of the given tool on the system path.
*
* @returns Promise<string[]> the paths of the tool
*/
export async function findInPath(tool: string): Promise<string[]> {
if (!tool) {
throw new Error("parameter 'tool' is required")
}
// build the list of extensions to try
const extensions: string[] = []
if (ioUtil.IS_WINDOWS && process.env['PATHEXT']) {
for (const extension of process.env['PATHEXT'].split(path.delimiter)) {
if (extension) {
extensions.push(extension)
} }
} }
}
// if it's rooted, return it if exists. otherwise return empty. // if it's rooted, return it if exists. otherwise return empty.
if (ioUtil.isRooted(tool)) { if (ioUtil.isRooted(tool)) {
const filePath: string = await ioUtil.tryGetExecutablePath(tool, extensions) const filePath: string = await ioUtil.tryGetExecutablePath(
tool,
extensions
)
if (filePath) { if (filePath) {
return [filePath] return filePath
}
return ''
} }
return [] // if any path separators, return empty
} if (tool.includes('/') || (ioUtil.IS_WINDOWS && tool.includes('\\'))) {
return ''
}
// if any path separators, return empty // build the list of directories
if (tool.includes(path.sep)) { //
return [] // Note, technically "where" checks the current directory on Windows. From a toolkit perspective,
} // it feels like we should not do this. Checking the current directory seems like more of a use
// case of a shell, and the which() function exposed by the toolkit should strive for consistency
// across platforms.
const directories: string[] = []
// build the list of directories if (process.env.PATH) {
// for (const p of process.env.PATH.split(path.delimiter)) {
// Note, technically "where" checks the current directory on Windows. From a toolkit perspective, if (p) {
// it feels like we should not do this. Checking the current directory seems like more of a use directories.push(p)
// case of a shell, and the which() function exposed by the toolkit should strive for consistency }
// across platforms.
const directories: string[] = []
if (process.env.PATH) {
for (const p of process.env.PATH.split(path.delimiter)) {
if (p) {
directories.push(p)
} }
} }
}
// find all matches // return the first match
const matches: string[] = [] for (const directory of directories) {
const filePath = await ioUtil.tryGetExecutablePath(
for (const directory of directories) { directory + path.sep + tool,
const filePath = await ioUtil.tryGetExecutablePath( extensions
path.join(directory, tool), )
extensions if (filePath) {
) return filePath
if (filePath) { }
matches.push(filePath)
} }
}
return matches return ''
} catch (err) {
throw new Error(`which failed with message ${err.message}`)
}
} }
function readCopyOptions(options: CopyOptions): Required<CopyOptions> { function readCopyOptions(options: CopyOptions): Required<CopyOptions> {
const force = options.force == null ? true : options.force const force = options.force == null ? true : options.force
const recursive = Boolean(options.recursive) const recursive = Boolean(options.recursive)
const copySourceDirectory = return {force, recursive}
options.copySourceDirectory == null
? true
: Boolean(options.copySourceDirectory)
return {force, recursive, copySourceDirectory}
} }
async function cpDirRecursive( async function cpDirRecursive(
-9
View File
@@ -1,14 +1,5 @@
# @actions/tool-cache Releases # @actions/tool-cache Releases
### 1.7.1
- [Fallback to os-releases file to get linux version](https://github.com/actions/toolkit/pull/594)
- [Update to latest @actions/io verison](https://github.com/actions/toolkit/pull/838)
### 1.7.0
- [Allow arbirtary headers when downloading tools to the tc](https://github.com/actions/toolkit/pull/530)
- [Export `isExplicitVersion` and `evaluateVersions` functions](https://github.com/actions/toolkit/pull/796)
- [Force overwrite on default when extracted compressed files](https://github.com/actions/toolkit/pull/807)
### 1.6.1 ### 1.6.1
- [Update @actions/core version](https://github.com/actions/toolkit/pull/636) - [Update @actions/core version](https://github.com/actions/toolkit/pull/636)
+1 -43
View File
@@ -116,7 +116,7 @@ describe('@actions/tool-cache-manifest', () => {
expect(file?.filename).toBe('sometool-1.2.3-linux-x64.tar.gz') expect(file?.filename).toBe('sometool-1.2.3-linux-x64.tar.gz')
}) })
it('can match with linux platform version spec from lsb-release', async () => { it('can match with linux platform version spec', async () => {
os.platform = 'linux' os.platform = 'linux'
os.arch = 'x64' os.arch = 'x64'
@@ -150,48 +150,6 @@ describe('@actions/tool-cache-manifest', () => {
expect(file?.filename).toBe('sometool-1.2.4-ubuntu1804-x64.tar.gz') expect(file?.filename).toBe('sometool-1.2.4-ubuntu1804-x64.tar.gz')
}) })
it('can match with linux platform version spec from os-release', async () => {
os.platform = 'linux'
os.arch = 'x64'
readLsbSpy.mockImplementation(() => {
return `NAME="Ubuntu"
VERSION="18.04.5 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.5 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic`
})
const manifest: mm.IToolRelease[] | null = await tc.getManifestFromRepo(
owner,
repo,
fakeToken
)
const release: tc.IToolRelease | undefined = await tc.findFromManifest(
'1.2.4',
true,
manifest
)
expect(release).toBeDefined()
expect(release?.version).toBe('1.2.4')
expect(release?.files.length).toBe(1)
const file = release?.files[0]
expect(file).toBeDefined()
expect(file?.arch).toBe('x64')
expect(file?.platform).toBe('linux')
expect(file?.download_url).toBe(
'https://github.com/actions/sometool/releases/tag/1.2.4-20200402.6/sometool-1.2.4-ubuntu1804-x64.tar.gz'
)
expect(file?.filename).toBe('sometool-1.2.4-ubuntu1804-x64.tar.gz')
})
it('can match with darwin platform version spec', async () => { it('can match with darwin platform version spec', async () => {
os.platform = 'darwin' os.platform = 'darwin'
os.arch = 'x64' os.arch = 'x64'
+56 -180
View File
@@ -122,9 +122,11 @@ describe('@actions/tool-cache', function() {
setResponseMessageFactory(() => { setResponseMessageFactory(() => {
const readStream = new stream.Readable() const readStream = new stream.Readable()
/* eslint-disable @typescript-eslint/unbound-method */
readStream._read = () => { readStream._read = () => {
readStream.destroy(new Error('uh oh')) readStream.destroy(new Error('uh oh'))
} }
/* eslint-enable @typescript-eslint/unbound-method */
return readStream return readStream
}) })
@@ -147,6 +149,7 @@ describe('@actions/tool-cache', function() {
.get('/retries-error-from-response-message-stream') .get('/retries-error-from-response-message-stream')
.reply(200, {}) .reply(200, {})
/* eslint-disable @typescript-eslint/unbound-method */
let attempt = 1 let attempt = 1
setResponseMessageFactory(() => { setResponseMessageFactory(() => {
const readStream = new stream.Readable() const readStream = new stream.Readable()
@@ -167,6 +170,7 @@ describe('@actions/tool-cache', function() {
return readStream return readStream
}) })
/* eslint-enable @typescript-eslint/unbound-method */
const downPath = await tc.downloadTool( const downPath = await tc.downloadTool(
'http://example.com/retries-error-from-response-message-stream' 'http://example.com/retries-error-from-response-message-stream'
@@ -239,10 +243,6 @@ describe('@actions/tool-cache', function() {
const _7zFile: string = path.join(tempDir, 'test.7z') const _7zFile: string = path.join(tempDir, 'test.7z')
await io.cp(path.join(__dirname, 'data', 'test.7z'), _7zFile) await io.cp(path.join(__dirname, 'data', 'test.7z'), _7zFile)
const destDir = path.join(tempDir, 'destination')
await io.mkdirP(destDir)
fs.writeFileSync(path.join(destDir, 'file.txt'), 'overwriteMe')
// extract/cache // extract/cache
const extPath: string = await tc.extract7z(_7zFile) const extPath: string = await tc.extract7z(_7zFile)
await tc.cacheDir(extPath, 'my-7z-contents', '1.1.0') await tc.cacheDir(extPath, 'my-7z-contents', '1.1.0')
@@ -251,9 +251,6 @@ describe('@actions/tool-cache', function() {
expect(fs.existsSync(toolPath)).toBeTruthy() expect(fs.existsSync(toolPath)).toBeTruthy()
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy() expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy() expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
expect(fs.readFileSync(path.join(toolPath, 'file.txt'), 'utf8')).toBe(
'file.txt contents'
)
expect( expect(
fs.existsSync(path.join(toolPath, 'file-with-ç-character.txt')) fs.existsSync(path.join(toolPath, 'file-with-ç-character.txt'))
).toBeTruthy() ).toBeTruthy()
@@ -350,22 +347,6 @@ describe('@actions/tool-cache', function() {
await io.rmRF(tempDir) await io.rmRF(tempDir)
} }
}) })
it.each(['pwsh', 'powershell'])(
'unzip properly fails with bad path (%s)',
async powershellTool => {
const originalPath = process.env['PATH']
try {
if (powershellTool === 'powershell' && IS_WINDOWS) {
//remove pwsh from PATH temporarily to test fallback case
process.env['PATH'] = removePWSHFromPath(process.env['PATH'])
}
await expect(tc.extractZip('badPath')).rejects.toThrow()
} finally {
process.env['PATH'] = originalPath
}
}
)
} else if (IS_MAC) { } else if (IS_MAC) {
it('extract .xar', async () => { it('extract .xar', async () => {
const tempDir = path.join(tempPath, 'test-install.xar') const tempDir = path.join(tempPath, 'test-install.xar')
@@ -379,21 +360,14 @@ describe('@actions/tool-cache', function() {
cwd: sourcePath cwd: sourcePath
}) })
const destDir = path.join(tempDir, 'destination')
await io.mkdirP(destDir)
fs.writeFileSync(path.join(destDir, 'file.txt'), 'overwriteMe')
// extract/cache // extract/cache
const extPath: string = await tc.extractXar(targetPath, destDir, ['-x']) const extPath: string = await tc.extractXar(targetPath, undefined, '-x')
await tc.cacheDir(extPath, 'my-xar-contents', '1.1.0') await tc.cacheDir(extPath, 'my-xar-contents', '1.1.0')
const toolPath: string = tc.find('my-xar-contents', '1.1.0') const toolPath: string = tc.find('my-xar-contents', '1.1.0')
expect(fs.existsSync(toolPath)).toBeTruthy() expect(fs.existsSync(toolPath)).toBeTruthy()
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy() expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy() expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
expect(fs.readFileSync(path.join(toolPath, 'file.txt'), 'utf8')).toBe(
'file.txt contents'
)
expect( expect(
fs.existsSync(path.join(toolPath, 'file-with-ç-character.txt')) fs.existsSync(path.join(toolPath, 'file-with-ç-character.txt'))
).toBeTruthy() ).toBeTruthy()
@@ -488,23 +462,14 @@ describe('@actions/tool-cache', function() {
const _tgzFile: string = path.join(tempDir, 'test.tar.gz') const _tgzFile: string = path.join(tempDir, 'test.tar.gz')
await io.cp(path.join(__dirname, 'data', 'test.tar.gz'), _tgzFile) await io.cp(path.join(__dirname, 'data', 'test.tar.gz'), _tgzFile)
//Create file to overwrite
const destDir = path.join(tempDir, 'extract-dest')
await io.rmRF(destDir)
await io.mkdirP(destDir)
fs.writeFileSync(path.join(destDir, 'file.txt'), 'overwriteMe')
// extract/cache // extract/cache
const extPath: string = await tc.extractTar(_tgzFile, destDir) const extPath: string = await tc.extractTar(_tgzFile)
await tc.cacheDir(extPath, 'my-tgz-contents', '1.1.0') await tc.cacheDir(extPath, 'my-tgz-contents', '1.1.0')
const toolPath: string = tc.find('my-tgz-contents', '1.1.0') const toolPath: string = tc.find('my-tgz-contents', '1.1.0')
expect(fs.existsSync(toolPath)).toBeTruthy() expect(fs.existsSync(toolPath)).toBeTruthy()
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy() expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy() expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
expect(fs.readFileSync(path.join(toolPath, 'file.txt'), 'utf8')).toBe(
'file.txt contents'
)
expect( expect(
fs.existsSync(path.join(toolPath, 'file-with-ç-character.txt')) fs.existsSync(path.join(toolPath, 'file-with-ç-character.txt'))
).toBeTruthy() ).toBeTruthy()
@@ -535,9 +500,6 @@ describe('@actions/tool-cache', function() {
expect(fs.existsSync(toolPath)).toBeTruthy() expect(fs.existsSync(toolPath)).toBeTruthy()
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy() expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy() expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
expect(fs.readFileSync(path.join(toolPath, 'file.txt'), 'utf8')).toBe(
'file.txt contents'
)
expect( expect(
fs.existsSync(path.join(toolPath, 'file-with-ç-character.txt')) fs.existsSync(path.join(toolPath, 'file-with-ç-character.txt'))
).toBeTruthy() ).toBeTruthy()
@@ -558,14 +520,8 @@ describe('@actions/tool-cache', function() {
const _txzFile: string = path.join(tempDir, 'test.tar.xz') const _txzFile: string = path.join(tempDir, 'test.tar.xz')
await io.cp(path.join(__dirname, 'data', 'test.tar.xz'), _txzFile) await io.cp(path.join(__dirname, 'data', 'test.tar.xz'), _txzFile)
//Create file to overwrite
const destDir = path.join(tempDir, 'extract-dest')
await io.rmRF(destDir)
await io.mkdirP(destDir)
fs.writeFileSync(path.join(destDir, 'file.txt'), 'overwriteMe')
// extract/cache // extract/cache
const extPath: string = await tc.extractTar(_txzFile, destDir, 'x') const extPath: string = await tc.extractTar(_txzFile, undefined, 'x')
await tc.cacheDir(extPath, 'my-txz-contents', '1.1.0') await tc.cacheDir(extPath, 'my-txz-contents', '1.1.0')
const toolPath: string = tc.find('my-txz-contents', '1.1.0') const toolPath: string = tc.find('my-txz-contents', '1.1.0')
@@ -578,70 +534,58 @@ describe('@actions/tool-cache', function() {
).toBe('foo/hello: world') ).toBe('foo/hello: world')
}) })
it.each(['pwsh', 'powershell'])( it('installs a zip and finds it', async () => {
'installs a zip and finds it (%s)', const tempDir = path.join(__dirname, 'test-install-zip')
async powershellTool => { try {
const tempDir = path.join(__dirname, 'test-install-zip') await io.mkdirP(tempDir)
const originalPath = process.env['PATH']
try {
await io.mkdirP(tempDir)
// stage the layout for a zip file: // stage the layout for a zip file:
// file.txt // file.txt
// folder/nested-file.txt // folder/nested-file.txt
const stagingDir = path.join(tempDir, 'zip-staging') const stagingDir = path.join(tempDir, 'zip-staging')
await io.mkdirP(path.join(stagingDir, 'folder')) await io.mkdirP(path.join(stagingDir, 'folder'))
fs.writeFileSync(path.join(stagingDir, 'file.txt'), '') fs.writeFileSync(path.join(stagingDir, 'file.txt'), '')
fs.writeFileSync(path.join(stagingDir, 'folder', 'nested-file.txt'), '') fs.writeFileSync(path.join(stagingDir, 'folder', 'nested-file.txt'), '')
// create the zip // create the zip
const zipFile = path.join(tempDir, 'test.zip') const zipFile = path.join(tempDir, 'test.zip')
await io.rmRF(zipFile) await io.rmRF(zipFile)
if (IS_WINDOWS) { if (IS_WINDOWS) {
const escapedStagingPath = stagingDir.replace(/'/g, "''") // double-up single quotes const escapedStagingPath = stagingDir.replace(/'/g, "''") // double-up single quotes
const escapedZipFile = zipFile.replace(/'/g, "''") const escapedZipFile = zipFile.replace(/'/g, "''")
const powershellPath = const powershellPath =
(await io.which('pwsh', false)) || (await io.which('pwsh', false)) ||
(await io.which('powershell', true)) (await io.which('powershell', true))
const args = [ const args = [
'-NoLogo', '-NoLogo',
'-Sta', '-Sta',
'-NoProfile', '-NoProfile',
'-NonInteractive', '-NonInteractive',
'-ExecutionPolicy', '-ExecutionPolicy',
'Unrestricted', 'Unrestricted',
'-Command', '-Command',
`$ErrorActionPreference = 'Stop' ; Add-Type -AssemblyName System.IO.Compression.FileSystem ; [System.IO.Compression.ZipFile]::CreateFromDirectory('${escapedStagingPath}', '${escapedZipFile}')` `$ErrorActionPreference = 'Stop' ; Add-Type -AssemblyName System.IO.Compression.FileSystem ; [System.IO.Compression.ZipFile]::CreateFromDirectory('${escapedStagingPath}', '${escapedZipFile}')`
] ]
await exec.exec(`"${powershellPath}"`, args) await exec.exec(`"${powershellPath}"`, args)
} else { } else {
const zipPath: string = await io.which('zip', true) const zipPath: string = await io.which('zip', true)
await exec.exec(`"${zipPath}`, [zipFile, '-r', '.'], { await exec.exec(`"${zipPath}`, [zipFile, '-r', '.'], {cwd: stagingDir})
cwd: stagingDir
})
}
if (powershellTool === 'powershell' && IS_WINDOWS) {
//remove pwsh from PATH temporarily to test fallback case
process.env['PATH'] = removePWSHFromPath(process.env['PATH'])
}
const extPath: string = await tc.extractZip(zipFile)
await tc.cacheDir(extPath, 'foo', '1.1.0')
const toolPath: string = tc.find('foo', '1.1.0')
expect(fs.existsSync(toolPath)).toBeTruthy()
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
expect(
fs.existsSync(path.join(toolPath, 'folder', 'nested-file.txt'))
).toBeTruthy()
} finally {
await io.rmRF(tempDir)
process.env['PATH'] = originalPath
} }
const extPath: string = await tc.extractZip(zipFile)
await tc.cacheDir(extPath, 'foo', '1.1.0')
const toolPath: string = tc.find('foo', '1.1.0')
expect(fs.existsSync(toolPath)).toBeTruthy()
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
expect(
fs.existsSync(path.join(toolPath, 'folder', 'nested-file.txt'))
).toBeTruthy()
} finally {
await io.rmRF(tempDir)
} }
) })
it('installs a zip and extracts it to specified directory', async function() { it('installs a zip and extracts it to specified directory', async function() {
const tempDir = path.join(__dirname, 'test-install-zip') const tempDir = path.join(__dirname, 'test-install-zip')
@@ -653,7 +597,7 @@ describe('@actions/tool-cache', function() {
// folder/nested-file.txt // folder/nested-file.txt
const stagingDir = path.join(tempDir, 'zip-staging') const stagingDir = path.join(tempDir, 'zip-staging')
await io.mkdirP(path.join(stagingDir, 'folder')) await io.mkdirP(path.join(stagingDir, 'folder'))
fs.writeFileSync(path.join(stagingDir, 'file.txt'), 'originalText') fs.writeFileSync(path.join(stagingDir, 'file.txt'), '')
fs.writeFileSync(path.join(stagingDir, 'folder', 'nested-file.txt'), '') fs.writeFileSync(path.join(stagingDir, 'folder', 'nested-file.txt'), '')
// create the zip // create the zip
@@ -685,16 +629,12 @@ describe('@actions/tool-cache', function() {
await io.rmRF(destDir) await io.rmRF(destDir)
await io.mkdirP(destDir) await io.mkdirP(destDir)
try { try {
fs.writeFileSync(path.join(destDir, 'file.txt'), 'overwriteMe')
const extPath: string = await tc.extractZip(zipFile, destDir) const extPath: string = await tc.extractZip(zipFile, destDir)
await tc.cacheDir(extPath, 'foo', '1.1.0') await tc.cacheDir(extPath, 'foo', '1.1.0')
const toolPath: string = tc.find('foo', '1.1.0') const toolPath: string = tc.find('foo', '1.1.0')
expect(fs.existsSync(toolPath)).toBeTruthy() expect(fs.existsSync(toolPath)).toBeTruthy()
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy() expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy() expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
expect(fs.readFileSync(path.join(toolPath, 'file.txt'), 'utf8')).toBe(
'originalText'
)
expect( expect(
fs.existsSync(path.join(toolPath, 'folder', 'nested-file.txt')) fs.existsSync(path.join(toolPath, 'folder', 'nested-file.txt'))
).toBeTruthy() ).toBeTruthy()
@@ -823,61 +763,6 @@ describe('@actions/tool-cache', function() {
expect(err.toString()).toContain('404') expect(err.toString()).toContain('404')
} }
}) })
it('supports authorization headers', async function() {
nock('http://example.com', {
reqheaders: {
authorization: 'token abc123'
}
})
.get('/some-file-that-needs-authorization')
.reply(200, undefined)
await tc.downloadTool(
'http://example.com/some-file-that-needs-authorization',
undefined,
'token abc123'
)
})
it('supports custom headers', async function() {
nock('http://example.com', {
reqheaders: {
accept: 'application/octet-stream'
}
})
.get('/some-file-that-needs-headers')
.reply(200, undefined)
await tc.downloadTool(
'http://example.com/some-file-that-needs-headers',
undefined,
undefined,
{
accept: 'application/octet-stream'
}
)
})
it('supports authorization and custom headers', async function() {
nock('http://example.com', {
reqheaders: {
accept: 'application/octet-stream',
authorization: 'token abc123'
}
})
.get('/some-file-that-needs-authorization-and-headers')
.reply(200, undefined)
await tc.downloadTool(
'http://example.com/some-file-that-needs-authorization-and-headers',
undefined,
'token abc123',
{
accept: 'application/octet-stream'
}
)
})
}) })
/** /**
@@ -903,12 +788,3 @@ function setGlobal<T>(key: string, value: T | undefined): void {
g[key] = value g[key] = value
} }
} }
function removePWSHFromPath(pathEnv: string | undefined): string {
return (pathEnv || '')
.split(';')
.filter(segment => {
return !segment.startsWith(`C:\\Program Files\\PowerShell`)
})
.join(';')
}
+14 -14
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/tool-cache", "name": "@actions/tool-cache",
"version": "1.7.1", "version": "1.6.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -26,9 +26,9 @@
} }
}, },
"@actions/io": { "@actions/io": {
"version": "1.1.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz",
"integrity": "sha512-Qi4JoKXjmE0O67wAOH6y0n26QXhMKMFo7GD/4IXNVcrtLjUlGjGuVys6pQgwF3ArfGTQu0XpqaNr0YhED2RaRA==" "integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg=="
}, },
"@types/nock": { "@types/nock": {
"version": "10.0.3", "version": "10.0.3",
@@ -123,24 +123,24 @@
"dev": true "dev": true
}, },
"lodash": { "lodash": {
"version": "4.17.21", "version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
"dev": true "dev": true
}, },
"minimist": { "minimist": {
"version": "1.2.5", "version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true "dev": true
}, },
"mkdirp": { "mkdirp": {
"version": "0.5.5", "version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true, "dev": true,
"requires": { "requires": {
"minimist": "^1.2.5" "minimist": "0.0.8"
} }
}, },
"ms": { "ms": {
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/tool-cache", "name": "@actions/tool-cache",
"version": "1.7.1", "version": "1.6.1",
"description": "Actions tool-cache lib", "description": "Actions tool-cache lib",
"keywords": [ "keywords": [
"github", "github",
@@ -39,7 +39,7 @@
"@actions/core": "^1.2.6", "@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.1.1", "@actions/io": "^1.0.1",
"semver": "^6.1.0", "semver": "^6.1.0",
"uuid": "^3.3.2" "uuid": "^3.3.2"
}, },
+5 -15
View File
@@ -135,15 +135,8 @@ export function _getOsVersion(): string {
const lines = lsbContents.split('\n') const lines = lsbContents.split('\n')
for (const line of lines) { for (const line of lines) {
const parts = line.split('=') const parts = line.split('=')
if ( if (parts.length === 2 && parts[0].trim() === 'DISTRIB_RELEASE') {
parts.length === 2 && version = parts[1].trim()
(parts[0].trim() === 'VERSION_ID' ||
parts[0].trim() === 'DISTRIB_RELEASE')
) {
version = parts[1]
.trim()
.replace(/^"/, '')
.replace(/"$/, '')
break break
} }
} }
@@ -154,14 +147,11 @@ export function _getOsVersion(): string {
} }
export function _readLinuxVersionFile(): string { export function _readLinuxVersionFile(): string {
const lsbReleaseFile = '/etc/lsb-release' const lsbFile = '/etc/lsb-release'
const osReleaseFile = '/etc/os-release'
let contents = '' let contents = ''
if (fs.existsSync(lsbReleaseFile)) { if (fs.existsSync(lsbFile)) {
contents = fs.readFileSync(lsbReleaseFile).toString() contents = fs.readFileSync(lsbFile).toString()
} else if (fs.existsSync(osReleaseFile)) {
contents = fs.readFileSync(osReleaseFile).toString()
} }
return contents return contents
+25 -79
View File
@@ -32,14 +32,12 @@ const userAgent = 'actions/tool-cache'
* @param url url of tool to download * @param url url of tool to download
* @param dest path to download tool * @param dest path to download tool
* @param auth authorization header * @param auth authorization header
* @param headers other headers
* @returns path to downloaded tool * @returns path to downloaded tool
*/ */
export async function downloadTool( export async function downloadTool(
url: string, url: string,
dest?: string, dest?: string,
auth?: string, auth?: string
headers?: IHeaders
): Promise<string> { ): Promise<string> {
dest = dest || path.join(_getTempDirectory(), uuidV4()) dest = dest || path.join(_getTempDirectory(), uuidV4())
await io.mkdirP(path.dirname(dest)) await io.mkdirP(path.dirname(dest))
@@ -58,7 +56,7 @@ export async function downloadTool(
const retryHelper = new RetryHelper(maxAttempts, minSeconds, maxSeconds) const retryHelper = new RetryHelper(maxAttempts, minSeconds, maxSeconds)
return await retryHelper.execute( return await retryHelper.execute(
async () => { async () => {
return await downloadToolAttempt(url, dest || '', auth, headers) return await downloadToolAttempt(url, dest || '', auth)
}, },
(err: Error) => { (err: Error) => {
if (err instanceof HTTPError && err.httpStatusCode) { if (err instanceof HTTPError && err.httpStatusCode) {
@@ -81,8 +79,7 @@ export async function downloadTool(
async function downloadToolAttempt( async function downloadToolAttempt(
url: string, url: string,
dest: string, dest: string,
auth?: string, auth?: string
headers?: IHeaders
): Promise<string> { ): Promise<string> {
if (fs.existsSync(dest)) { if (fs.existsSync(dest)) {
throw new Error(`Destination file path ${dest} already exists`) throw new Error(`Destination file path ${dest} already exists`)
@@ -93,12 +90,12 @@ async function downloadToolAttempt(
allowRetries: false allowRetries: false
}) })
let headers: IHeaders | undefined
if (auth) { if (auth) {
core.debug('set auth') core.debug('set auth')
if (headers === undefined) { headers = {
headers = {} authorization: auth
} }
headers.authorization = auth
} }
const response: httpm.HttpClientResponse = await http.get(url, headers) const response: httpm.HttpClientResponse = await http.get(url, headers)
@@ -272,7 +269,6 @@ export async function extractTar(
if (isGnuTar) { if (isGnuTar) {
// Suppress warnings when using GNU tar to extract archives created by BSD tar // Suppress warnings when using GNU tar to extract archives created by BSD tar
args.push('--warning=no-unknown-keyword') args.push('--warning=no-unknown-keyword')
args.push('--overwrite')
} }
args.push('-C', destArg, '-f', fileArg) args.push('-C', destArg, '-f', fileArg)
@@ -345,55 +341,21 @@ async function extractZipWin(file: string, dest: string): Promise<void> {
// build the powershell command // build the powershell command
const escapedFile = file.replace(/'/g, "''").replace(/"|\n|\r/g, '') // double-up single quotes, remove double quotes and newlines const escapedFile = file.replace(/'/g, "''").replace(/"|\n|\r/g, '') // double-up single quotes, remove double quotes and newlines
const escapedDest = dest.replace(/'/g, "''").replace(/"|\n|\r/g, '') const escapedDest = dest.replace(/'/g, "''").replace(/"|\n|\r/g, '')
const pwshPath = await io.which('pwsh', false) const command = `$ErrorActionPreference = 'Stop' ; try { Add-Type -AssemblyName System.IO.Compression.FileSystem } catch { } ; [System.IO.Compression.ZipFile]::ExtractToDirectory('${escapedFile}', '${escapedDest}')`
//To match the file overwrite behavior on nix systems, we use the overwrite = true flag for ExtractToDirectory // run powershell
//and the -Force flag for Expand-Archive as a fallback const powershellPath = await io.which('powershell', true)
if (pwshPath) { const args = [
//attempt to use pwsh with ExtractToDirectory, if this fails attempt Expand-Archive '-NoLogo',
const pwshCommand = [ '-Sta',
`$ErrorActionPreference = 'Stop' ;`, '-NoProfile',
`try { Add-Type -AssemblyName System.IO.Compression.ZipFile } catch { } ;`, '-NonInteractive',
`try { [System.IO.Compression.ZipFile]::ExtractToDirectory('${escapedFile}', '${escapedDest}', $true) }`, '-ExecutionPolicy',
`catch { if (($_.Exception.GetType().FullName -eq 'System.Management.Automation.MethodException') -or ($_.Exception.GetType().FullName -eq 'System.Management.Automation.RuntimeException') ){ Expand-Archive -LiteralPath '${escapedFile}' -DestinationPath '${escapedDest}' -Force } else { throw $_ } } ;` 'Unrestricted',
].join(' ') '-Command',
command
const args = [ ]
'-NoLogo', await exec(`"${powershellPath}"`, args)
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy',
'Unrestricted',
'-Command',
pwshCommand
]
core.debug(`Using pwsh at path: ${pwshPath}`)
await exec(`"${pwshPath}"`, args)
} else {
const powershellCommand = [
`$ErrorActionPreference = 'Stop' ;`,
`try { Add-Type -AssemblyName System.IO.Compression.FileSystem } catch { } ;`,
`if ((Get-Command -Name Expand-Archive -Module Microsoft.PowerShell.Archive -ErrorAction Ignore)) { Expand-Archive -LiteralPath '${escapedFile}' -DestinationPath '${escapedDest}' -Force }`,
`else {[System.IO.Compression.ZipFile]::ExtractToDirectory('${escapedFile}', '${escapedDest}', $true) }`
].join(' ')
const args = [
'-NoLogo',
'-Sta',
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy',
'Unrestricted',
'-Command',
powershellCommand
]
const powershellPath = await io.which('powershell', true)
core.debug(`Using powershell at path: ${powershellPath}`)
await exec(`"${powershellPath}"`, args)
}
} }
async function extractZipNix(file: string, dest: string): Promise<void> { async function extractZipNix(file: string, dest: string): Promise<void> {
@@ -402,7 +364,6 @@ async function extractZipNix(file: string, dest: string): Promise<void> {
if (!core.isDebug()) { if (!core.isDebug()) {
args.unshift('-q') args.unshift('-q')
} }
args.unshift('-o') //overwrite with -o, otherwise a prompt is shown which freezes the run
await exec(`"${unzipPath}"`, args, {cwd: dest}) await exec(`"${unzipPath}"`, args, {cwd: dest})
} }
@@ -508,9 +469,9 @@ export function find(
arch = arch || os.arch() arch = arch || os.arch()
// attempt to resolve an explicit version // attempt to resolve an explicit version
if (!isExplicitVersion(versionSpec)) { if (!_isExplicitVersion(versionSpec)) {
const localVersions: string[] = findAllVersions(toolName, arch) const localVersions: string[] = findAllVersions(toolName, arch)
const match = evaluateVersions(localVersions, versionSpec) const match = _evaluateVersions(localVersions, versionSpec)
versionSpec = match versionSpec = match
} }
@@ -550,7 +511,7 @@ export function findAllVersions(toolName: string, arch?: string): string[] {
if (fs.existsSync(toolPath)) { if (fs.existsSync(toolPath)) {
const children: string[] = fs.readdirSync(toolPath) const children: string[] = fs.readdirSync(toolPath)
for (const child of children) { for (const child of children) {
if (isExplicitVersion(child)) { if (_isExplicitVersion(child)) {
const fullPath = path.join(toolPath, child, arch || '') const fullPath = path.join(toolPath, child, arch || '')
if (fs.existsSync(fullPath) && fs.existsSync(`${fullPath}.complete`)) { if (fs.existsSync(fullPath) && fs.existsSync(`${fullPath}.complete`)) {
versions.push(child) versions.push(child)
@@ -688,12 +649,7 @@ function _completeToolPath(tool: string, version: string, arch?: string): void {
core.debug('finished caching tool') core.debug('finished caching tool')
} }
/** function _isExplicitVersion(versionSpec: string): boolean {
* Check if version string is explicit
*
* @param versionSpec version string to check
*/
export function isExplicitVersion(versionSpec: string): boolean {
const c = semver.clean(versionSpec) || '' const c = semver.clean(versionSpec) || ''
core.debug(`isExplicit: ${c}`) core.debug(`isExplicit: ${c}`)
@@ -703,17 +659,7 @@ export function isExplicitVersion(versionSpec: string): boolean {
return valid return valid
} }
/** function _evaluateVersions(versions: string[], versionSpec: string): string {
* Get the highest satisfiying semantic version in `versions` which satisfies `versionSpec`
*
* @param versions array of versions to evaluate
* @param versionSpec semantic version spec to satisfy
*/
export function evaluateVersions(
versions: string[],
versionSpec: string
): string {
let version = '' let version = ''
core.debug(`evaluating ${versions.length} versions`) core.debug(`evaluating ${versions.length} versions`)
versions = versions.sort((a, b) => { versions = versions.sort((a, b) => {
-89
View File
@@ -1,89 +0,0 @@
#!/usr/bin/env node
/*
This script takes the output of npm audit --json from stdin
and writes a filtered version to stdout.
The filtered version will have the entries listed in `AUDIT_ALLOW_LIST` removed.
Specifically, each property of `vulnerabilities` in the input is matched by name in the allow list.
Sample output of `npm audit --json` (NPM v6):
{
"actions": [
{
"action": "review",
"module": "trim-newlines",
"resolves": [
{
"id": 1753,
"path": "lerna>@lerna/publish>@lerna/version>@lerna/conventional-commits>conventional-changelog-core>get-pkg-repo>meow>trim-newlines",
"dev": true,
"optional": false,
"bundled": false
}
]
}
],
// Other properties ...
}
The reason we have this script is that there may be low-severity or unexploitable vulnerabilities
that have not yet been fixed in newer versions of the package.
Note: if we update to NPM v7, we will have to change this script because the `npm audit` output will be different.
See commit 935647112d96fa5cf82e61314f7135376d24f291 in https://github.com/actions/toolkit/pull/846.
*/
'use strict'
const fs = require('fs')
const USAGE = "Usage: npm audit --json | scripts/audit-allow-list"
// To add entires to the allow list:
// - Run `npm audit --json`
// - Copy `path` from each `actions[k].resolves` you want to allow
// - Fill in the `advisoryUrl` and `justification` (these are just for documentation)
const AUDIT_ALLOW_LIST = [
{
path: "lerna>@lerna/publish>@lerna/version>@lerna/conventional-commits>conventional-changelog-core>get-pkg-repo>meow>trim-newlines",
advisoryUrl: "https://www.npmjs.com/advisories/1753",
justification: "dependency of lerna (dev only); low severity"
},
{
path: "lerna>@lerna/version>@lerna/conventional-commits>conventional-changelog-core>get-pkg-repo>meow>trim-newlines",
advisoryUrl: "https://www.npmjs.com/advisories/1753",
justification: "dependency of lerna (dev only); low severity"
}
]
/**
* @param audits - JavaScript object matching the schema of `npm audit --json`
* @param allowedPaths - List of dependency paths to exclude from the audit
*/
function filterVulnerabilities(audits, allowedPaths) {
const vulnerabilities = audits.actions.flatMap(x => x.resolves)
return vulnerabilities.filter(x => !allowedPaths.includes(x.path))
}
const input = fs.readFileSync("/dev/stdin", "utf-8")
if (input === "") {
console.error(USAGE)
process.exit(1)
}
const audits = JSON.parse(input)
const allowedPaths = AUDIT_ALLOW_LIST.map(x => x.path)
// This function assumes `audits` has the right structure.
// Just let the error terminate the process if the input doesn't match the schema.
const remainingVulnerabilities = filterVulnerabilities(audits, allowedPaths)
// `npm audit` will return exit code 1 if it finds vulnerabilities.
// This script should do the same.
const numVulnerabilities = remainingVulnerabilities.length
if (numVulnerabilities > 0) {
const pluralized = numVulnerabilities === 1 ? "y" : "ies"
console.log(`Found ${numVulnerabilities} unrecognized vulnerabilit${pluralized} from \`npm audit\`:`)
console.log(JSON.stringify(remainingVulnerabilities, null, 2))
process.exit(1)
}
+2 -1
View File
@@ -5,7 +5,8 @@
"strict": true, "strict": true,
"declaration": true, "declaration": true,
"target": "es6", "target": "es6",
"sourceMap": true "sourceMap": true,
"lib": ["es6"]
}, },
"exclude": [ "exclude": [
"node_modules", "node_modules",