diff --git a/README.md b/README.md index 2ee94438..6271c376 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,13 @@ Optional - The [password](https://vercel.com/docs/concepts/projects/overview#pas Optional - The [header](https://vercel.com/docs/security/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation) to bypass protection for automation +### `basic_auth_credentials_base64` + +Optional - Use if your app is protected with basic auth. provide your base64 encoded credentials in form `username:password`, see [basic auth](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization#basic_authentication) + +### `skip_health_check`` +Optional - Skip the health check for status code 200 and return the URL immediately after successful deployment. Defaults to `false`. + ### `path` Optional - The URL that tests should run against (eg. `path: "https://vercel.com"`). diff --git a/action.js b/action.js index 15a1e676..81b977f7 100644 --- a/action.js +++ b/action.js @@ -15,6 +15,7 @@ const waitForUrl = async ({ maxTimeout, checkIntervalInMilliseconds, vercelPassword, + basicAuthCredentials, protectionBypassHeader, path, }) => { @@ -46,6 +47,12 @@ const waitForUrl = async ({ }; } + if (basicAuthCredentials) { + headers = { + 'Authorization': `Basic ${basicAuthCredentials}` + }; + } + let checkUri = new URL(path, url); await axios.get(checkUri.toString(), { @@ -289,6 +296,8 @@ const run = async () => { const VERCEL_PASSWORD = core.getInput('vercel_password'); const VERCEL_PROTECTION_BYPASS_HEADER = core.getInput('vercel_protection_bypass_header'); const ENVIRONMENT = core.getInput('environment'); + const BASIC_AUTH_CREDENTIALS_BASE_64 = core.getInput('basic_auth_credentials_base64'); + const SKIP_HEALTH_CHECK = core.getInput('skip_health_check') === 'true'; const MAX_TIMEOUT = Number(core.getInput('max_timeout')) || 60; const ALLOW_INACTIVE = Boolean(core.getInput('allow_inactive')) || false; const PATH = core.getInput('path') || '/'; @@ -367,6 +376,11 @@ const run = async () => { // Set output core.setOutput('url', targetUrl); + if (SKIP_HEALTH_CHECK) { + console.log('Skipping health check'); + return; + } + // Wait for url to respond with a success console.log(`Waiting for a status code 200 from: ${targetUrl}`); @@ -376,6 +390,7 @@ const run = async () => { checkIntervalInMilliseconds: CHECK_INTERVAL_IN_MS, vercelPassword: VERCEL_PASSWORD, protectionBypassHeader: VERCEL_PROTECTION_BYPASS_HEADER, + basicAuthCredentials: BASIC_AUTH_CREDENTIALS_BASE_64, path: PATH, }); } catch (error) { diff --git a/action.yml b/action.yml index 290c9cca..9df233f1 100644 --- a/action.yml +++ b/action.yml @@ -26,6 +26,13 @@ inputs: vercel_protection_bypass_header: description: 'Vercel protection bypass for automation' required: false + basic_auth_credentials_base64: + description: 'In case the app is protected with Basic Auth, provide base64 encoded credentials in form `username:password`' + required: false + skip_health_check: + default: 'false' + description: 'Skip health check for status code 200 after deployment' + required: false path: description: 'The path to check. Defaults to the index of the domain' default: '/' diff --git a/dist/index.js b/dist/index.js index 30fde42a..dd5777be 100644 --- a/dist/index.js +++ b/dist/index.js @@ -21,6 +21,8 @@ const waitForUrl = async ({ maxTimeout, checkIntervalInMilliseconds, vercelPassword, + basicAuthCredentials, + protectionBypassHeader, path, }) => { const iterations = calculateIterations( @@ -45,6 +47,18 @@ const waitForUrl = async ({ core.setOutput('vercel_jwt', jwt); } + if (protectionBypassHeader) { + headers = { + 'x-vercel-protection-bypass': protectionBypassHeader + }; + } + + if (basicAuthCredentials) { + headers = { + 'Authorization': `Basic ${basicAuthCredentials}` + }; + } + let checkUri = new URL(path, url); await axios.get(checkUri.toString(), { @@ -234,15 +248,22 @@ const waitForDeploymentToStart = async ({ return deployment; } - throw new Error(`no ${actorName} deployment found`); - } catch (e) { console.log( `Could not find any deployments for actor ${actorName}, retrying (attempt ${ i + 1 } / ${iterations})` ); - await wait(checkIntervalInMilliseconds); + } catch(e) { + console.log( + `Error while fetching deployments, retrying (attempt ${ + i + 1 + } / ${iterations})` + ); + + console.error(e) } + + await wait(checkIntervalInMilliseconds); } return null; @@ -279,7 +300,10 @@ const run = async () => { // Inputs const GITHUB_TOKEN = core.getInput('token', { required: true }); const VERCEL_PASSWORD = core.getInput('vercel_password'); + const VERCEL_PROTECTION_BYPASS_HEADER = core.getInput('vercel_protection_bypass_header'); const ENVIRONMENT = core.getInput('environment'); + const BASIC_AUTH_CREDENTIALS_BASE_64 = core.getInput('basic_auth_credentials_base64'); + const SKIP_HEALTH_CHECK = core.getInput('skip_health_check') === 'true'; const MAX_TIMEOUT = Number(core.getInput('max_timeout')) || 60; const ALLOW_INACTIVE = Boolean(core.getInput('allow_inactive')) || false; const PATH = core.getInput('path') || '/'; @@ -326,7 +350,7 @@ const run = async () => { sha: sha, environment: ENVIRONMENT, actorName: 'vercel[bot]', - maxTimeout: MAX_TIMEOUT / 2, + maxTimeout: MAX_TIMEOUT, checkIntervalInMilliseconds: CHECK_INTERVAL_IN_MS, }); @@ -358,6 +382,11 @@ const run = async () => { // Set output core.setOutput('url', targetUrl); + if (SKIP_HEALTH_CHECK) { + console.log('Skipping health check'); + return; + } + // Wait for url to respond with a success console.log(`Waiting for a status code 200 from: ${targetUrl}`); @@ -366,6 +395,8 @@ const run = async () => { maxTimeout: MAX_TIMEOUT, checkIntervalInMilliseconds: CHECK_INTERVAL_IN_MS, vercelPassword: VERCEL_PASSWORD, + protectionBypassHeader: VERCEL_PROTECTION_BYPASS_HEADER, + basicAuthCredentials: BASIC_AUTH_CREDENTIALS_BASE_64, path: PATH, }); } catch (error) { @@ -6577,6 +6608,29 @@ var Writable = (__nccwpck_require__(2781).Writable); var assert = __nccwpck_require__(9491); var debug = __nccwpck_require__(1133); +// Whether to use the native URL object or the legacy url module +var useNativeURL = false; +try { + assert(new URL()); +} +catch (error) { + useNativeURL = error.code === "ERR_INVALID_URL"; +} + +// URL fields to preserve in copy operations +var preservedUrlFields = [ + "auth", + "host", + "hostname", + "href", + "path", + "pathname", + "port", + "protocol", + "query", + "search", +]; + // Create handlers that pass events from native requests var events = ["abort", "aborted", "connect", "error", "socket", "timeout"]; var eventHandlers = Object.create(null); @@ -6586,19 +6640,20 @@ events.forEach(function (event) { }; }); +// Error types with codes var InvalidUrlError = createErrorType( "ERR_INVALID_URL", "Invalid URL", TypeError ); -// Error types with codes var RedirectionError = createErrorType( "ERR_FR_REDIRECTION_FAILURE", "Redirected request failed" ); var TooManyRedirectsError = createErrorType( "ERR_FR_TOO_MANY_REDIRECTS", - "Maximum number of redirects exceeded" + "Maximum number of redirects exceeded", + RedirectionError ); var MaxBodyLengthExceededError = createErrorType( "ERR_FR_MAX_BODY_LENGTH_EXCEEDED", @@ -6609,6 +6664,9 @@ var WriteAfterEndError = createErrorType( "write after end" ); +// istanbul ignore next +var destroy = Writable.prototype.destroy || noop; + // An HTTP(S) request that can be redirected function RedirectableRequest(options, responseCallback) { // Initialize the request @@ -6630,7 +6688,13 @@ function RedirectableRequest(options, responseCallback) { // React to responses of native requests var self = this; this._onNativeResponse = function (response) { - self._processResponse(response); + try { + self._processResponse(response); + } + catch (cause) { + self.emit("error", cause instanceof RedirectionError ? + cause : new RedirectionError({ cause: cause })); + } }; // Perform the first request @@ -6639,10 +6703,17 @@ function RedirectableRequest(options, responseCallback) { RedirectableRequest.prototype = Object.create(Writable.prototype); RedirectableRequest.prototype.abort = function () { - abortRequest(this._currentRequest); + destroyRequest(this._currentRequest); + this._currentRequest.abort(); this.emit("abort"); }; +RedirectableRequest.prototype.destroy = function (error) { + destroyRequest(this._currentRequest, error); + destroy.call(this, error); + return this; +}; + // Writes buffered data to the current native request RedirectableRequest.prototype.write = function (data, encoding, callback) { // Writing is not allowed if end has been called @@ -6755,6 +6826,7 @@ RedirectableRequest.prototype.setTimeout = function (msecs, callback) { self.removeListener("abort", clearTimer); self.removeListener("error", clearTimer); self.removeListener("response", clearTimer); + self.removeListener("close", clearTimer); if (callback) { self.removeListener("timeout", callback); } @@ -6781,6 +6853,7 @@ RedirectableRequest.prototype.setTimeout = function (msecs, callback) { this.on("abort", clearTimer); this.on("error", clearTimer); this.on("response", clearTimer); + this.on("close", clearTimer); return this; }; @@ -6839,8 +6912,7 @@ RedirectableRequest.prototype._performRequest = function () { var protocol = this._options.protocol; var nativeProtocol = this._options.nativeProtocols[protocol]; if (!nativeProtocol) { - this.emit("error", new TypeError("Unsupported protocol " + protocol)); - return; + throw new TypeError("Unsupported protocol " + protocol); } // If specified, use the agent corresponding to the protocol @@ -6932,15 +7004,14 @@ RedirectableRequest.prototype._processResponse = function (response) { } // The response is a redirect, so abort the current request - abortRequest(this._currentRequest); + destroyRequest(this._currentRequest); // Discard the remainder of the response to avoid waiting for data response.destroy(); // RFC7231ยง6.4: A client SHOULD detect and intervene // in cyclical redirections (i.e., "infinite" redirection loops). if (++this._redirectCount > this._options.maxRedirects) { - this.emit("error", new TooManyRedirectsError()); - return; + throw new TooManyRedirectsError(); } // Store the request headers if applicable @@ -6974,33 +7045,23 @@ RedirectableRequest.prototype._processResponse = function (response) { var currentHostHeader = removeMatchingHeaders(/^host$/i, this._options.headers); // If the redirect is relative, carry over the host of the last request - var currentUrlParts = url.parse(this._currentUrl); + var currentUrlParts = parseUrl(this._currentUrl); var currentHost = currentHostHeader || currentUrlParts.host; var currentUrl = /^\w+:/.test(location) ? this._currentUrl : url.format(Object.assign(currentUrlParts, { host: currentHost })); - // Determine the URL of the redirection - var redirectUrl; - try { - redirectUrl = url.resolve(currentUrl, location); - } - catch (cause) { - this.emit("error", new RedirectionError({ cause: cause })); - return; - } - // Create the redirected request - debug("redirecting to", redirectUrl); + var redirectUrl = resolveUrl(location, currentUrl); + debug("redirecting to", redirectUrl.href); this._isRedirect = true; - var redirectUrlParts = url.parse(redirectUrl); - Object.assign(this._options, redirectUrlParts); + spreadUrlObject(redirectUrl, this._options); // Drop confidential headers when redirecting to a less secure protocol // or to a different domain that is not a superdomain - if (redirectUrlParts.protocol !== currentUrlParts.protocol && - redirectUrlParts.protocol !== "https:" || - redirectUrlParts.host !== currentHost && - !isSubdomain(redirectUrlParts.host, currentHost)) { + if (redirectUrl.protocol !== currentUrlParts.protocol && + redirectUrl.protocol !== "https:" || + redirectUrl.host !== currentHost && + !isSubdomain(redirectUrl.host, currentHost)) { removeMatchingHeaders(/^(?:authorization|cookie)$/i, this._options.headers); } @@ -7015,23 +7076,12 @@ RedirectableRequest.prototype._processResponse = function (response) { method: method, headers: requestHeaders, }; - try { - beforeRedirect(this._options, responseDetails, requestDetails); - } - catch (err) { - this.emit("error", err); - return; - } + beforeRedirect(this._options, responseDetails, requestDetails); this._sanitizeOptions(this._options); } // Perform the redirected request - try { - this._performRequest(); - } - catch (cause) { - this.emit("error", new RedirectionError({ cause: cause })); - } + this._performRequest(); }; // Wraps the key/value object of protocols with redirect functionality @@ -7051,27 +7101,16 @@ function wrap(protocols) { // Executes a request, following redirects function request(input, options, callback) { - // Parse parameters - if (isString(input)) { - var parsed; - try { - parsed = urlToOptions(new URL(input)); - } - catch (err) { - /* istanbul ignore next */ - parsed = url.parse(input); - } - if (!isString(parsed.protocol)) { - throw new InvalidUrlError({ input }); - } - input = parsed; + // Parse parameters, ensuring that input is an object + if (isURL(input)) { + input = spreadUrlObject(input); } - else if (URL && (input instanceof URL)) { - input = urlToOptions(input); + else if (isString(input)) { + input = spreadUrlObject(parseUrl(input)); } else { callback = options; - options = input; + options = validateUrl(input); input = { protocol: protocol }; } if (isFunction(options)) { @@ -7110,27 +7149,57 @@ function wrap(protocols) { return exports; } -/* istanbul ignore next */ function noop() { /* empty */ } -// from https://github.com/nodejs/node/blob/master/lib/internal/url.js -function urlToOptions(urlObject) { - var options = { - protocol: urlObject.protocol, - hostname: urlObject.hostname.startsWith("[") ? - /* istanbul ignore next */ - urlObject.hostname.slice(1, -1) : - urlObject.hostname, - hash: urlObject.hash, - search: urlObject.search, - pathname: urlObject.pathname, - path: urlObject.pathname + urlObject.search, - href: urlObject.href, - }; - if (urlObject.port !== "") { - options.port = Number(urlObject.port); +function parseUrl(input) { + var parsed; + /* istanbul ignore else */ + if (useNativeURL) { + parsed = new URL(input); + } + else { + // Ensure the URL is valid and absolute + parsed = validateUrl(url.parse(input)); + if (!isString(parsed.protocol)) { + throw new InvalidUrlError({ input }); + } + } + return parsed; +} + +function resolveUrl(relative, base) { + /* istanbul ignore next */ + return useNativeURL ? new URL(relative, base) : parseUrl(url.resolve(base, relative)); +} + +function validateUrl(input) { + if (/^\[/.test(input.hostname) && !/^\[[:0-9a-f]+\]$/i.test(input.hostname)) { + throw new InvalidUrlError({ input: input.href || input }); + } + if (/^\[/.test(input.host) && !/^\[[:0-9a-f]+\](:\d+)?$/i.test(input.host)) { + throw new InvalidUrlError({ input: input.href || input }); } - return options; + return input; +} + +function spreadUrlObject(urlObject, target) { + var spread = target || {}; + for (var key of preservedUrlFields) { + spread[key] = urlObject[key]; + } + + // Fix IPv6 hostname + if (spread.hostname.startsWith("[")) { + spread.hostname = spread.hostname.slice(1, -1); + } + // Ensure port is a number + if (spread.port !== "") { + spread.port = Number(spread.port); + } + // Concatenate path + spread.path = spread.search ? spread.pathname + spread.search : spread.pathname; + + return spread; } function removeMatchingHeaders(regex, headers) { @@ -7156,17 +7225,25 @@ function createErrorType(code, message, baseClass) { // Attach constructor and set default properties CustomError.prototype = new (baseClass || Error)(); - CustomError.prototype.constructor = CustomError; - CustomError.prototype.name = "Error [" + code + "]"; + Object.defineProperties(CustomError.prototype, { + constructor: { + value: CustomError, + enumerable: false, + }, + name: { + value: "Error [" + code + "]", + enumerable: false, + }, + }); return CustomError; } -function abortRequest(request) { +function destroyRequest(request, error) { for (var event of events) { request.removeListener(event, eventHandlers[event]); } request.on("error", noop); - request.abort(); + request.destroy(error); } function isSubdomain(subdomain, domain) { @@ -7187,6 +7264,10 @@ function isBuffer(value) { return typeof value === "object" && ("length" in value); } +function isURL(value) { + return URL && value instanceof URL; +} + // Exports module.exports = wrap({ http: http, https: https }); module.exports.wrap = wrap;