pipeline { agent { label 'docker-multiarch' } options { buildDiscarder(logRotator(numToKeepStr: '5')) disableConcurrentBuilds() ansiColor('xterm') } environment { IMAGE = 'nginx-proxy-manager' BUILD_VERSION = getVersion() BUILD_COMMIT = getCommit() MAJOR_VERSION = '3' BRANCH_LOWER = "${BRANCH_NAME.toLowerCase().replaceAll('/', '-')}" COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}" COMPOSE_FILE = 'docker/docker-compose.ci.yml' COMPOSE_INTERACTIVE_NO_CLI = 1 BUILDX_NAME = "${COMPOSE_PROJECT_NAME}" DOCS_BUCKET = 'jc21-npm-site-next' // TODO: change to prod when official DOCS_CDN = 'E2Z0128EHS0Q23' // TODO: same } stages { stage('Environment') { parallel { stage('Master') { when { branch 'master' } steps { script { env.BUILDX_PUSH_TAGS = "-t docker.io/jc21/${IMAGE}:${BUILD_VERSION} -t docker.io/jc21/${IMAGE}:${MAJOR_VERSION} -t docker.io/jc21/${IMAGE}:latest" } } } stage('Other') { when { not { branch 'master' } } steps { script { // Defaults to the Branch name, which is applies to all branches AND pr's env.BUILDX_PUSH_TAGS = "-t docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}" } } } stage('Versions') { steps { // Is this frontend version stuff still applicable? sh 'cat frontend/package.json | jq --arg BUILD_VERSION "${BUILD_VERSION}" \'.version = $BUILD_VERSION\' | sponge frontend/package.json' sh 'echo -e "\\E[1;36mFrontend Version is:\\E[1;33m $(cat frontend/package.json | jq -r .version)\\E[0m"' sh 'sed -i -E "s/(version-)[0-9]+\\.[0-9]+\\.[0-9]+(-green)/\\1${BUILD_VERSION}\\2/" README.md' } } } } stage('Frontend') { steps { sh './scripts/ci/frontend-build' } post { always { junit 'frontend/eslint.xml' junit 'frontend/junit.xml' } } } stage('Backend') { steps { withCredentials([usernamePassword(credentialsId: 'oss-index-token', passwordVariable: 'NANCY_TOKEN', usernameVariable: 'NANCY_USER')]) { sh '''docker build --pull --no-cache --squash --compress \\ -t ${IMAGE}:ci-${BUILD_NUMBER} \\ -f docker/Dockerfile \\ --build-arg TARGETPLATFORM=linux/amd64 \\ --build-arg BUILDPLATFORM=linux/amd64 \\ --build-arg BUILD_DATE="$(date '+%Y-%m-%d %T %Z')" \\ --build-arg BUILD_VERSION="${BUILD_VERSION}" \\ --build-arg BUILD_COMMIT="${BUILD_COMMIT}" \\ --build-arg SENTRY_DSN="${SENTRY_DSN:-}" \\ --build-arg GOPROXY="${GOPROXY:-}" \\ --build-arg GOPRIVATE="${GOPRIVATE:-}" \\ --build-arg NANCY_USER="${NANCY_USER}" \\ --build-arg NANCY_TOKEN="${NANCY_TOKEN}" \\ . ''' } } } stage('Test') { when { not { equals expected: 'UNSTABLE', actual: currentBuild.result } } steps { // Bring up a stack sh 'docker-compose up -d fullstack' sh './scripts/wait-healthy $(docker-compose ps -q fullstack) 120' // Run tests sh 'rm -rf test/results' sh 'docker-compose up cypress' // Get results sh 'docker cp -L "$(docker-compose ps -q cypress):/test/results" test/' } post { always { // Dumps to analyze later sh 'mkdir -p debug' sh 'docker-compose logs fullstack | gzip > debug/docker_fullstack.log.gz' // Cypress videos and screenshot artifacts dir(path: 'test/results') { archiveArtifacts allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml' } junit 'test/results/junit/*' } } } stage('Docs') { when { not { equals expected: 'UNSTABLE', actual: currentBuild.result } } steps { dir(path: 'docs') { sh 'yarn install' sh 'yarn build' } // API Docs: sh 'docker-compose exec -T fullstack curl -s --output /temp-docs/api-schema.json "http://fullstack:81/api/schema"' sh 'mkdir -p "docs/.vuepress/dist/api"' sh 'mv docs/api-schema.json docs/.vuepress/dist/api/' dir(path: 'docs/.vuepress/dist') { sh 'tar -czf ../../docs.tgz *' } archiveArtifacts(artifacts: 'docs/docs.tgz', allowEmptyArchive: false) } } stage('MultiArch Build') { when { not { equals expected: 'UNSTABLE', actual: currentBuild.result } } steps { withCredentials([string(credentialsId: 'npm-sentry-dsn', variable: 'SENTRY_DSN')]) { withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { // Docker Login sh "docker login -u '${duser}' -p '${dpass}'" // Buildx with push from cache sh "./scripts/buildx --push ${BUILDX_PUSH_TAGS}" // sh './scripts/buildx -o type=local,dest=docker-build' } } } } stage('Docs Deploy') { when { allOf { branch 'master' not { equals expected: 'UNSTABLE', actual: currentBuild.result } } } steps { withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', accessKeyVariable: 'AWS_ACCESS_KEY_ID', credentialsId: 'npm-s3-docs', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']]) { sh """docker run --rm \\ --name \${COMPOSE_PROJECT_NAME}-docs-upload \\ -e S3_BUCKET=$DOCS_BUCKET \\ -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \\ -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \\ -v \$(pwd):/app \\ -w /app \\ jc21/ci-tools \\ scripts/docs-upload /app/docs/.vuepress/dist/ """ sh """docker run --rm \\ --name \${COMPOSE_PROJECT_NAME}-docs-invalidate \\ -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \\ -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \\ jc21/ci-tools \\ aws cloudfront create-invalidation --distribution-id $DOCS_CDN --paths '/*' """ } } } stage('PR Comment') { when { allOf { changeRequest() not { equals expected: 'UNSTABLE', actual: currentBuild.result } } } steps { script { def comment = pullRequest.comment("This is an automated message from CI:\n\nDocker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/jc21/${IMAGE}) as `jc21/${IMAGE}:git-3-${BRANCH_LOWER}`\n\n**Note:** ensure you backup your NPM instance before testing this PR image! Especially if this PR contains database changes.") } } } /* stage('Artifacts') { when { allOf { not { equals expected: 'UNSTABLE', actual: currentBuild.result } } } steps { sh 'mkdir -p artifacts' // Multiarch builds dir(path: 'docker-build/linux_amd64/app') { sh 'zip -qr ../../../artifacts/linux_amd64.zip *' } dir(path: 'docker-build/linux_arm64/app') { sh 'zip -qr ../../../artifacts/linux_arm64.zip *' } dir(path: 'docker-build/linux_arm_v7/app') { sh 'zip -qr ../../../artifacts/linux_arm_v7.zip *' } // Archive them dir(path: 'artifacts') { archiveArtifacts artifacts: '** /*' } } } */ } post { always { sh 'docker-compose down --rmi all --remove-orphans --volumes -t 30' sh './scripts/build-cleanup' sh 'echo Reverting ownership' sh 'docker run --rm -v $(pwd):/data node:latest chown -R "$(id -u):$(id -g)" /data' } success { juxtapose event: 'success' sh 'figlet "SUCCESS"' } failure { archiveArtifacts(artifacts: 'debug/**.*', allowEmptyArchive: true) juxtapose event: 'failure' sh 'figlet "FAILURE"' } unstable { archiveArtifacts(artifacts: 'debug/**.*', allowEmptyArchive: true) juxtapose event: 'unstable' sh 'figlet "UNSTABLE"' } } } def getVersion() { ver = sh(script: 'cat .version', returnStdout: true) return ver.trim() } def getCommit() { ver = sh(script: 'git log -n 1 --format=%h', returnStdout: true) return ver.trim() }