jenkins: Add scripts to make a release cut

This creates a new release publish by using the latest unstable
snapshots and merging those snapshots into a new pair of release
snapshots. In short, we do this:

  [mirror] debian -s\
                     > core-full-$stamp
  [repo]   core   -s/        \-s> $release-full-$stamp
                                       \-p> $release

  [repo]   $board-bsp
                \-s> unstable-bsp-$board-$stamp
                             \-s> $release-bsp-$board-$stamp
                                       \-p> $release-bsp-$board

Arrows with "s" in the middle are snapshot merges. A snapshot is created
from snapshots from the parent repo, mirror, or snapshot.

Arrows with "p" in the middle are publishes -- the final export to the
publicly viewable filesystem.

With this design, each release is freed from the upstream unstable churn
by one level of snapshots, which allows us to filter or merge in
packages from other snapshots, if necessary.

Change-Id: I4a2f6b0e4bbf8d8e8dd9b5eb575a524134bf912d
diff --git a/cicd/jobs/task_release_cut.jenkins b/cicd/jobs/task_release_cut.jenkins
new file mode 100644
index 0000000..437c43d
--- /dev/null
+++ b/cicd/jobs/task_release_cut.jenkins
@@ -0,0 +1,28 @@
+#!/usr/bin/env groovy
+
+pipelineJob("task.release.cut") {
+    description("Create a new release")
+
+    parameters {
+        stringParam('name',
+                    '',
+                    'The name of the new release to create')
+        stringParam('boards',
+                    '',
+                    'A space separated list of boards to include in this release')
+    }
+
+    definition {
+        cpsScm {
+            scm {
+                git {
+                    remote {
+                        url('https://coral.googlesource.com/gke-jenkins')
+                    }
+                    branches('*/master')
+                }
+            }
+            scriptPath("cicd/pipelines/tasks/task_release_cut.jenkins")
+        }
+    }
+}
diff --git a/cicd/pipelines/tasks/task_release_cut.jenkins b/cicd/pipelines/tasks/task_release_cut.jenkins
new file mode 100644
index 0000000..748095b
--- /dev/null
+++ b/cicd/pipelines/tasks/task_release_cut.jenkins
@@ -0,0 +1,79 @@
+#!/usr/bin/env groovy
+
+String getLatestSnapshot(repository_stem) {
+    def script = """
+        aptly snapshot list --sort=time --raw \
+            | grep -E '^${repository_stem}-' \
+            | tail -n1
+    """
+
+    return sh(returnStdout: true, script: script).trim()
+}
+
+def installGpgKeyring() {
+    sh """
+       install -d -m 700 -o root -g root /var/lib/aptly/.gnupg
+       tar -C /var/lib/aptly/.gnupg -zxf /var/lib/aptly/keyring/release-keyring.tar.gz
+       chown -R root:root /var/lib/aptly/.gnupg
+       find /var/lib/aptly/.gnupg -type d -exec chmod 700 '{}' ';'
+       find /var/lib/aptly/.gnupg -type f -exec chmod 600 '{}' ';'
+       """
+}
+
+def workspacePath = "/home/jenkins/workspace"
+def buildLabel = "task.publish.unstable-${UUID.randomUUID().toString()}"
+def sourcePath = "${workspacePath}/src"
+
+// FIXME(jtgans): Get rid of privileged! This is a security risk!
+def jnlpContainer = containerTemplate(name: 'jnlp',
+                                      image: 'jenkins/jnlp-slave:alpine')
+def debianContainer = containerTemplate(name: 'debian',
+                                        image: 'gcr.io/mendel-linux-cloud-infra/mendel-builder:latest',
+                                        command: 'cat',
+                                        args: '',
+                                        ttyEnabled: true,
+                                        privileged: true,
+                                        alwaysPullImage: true)
+def aptlyVolume = persistentVolumeClaim(claimName: 'aptly-state', mountPath: '/var/lib/aptly')
+def gpgVolume = secretVolume(secretName: 'mendel-release-credentials', mountPath: '/var/lib/aptly/keyring')
+
+podTemplate(label: buildLabel, containers: [jnlpContainer, debianContainer], volumes: [aptlyVolume, gpgVolume], envVars: []) {
+    node(buildLabel) {
+        dir(sourcePath) {
+            container('debian') {
+                def date = new Date()
+                String stamp = date.format("yyyyMMdd-HHmmss")
+                def releaseName = params.release
+                def boards = params.boards.split(' ')
+
+                if (boards.size() == 0) {
+                    error 'No boards to create releases for!'
+                }
+
+                sh "cp /etc/aptly.conf ~/.aptly.conf"
+
+                withEnv(['GNUPGHOME=/var/lib/aptly/.gnupg']) {
+                    installGpgKeyring()
+
+                    def unstableCoreSnapshotName = getLatestSnapshot('core-full-unstable')
+                    def releasedCoreSnapshotName = "core-full-${releaseName}-${stamp}"
+
+                    sh """
+                       aptly snapshot merge ${releasedCoreSnapshotName} ${unstableCoreSnapshotName}
+                       aptly publish snapshot --batch --force-overwrite --passphrase-file=/var/lib/aptly/keyring/passphrase.txt --architectures=source,amd64,arm64,armhf --distribution=${releaseName} ${releasedCoreSnapshotName} filesystem:public:${releaseName}
+                       """
+
+                    for (board in param.boards.split(' ')) {
+                        def unstableBspSnapshotName = getLatestSnapshot('unstable-bsp-${board}')
+                        def releasedBspSnapshotName = "${releaseName}-bsp-${board}-${stamp}"
+
+                        sh """
+                           aptly snapshot merge ${releasedBspSnapshotName} ${bspSnapshotName}
+                           aptly publish snapshot --batch --force-overwrite --passphrase-file=/var/lib/aptly/keyring/passphrase.txt --architectures=source,amd64,arm64,armhf --distribution=${releaseName} ${releasedBspSnapshotName} filesystem:public:${releaseName}-bsp-${board}
+                           """
+                    }
+                }
+            }
+        }
+    }
+}