Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[INF-118] ANSIBLE UPDATE WORK - On hold until after holidays - Updated to 1.0.8 #3

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ The only required input is `project-name`.
that CodeBuild requires.
By default, the action uses the buildspec file location
that you configured in the CodeBuild project.
1. **compute-type-override** (optional) :
The name of a compute type for this build that overrides the one specified
in the build project.
1. **environment-type-override** (optional) :
A container type for this build that overrides the one specified in the
build project.
1. **image-override** (optional) :
The name of an image for this build that overrides the one specified
in the build project.
1. **env-vars-for-codebuild** (optional) :
A comma-separated list of the names of environment variables
that the action passes from GitHub Actions to CodeBuild.
Expand All @@ -37,6 +46,31 @@ The only required input is `project-name`.
the one defined here replaces the one in the CodeBuild project.
For a list of CodeBuild environment variables, see

1. **update-interval** (optional) :
Update interval as seconds for how often the API is called to check on the status.

A higher value mitigates the chance of hitting API rate-limiting especially when
running many instances of this action in parallel, but also introduces a larger
potential time overhead (ranging from 0 to update interval) for the action to
fetch the build result and finish.

Lower value limits the potential time overhead worst case but it may hit the API
rate-limit more often, depending on the use-case.

The default value is 30.

1. **update-back-off** (optional) :
Base back-off time in seconds for the update interval.

When API rate-limiting is hit the back-off time, augmented with jitter, will be
added to the next update interval.
E.g. with update interval of 30 and back-off time of 15, upon hitting the rate-limit
the next interval for the update call will be 30 + random_between(0, 15 _ 2 \*\* 0))
seconds and if the rate-limit is hit again the next interval will be
30 + random_between(0, 15 _ 2 \*\* 1) and so on.

The default value is 15.

### Outputs

1. **aws-build-id** : The CodeBuild build ID of the build that the action ran.
Expand Down Expand Up @@ -136,7 +170,7 @@ the only CodeBuild Run input you need to provide is the project name.
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-2
- name: Run CodeBuild
uses: aws-actions/aws-codebuild-run-build@v1.0.3
uses: aws-actions/aws-codebuild-run-build@v1
with:
project-name: CodeBuildProjectName
```
Expand All @@ -158,10 +192,13 @@ this will overwrite them.
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-2
- name: Run CodeBuild
uses: aws-actions/aws-codebuild-run-build@v1.0.3
uses: aws-actions/aws-codebuild-run-build@v1
with:
project-name: CodeBuildProjectName
buildspec-override: path/to/buildspec.yaml
compute-type-override: compute-type
environment-type-override: environment-type
image-override: ecr-image-uri
env-vars-for-codebuild: |
custom,
requester,
Expand Down
20 changes: 18 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,28 @@ inputs:
buildspec-override:
description: 'Buildspec Override'
required: false
compute-type-override:
description: 'The name of a compute type for this build that overrides the one specified in the build project.'
required: false
environment-type-override:
description: 'A container type for this build that overrides the one specified in the build project.'
required: false
image-override:
description: 'The name of an image for this build that overrides the one specified in the build project.'
required: false
env-vars-for-codebuild:
description: 'Comma separated list of environment variables to send to CodeBuild'
required: false
update-interval:
description: 'How often the action calls the API for updates'
required: false
update-back-off:
description: 'Base back-off time for the update calls for API if rate-limiting is encountered'
required: false

outputs:
aws-build-id:
description: 'The AWS CodeBuild Build ID for this build.'
runs:
using: 'node12'
main: 'dist/index.js'
using: 'node16'
main: 'dist/index.js'
162 changes: 130 additions & 32 deletions code-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,41 @@ function runBuild() {
// get a codeBuild instance from the SDK
const sdk = buildSdk();

const inputs = githubInputs();

const config = (({ updateInterval, updateBackOff }) => ({
updateInterval,
updateBackOff,
}))(inputs);

// Get input options for startBuild
const params = inputs2Parameters(githubInputs());
const params = inputs2Parameters(inputs);

return build(sdk, params);
return build(sdk, params, config);
}

async function build(sdk, params) {
async function build(sdk, params, config) {
// Start the build
const start = await sdk.codeBuild.startBuild(params).promise();

// Wait for the build to "complete"
return waitForBuildEndTime(sdk, start.build);
return waitForBuildEndTime(sdk, start.build, config);
}

async function waitForBuildEndTime(sdk, { id, logs }, nextToken) {
const {
codeBuild,
cloudWatchLogs,
wait = 1000 * 30,
backOff = 1000 * 15,
} = sdk;
async function waitForBuildEndTime(
sdk,
{ id, logs },
{ updateInterval, updateBackOff },
seqEmptyLogs,
totalEvents,
throttleCount,
nextToken
) {
const { codeBuild, cloudWatchLogs } = sdk;

totalEvents = totalEvents || 0;
seqEmptyLogs = seqEmptyLogs || 0;
throttleCount = throttleCount || 0;

// Get the CloudWatchLog info
const startFromHead = true;
Expand All @@ -55,7 +69,12 @@ async function waitForBuildEndTime(sdk, { id, logs }, nextToken) {
// The CloudWatchLog _may_ not be set up, only make the call if we have a logGroupName
logGroupName &&
cloudWatchLogs
.getLogEvents({ logGroupName, logStreamName, startFromHead, nextToken })
.getLogEvents({
logGroupName,
logStreamName,
startFromHead,
nextToken,
})
.promise(),
]).catch((err) => {
errObject = err;
Expand All @@ -70,16 +89,24 @@ async function waitForBuildEndTime(sdk, { id, logs }, nextToken) {
if (errObject) {
//We caught an error in trying to make the AWS api call, and are now checking to see if it was just a rate limiting error
if (errObject.message && errObject.message.search("Rate exceeded") !== -1) {
//We were rate-limited, so add `backOff` seconds to the wait time
let newWait = wait + backOff;
// We were rate-limited, so add backoff with Full Jitter, ref: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
let jitteredBackOff = Math.floor(
Math.random() * (updateBackOff * 2 ** throttleCount)
);
let newWait = updateInterval + jitteredBackOff;
throttleCount++;

//Sleep before trying again
await new Promise((resolve) => setTimeout(resolve, newWait));

// Try again from the same token position
return waitForBuildEndTime(
{ ...sdk, wait: newWait },
{ ...sdk },
{ id, logs },
{ updateInterval: newWait, updateBackOff },
seqEmptyLogs,
totalEvents,
throttleCount,
nextToken
);
} else {
Expand All @@ -92,20 +119,48 @@ async function waitForBuildEndTime(sdk, { id, logs }, nextToken) {
const [current] = batch.builds;
const { nextForwardToken, events = [] } = cloudWatch;

// GetLogEvents can return partial/empty responses even when there is data.
// We wait for two consecutive empty log responses to minimize false positive on EOF.
// Empty response counter starts after any logs have been received, or when the build completes.
if (events.length == 0 && (totalEvents > 0 || current.endTime)) {
seqEmptyLogs++;
} else {
seqEmptyLogs = 0;
}
totalEvents += events.length;

// stdout the CloudWatchLog (everyone likes progress...)
// CloudWatchLogs have line endings.
// I trim and then log each line
// to ensure that the line ending is OS specific.
events.forEach(({ message }) => console.log(message.trimEnd()));

// We did it! We can stop looking!
if (current.endTime && !events.length) return current;
// Stop after the build is ended and we've received two consecutive empty log responses
if (current.endTime && seqEmptyLogs >= 2) {
return current;
}

// More to do: Sleep for a few seconds to avoid rate limiting
await new Promise((resolve) => setTimeout(resolve, wait));
// If never throttled and build is complete, halve CWL polling delay to minimize latency
await new Promise((resolve) =>
setTimeout(
resolve,
current.endTime && throttleCount == 0
? updateInterval / 2
: updateInterval
)
);

// Try again
return waitForBuildEndTime(sdk, current, nextForwardToken);
return waitForBuildEndTime(
sdk,
current,
{ updateInterval, updateBackOff },
seqEmptyLogs,
totalEvents,
throttleCount,
nextForwardToken
);
}

function githubInputs() {
Expand All @@ -117,7 +172,7 @@ function githubInputs() {
// So I use the raw ENV.
// There is a complexity here because for pull request
// the GITHUB_SHA value is NOT the correct value.
// See: https://github.com/sailthru/aws-codebuild-run-build/issues/36
// See: https://github.com/aws-actions/aws-codebuild-run-build/issues/36
const sourceVersion =
process.env[`GITHUB_EVENT_NAME`] === "pull_request"
? (((payload || {}).pull_request || {}).head || {}).sha
Expand All @@ -127,19 +182,44 @@ function githubInputs() {
const buildspecOverride =
core.getInput("buildspec-override", { required: false }) || undefined;

const computeTypeOverride =
core.getInput("compute-type-override", { required: false }) || undefined;

const environmentTypeOverride =
core.getInput("environment-type-override", { required: false }) ||
undefined;
const imageOverride =
core.getInput("image-override", { required: false }) || undefined;

const envPassthrough = core
.getInput("env-vars-for-codebuild", { required: false })
.split(",")
.map((i) => i.trim())
.filter((i) => i !== "");

const updateInterval =
parseInt(
core.getInput("update-interval", { required: false }) || "30",
10
) * 1000;
const updateBackOff =
parseInt(
core.getInput("update-back-off", { required: false }) || "15",
10
) * 1000;

return {
projectName,
owner,
repo,
sourceVersion,
buildspecOverride,
computeTypeOverride,
environmentTypeOverride,
imageOverride,
envPassthrough,
updateInterval,
updateBackOff,
};
}

Expand All @@ -150,8 +230,15 @@ function inputs2Parameters(inputs) {
repo,
sourceVersion,
buildspecOverride,
computeTypeOverride,
environmentTypeOverride,
imageOverride,
envPassthrough = [],
} = inputs;

const sourceTypeOverride = "GITHUB";
const sourceLocationOverride = `https://github.com/${owner}/${repo}.git`;

const environmentVariablesOverride = Object.entries(process.env)
.filter(
([key]) => key.startsWith("GITHUB_") || envPassthrough.includes(key)
Expand All @@ -162,36 +249,47 @@ function inputs2Parameters(inputs) {
// This way the GitHub events can manage the builds.
return {
projectName,
sourceVersion,
sourceTypeOverride,
sourceLocationOverride,
buildspecOverride,
computeTypeOverride,
environmentTypeOverride,
imageOverride,
environmentVariablesOverride,
};
}

function buildSdk() {
const codeBuild = new aws.CodeBuild({
customUserAgent: "sailthru/aws-codebuild-run-build",
customUserAgent: "aws-actions/aws-codebuild-run-build",
});

const cloudWatchLogs = new aws.CloudWatchLogs({
customUserAgent: "sailthru/aws-codebuild-run-build",
customUserAgent: "aws-actions/aws-codebuild-run-build",
});

assert(
codeBuild.config.credentials && cloudWatchLogs.config.credentials,
"No credentials. Try adding @sailthru/configure-aws-credentials earlier in your job to set up AWS credentials."
"No credentials. Try adding @aws-actions/configure-aws-credentials earlier in your job to set up 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 };
const logs = {
logGroupName: undefined,
logStreamName: undefined,
};
if (Arn) {
const [logGroupName, logStreamName] = Arn.split(":log-group:")
.pop()
.split(":log-stream:");
if (logGroupName !== "null" && logStreamName !== "null") {
logs.logGroupName = logGroupName;
logs.logStreamName = logStreamName;
}
}
return logs;
}
Loading
Loading