Skip to content

Commit

Permalink
Merge pull request #4039 from alphagov/bk-stats-comment
Browse files Browse the repository at this point in the history
Add stats comment to Pull Requests
  • Loading branch information
domoscargin committed Aug 23, 2023
2 parents c0aa275 + 7ee4596 commit 85d49ed
Show file tree
Hide file tree
Showing 16 changed files with 228 additions and 28 deletions.
1 change: 1 addition & 0 deletions .github/workflows/actions/install-node/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ runs:
key: npm-install-${{ runner.os }}-${{ hashFiles('package-lock.json', '**/package.json') }}
path: |
node_modules
.github/workflows/scripts/node_modules
docs/examples/*/node_modules
packages/*/node_modules
shared/*/node_modules
Expand Down
107 changes: 107 additions & 0 deletions .github/workflows/scripts/comments.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { readFile } from 'node:fs/promises'
import { join } from 'path'

import { getFileSizes } from '@govuk-frontend/lib/files'
import { getStats, modulePaths } from '@govuk-frontend/stats'

/**
* Posts the content of multiple diffs in parallel on the given GitHub issue
Expand Down Expand Up @@ -60,6 +64,58 @@ export async function commentDiff(
}
}

/**
* Generates comment for stats
*
* @param {GithubActionContext} githubActionContext
* @param {number} issueNumber
* @param {DiffComment} statComment
*/
export async function commentStats(
githubActionContext,
issueNumber,
{ path, titleText, markerText }
) {
const { WORKSPACE_DIR = '' } = process.env
const reviewAppURL = getReviewAppUrl(issueNumber)

const distPath = join(WORKSPACE_DIR, 'dist')
const packagePath = join(WORKSPACE_DIR, 'packages/govuk-frontend/dist/govuk')

// File sizes
const fileSizeTitle = '### File sizes'
const fileSizeRows = [
...(await getFileSizes(join(distPath, '**/*.{css,js,mjs}'))),
...(await getFileSizes(join(packagePath, '*.{css,js,mjs}')))
]

const fileSizeHeaders = ['File', 'Size']
const fileSizeTable = renderTable(fileSizeHeaders, fileSizeRows)
const fileSizeText = [fileSizeTitle, fileSizeTable].join('\n')

// Module sizes
const modulesTitle = '### Modules'
const modulesRows = (await Promise.all(modulePaths.map(getStats))).map(
([modulePath, moduleSize]) => {
const statsPath = `docs/stats/${modulePath.replace('mjs', 'html')}`
const statsURL = new URL(statsPath, reviewAppURL)

return [`[${modulePath}](${statsURL})`, moduleSize]
}
)

const modulesHeaders = ['File', 'Size']
const modulesTable = renderTable(modulesHeaders, modulesRows)
const modulesFooter = `[View stats and visualisations on the review app](${reviewAppURL})`
const modulesText = [modulesTitle, modulesTable, modulesFooter].join('\n')

await comment(githubActionContext, issueNumber, {
markerText,
titleText,
bodyText: [fileSizeText, modulesText].join('\n')
})
}

/**
* @param {GithubActionContext} githubContext - GitHub Action context
* @param {number} issueNumber - The number of the issue/PR on which to post the comment
Expand Down Expand Up @@ -131,6 +187,48 @@ function renderCommentFooter({ context, commit }) {
return `[Action run](${githubActionRunUrl(context)}) for ${commit}`
}

/**
* Renders a GitHub Markdown table.
*
* @param {string[]} headers - An array containing the table headers.
* @param {string[][]} rows - An array of arrays containing the row data for the table.
* @returns {string} The GitHub Markdown table as a string.
*/
function renderTable(headers, rows) {
if (!rows.every((row) => row.length === headers.length)) {
throw new Error(
'All rows must have the same number of elements as the headers.'
)
}

/**
* @example
* ```md
* | File | Size |
* ```
*/
const headerRow = `| ${headers.join(' | ')} |`

/**
* @example
* ```md
* | --- | --- |
* ```
*/
const headerSeparator = `| ${Array(headers.length).fill('---').join(' | ')} |`

/**
* @example
* ```md
* | packages/govuk-frontend/dist/example.mjs | 100 KiB |
* ```
*/
const rowStrings = rows.map((row) => `| ${row.join(' | ')} |`)

// Combine headers, header separator, and rows to form the table
return `${[headerRow, headerSeparator, ...rowStrings].join('\n')}\n`
}

/**
* Generates a URL to the GitHub action run
*
Expand All @@ -143,6 +241,15 @@ function githubActionRunUrl(context) {
return `https://github.com/${repo.owner}/${repo.repo}/actions/runs/${runId}/attempts/${process.env.GITHUB_RUN_ATTEMPT}`
}

/**
* @param {number} prNumber - The PR number
* @param {string} path - URL path
* @returns {URL} - The Review App preview URL
*/
function getReviewAppUrl(prNumber, path = '/') {
return new URL(path, `https://govuk-frontend-pr-${prNumber}.herokuapp.com`)
}

/**
* @typedef {object} GithubActionContext
* @property {import('@octokit/rest').Octokit} github - The pre-authenticated Octokit provided by GitHub actions
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@
"node": "^18.12.0",
"npm": "^8.1.0 || ^9.1.0"
},
"license": "MIT"
"license": "MIT",
"dependencies": {
"@govuk-frontend/lib": "*",
"@govuk-frontend/stats": "*"
}
}
41 changes: 41 additions & 0 deletions .github/workflows/stats-comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Stats comment

on:
workflow_call:
workflow_dispatch:

jobs:
generate-stats:
name: Generate stats
runs-on: ubuntu-latest

# Skip when token write permissions are unavailable on forks
if: ${{ !github.event.pull_request.head.repo.fork }}

steps:
- name: Checkout code
uses: actions/checkout@v3.5.3

- name: Restore dependencies
uses: ./.github/workflows/actions/install-node

- name: Build
uses: ./.github/workflows/actions/build

- name: Add comment to PR
uses: actions/github-script@v6.4.1
env:
WORKSPACE_DIR: ${{ github.workspace }}
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const { commentStats } = await import('${{ github.workspace }}/.github/workflows/scripts/comments.mjs')
// PR information
const issueNumber = ${{ github.event.pull_request.number }}
const commit = '${{ github.event.pull_request.head.sha }}'
const options = {
path: '${{ github.workspace }}',
titleText: ':clipboard: Stats',
markerText: 'stats'
}
await commentStats({ github, context, commit }, issueNumber, options)
11 changes: 11 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,14 @@ jobs:
# (after install and build have been cached)
uses: ./.github/workflows/screenshots.yml
secrets: inherit

generate-stats:
name: Stats comment
needs: [install, build]

permissions:
pull-requests: write

# Run existing "Stats comment" workflow
# (after install and build have been cached)
uses: ./.github/workflows/stats-comment.yml
23 changes: 22 additions & 1 deletion package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"@types/gulp": "^4.0.13",
"@types/jest": "^29.5.3",
"@types/jest-axe": "^3.5.5",
"@types/js-yaml": "^4.0.5",
"@types/marked": "^5.0.1",
"@types/node": "^20.5.1",
"@types/nunjucks": "^3.2.3",
Expand Down
7 changes: 1 addition & 6 deletions packages/govuk-frontend-review/src/app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,7 @@ export default async () => {

// Add build stats
app.locals.stats = Object.fromEntries(
await Promise.all(
modulePaths.map(async (modulePath) => [
modulePath,
await getStats(modulePath)
])
)
await Promise.all(modulePaths.map(getStats))
)

// Handle the banner component serverside.
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* Nunjucks filters
*/
export { componentNameToMacroName } from '@govuk-frontend/lib/names'
export { formatNumber } from './format-number.mjs'
export { highlight } from './highlight.mjs'
export { slugify } from './slugify.mjs'
export { unslugify } from './unslugify.mjs'
8 changes: 4 additions & 4 deletions packages/govuk-frontend-review/src/views/index.njk
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
<h2 class="govuk-heading-l">JavaScript modules</h2>

{% set bundleKey = "all" %}
{% set bundleSize = stats[bundleKey + ".mjs"]["total"] %}
{% set bundleSize = stats[bundleKey + ".mjs"] %}

{% set bundleHtml %}
Total bundle size<br>
Expand All @@ -109,13 +109,13 @@
html: bundleHtml
},
value: {
text: (bundleSize / 1000) | round(2) | formatNumber + " KB"
text: bundleSize
}
}] %}

{% for componentName in componentNamesWithJavaScript | sort %}
{% set componentKey = "components/" + componentName + "/" + componentName %}
{% set componentSize = stats[componentKey + ".mjs"]["total"] %}
{% set componentSize = stats[componentKey + ".mjs"] %}

{% set componentHtml %}
{{ componentName | unslugify }}<br>
Expand All @@ -128,7 +128,7 @@
html: componentHtml
},
value: {
text: (componentSize / 1000) | round(2) | formatNumber + " KB"
text: componentSize
}
}), rows) %}
{% endfor %}
Expand Down
28 changes: 27 additions & 1 deletion shared/lib/files.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const { readFile } = require('fs/promises')
const { readFile, stat } = require('fs/promises')
const { parse, relative, basename } = require('path')

const { paths } = require('@govuk-frontend/config')
const { filesize } = require('filesize')
const { glob } = require('glob')
const yaml = require('js-yaml')
const { minimatch } = require('minimatch')
Expand Down Expand Up @@ -45,6 +46,29 @@ async function getDirectories(directoryPath) {
return listing.map((directoryPath) => basename(directoryPath)).sort()
}

/**
* Get file size entries
*
* @param {string} directoryPath - Minimatch pattern to directory
* @param {import('glob').GlobOptionsWithFileTypesUnset} [options] - Glob options
* @returns {Promise<[string, string][]>} - File entries with name and size
*/
async function getFileSizes(directoryPath, options = {}) {
const filesForAnalysis = await getListing(directoryPath, options)
return Promise.all(filesForAnalysis.map(getFileSize))
}

/**
* Get file size entry
*
* @param {string} filePath - File path
* @returns {Promise<[string, string]>} - File entry with name and size
*/
async function getFileSize(filePath) {
const { size } = await stat(filePath)
return [filePath, `${filesize(size, { base: 2 })}`]
}

/**
* Directory listing array filter
* Returns true for files matching every pattern
Expand Down Expand Up @@ -84,6 +108,8 @@ async function getYaml(configPath) {
module.exports = {
filterPath,
getDirectories,
getFileSizes,
getFileSize,
getListing,
getYaml,
mapPathTo
Expand Down
Loading

0 comments on commit 85d49ed

Please sign in to comment.