-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' of https://github.com/johnny-human/cdk-github-deploy
- Loading branch information
Showing
11 changed files
with
2,471 additions
and
907 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,11 @@ | ||
module.exports = { | ||
clearMocks: true, | ||
moduleFileExtensions: ["js", "ts"], | ||
testEnvironment: "node", | ||
testMatch: ["**/*.test.ts"], | ||
testRunner: "jest-circus/runner", | ||
transform: { | ||
"^.+\\.ts$": "ts-jest" | ||
}, | ||
verbose: true | ||
}; | ||
clearMocks: true, | ||
moduleFileExtensions: ['js', 'ts'], | ||
testEnvironment: 'node', | ||
testMatch: ['**/*.test.ts'], | ||
testRunner: 'jest-circus/runner', | ||
transform: { | ||
'^.+\\.ts$': 'ts-jest' | ||
}, | ||
verbose: true | ||
} |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import * as core from '@actions/core' | ||
import { runCommand } from './utils' | ||
|
||
export type AssetsConfiguration = { | ||
/** | ||
* Stacks where assets should be published from | ||
*/ | ||
stacks: Array<string> | ||
} | ||
|
||
export const assets = async (config: AssetsConfiguration) => { | ||
const AWS_ACCESS_KEY_ID = `AWS_ACCESS_KEY_ID='${process.env['AWS_ACCESS_KEY_ID']}'` | ||
const AWS_SECRET_ACCESS_KEY = `AWS_SECRET_ACCESS_KEY='${process.env['AWS_SECRET_ACCESS_KEY']}'` | ||
const AWS_REGION = `AWS_REGION='${process.env['AWS_REGION']}'` | ||
|
||
try { | ||
config.stacks.map(async (_: any, i: number) => { | ||
const assetFilePath = `${config.stacks[i]}.assets.json` | ||
|
||
const result = await runCommand( | ||
`${AWS_ACCESS_KEY_ID} ${AWS_SECRET_ACCESS_KEY} ${AWS_REGION} node node_modules/cdk-assets/bin/cdk-assets publish -p cdk.out/${assetFilePath}` | ||
) | ||
core.debug(result) | ||
}) | ||
} catch (error) { | ||
core.error(error as string) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,187 +1,139 @@ | ||
import * as core from '@actions/core' | ||
import * as aws from 'aws-sdk' | ||
import { CreateChangeSetInput, CreateStackInput } from './main' | ||
import ShortUniqueId from 'short-unique-id' | ||
|
||
const uid = new ShortUniqueId() | ||
|
||
export type Stack = aws.CloudFormation.Stack | ||
|
||
export async function cleanupChangeSet( | ||
cfn: aws.CloudFormation, | ||
stack: Stack, | ||
params: CreateChangeSetInput, | ||
noEmptyChangeSet?: boolean, | ||
import pLimit from 'p-limit' | ||
import * as fs from 'fs' | ||
import * as path from 'path' | ||
import { parseParameters, pickOption, runCommand } from './utils' | ||
import { deployStack, getStackOutputs } from './deployStack' | ||
|
||
export type Configuration = { | ||
capabilities?: string | ||
roleARN?: string | ||
disableRollback?: boolean | ||
timeoutInMinutes?: number | ||
tags?: aws.CloudFormation.Tags | ||
terminationProtection?: boolean | ||
parameterOverrides?: string | ||
noEmptyChangeSet?: boolean | ||
noExecuteChangeSet?: boolean | ||
noDeleteFailedChangeSet?: boolean | ||
): Promise<string | undefined> { | ||
const knownErrorMessages = [ | ||
`No updates are to be performed`, | ||
`The submitted information didn't contain changes` | ||
] | ||
|
||
const changeSetStatus = await cfn | ||
.describeChangeSet({ | ||
ChangeSetName: params.ChangeSetName, | ||
StackName: params.StackName | ||
}) | ||
.promise() | ||
|
||
if (changeSetStatus.Status === 'FAILED') { | ||
core.debug(`${stack.StackName}: Deleting failed Change Set`) | ||
|
||
if (noDeleteFailedChangeSet === false) { | ||
await cfn | ||
.deleteChangeSet({ | ||
ChangeSetName: params.ChangeSetName, | ||
StackName: params.StackName | ||
}) | ||
.promise() | ||
} | ||
} | ||
|
||
if ( | ||
noEmptyChangeSet && | ||
knownErrorMessages.some(err => | ||
changeSetStatus.StatusReason?.includes(err) | ||
) | ||
) { | ||
return stack.StackId | ||
} | ||
export type DeployConfig = { | ||
stacks: Array<string> | ||
concurrency?: number | ||
} | ||
|
||
throw new Error( | ||
`Failed to create Change Set: ${changeSetStatus.StatusReason}` | ||
) | ||
} | ||
export type TaskConfig = { | ||
stack: string | ||
} | ||
|
||
export async function updateStack( | ||
cfn: aws.CloudFormation, | ||
stack: Stack, | ||
params: CreateChangeSetInput, | ||
noEmptyChangeSet?: boolean, | ||
noExecuteChangeSet?: boolean, | ||
noDeleteFailedChangeSet?: boolean | ||
): Promise<string | undefined> { | ||
core.debug(`${stack.StackName}: Creating CloudFormation Change Set`) | ||
await cfn.createChangeSet(params).promise() | ||
// The custom client configuration for the CloudFormation clients. | ||
const clientConfiguration = { | ||
customUserAgent: 'aws-cloudformation-github-deploy-for-github-actions' | ||
} | ||
|
||
export const deploy = async (config: Configuration & DeployConfig) => { | ||
const { | ||
concurrency, | ||
stacks, | ||
capabilities, | ||
roleARN, | ||
disableRollback, | ||
timeoutInMinutes, | ||
tags, | ||
terminationProtection, | ||
parameterOverrides, | ||
noEmptyChangeSet, | ||
noExecuteChangeSet, | ||
noDeleteFailedChangeSet | ||
} = config | ||
try { | ||
core.debug( | ||
`${stack.StackName}: Waiting for CloudFormation Change Set creation` | ||
) | ||
await cfn | ||
.waitFor('changeSetCreateComplete', { | ||
ChangeSetName: params.ChangeSetName, | ||
StackName: params.StackName | ||
}) | ||
.promise() | ||
} catch (_) { | ||
return cleanupChangeSet( | ||
cfn, | ||
stack, | ||
params, | ||
noEmptyChangeSet, | ||
noDeleteFailedChangeSet | ||
const cfn = new aws.CloudFormation({ ...clientConfiguration }) | ||
const limit = pLimit(concurrency || 5) | ||
|
||
const tasks = config.stacks.map((_: any, i: number) => | ||
limit(() => | ||
task(cfn, { | ||
stack: stacks[i], | ||
capabilities, | ||
roleARN, | ||
disableRollback, | ||
timeoutInMinutes, | ||
tags, | ||
terminationProtection, | ||
parameterOverrides, | ||
noEmptyChangeSet, | ||
noExecuteChangeSet, | ||
noDeleteFailedChangeSet | ||
}).catch((err: any) => { | ||
core.error(`${stacks[i]}: Error`) | ||
throw err | ||
}) | ||
) | ||
) | ||
} | ||
|
||
if (noExecuteChangeSet === true) { | ||
core.debug(`${stack.StackName}: Not executing the change set`) | ||
return stack.StackId | ||
await Promise.all(tasks) | ||
} catch (err) { | ||
if (err instanceof Error || typeof err === 'string') { | ||
core.setFailed(err) | ||
// @ts-ignore | ||
core.debug(err.stack) | ||
} | ||
} | ||
} | ||
|
||
core.debug(`${stack.StackName}: Executing CloudFormation change set`) | ||
await cfn | ||
.executeChangeSet({ | ||
ChangeSetName: params.ChangeSetName, | ||
StackName: params.StackName | ||
}) | ||
.promise() | ||
const getTemplateBody = (stack: string) => { | ||
const { GITHUB_WORKSPACE = __dirname } = process.env | ||
|
||
core.debug(`${stack.StackName}: Updating CloudFormation stack`) | ||
await cfn | ||
.waitFor('stackUpdateComplete', { StackName: stack.StackId }) | ||
.promise() | ||
core.debug(`${stack}: Loading Stack template`) | ||
|
||
return stack.StackId | ||
} | ||
const file = `cdk.out/${stack}.template.json` | ||
const filePath = path.isAbsolute(file) | ||
? file | ||
: path.join(GITHUB_WORKSPACE, file) | ||
|
||
async function getStack( | ||
cfn: aws.CloudFormation, | ||
stackNameOrId: string | ||
): Promise<Stack | undefined> { | ||
try { | ||
const stacks = await cfn | ||
.describeStacks({ | ||
StackName: stackNameOrId | ||
}) | ||
.promise() | ||
return stacks.Stacks?.[0] | ||
} catch (e) { | ||
if (e instanceof Error && e.message.match(/does not exist/)) { | ||
return undefined | ||
} | ||
throw e | ||
} | ||
return fs.readFileSync(filePath, 'utf8') | ||
} | ||
|
||
export async function deployStack( | ||
const task = async ( | ||
cfn: aws.CloudFormation, | ||
params: CreateStackInput, | ||
noEmptyChangeSet?: boolean, | ||
noExecuteChangeSet?: boolean, | ||
noDeleteFailedChangeSet?: boolean | ||
): Promise<string | undefined> { | ||
const stack = await getStack(cfn, params.StackName) | ||
|
||
if (!stack) { | ||
core.debug(`${params.StackName}: Creating CloudFormation Stack`) | ||
config: Configuration & TaskConfig | ||
): Promise<void> => { | ||
// CloudFormation Stack Parameter for the creation or update | ||
const params: aws.CloudFormation.Types.CreateStackInput = { | ||
StackName: config.stack, | ||
TemplateBody: getTemplateBody(config.stack), | ||
EnableTerminationProtection: config.terminationProtection, | ||
RoleARN: config.roleARN, | ||
DisableRollback: config.disableRollback, | ||
TimeoutInMinutes: config.timeoutInMinutes, | ||
Tags: config.tags | ||
} | ||
|
||
const stack = await cfn.createStack(params).promise() | ||
await cfn | ||
.waitFor('stackCreateComplete', { StackName: params.StackName }) | ||
.promise() | ||
if (config.capabilities) { | ||
params.Capabilities = [ | ||
...config.capabilities.split(',').map(cap => cap.trim()) | ||
] | ||
} | ||
|
||
return stack.StackId | ||
if (config.parameterOverrides) { | ||
params.Parameters = parseParameters(config.parameterOverrides.trim()) | ||
} | ||
|
||
return await updateStack( | ||
const stackId = await deployStack( | ||
cfn, | ||
stack, | ||
{ | ||
ChangeSetName: `${params.StackName}-${uid.seq()}`, | ||
...{ | ||
StackName: params.StackName, | ||
TemplateBody: params.TemplateBody, | ||
TemplateURL: params.TemplateURL, | ||
Parameters: params.Parameters, | ||
Capabilities: params.Capabilities, | ||
ResourceTypes: params.ResourceTypes, | ||
RoleARN: params.RoleARN, | ||
RollbackConfiguration: params.RollbackConfiguration, | ||
NotificationARNs: params.NotificationARNs, | ||
Tags: params.Tags | ||
} | ||
}, | ||
noEmptyChangeSet, | ||
noExecuteChangeSet, | ||
noDeleteFailedChangeSet | ||
params, | ||
config.noEmptyChangeSet, | ||
config.noExecuteChangeSet, | ||
config.noDeleteFailedChangeSet | ||
) | ||
} | ||
core.setOutput(`${config.stack}-stack-id`, stackId || 'UNKNOWN') | ||
|
||
export async function getStackOutputs( | ||
cfn: aws.CloudFormation, | ||
stackId: string | ||
): Promise<Map<string, string>> { | ||
const outputs = new Map<string, string>() | ||
const stack = await getStack(cfn, stackId) | ||
|
||
if (stack && stack.Outputs) { | ||
for (const output of stack.Outputs) { | ||
if (output.OutputKey && output.OutputValue) { | ||
outputs.set(output.OutputKey, output.OutputValue) | ||
} | ||
if (stackId) { | ||
const outputs = await getStackOutputs(cfn, stackId) | ||
for (const [logicalId, value] of outputs) { | ||
core.setOutput(`${config.stack}_output_${logicalId}`, value) | ||
} | ||
} | ||
|
||
return outputs | ||
} |
Oops, something went wrong.