Commit e8be244d authored by Robert Knight's avatar Robert Knight

Semi-automatically publish a client release from Jenkins

When performing a Jenkins CI build of the "master" branch, prompt the
user to deploy and then after they approve, execute the same release
process that a developer currently executes on their machine, namely:

 1. Update the changelog
 2. Run "yarn version" to update the package version, create a tag and GitHub release.

After these steps the release process continues on Jenkins as before,
with an npm package published and the build deployed to an S3-backed CDN
using the s3-npm-publish tool.

Enabling publishing the release entirely from Jenkins required some
additional changes:

 - Use a Debian Stretch-based Node container, to get a newer Git
   version which supports "taggerdate", used by the changelog scripts.
 - Disable Git tag signing, since this is not set up on Jenkins.
 - Do not run "make test" from the preversion script, since this has
   already been run by Jenkins earlier in the process.

Additionally to facilitate testing of this branch I added support for
specifying a prerelease version suffix. This made it possible to test
the whole deployment process without updating the live version of the
client. In future this will likely come in useful for doing staging/QA
releases.
parent 8be6683e
...@@ -3,8 +3,32 @@ ...@@ -3,8 +3,32 @@
node { node {
checkout scm checkout scm
nodeEnv = docker.image("node:10") nodeEnv = docker.image("node:10-stretch")
workspace = pwd() workspace = pwd()
// Tag used when deploying to NPM.
npmTag = "latest"
// Git branch which releases are deployed from.
releaseFromBranch = "master"
// Pre-release suffix added to new package version number when deploying.
// If this is empty, the new deployed version will become the live version.
//
// Note that once an npm package has been published with a given version,
// it is *not* possible to overwrite that version in future (eg. you cannot
// publish "v1.1-testing" twice).
versionSuffix = ""
if (versionSuffix != "") {
npmTag = "prerelease"
}
pkgName = sh (
script: 'cat package.json | jq -r .name',
returnStdout: true
).trim()
pkgVersion = sh ( pkgVersion = sh (
script: 'cat package.json | jq -r .version', script: 'cat package.json | jq -r .version',
returnStdout: true returnStdout: true
...@@ -12,7 +36,7 @@ node { ...@@ -12,7 +36,7 @@ node {
stage('Build') { stage('Build') {
nodeEnv.inside("-e HOME=${workspace}") { nodeEnv.inside("-e HOME=${workspace}") {
sh "echo Building Hypothesis client \"${pkgVersion}\"" sh "echo Building Hypothesis client"
sh 'make clean' sh 'make clean'
sh 'make' sh 'make'
} }
...@@ -24,36 +48,77 @@ node { ...@@ -24,36 +48,77 @@ node {
} }
} }
if (isTag()) { if (env.BRANCH_NAME != releaseFromBranch) {
stage('Publish') { echo "Skipping deployment because this is not the ${releaseFromBranch} branch"
nodeEnv.inside("-e HOME=${workspace}") { return
withCredentials([ }
[$class: 'StringBinding', credentialsId: 'npm-token', variable: 'NPM_TOKEN']]) {
stage('Publish') {
// Use `npm` rather than `yarn` for publishing. input(message: "Publish new client release?")
// See https://github.com/yarnpkg/yarn/pull/3391.
sh "echo '//registry.npmjs.org/:_authToken=${env.NPM_TOKEN}' >> \$HOME/.npmrc"
sh "npm publish"
}
}
// Upload the contents of the package to an S3 bucket, which it newPkgVersion = bumpMinorVersion(pkgVersion)
// will then be served from. if (versionSuffix != "") {
docker.image('nickstenning/s3-npm-publish') newPkgVersion = newPkgVersion + "-" + versionSuffix
.withRun('', "hypothesis@${pkgVersion} s3://cdn.hypothes.is") { c ->
sh "docker logs --follow ${c.id}"
}
} }
echo "Publishing ${pkgName} v${newPkgVersion} from ${releaseFromBranch} branch."
nodeEnv.inside("-e HOME=${workspace} -e BRANCH_NAME=${env.BRANCH_NAME}") {
withCredentials([
string(credentialsId: 'npm-token', variable: 'NPM_TOKEN'),
usernamePassword(credentialsId: 'github-jenkins-user',
passwordVariable: 'GITHUB_TOKEN',
usernameVariable: 'GITHUB_USERNAME')]) {
// Configure commit author for version bump commit and auth credentials
// for pushing tag to GitHub.
//
// See https://git-scm.com/docs/git-credential-store
sh """
git config user.email ${env.GITHUB_USERNAME}@hypothes.is
git config user.name ${env.GITHUB_USERNAME}
git config credential.helper store
echo https://${env.GITHUB_USERNAME}:${env.GITHUB_TOKEN}@github.com >> \$HOME/.git-credentials
"""
// Fetch information about tags so that changelog generation script
// can produce diff since last tag. Also remove local tags that no
// longer exist on the remote.
//
// The `--prune-tags` option is not supported in Git 2.11 so we
// use the workaround from https://github.com/git/git/commit/97716d217c1ea00adfc64e4f6bb85c1236d661ff
sh "git fetch --quiet --prune origin 'refs/tags/*:refs/tags/*' "
// Bump the package version, update the changelog and create the tag
// and GitHub release.
sh "yarn version --new-version ${newPkgVersion}"
// Publish the updated package to the npm registry.
// Use `npm` rather than `yarn` for publishing.
// See https://github.com/yarnpkg/yarn/pull/3391.
sh "echo '//registry.npmjs.org/:_authToken=${env.NPM_TOKEN}' >> \$HOME/.npmrc"
sh "npm publish --tag ${npmTag}"
}
}
echo "Uploading package ${pkgName} v${newPkgVersion} to CDN"
// Upload the contents of the package to an S3 bucket, which it
// will then be served from.
docker.image('nickstenning/s3-npm-publish')
.withRun('', "${pkgName}@${newPkgVersion} s3://cdn.hypothes.is") { c ->
sh "docker logs --follow ${c.id}"
}
} }
} }
boolean isTag() { // Increment the minor part of a `MAJOR.MINOR.PATCH` semver version.
try { String bumpMinorVersion(String version) {
sh 'git fetch --tags' def parts = version.tokenize('.')
sh 'git describe --exact-match --tags' if (parts.size() != 3) {
return true throw new IllegalArgumentException("${version} is not a valid MAJOR.MINOR.PATCH version")
} catch (Exception e) {
echo e.toString()
return false
} }
def newMinorVersion = parts[1].toInteger() + 1
return "${parts[0]}.${newMinorVersion}.${parts[2]}"
} }
...@@ -4,7 +4,9 @@ set -eu ...@@ -4,7 +4,9 @@ set -eu
cd "$(dirname "$0")" cd "$(dirname "$0")"
git push https://github.com/hypothesis/client.git master:master --follow-tags # nb. The remote refname is fully qualified because this script is run in a CI
# environment where not all heads may have been fetched.
git push https://github.com/hypothesis/client.git HEAD:refs/heads/$BRANCH_NAME --follow-tags
# Wait a moment to give GitHub a chance to realize that the tag exists # Wait a moment to give GitHub a chance to realize that the tag exists
sleep 2 sleep 2
......
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
set -eu set -eu
# Check that tag signing works # Check that tag creation works.
git tag --sign --message "Dummy Tag" dummy-tag # The tag is not currently signed because Jenkins does not have this set up.
git tag --message "Dummy Tag" dummy-tag
git tag --delete dummy-tag > /dev/null git tag --delete dummy-tag > /dev/null
# Check GitHub API access token # Check GitHub API access token
...@@ -16,6 +17,3 @@ if [ "$CAN_PUSH" != "true" ]; then ...@@ -16,6 +17,3 @@ if [ "$CAN_PUSH" != "true" ]; then
echo "Cannot push to GitHub using the access token '$GITHUB_TOKEN'" echo "Cannot push to GitHub using the access token '$GITHUB_TOKEN'"
exit 1 exit 1
fi fi
# Check that we're not releasing broken code
make test
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment