diff --git a/README.md b/README.md index fd65e10..e4cfe87 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ In order to use this tool, you must first `git checkout` the commit that you want to test. ``` -npx @aws-actions/codebuild-run-build -p ProjectName -r remoteName +npx https://github.com/aws-actions/aws-codebuild-run-build.git -p ProjectName -r remoteName ``` This will use whatever commit you have checked out diff --git a/action.yml b/action.yml index 8feaae4..667b22a 100644 --- a/action.yml +++ b/action.yml @@ -10,6 +10,9 @@ inputs: buildspec-override: description: 'Buildspec Override' required: false + batch: + description: 'Run as an AWS CodeBuild batch' + required: false env-vars-for-codebuild: description: 'Comma separated list of environment variables to send to CodeBuild' required: false diff --git a/code-build.js b/code-build.js index 3087510..eccd50b 100644 --- a/code-build.js +++ b/code-build.js @@ -9,7 +9,9 @@ const assert = require("assert"); module.exports = { runBuild, build, + buildBatch, waitForBuildEndTime, + waitForBatchBuildEndTime, inputs2Parameters, githubInputs, buildSdk, @@ -22,8 +24,13 @@ function runBuild() { // Get input options for startBuild const params = inputs2Parameters(githubInputs()); + const isBatch = core.getInput("batch").toUpper() === "TRUE"; - return build(sdk, params); + if (isBatch) { + return buildBatch(sdk, params); + } else { + return build(sdk, params); + } } async function build(sdk, params) { @@ -34,6 +41,113 @@ async function build(sdk, params) { return waitForBuildEndTime(sdk, start.build); } +async function buildBatch(sdk, params) { + // Start the batch + const { buildBatch } = await sdk.codeBuild.startBuildBatch(params).promise(); + const { id } = buildBatch; + + // Wait for the batch to "complete" + return waitForBatchBuildEndTime(sdk, { id }); +} + +async function waitForBatchBuildEndTime(sdk, { id, observedBuilds = [] }) { + const { codeBuild, wait = 2000 } = sdk; + + /* Batch builds take a long time, + * and there is a fare amount + * of eventual constancy involved. + * The first time I enter, + * I never expect this wait + * to impact performance. + * But for every recursive call, + * this wait makes a simple gate + * to keep from throttling myself. + */ + await new Promise((resolve) => setTimeout(resolve, wait)); + + const { buildBatches } = await codeBuild + .batchGetBuildBatches({ ids: [id] }) + .promise(); + const [current] = buildBatches; + const { buildGroups } = current; + + /* Immediately after the batch is started, + * the build group will be empty. + * I have to wait for the first build, + * that will process the list/matrix + * that start all the builds + * that do the work. + */ + if (!buildGroups) return waitForBatchBuildEndTime(sdk, { id }); + + // The build ids I have not yet waited for. + const ids = buildGroups + .map(({ currentBuildSummary }) => currentBuildSummary.arn) + .filter((arn) => !observedBuilds.includes(arn)); + + /* Don't try and get the status of 0 builds. + * It would be nice to not have an if here, + * but it is nicer to not make the remote call. + */ + if (ids.length) { + // Get the information for the builds to wait for + const { builds } = await codeBuild.batchGetBuilds({ ids }).promise(); + + for (const build of builds) { + console.log(`=========== START: ${build.id} =============`); + await waitForBuildEndTime(sdk, build) + /* Just because this build failed, + * I still need to stream the other results. + * waitForBuildEndTime is supposed to handle + * all retirable errors. + * It may be better to + * gather up these errors + * and throw when the batch has completed. + */ + .catch((e) => { + console.log(`Error in build ${build.id}: ${e.stack} `); + }); + console.log(`============================================`); + } + + /* Update the observed builds + * since they have now been observed. + * The `currentBuildSummary` does not have the id. + * It only has the arn, + * so while the id is nice to pass around, + * the arn is what I want to keep here. + * Otherwise each build will need to be streamed twice. + */ + observedBuilds = observedBuilds.concat(builds.map(({ arn }) => arn)); + } + + /* Just because I have processed + * all the builds in the buildGroup + * this does not mean + * that the batch is complete. + * This is especially true with the first build. + * The first build in the batch generally + * is how all the needed builds are calculated. + * So the first time I'm here, + * I expect to have 1 build. + * After that first build, + * I expect to recurse + * and then have the complete set. + * But why not just recurse until + * the batch is no longer in progress? + */ + if (current.buildBatchStatus === "IN_PROGRESS") + return waitForBatchBuildEndTime(sdk, { + id, + observedBuilds, + }); + + /* If the batch is not IN_PROGRESS + * this is as good as it gets. + */ + return current; +} + async function waitForBuildEndTime(sdk, { id, logs }, nextToken) { const { codeBuild, diff --git a/local.js b/local.js index 82868ff..337a486 100755 --- a/local.js +++ b/local.js @@ -8,7 +8,7 @@ const cb = require("./code-build"); const assert = require("assert"); const yargs = require("yargs"); -const { projectName, buildspecOverride, envPassthrough, remote } = yargs +const { projectName, buildspecOverride, envPassthrough, remote, batch } = yargs .option("project-name", { alias: "p", describe: "AWS CodeBuild Project Name", @@ -30,6 +30,11 @@ const { projectName, buildspecOverride, envPassthrough, remote } = yargs describe: "remote name to publish to", default: "origin", type: "string", + }) + .option("batch", { + describe: "Run as AWS CodeBuild batch build", + type: "boolean", + default: false, }).argv; const BRANCH_NAME = uuid(); @@ -46,7 +51,8 @@ const sdk = cb.buildSdk(); pushBranch(remote, BRANCH_NAME); -cb.build(sdk, params) +// Need to select batch or build mode +(batch ? cb.buildBatch(sdk, params) : cb.build(sdk, params)) .then(() => deleteBranch(remote, BRANCH_NAME)) .catch((err) => { deleteBranch(remote, BRANCH_NAME); diff --git a/test/code-build-test.js b/test/code-build-test.js index 2ab2733..a148d63 100644 --- a/test/code-build-test.js +++ b/test/code-build-test.js @@ -6,6 +6,7 @@ const { githubInputs, inputs2Parameters, waitForBuildEndTime, + waitForBatchBuildEndTime, } = require("../code-build"); const { expect } = require("chai"); @@ -418,7 +419,97 @@ describe("waitForBuildEndTime", () => { }); }); -function help(builds, logs) { +describe("waitForBatchBuildEndTime", () => { + it("basic usage", async () => { + 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", + arn: buildID, + }, + ], + }, + { + builds: [ + { + id: buildID, + logs: { cloudWatchLogsArn }, + endTime: "endTime", + arn: buildID, + }, + ], + }, + ]; + const logReplies = [{ events: [] }]; + const batchReplies = [ + { + buildBatches: [{}], + }, + { + buildBatches: [ + { + buildBatchStatus: "IN_PROGRESS", + buildGroups: [ + { + currentBuildSummary: { + arn: buildID, + }, + }, + ], + }, + ], + }, + { + buildBatches: [ + { + buildBatchStatus: "SUCCESS", + buildGroups: [ + { + currentBuildSummary: { + arn: buildID, + }, + }, + ], + }, + ], + }, + ]; + let countBuild = 0; + let countLog = 0; + let countBatch = 0; + const sdk = help( + () => buildReplies[countBuild++], + () => logReplies[countLog++], + () => batchReplies[countBatch++] + ); + + const test = await waitForBatchBuildEndTime(sdk, { + id: buildID, + logs: { cloudWatchLogsArn }, + }); + + expect(test).to.equal(batchReplies.pop().buildBatches[0]); + + /* These counts extensively tests + * the underlying logic. + * Given the linearity + * of the function, + * I assert that this is adequate. + */ + expect(countBuild).to.equal(2); + expect(countLog).to.equal(1); + expect(countBatch).to.equal(3); + }); +}); + +function help(builds, logs, batches) { const codeBuild = { batchGetBuilds() { return { @@ -427,6 +518,13 @@ function help(builds, logs) { }, }; }, + batchGetBuildBatches() { + return { + async promise() { + return ret(batches); + }, + }; + }, }; const cloudWatchLogs = { diff --git a/wallaby.conf.js b/wallaby.conf.js new file mode 100644 index 0000000..f7e5272 --- /dev/null +++ b/wallaby.conf.js @@ -0,0 +1,12 @@ +// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +module.exports = function () { + return { + files: ["./code-build.js"], + tests: ["test/code-build-test.js"], + testFramework: "mocha", + env: { type: "node" }, + debug: true, + }; +};