Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
johnny-human committed Feb 27, 2023
2 parents 97797b7 + b8fa0ae commit 59199eb
Show file tree
Hide file tree
Showing 11 changed files with 2,471 additions and 907 deletions.
767 changes: 445 additions & 322 deletions dist/index.js

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions jest.config.js
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
}
1,619 changes: 1,546 additions & 73 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"@actions/core": "1.10.0",
"aws-sdk": "2.1237.0",
"p-limit": "^3.1.0",
"short-unique-id": "^4.4.4"
"short-unique-id": "^4.4.4",
"cdk-assets": "^2.62.2"
},
"devDependencies": {
"@types/jest": "29.2.0",
Expand Down
28 changes: 28 additions & 0 deletions src/assets.ts
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)
}
}
268 changes: 110 additions & 158 deletions src/deploy.ts
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
}
Loading

0 comments on commit 59199eb

Please sign in to comment.