Compare commits

..

1 Commits

Author SHA1 Message Date
Brian Cristante bcb0e62b16 Copy over http-client files 2022-04-28 09:33:03 -04:00
103 changed files with 18963 additions and 11662 deletions
+9 -9
View File
@@ -22,12 +22,12 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Set Node.js 16.x - name: Set Node.js 12.x
uses: actions/setup-node@v3 uses: actions/setup-node@v1
with: with:
node-version: 16.x node-version: 12.x
# In order to upload & download artifacts from a shell script, certain env variables need to be set that are only available in the # In order to upload & download artifacts from a shell script, certain env variables need to be set that are only available in the
# node context. This runs a local action that gets and sets the necessary env variables that are needed # node context. This runs a local action that gets and sets the necessary env variables that are needed
@@ -55,16 +55,16 @@ jobs:
- name: Create files that will be uploaded - name: Create files that will be uploaded
run: | run: |
mkdir artifact-path mkdir artifact-path
echo '${{ env.non-gzip-artifact-content }}' > artifact-path/world.txt echo ${{ env.non-gzip-artifact-content }} > artifact-path/world.txt
echo '${{ env.gzip-artifact-content }}' > artifact-path/gzip.txt echo ${{ env.gzip-artifact-content }} > artifact-path/gzip.txt
touch artifact-path/empty.txt touch artifact-path/empty.txt
# We're using node -e to call the functions directly available in the @actions/artifact package # We're using node -e to call the functions directly available in the @actions/artifact package
- name: Upload artifacts using uploadArtifact() - name: Upload artifacts using uploadArtifact()
run: | run: |
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().uploadArtifact('my-artifact-1',['artifact-path/world.txt'], process.argv[1]))" "${{ github.workspace }}" 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'], process.argv[1]))" "${{ 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'], process.argv[1]))" "${{ 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() - name: Download artifacts using downloadArtifact()
run: | run: |
+7 -6
View File
@@ -18,12 +18,12 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Set Node.js 16.x - name: Set Node.js 12.x
uses: actions/setup-node@v3 uses: actions/setup-node@v1
with: with:
node-version: 16.x node-version: 12.x
- name: npm install - name: npm install
run: npm install run: npm install
@@ -31,8 +31,9 @@ jobs:
- name: Bootstrap - name: Bootstrap
run: npm run bootstrap run: npm run bootstrap
- name: audit tools (without allow-list) - name: audit tools
run: npm audit --audit-level=moderate # `|| 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 packages - name: audit packages
run: npm run audit-all run: npm run audit-all
+4 -4
View File
@@ -22,12 +22,12 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Set Node.js 16.x - name: Set Node.js 12.x
uses: actions/setup-node@v3 uses: actions/setup-node@v1
with: with:
node-version: 16.x node-version: 12.x
# In order to save & restore cache from a shell script, certain env variables need to be set that are only available in the # In order to save & restore cache from a shell script, certain env variables need to be set that are only available in the
# node context. This runs a local action that gets and sets the necessary env variables that are needed # node context. This runs a local action that gets and sets the necessary env variables that are needed
-90
View File
@@ -1,90 +0,0 @@
name: cache-windows-bsd-unit-tests
on:
push:
branches:
- main
paths-ignore:
- '**.md'
pull_request:
paths-ignore:
- '**.md'
jobs:
build:
name: Build
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- shell: bash
run: |
rm "C:\Program Files\Git\usr\bin\tar.exe"
- name: Set Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
# In order to save & restore cache from a shell script, certain env variables need to be set that are only available in the
# node context. This runs a local action that gets and sets the necessary env variables that are needed
- name: Set env variables
uses: ./packages/cache/__tests__/__fixtures__/
# Need root node_modules because certain npm packages like jest are configured for the entire repository and it won't be possible
# without these to just compile the cache package
- name: Install root npm packages
run: npm ci
- name: Compile cache package
run: |
npm ci
npm run tsc
working-directory: packages/cache
- name: Generate files in working directory
shell: bash
run: packages/cache/__tests__/create-cache-files.sh ${{ runner.os }} test-cache
- name: Generate files outside working directory
shell: bash
run: packages/cache/__tests__/create-cache-files.sh ${{ runner.os }} ~/test-cache
# We're using node -e to call the functions directly available in the @actions/cache package
- name: Save cache using saveCache()
run: |
node -e "Promise.resolve(require('./packages/cache/lib/cache').saveCache(['test-cache','~/test-cache'],'test-${{ runner.os }}-${{ github.run_id }}'))"
- name: Delete cache folders before restoring
shell: bash
run: |
rm -rf test-cache
rm -rf ~/test-cache
- name: Restore cache using restoreCache() with http-client
run: |
node -e "Promise.resolve(require('./packages/cache/lib/cache').restoreCache(['test-cache','~/test-cache'],'test-${{ runner.os }}-${{ github.run_id }}',[],{useAzureSdk: false}))"
- name: Verify cache restored with http-client
shell: bash
run: |
packages/cache/__tests__/verify-cache-files.sh ${{ runner.os }} test-cache
packages/cache/__tests__/verify-cache-files.sh ${{ runner.os }} ~/test-cache
- name: Delete cache folders before restoring
shell: bash
run: |
rm -rf test-cache
rm -rf ~/test-cache
- name: Restore cache using restoreCache() with Azure SDK
run: |
node -e "Promise.resolve(require('./packages/cache/lib/cache').restoreCache(['test-cache','~/test-cache'],'test-${{ runner.os }}-${{ github.run_id }}'))"
- name: Verify cache restored with Azure SDK
shell: bash
run: |
packages/cache/__tests__/verify-cache-files.sh ${{ runner.os }} test-cache
packages/cache/__tests__/verify-cache-files.sh ${{ runner.os }} ~/test-cache
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
+21 -21
View File
@@ -5,60 +5,60 @@ on:
inputs: inputs:
package: package:
required: true required: true
description: 'core, artifact, cache, exec, github, glob, http-client, io, tool-cache' description: 'core, artifact, cache, exec, github, glob, io, tool-cache'
jobs: jobs:
test: test:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- name: setup repo - name: setup repo
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: verify package exists - name: verify package exists
run: ls packages/${{ github.event.inputs.package }} run: ls packages/${{ github.event.inputs.package }}
- name: Set Node.js 16.x - name: Set Node.js 12.x
uses: actions/setup-node@v3 uses: actions/setup-node@v1
with: with:
node-version: 16.x node-version: 12.x
- name: npm install - name: npm install
run: npm install run: npm install
- name: bootstrap - name: bootstrap
run: npm run bootstrap run: npm run bootstrap
- name: build - name: build
run: npm run build run: npm run build
- name: test - name: test
run: npm run test run: npm run test
- name: pack - name: pack
run: npm pack run: npm pack
working-directory: packages/${{ github.event.inputs.package }} working-directory: packages/${{ github.event.inputs.package }}
- name: upload artifact - name: upload artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v2
with: with:
name: ${{ github.event.inputs.package }} name: ${{ github.event.inputs.package }}
path: packages/${{ github.event.inputs.package }}/*.tgz path: packages/${{ github.event.inputs.package }}/*.tgz
publish: publish:
runs-on: macos-latest runs-on: macos-latest
needs: test needs: test
environment: npm-publish environment: npm-publish
steps: steps:
- name: download artifact - name: download artifact
uses: actions/download-artifact@v3 uses: actions/download-artifact@v2
with: with:
name: ${{ github.event.inputs.package }} name: ${{ github.event.inputs.package }}
- name: setup authentication - name: setup authentication
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc
env: env:
NPM_TOKEN: ${{ secrets.TOKEN }} NPM_TOKEN: ${{ secrets.TOKEN }}
- name: publish - name: publish
@@ -68,13 +68,13 @@ jobs:
if: failure() if: failure()
run: | 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 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: env:
SLACK_WEBHOOK: ${{ secrets.SLACK }} SLACK_WEBHOOK: ${{ secrets.SLACK }}
- name: notify slack on success - name: notify slack on success
if: success() if: success()
run: | run: |
curl -X POST -H 'Content-type: application/json' --data '{"text":":dance: Successfully published a new version of ${{ github.event.inputs.package }}"}' $SLACK_WEBHOOK curl -X POST -H 'Content-type: application/json' --data '{"text":":dance: Successfully published a new version of ${{ github.event.inputs.package }}"}' $SLACK_WEBHOOK
env: env:
SLACK_WEBHOOK: ${{ secrets.SLACK }} SLACK_WEBHOOK: ${{ secrets.SLACK }}
+5 -5
View File
@@ -23,12 +23,12 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Set Node.js 16.x - name: Set Node.js 12.x
uses: actions/setup-node@v3 uses: actions/setup-node@v1
with: with:
node-version: 16.x node-version: 12.x
- name: npm install - name: npm install
run: npm install run: npm install
@@ -40,7 +40,7 @@ jobs:
run: npm run build run: npm run build
- name: npm test - name: npm test
run: npm test -- --runInBand run: npm test
env: env:
GITHUB_TOKEN: ${{ github.token }} GITHUB_TOKEN: ${{ github.token }}
+2 -2
View File
@@ -9,7 +9,7 @@ jobs:
if: ${{ github.repository_owner == 'actions' }} if: ${{ github.repository_owner == 'actions' }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Update Octokit - name: Update Octokit
working-directory: packages/github working-directory: packages/github
run: | run: |
@@ -30,7 +30,7 @@ jobs:
fi fi
- name: Create PR - name: Create PR
if: ${{steps.status.outputs.createPR}} if: ${{steps.status.outputs.createPR}}
uses: actions/github-script@v6 uses: actions/github-script@v2
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GITHUB_TOKEN}}
script: | script: |
-9
View File
@@ -46,15 +46,6 @@ $ npm install @actions/glob
``` ```
<br/> <br/>
:phone: [@actions/http-client](packages/http-client)
A lightweight HTTP client optimized for building actions. Read more [here](packages/http-client)
```bash
$ npm install @actions/http-client
```
<br/>
:pencil2: [@actions/io](packages/io) :pencil2: [@actions/io](packages/io)
Provides disk i/o functions like cp, mv, rmRF, which etc. Read more [here](packages/io) Provides disk i/o functions like cp, mv, rmRF, which etc. Read more [here](packages/io)
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
os: [ubuntu-16.04, windows-2019] os: [ubuntu-16.04, windows-2019]
runs-on: ${{matrix.os}} runs-on: ${{matrix.os}}
actions: actions:
- uses: actions/setup-node@v3 - uses: actions/setup-node@v1
with: with:
version: ${{matrix.node}} version: ${{matrix.node}}
- run: | - run: |
+1 -15
View File
@@ -50,18 +50,7 @@ function setSecret(secret: string): void {}
Now, future logs containing BAR will be masked. E.g. running `echo "Hello FOO BAR World"` will now print `Hello FOO **** World`. Now, future logs containing BAR will be masked. E.g. running `echo "Hello FOO BAR World"` will now print `Hello FOO **** World`.
**WARNING** The add-mask and setSecret commands only support single-line **WARNING** The add-mask and setSecret commands only support single line secrets. To register a multiline secrets you must register each line individually otherwise it will not be masked.
secrets or multi-line secrets that have been escaped. `@actions/core`
`setSecret` will escape the string you provide by default. When an escaped
multi-line string is provided the whole string and each of its lines
individually will be masked. For example you can mask `first\nsecond\r\nthird`
using:
```sh
echo "::add-mask::first%0Asecond%0D%0Athird"
```
This will mask `first%0Asecond%0D%0Athird`, `first`, `second` and `third`.
**WARNING** Do **not** mask short values if you can avoid it, it could render your output unreadable (and future steps' output as well). **WARNING** Do **not** mask short values if you can avoid it, it could render your output unreadable (and future steps' output as well).
For example, if you mask the letter `l`, running `echo "Hello FOO BAR World"` will now print `He*********o FOO BAR Wor****d` For example, if you mask the letter `l`, running `echo "Hello FOO BAR World"` will now print `He*********o FOO BAR Wor****d`
@@ -111,12 +100,9 @@ There are several commands to emit different levels of log output:
| log level | example usage | | log level | example usage |
|---|---| |---|---|
| [debug](action-debugging.md) | `echo "::debug::My debug message"` | | [debug](action-debugging.md) | `echo "::debug::My debug message"` |
| notice | `echo "::notice::My notice message"` |
| warning | `echo "::warning::My warning message"` | | warning | `echo "::warning::My warning message"` |
| error | `echo "::error::My error message"` | | error | `echo "::error::My error message"` |
Additional syntax options are described at [the workflow command documentation](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-a-debug-message).
### Command Echoing ### Command Echoing
By default, the echoing of commands to stdout only occurs if [Step Debugging is enabled](./action-debugging.md#How-to-Access-Step-Debug-Logs) By default, the echoing of commands to stdout only occurs if [Step Debugging is enabled](./action-debugging.md#How-to-Access-Step-Debug-Logs)
+1 -1
View File
@@ -18,7 +18,7 @@ e.g. To use https://github.com/actions/setup-node, users will author:
```yaml ```yaml
steps: steps:
using: actions/setup-node@v3 using: actions/setup-node@v1
``` ```
# Define Metadata # Define Metadata
+3 -3
View File
@@ -4,7 +4,7 @@ Self-hosted runners [can be configured](https://help.github.com/en/actions/hosti
For actions to **just work** behind a proxy server: For actions to **just work** behind a proxy server:
1. Use [tool-cache](/packages/tool-cache) version >= 1.3.1 1. Use [tool-cache] version >= 1.3.1
2. Optionally use [actions/http-client](/packages/http-client) 2. Optionally use [actions/http-client](https://github.com/actions/http-client)
If you are using other http clients, refer to the [environment variables set by the runner](https://help.github.com/en/actions/hosting-your-own-runners/using-a-proxy-server-with-self-hosted-runners). If you are using other http clients, refer to the [environment variables set by the runner](https://help.github.com/en/actions/hosting-your-own-runners/using-a-proxy-server-with-self-hosted-runners).
+6846 -7783
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -15,7 +15,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^27.0.2", "@types/jest": "^27.0.2",
"@types/node": "^16.18.1", "@types/node": "^12.20.13",
"@types/signale": "^1.4.1", "@types/signale": "^1.4.1",
"@typescript-eslint/parser": "^4.0.0", "@typescript-eslint/parser": "^4.0.0",
"concurrently": "^6.1.0", "concurrently": "^6.1.0",
@@ -24,7 +24,7 @@
"eslint-plugin-jest": "^22.21.0", "eslint-plugin-jest": "^22.21.0",
"flow-bin": "^0.115.0", "flow-bin": "^0.115.0",
"jest": "^27.2.5", "jest": "^27.2.5",
"lerna": "^5.4.0", "lerna": "^4.0.0",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"ts-jest": "^27.0.5", "ts-jest": "^27.0.5",
"typescript": "^3.9.9" "typescript": "^3.9.9"
+1 -17
View File
@@ -77,20 +77,4 @@
### 1.0.0 ### 1.0.0
- Update `lockfileVersion` to `v2` in `package-lock.json` [#1009](https://github.com/actions/toolkit/pull/1009) - Update `lockfileVersion` to `v2` in `package-lock.json [#1009](https://github.com/actions/toolkit/pull/1009)
### 1.0.1
- Update to v2.0.0 of `@actions/http-client`
### 1.0.2
- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
### 1.1.0
- Add `x-actions-results-crc64` and `x-actions-results-md5` checksum headers on upload [#1063](https://github.com/actions/toolkit/pull/1063)
### 1.1.1
- Fixed a bug in Node16 where if an HTTP download finished too quickly (<1ms, e.g. when it's mocked) we attempt to delete a temp file that has not been created yet [#1278](https://github.com/actions/toolkit/pull/1278/commits/b9de68a590daf37c6747e38d3cb4f1dd2cfb791c)
-57
View File
@@ -1,57 +0,0 @@
import CRC64, {CRC64DigestEncoding} from '../src/internal/crc64'
const fixtures = {
data:
'🚀 👉😎👉 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n',
expected: {
hex: '846CE4ADAD6223ED',
base64: '7SNira3kbIQ=',
buffer: Buffer.from([0xed, 0x23, 0x62, 0xad, 0xad, 0xe4, 0x6c, 0x84])
}
}
function assertEncodings(crc: CRC64): void {
const encodings = Object.keys(fixtures.expected) as CRC64DigestEncoding[]
for (const encoding of encodings) {
expect(crc.digest(encoding)).toEqual(fixtures.expected[encoding])
}
}
describe('@actions/artifact/src/internal/crc64', () => {
it('CRC64 from string', async () => {
const crc = new CRC64()
crc.update(fixtures.data)
assertEncodings(crc)
})
it('CRC64 from buffer', async () => {
const crc = new CRC64()
const buf = Buffer.from(fixtures.data)
crc.update(buf)
assertEncodings(crc)
})
it('CRC64 from split data', async () => {
const crc = new CRC64()
const splits = fixtures.data.split('\n').slice(0, -1)
for (const split of splits) {
crc.update(`${split}\n`)
}
assertEncodings(crc)
})
it('flips 64 bits', async () => {
const tests = [
[BigInt(0), BigInt('0xffffffffffffffff')],
[BigInt('0xffffffffffffffff'), BigInt(0)],
[BigInt('0xdeadbeef'), BigInt('0xffffffff21524110')]
]
for (const [input, expected] of tests) {
expect(CRC64.flip64Bits(input)).toEqual(expected)
}
})
})
+3 -2
View File
@@ -3,6 +3,7 @@ import * as net from 'net'
import * as core from '@actions/core' import * as core from '@actions/core'
import * as configVariables from '../src/internal/config-variables' import * as configVariables from '../src/internal/config-variables'
import {retry} from '../src/internal/requestUtils' import {retry} from '../src/internal/requestUtils'
import {IHttpClientResponse} from '@actions/http-client/interfaces'
import {HttpClientResponse} from '@actions/http-client' import {HttpClientResponse} from '@actions/http-client'
jest.mock('../src/internal/config-variables') jest.mock('../src/internal/config-variables')
@@ -41,7 +42,7 @@ async function testRetry(
async function handleResponse( async function handleResponse(
testResponseCode: number | undefined testResponseCode: number | undefined
): Promise<HttpClientResponse> { ): Promise<IHttpClientResponse> {
if (!testResponseCode) { if (!testResponseCode) {
throw new Error( throw new Error(
'Test incorrectly set up. reverse.pop() was called too many times so not enough test response codes were supplied' 'Test incorrectly set up. reverse.pop() was called too many times so not enough test response codes were supplied'
@@ -71,7 +72,7 @@ async function emptyMockReadBody(): Promise<string> {
async function setupSingleMockResponse( async function setupSingleMockResponse(
statusCode: number statusCode: number
): Promise<HttpClientResponse> { ): Promise<IHttpClientResponse> {
const mockMessage = new http.IncomingMessage(new net.Socket()) const mockMessage = new http.IncomingMessage(new net.Socket())
const mockReadBody = emptyMockReadBody const mockReadBody = emptyMockReadBody
mockMessage.statusCode = statusCode mockMessage.statusCode = statusCode
+13 -88
View File
@@ -6,28 +6,13 @@ import {promises as fs} from 'fs'
import {createGZipFileOnDisk} from '../src/internal/upload-gzip' import {createGZipFileOnDisk} from '../src/internal/upload-gzip'
const root = path.join(__dirname, '_temp', 'upload-gzip') const root = path.join(__dirname, '_temp', 'upload-gzip')
const tempGzFilePath = path.join(root, 'file.gz') const tempGzipFilePath = path.join(root, 'file1.gzip')
const tempGzipFilePath = path.join(root, 'file.gzip') const tempZipFilePath = path.join(root, 'file2.zip')
const tempTgzFilePath = path.join(root, 'file.tgz') const tempTarlzFilePath = path.join(root, 'file3.tar.lz')
const tempTazFilePath = path.join(root, 'file.taz') const tempGzFilePath = path.join(root, 'file4.tar.gz')
const tempZFilePath = path.join(root, 'file.Z') const tempBz2FilePath = path.join(root, 'file5.tar.bz2')
const tempTaZFilePath = path.join(root, 'file.taZ') const temp7zFilePath = path.join(root, 'file6.7z')
const tempBz2FilePath = path.join(root, 'file.bz2') const tempNormalFilePath = path.join(root, 'file6.txt')
const tempTbzFilePath = path.join(root, 'file.tbz')
const tempTbz2FilePath = path.join(root, 'file.tbz2')
const tempTz2FilePath = path.join(root, 'file.tz2')
const tempLzFilePath = path.join(root, 'file.lz')
const tempLzmaFilePath = path.join(root, 'file.lzma')
const tempTlzFilePath = path.join(root, 'file.tlz')
const tempLzoFilePath = path.join(root, 'file.lzo')
const tempXzFilePath = path.join(root, 'file.xz')
const tempTxzFilePath = path.join(root, 'file.txz')
const tempZstFilePath = path.join(root, 'file.zst')
const tempZstdFilePath = path.join(root, 'file.zstd')
const tempTzstFilePath = path.join(root, 'file.tzst')
const tempZipFilePath = path.join(root, 'file.zip')
const temp7zFilePath = path.join(root, 'file.7z')
const tempNormalFilePath = path.join(root, 'file.txt')
jest.mock('../src/internal/config-variables') jest.mock('../src/internal/config-variables')
@@ -42,26 +27,11 @@ beforeAll(async () => {
// clear temp directory and create files that will be "uploaded" // clear temp directory and create files that will be "uploaded"
await io.rmRF(root) await io.rmRF(root)
await fs.mkdir(path.join(root)) await fs.mkdir(path.join(root))
await fs.writeFile(tempGzFilePath, 'a file with a .gz file extension')
await fs.writeFile(tempGzipFilePath, 'a file with a .gzip file extension') await fs.writeFile(tempGzipFilePath, 'a file with a .gzip file extension')
await fs.writeFile(tempTgzFilePath, 'a file with a .tgz file extension')
await fs.writeFile(tempTazFilePath, 'a file with a .taz file extension')
await fs.writeFile(tempZFilePath, 'a file with a .Z file extension')
await fs.writeFile(tempTaZFilePath, 'a file with a .taZ file extension')
await fs.writeFile(tempBz2FilePath, 'a file with a .bz2 file extension')
await fs.writeFile(tempTbzFilePath, 'a file with a .tbz file extension')
await fs.writeFile(tempTbz2FilePath, 'a file with a .tbz2 file extension')
await fs.writeFile(tempTz2FilePath, 'a file with a .tz2 file extension')
await fs.writeFile(tempLzFilePath, 'a file with a .lz file extension')
await fs.writeFile(tempLzmaFilePath, 'a file with a .lzma file extension')
await fs.writeFile(tempTlzFilePath, 'a file with a .tlz file extension')
await fs.writeFile(tempLzoFilePath, 'a file with a .lzo file extension')
await fs.writeFile(tempXzFilePath, 'a file with a .xz file extension')
await fs.writeFile(tempTxzFilePath, 'a file with a .txz file extension')
await fs.writeFile(tempZstFilePath, 'a file with a .zst file extension')
await fs.writeFile(tempZstdFilePath, 'a file with a .zstd file extension')
await fs.writeFile(tempTzstFilePath, 'a file with a .tzst file extension')
await fs.writeFile(tempZipFilePath, 'a file with a .zip 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(temp7zFilePath, 'a file with a .7z file extension')
await fs.writeFile(tempNormalFilePath, 'a file with a .txt file extension') await fs.writeFile(tempNormalFilePath, 'a file with a .txt file extension')
}) })
@@ -70,66 +40,21 @@ test('Number.MAX_SAFE_INTEGER is returned when an existing compressed file is us
// create temporary file // create temporary file
const tempFile = await tmp.file() const tempFile = await tmp.file()
expect(await createGZipFileOnDisk(tempGzFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(tempGzipFilePath, tempFile.path)).toEqual( expect(await createGZipFileOnDisk(tempGzipFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER Number.MAX_SAFE_INTEGER
) )
expect(await createGZipFileOnDisk(tempTgzFilePath, tempFile.path)).toEqual( expect(await createGZipFileOnDisk(tempZipFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER Number.MAX_SAFE_INTEGER
) )
expect(await createGZipFileOnDisk(tempTazFilePath, tempFile.path)).toEqual( expect(await createGZipFileOnDisk(tempTarlzFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER Number.MAX_SAFE_INTEGER
) )
expect(await createGZipFileOnDisk(tempZFilePath, tempFile.path)).toEqual( expect(await createGZipFileOnDisk(tempGzFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(tempTaZFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER Number.MAX_SAFE_INTEGER
) )
expect(await createGZipFileOnDisk(tempBz2FilePath, tempFile.path)).toEqual( expect(await createGZipFileOnDisk(tempBz2FilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER Number.MAX_SAFE_INTEGER
) )
expect(await createGZipFileOnDisk(tempTbzFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(tempTbz2FilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(tempTz2FilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(tempLzFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(tempLzmaFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(tempTlzFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(tempLzoFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(tempXzFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(tempTxzFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(tempZstFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(tempZstdFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(tempTzstFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(tempZipFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER
)
expect(await createGZipFileOnDisk(temp7zFilePath, tempFile.path)).toEqual( expect(await createGZipFileOnDisk(temp7zFilePath, tempFile.path)).toEqual(
Number.MAX_SAFE_INTEGER Number.MAX_SAFE_INTEGER
) )
+2 -19
View File
@@ -10,7 +10,6 @@ import {
getInitialRetryIntervalInMilliseconds, getInitialRetryIntervalInMilliseconds,
getRetryMultiplier getRetryMultiplier
} from '../src/internal/config-variables' } from '../src/internal/config-variables'
import {Readable} from 'stream'
jest.mock('../src/internal/config-variables') jest.mock('../src/internal/config-variables')
@@ -75,20 +74,15 @@ describe('Utils', () => {
const size = 24 const size = 24
const uncompressedLength = 100 const uncompressedLength = 100
const range = 'bytes 0-199/200' const range = 'bytes 0-199/200'
const digest = {
crc64: 'bSzITYnW/P8=',
md5: 'Xiv1fT9AxLbfadrxk2y3ZvgyN0tPwCWafL/wbi9w8mk='
}
const headers = utils.getUploadHeaders( const headers = utils.getUploadHeaders(
contentType, contentType,
true, true,
true, true,
uncompressedLength, uncompressedLength,
size, size,
range, range
digest
) )
expect(Object.keys(headers).length).toEqual(10) expect(Object.keys(headers).length).toEqual(8)
expect(headers['Accept']).toEqual( expect(headers['Accept']).toEqual(
`application/json;api-version=${utils.getApiVersion()}` `application/json;api-version=${utils.getApiVersion()}`
) )
@@ -99,8 +93,6 @@ describe('Utils', () => {
expect(headers['x-tfs-filelength']).toEqual(uncompressedLength) expect(headers['x-tfs-filelength']).toEqual(uncompressedLength)
expect(headers['Content-Length']).toEqual(size) expect(headers['Content-Length']).toEqual(size)
expect(headers['Content-Range']).toEqual(range) expect(headers['Content-Range']).toEqual(range)
expect(headers['x-actions-results-crc64']).toEqual(digest.crc64)
expect(headers['x-actions-results-md5']).toEqual(digest.md5)
}) })
it('Test constructing upload headers with only required parameter', () => { it('Test constructing upload headers with only required parameter', () => {
@@ -227,13 +219,4 @@ describe('Utils', () => {
const size2 = (await fs.promises.stat(emptyFile2)).size const size2 = (await fs.promises.stat(emptyFile2)).size
expect(size2).toEqual(0) expect(size2).toEqual(0)
}) })
it('Creates a digest from a readable stream', async () => {
const data = 'lorem ipsum'
const stream = Readable.from(data)
const digest = await utils.digestForStream(stream)
expect(digest.crc64).toBe('bSzITYnW/P8=')
expect(digest.md5).toBe('gKdR/eV3AoZAxBkADjPrpg==')
})
}) })
+20 -35
View File
@@ -1,16 +1,16 @@
{ {
"name": "@actions/artifact", "name": "@actions/artifact",
"version": "1.1.1", "version": "0.6.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@actions/artifact", "name": "@actions/artifact",
"version": "1.1.1", "version": "0.6.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.9.1", "@actions/core": "^1.2.6",
"@actions/http-client": "^2.0.1", "@actions/http-client": "^1.0.11",
"tmp": "^0.2.1", "tmp": "^0.2.1",
"tmp-promise": "^3.0.2" "tmp-promise": "^3.0.2"
}, },
@@ -20,20 +20,19 @@
} }
}, },
"node_modules/@actions/core": { "node_modules/@actions/core": {
"version": "1.9.1", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz",
"integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==", "integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==",
"dependencies": { "dependencies": {
"@actions/http-client": "^2.0.1", "@actions/http-client": "^1.0.11"
"uuid": "^8.3.2"
} }
}, },
"node_modules/@actions/http-client": { "node_modules/@actions/http-client": {
"version": "2.0.1", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"dependencies": { "dependencies": {
"tunnel": "^0.0.6" "tunnel": "0.0.6"
} }
}, },
"node_modules/@types/tmp": { "node_modules/@types/tmp": {
@@ -180,14 +179,6 @@
"node": ">=4.2.0" "node": ">=4.2.0"
} }
}, },
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/wrappy": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -196,20 +187,19 @@
}, },
"dependencies": { "dependencies": {
"@actions/core": { "@actions/core": {
"version": "1.9.1", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz",
"integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==", "integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==",
"requires": { "requires": {
"@actions/http-client": "^2.0.1", "@actions/http-client": "^1.0.11"
"uuid": "^8.3.2"
} }
}, },
"@actions/http-client": { "@actions/http-client": {
"version": "2.0.1", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"requires": { "requires": {
"tunnel": "^0.0.6" "tunnel": "0.0.6"
} }
}, },
"@types/tmp": { "@types/tmp": {
@@ -325,11 +315,6 @@
"integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==",
"dev": true "dev": true
}, },
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/artifact", "name": "@actions/artifact",
"version": "1.1.1", "version": "1.0.0",
"preview": true, "preview": true,
"description": "Actions artifact lib", "description": "Actions artifact lib",
"keywords": [ "keywords": [
@@ -37,8 +37,8 @@
"url": "https://github.com/actions/toolkit/issues" "url": "https://github.com/actions/toolkit/issues"
}, },
"dependencies": { "dependencies": {
"@actions/core": "^1.9.1", "@actions/core": "^1.2.6",
"@actions/http-client": "^2.0.1", "@actions/http-client": "^1.0.11",
"tmp": "^0.2.1", "tmp": "^0.2.1",
"tmp-promise": "^3.0.2" "tmp-promise": "^3.0.2"
}, },
@@ -203,7 +203,7 @@ Note: The size of downloaded zips can differ significantly from the reported siz
await createDirectoriesForArtifact( await createDirectoriesForArtifact(
downloadSpecification.directoryStructure downloadSpecification.directoryStructure
) )
core.info('Directory structure has been set up for the artifact') core.info('Directory structure has been setup for the artifact')
await createEmptyFilesForArtifact( await createEmptyFilesForArtifact(
downloadSpecification.emptyFilesToCreate downloadSpecification.emptyFilesToCreate
) )
-317
View File
@@ -1,317 +0,0 @@
/**
* CRC64: cyclic redundancy check, 64-bits
*
* In order to validate that artifacts are not being corrupted over the wire, this redundancy check allows us to
* validate that there was no corruption during transmission. The implementation here is based on Go's hash/crc64 pkg,
* but without the slicing-by-8 optimization: https://cs.opensource.google/go/go/+/master:src/hash/crc64/crc64.go
*
* This implementation uses a pregenerated table based on 0x9A6C9329AC4BC9B5 as the polynomial, the same polynomial that
* is used for Azure Storage: https://github.com/Azure/azure-storage-net/blob/cbe605f9faa01bfc3003d75fc5a16b2eaccfe102/Lib/Common/Core/Util/Crc64.cs#L27
*/
// when transpile target is >= ES2020 (after dropping node 12) these can be changed to bigint literals - ts(2737)
const PREGEN_POLY_TABLE = [
BigInt('0x0000000000000000'),
BigInt('0x7F6EF0C830358979'),
BigInt('0xFEDDE190606B12F2'),
BigInt('0x81B31158505E9B8B'),
BigInt('0xC962E5739841B68F'),
BigInt('0xB60C15BBA8743FF6'),
BigInt('0x37BF04E3F82AA47D'),
BigInt('0x48D1F42BC81F2D04'),
BigInt('0xA61CECB46814FE75'),
BigInt('0xD9721C7C5821770C'),
BigInt('0x58C10D24087FEC87'),
BigInt('0x27AFFDEC384A65FE'),
BigInt('0x6F7E09C7F05548FA'),
BigInt('0x1010F90FC060C183'),
BigInt('0x91A3E857903E5A08'),
BigInt('0xEECD189FA00BD371'),
BigInt('0x78E0FF3B88BE6F81'),
BigInt('0x078E0FF3B88BE6F8'),
BigInt('0x863D1EABE8D57D73'),
BigInt('0xF953EE63D8E0F40A'),
BigInt('0xB1821A4810FFD90E'),
BigInt('0xCEECEA8020CA5077'),
BigInt('0x4F5FFBD87094CBFC'),
BigInt('0x30310B1040A14285'),
BigInt('0xDEFC138FE0AA91F4'),
BigInt('0xA192E347D09F188D'),
BigInt('0x2021F21F80C18306'),
BigInt('0x5F4F02D7B0F40A7F'),
BigInt('0x179EF6FC78EB277B'),
BigInt('0x68F0063448DEAE02'),
BigInt('0xE943176C18803589'),
BigInt('0x962DE7A428B5BCF0'),
BigInt('0xF1C1FE77117CDF02'),
BigInt('0x8EAF0EBF2149567B'),
BigInt('0x0F1C1FE77117CDF0'),
BigInt('0x7072EF2F41224489'),
BigInt('0x38A31B04893D698D'),
BigInt('0x47CDEBCCB908E0F4'),
BigInt('0xC67EFA94E9567B7F'),
BigInt('0xB9100A5CD963F206'),
BigInt('0x57DD12C379682177'),
BigInt('0x28B3E20B495DA80E'),
BigInt('0xA900F35319033385'),
BigInt('0xD66E039B2936BAFC'),
BigInt('0x9EBFF7B0E12997F8'),
BigInt('0xE1D10778D11C1E81'),
BigInt('0x606216208142850A'),
BigInt('0x1F0CE6E8B1770C73'),
BigInt('0x8921014C99C2B083'),
BigInt('0xF64FF184A9F739FA'),
BigInt('0x77FCE0DCF9A9A271'),
BigInt('0x08921014C99C2B08'),
BigInt('0x4043E43F0183060C'),
BigInt('0x3F2D14F731B68F75'),
BigInt('0xBE9E05AF61E814FE'),
BigInt('0xC1F0F56751DD9D87'),
BigInt('0x2F3DEDF8F1D64EF6'),
BigInt('0x50531D30C1E3C78F'),
BigInt('0xD1E00C6891BD5C04'),
BigInt('0xAE8EFCA0A188D57D'),
BigInt('0xE65F088B6997F879'),
BigInt('0x9931F84359A27100'),
BigInt('0x1882E91B09FCEA8B'),
BigInt('0x67EC19D339C963F2'),
BigInt('0xD75ADABD7A6E2D6F'),
BigInt('0xA8342A754A5BA416'),
BigInt('0x29873B2D1A053F9D'),
BigInt('0x56E9CBE52A30B6E4'),
BigInt('0x1E383FCEE22F9BE0'),
BigInt('0x6156CF06D21A1299'),
BigInt('0xE0E5DE5E82448912'),
BigInt('0x9F8B2E96B271006B'),
BigInt('0x71463609127AD31A'),
BigInt('0x0E28C6C1224F5A63'),
BigInt('0x8F9BD7997211C1E8'),
BigInt('0xF0F5275142244891'),
BigInt('0xB824D37A8A3B6595'),
BigInt('0xC74A23B2BA0EECEC'),
BigInt('0x46F932EAEA507767'),
BigInt('0x3997C222DA65FE1E'),
BigInt('0xAFBA2586F2D042EE'),
BigInt('0xD0D4D54EC2E5CB97'),
BigInt('0x5167C41692BB501C'),
BigInt('0x2E0934DEA28ED965'),
BigInt('0x66D8C0F56A91F461'),
BigInt('0x19B6303D5AA47D18'),
BigInt('0x980521650AFAE693'),
BigInt('0xE76BD1AD3ACF6FEA'),
BigInt('0x09A6C9329AC4BC9B'),
BigInt('0x76C839FAAAF135E2'),
BigInt('0xF77B28A2FAAFAE69'),
BigInt('0x8815D86ACA9A2710'),
BigInt('0xC0C42C4102850A14'),
BigInt('0xBFAADC8932B0836D'),
BigInt('0x3E19CDD162EE18E6'),
BigInt('0x41773D1952DB919F'),
BigInt('0x269B24CA6B12F26D'),
BigInt('0x59F5D4025B277B14'),
BigInt('0xD846C55A0B79E09F'),
BigInt('0xA72835923B4C69E6'),
BigInt('0xEFF9C1B9F35344E2'),
BigInt('0x90973171C366CD9B'),
BigInt('0x1124202993385610'),
BigInt('0x6E4AD0E1A30DDF69'),
BigInt('0x8087C87E03060C18'),
BigInt('0xFFE938B633338561'),
BigInt('0x7E5A29EE636D1EEA'),
BigInt('0x0134D92653589793'),
BigInt('0x49E52D0D9B47BA97'),
BigInt('0x368BDDC5AB7233EE'),
BigInt('0xB738CC9DFB2CA865'),
BigInt('0xC8563C55CB19211C'),
BigInt('0x5E7BDBF1E3AC9DEC'),
BigInt('0x21152B39D3991495'),
BigInt('0xA0A63A6183C78F1E'),
BigInt('0xDFC8CAA9B3F20667'),
BigInt('0x97193E827BED2B63'),
BigInt('0xE877CE4A4BD8A21A'),
BigInt('0x69C4DF121B863991'),
BigInt('0x16AA2FDA2BB3B0E8'),
BigInt('0xF86737458BB86399'),
BigInt('0x8709C78DBB8DEAE0'),
BigInt('0x06BAD6D5EBD3716B'),
BigInt('0x79D4261DDBE6F812'),
BigInt('0x3105D23613F9D516'),
BigInt('0x4E6B22FE23CC5C6F'),
BigInt('0xCFD833A67392C7E4'),
BigInt('0xB0B6C36E43A74E9D'),
BigInt('0x9A6C9329AC4BC9B5'),
BigInt('0xE50263E19C7E40CC'),
BigInt('0x64B172B9CC20DB47'),
BigInt('0x1BDF8271FC15523E'),
BigInt('0x530E765A340A7F3A'),
BigInt('0x2C608692043FF643'),
BigInt('0xADD397CA54616DC8'),
BigInt('0xD2BD67026454E4B1'),
BigInt('0x3C707F9DC45F37C0'),
BigInt('0x431E8F55F46ABEB9'),
BigInt('0xC2AD9E0DA4342532'),
BigInt('0xBDC36EC59401AC4B'),
BigInt('0xF5129AEE5C1E814F'),
BigInt('0x8A7C6A266C2B0836'),
BigInt('0x0BCF7B7E3C7593BD'),
BigInt('0x74A18BB60C401AC4'),
BigInt('0xE28C6C1224F5A634'),
BigInt('0x9DE29CDA14C02F4D'),
BigInt('0x1C518D82449EB4C6'),
BigInt('0x633F7D4A74AB3DBF'),
BigInt('0x2BEE8961BCB410BB'),
BigInt('0x548079A98C8199C2'),
BigInt('0xD53368F1DCDF0249'),
BigInt('0xAA5D9839ECEA8B30'),
BigInt('0x449080A64CE15841'),
BigInt('0x3BFE706E7CD4D138'),
BigInt('0xBA4D61362C8A4AB3'),
BigInt('0xC52391FE1CBFC3CA'),
BigInt('0x8DF265D5D4A0EECE'),
BigInt('0xF29C951DE49567B7'),
BigInt('0x732F8445B4CBFC3C'),
BigInt('0x0C41748D84FE7545'),
BigInt('0x6BAD6D5EBD3716B7'),
BigInt('0x14C39D968D029FCE'),
BigInt('0x95708CCEDD5C0445'),
BigInt('0xEA1E7C06ED698D3C'),
BigInt('0xA2CF882D2576A038'),
BigInt('0xDDA178E515432941'),
BigInt('0x5C1269BD451DB2CA'),
BigInt('0x237C997575283BB3'),
BigInt('0xCDB181EAD523E8C2'),
BigInt('0xB2DF7122E51661BB'),
BigInt('0x336C607AB548FA30'),
BigInt('0x4C0290B2857D7349'),
BigInt('0x04D364994D625E4D'),
BigInt('0x7BBD94517D57D734'),
BigInt('0xFA0E85092D094CBF'),
BigInt('0x856075C11D3CC5C6'),
BigInt('0x134D926535897936'),
BigInt('0x6C2362AD05BCF04F'),
BigInt('0xED9073F555E26BC4'),
BigInt('0x92FE833D65D7E2BD'),
BigInt('0xDA2F7716ADC8CFB9'),
BigInt('0xA54187DE9DFD46C0'),
BigInt('0x24F29686CDA3DD4B'),
BigInt('0x5B9C664EFD965432'),
BigInt('0xB5517ED15D9D8743'),
BigInt('0xCA3F8E196DA80E3A'),
BigInt('0x4B8C9F413DF695B1'),
BigInt('0x34E26F890DC31CC8'),
BigInt('0x7C339BA2C5DC31CC'),
BigInt('0x035D6B6AF5E9B8B5'),
BigInt('0x82EE7A32A5B7233E'),
BigInt('0xFD808AFA9582AA47'),
BigInt('0x4D364994D625E4DA'),
BigInt('0x3258B95CE6106DA3'),
BigInt('0xB3EBA804B64EF628'),
BigInt('0xCC8558CC867B7F51'),
BigInt('0x8454ACE74E645255'),
BigInt('0xFB3A5C2F7E51DB2C'),
BigInt('0x7A894D772E0F40A7'),
BigInt('0x05E7BDBF1E3AC9DE'),
BigInt('0xEB2AA520BE311AAF'),
BigInt('0x944455E88E0493D6'),
BigInt('0x15F744B0DE5A085D'),
BigInt('0x6A99B478EE6F8124'),
BigInt('0x224840532670AC20'),
BigInt('0x5D26B09B16452559'),
BigInt('0xDC95A1C3461BBED2'),
BigInt('0xA3FB510B762E37AB'),
BigInt('0x35D6B6AF5E9B8B5B'),
BigInt('0x4AB846676EAE0222'),
BigInt('0xCB0B573F3EF099A9'),
BigInt('0xB465A7F70EC510D0'),
BigInt('0xFCB453DCC6DA3DD4'),
BigInt('0x83DAA314F6EFB4AD'),
BigInt('0x0269B24CA6B12F26'),
BigInt('0x7D0742849684A65F'),
BigInt('0x93CA5A1B368F752E'),
BigInt('0xECA4AAD306BAFC57'),
BigInt('0x6D17BB8B56E467DC'),
BigInt('0x12794B4366D1EEA5'),
BigInt('0x5AA8BF68AECEC3A1'),
BigInt('0x25C64FA09EFB4AD8'),
BigInt('0xA4755EF8CEA5D153'),
BigInt('0xDB1BAE30FE90582A'),
BigInt('0xBCF7B7E3C7593BD8'),
BigInt('0xC399472BF76CB2A1'),
BigInt('0x422A5673A732292A'),
BigInt('0x3D44A6BB9707A053'),
BigInt('0x759552905F188D57'),
BigInt('0x0AFBA2586F2D042E'),
BigInt('0x8B48B3003F739FA5'),
BigInt('0xF42643C80F4616DC'),
BigInt('0x1AEB5B57AF4DC5AD'),
BigInt('0x6585AB9F9F784CD4'),
BigInt('0xE436BAC7CF26D75F'),
BigInt('0x9B584A0FFF135E26'),
BigInt('0xD389BE24370C7322'),
BigInt('0xACE74EEC0739FA5B'),
BigInt('0x2D545FB4576761D0'),
BigInt('0x523AAF7C6752E8A9'),
BigInt('0xC41748D84FE75459'),
BigInt('0xBB79B8107FD2DD20'),
BigInt('0x3ACAA9482F8C46AB'),
BigInt('0x45A459801FB9CFD2'),
BigInt('0x0D75ADABD7A6E2D6'),
BigInt('0x721B5D63E7936BAF'),
BigInt('0xF3A84C3BB7CDF024'),
BigInt('0x8CC6BCF387F8795D'),
BigInt('0x620BA46C27F3AA2C'),
BigInt('0x1D6554A417C62355'),
BigInt('0x9CD645FC4798B8DE'),
BigInt('0xE3B8B53477AD31A7'),
BigInt('0xAB69411FBFB21CA3'),
BigInt('0xD407B1D78F8795DA'),
BigInt('0x55B4A08FDFD90E51'),
BigInt('0x2ADA5047EFEC8728')
]
export type CRC64DigestEncoding = 'hex' | 'base64' | 'buffer'
class CRC64 {
private _crc: bigint
constructor() {
this._crc = BigInt(0)
}
update(data: Buffer | string): void {
const buffer = typeof data === 'string' ? Buffer.from(data) : data
let crc = CRC64.flip64Bits(this._crc)
for (const dataByte of buffer) {
const crcByte = Number(crc & BigInt(0xff))
crc = PREGEN_POLY_TABLE[crcByte ^ dataByte] ^ (crc >> BigInt(8))
}
this._crc = CRC64.flip64Bits(crc)
}
digest(encoding?: CRC64DigestEncoding): string | Buffer {
switch (encoding) {
case 'hex':
return this._crc.toString(16).toUpperCase()
case 'base64':
return this.toBuffer().toString('base64')
default:
return this.toBuffer()
}
}
private toBuffer(): Buffer {
return Buffer.from(
[0, 8, 16, 24, 32, 40, 48, 56].map(s =>
Number((this._crc >> BigInt(s)) & BigInt(0xff))
)
)
}
static flip64Bits(n: bigint): bigint {
return (BigInt(1) << BigInt(64)) - BigInt(1) - n
}
}
export default CRC64
@@ -18,7 +18,7 @@ import {URL} from 'url'
import {StatusReporter} from './status-reporter' import {StatusReporter} from './status-reporter'
import {performance} from 'perf_hooks' import {performance} from 'perf_hooks'
import {ListArtifactsResponse, QueryArtifactResponse} from './contracts' import {ListArtifactsResponse, QueryArtifactResponse} from './contracts'
import {HttpClientResponse} from '@actions/http-client' import {IHttpClientResponse} from '@actions/http-client/interfaces'
import {HttpManager} from './http-manager' import {HttpManager} from './http-manager'
import {DownloadItem} from './download-specification' import {DownloadItem} from './download-specification'
import {getDownloadFileConcurrency, getRetryLimit} from './config-variables' import {getDownloadFileConcurrency, getRetryLimit} from './config-variables'
@@ -152,7 +152,7 @@ export class DownloadHttpClient {
const headers = getDownloadHeaders('application/json', true, true) const headers = getDownloadHeaders('application/json', true, true)
// a single GET request is used to download a file // a single GET request is used to download a file
const makeDownloadRequest = async (): Promise<HttpClientResponse> => { const makeDownloadRequest = async (): Promise<IHttpClientResponse> => {
const client = this.downloadHttpManager.getClient(httpClientIndex) const client = this.downloadHttpManager.getClient(httpClientIndex)
return await client.get(artifactLocation, headers) return await client.get(artifactLocation, headers)
} }
@@ -219,20 +219,13 @@ export class DownloadHttpClient {
fileDownloadPath: string fileDownloadPath: string
): Promise<void> => { ): Promise<void> => {
destinationStream.close() destinationStream.close()
// await until file is created at downloadpath; node15 and up fs.createWriteStream had not created a file yet
await new Promise<void>(resolve => {
destinationStream.on('close', resolve)
if (destinationStream.writableFinished) {
resolve()
}
})
await rmFile(fileDownloadPath) await rmFile(fileDownloadPath)
destinationStream = fs.createWriteStream(fileDownloadPath) destinationStream = fs.createWriteStream(fileDownloadPath)
} }
// keep trying to download a file until a retry limit has been reached // keep trying to download a file until a retry limit has been reached
while (retryCount <= retryLimit) { while (retryCount <= retryLimit) {
let response: HttpClientResponse let response: IHttpClientResponse
try { try {
response = await makeDownloadRequest() response = await makeDownloadRequest()
} catch (error) { } catch (error) {
@@ -302,7 +295,7 @@ export class DownloadHttpClient {
* @param isGzip a boolean denoting if the content is compressed using gzip and if we need to decode it * @param isGzip a boolean denoting if the content is compressed using gzip and if we need to decode it
*/ */
async pipeResponseToFile( async pipeResponseToFile(
response: HttpClientResponse, response: IHttpClientResponse,
destinationStream: fs.WriteStream, destinationStream: fs.WriteStream,
isGzip: boolean isGzip: boolean
): Promise<void> { ): Promise<void> {
@@ -311,7 +304,7 @@ export class DownloadHttpClient {
const gunzip = zlib.createGunzip() const gunzip = zlib.createGunzip()
response.message response.message
.on('error', error => { .on('error', error => {
core.info( core.error(
`An error occurred while attempting to read the response stream` `An error occurred while attempting to read the response stream`
) )
gunzip.close() gunzip.close()
@@ -320,7 +313,7 @@ export class DownloadHttpClient {
}) })
.pipe(gunzip) .pipe(gunzip)
.on('error', error => { .on('error', error => {
core.info( core.error(
`An error occurred while attempting to decompress the response stream` `An error occurred while attempting to decompress the response stream`
) )
destinationStream.close() destinationStream.close()
@@ -331,7 +324,7 @@ export class DownloadHttpClient {
resolve() resolve()
}) })
.on('error', error => { .on('error', error => {
core.info( core.error(
`An error occurred while writing a downloaded file to ${destinationStream.path}` `An error occurred while writing a downloaded file to ${destinationStream.path}`
) )
reject(error) reject(error)
@@ -339,7 +332,7 @@ export class DownloadHttpClient {
} else { } else {
response.message response.message
.on('error', error => { .on('error', error => {
core.info( core.error(
`An error occurred while attempting to read the response stream` `An error occurred while attempting to read the response stream`
) )
destinationStream.close() destinationStream.close()
@@ -350,7 +343,7 @@ export class DownloadHttpClient {
resolve() resolve()
}) })
.on('error', error => { .on('error', error => {
core.info( core.error(
`An error occurred while writing a downloaded file to ${destinationStream.path}` `An error occurred while writing a downloaded file to ${destinationStream.path}`
) )
reject(error) reject(error)
@@ -1,4 +1,4 @@
import {HttpClient} from '@actions/http-client' import {HttpClient} from '@actions/http-client/index'
import {createHttpClient} from './utils' import {createHttpClient} from './utils'
/** /**
@@ -1,4 +1,4 @@
import {HttpClientResponse} from '@actions/http-client' import {IHttpClientResponse} from '@actions/http-client/interfaces'
import { import {
isRetryableStatusCode, isRetryableStatusCode,
isSuccessStatusCode, isSuccessStatusCode,
@@ -11,11 +11,11 @@ import {getRetryLimit} from './config-variables'
export async function retry( export async function retry(
name: string, name: string,
operation: () => Promise<HttpClientResponse>, operation: () => Promise<IHttpClientResponse>,
customErrorMessages: Map<number, string>, customErrorMessages: Map<number, string>,
maxAttempts: number maxAttempts: number
): Promise<HttpClientResponse> { ): Promise<IHttpClientResponse> {
let response: HttpClientResponse | undefined = undefined let response: IHttpClientResponse | undefined = undefined
let statusCode: number | undefined = undefined let statusCode: number | undefined = undefined
let isRetryable = false let isRetryable = false
let errorMessage = '' let errorMessage = ''
@@ -71,9 +71,9 @@ export async function retry(
export async function retryHttpClientRequest( export async function retryHttpClientRequest(
name: string, name: string,
method: () => Promise<HttpClientResponse>, method: () => Promise<IHttpClientResponse>,
customErrorMessages: Map<number, string> = new Map(), customErrorMessages: Map<number, string> = new Map(),
maxAttempts = getRetryLimit() maxAttempts = getRetryLimit()
): Promise<HttpClientResponse> { ): Promise<IHttpClientResponse> {
return await retry(name, method, customErrorMessages, maxAttempts) return await retry(name, method, customErrorMessages, maxAttempts)
} }
+7 -22
View File
@@ -9,27 +9,12 @@ const stat = promisify(fs.stat)
* 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 * 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 = [ const gzipExemptFileExtensions = [
'.gz', // GZIP '.gzip',
'.gzip', // GZIP '.zip',
'.tgz', // GZIP '.tar.lz',
'.taz', // GZIP '.tar.gz',
'.Z', // COMPRESS '.tar.bz2',
'.taZ', // COMPRESS '.7z'
'.bz2', // BZIP2
'.tbz', // BZIP2
'.tbz2', // BZIP2
'.tz2', // BZIP2
'.lz', // LZIP
'.lzma', // LZMA
'.tlz', // LZMA
'.lzo', // LZOP
'.xz', // XZ
'.txz', // XZ
'.zst', // ZSTD
'.zstd', // ZSTD
'.tzst', // ZSTD
'.zip', // ZIP
'.7z' // 7ZIP
] ]
/** /**
@@ -62,7 +47,7 @@ export async function createGZipFileOnDisk(
outputStream.on('error', error => { outputStream.on('error', error => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error) console.log(error)
reject(error) reject
}) })
}) })
} }
@@ -9,7 +9,6 @@ import {
UploadResults UploadResults
} from './contracts' } from './contracts'
import { import {
digestForStream,
getArtifactUrl, getArtifactUrl,
getContentRange, getContentRange,
getUploadHeaders, getUploadHeaders,
@@ -32,7 +31,8 @@ import {promisify} from 'util'
import {URL} from 'url' import {URL} from 'url'
import {performance} from 'perf_hooks' import {performance} from 'perf_hooks'
import {StatusReporter} from './status-reporter' import {StatusReporter} from './status-reporter'
import {HttpCodes, HttpClientResponse} from '@actions/http-client' import {HttpCodes} from '@actions/http-client'
import {IHttpClientResponse} from '@actions/http-client/interfaces'
import {HttpManager} from './http-manager' import {HttpManager} from './http-manager'
import {UploadSpecification} from './upload-specification' import {UploadSpecification} from './upload-specification'
import {UploadOptions} from './upload-options' import {UploadOptions} from './upload-options'
@@ -406,9 +406,6 @@ export class UploadHttpClient {
isGzip: boolean, isGzip: boolean,
totalFileSize: number totalFileSize: number
): Promise<boolean> { ): Promise<boolean> {
// open a new stream and read it to compute the digest
const digest = await digestForStream(openStream())
// prepare all the necessary headers before making any http call // prepare all the necessary headers before making any http call
const headers = getUploadHeaders( const headers = getUploadHeaders(
'application/octet-stream', 'application/octet-stream',
@@ -416,11 +413,10 @@ export class UploadHttpClient {
isGzip, isGzip,
totalFileSize, totalFileSize,
end - start + 1, end - start + 1,
getContentRange(start, end, uploadFileSize), getContentRange(start, end, uploadFileSize)
digest
) )
const uploadChunkRequest = async (): Promise<HttpClientResponse> => { const uploadChunkRequest = async (): Promise<IHttpClientResponse> => {
const client = this.uploadHttpManager.getClient(httpClientIndex) const client = this.uploadHttpManager.getClient(httpClientIndex)
return await client.sendStream('PUT', resourceUrl, openStream(), headers) return await client.sendStream('PUT', resourceUrl, openStream(), headers)
} }
@@ -431,7 +427,7 @@ export class UploadHttpClient {
// Increments the current retry count and then checks if the retry limit has been reached // Increments the current retry count and then checks if the retry limit has been reached
// If there have been too many retries, fail so the download stops // If there have been too many retries, fail so the download stops
const incrementAndCheckRetryLimit = ( const incrementAndCheckRetryLimit = (
response?: HttpClientResponse response?: IHttpClientResponse
): boolean => { ): boolean => {
retryCount++ retryCount++
if (retryCount > retryLimit) { if (retryCount > retryLimit) {
@@ -468,7 +464,7 @@ export class UploadHttpClient {
// allow for failed chunks to be retried multiple times // allow for failed chunks to be retried multiple times
while (retryCount <= retryLimit) { while (retryCount <= retryLimit) {
let response: HttpClientResponse let response: IHttpClientResponse
try { try {
response = await uploadChunkRequest() response = await uploadChunkRequest()
@@ -25,7 +25,7 @@ export function getUploadSpecification(
if (!fs.existsSync(rootDirectory)) { if (!fs.existsSync(rootDirectory)) {
throw new Error(`Provided rootDirectory ${rootDirectory} does not exist`) throw new Error(`Provided rootDirectory ${rootDirectory} does not exist`)
} }
if (!fs.statSync(rootDirectory).isDirectory()) { if (!fs.lstatSync(rootDirectory).isDirectory()) {
throw new Error( throw new Error(
`Provided rootDirectory ${rootDirectory} is not a valid directory` `Provided rootDirectory ${rootDirectory} is not a valid directory`
) )
@@ -57,7 +57,7 @@ export function getUploadSpecification(
if (!fs.existsSync(file)) { if (!fs.existsSync(file)) {
throw new Error(`File ${file} does not exist`) throw new Error(`File ${file} does not exist`)
} }
if (!fs.statSync(file).isDirectory()) { if (!fs.lstatSync(file).isDirectory()) {
// Normalize and resolve, this allows for either absolute or relative paths to be used // Normalize and resolve, this allows for either absolute or relative paths to be used
file = normalize(file) file = normalize(file)
file = resolve(file) file = resolve(file)
+11 -43
View File
@@ -1,9 +1,9 @@
import crypto from 'crypto'
import {promises as fs} from 'fs'
import {IncomingHttpHeaders, OutgoingHttpHeaders} from 'http'
import {debug, info, warning} from '@actions/core' import {debug, info, warning} from '@actions/core'
import {HttpCodes, HttpClient, HttpClientResponse} from '@actions/http-client' import {promises as fs} from 'fs'
import {BearerCredentialHandler} from '@actions/http-client/lib/auth' import {HttpCodes, HttpClient} from '@actions/http-client'
import {BearerCredentialHandler} from '@actions/http-client/auth'
import {IHeaders, IHttpClientResponse} from '@actions/http-client/interfaces'
import {IncomingHttpHeaders} from 'http'
import { import {
getRuntimeToken, getRuntimeToken,
getRuntimeUrl, getRuntimeUrl,
@@ -11,7 +11,6 @@ import {
getRetryMultiplier, getRetryMultiplier,
getInitialRetryIntervalInMilliseconds getInitialRetryIntervalInMilliseconds
} from './config-variables' } from './config-variables'
import CRC64 from './crc64'
/** /**
* Returns a retry time in milliseconds that exponentially gets larger * Returns a retry time in milliseconds that exponentially gets larger
@@ -140,8 +139,8 @@ export function getDownloadHeaders(
contentType: string, contentType: string,
isKeepAlive?: boolean, isKeepAlive?: boolean,
acceptGzip?: boolean acceptGzip?: boolean
): OutgoingHttpHeaders { ): IHeaders {
const requestOptions: OutgoingHttpHeaders = {} const requestOptions: IHeaders = {}
if (contentType) { if (contentType) {
requestOptions['Content-Type'] = contentType requestOptions['Content-Type'] = contentType
@@ -181,10 +180,9 @@ export function getUploadHeaders(
isGzip?: boolean, isGzip?: boolean,
uncompressedLength?: number, uncompressedLength?: number,
contentLength?: number, contentLength?: number,
contentRange?: string, contentRange?: string
digest?: StreamDigest ): IHeaders {
): OutgoingHttpHeaders { const requestOptions: IHeaders = {}
const requestOptions: OutgoingHttpHeaders = {}
requestOptions['Accept'] = `application/json;api-version=${getApiVersion()}` requestOptions['Accept'] = `application/json;api-version=${getApiVersion()}`
if (contentType) { if (contentType) {
requestOptions['Content-Type'] = contentType requestOptions['Content-Type'] = contentType
@@ -204,10 +202,6 @@ export function getUploadHeaders(
if (contentRange) { if (contentRange) {
requestOptions['Content-Range'] = contentRange requestOptions['Content-Range'] = contentRange
} }
if (digest) {
requestOptions['x-actions-results-crc64'] = digest.crc64
requestOptions['x-actions-results-md5'] = digest.md5
}
return requestOptions return requestOptions
} }
@@ -233,7 +227,7 @@ export function getArtifactUrl(): string {
* Certain information such as the TLSSocket and the Readable state are not really useful for diagnostic purposes so they can be avoided. * Certain information such as the TLSSocket and the Readable state are not really useful for diagnostic purposes so they can be avoided.
* Other information such as the headers, the response code and message might be useful, so this is displayed. * Other information such as the headers, the response code and message might be useful, so this is displayed.
*/ */
export function displayHttpDiagnostics(response: HttpClientResponse): void { export function displayHttpDiagnostics(response: IHttpClientResponse): void {
info( info(
`##### Begin Diagnostic HTTP information ##### `##### Begin Diagnostic HTTP information #####
Status Code: ${response.message.statusCode} Status Code: ${response.message.statusCode}
@@ -297,29 +291,3 @@ export function getProperRetention(
export async function sleep(milliseconds: number): Promise<void> { export async function sleep(milliseconds: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, milliseconds)) return new Promise(resolve => setTimeout(resolve, milliseconds))
} }
export interface StreamDigest {
crc64: string
md5: string
}
export async function digestForStream(
stream: NodeJS.ReadableStream
): Promise<StreamDigest> {
return new Promise((resolve, reject) => {
const crc64 = new CRC64()
const md5 = crypto.createHash('md5')
stream
.on('data', data => {
crc64.update(data)
md5.update(data)
})
.on('end', () =>
resolve({
crc64: crc64.digest('base64') as string,
md5: md5.digest('base64')
})
)
.on('error', reject)
})
}
+4 -11
View File
@@ -2,17 +2,17 @@
> Functions necessary for caching dependencies and build outputs to improve workflow execution time. > Functions necessary for caching dependencies and build outputs to improve workflow execution time.
See ["Caching dependencies to speed up workflows"](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows) for how caching works. 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 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.
## Usage ## 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). 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 #### 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. 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 ```js
const cache = require('@actions/cache'); const cache = require('@actions/cache');
@@ -26,7 +26,7 @@ const cacheId = await cache.saveCache(paths, key)
#### Restore Cache #### Restore Cache
Restores a cache based on `key` and `restoreKeys` to the `paths` provided. Function returns the cache key for cache hit and returns undefined if cache not found. Restores a cache based on `key` and `restoreKeys` to the `paths` provided. Function returns the cache key for cache hit and returns undefined if cache not found.
```js ```js
const cache = require('@actions/cache'); const cache = require('@actions/cache');
@@ -42,10 +42,3 @@ const restoreKeys = [
const cacheKey = await cache.restoreCache(paths, key, restoreKeys) const cacheKey = await cache.restoreCache(paths, key, restoreKeys)
``` ```
##### Cache segment restore timeout
A cache gets downloaded in multiple segments of fixed sizes (now `128MB` to fail-fast, previously `1GB` for a `32-bit` runner and `2GB` for a `64-bit` runner were used). Sometimes, a segment download gets stuck which causes the workflow job to be stuck forever and fail. Version `v3.0.4` of cache package introduces a segment download timeout. The segment download timeout will allow the segment download to get aborted and hence allow the job to proceed with a cache miss.
Default value of this timeout is 10 minutes (starting `v3.2.1` and higher, previously 60 minutes in versions between `v.3.0.4` and `v3.2.0`, both included) and can be customized by specifying an [environment variable](https://docs.github.com/en/actions/learn-github-actions/environment-variables) named `SEGMENT_DOWNLOAD_TIMEOUT_MINS` with timeout value in minutes.
+2 -106
View File
@@ -5,158 +5,54 @@
- Initial release - Initial release
### 0.2.0 ### 0.2.0
- Fixes issues with the zstd compression algorithm on Windows and Ubuntu 16.04 [#469](https://github.com/actions/toolkit/pull/469) - Fixes issues with the zstd compression algorithm on Windows and Ubuntu 16.04 [#469](https://github.com/actions/toolkit/pull/469)
### 0.2.1 ### 0.2.1
- Fix to await async function getCompressionMethod - Fix to await async function getCompressionMethod
### 1.0.0 ### 1.0.0
- Downloads Azure-hosted caches using the Azure SDK for speed and reliability - Downloads Azure-hosted caches using the Azure SDK for speed and reliability
- Displays download progress - Displays download progress
- Includes changes that break compatibility with earlier versions, including: - Includes changes that break compatibility with earlier versions, including:
- `retry`, `retryTypedResponse`, and `retryHttpClientResponse` moved from `cacheHttpClient` to `requestUtils` - `retry`, `retryTypedResponse`, and `retryHttpClientResponse` moved from `cacheHttpClient` to `requestUtils`
### 1.0.1 ### 1.0.1
- Fix bug in downloading large files (> 2 GBs) with the Azure SDK - Fix bug in downloading large files (> 2 GBs) with the Azure SDK
### 1.0.2 ### 1.0.2
- Use posix archive format to add support for some tools - Use posix archive format to add support for some tools
### 1.0.3 ### 1.0.3
- Use http-client v1.0.9 - Use http-client v1.0.9
- Fixes error handling so retries are not attempted on non-retryable errors (409 Conflict, for example) - Fixes error handling so retries are not attempted on non-retryable errors (409 Conflict, for example)
- Adds 5 second delay between retry attempts - Adds 5 second delay between retry attempts
### 1.0.4 ### 1.0.4
- Use @actions/core v1.2.6 - Use @actions/core v1.2.6
- Fixes uploadChunk to throw an error if any unsuccessful response code is received - Fixes uploadChunk to throw an error if any unsuccessful response code is received
### 1.0.5 ### 1.0.5
- Fix to ensure Windows cache paths get resolved correctly - Fix to ensure Windows cache paths get resolved correctly
### 1.0.6 ### 1.0.6
- Make caching more verbose [#650](https://github.com/actions/toolkit/pull/650) - Make caching more verbose [#650](https://github.com/actions/toolkit/pull/650)
- Use GNU tar on macOS if available [#701](https://github.com/actions/toolkit/pull/701) - Use GNU tar on macOS if available [#701](https://github.com/actions/toolkit/pull/701)
### 1.0.7 ### 1.0.7
- Fixes permissions issue extracting archives with GNU tar on macOS ([issue](https://github.com/actions/cache/issues/527)) - Fixes permissions issue extracting archives with GNU tar on macOS ([issue](https://github.com/actions/cache/issues/527))
### 1.0.8 ### 1.0.8
- Increase the allowed artifact cache size from 5GB to 10GB ([issue](https://github.com/actions/cache/discussions/497)) - Increase the allowed artifact cache size from 5GB to 10GB ([issue](https://github.com/actions/cache/discussions/497))
### 1.0.9 ### 1.0.9
- Use @azure/ms-rest-js v2.6.0
- Use @azure/ms-rest-js v2.6.0 - Use @azure/storage-blob v12.8.0
- Use @azure/storage-blob v12.8.0
### 1.0.10 ### 1.0.10
- Update `lockfileVersion` to `v2` in `package-lock.json [#1022](https://github.com/actions/toolkit/pull/1022) - Update `lockfileVersion` to `v2` in `package-lock.json [#1022](https://github.com/actions/toolkit/pull/1022)
### 1.0.11 ### 1.0.11
- Fix file downloads > 2GB([issue](https://github.com/actions/cache/issues/773)) - Fix file downloads > 2GB([issue](https://github.com/actions/cache/issues/773))
### 2.0.0 ### 2.0.0
- Added support to check if Actions cache service feature is available or not [#1028](https://github.com/actions/toolkit/pull/1028) - Added support to check if Actions cache service feature is available or not [#1028](https://github.com/actions/toolkit/pull/1028)
### 2.0.3
- Update to v2.0.0 of `@actions/http-client`
### 2.0.4
- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
### 2.0.5
- Fix to avoid saving empty cache when no files are available for caching. ([issue](https://github.com/actions/cache/issues/624))
### 2.0.6
- Fix `Tar failed with error: The process '/usr/bin/tar' failed with exit code 1` issue when temp directory where tar is getting created is actually the subdirectory of the path mentioned by the user for caching. ([issue](https://github.com/actions/cache/issues/689))
### 3.0.0
- Updated actions/cache to suppress Actions cache server error and log warning for those error [#1122](https://github.com/actions/toolkit/pull/1122)
### 3.0.1
- Fix [#833](https://github.com/actions/cache/issues/833) - cache doesn't work with github workspace directory.
- Fix [#809](https://github.com/actions/cache/issues/809) `zstd -d: no such file or directory` error on AWS self-hosted runners.
### 3.0.2
- Added 1 hour timeout for the download stuck issue [#810](https://github.com/actions/cache/issues/810).
### 3.0.3
- Bug fixes for download stuck issue [#810](https://github.com/actions/cache/issues/810).
### 3.0.4
- Fix zstd not working for windows on gnu tar in issues [#888](https://github.com/actions/cache/issues/888) and [#891](https://github.com/actions/cache/issues/891).
- Allowing users to provide a custom timeout as input for aborting download of a cache segment using an environment variable `SEGMENT_DOWNLOAD_TIMEOUT_MINS`. Default is 60 minutes.
### 3.0.5
- Update `@actions/cache` to use `@actions/core@^1.10.0`
### 3.0.6
- Added `@azure/abort-controller` to dependencies to fix compatibility issue with ESM [#1208](https://github.com/actions/toolkit/issues/1208)
### 3.1.0-beta.1
- Update actions/cache on windows to use gnu tar and zstd by default and fallback to bsdtar and zstd if gnu tar is not available. ([issue](https://github.com/actions/cache/issues/984))
### 3.1.0-beta.2
- Added support for fallback to gzip to restore old caches on windows.
### 3.1.0-beta.3
- Bug Fixes for fallback to gzip to restore old caches on windows and bsdtar if gnutar is not available.
### 3.1.0
- Update actions/cache on windows to use gnu tar and zstd by default
- Update actions/cache on windows to fallback to bsdtar and zstd if gnu tar is not available.
- Added support for fallback to gzip to restore old caches on windows.
### 3.1.1
- Reverted changes in 3.1.0 to fix issue with symlink restoration on windows.
- Added support for verbose logging about cache version during cache miss.
### 3.1.2
- Fix issue with symlink restoration on windows.
### 3.1.3
- Fix to prevent from setting MYSYS environement variable globally [#1329](https://github.com/actions/toolkit/pull/1329).
### 3.1.4
- Fix zstd not being used due to `zstd --version` output change in zstd 1.5.4 release. See [#1353](https://github.com/actions/toolkit/pull/1353).
### 3.2.0
- Add `lookupOnly` to cache restore `DownloadOptions`.
### 3.2.1
- Updated @azure/storage-blob to `v12.13.0`
+6 -17
View File
@@ -7,7 +7,7 @@ jest.mock('../src/internal/downloadUtils')
test('getCacheVersion with one path returns version', async () => { test('getCacheVersion with one path returns version', async () => {
const paths = ['node_modules'] const paths = ['node_modules']
const result = getCacheVersion(paths, undefined, true) const result = getCacheVersion(paths)
expect(result).toEqual( expect(result).toEqual(
'b3e0c6cb5ecf32614eeb2997d905b9c297046d7cbf69062698f25b14b4cb0985' 'b3e0c6cb5ecf32614eeb2997d905b9c297046d7cbf69062698f25b14b4cb0985'
) )
@@ -15,7 +15,7 @@ test('getCacheVersion with one path returns version', async () => {
test('getCacheVersion with multiple paths returns version', async () => { test('getCacheVersion with multiple paths returns version', async () => {
const paths = ['node_modules', 'dist'] const paths = ['node_modules', 'dist']
const result = getCacheVersion(paths, undefined, true) const result = getCacheVersion(paths)
expect(result).toEqual( expect(result).toEqual(
'165c3053bc646bf0d4fac17b1f5731caca6fe38e0e464715c0c3c6b6318bf436' '165c3053bc646bf0d4fac17b1f5731caca6fe38e0e464715c0c3c6b6318bf436'
) )
@@ -23,33 +23,22 @@ test('getCacheVersion with multiple paths returns version', async () => {
test('getCacheVersion with zstd compression returns version', async () => { test('getCacheVersion with zstd compression returns version', async () => {
const paths = ['node_modules'] const paths = ['node_modules']
const result = getCacheVersion(paths, CompressionMethod.Zstd, true) const result = getCacheVersion(paths, CompressionMethod.Zstd)
expect(result).toEqual( expect(result).toEqual(
'273877e14fd65d270b87a198edbfa2db5a43de567c9a548d2a2505b408befe24' '273877e14fd65d270b87a198edbfa2db5a43de567c9a548d2a2505b408befe24'
) )
}) })
test('getCacheVersion with gzip compression returns version', async () => { test('getCacheVersion with gzip compression does not change vesion', async () => {
const paths = ['node_modules'] const paths = ['node_modules']
const result = getCacheVersion(paths, CompressionMethod.Gzip, true) const result = getCacheVersion(paths, CompressionMethod.Gzip)
expect(result).toEqual( expect(result).toEqual(
'470e252814dbffc9524891b17cf4e5749b26c1b5026e63dd3f00972db2393117' 'b3e0c6cb5ecf32614eeb2997d905b9c297046d7cbf69062698f25b14b4cb0985'
) )
}) })
test('getCacheVersion with enableCrossOsArchive as false returns version on windows', async () => {
if (process.platform === 'win32') {
const paths = ['node_modules']
const result = getCacheVersion(paths)
expect(result).toEqual(
'2db19d6596dc34f51f0043120148827a264863f5c6ac857569c2af7119bad14e'
)
}
})
test('downloadCache uses http-client for non-Azure URLs', async () => { test('downloadCache uses http-client for non-Azure URLs', async () => {
const downloadCacheHttpClientMock = jest.spyOn( const downloadCacheHttpClientMock = jest.spyOn(
downloadUtils, downloadUtils,
-6
View File
@@ -32,9 +32,3 @@ test('assertDefined throws if undefined', () => {
test('assertDefined returns value', () => { test('assertDefined returns value', () => {
expect(cacheUtils.assertDefined('test', 5)).toBe(5) expect(cacheUtils.assertDefined('test', 5)).toBe(5)
}) })
test('resolvePaths works on github workspace directory', async () => {
const workspace = process.env['GITHUB_WORKSPACE'] ?? '.'
const paths = await cacheUtils.resolvePaths([workspace])
expect(paths.length).toBeGreaterThan(0)
})
+2 -28
View File
@@ -8,8 +8,6 @@ import {
const useAzureSdk = true const useAzureSdk = true
const downloadConcurrency = 8 const downloadConcurrency = 8
const timeoutInMs = 30000 const timeoutInMs = 30000
const segmentTimeoutInMs = 600000
const lookupOnly = false
const uploadConcurrency = 4 const uploadConcurrency = 4
const uploadChunkSize = 32 * 1024 * 1024 const uploadChunkSize = 32 * 1024 * 1024
@@ -19,9 +17,7 @@ test('getDownloadOptions sets defaults', async () => {
expect(actualOptions).toEqual({ expect(actualOptions).toEqual({
useAzureSdk, useAzureSdk,
downloadConcurrency, downloadConcurrency,
timeoutInMs, timeoutInMs
segmentTimeoutInMs,
lookupOnly
}) })
}) })
@@ -29,9 +25,7 @@ test('getDownloadOptions overrides all settings', async () => {
const expectedOptions: DownloadOptions = { const expectedOptions: DownloadOptions = {
useAzureSdk: false, useAzureSdk: false,
downloadConcurrency: 14, downloadConcurrency: 14,
timeoutInMs: 20000, timeoutInMs: 20000
segmentTimeoutInMs: 3600000,
lookupOnly: true
} }
const actualOptions = getDownloadOptions(expectedOptions) const actualOptions = getDownloadOptions(expectedOptions)
@@ -58,23 +52,3 @@ test('getUploadOptions overrides all settings', async () => {
expect(actualOptions).toEqual(expectedOptions) expect(actualOptions).toEqual(expectedOptions)
}) })
test('getDownloadOptions overrides download timeout minutes', async () => {
const expectedOptions: DownloadOptions = {
useAzureSdk: false,
downloadConcurrency: 14,
timeoutInMs: 20000,
segmentTimeoutInMs: 3600000,
lookupOnly: true
}
process.env.SEGMENT_DOWNLOAD_TIMEOUT_MINS = '10'
const actualOptions = getDownloadOptions(expectedOptions)
expect(actualOptions.useAzureSdk).toEqual(expectedOptions.useAzureSdk)
expect(actualOptions.downloadConcurrency).toEqual(
expectedOptions.downloadConcurrency
)
expect(actualOptions.timeoutInMs).toEqual(expectedOptions.timeoutInMs)
expect(actualOptions.segmentTimeoutInMs).toEqual(600000)
expect(actualOptions.lookupOnly).toEqual(expectedOptions.lookupOnly)
})
+5 -48
View File
@@ -73,17 +73,13 @@ test('restore with no cache found', async () => {
test('restore with server error should fail', async () => { test('restore with server error should fail', async () => {
const paths = ['node_modules'] const paths = ['node_modules']
const key = 'node-test' const key = 'node-test'
const logWarningMock = jest.spyOn(core, 'warning')
jest.spyOn(cacheHttpClient, 'getCacheEntry').mockImplementation(() => { jest.spyOn(cacheHttpClient, 'getCacheEntry').mockImplementation(() => {
throw new Error('HTTP Error Occurred') throw new Error('HTTP Error Occurred')
}) })
const cacheKey = await restoreCache(paths, key) await expect(restoreCache(paths, key)).rejects.toThrowError(
expect(cacheKey).toBe(undefined) 'HTTP Error Occurred'
expect(logWarningMock).toHaveBeenCalledTimes(1)
expect(logWarningMock).toHaveBeenCalledWith(
'Failed to restore: HTTP Error Occurred'
) )
}) })
@@ -142,8 +138,7 @@ test('restore with gzip compressed cache found', async () => {
expect(cacheKey).toBe(key) expect(cacheKey).toBe(key)
expect(getCacheMock).toHaveBeenCalledWith([key], paths, { expect(getCacheMock).toHaveBeenCalledWith([key], paths, {
compressionMethod: compression, compressionMethod: compression
enableCrossOsArchive: false
}) })
expect(createTempDirectoryMock).toHaveBeenCalledTimes(1) expect(createTempDirectoryMock).toHaveBeenCalledTimes(1)
expect(downloadCacheMock).toHaveBeenCalledWith( expect(downloadCacheMock).toHaveBeenCalledWith(
@@ -202,8 +197,7 @@ test('restore with zstd compressed cache found', async () => {
expect(cacheKey).toBe(key) expect(cacheKey).toBe(key)
expect(getCacheMock).toHaveBeenCalledWith([key], paths, { expect(getCacheMock).toHaveBeenCalledWith([key], paths, {
compressionMethod: compression, compressionMethod: compression
enableCrossOsArchive: false
}) })
expect(createTempDirectoryMock).toHaveBeenCalledTimes(1) expect(createTempDirectoryMock).toHaveBeenCalledTimes(1)
expect(downloadCacheMock).toHaveBeenCalledWith( expect(downloadCacheMock).toHaveBeenCalledWith(
@@ -260,8 +254,7 @@ test('restore with cache found for restore key', async () => {
expect(cacheKey).toBe(restoreKey) expect(cacheKey).toBe(restoreKey)
expect(getCacheMock).toHaveBeenCalledWith([key, restoreKey], paths, { expect(getCacheMock).toHaveBeenCalledWith([key, restoreKey], paths, {
compressionMethod: compression, compressionMethod: compression
enableCrossOsArchive: false
}) })
expect(createTempDirectoryMock).toHaveBeenCalledTimes(1) expect(createTempDirectoryMock).toHaveBeenCalledTimes(1)
expect(downloadCacheMock).toHaveBeenCalledWith( expect(downloadCacheMock).toHaveBeenCalledWith(
@@ -276,39 +269,3 @@ test('restore with cache found for restore key', async () => {
expect(extractTarMock).toHaveBeenCalledWith(archivePath, compression) expect(extractTarMock).toHaveBeenCalledWith(archivePath, compression)
expect(getCompressionMock).toHaveBeenCalledTimes(1) expect(getCompressionMock).toHaveBeenCalledTimes(1)
}) })
test('restore with dry run', async () => {
const paths = ['node_modules']
const key = 'node-test'
const options = {lookupOnly: true}
const cacheEntry: ArtifactCacheEntry = {
cacheKey: key,
scope: 'refs/heads/main',
archiveLocation: 'www.actionscache.test/download'
}
const getCacheMock = jest.spyOn(cacheHttpClient, 'getCacheEntry')
getCacheMock.mockImplementation(async () => {
return Promise.resolve(cacheEntry)
})
const createTempDirectoryMock = jest.spyOn(cacheUtils, 'createTempDirectory')
const downloadCacheMock = jest.spyOn(cacheHttpClient, 'downloadCache')
const compression = CompressionMethod.Gzip
const getCompressionMock = jest
.spyOn(cacheUtils, 'getCompressionMethod')
.mockReturnValue(Promise.resolve(compression))
const cacheKey = await restoreCache(paths, key, undefined, options)
expect(cacheKey).toBe(key)
expect(getCompressionMock).toHaveBeenCalledTimes(1)
expect(getCacheMock).toHaveBeenCalledWith([key], paths, {
compressionMethod: compression,
enableCrossOsArchive: false
})
// creating a tempDir and downloading the cache are skipped
expect(createTempDirectoryMock).toHaveBeenCalledTimes(0)
expect(downloadCacheMock).toHaveBeenCalledTimes(0)
})
+18 -55
View File
@@ -5,7 +5,7 @@ import * as cacheHttpClient from '../src/internal/cacheHttpClient'
import * as cacheUtils from '../src/internal/cacheUtils' import * as cacheUtils from '../src/internal/cacheUtils'
import {CacheFilename, CompressionMethod} from '../src/internal/constants' import {CacheFilename, CompressionMethod} from '../src/internal/constants'
import * as tar from '../src/internal/tar' import * as tar from '../src/internal/tar'
import {TypedResponse} from '@actions/http-client/lib/interfaces' import {ITypedResponse} from '@actions/http-client/interfaces'
import { import {
ReserveCacheResponse, ReserveCacheResponse,
ITypedResponseWithError ITypedResponseWithError
@@ -48,7 +48,6 @@ test('save with large cache outputs should fail', async () => {
const cachePaths = [path.resolve(filePath)] const cachePaths = [path.resolve(filePath)]
const createTarMock = jest.spyOn(tar, 'createTar') const createTarMock = jest.spyOn(tar, 'createTar')
const logWarningMock = jest.spyOn(core, 'warning')
const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit
jest jest
@@ -59,11 +58,8 @@ test('save with large cache outputs should fail', async () => {
.spyOn(cacheUtils, 'getCompressionMethod') .spyOn(cacheUtils, 'getCompressionMethod')
.mockReturnValueOnce(Promise.resolve(compression)) .mockReturnValueOnce(Promise.resolve(compression))
const cacheId = await saveCache([filePath], primaryKey) await expect(saveCache([filePath], primaryKey)).rejects.toThrowError(
expect(cacheId).toBe(-1) 'Cache size of ~11264 MB (11811160064 B) is over the 10GB limit, not saving cache.'
expect(logWarningMock).toHaveBeenCalledTimes(1)
expect(logWarningMock).toHaveBeenCalledWith(
'Failed to save: Cache size of ~11264 MB (11811160064 B) is over the 10GB limit, not saving cache.'
) )
const archiveFolder = '/foo/bar' const archiveFolder = '/foo/bar'
@@ -83,7 +79,6 @@ test('save with large cache outputs should fail in GHES with error message', asy
const cachePaths = [path.resolve(filePath)] const cachePaths = [path.resolve(filePath)]
const createTarMock = jest.spyOn(tar, 'createTar') const createTarMock = jest.spyOn(tar, 'createTar')
const logWarningMock = jest.spyOn(core, 'warning')
const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit
jest jest
@@ -111,11 +106,8 @@ test('save with large cache outputs should fail in GHES with error message', asy
return response return response
}) })
const cacheId = await saveCache([filePath], primaryKey) await expect(saveCache([filePath], primaryKey)).rejects.toThrowError(
expect(cacheId).toBe(-1) 'The cache filesize must be between 0 and 1073741824 bytes'
expect(logWarningMock).toHaveBeenCalledTimes(1)
expect(logWarningMock).toHaveBeenCalledWith(
'Failed to save: The cache filesize must be between 0 and 1073741824 bytes'
) )
const archiveFolder = '/foo/bar' const archiveFolder = '/foo/bar'
@@ -135,7 +127,6 @@ test('save with large cache outputs should fail in GHES without error message',
const cachePaths = [path.resolve(filePath)] const cachePaths = [path.resolve(filePath)]
const createTarMock = jest.spyOn(tar, 'createTar') const createTarMock = jest.spyOn(tar, 'createTar')
const logWarningMock = jest.spyOn(core, 'warning')
const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit
jest jest
@@ -159,11 +150,8 @@ test('save with large cache outputs should fail in GHES without error message',
return response return response
}) })
const cacheId = await saveCache([filePath], primaryKey) await expect(saveCache([filePath], primaryKey)).rejects.toThrowError(
expect(cacheId).toBe(-1) 'Cache size of ~11264 MB (11811160064 B) is over the data cap limit, not saving cache.'
expect(logWarningMock).toHaveBeenCalledTimes(1)
expect(logWarningMock).toHaveBeenCalledWith(
'Failed to save: Cache size of ~11264 MB (11811160064 B) is over the data cap limit, not saving cache.'
) )
const archiveFolder = '/foo/bar' const archiveFolder = '/foo/bar'
@@ -180,12 +168,11 @@ test('save with large cache outputs should fail in GHES without error message',
test('save with reserve cache failure should fail', async () => { test('save with reserve cache failure should fail', async () => {
const paths = ['node_modules'] const paths = ['node_modules']
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43' const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
const logInfoMock = jest.spyOn(core, 'info')
const reserveCacheMock = jest const reserveCacheMock = jest
.spyOn(cacheHttpClient, 'reserveCache') .spyOn(cacheHttpClient, 'reserveCache')
.mockImplementation(async () => { .mockImplementation(async () => {
const response: TypedResponse<ReserveCacheResponse> = { const response: ITypedResponse<ReserveCacheResponse> = {
statusCode: 500, statusCode: 500,
result: null, result: null,
headers: {} headers: {}
@@ -200,18 +187,12 @@ test('save with reserve cache failure should fail', async () => {
.spyOn(cacheUtils, 'getCompressionMethod') .spyOn(cacheUtils, 'getCompressionMethod')
.mockReturnValueOnce(Promise.resolve(compression)) .mockReturnValueOnce(Promise.resolve(compression))
const cacheId = await saveCache(paths, primaryKey) await expect(saveCache(paths, primaryKey)).rejects.toThrowError(
expect(cacheId).toBe(-1) `Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
expect(logInfoMock).toHaveBeenCalledTimes(1)
expect(logInfoMock).toHaveBeenCalledWith(
`Failed to save: Unable to reserve cache with key ${primaryKey}, another job may be creating this cache. More details: undefined`
) )
expect(reserveCacheMock).toHaveBeenCalledTimes(1) expect(reserveCacheMock).toHaveBeenCalledTimes(1)
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, paths, { expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, paths, {
cacheSize: undefined, compressionMethod: compression
compressionMethod: compression,
enableCrossOsArchive: false
}) })
expect(createTarMock).toHaveBeenCalledTimes(1) expect(createTarMock).toHaveBeenCalledTimes(1)
expect(saveCacheMock).toHaveBeenCalledTimes(0) expect(saveCacheMock).toHaveBeenCalledTimes(0)
@@ -222,12 +203,12 @@ test('save with server error should fail', async () => {
const filePath = 'node_modules' const filePath = 'node_modules'
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43' const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
const cachePaths = [path.resolve(filePath)] const cachePaths = [path.resolve(filePath)]
const logWarningMock = jest.spyOn(core, 'warning')
const cacheId = 4 const cacheId = 4
const reserveCacheMock = jest const reserveCacheMock = jest
.spyOn(cacheHttpClient, 'reserveCache') .spyOn(cacheHttpClient, 'reserveCache')
.mockImplementation(async () => { .mockImplementation(async () => {
const response: TypedResponse<ReserveCacheResponse> = { const response: ITypedResponse<ReserveCacheResponse> = {
statusCode: 500, statusCode: 500,
result: {cacheId}, result: {cacheId},
headers: {} headers: {}
@@ -247,17 +228,12 @@ test('save with server error should fail', async () => {
.spyOn(cacheUtils, 'getCompressionMethod') .spyOn(cacheUtils, 'getCompressionMethod')
.mockReturnValueOnce(Promise.resolve(compression)) .mockReturnValueOnce(Promise.resolve(compression))
await saveCache([filePath], primaryKey) await expect(saveCache([filePath], primaryKey)).rejects.toThrowError(
expect(logWarningMock).toHaveBeenCalledTimes(1) 'HTTP Error Occurred'
expect(logWarningMock).toHaveBeenCalledWith(
'Failed to save: HTTP Error Occurred'
) )
expect(reserveCacheMock).toHaveBeenCalledTimes(1) expect(reserveCacheMock).toHaveBeenCalledTimes(1)
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, [filePath], { expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, [filePath], {
cacheSize: undefined, compressionMethod: compression
compressionMethod: compression,
enableCrossOsArchive: false
}) })
const archiveFolder = '/foo/bar' const archiveFolder = '/foo/bar'
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd) const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
@@ -281,7 +257,7 @@ test('save with valid inputs uploads a cache', async () => {
const reserveCacheMock = jest const reserveCacheMock = jest
.spyOn(cacheHttpClient, 'reserveCache') .spyOn(cacheHttpClient, 'reserveCache')
.mockImplementation(async () => { .mockImplementation(async () => {
const response: TypedResponse<ReserveCacheResponse> = { const response: ITypedResponse<ReserveCacheResponse> = {
statusCode: 500, statusCode: 500,
result: {cacheId}, result: {cacheId},
headers: {} headers: {}
@@ -300,9 +276,7 @@ test('save with valid inputs uploads a cache', async () => {
expect(reserveCacheMock).toHaveBeenCalledTimes(1) expect(reserveCacheMock).toHaveBeenCalledTimes(1)
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, [filePath], { expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, [filePath], {
cacheSize: undefined, compressionMethod: compression
compressionMethod: compression,
enableCrossOsArchive: false
}) })
const archiveFolder = '/foo/bar' const archiveFolder = '/foo/bar'
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd) const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
@@ -316,14 +290,3 @@ test('save with valid inputs uploads a cache', async () => {
expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archiveFile, undefined) expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archiveFile, undefined)
expect(getCompressionMock).toHaveBeenCalledTimes(1) expect(getCompressionMock).toHaveBeenCalledTimes(1)
}) })
test('save with non existing path should not save cache', async () => {
const path = 'node_modules'
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
jest.spyOn(cacheUtils, 'resolvePaths').mockImplementation(async () => {
return []
})
await expect(saveCache([path], primaryKey)).rejects.toThrowError(
`Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved.`
)
})
+57 -272
View File
@@ -1,14 +1,7 @@
import * as exec from '@actions/exec' import * as exec from '@actions/exec'
import * as io from '@actions/io' import * as io from '@actions/io'
import * as path from 'path' import * as path from 'path'
import { import {CacheFilename, CompressionMethod} from '../src/internal/constants'
CacheFilename,
CompressionMethod,
GnuTarPathOnWindows,
ManifestFilename,
SystemTarPathOnWindows,
TarFilename
} from '../src/internal/constants'
import * as tar from '../src/internal/tar' import * as tar from '../src/internal/tar'
import * as utils from '../src/internal/cacheUtils' import * as utils from '../src/internal/cacheUtils'
// eslint-disable-next-line @typescript-eslint/no-require-imports // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -20,9 +13,7 @@ jest.mock('@actions/io')
const IS_WINDOWS = process.platform === 'win32' const IS_WINDOWS = process.platform === 'win32'
const IS_MAC = process.platform === 'darwin' const IS_MAC = process.platform === 'darwin'
const defaultTarPath = IS_MAC ? 'gtar' : 'tar' const defaultTarPath = process.platform === 'darwin' ? 'gtar' : 'tar'
const defaultEnv = {MSYS: 'winsymlinks:nativestrict'}
function getTempDir(): string { function getTempDir(): string {
return path.join(__dirname, '_temp', 'tar') return path.join(__dirname, '_temp', 'tar')
@@ -37,10 +28,6 @@ beforeAll(async () => {
await jest.requireActual('@actions/io').rmRF(getTempDir()) await jest.requireActual('@actions/io').rmRF(getTempDir())
}) })
beforeEach(async () => {
jest.restoreAllMocks()
})
afterAll(async () => { afterAll(async () => {
delete process.env['GITHUB_WORKSPACE'] delete process.env['GITHUB_WORKSPACE']
await jest.requireActual('@actions/io').rmRF(getTempDir()) await jest.requireActual('@actions/io').rmRF(getTempDir())
@@ -54,15 +41,16 @@ test('zstd extract tar', async () => {
? `${process.env['windir']}\\fakepath\\cache.tar` ? `${process.env['windir']}\\fakepath\\cache.tar`
: 'cache.tar' : 'cache.tar'
const workspace = process.env['GITHUB_WORKSPACE'] const workspace = process.env['GITHUB_WORKSPACE']
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
await tar.extractTar(archivePath, CompressionMethod.Zstd) await tar.extractTar(archivePath, CompressionMethod.Zstd)
expect(mkdirMock).toHaveBeenCalledWith(workspace) expect(mkdirMock).toHaveBeenCalledWith(workspace)
expect(execMock).toHaveBeenCalledTimes(1) expect(execMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledWith( expect(execMock).toHaveBeenCalledWith(
`"${defaultTarPath}"`,
[ [
`"${tarPath}"`, '--use-compress-program',
'zstd -d --long=30',
'-xf', '-xf',
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath, IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
'-P', '-P',
@@ -70,70 +58,11 @@ test('zstd extract tar', async () => {
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace
] ]
.concat(IS_WINDOWS ? ['--force-local'] : []) .concat(IS_WINDOWS ? ['--force-local'] : [])
.concat(IS_MAC ? ['--delay-directory-restore'] : []) .concat(IS_MAC ? ['--delay-directory-restore'] : []),
.concat([ {cwd: undefined}
'--use-compress-program',
IS_WINDOWS ? '"zstd -d --long=30"' : 'unzstd --long=30'
])
.join(' '),
undefined,
{
cwd: undefined,
env: expect.objectContaining(defaultEnv)
}
) )
}) })
test('zstd extract tar with windows BSDtar', async () => {
if (IS_WINDOWS) {
const mkdirMock = jest.spyOn(io, 'mkdirP')
const execMock = jest.spyOn(exec, 'exec')
jest
.spyOn(utils, 'getGnuTarPathOnWindows')
.mockReturnValue(Promise.resolve(''))
const archivePath = `${process.env['windir']}\\fakepath\\cache.tar`
const workspace = process.env['GITHUB_WORKSPACE']
const tarPath = SystemTarPathOnWindows
await tar.extractTar(archivePath, CompressionMethod.Zstd)
expect(mkdirMock).toHaveBeenCalledWith(workspace)
expect(execMock).toHaveBeenCalledTimes(2)
expect(execMock).toHaveBeenNthCalledWith(
1,
[
'zstd -d --long=30 --force -o',
TarFilename.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/')
].join(' '),
undefined,
{
cwd: undefined,
env: expect.objectContaining(defaultEnv)
}
)
expect(execMock).toHaveBeenNthCalledWith(
2,
[
`"${tarPath}"`,
'-xf',
TarFilename.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
'-P',
'-C',
workspace?.replace(/\\/g, '/')
].join(' '),
undefined,
{
cwd: undefined,
env: expect.objectContaining(defaultEnv)
}
)
}
})
test('gzip extract tar', async () => { test('gzip extract tar', async () => {
const mkdirMock = jest.spyOn(io, 'mkdirP') const mkdirMock = jest.spyOn(io, 'mkdirP')
const execMock = jest.spyOn(exec, 'exec') const execMock = jest.spyOn(exec, 'exec')
@@ -145,58 +74,51 @@ test('gzip extract tar', async () => {
await tar.extractTar(archivePath, CompressionMethod.Gzip) await tar.extractTar(archivePath, CompressionMethod.Gzip)
expect(mkdirMock).toHaveBeenCalledWith(workspace) expect(mkdirMock).toHaveBeenCalledWith(workspace)
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath const tarPath = IS_WINDOWS
? `${process.env['windir']}\\System32\\tar.exe`
: defaultTarPath
expect(execMock).toHaveBeenCalledTimes(1) expect(execMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledWith( expect(execMock).toHaveBeenCalledWith(
`"${tarPath}"`,
[ [
`"${tarPath}"`, '-z',
'-xf', '-xf',
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath, IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
'-P', '-P',
'-C', '-C',
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace
] ].concat(IS_MAC ? ['--delay-directory-restore'] : []),
.concat(IS_WINDOWS ? ['--force-local'] : []) {cwd: undefined}
.concat(IS_MAC ? ['--delay-directory-restore'] : [])
.concat(['-z'])
.join(' '),
undefined,
{
cwd: undefined,
env: expect.objectContaining(defaultEnv)
}
) )
}) })
test('gzip extract GNU tar on windows with GNUtar in path', async () => { test('gzip extract GNU tar on windows', async () => {
if (IS_WINDOWS) { if (IS_WINDOWS) {
// GNU tar present in path but not at default location jest.spyOn(fs, 'existsSync').mockReturnValueOnce(false)
jest
.spyOn(utils, 'getGnuTarPathOnWindows') const isGnuMock = jest
.mockReturnValue(Promise.resolve('tar')) .spyOn(utils, 'isGnuTarInstalled')
.mockReturnValue(Promise.resolve(true))
const execMock = jest.spyOn(exec, 'exec') const execMock = jest.spyOn(exec, 'exec')
const archivePath = `${process.env['windir']}\\fakepath\\cache.tar` const archivePath = `${process.env['windir']}\\fakepath\\cache.tar`
const workspace = process.env['GITHUB_WORKSPACE'] const workspace = process.env['GITHUB_WORKSPACE']
await tar.extractTar(archivePath, CompressionMethod.Gzip) await tar.extractTar(archivePath, CompressionMethod.Gzip)
expect(isGnuMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledTimes(1) expect(execMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledWith( expect(execMock).toHaveBeenCalledWith(
`"tar"`,
[ [
`"tar"`, '-z',
'-xf', '-xf',
archivePath.replace(/\\/g, '/'), archivePath.replace(/\\/g, '/'),
'-P', '-P',
'-C', '-C',
workspace?.replace(/\\/g, '/'), workspace?.replace(/\\/g, '/'),
'--force-local', '--force-local'
'-z' ],
].join(' '), {cwd: undefined}
undefined,
{
cwd: undefined,
env: expect.objectContaining(defaultEnv)
}
) )
} }
}) })
@@ -212,99 +134,29 @@ test('zstd create tar', async () => {
await tar.createTar(archiveFolder, sourceDirectories, CompressionMethod.Zstd) await tar.createTar(archiveFolder, sourceDirectories, CompressionMethod.Zstd)
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
expect(execMock).toHaveBeenCalledTimes(1) expect(execMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledWith( expect(execMock).toHaveBeenCalledWith(
`"${defaultTarPath}"`,
[ [
`"${tarPath}"`,
'--posix', '--posix',
'--use-compress-program',
'zstd -T0 --long=30',
'-cf', '-cf',
IS_WINDOWS ? CacheFilename.Zstd.replace(/\\/g, '/') : CacheFilename.Zstd, IS_WINDOWS ? CacheFilename.Zstd.replace(/\\/g, '/') : CacheFilename.Zstd,
'--exclude',
IS_WINDOWS ? CacheFilename.Zstd.replace(/\\/g, '/') : CacheFilename.Zstd,
'-P', '-P',
'-C', '-C',
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace, IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace,
'--files-from', '--files-from',
ManifestFilename 'manifest.txt'
] ]
.concat(IS_WINDOWS ? ['--force-local'] : []) .concat(IS_WINDOWS ? ['--force-local'] : [])
.concat(IS_MAC ? ['--delay-directory-restore'] : []) .concat(IS_MAC ? ['--delay-directory-restore'] : []),
.concat([
'--use-compress-program',
IS_WINDOWS ? '"zstd -T0 --long=30"' : 'zstdmt --long=30'
])
.join(' '),
undefined, // args
{ {
cwd: archiveFolder, cwd: archiveFolder
env: expect.objectContaining(defaultEnv)
} }
) )
}) })
test('zstd create tar with windows BSDtar', async () => {
if (IS_WINDOWS) {
const execMock = jest.spyOn(exec, 'exec')
jest
.spyOn(utils, 'getGnuTarPathOnWindows')
.mockReturnValue(Promise.resolve(''))
const archiveFolder = getTempDir()
const workspace = process.env['GITHUB_WORKSPACE']
const sourceDirectories = ['~/.npm/cache', `${workspace}/dist`]
await fs.promises.mkdir(archiveFolder, {recursive: true})
await tar.createTar(
archiveFolder,
sourceDirectories,
CompressionMethod.Zstd
)
const tarPath = SystemTarPathOnWindows
expect(execMock).toHaveBeenCalledTimes(2)
expect(execMock).toHaveBeenNthCalledWith(
1,
[
`"${tarPath}"`,
'--posix',
'-cf',
TarFilename.replace(/\\/g, '/'),
'--exclude',
TarFilename.replace(/\\/g, '/'),
'-P',
'-C',
workspace?.replace(/\\/g, '/'),
'--files-from',
ManifestFilename
].join(' '),
undefined, // args
{
cwd: archiveFolder,
env: expect.objectContaining(defaultEnv)
}
)
expect(execMock).toHaveBeenNthCalledWith(
2,
[
'zstd -T0 --long=30 --force -o',
CacheFilename.Zstd.replace(/\\/g, '/'),
TarFilename.replace(/\\/g, '/')
].join(' '),
undefined, // args
{
cwd: archiveFolder,
env: expect.objectContaining(defaultEnv)
}
)
}
})
test('gzip create tar', async () => { test('gzip create tar', async () => {
const execMock = jest.spyOn(exec, 'exec') const execMock = jest.spyOn(exec, 'exec')
@@ -316,31 +168,26 @@ test('gzip create tar', async () => {
await tar.createTar(archiveFolder, sourceDirectories, CompressionMethod.Gzip) await tar.createTar(archiveFolder, sourceDirectories, CompressionMethod.Gzip)
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath const tarPath = IS_WINDOWS
? `${process.env['windir']}\\System32\\tar.exe`
: defaultTarPath
expect(execMock).toHaveBeenCalledTimes(1) expect(execMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledWith( expect(execMock).toHaveBeenCalledWith(
`"${tarPath}"`,
[ [
`"${tarPath}"`,
'--posix', '--posix',
'-z',
'-cf', '-cf',
IS_WINDOWS ? CacheFilename.Gzip.replace(/\\/g, '/') : CacheFilename.Gzip, IS_WINDOWS ? CacheFilename.Gzip.replace(/\\/g, '/') : CacheFilename.Gzip,
'--exclude',
IS_WINDOWS ? CacheFilename.Gzip.replace(/\\/g, '/') : CacheFilename.Gzip,
'-P', '-P',
'-C', '-C',
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace, IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace,
'--files-from', '--files-from',
ManifestFilename 'manifest.txt'
] ].concat(IS_MAC ? ['--delay-directory-restore'] : []),
.concat(IS_WINDOWS ? ['--force-local'] : [])
.concat(IS_MAC ? ['--delay-directory-restore'] : [])
.concat(['-z'])
.join(' '),
undefined, // args
{ {
cwd: archiveFolder, cwd: archiveFolder
env: expect.objectContaining(defaultEnv)
} }
) )
}) })
@@ -354,74 +201,22 @@ test('zstd list tar', async () => {
await tar.listTar(archivePath, CompressionMethod.Zstd) await tar.listTar(archivePath, CompressionMethod.Zstd)
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
expect(execMock).toHaveBeenCalledTimes(1) expect(execMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledWith( expect(execMock).toHaveBeenCalledWith(
`"${defaultTarPath}"`,
[ [
`"${tarPath}"`, '--use-compress-program',
'zstd -d --long=30',
'-tf', '-tf',
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath, IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
'-P' '-P'
] ]
.concat(IS_WINDOWS ? ['--force-local'] : []) .concat(IS_WINDOWS ? ['--force-local'] : [])
.concat(IS_MAC ? ['--delay-directory-restore'] : []) .concat(IS_MAC ? ['--delay-directory-restore'] : []),
.concat([ {cwd: undefined}
'--use-compress-program',
IS_WINDOWS ? '"zstd -d --long=30"' : 'unzstd --long=30'
])
.join(' '),
undefined,
{
cwd: undefined,
env: expect.objectContaining(defaultEnv)
}
) )
}) })
test('zstd list tar with windows BSDtar', async () => {
if (IS_WINDOWS) {
const execMock = jest.spyOn(exec, 'exec')
jest
.spyOn(utils, 'getGnuTarPathOnWindows')
.mockReturnValue(Promise.resolve(''))
const archivePath = `${process.env['windir']}\\fakepath\\cache.tar`
await tar.listTar(archivePath, CompressionMethod.Zstd)
const tarPath = SystemTarPathOnWindows
expect(execMock).toHaveBeenCalledTimes(2)
expect(execMock).toHaveBeenNthCalledWith(
1,
[
'zstd -d --long=30 --force -o',
TarFilename.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/')
].join(' '),
undefined,
{
cwd: undefined,
env: expect.objectContaining(defaultEnv)
}
)
expect(execMock).toHaveBeenNthCalledWith(
2,
[
`"${tarPath}"`,
'-tf',
TarFilename.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
'-P'
].join(' '),
undefined,
{
cwd: undefined,
env: expect.objectContaining(defaultEnv)
}
)
}
})
test('zstdWithoutLong list tar', async () => { test('zstdWithoutLong list tar', async () => {
const execMock = jest.spyOn(exec, 'exec') const execMock = jest.spyOn(exec, 'exec')
@@ -431,24 +226,19 @@ test('zstdWithoutLong list tar', async () => {
await tar.listTar(archivePath, CompressionMethod.ZstdWithoutLong) await tar.listTar(archivePath, CompressionMethod.ZstdWithoutLong)
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
expect(execMock).toHaveBeenCalledTimes(1) expect(execMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledWith( expect(execMock).toHaveBeenCalledWith(
`"${defaultTarPath}"`,
[ [
`"${tarPath}"`, '--use-compress-program',
'zstd -d',
'-tf', '-tf',
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath, IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
'-P' '-P'
] ]
.concat(IS_WINDOWS ? ['--force-local'] : []) .concat(IS_WINDOWS ? ['--force-local'] : [])
.concat(IS_MAC ? ['--delay-directory-restore'] : []) .concat(IS_MAC ? ['--delay-directory-restore'] : []),
.concat(['--use-compress-program', IS_WINDOWS ? '"zstd -d"' : 'unzstd']) {cwd: undefined}
.join(' '),
undefined,
{
cwd: undefined,
env: expect.objectContaining(defaultEnv)
}
) )
}) })
@@ -460,23 +250,18 @@ test('gzip list tar', async () => {
await tar.listTar(archivePath, CompressionMethod.Gzip) await tar.listTar(archivePath, CompressionMethod.Gzip)
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath const tarPath = IS_WINDOWS
? `${process.env['windir']}\\System32\\tar.exe`
: defaultTarPath
expect(execMock).toHaveBeenCalledTimes(1) expect(execMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledWith( expect(execMock).toHaveBeenCalledWith(
`"${tarPath}"`,
[ [
`"${tarPath}"`, '-z',
'-tf', '-tf',
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath, IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
'-P' '-P'
] ].concat(IS_MAC ? ['--delay-directory-restore'] : []),
.concat(IS_WINDOWS ? ['--force-local'] : []) {cwd: undefined}
.concat(IS_MAC ? ['--delay-directory-restore'] : [])
.concat(['-z'])
.join(' '),
undefined,
{
cwd: undefined,
env: expect.objectContaining(defaultEnv)
}
) )
}) })
+164 -168
View File
@@ -1,52 +1,39 @@
{ {
"name": "@actions/cache", "name": "@actions/cache",
"version": "3.2.1", "version": "2.0.2",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@actions/cache", "name": "@actions/cache",
"version": "3.2.1", "version": "2.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.10.0", "@actions/core": "^1.2.6",
"@actions/exec": "^1.0.1", "@actions/exec": "^1.0.1",
"@actions/glob": "^0.1.0", "@actions/glob": "^0.1.0",
"@actions/http-client": "^2.0.1", "@actions/http-client": "^1.0.9",
"@actions/io": "^1.0.1", "@actions/io": "^1.0.1",
"@azure/abort-controller": "^1.1.0",
"@azure/ms-rest-js": "^2.6.0", "@azure/ms-rest-js": "^2.6.0",
"@azure/storage-blob": "^12.13.0", "@azure/storage-blob": "^12.8.0",
"semver": "^6.1.0", "semver": "^6.1.0",
"uuid": "^3.3.3" "uuid": "^3.3.3"
}, },
"devDependencies": { "devDependencies": {
"@types/semver": "^6.0.0", "@types/semver": "^6.0.0",
"@types/uuid": "^3.4.5", "@types/uuid": "^3.4.5",
"typescript": "^4.8.0" "typescript": "^3.8.3"
} }
}, },
"node_modules/@actions/core": { "node_modules/@actions/core": {
"version": "1.10.0", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==", "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
"dependencies": {
"@actions/http-client": "^2.0.1",
"uuid": "^8.3.2"
}
},
"node_modules/@actions/core/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
}, },
"node_modules/@actions/exec": { "node_modules/@actions/exec": {
"version": "1.1.1", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.4.tgz",
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", "integrity": "sha512-4DPChWow9yc9W3WqEbUj8Nr86xkpyE29ZzWjXucHItclLbEW6jr80Zx4nqv18QL6KK65+cifiQZXvnqgTV6oHw==",
"dependencies": { "dependencies": {
"@actions/io": "^1.0.1" "@actions/io": "^1.0.1"
} }
@@ -61,27 +48,35 @@
} }
}, },
"node_modules/@actions/http-client": { "node_modules/@actions/http-client": {
"version": "2.0.1", "version": "1.0.9",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.9.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "integrity": "sha512-0O4SsJ7q+MK0ycvXPl2e6bMXV7dxAXOGjrXS1eTF9s2S401Tp6c/P3c3Joz04QefC1J6Gt942Wl2jbm3f4mLcg==",
"dependencies": { "dependencies": {
"tunnel": "^0.0.6" "tunnel": "0.0.6"
}
},
"node_modules/@actions/http-client/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/@actions/io": { "node_modules/@actions/io": {
"version": "1.1.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz",
"integrity": "sha512-d+RwPlMp+2qmBfeLYPLXuSRykDIFEwdTA0MMxzS9kh4kvP1ftrc/9fzy6pX6qAjthdXruHQ6/6kjT/DNo5ALuw==" "integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg=="
}, },
"node_modules/@azure/abort-controller": { "node_modules/@azure/abort-controller": {
"version": "1.1.0", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.4.tgz",
"integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "integrity": "sha512-lNUmDRVGpanCsiUN3NWxFTdwmdFI53xwhkTFfHDGTYk46ca7Ind3nanJc+U6Zj9Tv+9nTCWRBscWEW1DyKOpTw==",
"dependencies": { "dependencies": {
"tslib": "^2.2.0" "tslib": "^2.0.0"
}, },
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=8.0.0"
} }
}, },
"node_modules/@azure/abort-controller/node_modules/tslib": { "node_modules/@azure/abort-controller/node_modules/tslib": {
@@ -112,27 +107,28 @@
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}, },
"node_modules/@azure/core-http": { "node_modules/@azure/core-http": {
"version": "3.0.1", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.1.tgz", "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-2.2.3.tgz",
"integrity": "sha512-A3x+um3cAPgQe42Lu7Iv/x8/fNjhL/nIoEfqFxfn30EyxK6zC13n+OUxzZBRC0IzQqssqIbt4INf5YG7lYYFtw==", "integrity": "sha512-xr8AeszxP418rI//W38NfJDDr0kbVAPZkURZnZ+Fle+lLWeURjDE5zNIuocA1wUPoKSP8iXc0ApW6nPtbLGswA==",
"dependencies": { "dependencies": {
"@azure/abort-controller": "^1.0.0", "@azure/abort-controller": "^1.0.0",
"@azure/core-asynciterator-polyfill": "^1.0.0",
"@azure/core-auth": "^1.3.0", "@azure/core-auth": "^1.3.0",
"@azure/core-tracing": "1.0.0-preview.13", "@azure/core-tracing": "1.0.0-preview.13",
"@azure/core-util": "^1.1.1",
"@azure/logger": "^1.0.0", "@azure/logger": "^1.0.0",
"@types/node-fetch": "^2.5.0", "@types/node-fetch": "^2.5.0",
"@types/tunnel": "^0.0.3", "@types/tunnel": "^0.0.3",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.6",
"process": "^0.11.10", "process": "^0.11.10",
"tough-cookie": "^4.0.0",
"tslib": "^2.2.0", "tslib": "^2.2.0",
"tunnel": "^0.0.6", "tunnel": "^0.0.6",
"uuid": "^8.3.0", "uuid": "^8.3.0",
"xml2js": "^0.5.0" "xml2js": "^0.4.19"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=12.0.0"
} }
}, },
"node_modules/@azure/core-http/node_modules/form-data": { "node_modules/@azure/core-http/node_modules/form-data": {
@@ -148,10 +144,23 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/@azure/core-http/node_modules/tough-cookie": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
"integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==",
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.1.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@azure/core-http/node_modules/tslib": { "node_modules/@azure/core-http/node_modules/tslib": {
"version": "2.5.0", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}, },
"node_modules/@azure/core-http/node_modules/uuid": { "node_modules/@azure/core-http/node_modules/uuid": {
"version": "8.3.2", "version": "8.3.2",
@@ -214,23 +223,6 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}, },
"node_modules/@azure/core-util": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.2.0.tgz",
"integrity": "sha512-ffGIw+Qs8bNKNLxz5UPkz4/VBM/EZY07mPve1ZYFqYUdPwFqRj0RPk0U7LZMOfT7GCck9YjuT1Rfp1PApNl1ng==",
"dependencies": {
"@azure/abort-controller": "^1.0.0",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@azure/core-util/node_modules/tslib": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
},
"node_modules/@azure/logger": { "node_modules/@azure/logger": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.3.tgz",
@@ -248,19 +240,19 @@
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}, },
"node_modules/@azure/ms-rest-js": { "node_modules/@azure/ms-rest-js": {
"version": "2.6.6", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.6.6.tgz", "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.6.0.tgz",
"integrity": "sha512-WYIda8VvrkZE68xHgOxUXvjThxNf1nnGPPe0rAljqK5HJHIZ12Pi3YhEDOn3Ge7UnwaaM3eFO0VtAy4nGVI27Q==", "integrity": "sha512-4C5FCtvEzWudblB+h92/TYYPiq7tuElX8icVYToxOdggnYqeec4Se14mjse5miInKtZahiFHdl8lZA/jziEc5g==",
"dependencies": { "dependencies": {
"@azure/core-auth": "^1.1.4", "@azure/core-auth": "^1.1.4",
"abort-controller": "^3.0.0", "abort-controller": "^3.0.0",
"form-data": "^2.5.0", "form-data": "^2.5.0",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.0",
"tough-cookie": "^3.0.1", "tough-cookie": "^3.0.1",
"tslib": "^1.10.0", "tslib": "^1.10.0",
"tunnel": "0.0.6", "tunnel": "0.0.6",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"xml2js": "^0.5.0" "xml2js": "^0.4.19"
} }
}, },
"node_modules/@azure/ms-rest-js/node_modules/uuid": { "node_modules/@azure/ms-rest-js/node_modules/uuid": {
@@ -272,12 +264,12 @@
} }
}, },
"node_modules/@azure/storage-blob": { "node_modules/@azure/storage-blob": {
"version": "12.13.0", "version": "12.8.0",
"resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.13.0.tgz", "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.8.0.tgz",
"integrity": "sha512-t3Q2lvBMJucgTjQcP5+hvEJMAsJSk0qmAnjDLie2td017IiduZbbC9BOcFfmwzR6y6cJdZOuewLCNFmEx9IrXA==", "integrity": "sha512-c8+Wz19xauW0bGkTCoqZH4dYfbtBniPiGiRQOn1ca6G5jsjr4azwaTk9gwjVY8r3vY2Taf95eivLzipfIfiS4A==",
"dependencies": { "dependencies": {
"@azure/abort-controller": "^1.0.0", "@azure/abort-controller": "^1.0.0",
"@azure/core-http": "^3.0.0", "@azure/core-http": "^2.0.0",
"@azure/core-lro": "^2.2.0", "@azure/core-lro": "^2.2.0",
"@azure/core-paging": "^1.1.1", "@azure/core-paging": "^1.1.1",
"@azure/core-tracing": "1.0.0-preview.13", "@azure/core-tracing": "1.0.0-preview.13",
@@ -286,7 +278,7 @@
"tslib": "^2.2.0" "tslib": "^2.2.0"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=12.0.0"
} }
}, },
"node_modules/@azure/storage-blob/node_modules/tslib": { "node_modules/@azure/storage-blob/node_modules/tslib": {
@@ -303,14 +295,14 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "18.14.6", "version": "17.0.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.6.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.14.tgz",
"integrity": "sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==" "integrity": "sha512-SbjLmERksKOGzWzPNuW7fJM7fk3YXVTFiZWB/Hs99gwhk+/dnrQRPBQjPW9aO+fi1tAffi9PrwFvsmOKmDTyng=="
}, },
"node_modules/@types/node-fetch": { "node_modules/@types/node-fetch": {
"version": "2.6.2", "version": "2.5.12",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz",
"integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==",
"dependencies": { "dependencies": {
"@types/node": "*", "@types/node": "*",
"form-data": "^3.0.0" "form-data": "^3.0.0"
@@ -460,9 +452,9 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@@ -492,7 +484,7 @@
"node_modules/process": { "node_modules/process": {
"version": "0.11.10", "version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
"engines": { "engines": {
"node": ">= 0.6.0" "node": ">= 0.6.0"
} }
@@ -555,9 +547,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "4.8.4", "version": "3.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
"integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
"dev": true, "dev": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
@@ -567,6 +559,14 @@
"node": ">=4.2.0" "node": ">=4.2.0"
} }
}, },
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/uuid": { "node_modules/uuid": {
"version": "3.4.0", "version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
@@ -591,9 +591,9 @@
} }
}, },
"node_modules/xml2js": { "node_modules/xml2js": {
"version": "0.5.0", "version": "0.4.23",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
"dependencies": { "dependencies": {
"sax": ">=0.6.0", "sax": ">=0.6.0",
"xmlbuilder": "~11.0.0" "xmlbuilder": "~11.0.0"
@@ -613,25 +613,14 @@
}, },
"dependencies": { "dependencies": {
"@actions/core": { "@actions/core": {
"version": "1.10.0", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==", "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
"requires": {
"@actions/http-client": "^2.0.1",
"uuid": "^8.3.2"
},
"dependencies": {
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
}
}
}, },
"@actions/exec": { "@actions/exec": {
"version": "1.1.1", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.4.tgz",
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", "integrity": "sha512-4DPChWow9yc9W3WqEbUj8Nr86xkpyE29ZzWjXucHItclLbEW6jr80Zx4nqv18QL6KK65+cifiQZXvnqgTV6oHw==",
"requires": { "requires": {
"@actions/io": "^1.0.1" "@actions/io": "^1.0.1"
} }
@@ -646,24 +635,31 @@
} }
}, },
"@actions/http-client": { "@actions/http-client": {
"version": "2.0.1", "version": "1.0.9",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.9.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "integrity": "sha512-0O4SsJ7q+MK0ycvXPl2e6bMXV7dxAXOGjrXS1eTF9s2S401Tp6c/P3c3Joz04QefC1J6Gt942Wl2jbm3f4mLcg==",
"requires": { "requires": {
"tunnel": "^0.0.6" "tunnel": "0.0.6"
},
"dependencies": {
"tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
}
} }
}, },
"@actions/io": { "@actions/io": {
"version": "1.1.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz",
"integrity": "sha512-d+RwPlMp+2qmBfeLYPLXuSRykDIFEwdTA0MMxzS9kh4kvP1ftrc/9fzy6pX6qAjthdXruHQ6/6kjT/DNo5ALuw==" "integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg=="
}, },
"@azure/abort-controller": { "@azure/abort-controller": {
"version": "1.1.0", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.4.tgz",
"integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "integrity": "sha512-lNUmDRVGpanCsiUN3NWxFTdwmdFI53xwhkTFfHDGTYk46ca7Ind3nanJc+U6Zj9Tv+9nTCWRBscWEW1DyKOpTw==",
"requires": { "requires": {
"tslib": "^2.2.0" "tslib": "^2.0.0"
}, },
"dependencies": { "dependencies": {
"tslib": { "tslib": {
@@ -695,24 +691,25 @@
} }
}, },
"@azure/core-http": { "@azure/core-http": {
"version": "3.0.1", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.1.tgz", "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-2.2.3.tgz",
"integrity": "sha512-A3x+um3cAPgQe42Lu7Iv/x8/fNjhL/nIoEfqFxfn30EyxK6zC13n+OUxzZBRC0IzQqssqIbt4INf5YG7lYYFtw==", "integrity": "sha512-xr8AeszxP418rI//W38NfJDDr0kbVAPZkURZnZ+Fle+lLWeURjDE5zNIuocA1wUPoKSP8iXc0ApW6nPtbLGswA==",
"requires": { "requires": {
"@azure/abort-controller": "^1.0.0", "@azure/abort-controller": "^1.0.0",
"@azure/core-asynciterator-polyfill": "^1.0.0",
"@azure/core-auth": "^1.3.0", "@azure/core-auth": "^1.3.0",
"@azure/core-tracing": "1.0.0-preview.13", "@azure/core-tracing": "1.0.0-preview.13",
"@azure/core-util": "^1.1.1",
"@azure/logger": "^1.0.0", "@azure/logger": "^1.0.0",
"@types/node-fetch": "^2.5.0", "@types/node-fetch": "^2.5.0",
"@types/tunnel": "^0.0.3", "@types/tunnel": "^0.0.3",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.6",
"process": "^0.11.10", "process": "^0.11.10",
"tough-cookie": "^4.0.0",
"tslib": "^2.2.0", "tslib": "^2.2.0",
"tunnel": "^0.0.6", "tunnel": "^0.0.6",
"uuid": "^8.3.0", "uuid": "^8.3.0",
"xml2js": "^0.5.0" "xml2js": "^0.4.19"
}, },
"dependencies": { "dependencies": {
"form-data": { "form-data": {
@@ -725,10 +722,20 @@
"mime-types": "^2.1.12" "mime-types": "^2.1.12"
} }
}, },
"tough-cookie": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
"integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==",
"requires": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.1.2"
}
},
"tslib": { "tslib": {
"version": "2.5.0", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}, },
"uuid": { "uuid": {
"version": "8.3.2", "version": "8.3.2",
@@ -787,22 +794,6 @@
} }
} }
}, },
"@azure/core-util": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.2.0.tgz",
"integrity": "sha512-ffGIw+Qs8bNKNLxz5UPkz4/VBM/EZY07mPve1ZYFqYUdPwFqRj0RPk0U7LZMOfT7GCck9YjuT1Rfp1PApNl1ng==",
"requires": {
"@azure/abort-controller": "^1.0.0",
"tslib": "^2.2.0"
},
"dependencies": {
"tslib": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
}
}
},
"@azure/logger": { "@azure/logger": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.3.tgz",
@@ -819,19 +810,19 @@
} }
}, },
"@azure/ms-rest-js": { "@azure/ms-rest-js": {
"version": "2.6.6", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.6.6.tgz", "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.6.0.tgz",
"integrity": "sha512-WYIda8VvrkZE68xHgOxUXvjThxNf1nnGPPe0rAljqK5HJHIZ12Pi3YhEDOn3Ge7UnwaaM3eFO0VtAy4nGVI27Q==", "integrity": "sha512-4C5FCtvEzWudblB+h92/TYYPiq7tuElX8icVYToxOdggnYqeec4Se14mjse5miInKtZahiFHdl8lZA/jziEc5g==",
"requires": { "requires": {
"@azure/core-auth": "^1.1.4", "@azure/core-auth": "^1.1.4",
"abort-controller": "^3.0.0", "abort-controller": "^3.0.0",
"form-data": "^2.5.0", "form-data": "^2.5.0",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.0",
"tough-cookie": "^3.0.1", "tough-cookie": "^3.0.1",
"tslib": "^1.10.0", "tslib": "^1.10.0",
"tunnel": "0.0.6", "tunnel": "0.0.6",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"xml2js": "^0.5.0" "xml2js": "^0.4.19"
}, },
"dependencies": { "dependencies": {
"uuid": { "uuid": {
@@ -842,12 +833,12 @@
} }
}, },
"@azure/storage-blob": { "@azure/storage-blob": {
"version": "12.13.0", "version": "12.8.0",
"resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.13.0.tgz", "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.8.0.tgz",
"integrity": "sha512-t3Q2lvBMJucgTjQcP5+hvEJMAsJSk0qmAnjDLie2td017IiduZbbC9BOcFfmwzR6y6cJdZOuewLCNFmEx9IrXA==", "integrity": "sha512-c8+Wz19xauW0bGkTCoqZH4dYfbtBniPiGiRQOn1ca6G5jsjr4azwaTk9gwjVY8r3vY2Taf95eivLzipfIfiS4A==",
"requires": { "requires": {
"@azure/abort-controller": "^1.0.0", "@azure/abort-controller": "^1.0.0",
"@azure/core-http": "^3.0.0", "@azure/core-http": "^2.0.0",
"@azure/core-lro": "^2.2.0", "@azure/core-lro": "^2.2.0",
"@azure/core-paging": "^1.1.1", "@azure/core-paging": "^1.1.1",
"@azure/core-tracing": "1.0.0-preview.13", "@azure/core-tracing": "1.0.0-preview.13",
@@ -869,14 +860,14 @@
"integrity": "sha512-BuJuXRSJNQ3QoKA6GWWDyuLpOUck+9hAXNMCnrloc1aWVoy6Xq6t9PUV08aBZ4Lutqq2LEHM486bpZqoViScog==" "integrity": "sha512-BuJuXRSJNQ3QoKA6GWWDyuLpOUck+9hAXNMCnrloc1aWVoy6Xq6t9PUV08aBZ4Lutqq2LEHM486bpZqoViScog=="
}, },
"@types/node": { "@types/node": {
"version": "18.14.6", "version": "17.0.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.6.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.14.tgz",
"integrity": "sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==" "integrity": "sha512-SbjLmERksKOGzWzPNuW7fJM7fk3YXVTFiZWB/Hs99gwhk+/dnrQRPBQjPW9aO+fi1tAffi9PrwFvsmOKmDTyng=="
}, },
"@types/node-fetch": { "@types/node-fetch": {
"version": "2.6.2", "version": "2.5.12",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz",
"integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==",
"requires": { "requires": {
"@types/node": "*", "@types/node": "*",
"form-data": "^3.0.0" "form-data": "^3.0.0"
@@ -998,9 +989,9 @@
} }
}, },
"minimatch": { "minimatch": {
"version": "3.1.2", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@@ -1016,7 +1007,7 @@
"process": { "process": {
"version": "0.11.10", "version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
}, },
"psl": { "psl": {
"version": "1.8.0", "version": "1.8.0",
@@ -1064,11 +1055,16 @@
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
}, },
"typescript": { "typescript": {
"version": "4.8.4", "version": "3.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
"integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
"dev": true "dev": true
}, },
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
},
"uuid": { "uuid": {
"version": "3.4.0", "version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
@@ -1089,9 +1085,9 @@
} }
}, },
"xml2js": { "xml2js": {
"version": "0.5.0", "version": "0.4.23",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
"requires": { "requires": {
"sax": ">=0.6.0", "sax": ">=0.6.0",
"xmlbuilder": "~11.0.0" "xmlbuilder": "~11.0.0"
+6 -7
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/cache", "name": "@actions/cache",
"version": "3.2.1", "version": "2.0.2",
"preview": true, "preview": true,
"description": "Actions cache lib", "description": "Actions cache lib",
"keywords": [ "keywords": [
@@ -37,20 +37,19 @@
"url": "https://github.com/actions/toolkit/issues" "url": "https://github.com/actions/toolkit/issues"
}, },
"dependencies": { "dependencies": {
"@actions/core": "^1.10.0", "@actions/core": "^1.2.6",
"@actions/exec": "^1.0.1", "@actions/exec": "^1.0.1",
"@actions/glob": "^0.1.0", "@actions/glob": "^0.1.0",
"@actions/http-client": "^2.0.1", "@actions/http-client": "^1.0.9",
"@actions/io": "^1.0.1", "@actions/io": "^1.0.1",
"@azure/abort-controller": "^1.1.0",
"@azure/ms-rest-js": "^2.6.0", "@azure/ms-rest-js": "^2.6.0",
"@azure/storage-blob": "^12.13.0", "@azure/storage-blob": "^12.8.0",
"semver": "^6.1.0", "semver": "^6.1.0",
"uuid": "^3.3.3" "uuid": "^3.3.3"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^3.8.3",
"@types/semver": "^6.0.0", "@types/semver": "^6.0.0",
"@types/uuid": "^3.4.5", "@types/uuid": "^3.4.5"
"typescript": "^4.8.0"
} }
} }
+20 -56
View File
@@ -60,15 +60,13 @@ export function isFeatureAvailable(): boolean {
* @param primaryKey an explicit key for restoring the cache * @param primaryKey an explicit key for restoring the cache
* @param restoreKeys an optional ordered list of keys to use for restoring the cache if no cache hit occurred for key * @param restoreKeys an optional ordered list of keys to use for restoring the cache if no cache hit occurred for key
* @param downloadOptions cache download options * @param downloadOptions cache download options
* @param enableCrossOsArchive an optional boolean enabled to restore on windows any cache created on any platform
* @returns string returns the key for the cache hit, otherwise returns undefined * @returns string returns the key for the cache hit, otherwise returns undefined
*/ */
export async function restoreCache( export async function restoreCache(
paths: string[], paths: string[],
primaryKey: string, primaryKey: string,
restoreKeys?: string[], restoreKeys?: string[],
options?: DownloadOptions, options?: DownloadOptions
enableCrossOsArchive = false
): Promise<string | undefined> { ): Promise<string | undefined> {
checkPaths(paths) checkPaths(paths)
@@ -88,29 +86,23 @@ export async function restoreCache(
} }
const compressionMethod = await utils.getCompressionMethod() const compressionMethod = await utils.getCompressionMethod()
let archivePath = ''
// path are needed to compute version
const cacheEntry = await cacheHttpClient.getCacheEntry(keys, paths, {
compressionMethod
})
if (!cacheEntry?.archiveLocation) {
// Cache not found
return undefined
}
const archivePath = path.join(
await utils.createTempDirectory(),
utils.getCacheFileName(compressionMethod)
)
core.debug(`Archive Path: ${archivePath}`)
try { try {
// path are needed to compute version
const cacheEntry = await cacheHttpClient.getCacheEntry(keys, paths, {
compressionMethod,
enableCrossOsArchive
})
if (!cacheEntry?.archiveLocation) {
// Cache not found
return undefined
}
if (options?.lookupOnly) {
core.info('Lookup only - skipping download')
return cacheEntry.cacheKey
}
archivePath = path.join(
await utils.createTempDirectory(),
utils.getCacheFileName(compressionMethod)
)
core.debug(`Archive Path: ${archivePath}`)
// Download the cache from the cache entry // Download the cache from the cache entry
await cacheHttpClient.downloadCache( await cacheHttpClient.downloadCache(
cacheEntry.archiveLocation, cacheEntry.archiveLocation,
@@ -131,16 +123,6 @@ export async function restoreCache(
await extractTar(archivePath, compressionMethod) await extractTar(archivePath, compressionMethod)
core.info('Cache restored successfully') core.info('Cache restored successfully')
return cacheEntry.cacheKey
} catch (error) {
const typedError = error as Error
if (typedError.name === ValidationError.name) {
throw error
} else {
// Supress all non-validation cache related errors because caching should be optional
core.warning(`Failed to restore: ${(error as Error).message}`)
}
} finally { } finally {
// Try to delete the archive to save space // Try to delete the archive to save space
try { try {
@@ -150,7 +132,7 @@ export async function restoreCache(
} }
} }
return undefined return cacheEntry.cacheKey
} }
/** /**
@@ -158,32 +140,24 @@ export async function restoreCache(
* *
* @param paths a list of file paths to be cached * @param paths a list of file paths to be cached
* @param key an explicit key for restoring the cache * @param key an explicit key for restoring the cache
* @param enableCrossOsArchive an optional boolean enabled to save cache on windows which could be restored on any platform
* @param options cache upload options * @param options cache upload options
* @returns number returns cacheId if the cache was saved successfully and throws an error if save fails * @returns number returns cacheId if the cache was saved successfully and throws an error if save fails
*/ */
export async function saveCache( export async function saveCache(
paths: string[], paths: string[],
key: string, key: string,
options?: UploadOptions, options?: UploadOptions
enableCrossOsArchive = false
): Promise<number> { ): Promise<number> {
checkPaths(paths) checkPaths(paths)
checkKey(key) checkKey(key)
const compressionMethod = await utils.getCompressionMethod() const compressionMethod = await utils.getCompressionMethod()
let cacheId = -1 let cacheId = null
const cachePaths = await utils.resolvePaths(paths) const cachePaths = await utils.resolvePaths(paths)
core.debug('Cache Paths:') core.debug('Cache Paths:')
core.debug(`${JSON.stringify(cachePaths)}`) core.debug(`${JSON.stringify(cachePaths)}`)
if (cachePaths.length === 0) {
throw new Error(
`Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved.`
)
}
const archiveFolder = await utils.createTempDirectory() const archiveFolder = await utils.createTempDirectory()
const archivePath = path.join( const archivePath = path.join(
archiveFolder, archiveFolder,
@@ -216,7 +190,6 @@ export async function saveCache(
paths, paths,
{ {
compressionMethod, compressionMethod,
enableCrossOsArchive,
cacheSize: archiveFileSize cacheSize: archiveFileSize
} }
) )
@@ -238,15 +211,6 @@ export async function saveCache(
core.debug(`Saving Cache (ID: ${cacheId})`) core.debug(`Saving Cache (ID: ${cacheId})`)
await cacheHttpClient.saveCache(cacheId, archivePath, options) await cacheHttpClient.saveCache(cacheId, archivePath, options)
} catch (error) {
const typedError = error as Error
if (typedError.name === ValidationError.name) {
throw error
} else if (typedError.name === ReserveCacheError.name) {
core.info(`Failed to save: ${typedError.message}`)
} else {
core.warning(`Failed to save: ${typedError.message}`)
}
} finally { } finally {
// Try to delete the archive to save space // Try to delete the archive to save space
try { try {
+14 -65
View File
@@ -1,10 +1,7 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {HttpClient} from '@actions/http-client' import {HttpClient} from '@actions/http-client'
import {BearerCredentialHandler} from '@actions/http-client/lib/auth' import {BearerCredentialHandler} from '@actions/http-client/auth'
import { import {IRequestOptions, ITypedResponse} from '@actions/http-client/interfaces'
RequestOptions,
TypedResponse
} from '@actions/http-client/lib/interfaces'
import * as crypto from 'crypto' import * as crypto from 'crypto'
import * as fs from 'fs' import * as fs from 'fs'
import {URL} from 'url' import {URL} from 'url'
@@ -17,8 +14,7 @@ import {
CommitCacheRequest, CommitCacheRequest,
ReserveCacheRequest, ReserveCacheRequest,
ReserveCacheResponse, ReserveCacheResponse,
ITypedResponseWithError, ITypedResponseWithError
ArtifactCacheList
} from './contracts' } from './contracts'
import {downloadCacheHttpClient, downloadCacheStorageSDK} from './downloadUtils' import {downloadCacheHttpClient, downloadCacheStorageSDK} from './downloadUtils'
import { import {
@@ -50,8 +46,8 @@ function createAcceptHeader(type: string, apiVersion: string): string {
return `${type};api-version=${apiVersion}` return `${type};api-version=${apiVersion}`
} }
function getRequestOptions(): RequestOptions { function getRequestOptions(): IRequestOptions {
const requestOptions: RequestOptions = { const requestOptions: IRequestOptions = {
headers: { headers: {
Accept: createAcceptHeader('application/json', '6.0-preview.1') Accept: createAcceptHeader('application/json', '6.0-preview.1')
} }
@@ -73,21 +69,13 @@ function createHttpClient(): HttpClient {
export function getCacheVersion( export function getCacheVersion(
paths: string[], paths: string[],
compressionMethod?: CompressionMethod, compressionMethod?: CompressionMethod
enableCrossOsArchive = false
): string { ): string {
const components = paths const components = paths.concat(
!compressionMethod || compressionMethod === CompressionMethod.Gzip
// Add compression method to cache version to restore ? []
// compressed cache as per compression method : [compressionMethod]
if (compressionMethod) { )
components.push(compressionMethod)
}
// Only check for windows platforms if enableCrossOsArchive is false
if (process.platform === 'win32' && !enableCrossOsArchive) {
components.push('windows-only')
}
// Add salt to cache version to support breaking changes in cache entry // Add salt to cache version to support breaking changes in cache entry
components.push(versionSalt) components.push(versionSalt)
@@ -104,11 +92,7 @@ export async function getCacheEntry(
options?: InternalCacheOptions options?: InternalCacheOptions
): Promise<ArtifactCacheEntry | null> { ): Promise<ArtifactCacheEntry | null> {
const httpClient = createHttpClient() const httpClient = createHttpClient()
const version = getCacheVersion( const version = getCacheVersion(paths, options?.compressionMethod)
paths,
options?.compressionMethod,
options?.enableCrossOsArchive
)
const resource = `cache?keys=${encodeURIComponent( const resource = `cache?keys=${encodeURIComponent(
keys.join(',') keys.join(',')
)}&version=${version}` )}&version=${version}`
@@ -116,12 +100,7 @@ export async function getCacheEntry(
const response = await retryTypedResponse('getCacheEntry', async () => const response = await retryTypedResponse('getCacheEntry', async () =>
httpClient.getJson<ArtifactCacheEntry>(getCacheApiUrl(resource)) httpClient.getJson<ArtifactCacheEntry>(getCacheApiUrl(resource))
) )
// Cache not found
if (response.statusCode === 204) { if (response.statusCode === 204) {
// List cache for primary key only if cache miss occurs
if (core.isDebug()) {
await printCachesListForDiagnostics(keys[0], httpClient, version)
}
return null return null
} }
if (!isSuccessStatusCode(response.statusCode)) { if (!isSuccessStatusCode(response.statusCode)) {
@@ -131,7 +110,6 @@ export async function getCacheEntry(
const cacheResult = response.result const cacheResult = response.result
const cacheDownloadUrl = cacheResult?.archiveLocation const cacheDownloadUrl = cacheResult?.archiveLocation
if (!cacheDownloadUrl) { if (!cacheDownloadUrl) {
// Cache achiveLocation not found. This should never happen, and hence bail out.
throw new Error('Cache not found.') throw new Error('Cache not found.')
} }
core.setSecret(cacheDownloadUrl) core.setSecret(cacheDownloadUrl)
@@ -141,31 +119,6 @@ export async function getCacheEntry(
return cacheResult return cacheResult
} }
async function printCachesListForDiagnostics(
key: string,
httpClient: HttpClient,
version: string
): Promise<void> {
const resource = `caches?key=${encodeURIComponent(key)}`
const response = await retryTypedResponse('listCache', async () =>
httpClient.getJson<ArtifactCacheList>(getCacheApiUrl(resource))
)
if (response.statusCode === 200) {
const cacheListResult = response.result
const totalCount = cacheListResult?.totalCount
if (totalCount && totalCount > 0) {
core.debug(
`No matching cache found for cache key '${key}', version '${version} and scope ${process.env['GITHUB_REF']}. There exist one or more cache(s) with similar key but they have different version or scope. See more info on cache matching here: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#matching-a-cache-key \nOther caches with similar key:`
)
for (const cacheEntry of cacheListResult?.artifactCaches || []) {
core.debug(
`Cache Key: ${cacheEntry?.cacheKey}, Cache Version: ${cacheEntry?.cacheVersion}, Cache Scope: ${cacheEntry?.scope}, Cache Created: ${cacheEntry?.creationTime}`
)
}
}
}
}
export async function downloadCache( export async function downloadCache(
archiveLocation: string, archiveLocation: string,
archivePath: string, archivePath: string,
@@ -193,11 +146,7 @@ export async function reserveCache(
options?: InternalCacheOptions options?: InternalCacheOptions
): Promise<ITypedResponseWithError<ReserveCacheResponse>> { ): Promise<ITypedResponseWithError<ReserveCacheResponse>> {
const httpClient = createHttpClient() const httpClient = createHttpClient()
const version = getCacheVersion( const version = getCacheVersion(paths, options?.compressionMethod)
paths,
options?.compressionMethod,
options?.enableCrossOsArchive
)
const reserveCacheRequest: ReserveCacheRequest = { const reserveCacheRequest: ReserveCacheRequest = {
key, key,
@@ -326,7 +275,7 @@ async function commitCache(
httpClient: HttpClient, httpClient: HttpClient,
cacheId: number, cacheId: number,
filesize: number filesize: number
): Promise<TypedResponse<null>> { ): Promise<ITypedResponse<null>> {
const commitCacheRequest: CommitCacheRequest = {size: filesize} const commitCacheRequest: CommitCacheRequest = {size: filesize}
return await retryTypedResponse('commitCache', async () => return await retryTypedResponse('commitCache', async () =>
httpClient.postJson<null>( httpClient.postJson<null>(
+22 -29
View File
@@ -7,11 +7,7 @@ import * as path from 'path'
import * as semver from 'semver' import * as semver from 'semver'
import * as util from 'util' import * as util from 'util'
import {v4 as uuidV4} from 'uuid' import {v4 as uuidV4} from 'uuid'
import { import {CacheFilename, CompressionMethod} from './constants'
CacheFilename,
CompressionMethod,
GnuTarPathOnWindows
} from './constants'
// From https://github.com/actions/toolkit/blob/main/packages/tool-cache/src/tool-cache.ts#L23 // From https://github.com/actions/toolkit/blob/main/packages/tool-cache/src/tool-cache.ts#L23
export async function createTempDirectory(): Promise<string> { export async function createTempDirectory(): Promise<string> {
@@ -56,12 +52,7 @@ export async function resolvePaths(patterns: string[]): Promise<string[]> {
.replace(new RegExp(`\\${path.sep}`, 'g'), '/') .replace(new RegExp(`\\${path.sep}`, 'g'), '/')
core.debug(`Matched: ${relativeFile}`) core.debug(`Matched: ${relativeFile}`)
// Paths are made relative so the tar entries are all relative to the root of the workspace. // Paths are made relative so the tar entries are all relative to the root of the workspace.
if (relativeFile === '') { paths.push(`${relativeFile}`)
// path.relative returns empty string if workspace and file are equal
paths.push('.')
} else {
paths.push(`${relativeFile}`)
}
} }
return paths return paths
@@ -71,15 +62,11 @@ export async function unlinkFile(filePath: fs.PathLike): Promise<void> {
return util.promisify(fs.unlink)(filePath) return util.promisify(fs.unlink)(filePath)
} }
async function getVersion( async function getVersion(app: string): Promise<string> {
app: string, core.debug(`Checking ${app} --version`)
additionalArgs: string[] = []
): Promise<string> {
let versionOutput = '' let versionOutput = ''
additionalArgs.push('--version')
core.debug(`Checking ${app} ${additionalArgs.join(' ')}`)
try { try {
await exec.exec(`${app}`, additionalArgs, { await exec.exec(`${app} --version`, [], {
ignoreReturnCode: true, ignoreReturnCode: true,
silent: true, silent: true,
listeners: { listeners: {
@@ -98,14 +85,23 @@ async function getVersion(
// Use zstandard if possible to maximize cache performance // Use zstandard if possible to maximize cache performance
export async function getCompressionMethod(): Promise<CompressionMethod> { export async function getCompressionMethod(): Promise<CompressionMethod> {
const versionOutput = await getVersion('zstd', ['--quiet']) if (process.platform === 'win32' && !(await isGnuTarInstalled())) {
const version = semver.clean(versionOutput) // Disable zstd due to bug https://github.com/actions/cache/issues/301
core.debug(`zstd version: ${version}`)
if (versionOutput === '') {
return CompressionMethod.Gzip return CompressionMethod.Gzip
} else { }
const versionOutput = await getVersion('zstd')
const version = semver.clean(versionOutput)
if (!versionOutput.toLowerCase().includes('zstd command line interface')) {
// zstd is not installed
return CompressionMethod.Gzip
} else if (!version || semver.lt(version, 'v1.3.2')) {
// zstd is installed but using a version earlier than v1.3.2
// v1.3.2 is required to use the `--long` options in zstd
return CompressionMethod.ZstdWithoutLong return CompressionMethod.ZstdWithoutLong
} else {
return CompressionMethod.Zstd
} }
} }
@@ -115,12 +111,9 @@ export function getCacheFileName(compressionMethod: CompressionMethod): string {
: CacheFilename.Zstd : CacheFilename.Zstd
} }
export async function getGnuTarPathOnWindows(): Promise<string> { export async function isGnuTarInstalled(): Promise<boolean> {
if (fs.existsSync(GnuTarPathOnWindows)) {
return GnuTarPathOnWindows
}
const versionOutput = await getVersion('tar') const versionOutput = await getVersion('tar')
return versionOutput.toLowerCase().includes('gnu tar') ? io.which('tar') : '' return versionOutput.toLowerCase().includes('gnu tar')
} }
export function assertDefined<T>(name: string, value?: T): T { export function assertDefined<T>(name: string, value?: T): T {
-15
View File
@@ -11,11 +11,6 @@ export enum CompressionMethod {
Zstd = 'zstd' Zstd = 'zstd'
} }
export enum ArchiveToolType {
GNU = 'gnu',
BSD = 'bsd'
}
// The default number of retry attempts. // The default number of retry attempts.
export const DefaultRetryAttempts = 2 export const DefaultRetryAttempts = 2
@@ -26,13 +21,3 @@ export const DefaultRetryDelay = 5000
// over the socket during this period, the socket is destroyed and the download // over the socket during this period, the socket is destroyed and the download
// is aborted. // is aborted.
export const SocketTimeout = 5000 export const SocketTimeout = 5000
// The default path of GNUtar on hosted Windows runners
export const GnuTarPathOnWindows = `${process.env['PROGRAMFILES']}\\Git\\usr\\bin\\tar.exe`
// The default path of BSDtar on hosted Windows runners
export const SystemTarPathOnWindows = `${process.env['SYSTEMDRIVE']}\\Windows\\System32\\tar.exe`
export const TarFilename = 'cache.tar'
export const ManifestFilename = 'manifest.txt'
+2 -14
View File
@@ -1,24 +1,18 @@
import {CompressionMethod} from './constants' import {CompressionMethod} from './constants'
import {TypedResponse} from '@actions/http-client/lib/interfaces' import {ITypedResponse} from '@actions/http-client/interfaces'
import {HttpClientError} from '@actions/http-client' import {HttpClientError} from '@actions/http-client'
export interface ITypedResponseWithError<T> extends TypedResponse<T> { export interface ITypedResponseWithError<T> extends ITypedResponse<T> {
error?: HttpClientError error?: HttpClientError
} }
export interface ArtifactCacheEntry { export interface ArtifactCacheEntry {
cacheKey?: string cacheKey?: string
scope?: string scope?: string
cacheVersion?: string
creationTime?: string creationTime?: string
archiveLocation?: string archiveLocation?: string
} }
export interface ArtifactCacheList {
totalCount: number
artifactCaches?: ArtifactCacheEntry[]
}
export interface CommitCacheRequest { export interface CommitCacheRequest {
size: number size: number
} }
@@ -35,11 +29,5 @@ export interface ReserveCacheResponse {
export interface InternalCacheOptions { export interface InternalCacheOptions {
compressionMethod?: CompressionMethod compressionMethod?: CompressionMethod
enableCrossOsArchive?: boolean
cacheSize?: number cacheSize?: number
} }
export interface ArchiveTool {
path: string
type: string
}
+13 -37
View File
@@ -1,5 +1,6 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {HttpClient, HttpClientResponse} from '@actions/http-client' import {HttpClient} from '@actions/http-client'
import {IHttpClientResponse} from '@actions/http-client/interfaces'
import {BlockBlobClient} from '@azure/storage-blob' import {BlockBlobClient} from '@azure/storage-blob'
import {TransferProgressEvent} from '@azure/ms-rest-js' import {TransferProgressEvent} from '@azure/ms-rest-js'
import * as buffer from 'buffer' import * as buffer from 'buffer'
@@ -12,8 +13,6 @@ import {SocketTimeout} from './constants'
import {DownloadOptions} from '../options' import {DownloadOptions} from '../options'
import {retryHttpClientResponse} from './requestUtils' import {retryHttpClientResponse} from './requestUtils'
import {AbortController} from '@azure/abort-controller'
/** /**
* Pipes the body of a HTTP response to a stream * Pipes the body of a HTTP response to a stream
* *
@@ -21,7 +20,7 @@ import {AbortController} from '@azure/abort-controller'
* @param output the writable stream * @param output the writable stream
*/ */
async function pipeResponseToStream( async function pipeResponseToStream(
response: HttpClientResponse, response: IHttpClientResponse,
output: NodeJS.WritableStream output: NodeJS.WritableStream
): Promise<void> { ): Promise<void> {
const pipeline = util.promisify(stream.pipeline) const pipeline = util.promisify(stream.pipeline)
@@ -242,17 +241,14 @@ export async function downloadCacheStorageSDK(
// If the file exceeds the buffer maximum length (~1 GB on 32-bit systems and ~2 GB // 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 // 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. // ~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)
// Updated segment size to 128MB = 134217728 bytes, to complete a segment faster and fail fast
const maxSegmentSize = Math.min(134217728, buffer.constants.MAX_LENGTH)
const downloadProgress = new DownloadProgress(contentLength) const downloadProgress = new DownloadProgress(contentLength)
const fd = fs.openSync(archivePath, 'w') const fd = fs.openSync(archivePath, 'w')
try { try {
downloadProgress.startDisplayTimer() downloadProgress.startDisplayTimer()
const controller = new AbortController()
const abortSignal = controller.signal
while (!downloadProgress.isDone()) { while (!downloadProgress.isDone()) {
const segmentStart = const segmentStart =
downloadProgress.segmentOffset + downloadProgress.segmentSize downloadProgress.segmentOffset + downloadProgress.segmentSize
@@ -263,22 +259,17 @@ export async function downloadCacheStorageSDK(
) )
downloadProgress.nextSegment(segmentSize) downloadProgress.nextSegment(segmentSize)
const result = await promiseWithTimeout(
options.segmentTimeoutInMs || 3600000, const result = await client.downloadToBuffer(
client.downloadToBuffer(segmentStart, segmentSize, { segmentStart,
abortSignal, segmentSize,
{
concurrency: options.downloadConcurrency, concurrency: options.downloadConcurrency,
onProgress: downloadProgress.onProgress() onProgress: downloadProgress.onProgress()
}) }
) )
if (result === 'timeout') {
controller.abort() fs.writeFileSync(fd, result)
throw new Error(
'Aborting cache download as the download time exceeded the timeout.'
)
} else if (Buffer.isBuffer(result)) {
fs.writeFileSync(fd, result)
}
} }
} finally { } finally {
downloadProgress.stopDisplayTimer() downloadProgress.stopDisplayTimer()
@@ -286,18 +277,3 @@ export async function downloadCacheStorageSDK(
} }
} }
} }
const promiseWithTimeout = async (
timeoutMs: number,
promise: Promise<Buffer>
): Promise<unknown> => {
let timeoutHandle: NodeJS.Timeout
const timeoutPromise = new Promise(resolve => {
timeoutHandle = setTimeout(() => resolve('timeout'), timeoutMs)
})
return Promise.race([promise, timeoutPromise]).then(result => {
clearTimeout(timeoutHandle)
return result
})
}
+6 -9
View File
@@ -1,9 +1,6 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import { import {HttpCodes, HttpClientError} from '@actions/http-client'
HttpCodes, import {IHttpClientResponse} from '@actions/http-client/interfaces'
HttpClientError,
HttpClientResponse
} from '@actions/http-client'
import {DefaultRetryDelay, DefaultRetryAttempts} from './constants' import {DefaultRetryDelay, DefaultRetryAttempts} from './constants'
import {ITypedResponseWithError} from './contracts' import {ITypedResponseWithError} from './contracts'
@@ -106,7 +103,7 @@ export async function retryTypedResponse<T>(
maxAttempts, maxAttempts,
delay, delay,
// If the error object contains the statusCode property, extract it and return // If the error object contains the statusCode property, extract it and return
// an TypedResponse<T> so it can be processed by the retry logic. // an ITypedResponse<T> so it can be processed by the retry logic.
(error: Error) => { (error: Error) => {
if (error instanceof HttpClientError) { if (error instanceof HttpClientError) {
return { return {
@@ -124,14 +121,14 @@ export async function retryTypedResponse<T>(
export async function retryHttpClientResponse( export async function retryHttpClientResponse(
name: string, name: string,
method: () => Promise<HttpClientResponse>, method: () => Promise<IHttpClientResponse>,
maxAttempts = DefaultRetryAttempts, maxAttempts = DefaultRetryAttempts,
delay = DefaultRetryDelay delay = DefaultRetryDelay
): Promise<HttpClientResponse> { ): Promise<IHttpClientResponse> {
return await retry( return await retry(
name, name,
method, method,
(response: HttpClientResponse) => response.message.statusCode, (response: IHttpClientResponse) => response.message.statusCode,
maxAttempts, maxAttempts,
delay delay
) )
+105 -247
View File
@@ -3,28 +3,23 @@ import * as io from '@actions/io'
import {existsSync, writeFileSync} from 'fs' import {existsSync, writeFileSync} from 'fs'
import * as path from 'path' import * as path from 'path'
import * as utils from './cacheUtils' import * as utils from './cacheUtils'
import {ArchiveTool} from './contracts' import {CompressionMethod} from './constants'
import {
CompressionMethod,
SystemTarPathOnWindows,
ArchiveToolType,
TarFilename,
ManifestFilename
} from './constants'
const IS_WINDOWS = process.platform === 'win32' async function getTarPath(
args: string[],
// Returns tar path and type: BSD or GNU compressionMethod: CompressionMethod
async function getTarPath(): Promise<ArchiveTool> { ): Promise<string> {
switch (process.platform) { switch (process.platform) {
case 'win32': { case 'win32': {
const gnuTar = await utils.getGnuTarPathOnWindows() const systemTar = `${process.env['windir']}\\System32\\tar.exe`
const systemTar = SystemTarPathOnWindows if (compressionMethod !== CompressionMethod.Gzip) {
if (gnuTar) { // We only use zstandard compression on windows when gnu tar is installed due to
// Use GNUtar as default on windows // a bug with compressing large files with bsdtar + zstd
return <ArchiveTool>{path: gnuTar, type: ArchiveToolType.GNU} args.push('--force-local')
} else if (existsSync(systemTar)) { } else if (existsSync(systemTar)) {
return <ArchiveTool>{path: systemTar, type: ArchiveToolType.BSD} return systemTar
} else if (await utils.isGnuTarInstalled()) {
args.push('--force-local')
} }
break break
} }
@@ -32,244 +27,33 @@ async function getTarPath(): Promise<ArchiveTool> {
const gnuTar = await io.which('gtar', false) const gnuTar = await io.which('gtar', false)
if (gnuTar) { if (gnuTar) {
// fix permission denied errors when extracting BSD tar archive with GNU tar - https://github.com/actions/cache/issues/527 // fix permission denied errors when extracting BSD tar archive with GNU tar - https://github.com/actions/cache/issues/527
return <ArchiveTool>{path: gnuTar, type: ArchiveToolType.GNU} args.push('--delay-directory-restore')
} else { return gnuTar
return <ArchiveTool>{
path: await io.which('tar', true),
type: ArchiveToolType.BSD
}
} }
break
} }
default: default:
break break
} }
// Default assumption is GNU tar is present in path return await io.which('tar', true)
return <ArchiveTool>{
path: await io.which('tar', true),
type: ArchiveToolType.GNU
}
} }
// Return arguments for tar as per tarPath, compressionMethod, method type and os async function execTar(
async function getTarArgs( args: string[],
tarPath: ArchiveTool,
compressionMethod: CompressionMethod, compressionMethod: CompressionMethod,
type: string, cwd?: string
archivePath = '' ): Promise<void> {
): Promise<string[]> { try {
const args = [`"${tarPath.path}"`] await exec(`"${await getTarPath(args, compressionMethod)}"`, args, {cwd})
const cacheFileName = utils.getCacheFileName(compressionMethod) } catch (error) {
const tarFile = 'cache.tar' throw new Error(`Tar failed with error: ${error?.message}`)
const workingDirectory = getWorkingDirectory()
// Speficic args for BSD tar on windows for workaround
const BSD_TAR_ZSTD =
tarPath.type === ArchiveToolType.BSD &&
compressionMethod !== CompressionMethod.Gzip &&
IS_WINDOWS
// Method specific args
switch (type) {
case 'create':
args.push(
'--posix',
'-cf',
BSD_TAR_ZSTD
? tarFile
: cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
'--exclude',
BSD_TAR_ZSTD
? tarFile
: cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
'-P',
'-C',
workingDirectory.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
'--files-from',
ManifestFilename
)
break
case 'extract':
args.push(
'-xf',
BSD_TAR_ZSTD
? tarFile
: archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
'-P',
'-C',
workingDirectory.replace(new RegExp(`\\${path.sep}`, 'g'), '/')
)
break
case 'list':
args.push(
'-tf',
BSD_TAR_ZSTD
? tarFile
: archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
'-P'
)
break
} }
// Platform specific args
if (tarPath.type === ArchiveToolType.GNU) {
switch (process.platform) {
case 'win32':
args.push('--force-local')
break
case 'darwin':
args.push('--delay-directory-restore')
break
}
}
return args
}
// Returns commands to run tar and compression program
async function getCommands(
compressionMethod: CompressionMethod,
type: string,
archivePath = ''
): Promise<string[]> {
let args
const tarPath = await getTarPath()
const tarArgs = await getTarArgs(
tarPath,
compressionMethod,
type,
archivePath
)
const compressionArgs =
type !== 'create'
? await getDecompressionProgram(tarPath, compressionMethod, archivePath)
: await getCompressionProgram(tarPath, compressionMethod)
const BSD_TAR_ZSTD =
tarPath.type === ArchiveToolType.BSD &&
compressionMethod !== CompressionMethod.Gzip &&
IS_WINDOWS
if (BSD_TAR_ZSTD && type !== 'create') {
args = [[...compressionArgs].join(' '), [...tarArgs].join(' ')]
} else {
args = [[...tarArgs].join(' '), [...compressionArgs].join(' ')]
}
if (BSD_TAR_ZSTD) {
return args
}
return [args.join(' ')]
} }
function getWorkingDirectory(): string { function getWorkingDirectory(): string {
return process.env['GITHUB_WORKSPACE'] ?? process.cwd() return process.env['GITHUB_WORKSPACE'] ?? process.cwd()
} }
// Common function for extractTar and listTar to get the compression method
async function getDecompressionProgram(
tarPath: ArchiveTool,
compressionMethod: CompressionMethod,
archivePath: string
): Promise<string[]> {
// -d: Decompress.
// unzstd is equivalent to 'zstd -d'
// --long=#: Enables long distance matching with # bits. Maximum is 30 (1GB) on 32-bit OS and 31 (2GB) on 64-bit.
// Using 30 here because we also support 32-bit self-hosted runners.
const BSD_TAR_ZSTD =
tarPath.type === ArchiveToolType.BSD &&
compressionMethod !== CompressionMethod.Gzip &&
IS_WINDOWS
switch (compressionMethod) {
case CompressionMethod.Zstd:
return BSD_TAR_ZSTD
? [
'zstd -d --long=30 --force -o',
TarFilename,
archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/')
]
: [
'--use-compress-program',
IS_WINDOWS ? '"zstd -d --long=30"' : 'unzstd --long=30'
]
case CompressionMethod.ZstdWithoutLong:
return BSD_TAR_ZSTD
? [
'zstd -d --force -o',
TarFilename,
archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/')
]
: ['--use-compress-program', IS_WINDOWS ? '"zstd -d"' : 'unzstd']
default:
return ['-z']
}
}
// Used for creating the archive
// -T#: Compress using # working thread. If # is 0, attempt to detect and use the number of physical CPU cores.
// zstdmt is equivalent to 'zstd -T0'
// --long=#: Enables long distance matching with # bits. Maximum is 30 (1GB) on 32-bit OS and 31 (2GB) on 64-bit.
// Using 30 here because we also support 32-bit self-hosted runners.
// Long range mode is added to zstd in v1.3.2 release, so we will not use --long in older version of zstd.
async function getCompressionProgram(
tarPath: ArchiveTool,
compressionMethod: CompressionMethod
): Promise<string[]> {
const cacheFileName = utils.getCacheFileName(compressionMethod)
const BSD_TAR_ZSTD =
tarPath.type === ArchiveToolType.BSD &&
compressionMethod !== CompressionMethod.Gzip &&
IS_WINDOWS
switch (compressionMethod) {
case CompressionMethod.Zstd:
return BSD_TAR_ZSTD
? [
'zstd -T0 --long=30 --force -o',
cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
TarFilename
]
: [
'--use-compress-program',
IS_WINDOWS ? '"zstd -T0 --long=30"' : 'zstdmt --long=30'
]
case CompressionMethod.ZstdWithoutLong:
return BSD_TAR_ZSTD
? [
'zstd -T0 --force -o',
cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
TarFilename
]
: ['--use-compress-program', IS_WINDOWS ? '"zstd -T0"' : 'zstdmt']
default:
return ['-z']
}
}
// Executes all commands as separate processes
async function execCommands(commands: string[], cwd?: string): Promise<void> {
for (const command of commands) {
try {
await exec(command, undefined, {
cwd,
env: {...(process.env as object), MSYS: 'winsymlinks:nativestrict'}
})
} catch (error) {
throw new Error(
`${command.split(' ')[0]} failed with error: ${error?.message}`
)
}
}
}
// List the contents of a tar
export async function listTar(
archivePath: string,
compressionMethod: CompressionMethod
): Promise<void> {
const commands = await getCommands(compressionMethod, 'list', archivePath)
await execCommands(commands)
}
// Extract a tar
export async function extractTar( export async function extractTar(
archivePath: string, archivePath: string,
compressionMethod: CompressionMethod compressionMethod: CompressionMethod
@@ -277,21 +61,95 @@ export async function extractTar(
// Create directory to extract tar into // Create directory to extract tar into
const workingDirectory = getWorkingDirectory() const workingDirectory = getWorkingDirectory()
await io.mkdirP(workingDirectory) await io.mkdirP(workingDirectory)
const commands = await getCommands(compressionMethod, 'extract', archivePath) // --d: Decompress.
await execCommands(commands) // --long=#: Enables long distance matching with # bits. Maximum is 30 (1GB) on 32-bit OS and 31 (2GB) on 64-bit.
// Using 30 here because we also support 32-bit self-hosted runners.
function getCompressionProgram(): string[] {
switch (compressionMethod) {
case CompressionMethod.Zstd:
return ['--use-compress-program', 'zstd -d --long=30']
case CompressionMethod.ZstdWithoutLong:
return ['--use-compress-program', 'zstd -d']
default:
return ['-z']
}
}
const args = [
...getCompressionProgram(),
'-xf',
archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
'-P',
'-C',
workingDirectory.replace(new RegExp(`\\${path.sep}`, 'g'), '/')
]
await execTar(args, compressionMethod)
} }
// Create a tar
export async function createTar( export async function createTar(
archiveFolder: string, archiveFolder: string,
sourceDirectories: string[], sourceDirectories: string[],
compressionMethod: CompressionMethod compressionMethod: CompressionMethod
): Promise<void> { ): Promise<void> {
// Write source directories to manifest.txt to avoid command length limits // Write source directories to manifest.txt to avoid command length limits
const manifestFilename = 'manifest.txt'
const cacheFileName = utils.getCacheFileName(compressionMethod)
writeFileSync( writeFileSync(
path.join(archiveFolder, ManifestFilename), path.join(archiveFolder, manifestFilename),
sourceDirectories.join('\n') sourceDirectories.join('\n')
) )
const commands = await getCommands(compressionMethod, 'create') const workingDirectory = getWorkingDirectory()
await execCommands(commands, archiveFolder)
// -T#: Compress using # working thread. If # is 0, attempt to detect and use the number of physical CPU cores.
// --long=#: Enables long distance matching with # bits. Maximum is 30 (1GB) on 32-bit OS and 31 (2GB) on 64-bit.
// Using 30 here because we also support 32-bit self-hosted runners.
// Long range mode is added to zstd in v1.3.2 release, so we will not use --long in older version of zstd.
function getCompressionProgram(): string[] {
switch (compressionMethod) {
case CompressionMethod.Zstd:
return ['--use-compress-program', 'zstd -T0 --long=30']
case CompressionMethod.ZstdWithoutLong:
return ['--use-compress-program', 'zstd -T0']
default:
return ['-z']
}
}
const args = [
'--posix',
...getCompressionProgram(),
'-cf',
cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
'-P',
'-C',
workingDirectory.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
'--files-from',
manifestFilename
]
await execTar(args, compressionMethod, archiveFolder)
}
export async function listTar(
archivePath: string,
compressionMethod: CompressionMethod
): Promise<void> {
// --d: Decompress.
// --long=#: Enables long distance matching with # bits.
// Maximum is 30 (1GB) on 32-bit OS and 31 (2GB) on 64-bit.
// Using 30 here because we also support 32-bit self-hosted runners.
function getCompressionProgram(): string[] {
switch (compressionMethod) {
case CompressionMethod.Zstd:
return ['--use-compress-program', 'zstd -d --long=30']
case CompressionMethod.ZstdWithoutLong:
return ['--use-compress-program', 'zstd -d']
default:
return ['-z']
}
}
const args = [
...getCompressionProgram(),
'-tf',
archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
'-P'
]
await execTar(args, compressionMethod)
} }
+1 -41
View File
@@ -46,22 +46,6 @@ export interface DownloadOptions {
* @default 30000 * @default 30000
*/ */
timeoutInMs?: number timeoutInMs?: number
/**
* Time after which a segment download should be aborted if stuck
*
* @default 3600000
*/
segmentTimeoutInMs?: number
/**
* Weather to skip downloading the cache entry.
* If lookupOnly is set to true, the restore function will only check if
* a matching cache entry exists and return the cache key if it does.
*
* @default false
*/
lookupOnly?: boolean
} }
/** /**
@@ -100,9 +84,7 @@ export function getDownloadOptions(copy?: DownloadOptions): DownloadOptions {
const result: DownloadOptions = { const result: DownloadOptions = {
useAzureSdk: true, useAzureSdk: true,
downloadConcurrency: 8, downloadConcurrency: 8,
timeoutInMs: 30000, timeoutInMs: 30000
segmentTimeoutInMs: 600000,
lookupOnly: false
} }
if (copy) { if (copy) {
@@ -117,33 +99,11 @@ export function getDownloadOptions(copy?: DownloadOptions): DownloadOptions {
if (typeof copy.timeoutInMs === 'number') { if (typeof copy.timeoutInMs === 'number') {
result.timeoutInMs = copy.timeoutInMs result.timeoutInMs = copy.timeoutInMs
} }
if (typeof copy.segmentTimeoutInMs === 'number') {
result.segmentTimeoutInMs = copy.segmentTimeoutInMs
}
if (typeof copy.lookupOnly === 'boolean') {
result.lookupOnly = copy.lookupOnly
}
} }
const segmentDownloadTimeoutMins =
process.env['SEGMENT_DOWNLOAD_TIMEOUT_MINS']
if (
segmentDownloadTimeoutMins &&
!isNaN(Number(segmentDownloadTimeoutMins)) &&
isFinite(Number(segmentDownloadTimeoutMins))
) {
result.segmentTimeoutInMs = Number(segmentDownloadTimeoutMins) * 60 * 1000
}
core.debug(`Use Azure SDK: ${result.useAzureSdk}`) core.debug(`Use Azure SDK: ${result.useAzureSdk}`)
core.debug(`Download concurrency: ${result.downloadConcurrency}`) core.debug(`Download concurrency: ${result.downloadConcurrency}`)
core.debug(`Request timeout (ms): ${result.timeoutInMs}`) core.debug(`Request timeout (ms): ${result.timeoutInMs}`)
core.debug(
`Cache segment download timeout mins env var: ${process.env['SEGMENT_DOWNLOAD_TIMEOUT_MINS']}`
)
core.debug(`Segment download timeout (ms): ${result.segmentTimeoutInMs}`)
core.debug(`Lookup only: ${result.lookupOnly}`)
return result return result
} }
+1 -2
View File
@@ -4,8 +4,7 @@
"baseUrl": "./", "baseUrl": "./",
"outDir": "./lib", "outDir": "./lib",
"rootDir": "./src", "rootDir": "./src",
"lib": ["es6", "dom"], "lib": ["es6", "dom"]
"useUnknownInCatchVariables": false
}, },
"include": [ "include": [
"./src" "./src"
+3 -26
View File
@@ -121,7 +121,7 @@ const result = await core.group('Do something async', async () => {
This library has 3 methods that will produce [annotations](https://docs.github.com/en/rest/reference/checks#create-a-check-run). This library has 3 methods that will produce [annotations](https://docs.github.com/en/rest/reference/checks#create-a-check-run).
```js ```js
core.error('This is a bad error, action may still succeed though.') 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.warning('Something went wrong, but it\'s not bad enough to fail the build.')
@@ -163,7 +163,7 @@ export interface AnnotationProperties {
startColumn?: number startColumn?: number
/** /**
* The end column for the annotation. Cannot be sent when `startLine` and `endLine` are different values. * The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
* Defaults to `startColumn` when `startColumn` is provided. * Defaults to `startColumn` when `startColumn` is provided.
*/ */
endColumn?: number endColumn?: number
@@ -309,27 +309,4 @@ outputs:
runs: runs:
using: 'node12' using: 'node12'
main: 'dist/index.js' main: 'dist/index.js'
``` ```
#### Filesystem path helpers
You can use these methods to manipulate file paths across operating systems.
The `toPosixPath` function converts input paths to Posix-style (Linux) paths.
The `toWin32Path` function converts input paths to Windows-style paths. These
functions work independently of the underlying runner operating system.
```js
toPosixPath('\\foo\\bar') // => /foo/bar
toWin32Path('/foo/bar') // => \foo\bar
```
The `toPlatformPath` function converts input paths to the expected value on the runner's operating system.
```js
// On a Windows runner.
toPlatformPath('/foo/bar') // => \foo\bar
// On a Linux runner.
toPlatformPath('\\foo\\bar') // => /foo/bar
```
-21
View File
@@ -1,26 +1,5 @@
# @actions/core Releases # @actions/core Releases
### 1.10.0
- `saveState` and `setOutput` now use environment files if available [#1178](https://github.com/actions/toolkit/pull/1178)
- `getMultilineInput` now correctly trims whitespace by default [#1185](https://github.com/actions/toolkit/pull/1185)
### 1.9.1
- Randomize delimiter when calling `core.exportVariable`
### 1.9.0
- Added `toPosixPath`, `toWin32Path` and `toPlatformPath` utilities [#1102](https://github.com/actions/toolkit/pull/1102)
### 1.8.2
- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
### 1.8.1
- Update to v2.0.0 of `@actions/http-client`
### 1.8.0
- Deprecate `markdownSummary` extension export in favor of `summary`
- https://github.com/actions/toolkit/pull/1072
- https://github.com/actions/toolkit/pull/1073
### 1.7.0 ### 1.7.0
- [Added `markdownSummary` extension](https://github.com/actions/toolkit/pull/1014) - [Added `markdownSummary` extension](https://github.com/actions/toolkit/pull/1014)
+20 -207
View File
@@ -4,9 +4,6 @@ import * as path from 'path'
import * as core from '../src/core' import * as core from '../src/core'
import {HttpClient} from '@actions/http-client' import {HttpClient} from '@actions/http-client'
import {toCommandProperties} from '../src/utils' import {toCommandProperties} from '../src/utils'
import * as uuid from 'uuid'
jest.mock('uuid')
/* eslint-disable @typescript-eslint/unbound-method */ /* eslint-disable @typescript-eslint/unbound-method */
@@ -33,22 +30,17 @@ const testEnvVars = {
INPUT_BOOLEAN_INPUT_FALSE3: 'FALSE', INPUT_BOOLEAN_INPUT_FALSE3: 'FALSE',
INPUT_WRONG_BOOLEAN_INPUT: 'wrong', INPUT_WRONG_BOOLEAN_INPUT: 'wrong',
INPUT_WITH_TRAILING_WHITESPACE: ' some val ', INPUT_WITH_TRAILING_WHITESPACE: ' some val ',
INPUT_MY_INPUT_LIST: 'val1\nval2\nval3', INPUT_MY_INPUT_LIST: 'val1\nval2\nval3',
INPUT_LIST_WITH_TRAILING_WHITESPACE: ' val1 \n val2 \n ',
// Save inputs // Save inputs
STATE_TEST_1: 'state_val', STATE_TEST_1: 'state_val',
// File Commands // File Commands
GITHUB_PATH: '', GITHUB_PATH: '',
GITHUB_ENV: '', GITHUB_ENV: ''
GITHUB_OUTPUT: '',
GITHUB_STATE: ''
} }
const UUID = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
const DELIMITER = `ghadelimiter_${UUID}`
describe('@actions/core', () => { describe('@actions/core', () => {
beforeAll(() => { beforeAll(() => {
const filePath = path.join(__dirname, `test`) const filePath = path.join(__dirname, `test`)
@@ -62,14 +54,6 @@ describe('@actions/core', () => {
process.env[key] = testEnvVars[key as keyof typeof testEnvVars] process.env[key] = testEnvVars[key as keyof typeof testEnvVars]
} }
process.stdout.write = jest.fn() process.stdout.write = jest.fn()
jest.spyOn(uuid, 'v4').mockImplementation(() => {
return UUID
})
})
afterEach(() => {
jest.restoreAllMocks()
}) })
it('legacy exportVariable produces the correct command and sets the env', () => { it('legacy exportVariable produces the correct command and sets the env', () => {
@@ -107,7 +91,7 @@ describe('@actions/core', () => {
core.exportVariable('my var', 'var val') core.exportVariable('my var', 'var val')
verifyFileCommand( verifyFileCommand(
command, command,
`my var<<${DELIMITER}${os.EOL}var val${os.EOL}${DELIMITER}${os.EOL}` `my var<<_GitHubActionsFileCommandDelimeter_${os.EOL}var val${os.EOL}_GitHubActionsFileCommandDelimeter_${os.EOL}`
) )
}) })
@@ -117,7 +101,7 @@ describe('@actions/core', () => {
core.exportVariable('my var', true) core.exportVariable('my var', true)
verifyFileCommand( verifyFileCommand(
command, command,
`my var<<${DELIMITER}${os.EOL}true${os.EOL}${DELIMITER}${os.EOL}` `my var<<_GitHubActionsFileCommandDelimeter_${os.EOL}true${os.EOL}_GitHubActionsFileCommandDelimeter_${os.EOL}`
) )
}) })
@@ -127,45 +111,13 @@ describe('@actions/core', () => {
core.exportVariable('my var', 5) core.exportVariable('my var', 5)
verifyFileCommand( verifyFileCommand(
command, command,
`my var<<${DELIMITER}${os.EOL}5${os.EOL}${DELIMITER}${os.EOL}` `my var<<_GitHubActionsFileCommandDelimeter_${os.EOL}5${os.EOL}_GitHubActionsFileCommandDelimeter_${os.EOL}`
) )
}) })
it('exportVariable does not allow delimiter as value', () => {
const command = 'ENV'
createFileCommandFile(command)
expect(() => {
core.exportVariable('my var', `good stuff ${DELIMITER} bad stuff`)
}).toThrow(
`Unexpected input: value should not contain the delimiter "${DELIMITER}"`
)
const filePath = path.join(__dirname, `test/${command}`)
fs.unlinkSync(filePath)
})
it('exportVariable does not allow delimiter as name', () => {
const command = 'ENV'
createFileCommandFile(command)
expect(() => {
core.exportVariable(`good stuff ${DELIMITER} bad stuff`, 'test')
}).toThrow(
`Unexpected input: name should not contain the delimiter "${DELIMITER}"`
)
const filePath = path.join(__dirname, `test/${command}`)
fs.unlinkSync(filePath)
})
it('setSecret produces the correct command', () => { it('setSecret produces the correct command', () => {
core.setSecret('secret val') core.setSecret('secret val')
core.setSecret('multi\nline\r\nsecret') assertWriteCalls([`::add-mask::secret val${os.EOL}`])
assertWriteCalls([
`::add-mask::secret val${os.EOL}`,
`::add-mask::multi%0Aline%0D%0Asecret${os.EOL}`
])
}) })
it('prependPath produces the correct commands and sets the env', () => { it('prependPath produces the correct commands and sets the env', () => {
@@ -218,6 +170,14 @@ describe('@actions/core', () => {
) )
}) })
it('getMultilineInput works', () => {
expect(core.getMultilineInput('my input list')).toEqual([
'val1',
'val2',
'val3'
])
})
it('getInput trims whitespace by default', () => { it('getInput trims whitespace by default', () => {
expect(core.getInput('with trailing whitespace')).toBe('some val') expect(core.getInput('with trailing whitespace')).toBe('some val')
}) })
@@ -258,38 +218,7 @@ describe('@actions/core', () => {
) )
}) })
it('getMultilineInput works', () => { it('setOutput produces the correct command', () => {
expect(core.getMultilineInput('my input list')).toEqual([
'val1',
'val2',
'val3'
])
})
it('getMultilineInput trims whitespace by default', () => {
expect(core.getMultilineInput('list with trailing whitespace')).toEqual([
'val1',
'val2'
])
})
it('getMultilineInput trims whitespace when option is explicitly true', () => {
expect(
core.getMultilineInput('list with trailing whitespace', {
trimWhitespace: true
})
).toEqual(['val1', 'val2'])
})
it('getMultilineInput does not trim whitespace when option is false', () => {
expect(
core.getMultilineInput('list with trailing whitespace', {
trimWhitespace: false
})
).toEqual([' val1 ', ' val2 ', ' '])
})
it('legacy setOutput produces the correct command', () => {
core.setOutput('some output', 'some value') core.setOutput('some output', 'some value')
assertWriteCalls([ assertWriteCalls([
os.EOL, os.EOL,
@@ -297,74 +226,16 @@ describe('@actions/core', () => {
]) ])
}) })
it('legacy setOutput handles bools', () => { it('setOutput handles bools', () => {
core.setOutput('some output', false) core.setOutput('some output', false)
assertWriteCalls([os.EOL, `::set-output name=some output::false${os.EOL}`]) assertWriteCalls([os.EOL, `::set-output name=some output::false${os.EOL}`])
}) })
it('legacy setOutput handles numbers', () => { it('setOutput handles numbers', () => {
core.setOutput('some output', 1.01) core.setOutput('some output', 1.01)
assertWriteCalls([os.EOL, `::set-output name=some output::1.01${os.EOL}`]) assertWriteCalls([os.EOL, `::set-output name=some output::1.01${os.EOL}`])
}) })
it('setOutput produces the correct command and sets the output', () => {
const command = 'OUTPUT'
createFileCommandFile(command)
core.setOutput('my out', 'out val')
verifyFileCommand(
command,
`my out<<${DELIMITER}${os.EOL}out val${os.EOL}${DELIMITER}${os.EOL}`
)
})
it('setOutput handles boolean inputs', () => {
const command = 'OUTPUT'
createFileCommandFile(command)
core.setOutput('my out', true)
verifyFileCommand(
command,
`my out<<${DELIMITER}${os.EOL}true${os.EOL}${DELIMITER}${os.EOL}`
)
})
it('setOutput handles number inputs', () => {
const command = 'OUTPUT'
createFileCommandFile(command)
core.setOutput('my out', 5)
verifyFileCommand(
command,
`my out<<${DELIMITER}${os.EOL}5${os.EOL}${DELIMITER}${os.EOL}`
)
})
it('setOutput does not allow delimiter as value', () => {
const command = 'OUTPUT'
createFileCommandFile(command)
expect(() => {
core.setOutput('my out', `good stuff ${DELIMITER} bad stuff`)
}).toThrow(
`Unexpected input: value should not contain the delimiter "${DELIMITER}"`
)
const filePath = path.join(__dirname, `test/${command}`)
fs.unlinkSync(filePath)
})
it('setOutput does not allow delimiter as name', () => {
const command = 'OUTPUT'
createFileCommandFile(command)
expect(() => {
core.setOutput(`good stuff ${DELIMITER} bad stuff`, 'test')
}).toThrow(
`Unexpected input: name should not contain the delimiter "${DELIMITER}"`
)
const filePath = path.join(__dirname, `test/${command}`)
fs.unlinkSync(filePath)
})
it('setFailed sets the correct exit code and failure message', () => { it('setFailed sets the correct exit code and failure message', () => {
core.setFailed('Failure message') core.setFailed('Failure message')
expect(process.exitCode).toBe(core.ExitCode.Failure) expect(process.exitCode).toBe(core.ExitCode.Failure)
@@ -530,79 +401,21 @@ describe('@actions/core', () => {
assertWriteCalls([`::debug::%0D%0Adebug%0A${os.EOL}`]) assertWriteCalls([`::debug::%0D%0Adebug%0A${os.EOL}`])
}) })
it('legacy saveState produces the correct command', () => { it('saveState produces the correct command', () => {
core.saveState('state_1', 'some value') core.saveState('state_1', 'some value')
assertWriteCalls([`::save-state name=state_1::some value${os.EOL}`]) assertWriteCalls([`::save-state name=state_1::some value${os.EOL}`])
}) })
it('legacy saveState handles numbers', () => { it('saveState handles numbers', () => {
core.saveState('state_1', 1) core.saveState('state_1', 1)
assertWriteCalls([`::save-state name=state_1::1${os.EOL}`]) assertWriteCalls([`::save-state name=state_1::1${os.EOL}`])
}) })
it('legacy saveState handles bools', () => { it('saveState handles bools', () => {
core.saveState('state_1', true) core.saveState('state_1', true)
assertWriteCalls([`::save-state name=state_1::true${os.EOL}`]) assertWriteCalls([`::save-state name=state_1::true${os.EOL}`])
}) })
it('saveState produces the correct command and saves the state', () => {
const command = 'STATE'
createFileCommandFile(command)
core.saveState('my state', 'out val')
verifyFileCommand(
command,
`my state<<${DELIMITER}${os.EOL}out val${os.EOL}${DELIMITER}${os.EOL}`
)
})
it('saveState handles boolean inputs', () => {
const command = 'STATE'
createFileCommandFile(command)
core.saveState('my state', true)
verifyFileCommand(
command,
`my state<<${DELIMITER}${os.EOL}true${os.EOL}${DELIMITER}${os.EOL}`
)
})
it('saveState handles number inputs', () => {
const command = 'STATE'
createFileCommandFile(command)
core.saveState('my state', 5)
verifyFileCommand(
command,
`my state<<${DELIMITER}${os.EOL}5${os.EOL}${DELIMITER}${os.EOL}`
)
})
it('saveState does not allow delimiter as value', () => {
const command = 'STATE'
createFileCommandFile(command)
expect(() => {
core.saveState('my state', `good stuff ${DELIMITER} bad stuff`)
}).toThrow(
`Unexpected input: value should not contain the delimiter "${DELIMITER}"`
)
const filePath = path.join(__dirname, `test/${command}`)
fs.unlinkSync(filePath)
})
it('saveState does not allow delimiter as name', () => {
const command = 'STATE'
createFileCommandFile(command)
expect(() => {
core.saveState(`good stuff ${DELIMITER} bad stuff`, 'test')
}).toThrow(
`Unexpected input: name should not contain the delimiter "${DELIMITER}"`
)
const filePath = path.join(__dirname, `test/${command}`)
fs.unlinkSync(filePath)
})
it('getState gets wrapper action state', () => { it('getState gets wrapper action state', () => {
expect(core.getState('TEST_1')).toBe('state_val') expect(core.getState('TEST_1')).toBe('state_val')
}) })
@@ -1,10 +1,9 @@
import * as fs from 'fs' import * as fs from 'fs'
import * as os from 'os' import * as os from 'os'
import path from 'path' import path from 'path'
import {summary, SUMMARY_ENV_VAR} from '../src/summary' import {markdownSummary, SUMMARY_ENV_VAR} from '../src/markdown-summary'
const testDirectoryPath = path.join(__dirname, 'test') const testFilePath = path.join(__dirname, 'test', 'test-summary.md')
const testFilePath = path.join(testDirectoryPath, 'test-summary.md')
async function assertSummary(expected: string): Promise<void> { async function assertSummary(expected: string): Promise<void> {
const file = await fs.promises.readFile(testFilePath, {encoding: 'utf8'}) const file = await fs.promises.readFile(testFilePath, {encoding: 'utf8'})
@@ -68,12 +67,11 @@ const fixtures = {
} }
} }
describe('@actions/core/src/summary', () => { describe('@actions/core/src/markdown-summary', () => {
beforeEach(async () => { beforeEach(async () => {
process.env[SUMMARY_ENV_VAR] = testFilePath process.env[SUMMARY_ENV_VAR] = testFilePath
await fs.promises.mkdir(testDirectoryPath, {recursive: true})
await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'}) await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'})
summary.emptyBuffer() markdownSummary.emptyBuffer()
}) })
afterAll(async () => { afterAll(async () => {
@@ -82,39 +80,39 @@ describe('@actions/core/src/summary', () => {
it('throws if summary env var is undefined', async () => { it('throws if summary env var is undefined', async () => {
process.env[SUMMARY_ENV_VAR] = undefined process.env[SUMMARY_ENV_VAR] = undefined
const write = summary.addRaw(fixtures.text).write() const write = markdownSummary.addRaw(fixtures.text).write()
await expect(write).rejects.toThrow() await expect(write).rejects.toThrow()
}) })
it('throws if summary file does not exist', async () => { it('throws if summary file does not exist', async () => {
await fs.promises.unlink(testFilePath) await fs.promises.unlink(testFilePath)
const write = summary.addRaw(fixtures.text).write() const write = markdownSummary.addRaw(fixtures.text).write()
await expect(write).rejects.toThrow() await expect(write).rejects.toThrow()
}) })
it('appends text to summary file', async () => { it('appends text to summary file', async () => {
await fs.promises.writeFile(testFilePath, '# ', {encoding: 'utf8'}) await fs.promises.writeFile(testFilePath, '# ', {encoding: 'utf8'})
await summary.addRaw(fixtures.text).write() await markdownSummary.addRaw(fixtures.text).write()
await assertSummary(`# ${fixtures.text}`) await assertSummary(`# ${fixtures.text}`)
}) })
it('overwrites text to summary file', async () => { it('overwrites text to summary file', async () => {
await fs.promises.writeFile(testFilePath, 'overwrite', {encoding: 'utf8'}) await fs.promises.writeFile(testFilePath, 'overwrite', {encoding: 'utf8'})
await summary.addRaw(fixtures.text).write({overwrite: true}) await markdownSummary.addRaw(fixtures.text).write({overwrite: true})
await assertSummary(fixtures.text) await assertSummary(fixtures.text)
}) })
it('appends text with EOL to summary file', async () => { it('appends text with EOL to summary file', async () => {
await fs.promises.writeFile(testFilePath, '# ', {encoding: 'utf8'}) await fs.promises.writeFile(testFilePath, '# ', {encoding: 'utf8'})
await summary.addRaw(fixtures.text, true).write() await markdownSummary.addRaw(fixtures.text, true).write()
await assertSummary(`# ${fixtures.text}${os.EOL}`) await assertSummary(`# ${fixtures.text}${os.EOL}`)
}) })
it('chains appends text to summary file', async () => { it('chains appends text to summary file', async () => {
await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'}) await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'})
await summary await markdownSummary
.addRaw(fixtures.text) .addRaw(fixtures.text)
.addRaw(fixtures.text) .addRaw(fixtures.text)
.addRaw(fixtures.text) .addRaw(fixtures.text)
@@ -124,33 +122,33 @@ describe('@actions/core/src/summary', () => {
it('empties buffer after write', async () => { it('empties buffer after write', async () => {
await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'}) await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'})
await summary.addRaw(fixtures.text).write() await markdownSummary.addRaw(fixtures.text).write()
await assertSummary(fixtures.text) await assertSummary(fixtures.text)
expect(summary.isEmptyBuffer()).toBe(true) expect(markdownSummary.isEmptyBuffer()).toBe(true)
}) })
it('returns summary buffer as string', () => { it('returns summary buffer as string', () => {
summary.addRaw(fixtures.text) markdownSummary.addRaw(fixtures.text)
expect(summary.stringify()).toEqual(fixtures.text) expect(markdownSummary.stringify()).toEqual(fixtures.text)
}) })
it('return correct values for isEmptyBuffer', () => { it('return correct values for isEmptyBuffer', () => {
summary.addRaw(fixtures.text) markdownSummary.addRaw(fixtures.text)
expect(summary.isEmptyBuffer()).toBe(false) expect(markdownSummary.isEmptyBuffer()).toBe(false)
summary.emptyBuffer() markdownSummary.emptyBuffer()
expect(summary.isEmptyBuffer()).toBe(true) expect(markdownSummary.isEmptyBuffer()).toBe(true)
}) })
it('clears a buffer and summary file', async () => { it('clears a buffer and summary file', async () => {
await fs.promises.writeFile(testFilePath, 'content', {encoding: 'utf8'}) await fs.promises.writeFile(testFilePath, 'content', {encoding: 'utf8'})
await summary.clear() await markdownSummary.clear()
await assertSummary('') await assertSummary('')
expect(summary.isEmptyBuffer()).toBe(true) expect(markdownSummary.isEmptyBuffer()).toBe(true)
}) })
it('adds EOL', async () => { it('adds EOL', async () => {
await summary await markdownSummary
.addRaw(fixtures.text) .addRaw(fixtures.text)
.addEOL() .addEOL()
.write() .write()
@@ -158,37 +156,37 @@ describe('@actions/core/src/summary', () => {
}) })
it('adds a code block without language', async () => { it('adds a code block without language', async () => {
await summary.addCodeBlock(fixtures.code).write() await markdownSummary.addCodeBlock(fixtures.code).write()
const expected = `<pre><code>func fork() {\n for {\n go fork()\n }\n}</code></pre>${os.EOL}` const expected = `<pre><code>func fork() {\n for {\n go fork()\n }\n}</code></pre>${os.EOL}`
await assertSummary(expected) await assertSummary(expected)
}) })
it('adds a code block with a language', async () => { it('adds a code block with a language', async () => {
await summary.addCodeBlock(fixtures.code, 'go').write() await markdownSummary.addCodeBlock(fixtures.code, 'go').write()
const expected = `<pre lang="go"><code>func fork() {\n for {\n go fork()\n }\n}</code></pre>${os.EOL}` const expected = `<pre lang="go"><code>func fork() {\n for {\n go fork()\n }\n}</code></pre>${os.EOL}`
await assertSummary(expected) await assertSummary(expected)
}) })
it('adds an unordered list', async () => { it('adds an unordered list', async () => {
await summary.addList(fixtures.list).write() await markdownSummary.addList(fixtures.list).write()
const expected = `<ul><li>foo</li><li>bar</li><li>baz</li><li>💣</li></ul>${os.EOL}` const expected = `<ul><li>foo</li><li>bar</li><li>baz</li><li>💣</li></ul>${os.EOL}`
await assertSummary(expected) await assertSummary(expected)
}) })
it('adds an ordered list', async () => { it('adds an ordered list', async () => {
await summary.addList(fixtures.list, true).write() await markdownSummary.addList(fixtures.list, true).write()
const expected = `<ol><li>foo</li><li>bar</li><li>baz</li><li>💣</li></ol>${os.EOL}` const expected = `<ol><li>foo</li><li>bar</li><li>baz</li><li>💣</li></ol>${os.EOL}`
await assertSummary(expected) await assertSummary(expected)
}) })
it('adds a table', async () => { it('adds a table', async () => {
await summary.addTable(fixtures.table).write() await markdownSummary.addTable(fixtures.table).write()
const expected = `<table><tr><th>foo</th><th>bar</th><th>baz</th><td rowspan="3">tall</td></tr><tr><td>one</td><td>two</td><td>three</td></tr><tr><td colspan="3">wide</td></tr></table>${os.EOL}` const expected = `<table><tr><th>foo</th><th>bar</th><th>baz</th><td rowspan="3">tall</td></tr><tr><td>one</td><td>two</td><td>three</td></tr><tr><td colspan="3">wide</td></tr></table>${os.EOL}`
await assertSummary(expected) await assertSummary(expected)
}) })
it('adds a details element', async () => { it('adds a details element', async () => {
await summary await markdownSummary
.addDetails(fixtures.details.label, fixtures.details.content) .addDetails(fixtures.details.label, fixtures.details.content)
.write() .write()
const expected = `<details><summary>open me</summary>🎉 surprise</details>${os.EOL}` const expected = `<details><summary>open me</summary>🎉 surprise</details>${os.EOL}`
@@ -196,13 +194,13 @@ describe('@actions/core/src/summary', () => {
}) })
it('adds an image with alt text', async () => { it('adds an image with alt text', async () => {
await summary.addImage(fixtures.img.src, fixtures.img.alt).write() await markdownSummary.addImage(fixtures.img.src, fixtures.img.alt).write()
const expected = `<img src="https://github.com/actions.png" alt="actions logo">${os.EOL}` const expected = `<img src="https://github.com/actions.png" alt="actions logo">${os.EOL}`
await assertSummary(expected) await assertSummary(expected)
}) })
it('adds an image with custom dimensions', async () => { it('adds an image with custom dimensions', async () => {
await summary await markdownSummary
.addImage(fixtures.img.src, fixtures.img.alt, fixtures.img.options) .addImage(fixtures.img.src, fixtures.img.alt, fixtures.img.options)
.write() .write()
const expected = `<img src="https://github.com/actions.png" alt="actions logo" width="32" height="32">${os.EOL}` const expected = `<img src="https://github.com/actions.png" alt="actions logo" width="32" height="32">${os.EOL}`
@@ -210,7 +208,7 @@ describe('@actions/core/src/summary', () => {
}) })
it('adds an image with custom dimensions', async () => { it('adds an image with custom dimensions', async () => {
await summary await markdownSummary
.addImage(fixtures.img.src, fixtures.img.alt, fixtures.img.options) .addImage(fixtures.img.src, fixtures.img.alt, fixtures.img.options)
.write() .write()
const expected = `<img src="https://github.com/actions.png" alt="actions logo" width="32" height="32">${os.EOL}` const expected = `<img src="https://github.com/actions.png" alt="actions logo" width="32" height="32">${os.EOL}`
@@ -219,21 +217,21 @@ describe('@actions/core/src/summary', () => {
it('adds headings h1...h6', async () => { it('adds headings h1...h6', async () => {
for (const i of [1, 2, 3, 4, 5, 6]) { for (const i of [1, 2, 3, 4, 5, 6]) {
summary.addHeading('heading', i) markdownSummary.addHeading('heading', i)
} }
await summary.write() await markdownSummary.write()
const expected = `<h1>heading</h1>${os.EOL}<h2>heading</h2>${os.EOL}<h3>heading</h3>${os.EOL}<h4>heading</h4>${os.EOL}<h5>heading</h5>${os.EOL}<h6>heading</h6>${os.EOL}` const expected = `<h1>heading</h1>${os.EOL}<h2>heading</h2>${os.EOL}<h3>heading</h3>${os.EOL}<h4>heading</h4>${os.EOL}<h5>heading</h5>${os.EOL}<h6>heading</h6>${os.EOL}`
await assertSummary(expected) await assertSummary(expected)
}) })
it('adds h1 if heading level not specified', async () => { it('adds h1 if heading level not specified', async () => {
await summary.addHeading('heading').write() await markdownSummary.addHeading('heading').write()
const expected = `<h1>heading</h1>${os.EOL}` const expected = `<h1>heading</h1>${os.EOL}`
await assertSummary(expected) await assertSummary(expected)
}) })
it('uses h1 if heading level is garbage or out of range', async () => { it('uses h1 if heading level is garbage or out of range', async () => {
await summary await markdownSummary
.addHeading('heading', 'foobar') .addHeading('heading', 'foobar')
.addHeading('heading', 1337) .addHeading('heading', 1337)
.addHeading('heading', -1) .addHeading('heading', -1)
@@ -244,31 +242,35 @@ describe('@actions/core/src/summary', () => {
}) })
it('adds a separator', async () => { it('adds a separator', async () => {
await summary.addSeparator().write() await markdownSummary.addSeparator().write()
const expected = `<hr>${os.EOL}` const expected = `<hr>${os.EOL}`
await assertSummary(expected) await assertSummary(expected)
}) })
it('adds a break', async () => { it('adds a break', async () => {
await summary.addBreak().write() await markdownSummary.addBreak().write()
const expected = `<br>${os.EOL}` const expected = `<br>${os.EOL}`
await assertSummary(expected) await assertSummary(expected)
}) })
it('adds a quote', async () => { it('adds a quote', async () => {
await summary.addQuote(fixtures.quote.text).write() await markdownSummary.addQuote(fixtures.quote.text).write()
const expected = `<blockquote>Where the world builds software</blockquote>${os.EOL}` const expected = `<blockquote>Where the world builds software</blockquote>${os.EOL}`
await assertSummary(expected) await assertSummary(expected)
}) })
it('adds a quote with citation', async () => { it('adds a quote with citation', async () => {
await summary.addQuote(fixtures.quote.text, fixtures.quote.cite).write() await markdownSummary
.addQuote(fixtures.quote.text, fixtures.quote.cite)
.write()
const expected = `<blockquote cite="https://github.com/about">Where the world builds software</blockquote>${os.EOL}` const expected = `<blockquote cite="https://github.com/about">Where the world builds software</blockquote>${os.EOL}`
await assertSummary(expected) await assertSummary(expected)
}) })
it('adds a link with href', async () => { it('adds a link with href', async () => {
await summary.addLink(fixtures.link.text, fixtures.link.href).write() await markdownSummary
.addLink(fixtures.link.text, fixtures.link.href)
.write()
const expected = `<a href="https://github.com/">GitHub</a>${os.EOL}` const expected = `<a href="https://github.com/">GitHub</a>${os.EOL}`
await assertSummary(expected) await assertSummary(expected)
}) })
-162
View File
@@ -1,162 +0,0 @@
import * as path from 'path'
import {toPlatformPath, toPosixPath, toWin32Path} from '../src/path-utils'
describe('#toPosixPath', () => {
const cases: {
only?: boolean
name: string
input: string
expected: string
}[] = [
{
name: 'empty string',
input: '',
expected: ''
},
{
name: 'single value',
input: 'foo',
expected: 'foo'
},
{
name: 'with posix relative',
input: 'foo/bar/baz',
expected: 'foo/bar/baz'
},
{
name: 'with posix absolute',
input: '/foo/bar/baz',
expected: '/foo/bar/baz'
},
{
name: 'with win32 relative',
input: 'foo\\bar\\baz',
expected: 'foo/bar/baz'
},
{
name: 'with win32 absolute',
input: '\\foo\\bar\\baz',
expected: '/foo/bar/baz'
},
{
name: 'with a mix',
input: '\\foo/bar/baz',
expected: '/foo/bar/baz'
}
]
for (const tc of cases) {
const fn = tc.only ? it.only : it
fn(tc.name, () => {
const result = toPosixPath(tc.input)
expect(result).toEqual(tc.expected)
})
}
})
describe('#toWin32Path', () => {
const cases: {
only?: boolean
name: string
input: string
expected: string
}[] = [
{
name: 'empty string',
input: '',
expected: ''
},
{
name: 'single value',
input: 'foo',
expected: 'foo'
},
{
name: 'with posix relative',
input: 'foo/bar/baz',
expected: 'foo\\bar\\baz'
},
{
name: 'with posix absolute',
input: '/foo/bar/baz',
expected: '\\foo\\bar\\baz'
},
{
name: 'with win32 relative',
input: 'foo\\bar\\baz',
expected: 'foo\\bar\\baz'
},
{
name: 'with win32 absolute',
input: '\\foo\\bar\\baz',
expected: '\\foo\\bar\\baz'
},
{
name: 'with a mix',
input: '\\foo/bar\\baz',
expected: '\\foo\\bar\\baz'
}
]
for (const tc of cases) {
const fn = tc.only ? it.only : it
fn(tc.name, () => {
const result = toWin32Path(tc.input)
expect(result).toEqual(tc.expected)
})
}
})
describe('#toPlatformPath', () => {
const cases: {
only?: boolean
name: string
input: string
expected: string
}[] = [
{
name: 'empty string',
input: '',
expected: ''
},
{
name: 'single value',
input: 'foo',
expected: 'foo'
},
{
name: 'with posix relative',
input: 'foo/bar/baz',
expected: path.join('foo', 'bar', 'baz')
},
{
name: 'with posix absolute',
input: '/foo/bar/baz',
expected: path.join(path.sep, 'foo', 'bar', 'baz')
},
{
name: 'with win32 relative',
input: 'foo\\bar\\baz',
expected: path.join('foo', 'bar', 'baz')
},
{
name: 'with win32 absolute',
input: '\\foo\\bar\\baz',
expected: path.join(path.sep, 'foo', 'bar', 'baz')
},
{
name: 'with a mix',
input: '\\foo/bar\\baz',
expected: path.join(path.sep, 'foo', 'bar', 'baz')
}
]
for (const tc of cases) {
const fn = tc.only ? it.only : it
fn(tc.name, () => {
const result = toPlatformPath(tc.input)
expect(result).toEqual(tc.expected)
})
}
})
+12 -39
View File
@@ -1,28 +1,26 @@
{ {
"name": "@actions/core", "name": "@actions/core",
"version": "1.10.0", "version": "1.7.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@actions/core", "name": "@actions/core",
"version": "1.10.0", "version": "1.6.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/http-client": "^2.0.1", "@actions/http-client": "^1.0.11"
"uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^12.0.2", "@types/node": "^12.0.2"
"@types/uuid": "^8.3.4"
} }
}, },
"node_modules/@actions/http-client": { "node_modules/@actions/http-client": {
"version": "2.0.1", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"dependencies": { "dependencies": {
"tunnel": "^0.0.6" "tunnel": "0.0.6"
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
@@ -31,12 +29,6 @@
"integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==", "integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==",
"dev": true "dev": true
}, },
"node_modules/@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true
},
"node_modules/tunnel": { "node_modules/tunnel": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
@@ -44,23 +36,15 @@
"engines": { "engines": {
"node": ">=0.6.11 <=0.7.0 || >=0.7.3" "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
} }
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
} }
}, },
"dependencies": { "dependencies": {
"@actions/http-client": { "@actions/http-client": {
"version": "2.0.1", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"requires": { "requires": {
"tunnel": "^0.0.6" "tunnel": "0.0.6"
} }
}, },
"@types/node": { "@types/node": {
@@ -69,21 +53,10 @@
"integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==", "integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==",
"dev": true "dev": true
}, },
"@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true
},
"tunnel": { "tunnel": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
} }
} }
} }
+3 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/core", "name": "@actions/core",
"version": "1.10.0", "version": "1.7.0",
"description": "Actions core lib", "description": "Actions core lib",
"keywords": [ "keywords": [
"github", "github",
@@ -36,11 +36,9 @@
"url": "https://github.com/actions/toolkit/issues" "url": "https://github.com/actions/toolkit/issues"
}, },
"dependencies": { "dependencies": {
"@actions/http-client": "^2.0.1", "@actions/http-client": "^1.0.11"
"uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^12.0.2", "@types/node": "^12.0.2"
"@types/uuid": "^8.3.4"
} }
} }
+13 -35
View File
@@ -1,5 +1,5 @@
import {issue, issueCommand} from './command' import {issue, issueCommand} from './command'
import {issueFileCommand, prepareKeyValueMessage} from './file-command' import {issueCommand as issueFileCommand} from './file-command'
import {toCommandProperties, toCommandValue} from './utils' import {toCommandProperties, toCommandValue} from './utils'
import * as os from 'os' import * as os from 'os'
@@ -34,7 +34,7 @@ export enum ExitCode {
} }
/** /**
* Optional properties that can be sent with annotation commands (notice, error, and warning) * 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. * See: https://docs.github.com/en/rest/reference/checks#create-a-check-run for more information about annotations.
*/ */
export interface AnnotationProperties { export interface AnnotationProperties {
@@ -64,7 +64,7 @@ export interface AnnotationProperties {
startColumn?: number startColumn?: number
/** /**
* The end column for the annotation. Cannot be sent when `startLine` and `endLine` are different values. * The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
* Defaults to `startColumn` when `startColumn` is provided. * Defaults to `startColumn` when `startColumn` is provided.
*/ */
endColumn?: number endColumn?: number
@@ -86,10 +86,12 @@ export function exportVariable(name: string, val: any): void {
const filePath = process.env['GITHUB_ENV'] || '' const filePath = process.env['GITHUB_ENV'] || ''
if (filePath) { if (filePath) {
return issueFileCommand('ENV', prepareKeyValueMessage(name, val)) const delimiter = '_GitHubActionsFileCommandDelimeter_'
const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`
issueFileCommand('ENV', commandValue)
} else {
issueCommand('set-env', {name}, convertedVal)
} }
issueCommand('set-env', {name}, convertedVal)
} }
/** /**
@@ -153,11 +155,7 @@ export function getMultilineInput(
.split('\n') .split('\n')
.filter(x => x !== '') .filter(x => x !== '')
if (options && options.trimWhitespace === false) { return inputs
return inputs
}
return inputs.map(input => input.trim())
} }
/** /**
@@ -190,13 +188,8 @@ export function getBooleanInput(name: string, options?: InputOptions): boolean {
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function setOutput(name: string, value: any): void { export function setOutput(name: string, value: any): void {
const filePath = process.env['GITHUB_OUTPUT'] || ''
if (filePath) {
return issueFileCommand('OUTPUT', prepareKeyValueMessage(name, value))
}
process.stdout.write(os.EOL) process.stdout.write(os.EOL)
issueCommand('set-output', {name}, toCommandValue(value)) issueCommand('set-output', {name}, value)
} }
/** /**
@@ -350,12 +343,7 @@ export async function group<T>(name: string, fn: () => Promise<T>): Promise<T> {
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function saveState(name: string, value: any): void { export function saveState(name: string, value: any): void {
const filePath = process.env['GITHUB_STATE'] || '' issueCommand('save-state', {name}, value)
if (filePath) {
return issueFileCommand('STATE', prepareKeyValueMessage(name, value))
}
issueCommand('save-state', {name}, toCommandValue(value))
} }
/** /**
@@ -373,16 +361,6 @@ export async function getIDToken(aud?: string): Promise<string> {
} }
/** /**
* Summary exports * Markdown summary exports
*/ */
export {summary} from './summary' export {markdownSummary} from './markdown-summary'
/**
* @deprecated use core.summary
*/
export {markdownSummary} from './summary'
/**
* Path exports
*/
export {toPosixPath, toWin32Path, toPlatformPath} from './path-utils'
+1 -24
View File
@@ -5,10 +5,9 @@
import * as fs from 'fs' import * as fs from 'fs'
import * as os from 'os' import * as os from 'os'
import {v4 as uuidv4} from 'uuid'
import {toCommandValue} from './utils' import {toCommandValue} from './utils'
export function issueFileCommand(command: string, message: any): void { export function issueCommand(command: string, message: any): void {
const filePath = process.env[`GITHUB_${command}`] const filePath = process.env[`GITHUB_${command}`]
if (!filePath) { if (!filePath) {
throw new Error( throw new Error(
@@ -23,25 +22,3 @@ export function issueFileCommand(command: string, message: any): void {
encoding: 'utf8' encoding: 'utf8'
}) })
} }
export function prepareKeyValueMessage(key: string, value: any): string {
const delimiter = `ghadelimiter_${uuidv4()}`
const convertedValue = toCommandValue(value)
// These should realistically never happen, but just in case someone finds a
// way to exploit uuid generation let's not allow keys or values that contain
// the delimiter.
if (key.includes(delimiter)) {
throw new Error(
`Unexpected input: name should not contain the delimiter "${delimiter}"`
)
}
if (convertedValue.includes(delimiter)) {
throw new Error(
`Unexpected input: value should not contain the delimiter "${delimiter}"`
)
}
return `${key}<<${delimiter}${os.EOL}${convertedValue}${os.EOL}${delimiter}`
}
@@ -4,7 +4,7 @@ const {access, appendFile, writeFile} = promises
export const SUMMARY_ENV_VAR = 'GITHUB_STEP_SUMMARY' export const SUMMARY_ENV_VAR = 'GITHUB_STEP_SUMMARY'
export const SUMMARY_DOCS_URL = export const SUMMARY_DOCS_URL =
'https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary' 'https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-markdown-summary'
export type SummaryTableRow = (SummaryTableCell | string)[] export type SummaryTableRow = (SummaryTableCell | string)[]
@@ -51,7 +51,7 @@ export interface SummaryWriteOptions {
overwrite?: boolean overwrite?: boolean
} }
class Summary { class MarkdownSummary {
private _buffer: string private _buffer: string
private _filePath?: string private _filePath?: string
@@ -73,7 +73,7 @@ class Summary {
const pathFromEnv = process.env[SUMMARY_ENV_VAR] const pathFromEnv = process.env[SUMMARY_ENV_VAR]
if (!pathFromEnv) { if (!pathFromEnv) {
throw new Error( throw new Error(
`Unable to find environment variable for $${SUMMARY_ENV_VAR}. Check if your runtime environment supports job summaries.` `Unable to find environment variable for $${SUMMARY_ENV_VAR}. Check if your runtime environment supports markdown summaries.`
) )
} }
@@ -119,9 +119,9 @@ class Summary {
* *
* @param {SummaryWriteOptions} [options] (optional) options for write operation * @param {SummaryWriteOptions} [options] (optional) options for write operation
* *
* @returns {Promise<Summary>} summary instance * @returns {Promise<MarkdownSummary>} markdown summary instance
*/ */
async write(options?: SummaryWriteOptions): Promise<Summary> { async write(options?: SummaryWriteOptions): Promise<MarkdownSummary> {
const overwrite = !!options?.overwrite const overwrite = !!options?.overwrite
const filePath = await this.filePath() const filePath = await this.filePath()
const writeFunc = overwrite ? writeFile : appendFile const writeFunc = overwrite ? writeFile : appendFile
@@ -132,9 +132,9 @@ class Summary {
/** /**
* Clears the summary buffer and wipes the summary file * Clears the summary buffer and wipes the summary file
* *
* @returns {Summary} summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
async clear(): Promise<Summary> { async clear(): Promise<MarkdownSummary> {
return this.emptyBuffer().write({overwrite: true}) return this.emptyBuffer().write({overwrite: true})
} }
@@ -159,9 +159,9 @@ class Summary {
/** /**
* Resets the summary buffer without writing to summary file * Resets the summary buffer without writing to summary file
* *
* @returns {Summary} summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
emptyBuffer(): Summary { emptyBuffer(): MarkdownSummary {
this._buffer = '' this._buffer = ''
return this return this
} }
@@ -172,9 +172,9 @@ class Summary {
* @param {string} text content to add * @param {string} text content to add
* @param {boolean} [addEOL=false] (optional) append an EOL to the raw text (default: false) * @param {boolean} [addEOL=false] (optional) append an EOL to the raw text (default: false)
* *
* @returns {Summary} summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
addRaw(text: string, addEOL = false): Summary { addRaw(text: string, addEOL = false): MarkdownSummary {
this._buffer += text this._buffer += text
return addEOL ? this.addEOL() : this return addEOL ? this.addEOL() : this
} }
@@ -182,9 +182,9 @@ class Summary {
/** /**
* Adds the operating system-specific end-of-line marker to the buffer * Adds the operating system-specific end-of-line marker to the buffer
* *
* @returns {Summary} summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
addEOL(): Summary { addEOL(): MarkdownSummary {
return this.addRaw(EOL) return this.addRaw(EOL)
} }
@@ -194,9 +194,9 @@ class Summary {
* @param {string} code content to render within fenced code block * @param {string} code content to render within fenced code block
* @param {string} lang (optional) language to syntax highlight code * @param {string} lang (optional) language to syntax highlight code
* *
* @returns {Summary} summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
addCodeBlock(code: string, lang?: string): Summary { addCodeBlock(code: string, lang?: string): MarkdownSummary {
const attrs = { const attrs = {
...(lang && {lang}) ...(lang && {lang})
} }
@@ -210,9 +210,9 @@ class Summary {
* @param {string[]} items list of items to render * @param {string[]} items list of items to render
* @param {boolean} [ordered=false] (optional) if the rendered list should be ordered or not (default: false) * @param {boolean} [ordered=false] (optional) if the rendered list should be ordered or not (default: false)
* *
* @returns {Summary} summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
addList(items: string[], ordered = false): Summary { addList(items: string[], ordered = false): MarkdownSummary {
const tag = ordered ? 'ol' : 'ul' const tag = ordered ? 'ol' : 'ul'
const listItems = items.map(item => this.wrap('li', item)).join('') const listItems = items.map(item => this.wrap('li', item)).join('')
const element = this.wrap(tag, listItems) const element = this.wrap(tag, listItems)
@@ -224,9 +224,9 @@ class Summary {
* *
* @param {SummaryTableCell[]} rows table rows * @param {SummaryTableCell[]} rows table rows
* *
* @returns {Summary} summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
addTable(rows: SummaryTableRow[]): Summary { addTable(rows: SummaryTableRow[]): MarkdownSummary {
const tableBody = rows const tableBody = rows
.map(row => { .map(row => {
const cells = row const cells = row
@@ -260,9 +260,9 @@ class Summary {
* @param {string} label text for the closed state * @param {string} label text for the closed state
* @param {string} content collapsable content * @param {string} content collapsable content
* *
* @returns {Summary} summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
addDetails(label: string, content: string): Summary { addDetails(label: string, content: string): MarkdownSummary {
const element = this.wrap('details', this.wrap('summary', label) + content) const element = this.wrap('details', this.wrap('summary', label) + content)
return this.addRaw(element).addEOL() return this.addRaw(element).addEOL()
} }
@@ -274,9 +274,13 @@ class Summary {
* @param {string} alt text description of the image * @param {string} alt text description of the image
* @param {SummaryImageOptions} options (optional) addition image attributes * @param {SummaryImageOptions} options (optional) addition image attributes
* *
* @returns {Summary} summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
addImage(src: string, alt: string, options?: SummaryImageOptions): Summary { addImage(
src: string,
alt: string,
options?: SummaryImageOptions
): MarkdownSummary {
const {width, height} = options || {} const {width, height} = options || {}
const attrs = { const attrs = {
...(width && {width}), ...(width && {width}),
@@ -293,9 +297,9 @@ class Summary {
* @param {string} text heading text * @param {string} text heading text
* @param {number | string} [level=1] (optional) the heading level, default: 1 * @param {number | string} [level=1] (optional) the heading level, default: 1
* *
* @returns {Summary} summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
addHeading(text: string, level?: number | string): Summary { addHeading(text: string, level?: number | string): MarkdownSummary {
const tag = `h${level}` const tag = `h${level}`
const allowedTag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag) const allowedTag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag)
? tag ? tag
@@ -307,9 +311,9 @@ class Summary {
/** /**
* Adds an HTML thematic break (<hr>) to the summary buffer * Adds an HTML thematic break (<hr>) to the summary buffer
* *
* @returns {Summary} summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
addSeparator(): Summary { addSeparator(): MarkdownSummary {
const element = this.wrap('hr', null) const element = this.wrap('hr', null)
return this.addRaw(element).addEOL() return this.addRaw(element).addEOL()
} }
@@ -317,9 +321,9 @@ class Summary {
/** /**
* Adds an HTML line break (<br>) to the summary buffer * Adds an HTML line break (<br>) to the summary buffer
* *
* @returns {Summary} summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
addBreak(): Summary { addBreak(): MarkdownSummary {
const element = this.wrap('br', null) const element = this.wrap('br', null)
return this.addRaw(element).addEOL() return this.addRaw(element).addEOL()
} }
@@ -330,9 +334,9 @@ class Summary {
* @param {string} text quote text * @param {string} text quote text
* @param {string} cite (optional) citation url * @param {string} cite (optional) citation url
* *
* @returns {Summary} summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
addQuote(text: string, cite?: string): Summary { addQuote(text: string, cite?: string): MarkdownSummary {
const attrs = { const attrs = {
...(cite && {cite}) ...(cite && {cite})
} }
@@ -346,18 +350,13 @@ class Summary {
* @param {string} text link text/content * @param {string} text link text/content
* @param {string} href hyperlink * @param {string} href hyperlink
* *
* @returns {Summary} summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
addLink(text: string, href: string): Summary { addLink(text: string, href: string): MarkdownSummary {
const element = this.wrap('a', text, {href}) const element = this.wrap('a', text, {href})
return this.addRaw(element).addEOL() return this.addRaw(element).addEOL()
} }
} }
const _summary = new Summary() // singleton export
export const markdownSummary = new MarkdownSummary()
/**
* @deprecated use `core.summary`
*/
export const markdownSummary = _summary
export const summary = _summary
+3 -3
View File
@@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/no-extraneous-class */ /* eslint-disable @typescript-eslint/no-extraneous-class */
import * as actions_http_client from '@actions/http-client' import * as actions_http_client from '@actions/http-client'
import {RequestOptions} from '@actions/http-client/lib/interfaces' import {IRequestOptions} from '@actions/http-client/interfaces'
import {HttpClient} from '@actions/http-client' import {HttpClient} from '@actions/http-client'
import {BearerCredentialHandler} from '@actions/http-client/lib/auth' import {BearerCredentialHandler} from '@actions/http-client/auth'
import {debug, setSecret} from './core' import {debug, setSecret} from './core'
interface TokenResponse { interface TokenResponse {
value?: string value?: string
@@ -13,7 +13,7 @@ export class OidcClient {
allowRetry = true, allowRetry = true,
maxRetry = 10 maxRetry = 10
): actions_http_client.HttpClient { ): actions_http_client.HttpClient {
const requestOptions: RequestOptions = { const requestOptions: IRequestOptions = {
allowRetries: allowRetry, allowRetries: allowRetry,
maxRetries: maxRetry maxRetries: maxRetry
} }
-35
View File
@@ -1,35 +0,0 @@
import * as path from 'path'
/**
* toPosixPath converts the given path to the posix form. On Windows, \\ will be
* replaced with /.
*
* @param pth. Path to transform.
* @return string Posix path.
*/
export function toPosixPath(pth: string): string {
return pth.replace(/[\\]/g, '/')
}
/**
* toWin32Path converts the given path to the win32 form. On Linux, / will be
* replaced with \\.
*
* @param pth. Path to transform.
* @return string Win32 path.
*/
export function toWin32Path(pth: string): string {
return pth.replace(/[/]/g, '\\')
}
/**
* toPlatformPath converts the given path to a platform-specific path. It does
* this by replacing instances of / and \ with the platform-specific path
* separator.
*
* @param pth The path to platformize.
* @return string The platform-specific path.
*/
export function toPlatformPath(pth: string): string {
return pth.replace(/[/\\]/g, path.sep)
}
-14
View File
@@ -1,19 +1,5 @@
# @actions/github Releases # @actions/github Releases
### 5.1.1
- Export default octokit options [#1188](https://github.com/actions/toolkit/pull/1188)
### 5.1.0
- Add additionalPlugins parameter to getOctokit method [#1181](https://github.com/actions/toolkit/pull/1181)
- Dependency updates [#1180](https://github.com/actions/toolkit/pull/1180)
### 5.0.3
- - Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
### 5.0.2
- Update to v2.0.0 of `@actions/http-client`
### 5.0.1 ### 5.0.1
- [Update Octokit Dependencies](https://github.com/actions/toolkit/pull/1037) - [Update Octokit Dependencies](https://github.com/actions/toolkit/pull/1037)
### 5.0.0 ### 5.0.0
@@ -22,7 +22,7 @@ describe('@actions/github', () => {
proxyServer.listen(port, () => resolve()) proxyServer.listen(port, () => resolve())
}) })
proxyServer.on('connect', req => { proxyServer.on('connect', req => {
proxyConnects.push(req.url ?? '') proxyConnects.push(req.url)
}) })
}) })
+1 -1
View File
@@ -18,7 +18,7 @@ describe('@actions/github', () => {
proxyServer.listen(port, () => resolve(null)) proxyServer.listen(port, () => resolve(null))
}) })
proxyServer.on('connect', req => { proxyServer.on('connect', req => {
proxyConnects.push(req.url ?? '') proxyConnects.push(req.url)
}) })
}) })
+11 -11
View File
@@ -1,15 +1,15 @@
{ {
"name": "@actions/github", "name": "@actions/github",
"version": "5.1.1", "version": "5.0.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@actions/github", "name": "@actions/github",
"version": "5.1.1", "version": "5.0.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/http-client": "^2.0.1", "@actions/http-client": "^1.0.11",
"@octokit/core": "^3.6.0", "@octokit/core": "^3.6.0",
"@octokit/plugin-paginate-rest": "^2.17.0", "@octokit/plugin-paginate-rest": "^2.17.0",
"@octokit/plugin-rest-endpoint-methods": "^5.13.0" "@octokit/plugin-rest-endpoint-methods": "^5.13.0"
@@ -19,11 +19,11 @@
} }
}, },
"node_modules/@actions/http-client": { "node_modules/@actions/http-client": {
"version": "2.0.1", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"dependencies": { "dependencies": {
"tunnel": "^0.0.6" "tunnel": "0.0.6"
} }
}, },
"node_modules/@octokit/auth-token": { "node_modules/@octokit/auth-token": {
@@ -361,11 +361,11 @@
}, },
"dependencies": { "dependencies": {
"@actions/http-client": { "@actions/http-client": {
"version": "2.0.1", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"requires": { "requires": {
"tunnel": "^0.0.6" "tunnel": "0.0.6"
} }
}, },
"@octokit/auth-token": { "@octokit/auth-token": {
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/github", "name": "@actions/github",
"version": "5.1.1", "version": "5.0.1",
"description": "Actions github lib", "description": "Actions github lib",
"keywords": [ "keywords": [
"github", "github",
@@ -38,7 +38,7 @@
"url": "https://github.com/actions/toolkit/issues" "url": "https://github.com/actions/toolkit/issues"
}, },
"dependencies": { "dependencies": {
"@actions/http-client": "^2.0.1", "@actions/http-client": "^1.0.11",
"@octokit/core": "^3.6.0", "@octokit/core": "^3.6.0",
"@octokit/plugin-paginate-rest": "^2.17.0", "@octokit/plugin-paginate-rest": "^2.17.0",
"@octokit/plugin-rest-endpoint-methods": "^5.13.0" "@octokit/plugin-rest-endpoint-methods": "^5.13.0"
+3 -5
View File
@@ -2,7 +2,7 @@ import * as Context from './context'
import {GitHub, getOctokitOptions} from './utils' import {GitHub, getOctokitOptions} from './utils'
// octokit + plugins // octokit + plugins
import {OctokitOptions, OctokitPlugin} from '@octokit/core/dist-types/types' import {OctokitOptions} from '@octokit/core/dist-types/types'
export const context = new Context.Context() export const context = new Context.Context()
@@ -14,9 +14,7 @@ export const context = new Context.Context()
*/ */
export function getOctokit( export function getOctokit(
token: string, token: string,
options?: OctokitOptions, options?: OctokitOptions
...additionalPlugins: OctokitPlugin[]
): InstanceType<typeof GitHub> { ): InstanceType<typeof GitHub> {
const GitHubWithPlugins = GitHub.plugin(...additionalPlugins) return new GitHub(getOctokitOptions(token, options))
return new GitHubWithPlugins(getOctokitOptions(token, options))
} }
+1 -1
View File
@@ -10,7 +10,7 @@ import {paginateRest} from '@octokit/plugin-paginate-rest'
export const context = new Context.Context() export const context = new Context.Context()
const baseUrl = Utils.getApiBaseUrl() const baseUrl = Utils.getApiBaseUrl()
export const defaults: OctokitOptions = { const defaults = {
baseUrl, baseUrl,
request: { request: {
agent: Utils.getProxyAgent(baseUrl) agent: Utils.getProxyAgent(baseUrl)
-3
View File
@@ -1,8 +1,5 @@
# @actions/glob Releases # @actions/glob Releases
### 0.4.0
- Pass in the current workspace as a parameter to HashFiles [#1318](https://github.com/actions/toolkit/pull/1318)
### 0.3.0 ### 0.3.0
- Added a `verbose` option to HashFiles [#1052](https://github.com/actions/toolkit/pull/1052/files) - Added a `verbose` option to HashFiles [#1052](https://github.com/actions/toolkit/pull/1052/files)
+2 -7
View File
@@ -51,7 +51,6 @@ describe('globber', () => {
) )
}) })
const emptyDirectory = ''
it('followSymbolicLinks set to true', async () => { it('followSymbolicLinks set to true', async () => {
const root = path.join(getTestTemp(), 'set-to-true') const root = path.join(getTestTemp(), 'set-to-true')
await fs.mkdir(path.join(root, 'realdir'), {recursive: true}) await fs.mkdir(path.join(root, 'realdir'), {recursive: true})
@@ -61,9 +60,7 @@ describe('globber', () => {
path.join(root, 'symDir') path.join(root, 'symDir')
) )
const testPath = path.join(root, `symDir`) const testPath = path.join(root, `symDir`)
const hash = await hashFiles(testPath, emptyDirectory, { const hash = await hashFiles(testPath, {followSymbolicLinks: true})
followSymbolicLinks: true
})
expect(hash).toEqual( expect(hash).toEqual(
'd8a411e8f8643821bed189e627ff57151918aa554c00c10b31c693ab2dded273' 'd8a411e8f8643821bed189e627ff57151918aa554c00c10b31c693ab2dded273'
) )
@@ -83,9 +80,7 @@ describe('globber', () => {
path.join(root, 'symDir') path.join(root, 'symDir')
) )
const testPath = path.join(root, 'symdir') const testPath = path.join(root, 'symdir')
const hash = await hashFiles(testPath, emptyDirectory, { const hash = await hashFiles(testPath, {followSymbolicLinks: false})
followSymbolicLinks: false
})
expect(hash).toEqual('') expect(hash).toEqual('')
}) })
+47 -51
View File
@@ -1,45 +1,27 @@
{ {
"name": "@actions/glob", "name": "@actions/glob",
"version": "0.4.0", "version": "0.3.0",
"lockfileVersion": 3, "lockfileVersion": 2,
"requires": true, "requires": true,
"description": "Actions glob lib",
"files": [
"lib",
"!.DS_Store"
],
"homepage": "https://github.com/actions/toolkit/tree/main/packages/glob",
"keywords": [
"github",
"actions",
"glob"
],
"license": "MIT",
"main": "lib/glob.js",
"preview": true,
"types": "lib/glob.d.ts",
"packages": { "packages": {
"": { "": {
"name": "@actions/glob", "name": "@actions/glob",
"version": "0.4.0", "version": "0.2.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.9.1", "@actions/core": "^1.2.6",
"minimatch": "^3.0.4" "minimatch": "^3.0.4"
} }
}, },
"node_modules/@actions/core": { "node_modules/@actions/core": {
"version": "1.10.0", "version": "1.2.6",
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
"dependencies": { "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
"@actions/http-client": "^2.0.1",
"uuid": "^8.3.2"
}
}, },
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
@@ -53,12 +35,12 @@
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@@ -67,24 +49,38 @@
} }
} }
}, },
"bugs": { "dependencies": {
"url": "https://github.com/actions/toolkit/issues" "@actions/core": {
}, "version": "1.2.6",
"directories": { "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
"lib": "lib", "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
"test": "__tests__" },
}, "balanced-match": {
"publishConfig": { "version": "1.0.0",
"access": "public" "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
}, "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
"repository": { },
"directory": "packages/glob", "brace-expansion": {
"type": "git", "version": "1.1.11",
"url": "git+https://github.com/actions/toolkit.git" "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
}, "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"scripts": { "requires": {
"audit-moderate": "npm install && npm audit --json --audit-level=moderate > audit.json", "balanced-match": "^1.0.0",
"test": "echo \"Error: run tests from root\" && exit 1", "concat-map": "0.0.1"
"tsc": "tsc" }
},
"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"
}
}
} }
} }
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/glob", "name": "@actions/glob",
"version": "0.4.0", "version": "0.3.0",
"preview": true, "preview": true,
"description": "Actions glob lib", "description": "Actions glob lib",
"keywords": [ "keywords": [
@@ -37,7 +37,7 @@
"url": "https://github.com/actions/toolkit/issues" "url": "https://github.com/actions/toolkit/issues"
}, },
"dependencies": { "dependencies": {
"@actions/core": "^1.9.1", "@actions/core": "^1.2.6",
"minimatch": "^3.0.4" "minimatch": "^3.0.4"
} }
} }
+1 -4
View File
@@ -22,13 +22,10 @@ export async function create(
* Computes the sha256 hash of a glob * Computes the sha256 hash of a glob
* *
* @param patterns Patterns separated by newlines * @param patterns Patterns separated by newlines
* @param currentWorkspace Workspace used when matching files
* @param options Glob options * @param options Glob options
* @param verbose Enables verbose logging
*/ */
export async function hashFiles( export async function hashFiles(
patterns: string, patterns: string,
currentWorkspace = '',
options?: HashFileOptions, options?: HashFileOptions,
verbose: Boolean = false verbose: Boolean = false
): Promise<string> { ): Promise<string> {
@@ -37,5 +34,5 @@ export async function hashFiles(
followSymbolicLinks = options.followSymbolicLinks followSymbolicLinks = options.followSymbolicLinks
} }
const globber = await create(patterns, {followSymbolicLinks}) const globber = await create(patterns, {followSymbolicLinks})
return _hashFiles(globber, currentWorkspace, verbose) return _hashFiles(globber, verbose)
} }
+1 -4
View File
@@ -8,14 +8,11 @@ import {Globber} from './glob'
export async function hashFiles( export async function hashFiles(
globber: Globber, globber: Globber,
currentWorkspace: string,
verbose: Boolean = false verbose: Boolean = false
): Promise<string> { ): Promise<string> {
const writeDelegate = verbose ? core.info : core.debug const writeDelegate = verbose ? core.info : core.debug
let hasMatch = false let hasMatch = false
const githubWorkspace = currentWorkspace const githubWorkspace = process.env['GITHUB_WORKSPACE'] ?? process.cwd()
? currentWorkspace
: process.env['GITHUB_WORKSPACE'] ?? process.cwd()
const result = crypto.createHash('sha256') const result = crypto.createHash('sha256')
let count = 0 let count = 0
for await (const file of globber.globGenerator()) { for await (const file of globber.globGenerator()) {
+3
View File
@@ -1,2 +1,5 @@
_out
node_modules
.DS_Store
testoutput.txt testoutput.txt
npm-debug.log npm-debug.log
+19 -13
View File
@@ -1,11 +1,18 @@
# `@actions/http-client`
A lightweight HTTP client optimized for building actions. <p align="center">
<img src="actions.png">
</p>
# Actions Http-Client
[![Http Status](https://github.com/actions/http-client/workflows/http-tests/badge.svg)](https://github.com/actions/http-client/actions)
A lightweight HTTP client optimized for use with actions, TypeScript with generics and async await.
## Features ## Features
- HTTP client with TypeScript generics and async/await/Promises - HTTP client with TypeScript generics and async/await/Promises
- Typings included! - Typings included so no need to acquire separately (great for intellisense and no versioning drift)
- [Proxy support](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/about-self-hosted-runners#using-a-proxy-server-with-self-hosted-runners) just works with actions and the runner - [Proxy support](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/about-self-hosted-runners#using-a-proxy-server-with-self-hosted-runners) just works with actions and the runner
- Targets ES2019 (runner runs actions with node 12+). Only supported on node 12+. - Targets ES2019 (runner runs actions with node 12+). Only supported on node 12+.
- Basic, Bearer and PAT Support out of the box. Extensible handlers for others. - Basic, Bearer and PAT Support out of the box. Extensible handlers for others.
@@ -21,7 +28,7 @@ npm install @actions/http-client --save
## Samples ## Samples
See the [tests](./__tests__) for detailed examples. See the [HTTP](./__tests__) tests for detailed examples.
## Errors ## Errors
@@ -32,13 +39,13 @@ The HTTP client does not throw unless truly exceptional.
* A request that successfully executes resulting in a 404, 500 etc... will return a response object with a status code and a body. * A request that successfully executes resulting in a 404, 500 etc... will return a response object with a status code and a body.
* Redirects (3xx) will be followed by default. * Redirects (3xx) will be followed by default.
See the [tests](./__tests__) for detailed examples. See [HTTP tests](./__tests__) for detailed examples.
## Debugging ## Debugging
To enable detailed console logging of all HTTP requests and responses, set the NODE_DEBUG environment varible: To enable detailed console logging of all HTTP requests and responses, set the NODE_DEBUG environment varible:
```shell ```
export NODE_DEBUG=http export NODE_DEBUG=http
``` ```
@@ -56,18 +63,17 @@ We welcome PRs. Please create an issue and if applicable, a design before proce
once: once:
``` ```bash
npm install $ npm install
``` ```
To build: To build:
``` ```bash
npm run build $ npm run build
``` ```
To run all tests: To run all tests:
```bash
``` $ npm test
npm test
``` ```
-17
View File
@@ -1,22 +1,5 @@
## Releases ## Releases
## 2.1.0
- Add support for `*` and subdomains in `no_proxy` [#1355](https://github.com/actions/toolkit/pull/1355) [#1223](https://github.com/actions/toolkit/pull/1223)
- Bypass proxy for loopback IP adresses [#1360](https://github.com/actions/toolkit/pull/1360))
## 2.0.1
- Fix an issue with missing `tunnel` dependency [#1085](https://github.com/actions/toolkit/pull/1085)
## 2.0.0
- The package is now compiled with TypeScript's [`strict` compiler setting](https://www.typescriptlang.org/tsconfig#strict). To comply with stricter rules:
- Some exported types now include `| null` or `| undefined`, matching their actual behavior.
- Types implementing the method `RequestHandler.handleAuthentication()` now throw an `Error` rather than returning `null` if they do not support handling an HTTP 401 response. Callers can still use `canHandleAuthentication()` to determine if this handling is supported or not.
- Types using `any` have been scoped to more specific types.
- Following TypeScript's naming conventions, exported interfaces no longer begin with the prefix `I-`.
- Delete the `IHttpClientResponse` interface in favor of the `HttpClientResponse` class.
- Delete the `IHeaders` interface in favor of `http.OutgoingHttpHeaders`.
- The source code of the package was moved to build with [actions/toolkit](https://github.com/actions/toolkit).
## 1.0.11 ## 1.0.11
Contains a bug fix where proxy is defined without a user and password. see [PR here](https://github.com/actions/http-client/pull/42) Contains a bug fix where proxy is defined without a user and password. see [PR here](https://github.com/actions/http-client/pull/42)
+29 -41
View File
@@ -1,5 +1,5 @@
import * as httpm from '../lib' import * as httpm from '../_out'
import * as am from '../lib/auth' import * as am from '../_out/auth'
describe('auth', () => { describe('auth', () => {
beforeEach(() => {}) beforeEach(() => {})
@@ -7,67 +7,55 @@ describe('auth', () => {
afterEach(() => {}) afterEach(() => {})
it('does basic http get request with basic auth', async () => { it('does basic http get request with basic auth', async () => {
const bh: am.BasicCredentialHandler = new am.BasicCredentialHandler( let bh: am.BasicCredentialHandler = new am.BasicCredentialHandler(
'johndoe', 'johndoe',
'password' 'password'
) )
const http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [ let http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [bh])
bh let res: httpm.HttpClientResponse = await http.get('http://httpbin.org/get')
])
const res: httpm.HttpClientResponse = await http.get(
'http://postman-echo.com/get'
)
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
const auth: string = obj.headers.authorization let auth: string = obj.headers.Authorization
const creds: string = Buffer.from( let creds: string = Buffer.from(
auth.substring('Basic '.length), auth.substring('Basic '.length),
'base64' 'base64'
).toString() ).toString()
expect(creds).toBe('johndoe:password') expect(creds).toBe('johndoe:password')
expect(obj.url).toBe('http://postman-echo.com/get') expect(obj.url).toBe('http://httpbin.org/get')
}) })
it('does basic http get request with pat token auth', async () => { it('does basic http get request with pat token auth', async () => {
const token = 'scbfb44vxzku5l4xgc3qfazn3lpk4awflfryc76esaiq7aypcbhs' let token: string = 'scbfb44vxzku5l4xgc3qfazn3lpk4awflfryc76esaiq7aypcbhs'
const ph: am.PersonalAccessTokenCredentialHandler = new am.PersonalAccessTokenCredentialHandler( let ph: am.PersonalAccessTokenCredentialHandler = new am.PersonalAccessTokenCredentialHandler(
token token
) )
const http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [ let http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [ph])
ph let res: httpm.HttpClientResponse = await http.get('http://httpbin.org/get')
])
const res: httpm.HttpClientResponse = await http.get(
'http://postman-echo.com/get'
)
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
const auth: string = obj.headers.authorization let auth: string = obj.headers.Authorization
const creds: string = Buffer.from( let creds: string = Buffer.from(
auth.substring('Basic '.length), auth.substring('Basic '.length),
'base64' 'base64'
).toString() ).toString()
expect(creds).toBe(`PAT:${token}`) expect(creds).toBe('PAT:' + token)
expect(obj.url).toBe('http://postman-echo.com/get') expect(obj.url).toBe('http://httpbin.org/get')
}) })
it('does basic http get request with pat token auth', async () => { it('does basic http get request with pat token auth', async () => {
const token = 'scbfb44vxzku5l4xgc3qfazn3lpk4awflfryc76esaiq7aypcbhs' let token: string = 'scbfb44vxzku5l4xgc3qfazn3lpk4awflfryc76esaiq7aypcbhs'
const ph: am.BearerCredentialHandler = new am.BearerCredentialHandler(token) let ph: am.BearerCredentialHandler = new am.BearerCredentialHandler(token)
const http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [ let http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [ph])
ph let res: httpm.HttpClientResponse = await http.get('http://httpbin.org/get')
])
const res: httpm.HttpClientResponse = await http.get(
'http://postman-echo.com/get'
)
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
const auth: string = obj.headers.authorization let auth: string = obj.headers.Authorization
expect(auth).toBe(`Bearer ${token}`) expect(auth).toBe('Bearer ' + token)
expect(obj.url).toBe('http://postman-echo.com/get') expect(obj.url).toBe('http://httpbin.org/get')
}) })
}) })
+193 -194
View File
@@ -1,10 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ import * as httpm from '../_out'
import * as ifm from '../_out/interfaces'
import * as httpm from '..'
import * as path from 'path' import * as path from 'path'
import * as fs from 'fs' import * as fs from 'fs'
const sampleFilePath: string = path.join(__dirname, 'testoutput.txt') let sampleFilePath: string = path.join(__dirname, 'testoutput.txt')
interface HttpBinData { interface HttpBinData {
url: string url: string
@@ -24,7 +23,7 @@ describe('basics', () => {
afterEach(() => {}) afterEach(() => {})
it('constructs', () => { it('constructs', () => {
const http: httpm.HttpClient = new httpm.HttpClient('thttp-client-tests') let http: httpm.HttpClient = new httpm.HttpClient('thttp-client-tests')
expect(http).toBeDefined() expect(http).toBeDefined()
}) })
@@ -33,343 +32,343 @@ describe('basics', () => {
// "args": {}, // "args": {},
// "headers": { // "headers": {
// "Connection": "close", // "Connection": "close",
// "Host": "postman-echo.com", // "Host": "httpbin.org",
// "user-agent": "typed-test-client-tests" // "User-Agent": "typed-test-client-tests"
// }, // },
// "origin": "173.95.152.44", // "origin": "173.95.152.44",
// "url": "https://postman-echo.com/get" // "url": "https://httpbin.org/get"
// } // }
it('does basic http get request', async () => { it('does basic http get request', async done => {
const res: httpm.HttpClientResponse = await _http.get( let res: httpm.HttpClientResponse = await _http.get(
'http://postman-echo.com/get' 'http://httpbin.org/get'
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
expect(obj.url).toBe('http://postman-echo.com/get') expect(obj.url).toBe('http://httpbin.org/get')
expect(obj.headers['user-agent']).toBeTruthy() expect(obj.headers['User-Agent']).toBeTruthy()
done()
}) })
it('does basic http get request with no user agent', async () => { it('does basic http get request with no user agent', async done => {
const http: httpm.HttpClient = new httpm.HttpClient() let http: httpm.HttpClient = new httpm.HttpClient()
const res: httpm.HttpClientResponse = await http.get( let res: httpm.HttpClientResponse = await http.get('http://httpbin.org/get')
'http://postman-echo.com/get' expect(res.message.statusCode).toBe(200)
let body: string = await res.readBody()
let obj: any = JSON.parse(body)
expect(obj.url).toBe('http://httpbin.org/get')
expect(obj.headers['User-Agent']).toBeFalsy()
done()
})
it('does basic https get request', async done => {
let res: httpm.HttpClientResponse = await _http.get(
'https://httpbin.org/get'
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
expect(obj.url).toBe('http://postman-echo.com/get') expect(obj.url).toBe('https://httpbin.org/get')
expect(obj.headers['user-agent']).toBeFalsy() done()
}) })
it('does basic https get request', async () => { it('does basic http get request with default headers', async done => {
const res: httpm.HttpClientResponse = await _http.get( let http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [], {
'https://postman-echo.com/get' headers: {
) Accept: 'application/json',
expect(res.message.statusCode).toBe(200) 'Content-Type': 'application/json'
const body: string = await res.readBody()
const obj = JSON.parse(body)
expect(obj.url).toBe('https://postman-echo.com/get')
})
it('does basic http get request with default headers', async () => {
const http: httpm.HttpClient = new httpm.HttpClient(
'http-client-tests',
[],
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
} }
) })
const res: httpm.HttpClientResponse = await http.get( let res: httpm.HttpClientResponse = await http.get('http://httpbin.org/get')
'http://postman-echo.com/get'
)
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
expect(obj.headers.accept).toBe('application/json') expect(obj.headers.Accept).toBe('application/json')
expect(obj.headers['content-type']).toBe('application/json') expect(obj.headers['Content-Type']).toBe('application/json')
expect(obj.url).toBe('http://postman-echo.com/get') expect(obj.url).toBe('http://httpbin.org/get')
done()
}) })
it('does basic http get request with merged headers', async () => { it('does basic http get request with merged headers', async done => {
const http: httpm.HttpClient = new httpm.HttpClient( let http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [], {
'http-client-tests', headers: {
[], Accept: 'application/json',
{ 'Content-Type': 'application/json'
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
} }
) })
const res: httpm.HttpClientResponse = await http.get( let res: httpm.HttpClientResponse = await http.get(
'http://postman-echo.com/get', 'http://httpbin.org/get',
{ {
'content-type': 'application/x-www-form-urlencoded' 'content-type': 'application/x-www-form-urlencoded'
} }
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
expect(obj.headers.accept).toBe('application/json') expect(obj.headers.Accept).toBe('application/json')
expect(obj.headers['content-type']).toBe( expect(obj.headers['Content-Type']).toBe(
'application/x-www-form-urlencoded' 'application/x-www-form-urlencoded'
) )
expect(obj.url).toBe('http://postman-echo.com/get') expect(obj.url).toBe('http://httpbin.org/get')
done()
}) })
it('pipes a get request', async () => { it('pipes a get request', () => {
return new Promise<void>(async resolve => { return new Promise<string>(async (resolve, reject) => {
const file = fs.createWriteStream(sampleFilePath) let file: NodeJS.WritableStream = fs.createWriteStream(sampleFilePath)
;(await _http.get('https://postman-echo.com/get')).message ;(await _http.get('https://httpbin.org/get')).message
.pipe(file) .pipe(file)
.on('close', () => { .on('close', () => {
const body: string = fs.readFileSync(sampleFilePath).toString() let body: string = fs.readFileSync(sampleFilePath).toString()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
expect(obj.url).toBe('https://postman-echo.com/get') expect(obj.url).toBe('https://httpbin.org/get')
resolve() resolve()
}) })
}) })
}) })
it('does basic get request with redirects', async () => { it('does basic get request with redirects', async done => {
const res: httpm.HttpClientResponse = await _http.get( let res: httpm.HttpClientResponse = await _http.get(
`https://postman-echo.com/redirect-to?url=${encodeURIComponent( 'https://httpbin.org/redirect-to?url=' +
'https://postman-echo.com/get' encodeURIComponent('https://httpbin.org/get')
)}`
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
expect(obj.url).toBe('https://postman-echo.com/get') expect(obj.url).toBe('https://httpbin.org/get')
done()
}) })
it('does basic get request with redirects (303)', async () => { it('does basic get request with redirects (303)', async done => {
const res: httpm.HttpClientResponse = await _http.get( let res: httpm.HttpClientResponse = await _http.get(
`https://postman-echo.com/redirect-to?url=${encodeURIComponent( 'https://httpbin.org/redirect-to?url=' +
'https://postman-echo.com/get' encodeURIComponent('https://httpbin.org/get') +
)}&status_code=303` '&status_code=303'
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
expect(obj.url).toBe('https://postman-echo.com/get') expect(obj.url).toBe('https://httpbin.org/get')
done()
}) })
it('returns 404 for not found get request on redirect', async () => { it('returns 404 for not found get request on redirect', async done => {
const res: httpm.HttpClientResponse = await _http.get( let res: httpm.HttpClientResponse = await _http.get(
`https://postman-echo.com/redirect-to?url=${encodeURIComponent( 'https://httpbin.org/redirect-to?url=' +
'https://postman-echo.com/status/404' encodeURIComponent('https://httpbin.org/status/404') +
)}&status_code=303` '&status_code=303'
) )
expect(res.message.statusCode).toBe(404) expect(res.message.statusCode).toBe(404)
await res.readBody() let body: string = await res.readBody()
done()
}) })
it('does not follow redirects if disabled', async () => { it('does not follow redirects if disabled', async done => {
const http: httpm.HttpClient = new httpm.HttpClient( let http: httpm.HttpClient = new httpm.HttpClient(
'typed-test-client-tests', 'typed-test-client-tests',
undefined, null,
{allowRedirects: false} {allowRedirects: false}
) )
const res: httpm.HttpClientResponse = await http.get( let res: httpm.HttpClientResponse = await http.get(
`https://postman-echo.com/redirect-to?url=${encodeURIComponent( 'https://httpbin.org/redirect-to?url=' +
'https://postman-echo.com/get' encodeURIComponent('https://httpbin.org/get')
)}`
) )
expect(res.message.statusCode).toBe(302) expect(res.message.statusCode).toBe(302)
await res.readBody() let body: string = await res.readBody()
done()
}) })
it('does not pass auth with diff hostname redirects', async () => { it('does not pass auth with diff hostname redirects', async done => {
const headers = { let headers = {
accept: 'application/json', accept: 'application/json',
authorization: 'shhh' authorization: 'shhh'
} }
const res: httpm.HttpClientResponse = await _http.get( let res: httpm.HttpClientResponse = await _http.get(
`https://postman-echo.com/redirect-to?url=${encodeURIComponent( 'https://httpbin.org/redirect-to?url=' +
'https://www.postman-echo.com/get' encodeURIComponent('https://www.httpbin.org/get'),
)}`,
headers headers
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
// httpbin "fixes" the casing // httpbin "fixes" the casing
expect(obj.headers[httpm.Headers.Accept]).toBe('application/json') expect(obj.headers['Accept']).toBe('application/json')
expect(obj.headers['Authorization']).toBeUndefined() expect(obj.headers['Authorization']).toBeUndefined()
expect(obj.headers['authorization']).toBeUndefined() expect(obj.headers['authorization']).toBeUndefined()
expect(obj.url).toBe('https://www.postman-echo.com/get') expect(obj.url).toBe('https://www.httpbin.org/get')
done()
}) })
it('does not pass Auth with diff hostname redirects', async () => { it('does not pass Auth with diff hostname redirects', async done => {
const headers = { let headers = {
Accept: 'application/json', Accept: 'application/json',
Authorization: 'shhh' Authorization: 'shhh'
} }
const res: httpm.HttpClientResponse = await _http.get( let res: httpm.HttpClientResponse = await _http.get(
`https://postman-echo.com/redirect-to?url=${encodeURIComponent( 'https://httpbin.org/redirect-to?url=' +
'https://www.postman-echo.com/get' encodeURIComponent('https://www.httpbin.org/get'),
)}`,
headers headers
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
// httpbin "fixes" the casing // httpbin "fixes" the casing
expect(obj.headers[httpm.Headers.Accept]).toBe('application/json') expect(obj.headers['Accept']).toBe('application/json')
expect(obj.headers['Authorization']).toBeUndefined() expect(obj.headers['Authorization']).toBeUndefined()
expect(obj.headers['authorization']).toBeUndefined() expect(obj.headers['authorization']).toBeUndefined()
expect(obj.url).toBe('https://www.postman-echo.com/get') expect(obj.url).toBe('https://www.httpbin.org/get')
done()
}) })
it('does basic head request', async () => { it('does basic head request', async done => {
const res: httpm.HttpClientResponse = await _http.head( let res: httpm.HttpClientResponse = await _http.head(
'http://postman-echo.com/get' 'http://httpbin.org/get'
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
done()
}) })
it('does basic http delete request', async () => { it('does basic http delete request', async done => {
const res: httpm.HttpClientResponse = await _http.del( let res: httpm.HttpClientResponse = await _http.del(
'http://postman-echo.com/delete' 'http://httpbin.org/delete'
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
JSON.parse(body) let obj: any = JSON.parse(body)
done()
}) })
it('does basic http post request', async () => { it('does basic http post request', async done => {
const b = 'Hello World!' let b: string = 'Hello World!'
const res: httpm.HttpClientResponse = await _http.post( let res: httpm.HttpClientResponse = await _http.post(
'http://postman-echo.com/post', 'http://httpbin.org/post',
b b
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
expect(obj.data).toBe(b) expect(obj.data).toBe(b)
expect(obj.url).toBe('http://postman-echo.com/post') expect(obj.url).toBe('http://httpbin.org/post')
done()
}) })
it('does basic http patch request', async () => { it('does basic http patch request', async done => {
const b = 'Hello World!' let b: string = 'Hello World!'
const res: httpm.HttpClientResponse = await _http.patch( let res: httpm.HttpClientResponse = await _http.patch(
'http://postman-echo.com/patch', 'http://httpbin.org/patch',
b b
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
expect(obj.data).toBe(b) expect(obj.data).toBe(b)
expect(obj.url).toBe('http://postman-echo.com/patch') expect(obj.url).toBe('http://httpbin.org/patch')
done()
}) })
it('does basic http options request', async () => { it('does basic http options request', async done => {
const res: httpm.HttpClientResponse = await _http.options( let res: httpm.HttpClientResponse = await _http.options(
'http://postman-echo.com' 'http://httpbin.org'
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
await res.readBody() let body: string = await res.readBody()
done()
}) })
it('returns 404 for not found get request', async () => { it('returns 404 for not found get request', async done => {
const res: httpm.HttpClientResponse = await _http.get( let res: httpm.HttpClientResponse = await _http.get(
'http://postman-echo.com/status/404' 'http://httpbin.org/status/404'
) )
expect(res.message.statusCode).toBe(404) expect(res.message.statusCode).toBe(404)
await res.readBody() let body: string = await res.readBody()
done()
}) })
it('gets a json object', async () => { it('gets a json object', async () => {
const jsonObj = await _http.getJson<HttpBinData>( let jsonObj: ifm.ITypedResponse<HttpBinData> = await _http.getJson<
'https://postman-echo.com/get' HttpBinData
) >('https://httpbin.org/get')
expect(jsonObj.statusCode).toBe(200) expect(jsonObj.statusCode).toBe(200)
expect(jsonObj.result).toBeDefined() expect(jsonObj.result).toBeDefined()
expect(jsonObj.result?.url).toBe('https://postman-echo.com/get') expect(jsonObj.result.url).toBe('https://httpbin.org/get')
expect(jsonObj.result?.headers[httpm.Headers.Accept]).toBe( expect(jsonObj.result.headers['Accept']).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
expect(jsonObj.headers['content-type']).toContain( expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
}) })
it('getting a non existent json object returns null', async () => { it('getting a non existent json object returns null', async () => {
const jsonObj = await _http.getJson<HttpBinData>( let jsonObj: ifm.ITypedResponse<HttpBinData> = await _http.getJson<
'https://postman-echo.com/status/404' HttpBinData
) >('https://httpbin.org/status/404')
expect(jsonObj.statusCode).toBe(404) expect(jsonObj.statusCode).toBe(404)
expect(jsonObj.result).toBeNull() expect(jsonObj.result).toBeNull()
}) })
it('posts a json object', async () => { it('posts a json object', async () => {
const res = {name: 'foo'} let res: any = {name: 'foo'}
const restRes = await _http.postJson<HttpBinData>( let restRes: ifm.ITypedResponse<HttpBinData> = await _http.postJson<
'https://postman-echo.com/post', HttpBinData
res >('https://httpbin.org/post', res)
)
expect(restRes.statusCode).toBe(200) expect(restRes.statusCode).toBe(200)
expect(restRes.result).toBeDefined() expect(restRes.result).toBeDefined()
expect(restRes.result?.url).toBe('https://postman-echo.com/post') expect(restRes.result.url).toBe('https://httpbin.org/post')
expect(restRes.result?.json.name).toBe('foo') expect(restRes.result.json.name).toBe('foo')
expect(restRes.result?.headers[httpm.Headers.Accept]).toBe( expect(restRes.result.headers['Accept']).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
expect(restRes.result?.headers['content-type']).toBe( expect(restRes.result.headers['Content-Type']).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
expect(restRes.headers['content-type']).toContain( expect(restRes.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
}) })
it('puts a json object', async () => { it('puts a json object', async () => {
const res = {name: 'foo'} let res: any = {name: 'foo'}
const restRes = await _http.putJson<HttpBinData>( let restRes: ifm.ITypedResponse<HttpBinData> = await _http.putJson<
'https://postman-echo.com/put', HttpBinData
res >('https://httpbin.org/put', res)
)
expect(restRes.statusCode).toBe(200) expect(restRes.statusCode).toBe(200)
expect(restRes.result).toBeDefined() expect(restRes.result).toBeDefined()
expect(restRes.result?.url).toBe('https://postman-echo.com/put') expect(restRes.result.url).toBe('https://httpbin.org/put')
expect(restRes.result?.json.name).toBe('foo') expect(restRes.result.json.name).toBe('foo')
expect(restRes.result?.headers[httpm.Headers.Accept]).toBe( expect(restRes.result.headers['Accept']).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
expect(restRes.result?.headers['content-type']).toBe( expect(restRes.result.headers['Content-Type']).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
expect(restRes.headers['content-type']).toContain( expect(restRes.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
}) })
it('patch a json object', async () => { it('patch a json object', async () => {
const res = {name: 'foo'} let res: any = {name: 'foo'}
const restRes = await _http.patchJson<HttpBinData>( let restRes: ifm.ITypedResponse<HttpBinData> = await _http.patchJson<
'https://postman-echo.com/patch', HttpBinData
res >('https://httpbin.org/patch', res)
)
expect(restRes.statusCode).toBe(200) expect(restRes.statusCode).toBe(200)
expect(restRes.result).toBeDefined() expect(restRes.result).toBeDefined()
expect(restRes.result?.url).toBe('https://postman-echo.com/patch') expect(restRes.result.url).toBe('https://httpbin.org/patch')
expect(restRes.result?.json.name).toBe('foo') expect(restRes.result.json.name).toBe('foo')
expect(restRes.result?.headers[httpm.Headers.Accept]).toBe( expect(restRes.result.headers['Accept']).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
expect(restRes.result?.headers['content-type']).toBe( expect(restRes.result.headers['Content-Type']).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
expect(restRes.headers['content-type']).toContain( expect(restRes.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
}) })
+38 -42
View File
@@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ import * as httpm from '../_out'
import * as ifm from '../_out/interfaces'
import * as httpm from '..'
describe('headers', () => { describe('headers', () => {
let _http: httpm.HttpClient let _http: httpm.HttpClient
@@ -10,109 +9,106 @@ describe('headers', () => {
}) })
it('preserves existing headers on getJson', async () => { it('preserves existing headers on getJson', async () => {
const additionalHeaders = {[httpm.Headers.Accept]: 'foo'} let additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
let jsonObj = await _http.getJson<any>( let jsonObj: ifm.ITypedResponse<any> = await _http.getJson<any>(
'https://postman-echo.com/get', 'https://httpbin.org/get',
additionalHeaders additionalHeaders
) )
expect(jsonObj.result.headers[httpm.Headers.Accept]).toBe('foo') expect(jsonObj.result.headers['Accept']).toBe('foo')
expect(jsonObj.headers[httpm.Headers.ContentType]).toContain( expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
const httpWithHeaders = new httpm.HttpClient() let httpWithHeaders = new httpm.HttpClient()
httpWithHeaders.requestOptions = { httpWithHeaders.requestOptions = {
headers: { headers: {
[httpm.Headers.Accept]: 'baz' [httpm.Headers.Accept]: 'baz'
} }
} }
jsonObj = await httpWithHeaders.getJson<any>('https://postman-echo.com/get') jsonObj = await httpWithHeaders.getJson<any>('https://httpbin.org/get')
expect(jsonObj.result.headers[httpm.Headers.Accept]).toBe('baz') expect(jsonObj.result.headers['Accept']).toBe('baz')
expect(jsonObj.headers[httpm.Headers.ContentType]).toContain( expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
}) })
it('preserves existing headers on postJson', async () => { it('preserves existing headers on postJson', async () => {
const additionalHeaders = {[httpm.Headers.Accept]: 'foo'} let additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
let jsonObj = await _http.postJson<any>( let jsonObj: ifm.ITypedResponse<any> = await _http.postJson<any>(
'https://postman-echo.com/post', 'https://httpbin.org/post',
{}, {},
additionalHeaders additionalHeaders
) )
expect(jsonObj.result.headers[httpm.Headers.Accept]).toBe('foo') expect(jsonObj.result.headers['Accept']).toBe('foo')
expect(jsonObj.headers[httpm.Headers.ContentType]).toContain( expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
const httpWithHeaders = new httpm.HttpClient() let httpWithHeaders = new httpm.HttpClient()
httpWithHeaders.requestOptions = { httpWithHeaders.requestOptions = {
headers: { headers: {
[httpm.Headers.Accept]: 'baz' [httpm.Headers.Accept]: 'baz'
} }
} }
jsonObj = await httpWithHeaders.postJson<any>( jsonObj = await httpWithHeaders.postJson<any>(
'https://postman-echo.com/post', 'https://httpbin.org/post',
{} {}
) )
expect(jsonObj.result.headers[httpm.Headers.Accept]).toBe('baz') expect(jsonObj.result.headers['Accept']).toBe('baz')
expect(jsonObj.headers[httpm.Headers.ContentType]).toContain( expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
}) })
it('preserves existing headers on putJson', async () => { it('preserves existing headers on putJson', async () => {
const additionalHeaders = {[httpm.Headers.Accept]: 'foo'} let additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
let jsonObj = await _http.putJson<any>( let jsonObj: ifm.ITypedResponse<any> = await _http.putJson<any>(
'https://postman-echo.com/put', 'https://httpbin.org/put',
{}, {},
additionalHeaders additionalHeaders
) )
expect(jsonObj.result.headers[httpm.Headers.Accept]).toBe('foo') expect(jsonObj.result.headers['Accept']).toBe('foo')
expect(jsonObj.headers[httpm.Headers.ContentType]).toContain( expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
const httpWithHeaders = new httpm.HttpClient() let httpWithHeaders = new httpm.HttpClient()
httpWithHeaders.requestOptions = { httpWithHeaders.requestOptions = {
headers: { headers: {
[httpm.Headers.Accept]: 'baz' [httpm.Headers.Accept]: 'baz'
} }
} }
jsonObj = await httpWithHeaders.putJson<any>( jsonObj = await httpWithHeaders.putJson<any>('https://httpbin.org/put', {})
'https://postman-echo.com/put', expect(jsonObj.result.headers['Accept']).toBe('baz')
{} expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
)
expect(jsonObj.result.headers[httpm.Headers.Accept]).toBe('baz')
expect(jsonObj.headers[httpm.Headers.ContentType]).toContain(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
}) })
it('preserves existing headers on patchJson', async () => { it('preserves existing headers on patchJson', async () => {
const additionalHeaders = {[httpm.Headers.Accept]: 'foo'} let additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
let jsonObj = await _http.patchJson<any>( let jsonObj: ifm.ITypedResponse<any> = await _http.patchJson<any>(
'https://postman-echo.com/patch', 'https://httpbin.org/patch',
{}, {},
additionalHeaders additionalHeaders
) )
expect(jsonObj.result.headers[httpm.Headers.Accept]).toBe('foo') expect(jsonObj.result.headers['Accept']).toBe('foo')
expect(jsonObj.headers[httpm.Headers.ContentType]).toContain( expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
const httpWithHeaders = new httpm.HttpClient() let httpWithHeaders = new httpm.HttpClient()
httpWithHeaders.requestOptions = { httpWithHeaders.requestOptions = {
headers: { headers: {
[httpm.Headers.Accept]: 'baz' [httpm.Headers.Accept]: 'baz'
} }
} }
jsonObj = await httpWithHeaders.patchJson<any>( jsonObj = await httpWithHeaders.patchJson<any>(
'https://postman-echo.com/patch', 'https://httpbin.org/patch',
{} {}
) )
expect(jsonObj.result.headers[httpm.Headers.Accept]).toBe('baz') expect(jsonObj.result.headers['Accept']).toBe('baz')
expect(jsonObj.headers[httpm.Headers.ContentType]).toContain( expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
}) })
@@ -1,4 +1,4 @@
import * as httpm from '../lib' import * as httpm from '../_out'
describe('basics', () => { describe('basics', () => {
let _http: httpm.HttpClient let _http: httpm.HttpClient
@@ -11,63 +11,69 @@ describe('basics', () => {
_http.dispose() _http.dispose()
}) })
it('does basic http get request with keepAlive true', async () => { it('does basic http get request with keepAlive true', async done => {
const res: httpm.HttpClientResponse = await _http.get( let res: httpm.HttpClientResponse = await _http.get(
'http://postman-echo.com/get' 'http://httpbin.org/get'
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
expect(obj.url).toBe('http://postman-echo.com/get') expect(obj.url).toBe('http://httpbin.org/get')
done()
}) })
it('does basic head request with keepAlive true', async () => { it('does basic head request with keepAlive true', async done => {
const res: httpm.HttpClientResponse = await _http.head( let res: httpm.HttpClientResponse = await _http.head(
'http://postman-echo.com/get' 'http://httpbin.org/get'
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
done()
}) })
it('does basic http delete request with keepAlive true', async () => { it('does basic http delete request with keepAlive true', async done => {
const res: httpm.HttpClientResponse = await _http.del( let res: httpm.HttpClientResponse = await _http.del(
'http://postman-echo.com/delete' 'http://httpbin.org/delete'
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
JSON.parse(body) let obj: any = JSON.parse(body)
done()
}) })
it('does basic http post request with keepAlive true', async () => { it('does basic http post request with keepAlive true', async done => {
const b = 'Hello World!' let b: string = 'Hello World!'
const res: httpm.HttpClientResponse = await _http.post( let res: httpm.HttpClientResponse = await _http.post(
'http://postman-echo.com/post', 'http://httpbin.org/post',
b b
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
expect(obj.data).toBe(b) expect(obj.data).toBe(b)
expect(obj.url).toBe('http://postman-echo.com/post') expect(obj.url).toBe('http://httpbin.org/post')
done()
}) })
it('does basic http patch request with keepAlive true', async () => { it('does basic http patch request with keepAlive true', async done => {
const b = 'Hello World!' let b: string = 'Hello World!'
const res: httpm.HttpClientResponse = await _http.patch( let res: httpm.HttpClientResponse = await _http.patch(
'http://postman-echo.com/patch', 'http://httpbin.org/patch',
b b
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
expect(obj.data).toBe(b) expect(obj.data).toBe(b)
expect(obj.url).toBe('http://postman-echo.com/patch') expect(obj.url).toBe('http://httpbin.org/patch')
done()
}) })
it('does basic http options request with keepAlive true', async () => { it('does basic http options request with keepAlive true', async done => {
const res: httpm.HttpClientResponse = await _http.options( let res: httpm.HttpClientResponse = await _http.options(
'http://postman-echo.com' 'http://httpbin.org'
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
await res.readBody() let body: string = await res.readBody()
done()
}) })
}) })
+53 -131
View File
@@ -1,25 +1,23 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as http from 'http' import * as http from 'http'
import * as httpm from '../lib/' import * as httpm from '../_out'
import * as pm from '../lib/proxy' import * as pm from '../_out/proxy'
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports import * as proxy from 'proxy'
const proxy = require('proxy') import * as tunnelm from 'tunnel'
let _proxyConnects: string[] let _proxyConnects: string[]
let _proxyServer: http.Server let _proxyServer: http.Server
const _proxyUrl = 'http://127.0.0.1:8080' let _proxyUrl = 'http://127.0.0.1:8080'
describe('proxy', () => { describe('proxy', () => {
beforeAll(async () => { beforeAll(async () => {
// Start proxy server // Start proxy server
_proxyServer = proxy() _proxyServer = proxy()
await new Promise<void>(resolve => { await new Promise(resolve => {
const port = Number(_proxyUrl.split(':')[2]) const port = Number(_proxyUrl.split(':')[2])
_proxyServer.listen(port, () => resolve()) _proxyServer.listen(port, () => resolve())
}) })
_proxyServer.on('connect', req => { _proxyServer.on('connect', req => {
_proxyConnects.push(req.url ?? '') _proxyConnects.push(req.url)
}) })
}) })
@@ -34,250 +32,175 @@ describe('proxy', () => {
_clearVars() _clearVars()
// Stop proxy server // Stop proxy server
await new Promise<void>(resolve => { await new Promise(resolve => {
_proxyServer.once('close', () => resolve()) _proxyServer.once('close', () => resolve())
_proxyServer.close() _proxyServer.close()
}) })
}) })
it('getProxyUrl does not return proxyUrl if variables not set', () => { it('getProxyUrl does not return proxyUrl if variables not set', () => {
const proxyUrl = pm.getProxyUrl(new URL('https://github.com')) let proxyUrl = pm.getProxyUrl(new URL('https://github.com'))
expect(proxyUrl).toBeUndefined() expect(proxyUrl).toBeUndefined()
}) })
it('getProxyUrl returns proxyUrl if https_proxy set for https url', () => { it('getProxyUrl returns proxyUrl if https_proxy set for https url', () => {
process.env['https_proxy'] = 'https://myproxysvr' process.env['https_proxy'] = 'https://myproxysvr'
const proxyUrl = pm.getProxyUrl(new URL('https://github.com')) let proxyUrl = pm.getProxyUrl(new URL('https://github.com'))
expect(proxyUrl).toBeDefined() expect(proxyUrl).toBeDefined()
}) })
it('getProxyUrl does not return proxyUrl if http_proxy set for https url', () => { it('getProxyUrl does not return proxyUrl if http_proxy set for https url', () => {
process.env['http_proxy'] = 'https://myproxysvr' process.env['http_proxy'] = 'https://myproxysvr'
const proxyUrl = pm.getProxyUrl(new URL('https://github.com')) let proxyUrl = pm.getProxyUrl(new URL('https://github.com'))
expect(proxyUrl).toBeUndefined() expect(proxyUrl).toBeUndefined()
}) })
it('getProxyUrl returns proxyUrl if http_proxy set for http url', () => { it('getProxyUrl returns proxyUrl if http_proxy set for http url', () => {
process.env['http_proxy'] = 'http://myproxysvr' process.env['http_proxy'] = 'http://myproxysvr'
const proxyUrl = pm.getProxyUrl(new URL('http://github.com')) let proxyUrl = pm.getProxyUrl(new URL('http://github.com'))
expect(proxyUrl).toBeDefined() expect(proxyUrl).toBeDefined()
}) })
it('getProxyUrl does not return proxyUrl if https_proxy set and in no_proxy list', () => { it('getProxyUrl does not return proxyUrl if https_proxy set and in no_proxy list', () => {
process.env['https_proxy'] = 'https://myproxysvr' process.env['https_proxy'] = 'https://myproxysvr'
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080' process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
const proxyUrl = pm.getProxyUrl(new URL('https://myserver')) let proxyUrl = pm.getProxyUrl(new URL('https://myserver'))
expect(proxyUrl).toBeUndefined() expect(proxyUrl).toBeUndefined()
}) })
it('getProxyUrl returns proxyUrl if https_proxy set and not in no_proxy list', () => { it('getProxyUrl returns proxyUrl if https_proxy set and not in no_proxy list', () => {
process.env['https_proxy'] = 'https://myproxysvr' process.env['https_proxy'] = 'https://myproxysvr'
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080' process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
const proxyUrl = pm.getProxyUrl(new URL('https://github.com')) let proxyUrl = pm.getProxyUrl(new URL('https://github.com'))
expect(proxyUrl).toBeDefined() expect(proxyUrl).toBeDefined()
}) })
it('getProxyUrl does not return proxyUrl if http_proxy set and in no_proxy list', () => { it('getProxyUrl does not return proxyUrl if http_proxy set and in no_proxy list', () => {
process.env['http_proxy'] = 'http://myproxysvr' process.env['http_proxy'] = 'http://myproxysvr'
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080' process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
const proxyUrl = pm.getProxyUrl(new URL('http://myserver')) let proxyUrl = pm.getProxyUrl(new URL('http://myserver'))
expect(proxyUrl).toBeUndefined() expect(proxyUrl).toBeUndefined()
}) })
it('getProxyUrl returns proxyUrl if http_proxy set and not in no_proxy list', () => { it('getProxyUrl returns proxyUrl if http_proxy set and not in no_proxy list', () => {
process.env['http_proxy'] = 'http://myproxysvr' process.env['http_proxy'] = 'http://myproxysvr'
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080' process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
const proxyUrl = pm.getProxyUrl(new URL('http://github.com')) let proxyUrl = pm.getProxyUrl(new URL('http://github.com'))
expect(proxyUrl).toBeDefined() expect(proxyUrl).toBeDefined()
}) })
it('getProxyUrl returns proxyUrl if http_proxy has no protocol', () => {
process.env['http_proxy'] = 'myproxysvr'
const proxyUrl = pm.getProxyUrl(new URL('http://github.com'))
expect(proxyUrl?.toString()).toBe('http://myproxysvr/')
})
it('checkBypass returns true if host as no_proxy list', () => { it('checkBypass returns true if host as no_proxy list', () => {
process.env['no_proxy'] = 'myserver' process.env['no_proxy'] = 'myserver'
const bypass = pm.checkBypass(new URL('https://myserver')) let bypass = pm.checkBypass(new URL('https://myserver'))
expect(bypass).toBeTruthy() expect(bypass).toBeTruthy()
}) })
it('checkBypass returns true if host in no_proxy list', () => { it('checkBypass returns true if host in no_proxy list', () => {
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080' process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
const bypass = pm.checkBypass(new URL('https://myserver')) let bypass = pm.checkBypass(new URL('https://myserver'))
expect(bypass).toBeTruthy() expect(bypass).toBeTruthy()
}) })
it('checkBypass returns true if host in no_proxy list with spaces', () => { it('checkBypass returns true if host in no_proxy list with spaces', () => {
process.env['no_proxy'] = 'otherserver, myserver ,anotherserver:8080' process.env['no_proxy'] = 'otherserver, myserver ,anotherserver:8080'
const bypass = pm.checkBypass(new URL('https://myserver')) let bypass = pm.checkBypass(new URL('https://myserver'))
expect(bypass).toBeTruthy() expect(bypass).toBeTruthy()
}) })
it('checkBypass returns true if host in no_proxy list with port', () => { it('checkBypass returns true if host in no_proxy list with port', () => {
process.env['no_proxy'] = 'otherserver, myserver:8080 ,anotherserver' process.env['no_proxy'] = 'otherserver, myserver:8080 ,anotherserver'
const bypass = pm.checkBypass(new URL('https://myserver:8080')) let bypass = pm.checkBypass(new URL('https://myserver:8080'))
expect(bypass).toBeTruthy() expect(bypass).toBeTruthy()
}) })
it('checkBypass returns true if host with port in no_proxy list without port', () => { it('checkBypass returns true if host with port in no_proxy list without port', () => {
process.env['no_proxy'] = 'otherserver, myserver ,anotherserver' process.env['no_proxy'] = 'otherserver, myserver ,anotherserver'
const bypass = pm.checkBypass(new URL('https://myserver:8080')) let bypass = pm.checkBypass(new URL('https://myserver:8080'))
expect(bypass).toBeTruthy() expect(bypass).toBeTruthy()
}) })
it('checkBypass returns true if host in no_proxy list with default https port', () => { it('checkBypass returns true if host in no_proxy list with default https port', () => {
process.env['no_proxy'] = 'otherserver, myserver:443 ,anotherserver' process.env['no_proxy'] = 'otherserver, myserver:443 ,anotherserver'
const bypass = pm.checkBypass(new URL('https://myserver')) let bypass = pm.checkBypass(new URL('https://myserver'))
expect(bypass).toBeTruthy() expect(bypass).toBeTruthy()
}) })
it('checkBypass returns true if host in no_proxy list with default http port', () => { it('checkBypass returns true if host in no_proxy list with default http port', () => {
process.env['no_proxy'] = 'otherserver, myserver:80 ,anotherserver' process.env['no_proxy'] = 'otherserver, myserver:80 ,anotherserver'
const bypass = pm.checkBypass(new URL('http://myserver')) let bypass = pm.checkBypass(new URL('http://myserver'))
expect(bypass).toBeTruthy() expect(bypass).toBeTruthy()
}) })
it('checkBypass returns false if host not in no_proxy list', () => { it('checkBypass returns false if host not in no_proxy list', () => {
process.env['no_proxy'] = 'otherserver, myserver ,anotherserver:8080' process.env['no_proxy'] = 'otherserver, myserver ,anotherserver:8080'
const bypass = pm.checkBypass(new URL('https://github.com')) let bypass = pm.checkBypass(new URL('https://github.com'))
expect(bypass).toBeFalsy() expect(bypass).toBeFalsy()
}) })
it('checkBypass returns false if empty no_proxy', () => { it('checkBypass returns false if empty no_proxy', () => {
process.env['no_proxy'] = '' process.env['no_proxy'] = ''
const bypass = pm.checkBypass(new URL('https://github.com')) let bypass = pm.checkBypass(new URL('https://github.com'))
expect(bypass).toBeFalsy() expect(bypass).toBeFalsy()
}) })
it('checkBypass returns true if host with subdomain in no_proxy', () => {
process.env['no_proxy'] = 'myserver.com'
const bypass = pm.checkBypass(new URL('https://sub.myserver.com'))
expect(bypass).toBeTruthy()
})
it('checkBypass returns false if no_proxy is subdomain', () => {
process.env['no_proxy'] = 'myserver.com'
const bypass = pm.checkBypass(new URL('https://myserver.com.evil.org'))
expect(bypass).toBeFalsy()
})
it('checkBypass returns false if no_proxy is part of domain', () => {
process.env['no_proxy'] = 'myserver.com'
const bypass = pm.checkBypass(new URL('https://evilmyserver.com'))
expect(bypass).toBeFalsy()
})
// Do not strip leading dots as per https://github.com/actions/runner/blob/97195bad5870e2ad0915ebfef1616083aacf5818/docs/adrs/0263-proxy-support.md
it('checkBypass returns false if host with leading dot in no_proxy', () => {
process.env['no_proxy'] = '.myserver.com'
const bypass = pm.checkBypass(new URL('https://myserver.com'))
expect(bypass).toBeFalsy()
})
it('checkBypass returns true if host with subdomain in no_proxy defined with leading "."', () => {
process.env['no_proxy'] = '.myserver.com'
const bypass = pm.checkBypass(new URL('https://sub.myserver.com'))
expect(bypass).toBeTruthy()
})
it('checkBypass returns true if no_proxy is "*"', () => {
process.env['no_proxy'] = '*'
const bypass = pm.checkBypass(new URL('https://anything.whatsoever.com'))
expect(bypass).toBeTruthy()
})
it('checkBypass returns true if no_proxy contains comma separated "*"', () => {
process.env['no_proxy'] = 'domain.com,* , example.com'
const bypass = pm.checkBypass(new URL('https://anything.whatsoever.com'))
expect(bypass).toBeTruthy()
})
it('HttpClient does basic http get request through proxy', async () => { it('HttpClient does basic http get request through proxy', async () => {
process.env['http_proxy'] = _proxyUrl process.env['http_proxy'] = _proxyUrl
const httpClient = new httpm.HttpClient() const httpClient = new httpm.HttpClient()
const res: httpm.HttpClientResponse = await httpClient.get( let res: httpm.HttpClientResponse = await httpClient.get(
'http://postman-echo.com/get' 'http://httpbin.org/get'
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
expect(obj.url).toBe('http://postman-echo.com/get') expect(obj.url).toBe('http://httpbin.org/get')
expect(_proxyConnects).toEqual(['postman-echo.com:80']) expect(_proxyConnects).toEqual(['httpbin.org:80'])
}) })
it('HttpClient does basic http get request when bypass proxy', async () => { it('HttoClient does basic http get request when bypass proxy', async () => {
process.env['http_proxy'] = _proxyUrl process.env['http_proxy'] = _proxyUrl
process.env['no_proxy'] = 'postman-echo.com' process.env['no_proxy'] = 'httpbin.org'
const httpClient = new httpm.HttpClient() const httpClient = new httpm.HttpClient()
const res: httpm.HttpClientResponse = await httpClient.get( let res: httpm.HttpClientResponse = await httpClient.get(
'http://postman-echo.com/get' 'http://httpbin.org/get'
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
expect(obj.url).toBe('http://postman-echo.com/get') expect(obj.url).toBe('http://httpbin.org/get')
expect(_proxyConnects).toHaveLength(0) expect(_proxyConnects).toHaveLength(0)
}) })
it('HttpClient does basic https get request through proxy', async () => { it('HttpClient does basic https get request through proxy', async () => {
process.env['https_proxy'] = _proxyUrl process.env['https_proxy'] = _proxyUrl
const httpClient = new httpm.HttpClient() const httpClient = new httpm.HttpClient()
const res: httpm.HttpClientResponse = await httpClient.get( let res: httpm.HttpClientResponse = await httpClient.get(
'https://postman-echo.com/get' 'https://httpbin.org/get'
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
expect(obj.url).toBe('https://postman-echo.com/get') expect(obj.url).toBe('https://httpbin.org/get')
expect(_proxyConnects).toEqual(['postman-echo.com:443']) expect(_proxyConnects).toEqual(['httpbin.org:443'])
}) })
it('HttpClient does basic https get request when bypass proxy', async () => { it('HttpClient does basic https get request when bypass proxy', async () => {
process.env['https_proxy'] = _proxyUrl process.env['https_proxy'] = _proxyUrl
process.env['no_proxy'] = 'postman-echo.com' process.env['no_proxy'] = 'httpbin.org'
const httpClient = new httpm.HttpClient() const httpClient = new httpm.HttpClient()
const res: httpm.HttpClientResponse = await httpClient.get( let res: httpm.HttpClientResponse = await httpClient.get(
'https://postman-echo.com/get' 'https://httpbin.org/get'
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() let body: string = await res.readBody()
const obj = JSON.parse(body) let obj: any = JSON.parse(body)
expect(obj.url).toBe('https://postman-echo.com/get') expect(obj.url).toBe('https://httpbin.org/get')
expect(_proxyConnects).toHaveLength(0) expect(_proxyConnects).toHaveLength(0)
}) })
it('HttpClient bypasses proxy for loopback addresses (localhost, ::1, 127.*)', async () => {
// setup a server listening on localhost:8091
const server = http.createServer((request, response) => {
response.writeHead(200)
request.pipe(response)
})
server.listen(8091)
try {
process.env['http_proxy'] = _proxyUrl
const httpClient = new httpm.HttpClient()
let res = await httpClient.get('http://localhost:8091')
expect(res.message.statusCode).toBe(200)
res = await httpClient.get('http://127.0.0.1:8091')
expect(res.message.statusCode).toBe(200)
// no support for ipv6 for now
expect(httpClient.get('http://[::1]:8091')).rejects.toThrow()
// proxy at _proxyUrl was ignored
expect(_proxyConnects).toEqual([])
} finally {
server.close()
}
})
it('proxyAuth not set in tunnel agent when authentication is not provided', async () => { it('proxyAuth not set in tunnel agent when authentication is not provided', async () => {
process.env['https_proxy'] = 'http://127.0.0.1:8080' process.env['https_proxy'] = 'http://127.0.0.1:8080'
const httpClient = new httpm.HttpClient() const httpClient = new httpm.HttpClient()
const agent: any = httpClient.getAgent('https://some-url') let agent: tunnelm.TunnelingAgent = httpClient.getAgent('https://some-url')
// eslint-disable-next-line no-console
console.log(agent) console.log(agent)
expect(agent.proxyOptions.host).toBe('127.0.0.1') expect(agent.proxyOptions.host).toBe('127.0.0.1')
expect(agent.proxyOptions.port).toBe('8080') expect(agent.proxyOptions.port).toBe('8080')
@@ -287,8 +210,7 @@ describe('proxy', () => {
it('proxyAuth is set in tunnel agent when authentication is provided', async () => { it('proxyAuth is set in tunnel agent when authentication is provided', async () => {
process.env['https_proxy'] = 'http://user:password@127.0.0.1:8080' process.env['https_proxy'] = 'http://user:password@127.0.0.1:8080'
const httpClient = new httpm.HttpClient() const httpClient = new httpm.HttpClient()
const agent: any = httpClient.getAgent('https://some-url') let agent: tunnelm.TunnelingAgent = httpClient.getAgent('https://some-url')
// eslint-disable-next-line no-console
console.log(agent) console.log(agent)
expect(agent.proxyOptions.host).toBe('127.0.0.1') expect(agent.proxyOptions.host).toBe('127.0.0.1')
expect(agent.proxyOptions.port).toBe('8080') expect(agent.proxyOptions.port).toBe('8080')
@@ -296,7 +218,7 @@ describe('proxy', () => {
}) })
}) })
function _clearVars(): void { function _clearVars() {
delete process.env.http_proxy delete process.env.http_proxy
delete process.env.HTTP_PROXY delete process.env.HTTP_PROXY
delete process.env.https_proxy delete process.env.https_proxy
+10287 -125
View File
File diff suppressed because it is too large Load Diff
+25 -34
View File
@@ -1,48 +1,39 @@
{ {
"name": "@actions/http-client", "name": "@actions/http-client",
"version": "2.1.0", "version": "1.0.11",
"description": "Actions Http Client", "description": "Actions Http Client",
"keywords": [ "main": "index.js",
"github", "scripts": {
"actions", "build": "rm -Rf ./_out && tsc && cp package*.json ./_out && cp *.md ./_out && cp LICENSE ./_out && cp actions.png ./_out",
"http" "test": "jest",
], "format": "prettier --write *.ts && prettier --write **/*.ts",
"homepage": "https://github.com/actions/toolkit/tree/main/packages/http-client", "format-check": "prettier --check *.ts && prettier --check **/*.ts",
"license": "MIT", "audit-check": "npm audit --audit-level=moderate"
"main": "lib/index.js",
"types": "lib/index.d.ts",
"directories": {
"lib": "lib",
"test": "__tests__"
},
"files": [
"lib",
"!.DS_Store"
],
"publishConfig": {
"access": "public"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/actions/toolkit.git", "url": "git+https://github.com/actions/http-client.git"
"directory": "packages/http-client"
},
"scripts": {
"audit-moderate": "npm install && npm audit --json --audit-level=moderate > audit.json",
"test": "echo \"Error: run tests from root\" && exit 1",
"build": "tsc",
"format": "prettier --write **/*.ts",
"format-check": "prettier --check **/*.ts",
"tsc": "tsc"
}, },
"keywords": [
"Actions",
"Http"
],
"author": "GitHub, Inc.",
"license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/actions/toolkit/issues" "url": "https://github.com/actions/http-client/issues"
}, },
"homepage": "https://github.com/actions/http-client#readme",
"devDependencies": { "devDependencies": {
"@types/tunnel": "0.0.3", "@types/jest": "^25.1.4",
"proxy": "^1.0.1" "@types/node": "^12.12.31",
"jest": "^25.1.0",
"prettier": "^2.0.4",
"proxy": "^1.0.1",
"ts-jest": "^25.2.1",
"typescript": "^3.8.3"
}, },
"dependencies": { "dependencies": {
"tunnel": "^0.0.6" "tunnel": "0.0.6"
} }
} }
+34 -34
View File
@@ -1,8 +1,6 @@
import * as http from 'http' import ifm = require('./interfaces')
import * as ifm from './interfaces'
import {HttpClientResponse} from './index'
export class BasicCredentialHandler implements ifm.RequestHandler { export class BasicCredentialHandler implements ifm.IRequestHandler {
username: string username: string
password: string password: string
@@ -11,26 +9,27 @@ export class BasicCredentialHandler implements ifm.RequestHandler {
this.password = password this.password = password
} }
prepareRequest(options: http.RequestOptions): void { prepareRequest(options: any): void {
if (!options.headers) { options.headers['Authorization'] =
throw Error('The request has no headers') 'Basic ' +
} Buffer.from(this.username + ':' + this.password).toString('base64')
options.headers['Authorization'] = `Basic ${Buffer.from(
`${this.username}:${this.password}`
).toString('base64')}`
} }
// This handler cannot handle 401 // This handler cannot handle 401
canHandleAuthentication(): boolean { canHandleAuthentication(response: ifm.IHttpClientResponse): boolean {
return false return false
} }
async handleAuthentication(): Promise<HttpClientResponse> { handleAuthentication(
throw new Error('not implemented') httpClient: ifm.IHttpClient,
requestInfo: ifm.IRequestInfo,
objs
): Promise<ifm.IHttpClientResponse> {
return null
} }
} }
export class BearerCredentialHandler implements ifm.RequestHandler { export class BearerCredentialHandler implements ifm.IRequestHandler {
token: string token: string
constructor(token: string) { constructor(token: string) {
@@ -39,25 +38,26 @@ export class BearerCredentialHandler implements ifm.RequestHandler {
// currently implements pre-authorization // currently implements pre-authorization
// TODO: support preAuth = false where it hooks on 401 // TODO: support preAuth = false where it hooks on 401
prepareRequest(options: http.RequestOptions): void { prepareRequest(options: any): void {
if (!options.headers) { options.headers['Authorization'] = 'Bearer ' + this.token
throw Error('The request has no headers')
}
options.headers['Authorization'] = `Bearer ${this.token}`
} }
// This handler cannot handle 401 // This handler cannot handle 401
canHandleAuthentication(): boolean { canHandleAuthentication(response: ifm.IHttpClientResponse): boolean {
return false return false
} }
async handleAuthentication(): Promise<HttpClientResponse> { handleAuthentication(
throw new Error('not implemented') httpClient: ifm.IHttpClient,
requestInfo: ifm.IRequestInfo,
objs
): Promise<ifm.IHttpClientResponse> {
return null
} }
} }
export class PersonalAccessTokenCredentialHandler export class PersonalAccessTokenCredentialHandler
implements ifm.RequestHandler { implements ifm.IRequestHandler {
token: string token: string
constructor(token: string) { constructor(token: string) {
@@ -66,21 +66,21 @@ export class PersonalAccessTokenCredentialHandler
// currently implements pre-authorization // currently implements pre-authorization
// TODO: support preAuth = false where it hooks on 401 // TODO: support preAuth = false where it hooks on 401
prepareRequest(options: http.RequestOptions): void { prepareRequest(options: any): void {
if (!options.headers) { options.headers['Authorization'] =
throw Error('The request has no headers') 'Basic ' + Buffer.from('PAT:' + this.token).toString('base64')
}
options.headers['Authorization'] = `Basic ${Buffer.from(
`PAT:${this.token}`
).toString('base64')}`
} }
// This handler cannot handle 401 // This handler cannot handle 401
canHandleAuthentication(): boolean { canHandleAuthentication(response: ifm.IHttpClientResponse): boolean {
return false return false
} }
async handleAuthentication(): Promise<HttpClientResponse> { handleAuthentication(
throw new Error('not implemented') httpClient: ifm.IHttpClient,
requestInfo: ifm.IRequestInfo,
objs
): Promise<ifm.IHttpClientResponse> {
return null
} }
} }
+178 -183
View File
@@ -1,11 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ import http = require('http')
import https = require('https')
import ifm = require('./interfaces')
import pm = require('./proxy')
import * as http from 'http' let tunnel: any
import * as https from 'https'
import * as ifm from './interfaces'
import * as net from 'net'
import * as pm from './proxy'
import * as tunnel from 'tunnel'
export enum HttpCodes { export enum HttpCodes {
OK = 200, OK = 200,
@@ -51,7 +49,7 @@ export enum MediaTypes {
* @param serverUrl The server URL where the request will be sent. For example, https://api.github.com * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com
*/ */
export function getProxyUrl(serverUrl: string): string { export function getProxyUrl(serverUrl: string): string {
const proxyUrl = pm.getProxyUrl(new URL(serverUrl)) let proxyUrl = pm.getProxyUrl(new URL(serverUrl))
return proxyUrl ? proxyUrl.href : '' return proxyUrl ? proxyUrl.href : ''
} }
@@ -79,18 +77,18 @@ export class HttpClientError extends Error {
Object.setPrototypeOf(this, HttpClientError.prototype) Object.setPrototypeOf(this, HttpClientError.prototype)
} }
statusCode: number public statusCode: number
result?: any public result?: any
} }
export class HttpClientResponse { export class HttpClientResponse implements ifm.IHttpClientResponse {
constructor(message: http.IncomingMessage) { constructor(message: http.IncomingMessage) {
this.message = message this.message = message
} }
message: http.IncomingMessage public message: http.IncomingMessage
async readBody(): Promise<string> { readBody(): Promise<string> {
return new Promise<string>(async resolve => { return new Promise<string>(async (resolve, reject) => {
let output = Buffer.alloc(0) let output = Buffer.alloc(0)
this.message.on('data', (chunk: Buffer) => { this.message.on('data', (chunk: Buffer) => {
@@ -104,32 +102,32 @@ export class HttpClientResponse {
} }
} }
export function isHttps(requestUrl: string): boolean { export function isHttps(requestUrl: string) {
const parsedUrl: URL = new URL(requestUrl) let parsedUrl: URL = new URL(requestUrl)
return parsedUrl.protocol === 'https:' return parsedUrl.protocol === 'https:'
} }
export class HttpClient { export class HttpClient {
userAgent: string | undefined userAgent: string | undefined
handlers: ifm.RequestHandler[] handlers: ifm.IRequestHandler[]
requestOptions: ifm.RequestOptions | undefined requestOptions: ifm.IRequestOptions
private _ignoreSslError = false private _ignoreSslError: boolean = false
private _socketTimeout: number | undefined private _socketTimeout: number
private _allowRedirects = true private _allowRedirects: boolean = true
private _allowRedirectDowngrade = false private _allowRedirectDowngrade: boolean = false
private _maxRedirects = 50 private _maxRedirects: number = 50
private _allowRetries = false private _allowRetries: boolean = false
private _maxRetries = 1 private _maxRetries: number = 1
private _agent: any private _agent
private _proxyAgent: any private _proxyAgent
private _keepAlive = false private _keepAlive: boolean = false
private _disposed = false private _disposed: boolean = false
constructor( constructor(
userAgent?: string, userAgent?: string,
handlers?: ifm.RequestHandler[], handlers?: ifm.IRequestHandler[],
requestOptions?: ifm.RequestOptions requestOptions?: ifm.IRequestOptions
) { ) {
this.userAgent = userAgent this.userAgent = userAgent
this.handlers = handlers || [] this.handlers = handlers || []
@@ -167,64 +165,64 @@ export class HttpClient {
} }
} }
async options( public options(
requestUrl: string, requestUrl: string,
additionalHeaders?: http.OutgoingHttpHeaders additionalHeaders?: ifm.IHeaders
): Promise<HttpClientResponse> { ): Promise<ifm.IHttpClientResponse> {
return this.request('OPTIONS', requestUrl, null, additionalHeaders || {}) return this.request('OPTIONS', requestUrl, null, additionalHeaders || {})
} }
async get( public get(
requestUrl: string, requestUrl: string,
additionalHeaders?: http.OutgoingHttpHeaders additionalHeaders?: ifm.IHeaders
): Promise<HttpClientResponse> { ): Promise<ifm.IHttpClientResponse> {
return this.request('GET', requestUrl, null, additionalHeaders || {}) return this.request('GET', requestUrl, null, additionalHeaders || {})
} }
async del( public del(
requestUrl: string, requestUrl: string,
additionalHeaders?: http.OutgoingHttpHeaders additionalHeaders?: ifm.IHeaders
): Promise<HttpClientResponse> { ): Promise<ifm.IHttpClientResponse> {
return this.request('DELETE', requestUrl, null, additionalHeaders || {}) return this.request('DELETE', requestUrl, null, additionalHeaders || {})
} }
async post( public post(
requestUrl: string, requestUrl: string,
data: string, data: string,
additionalHeaders?: http.OutgoingHttpHeaders additionalHeaders?: ifm.IHeaders
): Promise<HttpClientResponse> { ): Promise<ifm.IHttpClientResponse> {
return this.request('POST', requestUrl, data, additionalHeaders || {}) return this.request('POST', requestUrl, data, additionalHeaders || {})
} }
async patch( public patch(
requestUrl: string, requestUrl: string,
data: string, data: string,
additionalHeaders?: http.OutgoingHttpHeaders additionalHeaders?: ifm.IHeaders
): Promise<HttpClientResponse> { ): Promise<ifm.IHttpClientResponse> {
return this.request('PATCH', requestUrl, data, additionalHeaders || {}) return this.request('PATCH', requestUrl, data, additionalHeaders || {})
} }
async put( public put(
requestUrl: string, requestUrl: string,
data: string, data: string,
additionalHeaders?: http.OutgoingHttpHeaders additionalHeaders?: ifm.IHeaders
): Promise<HttpClientResponse> { ): Promise<ifm.IHttpClientResponse> {
return this.request('PUT', requestUrl, data, additionalHeaders || {}) return this.request('PUT', requestUrl, data, additionalHeaders || {})
} }
async head( public head(
requestUrl: string, requestUrl: string,
additionalHeaders?: http.OutgoingHttpHeaders additionalHeaders?: ifm.IHeaders
): Promise<HttpClientResponse> { ): Promise<ifm.IHttpClientResponse> {
return this.request('HEAD', requestUrl, null, additionalHeaders || {}) return this.request('HEAD', requestUrl, null, additionalHeaders || {})
} }
async sendStream( public sendStream(
verb: string, verb: string,
requestUrl: string, requestUrl: string,
stream: NodeJS.ReadableStream, stream: NodeJS.ReadableStream,
additionalHeaders?: http.OutgoingHttpHeaders additionalHeaders?: ifm.IHeaders
): Promise<HttpClientResponse> { ): Promise<ifm.IHttpClientResponse> {
return this.request(verb, requestUrl, stream, additionalHeaders) return this.request(verb, requestUrl, stream, additionalHeaders)
} }
@@ -232,28 +230,28 @@ export class HttpClient {
* Gets a typed object from an endpoint * Gets a typed object from an endpoint
* Be aware that not found returns a null. Other errors (4xx, 5xx) reject the promise * Be aware that not found returns a null. Other errors (4xx, 5xx) reject the promise
*/ */
async getJson<T>( public async getJson<T>(
requestUrl: string, requestUrl: string,
additionalHeaders: http.OutgoingHttpHeaders = {} additionalHeaders: ifm.IHeaders = {}
): Promise<ifm.TypedResponse<T>> { ): Promise<ifm.ITypedResponse<T>> {
additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader( additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(
additionalHeaders, additionalHeaders,
Headers.Accept, Headers.Accept,
MediaTypes.ApplicationJson MediaTypes.ApplicationJson
) )
const res: HttpClientResponse = await this.get( let res: ifm.IHttpClientResponse = await this.get(
requestUrl, requestUrl,
additionalHeaders additionalHeaders
) )
return this._processResponse<T>(res, this.requestOptions) return this._processResponse<T>(res, this.requestOptions)
} }
async postJson<T>( public async postJson<T>(
requestUrl: string, requestUrl: string,
obj: any, obj: any,
additionalHeaders: http.OutgoingHttpHeaders = {} additionalHeaders: ifm.IHeaders = {}
): Promise<ifm.TypedResponse<T>> { ): Promise<ifm.ITypedResponse<T>> {
const data: string = JSON.stringify(obj, null, 2) let data: string = JSON.stringify(obj, null, 2)
additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader( additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(
additionalHeaders, additionalHeaders,
Headers.Accept, Headers.Accept,
@@ -264,7 +262,7 @@ export class HttpClient {
Headers.ContentType, Headers.ContentType,
MediaTypes.ApplicationJson MediaTypes.ApplicationJson
) )
const res: HttpClientResponse = await this.post( let res: ifm.IHttpClientResponse = await this.post(
requestUrl, requestUrl,
data, data,
additionalHeaders additionalHeaders
@@ -272,12 +270,12 @@ export class HttpClient {
return this._processResponse<T>(res, this.requestOptions) return this._processResponse<T>(res, this.requestOptions)
} }
async putJson<T>( public async putJson<T>(
requestUrl: string, requestUrl: string,
obj: any, obj: any,
additionalHeaders: http.OutgoingHttpHeaders = {} additionalHeaders: ifm.IHeaders = {}
): Promise<ifm.TypedResponse<T>> { ): Promise<ifm.ITypedResponse<T>> {
const data: string = JSON.stringify(obj, null, 2) let data: string = JSON.stringify(obj, null, 2)
additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader( additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(
additionalHeaders, additionalHeaders,
Headers.Accept, Headers.Accept,
@@ -288,7 +286,7 @@ export class HttpClient {
Headers.ContentType, Headers.ContentType,
MediaTypes.ApplicationJson MediaTypes.ApplicationJson
) )
const res: HttpClientResponse = await this.put( let res: ifm.IHttpClientResponse = await this.put(
requestUrl, requestUrl,
data, data,
additionalHeaders additionalHeaders
@@ -296,12 +294,12 @@ export class HttpClient {
return this._processResponse<T>(res, this.requestOptions) return this._processResponse<T>(res, this.requestOptions)
} }
async patchJson<T>( public async patchJson<T>(
requestUrl: string, requestUrl: string,
obj: any, obj: any,
additionalHeaders: http.OutgoingHttpHeaders = {} additionalHeaders: ifm.IHeaders = {}
): Promise<ifm.TypedResponse<T>> { ): Promise<ifm.ITypedResponse<T>> {
const data: string = JSON.stringify(obj, null, 2) let data: string = JSON.stringify(obj, null, 2)
additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader( additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(
additionalHeaders, additionalHeaders,
Headers.Accept, Headers.Accept,
@@ -312,7 +310,7 @@ export class HttpClient {
Headers.ContentType, Headers.ContentType,
MediaTypes.ApplicationJson MediaTypes.ApplicationJson
) )
const res: HttpClientResponse = await this.patch( let res: ifm.IHttpClientResponse = await this.patch(
requestUrl, requestUrl,
data, data,
additionalHeaders additionalHeaders
@@ -325,28 +323,28 @@ export class HttpClient {
* All other methods such as get, post, patch, and request ultimately call this. * All other methods such as get, post, patch, and request ultimately call this.
* Prefer get, del, post and patch * Prefer get, del, post and patch
*/ */
async request( public async request(
verb: string, verb: string,
requestUrl: string, requestUrl: string,
data: string | NodeJS.ReadableStream | null, data: string | NodeJS.ReadableStream,
headers?: http.OutgoingHttpHeaders headers: ifm.IHeaders
): Promise<HttpClientResponse> { ): Promise<ifm.IHttpClientResponse> {
if (this._disposed) { if (this._disposed) {
throw new Error('Client has already been disposed.') throw new Error('Client has already been disposed.')
} }
const parsedUrl = new URL(requestUrl) let parsedUrl = new URL(requestUrl)
let info: ifm.RequestInfo = this._prepareRequest(verb, parsedUrl, headers) let info: ifm.IRequestInfo = this._prepareRequest(verb, parsedUrl, headers)
// Only perform retries on reads since writes may not be idempotent. // Only perform retries on reads since writes may not be idempotent.
const maxTries: number = let maxTries: number =
this._allowRetries && RetryableHttpVerbs.includes(verb) this._allowRetries && RetryableHttpVerbs.indexOf(verb) != -1
? this._maxRetries + 1 ? this._maxRetries + 1
: 1 : 1
let numTries = 0 let numTries: number = 0
let response: HttpClientResponse | undefined let response: HttpClientResponse
do { while (numTries < maxTries) {
response = await this.requestRaw(info, data) response = await this.requestRaw(info, data)
// Check if it's an authentication challenge // Check if it's an authentication challenge
@@ -355,11 +353,11 @@ export class HttpClient {
response.message && response.message &&
response.message.statusCode === HttpCodes.Unauthorized response.message.statusCode === HttpCodes.Unauthorized
) { ) {
let authenticationHandler: ifm.RequestHandler | undefined let authenticationHandler: ifm.IRequestHandler
for (const handler of this.handlers) { for (let i = 0; i < this.handlers.length; i++) {
if (handler.canHandleAuthentication(response)) { if (this.handlers[i].canHandleAuthentication(response)) {
authenticationHandler = handler authenticationHandler = this.handlers[i]
break break
} }
} }
@@ -375,21 +373,19 @@ export class HttpClient {
let redirectsRemaining: number = this._maxRedirects let redirectsRemaining: number = this._maxRedirects
while ( while (
response.message.statusCode && HttpRedirectCodes.indexOf(response.message.statusCode) != -1 &&
HttpRedirectCodes.includes(response.message.statusCode) &&
this._allowRedirects && this._allowRedirects &&
redirectsRemaining > 0 redirectsRemaining > 0
) { ) {
const redirectUrl: string | undefined = const redirectUrl: string | null = response.message.headers['location']
response.message.headers['location']
if (!redirectUrl) { if (!redirectUrl) {
// if there's no location to redirect to, we won't // if there's no location to redirect to, we won't
break break
} }
const parsedRedirectUrl = new URL(redirectUrl) let parsedRedirectUrl = new URL(redirectUrl)
if ( if (
parsedUrl.protocol === 'https:' && parsedUrl.protocol == 'https:' &&
parsedUrl.protocol !== parsedRedirectUrl.protocol && parsedUrl.protocol != parsedRedirectUrl.protocol &&
!this._allowRedirectDowngrade !this._allowRedirectDowngrade
) { ) {
throw new Error( throw new Error(
@@ -403,7 +399,7 @@ export class HttpClient {
// strip authorization header if redirected to a different hostname // strip authorization header if redirected to a different hostname
if (parsedRedirectUrl.hostname !== parsedUrl.hostname) { if (parsedRedirectUrl.hostname !== parsedUrl.hostname) {
for (const header in headers) { for (let header in headers) {
// header names are case insensitive // header names are case insensitive
if (header.toLowerCase() === 'authorization') { if (header.toLowerCase() === 'authorization') {
delete headers[header] delete headers[header]
@@ -417,10 +413,7 @@ export class HttpClient {
redirectsRemaining-- redirectsRemaining--
} }
if ( if (HttpResponseRetryCodes.indexOf(response.message.statusCode) == -1) {
!response.message.statusCode ||
!HttpResponseRetryCodes.includes(response.message.statusCode)
) {
// If not a retry code, return immediately instead of retrying // If not a retry code, return immediately instead of retrying
return response return response
} }
@@ -431,7 +424,7 @@ export class HttpClient {
await response.readBody() await response.readBody()
await this._performExponentialBackoff(numTries) await this._performExponentialBackoff(numTries)
} }
} while (numTries < maxTries) }
return response return response
} }
@@ -439,7 +432,7 @@ export class HttpClient {
/** /**
* Needs to be called if keepAlive is set to true in request options. * Needs to be called if keepAlive is set to true in request options.
*/ */
dispose(): void { public dispose() {
if (this._agent) { if (this._agent) {
this._agent.destroy() this._agent.destroy()
} }
@@ -452,20 +445,20 @@ export class HttpClient {
* @param info * @param info
* @param data * @param data
*/ */
async requestRaw( public requestRaw(
info: ifm.RequestInfo, info: ifm.IRequestInfo,
data: string | NodeJS.ReadableStream | null data: string | NodeJS.ReadableStream
): Promise<HttpClientResponse> { ): Promise<ifm.IHttpClientResponse> {
return new Promise<HttpClientResponse>((resolve, reject) => { return new Promise<ifm.IHttpClientResponse>((resolve, reject) => {
function callbackForResult(err?: Error, res?: HttpClientResponse): void { let callbackForResult = function (
err: any,
res: ifm.IHttpClientResponse
) {
if (err) { if (err) {
reject(err) reject(err)
} else if (!res) {
// If `err` is not passed, then `res` must be passed.
reject(new Error('Unknown error'))
} else {
resolve(res)
} }
resolve(res)
} }
this.requestRawWithCallback(info, data, callbackForResult) this.requestRawWithCallback(info, data, callbackForResult)
@@ -478,35 +471,33 @@ export class HttpClient {
* @param data * @param data
* @param onResult * @param onResult
*/ */
requestRawWithCallback( public requestRawWithCallback(
info: ifm.RequestInfo, info: ifm.IRequestInfo,
data: string | NodeJS.ReadableStream | null, data: string | NodeJS.ReadableStream,
onResult: (err?: Error, res?: HttpClientResponse) => void onResult: (err: any, res: ifm.IHttpClientResponse) => void
): void { ): void {
let socket
if (typeof data === 'string') { if (typeof data === 'string') {
if (!info.options.headers) {
info.options.headers = {}
}
info.options.headers['Content-Length'] = Buffer.byteLength(data, 'utf8') info.options.headers['Content-Length'] = Buffer.byteLength(data, 'utf8')
} }
let callbackCalled = false let callbackCalled: boolean = false
function handleResult(err?: Error, res?: HttpClientResponse): void { let handleResult = (err: any, res: HttpClientResponse) => {
if (!callbackCalled) { if (!callbackCalled) {
callbackCalled = true callbackCalled = true
onResult(err, res) onResult(err, res)
} }
} }
const req: http.ClientRequest = info.httpModule.request( let req: http.ClientRequest = info.httpModule.request(
info.options, info.options,
(msg: http.IncomingMessage) => { (msg: http.IncomingMessage) => {
const res: HttpClientResponse = new HttpClientResponse(msg) let res: HttpClientResponse = new HttpClientResponse(msg)
handleResult(undefined, res) handleResult(null, res)
} }
) )
let socket: net.Socket
req.on('socket', sock => { req.on('socket', sock => {
socket = sock socket = sock
}) })
@@ -516,13 +507,13 @@ export class HttpClient {
if (socket) { if (socket) {
socket.end() socket.end()
} }
handleResult(new Error(`Request timeout: ${info.options.path}`)) handleResult(new Error('Request timeout: ' + info.options.path), null)
}) })
req.on('error', function(err) { req.on('error', function (err) {
// err has statusCode property // err has statusCode property
// res should have headers // res should have headers
handleResult(err) handleResult(err, null)
}) })
if (data && typeof data === 'string') { if (data && typeof data === 'string') {
@@ -530,7 +521,7 @@ export class HttpClient {
} }
if (data && typeof data !== 'string') { if (data && typeof data !== 'string') {
data.on('close', function() { data.on('close', function () {
req.end() req.end()
}) })
@@ -545,17 +536,17 @@ export class HttpClient {
* routing through a proxy server - depending upon the url and proxy environment variables. * routing through a proxy server - depending upon the url and proxy environment variables.
* @param serverUrl The server URL where the request will be sent. For example, https://api.github.com * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com
*/ */
getAgent(serverUrl: string): http.Agent { public getAgent(serverUrl: string): http.Agent {
const parsedUrl = new URL(serverUrl) let parsedUrl = new URL(serverUrl)
return this._getAgent(parsedUrl) return this._getAgent(parsedUrl)
} }
private _prepareRequest( private _prepareRequest(
method: string, method: string,
requestUrl: URL, requestUrl: URL,
headers?: http.OutgoingHttpHeaders headers: ifm.IHeaders
): ifm.RequestInfo { ): ifm.IRequestInfo {
const info: ifm.RequestInfo = <ifm.RequestInfo>{} const info: ifm.IRequestInfo = <ifm.IRequestInfo>{}
info.parsedUrl = requestUrl info.parsedUrl = requestUrl
const usingSsl: boolean = info.parsedUrl.protocol === 'https:' const usingSsl: boolean = info.parsedUrl.protocol === 'https:'
@@ -579,22 +570,23 @@ export class HttpClient {
// gives handlers an opportunity to participate // gives handlers an opportunity to participate
if (this.handlers) { if (this.handlers) {
for (const handler of this.handlers) { this.handlers.forEach(handler => {
handler.prepareRequest(info.options) handler.prepareRequest(info.options)
} })
} }
return info return info
} }
private _mergeHeaders( private _mergeHeaders(headers: ifm.IHeaders): ifm.IHeaders {
headers?: http.OutgoingHttpHeaders const lowercaseKeys = obj =>
): http.OutgoingHttpHeaders { Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {})
if (this.requestOptions && this.requestOptions.headers) { if (this.requestOptions && this.requestOptions.headers) {
return Object.assign( return Object.assign(
{}, {},
lowercaseKeys(this.requestOptions.headers), lowercaseKeys(this.requestOptions.headers),
lowercaseKeys(headers || {}) lowercaseKeys(headers)
) )
} }
@@ -602,11 +594,14 @@ export class HttpClient {
} }
private _getExistingOrDefaultHeader( private _getExistingOrDefaultHeader(
additionalHeaders: http.OutgoingHttpHeaders, additionalHeaders: ifm.IHeaders,
header: string, header: string,
_default: string _default: string
): string | number | string[] { ) {
let clientHeader: string | undefined const lowercaseKeys = obj =>
Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {})
let clientHeader: string
if (this.requestOptions && this.requestOptions.headers) { if (this.requestOptions && this.requestOptions.headers) {
clientHeader = lowercaseKeys(this.requestOptions.headers)[header] clientHeader = lowercaseKeys(this.requestOptions.headers)[header]
} }
@@ -615,8 +610,8 @@ export class HttpClient {
private _getAgent(parsedUrl: URL): http.Agent { private _getAgent(parsedUrl: URL): http.Agent {
let agent let agent
const proxyUrl = pm.getProxyUrl(parsedUrl) let proxyUrl: URL = pm.getProxyUrl(parsedUrl)
const useProxy = proxyUrl && proxyUrl.hostname let useProxy = proxyUrl && proxyUrl.hostname
if (this._keepAlive && useProxy) { if (this._keepAlive && useProxy) {
agent = this._proxyAgent agent = this._proxyAgent
@@ -627,20 +622,24 @@ export class HttpClient {
} }
// if agent is already assigned use that agent. // if agent is already assigned use that agent.
if (agent) { if (!!agent) {
return agent return agent
} }
const usingSsl = parsedUrl.protocol === 'https:' const usingSsl = parsedUrl.protocol === 'https:'
let maxSockets = 100 let maxSockets = 100
if (this.requestOptions) { if (!!this.requestOptions) {
maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets
} }
// This is `useProxy` again, but we need to check `proxyURl` directly for TypeScripts's flow analysis. if (useProxy) {
if (proxyUrl && proxyUrl.hostname) { // If using proxy, need tunnel
if (!tunnel) {
tunnel = require('tunnel')
}
const agentOptions = { const agentOptions = {
maxSockets, maxSockets: maxSockets,
keepAlive: this._keepAlive, keepAlive: this._keepAlive,
proxy: { proxy: {
...((proxyUrl.username || proxyUrl.password) && { ...((proxyUrl.username || proxyUrl.password) && {
@@ -665,7 +664,7 @@ export class HttpClient {
// if reusing agent across request and tunneling agent isn't assigned create a new agent // if reusing agent across request and tunneling agent isn't assigned create a new agent
if (this._keepAlive && !agent) { if (this._keepAlive && !agent) {
const options = {keepAlive: this._keepAlive, maxSockets} const options = {keepAlive: this._keepAlive, maxSockets: maxSockets}
agent = usingSsl ? new https.Agent(options) : new http.Agent(options) agent = usingSsl ? new https.Agent(options) : new http.Agent(options)
this._agent = agent this._agent = agent
} }
@@ -687,51 +686,50 @@ export class HttpClient {
return agent return agent
} }
private async _performExponentialBackoff(retryNumber: number): Promise<void> { private _performExponentialBackoff(retryNumber: number): Promise<void> {
retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber) retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber)
const ms: number = ExponentialBackoffTimeSlice * Math.pow(2, retryNumber) const ms: number = ExponentialBackoffTimeSlice * Math.pow(2, retryNumber)
return new Promise(resolve => setTimeout(() => resolve(), ms)) return new Promise(resolve => setTimeout(() => resolve(), ms))
} }
private async _processResponse<T>( private static dateTimeDeserializer(key: any, value: any): any {
res: HttpClientResponse, if (typeof value === 'string') {
options?: ifm.RequestOptions let a = new Date(value)
): Promise<ifm.TypedResponse<T>> { if (!isNaN(a.valueOf())) {
return new Promise<ifm.TypedResponse<T>>(async (resolve, reject) => { return a
const statusCode = res.message.statusCode || 0 }
}
const response: ifm.TypedResponse<T> = { return value
statusCode, }
private async _processResponse<T>(
res: ifm.IHttpClientResponse,
options: ifm.IRequestOptions
): Promise<ifm.ITypedResponse<T>> {
return new Promise<ifm.ITypedResponse<T>>(async (resolve, reject) => {
const statusCode: number = res.message.statusCode
const response: ifm.ITypedResponse<T> = {
statusCode: statusCode,
result: null, result: null,
headers: {} headers: {}
} }
// not found leads to null obj returned // not found leads to null obj returned
if (statusCode === HttpCodes.NotFound) { if (statusCode == HttpCodes.NotFound) {
resolve(response) resolve(response)
} }
// get the result from the body
function dateTimeDeserializer(key: any, value: any): any {
if (typeof value === 'string') {
const a = new Date(value)
if (!isNaN(a.valueOf())) {
return a
}
}
return value
}
let obj: any let obj: any
let contents: string | undefined let contents: string
// get the result from the body
try { try {
contents = await res.readBody() contents = await res.readBody()
if (contents && contents.length > 0) { if (contents && contents.length > 0) {
if (options && options.deserializeDates) { if (options && options.deserializeDates) {
obj = JSON.parse(contents, dateTimeDeserializer) obj = JSON.parse(contents, HttpClient.dateTimeDeserializer)
} else { } else {
obj = JSON.parse(contents) obj = JSON.parse(contents)
} }
@@ -755,10 +753,10 @@ export class HttpClient {
// it may be the case that the exception is in the body message as string // it may be the case that the exception is in the body message as string
msg = contents msg = contents
} else { } else {
msg = `Failed request: (${statusCode})` msg = 'Failed request: (' + statusCode + ')'
} }
const err = new HttpClientError(msg, statusCode) let err = new HttpClientError(msg, statusCode)
err.result = response.result err.result = response.result
reject(err) reject(err)
@@ -768,6 +766,3 @@ export class HttpClient {
}) })
} }
} }
const lowercaseKeys = (obj: {[index: string]: any}): any =>
Object.keys(obj).reduce((c: any, k) => ((c[k.toLowerCase()] = obj[k]), c), {})
+43 -36
View File
@@ -1,76 +1,83 @@
import * as http from 'http' import http = require('http')
import * as https from 'https'
import {HttpClientResponse} from './index'
export interface HttpClient { export interface IHeaders {
[key: string]: any
}
export interface IHttpClient {
options( options(
requestUrl: string, requestUrl: string,
additionalHeaders?: http.OutgoingHttpHeaders additionalHeaders?: IHeaders
): Promise<HttpClientResponse> ): Promise<IHttpClientResponse>
get( get(
requestUrl: string, requestUrl: string,
additionalHeaders?: http.OutgoingHttpHeaders additionalHeaders?: IHeaders
): Promise<HttpClientResponse> ): Promise<IHttpClientResponse>
del( del(
requestUrl: string, requestUrl: string,
additionalHeaders?: http.OutgoingHttpHeaders additionalHeaders?: IHeaders
): Promise<HttpClientResponse> ): Promise<IHttpClientResponse>
post( post(
requestUrl: string, requestUrl: string,
data: string, data: string,
additionalHeaders?: http.OutgoingHttpHeaders additionalHeaders?: IHeaders
): Promise<HttpClientResponse> ): Promise<IHttpClientResponse>
patch( patch(
requestUrl: string, requestUrl: string,
data: string, data: string,
additionalHeaders?: http.OutgoingHttpHeaders additionalHeaders?: IHeaders
): Promise<HttpClientResponse> ): Promise<IHttpClientResponse>
put( put(
requestUrl: string, requestUrl: string,
data: string, data: string,
additionalHeaders?: http.OutgoingHttpHeaders additionalHeaders?: IHeaders
): Promise<HttpClientResponse> ): Promise<IHttpClientResponse>
sendStream( sendStream(
verb: string, verb: string,
requestUrl: string, requestUrl: string,
stream: NodeJS.ReadableStream, stream: NodeJS.ReadableStream,
additionalHeaders?: http.OutgoingHttpHeaders additionalHeaders?: IHeaders
): Promise<HttpClientResponse> ): Promise<IHttpClientResponse>
request( request(
verb: string, verb: string,
requestUrl: string, requestUrl: string,
data: string | NodeJS.ReadableStream, data: string | NodeJS.ReadableStream,
headers: http.OutgoingHttpHeaders headers: IHeaders
): Promise<HttpClientResponse> ): Promise<IHttpClientResponse>
requestRaw( requestRaw(
info: RequestInfo, info: IRequestInfo,
data: string | NodeJS.ReadableStream data: string | NodeJS.ReadableStream
): Promise<HttpClientResponse> ): Promise<IHttpClientResponse>
requestRawWithCallback( requestRawWithCallback(
info: RequestInfo, info: IRequestInfo,
data: string | NodeJS.ReadableStream, data: string | NodeJS.ReadableStream,
onResult: (err?: Error, res?: HttpClientResponse) => void onResult: (err: any, res: IHttpClientResponse) => void
): void ): void
} }
export interface RequestHandler { export interface IRequestHandler {
prepareRequest(options: http.RequestOptions): void prepareRequest(options: http.RequestOptions): void
canHandleAuthentication(response: HttpClientResponse): boolean canHandleAuthentication(response: IHttpClientResponse): boolean
handleAuthentication( handleAuthentication(
httpClient: HttpClient, httpClient: IHttpClient,
requestInfo: RequestInfo, requestInfo: IRequestInfo,
data: string | NodeJS.ReadableStream | null objs
): Promise<HttpClientResponse> ): Promise<IHttpClientResponse>
} }
export interface RequestInfo { export interface IHttpClientResponse {
message: http.IncomingMessage
readBody(): Promise<string>
}
export interface IRequestInfo {
options: http.RequestOptions options: http.RequestOptions
parsedUrl: URL parsedUrl: URL
httpModule: typeof http | typeof https httpModule: any
} }
export interface RequestOptions { export interface IRequestOptions {
headers?: http.OutgoingHttpHeaders headers?: IHeaders
socketTimeout?: number socketTimeout?: number
ignoreSslError?: boolean ignoreSslError?: boolean
allowRedirects?: boolean allowRedirects?: boolean
@@ -84,8 +91,8 @@ export interface RequestOptions {
maxRetries?: number maxRetries?: number
} }
export interface TypedResponse<T> { export interface ITypedResponse<T> {
statusCode: number statusCode: number
result: T | null result: T | null
headers: http.IncomingHttpHeaders headers: Object
} }
+17 -46
View File
@@ -1,28 +1,23 @@
export function getProxyUrl(reqUrl: URL): URL | undefined { export function getProxyUrl(reqUrl: URL): URL | undefined {
const usingSsl = reqUrl.protocol === 'https:' let usingSsl = reqUrl.protocol === 'https:'
let proxyUrl: URL
if (checkBypass(reqUrl)) { if (checkBypass(reqUrl)) {
return undefined return proxyUrl
} }
const proxyVar = (() => { let proxyVar: string
if (usingSsl) { if (usingSsl) {
return process.env['https_proxy'] || process.env['HTTPS_PROXY'] proxyVar = process.env['https_proxy'] || process.env['HTTPS_PROXY']
} else { } else {
return process.env['http_proxy'] || process.env['HTTP_PROXY'] proxyVar = process.env['http_proxy'] || process.env['HTTP_PROXY']
} }
})()
if (proxyVar) { if (proxyVar) {
try { proxyUrl = new URL(proxyVar)
return new URL(proxyVar)
} catch {
if (!proxyVar.startsWith('http://') && !proxyVar.startsWith('https://'))
return new URL(`http://${proxyVar}`)
}
} else {
return undefined
} }
return proxyUrl
} }
export function checkBypass(reqUrl: URL): boolean { export function checkBypass(reqUrl: URL): boolean {
@@ -30,18 +25,13 @@ export function checkBypass(reqUrl: URL): boolean {
return false return false
} }
const reqHost = reqUrl.hostname let noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || ''
if (isLoopbackAddress(reqHost)) {
return true
}
const noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || ''
if (!noProxy) { if (!noProxy) {
return false return false
} }
// Determine the request port // Determine the request port
let reqPort: number | undefined let reqPort: number
if (reqUrl.port) { if (reqUrl.port) {
reqPort = Number(reqUrl.port) reqPort = Number(reqUrl.port)
} else if (reqUrl.protocol === 'http:') { } else if (reqUrl.protocol === 'http:') {
@@ -51,39 +41,20 @@ export function checkBypass(reqUrl: URL): boolean {
} }
// Format the request hostname and hostname with port // Format the request hostname and hostname with port
const upperReqHosts = [reqUrl.hostname.toUpperCase()] let upperReqHosts = [reqUrl.hostname.toUpperCase()]
if (typeof reqPort === 'number') { if (typeof reqPort === 'number') {
upperReqHosts.push(`${upperReqHosts[0]}:${reqPort}`) upperReqHosts.push(`${upperReqHosts[0]}:${reqPort}`)
} }
// Compare request host against noproxy // Compare request host against noproxy
for (const upperNoProxyItem of noProxy for (let upperNoProxyItem of noProxy
.split(',') .split(',')
.map(x => x.trim().toUpperCase()) .map(x => x.trim().toUpperCase())
.filter(x => x)) { .filter(x => x)) {
if ( if (upperReqHosts.some(x => x === upperNoProxyItem)) {
upperNoProxyItem === '*' ||
upperReqHosts.some(
x =>
x === upperNoProxyItem ||
x.endsWith(`.${upperNoProxyItem}`) ||
(upperNoProxyItem.startsWith('.') &&
x.endsWith(`${upperNoProxyItem}`))
)
) {
return true return true
} }
} }
return false return false
} }
function isLoopbackAddress(host: string): boolean {
const hostLower = host.toLowerCase()
return (
hostLower === 'localhost' ||
hostLower.startsWith('127.') ||
hostLower.startsWith('[::1]') ||
hostLower.startsWith('[0:0:0:0:0:0:0:1]')
)
}
-11
View File
@@ -1,11 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./lib",
"rootDir": "./src",
"moduleResolution": "node"
},
"include": [
"./src"
]
}
-3
View File
@@ -1,8 +1,5 @@
# @actions/io Releases # @actions/io Releases
### 1.1.3
- Replace `child_process.exec` with `fs.rm` in `rmRF` for all OS implementations [#1373](https://github.com/actions/toolkit/pull/1373)
### 1.1.2 ### 1.1.2
- Update `lockfileVersion` to `v2` in `package-lock.json [#1020](https://github.com/actions/toolkit/pull/1020) - Update `lockfileVersion` to `v2` in `package-lock.json [#1020](https://github.com/actions/toolkit/pull/1020)
+24 -16
View File
@@ -3,7 +3,6 @@ import {promises as fs} from 'fs'
import * as os from 'os' import * as os from 'os'
import * as path from 'path' import * as path from 'path'
import * as io from '../src/io' import * as io from '../src/io'
import * as ioUtil from '../src/io-util'
describe('cp', () => { describe('cp', () => {
beforeAll(async () => { beforeAll(async () => {
@@ -332,22 +331,11 @@ describe('rmRF', () => {
await fs.appendFile(filePath, 'some data') await fs.appendFile(filePath, 'some data')
await assertExists(filePath) await assertExists(filePath)
// For windows we need to explicitly set an exclusive lock flag, because by default Node will open the file with the 'Delete' FileShare flag. const fd = await fs.open(filePath, 'r')
// See the exclusive lock windows flag definition: await io.rmRF(testPath)
// https://github.com/nodejs/node/blob/c2e4b1fa9ad0b744616c4e4c13a5017772a630c4/deps/uv/src/win/fs.c#L499-L513
const fd = await fs.open( await assertNotExists(testPath)
filePath,
fs.constants.O_RDONLY | ioUtil.UV_FS_O_EXLOCK
)
if (ioUtil.IS_WINDOWS) {
// On Windows, we expect an error due to an lstat call implementation in the underlying libuv code.
// See https://github.com/libuv/libuv/issues/3267 is resolved
await expect(async () => io.rmRF(testPath)).rejects.toThrow('EBUSY')
} else {
await io.rmRF(testPath)
await assertNotExists(testPath)
}
await fd.close() await fd.close()
await io.rmRF(testPath) await io.rmRF(testPath)
await assertNotExists(testPath) await assertNotExists(testPath)
@@ -385,6 +373,26 @@ describe('rmRF', () => {
await assertNotExists(file) await assertNotExists(file)
}) })
it('removes symlink folder with rmRF', async () => {
// create the following layout:
// real_directory
// real_directory/real_file
// symlink_directory -> real_directory
const root: string = path.join(getTestTemp(), 'rmRF_sym_dir_test')
const realDirectory: string = path.join(root, 'real_directory')
const realFile: string = path.join(root, 'real_directory', 'real_file')
const symlinkDirectory: string = path.join(root, 'symlink_directory')
await io.mkdirP(realDirectory)
await fs.writeFile(realFile, 'test file content')
await createSymlinkDir(realDirectory, symlinkDirectory)
await assertExists(path.join(symlinkDirectory, 'real_file'))
await io.rmRF(symlinkDirectory)
await assertExists(realDirectory)
await assertExists(realFile)
await assertNotExists(symlinkDirectory)
})
// creating a symlink to a file on Windows requires elevated // creating a symlink to a file on Windows requires elevated
if (os.platform() !== 'win32') { if (os.platform() !== 'win32') {
it('removes symlink file with rmRF', async () => { it('removes symlink file with rmRF', async () => {
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@actions/io", "name": "@actions/io",
"version": "1.1.3", "version": "1.1.2",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@actions/io", "name": "@actions/io",
"version": "1.1.3", "version": "1.1.2",
"license": "MIT" "license": "MIT"
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/io", "name": "@actions/io",
"version": "1.1.3", "version": "1.1.2",
"description": "Actions io lib", "description": "Actions io lib",
"keywords": [ "keywords": [
"github", "github",
+1 -6
View File
@@ -6,21 +6,16 @@ export const {
copyFile, copyFile,
lstat, lstat,
mkdir, mkdir,
open,
readdir, readdir,
readlink, readlink,
rename, rename,
rm,
rmdir, rmdir,
stat, stat,
symlink, symlink,
unlink unlink
} = fs.promises } = fs.promises
// export const {open} = 'fs'
export const IS_WINDOWS = process.platform === 'win32' export const IS_WINDOWS = process.platform === 'win32'
// See https://github.com/nodejs/node/blob/d0153aee367422d0858105abec186da4dff0a0c5/deps/uv/include/uv/win.h#L691
export const UV_FS_O_EXLOCK = 0x10000000
export const READONLY = fs.constants.O_RDONLY
export async function exists(fsPath: string): Promise<boolean> { export async function exists(fsPath: string): Promise<boolean> {
try { try {
+49 -11
View File
@@ -1,7 +1,12 @@
import {ok} from 'assert' import {ok} from 'assert'
import * as childProcess from 'child_process'
import * as path from 'path' import * as path from 'path'
import {promisify} from 'util'
import * as ioUtil from './io-util' import * as ioUtil from './io-util'
const exec = promisify(childProcess.exec)
const execFile = promisify(childProcess.execFile)
/** /**
* Interface for cp/mv options * Interface for cp/mv options
*/ */
@@ -111,6 +116,9 @@ export async function mv(
*/ */
export async function rmRF(inputPath: string): Promise<void> { export async function rmRF(inputPath: string): Promise<void> {
if (ioUtil.IS_WINDOWS) { if (ioUtil.IS_WINDOWS) {
// Node doesn't provide a delete operation, only an unlink function. This means that if the file is being used by another
// program (e.g. antivirus), it won't be deleted. To address this, we shell out the work to rd/del.
// Check for invalid characters // Check for invalid characters
// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
if (/[*"<>|]/.test(inputPath)) { if (/[*"<>|]/.test(inputPath)) {
@@ -118,17 +126,47 @@ export async function rmRF(inputPath: string): Promise<void> {
'File path must not contain `*`, `"`, `<`, `>` or `|` on Windows' 'File path must not contain `*`, `"`, `<`, `>` or `|` on Windows'
) )
} }
} try {
try { const cmdPath = ioUtil.getCmdPath()
// note if path does not exist, error is silent if (await ioUtil.isDirectory(inputPath, true)) {
await ioUtil.rm(inputPath, { await exec(`${cmdPath} /s /c "rd /s /q "%inputPath%""`, {
force: true, env: {inputPath}
maxRetries: 3, })
recursive: true, } else {
retryDelay: 300 await exec(`${cmdPath} /s /c "del /f /a "%inputPath%""`, {
}) env: {inputPath}
} catch (err) { })
throw new Error(`File was unable to be removed ${err}`) }
} catch (err) {
// if you try to delete a file that doesn't exist, desired result is achieved
// other errors are valid
if (err.code !== 'ENOENT') throw err
}
// Shelling out fails to remove a symlink folder with missing source, this unlink catches that
try {
await ioUtil.unlink(inputPath)
} catch (err) {
// if you try to delete a file that doesn't exist, desired result is achieved
// other errors are valid
if (err.code !== 'ENOENT') throw err
}
} else {
let isDir = false
try {
isDir = await ioUtil.isDirectory(inputPath)
} catch (err) {
// if you try to delete a file that doesn't exist, desired result is achieved
// other errors are valid
if (err.code !== 'ENOENT') throw err
return
}
if (isDir) {
await execFile(`rm`, [`-rf`, `${inputPath}`])
} else {
await ioUtil.unlink(inputPath)
}
} }
} }
-8
View File
@@ -1,13 +1,5 @@
# @actions/tool-cache Releases # @actions/tool-cache Releases
### 2.0.1
- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
### 2.0.0
- Update to v2.0.0 of `@actions/http-client`
- The type of the `headers` parameter in the exported function `downloadTool` has been narrowed from `{ [header: string]: any }` to `{ [header: string]: number | string | string[] | undefined; }` (that is, `http.OutgoingHttpHeaders`).
This is strictly a compile-time change for TypeScript consumers. Previous attempts to use a header value of a type other than those now accepted would have resulted in an error at run time.
### 1.7.2 ### 1.7.2
- Update `lockfileVersion` to `v2` in `package-lock.json [#1025](https://github.com/actions/toolkit/pull/1025) - Update `lockfileVersion` to `v2` in `package-lock.json [#1025](https://github.com/actions/toolkit/pull/1025)
+271 -81
View File
@@ -1,74 +1,61 @@
{ {
"name": "@actions/tool-cache", "name": "@actions/tool-cache",
"version": "2.0.1", "version": "1.7.2",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@actions/tool-cache", "name": "@actions/tool-cache",
"version": "2.0.1", "version": "1.7.2",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.2.6", "@actions/core": "^1.2.6",
"@actions/exec": "^1.0.0", "@actions/exec": "^1.0.0",
"@actions/http-client": "^2.0.1", "@actions/http-client": "^1.0.8",
"@actions/io": "^1.1.1", "@actions/io": "^1.1.1",
"semver": "^6.1.0", "semver": "^6.1.0",
"uuid": "^3.3.2" "uuid": "^3.3.2"
}, },
"devDependencies": { "devDependencies": {
"@types/nock": "^11.1.0", "@types/nock": "^10.0.3",
"@types/semver": "^6.0.0", "@types/semver": "^6.0.0",
"@types/uuid": "^3.4.4", "@types/uuid": "^3.4.4",
"nock": "^13.2.9" "nock": "^10.0.6"
} }
}, },
"node_modules/@actions/core": { "node_modules/@actions/core": {
"version": "1.9.1", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
"integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==", "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
"dependencies": {
"@actions/http-client": "^2.0.1",
"uuid": "^8.3.2"
}
},
"node_modules/@actions/core/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
}, },
"node_modules/@actions/exec": { "node_modules/@actions/exec": {
"version": "1.1.1", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.3.tgz",
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", "integrity": "sha512-TogJGnueOmM7ntCi0ASTUj4LapRRtDfj57Ja4IhPmg2fls28uVOPbAn8N+JifaOumN2UG3oEO/Ixek2A4NcYSA==",
"dependencies": { "dependencies": {
"@actions/io": "^1.0.1" "@actions/io": "^1.0.1"
} }
}, },
"node_modules/@actions/http-client": { "node_modules/@actions/http-client": {
"version": "2.0.1", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.8.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "integrity": "sha512-G4JjJ6f9Hb3Zvejj+ewLLKLf99ZC+9v+yCxoYf9vSyH+WkzPLB2LuUtRMGNkooMqdugGBFStIKXOuvH1W+EctA==",
"dependencies": { "dependencies": {
"tunnel": "^0.0.6" "tunnel": "0.0.6"
} }
}, },
"node_modules/@actions/io": { "node_modules/@actions/io": {
"version": "1.1.2", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.1.tgz",
"integrity": "sha512-d+RwPlMp+2qmBfeLYPLXuSRykDIFEwdTA0MMxzS9kh4kvP1ftrc/9fzy6pX6qAjthdXruHQ6/6kjT/DNo5ALuw==" "integrity": "sha512-Qi4JoKXjmE0O67wAOH6y0n26QXhMKMFo7GD/4IXNVcrtLjUlGjGuVys6pQgwF3ArfGTQu0XpqaNr0YhED2RaRA=="
}, },
"node_modules/@types/nock": { "node_modules/@types/nock": {
"version": "11.1.0", "version": "10.0.3",
"resolved": "https://registry.npmjs.org/@types/nock/-/nock-11.1.0.tgz", "resolved": "https://registry.npmjs.org/@types/nock/-/nock-10.0.3.tgz",
"integrity": "sha512-jI/ewavBQ7X5178262JQR0ewicPAcJhXS/iFaNJl0VHLfyosZ/kwSrsa6VNQNSO8i9d8SqdRgOtZSOKJ/+iNMw==", "integrity": "sha512-OthuN+2FuzfZO3yONJ/QVjKmLEuRagS9TV9lEId+WHL9KhftYG+/2z+pxlr0UgVVXSpVD8woie/3fzQn8ft/Ow==",
"deprecated": "This is a stub types definition. nock provides its own type definitions, so you do not need this installed.",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"nock": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
@@ -92,6 +79,41 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/assertion-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
"dev": true,
"engines": {
"node": "*"
}
},
"node_modules/chai": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
"integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==",
"dev": true,
"dependencies": {
"assertion-error": "^1.1.0",
"check-error": "^1.0.2",
"deep-eql": "^3.0.1",
"get-func-name": "^2.0.0",
"pathval": "^1.1.0",
"type-detect": "^4.0.5"
},
"engines": {
"node": ">=4"
}
},
"node_modules/check-error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
"dev": true,
"engines": {
"node": "*"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@@ -102,6 +124,33 @@
"ms": "^2.1.1" "ms": "^2.1.1"
} }
}, },
"node_modules/deep-eql": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
"integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
"dev": true,
"dependencies": {
"type-detect": "^4.0.0"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/deep-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
"integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
"dev": true
},
"node_modules/get-func-name": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
"dev": true,
"engines": {
"node": "*"
}
},
"node_modules/json-stringify-safe": { "node_modules/json-stringify-safe": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
@@ -114,6 +163,24 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true "dev": true
}, },
"node_modules/minimist": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"node_modules/mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"dependencies": {
"minimist": "^1.2.5"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -121,27 +188,59 @@
"dev": true "dev": true
}, },
"node_modules/nock": { "node_modules/nock": {
"version": "13.2.9", "version": "10.0.6",
"resolved": "https://registry.npmjs.org/nock/-/nock-13.2.9.tgz", "resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz",
"integrity": "sha512-1+XfJNYF1cjGB+TKMWi29eZ0b82QOvQs2YoLNzbpWGqFMtRQHTa57osqdGj4FrFPgkO4D4AZinzUJR9VvW3QUA==", "integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"chai": "^4.1.2",
"debug": "^4.1.0", "debug": "^4.1.0",
"deep-equal": "^1.0.0",
"json-stringify-safe": "^5.0.1", "json-stringify-safe": "^5.0.1",
"lodash": "^4.17.21", "lodash": "^4.17.5",
"propagate": "^2.0.0" "mkdirp": "^0.5.0",
"propagate": "^1.0.0",
"qs": "^6.5.1",
"semver": "^5.5.0"
}, },
"engines": { "engines": {
"node": ">= 10.13" "node": ">= 6.0"
}
},
"node_modules/nock/node_modules/semver": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
"dev": true,
"bin": {
"semver": "bin/semver"
}
},
"node_modules/pathval": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
"integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
"dev": true,
"engines": {
"node": "*"
} }
}, },
"node_modules/propagate": { "node_modules/propagate": {
"version": "2.0.1", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz",
"integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=",
"dev": true,
"engines": [
"node >= 0.8.1"
]
},
"node_modules/qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">= 8" "node": ">=0.6"
} }
}, },
"node_modules/semver": { "node_modules/semver": {
@@ -160,6 +259,15 @@
"node": ">=0.6.11 <=0.7.0 || >=0.7.3" "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
} }
}, },
"node_modules/type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/uuid": { "node_modules/uuid": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
@@ -172,49 +280,38 @@
}, },
"dependencies": { "dependencies": {
"@actions/core": { "@actions/core": {
"version": "1.9.1", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
"integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==", "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
"requires": {
"@actions/http-client": "^2.0.1",
"uuid": "^8.3.2"
},
"dependencies": {
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
}
}
}, },
"@actions/exec": { "@actions/exec": {
"version": "1.1.1", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.3.tgz",
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", "integrity": "sha512-TogJGnueOmM7ntCi0ASTUj4LapRRtDfj57Ja4IhPmg2fls28uVOPbAn8N+JifaOumN2UG3oEO/Ixek2A4NcYSA==",
"requires": { "requires": {
"@actions/io": "^1.0.1" "@actions/io": "^1.0.1"
} }
}, },
"@actions/http-client": { "@actions/http-client": {
"version": "2.0.1", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.8.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "integrity": "sha512-G4JjJ6f9Hb3Zvejj+ewLLKLf99ZC+9v+yCxoYf9vSyH+WkzPLB2LuUtRMGNkooMqdugGBFStIKXOuvH1W+EctA==",
"requires": { "requires": {
"tunnel": "^0.0.6" "tunnel": "0.0.6"
} }
}, },
"@actions/io": { "@actions/io": {
"version": "1.1.2", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.1.tgz",
"integrity": "sha512-d+RwPlMp+2qmBfeLYPLXuSRykDIFEwdTA0MMxzS9kh4kvP1ftrc/9fzy6pX6qAjthdXruHQ6/6kjT/DNo5ALuw==" "integrity": "sha512-Qi4JoKXjmE0O67wAOH6y0n26QXhMKMFo7GD/4IXNVcrtLjUlGjGuVys6pQgwF3ArfGTQu0XpqaNr0YhED2RaRA=="
}, },
"@types/nock": { "@types/nock": {
"version": "11.1.0", "version": "10.0.3",
"resolved": "https://registry.npmjs.org/@types/nock/-/nock-11.1.0.tgz", "resolved": "https://registry.npmjs.org/@types/nock/-/nock-10.0.3.tgz",
"integrity": "sha512-jI/ewavBQ7X5178262JQR0ewicPAcJhXS/iFaNJl0VHLfyosZ/kwSrsa6VNQNSO8i9d8SqdRgOtZSOKJ/+iNMw==", "integrity": "sha512-OthuN+2FuzfZO3yONJ/QVjKmLEuRagS9TV9lEId+WHL9KhftYG+/2z+pxlr0UgVVXSpVD8woie/3fzQn8ft/Ow==",
"dev": true, "dev": true,
"requires": { "requires": {
"nock": "*" "@types/node": "*"
} }
}, },
"@types/node": { "@types/node": {
@@ -238,6 +335,32 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"assertion-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
"dev": true
},
"chai": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
"integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==",
"dev": true,
"requires": {
"assertion-error": "^1.1.0",
"check-error": "^1.0.2",
"deep-eql": "^3.0.1",
"get-func-name": "^2.0.0",
"pathval": "^1.1.0",
"type-detect": "^4.0.5"
}
},
"check-error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
"dev": true
},
"debug": { "debug": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@@ -247,6 +370,27 @@
"ms": "^2.1.1" "ms": "^2.1.1"
} }
}, },
"deep-eql": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
"integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
"dev": true,
"requires": {
"type-detect": "^4.0.0"
}
},
"deep-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
"integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
"dev": true
},
"get-func-name": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
"dev": true
},
"json-stringify-safe": { "json-stringify-safe": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
@@ -259,6 +403,21 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true "dev": true
}, },
"minimist": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"ms": { "ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -266,21 +425,46 @@
"dev": true "dev": true
}, },
"nock": { "nock": {
"version": "13.2.9", "version": "10.0.6",
"resolved": "https://registry.npmjs.org/nock/-/nock-13.2.9.tgz", "resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz",
"integrity": "sha512-1+XfJNYF1cjGB+TKMWi29eZ0b82QOvQs2YoLNzbpWGqFMtRQHTa57osqdGj4FrFPgkO4D4AZinzUJR9VvW3QUA==", "integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==",
"dev": true, "dev": true,
"requires": { "requires": {
"chai": "^4.1.2",
"debug": "^4.1.0", "debug": "^4.1.0",
"deep-equal": "^1.0.0",
"json-stringify-safe": "^5.0.1", "json-stringify-safe": "^5.0.1",
"lodash": "^4.17.21", "lodash": "^4.17.5",
"propagate": "^2.0.0" "mkdirp": "^0.5.0",
"propagate": "^1.0.0",
"qs": "^6.5.1",
"semver": "^5.5.0"
},
"dependencies": {
"semver": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
"dev": true
}
} }
}, },
"pathval": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
"integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
"dev": true
},
"propagate": { "propagate": {
"version": "2.0.1", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz",
"integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=",
"dev": true
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
"dev": true "dev": true
}, },
"semver": { "semver": {
@@ -293,6 +477,12 @@
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
}, },
"type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true
},
"uuid": { "uuid": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/tool-cache", "name": "@actions/tool-cache",
"version": "2.0.1", "version": "1.7.2",
"description": "Actions tool-cache lib", "description": "Actions tool-cache lib",
"keywords": [ "keywords": [
"github", "github",
@@ -38,15 +38,15 @@
"dependencies": { "dependencies": {
"@actions/core": "^1.2.6", "@actions/core": "^1.2.6",
"@actions/exec": "^1.0.0", "@actions/exec": "^1.0.0",
"@actions/http-client": "^2.0.1", "@actions/http-client": "^1.0.8",
"@actions/io": "^1.1.1", "@actions/io": "^1.1.1",
"semver": "^6.1.0", "semver": "^6.1.0",
"uuid": "^3.3.2" "uuid": "^3.3.2"
}, },
"devDependencies": { "devDependencies": {
"@types/nock": "^11.1.0", "@types/nock": "^10.0.3",
"@types/semver": "^6.0.0", "@types/semver": "^6.0.0",
"@types/uuid": "^3.4.4", "@types/uuid": "^3.4.4",
"nock": "^13.2.9" "nock": "^10.0.6"
} }
} }

Some files were not shown because too many files have changed in this diff Show More