Skip to content
This repository was archived by the owner on Feb 1, 2025. It is now read-only.

Commit 909552f

Browse files
author
true
committed
feat(deployment): CLI command for deploying applications without the need to define own cdk
1 parent da6969c commit 909552f

13 files changed

+1565
-225
lines changed

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,8 @@ cdk.out/**
1313

1414
cdk.json
1515
!cdk/cdk.json
16+
17+
.yarn*
18+
.yarn/**
19+
20+
**/cdk.out

cdk/example.ts cdk/app.ts

+17-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env node
22
import 'source-map-support/register'
3-
3+
import * as path from 'path'
44
import { HttpApi } from '@aws-cdk/aws-apigatewayv2-alpha'
55
import { HttpLambdaIntegration } from '@aws-cdk/aws-apigatewayv2-integrations-alpha'
66
import { App, CfnOutput, Duration, RemovalPolicy, Stack, StackProps, SymlinkFollowMode } from 'aws-cdk-lib'
@@ -12,20 +12,25 @@ import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment'
1212

1313
const app = new App()
1414

15+
const commandCwd = process.cwd()
16+
const cdkFolder = __dirname
17+
18+
console.log({ commandCwd, cdkFolder })
19+
1520
class NextStandaloneStack extends Stack {
1621
constructor(scope: App, id: string, props?: StackProps) {
1722
super(scope, id, props)
1823

1924
const config = {
20-
assetsZipPath: './next.out/assetsLayer.zip',
21-
codeZipPath: './next.out/code.zip',
22-
dependenciesZipPath: './next.out/dependenciesLayer.zip',
25+
assetsZipPath: path.resolve(commandCwd, './next.out/assetsLayer.zip'),
26+
codeZipPath: path.resolve(commandCwd, './next.out/code.zip'),
27+
dependenciesZipPath: path.resolve(commandCwd, './next.out/dependenciesLayer.zip'),
28+
sharpLayerZipPath: path.resolve(cdkFolder, '../dist/sharp-layer.zip'),
29+
nextLayerZipPath: path.resolve(cdkFolder, '../dist/next-layer.zip'),
30+
imageHandlerZipPath: path.resolve(cdkFolder, '../dist/image-handler.zip'),
2331
customServerHandler: 'handler.handler',
2432
customImageHandler: 'index.handler',
2533
cfnViewerCertificate: undefined,
26-
sharpLayerZipPath: './dist/sharp-layer.zip',
27-
nextLayerZipPath: './dist/next-layer.zip',
28-
imageHandlerZipPath: './dist/image-handler.zip',
2934
...props,
3035
}
3136

@@ -164,6 +169,10 @@ class NextStandaloneStack extends Stack {
164169
}
165170
}
166171

167-
new NextStandaloneStack(app, 'StandaloneNextjsStack-Temporary')
172+
if (!process.env.STACK_NAME) {
173+
throw new Error('Name of CDK stack was not specified!')
174+
}
175+
176+
new NextStandaloneStack(app, process.env.STACK_NAME)
168177

169178
app.synth()

cdk/cdk.json

-4
This file was deleted.

lib/cli.ts

+17-209
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import { Command } from 'commander'
2-
import { mkdirSync, rmSync, symlinkSync, writeFileSync } from 'fs'
3-
import { tmpdir } from 'os'
42
import path from 'path'
5-
import { simpleGit } from 'simple-git'
63
import packageJson from '../package.json'
7-
import { skipCiFlag } from './consts'
8-
import { bumpCalculator, bumpMapping, BumpType, findInFile, isValidTag, replaceVersionInCommonFiles, zipFolder, zipMultipleFoldersOrFiles } from './utils'
4+
import { deployHandler } from './cli/deploy'
5+
import { guessHandler } from './cli/guess'
6+
import { packHandler } from './cli/pack'
7+
import { shipitHandler } from './cli/shipit'
8+
import { wrapProcess } from './utils'
99

1010
const commandCwd = process.cwd()
11-
const nextServerConfigRegex = /(?<=conf: )(.*)(?=,)/
12-
const scriptDir = path.dirname(__filename)
13-
1411
const program = new Command()
1512

1613
program
@@ -35,7 +32,7 @@ program
3532
.option(
3633
'--handlerPath',
3734
'Path to custom handler to be used to handle ApiGw events. By default this is provided for you.',
38-
path.resolve(scriptDir, './server-handler.js'),
35+
path.resolve(path.dirname(__filename), './server-handler.js'),
3936
)
4037
.option(
4138
'--outputFolder',
@@ -44,103 +41,8 @@ program
4441
)
4542
.action(async (options) => {
4643
const { standaloneFolder, publicFolder, handlerPath, outputFolder } = options
47-
48-
// @TODO: Validate that output folder exists.
49-
// @TODO: Validate server.js exists and we can match data.
50-
// @TODO: Validate that public folder is using `assets` subfolder.
51-
52-
// Dependencies layer configuration
53-
const nodeModulesFolderPath = path.resolve(standaloneFolder, './node_modules')
54-
const depsLambdaFolder = 'nodejs/node_modules'
55-
const lambdaNodeModulesPath = path.resolve('/opt', depsLambdaFolder)
56-
const dependenciesOutputPath = path.resolve(outputFolder, 'dependenciesLayer.zip')
57-
58-
// Code layer configuration
59-
const generatedNextServerPath = path.resolve(standaloneFolder, './server.js')
60-
const codeOutputPath = path.resolve(outputFolder, 'code.zip')
61-
62-
// Assets bundle configuration
63-
const buildIdPath = path.resolve(commandCwd, './.next/BUILD_ID')
64-
const generatedStaticContentPath = path.resolve(commandCwd, '.next/static')
65-
const generatedStaticRemapping = '_next/static'
66-
const assetsOutputPath = path.resolve(outputFolder, 'assetsLayer.zip')
67-
68-
// Clean output directory before continuing
69-
rmSync(outputFolder, { force: true, recursive: true })
70-
mkdirSync(outputFolder)
71-
72-
// Zip dependencies from standalone output in a layer-compatible format.
73-
await zipFolder({
74-
outputName: dependenciesOutputPath,
75-
folderPath: nodeModulesFolderPath,
76-
dir: depsLambdaFolder,
77-
})
78-
79-
// Zip staticly generated assets and public folder.
80-
await zipMultipleFoldersOrFiles({
81-
outputName: assetsOutputPath,
82-
inputDefinition: [
83-
{
84-
isFile: true,
85-
name: 'BUILD_ID',
86-
path: buildIdPath,
87-
},
88-
{
89-
path: publicFolder,
90-
},
91-
{
92-
path: generatedStaticContentPath,
93-
dir: generatedStaticRemapping,
94-
},
95-
],
96-
})
97-
98-
// Create a symlink for node_modules so they point to the separately packaged layer.
99-
// We need to create symlink because we are not using NodejsFunction in CDK as bundling is custom.
100-
const tmpFolder = tmpdir()
101-
102-
const symlinkPath = path.resolve(tmpFolder, `./node_modules_${Math.random()}`)
103-
symlinkSync(lambdaNodeModulesPath, symlinkPath)
104-
105-
const nextConfig = findInFile(generatedNextServerPath, nextServerConfigRegex)
106-
const configPath = path.resolve(tmpFolder, `./config.json_${Math.random()}`)
107-
writeFileSync(configPath, nextConfig, 'utf-8')
108-
109-
// Zip codebase including symlinked node_modules and handler.
110-
await zipMultipleFoldersOrFiles({
111-
outputName: codeOutputPath,
112-
inputDefinition: [
113-
{
114-
isGlob: true,
115-
cwd: standaloneFolder,
116-
path: '**/*',
117-
ignore: ['**/node_modules/**', '*.zip'],
118-
},
119-
{
120-
// Ensure hidden files are included.
121-
isGlob: true,
122-
cwd: standaloneFolder,
123-
path: '.*/**/*',
124-
},
125-
{
126-
isFile: true,
127-
path: handlerPath,
128-
name: 'handler.js',
129-
},
130-
{
131-
isFile: true,
132-
path: symlinkPath,
133-
name: 'node_modules',
134-
},
135-
{
136-
isFile: true,
137-
path: configPath,
138-
name: 'config.json',
139-
},
140-
],
141-
})
142-
143-
console.log('Your NextJS project was succefully prepared for Lambda.')
44+
console.log('Our config is: ', options)
45+
wrapProcess(packHandler({ commandCwd, handlerPath, outputFolder, publicFolder, standaloneFolder }))
14446
})
14547

14648
program
@@ -151,20 +53,8 @@ program
15153
.option('-t, --tagPrefix <prefix>', 'Prefix version with string of your choice', 'v')
15254
.action(async (commitMessage, latestVersion, options) => {
15355
const { tagPrefix } = options
154-
155-
if (!isValidTag(latestVersion, tagPrefix)) {
156-
throw new Error(`Invalid version found - ${latestVersion}!`)
157-
}
158-
159-
const match = bumpMapping.find(({ test }) => commitMessage.match(test))
160-
if (!match) {
161-
throw new Error('No mapping for for suplied commit message.')
162-
}
163-
164-
const nextTag = bumpCalculator(latestVersion.replace(tagPrefix, ''), match?.bump)
165-
const nextTagWithPrefix = tagPrefix + nextTag
166-
167-
console.log(nextTagWithPrefix)
56+
console.log('Our config is: ', options)
57+
wrapProcess(guessHandler({ commitMessage, latestVersion, tagPrefix }))
16858
})
16959

17060
program
@@ -179,102 +69,20 @@ program
17969
.option('--gitEmail <email>', 'User email to be used for commits.', '[email protected]')
18070
.action(async (options) => {
18171
const { tagPrefix, failOnMissingCommit, releaseBranchPrefix, forceBump, gitUser, gitEmail } = options
182-
18372
console.log('Our config is: ', options)
184-
185-
const git = simpleGit()
186-
187-
git.addConfig('user.name', gitUser)
188-
git.addConfig('user.email', gitEmail)
189-
190-
const tags = await git.tags()
191-
const log = await git.log()
192-
const branch = await git.branch()
193-
const [remote] = await git.getRemotes()
194-
195-
const latestCommit = log.latest?.hash
196-
const latestTag = tags.latest ?? '0.0.0'
197-
const currentTag = latestTag.replace(tagPrefix, '')
198-
199-
console.log('Current version: ', latestTag)
200-
201-
if (!isValidTag(latestTag, tagPrefix)) {
202-
throw new Error(`Invalid tag found - ${latestTag}!`)
203-
}
204-
205-
if (!latestCommit) {
206-
throw new Error('Latest commit was not found!')
207-
}
208-
209-
const commits = await git.log({
210-
from: tags.latest,
211-
to: latestCommit,
212-
})
213-
214-
if (commits.total < 1 && failOnMissingCommit) {
215-
throw new Error('No new commits since last tag.')
216-
}
217-
218-
const bumps: BumpType[] = []
219-
220-
commits.all.forEach(({ message, body }) => {
221-
const match = bumpMapping.find(({ test, scanBody }) => (scanBody ? body : message).match(test))
222-
if (!match) {
223-
console.warn(`Invalid commit, cannot match bump - ${message}!`)
224-
} else {
225-
bumps.push(match?.bump)
226-
}
227-
})
228-
229-
console.log('Bumps: ', bumps)
230-
231-
// Bump minor in case nothing is found.
232-
if (bumps.length < 1 && forceBump) {
233-
console.log('Forcing patch bump!')
234-
bumps.push(BumpType.Patch)
235-
}
236-
237-
const nextTag = bumps.reduce((acc, curr) => bumpCalculator(acc, curr), currentTag)
238-
const nextTagWithPrefix = tagPrefix + nextTag
239-
const releaseBranch = `${releaseBranchPrefix}${nextTagWithPrefix}`
240-
console.log(`Next version is - ${nextTagWithPrefix}!`)
241-
242-
if (currentTag === nextTag) {
243-
throw new Error('Failed to bump version!')
244-
}
245-
246-
const replacementResults = replaceVersionInCommonFiles(currentTag, nextTag)
247-
console.log(`Replaced version in files.`, replacementResults)
248-
249-
// Commit changed files (versions) and create a release commit with skip ci flag.
250-
await git
251-
//
252-
.add('./*')
253-
.raw('commit', '--message', `Release: ${nextTagWithPrefix} ${skipCiFlag}`)
254-
// Create tag and push it to master.
255-
.addTag(nextTagWithPrefix)
256-
257-
git.push(remote.name, branch.current)
258-
git.pushTags()
259-
260-
// As current branch commit includes skip ci flag, we want to ommit this flag for release branch so pipeline can run (omitting infinite loop).
261-
// So we are overwriting last commit message and pushing to release branch.
262-
await git
263-
//
264-
.raw('commit', '--message', `Release: ${nextTagWithPrefix}`, '--amend')
265-
.push(remote.name, `${branch.current}:${releaseBranch}`)
266-
267-
// @Note: CI/CD should not be listening for tags in master, it should listen to release branch.
268-
// @TODO: Include commits and commit bodies in release commit so Jira can pick it up.
269-
270-
console.log(`Successfuly tagged and created new branch - ${releaseBranch}`)
73+
wrapProcess(shipitHandler({ tagPrefix, gitEmail, gitUser, failOnMissingCommit, forceBump, releaseBranchPrefix }))
27174
})
27275

27376
program
27477
.command('deploy')
27578
.description('Deploy Next application via CDK')
79+
.option('--stackName <name>', 'Name of the stack to be deployed.', 'StandaloneNextjsStack-Temporary')
80+
.option('--tsconfigPath <path>', 'Absolute path to config.', path.resolve(__dirname, '../tsconfig.json'))
81+
.option('--appPath <path>', 'Absolute path to app.', path.resolve(__dirname, '../cdk/app.ts'))
27682
.action(async (options) => {
277-
// @TODO: Add support for CLI CDK deployments via provided CDK example.
83+
const { stackName, appPath, tsconfigPath } = options
84+
console.log('Our config is: ', options)
85+
wrapProcess(deployHandler({ stackName, appPath, tsconfigPath }))
27886
})
27987

28088
program.parse(process.argv)

lib/cli/deploy.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { executeAsyncCmd } from '../utils'
2+
3+
interface Props {
4+
stackName: string
5+
tsconfigPath: string
6+
appPath: string
7+
}
8+
9+
const cdkExecutable = require.resolve('aws-cdk/bin/cdk')
10+
11+
export const deployHandler = async ({ stackName, tsconfigPath, appPath }: Props) => {
12+
// Using SWC as it's way faster.
13+
// NPX so ts-node does not need to be a dependency.
14+
// All paths are absolute.
15+
const cdkApp = `npx ts-node --swc --project ${tsconfigPath} ${appPath}`
16+
17+
await executeAsyncCmd({
18+
cmd: `STACK_NAME=${stackName} ${cdkExecutable} deploy --app "${cdkApp}"`,
19+
})
20+
}

lib/cli/guess.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { bumpCalculator, bumpMapping, isValidTag } from '../utils'
2+
3+
interface Props {
4+
tagPrefix: string
5+
latestVersion: string
6+
commitMessage: string
7+
}
8+
9+
export const guessHandler = async ({ latestVersion, tagPrefix, commitMessage }: Props) => {
10+
if (!isValidTag(latestVersion, tagPrefix)) {
11+
throw new Error(`Invalid version found - ${latestVersion}!`)
12+
}
13+
14+
const match = bumpMapping.find(({ test }) => commitMessage.match(test))
15+
if (!match) {
16+
throw new Error('No mapping for for suplied commit message.')
17+
}
18+
19+
const nextTag = bumpCalculator(latestVersion.replace(tagPrefix, ''), match?.bump)
20+
const nextTagWithPrefix = tagPrefix + nextTag
21+
22+
console.log(nextTagWithPrefix)
23+
}

0 commit comments

Comments
 (0)