Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4eaf5d56fb | |||
| 0a588c33a3 | |||
| 8360baed2c | |||
| 5c3e1c231d | |||
| fe8d95a8fc | |||
| b2c6bee10a | |||
| eb88fce3c0 | |||
| a7aa89a929 | |||
| d7dd89f52b | |||
| 3da67ac4cb | |||
| 0bab3623f4 | |||
| af75719a1e | |||
| d9212ff45b | |||
| 2b58973dac | |||
| 4631854e0f | |||
| 09e9478907 | |||
| ea81280a4d | |||
| 1f8d7b5a64 | |||
| 1c03cd3284 | |||
| 1162975200 | |||
| 3ceb264e9b | |||
| 619566e5b8 | |||
| 547e30cada | |||
| 22e5d95310 | |||
| 1c86c4c890 | |||
| c7ec4073b7 | |||
| d0f4aae179 | |||
| dac801e6b9 | |||
| 33891d9aef | |||
| cca2b1808b | |||
| 5d9c674092 | |||
| aa1968c9e9 | |||
| f55900670f | |||
| 0a94a783ee | |||
| 9c6e7d8265 | |||
| 5afccaa9db | |||
| 0c1cb726c3 | |||
| f0b00fd201 | |||
| ff90431d27 | |||
| a2adaa856b | |||
| 662a937248 | |||
| 330dc0b5b8 | |||
| 58dfa1c4ac | |||
| 456cf5a97f | |||
| 7965cc3c7d | |||
| f541fb1ac9 | |||
| a6114b695e | |||
| 885469e8ce | |||
| 962ff70002 | |||
| 8071504f3c | |||
| 9df74283c2 | |||
| 4831d7a53b | |||
| 53a752919b | |||
| c45ad60078 | |||
| f7330892f1 | |||
| 1322acbcca | |||
| bdacfc4c65 | |||
| 4564768940 | |||
| a31b7eca9e | |||
| 9167ce1f3a | |||
| 11601c0d2d | |||
| b9414eecb3 | |||
| 243a8bba07 | |||
| c5e1af5dc3 | |||
| c9af6bb1b3 | |||
| bf4ce74a0f | |||
| db21627995 | |||
| bb2f39337d | |||
| dc4b4dab1d | |||
| 8df94d9879 | |||
| c5035362ab | |||
| 439eaced07 | |||
| 51dc07a106 | |||
| 36b8c66aec | |||
| aa29345ae8 | |||
| e1a7863be6 | |||
| c507914181 | |||
| a65bca60a1 | |||
| a1b068ec31 | |||
| 6e33c78c3d | |||
| 9ac66375a0 | |||
| ddd04b6997 | |||
| 566ea66979 | |||
| 0d74e9080a | |||
| 8dc2d6eb6a | |||
| 3bd746139f | |||
| f915ace085 | |||
| 1bafbed467 | |||
| 98549fbf21 | |||
| b33912b7cc | |||
| cac7db2d19 | |||
| 1c367e0a26 | |||
| 09e59b9a5c | |||
| fecf6cdd59 | |||
| 2b97eb3192 | |||
| ed490dc20d |
+2
-1
@@ -1,3 +1,4 @@
|
||||
node_modules/
|
||||
packages/*/node_modules/
|
||||
packages/*/lib/
|
||||
packages/*/lib/
|
||||
packages/glob/__tests__/_temp
|
||||
+18
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"plugins": ["jest", "@typescript-eslint"],
|
||||
"extends": ["plugin:github/es6"],
|
||||
"extends": ["plugin:github/recommended"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 9,
|
||||
@@ -9,20 +9,34 @@
|
||||
},
|
||||
"rules": {
|
||||
"eslint-comments/no-use": "off",
|
||||
"github/no-then": "off",
|
||||
"import/no-namespace": "off",
|
||||
"no-shadow": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-undef": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
|
||||
"@typescript-eslint/no-require-imports": "error",
|
||||
"@typescript-eslint/array-type": "error",
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
"@typescript-eslint/ban-ts-ignore": "error",
|
||||
"@typescript-eslint/ban-ts-comment": "error",
|
||||
"camelcase": "off",
|
||||
"@typescript-eslint/camelcase": "off",
|
||||
"@typescript-eslint/class-name-casing": "error",
|
||||
"@typescript-eslint/consistent-type-assertions": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
|
||||
"@typescript-eslint/func-call-spacing": ["error", "never"],
|
||||
"@typescript-eslint/generic-type-naming": ["error", "^[A-Z][A-Za-z]*$"],
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"format": null,
|
||||
"filter": {
|
||||
// you can expand this regex as you find more cases that require quoting that you want to allow
|
||||
"regex": "^[A-Z][A-Za-z]*$",
|
||||
"match": true
|
||||
},
|
||||
"selector": "memberLike"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-array-constructor": "error",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
@@ -32,7 +46,6 @@
|
||||
"@typescript-eslint/no-misused-new": "error",
|
||||
"@typescript-eslint/no-namespace": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||
"@typescript-eslint/no-object-literal-type-assertion": "error",
|
||||
"@typescript-eslint/no-unnecessary-qualifier": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
"@typescript-eslint/no-useless-constructor": "error",
|
||||
@@ -40,7 +53,6 @@
|
||||
"@typescript-eslint/prefer-for-of": "warn",
|
||||
"@typescript-eslint/prefer-function-type": "warn",
|
||||
"@typescript-eslint/prefer-includes": "error",
|
||||
"@typescript-eslint/prefer-interface": "error",
|
||||
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
||||
"@typescript-eslint/promise-function-async": "error",
|
||||
"@typescript-eslint/require-array-sort-compare": "error",
|
||||
|
||||
@@ -31,8 +31,9 @@ jobs:
|
||||
- name: Bootstrap
|
||||
run: npm run bootstrap
|
||||
|
||||
# - name: audit tools #disabled while we wait for https://github.com/actions/toolkit/issues/539
|
||||
# run: npm audit --audit-level=moderate
|
||||
- name: audit tools
|
||||
# `|| npm audit` to pretty-print the output if vulnerabilies are found after filtering.
|
||||
run: npm audit --audit-level=moderate --json | scripts/audit-allow-list || npm audit --audit-level=moderate
|
||||
|
||||
- name: audit packages
|
||||
run: npm run audit-all
|
||||
|
||||
@@ -2,6 +2,8 @@ name: "Code Scanning - Action"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@@ -6,7 +6,7 @@ Problem Matchers are a way to scan the output of actions for a specified regex p
|
||||
|
||||
Currently, GitHub Actions limit the annotation count in a workflow run.
|
||||
|
||||
- 10 warning annotations and 10 error annotations per step
|
||||
- 10 warning annotations, 10 error annotations, and 10 notice annotations per step
|
||||
- 50 annotations per job (sum of annotations from all the steps)
|
||||
- 50 annotations per run (separate from the job annotations, these annotations aren’t created by users)
|
||||
|
||||
@@ -144,6 +144,6 @@ Use ECMAScript regular expression syntax when testing patterns.
|
||||
|
||||
### File property getting dropped
|
||||
|
||||
[Enable debug logging](https://help.github.com/en/actions/configuring-and-managing-workflows/managing-a-workflow-run#enabling-debug-logging) to determine why the file is getting dropped.
|
||||
[Enable debug logging](https://docs.github.com/en/actions/managing-workflow-runs/enabling-debug-logging) to determine why the file is getting dropped.
|
||||
|
||||
This usually happens when the file does not exist or is not under the workflow repo.
|
||||
|
||||
Generated
+32146
-12842
File diff suppressed because it is too large
Load Diff
+13
-12
@@ -9,24 +9,25 @@
|
||||
"format": "prettier --write packages/**/*.ts",
|
||||
"format-check": "prettier --check packages/**/*.ts",
|
||||
"lint": "eslint packages/**/*.ts",
|
||||
"lint-fix": "eslint packages/**/*.ts --fix",
|
||||
"new-package": "scripts/create-package",
|
||||
"test": "jest --testTimeout 10000"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^24.0.11",
|
||||
"@types/node": "^12.12.47",
|
||||
"@types/signale": "^1.2.1",
|
||||
"@typescript-eslint/parser": "^2.2.7",
|
||||
"concurrently": "^4.1.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-github": "^2.0.0",
|
||||
"eslint-plugin-jest": "^22.5.1",
|
||||
"@types/jest": "^24.9.1",
|
||||
"@types/node": "^12.20.13",
|
||||
"@types/signale": "^1.4.1",
|
||||
"@typescript-eslint/parser": "^4.0.0",
|
||||
"concurrently": "^6.1.0",
|
||||
"eslint": "^7.23.0",
|
||||
"eslint-plugin-github": "^4.1.3",
|
||||
"eslint-plugin-jest": "^22.21.0",
|
||||
"flow-bin": "^0.115.0",
|
||||
"jest": "^25.1.0",
|
||||
"jest-circus": "^24.7.1",
|
||||
"lerna": "^3.18.4",
|
||||
"jest": "^26.6.3",
|
||||
"jest-circus": "^24.9.0",
|
||||
"lerna": "^4.0.0",
|
||||
"prettier": "^1.19.1",
|
||||
"ts-jest": "^25.4.0",
|
||||
"ts-jest": "^26.5.6",
|
||||
"typescript": "^3.9.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,3 +58,6 @@
|
||||
|
||||
- Bump @actions/http-client to version 1.0.11 to fix proxy related issues during artifact upload and download
|
||||
|
||||
### 0.5.2
|
||||
|
||||
- Add HTTP 500 as a retryable status code for artifact upload and download.
|
||||
@@ -71,7 +71,7 @@ describe('Download Tests', () => {
|
||||
setupFailedResponse()
|
||||
const downloadHttpClient = new DownloadHttpClient()
|
||||
expect(downloadHttpClient.listArtifacts()).rejects.toThrow(
|
||||
'List Artifacts failed: Artifact service responded with 500'
|
||||
'List Artifacts failed: Artifact service responded with 400'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -113,7 +113,7 @@ describe('Download Tests', () => {
|
||||
configVariables.getRuntimeUrl()
|
||||
)
|
||||
).rejects.toThrow(
|
||||
`Get Container Items failed: Artifact service responded with 500`
|
||||
`Get Container Items failed: Artifact service responded with 400`
|
||||
)
|
||||
})
|
||||
|
||||
@@ -166,7 +166,7 @@ describe('Download Tests', () => {
|
||||
it('Test retryable status codes during artifact download', async () => {
|
||||
// The first http response should return a retryable status call while the subsequent call should return a 200 so
|
||||
// the download should successfully finish
|
||||
const retryableStatusCodes = [429, 502, 503, 504]
|
||||
const retryableStatusCodes = [429, 500, 502, 503, 504]
|
||||
for (const statusCode of retryableStatusCodes) {
|
||||
const fileContents = Buffer.from('try, try again\n', defaultEncoding)
|
||||
const targetPath = path.join(root, `FileC-${statusCode}.txt`)
|
||||
@@ -357,7 +357,7 @@ describe('Download Tests', () => {
|
||||
plaintext: Buffer | string
|
||||
): Promise<Buffer> {
|
||||
if (isGzip) {
|
||||
return <Buffer>await promisify(gzip)(plaintext)
|
||||
return await promisify(gzip)(plaintext)
|
||||
} else if (typeof plaintext === 'string') {
|
||||
return Buffer.from(plaintext, defaultEncoding)
|
||||
} else {
|
||||
@@ -468,7 +468,7 @@ describe('Download Tests', () => {
|
||||
function setupFailedResponse(): void {
|
||||
jest.spyOn(HttpClient.prototype, 'get').mockImplementationOnce(async () => {
|
||||
const mockMessage = new http.IncomingMessage(new net.Socket())
|
||||
mockMessage.statusCode = 500
|
||||
mockMessage.statusCode = 400
|
||||
return new Promise<HttpClientResponse>(resolve => {
|
||||
resolve({
|
||||
message: mockMessage,
|
||||
|
||||
@@ -107,8 +107,8 @@ test('retry fails after exhausting retries', async () => {
|
||||
})
|
||||
|
||||
test('retry fails after non-retryable status code', async () => {
|
||||
await testRetry([500, 200], {
|
||||
responseCode: 500,
|
||||
errorMessage: 'test failed: Artifact service responded with 500'
|
||||
await testRetry([400, 200], {
|
||||
responseCode: 400,
|
||||
errorMessage: 'test failed: Artifact service responded with 400'
|
||||
})
|
||||
})
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/artifact",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/artifact",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"preview": true,
|
||||
"description": "Actions artifact lib",
|
||||
"keywords": [
|
||||
|
||||
@@ -69,7 +69,7 @@ export async function retry(
|
||||
throw Error(`${name} failed: ${errorMessage}`)
|
||||
}
|
||||
|
||||
export async function retryHttpClientRequest<T>(
|
||||
export async function retryHttpClientRequest(
|
||||
name: string,
|
||||
method: () => Promise<IHttpClientResponse>,
|
||||
customErrorMessages: Map<number, string> = new Map(),
|
||||
|
||||
@@ -72,8 +72,9 @@ export function isRetryableStatusCode(statusCode: number | undefined): boolean {
|
||||
|
||||
const retryableStatusCodes = [
|
||||
HttpCodes.BadGateway,
|
||||
HttpCodes.ServiceUnavailable,
|
||||
HttpCodes.GatewayTimeout,
|
||||
HttpCodes.InternalServerError,
|
||||
HttpCodes.ServiceUnavailable,
|
||||
HttpCodes.TooManyRequests,
|
||||
413 // Payload Too Large
|
||||
]
|
||||
|
||||
+4
-3
@@ -87,7 +87,7 @@ test('download progress tracked correctly', () => {
|
||||
expect(progress.isDone()).toBe(true)
|
||||
})
|
||||
|
||||
test('display timer works correctly', () => {
|
||||
test('display timer works correctly', done => {
|
||||
const progress = new DownloadProgress(1000)
|
||||
|
||||
const infoMock = jest.spyOn(core, 'info')
|
||||
@@ -103,6 +103,7 @@ test('display timer works correctly', () => {
|
||||
const test2 = (): void => {
|
||||
check()
|
||||
expect(progress.timeoutHandle).toBeUndefined()
|
||||
done()
|
||||
}
|
||||
|
||||
// Validate the progress is displayed, stop the timer, and call test2.
|
||||
@@ -112,7 +113,7 @@ test('display timer works correctly', () => {
|
||||
progress.stopDisplayTimer()
|
||||
progress.setReceivedBytes(1000)
|
||||
|
||||
setTimeout(() => test2(), 100)
|
||||
setTimeout(() => test2(), 500)
|
||||
}
|
||||
|
||||
// Start the timer, update the received bytes, and call test1.
|
||||
@@ -122,7 +123,7 @@ test('display timer works correctly', () => {
|
||||
|
||||
progress.setReceivedBytes(500)
|
||||
|
||||
setTimeout(() => test1(), 100)
|
||||
setTimeout(() => test1(), 500)
|
||||
}
|
||||
|
||||
start()
|
||||
|
||||
@@ -30,7 +30,6 @@ async function handleResponse(
|
||||
response: ITestResponse | undefined
|
||||
): Promise<ITestResponse> {
|
||||
if (!response) {
|
||||
// eslint-disable-next-line no-undef
|
||||
fail('Retry method called too many times')
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ beforeAll(() => {
|
||||
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
||||
jest.spyOn(core, 'error').mockImplementation(() => {})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
||||
jest.spyOn(cacheUtils, 'getCacheFileName').mockImplementation(cm => {
|
||||
const actualUtils = jest.requireActual('../src/internal/cacheUtils')
|
||||
return actualUtils.getCacheFileName(cm)
|
||||
|
||||
-1
@@ -17,7 +17,6 @@ beforeAll(() => {
|
||||
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
||||
jest.spyOn(core, 'error').mockImplementation(() => {})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
||||
jest.spyOn(cacheUtils, 'getCacheFileName').mockImplementation(cm => {
|
||||
const actualUtils = jest.requireActual('../src/internal/cacheUtils')
|
||||
return actualUtils.getCacheFileName(cm)
|
||||
|
||||
+4
-26
@@ -59,7 +59,6 @@
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.1",
|
||||
"json5": "^2.1.2",
|
||||
"lodash": "^4.17.13",
|
||||
"resolve": "^1.3.2",
|
||||
"semver": "^5.4.1",
|
||||
"source-map": "^0.5.0"
|
||||
@@ -79,7 +78,6 @@
|
||||
"requires": {
|
||||
"@babel/types": "^7.9.0",
|
||||
"jsesc": "^2.5.1",
|
||||
"lodash": "^4.17.13",
|
||||
"source-map": "^0.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -134,8 +132,7 @@
|
||||
"@babel/helper-simple-access": "^7.8.3",
|
||||
"@babel/helper-split-export-declaration": "^7.8.3",
|
||||
"@babel/template": "^7.8.6",
|
||||
"@babel/types": "^7.9.0",
|
||||
"lodash": "^4.17.13"
|
||||
"@babel/types": "^7.9.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-optimise-call-expression": {
|
||||
@@ -293,8 +290,7 @@
|
||||
"@babel/parser": "^7.9.0",
|
||||
"@babel/types": "^7.9.0",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0",
|
||||
"lodash": "^4.17.13"
|
||||
"globals": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
@@ -303,7 +299,6 @@
|
||||
"integrity": "sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng==",
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.9.0",
|
||||
"lodash": "^4.17.13",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
@@ -2557,7 +2552,6 @@
|
||||
"whatwg-encoding": "^1.0.5",
|
||||
"whatwg-mimetype": "^2.3.0",
|
||||
"whatwg-url": "^7.0.0",
|
||||
"ws": "^7.0.0",
|
||||
"xml-name-validator": "^3.0.0"
|
||||
}
|
||||
},
|
||||
@@ -2632,11 +2626,6 @@
|
||||
"p-locate": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
||||
},
|
||||
"lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
@@ -3202,10 +3191,7 @@
|
||||
"request-promise-core": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz",
|
||||
"integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
"integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ=="
|
||||
},
|
||||
"request-promise-native": {
|
||||
"version": "1.0.8",
|
||||
@@ -3734,10 +3720,7 @@
|
||||
"string-similarity": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-1.1.0.tgz",
|
||||
"integrity": "sha1-PGZJiFikZex8QMfYFzm72ZWQSRQ=",
|
||||
"requires": {
|
||||
"lodash": "^4.13.1"
|
||||
}
|
||||
"integrity": "sha1-PGZJiFikZex8QMfYFzm72ZWQSRQ="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "4.2.0",
|
||||
@@ -4184,11 +4167,6 @@
|
||||
"typedarray-to-buffer": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.2.3.tgz",
|
||||
"integrity": "sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ=="
|
||||
},
|
||||
"xml-name-validator": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
|
||||
|
||||
Vendored
+25
-16
@@ -166,24 +166,33 @@ export async function saveCache(
|
||||
|
||||
core.debug(`Archive Path: ${archivePath}`)
|
||||
|
||||
await createTar(archiveFolder, cachePaths, compressionMethod)
|
||||
if (core.isDebug()) {
|
||||
await listTar(archivePath, compressionMethod)
|
||||
}
|
||||
try {
|
||||
await createTar(archiveFolder, cachePaths, compressionMethod)
|
||||
if (core.isDebug()) {
|
||||
await listTar(archivePath, compressionMethod)
|
||||
}
|
||||
|
||||
const fileSizeLimit = 5 * 1024 * 1024 * 1024 // 5GB per repo limit
|
||||
const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath)
|
||||
core.debug(`File Size: ${archiveFileSize}`)
|
||||
if (archiveFileSize > fileSizeLimit) {
|
||||
throw new Error(
|
||||
`Cache size of ~${Math.round(
|
||||
archiveFileSize / (1024 * 1024)
|
||||
)} MB (${archiveFileSize} B) is over the 5GB limit, not saving cache.`
|
||||
)
|
||||
}
|
||||
const fileSizeLimit = 5 * 1024 * 1024 * 1024 // 5GB per repo limit
|
||||
const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath)
|
||||
core.debug(`File Size: ${archiveFileSize}`)
|
||||
if (archiveFileSize > fileSizeLimit) {
|
||||
throw new Error(
|
||||
`Cache size of ~${Math.round(
|
||||
archiveFileSize / (1024 * 1024)
|
||||
)} MB (${archiveFileSize} B) is over the 5GB limit, not saving cache.`
|
||||
)
|
||||
}
|
||||
|
||||
core.debug(`Saving Cache (ID: ${cacheId})`)
|
||||
await cacheHttpClient.saveCache(cacheId, archivePath, options)
|
||||
core.debug(`Saving Cache (ID: ${cacheId})`)
|
||||
await cacheHttpClient.saveCache(cacheId, archivePath, options)
|
||||
} finally {
|
||||
// Try to delete the archive to save space
|
||||
try {
|
||||
await utils.unlinkFile(archivePath)
|
||||
} catch (error) {
|
||||
core.debug(`Failed to delete archive: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
return cacheId
|
||||
}
|
||||
|
||||
+1
-1
@@ -133,7 +133,7 @@ export class DownloadProgress {
|
||||
*
|
||||
* @param delayInMs the delay between each write
|
||||
*/
|
||||
startDisplayTimer(delayInMs: number = 1000): void {
|
||||
startDisplayTimer(delayInMs = 1000): void {
|
||||
const displayCallback = (): void => {
|
||||
this.display()
|
||||
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ export async function retryTypedResponse<T>(
|
||||
)
|
||||
}
|
||||
|
||||
export async function retryHttpClientResponse<T>(
|
||||
export async function retryHttpClientResponse(
|
||||
name: string,
|
||||
method: () => Promise<IHttpClientResponse>,
|
||||
maxAttempts = DefaultRetryAttempts,
|
||||
|
||||
@@ -23,6 +23,7 @@ Outputs can be set with `setOutput` which makes them available to be mapped into
|
||||
```js
|
||||
const myInput = core.getInput('inputName', { required: true });
|
||||
const myBooleanInput = core.getBooleanInput('booleanInputName', { required: true });
|
||||
const myMultilineInput = core.getMultilineInput('multilineInputName', { required: true });
|
||||
core.setOutput('outputKey', 'outputVal');
|
||||
```
|
||||
|
||||
@@ -91,6 +92,8 @@ try {
|
||||
|
||||
// Do stuff
|
||||
core.info('Output to the actions build log')
|
||||
|
||||
core.notice('This is a message that will also emit an annotation')
|
||||
}
|
||||
catch (err) {
|
||||
core.error(`Error ${err}, action may still succeed though`);
|
||||
@@ -114,6 +117,54 @@ const result = await core.group('Do something async', async () => {
|
||||
})
|
||||
```
|
||||
|
||||
#### Annotations
|
||||
|
||||
This library has 3 methods that will produce [annotations](https://docs.github.com/en/rest/reference/checks#create-a-check-run).
|
||||
```js
|
||||
core.error('This is a bad error. This will also fail the build.')
|
||||
|
||||
core.warning('Something went wrong, but it\'s not bad enough to fail the build.')
|
||||
|
||||
core.notice('Something happened that you might want to know about.')
|
||||
```
|
||||
|
||||
These will surface to the UI in the Actions page and on Pull Requests. They look something like this:
|
||||
|
||||

|
||||
|
||||
These annotations can also be attached to particular lines and columns of your source files to show exactly where a problem is occuring.
|
||||
|
||||
These options are:
|
||||
```typescript
|
||||
export interface AnnotationProperties {
|
||||
/**
|
||||
* A title for the annotation.
|
||||
*/
|
||||
title?: string
|
||||
|
||||
/**
|
||||
* The start line for the annotation.
|
||||
*/
|
||||
startLine?: number
|
||||
|
||||
/**
|
||||
* The end line for the annotation. Defaults to `startLine` when `startLine` is provided.
|
||||
*/
|
||||
endLine?: number
|
||||
|
||||
/**
|
||||
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
|
||||
*/
|
||||
startColumn?: number
|
||||
|
||||
/**
|
||||
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
|
||||
* Defaults to `startColumn` when `startColumn` is provided.
|
||||
*/
|
||||
endColumn?: number
|
||||
}
|
||||
```
|
||||
|
||||
#### Styling output
|
||||
|
||||
Colored output is supported in the Action logs via standard [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code). 3/4 bit, 8 bit and 24 bit colors are all supported.
|
||||
@@ -206,3 +257,51 @@ var pid = core.getState("pidToKill");
|
||||
|
||||
process.kill(pid);
|
||||
```
|
||||
|
||||
#### OIDC Token
|
||||
|
||||
You can use these methods to interact with the GitHub OIDC provider and get a JWT ID token which would help to get access token from third party cloud providers.
|
||||
|
||||
**Method Name**: getIDToken()
|
||||
|
||||
**Inputs**
|
||||
|
||||
audience : optional
|
||||
|
||||
**Outputs**
|
||||
|
||||
A [JWT](https://jwt.io/) ID Token
|
||||
|
||||
In action's `main.ts`:
|
||||
```js
|
||||
const core = require('@actions/core');
|
||||
async function getIDTokenAction(): Promise<void> {
|
||||
|
||||
const audience = core.getInput('audience', {required: false})
|
||||
|
||||
const id_token1 = await core.getIDToken() // ID Token with default audience
|
||||
const id_token2 = await core.getIDToken(audience) // ID token with custom audience
|
||||
|
||||
// this id_token can be used to get access token from third party cloud providers
|
||||
}
|
||||
getIDTokenAction()
|
||||
```
|
||||
|
||||
In action's `actions.yml`:
|
||||
|
||||
```yaml
|
||||
name: 'GetIDToken'
|
||||
description: 'Get ID token from Github OIDC provider'
|
||||
inputs:
|
||||
audience:
|
||||
description: 'Audience for which the ID token is intended for'
|
||||
required: false
|
||||
outputs:
|
||||
id_token1:
|
||||
description: 'ID token obtained from OIDC provider'
|
||||
id_token2:
|
||||
description: 'ID token obtained from OIDC provider'
|
||||
runs:
|
||||
using: 'node12'
|
||||
main: 'dist/index.js'
|
||||
```
|
||||
@@ -1,5 +1,19 @@
|
||||
# @actions/core Releases
|
||||
|
||||
### 1.6.0
|
||||
- [Added OIDC Client function `getIDToken`](https://github.com/actions/toolkit/pull/919)
|
||||
- [Added `file` parameter to `AnnotationProperties`](https://github.com/actions/toolkit/pull/896)
|
||||
|
||||
### 1.5.0
|
||||
- [Added support for notice annotations and more annotation fields](https://github.com/actions/toolkit/pull/855)
|
||||
|
||||
### 1.4.0
|
||||
- [Added the `getMultilineInput` function](https://github.com/actions/toolkit/pull/829)
|
||||
|
||||
### 1.3.0
|
||||
- [Added the trimWhitespace option to getInput](https://github.com/actions/toolkit/pull/802)
|
||||
- [Added the getBooleanInput function](https://github.com/actions/toolkit/pull/725)
|
||||
|
||||
### 1.2.7
|
||||
- [Prepend newline for set-output](https://github.com/actions/toolkit/pull/772)
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
import * as core from '../src/core'
|
||||
import {HttpClient} from '@actions/http-client'
|
||||
import {toCommandProperties} from '../src/utils'
|
||||
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
|
||||
@@ -27,6 +29,9 @@ const testEnvVars = {
|
||||
INPUT_BOOLEAN_INPUT_FALSE2: 'False',
|
||||
INPUT_BOOLEAN_INPUT_FALSE3: 'FALSE',
|
||||
INPUT_WRONG_BOOLEAN_INPUT: 'wrong',
|
||||
INPUT_WITH_TRAILING_WHITESPACE: ' some val ',
|
||||
|
||||
INPUT_MY_INPUT_LIST: 'val1\nval2\nval3',
|
||||
|
||||
// Save inputs
|
||||
STATE_TEST_1: 'state_val',
|
||||
@@ -165,6 +170,30 @@ describe('@actions/core', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('getMultilineInput works', () => {
|
||||
expect(core.getMultilineInput('my input list')).toEqual([
|
||||
'val1',
|
||||
'val2',
|
||||
'val3'
|
||||
])
|
||||
})
|
||||
|
||||
it('getInput trims whitespace by default', () => {
|
||||
expect(core.getInput('with trailing whitespace')).toBe('some val')
|
||||
})
|
||||
|
||||
it('getInput trims whitespace when option is explicitly true', () => {
|
||||
expect(
|
||||
core.getInput('with trailing whitespace', {trimWhitespace: true})
|
||||
).toBe('some val')
|
||||
})
|
||||
|
||||
it('getInput does not trim whitespace when option is false', () => {
|
||||
expect(
|
||||
core.getInput('with trailing whitespace', {trimWhitespace: false})
|
||||
).toBe(' some val ')
|
||||
})
|
||||
|
||||
it('getInput gets non-required boolean input', () => {
|
||||
expect(core.getBooleanInput('boolean input')).toBe(true)
|
||||
})
|
||||
@@ -242,6 +271,20 @@ describe('@actions/core', () => {
|
||||
assertWriteCalls([`::error::Error: ${message}${os.EOL}`])
|
||||
})
|
||||
|
||||
it('error handles parameters correctly', () => {
|
||||
const message = 'this is my error message'
|
||||
core.error(new Error(message), {
|
||||
title: 'A title',
|
||||
startColumn: 1,
|
||||
endColumn: 2,
|
||||
startLine: 5,
|
||||
endLine: 5
|
||||
})
|
||||
assertWriteCalls([
|
||||
`::error title=A title,line=5,endLine=5,col=1,endColumn=2::Error: ${message}${os.EOL}`
|
||||
])
|
||||
})
|
||||
|
||||
it('warning sets the correct message', () => {
|
||||
core.warning('Warning')
|
||||
assertWriteCalls([`::warning::Warning${os.EOL}`])
|
||||
@@ -258,6 +301,38 @@ describe('@actions/core', () => {
|
||||
assertWriteCalls([`::warning::Error: ${message}${os.EOL}`])
|
||||
})
|
||||
|
||||
it('warning handles parameters correctly', () => {
|
||||
const message = 'this is my error message'
|
||||
core.warning(new Error(message), {
|
||||
title: 'A title',
|
||||
startColumn: 1,
|
||||
endColumn: 2,
|
||||
startLine: 5,
|
||||
endLine: 5
|
||||
})
|
||||
assertWriteCalls([
|
||||
`::warning title=A title,line=5,endLine=5,col=1,endColumn=2::Error: ${message}${os.EOL}`
|
||||
])
|
||||
})
|
||||
|
||||
it('annotations map field names correctly', () => {
|
||||
const commandProperties = toCommandProperties({
|
||||
title: 'A title',
|
||||
startColumn: 1,
|
||||
endColumn: 2,
|
||||
startLine: 5,
|
||||
endLine: 5
|
||||
})
|
||||
expect(commandProperties.title).toBe('A title')
|
||||
expect(commandProperties.col).toBe(1)
|
||||
expect(commandProperties.endColumn).toBe(2)
|
||||
expect(commandProperties.line).toBe(5)
|
||||
expect(commandProperties.endLine).toBe(5)
|
||||
|
||||
expect(commandProperties.startColumn).toBeUndefined()
|
||||
expect(commandProperties.startLine).toBeUndefined()
|
||||
})
|
||||
|
||||
it('startGroup starts a new group', () => {
|
||||
core.startGroup('my-group')
|
||||
assertWriteCalls([`::group::my-group${os.EOL}`])
|
||||
@@ -360,3 +435,20 @@ function verifyFileCommand(command: string, expectedContents: string): void {
|
||||
fs.unlinkSync(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
function getTokenEndPoint(): string {
|
||||
return 'https://vstoken.actions.githubusercontent.com/.well-known/openid-configuration'
|
||||
}
|
||||
|
||||
describe('oidc-client-tests', () => {
|
||||
it('Get Http Client', async () => {
|
||||
const http = new HttpClient('actions/oidc-client')
|
||||
expect(http).toBeDefined()
|
||||
})
|
||||
|
||||
it('HTTP get request to get token endpoint', async () => {
|
||||
const http = new HttpClient('actions/oidc-client')
|
||||
const res = await http.get(getTokenEndPoint())
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
Generated
+50
-2
@@ -1,14 +1,62 @@
|
||||
{
|
||||
"name": "@actions/core",
|
||||
"version": "1.2.7",
|
||||
"lockfileVersion": 1,
|
||||
"version": "1.6.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@actions/core",
|
||||
"version": "1.6.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^1.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^12.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/http-client": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
|
||||
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
|
||||
"dependencies": {
|
||||
"tunnel": "0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "12.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz",
|
||||
"integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tunnel": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
|
||||
"engines": {
|
||||
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/http-client": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
|
||||
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
|
||||
"requires": {
|
||||
"tunnel": "0.0.6"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "12.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz",
|
||||
"integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==",
|
||||
"dev": true
|
||||
},
|
||||
"tunnel": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/core",
|
||||
"version": "1.2.7",
|
||||
"version": "1.6.0",
|
||||
"description": "Actions core lib",
|
||||
"keywords": [
|
||||
"github",
|
||||
@@ -35,6 +35,9 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/actions/toolkit/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^1.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^12.0.2"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {toCommandValue} from './utils'
|
||||
// We use any as a valid input type
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
interface CommandProperties {
|
||||
export interface CommandProperties {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export function issueCommand(
|
||||
process.stdout.write(cmd.toString() + os.EOL)
|
||||
}
|
||||
|
||||
export function issue(name: string, message: string = ''): void {
|
||||
export function issue(name: string, message = ''): void {
|
||||
issueCommand(name, {}, message)
|
||||
}
|
||||
|
||||
|
||||
+105
-7
@@ -1,16 +1,21 @@
|
||||
import {issue, issueCommand} from './command'
|
||||
import {issueCommand as issueFileCommand} from './file-command'
|
||||
import {toCommandValue} from './utils'
|
||||
import {toCommandProperties, toCommandValue} from './utils'
|
||||
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
|
||||
import {OidcClient} from './oidc-utils'
|
||||
|
||||
/**
|
||||
* Interface for getInput options
|
||||
*/
|
||||
export interface InputOptions {
|
||||
/** Optional. Whether the input is required. If required and not present, will throw. Defaults to false */
|
||||
required?: boolean
|
||||
|
||||
/** Optional. Whether leading/trailing whitespace will be trimmed for the input. Defaults to true */
|
||||
trimWhitespace?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,6 +33,38 @@ export enum ExitCode {
|
||||
Failure = 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional properties that can be sent with annotatation commands (notice, error, and warning)
|
||||
* See: https://docs.github.com/en/rest/reference/checks#create-a-check-run for more information about annotations.
|
||||
*/
|
||||
export interface AnnotationProperties {
|
||||
/**
|
||||
* A title for the annotation.
|
||||
*/
|
||||
title?: string
|
||||
|
||||
/**
|
||||
* The start line for the annotation.
|
||||
*/
|
||||
startLine?: number
|
||||
|
||||
/**
|
||||
* The end line for the annotation. Defaults to `startLine` when `startLine` is provided.
|
||||
*/
|
||||
endLine?: number
|
||||
|
||||
/**
|
||||
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
|
||||
*/
|
||||
startColumn?: number
|
||||
|
||||
/**
|
||||
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
|
||||
* Defaults to `startColumn` when `startColumn` is provided.
|
||||
*/
|
||||
endColumn?: number
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
// Variables
|
||||
//-----------------------------------------------------------------------
|
||||
@@ -75,7 +112,9 @@ export function addPath(inputPath: string): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of an input. The value is also trimmed.
|
||||
* Gets the value of an input.
|
||||
* Unless trimWhitespace is set to false in InputOptions, the value is also trimmed.
|
||||
* Returns an empty string if the value is not defined.
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
@@ -88,9 +127,32 @@ export function getInput(name: string, options?: InputOptions): string {
|
||||
throw new Error(`Input required and not supplied: ${name}`)
|
||||
}
|
||||
|
||||
if (options && options.trimWhitespace === false) {
|
||||
return val
|
||||
}
|
||||
|
||||
return val.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the values of an multiline input. Each value is also trimmed.
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
* @returns string[]
|
||||
*
|
||||
*/
|
||||
export function getMultilineInput(
|
||||
name: string,
|
||||
options?: InputOptions
|
||||
): string[] {
|
||||
const inputs: string[] = getInput(name, options)
|
||||
.split('\n')
|
||||
.filter(x => x !== '')
|
||||
|
||||
return inputs
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the input value of the boolean type in the YAML 1.2 "core schema" specification.
|
||||
* Support boolean input list: `true | True | TRUE | false | False | FALSE` .
|
||||
@@ -171,17 +233,49 @@ export function debug(message: string): void {
|
||||
/**
|
||||
* Adds an error issue
|
||||
* @param message error issue message. Errors will be converted to string via toString()
|
||||
* @param properties optional properties to add to the annotation.
|
||||
*/
|
||||
export function error(message: string | Error): void {
|
||||
issue('error', message instanceof Error ? message.toString() : message)
|
||||
export function error(
|
||||
message: string | Error,
|
||||
properties: AnnotationProperties = {}
|
||||
): void {
|
||||
issueCommand(
|
||||
'error',
|
||||
toCommandProperties(properties),
|
||||
message instanceof Error ? message.toString() : message
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an warning issue
|
||||
* Adds a warning issue
|
||||
* @param message warning issue message. Errors will be converted to string via toString()
|
||||
* @param properties optional properties to add to the annotation.
|
||||
*/
|
||||
export function warning(message: string | Error): void {
|
||||
issue('warning', message instanceof Error ? message.toString() : message)
|
||||
export function warning(
|
||||
message: string | Error,
|
||||
properties: AnnotationProperties = {}
|
||||
): void {
|
||||
issueCommand(
|
||||
'warning',
|
||||
toCommandProperties(properties),
|
||||
message instanceof Error ? message.toString() : message
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a notice issue
|
||||
* @param message notice issue message. Errors will be converted to string via toString()
|
||||
* @param properties optional properties to add to the annotation.
|
||||
*/
|
||||
export function notice(
|
||||
message: string | Error,
|
||||
properties: AnnotationProperties = {}
|
||||
): void {
|
||||
issueCommand(
|
||||
'notice',
|
||||
toCommandProperties(properties),
|
||||
message instanceof Error ? message.toString() : message
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -256,3 +350,7 @@ export function saveState(name: string, value: any): void {
|
||||
export function getState(name: string): string {
|
||||
return process.env[`STATE_${name}`] || ''
|
||||
}
|
||||
|
||||
export async function getIDToken(aud?: string): Promise<string> {
|
||||
return await OidcClient.getIDToken(aud)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/* eslint-disable @typescript-eslint/no-extraneous-class */
|
||||
import * as actions_http_client from '@actions/http-client'
|
||||
import {IRequestOptions} from '@actions/http-client/interfaces'
|
||||
import {HttpClient} from '@actions/http-client'
|
||||
import {BearerCredentialHandler} from '@actions/http-client/auth'
|
||||
import {debug, setSecret} from './core'
|
||||
interface TokenResponse {
|
||||
value?: string
|
||||
}
|
||||
|
||||
export class OidcClient {
|
||||
private static createHttpClient(
|
||||
allowRetry = true,
|
||||
maxRetry = 10
|
||||
): actions_http_client.HttpClient {
|
||||
const requestOptions: IRequestOptions = {
|
||||
allowRetries: allowRetry,
|
||||
maxRetries: maxRetry
|
||||
}
|
||||
|
||||
return new HttpClient(
|
||||
'actions/oidc-client',
|
||||
[new BearerCredentialHandler(OidcClient.getRequestToken())],
|
||||
requestOptions
|
||||
)
|
||||
}
|
||||
|
||||
private static getRequestToken(): string {
|
||||
const token = process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN']
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
'Unable to get ACTIONS_ID_TOKEN_REQUEST_TOKEN env variable'
|
||||
)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
private static getIDTokenUrl(): string {
|
||||
const runtimeUrl = process.env['ACTIONS_ID_TOKEN_REQUEST_URL']
|
||||
if (!runtimeUrl) {
|
||||
throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable')
|
||||
}
|
||||
return runtimeUrl
|
||||
}
|
||||
|
||||
private static async getCall(id_token_url: string): Promise<string> {
|
||||
const httpclient = OidcClient.createHttpClient()
|
||||
|
||||
const res = await httpclient
|
||||
.getJson<TokenResponse>(id_token_url)
|
||||
.catch(error => {
|
||||
throw new Error(
|
||||
`Failed to get ID Token. \n
|
||||
Error Code : ${error.statusCode}\n
|
||||
Error Message: ${error.result.message}`
|
||||
)
|
||||
})
|
||||
|
||||
const id_token = res.result?.value
|
||||
if (!id_token) {
|
||||
throw new Error('Response json body do not have ID Token field')
|
||||
}
|
||||
return id_token
|
||||
}
|
||||
|
||||
static async getIDToken(audience?: string): Promise<string> {
|
||||
try {
|
||||
// New ID Token is requested from action service
|
||||
let id_token_url: string = OidcClient.getIDTokenUrl()
|
||||
if (audience) {
|
||||
const encodedAudience = encodeURIComponent(audience)
|
||||
id_token_url = `${id_token_url}&audience=${encodedAudience}`
|
||||
}
|
||||
|
||||
debug(`ID token url is ${id_token_url}`)
|
||||
|
||||
const id_token = await OidcClient.getCall(id_token_url)
|
||||
setSecret(id_token)
|
||||
return id_token
|
||||
} catch (error) {
|
||||
throw new Error(`Error message: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
// We use any as a valid input type
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import {AnnotationProperties} from './core'
|
||||
import {CommandProperties} from './command'
|
||||
|
||||
/**
|
||||
* Sanitizes an input into a string so it can be passed into issueCommand safely
|
||||
* @param input input to sanitize into a string
|
||||
@@ -13,3 +16,25 @@ export function toCommandValue(input: any): string {
|
||||
}
|
||||
return JSON.stringify(input)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param annotationProperties
|
||||
* @returns The command properties to send with the actual annotation command
|
||||
* See IssueCommandProperties: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs#L646
|
||||
*/
|
||||
export function toCommandProperties(
|
||||
annotationProperties: AnnotationProperties
|
||||
): CommandProperties {
|
||||
if (!Object.keys(annotationProperties).length) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
title: annotationProperties.title,
|
||||
line: annotationProperties.startLine,
|
||||
endLine: annotationProperties.endLine,
|
||||
col: annotationProperties.startColumn,
|
||||
endColumn: annotationProperties.endColumn
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @actions/exec Releases
|
||||
|
||||
### 1.1.0
|
||||
|
||||
- [Fix stdline dropping large output](https://github.com/actions/toolkit/pull/773)
|
||||
- [Add getExecOutput function](https://github.com/actions/toolkit/pull/814)
|
||||
- [Better error for bad cwd](https://github.com/actions/toolkit/pull/793)
|
||||
|
||||
### 1.0.3
|
||||
|
||||
- [Add \"types\" to package.json](https://github.com/actions/toolkit/pull/221)
|
||||
|
||||
@@ -286,6 +286,31 @@ describe('@actions/exec', () => {
|
||||
expect(stderrCalled).toBeTruthy()
|
||||
})
|
||||
|
||||
it('Handles large stdline', async () => {
|
||||
const stdlinePath: string = path.join(
|
||||
__dirname,
|
||||
'scripts',
|
||||
'stdlineoutput.js'
|
||||
)
|
||||
const nodePath: string = await io.which('node', true)
|
||||
|
||||
const _testExecOptions = getExecOptions()
|
||||
let largeLine = ''
|
||||
_testExecOptions.listeners = {
|
||||
stdline: (line: string) => {
|
||||
largeLine = line
|
||||
}
|
||||
}
|
||||
|
||||
const exitCode = await exec.exec(
|
||||
`"${nodePath}"`,
|
||||
[stdlinePath],
|
||||
_testExecOptions
|
||||
)
|
||||
expect(exitCode).toBe(0)
|
||||
expect(Buffer.byteLength(largeLine)).toEqual(2 ** 16 + 1)
|
||||
})
|
||||
|
||||
it('Handles stdin shell', async () => {
|
||||
let command: string
|
||||
if (IS_WINDOWS) {
|
||||
@@ -538,6 +563,22 @@ describe('@actions/exec', () => {
|
||||
expect(output.trim()).toBe(`args[0]: "hello"${os.EOL}args[1]: "world"`)
|
||||
})
|
||||
|
||||
it('Exec roots throws friendly error when bad cwd is specified', async () => {
|
||||
const execOptions = getExecOptions()
|
||||
execOptions.cwd = 'nonexistent/path'
|
||||
|
||||
await expect(exec.exec('ls', ['-all'], execOptions)).rejects.toThrowError(
|
||||
`The cwd: ${execOptions.cwd} does not exist!`
|
||||
)
|
||||
})
|
||||
|
||||
it('Exec roots does not throw when valid cwd is provided', async () => {
|
||||
const execOptions = getExecOptions()
|
||||
execOptions.cwd = './'
|
||||
|
||||
await expect(exec.exec('ls', ['-all'], execOptions)).resolves.toBe(0)
|
||||
})
|
||||
|
||||
it('Exec roots relative tool path using rooted options.cwd', async () => {
|
||||
let command: string
|
||||
if (IS_WINDOWS) {
|
||||
@@ -620,6 +661,165 @@ describe('@actions/exec', () => {
|
||||
expect(output.trim()).toBe(`args[0]: "hello"${os.EOL}args[1]: "world"`)
|
||||
})
|
||||
|
||||
it('correctly outputs for getExecOutput', async () => {
|
||||
const stdErrPath: string = path.join(
|
||||
__dirname,
|
||||
'scripts',
|
||||
'stderroutput.js'
|
||||
)
|
||||
const stdOutPath: string = path.join(
|
||||
__dirname,
|
||||
'scripts',
|
||||
'stdoutoutput.js'
|
||||
)
|
||||
const nodePath: string = await io.which('node', true)
|
||||
|
||||
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
|
||||
`"${nodePath}"`,
|
||||
[stdOutPath],
|
||||
getExecOptions()
|
||||
)
|
||||
|
||||
expect(exitCodeOut).toBe(0)
|
||||
expect(stdout).toBe('this is output to stdout')
|
||||
|
||||
const {exitCode: exitCodeErr, stderr} = await exec.getExecOutput(
|
||||
`"${nodePath}"`,
|
||||
[stdErrPath],
|
||||
getExecOptions()
|
||||
)
|
||||
expect(exitCodeErr).toBe(0)
|
||||
expect(stderr).toBe('this is output to stderr')
|
||||
})
|
||||
|
||||
it('correctly outputs for getExecOutput with additional listeners', async () => {
|
||||
const stdErrPath: string = path.join(
|
||||
__dirname,
|
||||
'scripts',
|
||||
'stderroutput.js'
|
||||
)
|
||||
const stdOutPath: string = path.join(
|
||||
__dirname,
|
||||
'scripts',
|
||||
'stdoutoutput.js'
|
||||
)
|
||||
|
||||
const nodePath: string = await io.which('node', true)
|
||||
let listenerOut = ''
|
||||
|
||||
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
|
||||
`"${nodePath}"`,
|
||||
[stdOutPath],
|
||||
{
|
||||
...getExecOptions(),
|
||||
listeners: {
|
||||
stdout: data => {
|
||||
listenerOut = data.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(exitCodeOut).toBe(0)
|
||||
expect(stdout).toBe('this is output to stdout')
|
||||
expect(listenerOut).toBe('this is output to stdout')
|
||||
|
||||
let listenerErr = ''
|
||||
const {exitCode: exitCodeErr, stderr} = await exec.getExecOutput(
|
||||
`"${nodePath}"`,
|
||||
[stdErrPath],
|
||||
{
|
||||
...getExecOptions(),
|
||||
listeners: {
|
||||
stderr: data => {
|
||||
listenerErr = data.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
expect(exitCodeErr).toBe(0)
|
||||
expect(stderr).toBe('this is output to stderr')
|
||||
expect(listenerErr).toBe('this is output to stderr')
|
||||
})
|
||||
|
||||
it('correctly outputs for getExecOutput when total size exceeds buffer size', async () => {
|
||||
const stdErrPath: string = path.join(
|
||||
__dirname,
|
||||
'scripts',
|
||||
'stderroutput.js'
|
||||
)
|
||||
const stdOutPath: string = path.join(
|
||||
__dirname,
|
||||
'scripts',
|
||||
'stdoutoutputlarge.js'
|
||||
)
|
||||
|
||||
const nodePath: string = await io.which('node', true)
|
||||
let listenerOut = ''
|
||||
|
||||
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
|
||||
`"${nodePath}"`,
|
||||
[stdOutPath],
|
||||
{
|
||||
...getExecOptions(),
|
||||
listeners: {
|
||||
stdout: data => {
|
||||
listenerOut += data.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(exitCodeOut).toBe(0)
|
||||
expect(Buffer.byteLength(stdout || '', 'utf8')).toBe(2 ** 25)
|
||||
expect(Buffer.byteLength(listenerOut, 'utf8')).toBe(2 ** 25)
|
||||
|
||||
let listenerErr = ''
|
||||
const {exitCode: exitCodeErr, stderr} = await exec.getExecOutput(
|
||||
`"${nodePath}"`,
|
||||
[stdErrPath],
|
||||
{
|
||||
...getExecOptions(),
|
||||
listeners: {
|
||||
stderr: data => {
|
||||
listenerErr = data.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
expect(exitCodeErr).toBe(0)
|
||||
expect(stderr).toBe('this is output to stderr')
|
||||
expect(listenerErr).toBe('this is output to stderr')
|
||||
})
|
||||
|
||||
it('correctly outputs for getExecOutput with multi-byte characters', async () => {
|
||||
const stdOutPath: string = path.join(
|
||||
__dirname,
|
||||
'scripts',
|
||||
'stdoutputspecial.js'
|
||||
)
|
||||
|
||||
const nodePath: string = await io.which('node', true)
|
||||
let numStdOutBufferCalls = 0
|
||||
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
|
||||
`"${nodePath}"`,
|
||||
[stdOutPath],
|
||||
{
|
||||
...getExecOptions(),
|
||||
listeners: {
|
||||
stdout: () => {
|
||||
numStdOutBufferCalls += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(exitCodeOut).toBe(0)
|
||||
//one call for each half of the © character, ensuring it was actually split and not sent together
|
||||
expect(numStdOutBufferCalls).toBe(2)
|
||||
expect(stdout).toBe('©')
|
||||
})
|
||||
|
||||
if (IS_WINDOWS) {
|
||||
it('Exec roots relative tool path using process.cwd (Windows path separator)', async () => {
|
||||
let exitCode: number
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
//Default highWaterMark for readable stream buffers us 64K (2^16)
|
||||
//so we go over that to get more than a buffer's worth
|
||||
const os = require('os')
|
||||
|
||||
process.stdout.write('a'.repeat(2**16 + 1) + os.EOL);
|
||||
@@ -0,0 +1,3 @@
|
||||
//Default highWaterMark for readable stream buffers us 64K (2^16)
|
||||
//so we go over that to get more than a buffer's worth
|
||||
process.stdout.write('a\n'.repeat(2**24));
|
||||
@@ -0,0 +1,8 @@
|
||||
//first half of © character
|
||||
process.stdout.write(Buffer.from([0xC2]), (err) => {
|
||||
//write in the callback so that the second byte is sent separately
|
||||
setTimeout(() => {
|
||||
process.stdout.write(Buffer.from([0xA9])) //second half of © character
|
||||
}, 5000)
|
||||
|
||||
})
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/exec",
|
||||
"version": "1.0.4",
|
||||
"version": "1.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/exec",
|
||||
"version": "1.0.4",
|
||||
"version": "1.1.0",
|
||||
"description": "Actions exec lib",
|
||||
"keywords": [
|
||||
"github",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {ExecOptions} from './interfaces'
|
||||
import {StringDecoder} from 'string_decoder'
|
||||
import {ExecOptions, ExecOutput, ExecListeners} from './interfaces'
|
||||
import * as tr from './toolrunner'
|
||||
|
||||
export {ExecOptions}
|
||||
export {ExecOptions, ExecOutput, ExecListeners}
|
||||
|
||||
/**
|
||||
* Exec a command.
|
||||
@@ -28,3 +29,62 @@ export async function exec(
|
||||
const runner: tr.ToolRunner = new tr.ToolRunner(toolPath, args, options)
|
||||
return runner.exec()
|
||||
}
|
||||
|
||||
/**
|
||||
* Exec a command and get the output.
|
||||
* Output will be streamed to the live console.
|
||||
* Returns promise with the exit code and collected stdout and stderr
|
||||
*
|
||||
* @param commandLine command to execute (can include additional args). Must be correctly escaped.
|
||||
* @param args optional arguments for tool. Escaping is handled by the lib.
|
||||
* @param options optional exec options. See ExecOptions
|
||||
* @returns Promise<ExecOutput> exit code, stdout, and stderr
|
||||
*/
|
||||
|
||||
export async function getExecOutput(
|
||||
commandLine: string,
|
||||
args?: string[],
|
||||
options?: ExecOptions
|
||||
): Promise<ExecOutput> {
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
|
||||
//Using string decoder covers the case where a mult-byte character is split
|
||||
const stdoutDecoder = new StringDecoder('utf8')
|
||||
const stderrDecoder = new StringDecoder('utf8')
|
||||
|
||||
const originalStdoutListener = options?.listeners?.stdout
|
||||
const originalStdErrListener = options?.listeners?.stderr
|
||||
|
||||
const stdErrListener = (data: Buffer): void => {
|
||||
stderr += stderrDecoder.write(data)
|
||||
if (originalStdErrListener) {
|
||||
originalStdErrListener(data)
|
||||
}
|
||||
}
|
||||
|
||||
const stdOutListener = (data: Buffer): void => {
|
||||
stdout += stdoutDecoder.write(data)
|
||||
if (originalStdoutListener) {
|
||||
originalStdoutListener(data)
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: ExecListeners = {
|
||||
...options?.listeners,
|
||||
stdout: stdOutListener,
|
||||
stderr: stdErrListener
|
||||
}
|
||||
|
||||
const exitCode = await exec(commandLine, args, {...options, listeners})
|
||||
|
||||
//flush any remaining characters
|
||||
stdout += stdoutDecoder.end()
|
||||
stderr += stderrDecoder.end()
|
||||
|
||||
return {
|
||||
exitCode,
|
||||
stdout,
|
||||
stderr
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,15 +34,39 @@ export interface ExecOptions {
|
||||
input?: Buffer
|
||||
|
||||
/** optional. Listeners for output. Callback functions that will be called on these events */
|
||||
listeners?: {
|
||||
stdout?: (data: Buffer) => void
|
||||
|
||||
stderr?: (data: Buffer) => void
|
||||
|
||||
stdline?: (data: string) => void
|
||||
|
||||
errline?: (data: string) => void
|
||||
|
||||
debug?: (data: string) => void
|
||||
}
|
||||
listeners?: ExecListeners
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for the output of getExecOutput()
|
||||
*/
|
||||
export interface ExecOutput {
|
||||
/**The exit code of the process */
|
||||
exitCode: number
|
||||
|
||||
/**The entire stdout of the process as a string */
|
||||
stdout: string
|
||||
|
||||
/**The entire stderr of the process as a string */
|
||||
stderr: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The user defined listeners for an exec call
|
||||
*/
|
||||
export interface ExecListeners {
|
||||
/** A call back for each buffer of stdout */
|
||||
stdout?: (data: Buffer) => void
|
||||
|
||||
/** A call back for each buffer of stderr */
|
||||
stderr?: (data: Buffer) => void
|
||||
|
||||
/** A call back for each line of stdout */
|
||||
stdline?: (data: string) => void
|
||||
|
||||
/** A call back for each line of stderr */
|
||||
errline?: (data: string) => void
|
||||
|
||||
/** A call back for each debug log */
|
||||
debug?: (data: string) => void
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export class ToolRunner extends events.EventEmitter {
|
||||
data: Buffer,
|
||||
strBuffer: string,
|
||||
onLine: (line: string) => void
|
||||
): void {
|
||||
): string {
|
||||
try {
|
||||
let s = strBuffer + data.toString()
|
||||
let n = s.indexOf(os.EOL)
|
||||
@@ -98,10 +98,12 @@ export class ToolRunner extends events.EventEmitter {
|
||||
n = s.indexOf(os.EOL)
|
||||
}
|
||||
|
||||
strBuffer = s
|
||||
return s
|
||||
} catch (err) {
|
||||
// streaming lines to console is best effort. Don't fail a build.
|
||||
this._debug(`error processing line. Failed with error ${err}`)
|
||||
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,7 +416,7 @@ export class ToolRunner extends events.EventEmitter {
|
||||
// otherwise verify it exists (add extension on Windows if necessary)
|
||||
this.toolPath = await io.which(this.toolPath, true)
|
||||
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
return new Promise<number>(async (resolve, reject) => {
|
||||
this._debug(`exec tool: ${this.toolPath}`)
|
||||
this._debug('arguments:')
|
||||
for (const arg of this.args) {
|
||||
@@ -433,6 +435,10 @@ export class ToolRunner extends events.EventEmitter {
|
||||
this._debug(message)
|
||||
})
|
||||
|
||||
if (this.options.cwd && !(await ioUtil.exists(this.options.cwd))) {
|
||||
return reject(new Error(`The cwd: ${this.options.cwd} does not exist!`))
|
||||
}
|
||||
|
||||
const fileName = this._getSpawnFileName()
|
||||
const cp = child.spawn(
|
||||
fileName,
|
||||
@@ -440,7 +446,7 @@ export class ToolRunner extends events.EventEmitter {
|
||||
this._getSpawnOptions(this.options, fileName)
|
||||
)
|
||||
|
||||
const stdbuffer = ''
|
||||
let stdbuffer = ''
|
||||
if (cp.stdout) {
|
||||
cp.stdout.on('data', (data: Buffer) => {
|
||||
if (this.options.listeners && this.options.listeners.stdout) {
|
||||
@@ -451,15 +457,19 @@ export class ToolRunner extends events.EventEmitter {
|
||||
optionsNonNull.outStream.write(data)
|
||||
}
|
||||
|
||||
this._processLineBuffer(data, stdbuffer, (line: string) => {
|
||||
if (this.options.listeners && this.options.listeners.stdline) {
|
||||
this.options.listeners.stdline(line)
|
||||
stdbuffer = this._processLineBuffer(
|
||||
data,
|
||||
stdbuffer,
|
||||
(line: string) => {
|
||||
if (this.options.listeners && this.options.listeners.stdline) {
|
||||
this.options.listeners.stdline(line)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const errbuffer = ''
|
||||
let errbuffer = ''
|
||||
if (cp.stderr) {
|
||||
cp.stderr.on('data', (data: Buffer) => {
|
||||
state.processStderr = true
|
||||
@@ -478,11 +488,15 @@ export class ToolRunner extends events.EventEmitter {
|
||||
s.write(data)
|
||||
}
|
||||
|
||||
this._processLineBuffer(data, errbuffer, (line: string) => {
|
||||
if (this.options.listeners && this.options.listeners.errline) {
|
||||
this.options.listeners.errline(line)
|
||||
errbuffer = this._processLineBuffer(
|
||||
data,
|
||||
errbuffer,
|
||||
(line: string) => {
|
||||
if (this.options.listeners && this.options.listeners.errline) {
|
||||
this.options.listeners.errline(line)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -615,13 +629,13 @@ class ExecState extends events.EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
processClosed: boolean = false // tracks whether the process has exited and stdio is closed
|
||||
processError: string = ''
|
||||
processExitCode: number = 0
|
||||
processExited: boolean = false // tracks whether the process has exited
|
||||
processStderr: boolean = false // tracks whether stderr was written to
|
||||
processClosed = false // tracks whether the process has exited and stdio is closed
|
||||
processError = ''
|
||||
processExitCode = 0
|
||||
processExited = false // tracks whether the process has exited
|
||||
processStderr = false // tracks whether stderr was written to
|
||||
private delay = 10000 // 10 seconds
|
||||
private done: boolean = false
|
||||
private done = false
|
||||
private options: im.ExecOptions
|
||||
private timeout: NodeJS.Timer | null = null
|
||||
private toolPath: string
|
||||
|
||||
@@ -59,18 +59,19 @@ const newIssue = await octokit.rest.issues.create({
|
||||
|
||||
## Webhook payload typescript definitions
|
||||
|
||||
The npm module `@octokit/webhooks` provides type definitions for the response payloads. You can cast the payload to these types for better type information.
|
||||
The npm module `@octokit/webhooks-definitions` provides type definitions for the response payloads. You can cast the payload to these types for better type information.
|
||||
|
||||
First, install the npm module `npm install @octokit/webhooks`
|
||||
First, install the npm module `npm install @octokit/webhooks-definitions`
|
||||
|
||||
Then, assert the type based on the eventName
|
||||
```ts
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import * as Webhooks from '@octokit/webhooks'
|
||||
import {PushEvent} from '@octokit/webhooks-definitions/schema'
|
||||
|
||||
if (github.context.eventName === 'push') {
|
||||
const pushPayload = github.context.payload as Webhooks.WebhookPayloadPush
|
||||
core.info(`The head commit is: ${pushPayload.head}`)
|
||||
const pushPayload = github.context.payload as PushEvent
|
||||
core.info(`The head commit is: ${pushPayload.head_commit}`)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
### 5.0.0
|
||||
- [Update @actions/github to include latest octokit definitions](https://github.com/actions/toolkit/pull/783)
|
||||
- [Add urls to context](https://github.com/actions/toolkit/pull/794)
|
||||
|
||||
### 4.0.0
|
||||
- [Add execution state information to context](https://github.com/actions/toolkit/pull/499)
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as path from 'path'
|
||||
import {Context} from '../src/context'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
describe('@actions/context', () => {
|
||||
let context: Context
|
||||
|
||||
Generated
+1431
-1203
File diff suppressed because it is too large
Load Diff
@@ -44,7 +44,7 @@
|
||||
"@octokit/plugin-rest-endpoint-methods": "^5.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^25.1.0",
|
||||
"proxy": "^1.0.1"
|
||||
"jest": "^26.6.3",
|
||||
"proxy": "^1.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ export class Context {
|
||||
job: string
|
||||
runNumber: number
|
||||
runId: number
|
||||
apiUrl: string
|
||||
serverUrl: string
|
||||
graphqlUrl: string
|
||||
|
||||
/**
|
||||
* Hydrate the context from the environment
|
||||
@@ -43,6 +46,10 @@ export class Context {
|
||||
this.job = process.env.GITHUB_JOB as string
|
||||
this.runNumber = parseInt(process.env.GITHUB_RUN_NUMBER as string, 10)
|
||||
this.runId = parseInt(process.env.GITHUB_RUN_ID as string, 10)
|
||||
this.apiUrl = process.env.GITHUB_API_URL ?? `https://api.github.com`
|
||||
this.serverUrl = process.env.GITHUB_SERVER_URL ?? `https://github.com`
|
||||
this.graphqlUrl =
|
||||
process.env.GITHUB_GRAPHQL_URL ?? `https://api.github.com/graphql`
|
||||
}
|
||||
|
||||
get issue(): {owner: string; repo: string; number: number} {
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
# @actions/glob Releases
|
||||
|
||||
### 0.2.0
|
||||
- [Added the hashFiles function to Glob](https://github.com/actions/toolkit/pull/830)
|
||||
- [Added an option to filter out directories](https://github.com/actions/toolkit/pull/728)
|
||||
|
||||
### 0.1.2
|
||||
|
||||
- [Fix bug where files were matched incorrectly](https://github.com/actions/toolkit/pull/805)
|
||||
|
||||
### 0.1.1
|
||||
|
||||
- Update @actions/core version
|
||||
### 0.1.0
|
||||
|
||||
- Initial release
|
||||
|
||||
### 0.1.1
|
||||
|
||||
- Update @actions/core version
|
||||
@@ -0,0 +1,126 @@
|
||||
import * as io from '../../io/src/io'
|
||||
import * as path from 'path'
|
||||
import {hashFiles} from '../src/glob'
|
||||
import {promises as fs} from 'fs'
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32'
|
||||
|
||||
/**
|
||||
* These test focus on the ability of globber to find files
|
||||
* and not on the pattern matching aspect
|
||||
*/
|
||||
describe('globber', () => {
|
||||
beforeAll(async () => {
|
||||
await io.rmRF(getTestTemp())
|
||||
})
|
||||
|
||||
it('basic hashfiles test', async () => {
|
||||
const root = path.join(getTestTemp(), 'basic-hashfiles')
|
||||
await fs.mkdir(path.join(root), {recursive: true})
|
||||
await fs.writeFile(path.join(root, 'test.txt'), 'test file content')
|
||||
const hash = await hashFiles(`${root}/*`)
|
||||
expect(hash).toEqual(
|
||||
'd8a411e8f8643821bed189e627ff57151918aa554c00c10b31c693ab2dded273'
|
||||
)
|
||||
})
|
||||
|
||||
it('basic hashfiles no match should return empty string', async () => {
|
||||
const root = path.join(getTestTemp(), 'empty-hashfiles')
|
||||
const hash = await hashFiles(`${root}/*`)
|
||||
expect(hash).toEqual('')
|
||||
})
|
||||
|
||||
it('followSymbolicLinks defaults to true', async () => {
|
||||
const root = path.join(
|
||||
getTestTemp(),
|
||||
'defaults-to-follow-symbolic-links-true'
|
||||
)
|
||||
await fs.mkdir(path.join(root, 'realdir'), {recursive: true})
|
||||
await fs.writeFile(
|
||||
path.join(root, 'realdir', 'file.txt'),
|
||||
'test file content'
|
||||
)
|
||||
await createSymlinkDir(
|
||||
path.join(root, 'realdir'),
|
||||
path.join(root, 'symDir')
|
||||
)
|
||||
const testPath = path.join(root, `symDir`)
|
||||
const hash = await hashFiles(testPath)
|
||||
expect(hash).toEqual(
|
||||
'd8a411e8f8643821bed189e627ff57151918aa554c00c10b31c693ab2dded273'
|
||||
)
|
||||
})
|
||||
|
||||
it('followSymbolicLinks set to true', async () => {
|
||||
const root = path.join(getTestTemp(), 'set-to-true')
|
||||
await fs.mkdir(path.join(root, 'realdir'), {recursive: true})
|
||||
await fs.writeFile(path.join(root, 'realdir', 'file'), 'test file content')
|
||||
await createSymlinkDir(
|
||||
path.join(root, 'realdir'),
|
||||
path.join(root, 'symDir')
|
||||
)
|
||||
const testPath = path.join(root, `symDir`)
|
||||
const hash = await hashFiles(testPath, {followSymbolicLinks: true})
|
||||
expect(hash).toEqual(
|
||||
'd8a411e8f8643821bed189e627ff57151918aa554c00c10b31c693ab2dded273'
|
||||
)
|
||||
})
|
||||
|
||||
it('followSymbolicLinks set to false', async () => {
|
||||
// Create the following layout:
|
||||
// <root>
|
||||
// <root>/folder-a
|
||||
// <root>/folder-a/file
|
||||
// <root>/symDir -> <root>/folder-a
|
||||
const root = path.join(getTestTemp(), 'set-to-false')
|
||||
await fs.mkdir(path.join(root, 'realdir'), {recursive: true})
|
||||
await fs.writeFile(path.join(root, 'realdir', 'file'), 'test file content')
|
||||
await createSymlinkDir(
|
||||
path.join(root, 'realdir'),
|
||||
path.join(root, 'symDir')
|
||||
)
|
||||
const testPath = path.join(root, 'symdir')
|
||||
const hash = await hashFiles(testPath, {followSymbolicLinks: false})
|
||||
expect(hash).toEqual('')
|
||||
})
|
||||
|
||||
it('multipath test basic', async () => {
|
||||
// Create the following layout:
|
||||
// <root>
|
||||
// <root>/folder-a
|
||||
// <root>/folder-a/file
|
||||
// <root>/symDir -> <root>/folder-a
|
||||
const root = path.join(getTestTemp(), 'set-to-false')
|
||||
await fs.mkdir(path.join(root, 'dir1'), {recursive: true})
|
||||
await fs.mkdir(path.join(root, 'dir2'), {recursive: true})
|
||||
await fs.writeFile(
|
||||
path.join(root, 'dir1', 'testfile1.txt'),
|
||||
'test file content'
|
||||
)
|
||||
await fs.writeFile(
|
||||
path.join(root, 'dir2', 'testfile2.txt'),
|
||||
'test file content'
|
||||
)
|
||||
const testPath = `${path.join(root, 'dir1')}\n${path.join(root, 'dir2')}`
|
||||
const hash = await hashFiles(testPath)
|
||||
expect(hash).toEqual(
|
||||
'4e911ea5824830b6a3ec096c7833d5af8381c189ffaa825c3503a5333a73eadc'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
function getTestTemp(): string {
|
||||
return path.join(__dirname, '_temp', 'hash_files')
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a symlink directory on OSX/Linux, and a junction point directory on Windows.
|
||||
* A symlink directory is not created on Windows since it requires an elevated context.
|
||||
*/
|
||||
async function createSymlinkDir(real: string, link: string): Promise<void> {
|
||||
if (IS_WINDOWS) {
|
||||
await fs.symlink(real, link, 'junction')
|
||||
} else {
|
||||
await fs.symlink(real, link)
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,41 @@ describe('globber', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('defaults to matchDirectories=true', async () => {
|
||||
// Create the following layout:
|
||||
// <root>
|
||||
// <root>/folder-a
|
||||
// <root>/folder-a/file
|
||||
const root = path.join(getTestTemp(), 'defaults-to-match-directories-true')
|
||||
await fs.mkdir(path.join(root, 'folder-a'), {recursive: true})
|
||||
await fs.writeFile(path.join(root, 'folder-a', 'file'), 'test file content')
|
||||
|
||||
const itemPaths = await glob(root, {})
|
||||
expect(itemPaths).toEqual([
|
||||
root,
|
||||
path.join(root, 'folder-a'),
|
||||
path.join(root, 'folder-a', 'file')
|
||||
])
|
||||
})
|
||||
|
||||
it('does not match file with trailing slash when implicitDescendants=true', async () => {
|
||||
// Create the following layout:
|
||||
// <root>
|
||||
// <root>/file
|
||||
const root = path.join(
|
||||
getTestTemp(),
|
||||
'defaults-to-implicit-descendants-true'
|
||||
)
|
||||
|
||||
const filePath = path.join(root, 'file')
|
||||
|
||||
await fs.mkdir(root, {recursive: true})
|
||||
await fs.writeFile(filePath, 'test file content')
|
||||
|
||||
const itemPaths = await glob(`${filePath}/`, {})
|
||||
expect(itemPaths).toEqual([])
|
||||
})
|
||||
|
||||
it('defaults to omitBrokenSymbolicLinks=true', async () => {
|
||||
// Create the following layout:
|
||||
// <root>
|
||||
@@ -343,6 +378,34 @@ describe('globber', () => {
|
||||
expect(itemPaths).toEqual([])
|
||||
})
|
||||
|
||||
it('does not return directories when match directories false', async () => {
|
||||
// Create the following layout:
|
||||
// <root>/file-1
|
||||
// <root>/dir-1
|
||||
// <root>/dir-1/file-2
|
||||
// <root>/dir-1/dir-2
|
||||
// <root>/dir-1/dir-2/file-3
|
||||
const root = path.join(
|
||||
getTestTemp(),
|
||||
'does-not-return-directories-when-match-directories-false'
|
||||
)
|
||||
await fs.mkdir(path.join(root, 'dir-1', 'dir-2'), {recursive: true})
|
||||
await fs.writeFile(path.join(root, 'file-1'), '')
|
||||
await fs.writeFile(path.join(root, 'dir-1', 'file-2'), '')
|
||||
await fs.writeFile(path.join(root, 'dir-1', 'dir-2', 'file-3'), '')
|
||||
|
||||
const pattern = `${root}${path.sep}**`
|
||||
expect(
|
||||
await glob(pattern, {
|
||||
matchDirectories: false
|
||||
})
|
||||
).toEqual([
|
||||
path.join(root, 'dir-1', 'dir-2', 'file-3'),
|
||||
path.join(root, 'dir-1', 'file-2'),
|
||||
path.join(root, 'file-1')
|
||||
])
|
||||
})
|
||||
|
||||
it('does not search paths that are not partial matches', async () => {
|
||||
// Create the following layout:
|
||||
// <root>
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('pattern', () => {
|
||||
it('escapes homedir', async () => {
|
||||
const home = path.join(getTestTemp(), 'home-with-[and]')
|
||||
await fs.mkdir(home, {recursive: true})
|
||||
const pattern = new Pattern('~/m*', undefined, home)
|
||||
const pattern = new Pattern('~/m*', false, undefined, home)
|
||||
|
||||
expect(pattern.searchPath).toBe(home)
|
||||
expect(pattern.match(path.join(home, 'match'))).toBeTruthy()
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/glob",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/glob",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"preview": true,
|
||||
"description": "Actions glob lib",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {Globber, DefaultGlobber} from './internal-globber'
|
||||
import {GlobOptions} from './internal-glob-options'
|
||||
import {HashFileOptions} from './internal-hash-file-options'
|
||||
import {hashFiles as _hashFiles} from './internal-hash-files'
|
||||
|
||||
export {Globber, GlobOptions}
|
||||
|
||||
@@ -15,3 +17,21 @@ export async function create(
|
||||
): Promise<Globber> {
|
||||
return await DefaultGlobber.create(patterns, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the sha256 hash of a glob
|
||||
*
|
||||
* @param patterns Patterns separated by newlines
|
||||
* @param options Glob options
|
||||
*/
|
||||
export async function hashFiles(
|
||||
patterns: string,
|
||||
options?: HashFileOptions
|
||||
): Promise<string> {
|
||||
let followSymbolicLinks = true
|
||||
if (options && typeof options.followSymbolicLinks === 'boolean') {
|
||||
followSymbolicLinks = options.followSymbolicLinks
|
||||
}
|
||||
const globber = await create(patterns, {followSymbolicLinks})
|
||||
return _hashFiles(globber)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export function getOptions(copy?: GlobOptions): GlobOptions {
|
||||
const result: GlobOptions = {
|
||||
followSymbolicLinks: true,
|
||||
implicitDescendants: true,
|
||||
matchDirectories: true,
|
||||
omitBrokenSymbolicLinks: true
|
||||
}
|
||||
|
||||
@@ -22,6 +23,11 @@ export function getOptions(copy?: GlobOptions): GlobOptions {
|
||||
core.debug(`implicitDescendants '${result.implicitDescendants}'`)
|
||||
}
|
||||
|
||||
if (typeof copy.matchDirectories === 'boolean') {
|
||||
result.matchDirectories = copy.matchDirectories
|
||||
core.debug(`matchDirectories '${result.matchDirectories}'`)
|
||||
}
|
||||
|
||||
if (typeof copy.omitBrokenSymbolicLinks === 'boolean') {
|
||||
result.omitBrokenSymbolicLinks = copy.omitBrokenSymbolicLinks
|
||||
core.debug(`omitBrokenSymbolicLinks '${result.omitBrokenSymbolicLinks}'`)
|
||||
|
||||
@@ -21,6 +21,14 @@ export interface GlobOptions {
|
||||
*/
|
||||
implicitDescendants?: boolean
|
||||
|
||||
/**
|
||||
* Indicates whether matching directories should be included in the
|
||||
* result set.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
matchDirectories?: boolean
|
||||
|
||||
/**
|
||||
* Indicates whether broken symbolic should be ignored and omitted from the
|
||||
* result set. Otherwise an error will be thrown.
|
||||
|
||||
@@ -66,7 +66,6 @@ export class DefaultGlobber implements Globber {
|
||||
async *globGenerator(): AsyncGenerator<string, void> {
|
||||
// Fill in defaults options
|
||||
const options = globOptionsHelper.getOptions(this.options)
|
||||
|
||||
// Implicit descendants?
|
||||
const patterns: Pattern[] = []
|
||||
for (const pattern of this.patterns) {
|
||||
@@ -77,12 +76,13 @@ export class DefaultGlobber implements Globber {
|
||||
pattern.segments[pattern.segments.length - 1] !== '**')
|
||||
) {
|
||||
patterns.push(
|
||||
new Pattern(pattern.negate, pattern.segments.concat('**'))
|
||||
new Pattern(pattern.negate, true, pattern.segments.concat('**'))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Push the search paths
|
||||
|
||||
const stack: SearchState[] = []
|
||||
for (const searchPath of patternHelper.getSearchPaths(patterns)) {
|
||||
core.debug(`Search path '${searchPath}'`)
|
||||
@@ -131,7 +131,7 @@ export class DefaultGlobber implements Globber {
|
||||
// Directory
|
||||
if (stats.isDirectory()) {
|
||||
// Matched
|
||||
if (match & MatchKind.Directory) {
|
||||
if (match & MatchKind.Directory && options.matchDirectories) {
|
||||
yield item.path
|
||||
}
|
||||
// Descend?
|
||||
@@ -180,6 +180,7 @@ export class DefaultGlobber implements Globber {
|
||||
}
|
||||
|
||||
result.searchPaths.push(...patternHelper.getSearchPaths(result.patterns))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Options to control globbing behavior
|
||||
*/
|
||||
export interface HashFileOptions {
|
||||
/**
|
||||
* Indicates whether to follow symbolic links. Generally should set to false
|
||||
* when deleting files.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
followSymbolicLinks?: boolean
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as crypto from 'crypto'
|
||||
import * as core from '@actions/core'
|
||||
import * as fs from 'fs'
|
||||
import * as stream from 'stream'
|
||||
import * as util from 'util'
|
||||
import * as path from 'path'
|
||||
import {Globber} from './glob'
|
||||
|
||||
export async function hashFiles(globber: Globber): Promise<string> {
|
||||
let hasMatch = false
|
||||
const githubWorkspace = process.env['GITHUB_WORKSPACE'] ?? process.cwd()
|
||||
const result = crypto.createHash('sha256')
|
||||
let count = 0
|
||||
for await (const file of globber.globGenerator()) {
|
||||
core.debug(file)
|
||||
if (!file.startsWith(`${githubWorkspace}${path.sep}`)) {
|
||||
core.debug(`Ignore '${file}' since it is not under GITHUB_WORKSPACE.`)
|
||||
continue
|
||||
}
|
||||
if (fs.statSync(file).isDirectory()) {
|
||||
core.debug(`Skip directory '${file}'.`)
|
||||
continue
|
||||
}
|
||||
const hash = crypto.createHash('sha256')
|
||||
const pipeline = util.promisify(stream.pipeline)
|
||||
await pipeline(fs.createReadStream(file), hash)
|
||||
result.write(hash.digest())
|
||||
count++
|
||||
if (!hasMatch) {
|
||||
hasMatch = true
|
||||
}
|
||||
}
|
||||
result.end()
|
||||
|
||||
if (hasMatch) {
|
||||
core.debug(`Found ${count} files to hash.`)
|
||||
return result.digest('hex')
|
||||
} else {
|
||||
core.debug(`No matches found for glob`)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
@@ -43,15 +43,27 @@ export class Pattern {
|
||||
*/
|
||||
private readonly rootRegExp: RegExp
|
||||
|
||||
/* eslint-disable no-dupe-class-members */
|
||||
// Disable no-dupe-class-members due to false positive for method overload
|
||||
// https://github.com/typescript-eslint/typescript-eslint/issues/291
|
||||
/**
|
||||
* Indicates that the pattern is implicitly added as opposed to user specified.
|
||||
*/
|
||||
private readonly isImplicitPattern: boolean
|
||||
|
||||
constructor(pattern: string)
|
||||
constructor(pattern: string, segments: undefined, homedir: string)
|
||||
constructor(negate: boolean, segments: string[])
|
||||
constructor(
|
||||
pattern: string,
|
||||
isImplicitPattern: boolean,
|
||||
segments: undefined,
|
||||
homedir: string
|
||||
)
|
||||
constructor(
|
||||
negate: boolean,
|
||||
isImplicitPattern: boolean,
|
||||
segments: string[],
|
||||
homedir?: string
|
||||
)
|
||||
constructor(
|
||||
patternOrNegate: string | boolean,
|
||||
isImplicitPattern = false,
|
||||
segments?: string[],
|
||||
homedir?: string
|
||||
) {
|
||||
@@ -107,6 +119,8 @@ export class Pattern {
|
||||
IS_WINDOWS ? 'i' : ''
|
||||
)
|
||||
|
||||
this.isImplicitPattern = isImplicitPattern
|
||||
|
||||
// Create minimatch
|
||||
const minimatchOptions: IMinimatchOptions = {
|
||||
dot: true,
|
||||
@@ -132,7 +146,7 @@ export class Pattern {
|
||||
// Append a trailing slash. Otherwise Minimatch will not match the directory immediately
|
||||
// preceding the globstar. For example, given the pattern `/foo/**`, Minimatch returns
|
||||
// false for `/foo` but returns true for `/foo/`. Append a trailing slash to handle that quirk.
|
||||
if (!itemPath.endsWith(path.sep)) {
|
||||
if (!itemPath.endsWith(path.sep) && this.isImplicitPattern === false) {
|
||||
// Note, this is safe because the constructor ensures the pattern has an absolute root.
|
||||
// For example, formats like C: and C:foo on Windows are resolved to an absolute root.
|
||||
itemPath = `${itemPath}${path.sep}`
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# @actions/io Releases
|
||||
|
||||
### 1.1.1
|
||||
- [Fixed a bug where we incorrectly escaped paths for rmrf](https://github.com/actions/toolkit/pull/828)
|
||||
|
||||
### 1.1.0
|
||||
|
||||
- Add `findInPath` method to locate all matching executables in the system path
|
||||
|
||||
@@ -556,6 +556,45 @@ describe('rmRF', () => {
|
||||
await assertNotExists(symlinkFile)
|
||||
await assertNotExists(outerDirectory)
|
||||
})
|
||||
} else {
|
||||
it('correctly escapes % on windows', async () => {
|
||||
const root: string = path.join(getTestTemp(), 'rmRF_escape_test_win')
|
||||
const directory: string = path.join(root, '%test%')
|
||||
await io.mkdirP(root)
|
||||
await io.mkdirP(directory)
|
||||
const oldEnv = process.env['test']
|
||||
process.env['test'] = 'thisshouldnotresolve'
|
||||
|
||||
await io.rmRF(directory)
|
||||
await assertNotExists(directory)
|
||||
process.env['test'] = oldEnv
|
||||
})
|
||||
|
||||
it('Should throw for invalid characters', async () => {
|
||||
const root: string = path.join(getTestTemp(), 'rmRF_invalidChar_Windows')
|
||||
const errorString =
|
||||
'File path must not contain `*`, `"`, `<`, `>` or `|` on Windows'
|
||||
await expect(io.rmRF(path.join(root, '"'))).rejects.toHaveProperty(
|
||||
'message',
|
||||
errorString
|
||||
)
|
||||
await expect(io.rmRF(path.join(root, '<'))).rejects.toHaveProperty(
|
||||
'message',
|
||||
errorString
|
||||
)
|
||||
await expect(io.rmRF(path.join(root, '>'))).rejects.toHaveProperty(
|
||||
'message',
|
||||
errorString
|
||||
)
|
||||
await expect(io.rmRF(path.join(root, '|'))).rejects.toHaveProperty(
|
||||
'message',
|
||||
errorString
|
||||
)
|
||||
await expect(io.rmRF(path.join(root, '*'))).rejects.toHaveProperty(
|
||||
'message',
|
||||
errorString
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
it('removes symlink folder with missing source using rmRF', async () => {
|
||||
|
||||
Generated
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@actions/io",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"lockfileVersion": 1
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/io",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"description": "Actions io lib",
|
||||
"keywords": [
|
||||
"github",
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function exists(fsPath: string): Promise<boolean> {
|
||||
|
||||
export async function isDirectory(
|
||||
fsPath: string,
|
||||
useStat: boolean = false
|
||||
useStat = false
|
||||
): Promise<boolean> {
|
||||
const stats = useStat ? await stat(fsPath) : await lstat(fsPath)
|
||||
return stats.isDirectory()
|
||||
@@ -166,3 +166,8 @@ function isUnixExecutable(stats: fs.Stats): boolean {
|
||||
((stats.mode & 64) > 0 && stats.uid === process.getuid())
|
||||
)
|
||||
}
|
||||
|
||||
// Get the path of cmd.exe in windows
|
||||
export function getCmdPath(): string {
|
||||
return process.env['COMSPEC'] ?? `cmd.exe`
|
||||
}
|
||||
|
||||
+17
-3
@@ -5,6 +5,7 @@ import {promisify} from 'util'
|
||||
import * as ioUtil from './io-util'
|
||||
|
||||
const exec = promisify(childProcess.exec)
|
||||
const execFile = promisify(childProcess.execFile)
|
||||
|
||||
/**
|
||||
* Interface for cp/mv options
|
||||
@@ -117,11 +118,24 @@ export async function rmRF(inputPath: string): Promise<void> {
|
||||
if (ioUtil.IS_WINDOWS) {
|
||||
// Node doesn't provide a delete operation, only an unlink function. This means that if the file is being used by another
|
||||
// program (e.g. antivirus), it won't be deleted. To address this, we shell out the work to rd/del.
|
||||
|
||||
// Check for invalid characters
|
||||
// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
|
||||
if (/[*"<>|]/.test(inputPath)) {
|
||||
throw new Error(
|
||||
'File path must not contain `*`, `"`, `<`, `>` or `|` on Windows'
|
||||
)
|
||||
}
|
||||
try {
|
||||
const cmdPath = ioUtil.getCmdPath()
|
||||
if (await ioUtil.isDirectory(inputPath, true)) {
|
||||
await exec(`rd /s /q "${inputPath}"`)
|
||||
await exec(`${cmdPath} /s /c "rd /s /q "%inputPath%""`, {
|
||||
env: {inputPath}
|
||||
})
|
||||
} else {
|
||||
await exec(`del /f /a "${inputPath}"`)
|
||||
await exec(`${cmdPath} /s /c "del /f /a "%inputPath%""`, {
|
||||
env: {inputPath}
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
// if you try to delete a file that doesn't exist, desired result is achieved
|
||||
@@ -149,7 +163,7 @@ export async function rmRF(inputPath: string): Promise<void> {
|
||||
}
|
||||
|
||||
if (isDir) {
|
||||
await exec(`rm -rf "${inputPath}"`)
|
||||
await execFile(`rm`, [`-rf`, `${inputPath}`])
|
||||
} else {
|
||||
await ioUtil.unlink(inputPath)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
# @actions/tool-cache Releases
|
||||
|
||||
### 1.7.1
|
||||
- [Fallback to os-releases file to get linux version](https://github.com/actions/toolkit/pull/594)
|
||||
- [Update to latest @actions/io verison](https://github.com/actions/toolkit/pull/838)
|
||||
|
||||
### 1.7.0
|
||||
- [Allow arbirtary headers when downloading tools to the tc](https://github.com/actions/toolkit/pull/530)
|
||||
- [Export `isExplicitVersion` and `evaluateVersions` functions](https://github.com/actions/toolkit/pull/796)
|
||||
- [Force overwrite on default when extracted compressed files](https://github.com/actions/toolkit/pull/807)
|
||||
|
||||
### 1.6.1
|
||||
- [Update @actions/core version](https://github.com/actions/toolkit/pull/636)
|
||||
- [Update @actions/core version](https://github.com/actions/toolkit/pull/636)
|
||||
|
||||
### 1.6.0
|
||||
- [Add extractXar function to extract XAR files](https://github.com/actions/toolkit/pull/207)
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('@actions/tool-cache-manifest', () => {
|
||||
expect(file?.filename).toBe('sometool-1.2.3-linux-x64.tar.gz')
|
||||
})
|
||||
|
||||
it('can match with linux platform version spec', async () => {
|
||||
it('can match with linux platform version spec from lsb-release', async () => {
|
||||
os.platform = 'linux'
|
||||
os.arch = 'x64'
|
||||
|
||||
@@ -150,6 +150,48 @@ describe('@actions/tool-cache-manifest', () => {
|
||||
expect(file?.filename).toBe('sometool-1.2.4-ubuntu1804-x64.tar.gz')
|
||||
})
|
||||
|
||||
it('can match with linux platform version spec from os-release', async () => {
|
||||
os.platform = 'linux'
|
||||
os.arch = 'x64'
|
||||
|
||||
readLsbSpy.mockImplementation(() => {
|
||||
return `NAME="Ubuntu"
|
||||
VERSION="18.04.5 LTS (Bionic Beaver)"
|
||||
ID=ubuntu
|
||||
ID_LIKE=debian
|
||||
PRETTY_NAME="Ubuntu 18.04.5 LTS"
|
||||
VERSION_ID="18.04"
|
||||
HOME_URL="https://www.ubuntu.com/"
|
||||
SUPPORT_URL="https://help.ubuntu.com/"
|
||||
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
|
||||
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
|
||||
VERSION_CODENAME=bionic
|
||||
UBUNTU_CODENAME=bionic`
|
||||
})
|
||||
|
||||
const manifest: mm.IToolRelease[] | null = await tc.getManifestFromRepo(
|
||||
owner,
|
||||
repo,
|
||||
fakeToken
|
||||
)
|
||||
const release: tc.IToolRelease | undefined = await tc.findFromManifest(
|
||||
'1.2.4',
|
||||
true,
|
||||
manifest
|
||||
)
|
||||
expect(release).toBeDefined()
|
||||
expect(release?.version).toBe('1.2.4')
|
||||
expect(release?.files.length).toBe(1)
|
||||
const file = release?.files[0]
|
||||
expect(file).toBeDefined()
|
||||
expect(file?.arch).toBe('x64')
|
||||
expect(file?.platform).toBe('linux')
|
||||
expect(file?.download_url).toBe(
|
||||
'https://github.com/actions/sometool/releases/tag/1.2.4-20200402.6/sometool-1.2.4-ubuntu1804-x64.tar.gz'
|
||||
)
|
||||
expect(file?.filename).toBe('sometool-1.2.4-ubuntu1804-x64.tar.gz')
|
||||
})
|
||||
|
||||
it('can match with darwin platform version spec', async () => {
|
||||
os.platform = 'darwin'
|
||||
os.arch = 'x64'
|
||||
|
||||
@@ -122,11 +122,9 @@ describe('@actions/tool-cache', function() {
|
||||
|
||||
setResponseMessageFactory(() => {
|
||||
const readStream = new stream.Readable()
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
readStream._read = () => {
|
||||
readStream.destroy(new Error('uh oh'))
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/unbound-method */
|
||||
return readStream
|
||||
})
|
||||
|
||||
@@ -149,7 +147,6 @@ describe('@actions/tool-cache', function() {
|
||||
.get('/retries-error-from-response-message-stream')
|
||||
.reply(200, {})
|
||||
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
let attempt = 1
|
||||
setResponseMessageFactory(() => {
|
||||
const readStream = new stream.Readable()
|
||||
@@ -170,7 +167,6 @@ describe('@actions/tool-cache', function() {
|
||||
|
||||
return readStream
|
||||
})
|
||||
/* eslint-enable @typescript-eslint/unbound-method */
|
||||
|
||||
const downPath = await tc.downloadTool(
|
||||
'http://example.com/retries-error-from-response-message-stream'
|
||||
@@ -243,6 +239,10 @@ describe('@actions/tool-cache', function() {
|
||||
const _7zFile: string = path.join(tempDir, 'test.7z')
|
||||
await io.cp(path.join(__dirname, 'data', 'test.7z'), _7zFile)
|
||||
|
||||
const destDir = path.join(tempDir, 'destination')
|
||||
await io.mkdirP(destDir)
|
||||
fs.writeFileSync(path.join(destDir, 'file.txt'), 'overwriteMe')
|
||||
|
||||
// extract/cache
|
||||
const extPath: string = await tc.extract7z(_7zFile)
|
||||
await tc.cacheDir(extPath, 'my-7z-contents', '1.1.0')
|
||||
@@ -251,6 +251,9 @@ describe('@actions/tool-cache', function() {
|
||||
expect(fs.existsSync(toolPath)).toBeTruthy()
|
||||
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
|
||||
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
|
||||
expect(fs.readFileSync(path.join(toolPath, 'file.txt'), 'utf8')).toBe(
|
||||
'file.txt contents'
|
||||
)
|
||||
expect(
|
||||
fs.existsSync(path.join(toolPath, 'file-with-ç-character.txt'))
|
||||
).toBeTruthy()
|
||||
@@ -347,6 +350,22 @@ describe('@actions/tool-cache', function() {
|
||||
await io.rmRF(tempDir)
|
||||
}
|
||||
})
|
||||
it.each(['pwsh', 'powershell'])(
|
||||
'unzip properly fails with bad path (%s)',
|
||||
async powershellTool => {
|
||||
const originalPath = process.env['PATH']
|
||||
try {
|
||||
if (powershellTool === 'powershell' && IS_WINDOWS) {
|
||||
//remove pwsh from PATH temporarily to test fallback case
|
||||
process.env['PATH'] = removePWSHFromPath(process.env['PATH'])
|
||||
}
|
||||
|
||||
await expect(tc.extractZip('badPath')).rejects.toThrow()
|
||||
} finally {
|
||||
process.env['PATH'] = originalPath
|
||||
}
|
||||
}
|
||||
)
|
||||
} else if (IS_MAC) {
|
||||
it('extract .xar', async () => {
|
||||
const tempDir = path.join(tempPath, 'test-install.xar')
|
||||
@@ -360,14 +379,21 @@ describe('@actions/tool-cache', function() {
|
||||
cwd: sourcePath
|
||||
})
|
||||
|
||||
const destDir = path.join(tempDir, 'destination')
|
||||
await io.mkdirP(destDir)
|
||||
fs.writeFileSync(path.join(destDir, 'file.txt'), 'overwriteMe')
|
||||
|
||||
// extract/cache
|
||||
const extPath: string = await tc.extractXar(targetPath, undefined, '-x')
|
||||
const extPath: string = await tc.extractXar(targetPath, destDir, ['-x'])
|
||||
await tc.cacheDir(extPath, 'my-xar-contents', '1.1.0')
|
||||
const toolPath: string = tc.find('my-xar-contents', '1.1.0')
|
||||
|
||||
expect(fs.existsSync(toolPath)).toBeTruthy()
|
||||
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
|
||||
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
|
||||
expect(fs.readFileSync(path.join(toolPath, 'file.txt'), 'utf8')).toBe(
|
||||
'file.txt contents'
|
||||
)
|
||||
expect(
|
||||
fs.existsSync(path.join(toolPath, 'file-with-ç-character.txt'))
|
||||
).toBeTruthy()
|
||||
@@ -462,14 +488,23 @@ describe('@actions/tool-cache', function() {
|
||||
const _tgzFile: string = path.join(tempDir, 'test.tar.gz')
|
||||
await io.cp(path.join(__dirname, 'data', 'test.tar.gz'), _tgzFile)
|
||||
|
||||
//Create file to overwrite
|
||||
const destDir = path.join(tempDir, 'extract-dest')
|
||||
await io.rmRF(destDir)
|
||||
await io.mkdirP(destDir)
|
||||
fs.writeFileSync(path.join(destDir, 'file.txt'), 'overwriteMe')
|
||||
|
||||
// extract/cache
|
||||
const extPath: string = await tc.extractTar(_tgzFile)
|
||||
const extPath: string = await tc.extractTar(_tgzFile, destDir)
|
||||
await tc.cacheDir(extPath, 'my-tgz-contents', '1.1.0')
|
||||
const toolPath: string = tc.find('my-tgz-contents', '1.1.0')
|
||||
|
||||
expect(fs.existsSync(toolPath)).toBeTruthy()
|
||||
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
|
||||
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
|
||||
expect(fs.readFileSync(path.join(toolPath, 'file.txt'), 'utf8')).toBe(
|
||||
'file.txt contents'
|
||||
)
|
||||
expect(
|
||||
fs.existsSync(path.join(toolPath, 'file-with-ç-character.txt'))
|
||||
).toBeTruthy()
|
||||
@@ -500,6 +535,9 @@ describe('@actions/tool-cache', function() {
|
||||
expect(fs.existsSync(toolPath)).toBeTruthy()
|
||||
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
|
||||
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
|
||||
expect(fs.readFileSync(path.join(toolPath, 'file.txt'), 'utf8')).toBe(
|
||||
'file.txt contents'
|
||||
)
|
||||
expect(
|
||||
fs.existsSync(path.join(toolPath, 'file-with-ç-character.txt'))
|
||||
).toBeTruthy()
|
||||
@@ -520,8 +558,14 @@ describe('@actions/tool-cache', function() {
|
||||
const _txzFile: string = path.join(tempDir, 'test.tar.xz')
|
||||
await io.cp(path.join(__dirname, 'data', 'test.tar.xz'), _txzFile)
|
||||
|
||||
//Create file to overwrite
|
||||
const destDir = path.join(tempDir, 'extract-dest')
|
||||
await io.rmRF(destDir)
|
||||
await io.mkdirP(destDir)
|
||||
fs.writeFileSync(path.join(destDir, 'file.txt'), 'overwriteMe')
|
||||
|
||||
// extract/cache
|
||||
const extPath: string = await tc.extractTar(_txzFile, undefined, 'x')
|
||||
const extPath: string = await tc.extractTar(_txzFile, destDir, 'x')
|
||||
await tc.cacheDir(extPath, 'my-txz-contents', '1.1.0')
|
||||
const toolPath: string = tc.find('my-txz-contents', '1.1.0')
|
||||
|
||||
@@ -534,58 +578,70 @@ describe('@actions/tool-cache', function() {
|
||||
).toBe('foo/hello: world')
|
||||
})
|
||||
|
||||
it('installs a zip and finds it', async () => {
|
||||
const tempDir = path.join(__dirname, 'test-install-zip')
|
||||
try {
|
||||
await io.mkdirP(tempDir)
|
||||
it.each(['pwsh', 'powershell'])(
|
||||
'installs a zip and finds it (%s)',
|
||||
async powershellTool => {
|
||||
const tempDir = path.join(__dirname, 'test-install-zip')
|
||||
const originalPath = process.env['PATH']
|
||||
try {
|
||||
await io.mkdirP(tempDir)
|
||||
|
||||
// stage the layout for a zip file:
|
||||
// file.txt
|
||||
// folder/nested-file.txt
|
||||
const stagingDir = path.join(tempDir, 'zip-staging')
|
||||
await io.mkdirP(path.join(stagingDir, 'folder'))
|
||||
fs.writeFileSync(path.join(stagingDir, 'file.txt'), '')
|
||||
fs.writeFileSync(path.join(stagingDir, 'folder', 'nested-file.txt'), '')
|
||||
// stage the layout for a zip file:
|
||||
// file.txt
|
||||
// folder/nested-file.txt
|
||||
const stagingDir = path.join(tempDir, 'zip-staging')
|
||||
await io.mkdirP(path.join(stagingDir, 'folder'))
|
||||
fs.writeFileSync(path.join(stagingDir, 'file.txt'), '')
|
||||
fs.writeFileSync(path.join(stagingDir, 'folder', 'nested-file.txt'), '')
|
||||
|
||||
// create the zip
|
||||
const zipFile = path.join(tempDir, 'test.zip')
|
||||
await io.rmRF(zipFile)
|
||||
if (IS_WINDOWS) {
|
||||
const escapedStagingPath = stagingDir.replace(/'/g, "''") // double-up single quotes
|
||||
const escapedZipFile = zipFile.replace(/'/g, "''")
|
||||
const powershellPath =
|
||||
(await io.which('pwsh', false)) ||
|
||||
(await io.which('powershell', true))
|
||||
const args = [
|
||||
'-NoLogo',
|
||||
'-Sta',
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-ExecutionPolicy',
|
||||
'Unrestricted',
|
||||
'-Command',
|
||||
`$ErrorActionPreference = 'Stop' ; Add-Type -AssemblyName System.IO.Compression.FileSystem ; [System.IO.Compression.ZipFile]::CreateFromDirectory('${escapedStagingPath}', '${escapedZipFile}')`
|
||||
]
|
||||
await exec.exec(`"${powershellPath}"`, args)
|
||||
} else {
|
||||
const zipPath: string = await io.which('zip', true)
|
||||
await exec.exec(`"${zipPath}`, [zipFile, '-r', '.'], {cwd: stagingDir})
|
||||
// create the zip
|
||||
const zipFile = path.join(tempDir, 'test.zip')
|
||||
await io.rmRF(zipFile)
|
||||
if (IS_WINDOWS) {
|
||||
const escapedStagingPath = stagingDir.replace(/'/g, "''") // double-up single quotes
|
||||
const escapedZipFile = zipFile.replace(/'/g, "''")
|
||||
const powershellPath =
|
||||
(await io.which('pwsh', false)) ||
|
||||
(await io.which('powershell', true))
|
||||
const args = [
|
||||
'-NoLogo',
|
||||
'-Sta',
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-ExecutionPolicy',
|
||||
'Unrestricted',
|
||||
'-Command',
|
||||
`$ErrorActionPreference = 'Stop' ; Add-Type -AssemblyName System.IO.Compression.FileSystem ; [System.IO.Compression.ZipFile]::CreateFromDirectory('${escapedStagingPath}', '${escapedZipFile}')`
|
||||
]
|
||||
await exec.exec(`"${powershellPath}"`, args)
|
||||
} else {
|
||||
const zipPath: string = await io.which('zip', true)
|
||||
await exec.exec(`"${zipPath}`, [zipFile, '-r', '.'], {
|
||||
cwd: stagingDir
|
||||
})
|
||||
}
|
||||
|
||||
if (powershellTool === 'powershell' && IS_WINDOWS) {
|
||||
//remove pwsh from PATH temporarily to test fallback case
|
||||
process.env['PATH'] = removePWSHFromPath(process.env['PATH'])
|
||||
}
|
||||
|
||||
const extPath: string = await tc.extractZip(zipFile)
|
||||
await tc.cacheDir(extPath, 'foo', '1.1.0')
|
||||
const toolPath: string = tc.find('foo', '1.1.0')
|
||||
|
||||
expect(fs.existsSync(toolPath)).toBeTruthy()
|
||||
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
|
||||
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
|
||||
expect(
|
||||
fs.existsSync(path.join(toolPath, 'folder', 'nested-file.txt'))
|
||||
).toBeTruthy()
|
||||
} finally {
|
||||
await io.rmRF(tempDir)
|
||||
process.env['PATH'] = originalPath
|
||||
}
|
||||
|
||||
const extPath: string = await tc.extractZip(zipFile)
|
||||
await tc.cacheDir(extPath, 'foo', '1.1.0')
|
||||
const toolPath: string = tc.find('foo', '1.1.0')
|
||||
|
||||
expect(fs.existsSync(toolPath)).toBeTruthy()
|
||||
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
|
||||
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
|
||||
expect(
|
||||
fs.existsSync(path.join(toolPath, 'folder', 'nested-file.txt'))
|
||||
).toBeTruthy()
|
||||
} finally {
|
||||
await io.rmRF(tempDir)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
it('installs a zip and extracts it to specified directory', async function() {
|
||||
const tempDir = path.join(__dirname, 'test-install-zip')
|
||||
@@ -597,7 +653,7 @@ describe('@actions/tool-cache', function() {
|
||||
// folder/nested-file.txt
|
||||
const stagingDir = path.join(tempDir, 'zip-staging')
|
||||
await io.mkdirP(path.join(stagingDir, 'folder'))
|
||||
fs.writeFileSync(path.join(stagingDir, 'file.txt'), '')
|
||||
fs.writeFileSync(path.join(stagingDir, 'file.txt'), 'originalText')
|
||||
fs.writeFileSync(path.join(stagingDir, 'folder', 'nested-file.txt'), '')
|
||||
|
||||
// create the zip
|
||||
@@ -629,12 +685,16 @@ describe('@actions/tool-cache', function() {
|
||||
await io.rmRF(destDir)
|
||||
await io.mkdirP(destDir)
|
||||
try {
|
||||
fs.writeFileSync(path.join(destDir, 'file.txt'), 'overwriteMe')
|
||||
const extPath: string = await tc.extractZip(zipFile, destDir)
|
||||
await tc.cacheDir(extPath, 'foo', '1.1.0')
|
||||
const toolPath: string = tc.find('foo', '1.1.0')
|
||||
expect(fs.existsSync(toolPath)).toBeTruthy()
|
||||
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
|
||||
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
|
||||
expect(fs.readFileSync(path.join(toolPath, 'file.txt'), 'utf8')).toBe(
|
||||
'originalText'
|
||||
)
|
||||
expect(
|
||||
fs.existsSync(path.join(toolPath, 'folder', 'nested-file.txt'))
|
||||
).toBeTruthy()
|
||||
@@ -843,3 +903,12 @@ function setGlobal<T>(key: string, value: T | undefined): void {
|
||||
g[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
function removePWSHFromPath(pathEnv: string | undefined): string {
|
||||
return (pathEnv || '')
|
||||
.split(';')
|
||||
.filter(segment => {
|
||||
return !segment.startsWith(`C:\\Program Files\\PowerShell`)
|
||||
})
|
||||
.join(';')
|
||||
}
|
||||
|
||||
Generated
+14
-14
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/tool-cache",
|
||||
"version": "1.6.1",
|
||||
"version": "1.7.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -26,9 +26,9 @@
|
||||
}
|
||||
},
|
||||
"@actions/io": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz",
|
||||
"integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg=="
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.1.tgz",
|
||||
"integrity": "sha512-Qi4JoKXjmE0O67wAOH6y0n26QXhMKMFo7GD/4IXNVcrtLjUlGjGuVys6pQgwF3ArfGTQu0XpqaNr0YhED2RaRA=="
|
||||
},
|
||||
"@types/nock": {
|
||||
"version": "10.0.3",
|
||||
@@ -123,24 +123,24 @@
|
||||
"dev": true
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.19",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
|
||||
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||
"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": "0.0.8"
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/tool-cache",
|
||||
"version": "1.6.1",
|
||||
"version": "1.7.1",
|
||||
"description": "Actions tool-cache lib",
|
||||
"keywords": [
|
||||
"github",
|
||||
@@ -39,7 +39,7 @@
|
||||
"@actions/core": "^1.2.6",
|
||||
"@actions/exec": "^1.0.0",
|
||||
"@actions/http-client": "^1.0.8",
|
||||
"@actions/io": "^1.0.1",
|
||||
"@actions/io": "^1.1.1",
|
||||
"semver": "^6.1.0",
|
||||
"uuid": "^3.3.2"
|
||||
},
|
||||
|
||||
@@ -135,8 +135,15 @@ export function _getOsVersion(): string {
|
||||
const lines = lsbContents.split('\n')
|
||||
for (const line of lines) {
|
||||
const parts = line.split('=')
|
||||
if (parts.length === 2 && parts[0].trim() === 'DISTRIB_RELEASE') {
|
||||
version = parts[1].trim()
|
||||
if (
|
||||
parts.length === 2 &&
|
||||
(parts[0].trim() === 'VERSION_ID' ||
|
||||
parts[0].trim() === 'DISTRIB_RELEASE')
|
||||
) {
|
||||
version = parts[1]
|
||||
.trim()
|
||||
.replace(/^"/, '')
|
||||
.replace(/"$/, '')
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -147,11 +154,14 @@ export function _getOsVersion(): string {
|
||||
}
|
||||
|
||||
export function _readLinuxVersionFile(): string {
|
||||
const lsbFile = '/etc/lsb-release'
|
||||
const lsbReleaseFile = '/etc/lsb-release'
|
||||
const osReleaseFile = '/etc/os-release'
|
||||
let contents = ''
|
||||
|
||||
if (fs.existsSync(lsbFile)) {
|
||||
contents = fs.readFileSync(lsbFile).toString()
|
||||
if (fs.existsSync(lsbReleaseFile)) {
|
||||
contents = fs.readFileSync(lsbReleaseFile).toString()
|
||||
} else if (fs.existsSync(osReleaseFile)) {
|
||||
contents = fs.readFileSync(osReleaseFile).toString()
|
||||
}
|
||||
|
||||
return contents
|
||||
|
||||
@@ -272,6 +272,7 @@ export async function extractTar(
|
||||
if (isGnuTar) {
|
||||
// Suppress warnings when using GNU tar to extract archives created by BSD tar
|
||||
args.push('--warning=no-unknown-keyword')
|
||||
args.push('--overwrite')
|
||||
}
|
||||
|
||||
args.push('-C', destArg, '-f', fileArg)
|
||||
@@ -344,21 +345,55 @@ async function extractZipWin(file: string, dest: string): Promise<void> {
|
||||
// build the powershell command
|
||||
const escapedFile = file.replace(/'/g, "''").replace(/"|\n|\r/g, '') // double-up single quotes, remove double quotes and newlines
|
||||
const escapedDest = dest.replace(/'/g, "''").replace(/"|\n|\r/g, '')
|
||||
const command = `$ErrorActionPreference = 'Stop' ; try { Add-Type -AssemblyName System.IO.Compression.FileSystem } catch { } ; [System.IO.Compression.ZipFile]::ExtractToDirectory('${escapedFile}', '${escapedDest}')`
|
||||
const pwshPath = await io.which('pwsh', false)
|
||||
|
||||
// run powershell
|
||||
const powershellPath = await io.which('powershell', true)
|
||||
const args = [
|
||||
'-NoLogo',
|
||||
'-Sta',
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-ExecutionPolicy',
|
||||
'Unrestricted',
|
||||
'-Command',
|
||||
command
|
||||
]
|
||||
await exec(`"${powershellPath}"`, args)
|
||||
//To match the file overwrite behavior on nix systems, we use the overwrite = true flag for ExtractToDirectory
|
||||
//and the -Force flag for Expand-Archive as a fallback
|
||||
if (pwshPath) {
|
||||
//attempt to use pwsh with ExtractToDirectory, if this fails attempt Expand-Archive
|
||||
const pwshCommand = [
|
||||
`$ErrorActionPreference = 'Stop' ;`,
|
||||
`try { Add-Type -AssemblyName System.IO.Compression.ZipFile } catch { } ;`,
|
||||
`try { [System.IO.Compression.ZipFile]::ExtractToDirectory('${escapedFile}', '${escapedDest}', $true) }`,
|
||||
`catch { if (($_.Exception.GetType().FullName -eq 'System.Management.Automation.MethodException') -or ($_.Exception.GetType().FullName -eq 'System.Management.Automation.RuntimeException') ){ Expand-Archive -LiteralPath '${escapedFile}' -DestinationPath '${escapedDest}' -Force } else { throw $_ } } ;`
|
||||
].join(' ')
|
||||
|
||||
const args = [
|
||||
'-NoLogo',
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-ExecutionPolicy',
|
||||
'Unrestricted',
|
||||
'-Command',
|
||||
pwshCommand
|
||||
]
|
||||
|
||||
core.debug(`Using pwsh at path: ${pwshPath}`)
|
||||
await exec(`"${pwshPath}"`, args)
|
||||
} else {
|
||||
const powershellCommand = [
|
||||
`$ErrorActionPreference = 'Stop' ;`,
|
||||
`try { Add-Type -AssemblyName System.IO.Compression.FileSystem } catch { } ;`,
|
||||
`if ((Get-Command -Name Expand-Archive -Module Microsoft.PowerShell.Archive -ErrorAction Ignore)) { Expand-Archive -LiteralPath '${escapedFile}' -DestinationPath '${escapedDest}' -Force }`,
|
||||
`else {[System.IO.Compression.ZipFile]::ExtractToDirectory('${escapedFile}', '${escapedDest}', $true) }`
|
||||
].join(' ')
|
||||
|
||||
const args = [
|
||||
'-NoLogo',
|
||||
'-Sta',
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-ExecutionPolicy',
|
||||
'Unrestricted',
|
||||
'-Command',
|
||||
powershellCommand
|
||||
]
|
||||
|
||||
const powershellPath = await io.which('powershell', true)
|
||||
core.debug(`Using powershell at path: ${powershellPath}`)
|
||||
|
||||
await exec(`"${powershellPath}"`, args)
|
||||
}
|
||||
}
|
||||
|
||||
async function extractZipNix(file: string, dest: string): Promise<void> {
|
||||
@@ -367,6 +402,7 @@ async function extractZipNix(file: string, dest: string): Promise<void> {
|
||||
if (!core.isDebug()) {
|
||||
args.unshift('-q')
|
||||
}
|
||||
args.unshift('-o') //overwrite with -o, otherwise a prompt is shown which freezes the run
|
||||
await exec(`"${unzipPath}"`, args, {cwd: dest})
|
||||
}
|
||||
|
||||
@@ -472,9 +508,9 @@ export function find(
|
||||
arch = arch || os.arch()
|
||||
|
||||
// attempt to resolve an explicit version
|
||||
if (!_isExplicitVersion(versionSpec)) {
|
||||
if (!isExplicitVersion(versionSpec)) {
|
||||
const localVersions: string[] = findAllVersions(toolName, arch)
|
||||
const match = _evaluateVersions(localVersions, versionSpec)
|
||||
const match = evaluateVersions(localVersions, versionSpec)
|
||||
versionSpec = match
|
||||
}
|
||||
|
||||
@@ -514,7 +550,7 @@ export function findAllVersions(toolName: string, arch?: string): string[] {
|
||||
if (fs.existsSync(toolPath)) {
|
||||
const children: string[] = fs.readdirSync(toolPath)
|
||||
for (const child of children) {
|
||||
if (_isExplicitVersion(child)) {
|
||||
if (isExplicitVersion(child)) {
|
||||
const fullPath = path.join(toolPath, child, arch || '')
|
||||
if (fs.existsSync(fullPath) && fs.existsSync(`${fullPath}.complete`)) {
|
||||
versions.push(child)
|
||||
@@ -652,7 +688,12 @@ function _completeToolPath(tool: string, version: string, arch?: string): void {
|
||||
core.debug('finished caching tool')
|
||||
}
|
||||
|
||||
function _isExplicitVersion(versionSpec: string): boolean {
|
||||
/**
|
||||
* Check if version string is explicit
|
||||
*
|
||||
* @param versionSpec version string to check
|
||||
*/
|
||||
export function isExplicitVersion(versionSpec: string): boolean {
|
||||
const c = semver.clean(versionSpec) || ''
|
||||
core.debug(`isExplicit: ${c}`)
|
||||
|
||||
@@ -662,7 +703,17 @@ function _isExplicitVersion(versionSpec: string): boolean {
|
||||
return valid
|
||||
}
|
||||
|
||||
function _evaluateVersions(versions: string[], versionSpec: string): string {
|
||||
/**
|
||||
* Get the highest satisfiying semantic version in `versions` which satisfies `versionSpec`
|
||||
*
|
||||
* @param versions array of versions to evaluate
|
||||
* @param versionSpec semantic version spec to satisfy
|
||||
*/
|
||||
|
||||
export function evaluateVersions(
|
||||
versions: string[],
|
||||
versionSpec: string
|
||||
): string {
|
||||
let version = ''
|
||||
core.debug(`evaluating ${versions.length} versions`)
|
||||
versions = versions.sort((a, b) => {
|
||||
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/*
|
||||
This script takes the output of npm audit --json from stdin
|
||||
and writes a filtered version to stdout.
|
||||
The filtered version will have the entries listed in `AUDIT_ALLOW_LIST` removed.
|
||||
Specifically, each property of `vulnerabilities` in the input is matched by name in the allow list.
|
||||
|
||||
Sample output of `npm audit --json` (NPM v6):
|
||||
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"action": "review",
|
||||
"module": "trim-newlines",
|
||||
"resolves": [
|
||||
{
|
||||
"id": 1753,
|
||||
"path": "lerna>@lerna/publish>@lerna/version>@lerna/conventional-commits>conventional-changelog-core>get-pkg-repo>meow>trim-newlines",
|
||||
"dev": true,
|
||||
"optional": false,
|
||||
"bundled": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
// Other properties ...
|
||||
}
|
||||
|
||||
|
||||
The reason we have this script is that there may be low-severity or unexploitable vulnerabilities
|
||||
that have not yet been fixed in newer versions of the package.
|
||||
|
||||
Note: if we update to NPM v7, we will have to change this script because the `npm audit` output will be different.
|
||||
See commit 935647112d96fa5cf82e61314f7135376d24f291 in https://github.com/actions/toolkit/pull/846.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
const fs = require('fs')
|
||||
|
||||
const USAGE = "Usage: npm audit --json | scripts/audit-allow-list"
|
||||
|
||||
// To add entires to the allow list:
|
||||
// - Run `npm audit --json`
|
||||
// - Copy `path` from each `actions[k].resolves` you want to allow
|
||||
// - Fill in the `advisoryUrl` and `justification` (these are just for documentation)
|
||||
const AUDIT_ALLOW_LIST = [
|
||||
{
|
||||
path: "lerna>@lerna/publish>@lerna/version>@lerna/conventional-commits>conventional-changelog-core>get-pkg-repo>meow>trim-newlines",
|
||||
advisoryUrl: "https://www.npmjs.com/advisories/1753",
|
||||
justification: "dependency of lerna (dev only); low severity"
|
||||
},
|
||||
{
|
||||
path: "lerna>@lerna/version>@lerna/conventional-commits>conventional-changelog-core>get-pkg-repo>meow>trim-newlines",
|
||||
advisoryUrl: "https://www.npmjs.com/advisories/1753",
|
||||
justification: "dependency of lerna (dev only); low severity"
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* @param audits - JavaScript object matching the schema of `npm audit --json`
|
||||
* @param allowedPaths - List of dependency paths to exclude from the audit
|
||||
*/
|
||||
function filterVulnerabilities(audits, allowedPaths) {
|
||||
const vulnerabilities = audits.actions.flatMap(x => x.resolves)
|
||||
return vulnerabilities.filter(x => !allowedPaths.includes(x.path))
|
||||
}
|
||||
|
||||
const input = fs.readFileSync("/dev/stdin", "utf-8")
|
||||
if (input === "") {
|
||||
console.error(USAGE)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const audits = JSON.parse(input)
|
||||
const allowedPaths = AUDIT_ALLOW_LIST.map(x => x.path)
|
||||
// This function assumes `audits` has the right structure.
|
||||
// Just let the error terminate the process if the input doesn't match the schema.
|
||||
const remainingVulnerabilities = filterVulnerabilities(audits, allowedPaths)
|
||||
|
||||
// `npm audit` will return exit code 1 if it finds vulnerabilities.
|
||||
// This script should do the same.
|
||||
const numVulnerabilities = remainingVulnerabilities.length
|
||||
if (numVulnerabilities > 0) {
|
||||
const pluralized = numVulnerabilities === 1 ? "y" : "ies"
|
||||
console.log(`Found ${numVulnerabilities} unrecognized vulnerabilit${pluralized} from \`npm audit\`:`)
|
||||
console.log(JSON.stringify(remainingVulnerabilities, null, 2))
|
||||
process.exit(1)
|
||||
}
|
||||
Reference in New Issue
Block a user