index.js (394 lines of code) (raw):

const { stripIndents } = require('common-tags'); const core = require('@actions/core'); const github = require('@actions/github'); const { execSync } = require('child_process'); const exec = require('@actions/exec'); const packageJSON = require('./package.json'); function getGithubCommentInput() { const input = core.getInput('github-comment'); if (input === 'true') return true; if (input === 'false') return false; return input; } const { context } = github; const githubToken = core.getInput('github-token'); const githubComment = getGithubCommentInput(); const workingDirectory = core.getInput('working-directory'); const prNumberRegExp = /{{\s*PR_NUMBER\s*}}/g; const branchRegExp = /{{\s*BRANCH\s*}}/g; function isPullRequestType(event) { return event.startsWith('pull_request'); } function slugify(str) { const slug = str .toString() .trim() .toLowerCase() .replace(/[_\s]+/g, '-') .replace(/[^\w-]+/g, '') .replace(/--+/g, '-') .replace(/^-+/, '') .replace(/-+$/, ''); core.debug(`before slugify: "${str}"; after slugify: "${slug}"`); return slug; } function retry(fn, retries) { async function attempt(retry) { try { return await fn(); } catch (error) { if (retry > retries) { throw error; } else { core.info(`retrying: attempt ${retry + 1} / ${retries + 1}`); await new Promise(resolve => setTimeout(resolve, 3000)); return attempt(retry + 1); } } } return attempt(1); } // Vercel function getVercelBin() { const input = core.getInput('vercel-version'); const fallback = packageJSON.dependencies.vercel; return `vercel@${input || fallback}`; } const vercelToken = core.getInput('vercel-token', { required: true }); const vercelArgs = core.getInput('vercel-args'); const vercelOrgId = core.getInput('vercel-org-id'); const vercelProjectId = core.getInput('vercel-project-id'); const vercelScope = core.getInput('scope'); const vercelProjectName = core.getInput('vercel-project-name'); const vercelBin = getVercelBin(); const aliasDomains = core .getInput('alias-domains') .split('\n') .filter(x => x !== '') .map(s => { let url = s; let branch = slugify(context.ref.replace('refs/heads/', '')); if (isPullRequestType(context.eventName)) { const pr = context.payload.pull_request || context.payload.pull_request_target; branch = slugify(pr.head.ref.replace('refs/heads/', '')); url = url.replace(prNumberRegExp, context.issue.number.toString()); } url = url.replace(branchRegExp, branch); return url; }); let octokit; if (githubToken) { octokit = new github.GitHub(githubToken); } async function setEnv() { core.info('set environment for vercel cli'); if (vercelOrgId) { core.info('set env variable : VERCEL_ORG_ID'); core.exportVariable('VERCEL_ORG_ID', vercelOrgId); } if (vercelProjectId) { core.info('set env variable : VERCEL_PROJECT_ID'); core.exportVariable('VERCEL_PROJECT_ID', vercelProjectId); } } function addVercelMetadata(key, value, providedArgs) { // returns a list for the metadata commands if key was not supplied by user in action parameters // returns an empty list if key was provided by user const pattern = `^${key}=.+`; const metadataRegex = new RegExp(pattern, 'g'); // eslint-disable-next-line no-restricted-syntax for (const arg of providedArgs) { if (arg.match(metadataRegex)) { return []; } } return ['-m', `${key}=${value}`]; } async function vercelDeploy(ref, commit) { let myOutput = ''; // eslint-disable-next-line no-unused-vars let myError = ''; const options = {}; options.listeners = { stdout: data => { myOutput += data.toString(); core.info(data.toString()); }, stderr: data => { // eslint-disable-next-line no-unused-vars myError += data.toString(); core.info(data.toString()); }, }; if (workingDirectory) { options.cwd = workingDirectory; } const providedArgs = vercelArgs.split(/ +/); const args = [ ...vercelArgs.split(/ +/), ...['-t', vercelToken], ...addVercelMetadata('githubCommitSha', context.sha, providedArgs), ...addVercelMetadata('githubCommitAuthorName', context.actor, providedArgs), ...addVercelMetadata( 'githubCommitAuthorLogin', context.actor, providedArgs, ), ...addVercelMetadata('githubDeployment', 1, providedArgs), ...addVercelMetadata('githubOrg', context.repo.owner, providedArgs), ...addVercelMetadata('githubRepo', context.repo.repo, providedArgs), ...addVercelMetadata('githubCommitOrg', context.repo.owner, providedArgs), ...addVercelMetadata('githubCommitRepo', context.repo.repo, providedArgs), ...addVercelMetadata('githubCommitMessage', `"${commit}"`, providedArgs), ...addVercelMetadata( 'githubCommitRef', ref.replace('refs/heads/', ''), providedArgs, ), ]; if (vercelScope) { core.info('using scope'); args.push('--scope', vercelScope); } await exec.exec('npx', [vercelBin, ...args], options); return myOutput; } async function vercelInspect(deploymentUrl) { // eslint-disable-next-line no-unused-vars let myOutput = ''; let myError = ''; const options = {}; options.listeners = { stdout: data => { // eslint-disable-next-line no-unused-vars myOutput += data.toString(); core.info(data.toString()); }, stderr: data => { myError += data.toString(); core.info(data.toString()); }, }; if (workingDirectory) { options.cwd = workingDirectory; } const args = [vercelBin, 'inspect', deploymentUrl, '-t', vercelToken]; if (vercelScope) { core.info('using scope'); args.push('--scope', vercelScope); } await exec.exec('npx', args, options); const match = myError.match(/^\s+name\s+(.+)$/m); return match && match.length ? match[1] : null; } async function findCommentsForEvent() { core.debug('find comments for event'); if (context.eventName === 'push') { core.debug('event is "commit", use "listCommentsForCommit"'); return octokit.repos.listCommentsForCommit({ ...context.repo, commit_sha: context.sha, }); } if (isPullRequestType(context.eventName)) { core.debug(`event is "${context.eventName}", use "listComments"`); return octokit.issues.listComments({ ...context.repo, issue_number: context.issue.number, }); } core.error('not supported event_type'); return []; } async function findPreviousComment(text) { if (!octokit) { return null; } core.info('find comment'); const { data: comments } = await findCommentsForEvent(); const vercelPreviewURLComment = comments.find(comment => comment.body.startsWith(text), ); if (vercelPreviewURLComment) { core.info('previous comment found'); return vercelPreviewURLComment.id; } core.info('previous comment not found'); return null; } function joinDeploymentUrls(deploymentUrl, aliasDomains_) { if (aliasDomains_.length) { const aliasUrls = aliasDomains_.map(domain => `https://${domain}`); return [deploymentUrl, ...aliasUrls].join('\n'); } return deploymentUrl; } function buildCommentPrefix(deploymentName) { return `:rocket: Built _${deploymentName}_ successfully!`; } function buildCommentBody(deploymentCommit, deploymentUrl, deploymentName) { if (!githubComment) { return undefined; } const prefix = `${buildCommentPrefix(deploymentName)}\n\n`; const rawGithubComment = prefix + (typeof githubComment === 'string' || githubComment instanceof String ? githubComment : stripIndents` * ${joinDeploymentUrls(deploymentUrl, aliasDomains)} * Built with commit ${deploymentCommit} > Issues? Visit #docs-elastic-dev in Slack `); return rawGithubComment .replace(/\{\{deploymentCommit\}\}/g, deploymentCommit) .replace(/\{\{deploymentName\}\}/g, deploymentName) .replace( /\{\{deploymentUrl\}\}/g, joinDeploymentUrls(deploymentUrl, aliasDomains), ); } async function createCommentOnCommit( deploymentCommit, deploymentUrl, deploymentName, ) { if (!octokit) { return; } const commentId = await findPreviousComment( buildCommentPrefix(deploymentName), ); const commentBody = buildCommentBody( deploymentCommit, deploymentUrl, deploymentName, ); if (commentId) { await octokit.repos.updateCommitComment({ ...context.repo, comment_id: commentId, body: commentBody, }); } else { await octokit.repos.createCommitComment({ ...context.repo, commit_sha: context.sha, body: commentBody, }); } } async function createCommentOnPullRequest( deploymentCommit, deploymentUrl, deploymentName, ) { if (!octokit) { return; } const commentId = await findPreviousComment( `:rocket: Built _${deploymentName}_ successfully!`, ); const commentBody = buildCommentBody( deploymentCommit, deploymentUrl, deploymentName, ); if (commentId) { await octokit.issues.updateComment({ ...context.repo, comment_id: commentId, body: commentBody, }); } else { await octokit.issues.createComment({ ...context.repo, issue_number: context.issue.number, body: commentBody, }); } } async function aliasDomainsToDeployment(deploymentUrl) { if (!deploymentUrl) { core.error('deployment url is null'); } const args = ['-t', vercelToken]; if (vercelScope) { core.info('using scope'); args.push('--scope', vercelScope); } const promises = aliasDomains.map(domain => retry( () => exec.exec('npx', [vercelBin, ...args, 'alias', deploymentUrl, domain]), 2, ), ); await Promise.all(promises); } async function run() { core.debug(`action : ${context.action}`); core.debug(`ref : ${context.ref}`); core.debug(`eventName : ${context.eventName}`); core.debug(`actor : ${context.actor}`); core.debug(`sha : ${context.sha}`); core.debug(`workflow : ${context.workflow}`); let { ref } = context; let { sha } = context; await setEnv(); let commit = execSync('git log -1 --pretty=format:%B') .toString() .trim(); if (github.context.eventName === 'push') { const pushPayload = github.context.payload; core.debug(`The head commit is: ${pushPayload.head_commit}`); } else if (isPullRequestType(github.context.eventName)) { const pullRequestPayload = github.context.payload; const pr = pullRequestPayload.pull_request || pullRequestPayload.pull_request_target; core.debug(`head : ${pr.head}`); ref = pr.head.ref; sha = pr.head.sha; core.debug(`The head ref is: ${pr.head.ref}`); core.debug(`The head sha is: ${pr.head.sha}`); if (octokit) { const { data: commitData } = await octokit.git.getCommit({ ...context.repo, commit_sha: sha, }); commit = commitData.message; core.debug(`The head commit is: ${commit}`); } } const deploymentUrl = await vercelDeploy(ref, commit); if (deploymentUrl) { core.info('set preview-url output'); if (aliasDomains && aliasDomains.length) { core.info('set preview-url output as first alias'); core.setOutput('preview-url', `https://${aliasDomains[0]}`); } else { core.setOutput('preview-url', deploymentUrl); } } else { core.warning('get preview-url error'); } const deploymentName = vercelProjectName || (await vercelInspect(deploymentUrl)); if (deploymentName) { core.info('set preview-name output'); core.setOutput('preview-name', deploymentName); } else { core.warning('get preview-name error'); } if (aliasDomains.length) { core.info('alias domains to this deployment'); await aliasDomainsToDeployment(deploymentUrl); } if (githubComment && githubToken) { if (context.issue.number) { core.info('this is related issue or pull_request'); await createCommentOnPullRequest(sha, deploymentUrl, deploymentName); } else if (context.eventName === 'push') { core.info('this is push event'); await createCommentOnCommit(sha, deploymentUrl, deploymentName); } } else { core.info('comment : disabled'); } } run().catch(error => { core.setFailed(`••• ERROR: ${error.message} •••`); });