Cleanup CI

Adds new npm scripts:
- `prepare-release` to prepare release assets
- `timestamp-release` to generate OTS proof
- `generate-release-notes` to automatically generate markdown release
notes for a given range between two tags

Updates CI to use the npm scripts internally.
This commit is contained in:
Luke Childs 2023-06-30 22:52:10 +07:00 committed by GitHub
parent 090c5b8e04
commit cb20f682e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 378 additions and 33 deletions

View File

@ -12,19 +12,37 @@ jobs:
run:
working-directory: ./server
steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- run: docker buildx create --use
- run: npm ci
- run: npm run build
# TODO Clean this up into a release script
- run: cd build && mv umbreld-amd64 umbreld && tar -czvf umbreld-${{ github.ref_name }}-amd64.tar.gz umbreld && rm umbreld
- run: cd build && mv umbreld-arm64 umbreld && tar -czvf umbreld-${{ github.ref_name }}-arm64.tar.gz umbreld && rm umbreld
- run: cd ../ && git archive --format=tar.gz --output server/build/umbrel-${{ github.ref_name }}.tar.gz --prefix=umbrel-${{ github.ref_name }}/ ${{ github.ref_name }}
- run: sha256sum build/* > build/SHA256SUMS
- run: npx opentimestamps stamp build/SHA256SUMS || true # Don't fail the release if timestamping fails
# TODO Generate release notes
- uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15
- name: Checkout codebase
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
# We need this to get all commit history and tags to generate release notes
fetch-depth: 0
- name: Setup Docker buildx
run: docker buildx create --use
- name: Install dependencies
run: npm ci
- name: Build release
run: npm run build
- name: Prepare release assets
run: npm run prepare-release -- ${{ github.ref_name }}
- name: Timestamp release
continue-on-error: true
run: npm run timestamp-release
- name: Generate release notes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run generate-release-notes
- name: Create GitHub Release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15
with:
draft: true
name: umbrelOS ${{ github.ref_name }}
files: server/build/*
files: server/release/*
body_path: server/release-notes.md

8
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "umbrel",
"lockfileVersion": 3,
"requires": true,
"packages": {}
"name": "umbrel",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@ -6,4 +6,4 @@
"dev:restart": "npm run dev:stop && npm run dev:start",
"dev:shell": "multipass shell umbrel-dev"
}
}
}

View File

@ -35,7 +35,7 @@ else
fi
echo "Detected architecture: ${binary_arch}"
binary_source_location="${UPDATE_ROOT}/server/build/umbreld-${binary_arch}"
binary_source_location="${UPDATE_ROOT}/server/build/linux_${binary_arch}/umbreld"
binary_destination_location="${UMBREL_ROOT}/bin/umbreld"
# Download umbreld release binary if we don't have a local dev build

2
server/.gitignore vendored
View File

@ -1,2 +1,4 @@
node_modules
build
release
release-notes.md

View File

@ -1,6 +1,5 @@
#!/usr/bin/env node
import path from 'node:path'
import os from 'node:os'
import caxa from 'caxa'
import fse from 'fs-extra'
@ -11,28 +10,19 @@ const $$ = $({stdio: 'inherit'})
const BUILD_DIRECTORY = 'build'
async function runMultiArchDockerBuilds(architectures) {
let buildDirectory = BUILD_DIRECTORY
if (architectures.length === 1) buildDirectory += `/linux_${architectures[0]}`
const platforms = architectures.map(architecture => `linux/${architecture}`).join(',')
await $$`docker buildx build --platform ${platforms} --output ${BUILD_DIRECTORY} .`
// Clean up Docker's platform-specific build directories
for (const buildSubDirectory of await fse.readdir(BUILD_DIRECTORY)) {
const platformBuildPath = `${BUILD_DIRECTORY}/${buildSubDirectory}`
const isPlatformBuild = buildSubDirectory.startsWith('linux_') && (await fse.stat(platformBuildPath)).isDirectory()
if (!isPlatformBuild) continue
for (const file of await fse.readdir(platformBuildPath)) {
await fse.move(`${platformBuildPath}/${file}`, `${BUILD_DIRECTORY}/${file}`, { overwrite: true })
}
await fse.remove(platformBuildPath)
}
await $$`docker buildx build --platform ${platforms} --output ${buildDirectory} .`
}
async function buildBinary() {
const {bin} = await fse.readJson('package.json')
const entrypoint = path.join('{{caxa}}', bin)
const architecture = os.arch() === 'x64' ? 'amd64' : os.arch()
await caxa({
input: '.',
exclude: [BUILD_DIRECTORY],
output: `${BUILD_DIRECTORY}/umbreld-${architecture}`,
output: `${BUILD_DIRECTORY}/umbreld`,
command: [
"{{caxa}}/node_modules/.bin/node",
entrypoint,

View File

@ -0,0 +1,87 @@
#!/usr/bin/env node
import {$} from 'execa'
import fse from 'fs-extra'
import got from 'got'
const githubUsernames = {
'Luke Childs <lukechilds123@gmail.com>': '@lukechilds',
'Nathan Fretz <nmfretz@gmail.com>': '@nmfretz',
'Mayank Chhabra <mayankchhabra9@gmail.com>': '@mayankchhabra',
}
async function lookupUsername(author) {
// If the username starts with @ we probably already have a GitHub username
if (author.startsWith('@')) return author.split(' ')[0]
// If we already havfe the username cached, return it
if (githubUsernames[author]) return githubUsernames[author]
// Attempt to lookup the GitHub username from the commit details
try {
console.log(`Looking up ${author}`)
const data = await got(`https://api.github.com/search/users?q=${author}`, {
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`
},
}).json()
const username = data?.items?.[0]?.login
if (username) {
// Cache for next time
githubUsernames[author] = `@${username}`
return githubUsernames[author]
}
} catch (error) {
console.log(error.message)
// If anything goes wrong just fall back to commit credentials
}
// If we didn't find anything, cache the original commit details to save additional failing lookups
// Also log the failure to help us find it so we can manually update
githubUsernames[author] = author
console.log('Failed')
// Return the original commit details if we didn't find anything
return author
}
async function main() {
let range = process.argv[2]
if (!range) {
const gitTag = await $`git tag --sort=-creatordate`
const tags = gitTag.stdout.split('\n')
range = `${tags[1]}...${tags[0]}`
}
const markdown = []
markdown.push('## Changes')
markdown.push('')
markdown.push(`https://github.com/getumbrel/umbrel/compare/${range}`)
markdown.push('')
const format = '===COMMIT_DELIMITER===%n%h%n%s%n%an <%ae>%n%b'
const log = await $`git log --pretty=format:${format} ${range}`
const commits = log.stdout.split('===COMMIT_DELIMITER===\n')
for (const commit of commits) {
const [hash, title, author, ...body] = commit.split('\n')
if (!hash) continue
const coAuthors = body
.filter(line => line.startsWith('Co-authored-by:'))
.map(line => line.replace('Co-authored-by: ', '').trim())
const authors = []
for (const person of [author, ...coAuthors]) {
authors.push(await lookupUsername(person))
}
markdown.push(`- ${title} (${hash}) ${authors.join(' ')}`)
}
console.log('')
console.log(markdown.join('\n'))
await fse.writeFile('release-notes.md', markdown.join('\n'))
}
await main()

213
server/package-lock.json generated
View File

@ -26,6 +26,7 @@
},
"devDependencies": {
"caxa": "^3.0.1",
"got": "^13.0.0",
"opentimestamps": "^0.4.9"
}
},
@ -61,6 +62,36 @@
"node": ">= 8"
}
},
"node_modules/@sindresorhus/is": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.4.1.tgz",
"integrity": "sha512-axlrvsHlHlFmKKMEg4VyvMzFr93JWJj4eIfXY1STVuO2fsImCa7ncaiG5gC8HKOX590AW5RtRsC41/B+OfrSqw==",
"dev": true,
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@szmarczak/http-timer": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
"integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
"dev": true,
"dependencies": {
"defer-to-connect": "^2.0.1"
},
"engines": {
"node": ">=14.16"
}
},
"node_modules/@types/http-cache-semantics": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==",
"dev": true
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
@ -473,6 +504,45 @@
"node": ">= 0.8"
}
},
"node_modules/cacheable-lookup": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
"integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==",
"dev": true,
"engines": {
"node": ">=14.16"
}
},
"node_modules/cacheable-request": {
"version": "10.2.12",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.12.tgz",
"integrity": "sha512-qtWGB5kn2OLjx47pYUkWicyOpK1vy9XZhq8yRTXOy+KAmjjESSRLx6SiExnnaGGUP1NM6/vmygMu0fGylNh9tw==",
"dev": true,
"dependencies": {
"@types/http-cache-semantics": "^4.0.1",
"get-stream": "^6.0.1",
"http-cache-semantics": "^4.1.1",
"keyv": "^4.5.2",
"mimic-response": "^4.0.0",
"normalize-url": "^8.0.0",
"responselike": "^3.0.0"
},
"engines": {
"node": ">=14.16"
}
},
"node_modules/cacheable-request/node_modules/mimic-response": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
"integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==",
"dev": true,
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@ -767,6 +837,15 @@
"node": ">=4.0.0"
}
},
"node_modules/defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -1131,6 +1210,15 @@
"node": ">= 0.12"
}
},
"node_modules/form-data-encoder": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz",
"integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==",
"dev": true,
"engines": {
"node": ">= 14.17"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -1270,6 +1358,31 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/got": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz",
"integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==",
"dev": true,
"dependencies": {
"@sindresorhus/is": "^5.2.0",
"@szmarczak/http-timer": "^5.0.1",
"cacheable-lookup": "^7.0.0",
"cacheable-request": "^10.2.8",
"decompress-response": "^6.0.0",
"form-data-encoder": "^2.1.2",
"get-stream": "^6.0.1",
"http2-wrapper": "^2.1.10",
"lowercase-keys": "^3.0.0",
"p-cancelable": "^3.0.0",
"responselike": "^3.0.0"
},
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sindresorhus/got?sponsor=1"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@ -1352,6 +1465,12 @@
"minimalistic-crypto-utils": "^1.0.1"
}
},
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@ -1382,6 +1501,19 @@
"npm": ">=1.3.7"
}
},
"node_modules/http2-wrapper": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz",
"integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==",
"dev": true,
"dependencies": {
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.2.0"
},
"engines": {
"node": ">=10.19.0"
}
},
"node_modules/human-signals": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
@ -1540,6 +1672,12 @@
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
"dev": true
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true
},
"node_modules/json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
@ -1623,6 +1761,15 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/keyv": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz",
"integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==",
"dev": true,
"dependencies": {
"json-buffer": "3.0.1"
}
},
"node_modules/lazystream": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
@ -1700,6 +1847,18 @@
"node": ">=0.6"
}
},
"node_modules/lowercase-keys": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
"integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==",
"dev": true,
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@ -1910,6 +2069,18 @@
"node": ">=0.10.0"
}
},
"node_modules/normalize-url": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz",
"integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==",
"dev": true,
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/npm-run-path": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz",
@ -2021,6 +2192,15 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"node_modules/p-cancelable": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
"integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==",
"dev": true,
"engines": {
"node": ">=12.20"
}
},
"node_modules/p-retry": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-5.1.2.tgz",
@ -2209,6 +2389,18 @@
}
]
},
"node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -2393,6 +2585,27 @@
"node": ">=0.6"
}
},
"node_modules/resolve-alpn": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
"dev": true
},
"node_modules/responselike": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz",
"integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==",
"dev": true,
"dependencies": {
"lowercase-keys": "^3.0.0"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/retry": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",

View File

@ -6,6 +6,9 @@
"bin": "./index.js",
"scripts": {
"start": "node index.js",
"prepare-release": "node prepare-release.js",
"timestamp-release": "ots-cli.js stamp release/SHA256SUMS",
"generate-release-notes": "node generate-release-notes.js",
"build": "node build.js"
},
"dependencies": {
@ -24,6 +27,7 @@
},
"devDependencies": {
"caxa": "^3.0.1",
"got": "^13.0.0",
"opentimestamps": "^0.4.9"
}
}

31
server/prepare-release.js Executable file
View File

@ -0,0 +1,31 @@
#!/usr/bin/env node
import fse from 'fs-extra'
import {$} from 'execa'
const BUILD_DIRECTORY = 'build'
const RELEASE_DIRECTORY = 'release'
async function main() {
const release = process.argv[2]
if (!release) throw new Error('Release argument is required.')
console.log('Cleaning release directory...')
await fse.remove(RELEASE_DIRECTORY)
await fse.ensureDir(RELEASE_DIRECTORY)
console.log('Preparing release assets...')
await Promise.all([
$`tar -czvf ${RELEASE_DIRECTORY}/umbreld-${release}-amd64.tar.gz -C ${BUILD_DIRECTORY}/linux_amd64 umbreld`,
$`tar -czvf ${RELEASE_DIRECTORY}/umbreld-${release}-arm64.tar.gz -C ${BUILD_DIRECTORY}/linux_arm64 umbreld`,
$`git archive --format=tar.gz --output ${RELEASE_DIRECTORY}/umbrel-${release}.tar.gz --prefix=umbrel-${release}/ HEAD`,
])
console.log('Generating checksums...')
const releaseAssets = await fse.readdir(RELEASE_DIRECTORY)
const checksums = await $({cwd: RELEASE_DIRECTORY})`sha256sum ${releaseAssets}`
console.log(checksums.stdout)
await fse.writeFile(`${RELEASE_DIRECTORY}/SHA256SUMS`, checksums.stdout)
}
await main()