Skip to content

Commit dd7ee41

Browse files
authored
Merge pull request #41 from hashicorp/brk.feat/reusable-workflow
feat: reusable workflow
2 parents 47f9c9e + c10dcba commit dd7ee41

File tree

6 files changed

+437
-365
lines changed

6 files changed

+437
-365
lines changed

.github/workflows/analyze.yml

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Copyright (c) HashiCorp, Inc.
2+
# SPDX-License-Identifier: MPL-2.0
3+
4+
name: 'Next.js Bundle Analysis'
5+
6+
on:
7+
workflow_call:
8+
inputs:
9+
node-version:
10+
description: 'The node version to use for the workflow.'
11+
required: false
12+
type: string
13+
default: '18'
14+
package-manager:
15+
description: 'Your preferred package manager. (npm, yarn, pnpm)'
16+
required: false
17+
type: string
18+
default: 'npm'
19+
working-directory:
20+
description: 'The directory your Next.js app lives in.'
21+
required: false
22+
type: string
23+
default: ./
24+
build-build-output-directory:
25+
description: 'The distDir specified in your Next.js config'
26+
required: false
27+
type: string
28+
default: '.next'
29+
build-command:
30+
description: 'If your app uses a custom build command, override this. Defaults to `next build`.'
31+
required: false
32+
type: string
33+
default: ./node_modules/.bin/next build
34+
35+
jobs:
36+
analyze:
37+
runs-on: ubuntu-latest
38+
permissions:
39+
contents: read # for checkout repository
40+
actions: read # for fetching base branch bundle stats
41+
pull-requests: write # for comments
42+
defaults:
43+
run:
44+
# change this if your nextjs app does not live at the root of the repo
45+
working-directory: ${{ inputs.working-directory }}
46+
steps:
47+
- uses: actions/checkout@v3
48+
49+
### Dependency installation ###
50+
- name: Install Node.js
51+
uses: actions/setup-node@v3
52+
with:
53+
node-version: ${{ inputs.node-version }}
54+
55+
# based on the package-manager input it will use npm, yarn, or pnpm
56+
- name: Install dependencies
57+
if: inputs.package-manager == 'npm' || inputs.package-manager == 'yarn'
58+
uses: bahmutov/npm-install@v1
59+
60+
- name: Install dependencies (pnpm)
61+
if: inputs.package-manager == 'pnpm'
62+
uses: pnpm/action-setup@v2
63+
id: pnpm-install
64+
with:
65+
version: 7
66+
run_install: true
67+
68+
### Next build ###
69+
- name: Restore Next build
70+
uses: actions/cache@v3
71+
id: restore-build-cache
72+
env:
73+
cache-name: cache-next-build
74+
with:
75+
# if you use a custom build directory, replace all instances of `.next` in this file with your build directory
76+
# ex: if your app builds to `dist`, replace `.next` with `dist`
77+
path: ${{ inputs.working-directory }}${{ inputs.build-output-directory }}/cache
78+
# change this if you prefer a more strict cache
79+
key: ${{ runner.os }}-build-${{ env.cache-name }}
80+
81+
- name: Build Next app
82+
run: ${{ inputs.build-command }}
83+
84+
### Bundle analysis ###
85+
# Here's the first place where next-bundle-analysis' own script is used
86+
# This step pulls the raw bundle stats for the current bundle
87+
- name: Analyze bundle
88+
run: npx -p nextjs-bundle-analysis report
89+
90+
- name: Upload bundle
91+
uses: actions/upload-artifact@v3
92+
with:
93+
name: bundle
94+
path: ${{ inputs.working-directory }}${{ inputs.build-output-directory }}/analyze/__bundle_analysis.json
95+
96+
- name: Download base branch bundle stats
97+
uses: dawidd6/action-download-artifact@v2
98+
if: success() && github.event.number
99+
with:
100+
workflow: nextjs_bundle_analysis.yml
101+
branch: ${{ github.event.pull_request.base.ref }}
102+
path: ${{ inputs.working-directory }}${{ inputs.build-output-directory }}/analyze/base
103+
104+
# And here's the second place - this runs after we have both the current and
105+
# base branch bundle stats, and will compare them to determine what changed.
106+
# There are two configurable arguments that come from package.json:
107+
#
108+
# - budget: optional, set a budget (bytes) against which size changes are measured
109+
# it's set to 350kb here by default, as informed by the following piece:
110+
# https://infrequently.org/2021/03/the-performance-inequality-gap/
111+
#
112+
# - red-status-percentage: sets the percent size increase where you get a red
113+
# status indicator, defaults to 20%
114+
#
115+
# Either of these arguments can be changed or removed by editing the `nextBundleAnalysis`
116+
# entry in your package.json file.
117+
- name: Compare with base branch bundle
118+
if: success() && github.event.number
119+
run: ls -laR ${{ inputs.working-directory }}${{ inputs.build-output-directory }}/analyze/base && npx -p nextjs-bundle-analysis compare
120+
121+
### PR commenting ###
122+
- name: Get Comment Body
123+
id: get-comment-body
124+
if: success() && github.event.number
125+
# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
126+
run: |
127+
echo "body<<EOF" >> $GITHUB_OUTPUT
128+
echo "$(cat ${{ inputs.working-directory }}${{ inputs.build-output-directory }}/analyze/__bundle_analysis_comment.txt)" >> $GITHUB_OUTPUT
129+
echo EOF >> $GITHUB_OUTPUT
130+
131+
- name: Find Comment
132+
uses: peter-evans/find-comment@v2
133+
if: success() && github.event.number
134+
id: fc
135+
with:
136+
issue-number: ${{ github.event.number }}
137+
body-includes: '<!-- __NEXTJS_BUNDLE -->'
138+
139+
- name: Create Comment
140+
uses: peter-evans/create-or-update-comment@v2
141+
if: success() && github.event.number && steps.fc.outputs.comment-id == 0
142+
with:
143+
issue-number: ${{ github.event.number }}
144+
body: ${{ steps.get-comment-body.outputs.body }}
145+
146+
- name: Update Comment
147+
uses: peter-evans/create-or-update-comment@v2
148+
if: success() && github.event.number && steps.fc.outputs.comment-id != 0
149+
with:
150+
issue-number: ${{ github.event.number }}
151+
body: ${{ steps.get-comment-body.outputs.body }}
152+
comment-id: ${{ steps.fc.outputs.comment-id }}
153+
edit-mode: replace

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@ Analyzes each PR's impact on your next.js app's bundle size and displays it usin
66

77
## Installation
88

9-
It's pretty simple to get this set up, just run the following command and answer the prompts. The command will create a `.github/workflows` directory in your project root and add a `next_bundle_analysis.yml` file to it - that's all it takes!
9+
It's pretty simple to get this set up. Run the following command and answer the prompts. The command will create a `.github/workflows` directory in your project root and add a `next_bundle_analysis.yml` file to it - that's all it takes!
1010

1111
```sh
1212
$ npx -p nextjs-bundle-analysis generate
1313
```
1414

15-
> **NOTE**: Due to github actions' lack of support for more complex actions, the experience of getting this set up is unusual in that it requires a generation script which copies most of the logic into your project directly. As soon as github adds support for the [features](https://github.com/actions/runner/pull/1144) [needed](https://github.com/actions/runner/pull/1144#discussion_r651087316) to properly package up this action, we'll put out an update that removes the extra boilerplate and makes usage much simpler. Until then, we all have no choice but to endure this unusual setup process.
16-
1715
## Configuration
1816

1917
Config values are written to `package.json` under the key `nextBundleAnalysis`, and can be changed there any time. You can directly edit the workflow file if you want to adjust your default branch or the directory that your nextjs app lives in (especially if you are using a `srcDir` or something similar).
@@ -29,7 +27,7 @@ Config values are written to `package.json` under the key `nextBundleAnalysis`,
2927
For example, if you build to `dist`, you should:
3028

3129
- Set `package.json.nextBundleAnalysis.buildOutputDirectory` to `"dist"`.
32-
- In `nextjs_bundle_analysis`, replace all instances of `.next` with `dist`.
30+
- In `next_bundle_analysis.yml`, update the `build-output-directory` input to `dist`.
3331

3432
### `budget (number)`
3533

generate.mjs

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Copyright (c) HashiCorp, Inc.
4+
* SPDX-License-Identifier: MPL-2.0
5+
*/
6+
import {
7+
intro,
8+
outro,
9+
confirm,
10+
select,
11+
spinner,
12+
isCancel,
13+
cancel,
14+
text,
15+
group,
16+
note,
17+
} from '@clack/prompts'
18+
import color from 'picocolors'
19+
import path from 'node:path'
20+
import fs from 'node:fs'
21+
import mkdirp from 'mkdirp'
22+
23+
const WORKFLOW_TEMPLATE_FILE = 'template.yml'
24+
const WORKFLOW_FILE = 'next_bundle_analysis.yml'
25+
26+
const DEFAULT_PACKAGE_CONFIG = {
27+
budget: 350,
28+
budgetPercentIncreaseRed: 20,
29+
minimumChangeThreshold: 0,
30+
buildOutputDirectory: '.next',
31+
showDetails: true,
32+
}
33+
34+
const DEFAULT_WORKFLOW_CONFIG = {
35+
baseBranch: 'main',
36+
nodeVersion: 18,
37+
packageManager: 'npm',
38+
workingDirectory: './',
39+
buildCommand: './node_modules/.bin/next build',
40+
buildOutputDirectory: DEFAULT_PACKAGE_CONFIG.buildOutputDirectory,
41+
}
42+
43+
async function number(opts) {
44+
const result = await text(opts)
45+
return Number.parseInt(result, 10)
46+
}
47+
48+
async function writePackageJsonConfig(config) {
49+
// write the config values to package.json
50+
const packageJsonPath = path.join(process.cwd(), 'package.json')
51+
const packageJsonContent = JSON.parse(
52+
fs.readFileSync(packageJsonPath, 'utf-8')
53+
)
54+
packageJsonContent.nextBundleAnalysis = {
55+
...DEFAULT_PACKAGE_CONFIG,
56+
...config,
57+
}
58+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContent, null, 2))
59+
}
60+
61+
async function writeWorkflowFile(config) {
62+
const packageJsonPath = new URL('package.json', import.meta.url)
63+
const packageJsonContent = JSON.parse(
64+
fs.readFileSync(packageJsonPath, 'utf-8')
65+
)
66+
const templatePath = new URL(WORKFLOW_TEMPLATE_FILE, import.meta.url)
67+
const workflowsPath = path.join(process.cwd(), '.github/workflows')
68+
const workflowFilePath = path.join(workflowsPath, WORKFLOW_FILE)
69+
70+
let template = fs.readFileSync(templatePath, 'utf-8')
71+
72+
// Specify the latest version
73+
template = template.replace('{PACKAGE_VERSION}', packageJsonContent.version)
74+
75+
// mkdir -p the .workflows directory
76+
mkdirp.sync(workflowsPath)
77+
78+
const areInputsChangedFromDefault = !Object.keys(
79+
DEFAULT_WORKFLOW_CONFIG
80+
).every((key) => DEFAULT_WORKFLOW_CONFIG[key] === config[key])
81+
82+
if (areInputsChangedFromDefault) {
83+
// update the base branch
84+
template = template.replace('- main', `- ${config.baseBranch}`)
85+
86+
// Update the inputs
87+
template = template
88+
.replace('# with:', 'with:')
89+
.replace(/#\s+node-version:.+$/m, ` node-version: ${config.nodeVersion}`)
90+
.replace(
91+
/#\s+package-manager:.+$/m,
92+
` package-manager: ${config.packageManager}`
93+
)
94+
.replace(
95+
/#\s+working-directory:.+$/m,
96+
` working-directory: ${config.workingDirectory}`
97+
)
98+
.replace(
99+
/#\s+build-output-directory:.+$/m,
100+
` build-output-directory: ${config.buildOutputDirectory}`
101+
)
102+
.replace(
103+
/#\s+build-command:.+$/m,
104+
` build-command: ${config.buildCommand}`
105+
)
106+
}
107+
108+
fs.writeFileSync(workflowFilePath, template)
109+
}
110+
111+
async function main() {
112+
console.log('\n', color.inverse(color.bold(' nextjs-bundle-analysis ')), '\n')
113+
114+
intro(color.inverse(' configuration '))
115+
116+
const packageConfig = await group({
117+
budget: async () => {
118+
const setBudget = await confirm({
119+
message: 'Would you like to set a performance budget?',
120+
})
121+
122+
if (setBudget) {
123+
return (
124+
(await number({
125+
message: `What would you like the maximum javascript on first load to be (in kb)? (default: ${DEFAULT_PACKAGE_CONFIG.budget})`,
126+
defaultValue: DEFAULT_PACKAGE_CONFIG.budget,
127+
})) * 1024
128+
)
129+
}
130+
131+
return DEFAULT_PACKAGE_CONFIG.budget
132+
},
133+
budgetPercentIncreaseRed: () =>
134+
number({
135+
message: `If you exceed this percentage of the budget or filesize, it will be highlighted in red (default: ${DEFAULT_PACKAGE_CONFIG.budgetPercentIncreaseRed})`,
136+
defaultValue: DEFAULT_PACKAGE_CONFIG.budgetPercentIncreaseRed,
137+
}),
138+
minimumChangeThreshold: () =>
139+
number({
140+
message: `If a page's size change is below this threshold (in bytes), it will be considered unchanged (default: ${DEFAULT_PACKAGE_CONFIG.minimumChangeThreshold})`,
141+
defaultValue: DEFAULT_PACKAGE_CONFIG.minimumChangeThreshold,
142+
}),
143+
buildOutputDirectory: () =>
144+
text({
145+
message: `Do you have a custom dist directory? (default: ${DEFAULT_PACKAGE_CONFIG.buildOutputDirectory})`,
146+
defaultValue: DEFAULT_PACKAGE_CONFIG.buildOutputDirectory,
147+
}),
148+
})
149+
150+
await writePackageJsonConfig(packageConfig)
151+
152+
outro('✅ Bundle analysis config written to package.json')
153+
154+
intro(color.inverse(' workflow file '))
155+
156+
const workflowConfig = await group({
157+
baseBranch: () =>
158+
text({
159+
message: `What's your base branch? (default: ${DEFAULT_WORKFLOW_CONFIG.baseBranch})`,
160+
defaultValue: DEFAULT_WORKFLOW_CONFIG.baseBranch,
161+
}),
162+
nodeVersion: () =>
163+
text({
164+
message: `What node version are you using? (default: ${DEFAULT_WORKFLOW_CONFIG.nodeVersion})`,
165+
defaultValue: DEFAULT_WORKFLOW_CONFIG.nodeVersion,
166+
}),
167+
packageManager: () =>
168+
select({
169+
message: 'What package manager do you use?',
170+
options: [
171+
{ value: 'npm', label: 'npm', hint: 'default' },
172+
{ value: 'yarn', label: 'yarn' },
173+
{ value: 'pnpm', label: 'pnpm' },
174+
],
175+
defaultValue: DEFAULT_WORKFLOW_CONFIG.nodeVersion,
176+
}),
177+
workingDirectory: () =>
178+
text({
179+
message: `What directory does your app live in? (default: ${DEFAULT_WORKFLOW_CONFIG.workingDirectory})`,
180+
defaultValue: DEFAULT_WORKFLOW_CONFIG.workingDirectory,
181+
}),
182+
buildCommand: () =>
183+
text({
184+
message: "What's your build command? (default: next build)",
185+
defaultValue: DEFAULT_WORKFLOW_CONFIG.buildCommand,
186+
}),
187+
})
188+
189+
await writeWorkflowFile({
190+
...workflowConfig,
191+
buildOutputDirectory: packageConfig.buildOutputDirectory,
192+
})
193+
194+
outro(
195+
'✅ Workflow file written to .github/workflows/next-js-bundle-analysis.yml'
196+
)
197+
}
198+
199+
main()

0 commit comments

Comments
 (0)