From b957538a191c27e6cf011aa414fe647567b1d601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Test=C3=A9?= Date: Wed, 11 Dec 2024 17:51:41 +0100 Subject: [PATCH 1/3] chore(ci): update actions version --- .github/workflows/check-dist.yml | 4 ++-- .github/workflows/linter.yml | 6 ++++-- .github/workflows/test_javascript.yml | 2 +- .github/workflows/test_spawn_terminate.yml | 4 ++-- .github/workflows/test_start_stop.yml | 4 ++-- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/check-dist.yml b/.github/workflows/check-dist.yml index dcb16c4..10efc6a 100644 --- a/.github/workflows/check-dist.yml +++ b/.github/workflows/check-dist.yml @@ -30,7 +30,7 @@ jobs: steps: - name: Checkout id: checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Setup Node.js id: setup-node @@ -62,7 +62,7 @@ jobs: - if: ${{ failure() && steps.diff.outcome == 'failure' }} name: Upload Artifact id: upload - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 with: name: dist path: dist/ diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index f56ca7f..363bb15 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -19,7 +19,9 @@ jobs: steps: - name: Checkout id: checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + fetch-depth: 0 - name: Setup Node.js id: setup-node @@ -34,7 +36,7 @@ jobs: - name: Lint Codebase id: super-linter - uses: super-linter/super-linter/slim@v5 + uses: super-linter/super-linter/slim@e1cb86b6e8d119f789513668b4b30bf17fe1efe4 env: DEFAULT_BRANCH: main FILTER_REGEX_EXCLUDE: dist/**/* diff --git a/.github/workflows/test_javascript.yml b/.github/workflows/test_javascript.yml index a041458..6c3bef0 100644 --- a/.github/workflows/test_javascript.yml +++ b/.github/workflows/test_javascript.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout id: checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Setup Node.js id: setup-node diff --git a/.github/workflows/test_spawn_terminate.yml b/.github/workflows/test_spawn_terminate.yml index d8ebcb1..0a4c7bc 100644 --- a/.github/workflows/test_spawn_terminate.yml +++ b/.github/workflows/test_spawn_terminate.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Checkout id: checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Test start instance id: test-start @@ -70,7 +70,7 @@ jobs: steps: - name: Checkout id: checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Test stop instance id: test-stop diff --git a/.github/workflows/test_start_stop.yml b/.github/workflows/test_start_stop.yml index c054624..91acf6a 100644 --- a/.github/workflows/test_start_stop.yml +++ b/.github/workflows/test_start_stop.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout id: checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Test start instance id: test-start @@ -56,7 +56,7 @@ jobs: steps: - name: Checkout id: checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Test stop instance id: test-stop From d749c5bb7c151a06492c85c0dd091159340de14e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Test=C3=A9?= Date: Thu, 12 Dec 2024 09:22:03 +0100 Subject: [PATCH 2/3] chore(ci): update javascript dependencies --- dist/index.js | 300 +++++++++++++++++++++++++++++++++++++++------- package-lock.json | 41 ++++--- 2 files changed, 283 insertions(+), 58 deletions(-) diff --git a/dist/index.js b/dist/index.js index 7331446..a8f12e7 100644 --- a/dist/index.js +++ b/dist/index.js @@ -31272,6 +31272,132 @@ function onConnectTimeout (socket) { module.exports = buildConnector +/***/ }), + +/***/ 4462: +/***/ ((module) => { + +"use strict"; + + +/** @type {Record} */ +const headerNameLowerCasedRecord = {} + +// https://developer.mozilla.org/docs/Web/HTTP/Headers +const wellknownHeaderNames = [ + 'Accept', + 'Accept-Encoding', + 'Accept-Language', + 'Accept-Ranges', + 'Access-Control-Allow-Credentials', + 'Access-Control-Allow-Headers', + 'Access-Control-Allow-Methods', + 'Access-Control-Allow-Origin', + 'Access-Control-Expose-Headers', + 'Access-Control-Max-Age', + 'Access-Control-Request-Headers', + 'Access-Control-Request-Method', + 'Age', + 'Allow', + 'Alt-Svc', + 'Alt-Used', + 'Authorization', + 'Cache-Control', + 'Clear-Site-Data', + 'Connection', + 'Content-Disposition', + 'Content-Encoding', + 'Content-Language', + 'Content-Length', + 'Content-Location', + 'Content-Range', + 'Content-Security-Policy', + 'Content-Security-Policy-Report-Only', + 'Content-Type', + 'Cookie', + 'Cross-Origin-Embedder-Policy', + 'Cross-Origin-Opener-Policy', + 'Cross-Origin-Resource-Policy', + 'Date', + 'Device-Memory', + 'Downlink', + 'ECT', + 'ETag', + 'Expect', + 'Expect-CT', + 'Expires', + 'Forwarded', + 'From', + 'Host', + 'If-Match', + 'If-Modified-Since', + 'If-None-Match', + 'If-Range', + 'If-Unmodified-Since', + 'Keep-Alive', + 'Last-Modified', + 'Link', + 'Location', + 'Max-Forwards', + 'Origin', + 'Permissions-Policy', + 'Pragma', + 'Proxy-Authenticate', + 'Proxy-Authorization', + 'RTT', + 'Range', + 'Referer', + 'Referrer-Policy', + 'Refresh', + 'Retry-After', + 'Sec-WebSocket-Accept', + 'Sec-WebSocket-Extensions', + 'Sec-WebSocket-Key', + 'Sec-WebSocket-Protocol', + 'Sec-WebSocket-Version', + 'Server', + 'Server-Timing', + 'Service-Worker-Allowed', + 'Service-Worker-Navigation-Preload', + 'Set-Cookie', + 'SourceMap', + 'Strict-Transport-Security', + 'Supports-Loading-Mode', + 'TE', + 'Timing-Allow-Origin', + 'Trailer', + 'Transfer-Encoding', + 'Upgrade', + 'Upgrade-Insecure-Requests', + 'User-Agent', + 'Vary', + 'Via', + 'WWW-Authenticate', + 'X-Content-Type-Options', + 'X-DNS-Prefetch-Control', + 'X-Frame-Options', + 'X-Permitted-Cross-Domain-Policies', + 'X-Powered-By', + 'X-Requested-With', + 'X-XSS-Protection' +] + +for (let i = 0; i < wellknownHeaderNames.length; ++i) { + const key = wellknownHeaderNames[i] + const lowerCasedKey = key.toLowerCase() + headerNameLowerCasedRecord[key] = headerNameLowerCasedRecord[lowerCasedKey] = + lowerCasedKey +} + +// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`. +Object.setPrototypeOf(headerNameLowerCasedRecord, null) + +module.exports = { + wellknownHeaderNames, + headerNameLowerCasedRecord +} + + /***/ }), /***/ 8045: @@ -32104,6 +32230,7 @@ const { InvalidArgumentError } = __nccwpck_require__(8045) const { Blob } = __nccwpck_require__(4300) const nodeUtil = __nccwpck_require__(3837) const { stringify } = __nccwpck_require__(3477) +const { headerNameLowerCasedRecord } = __nccwpck_require__(4462) const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v)) @@ -32313,6 +32440,15 @@ function parseKeepAliveTimeout (val) { return m ? parseInt(m[1], 10) * 1000 : null } +/** + * Retrieves a header name and returns its lowercase value. + * @param {string | Buffer} value Header name + * @returns {string} + */ +function headerNameToString (value) { + return headerNameLowerCasedRecord[value] || value.toLowerCase() +} + function parseHeaders (headers, obj = {}) { // For H2 support if (!Array.isArray(headers)) return headers @@ -32584,6 +32720,7 @@ module.exports = { isIterable, isAsyncIterable, isDestroyed, + headerNameToString, parseRawHeaders, parseHeaders, parseKeepAliveTimeout, @@ -36720,6 +36857,9 @@ function httpRedirectFetch (fetchParams, response) { // https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name request.headersList.delete('authorization') + // https://fetch.spec.whatwg.org/#authentication-entries + request.headersList.delete('proxy-authorization', true) + // "Cookie" and "Host" are forbidden request-headers, which undici doesn't implement. request.headersList.delete('cookie') request.headersList.delete('host') @@ -39228,14 +39368,18 @@ const { isBlobLike, toUSVString, ReadableStreamFrom } = __nccwpck_require__(3983 const assert = __nccwpck_require__(9491) const { isUint8Array } = __nccwpck_require__(9830) +let supportedHashes = [] + // https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable /** @type {import('crypto')|undefined} */ let crypto try { crypto = __nccwpck_require__(6113) + const possibleRelevantHashes = ['sha256', 'sha384', 'sha512'] + supportedHashes = crypto.getHashes().filter((hash) => possibleRelevantHashes.includes(hash)) +/* c8 ignore next 3 */ } catch { - } function responseURL (response) { @@ -39763,66 +39907,56 @@ function bytesMatch (bytes, metadataList) { return true } - // 3. If parsedMetadata is the empty set, return true. + // 3. If response is not eligible for integrity validation, return false. + // TODO + + // 4. If parsedMetadata is the empty set, return true. if (parsedMetadata.length === 0) { return true } - // 4. Let metadata be the result of getting the strongest + // 5. Let metadata be the result of getting the strongest // metadata from parsedMetadata. - const list = parsedMetadata.sort((c, d) => d.algo.localeCompare(c.algo)) - // get the strongest algorithm - const strongest = list[0].algo - // get all entries that use the strongest algorithm; ignore weaker - const metadata = list.filter((item) => item.algo === strongest) + const strongest = getStrongestMetadata(parsedMetadata) + const metadata = filterMetadataListByAlgorithm(parsedMetadata, strongest) - // 5. For each item in metadata: + // 6. For each item in metadata: for (const item of metadata) { // 1. Let algorithm be the alg component of item. const algorithm = item.algo // 2. Let expectedValue be the val component of item. - let expectedValue = item.hash + const expectedValue = item.hash // See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e // "be liberal with padding". This is annoying, and it's not even in the spec. - if (expectedValue.endsWith('==')) { - expectedValue = expectedValue.slice(0, -2) - } - // 3. Let actualValue be the result of applying algorithm to bytes. let actualValue = crypto.createHash(algorithm).update(bytes).digest('base64') - if (actualValue.endsWith('==')) { - actualValue = actualValue.slice(0, -2) + if (actualValue[actualValue.length - 1] === '=') { + if (actualValue[actualValue.length - 2] === '=') { + actualValue = actualValue.slice(0, -2) + } else { + actualValue = actualValue.slice(0, -1) + } } // 4. If actualValue is a case-sensitive match for expectedValue, // return true. - if (actualValue === expectedValue) { - return true - } - - let actualBase64URL = crypto.createHash(algorithm).update(bytes).digest('base64url') - - if (actualBase64URL.endsWith('==')) { - actualBase64URL = actualBase64URL.slice(0, -2) - } - - if (actualBase64URL === expectedValue) { + if (compareBase64Mixed(actualValue, expectedValue)) { return true } } - // 6. Return false. + // 7. Return false. return false } // https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options // https://www.w3.org/TR/CSP2/#source-list-syntax // https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1 -const parseHashWithOptions = /((?sha256|sha384|sha512)-(?[A-z0-9+/]{1}.*={0,2}))( +[\x21-\x7e]?)?/i +const parseHashWithOptions = /(?sha256|sha384|sha512)-((?[A-Za-z0-9+/]+|[A-Za-z0-9_-]+)={0,2}(?:\s|$)( +[!-~]*)?)?/i /** * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata @@ -39836,8 +39970,6 @@ function parseMetadata (metadata) { // 2. Let empty be equal to true. let empty = true - const supportedHashes = crypto.getHashes() - // 3. For each token returned by splitting metadata on spaces: for (const token of metadata.split(' ')) { // 1. Set empty to false. @@ -39847,7 +39979,11 @@ function parseMetadata (metadata) { const parsedToken = parseHashWithOptions.exec(token) // 3. If token does not parse, continue to the next token. - if (parsedToken === null || parsedToken.groups === undefined) { + if ( + parsedToken === null || + parsedToken.groups === undefined || + parsedToken.groups.algo === undefined + ) { // Note: Chromium blocks the request at this point, but Firefox // gives a warning that an invalid integrity was given. The // correct behavior is to ignore these, and subsequently not @@ -39856,11 +39992,11 @@ function parseMetadata (metadata) { } // 4. Let algorithm be the hash-algo component of token. - const algorithm = parsedToken.groups.algo + const algorithm = parsedToken.groups.algo.toLowerCase() // 5. If algorithm is a hash function recognized by the user // agent, add the parsed token to result. - if (supportedHashes.includes(algorithm.toLowerCase())) { + if (supportedHashes.includes(algorithm)) { result.push(parsedToken.groups) } } @@ -39873,6 +40009,82 @@ function parseMetadata (metadata) { return result } +/** + * @param {{ algo: 'sha256' | 'sha384' | 'sha512' }[]} metadataList + */ +function getStrongestMetadata (metadataList) { + // Let algorithm be the algo component of the first item in metadataList. + // Can be sha256 + let algorithm = metadataList[0].algo + // If the algorithm is sha512, then it is the strongest + // and we can return immediately + if (algorithm[3] === '5') { + return algorithm + } + + for (let i = 1; i < metadataList.length; ++i) { + const metadata = metadataList[i] + // If the algorithm is sha512, then it is the strongest + // and we can break the loop immediately + if (metadata.algo[3] === '5') { + algorithm = 'sha512' + break + // If the algorithm is sha384, then a potential sha256 or sha384 is ignored + } else if (algorithm[3] === '3') { + continue + // algorithm is sha256, check if algorithm is sha384 and if so, set it as + // the strongest + } else if (metadata.algo[3] === '3') { + algorithm = 'sha384' + } + } + return algorithm +} + +function filterMetadataListByAlgorithm (metadataList, algorithm) { + if (metadataList.length === 1) { + return metadataList + } + + let pos = 0 + for (let i = 0; i < metadataList.length; ++i) { + if (metadataList[i].algo === algorithm) { + metadataList[pos++] = metadataList[i] + } + } + + metadataList.length = pos + + return metadataList +} + +/** + * Compares two base64 strings, allowing for base64url + * in the second string. + * +* @param {string} actualValue always base64 + * @param {string} expectedValue base64 or base64url + * @returns {boolean} + */ +function compareBase64Mixed (actualValue, expectedValue) { + if (actualValue.length !== expectedValue.length) { + return false + } + for (let i = 0; i < actualValue.length; ++i) { + if (actualValue[i] !== expectedValue[i]) { + if ( + (actualValue[i] === '+' && expectedValue[i] === '-') || + (actualValue[i] === '/' && expectedValue[i] === '_') + ) { + continue + } + return false + } + } + + return true +} + // https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) { // TODO @@ -40288,7 +40500,8 @@ module.exports = { urlHasHttpsScheme, urlIsHttpHttpsScheme, readAllBytes, - normalizeMethodRecord + normalizeMethodRecord, + parseMetadata } @@ -42375,12 +42588,17 @@ function parseLocation (statusCode, headers) { // https://tools.ietf.org/html/rfc7231#section-6.4.4 function shouldRemoveHeader (header, removeContent, unknownOrigin) { - return ( - (header.length === 4 && header.toString().toLowerCase() === 'host') || - (removeContent && header.toString().toLowerCase().indexOf('content-') === 0) || - (unknownOrigin && header.length === 13 && header.toString().toLowerCase() === 'authorization') || - (unknownOrigin && header.length === 6 && header.toString().toLowerCase() === 'cookie') - ) + if (header.length === 4) { + return util.headerNameToString(header) === 'host' + } + if (removeContent && util.headerNameToString(header).startsWith('content-')) { + return true + } + if (unknownOrigin && (header.length === 13 || header.length === 6 || header.length === 19)) { + const name = util.headerNameToString(header) + return name === 'authorization' || name === 'cookie' || name === 'proxy-authorization' + } + return false } // https://tools.ietf.org/html/rfc7231#section-6.4 diff --git a/package-lock.json b/package-lock.json index 40f2bb3..6148d70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3884,12 +3884,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -4215,10 +4216,11 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -5588,10 +5590,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -6276,6 +6279,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -8488,12 +8492,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -9715,6 +9720,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -9935,9 +9941,10 @@ } }, "node_modules/undici": { - "version": "5.28.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.2.tgz", - "integrity": "sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==", + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "license": "MIT", "dependencies": { "@fastify/busboy": "^2.0.0" }, From 50a9171b3eedd19e75bdde6a2a293900a17bf033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Test=C3=A9?= Date: Thu, 12 Dec 2024 10:08:34 +0100 Subject: [PATCH 3/3] chore(ci): format codebase using prettier and standardjs --- .github/workflows/linter.yml | 1 + .github/workflows/registered_runner.yml | 5 +- .github/workflows/removed_runner.yml | 3 + .github/workflows/test_spawn_terminate.yml | 19 +++--- .github/workflows/test_start_stop.yml | 8 +-- README.md | 19 +++--- action.yaml | 23 ++++--- dist/index.js | 79 +++++++++++----------- package.json | 4 +- src/config.js | 12 ++-- src/index.js | 39 +++++------ src/slab.js | 28 ++++---- 12 files changed, 124 insertions(+), 116 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 363bb15..f97637f 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -44,4 +44,5 @@ jobs: JAVASCRIPT_DEFAULT_STYLE: prettier VALIDATE_ALL_CODEBASE: true VALIDATE_JSCPD: false + VALIDATE_JAVASCRIPT_STANDARD: false VALIDATE_MARKDOWN: false diff --git a/.github/workflows/registered_runner.yml b/.github/workflows/registered_runner.yml index 5d1bb10..47b00a7 100644 --- a/.github/workflows/registered_runner.yml +++ b/.github/workflows/registered_runner.yml @@ -8,11 +8,14 @@ on: required: true type: string +permissions: + contents: read + jobs: test-runner: name: Test Runner runs-on: ${{ inputs.runner-name }} - timeout-minutes: 5 # Job should be picked very quickly + timeout-minutes: 5 # Job should be picked very quickly steps: - name: Runner registered run: | diff --git a/.github/workflows/removed_runner.yml b/.github/workflows/removed_runner.yml index 07d9de5..37918c5 100644 --- a/.github/workflows/removed_runner.yml +++ b/.github/workflows/removed_runner.yml @@ -14,6 +14,9 @@ on: READ_REPO_TOKEN: required: true +permissions: + contents: read + jobs: test-removed-runner: name: Test Removed Runner diff --git a/.github/workflows/test_spawn_terminate.yml b/.github/workflows/test_spawn_terminate.yml index 0a4c7bc..caae9a1 100644 --- a/.github/workflows/test_spawn_terminate.yml +++ b/.github/workflows/test_spawn_terminate.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - provider: [ aws, hyperstack ] + provider: [aws, hyperstack] fail-fast: false outputs: runner-aws: ${{ steps.gen-output.outputs.runner_aws }} @@ -45,14 +45,14 @@ jobs: test-runner-alive-aws: name: Test runner is alive (AWS) - needs: [ action-start ] + needs: [action-start] uses: ./.github/workflows/registered_runner.yml with: runner-name: ${{ needs.action-start.outputs.runner-aws }} test-runner-alive-hyperstack: name: Test runner is alive (Hyperstack) - needs: [ action-start ] + needs: [action-start] uses: ./.github/workflows/registered_runner.yml with: runner-name: ${{ needs.action-start.outputs.runner-hyperstack }} @@ -60,12 +60,15 @@ jobs: action-stop: name: GitHub Actions Test (terminate) runs-on: ubuntu-latest - needs: [ action-start, test-runner-alive-aws, test-runner-alive-hyperstack ] + needs: [action-start, test-runner-alive-aws, test-runner-alive-hyperstack] if: ${{ always() && needs.action-start.result != 'skipped' }} strategy: matrix: - runner: [ "${{ needs.action-start.outputs.runner-aws }}", - "${{ needs.action-start.outputs.runner-hyperstack }}" ] + runner: + [ + '${{ needs.action-start.outputs.runner-aws }}', + '${{ needs.action-start.outputs.runner-hyperstack }}' + ] fail-fast: false steps: - name: Checkout @@ -84,7 +87,7 @@ jobs: test-runner-removed-aws: name: Test runner is removed (AWS) - needs: [ action-start, action-stop ] + needs: [action-start, action-stop] uses: ./.github/workflows/removed_runner.yml with: runner-name: ${{ needs.action-start.outputs.runner-aws }} @@ -94,7 +97,7 @@ jobs: test-runner-removed-hyperstack: name: Test runner is removed (Hyperstack) - needs: [ action-start, action-stop ] + needs: [action-start, action-stop] uses: ./.github/workflows/removed_runner.yml with: runner-name: ${{ needs.action-start.outputs.runner-hyperstack }} diff --git a/.github/workflows/test_start_stop.yml b/.github/workflows/test_start_stop.yml index 91acf6a..f10168b 100644 --- a/.github/workflows/test_start_stop.yml +++ b/.github/workflows/test_start_stop.yml @@ -24,7 +24,7 @@ jobs: action-start: name: GitHub Actions Test (start) runs-on: ubuntu-latest - needs: [ test-runner-exist ] + needs: [test-runner-exist] steps: - name: Checkout id: checkout @@ -43,7 +43,7 @@ jobs: test-runner-alive: name: Test runner is alive - needs: [ action-start ] + needs: [action-start] uses: ./.github/workflows/registered_runner.yml with: runner-name: ci-persistent-runner @@ -51,7 +51,7 @@ jobs: action-stop: name: GitHub Actions Test (stop) runs-on: ubuntu-latest - needs: [ action-start, test-runner-alive ] + needs: [action-start, test-runner-alive] if: ${{ always() && needs.action-start.result != 'skipped' }} steps: - name: Checkout @@ -70,7 +70,7 @@ jobs: test-runner-persist: name: Test runner is still registered - needs: [ action-stop ] + needs: [action-stop] uses: ./.github/workflows/removed_runner.yml with: runner-name: ci-persistent-runner diff --git a/README.md b/README.md index b60db35..113a939 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # On-demand self-hosted runner for GitHub Actions -Start your EC2 [self-hosted runner](https://docs.github.com/en/free-pro-team@latest/actions/hosting-your-own-runners) -right before you need it. -Run the job on it. Finally, stop it when you finish. +Start your EC2 +[self-hosted runner](https://docs.github.com/en/free-pro-team@latest/actions/hosting-your-own-runners) +right before you need it. Run the job on it. Finally, stop it when you finish. And all this automatically as a part of your GitHub Actions workflow. It relies on Slab CI bot to do all the heavy-lifting. @@ -22,7 +22,7 @@ See [below](#example) the YAML code of the depicted workflow. ### Inputs | Name | Required | Description | -|----------------|-----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| -------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mode` | Always required. | Specify here which mode you want to use: `start` to start a new runner, `stop` to stop the previously created runner. | | `github-token` | Always required. | GitHub Personal Access Token with the `repo` scope assigned. | | `slab-url` | Always required. | URL to Slab CI server. | @@ -33,13 +33,14 @@ See [below](#example) the YAML code of the depicted workflow. ### Outputs -| Name | Description | -|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `label` | Name of the unique label assigned to the runner. The label is used in two cases: to use as the input of `runs-on` property for the following jobs and to remove the runner from GitHub when it is not needed anymore. | +| Name | Description | +| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `label` | Name of the unique label assigned to the runner. The label is used in two cases: to use as the input of `runs-on` property for the following jobs and to remove the runner from GitHub when it is not needed anymore. | ### Examples -Here's an example workflow. It uses a backend profile declared in `ci/slab.toml` within the calling repository +Here's an example workflow. It uses a backend profile declared in `ci/slab.toml` +within the calling repository ```yml name: do-the-job @@ -62,7 +63,7 @@ jobs: profile: cpu-test do-the-job: - # ... # + # ... # stop-runner: name: Stop self-hosted EC2 runner diff --git a/action.yaml b/action.yaml index 041f8d3..7b5e41f 100644 --- a/action.yaml +++ b/action.yaml @@ -1,5 +1,7 @@ name: On-demand self-hosted runner for GitHub Actions -description: GitHub Action for automatic AWS EC2 instance provisioning as a GitHub Actions self-hosted runner. +description: + GitHub Action for automatic AWS EC2 instance provisioning as a GitHub Actions + self-hosted runner. author: zama-ai branding: icon: 'box' @@ -25,27 +27,26 @@ inputs: required: true backend: description: >- - Backend provider name to look for in slab.toml file in repository that uses the action. - This input is required if you use the 'start' mode. + Backend provider name to look for in slab.toml file in repository that + uses the action. This input is required if you use the 'start' mode. required: false profile: description: >- - Profile to use as described slab.toml file in repository that uses the action. - This input is required if you use the 'start' mode. + Profile to use as described slab.toml file in repository that uses the + action. This input is required if you use the 'start' mode. required: false label: description: >- - Name of the unique label assigned to the runner. - The label is used to remove the runner from GitHub when the runner is not needed anymore. - This input is required if you use the 'stop' mode. + Name of the unique label assigned to the runner. The label is used to + remove the runner from GitHub when the runner is not needed anymore. This + input is required if you use the 'stop' mode. required: false outputs: label: description: >- - Name of the unique label assigned to the runner. - The label is used in two cases: - - to use as the input of 'runs-on' property for the following jobs; + Name of the unique label assigned to the runner. The label is used in two + cases: - to use as the input of 'runs-on' property for the following jobs; - to remove the runner from GitHub when it is not needed anymore. runs: using: node20 diff --git a/dist/index.js b/dist/index.js index a8f12e7..23828ef 100644 --- a/dist/index.js +++ b/dist/index.js @@ -49806,31 +49806,31 @@ class Config { // if (!this.input.mode) { - throw new Error(`The 'mode' input is not specified`) + throw new Error("The 'mode' input is not specified") } if (!this.input.githubToken) { - throw new Error(`The 'github-token' input is not specified`) + throw new Error("The 'github-token' input is not specified") } if (!this.input.slabUrl) { - throw new Error(`The 'slab-url' input is not specified`) + throw new Error("The 'slab-url' input is not specified") } if (!this.input.jobSecret) { - throw new Error(`The 'job-secret' input is not specified`) + throw new Error("The 'job-secret' input is not specified") } if (this.input.mode === 'start') { if (!this.input.backend || !this.input.profile) { throw new Error( - `Not all the required inputs are provided for the 'start' mode` + "Not all the required inputs are provided for the 'start' mode" ) } } else if (this.input.mode === 'stop') { if (!this.input.label) { throw new Error( - `Not all the required inputs are provided for the 'stop' mode` + "Not all the required inputs are provided for the 'stop' mode" ) } } else { @@ -49935,7 +49935,7 @@ function getSignature(content) { return hmac.digest('hex') } -function concat_path(url, path) { +function concatPath(url, path) { if (url.endsWith('/')) { // Multiple '/' char at the end of URL is fine. return url.concat(path) @@ -49969,7 +49969,7 @@ async function startInstanceRequest() { core.info(`Request ${provider} instance start`) try { - response = await fetch(concat_path(url, 'job'), { + response = await fetch(concatPath(url, 'job'), { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -49980,7 +49980,7 @@ async function startInstanceRequest() { body: body.toString() }) } catch (error) { - core.error(`Fetch call has failed`) + core.error('Fetch call has failed') throw error } @@ -49988,9 +49988,9 @@ async function startInstanceRequest() { core.info(`${provider} instance start successfully requested`) return await response.json() } else { - const resp_body = await response.text() + const respBody = await response.text() core.error( - `${provider} instance start request has failed (HTTP status code: ${response.status}, body: ${resp_body})` + `${provider} instance start request has failed (HTTP status code: ${response.status}, body: ${respBody})` ) throw new Error('instance start request failed') } @@ -50012,7 +50012,7 @@ async function stopInstanceRequest(runnerName) { core.info(`Request instance stop (runner: ${runnerName})`) try { - response = await fetch(concat_path(url, 'job'), { + response = await fetch(concatPath(url, 'job'), { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -50031,9 +50031,9 @@ async function stopInstanceRequest(runnerName) { core.info('Instance stop successfully requested') return response.json() } else { - const resp_body = await response.text() + const respBody = await response.text() core.error( - `Instance stop request has failed (HTTP status code: ${response.status}, body: ${resp_body})` + `Instance stop request has failed (HTTP status code: ${response.status}, body: ${respBody})` ) throw new Error('instance stop request failed') } @@ -50051,15 +50051,15 @@ async function waitForInstance(taskId, taskName) { if (response.ok) { const body = await response.json() - const task_status = body[taskName].status.toLowerCase() + const taskStatus = body[taskName].status.toLowerCase() - if (task_status === 'done') { + if (taskStatus === 'done') { if (taskName === 'start') { await acknowledgeTaskDone(taskId) } await removeTask(taskId) return body - } else if (task_status === 'failed') { + } else if (taskStatus === 'failed') { core.error(`Instance task failed (details: ${body[taskName].details})`) core.error('Failure occurred while waiting for instance.') await removeTask(taskId) @@ -50080,7 +50080,7 @@ async function getTask(taskId) { let response try { - response = await fetch(concat_path(url, route)) + response = await fetch(concatPath(url, route)) } catch (error) { core.error(`Failed to fetch task status with ID: ${taskId}`) throw error @@ -50103,7 +50103,7 @@ async function removeTask(taskId) { let response try { - response = await fetch(concat_path(url, route), { + response = await fetch(concatPath(url, route), { method: 'DELETE' }) } catch (error) { @@ -50128,7 +50128,7 @@ async function acknowledgeTaskDone(taskId) { let response try { - response = await fetch(concat_path(url, route), { + response = await fetch(concatPath(url, route), { method: 'POST' }) } catch (error) { @@ -52102,19 +52102,18 @@ const slab = __nccwpck_require__(4156) const config = __nccwpck_require__(4570) const core = __nccwpck_require__(2186) const { waitForRunnerRegistered } = __nccwpck_require__(6989) -const utils = __nccwpck_require__(1608) function setOutput(label) { core.setOutput('label', label) } // This variable should only be defined for cleanup purpose. -let runner_name +let runnerName async function cleanup() { - if (runner_name) { + if (runnerName) { core.info('Stop instance after cancellation') - await slab.stopInstanceRequest(runner_name) + await slab.stopInstanceRequest(runnerName) } } @@ -52126,12 +52125,12 @@ process.on('SIGINT', async function () { async function start() { const provider = config.input.backend - let start_instance_response + let startInstanceResponse for (let i = 1; i <= 3; i++) { try { - start_instance_response = await slab.startInstanceRequest() - runner_name = start_instance_response.runner_name + startInstanceResponse = await slab.startInstanceRequest() + runnerName = startInstanceResponse.runner_name break } catch (error) { core.info('Retrying request now...') @@ -52144,49 +52143,47 @@ async function start() { } } - setOutput(start_instance_response.runner_name) + setOutput(startInstanceResponse.runner_name) core.info( `${provider} instance details: ${JSON.stringify( - start_instance_response.details + startInstanceResponse.details )}` ) try { - const wait_instance_response = await slab.waitForInstance( - start_instance_response.task_id, + const waitInstanceResponse = await slab.waitForInstance( + startInstanceResponse.task_id, 'start' ) - const instance_id = wait_instance_response.start.instance_id - core.info(`${provider} instance started with ID: ${instance_id}`) + const instanceId = waitInstanceResponse.start.instance_id + core.info(`${provider} instance started with ID: ${instanceId}`) - await waitForRunnerRegistered(start_instance_response.runner_name) + await waitForRunnerRegistered(startInstanceResponse.runner_name) } catch (error) { core.info(`Clean up after error, stop ${provider} instance`) - await slab.stopInstanceRequest(start_instance_response.runner_name) + await slab.stopInstanceRequest(startInstanceResponse.runner_name) } } async function stop() { - let stop_instance_response + let stopInstanceResponse for (let i = 1; i <= 3; i++) { try { - stop_instance_response = await slab.stopInstanceRequest( - config.input.label - ) + stopInstanceResponse = await slab.stopInstanceRequest(config.input.label) break } catch (error) { core.info('Retrying request now...') } if (i === 3) { - core.setFailed(`Instance stop request has failed after 3 attempts`) + core.setFailed('Instance stop request has failed after 3 attempts') } } - await slab.waitForInstance(stop_instance_response.task_id, 'stop') + await slab.waitForInstance(stopInstanceResponse.task_id, 'stop') core.info('Instance successfully stopped') } diff --git a/package.json b/package.json index 2025f12..aafd5a0 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,13 @@ "ci-test": "jest --passWithNoTests", "format:write": "prettier --write **/*.js", "format:check": "prettier --check **/*.js", + "format:codebase": "prettier --write **/*js **/*.yml **/*.yaml **/*.md", + "format:standardjs": "standard --fix", "lint": "npx eslint . -c ./.github/linters/.eslintrc.yml", "package": "ncc build src/index.js --license licenses.txt", "package:watch": "npm run package -- --watch", "test": "jest --passWithNoTests", - "all": "npm run format:write && npm run lint && npm run test && npm run package" + "all": "npm run format:standardjs && npm run format:write && npm run lint && npm run test && npm run package" }, "eslintConfig": { "extends": "./.github/linters/.eslintrc.yml" diff --git a/src/config.js b/src/config.js index 6518b4a..0cf74f7 100644 --- a/src/config.js +++ b/src/config.js @@ -28,31 +28,31 @@ class Config { // if (!this.input.mode) { - throw new Error(`The 'mode' input is not specified`) + throw new Error("The 'mode' input is not specified") } if (!this.input.githubToken) { - throw new Error(`The 'github-token' input is not specified`) + throw new Error("The 'github-token' input is not specified") } if (!this.input.slabUrl) { - throw new Error(`The 'slab-url' input is not specified`) + throw new Error("The 'slab-url' input is not specified") } if (!this.input.jobSecret) { - throw new Error(`The 'job-secret' input is not specified`) + throw new Error("The 'job-secret' input is not specified") } if (this.input.mode === 'start') { if (!this.input.backend || !this.input.profile) { throw new Error( - `Not all the required inputs are provided for the 'start' mode` + "Not all the required inputs are provided for the 'start' mode" ) } } else if (this.input.mode === 'stop') { if (!this.input.label) { throw new Error( - `Not all the required inputs are provided for the 'stop' mode` + "Not all the required inputs are provided for the 'stop' mode" ) } } else { diff --git a/src/index.js b/src/index.js index b3daf6a..d5e5c38 100644 --- a/src/index.js +++ b/src/index.js @@ -2,19 +2,18 @@ const slab = require('./slab') const config = require('./config') const core = require('@actions/core') const { waitForRunnerRegistered } = require('./gh') -const utils = require('./utils') function setOutput(label) { core.setOutput('label', label) } // This variable should only be defined for cleanup purpose. -let runner_name +let runnerName async function cleanup() { - if (runner_name) { + if (runnerName) { core.info('Stop instance after cancellation') - await slab.stopInstanceRequest(runner_name) + await slab.stopInstanceRequest(runnerName) } } @@ -26,12 +25,12 @@ process.on('SIGINT', async function () { async function start() { const provider = config.input.backend - let start_instance_response + let startInstanceResponse for (let i = 1; i <= 3; i++) { try { - start_instance_response = await slab.startInstanceRequest() - runner_name = start_instance_response.runner_name + startInstanceResponse = await slab.startInstanceRequest() + runnerName = startInstanceResponse.runner_name break } catch (error) { core.info('Retrying request now...') @@ -44,49 +43,47 @@ async function start() { } } - setOutput(start_instance_response.runner_name) + setOutput(startInstanceResponse.runner_name) core.info( `${provider} instance details: ${JSON.stringify( - start_instance_response.details + startInstanceResponse.details )}` ) try { - const wait_instance_response = await slab.waitForInstance( - start_instance_response.task_id, + const waitInstanceResponse = await slab.waitForInstance( + startInstanceResponse.task_id, 'start' ) - const instance_id = wait_instance_response.start.instance_id - core.info(`${provider} instance started with ID: ${instance_id}`) + const instanceId = waitInstanceResponse.start.instance_id + core.info(`${provider} instance started with ID: ${instanceId}`) - await waitForRunnerRegistered(start_instance_response.runner_name) + await waitForRunnerRegistered(startInstanceResponse.runner_name) } catch (error) { core.info(`Clean up after error, stop ${provider} instance`) - await slab.stopInstanceRequest(start_instance_response.runner_name) + await slab.stopInstanceRequest(startInstanceResponse.runner_name) } } async function stop() { - let stop_instance_response + let stopInstanceResponse for (let i = 1; i <= 3; i++) { try { - stop_instance_response = await slab.stopInstanceRequest( - config.input.label - ) + stopInstanceResponse = await slab.stopInstanceRequest(config.input.label) break } catch (error) { core.info('Retrying request now...') } if (i === 3) { - core.setFailed(`Instance stop request has failed after 3 attempts`) + core.setFailed('Instance stop request has failed after 3 attempts') } } - await slab.waitForInstance(stop_instance_response.task_id, 'stop') + await slab.waitForInstance(stopInstanceResponse.task_id, 'stop') core.info('Instance successfully stopped') } diff --git a/src/slab.js b/src/slab.js index b8e84bd..ca44854 100644 --- a/src/slab.js +++ b/src/slab.js @@ -9,7 +9,7 @@ function getSignature(content) { return hmac.digest('hex') } -function concat_path(url, path) { +function concatPath(url, path) { if (url.endsWith('/')) { // Multiple '/' char at the end of URL is fine. return url.concat(path) @@ -43,7 +43,7 @@ async function startInstanceRequest() { core.info(`Request ${provider} instance start`) try { - response = await fetch(concat_path(url, 'job'), { + response = await fetch(concatPath(url, 'job'), { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -54,7 +54,7 @@ async function startInstanceRequest() { body: body.toString() }) } catch (error) { - core.error(`Fetch call has failed`) + core.error('Fetch call has failed') throw error } @@ -62,9 +62,9 @@ async function startInstanceRequest() { core.info(`${provider} instance start successfully requested`) return await response.json() } else { - const resp_body = await response.text() + const respBody = await response.text() core.error( - `${provider} instance start request has failed (HTTP status code: ${response.status}, body: ${resp_body})` + `${provider} instance start request has failed (HTTP status code: ${response.status}, body: ${respBody})` ) throw new Error('instance start request failed') } @@ -86,7 +86,7 @@ async function stopInstanceRequest(runnerName) { core.info(`Request instance stop (runner: ${runnerName})`) try { - response = await fetch(concat_path(url, 'job'), { + response = await fetch(concatPath(url, 'job'), { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -105,9 +105,9 @@ async function stopInstanceRequest(runnerName) { core.info('Instance stop successfully requested') return response.json() } else { - const resp_body = await response.text() + const respBody = await response.text() core.error( - `Instance stop request has failed (HTTP status code: ${response.status}, body: ${resp_body})` + `Instance stop request has failed (HTTP status code: ${response.status}, body: ${respBody})` ) throw new Error('instance stop request failed') } @@ -125,15 +125,15 @@ async function waitForInstance(taskId, taskName) { if (response.ok) { const body = await response.json() - const task_status = body[taskName].status.toLowerCase() + const taskStatus = body[taskName].status.toLowerCase() - if (task_status === 'done') { + if (taskStatus === 'done') { if (taskName === 'start') { await acknowledgeTaskDone(taskId) } await removeTask(taskId) return body - } else if (task_status === 'failed') { + } else if (taskStatus === 'failed') { core.error(`Instance task failed (details: ${body[taskName].details})`) core.error('Failure occurred while waiting for instance.') await removeTask(taskId) @@ -154,7 +154,7 @@ async function getTask(taskId) { let response try { - response = await fetch(concat_path(url, route)) + response = await fetch(concatPath(url, route)) } catch (error) { core.error(`Failed to fetch task status with ID: ${taskId}`) throw error @@ -177,7 +177,7 @@ async function removeTask(taskId) { let response try { - response = await fetch(concat_path(url, route), { + response = await fetch(concatPath(url, route), { method: 'DELETE' }) } catch (error) { @@ -202,7 +202,7 @@ async function acknowledgeTaskDone(taskId) { let response try { - response = await fetch(concat_path(url, route), { + response = await fetch(concatPath(url, route), { method: 'POST' }) } catch (error) {