Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automate release #480

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
195 changes: 195 additions & 0 deletions .github/workflows/milestone.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
name: Milestone

on:
milestone:
types: [closed]


jobs:
milestone:
name: Close out Milestone
runs-on: ubuntu-20.04
outputs:
tagname: ${{ steps.news.outputs.tagname }}
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install Ruby Gems
run: |
sudo gem install octokit json
- name: Generate NEWS
id: news
run: |
echo "::set-output name=tagname::$(ruby $GITHUB_WORKSPACE/.github/workflows/milestone_close.rb)"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Change version number
id: version
env:
tagname: ${{ steps.news.outputs.tagname }}
run: |
ver=$tagname
ver=${ver#v}
ver=${ver%-*}
echo "::set-output name=version::$ver"
sed -i "s/^\(AC_INIT.*generator\],\)\(.*\)\(,\[flex-help.*\)$/\1[$ver]\3/" $GITHUB_WORKSPACE/configure.ac
- name: Commit and Push
run: |
mv $GITHUB_WORKSPACE/NEWS.new $GITHUB_WORKSPACE/NEWS
git add $GITHUB_WORKSPACE/NEWS
git add $GITHUB_WORKSPACE/configure.ac
git config --global user.email "runner@github.com"
git config --global user.name "GitHub Actions Runner"
git commit -m "chore(release): Update Version Number and NEWS"
git push
- name: Create Tag
env:
tagname: ${{ steps.news.outputs.tagname }}
run: |
git tag -a $tagname -m "chore(release): Prepare tag for release $tagname"
git push origin $tagname

release:
needs: milestone
name: Make release
runs-on: ubuntu-20.04
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Fast-forward to Milestone Tag
env:
ver: ${{ needs.milestone.outputs.version }}
tagname: ${{ needs.milestone.outputs.tagname }}
run: |
git fetch
git checkout $tagname
- name: apt
run: sudo apt-get install gcc-6 autoconf bison gettext autopoint help2man lzip texinfo texlive
- name: autogen
run: ./autogen.sh
- name: configure
run: ./configure
- name: make
run: make
- name: make check
run: make check
- name: make distcheck
run: make distcheck
- name: Make Git archives
env:
ver: ${{ needs.milestone.outputs.version }}
tagname: ${{ needs.milestone.outputs.tagname }}
run: |
git archive -o $tagname.tar.gz --prefix=flex-$ver/ $tagname
TZ=America/Los_Angeles git archive -o $tagname.zip --prefix=flex-$ver/ $tagname
- name: Prepare GPG
id: gpg
env:
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
gpghome: .ghgpg
run: |
echo "::set-output name=gpghome::$gpghome"
mkdir -m 700 $gpghome
export GNUPGHOME=$gpghome
gpg --version
echo "$GPG_SIGNING_KEY" | gpg --batch --import
- name: Sign tarballs
env:
GPG_SIGNING_PASSWD: ${{ secrets.GPG_SIGNING_PASSWD }}
ver: ${{ needs.milestone.outputs.version }}
tagname: ${{ needs.milestone.outputs.tagname }}
gpghome: ${{ steps.gpg.outputs.gpghome }}
run: |
export GNUPGHOME=$gpghome
echo "$GPG_SIGNING_PASSWD" | gpg --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign $tagname.tar.gz
echo "$GPG_SIGNING_PASSWD" | gpg --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign $tagname.zip
echo "$GPG_SIGNING_PASSWD" | gpg --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign flex-$ver.tar.gz
echo "$GPG_SIGNING_PASSWD" | gpg --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign flex-$ver.tar.lz
- name: Clean up GPG
env:
gpghome: ${{ steps.gpg.outputs.gpghome }}
run: |
export GNUPGHOME=$gpghome
rm -rf $gpghome
- name: Get artifact names
env:
ver: ${{ needs.milestone.outputs.version }}
tagname: ${{ needs.milestone.outputs.tagname }}
run: |
echo "SOURCE_GZ_ASC=$(echo $tagname.tar.gz.asc)" >> $GITHUB_ENV
echo "SOURCE_ZIP_ASC=$(echo $tagname.zip.asc)" >> $GITHUB_ENV
echo "ARTIFACT_GZ=$(echo flex-$ver.tar.gz)" >> $GITHUB_ENV
echo "ARTIFACT_LZ=$(echo flex-$ver.tar.lz)" >> $GITHUB_ENV
echo "ARTIFACT_GZ_ASC=$(echo flex-$ver.tar.gz.asc)" >> $GITHUB_ENV
echo "ARTIFACT_LZ_ASC=$(echo flex-$ver.tar.lz.asc)" >> $GITHUB_ENV
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ needs.milestone.outputs.tagname }}
release_name: Release ${{ needs.milestone.outputs.tagname }}
draft: false
prerelease: false
- name: Upload Release tar.gz
id: upload-release-asset-gz
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./${{ env.ARTIFACT_GZ }}
asset_name: ${{ env.ARTIFACT_GZ }}
asset_content_type: application/gzip
- name: Upload Release tar.gz.asc
id: upload-release-asset-gz-asc
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./${{ env.ARTIFACT_GZ_ASC }}
asset_name: ${{ env.ARTIFACT_GZ_ASC }}
asset_content_type: text/plain
- name: Upload Release tar.lz
id: upload-release-asset-lz
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./${{ env.ARTIFACT_LZ }}
asset_name: ${{ env.ARTIFACT_LZ }}
asset_content_type: application/lzip
- name: Upload Release tar.lz.asc
id: upload-release-asset-lz-asc
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./${{ env.ARTIFACT_LZ_ASC }}
asset_name: ${{ env.ARTIFACT_LZ_ASC }}
asset_content_type: text/plain
- name: Upload Source tar.gz.asc
id: upload-release-source-tar-gz-asc
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./${{ env.SOURCE_GZ_ASC }}
asset_name: ${{ env.SOURCE_GZ_ASC }}
asset_content_type: text/plain
- name: Upload Source zip.asc
id: upload-release-source-zip-asc
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./${{ env.SOURCE_ZIP_ASC }}
asset_name: ${{ env.SOURCE_ZIP_ASC }}
asset_content_type: text/plain
34 changes: 34 additions & 0 deletions .github/workflows/milestone_close.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
require 'octokit'
require 'json'

token = ENV["GITHUB_TOKEN"]

client = Octokit::Client.new(:auth_token => token)
client.auto_paginate = true

event = JSON.parse( File.read("#{ENV["GITHUB_EVENT_PATH"]}") )
milestone = {number: event["milestone"]["number"], title: event["milestone"]["title"]}

now = Time.now
news = Array.new

client.list_issues("#{ENV["GITHUB_REPOSITORY"]}", :milestone => milestone[:number], :state => "closed").each do |issue|
news.push "##{issue.number}: #{issue.title} (#{issue.milestone.title}) [#{issue.labels.reduce(" ") {|r, label| r + label.name + " "}}]"
end

infile = File.open("#{ENV["GITHUB_WORKSPACE"]}/NEWS")
outfile = File.open("#{ENV["GITHUB_WORKSPACE"]}/NEWS.new", "w")

outfile.write infile.gets

outfile.write "\n"
outfile.write "* Noteworthy changes in release #{milestone[:title]} (#{now.year}-#{now.month}-#{now.day})\n"
outfile.write "\n"
news.each {|n| outfile.write "#{n}\n"}

infile.each {|l| outfile.write l}

infile.close
outfile.close

puts "#{milestone[:title]}"
151 changes: 151 additions & 0 deletions DEPLOY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Automatic Deployment with Github Actions

The .github/workflows directory contains Github Actions scripts that build the master branch in response to three events:
1. When changes are pushed to Github
2. When a pull request is submitted
3. When a Milestone is closed

The build.yml script handles cases 1 and 2. The milestone.yml script handles case 3.
There is code duplication between these scripts. It's a side-effect of the Github Actions syntax and fixes are being explored.

The Milestone script takes a number of actions beyond a basic build & test:
* It updates the NEWS file to include the titles of closed issues assigned to the Milestone
* It update the flex version number in configure.ac to match the Milestone title
* It creates a new tag in the repository as the starting point for a Release
* It triggers a release build that produces signed tarballs from the new tag

## Setup for Code Signing
Code signing is based on GPG and Github repository secrets.

We'll discuss repository setup first, then key setup first.

## Github Repository Secrets
Go to your repository's settings and create two "Repository secrets" called:
- GPG_SIGNING_KEY
- GPG_SIGNING_PASSWD

The contents of these keys are an ASCII armored GPG key with the Sign capability and the passphrase to unlock that key.
If you already have keys you're comfortable using, paste them into the window when you set up the secrets. Otherwise, you
can update the value of a secret at any time by returning to the Settings>Secrets page and clicking Update.

## GPG Key Preparation
Since the scripts are meant to run unattended, you may not want to use your regular GPG signing key.
This section lays out a pattern for creating a detached Certifying and Signing key pair, with different passphrases.
This will help you revoke your Signing key if your CI toolchain (e.g. Github Actions) is compromised.

### Create a temporary GNUPGHOME
Create a temporary gpg working directory while you build your key pair. This simplifies changing only the Signing key's
passphrase later on.

> mygpghome=.tmpgpg
> mkdir -m 700 $mygpghome
> export GNUPGHOME=$mygpghome/

### Create a Certifying Key
Create a key with only the Certifying capability. This reserves the Sign, Encrypt, and Authenticate capabilities for subkeys.
The following example creates a primary Certifying key using 4096-bit RSA with a validity period of 3 years.
Fill in your name and email address, adjust the algorithm, and change the validity period as you see fit. The word "cert"
is required to set the key capabilities. A validity period of 1 - 3 years is recommended for this key.

> gpg --quick-gen-key 'Full Name <email@address.com>' rsa4096 cert 30d

You'll be asked to under a passphrase. This will remain private so make it memorable and strong.

If everything works, you'll see output like the following that includes your Certifying key's fingerprint.

Note that this key cannot be used for encryption. You may want to use
the command "--edit-key" to generate a subkey for this purpose.
pub rsa4096 2021-03-27 [C] [expires: 2021-04-26]
902C9CD6E95F4429A45A56BE3ADF9765D4D7E1F8
uid Full Name <email@address.com>

### Create a Signing Subkey
Create a subkey with only the Signing capability. The subkey uses the same algorithm as the primary key, but may have a shorter
validity period. A validity period of up to 1 year is recommended for this key. You'll use the primary key fingerprint as an
argument to this command.

> gpg --quick-add-key CBB56298C56B9A8E98CDDDC87FEBE45739D9C223 rsa4096 sign 15d

You'll be asked for the passphrase of your primary key. If key generation is successfull you'll be returned to the command
promp without errors. Use the following command to check that your subkey was created.

> gpg --list-secret-keys
gpg: checking the trustdb
gpg: marginals needed: 3 completes needed: 1 trust model: pgp
gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u
gpg: next trustdb check due at 2021-04-26
/home/user/.tmppgp/pubring.kbx
-----------------------------
sec rsa4096 2021-03-27 [C] [expires: 2021-04-26]
902C9CD6E95F4429A45A56BE3ADF9765D4D7E1F8
uid [ultimate] Full Name <email@address.com>
ssb rsa4096 2021-03-27 [S] [expires: 2021-04-11]

We'll need the fingerprint of the subkey in a moment. That's obtained by extending the previous command.

> gpg --list-secret-keys --with-colons
sec:u:4096:1:3ADF9765D4D7E1F8:1616804233:1619396233::u:::cSC:::+:::23::0:
fpr:::::::::902C9CD6E95F4429A45A56BE3ADF9765D4D7E1F8:
grp:::::::::AFD6763E4BE3060CE43266CA57B95AD49F672E53:
uid:u::::1616804233::4870E7BDEC077F3D4C65C5770E1B3C08F14A88B7::Full Name <email@address.com>::::::::::0:
ssb:u:4096:1:CFA1F2FA6F73BA07:1616804291:1618100291:::::s:::+:::23:
fpr:::::::::5EC1D4EDD47DAB3C1CD91047CFA1F2FA6F73BA07:
grp:::::::::AC82DE3133CE1A7DB065031E386D2141CACDDAC0:

The line that begins "ssb" is your Signing subkey and the "fpr" line below it contains your Signing key's fingerprint.

### Export your keys
We now have a Certifying & Signing key pair protected by a passphrase that you'll keep private. Export those keys
and keep them protected so you can generate new Signing keys, extend your Certifying key's validity, or revoke a
key if one is ever compromised. We first export the public certificates, then the full key pair, and finally the
Signing key on its own.

> gpg --export --armor --output public_cert.asc email@address.com
> gpg --export-secret-keys --armor --output secrets.asc 902C9CD6E95F4429A45A56BE3ADF9765D4D7E1F8
> gpg --export-secret-subkeys --armor --output sub_secrets.asc 5EC1D4EDD47DAB3C1CD91047CFA1F2FA6F73BA07!

Note that the public and primary secret key exports can take either your user ID or the key fingerprint as their argument.
The private subkey export takes your subkey fingerprint followed by an exclamation point.

Upload your public_cert.asc file to a PGP keyserver.
Back up all three of these files somewhere safe. For the remaining steps, we'll only need sub_secrets.asc.

### Reset GNUPGHOME
Now that your have exported your keys, we will delete the temporary GNUPGHOME and start a new one. This is the simplest way
to ensure that we're only changing the password of the Signing subkey.

> mygpghome=.tmpgpg
> rm -rf $mygpghome
> mkdir -m 700 $mygpghome
> export GNUPGHOME=$mygpghome/
> gpg --list-secret-keys
gpg: keybox '/home/user/.tmppgp/pubring.kbx' created
gpg: /home/user/.tmppgp/trustdb.gpg: trustdb created

### Change the password on your Signing Subkey
The Certifying & Signing keys are stored together in the secrets.asc file. The Signing key is stored alone in sub_secrets.asc.
We can make a copy of the Signing key that uses its own passphrase by importing only from sub_secrets.asc.

> gpg --import sub_secrets.asc
[ provide your primary key's passphrase when prompted ]

> gpg --list-secret-keys
/home/user/.tmppgp/pubring.kbx
-----------------------------
sec# rsa4096 2021-03-27 [C] [expires: 2021-04-26]
902C9CD6E95F4429A45A56BE3ADF9765D4D7E1F8
uid [ unknown] Full Name <email@address.com>
ssb rsa4096 2021-03-27 [S] [expires: 2021-04-11]

We know the primary key is detached by the octothorpe (#) beside its name. Also, the trust level in the key is "unknown" rather
that "ultimate" as it was when the primary key was present.

Now we can change the password and export the Signing key again.

> gpg --passwd email@address.com
[ provide your primary key's passphrase a final time ]
[ provide a new passphrase for the Signing key, twice ]

> gpg --export-secret-subkeys --armor --output sign_secret.asc 5EC1D4EDD47DAB3C1CD91047CFA1F2FA6F73BA07!
[ provide your Signing key's new passphrase when prompted ]