Skip to content

Commit

Permalink
Add script create-release-archive to prepare releases
Browse files Browse the repository at this point in the history
We want to edit the package.json file before making a release to remove
all the stuff that users don't need. This commit adds a Node script to
achieve this.
  • Loading branch information
lfdebrux committed Jun 16, 2022
1 parent 8241478 commit 0325cc7
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
usage-data-config.json
# ignore extension SCSS because it's dynamically generated
lib/extensions/_extensions.scss
# ignore any release archives
/govuk-prototype-kit*.zip
.env
.sass-cache
.DS_Store
Expand Down
9 changes: 9 additions & 0 deletions scripts/__snapshots__/create-release-archive.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`scripts/create-release-archive cleanPackageJson deletes all dev scripts 1`] = `
Object {
"build": "node lib/build/generate-assets",
"serve": "node listen-on-port",
"start": "node start",
}
`;
164 changes: 164 additions & 0 deletions scripts/create-release-archive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
const child_process = require('child_process') // eslint-disable-line camelcase
const fs = require('fs')
const os = require('os')
const path = require('path')

const packageJsonScriptsInclude = [
'start',
'build',
'serve'
]

function parseArgs (args) {
const argv = { _: [] }
for (const arg of args) {
if (arg === '-h' || arg === '--help') {
argv.help = true
} else {
argv._.push(arg)
}
}
return argv
}

function cli () {
const argv = parseArgs(process.argv.slice(2))

if (argv.help || argv._.length > 1) {
console.log(`
Usage:
scripts/create-release-archive [REF]
`)
process.exitCode = 0
return
}

const ref = argv.ref || 'HEAD'
const version = getReleaseVersion(argv.ref)
const newVersion = isNewVersion(version) ? 'new version' : 'version'

console.log(`Creating release archive for ${newVersion} ${version}`)

const name = `govuk-prototype-kit-${version}`
const releaseArchive = `${name}.zip`

const repoDir = path.resolve(__dirname, '..')
const workdir = fs.mkdtempSync(
path.join(os.tmpdir(), `govuk-prototype-kit--release-${version}`)
)

console.log(workdir)

copyReleaseFiles(repoDir, workdir, { prefix: name, ref: ref })

// Make the changes we want to make
// Currently just removing dev stuff from package.json
console.log('Updating package.json')
updatePackageJson(path.join(workdir, name, 'package.json'), cleanPackageJson)

// Create the release archive in the project root
zipReleaseFiles({
cwd: workdir, file: path.join(repoDir, releaseArchive), prefix: name
})

console.log(`Saved release archive to ${releaseArchive}`)

// Clean up
fs.rmSync(workdir, { force: true, recursive: true })
}

function updatePackageJson (file, updater) {
let pkg
pkg = JSON.parse(fs.readFileSync(file, { encoding: 'utf8' }))
pkg = updater(pkg)
fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + os.EOL, { encoding: 'utf8' })
// update package-lock.json to match
child_process.execSync('npm install', { cwd: path.dirname(file), encoding: 'utf8', stdio: 'inherit' })
}

function cleanPackageJson (pkg) {
// remove dev dependencies
delete pkg.devDependencies

// remove config for dev dependencies
delete pkg.jest
delete pkg.standard

// remove dev scripts
pkg.scripts = Object.fromEntries(
Object.entries(pkg.scripts)
.filter(([name]) => packageJsonScriptsInclude.includes(name))
)

return pkg
}

function getReleaseVersion (ref) {
if (!ref) {
const packageVersion = require('../package').version
if (isNewVersion(packageVersion)) {
return packageVersion
}
}

ref = ref || 'HEAD'

const versionString = child_process.execSync(`git describe --tags ${ref}`, { encoding: 'utf8' }).trim()
const version = versionString.slice(1) // drop the initial 'v'

return version
}

function isNewVersion (version) {
return !!child_process.spawnSync(
'git', ['rev-parse', `v${version}`]
).status
}

function copyReleaseFiles (src, dest, { prefix, ref }) {
// We are currently using the export-ignore directives in .gitattributes to
// decide which files to include in the release archive, so the easiest way
// to copy all the release files is `git archive`
child_process.execSync(
`git archive --format=tar --prefix="${prefix}/" ${ref} | tar -C ${dest} -xf -`,
{ cwd: src }
)
}

function zipReleaseFiles ({ cwd, file, prefix }) {
zipCreate(
{
cwd: cwd,
file: file,
exclude: path.join(prefix, 'node_modules', '*')
},
prefix
)
}

function zipCreate ({ cwd, file, exclude }, files) {
files = Array.isArray(files) ? files : [files]

const zipArgs = ['--exclude', exclude, '-r', file, ...files]
const ret = child_process.spawnSync(
'zip', zipArgs,
{ cwd: cwd, encoding: 'utf8', stdio: 'inherit' }
)
if (ret.status !== 0) {
throw `zip \\\n\t${zipArgs.join(' \\\n\t')}\n:Failed with status ${ret.status}` // eslint-disable-line
}
// insert a blank line for niceness
console.log()
}

// These exports are here only for tests
module.exports = {
cleanPackageJson,
updatePackageJson,
getReleaseVersion,
zipReleaseFiles
}

if (require.main === module) {
cli()
}
108 changes: 108 additions & 0 deletions scripts/create-release-archive.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/* eslint-env jest */

const child_process = require('child_process') // eslint-disable-line camelcase
const fs = require('fs')
const path = require('path')

const createReleaseArchive = require('./create-release-archive')

const repoDir = path.join(__dirname, '..')

describe('scripts/create-release-archive', () => {
describe('getReleaseVersion', () => {
it('tells us the version number of the release described by a ref', () => {
expect(
createReleaseArchive.getReleaseVersion('v12.1.0')
).toEqual('12.1.0')
})

it.todo('defaults to getting the version from package.json')
})

describe('cleanPackageJson', () => {
let packageJson

beforeEach(() => {
packageJson = JSON.parse(
fs.readFileSync(path.join(repoDir, 'package.json'), { encoding: 'utf8' })
)
})

it('deletes dev dependencies', () => {
expect(packageJson).toHaveProperty('devDependencies')
packageJson = createReleaseArchive.cleanPackageJson(packageJson)
expect(packageJson).not.toHaveProperty('devDependencies')
})

it('deletes configuration for dev tools', () => {
packageJson = createReleaseArchive.cleanPackageJson(packageJson)
expect(packageJson).not.toHaveProperty('jest')
expect(packageJson).not.toHaveProperty('standard')
})

it('deletes all dev scripts', () => {
expect(packageJson).toHaveProperty('scripts')
packageJson = createReleaseArchive.cleanPackageJson(packageJson)
expect(packageJson).not.toHaveProperty('scripts.lint')
expect(packageJson).not.toHaveProperty('scripts.test')
expect(packageJson.scripts).toMatchSnapshot()
})
})

describe('updatePackageJson', () => {
let fakeFs
let mockExecSync

beforeEach(() => {
fakeFs = {}
fakeFs['package.json'] = fs.readFileSync(path.join(repoDir, 'package.json'), { encoding: 'utf8' })
jest.spyOn(fs, 'readFileSync').mockImplementation((path) => fakeFs[path])
jest.spyOn(fs, 'writeFileSync').mockImplementation((path, data) => { fakeFs[path] = data })

mockExecSync = jest.spyOn(child_process, 'execSync').mockImplementation(() => {})
})

afterEach(() => {
jest.restoreAllMocks()
})

it('updates a package.json file using the function updater', () => {
createReleaseArchive.updatePackageJson('package.json', () => { return {} })
expect(fakeFs['package.json']).toMatch('{}')
})

it('preserves the formatting of the package.json file', () => {
const before = fakeFs['package.json']
createReleaseArchive.updatePackageJson('package.json', (x) => x)
expect(fakeFs['package.json']).toMatch(before)
})

it('runs npm install after changing the file', () => {
fakeFs['test/package.json'] = fakeFs['package.json']
createReleaseArchive.updatePackageJson('test/package.json', () => { return {} })

expect(mockExecSync).toHaveBeenCalledWith(
'npm install', expect.objectContaining({ cwd: 'test' })
)
})
})

describe('zipReleaseFiles', () => {
let mockSpawnSync

beforeEach(() => {
mockSpawnSync = jest.spyOn(child_process, 'spawnSync').mockImplementation(() => ({ status: 0 }))
})

afterEach(() => {
jest.restoreAllMocks()
})

it('zips release files', () => {
createReleaseArchive.zipReleaseFiles({ cwd: '/tmp', file: '/test.zip', prefix: 'test' })
expect(mockSpawnSync).toBeCalledWith(
'zip', ['--exclude', 'test/node_modules/*', '-r', '/test.zip', 'test'], expect.objectContaining({ cwd: '/tmp' })
)
})
})
})

0 comments on commit 0325cc7

Please sign in to comment.