From 91d96c7dec4f8c379794ed26d586d18eebbd1b32 Mon Sep 17 00:00:00 2001 From: seebees Date: Wed, 15 Jan 2020 15:07:27 -0800 Subject: [PATCH 1/4] feat: Implemenation initial commit --- action.yml | 18 +++ code-build.js | 138 +++++++++++++++++++++ index.js | 20 ++++ test/code-build-test.js | 258 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 434 insertions(+) create mode 100644 action.yml create mode 100644 code-build.js create mode 100644 index.js create mode 100644 test/code-build-test.js diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..a37d831 --- /dev/null +++ b/action.yml @@ -0,0 +1,18 @@ +name: '"AWS CodeBuild run project" Action For GitHub Actions' +description: 'Execute CodeBuild::startBuild for the current repo.' +branding: + icon: 'cloud' + color: 'orange' +inputs: + project-name: + description: 'AWS CodeBuild Project Name' + required: true + buildspec-override: + description: 'Buildspec Override' + required: false + env-vars: + description: 'Comma separated list of environment variables to send to CodeBuild' + required: false +runs: + using: 'node12' + main: 'dist/index.js' \ No newline at end of file diff --git a/code-build.js b/code-build.js new file mode 100644 index 0000000..a5703cd --- /dev/null +++ b/code-build.js @@ -0,0 +1,138 @@ +// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +const core = require("@actions/core"); +const github = require("@actions/github"); +const aws = require("aws-sdk"); +const assert = require("assert"); + +module.exports = { + startBuild, + _startBuild, + waitForBuildEndTime, + inputs2Parameters, + buildSdk, + logName +}; + +function startBuild() { + // get a codeBuild instance from the SDK + const sdk = buildSdk(); + + // Get input options for startBuild + const params = inputs2Parameters(); + + return _startBuild(sdk, params); +} + +async function _startBuild(sdk, params) { + // Start the build + const start = await sdk.codeBuild.startBuild(params).promise(); + + // Wait for the build to "complete" + const build = await waitForBuildEndTime(sdk, start.build); + + // Signal the outcome + assert( + build.buildStatus === "SUCCEEDED", + `Build status: ${build.buildStatus}` + ); +} + +async function waitForBuildEndTime(sdk, { id, logs }, nextToken) { + const { codeBuild, cloudWatchLogs, wait = 1000 * 5 } = sdk; + + // Get the CloudWatchLog info + const startFromHead = true; + const { cloudWatchLogsArn } = logs; + const { logGroupName, logStreamName } = logName(cloudWatchLogsArn); + + // Check the state + const [batch, cloudWatch = {}] = await Promise.all([ + codeBuild.batchGetBuilds({ ids: [id] }).promise(), + // The CloudWatchLog _may_ not be set up, only make the call if we have a logGroupName + logGroupName && + cloudWatchLogs + .getLogEvents({ logGroupName, logStreamName, startFromHead, nextToken }) + .promise() + ]); + // Pluck off the relevant state + const [current] = batch.builds; + const { nextForwardToken, events = [] } = cloudWatch; + + // stdout the CloudWatchLog (everyone likes progress...) + events.forEach(({ message }) => console.log(message)); + + // We did it! We can stop looking! + if (current.endTime && !events.length) return current; + + // More to do: Sleep for 5 seconds :) + await new Promise(resolve => setTimeout(resolve, wait)); + + // Try again + return waitForBuildEndTime(sdk, current, nextForwardToken); +} + +function inputs2Parameters() { + const projectName = core.getInput("project-name", { required: true }); + + // The github.context.sha is evaluated on import. + // This makes it hard to test. + // So I use the raw ENV + const sourceVersion = process.env[`GITHUB_SHA`]; + const sourceTypeOverride = "GITHUB"; + const { owner, repo } = github.context.repo; + const sourceLocationOverride = `https://github.com/${owner}/${repo}.git`; + + const buildspecOverride = + core.getInput("buildspec-override", { required: false }) || undefined; + + const envVars = core + .getInput("env-vars", { required: false }) + .split(",") + + .map(i => i.trim()); + + const environmentVariablesOverride = Object.entries(process.env) + .filter(([key]) => key.startsWith("GITHUB_") || envVars.includes(key)) + .map(([name, value]) => ({ name, value, type: "PLAINTEXT" })); + + // idempotencyToken is not set on purpose + return { + projectName, + sourceVersion, + sourceTypeOverride, + sourceLocationOverride, + buildspecOverride, + environmentVariablesOverride + }; +} + +function buildSdk() { + const codeBuild = new aws.CodeBuild({ + customUserAgent: "aws-codbuild-run-project" + }); + + const cloudWatchLogs = new aws.CloudWatchLogs({ + customUserAgent: "aws-codbuild-run-project" + }); + + assert( + codeBuild.config.credentials && cloudWatchLogs.config.credentials, + "No credentials. Try adding @aws-actions/configure-aws-credentials" + ); + + return { codeBuild, cloudWatchLogs }; +} + +function logName(Arn) { + const [logGroupName, logStreamName] = Arn.split(":log-group:") + .pop() + .split(":log-stream:"); + if (logGroupName === "null" || logStreamName === "null") + return { + logGroupName: undefined, + logStreamName: undefined + }; + return { logGroupName, logStreamName }; +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..0cba9c7 --- /dev/null +++ b/index.js @@ -0,0 +1,20 @@ +// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +const core = require("@actions/core"); +const { startBuild } = require("./code-build"); + +/* istanbul ignore next */ +if (require.main === module) { + run(); +} + +module.exports = run; + +async function run() { + try { + await startBuild(); + } catch (error) { + core.setFailed(error.message); + } +} diff --git a/test/code-build-test.js b/test/code-build-test.js new file mode 100644 index 0000000..dc05223 --- /dev/null +++ b/test/code-build-test.js @@ -0,0 +1,258 @@ +// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { + logName, + inputs2Parameters, + waitForBuildEndTime +} = require("../code-build"); +const { expect } = require("chai"); + +describe("logName", () => { + it("return the logGroupName and logStreamName from an ARN", () => { + const arn = + "arn:aws:logs:us-west-2:111122223333:log-group:/aws/codebuild/CloudWatchLogGroup:log-stream:1234abcd-12ab-34cd-56ef-1234567890ab"; + const test = logName(arn); + expect(test) + .to.haveOwnProperty("logGroupName") + .and.to.equal("/aws/codebuild/CloudWatchLogGroup"); + expect(test) + .to.haveOwnProperty("logStreamName") + .and.to.equal("1234abcd-12ab-34cd-56ef-1234567890ab"); + }); + + it("return undefined when the group and stream are null", () => { + const arn = + "arn:aws:logs:us-west-2:111122223333:log-group:null:log-stream:null"; + const test = logName(arn); + expect(test) + .to.haveOwnProperty("logGroupName") + .and.to.equal(undefined); + expect(test) + .to.haveOwnProperty("logStreamName") + .and.to.equal(undefined); + }); +}); + +describe("inputs2Parameters", () => { + const OLD_ENV = { ...process.env }; + afterEach(() => { + process.env = { ...OLD_ENV }; + }); + + const projectName = "project_name"; + const repoInfo = "owner/repo"; + const sha = "1234abcd-12ab-34cd-56ef-1234567890ab"; + + it("build basic parameters for codeBuild.startBuild", () => { + // This is how GITHUB injects its input values. + // It would be nice if there was an easy way to test this... + process.env[`INPUT_PROJECT-NAME`] = projectName; + process.env[`GITHUB_REPOSITORY`] = repoInfo; + process.env[`GITHUB_SHA`] = sha; + const test = inputs2Parameters(); + expect(test) + .to.haveOwnProperty("projectName") + .and.to.equal(projectName); + expect(test) + .to.haveOwnProperty("sourceVersion") + .and.to.equal(sha); + expect(test) + .to.haveOwnProperty("sourceTypeOverride") + .and.to.equal("GITHUB"); + expect(test) + .to.haveOwnProperty("sourceLocationOverride") + .and.to.equal(`https://github.com/owner/repo.git`); + expect(test) + .to.haveOwnProperty("buildspecOverride") + .and.to.equal(undefined); + + // I send everything that starts 'GITHUB_' + expect(test) + .to.haveOwnProperty("environmentVariablesOverride") + .and.to.have.lengthOf(2); + expect(test.environmentVariablesOverride[0]) + .to.haveOwnProperty("name") + .and.to.equal("GITHUB_REPOSITORY"); + expect(test.environmentVariablesOverride[0]) + .to.haveOwnProperty("value") + .and.to.equal(repoInfo); + expect(test.environmentVariablesOverride[0]) + .to.haveOwnProperty("type") + .and.to.equal("PLAINTEXT"); + + expect(test.environmentVariablesOverride[1]) + .to.haveOwnProperty("name") + .and.to.equal("GITHUB_SHA"); + expect(test.environmentVariablesOverride[1]) + .to.haveOwnProperty("value") + .and.to.equal(sha); + expect(test.environmentVariablesOverride[1]) + .to.haveOwnProperty("type") + .and.to.equal("PLAINTEXT"); + }); + + it("a project name is required.", () => { + expect(() => inputs2Parameters()).to.throw(); + }); + + it("wtf", () => { + // This is how GITHUB injects its input values. + // It would be nice if there was an easy way to test this... + process.env[`INPUT_PROJECT-NAME`] = projectName; + process.env[`GITHUB_REPOSITORY`] = repoInfo; + process.env[`GITHUB_SHA`] = sha; + + process.env[`INPUT_ENV-VARS`] = `one, two + , three, + four `; + + process.env.one = "_one_"; + process.env.two = "_two_"; + process.env.three = "_three_"; + process.env.four = "_four_"; + + const test = inputs2Parameters(); + + expect(test) + .to.haveOwnProperty("environmentVariablesOverride") + .and.to.have.lengthOf(6); + + expect(test.environmentVariablesOverride[2]) + .to.haveOwnProperty("name") + .and.to.equal("one"); + expect(test.environmentVariablesOverride[2]) + .to.haveOwnProperty("value") + .and.to.equal("_one_"); + expect(test.environmentVariablesOverride[2]) + .to.haveOwnProperty("type") + .and.to.equal("PLAINTEXT"); + + expect(test.environmentVariablesOverride[3]) + .to.haveOwnProperty("name") + .and.to.equal("two"); + expect(test.environmentVariablesOverride[3]) + .to.haveOwnProperty("value") + .and.to.equal("_two_"); + expect(test.environmentVariablesOverride[3]) + .to.haveOwnProperty("type") + .and.to.equal("PLAINTEXT"); + + expect(test.environmentVariablesOverride[4]) + .to.haveOwnProperty("name") + .and.to.equal("three"); + expect(test.environmentVariablesOverride[4]) + .to.haveOwnProperty("value") + .and.to.equal("_three_"); + expect(test.environmentVariablesOverride[4]) + .to.haveOwnProperty("type") + .and.to.equal("PLAINTEXT"); + + expect(test.environmentVariablesOverride[5]) + .to.haveOwnProperty("name") + .and.to.equal("four"); + expect(test.environmentVariablesOverride[5]) + .to.haveOwnProperty("value") + .and.to.equal("_four_"); + expect(test.environmentVariablesOverride[5]) + .to.haveOwnProperty("type") + .and.to.equal("PLAINTEXT"); + }); +}); + +describe("waitForBuildEndTime", () => { + it("basic usages", async () => { + let count = 0; + const buildID = "buildID"; + const cloudWatchLogsArn = + "arn:aws:logs:us-west-2:111122223333:log-group:/aws/codebuild/CloudWatchLogGroup:log-stream:1234abcd-12ab-34cd-56ef-1234567890ab"; + + const buildReplies = [ + { + builds: [ + { id: buildID, logs: { cloudWatchLogsArn }, endTime: "endTime" } + ] + } + ]; + const logReplies = [{ events: [] }]; + const sdk = help( + () => buildReplies[count++], + () => logReplies[count - 1] + ); + + const test = await waitForBuildEndTime(sdk, { + id: buildID, + logs: { cloudWatchLogsArn } + }); + + expect(test).to.equal(buildReplies.pop().builds[0]); + }); + + it("waits for a build endTime **and** no cloud watch log events", async function() { + this.timeout(25000); + let count = 0; + const buildID = "buildID"; + const nullArn = + "arn:aws:logs:us-west-2:111122223333:log-group:null:log-stream:null"; + const cloudWatchLogsArn = + "arn:aws:logs:us-west-2:111122223333:log-group:/aws/codebuild/CloudWatchLogGroup:log-stream:1234abcd-12ab-34cd-56ef-1234567890ab"; + + const buildReplies = [ + { builds: [{ id: buildID, logs: { cloudWatchLogsArn } }] }, + { + builds: [ + { id: buildID, logs: { cloudWatchLogsArn }, endTime: "endTime" } + ] + }, + { + builds: [ + { id: buildID, logs: { cloudWatchLogsArn }, endTime: "endTime" } + ] + } + ]; + const logReplies = [ + undefined, + { events: [{ message: "got one" }] }, + { events: [] } + ]; + const sdk = help( + () => buildReplies[count++], + () => logReplies[count - 1] + ); + + const test = await waitForBuildEndTime(sdk, { + id: buildID, + logs: { cloudWatchLogsArn: nullArn } + }); + expect(test).to.equal(buildReplies.pop().builds[0]); + }); +}); + +function help(builds, logs) { + const codeBuild = { + batchGetBuilds() { + return { + async promise() { + return ret(builds); + } + }; + } + }; + + const cloudWatchLogs = { + getLogEvents() { + return { + async promise() { + return ret(logs); + } + }; + } + }; + + return { codeBuild, cloudWatchLogs, wait: 10 }; + + function ret(thing) { + if (typeof thing === "function") return thing(); + return thing; + } +} From 9d9432dce5c871caab22e2fbeb8a7224c8cb7c8d Mon Sep 17 00:00:00 2001 From: seebees Date: Wed, 15 Jan 2020 15:54:43 -0800 Subject: [PATCH 2/4] Update code-build.js Co-Authored-By: Matt Bullock --- code-build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code-build.js b/code-build.js index a5703cd..9f7e411 100644 --- a/code-build.js +++ b/code-build.js @@ -119,7 +119,7 @@ function buildSdk() { assert( codeBuild.config.credentials && cloudWatchLogs.config.credentials, - "No credentials. Try adding @aws-actions/configure-aws-credentials" + "No credentials. Try adding @aws-actions/configure-aws-credentials earlier in your job to set up AWS credentials." ); return { codeBuild, cloudWatchLogs }; From c5a6fa5d4c3aefa84c3c1fb49b98107272d1aa66 Mon Sep 17 00:00:00 2001 From: seebees Date: Wed, 15 Jan 2020 16:04:26 -0800 Subject: [PATCH 3/4] fix --- code-build.js | 3 ++- test/code-build-test.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/code-build.js b/code-build.js index 9f7e411..49b6ad2 100644 --- a/code-build.js +++ b/code-build.js @@ -97,7 +97,8 @@ function inputs2Parameters() { .filter(([key]) => key.startsWith("GITHUB_") || envVars.includes(key)) .map(([name, value]) => ({ name, value, type: "PLAINTEXT" })); - // idempotencyToken is not set on purpose + // The idempotencyToken is intentionally not set. + // This way the GitHub events can manage the build. return { projectName, sourceVersion, diff --git a/test/code-build-test.js b/test/code-build-test.js index dc05223..f83e076 100644 --- a/test/code-build-test.js +++ b/test/code-build-test.js @@ -96,7 +96,7 @@ describe("inputs2Parameters", () => { expect(() => inputs2Parameters()).to.throw(); }); - it("wtf", () => { + it("can send env-vars", () => { // This is how GITHUB injects its input values. // It would be nice if there was an easy way to test this... process.env[`INPUT_PROJECT-NAME`] = projectName; From 7b4d2e41a780f735a1a74523444016ea9be95b04 Mon Sep 17 00:00:00 2001 From: seebees Date: Thu, 16 Jan 2020 14:11:23 -0800 Subject: [PATCH 4/4] updates --- action.yml | 2 +- code-build.js | 14 +++++++------- index.js | 4 ++-- test/code-build-test.js | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/action.yml b/action.yml index a37d831..e853c32 100644 --- a/action.yml +++ b/action.yml @@ -10,7 +10,7 @@ inputs: buildspec-override: description: 'Buildspec Override' required: false - env-vars: + env-passthrough: description: 'Comma separated list of environment variables to send to CodeBuild' required: false runs: diff --git a/code-build.js b/code-build.js index 49b6ad2..6f474c0 100644 --- a/code-build.js +++ b/code-build.js @@ -7,25 +7,25 @@ const aws = require("aws-sdk"); const assert = require("assert"); module.exports = { - startBuild, - _startBuild, + buildProject, + _buildProject, waitForBuildEndTime, inputs2Parameters, buildSdk, logName }; -function startBuild() { +function buildProject() { // get a codeBuild instance from the SDK const sdk = buildSdk(); // Get input options for startBuild const params = inputs2Parameters(); - return _startBuild(sdk, params); + return _buildProject(sdk, params); } -async function _startBuild(sdk, params) { +async function _buildProject(sdk, params) { // Start the build const start = await sdk.codeBuild.startBuild(params).promise(); @@ -88,7 +88,7 @@ function inputs2Parameters() { core.getInput("buildspec-override", { required: false }) || undefined; const envVars = core - .getInput("env-vars", { required: false }) + .getInput("env-passthrough", { required: false }) .split(",") .map(i => i.trim()); @@ -98,7 +98,7 @@ function inputs2Parameters() { .map(([name, value]) => ({ name, value, type: "PLAINTEXT" })); // The idempotencyToken is intentionally not set. - // This way the GitHub events can manage the build. + // This way the GitHub events can manage the builds. return { projectName, sourceVersion, diff --git a/index.js b/index.js index 0cba9c7..9521c56 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 const core = require("@actions/core"); -const { startBuild } = require("./code-build"); +const { buildProject } = require("./code-build"); /* istanbul ignore next */ if (require.main === module) { @@ -13,7 +13,7 @@ module.exports = run; async function run() { try { - await startBuild(); + await buildProject(); } catch (error) { core.setFailed(error.message); } diff --git a/test/code-build-test.js b/test/code-build-test.js index f83e076..d861edc 100644 --- a/test/code-build-test.js +++ b/test/code-build-test.js @@ -96,14 +96,14 @@ describe("inputs2Parameters", () => { expect(() => inputs2Parameters()).to.throw(); }); - it("can send env-vars", () => { + it("can send env-passthrough", () => { // This is how GITHUB injects its input values. // It would be nice if there was an easy way to test this... process.env[`INPUT_PROJECT-NAME`] = projectName; process.env[`GITHUB_REPOSITORY`] = repoInfo; process.env[`GITHUB_SHA`] = sha; - process.env[`INPUT_ENV-VARS`] = `one, two + process.env[`INPUT_ENV-PASSTHROUGH`] = `one, two , three, four `;