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

feat: support cfn outputs #67

Merged
merged 29 commits into from
Jan 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b3f18e2
new mechanism for deploying assets
kaizencc Jan 13, 2022
c4c9305
snapshots
kaizencc Jan 13, 2022
03d5ce6
cdkout
kaizencc Jan 13, 2022
7bacf66
chore: self mutation
invalid-email-address Jan 13, 2022
0fc3248
chore: self mutation
invalid-email-address Jan 13, 2022
55b42aa
chore: self mutation
invalid-email-address Jan 13, 2022
537de08
chore: self mutation
invalid-email-address Jan 13, 2022
a05c7ef
chore: self mutation
invalid-email-address Jan 13, 2022
64bd04e
chore: self mutation
invalid-email-address Jan 13, 2022
155709b
chore: self mutation
invalid-email-address Jan 13, 2022
138ccdc
chore: self mutation
invalid-email-address Jan 13, 2022
c71c14a
chore: self mutation
invalid-email-address Jan 13, 2022
b7813e5
chore: self mutation
invalid-email-address Jan 13, 2022
99992af
temp dir so that self-mutation stops
kaizencc Jan 13, 2022
d1af1e7
merge
kaizencc Jan 13, 2022
8af582a
feat: support cfn outputs
kaizencc Jan 14, 2022
a641db7
minor changes
kaizencc Jan 14, 2022
3124b82
add temp dir function
kaizencc Jan 14, 2022
c596936
chore: self mutation
invalid-email-address Jan 14, 2022
addcd9c
chore: self mutation
invalid-email-address Jan 14, 2022
50c5a7e
cleanup
kaizencc Jan 14, 2022
94d4a58
Merge branch 'conroy/assets' of https://github.com/cdklabs/cdk-pipeli…
kaizencc Jan 14, 2022
17c8144
chore: self mutation
invalid-email-address Jan 14, 2022
7e64e60
update tests
kaizencc Jan 14, 2022
79234ac
merge
kaizencc Jan 14, 2022
785d91e
Merge branch 'conroy/assets' of https://github.com/cdklabs/cdk-pipeli…
kaizencc Jan 14, 2022
51ace27
clean up code
kaizencc Jan 14, 2022
284acda
update readme'
kaizencc Jan 14, 2022
3b12527
Merge branch 'main' into conroy/outputs
rix0rrr Jan 19, 2022
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
2 changes: 1 addition & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ Name | Type | Description
**if**? | <code>string</code> | You can use the if conditional to prevent a job from running unless a condition is met.<br/>__*Optional*__
**name**? | <code>string</code> | The name of the job displayed on GitHub.<br/>__*Optional*__
**needs**? | <code>Array<string></code> | Identifies any jobs that must complete successfully before this job will run.<br/>__*Optional*__
**outputs**? | <code>Map<string, [JobStepOutput](#cdk-pipelines-github-jobstepoutput)></code> | A map of outputs for a job.<br/>__*Optional*__
**outputs**? | <code>Map<string, string></code> | A map of outputs for a job.<br/>__*Optional*__
**services**? | <code>Map<string, [ContainerOptions](#cdk-pipelines-github-containeroptions)></code> | Used to host service containers for a job in a workflow.<br/>__*Optional*__
**strategy**? | <code>[JobStrategy](#cdk-pipelines-github-jobstrategy)</code> | A strategy creates a build matrix for your jobs.<br/>__*Optional*__
**timeoutMinutes**? | <code>number</code> | The maximum number of minutes to let a job run before GitHub automatically cancels it.<br/>__*Default*__: 360
Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,6 @@ This is work in progress. The following features are still not supported:

* [ ] Credentials and roles (document permissions required, etc)
* [ ] Support Docker image assets
* [ ] Support Pre/post steps
* [ ] Support CFN output bindings
* [ ] Anti-tamper check for CI runs (`synth` should fail if `CI=1` and the workflow has changed)
* [ ] Revise Documentation

Expand Down
73 changes: 65 additions & 8 deletions src/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync } from 'fs';
import * as path from 'path';
import { Stage } from 'aws-cdk-lib';
import { EnvironmentPlaceholders } from 'aws-cdk-lib/cx-api';
import { PipelineBase, PipelineBaseProps, ShellStep, StackAsset, StackDeployment, Step } from 'aws-cdk-lib/pipelines';
import { PipelineBase, PipelineBaseProps, ShellStep, StackAsset, StackDeployment, StackOutputReference, Step } from 'aws-cdk-lib/pipelines';
import { AGraphNode, PipelineGraph, Graph, isGraph } from 'aws-cdk-lib/pipelines/lib/helpers-internal';
import { Construct } from 'constructs';
import * as decamelize from 'decamelize';
Expand Down Expand Up @@ -93,6 +93,7 @@ export class GitHubWorkflow extends PipelineBase {
private readonly buildContainer?: github.ContainerOptions;
private readonly preBuildSteps: github.JobStep[];
private readonly postBuildSteps: github.JobStep[];
private readonly jobOutputs: Record<string, github.JobStepOutput[]> = {};

constructor(scope: Construct, id: string, props: GitHubWorkflowProps) {
super(scope, id, props);
Expand Down Expand Up @@ -158,7 +159,6 @@ export class GitHubWorkflow extends PipelineBase {
}

// convert jobs to a map and make sure there are no duplicates

const jobmap: Record<string, github.Job> = {};
for (const job of jobs) {
if (job.id in jobmap) {
Expand All @@ -167,6 +167,9 @@ export class GitHubWorkflow extends PipelineBase {
jobmap[job.id] = snakeCaseKeys(job.definition);
}

// Update jobs with late-bound output requests
this.insertJobOutputs(jobmap);

const workflow = {
name: this.workflowName,
on: snakeCaseKeys(this.workflowTriggers, '_'),
Expand All @@ -184,6 +187,26 @@ export class GitHubWorkflow extends PipelineBase {
writeFileSync(this.workflowPath, yaml);
}

private insertJobOutputs(jobmap: Record<string, github.Job>) {
for (const [jobId, jobOutputs] of Object.entries(this.jobOutputs)) {
jobmap[jobId] = {
...jobmap[jobId],
outputs: {
...jobmap[jobId].outputs,
...this.renderJobOutputs(jobOutputs),
},
};
}
}

private renderJobOutputs(outputs: github.JobStepOutput[]) {
const renderedOutputs: Record<string, string> = {};
for (const output of outputs) {
renderedOutputs[output.outputName] = `\${{ steps.${output.stepId}.outputs.${output.outputName} }}`;
}
return renderedOutputs;
}

/**
* Make an action from the given node and/or step
*/
Expand Down Expand Up @@ -287,7 +310,6 @@ export class GitHubWorkflow extends PipelineBase {
if (stack.executionRoleArn) {
params['role-arn'] = resolve(stack.executionRoleArn);
}

const assumeRoleArn = stack.assumeRoleArn ? resolve(stack.assumeRoleArn) : undefined;

return {
Expand All @@ -300,6 +322,7 @@ export class GitHubWorkflow extends PipelineBase {
steps: [
...this.stepsToConfigureAws({ region, assumeRoleArn }),
{
id: 'Deploy',
uses: 'aws-actions/aws-cloudformation-github-deploy@v1',
with: params,
},
Expand Down Expand Up @@ -358,10 +381,40 @@ export class GitHubWorkflow extends PipelineBase {
};
}

private jobForScriptStep(node: AGraphNode, step: ShellStep): Job {
/**
* Searches for the stack that produced the output via the current
* job's dependencies.
*
* This function should always find a stack, since it is guaranteed
* that a CfnOutput comes from a referenced stack.
*/
private findStackOfOutput(ref: StackOutputReference, node: AGraphNode) {
for (const dep of node.allDeps) {
if (dep.data?.type === 'execute' && ref.isProducedBy(dep.data.stack)) {
return dep.uniqueId;
}
}
// Should never happen
throw new Error(`The output ${ref.outputName} is not referenced by any of the dependent stacks!`);
}

private addJobOutput(jobId: string, output: github.JobStepOutput) {
if (this.jobOutputs[jobId] === undefined) {
this.jobOutputs[jobId] = [output];
} else {
this.jobOutputs[jobId].push(output);
}
}

if (Object.keys(step.envFromCfnOutputs).length > 0) {
throw new Error('"envFromOutputs" is not supported');
private jobForScriptStep(node: AGraphNode, step: ShellStep): Job {
const envVariables: Record<string, string> = {};
for (const [envName, ref] of Object.entries(step.envFromCfnOutputs)) {
const jobId = this.findStackOfOutput(ref, node);
this.addJobOutput(jobId, {
outputName: ref.outputName,
stepId: 'Deploy',
});
envVariables[envName] = `\${{ needs.${jobId}.outputs.${ref.outputName} }}`;
}

const downloadInputs = new Array<github.JobStep>();
Expand Down Expand Up @@ -401,7 +454,10 @@ export class GitHubWorkflow extends PipelineBase {
},
runsOn: RUNS_ON,
needs: this.renderDependencies(node),
env: step.env,
env: {
...step.env,
...envVariables,
},
steps: [
...downloadInputs,
...installSteps,
Expand Down Expand Up @@ -518,7 +574,8 @@ function snakeCaseKeys<T = unknown>(obj: T, sep = '-'): T {

const result: Record<string, unknown> = {};
for (let [k, v] of Object.entries(obj)) {
if (typeof v === 'object' && v != null) {
// we don't want to snake case environment variables
if (k !== 'env' && typeof v === 'object' && v != null) {
v = snakeCaseKeys(v);
}
result[decamelize(k, { separator: sep })] = v;
Expand Down
2 changes: 1 addition & 1 deletion src/workflows-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export interface Job {
* A map of outputs for a job. Job outputs are available to all downstream
* jobs that depend on this job.
*/
readonly outputs?: Record<string, JobStepOutput>;
readonly outputs?: Record<string, string>;

/**
* A map of environment variables that are available to all steps in the
Expand Down
51 changes: 44 additions & 7 deletions test/__snapshots__/github.test.ts.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 19 additions & 2 deletions test/example-app.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { App, RemovalPolicy, Stack, Stage, StageProps } from 'aws-cdk-lib';
import { App, CfnOutput, RemovalPolicy, Stack, Stage, StageProps } from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { EnvironmentUtils } from 'aws-cdk-lib/cx-api';
Expand Down Expand Up @@ -78,12 +78,25 @@ export class GitHubExampleApp extends App {
],
});

pipeline.addStage(new MyStage(this, 'StageA', { env: EnvironmentUtils.parse(props.envA) }));
const myStage = new MyStage(this, 'StageA', { env: EnvironmentUtils.parse(props.envA) });
pipeline.addStage(myStage, {
pre: [new ShellStep('Pre', {
commands: ['echo hello'],
})],
post: [new ShellStep('Post', {
envFromCfnOutputs: {
FN_NAME: myStage.fnName,
},
commands: ['echo FN_NAME equals: $FN_NAME'],
})],
});

pipeline.addStage(new MyStage(this, 'StageB', { env: EnvironmentUtils.parse(props.envB) }));
}
}

class MyStage extends Stage {
public readonly fnName: CfnOutput;
constructor(scope: App, id: string, props: StageProps) {
super(scope, id, props);

Expand All @@ -104,6 +117,10 @@ class MyStage extends Stage {
},
});

this.fnName = new CfnOutput(fnStack, 'myout', {
value: fn.functionName,
});

bucket.grantRead(fn);
}
}
2 changes: 1 addition & 1 deletion test/manual-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ if (!account) {
const app = new GitHubExampleApp({
repoDir: '.',
envA: `aws://${account}/us-east-1`,
envB: `aws://${account}/eu-west-2`,
envB: `aws://${account}/eu-west-1`,
});

app.synth();