Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d3ad3eeb7f |
+1
-2
@@ -1,4 +1,3 @@
|
||||
node_modules/
|
||||
packages/*/node_modules/
|
||||
packages/*/lib/
|
||||
packages/glob/__tests__/_temp
|
||||
packages/*/lib/
|
||||
+6
-18
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"plugins": ["jest", "@typescript-eslint"],
|
||||
"extends": ["plugin:github/recommended"],
|
||||
"extends": ["plugin:github/es6"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 9,
|
||||
@@ -9,34 +9,20 @@
|
||||
},
|
||||
"rules": {
|
||||
"eslint-comments/no-use": "off",
|
||||
"github/no-then": "off",
|
||||
"import/no-namespace": "off",
|
||||
"no-shadow": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-undef": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
|
||||
"@typescript-eslint/no-require-imports": "error",
|
||||
"@typescript-eslint/array-type": "error",
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
"@typescript-eslint/ban-ts-comment": "error",
|
||||
"@typescript-eslint/ban-ts-ignore": "error",
|
||||
"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/func-call-spacing": ["error", "never"],
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"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/generic-type-naming": ["error", "^[A-Z][A-Za-z]*$"],
|
||||
"@typescript-eslint/no-array-constructor": "error",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
@@ -46,6 +32,7 @@
|
||||
"@typescript-eslint/no-misused-new": "error",
|
||||
"@typescript-eslint/no-namespace": "error",
|
||||
"@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-type-assertion": "error",
|
||||
"@typescript-eslint/no-useless-constructor": "error",
|
||||
@@ -53,6 +40,7 @@
|
||||
"@typescript-eslint/prefer-for-of": "warn",
|
||||
"@typescript-eslint/prefer-function-type": "warn",
|
||||
"@typescript-eslint/prefer-includes": "error",
|
||||
"@typescript-eslint/prefer-interface": "error",
|
||||
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
||||
"@typescript-eslint/promise-function-async": "error",
|
||||
"@typescript-eslint/require-array-sort-compare": "error",
|
||||
|
||||
@@ -44,27 +44,24 @@ jobs:
|
||||
npm ci
|
||||
npm run tsc
|
||||
working-directory: packages/artifact
|
||||
|
||||
|
||||
- name: Set artifact file contents
|
||||
shell: bash
|
||||
run: |
|
||||
echo "non-gzip-artifact-content=hello" >> $GITHUB_ENV
|
||||
echo "gzip-artifact-content=Some large amount of text that has a compression ratio that is greater than 100%. If greater than 100%, gzip is used to upload the file" >> $GITHUB_ENV
|
||||
echo "empty-artifact-content=_EMPTY_" >> $GITHUB_ENV
|
||||
|
||||
- name: Create files that will be uploaded
|
||||
run: |
|
||||
mkdir artifact-path
|
||||
mkdir artifact-path
|
||||
echo ${{ env.non-gzip-artifact-content }} > artifact-path/world.txt
|
||||
echo ${{ env.gzip-artifact-content }} > artifact-path/gzip.txt
|
||||
touch artifact-path/empty.txt
|
||||
|
||||
# We're using node -e to call the functions directly available in the @actions/artifact package
|
||||
- name: Upload artifacts using uploadArtifact()
|
||||
run: |
|
||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().uploadArtifact('my-artifact-1',['artifact-path/world.txt'], '${{ github.workspace }}'))"
|
||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().uploadArtifact('my-artifact-2',['artifact-path/gzip.txt'], '${{ github.workspace }}'))"
|
||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().uploadArtifact('my-artifact-3',['artifact-path/empty.txt'], '${{ github.workspace }}'))"
|
||||
|
||||
- name: Download artifacts using downloadArtifact()
|
||||
run: |
|
||||
@@ -72,15 +69,12 @@ jobs:
|
||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().downloadArtifact('my-artifact-1','artifact-1-directory'))"
|
||||
mkdir artifact-2-directory
|
||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().downloadArtifact('my-artifact-2','artifact-2-directory'))"
|
||||
mkdir artifact-3-directory
|
||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().downloadArtifact('my-artifact-3','artifact-3-directory'))"
|
||||
|
||||
|
||||
- name: Verify downloadArtifact()
|
||||
shell: bash
|
||||
run: |
|
||||
packages/artifact/__tests__/test-artifact-file.sh "artifact-1-directory/artifact-path/world.txt" "${{ env.non-gzip-artifact-content }}"
|
||||
packages/artifact/__tests__/test-artifact-file.sh "artifact-2-directory/artifact-path/gzip.txt" "${{ env.gzip-artifact-content }}"
|
||||
packages/artifact/__tests__/test-artifact-file.sh "artifact-3-directory/artifact-path/empty.txt" "${{ env.empty-artifact-content }}"
|
||||
|
||||
- name: Download artifacts using downloadAllArtifacts()
|
||||
run: |
|
||||
@@ -91,5 +85,4 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-1/artifact-path/world.txt" "${{ env.non-gzip-artifact-content }}"
|
||||
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-2/artifact-path/gzip.txt" "${{ env.gzip-artifact-content }}"
|
||||
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-3/artifact-path/empty.txt" "${{ env.empty-artifact-content }}"
|
||||
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-2/artifact-path/gzip.txt" "${{ env.gzip-artifact-content }}"
|
||||
@@ -31,9 +31,8 @@ jobs:
|
||||
- name: Bootstrap
|
||||
run: npm run bootstrap
|
||||
|
||||
- name: audit tools
|
||||
# `|| npm audit` to pretty-print the output if vulnerabilies are found after filtering.
|
||||
run: npm audit --audit-level=moderate --json | scripts/audit-allow-list || npm audit --audit-level=moderate
|
||||
# - name: audit tools #disabled while we wait for https://github.com/actions/toolkit/issues/539
|
||||
# run: npm audit --audit-level=moderate
|
||||
|
||||
- name: audit packages
|
||||
run: npm run audit-all
|
||||
|
||||
@@ -2,8 +2,6 @@ name: "Code Scanning - Action"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
name: Publish NPM
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
required: true
|
||||
description: 'core, artifact, cache, exec, github, glob, io, tool-cache'
|
||||
version:
|
||||
required: true
|
||||
description: 'the version of the package to publish'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Setup repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: npm install
|
||||
run: npm install
|
||||
|
||||
- name: bootstrap
|
||||
run: npm run bootstrap
|
||||
|
||||
- name: build
|
||||
run: npm run build
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: echo inputs
|
||||
run: echo ${{ github.event.inputs.package }} ${{ github.event.inputs.version }}
|
||||
|
||||
publish:
|
||||
runs-on: macos-latest
|
||||
needs: test
|
||||
environment: npm-publish
|
||||
steps:
|
||||
- name: Testing
|
||||
run: echo 'this is where we publish'
|
||||
|
||||
@@ -1,80 +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: Set Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
|
||||
- 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 }}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
name: "UpdateOctokit"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 18 * * 0' # sunday at 18 UTC
|
||||
|
||||
jobs:
|
||||
UpdateOctokit:
|
||||
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
* @actions/actions-runtime
|
||||
|
||||
/packages/artifact/ @actions/artifacts-actions
|
||||
/packages/cache/ @actions/actions-cache
|
||||
/packages/artifact/ @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
@@ -57,7 +57,7 @@ For example, if you mask the letter `l`, running `echo "Hello FOO BAR World"` wi
|
||||
|
||||
### Group and Ungroup Log Lines
|
||||
|
||||
Emitting a group with a title will instruct the logs to create a 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
|
||||
echo "::group::my title"
|
||||
@@ -72,7 +72,6 @@ function endGroup(): void {}
|
||||
```
|
||||
|
||||
### Problem Matchers
|
||||
|
||||
Problems matchers can be used to scan a build's output to automatically surface lines to the user that matches the provided pattern. A file path to a .json Problem Matcher must be provided. See [Problem Matchers](problem-matchers.md) for more information on how to define a Problem Matcher.
|
||||
|
||||
```bash
|
||||
@@ -82,7 +81,6 @@ echo "::remove-matcher owner=eslint-compact::"
|
||||
|
||||
`add-matcher` takes a path to a Problem Matcher file
|
||||
`remove-matcher` removes a Problem Matcher by owner
|
||||
|
||||
### Save State
|
||||
|
||||
Save a state to an environmental variable that can later be used in the main or post action.
|
||||
@@ -104,7 +102,6 @@ There are several commands to emit different levels of log output:
|
||||
| error | `echo "::error::My error message"` |
|
||||
|
||||
### Command Echoing
|
||||
|
||||
By default, the echoing of commands to stdout only occurs if [Step Debugging is enabled](./action-debugging.md#How-to-Access-Step-Debug-Logs)
|
||||
|
||||
You can enable or disable this for the current step by using the `echo` command.
|
||||
@@ -130,12 +127,12 @@ The `add-mask`, `debug`, `warning` and `error` commands do not support echoing.
|
||||
### Command Prompt
|
||||
|
||||
CMD processes the `"` character differently from other shells when echoing. In CMD, the above snippets should have the `"` characters removed in order to correctly process. For example, the set output command would be:
|
||||
|
||||
```cmd
|
||||
echo ::set-output name=FOO::BAR
|
||||
```
|
||||
|
||||
## Environment files
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -149,8 +146,7 @@ echo "FOO=BAR" >> $GITHUB_ENV
|
||||
|
||||
Running `$FOO` in a future step will now return `BAR`
|
||||
|
||||
For multiline strings, you may use a heredoc style syntax with your choice of delimeter. In the below example, we use `EOF`.
|
||||
|
||||
For multiline strings, you may use a heredoc style syntax with your choice of delimeter. In the below example, we use `EOF`
|
||||
```
|
||||
steps:
|
||||
- name: Set the value
|
||||
@@ -164,7 +160,6 @@ steps:
|
||||
This would set the value of the `JSON_RESPONSE` env variable to the value of the curl response.
|
||||
|
||||
The expected syntax for the heredoc style is:
|
||||
|
||||
```
|
||||
{VARIABLE_NAME}<<{DELIMETER}
|
||||
{VARIABLE_VALUE}
|
||||
@@ -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}`;
|
||||
|
||||
This is wrapped by the core addPath method:
|
||||
|
||||
```javascript
|
||||
export function addPath(inputPath: string): void {}
|
||||
```
|
||||
@@ -196,7 +190,6 @@ export function addPath(inputPath: string): void {}
|
||||
### Powershell
|
||||
|
||||
Powershell does not use UTF8 by default. You will want to make sure you write in the correct encoding. For example, to set the path:
|
||||
|
||||
```
|
||||
steps:
|
||||
- run: echo "mypath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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 aren’t created by users)
|
||||
|
||||
If your workflow may exceed these annotation counts, consider filtering of the log messages which the Problem Matcher is exposed to (e.g. by PR touched files, lines, or other).
|
||||
|
||||
## Single Line Matchers
|
||||
|
||||
Let's consider the ESLint compact output:
|
||||
@@ -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 second pattern loops through the remaining lines with `loop: true` until it fails to find a match, and surfaces these lines prominently in the UI.
|
||||
|
||||
Note that the pattern matches must be on consecutive lines. The following would not result in any match findings.
|
||||
|
||||
```
|
||||
test.js
|
||||
extraneous log line of no interest
|
||||
1:0 error Missing "use strict" statement strict
|
||||
5:10 error 'addOne' is defined but never used no-unused-vars
|
||||
✖ 2 problems (2 errors, 0 warnings)
|
||||
```
|
||||
|
||||
## Adding and Removing Problem Matchers
|
||||
|
||||
Problem Matchers are enabled and removed via the toolkit [commands](commands.md#problem-matchers).
|
||||
@@ -144,6 +124,6 @@ Use ECMAScript regular expression syntax when testing patterns.
|
||||
|
||||
### File property getting dropped
|
||||
|
||||
[Enable debug logging](https://docs.github.com/en/actions/managing-workflow-runs/enabling-debug-logging) to determine why the file is getting dropped.
|
||||
[Enable debug logging](https://help.github.com/en/actions/configuring-and-managing-workflows/managing-a-workflow-run#enabling-debug-logging) to determine why the file is getting dropped.
|
||||
|
||||
This usually happens when the file does not exist or is not under the workflow repo.
|
||||
|
||||
@@ -4,6 +4,7 @@ module.exports = {
|
||||
roots: ['<rootDir>/packages'],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/__tests__/*.test.ts'],
|
||||
testRunner: 'jest-circus/runner',
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
},
|
||||
|
||||
Generated
+15824
-11497
File diff suppressed because it is too large
Load Diff
+13
-13
@@ -9,24 +9,24 @@
|
||||
"format": "prettier --write packages/**/*.ts",
|
||||
"format-check": "prettier --check packages/**/*.ts",
|
||||
"lint": "eslint packages/**/*.ts",
|
||||
"lint-fix": "eslint packages/**/*.ts --fix",
|
||||
"new-package": "scripts/create-package",
|
||||
"test": "jest --testTimeout 10000"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/node": "^12.20.13",
|
||||
"@types/signale": "^1.4.1",
|
||||
"@typescript-eslint/parser": "^4.0.0",
|
||||
"concurrently": "^6.1.0",
|
||||
"eslint": "^7.23.0",
|
||||
"eslint-plugin-github": "^4.1.3",
|
||||
"eslint-plugin-jest": "^22.21.0",
|
||||
"@types/jest": "^24.0.11",
|
||||
"@types/node": "^12.12.47",
|
||||
"@types/signale": "^1.2.1",
|
||||
"@typescript-eslint/parser": "^2.2.7",
|
||||
"concurrently": "^4.1.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-github": "^2.0.0",
|
||||
"eslint-plugin-jest": "^22.5.1",
|
||||
"flow-bin": "^0.115.0",
|
||||
"jest": "^27.2.5",
|
||||
"lerna": "^4.0.0",
|
||||
"jest": "^25.1.0",
|
||||
"jest-circus": "^24.7.1",
|
||||
"lerna": "^3.18.4",
|
||||
"prettier": "^1.19.1",
|
||||
"ts-jest": "^27.0.5",
|
||||
"typescript": "^3.9.9"
|
||||
"ts-jest": "^25.4.0",
|
||||
"typescript": "^3.7.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,27 +54,3 @@
|
||||
|
||||
- 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.
|
||||
|
||||
### 0.6.0
|
||||
|
||||
- Support upload from named pipes [#748](https://github.com/actions/toolkit/pull/748)
|
||||
- Fixes to percentage values being greater than 100% when downloading all artifacts [#889](https://github.com/actions/toolkit/pull/889)
|
||||
- Improved logging and output during artifact upload [#949](https://github.com/actions/toolkit/pull/949)
|
||||
- Improvements to client-side validation for certain invalid characters not allowed during upload: [#951](https://github.com/actions/toolkit/pull/951)
|
||||
- Faster upload speeds for certain types of large files by exempting gzip compression [#956](https://github.com/actions/toolkit/pull/956)
|
||||
- More detailed logging when dealing with chunked uploads [#957](https://github.com/actions/toolkit/pull/957)
|
||||
|
||||
### 0.6.1
|
||||
|
||||
- Fix for failing 0 byte file uploads on Windows [#962](https://github.com/actions/toolkit/pull/962)
|
||||
|
||||
### 1.0.0
|
||||
|
||||
- Update `lockfileVersion` to `v2` in `package-lock.json [#1009](https://github.com/actions/toolkit/pull/1009)
|
||||
@@ -71,7 +71,7 @@ describe('Download Tests', () => {
|
||||
setupFailedResponse()
|
||||
const downloadHttpClient = new DownloadHttpClient()
|
||||
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()
|
||||
)
|
||||
).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 () => {
|
||||
// The first http response should return a retryable status call while the subsequent call should return a 200 so
|
||||
// the download should successfully finish
|
||||
const retryableStatusCodes = [429, 500, 502, 503, 504]
|
||||
const retryableStatusCodes = [429, 502, 503, 504]
|
||||
for (const statusCode of retryableStatusCodes) {
|
||||
const fileContents = Buffer.from('try, try again\n', defaultEncoding)
|
||||
const targetPath = path.join(root, `FileC-${statusCode}.txt`)
|
||||
@@ -357,7 +357,7 @@ describe('Download Tests', () => {
|
||||
plaintext: Buffer | string
|
||||
): Promise<Buffer> {
|
||||
if (isGzip) {
|
||||
return await promisify(gzip)(plaintext)
|
||||
return <Buffer>await promisify(gzip)(plaintext)
|
||||
} else if (typeof plaintext === 'string') {
|
||||
return Buffer.from(plaintext, defaultEncoding)
|
||||
} else {
|
||||
@@ -468,7 +468,7 @@ describe('Download Tests', () => {
|
||||
function setupFailedResponse(): void {
|
||||
jest.spyOn(HttpClient.prototype, 'get').mockImplementationOnce(async () => {
|
||||
const mockMessage = new http.IncomingMessage(new net.Socket())
|
||||
mockMessage.statusCode = 400
|
||||
mockMessage.statusCode = 500
|
||||
return new Promise<HttpClientResponse>(resolve => {
|
||||
resolve({
|
||||
message: mockMessage,
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import {
|
||||
checkArtifactName,
|
||||
checkArtifactFilePath
|
||||
} from '../src/internal/path-and-artifact-name-validation'
|
||||
import * as core from '@actions/core'
|
||||
|
||||
describe('Path and artifact name validation', () => {
|
||||
beforeAll(() => {
|
||||
// mock all output so that there is less noise when running tests
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {})
|
||||
jest.spyOn(core, 'debug').mockImplementation(() => {})
|
||||
jest.spyOn(core, 'info').mockImplementation(() => {})
|
||||
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
it('Check Artifact Name for any invalid characters', () => {
|
||||
const invalidNames = [
|
||||
'my\\artifact',
|
||||
'my/artifact',
|
||||
'my"artifact',
|
||||
'my:artifact',
|
||||
'my<artifact',
|
||||
'my>artifact',
|
||||
'my|artifact',
|
||||
'my*artifact',
|
||||
'my?artifact',
|
||||
''
|
||||
]
|
||||
for (const invalidName of invalidNames) {
|
||||
expect(() => {
|
||||
checkArtifactName(invalidName)
|
||||
}).toThrow()
|
||||
}
|
||||
|
||||
const validNames = [
|
||||
'my-normal-artifact',
|
||||
'myNormalArtifact',
|
||||
'm¥ñðrmålÄr†ï£å¢†'
|
||||
]
|
||||
for (const validName of validNames) {
|
||||
expect(() => {
|
||||
checkArtifactName(validName)
|
||||
}).not.toThrow()
|
||||
}
|
||||
})
|
||||
|
||||
it('Check Artifact File Path for any invalid characters', () => {
|
||||
const invalidNames = [
|
||||
'some/invalid"artifact/path',
|
||||
'some/invalid:artifact/path',
|
||||
'some/invalid<artifact/path',
|
||||
'some/invalid>artifact/path',
|
||||
'some/invalid|artifact/path',
|
||||
'some/invalid*artifact/path',
|
||||
'some/invalid?artifact/path',
|
||||
'some/invalid\rartifact/path',
|
||||
'some/invalid\nartifact/path',
|
||||
'some/invalid\r\nartifact/path',
|
||||
''
|
||||
]
|
||||
for (const invalidName of invalidNames) {
|
||||
expect(() => {
|
||||
checkArtifactFilePath(invalidName)
|
||||
}).toThrow()
|
||||
}
|
||||
|
||||
const validNames = [
|
||||
'my/perfectly-normal/artifact-path',
|
||||
'my/perfectly\\Normal/Artifact-path',
|
||||
'm¥/ñðrmål/Är†ï£å¢†'
|
||||
]
|
||||
for (const validName of validNames) {
|
||||
expect(() => {
|
||||
checkArtifactFilePath(validName)
|
||||
}).not.toThrow()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -107,8 +107,8 @@ test('retry fails after exhausting retries', async () => {
|
||||
})
|
||||
|
||||
test('retry fails after non-retryable status code', async () => {
|
||||
await testRetry([400, 200], {
|
||||
responseCode: 400,
|
||||
errorMessage: 'test failed: Artifact service responded with 400'
|
||||
await testRetry([500, 200], {
|
||||
responseCode: 500,
|
||||
errorMessage: 'test failed: Artifact service responded with 500'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,10 +18,8 @@ if [ ! -f "$path" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
actualContent=$(cat "$path")
|
||||
if [ "$expectedContent" == "_EMPTY_" ] && [ ! -s "$path" ]; then
|
||||
exit 0
|
||||
elif [ "$actualContent" != "$expectedContent" ]; then
|
||||
actualContent=$(cat $path)
|
||||
if [ "$actualContent" != "$expectedContent" ];then
|
||||
echo "File contents are not correct, expected $expectedContent, received $actualContent"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,79 +0,0 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as tmp from 'tmp-promise'
|
||||
import * as path from 'path'
|
||||
import * as io from '../../io/src/io'
|
||||
import {promises as fs} from 'fs'
|
||||
import {createGZipFileOnDisk} from '../src/internal/upload-gzip'
|
||||
|
||||
const root = path.join(__dirname, '_temp', 'upload-gzip')
|
||||
const tempGzipFilePath = path.join(root, 'file1.gzip')
|
||||
const tempZipFilePath = path.join(root, 'file2.zip')
|
||||
const tempTarlzFilePath = path.join(root, 'file3.tar.lz')
|
||||
const tempGzFilePath = path.join(root, 'file4.tar.gz')
|
||||
const tempBz2FilePath = path.join(root, 'file5.tar.bz2')
|
||||
const temp7zFilePath = path.join(root, 'file6.7z')
|
||||
const tempNormalFilePath = path.join(root, 'file6.txt')
|
||||
|
||||
jest.mock('../src/internal/config-variables')
|
||||
|
||||
beforeAll(async () => {
|
||||
// mock all output so that there is less noise when running tests
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {})
|
||||
jest.spyOn(core, 'debug').mockImplementation(() => {})
|
||||
jest.spyOn(core, 'info').mockImplementation(() => {})
|
||||
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
||||
jest.spyOn(core, 'error').mockImplementation(() => {})
|
||||
|
||||
// clear temp directory and create files that will be "uploaded"
|
||||
await io.rmRF(root)
|
||||
await fs.mkdir(path.join(root))
|
||||
await fs.writeFile(tempGzipFilePath, 'a file with a .gzip file extension')
|
||||
await fs.writeFile(tempZipFilePath, 'a file with a .zip file extension')
|
||||
await fs.writeFile(tempTarlzFilePath, 'a file with a tar.lz file extension')
|
||||
await fs.writeFile(tempGzFilePath, 'a file with a gz file file extension')
|
||||
await fs.writeFile(tempBz2FilePath, 'a file with a .bz2 file extension')
|
||||
await fs.writeFile(temp7zFilePath, 'a file with a .7z file extension')
|
||||
await fs.writeFile(tempNormalFilePath, 'a file with a .txt file extension')
|
||||
})
|
||||
|
||||
test('Number.MAX_SAFE_INTEGER is returned when an existing compressed file is used', async () => {
|
||||
// create temporary file
|
||||
const tempFile = await tmp.file()
|
||||
|
||||
expect(await createGZipFileOnDisk(tempGzipFilePath, tempFile.path)).toEqual(
|
||||
Number.MAX_SAFE_INTEGER
|
||||
)
|
||||
expect(await createGZipFileOnDisk(tempZipFilePath, tempFile.path)).toEqual(
|
||||
Number.MAX_SAFE_INTEGER
|
||||
)
|
||||
expect(await createGZipFileOnDisk(tempTarlzFilePath, tempFile.path)).toEqual(
|
||||
Number.MAX_SAFE_INTEGER
|
||||
)
|
||||
expect(await createGZipFileOnDisk(tempGzFilePath, tempFile.path)).toEqual(
|
||||
Number.MAX_SAFE_INTEGER
|
||||
)
|
||||
expect(await createGZipFileOnDisk(tempBz2FilePath, tempFile.path)).toEqual(
|
||||
Number.MAX_SAFE_INTEGER
|
||||
)
|
||||
expect(await createGZipFileOnDisk(temp7zFilePath, tempFile.path)).toEqual(
|
||||
Number.MAX_SAFE_INTEGER
|
||||
)
|
||||
expect(
|
||||
await createGZipFileOnDisk(tempNormalFilePath, tempFile.path)
|
||||
).not.toEqual(Number.MAX_SAFE_INTEGER)
|
||||
})
|
||||
|
||||
test('gzip file on disk gets successfully created', async () => {
|
||||
// create temporary file
|
||||
const tempFile = await tmp.file()
|
||||
|
||||
const gzipFileSize = await createGZipFileOnDisk(
|
||||
tempNormalFilePath,
|
||||
tempFile.path
|
||||
)
|
||||
const fileStat = await fs.stat(tempNormalFilePath)
|
||||
const totalFileSize = fileStat.size
|
||||
|
||||
// original file and gzip file should not be equal in size
|
||||
expect(gzipFileSize).not.toEqual(totalFileSize)
|
||||
})
|
||||
@@ -2,10 +2,6 @@ import * as http from 'http'
|
||||
import * as io from '../../io/src/io'
|
||||
import * as net from 'net'
|
||||
import * as path from 'path'
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {exec, execSync} from 'child_process'
|
||||
import {createGunzip} from 'zlib'
|
||||
import {promisify} from 'util'
|
||||
import {UploadHttpClient} from '../src/internal/upload-http-client'
|
||||
import * as core from '@actions/core'
|
||||
import {promises as fs} from 'fs'
|
||||
@@ -178,59 +174,6 @@ describe('Upload Tests', () => {
|
||||
expect(uploadResult.uploadSize).toEqual(expectedTotalSize)
|
||||
})
|
||||
|
||||
function hasMkfifo(): boolean {
|
||||
try {
|
||||
// make sure we drain the stdout
|
||||
return (
|
||||
process.platform !== 'win32' &&
|
||||
execSync('which mkfifo').toString().length > 0
|
||||
)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
const withMkfifoIt = hasMkfifo() ? it : it.skip
|
||||
withMkfifoIt(
|
||||
'Upload Artifact with content from named pipe - Success',
|
||||
async () => {
|
||||
// create a named pipe 'pipe' with content 'hello pipe'
|
||||
const content = Buffer.from('hello pipe')
|
||||
const pipeFilePath = path.join(root, 'pipe')
|
||||
await promisify(exec)('mkfifo pipe', {cwd: root})
|
||||
// don't want to await here as that would block until read
|
||||
fs.writeFile(pipeFilePath, content)
|
||||
|
||||
const artifactName = 'successful-artifact'
|
||||
const uploadSpecification: UploadSpecification[] = [
|
||||
{
|
||||
absoluteFilePath: pipeFilePath,
|
||||
uploadFilePath: `${artifactName}/pipe`
|
||||
}
|
||||
]
|
||||
|
||||
const uploadUrl = `${getRuntimeUrl()}_apis/resources/Containers/13`
|
||||
const uploadHttpClient = new UploadHttpClient()
|
||||
const uploadResult = await uploadHttpClient.uploadArtifactToFileContainer(
|
||||
uploadUrl,
|
||||
uploadSpecification
|
||||
)
|
||||
|
||||
// accesses the ReadableStream that was passed into sendStream
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
const stream = mocked(HttpClient.prototype.sendStream).mock.calls[0][2]
|
||||
expect(stream).not.toBeNull()
|
||||
// decompresses the passed stream
|
||||
const data: Buffer[] = []
|
||||
for await (const chunk of stream.pipe(createGunzip())) {
|
||||
data.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string))
|
||||
}
|
||||
const uploaded = Buffer.concat(data)
|
||||
|
||||
expect(uploadResult.failedItems.length).toEqual(0)
|
||||
expect(uploaded).toEqual(content)
|
||||
}
|
||||
)
|
||||
|
||||
it('Upload Artifact - Failed Single File Upload', async () => {
|
||||
const uploadSpecification: UploadSpecification[] = [
|
||||
{
|
||||
|
||||
@@ -46,6 +46,66 @@ describe('Utils', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('Check Artifact Name for any invalid characters', () => {
|
||||
const invalidNames = [
|
||||
'my\\artifact',
|
||||
'my/artifact',
|
||||
'my"artifact',
|
||||
'my:artifact',
|
||||
'my<artifact',
|
||||
'my>artifact',
|
||||
'my|artifact',
|
||||
'my*artifact',
|
||||
'my?artifact',
|
||||
''
|
||||
]
|
||||
for (const invalidName of invalidNames) {
|
||||
expect(() => {
|
||||
utils.checkArtifactName(invalidName)
|
||||
}).toThrow()
|
||||
}
|
||||
|
||||
const validNames = [
|
||||
'my-normal-artifact',
|
||||
'myNormalArtifact',
|
||||
'm¥ñðrmålÄr†ï£å¢†'
|
||||
]
|
||||
for (const validName of validNames) {
|
||||
expect(() => {
|
||||
utils.checkArtifactName(validName)
|
||||
}).not.toThrow()
|
||||
}
|
||||
})
|
||||
|
||||
it('Check Artifact File Path for any invalid characters', () => {
|
||||
const invalidNames = [
|
||||
'some/invalid"artifact/path',
|
||||
'some/invalid:artifact/path',
|
||||
'some/invalid<artifact/path',
|
||||
'some/invalid>artifact/path',
|
||||
'some/invalid|artifact/path',
|
||||
'some/invalid*artifact/path',
|
||||
'some/invalid?artifact/path',
|
||||
''
|
||||
]
|
||||
for (const invalidName of invalidNames) {
|
||||
expect(() => {
|
||||
utils.checkArtifactFilePath(invalidName)
|
||||
}).toThrow()
|
||||
}
|
||||
|
||||
const validNames = [
|
||||
'my/perfectly-normal/artifact-path',
|
||||
'my/perfectly\\Normal/Artifact-path',
|
||||
'm¥/ñðrmål/Är†ï£å¢†'
|
||||
]
|
||||
for (const validName of validNames) {
|
||||
expect(() => {
|
||||
utils.checkArtifactFilePath(validName)
|
||||
}).not.toThrow()
|
||||
}
|
||||
})
|
||||
|
||||
it('Test negative artifact retention throws', () => {
|
||||
expect(() => {
|
||||
utils.getProperRetention(-1, undefined)
|
||||
|
||||
Generated
+34
-220
@@ -1,217 +1,31 @@
|
||||
{
|
||||
"name": "@actions/artifact",
|
||||
"version": "0.6.1",
|
||||
"lockfileVersion": 2,
|
||||
"version": "0.5.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@actions/artifact",
|
||||
"version": "0.6.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.6",
|
||||
"@actions/http-client": "^1.0.11",
|
||||
"tmp": "^0.2.1",
|
||||
"tmp-promise": "^3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/tmp": "^0.2.1",
|
||||
"typescript": "^3.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/core": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz",
|
||||
"integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==",
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^1.0.11"
|
||||
}
|
||||
},
|
||||
"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/tmp": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.3.tgz",
|
||||
"integrity": "sha512-dDZH/tXzwjutnuk4UacGgFRwV+JSLaXL1ikvidfJprkb7L9Nx1njcRHHmi3Dsvt7pgqqTEeucQuOrWHPFgzVHA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "bin.js"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
|
||||
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
|
||||
"dependencies": {
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tmp-promise": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz",
|
||||
"integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==",
|
||||
"dependencies": {
|
||||
"tmp": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "3.9.10",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
|
||||
"integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz",
|
||||
"integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==",
|
||||
"requires": {
|
||||
"@actions/http-client": "^1.0.11"
|
||||
}
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
|
||||
"integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
|
||||
},
|
||||
"@actions/http-client": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
|
||||
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.8.tgz",
|
||||
"integrity": "sha512-G4JjJ6f9Hb3Zvejj+ewLLKLf99ZC+9v+yCxoYf9vSyH+WkzPLB2LuUtRMGNkooMqdugGBFStIKXOuvH1W+EctA==",
|
||||
"requires": {
|
||||
"tunnel": "0.0.6"
|
||||
}
|
||||
},
|
||||
"@types/tmp": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.3.tgz",
|
||||
"integrity": "sha512-dDZH/tXzwjutnuk4UacGgFRwV+JSLaXL1ikvidfJprkb7L9Nx1njcRHHmi3Dsvt7pgqqTEeucQuOrWHPFgzVHA==",
|
||||
"dev": true
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.1.0.tgz",
|
||||
"integrity": "sha512-6IwZ9HzWbCq6XoQWhxLpDjuADodH/MKXRUIDFudvgjcVdjFknvmR+DNsoUeer4XPrEnrZs04Jj+kfV9pFsrhmA=="
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
@@ -233,9 +47,9 @@
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
@@ -260,9 +74,9 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@@ -281,27 +95,27 @@
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
||||
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"tmp": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
|
||||
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz",
|
||||
"integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==",
|
||||
"requires": {
|
||||
"rimraf": "^3.0.0"
|
||||
"rimraf": "^2.6.3"
|
||||
}
|
||||
},
|
||||
"tmp-promise": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz",
|
||||
"integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-2.0.2.tgz",
|
||||
"integrity": "sha512-zl71nFWjPKW2KXs+73gEk8RmqvtAeXPxhWDkTUoa3MSMkjq3I+9OeknjF178MQoMYsdqL730hfzvNfEkePxq9Q==",
|
||||
"requires": {
|
||||
"tmp": "^0.2.0"
|
||||
"tmp": "0.1.0"
|
||||
}
|
||||
},
|
||||
"tunnel": {
|
||||
@@ -310,9 +124,9 @@
|
||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
|
||||
},
|
||||
"typescript": {
|
||||
"version": "3.9.10",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
|
||||
"integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==",
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
|
||||
"integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
|
||||
"dev": true
|
||||
},
|
||||
"wrappy": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/artifact",
|
||||
"version": "1.0.0",
|
||||
"version": "0.5.0",
|
||||
"preview": true,
|
||||
"description": "Actions artifact lib",
|
||||
"keywords": [
|
||||
@@ -38,12 +38,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.6",
|
||||
"@actions/http-client": "^1.0.11",
|
||||
"tmp": "^0.2.1",
|
||||
"tmp-promise": "^3.0.2"
|
||||
"@actions/http-client": "^1.0.7",
|
||||
"@types/tmp": "^0.1.0",
|
||||
"tmp": "^0.1.0",
|
||||
"tmp-promise": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/tmp": "^0.2.1",
|
||||
"typescript": "^3.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import {UploadOptions} from './upload-options'
|
||||
import {DownloadOptions} from './download-options'
|
||||
import {DownloadResponse} from './download-response'
|
||||
import {
|
||||
checkArtifactName,
|
||||
createDirectoriesForArtifact,
|
||||
createEmptyFilesForArtifact
|
||||
} from './utils'
|
||||
import {checkArtifactName} from './path-and-artifact-name-validation'
|
||||
import {DownloadHttpClient} from './download-http-client'
|
||||
import {getDownloadSpecification} from './download-specification'
|
||||
import {getWorkSpaceDirectory} from './config-variables'
|
||||
@@ -72,10 +72,6 @@ export class DefaultArtifactClient implements ArtifactClient {
|
||||
rootDirectory: string,
|
||||
options?: UploadOptions | undefined
|
||||
): Promise<UploadResponse> {
|
||||
core.info(
|
||||
`Starting artifact upload
|
||||
For more detailed logs during the artifact upload process, enable step-debugging: https://docs.github.com/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging#enabling-step-debug-logging`
|
||||
)
|
||||
checkArtifactName(name)
|
||||
|
||||
// Get specification for the files being uploaded
|
||||
@@ -107,11 +103,7 @@ For more detailed logs during the artifact upload process, enable step-debugging
|
||||
'No URL provided by the Artifact Service to upload an artifact to'
|
||||
)
|
||||
}
|
||||
|
||||
core.debug(`Upload Resource URL: ${response.fileContainerResourceUrl}`)
|
||||
core.info(
|
||||
`Container for artifact "${name}" successfully created. Starting upload of file(s)`
|
||||
)
|
||||
|
||||
// Upload each of the files that were found concurrently
|
||||
const uploadResult = await uploadHttpClient.uploadArtifactToFileContainer(
|
||||
@@ -122,27 +114,10 @@ For more detailed logs during the artifact upload process, enable step-debugging
|
||||
|
||||
// Update the size of the artifact to indicate we are done uploading
|
||||
// The uncompressed size is used for display when downloading a zip of the artifact from the UI
|
||||
core.info(
|
||||
`File upload process has finished. Finalizing the artifact upload`
|
||||
)
|
||||
await uploadHttpClient.patchArtifactSize(uploadResult.totalSize, name)
|
||||
|
||||
if (uploadResult.failedItems.length > 0) {
|
||||
core.info(
|
||||
`Upload finished. There were ${uploadResult.failedItems.length} items that failed to upload`
|
||||
)
|
||||
} else {
|
||||
core.info(
|
||||
`Artifact has been finalized. All files have been successfully uploaded!`
|
||||
)
|
||||
}
|
||||
|
||||
core.info(
|
||||
`
|
||||
The raw size of all the files that were specified for upload is ${uploadResult.totalSize} bytes
|
||||
The size of all the files that were uploaded is ${uploadResult.uploadSize} bytes. This takes into account any gzip compression used to reduce the upload size, time and storage
|
||||
|
||||
Note: The size of downloaded zips can differ significantly from the reported size. For more information see: https://github.com/actions/upload-artifact#zipped-artifact-downloads \r\n`
|
||||
`Finished uploading artifact ${name}. Reported size is ${uploadResult.uploadSize} bytes. There were ${uploadResult.failedItems.length} items that failed to upload`
|
||||
)
|
||||
|
||||
uploadResponse.artifactItems = uploadSpecification.map(
|
||||
@@ -240,9 +215,6 @@ Note: The size of downloaded zips can differ significantly from the reported siz
|
||||
while (downloadedArtifacts < artifacts.count) {
|
||||
const currentArtifactToDownload = artifacts.value[downloadedArtifacts]
|
||||
downloadedArtifacts += 1
|
||||
core.info(
|
||||
`starting download of artifact ${currentArtifactToDownload.name} : ${downloadedArtifacts}/${artifacts.count}`
|
||||
)
|
||||
|
||||
// Get container entries for the specific artifact
|
||||
const items = await downloadHttpClient.getContainerItems(
|
||||
|
||||
@@ -29,20 +29,8 @@ export interface PatchArtifactSizeSuccessResponse {
|
||||
}
|
||||
|
||||
export interface UploadResults {
|
||||
/**
|
||||
* The size in bytes of data that was transferred during the upload process to the actions backend service. This takes into account possible
|
||||
* gzip compression to reduce the amount of data that needs to be transferred
|
||||
*/
|
||||
uploadSize: number
|
||||
|
||||
/**
|
||||
* The raw size of the files that were specified for upload
|
||||
*/
|
||||
totalSize: number
|
||||
|
||||
/**
|
||||
* An array of files that failed to upload
|
||||
*/
|
||||
failedItems: string[]
|
||||
}
|
||||
|
||||
|
||||
@@ -228,6 +228,9 @@ export class DownloadHttpClient {
|
||||
let response: IHttpClientResponse
|
||||
try {
|
||||
response = await makeDownloadRequest()
|
||||
if (core.isDebug()) {
|
||||
displayHttpDiagnostics(response)
|
||||
}
|
||||
} catch (error) {
|
||||
// if an error is caught, it is usually indicative of a timeout so retry the download
|
||||
core.info('An error occurred while attempting to download a file')
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import {info} from '@actions/core'
|
||||
|
||||
/**
|
||||
* Invalid characters that cannot be in the artifact name or an uploaded file. Will be rejected
|
||||
* from the server if attempted to be sent over. These characters are not allowed due to limitations with certain
|
||||
* file systems such as NTFS. To maintain platform-agnostic behavior, all characters that are not supported by an
|
||||
* individual filesystem/platform will not be supported on all fileSystems/platforms
|
||||
*
|
||||
* FilePaths can include characters such as \ and / which are not permitted in the artifact name alone
|
||||
*/
|
||||
const invalidArtifactFilePathCharacters = new Map<string, string>([
|
||||
['"', ' Double quote "'],
|
||||
[':', ' Colon :'],
|
||||
['<', ' Less than <'],
|
||||
['>', ' Greater than >'],
|
||||
['|', ' Vertical bar |'],
|
||||
['*', ' Asterisk *'],
|
||||
['?', ' Question mark ?'],
|
||||
['\r', ' Carriage return \\r'],
|
||||
['\n', ' Line feed \\n']
|
||||
])
|
||||
|
||||
const invalidArtifactNameCharacters = new Map<string, string>([
|
||||
...invalidArtifactFilePathCharacters,
|
||||
['\\', ' Backslash \\'],
|
||||
['/', ' Forward slash /']
|
||||
])
|
||||
|
||||
/**
|
||||
* Scans the name of the artifact to make sure there are no illegal characters
|
||||
*/
|
||||
export function checkArtifactName(name: string): void {
|
||||
if (!name) {
|
||||
throw new Error(`Artifact name: ${name}, is incorrectly provided`)
|
||||
}
|
||||
|
||||
for (const [
|
||||
invalidCharacterKey,
|
||||
errorMessageForCharacter
|
||||
] of invalidArtifactNameCharacters) {
|
||||
if (name.includes(invalidCharacterKey)) {
|
||||
throw new Error(
|
||||
`Artifact name is not valid: ${name}. Contains the following character: ${errorMessageForCharacter}
|
||||
|
||||
Invalid characters include: ${Array.from(
|
||||
invalidArtifactNameCharacters.values()
|
||||
).toString()}
|
||||
|
||||
These characters are not allowed in the artifact name due to limitations with certain file systems such as NTFS. To maintain file system agnostic behavior, these characters are intentionally not allowed to prevent potential problems with downloads on different file systems.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
info(`Artifact name is valid!`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the name of the filePath used to make sure there are no illegal characters
|
||||
*/
|
||||
export function checkArtifactFilePath(path: string): void {
|
||||
if (!path) {
|
||||
throw new Error(`Artifact path: ${path}, is incorrectly provided`)
|
||||
}
|
||||
|
||||
for (const [
|
||||
invalidCharacterKey,
|
||||
errorMessageForCharacter
|
||||
] of invalidArtifactFilePathCharacters) {
|
||||
if (path.includes(invalidCharacterKey)) {
|
||||
throw new Error(
|
||||
`Artifact path is not valid: ${path}. Contains the following character: ${errorMessageForCharacter}
|
||||
|
||||
Invalid characters include: ${Array.from(
|
||||
invalidArtifactFilePathCharacters.values()
|
||||
).toString()}
|
||||
|
||||
The following characters are not allowed in files that are uploaded due to limitations with certain file systems such as NTFS. To maintain file system agnostic behavior, these characters are intentionally not allowed to prevent potential problems with downloads on different file systems.
|
||||
`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export async function retry(
|
||||
throw Error(`${name} failed: ${errorMessage}`)
|
||||
}
|
||||
|
||||
export async function retryHttpClientRequest(
|
||||
export async function retryHttpClientRequest<T>(
|
||||
name: string,
|
||||
method: () => Promise<IHttpClientResponse>,
|
||||
customErrorMessages: Map<number, string> = new Map(),
|
||||
|
||||
@@ -14,15 +14,16 @@ export class StatusReporter {
|
||||
private displayFrequencyInMilliseconds: number
|
||||
private largeFiles = new Map<string, string>()
|
||||
private totalFileStatus: NodeJS.Timeout | undefined
|
||||
private largeFileStatus: NodeJS.Timeout | undefined
|
||||
|
||||
constructor(displayFrequencyInMilliseconds: number) {
|
||||
this.totalFileStatus = undefined
|
||||
this.largeFileStatus = undefined
|
||||
this.displayFrequencyInMilliseconds = displayFrequencyInMilliseconds
|
||||
}
|
||||
|
||||
setTotalNumberOfFilesToProcess(fileTotal: number): void {
|
||||
this.totalNumberOfFilesToProcess = fileTotal
|
||||
this.processedCount = 0
|
||||
}
|
||||
|
||||
start(): void {
|
||||
@@ -42,29 +43,42 @@ export class StatusReporter {
|
||||
)}%)`
|
||||
)
|
||||
}, this.displayFrequencyInMilliseconds)
|
||||
|
||||
// displays extra information about any large files that take a significant amount of time to upload or download every 1 second
|
||||
this.largeFileStatus = setInterval(() => {
|
||||
for (const value of Array.from(this.largeFiles.values())) {
|
||||
info(value)
|
||||
}
|
||||
// delete all entries in the map after displaying the information so it will not be displayed again unless explicitly added
|
||||
this.largeFiles.clear()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// if there is a large file that is being uploaded in chunks, this is used to display extra information about the status of the upload
|
||||
updateLargeFileStatus(
|
||||
fileName: string,
|
||||
chunkStartIndex: number,
|
||||
chunkEndIndex: number,
|
||||
totalUploadFileSize: number
|
||||
numerator: number,
|
||||
denominator: number
|
||||
): void {
|
||||
// display 1 decimal place without any rounding
|
||||
const percentage = this.formatPercentage(chunkEndIndex, totalUploadFileSize)
|
||||
info(
|
||||
`Uploaded ${fileName} (${percentage.slice(
|
||||
0,
|
||||
percentage.indexOf('.') + 2
|
||||
)}%) bytes ${chunkStartIndex}:${chunkEndIndex}`
|
||||
)
|
||||
const percentage = this.formatPercentage(numerator, denominator)
|
||||
const displayInformation = `Uploading ${fileName} (${percentage.slice(
|
||||
0,
|
||||
percentage.indexOf('.') + 2
|
||||
)}%)`
|
||||
|
||||
// any previously added display information should be overwritten for the specific large file because a map is being used
|
||||
this.largeFiles.set(fileName, displayInformation)
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.totalFileStatus) {
|
||||
clearInterval(this.totalFileStatus)
|
||||
}
|
||||
|
||||
if (this.largeFileStatus) {
|
||||
clearInterval(this.largeFileStatus)
|
||||
}
|
||||
}
|
||||
|
||||
incrementProcessedCount(): void {
|
||||
|
||||
@@ -3,20 +3,6 @@ import * as zlib from 'zlib'
|
||||
import {promisify} from 'util'
|
||||
const stat = promisify(fs.stat)
|
||||
|
||||
/**
|
||||
* GZipping certain files that are already compressed will likely not yield further size reductions. Creating large temporary gzip
|
||||
* files then will just waste a lot of time before ultimately being discarded (especially for very large files).
|
||||
* If any of these types of files are encountered then on-disk gzip creation will be skipped and the original file will be uploaded as-is
|
||||
*/
|
||||
const gzipExemptFileExtensions = [
|
||||
'.gzip',
|
||||
'.zip',
|
||||
'.tar.lz',
|
||||
'.tar.gz',
|
||||
'.tar.bz2',
|
||||
'.7z'
|
||||
]
|
||||
|
||||
/**
|
||||
* Creates a Gzip compressed file of an original file at the provided temporary filepath location
|
||||
* @param {string} originalFilePath filepath of whatever will be compressed. The original file will be unmodified
|
||||
@@ -27,13 +13,6 @@ export async function createGZipFileOnDisk(
|
||||
originalFilePath: string,
|
||||
tempFilePath: string
|
||||
): Promise<number> {
|
||||
for (const gzipExemptExtension of gzipExemptFileExtensions) {
|
||||
if (originalFilePath.endsWith(gzipExemptExtension)) {
|
||||
// return a really large number so that the original file gets uploaded
|
||||
return Number.MAX_SAFE_INTEGER
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const inputStream = fs.createReadStream(originalFilePath)
|
||||
const gzip = zlib.createGzip()
|
||||
|
||||
@@ -219,41 +219,29 @@ export class UploadHttpClient {
|
||||
httpClientIndex: number,
|
||||
parameters: UploadFileParameters
|
||||
): Promise<UploadFileResult> {
|
||||
const fileStat: fs.Stats = await stat(parameters.file)
|
||||
const totalFileSize = fileStat.size
|
||||
const isFIFO = fileStat.isFIFO()
|
||||
const totalFileSize: number = (await stat(parameters.file)).size
|
||||
let offset = 0
|
||||
let isUploadSuccessful = true
|
||||
let failedChunkSizes = 0
|
||||
let uploadFileSize = 0
|
||||
let isGzip = true
|
||||
|
||||
// the file that is being uploaded is less than 64k in size to increase throughput and to minimize disk I/O
|
||||
// the file that is being uploaded is less than 64k in size, to increase throughput and to minimize disk I/O
|
||||
// for creating a new GZip file, an in-memory buffer is used for compression
|
||||
// with named pipes the file size is reported as zero in that case don't read the file in memory
|
||||
if (!isFIFO && totalFileSize < 65536) {
|
||||
core.debug(
|
||||
`${parameters.file} is less than 64k in size. Creating a gzip file in-memory to potentially reduce the upload size`
|
||||
)
|
||||
if (totalFileSize < 65536) {
|
||||
const buffer = await createGZipFileInBuffer(parameters.file)
|
||||
|
||||
// An open stream is needed in the event of a failure and we need to retry. If a NodeJS.ReadableStream is directly passed in,
|
||||
//An open stream is needed in the event of a failure and we need to retry. If a NodeJS.ReadableStream is directly passed in,
|
||||
// it will not properly get reset to the start of the stream if a chunk upload needs to be retried
|
||||
let openUploadStream: () => NodeJS.ReadableStream
|
||||
|
||||
if (totalFileSize < buffer.byteLength) {
|
||||
// compression did not help with reducing the size, use a readable stream from the original file for upload
|
||||
core.debug(
|
||||
`The gzip file created for ${parameters.file} did not help with reducing the size of the file. The original file will be uploaded as-is`
|
||||
)
|
||||
openUploadStream = () => fs.createReadStream(parameters.file)
|
||||
isGzip = false
|
||||
uploadFileSize = totalFileSize
|
||||
} else {
|
||||
// create a readable stream using a PassThrough stream that is both readable and writable
|
||||
core.debug(
|
||||
`A gzip file created for ${parameters.file} helped with reducing the size of the original file. The file will be uploaded using gzip.`
|
||||
)
|
||||
openUploadStream = () => {
|
||||
const passThrough = new stream.PassThrough()
|
||||
passThrough.end(buffer)
|
||||
@@ -289,9 +277,6 @@ export class UploadHttpClient {
|
||||
// the file that is being uploaded is greater than 64k in size, a temporary file gets created on disk using the
|
||||
// npm tmp-promise package and this file gets used to create a GZipped file
|
||||
const tempFile = await tmp.file()
|
||||
core.debug(
|
||||
`${parameters.file} is greater than 64k in size. Creating a gzip file on-disk ${tempFile.path} to potentially reduce the upload size`
|
||||
)
|
||||
|
||||
// create a GZip file of the original file being uploaded, the original file should not be modified in any way
|
||||
uploadFileSize = await createGZipFileOnDisk(
|
||||
@@ -302,18 +287,10 @@ export class UploadHttpClient {
|
||||
let uploadFilePath = tempFile.path
|
||||
|
||||
// compression did not help with size reduction, use the original file for upload and delete the temp GZip file
|
||||
// for named pipes totalFileSize is zero, this assumes compression did help
|
||||
if (!isFIFO && totalFileSize < uploadFileSize) {
|
||||
core.debug(
|
||||
`The gzip file created for ${parameters.file} did not help with reducing the size of the file. The original file will be uploaded as-is`
|
||||
)
|
||||
if (totalFileSize < uploadFileSize) {
|
||||
uploadFileSize = totalFileSize
|
||||
uploadFilePath = parameters.file
|
||||
isGzip = false
|
||||
} else {
|
||||
core.debug(
|
||||
`The gzip file created for ${parameters.file} is smaller than the original file. The file will be uploaded using gzip.`
|
||||
)
|
||||
}
|
||||
|
||||
let abortFileUpload = false
|
||||
@@ -324,8 +301,17 @@ export class UploadHttpClient {
|
||||
parameters.maxChunkSize
|
||||
)
|
||||
|
||||
const startChunkIndex = offset
|
||||
const endChunkIndex = offset + chunkSize - 1
|
||||
// if an individual file is greater than 100MB (1024*1024*100) in size, display extra information about the upload status
|
||||
if (uploadFileSize > 104857600) {
|
||||
this.statusReporter.updateLargeFileStatus(
|
||||
parameters.file,
|
||||
offset,
|
||||
uploadFileSize
|
||||
)
|
||||
}
|
||||
|
||||
const start = offset
|
||||
const end = offset + chunkSize - 1
|
||||
offset += parameters.maxChunkSize
|
||||
|
||||
if (abortFileUpload) {
|
||||
@@ -339,12 +325,12 @@ export class UploadHttpClient {
|
||||
parameters.resourceUrl,
|
||||
() =>
|
||||
fs.createReadStream(uploadFilePath, {
|
||||
start: startChunkIndex,
|
||||
end: endChunkIndex,
|
||||
start,
|
||||
end,
|
||||
autoClose: false
|
||||
}),
|
||||
startChunkIndex,
|
||||
endChunkIndex,
|
||||
start,
|
||||
end,
|
||||
uploadFileSize,
|
||||
isGzip,
|
||||
totalFileSize
|
||||
@@ -357,22 +343,11 @@ export class UploadHttpClient {
|
||||
failedChunkSizes += chunkSize
|
||||
core.warning(`Aborting upload for ${parameters.file} due to failure`)
|
||||
abortFileUpload = true
|
||||
} else {
|
||||
// if an individual file is greater than 8MB (1024*1024*8) in size, display extra information about the upload status
|
||||
if (uploadFileSize > 8388608) {
|
||||
this.statusReporter.updateLargeFileStatus(
|
||||
parameters.file,
|
||||
startChunkIndex,
|
||||
endChunkIndex,
|
||||
uploadFileSize
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the temporary file that was created as part of the upload. If the temp file does not get manually deleted by
|
||||
// calling cleanup, it gets removed when the node process exits. For more info see: https://www.npmjs.com/package/tmp-promise#about
|
||||
core.debug(`deleting temporary gzip file ${tempFile.path}`)
|
||||
await tempFile.cleanup()
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as fs from 'fs'
|
||||
import {debug} from '@actions/core'
|
||||
import {join, normalize, resolve} from 'path'
|
||||
import {checkArtifactFilePath} from './path-and-artifact-name-validation'
|
||||
import {checkArtifactName, checkArtifactFilePath} from './utils'
|
||||
|
||||
export interface UploadSpecification {
|
||||
absoluteFilePath: string
|
||||
@@ -19,7 +19,8 @@ export function getUploadSpecification(
|
||||
rootDirectory: string,
|
||||
artifactFiles: string[]
|
||||
): UploadSpecification[] {
|
||||
// artifact name was checked earlier on, no need to check again
|
||||
checkArtifactName(artifactName)
|
||||
|
||||
const specifications: UploadSpecification[] = []
|
||||
|
||||
if (!fs.existsSync(rootDirectory)) {
|
||||
|
||||
@@ -30,7 +30,7 @@ export function getExponentialRetryTimeInMilliseconds(
|
||||
const maxTime = minTime * getRetryMultiplier()
|
||||
|
||||
// returns a random number between the minTime (inclusive) and the maxTime (exclusive)
|
||||
return Math.trunc(Math.random() * (maxTime - minTime) + minTime)
|
||||
return Math.random() * (maxTime - minTime) + minTime
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,9 +72,8 @@ export function isRetryableStatusCode(statusCode: number | undefined): boolean {
|
||||
|
||||
const retryableStatusCodes = [
|
||||
HttpCodes.BadGateway,
|
||||
HttpCodes.GatewayTimeout,
|
||||
HttpCodes.InternalServerError,
|
||||
HttpCodes.ServiceUnavailable,
|
||||
HttpCodes.GatewayTimeout,
|
||||
HttpCodes.TooManyRequests,
|
||||
413 // Payload Too Large
|
||||
]
|
||||
@@ -237,6 +236,55 @@ Header Information: ${JSON.stringify(response.message.headers, undefined, 2)}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalid characters that cannot be in the artifact name or an uploaded file. Will be rejected
|
||||
* from the server if attempted to be sent over. These characters are not allowed due to limitations with certain
|
||||
* file systems such as NTFS. To maintain platform-agnostic behavior, all characters that are not supported by an
|
||||
* individual filesystem/platform will not be supported on all fileSystems/platforms
|
||||
*
|
||||
* FilePaths can include characters such as \ and / which are not permitted in the artifact name alone
|
||||
*/
|
||||
const invalidArtifactFilePathCharacters = ['"', ':', '<', '>', '|', '*', '?']
|
||||
const invalidArtifactNameCharacters = [
|
||||
...invalidArtifactFilePathCharacters,
|
||||
'\\',
|
||||
'/'
|
||||
]
|
||||
|
||||
/**
|
||||
* Scans the name of the artifact to make sure there are no illegal characters
|
||||
*/
|
||||
export function checkArtifactName(name: string): void {
|
||||
if (!name) {
|
||||
throw new Error(`Artifact name: ${name}, is incorrectly provided`)
|
||||
}
|
||||
|
||||
for (const invalidChar of invalidArtifactNameCharacters) {
|
||||
if (name.includes(invalidChar)) {
|
||||
throw new Error(
|
||||
`Artifact name is not valid: ${name}. Contains character: "${invalidChar}". Invalid artifact name characters include: ${invalidArtifactNameCharacters.toString()}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the name of the filePath used to make sure there are no illegal characters
|
||||
*/
|
||||
export function checkArtifactFilePath(path: string): void {
|
||||
if (!path) {
|
||||
throw new Error(`Artifact path: ${path}, is incorrectly provided`)
|
||||
}
|
||||
|
||||
for (const invalidChar of invalidArtifactFilePathCharacters) {
|
||||
if (path.includes(invalidChar)) {
|
||||
throw new Error(
|
||||
`Artifact path is not valid: ${path}. Contains character: "${invalidChar}". Invalid characters include: ${invalidArtifactFilePathCharacters.toString()}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createDirectoriesForArtifact(
|
||||
directories: string[]
|
||||
): Promise<void> {
|
||||
|
||||
Vendored
+15
-15
@@ -4,26 +4,12 @@
|
||||
|
||||
See ["Caching dependencies to speed up workflows"](https://help.github.com/github/automating-your-workflow-with-github-actions/caching-dependencies-to-speed-up-workflows) for how caching works.
|
||||
|
||||
Note that GitHub will remove any cache entries that have not been accessed in over 7 days. There is no limit on the number of caches you can store, but the total size of all caches in a repository is limited to 10 GB. If you exceed this limit, GitHub will save your cache but will begin evicting caches until the total size is less than 10 GB.
|
||||
Note that GitHub will remove any cache entries that have not been accessed in over 7 days. There is no limit on the number of caches you can store, but the total size of all caches in a repository is limited to 5 GB. If you exceed this limit, GitHub will save your cache but will begin evicting caches until the total size is less than 5 GB.
|
||||
|
||||
## Usage
|
||||
|
||||
This package is used by the v2+ versions of our first party cache action. You can find an example implementation in the cache repo [here](https://github.com/actions/cache).
|
||||
|
||||
#### Save Cache
|
||||
|
||||
Saves a cache containing the files in `paths` using the `key` provided. The files would be compressed using zstandard compression algorithm if zstd is installed, otherwise gzip is used. Function returns the cache id if the cache was saved succesfully and throws an error if cache upload fails.
|
||||
|
||||
```js
|
||||
const cache = require('@actions/cache');
|
||||
const paths = [
|
||||
'node_modules',
|
||||
'packages/*/node_modules/'
|
||||
]
|
||||
const key = 'npm-foobar-d5ea0750'
|
||||
const cacheId = await cache.saveCache(paths, key)
|
||||
```
|
||||
|
||||
#### Restore Cache
|
||||
|
||||
Restores a cache based on `key` and `restoreKeys` to the `paths` provided. Function returns the cache key for cache hit and returns undefined if cache not found.
|
||||
@@ -42,3 +28,17 @@ const restoreKeys = [
|
||||
const cacheKey = await cache.restoreCache(paths, key, restoreKeys)
|
||||
```
|
||||
|
||||
#### Save Cache
|
||||
|
||||
Saves a cache containing the files in `paths` using the `key` provided. The files would be compressed using zstandard compression algorithm if zstd is installed, otherwise gzip is used. Function returns the cache id if the cache was saved succesfully and throws an error if cache upload fails.
|
||||
|
||||
```js
|
||||
const cache = require('@actions/cache');
|
||||
const paths = [
|
||||
'node_modules',
|
||||
'packages/*/node_modules/'
|
||||
]
|
||||
const key = 'npm-foobar-d5ea0750'
|
||||
const cacheId = await cache.saveCache(paths, key)
|
||||
```
|
||||
|
||||
|
||||
Vendored
+1
-20
@@ -36,23 +36,4 @@
|
||||
|
||||
### 1.0.6
|
||||
- Make caching more verbose [#650](https://github.com/actions/toolkit/pull/650)
|
||||
- Use GNU tar on macOS if available [#701](https://github.com/actions/toolkit/pull/701)
|
||||
|
||||
### 1.0.7
|
||||
- Fixes permissions issue extracting archives with GNU tar on macOS ([issue](https://github.com/actions/cache/issues/527))
|
||||
|
||||
### 1.0.8
|
||||
- Increase the allowed artifact cache size from 5GB to 10GB ([issue](https://github.com/actions/cache/discussions/497))
|
||||
|
||||
### 1.0.9
|
||||
- Use @azure/ms-rest-js v2.6.0
|
||||
- Use @azure/storage-blob v12.8.0
|
||||
|
||||
### 1.0.10
|
||||
- Update `lockfileVersion` to `v2` in `package-lock.json [#1022](https://github.com/actions/toolkit/pull/1022)
|
||||
|
||||
### 1.0.11
|
||||
- Fix file downloads > 2GB([issue](https://github.com/actions/cache/issues/773))
|
||||
|
||||
### 2.0.0
|
||||
- Added support to check if Actions cache service feature is available or not [#1028](https://github.com/actions/toolkit/pull/1028)
|
||||
- Use GNU tar on macOS if available [#701](https://github.com/actions/toolkit/pull/701)
|
||||
+2
-2
@@ -3,10 +3,10 @@
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const filePath = process.env[`GITHUB_ENV`]
|
||||
fs.appendFileSync(filePath, `ACTIONS_RUNTIME_TOKEN=${process.env.ACTIONS_RUNTIME_TOKEN}${os.EOL}`, {
|
||||
fs.appendFileSync(filePath, `ACTIONS_RUNTIME_URL=${process.env.ACTIONS_RUNTIME_URL}${os.EOL}`, {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
fs.appendFileSync(filePath, `ACTIONS_CACHE_URL=${process.env.ACTIONS_CACHE_URL}${os.EOL}`, {
|
||||
fs.appendFileSync(filePath, `ACTIONS_RUNTIME_TOKEN=${process.env.ACTIONS_RUNTIME_TOKEN}${os.EOL}`, {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
fs.appendFileSync(filePath, `GITHUB_RUN_ID=${process.env.GITHUB_RUN_ID}${os.EOL}`, {
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import * as cache from '../src/cache'
|
||||
|
||||
test('isFeatureAvailable returns true if server url is set', () => {
|
||||
try {
|
||||
process.env['ACTIONS_CACHE_URL'] = 'http://cache.com'
|
||||
expect(cache.isFeatureAvailable()).toBe(true)
|
||||
} finally {
|
||||
delete process.env['ACTIONS_CACHE_URL']
|
||||
}
|
||||
})
|
||||
|
||||
test('isFeatureAvailable returns false if server url is not set', () => {
|
||||
expect(cache.isFeatureAvailable()).toBe(false)
|
||||
})
|
||||
+2
-2
@@ -2,10 +2,10 @@ import {promises as fs} from 'fs'
|
||||
import * as path from 'path'
|
||||
import * as cacheUtils from '../src/internal/cacheUtils'
|
||||
|
||||
test('getArchiveFileSizeInBytes returns file size', () => {
|
||||
test('getArchiveFileSizeIsBytes returns file size', () => {
|
||||
const filePath = path.join(__dirname, '__fixtures__', 'helloWorld.txt')
|
||||
|
||||
const size = cacheUtils.getArchiveFileSizeInBytes(filePath)
|
||||
const size = cacheUtils.getArchiveFileSizeIsBytes(filePath)
|
||||
|
||||
expect(size).toBe(11)
|
||||
})
|
||||
|
||||
+3
-4
@@ -87,7 +87,7 @@ test('download progress tracked correctly', () => {
|
||||
expect(progress.isDone()).toBe(true)
|
||||
})
|
||||
|
||||
test('display timer works correctly', done => {
|
||||
test('display timer works correctly', () => {
|
||||
const progress = new DownloadProgress(1000)
|
||||
|
||||
const infoMock = jest.spyOn(core, 'info')
|
||||
@@ -103,7 +103,6 @@ test('display timer works correctly', done => {
|
||||
const test2 = (): void => {
|
||||
check()
|
||||
expect(progress.timeoutHandle).toBeUndefined()
|
||||
done()
|
||||
}
|
||||
|
||||
// Validate the progress is displayed, stop the timer, and call test2.
|
||||
@@ -113,7 +112,7 @@ test('display timer works correctly', done => {
|
||||
progress.stopDisplayTimer()
|
||||
progress.setReceivedBytes(1000)
|
||||
|
||||
setTimeout(() => test2(), 500)
|
||||
setTimeout(() => test2(), 100)
|
||||
}
|
||||
|
||||
// Start the timer, update the received bytes, and call test1.
|
||||
@@ -123,7 +122,7 @@ test('display timer works correctly', done => {
|
||||
|
||||
progress.setReceivedBytes(500)
|
||||
|
||||
setTimeout(() => test1(), 500)
|
||||
setTimeout(() => test1(), 100)
|
||||
}
|
||||
|
||||
start()
|
||||
|
||||
@@ -30,6 +30,7 @@ async function handleResponse(
|
||||
response: ITestResponse | undefined
|
||||
): Promise<ITestResponse> {
|
||||
if (!response) {
|
||||
// eslint-disable-next-line no-undef
|
||||
fail('Retry method called too many times')
|
||||
}
|
||||
|
||||
|
||||
+10
-9
@@ -18,6 +18,7 @@ beforeAll(() => {
|
||||
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
||||
jest.spyOn(core, 'error').mockImplementation(() => {})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
||||
jest.spyOn(cacheUtils, 'getCacheFileName').mockImplementation(cm => {
|
||||
const actualUtils = jest.requireActual('../src/internal/cacheUtils')
|
||||
return actualUtils.getCacheFileName(cm)
|
||||
@@ -122,8 +123,8 @@ test('restore with gzip compressed cache found', async () => {
|
||||
const downloadCacheMock = jest.spyOn(cacheHttpClient, 'downloadCache')
|
||||
|
||||
const fileSize = 142
|
||||
const getArchiveFileSizeInBytesMock = jest
|
||||
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
||||
const getArchiveFileSizeIsBytesMock = jest
|
||||
.spyOn(cacheUtils, 'getArchiveFileSizeIsBytes')
|
||||
.mockReturnValue(fileSize)
|
||||
|
||||
const extractTarMock = jest.spyOn(tar, 'extractTar')
|
||||
@@ -146,7 +147,7 @@ test('restore with gzip compressed cache found', async () => {
|
||||
archivePath,
|
||||
undefined
|
||||
)
|
||||
expect(getArchiveFileSizeInBytesMock).toHaveBeenCalledWith(archivePath)
|
||||
expect(getArchiveFileSizeIsBytesMock).toHaveBeenCalledWith(archivePath)
|
||||
|
||||
expect(extractTarMock).toHaveBeenCalledTimes(1)
|
||||
expect(extractTarMock).toHaveBeenCalledWith(archivePath, compression)
|
||||
@@ -183,8 +184,8 @@ test('restore with zstd compressed cache found', async () => {
|
||||
const downloadCacheMock = jest.spyOn(cacheHttpClient, 'downloadCache')
|
||||
|
||||
const fileSize = 62915000
|
||||
const getArchiveFileSizeInBytesMock = jest
|
||||
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
||||
const getArchiveFileSizeIsBytesMock = jest
|
||||
.spyOn(cacheUtils, 'getArchiveFileSizeIsBytes')
|
||||
.mockReturnValue(fileSize)
|
||||
|
||||
const extractTarMock = jest.spyOn(tar, 'extractTar')
|
||||
@@ -205,7 +206,7 @@ test('restore with zstd compressed cache found', async () => {
|
||||
archivePath,
|
||||
undefined
|
||||
)
|
||||
expect(getArchiveFileSizeInBytesMock).toHaveBeenCalledWith(archivePath)
|
||||
expect(getArchiveFileSizeIsBytesMock).toHaveBeenCalledWith(archivePath)
|
||||
expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~60 MB (62915000 B)`)
|
||||
|
||||
expect(extractTarMock).toHaveBeenCalledTimes(1)
|
||||
@@ -240,8 +241,8 @@ test('restore with cache found for restore key', async () => {
|
||||
const downloadCacheMock = jest.spyOn(cacheHttpClient, 'downloadCache')
|
||||
|
||||
const fileSize = 142
|
||||
const getArchiveFileSizeInBytesMock = jest
|
||||
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
||||
const getArchiveFileSizeIsBytesMock = jest
|
||||
.spyOn(cacheUtils, 'getArchiveFileSizeIsBytes')
|
||||
.mockReturnValue(fileSize)
|
||||
|
||||
const extractTarMock = jest.spyOn(tar, 'extractTar')
|
||||
@@ -262,7 +263,7 @@ test('restore with cache found for restore key', async () => {
|
||||
archivePath,
|
||||
undefined
|
||||
)
|
||||
expect(getArchiveFileSizeInBytesMock).toHaveBeenCalledWith(archivePath)
|
||||
expect(getArchiveFileSizeIsBytesMock).toHaveBeenCalledWith(archivePath)
|
||||
expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~0 MB (142 B)`)
|
||||
|
||||
expect(extractTarMock).toHaveBeenCalledTimes(1)
|
||||
|
||||
+4
-3
@@ -17,6 +17,7 @@ beforeAll(() => {
|
||||
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
||||
jest.spyOn(core, 'error').mockImplementation(() => {})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
||||
jest.spyOn(cacheUtils, 'getCacheFileName').mockImplementation(cm => {
|
||||
const actualUtils = jest.requireActual('../src/internal/cacheUtils')
|
||||
return actualUtils.getCacheFileName(cm)
|
||||
@@ -46,9 +47,9 @@ test('save with large cache outputs should fail', async () => {
|
||||
|
||||
const createTarMock = jest.spyOn(tar, 'createTar')
|
||||
|
||||
const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit
|
||||
const cacheSize = 6 * 1024 * 1024 * 1024 //~6GB, over the 5GB limit
|
||||
jest
|
||||
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
||||
.spyOn(cacheUtils, 'getArchiveFileSizeIsBytes')
|
||||
.mockReturnValueOnce(cacheSize)
|
||||
const compression = CompressionMethod.Gzip
|
||||
const getCompressionMock = jest
|
||||
@@ -56,7 +57,7 @@ test('save with large cache outputs should fail', async () => {
|
||||
.mockReturnValueOnce(Promise.resolve(compression))
|
||||
|
||||
await expect(saveCache([filePath], primaryKey)).rejects.toThrowError(
|
||||
'Cache size of ~11264 MB (11811160064 B) is over the 10GB limit, not saving cache.'
|
||||
'Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache.'
|
||||
)
|
||||
|
||||
const archiveFolder = '/foo/bar'
|
||||
|
||||
Vendored
+7
-16
@@ -11,7 +11,6 @@ jest.mock('@actions/exec')
|
||||
jest.mock('@actions/io')
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32'
|
||||
const IS_MAC = process.platform === 'darwin'
|
||||
|
||||
const defaultTarPath = process.platform === 'darwin' ? 'gtar' : 'tar'
|
||||
|
||||
@@ -56,9 +55,7 @@ test('zstd extract tar', async () => {
|
||||
'-P',
|
||||
'-C',
|
||||
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace
|
||||
]
|
||||
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
||||
.concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
||||
].concat(IS_WINDOWS ? ['--force-local'] : []),
|
||||
{cwd: undefined}
|
||||
)
|
||||
})
|
||||
@@ -87,7 +84,7 @@ test('gzip extract tar', async () => {
|
||||
'-P',
|
||||
'-C',
|
||||
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace
|
||||
].concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
||||
],
|
||||
{cwd: undefined}
|
||||
)
|
||||
})
|
||||
@@ -148,9 +145,7 @@ test('zstd create tar', async () => {
|
||||
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace,
|
||||
'--files-from',
|
||||
'manifest.txt'
|
||||
]
|
||||
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
||||
.concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
||||
].concat(IS_WINDOWS ? ['--force-local'] : []),
|
||||
{
|
||||
cwd: archiveFolder
|
||||
}
|
||||
@@ -185,7 +180,7 @@ test('gzip create tar', async () => {
|
||||
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace,
|
||||
'--files-from',
|
||||
'manifest.txt'
|
||||
].concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
||||
],
|
||||
{
|
||||
cwd: archiveFolder
|
||||
}
|
||||
@@ -210,9 +205,7 @@ test('zstd list tar', async () => {
|
||||
'-tf',
|
||||
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
||||
'-P'
|
||||
]
|
||||
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
||||
.concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
||||
].concat(IS_WINDOWS ? ['--force-local'] : []),
|
||||
{cwd: undefined}
|
||||
)
|
||||
})
|
||||
@@ -235,9 +228,7 @@ test('zstdWithoutLong list tar', async () => {
|
||||
'-tf',
|
||||
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
||||
'-P'
|
||||
]
|
||||
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
||||
.concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
||||
].concat(IS_WINDOWS ? ['--force-local'] : []),
|
||||
{cwd: undefined}
|
||||
)
|
||||
})
|
||||
@@ -261,7 +252,7 @@ test('gzip list tar', async () => {
|
||||
'-tf',
|
||||
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
||||
'-P'
|
||||
].concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
||||
],
|
||||
{cwd: undefined}
|
||||
)
|
||||
})
|
||||
|
||||
+4322
-814
File diff suppressed because it is too large
Load Diff
Vendored
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/cache",
|
||||
"version": "2.0.0",
|
||||
"version": "1.0.6",
|
||||
"preview": true,
|
||||
"description": "Actions cache lib",
|
||||
"keywords": [
|
||||
@@ -42,8 +42,8 @@
|
||||
"@actions/glob": "^0.1.0",
|
||||
"@actions/http-client": "^1.0.9",
|
||||
"@actions/io": "^1.0.1",
|
||||
"@azure/ms-rest-js": "^2.6.0",
|
||||
"@azure/storage-blob": "^12.8.0",
|
||||
"@azure/ms-rest-js": "^2.0.7",
|
||||
"@azure/storage-blob": "^12.1.2",
|
||||
"semver": "^6.1.0",
|
||||
"uuid": "^3.3.3"
|
||||
},
|
||||
|
||||
Vendored
+18
-37
@@ -43,16 +43,6 @@ function checkKey(key: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* isFeatureAvailable to check the presence of Actions cache service
|
||||
*
|
||||
* @returns boolean return true if Actions cache service feature is available, otherwise false
|
||||
*/
|
||||
|
||||
export function isFeatureAvailable(): boolean {
|
||||
return !!process.env['ACTIONS_CACHE_URL']
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores cache from keys
|
||||
*
|
||||
@@ -114,7 +104,7 @@ export async function restoreCache(
|
||||
await listTar(archivePath, compressionMethod)
|
||||
}
|
||||
|
||||
const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath)
|
||||
const archiveFileSize = utils.getArchiveFileSizeIsBytes(archivePath)
|
||||
core.info(
|
||||
`Cache Size: ~${Math.round(
|
||||
archiveFileSize / (1024 * 1024)
|
||||
@@ -176,33 +166,24 @@ export async function saveCache(
|
||||
|
||||
core.debug(`Archive Path: ${archivePath}`)
|
||||
|
||||
try {
|
||||
await createTar(archiveFolder, cachePaths, compressionMethod)
|
||||
if (core.isDebug()) {
|
||||
await listTar(archivePath, compressionMethod)
|
||||
}
|
||||
|
||||
const fileSizeLimit = 10 * 1024 * 1024 * 1024 // 10GB 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 10GB 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}`)
|
||||
}
|
||||
await createTar(archiveFolder, cachePaths, compressionMethod)
|
||||
if (core.isDebug()) {
|
||||
await listTar(archivePath, compressionMethod)
|
||||
}
|
||||
|
||||
const fileSizeLimit = 5 * 1024 * 1024 * 1024 // 5GB per repo limit
|
||||
const archiveFileSize = utils.getArchiveFileSizeIsBytes(archivePath)
|
||||
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
|
||||
}
|
||||
|
||||
+8
-3
@@ -31,7 +31,12 @@ import {
|
||||
const versionSalt = '1.0'
|
||||
|
||||
function getCacheApiUrl(resource: string): string {
|
||||
const baseUrl: string = process.env['ACTIONS_CACHE_URL'] || ''
|
||||
// Ideally we just use ACTIONS_CACHE_URL
|
||||
const baseUrl: string = (
|
||||
process.env['ACTIONS_CACHE_URL'] ||
|
||||
process.env['ACTIONS_RUNTIME_URL'] ||
|
||||
''
|
||||
).replace('pipelines', 'artifactcache')
|
||||
if (!baseUrl) {
|
||||
throw new Error('Cache Service Url not found, unable to restore cache.')
|
||||
}
|
||||
@@ -214,7 +219,7 @@ async function uploadFile(
|
||||
options?: UploadOptions
|
||||
): Promise<void> {
|
||||
// Upload Chunks
|
||||
const fileSize = utils.getArchiveFileSizeInBytes(archivePath)
|
||||
const fileSize = fs.statSync(archivePath).size
|
||||
const resourceUrl = getCacheApiUrl(`caches/${cacheId.toString()}`)
|
||||
const fd = fs.openSync(archivePath, 'r')
|
||||
const uploadOptions = getUploadOptions(options)
|
||||
@@ -295,7 +300,7 @@ export async function saveCache(
|
||||
|
||||
// Commit Cache
|
||||
core.debug('Commiting cache')
|
||||
const cacheSize = utils.getArchiveFileSizeInBytes(archivePath)
|
||||
const cacheSize = utils.getArchiveFileSizeIsBytes(archivePath)
|
||||
core.info(
|
||||
`Cache Size: ~${Math.round(cacheSize / (1024 * 1024))} MB (${cacheSize} B)`
|
||||
)
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ export async function createTempDirectory(): Promise<string> {
|
||||
return dest
|
||||
}
|
||||
|
||||
export function getArchiveFileSizeInBytes(filePath: string): number {
|
||||
export function getArchiveFileSizeIsBytes(filePath: string): number {
|
||||
return fs.statSync(filePath).size
|
||||
}
|
||||
|
||||
|
||||
+3
-4
@@ -133,7 +133,7 @@ export class DownloadProgress {
|
||||
*
|
||||
* @param delayInMs the delay between each write
|
||||
*/
|
||||
startDisplayTimer(delayInMs = 1000): void {
|
||||
startDisplayTimer(delayInMs: number = 1000): void {
|
||||
const displayCallback = (): void => {
|
||||
this.display()
|
||||
|
||||
@@ -190,7 +190,7 @@ export async function downloadCacheHttpClient(
|
||||
|
||||
if (contentLengthHeader) {
|
||||
const expectedLength = parseInt(contentLengthHeader)
|
||||
const actualLength = utils.getArchiveFileSizeInBytes(archivePath)
|
||||
const actualLength = utils.getArchiveFileSizeIsBytes(archivePath)
|
||||
|
||||
if (actualLength !== expectedLength) {
|
||||
throw new Error(
|
||||
@@ -240,8 +240,7 @@ export async function downloadCacheStorageSDK(
|
||||
//
|
||||
// If the file exceeds the buffer maximum length (~1 GB on 32-bit systems and ~2 GB
|
||||
// on 64-bit systems), split the download into multiple segments
|
||||
// ~2 GB = 2147483647, beyond this, we start getting out of range error. So, capping it accordingly.
|
||||
const maxSegmentSize = Math.min(2147483647, buffer.constants.MAX_LENGTH)
|
||||
const maxSegmentSize = buffer.constants.MAX_LENGTH
|
||||
const downloadProgress = new DownloadProgress(contentLength)
|
||||
|
||||
const fd = fs.openSync(archivePath, 'w')
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ export async function retryTypedResponse<T>(
|
||||
)
|
||||
}
|
||||
|
||||
export async function retryHttpClientResponse(
|
||||
export async function retryHttpClientResponse<T>(
|
||||
name: string,
|
||||
method: () => Promise<IHttpClientResponse>,
|
||||
maxAttempts = DefaultRetryAttempts,
|
||||
|
||||
Vendored
-2
@@ -26,8 +26,6 @@ async function getTarPath(
|
||||
case 'darwin': {
|
||||
const gnuTar = await io.which('gtar', false)
|
||||
if (gnuTar) {
|
||||
// fix permission denied errors when extracting BSD tar archive with GNU tar - https://github.com/actions/cache/issues/527
|
||||
args.push('--delay-directory-restore')
|
||||
return gnuTar
|
||||
}
|
||||
break
|
||||
|
||||
+6
-116
@@ -16,14 +16,11 @@ import * as core from '@actions/core';
|
||||
|
||||
#### 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`.
|
||||
|
||||
Outputs can be set with `setOutput` which makes them available to be mapped into inputs of other actions to ensure they are decoupled.
|
||||
Action inputs can be read with `getInput`. Outputs can be set with `setOutput` which makes them available to be mapped into inputs of other actions to ensure they are decoupled.
|
||||
|
||||
```js
|
||||
const myInput = core.getInput('inputName', { required: true });
|
||||
const myBooleanInput = core.getBooleanInput('booleanInputName', { required: true });
|
||||
const myMultilineInput = core.getMultilineInput('multilineInputName', { required: true });
|
||||
|
||||
core.setOutput('outputKey', 'outputVal');
|
||||
```
|
||||
|
||||
@@ -65,10 +62,11 @@ catch (err) {
|
||||
// setFailed logs the message and sets a failing exit code
|
||||
core.setFailed(`Action failed with error ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
Note that `setNeutral` is not yet implemented in actions V2 but equivalent functionality is being planned.
|
||||
|
||||
```
|
||||
|
||||
#### 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).
|
||||
@@ -92,8 +90,6 @@ try {
|
||||
|
||||
// Do stuff
|
||||
core.info('Output to the actions build log')
|
||||
|
||||
core.notice('This is a message that will also emit an annotation')
|
||||
}
|
||||
catch (err) {
|
||||
core.error(`Error ${err}, action may still succeed though`);
|
||||
@@ -117,65 +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:
|
||||
|
||||

|
||||
|
||||
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 name of the file for which the annotation should be created.
|
||||
*/
|
||||
file?: 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
|
||||
|
||||
Colored output is supported in the Action logs via standard [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code). 3/4 bit, 8 bit and 24 bit colors are all supported.
|
||||
|
||||
Foreground colors:
|
||||
|
||||
```js
|
||||
// 3/4 bit
|
||||
core.info('\u001b[35mThis foreground will be magenta')
|
||||
@@ -188,7 +130,6 @@ core.info('\u001b[38;2;255;0;0mThis foreground will be bright red')
|
||||
```
|
||||
|
||||
Background colors:
|
||||
|
||||
```js
|
||||
// 3/4 bit
|
||||
core.info('\u001b[43mThis background will be yellow');
|
||||
@@ -215,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
|
||||
|
||||
```js
|
||||
core.info('\u001b[35mThis foreground will be magenta')
|
||||
core.info('This foreground will reset to the default')
|
||||
@@ -230,10 +170,9 @@ core.info(style.color.ansi16m.hex('#abcdef') + 'Hello world!')
|
||||
|
||||
#### Action state
|
||||
|
||||
You can use this library to save state and get state for sharing information between a given wrapper action:
|
||||
|
||||
**action.yml**:
|
||||
You can use this library to save state and get state for sharing information between a given wrapper action:
|
||||
|
||||
**action.yml**
|
||||
```yaml
|
||||
name: 'Wrapper action sample'
|
||||
inputs:
|
||||
@@ -254,7 +193,6 @@ core.saveState("pidToKill", 12345);
|
||||
```
|
||||
|
||||
In action's `cleanup.js`:
|
||||
|
||||
```js
|
||||
const core = require('@actions/core');
|
||||
|
||||
@@ -262,51 +200,3 @@ var pid = core.getState("pidToKill");
|
||||
|
||||
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'
|
||||
```
|
||||
@@ -1,22 +1,5 @@
|
||||
# @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
|
||||
- [Update `exportVariable` and `addPath` to use environment files](https://github.com/actions/toolkit/pull/571)
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
import * as core from '../src/core'
|
||||
import {HttpClient} from '@actions/http-client'
|
||||
import {toCommandProperties} from '../src/utils'
|
||||
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
|
||||
@@ -21,17 +19,6 @@ const testEnvVars = {
|
||||
INPUT_MISSING: '',
|
||||
'INPUT_SPECIAL_CHARS_\'\t"\\': '\'\t"\\ response ',
|
||||
INPUT_MULTIPLE_SPACES_VARIABLE: 'I have multiple spaces',
|
||||
INPUT_BOOLEAN_INPUT: 'true',
|
||||
INPUT_BOOLEAN_INPUT_TRUE1: 'true',
|
||||
INPUT_BOOLEAN_INPUT_TRUE2: 'True',
|
||||
INPUT_BOOLEAN_INPUT_TRUE3: 'TRUE',
|
||||
INPUT_BOOLEAN_INPUT_FALSE1: 'false',
|
||||
INPUT_BOOLEAN_INPUT_FALSE2: 'False',
|
||||
INPUT_BOOLEAN_INPUT_FALSE3: 'FALSE',
|
||||
INPUT_WRONG_BOOLEAN_INPUT: 'wrong',
|
||||
INPUT_WITH_TRAILING_WHITESPACE: ' some val ',
|
||||
|
||||
INPUT_MY_INPUT_LIST: 'val1\nval2\nval3',
|
||||
|
||||
// Save inputs
|
||||
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', () => {
|
||||
core.setOutput('some output', 'some value')
|
||||
assertWriteCalls([
|
||||
os.EOL,
|
||||
`::set-output name=some output::some value${os.EOL}`
|
||||
])
|
||||
assertWriteCalls([`::set-output name=some output::some value${os.EOL}`])
|
||||
})
|
||||
|
||||
it('setOutput handles bools', () => {
|
||||
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', () => {
|
||||
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', () => {
|
||||
@@ -271,21 +207,6 @@ describe('@actions/core', () => {
|
||||
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',
|
||||
file: 'root/test.txt',
|
||||
startColumn: 1,
|
||||
endColumn: 2,
|
||||
startLine: 5,
|
||||
endLine: 5
|
||||
})
|
||||
assertWriteCalls([
|
||||
`::error title=A title,file=root/test.txt,line=5,endLine=5,col=1,endColumn=2::Error: ${message}${os.EOL}`
|
||||
])
|
||||
})
|
||||
|
||||
it('warning sets the correct message', () => {
|
||||
core.warning('Warning')
|
||||
assertWriteCalls([`::warning::Warning${os.EOL}`])
|
||||
@@ -302,72 +223,6 @@ describe('@actions/core', () => {
|
||||
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',
|
||||
file: 'root/test.txt',
|
||||
startColumn: 1,
|
||||
endColumn: 2,
|
||||
startLine: 5,
|
||||
endLine: 5
|
||||
})
|
||||
assertWriteCalls([
|
||||
`::warning title=A title,file=root/test.txt,line=5,endLine=5,col=1,endColumn=2::Error: ${message}${os.EOL}`
|
||||
])
|
||||
})
|
||||
|
||||
it('notice sets the correct message', () => {
|
||||
core.notice('Notice')
|
||||
assertWriteCalls([`::notice::Notice${os.EOL}`])
|
||||
})
|
||||
|
||||
it('notice escapes the message', () => {
|
||||
core.notice('\r\nnotice\n')
|
||||
assertWriteCalls([`::notice::%0D%0Anotice%0A${os.EOL}`])
|
||||
})
|
||||
|
||||
it('notice handles an error object', () => {
|
||||
const message = 'this is my error message'
|
||||
core.notice(new Error(message))
|
||||
assertWriteCalls([`::notice::Error: ${message}${os.EOL}`])
|
||||
})
|
||||
|
||||
it('notice handles parameters correctly', () => {
|
||||
const message = 'this is my error message'
|
||||
core.notice(new Error(message), {
|
||||
title: 'A title',
|
||||
file: 'root/test.txt',
|
||||
startColumn: 1,
|
||||
endColumn: 2,
|
||||
startLine: 5,
|
||||
endLine: 5
|
||||
})
|
||||
assertWriteCalls([
|
||||
`::notice title=A title,file=root/test.txt,line=5,endLine=5,col=1,endColumn=2::Error: ${message}${os.EOL}`
|
||||
])
|
||||
})
|
||||
|
||||
it('annotations map field names correctly', () => {
|
||||
const commandProperties = toCommandProperties({
|
||||
title: 'A title',
|
||||
file: 'root/test.txt',
|
||||
startColumn: 1,
|
||||
endColumn: 2,
|
||||
startLine: 5,
|
||||
endLine: 5
|
||||
})
|
||||
expect(commandProperties.title).toBe('A title')
|
||||
expect(commandProperties.file).toBe('root/test.txt')
|
||||
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', () => {
|
||||
core.startGroup('my-group')
|
||||
assertWriteCalls([`::group::my-group${os.EOL}`])
|
||||
@@ -470,20 +325,3 @@ function verifyFileCommand(command: string, expectedContents: string): void {
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
Generated
+2
-50
@@ -1,62 +1,14 @@
|
||||
{
|
||||
"name": "@actions/core",
|
||||
"version": "1.6.0",
|
||||
"lockfileVersion": 2,
|
||||
"version": "1.2.6",
|
||||
"lockfileVersion": 1,
|
||||
"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": {
|
||||
"@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": {
|
||||
"version": "12.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz",
|
||||
"integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==",
|
||||
"dev": true
|
||||
},
|
||||
"tunnel": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/core",
|
||||
"version": "1.6.0",
|
||||
"version": "1.2.6",
|
||||
"description": "Actions core lib",
|
||||
"keywords": [
|
||||
"github",
|
||||
@@ -35,9 +35,6 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/actions/toolkit/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^1.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^12.0.2"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {toCommandValue} from './utils'
|
||||
// We use any as a valid input type
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export interface CommandProperties {
|
||||
interface CommandProperties {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export function issueCommand(
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
+7
-133
@@ -1,21 +1,16 @@
|
||||
import {issue, issueCommand} from './command'
|
||||
import {issueCommand as issueFileCommand} from './file-command'
|
||||
import {toCommandProperties, toCommandValue} from './utils'
|
||||
import {toCommandValue} from './utils'
|
||||
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
|
||||
import {OidcClient} from './oidc-utils'
|
||||
|
||||
/**
|
||||
* Interface for getInput options
|
||||
*/
|
||||
export interface InputOptions {
|
||||
/** Optional. Whether the input is required. If required and not present, will throw. Defaults to false */
|
||||
required?: boolean
|
||||
|
||||
/** Optional. Whether leading/trailing whitespace will be trimmed for the input. Defaults to true */
|
||||
trimWhitespace?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,43 +28,6 @@ export enum ExitCode {
|
||||
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 path of the file for which the annotation should be created.
|
||||
*/
|
||||
file?: 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
|
||||
//-----------------------------------------------------------------------
|
||||
@@ -117,9 +75,7 @@ export function addPath(inputPath: string): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of an input.
|
||||
* Unless trimWhitespace is set to false in InputOptions, the value is also trimmed.
|
||||
* Returns an empty string if the value is not defined.
|
||||
* Gets the value of an input. The value is also trimmed.
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
@@ -132,54 +88,9 @@ export function getInput(name: string, options?: InputOptions): string {
|
||||
throw new Error(`Input required and not supplied: ${name}`)
|
||||
}
|
||||
|
||||
if (options && options.trimWhitespace === false) {
|
||||
return val
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
@@ -188,7 +99,6 @@ export function getBooleanInput(name: string, options?: InputOptions): boolean {
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function setOutput(name: string, value: any): void {
|
||||
process.stdout.write(os.EOL)
|
||||
issueCommand('set-output', {name}, value)
|
||||
}
|
||||
|
||||
@@ -238,49 +148,17 @@ export function debug(message: string): void {
|
||||
/**
|
||||
* Adds an error issue
|
||||
* @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(
|
||||
message: string | Error,
|
||||
properties: AnnotationProperties = {}
|
||||
): void {
|
||||
issueCommand(
|
||||
'error',
|
||||
toCommandProperties(properties),
|
||||
message instanceof Error ? message.toString() : message
|
||||
)
|
||||
export function error(message: string | Error): void {
|
||||
issue('error', 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 properties optional properties to add to the annotation.
|
||||
*/
|
||||
export function warning(
|
||||
message: string | Error,
|
||||
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
|
||||
)
|
||||
export function warning(message: string | Error): void {
|
||||
issue('warning', message instanceof Error ? message.toString() : message)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -355,7 +233,3 @@ export function saveState(name: string, value: any): void {
|
||||
export function getState(name: string): string {
|
||||
return process.env[`STATE_${name}`] || ''
|
||||
}
|
||||
|
||||
export async function getIDToken(aud?: string): Promise<string> {
|
||||
return await OidcClient.getIDToken(aud)
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
// We use any as a valid input type
|
||||
/* 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
|
||||
* @param input input to sanitize into a string
|
||||
@@ -16,26 +13,3 @@ export function toCommandValue(input: any): string {
|
||||
}
|
||||
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,
|
||||
file: annotationProperties.file,
|
||||
line: annotationProperties.startLine,
|
||||
endLine: annotationProperties.endLine,
|
||||
col: annotationProperties.startColumn,
|
||||
endColumn: annotationProperties.endColumn
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
# @actions/exec Releases
|
||||
|
||||
### 1.1.1
|
||||
- Update `lockfileVersion` to `v2` in `package-lock.json [#1024](https://github.com/actions/toolkit/pull/1024)
|
||||
|
||||
### 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
|
||||
|
||||
- [Add \"types\" to package.json](https://github.com/actions/toolkit/pull/221)
|
||||
|
||||
@@ -286,31 +286,6 @@ describe('@actions/exec', () => {
|
||||
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 () => {
|
||||
let command: string
|
||||
if (IS_WINDOWS) {
|
||||
@@ -563,22 +538,6 @@ describe('@actions/exec', () => {
|
||||
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 () => {
|
||||
let command: string
|
||||
if (IS_WINDOWS) {
|
||||
@@ -661,165 +620,6 @@ describe('@actions/exec', () => {
|
||||
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) {
|
||||
it('Exec roots relative tool path using process.cwd (Windows path separator)', async () => {
|
||||
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)
|
||||
|
||||
})
|
||||
Generated
+2
-17
@@ -1,23 +1,8 @@
|
||||
{
|
||||
"name": "@actions/exec",
|
||||
"version": "1.1.1",
|
||||
"lockfileVersion": 2,
|
||||
"version": "1.0.4",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@actions/exec",
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/io": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/io": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.1.tgz",
|
||||
"integrity": "sha512-rhq+tfZukbtaus7xyUtwKfuiCRXd1hWSfmJNEpFgBQJ4woqPEpsBw04awicjwz9tyG2/MVhAEMfVn664Cri5zA=="
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/io": {
|
||||
"version": "1.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/exec",
|
||||
"version": "1.1.1",
|
||||
"version": "1.0.4",
|
||||
"description": "Actions exec lib",
|
||||
"keywords": [
|
||||
"github",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import {StringDecoder} from 'string_decoder'
|
||||
import {ExecOptions, ExecOutput, ExecListeners} from './interfaces'
|
||||
import {ExecOptions} from './interfaces'
|
||||
import * as tr from './toolrunner'
|
||||
|
||||
export {ExecOptions, ExecOutput, ExecListeners}
|
||||
export {ExecOptions}
|
||||
|
||||
/**
|
||||
* Exec a command.
|
||||
@@ -29,62 +28,3 @@ export async function exec(
|
||||
const runner: tr.ToolRunner = new tr.ToolRunner(toolPath, args, options)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,39 +34,15 @@ export interface ExecOptions {
|
||||
input?: Buffer
|
||||
|
||||
/** optional. Listeners for output. Callback functions that will be called on these events */
|
||||
listeners?: ExecListeners
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 */
|
||||
stdout: string
|
||||
|
||||
/**The entire stderr of the process as a string */
|
||||
stderr: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
listeners?: {
|
||||
stdout?: (data: Buffer) => void
|
||||
|
||||
stderr?: (data: Buffer) => void
|
||||
|
||||
stdline?: (data: string) => void
|
||||
|
||||
errline?: (data: string) => void
|
||||
|
||||
debug?: (data: string) => void
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import * as stream from 'stream'
|
||||
import * as im from './interfaces'
|
||||
import * as io from '@actions/io'
|
||||
import * as ioUtil from '@actions/io/lib/io-util'
|
||||
import {setTimeout} from 'timers'
|
||||
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
|
||||
@@ -84,7 +83,7 @@ export class ToolRunner extends events.EventEmitter {
|
||||
data: Buffer,
|
||||
strBuffer: string,
|
||||
onLine: (line: string) => void
|
||||
): string {
|
||||
): void {
|
||||
try {
|
||||
let s = strBuffer + data.toString()
|
||||
let n = s.indexOf(os.EOL)
|
||||
@@ -98,12 +97,10 @@ export class ToolRunner extends events.EventEmitter {
|
||||
n = s.indexOf(os.EOL)
|
||||
}
|
||||
|
||||
return s
|
||||
strBuffer = s
|
||||
} catch (err) {
|
||||
// streaming lines to console is best effort. Don't fail a build.
|
||||
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)
|
||||
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('arguments:')
|
||||
for (const arg of this.args) {
|
||||
@@ -435,10 +432,6 @@ export class ToolRunner extends events.EventEmitter {
|
||||
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 cp = child.spawn(
|
||||
fileName,
|
||||
@@ -446,7 +439,7 @@ export class ToolRunner extends events.EventEmitter {
|
||||
this._getSpawnOptions(this.options, fileName)
|
||||
)
|
||||
|
||||
let stdbuffer = ''
|
||||
const stdbuffer = ''
|
||||
if (cp.stdout) {
|
||||
cp.stdout.on('data', (data: Buffer) => {
|
||||
if (this.options.listeners && this.options.listeners.stdout) {
|
||||
@@ -457,19 +450,15 @@ export class ToolRunner extends events.EventEmitter {
|
||||
optionsNonNull.outStream.write(data)
|
||||
}
|
||||
|
||||
stdbuffer = this._processLineBuffer(
|
||||
data,
|
||||
stdbuffer,
|
||||
(line: string) => {
|
||||
if (this.options.listeners && this.options.listeners.stdline) {
|
||||
this.options.listeners.stdline(line)
|
||||
}
|
||||
this._processLineBuffer(data, stdbuffer, (line: string) => {
|
||||
if (this.options.listeners && this.options.listeners.stdline) {
|
||||
this.options.listeners.stdline(line)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
let errbuffer = ''
|
||||
const errbuffer = ''
|
||||
if (cp.stderr) {
|
||||
cp.stderr.on('data', (data: Buffer) => {
|
||||
state.processStderr = true
|
||||
@@ -488,15 +477,11 @@ export class ToolRunner extends events.EventEmitter {
|
||||
s.write(data)
|
||||
}
|
||||
|
||||
errbuffer = this._processLineBuffer(
|
||||
data,
|
||||
errbuffer,
|
||||
(line: string) => {
|
||||
if (this.options.listeners && this.options.listeners.errline) {
|
||||
this.options.listeners.errline(line)
|
||||
}
|
||||
this._processLineBuffer(data, errbuffer, (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
|
||||
processError = ''
|
||||
processExitCode = 0
|
||||
processExited = false // tracks whether the process has exited
|
||||
processStderr = false // tracks whether stderr was written to
|
||||
processClosed: boolean = false // tracks whether the process has exited and stdio is closed
|
||||
processError: string = ''
|
||||
processExitCode: number = 0
|
||||
processExited: boolean = false // tracks whether the process has exited
|
||||
processStderr: boolean = false // tracks whether stderr was written to
|
||||
private delay = 10000 // 10 seconds
|
||||
private done = false
|
||||
private done: boolean = false
|
||||
private options: im.ExecOptions
|
||||
private timeout: NodeJS.Timer | null = null
|
||||
private toolPath: string
|
||||
|
||||
@@ -22,7 +22,7 @@ async function run() {
|
||||
// You can also pass in additional options as a second parameter to getOctokit
|
||||
// const octokit = github.getOctokit(myToken, {userAgent: "MyActionVersion1"});
|
||||
|
||||
const { data: pullRequest } = await octokit.rest.pulls.get({
|
||||
const { data: pullRequest } = await octokit.pulls.get({
|
||||
owner: 'octokit',
|
||||
repo: 'rest.js',
|
||||
pull_number: 123,
|
||||
@@ -50,7 +50,7 @@ const github = require('@actions/github');
|
||||
|
||||
const context = github.context;
|
||||
|
||||
const newIssue = await octokit.rest.issues.create({
|
||||
const newIssue = await octokit.issues.create({
|
||||
...context.repo,
|
||||
title: 'New issue!',
|
||||
body: 'Hello Universe!'
|
||||
@@ -59,19 +59,18 @@ const newIssue = await octokit.rest.issues.create({
|
||||
|
||||
## 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
|
||||
```ts
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import {PushEvent} from '@octokit/webhooks-definitions/schema'
|
||||
|
||||
import * as Webhooks from '@octokit/webhooks'
|
||||
if (github.context.eventName === 'push') {
|
||||
const pushPayload = github.context.payload as PushEvent
|
||||
core.info(`The head commit is: ${pushPayload.head_commit}`)
|
||||
const pushPayload = github.context.payload as Webhooks.WebhookPayloadPush
|
||||
core.info(`The head commit is: ${pushPayload.head}`)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -91,7 +90,7 @@ const octokit = GitHub.plugin(enterpriseServer220Admin)
|
||||
const myToken = core.getInput('myToken');
|
||||
const myOctokit = new octokit(getOctokitOptions(token))
|
||||
// Create a new user
|
||||
myOctokit.rest.enterpriseAdmin.createUser({
|
||||
myOctokit.enterpriseAdmin.createUser({
|
||||
login: "testuser",
|
||||
email: "testuser@test.com",
|
||||
});
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# @actions/github Releases
|
||||
|
||||
### 5.0.1
|
||||
- [Update Octokit Dependencies](https://github.com/actions/toolkit/pull/1037)
|
||||
### 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
|
||||
- [Add execution state information to context](https://github.com/actions/toolkit/pull/499)
|
||||
- [Update Octokit Dependencies with some api breaking changes](https://github.com/actions/toolkit/pull/498)
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('@actions/github', () => {
|
||||
}
|
||||
|
||||
const octokit = getOctokit(token)
|
||||
const branch = await octokit.rest.repos.getBranch({
|
||||
const branch = await octokit.repos.getBranch({
|
||||
owner: 'actions',
|
||||
repo: 'toolkit',
|
||||
branch: 'main'
|
||||
@@ -85,7 +85,7 @@ describe('@actions/github', () => {
|
||||
agent: new https.Agent()
|
||||
}
|
||||
})
|
||||
const branch = await octokit.rest.repos.getBranch({
|
||||
const branch = await octokit.repos.getBranch({
|
||||
owner: 'actions',
|
||||
repo: 'toolkit',
|
||||
branch: 'main'
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('@actions/github', () => {
|
||||
proxyServer = proxy()
|
||||
await new Promise(resolve => {
|
||||
const port = Number(proxyUrl.split(':')[2])
|
||||
proxyServer.listen(port, () => resolve(null))
|
||||
proxyServer.listen(port, () => resolve())
|
||||
})
|
||||
proxyServer.on('connect', req => {
|
||||
proxyConnects.push(req.url)
|
||||
@@ -30,7 +30,7 @@ describe('@actions/github', () => {
|
||||
afterAll(async () => {
|
||||
// Stop proxy server
|
||||
await new Promise(resolve => {
|
||||
proxyServer.once('close', () => resolve(null))
|
||||
proxyServer.once('close', () => resolve())
|
||||
proxyServer.close()
|
||||
})
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('@actions/github', () => {
|
||||
return
|
||||
}
|
||||
const octokit = new GitHub(getOctokitOptions(token))
|
||||
const branch = await octokit.rest.repos.getBranch({
|
||||
const branch = await octokit.repos.getBranch({
|
||||
owner: 'actions',
|
||||
repo: 'toolkit',
|
||||
branch: 'main'
|
||||
@@ -60,7 +60,7 @@ describe('@actions/github', () => {
|
||||
return
|
||||
}
|
||||
const octokit = getOctokit(token)
|
||||
const branch = await octokit.rest.repos.getBranch({
|
||||
const branch = await octokit.repos.getBranch({
|
||||
owner: 'actions',
|
||||
repo: 'toolkit',
|
||||
branch: 'main'
|
||||
@@ -77,7 +77,7 @@ describe('@actions/github', () => {
|
||||
|
||||
// Valid token
|
||||
let octokit = new GitHub({auth: `token ${token}`})
|
||||
const branch = await octokit.rest.repos.getBranch({
|
||||
const branch = await octokit.repos.getBranch({
|
||||
owner: 'actions',
|
||||
repo: 'toolkit',
|
||||
branch: 'main'
|
||||
@@ -89,7 +89,7 @@ describe('@actions/github', () => {
|
||||
octokit = new GitHub({auth: `token asdf`})
|
||||
let failed = false
|
||||
try {
|
||||
await octokit.rest.repos.getBranch({
|
||||
await octokit.repos.getBranch({
|
||||
owner: 'actions',
|
||||
repo: 'toolkit',
|
||||
branch: 'main'
|
||||
|
||||
@@ -2,7 +2,6 @@ import * as path from 'path'
|
||||
import {Context} from '../src/context'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
describe('@actions/context', () => {
|
||||
let context: Context
|
||||
|
||||
@@ -3,6 +3,7 @@ module.exports = {
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.ts'],
|
||||
testRunner: 'jest-circus/runner',
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
},
|
||||
|
||||
Generated
+4673
-506
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/github",
|
||||
"version": "5.0.1",
|
||||
"version": "4.0.0",
|
||||
"description": "Actions github lib",
|
||||
"keywords": [
|
||||
"github",
|
||||
@@ -39,11 +39,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^1.0.11",
|
||||
"@octokit/core": "^3.6.0",
|
||||
"@octokit/plugin-paginate-rest": "^2.17.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^5.13.0"
|
||||
"@octokit/core": "^3.3.1",
|
||||
"@octokit/plugin-paginate-rest": "^2.13.3",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"proxy": "^1.0.2"
|
||||
"jest": "^25.1.0",
|
||||
"proxy": "^1.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,6 @@ export class Context {
|
||||
job: string
|
||||
runNumber: number
|
||||
runId: number
|
||||
apiUrl: string
|
||||
serverUrl: string
|
||||
graphqlUrl: string
|
||||
|
||||
/**
|
||||
* Hydrate the context from the environment
|
||||
@@ -46,10 +43,6 @@ export class Context {
|
||||
this.job = process.env.GITHUB_JOB as string
|
||||
this.runNumber = parseInt(process.env.GITHUB_RUN_NUMBER 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} {
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
# @actions/glob Releases
|
||||
|
||||
### 0.2.1
|
||||
- Update `lockfileVersion` to `v2` in `package-lock.json [#1023](https://github.com/actions/toolkit/pull/1023)
|
||||
|
||||
### 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
|
||||
|
||||
- Initial release
|
||||
|
||||
### 0.1.1
|
||||
|
||||
- Update @actions/core version
|
||||
@@ -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 () => {
|
||||
// Create the following layout:
|
||||
// <root>
|
||||
@@ -378,34 +343,6 @@ describe('globber', () => {
|
||||
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 () => {
|
||||
// Create the following layout:
|
||||
// <root>
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('pattern', () => {
|
||||
it('escapes homedir', async () => {
|
||||
const home = path.join(getTestTemp(), 'home-with-[and]')
|
||||
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.match(path.join(home, 'match'))).toBeTruthy()
|
||||
|
||||
Generated
+38
-84
@@ -1,86 +1,40 @@
|
||||
{
|
||||
"name": "@actions/glob",
|
||||
"version": "0.2.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@actions/glob",
|
||||
"version": "0.2.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.6",
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/core": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
|
||||
"integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
|
||||
"integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
"name": "@actions/glob",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@actions/core": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
|
||||
"integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/glob",
|
||||
"version": "0.2.1",
|
||||
"version": "0.1.1",
|
||||
"preview": true,
|
||||
"description": "Actions glob lib",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {Globber, DefaultGlobber} from './internal-globber'
|
||||
import {GlobOptions} from './internal-glob-options'
|
||||
import {HashFileOptions} from './internal-hash-file-options'
|
||||
import {hashFiles as _hashFiles} from './internal-hash-files'
|
||||
|
||||
export {Globber, GlobOptions}
|
||||
|
||||
@@ -17,21 +15,3 @@ export async function create(
|
||||
): Promise<Globber> {
|
||||
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 = {
|
||||
followSymbolicLinks: true,
|
||||
implicitDescendants: true,
|
||||
matchDirectories: true,
|
||||
omitBrokenSymbolicLinks: true
|
||||
}
|
||||
|
||||
@@ -23,11 +22,6 @@ export function getOptions(copy?: GlobOptions): GlobOptions {
|
||||
core.debug(`implicitDescendants '${result.implicitDescendants}'`)
|
||||
}
|
||||
|
||||
if (typeof copy.matchDirectories === 'boolean') {
|
||||
result.matchDirectories = copy.matchDirectories
|
||||
core.debug(`matchDirectories '${result.matchDirectories}'`)
|
||||
}
|
||||
|
||||
if (typeof copy.omitBrokenSymbolicLinks === 'boolean') {
|
||||
result.omitBrokenSymbolicLinks = copy.omitBrokenSymbolicLinks
|
||||
core.debug(`omitBrokenSymbolicLinks '${result.omitBrokenSymbolicLinks}'`)
|
||||
|
||||
@@ -21,14 +21,6 @@ export interface GlobOptions {
|
||||
*/
|
||||
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
|
||||
* result set. Otherwise an error will be thrown.
|
||||
|
||||
@@ -66,6 +66,7 @@ export class DefaultGlobber implements Globber {
|
||||
async *globGenerator(): AsyncGenerator<string, void> {
|
||||
// Fill in defaults options
|
||||
const options = globOptionsHelper.getOptions(this.options)
|
||||
|
||||
// Implicit descendants?
|
||||
const patterns: Pattern[] = []
|
||||
for (const pattern of this.patterns) {
|
||||
@@ -76,13 +77,12 @@ export class DefaultGlobber implements Globber {
|
||||
pattern.segments[pattern.segments.length - 1] !== '**')
|
||||
) {
|
||||
patterns.push(
|
||||
new Pattern(pattern.negate, true, pattern.segments.concat('**'))
|
||||
new Pattern(pattern.negate, pattern.segments.concat('**'))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Push the search paths
|
||||
|
||||
const stack: SearchState[] = []
|
||||
for (const searchPath of patternHelper.getSearchPaths(patterns)) {
|
||||
core.debug(`Search path '${searchPath}'`)
|
||||
@@ -131,7 +131,7 @@ export class DefaultGlobber implements Globber {
|
||||
// Directory
|
||||
if (stats.isDirectory()) {
|
||||
// Matched
|
||||
if (match & MatchKind.Directory && options.matchDirectories) {
|
||||
if (match & MatchKind.Directory) {
|
||||
yield item.path
|
||||
}
|
||||
// Descend?
|
||||
@@ -180,7 +180,6 @@ export class DefaultGlobber implements Globber {
|
||||
}
|
||||
|
||||
result.searchPaths.push(...patternHelper.getSearchPaths(result.patterns))
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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 ''
|
||||
}
|
||||
}
|
||||
@@ -43,27 +43,15 @@ export class Pattern {
|
||||
*/
|
||||
private readonly rootRegExp: RegExp
|
||||
|
||||
/**
|
||||
* Indicates that the pattern is implicitly added as opposed to user specified.
|
||||
*/
|
||||
private readonly isImplicitPattern: boolean
|
||||
/* eslint-disable no-dupe-class-members */
|
||||
// Disable no-dupe-class-members due to false positive for method overload
|
||||
// https://github.com/typescript-eslint/typescript-eslint/issues/291
|
||||
|
||||
constructor(pattern: string)
|
||||
constructor(
|
||||
pattern: string,
|
||||
isImplicitPattern: boolean,
|
||||
segments: undefined,
|
||||
homedir: string
|
||||
)
|
||||
constructor(
|
||||
negate: boolean,
|
||||
isImplicitPattern: boolean,
|
||||
segments: string[],
|
||||
homedir?: string
|
||||
)
|
||||
constructor(pattern: string, segments: undefined, homedir: string)
|
||||
constructor(negate: boolean, segments: string[])
|
||||
constructor(
|
||||
patternOrNegate: string | boolean,
|
||||
isImplicitPattern = false,
|
||||
segments?: string[],
|
||||
homedir?: string
|
||||
) {
|
||||
@@ -119,8 +107,6 @@ export class Pattern {
|
||||
IS_WINDOWS ? 'i' : ''
|
||||
)
|
||||
|
||||
this.isImplicitPattern = isImplicitPattern
|
||||
|
||||
// Create minimatch
|
||||
const minimatchOptions: IMinimatchOptions = {
|
||||
dot: true,
|
||||
@@ -146,7 +132,7 @@ export class Pattern {
|
||||
// Append a trailing slash. Otherwise Minimatch will not match the directory immediately
|
||||
// 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.
|
||||
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.
|
||||
// For example, formats like C: and C:foo on Windows are resolved to an absolute root.
|
||||
itemPath = `${itemPath}${path.sep}`
|
||||
|
||||
+1
-11
@@ -1,19 +1,9 @@
|
||||
# @actions/io Releases
|
||||
|
||||
### 1.1.2
|
||||
- Update `lockfileVersion` to `v2` in `package-lock.json [#1020](https://github.com/actions/toolkit/pull/1020)
|
||||
|
||||
### 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
|
||||
|
||||
- [Add \"types\" to package.json](https://github.com/actions/toolkit/pull/221)
|
||||
|
||||
### 1.0.0
|
||||
|
||||
- Initial release
|
||||
- Initial release
|
||||
@@ -3,12 +3,9 @@ import {promises as fs} from 'fs'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
import * as io from '../src/io'
|
||||
import * as ioUtil from '../src/io-util'
|
||||
|
||||
describe('cp', () => {
|
||||
beforeAll(async () => {
|
||||
await io.rmRF(getTestTemp())
|
||||
})
|
||||
|
||||
it('copies file with no flags', async () => {
|
||||
const root = path.join(getTestTemp(), 'cp_with_no_flags')
|
||||
const sourceFile = path.join(root, 'cp_source')
|
||||
@@ -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 () => {
|
||||
const root: string = path.join(getTestTemp(), 'cp_with_-r_nonexistent_dest')
|
||||
const sourceFolder: string = path.join(root, 'cp_source')
|
||||
@@ -192,10 +166,6 @@ describe('cp', () => {
|
||||
})
|
||||
|
||||
describe('mv', () => {
|
||||
beforeAll(async () => {
|
||||
await io.rmRF(getTestTemp())
|
||||
})
|
||||
|
||||
it('moves file with no flags', async () => {
|
||||
const root = path.join(getTestTemp(), ' mv_with_no_flags')
|
||||
const sourceFile = path.join(root, ' mv_source')
|
||||
@@ -294,10 +264,6 @@ describe('mv', () => {
|
||||
})
|
||||
|
||||
describe('rmRF', () => {
|
||||
beforeAll(async () => {
|
||||
await io.rmRF(getTestTemp())
|
||||
})
|
||||
|
||||
it('removes single folder with rmRF', async () => {
|
||||
const testPath = path.join(getTestTemp(), 'testFolder')
|
||||
|
||||
@@ -556,45 +522,6 @@ describe('rmRF', () => {
|
||||
await assertNotExists(symlinkFile)
|
||||
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 () => {
|
||||
@@ -886,13 +813,34 @@ describe('mkdirP', () => {
|
||||
(await fs.lstat(path.join(realDirPath, 'sub_dir'))).isDirectory()
|
||||
).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', () => {
|
||||
beforeAll(async () => {
|
||||
await io.rmRF(getTestTemp())
|
||||
})
|
||||
|
||||
it('which() finds file name', async () => {
|
||||
// create a executable file
|
||||
const testPath = path.join(getTestTemp(), 'which-finds-file-name')
|
||||
@@ -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(
|
||||
chmodOptions: string
|
||||
): Promise<void> {
|
||||
|
||||
Generated
+2
-10
@@ -1,13 +1,5 @@
|
||||
{
|
||||
"name": "@actions/io",
|
||||
"version": "1.1.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@actions/io",
|
||||
"version": "1.1.2",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
"version": "1.0.2",
|
||||
"lockfileVersion": 1
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/io",
|
||||
"version": "1.1.2",
|
||||
"version": "1.0.2",
|
||||
"description": "Actions io lib",
|
||||
"keywords": [
|
||||
"github",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {ok} from 'assert'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
@@ -33,7 +34,7 @@ export async function exists(fsPath: string): Promise<boolean> {
|
||||
|
||||
export async function isDirectory(
|
||||
fsPath: string,
|
||||
useStat = false
|
||||
useStat: boolean = false
|
||||
): Promise<boolean> {
|
||||
const stats = useStat ? await stat(fsPath) : await lstat(fsPath)
|
||||
return stats.isDirectory()
|
||||
@@ -58,6 +59,52 @@ export function isRooted(p: string): boolean {
|
||||
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.
|
||||
* @param filePath file path to check
|
||||
@@ -166,8 +213,3 @@ function isUnixExecutable(stats: fs.Stats): boolean {
|
||||
((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
@@ -1,11 +1,9 @@
|
||||
import {ok} from 'assert'
|
||||
import * as childProcess from 'child_process'
|
||||
import * as path from 'path'
|
||||
import {promisify} from 'util'
|
||||
import * as ioUtil from './io-util'
|
||||
|
||||
const exec = promisify(childProcess.exec)
|
||||
const execFile = promisify(childProcess.execFile)
|
||||
|
||||
/**
|
||||
* Interface for cp/mv options
|
||||
@@ -15,8 +13,6 @@ export interface CopyOptions {
|
||||
recursive?: boolean
|
||||
/** Optional. Whether to overwrite existing files in the destination. Defaults to true */
|
||||
force?: boolean
|
||||
/** Optional. Whether to copy the source directory along with all the files. Only takes effect when recursive=true and copying a directory. Default is true*/
|
||||
copySourceDirectory?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,7 +36,7 @@ export async function cp(
|
||||
dest: string,
|
||||
options: CopyOptions = {}
|
||||
): Promise<void> {
|
||||
const {force, recursive, copySourceDirectory} = readCopyOptions(options)
|
||||
const {force, recursive} = readCopyOptions(options)
|
||||
|
||||
const destStat = (await ioUtil.exists(dest)) ? await ioUtil.stat(dest) : null
|
||||
// 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.
|
||||
const newDest: string =
|
||||
destStat && destStat.isDirectory() && copySourceDirectory
|
||||
destStat && destStat.isDirectory()
|
||||
? path.join(dest, path.basename(source))
|
||||
: dest
|
||||
|
||||
@@ -118,24 +114,11 @@ export async function rmRF(inputPath: string): Promise<void> {
|
||||
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
|
||||
// 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 {
|
||||
const cmdPath = ioUtil.getCmdPath()
|
||||
if (await ioUtil.isDirectory(inputPath, true)) {
|
||||
await exec(`${cmdPath} /s /c "rd /s /q "%inputPath%""`, {
|
||||
env: {inputPath}
|
||||
})
|
||||
await exec(`rd /s /q "${inputPath}"`)
|
||||
} else {
|
||||
await exec(`${cmdPath} /s /c "del /f /a "%inputPath%""`, {
|
||||
env: {inputPath}
|
||||
})
|
||||
await exec(`del /f /a "${inputPath}"`)
|
||||
}
|
||||
} catch (err) {
|
||||
// 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) {
|
||||
await execFile(`rm`, [`-rf`, `${inputPath}`])
|
||||
await exec(`rm -rf "${inputPath}"`)
|
||||
} else {
|
||||
await ioUtil.unlink(inputPath)
|
||||
}
|
||||
@@ -178,8 +161,7 @@ export async function rmRF(inputPath: string): Promise<void> {
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export async function mkdirP(fsPath: string): Promise<void> {
|
||||
ok(fsPath, 'a path argument must be provided')
|
||||
await ioUtil.mkdir(fsPath, {recursive: true})
|
||||
await ioUtil.mkdirP(fsPath)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,95 +192,75 @@ export async function which(tool: string, check?: boolean): Promise<string> {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const matches: string[] = await findInPath(tool)
|
||||
|
||||
if (matches && matches.length > 0) {
|
||||
return matches[0]
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all occurrences of the given tool on the system path.
|
||||
*
|
||||
* @returns Promise<string[]> the paths of the tool
|
||||
*/
|
||||
export async function findInPath(tool: string): Promise<string[]> {
|
||||
if (!tool) {
|
||||
throw new Error("parameter 'tool' is required")
|
||||
}
|
||||
|
||||
// build the list of extensions to try
|
||||
const extensions: string[] = []
|
||||
if (ioUtil.IS_WINDOWS && process.env['PATHEXT']) {
|
||||
for (const extension of process.env['PATHEXT'].split(path.delimiter)) {
|
||||
if (extension) {
|
||||
extensions.push(extension)
|
||||
try {
|
||||
// build the list of extensions to try
|
||||
const extensions: string[] = []
|
||||
if (ioUtil.IS_WINDOWS && process.env.PATHEXT) {
|
||||
for (const extension of process.env.PATHEXT.split(path.delimiter)) {
|
||||
if (extension) {
|
||||
extensions.push(extension)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if it's rooted, return it if exists. otherwise return empty.
|
||||
if (ioUtil.isRooted(tool)) {
|
||||
const filePath: string = await ioUtil.tryGetExecutablePath(tool, extensions)
|
||||
// if it's rooted, return it if exists. otherwise return empty.
|
||||
if (ioUtil.isRooted(tool)) {
|
||||
const filePath: string = await ioUtil.tryGetExecutablePath(
|
||||
tool,
|
||||
extensions
|
||||
)
|
||||
|
||||
if (filePath) {
|
||||
return [filePath]
|
||||
if (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
|
||||
if (tool.includes(path.sep)) {
|
||||
return []
|
||||
}
|
||||
// build the list of directories
|
||||
//
|
||||
// Note, technically "where" checks the current directory on Windows. From a toolkit perspective,
|
||||
// it feels like we should not do this. Checking the current directory seems like more of a use
|
||||
// case of a shell, and the which() function exposed by the toolkit should strive for consistency
|
||||
// across platforms.
|
||||
const directories: string[] = []
|
||||
|
||||
// build the list of directories
|
||||
//
|
||||
// Note, technically "where" checks the current directory on Windows. From a toolkit perspective,
|
||||
// it feels like we should not do this. Checking the current directory seems like more of a use
|
||||
// case of a shell, and the which() function exposed by the toolkit should strive for consistency
|
||||
// across platforms.
|
||||
const directories: string[] = []
|
||||
|
||||
if (process.env.PATH) {
|
||||
for (const p of process.env.PATH.split(path.delimiter)) {
|
||||
if (p) {
|
||||
directories.push(p)
|
||||
if (process.env.PATH) {
|
||||
for (const p of process.env.PATH.split(path.delimiter)) {
|
||||
if (p) {
|
||||
directories.push(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// find all matches
|
||||
const matches: string[] = []
|
||||
|
||||
for (const directory of directories) {
|
||||
const filePath = await ioUtil.tryGetExecutablePath(
|
||||
path.join(directory, tool),
|
||||
extensions
|
||||
)
|
||||
if (filePath) {
|
||||
matches.push(filePath)
|
||||
// return the first match
|
||||
for (const directory of directories) {
|
||||
const filePath = await ioUtil.tryGetExecutablePath(
|
||||
directory + path.sep + tool,
|
||||
extensions
|
||||
)
|
||||
if (filePath) {
|
||||
return filePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches
|
||||
return ''
|
||||
} catch (err) {
|
||||
throw new Error(`which failed with message ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
function readCopyOptions(options: CopyOptions): Required<CopyOptions> {
|
||||
const force = options.force == null ? true : options.force
|
||||
const recursive = Boolean(options.recursive)
|
||||
const copySourceDirectory =
|
||||
options.copySourceDirectory == null
|
||||
? true
|
||||
: Boolean(options.copySourceDirectory)
|
||||
return {force, recursive, copySourceDirectory}
|
||||
return {force, recursive}
|
||||
}
|
||||
|
||||
async function cpDirRecursive(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user