Skip to content

Commit

Permalink
Add poll-interval input (#99) (#100)
Browse files Browse the repository at this point in the history
Co-authored-by: Nick Amoscato <nick@amoscato.com>
  • Loading branch information
Vadorequest and namoscato authored May 29, 2023
1 parent 1cef290 commit 3d536f0
Show file tree
Hide file tree
Showing 15 changed files with 120 additions and 53 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ plugins:
- jest
parser: '@typescript-eslint/parser'
parserOptions:
project: ./tsconfig.json
project: ./tsconfig.eslint.json
rules: # See https://eslint.org/docs/rules
semi:
- error
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
coverage/
node_modules/
lib/
README.md
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
with:
deployment-url: nextjs-bzyss249z.vercel.app # TODO Replace by the domain you want to test
timeout: 10 # Wait for 10 seconds before failing
poll-interval: 1 # Wait for 1 second before each retry

- name: Display deployment status
run: "echo The deployment at ${{ fromJson(steps.await-vercel.outputs.deploymentDetails).url }} is ${{ fromJson(steps.await-vercel.outputs.deploymentDetails).readyState }}"
Expand All @@ -35,13 +36,13 @@ It waits until a Vercel deployment domain is marked as "READY". _(See [`readySta
You must know the domain url you want to await for and provide it as `deployment-url` input.

## Why/when should you use it?
If you're using Vercel to deploy your apps, and you use some custom deployment pipeline using GitHub Actions,
If you're using Vercel to deploy your apps, and you use some custom deployment pipeline using GitHub Actions,
you might need to wait for a deployment to be ready before running other processes _(e.g: Your end-to-end tests using [Cypress](https://www.cypress.io/))_.

> For instance, if you don't wait for the deployment to be ready,
> For instance, if you don't wait for the deployment to be ready,
then you might sometimes run your E2E tests suite against the Vercel's login page, instead of your actual deployment.

If your GitHub Actions sometimes succeeds but sometimes fails, then you probably need to await for the domain to be ready.
If your GitHub Actions sometimes succeeds but sometimes fails, then you probably need to await for the domain to be ready.
This action might help doing so, as it will wait until the Vercel deployment is really ready, before starting your next GitHub Action step.

## What else does this action do?
Expand Down Expand Up @@ -71,21 +72,24 @@ Name | Description
--- | ---
`VERCEL_TOKEN` | Your [vercel token](https://vercel.com/account/tokens) is required to fetch the Vercel API on your behalf and get the status of your deployment. [See usage in code](https://github.com/UnlyEd/github-action-await-vercel/search?q=VERCEL_TOKEN)

> _**N.B**: You don't have to use a GitHub Secret to provide the `VERCEL_TOKEN`. But you should do so, as it's a good security practice, because this way the token will be [hidden in the logs (encrypted)](https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets)._
> _**N.B**: You don't have to use a GitHub Secret to provide the `VERCEL_TOKEN`. But you should do so, as it's a good security practice, because this way the token will be [hidden in the logs (encrypted)](https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets)._

### Action's API

#### Inputs
Name | Required | Default | Description
--- | --- |--- |---
`deployment-url`|✅| |Deployment domain (e.g: `my-app-hfq88g3jt.vercel.app`, `my-app.vercel.app`, etc.).
`timeout`|✖️|`10`|How long (in seconds) the action waits for the deployment status to reach either `READY` or `ERROR` state.
`timeout`|✖️|`10`|Duration (in seconds) the action waits for the deployment status to reach either `READY` or `ERROR` state.
`poll-interval`|✖️|`1`|Duration (in seconds) the action waits in between polled Vercel API requests.

> **Tip**: You might want to adapt the `timeout` to your use case.
> - For instance, if you're calling this action **right after having triggered the Vercel deployment**, then it'll go through `INITIALIZING > ANALYZING > BUILDING > DEPLOYING` phases before reaching `READY` or `ERROR` state.
> This might take quite some time (depending on your project), and increasing the timeout to `600` (10mn) (or similar) is probably what you'll want to do in such case, because you need to take into account the time it'll take for Vercel to deploy.
> - The default of `10` seconds is because we _assume_ you'll call this action after the deployment has reached `BUILDING` state, and the time it takes for Vercel to reach `READY` or `ERROR` from `BUILDING` is rather short.

> **Tip**: `poll-interval` prevents spamming Vercel's API such that the number of requests stays within their rate limits. [Vercel allows](https://vercel.com/docs/concepts/limits/overview#rate-limits) 500 deployment retrievals every minute, and the 1-second default value will allow for about 8 concurrent executions of this GitHub Action.

#### Outputs
This action forwards the [Vercel API response](https://vercel.com/docs/api#endpoints/deployments/get-a-single-deployment/response-parameters) as return value.

Expand Down Expand Up @@ -131,7 +135,7 @@ jobs:

Check the documentation to see what information [`deploymentDetails`](https://vercel.com/docs/api#endpoints/deployments/get-a-single-deployment/response-parameters) contains.

### 2. Dynamically resolve the Vercel deployment url
### 2. Dynamically resolve the Vercel deployment url

This is a real-world use case example, from [Next Right Now](https://github.com/UnlyEd/next-right-now).

Expand Down
18 changes: 18 additions & 0 deletions __tests__/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { millisecondsFromInput } from '../src/config';

describe('millisecondsFromInput', () => {
let prevEnvValue: string | undefined;

beforeEach(() => {
prevEnvValue = process.env.INPUT_TIMEOUT;
process.env.INPUT_TIMEOUT = '10';
});

afterEach(() => {
process.env.INPUT_TIMEOUT = prevEnvValue;
});

it('should convert seconds string to milliseconds', () => {
expect(millisecondsFromInput('timeout')).toBe(10_000);
});
});
15 changes: 13 additions & 2 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as cp from 'child_process';
import * as path from 'path';
import * as process from 'process';
import {BUILD_DIR, BUILD_MAIN_FILENAME} from '../src/config';
import { BUILD_DIR, BUILD_MAIN_FILENAME } from '../src/config';

/**
* Enhance the Node.js environment "global" variable to add our own types
Expand Down Expand Up @@ -39,7 +39,15 @@ function exec_lib(options: cp.ExecFileSyncOptions): string {

try {
// console.debug(`Running command "${nodeBinaryPath} ${mainFilePath}"`);
return cp.execFileSync(nodeBinaryPath, [mainFilePath], options).toString();
return cp
.execFileSync(nodeBinaryPath, [mainFilePath], {
env: {
NODE_ENV: 'test',
...options.env,
},
...options,
})
.toString();
} catch (e) {
console.error(e?.output?.toString());
console.error(e);
Expand All @@ -63,6 +71,7 @@ describe('Functional test', () => {
env: {
'INPUT_DEPLOYMENT-URL': CORRECT_DOMAIN,
'INPUT_TIMEOUT': MAX_TIMEOUT,
'INPUT_POLL-INTERVAL': '1',
'VERCEL_TOKEN': process.env.VERCEL_TOKEN,
},
};
Expand Down Expand Up @@ -97,6 +106,7 @@ describe('Functional test', () => {
env: {
'INPUT_DEPLOYMENT-URL': 'i-am-wrong-domain.vercel.app',
'INPUT_TIMEOUT': MAX_TIMEOUT,
'INPUT_POLL-INTERVAL': '1',
'VERCEL_TOKEN': process.env.VERCEL_TOKEN,
},
};
Expand All @@ -118,6 +128,7 @@ describe('Functional test', () => {
env: {
'INPUT_DEPLOYMENT-URL': WRONG_DOMAIN,
'INPUT_TIMEOUT': MAX_TIMEOUT,
'INPUT_POLL-INTERVAL': '1',
'VERCEL_TOKEN': 'not valid',
},
};
Expand Down
7 changes: 6 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ inputs:
description: 'Url you want to wait for'
required: true
timeout:
description: 'Custom timeout value (default: "10" (seconds))'
description: 'Duration (in seconds) to wait for a terminal deployment status'
required: false
default: '10'
poll-interval:
description: 'Duration (in seconds) to wait in between polled Vercel API requests'
required: false
default: '1'
outputs:
deploymentDetails:
description: 'Forwarded Vercel API response - See https://vercel.com/docs/api#endpoints/deployments/get-a-single-deployment/response-parameters'
Expand Down
2 changes: 1 addition & 1 deletion github-action-runtime/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion github-action-runtime/index.js.map

Large diffs are not rendered by default.

21 changes: 10 additions & 11 deletions lib/awaitVercelDeployment.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,34 +37,33 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(require("@actions/core"));
const node_fetch_retry_1 = __importDefault(require("@adobe/node-fetch-retry"));
const promises_1 = require("timers/promises");
const config_1 = require("./config");
/**
* Awaits for the Vercel deployment to be in a "ready" state.
*
* @param baseUrl Base url of the Vercel deployment to await for.
* @param timeout Duration (in seconds) until we'll await for.
* When the timeout is reached, the Promise is rejected (the action will fail).
* When the `timeout` is reached, the Promise is rejected (the action will fail)
*/
const awaitVercelDeployment = (baseUrl, timeout) => {
const awaitVercelDeployment = ({ url, timeout, pollInterval }) => {
return new Promise((resolve, reject) => __awaiter(void 0, void 0, void 0, function* () {
let deployment = {};
const timeoutTime = new Date().getTime() + timeout;
while (new Date().getTime() < timeoutTime) {
deployment = (yield (0, node_fetch_retry_1.default)(`${config_1.VERCEL_BASE_API_ENDPOINT}/v11/now/deployments/get?url=${baseUrl}`, {
const retryMaxDuration = timeoutTime - new Date().getTime(); // constrain retries by remaining timeout duration
core.debug(`Retrieving deployment (retryMaxDuration=${retryMaxDuration}ms)`);
deployment = yield (0, node_fetch_retry_1.default)(`${config_1.VERCEL_BASE_API_ENDPOINT}/v11/now/deployments/get?url=${url}`, {
headers: {
Authorization: `Bearer ${process.env.VERCEL_TOKEN}`,
},
retryOptions: {
retryMaxDuration: timeout * 1000, // Convert seconds to milliseconds
},
})
.then((data) => data.json())
.catch((error) => reject(error)));
retryOptions: { retryMaxDuration },
}).then((data) => data.json());
core.debug(`Received these data from Vercel: ${JSON.stringify(deployment)}`);
if (deployment.readyState === 'READY' || deployment.readyState === 'ERROR') {
core.debug('Deployment has been found');
return resolve(deployment);
}
core.debug(`Waiting ${pollInterval}ms`);
yield (0, promises_1.setTimeout)(pollInterval);
}
core.debug(`Last deployment response: ${JSON.stringify(deployment)}`);
return reject('Timeout has been reached');
Expand Down
14 changes: 9 additions & 5 deletions lib/config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BUILD_MAIN_FILENAME = exports.BUILD_DIR = exports.DEFAULT_TIMEOUT = exports.VERCEL_BASE_API_ENDPOINT = void 0;
exports.millisecondsFromInput = exports.BUILD_MAIN_FILENAME = exports.BUILD_DIR = exports.VERCEL_BASE_API_ENDPOINT = void 0;
const core_1 = require("@actions/core");
exports.VERCEL_BASE_API_ENDPOINT = 'https://api.vercel.com';
/**
* Timeout (in seconds) used by default if no custom timeout is provided as input.
*/
exports.DEFAULT_TIMEOUT = 10;
/**
* Directory where the compiled version (JS) of the TS code is stored.
*
Expand All @@ -18,3 +15,10 @@ exports.BUILD_DIR = 'lib';
* XXX Should match the package.json:main value.
*/
exports.BUILD_MAIN_FILENAME = 'main.js';
/**
* Return the value of the specified action `input`, converted from seconds to milliseconds.
*/
function millisecondsFromInput(input) {
return +(0, core_1.getInput)(input) * 1000;
}
exports.millisecondsFromInput = millisecondsFromInput;
10 changes: 6 additions & 4 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@ const run = () => {
core.info('Debug mode is disabled. Read more at https://github.com/UnlyEd/github-action-await-vercel#how-to-enable-debug-logs');
}
try {
const urlToWait = core.getInput('deployment-url');
core.debug(`Url to wait for: ${urlToWait}`); // debug is only output if you set the secret `ACTIONS_RUNNER_DEBUG` to true https://github.com/actions/toolkit/blob/master/docs/action-debugging.md#how-to-access-step-debug-logs
const timeout = (+core.getInput('timeout') || config_1.DEFAULT_TIMEOUT) * 1000;
const url = core.getInput('deployment-url');
core.debug(`Url to wait for: ${url}`); // debug is only output if you set the secret `ACTIONS_RUNNER_DEBUG` to true https://github.com/actions/toolkit/blob/master/docs/action-debugging.md#how-to-access-step-debug-logs
const timeout = (0, config_1.millisecondsFromInput)('timeout');
core.debug(`Timeout used: ${timeout}`);
(0, awaitVercelDeployment_1.default)(urlToWait, timeout)
const pollInterval = (0, config_1.millisecondsFromInput)('poll-interval');
core.debug(`Poll interval used: ${pollInterval}`);
(0, awaitVercelDeployment_1.default)({ url, timeout, pollInterval })
.then((deployment) => {
core.setOutput('deploymentDetails', deployment);
})
Expand Down
34 changes: 23 additions & 11 deletions src/awaitVercelDeployment.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,50 @@
import * as core from '@actions/core';
import fetch from '@adobe/node-fetch-retry';
import { setTimeout } from 'timers/promises';
import { VERCEL_BASE_API_ENDPOINT } from './config';
import { VercelDeployment } from './types/VercelDeployment';

interface Options {
/** Base url of the Vercel deployment to await for */
url: string;
/** Duration (in milliseconds) to wait for a terminal deployment status */
timeout: number;
/** Duration (in milliseconds) to wait in between polled Vercel API requests */
pollInterval: number;
}

/**
* Awaits for the Vercel deployment to be in a "ready" state.
*
* @param baseUrl Base url of the Vercel deployment to await for.
* @param timeout Duration (in seconds) until we'll await for.
* When the timeout is reached, the Promise is rejected (the action will fail).
* When the `timeout` is reached, the Promise is rejected (the action will fail)
*/
const awaitVercelDeployment = (baseUrl: string, timeout: number): Promise<VercelDeployment> => {
const awaitVercelDeployment = ({ url, timeout, pollInterval }: Options): Promise<VercelDeployment> => {
return new Promise(async (resolve, reject) => {
let deployment: VercelDeployment = {};
const timeoutTime = new Date().getTime() + timeout;

while (new Date().getTime() < timeoutTime) {
deployment = (await fetch(`${VERCEL_BASE_API_ENDPOINT}/v11/now/deployments/get?url=${baseUrl}`, {
const retryMaxDuration = timeoutTime - new Date().getTime(); // constrain retries by remaining timeout duration

core.debug(`Retrieving deployment (retryMaxDuration=${retryMaxDuration}ms)`);
deployment = await fetch(`${VERCEL_BASE_API_ENDPOINT}/v11/now/deployments/get?url=${url}`, {
headers: {
Authorization: `Bearer ${process.env.VERCEL_TOKEN}`,
},
retryOptions: {
retryMaxDuration: timeout * 1000, // Convert seconds to milliseconds
},
})
.then((data) => data.json())
.catch((error: string) => reject(error))) as VercelDeployment;
retryOptions: { retryMaxDuration },
}).then<VercelDeployment>((data) => data.json());

core.debug(`Received these data from Vercel: ${JSON.stringify(deployment)}`);

if (deployment.readyState === 'READY' || deployment.readyState === 'ERROR') {
core.debug('Deployment has been found');
return resolve(deployment);
}

core.debug(`Waiting ${pollInterval}ms`);
await setTimeout(pollInterval);
}

core.debug(`Last deployment response: ${JSON.stringify(deployment)}`);

return reject('Timeout has been reached');
Expand Down
14 changes: 9 additions & 5 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
export const VERCEL_BASE_API_ENDPOINT = 'https://api.vercel.com';
import { getInput } from '@actions/core';

/**
* Timeout (in seconds) used by default if no custom timeout is provided as input.
*/
export const DEFAULT_TIMEOUT = 10;
export const VERCEL_BASE_API_ENDPOINT = 'https://api.vercel.com';

/**
* Directory where the compiled version (JS) of the TS code is stored.
Expand All @@ -18,3 +15,10 @@ export const BUILD_DIR = 'lib';
* XXX Should match the package.json:main value.
*/
export const BUILD_MAIN_FILENAME = 'main.js';

/**
* Return the value of the specified action `input`, converted from seconds to milliseconds.
*/
export function millisecondsFromInput(input: string): number {
return +getInput(input) * 1000;
}
13 changes: 8 additions & 5 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as core from '@actions/core';
import awaitVercelDeployment from './awaitVercelDeployment';
import { DEFAULT_TIMEOUT } from './config';
import { millisecondsFromInput } from './config';
import { VercelDeployment } from './types/VercelDeployment';

/**
Expand All @@ -27,13 +27,16 @@ const run = (): void => {
}

try {
const urlToWait: string = core.getInput('deployment-url');
core.debug(`Url to wait for: ${urlToWait}`); // debug is only output if you set the secret `ACTIONS_RUNNER_DEBUG` to true https://github.com/actions/toolkit/blob/master/docs/action-debugging.md#how-to-access-step-debug-logs
const url: string = core.getInput('deployment-url');
core.debug(`Url to wait for: ${url}`); // debug is only output if you set the secret `ACTIONS_RUNNER_DEBUG` to true https://github.com/actions/toolkit/blob/master/docs/action-debugging.md#how-to-access-step-debug-logs

const timeout: number = (+core.getInput('timeout') || DEFAULT_TIMEOUT) * 1000;
const timeout: number = millisecondsFromInput('timeout');
core.debug(`Timeout used: ${timeout}`);

awaitVercelDeployment(urlToWait, timeout)
const pollInterval: number = millisecondsFromInput('poll-interval');
core.debug(`Poll interval used: ${pollInterval}`);

awaitVercelDeployment({ url, timeout, pollInterval })
.then((deployment: VercelDeployment) => {
core.setOutput('deploymentDetails', deployment);
})
Expand Down
4 changes: 4 additions & 0 deletions tsconfig.eslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules"]
}

0 comments on commit 3d536f0

Please sign in to comment.