diff --git a/.circleci/config.yml b/.circleci/config.yml index 00a2039e91..4f51ff9ffa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -275,6 +275,7 @@ jobs: # The `destination` needs to be kept in synch with the value of # `AIO_ARTIFACT_PATH` in `aio/aio-builds-setup/Dockerfile` destination: aio/dist/aio-snapshot.tgz + - run: node ./aio/scripts/create-preview $CIRCLE_BUILD_NUM # This job should only be run on PR builds, where `CI_PULL_REQUEST` is not `false`. test_aio_preview: @@ -460,7 +461,3 @@ workflows: branches: only: - master - -notify: - webhooks: - - url: https://ngbuilds.io/circle-build diff --git a/.circleci/trigger-webhook.js b/.circleci/trigger-webhook.js new file mode 100644 index 0000000000..77860186bc --- /dev/null +++ b/.circleci/trigger-webhook.js @@ -0,0 +1,107 @@ +#!/usr/bin/env node + +/** + * Usage (cli): + * ``` + * node create-preview + * ``` + * + * Usage (JS): + * ```js + * require('./trigger-webhook'). + * triggerWebhook(buildNumber, jobName, webhookUrl). + * then(...); + * ``` + * + * Triggers a notification webhook with CircleCI specific info. + * + * It can be used for notifying external servers and trigger operations based on CircleCI job status + * (e.g. triggering the creation of a preview based on previously stored build atrifacts). + * + * The body of the sent payload is of the form: + * ```json + * { + * "payload": { + * "build_num": ${buildNumber} + * "build_parameters": { + * "CIRCLE_JOB": "${jobName}" + * } + * } + * } + * ``` + * + * When used from JS, it returns a promise which resolves to an object of the form: + * ```json + * { + * "statucCode": ${statusCode}, + * "responseText": "${responseText}" + * } + * ``` + * + * NOTE: + * - When used from the cli, the command will exit with an error code if the response's status code + * is outside the [200, 400) range. + * - When used from JS, the returned promise will be resolved, even if the response's status code is + * outside the [200, 400) range. It is up to the caller to decide how this should be handled. + */ + +// Imports +const {request} = require('https'); + +// Exports +module.exports = { + triggerWebhook, +}; + +// Run +if (require.resolve === module) { + _main(process.argv.slice(2)); +} + +// Helpers +function _main(args) { + triggerWebhook(...args). + then(({statusCode, responseText}) => (200 <= statusCode && statusCode < 400) ? + console.log(`Status: ${statusCode}\n${responseText}`) : + Promise.reject(new Error(`Request failed (status: ${statusCode}): ${responseText}`))). + catch(err => { + console.error(err); + process.exit(1); + }); +} + +function postJson(url, data) { + return new Promise((resolve, reject) => { + const opts = {method: 'post', headers: {'Content-Type': 'application/json'}}; + const onResponse = res => { + const statusCode = res.statusCode || -1; + let responseText = ''; + + res. + on('error', reject). + on('data', d => responseText += d). + on('end', () => resolve({statusCode, responseText})); + }; + + request(url, opts, onResponse). + on('error', reject). + end(JSON.stringify(data)); + }); +} + +async function triggerWebhook(buildNumber, jobName, webhookUrl) { + if (!buildNumber || !jobName || !webhookUrl || isNaN(buildNumber)) { + throw new Error( + 'Missing or invalid arguments.\n' + + 'Expected: buildNumber (number), jobName (string), webhookUrl (string)'); + } + + const data = { + payload: { + build_num: +buildNumber, + build_parameters: {CIRCLE_JOB: jobName}, + }, + }; + + return postJson(webhookUrl, data); +} diff --git a/aio/scripts/create-preview.js b/aio/scripts/create-preview.js new file mode 100644 index 0000000000..a9e26ce4eb --- /dev/null +++ b/aio/scripts/create-preview.js @@ -0,0 +1,37 @@ +#!/usr/bin/env node + +/** + * Usage: + * node create-preview + * + * Triggers the preview server to initiate the preview creation process for the specified CircleCI + * build number. It must be called _after_ the build artifacts have been created and stored on + * CircleCI. + */ + +// Imports +const {triggerWebhook} = require('../../.circleci/trigger-webhook'); + +// Constants +const JOB_NAME = 'aio_preview'; +const WEBHOOK_URL = 'https://ngbuilds.io/circle-build'; + +// Input +const buildNumber = process.argv[2]; + +// Run +triggerWebhook(buildNumber, JOB_NAME, WEBHOOK_URL). + then(({statusCode, responseText}) => isSuccess(statusCode) ? + console.log(`Status: ${statusCode}\n${responseText}`) : + Promise.reject(new Error(`Request failed (status: ${statusCode}): ${responseText}`))). + catch(err => { + console.error(err); + process.exit(1); + }); + +// Helpers +function isSuccess(statusCode) { + // Getting a 409 response from the preview server means that the preview has already been created + // for the corresponding PR/SHA, so our objective has been accomplished. + return (200 <= statusCode && statusCode < 400) || (statusCode === 409); +}