diff --git a/.github/workflows/milestone.yml b/.github/workflows/milestone.yml new file mode 100644 index 00000000..60e6b4ca --- /dev/null +++ b/.github/workflows/milestone.yml @@ -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 diff --git a/.github/workflows/milestone_close.rb b/.github/workflows/milestone_close.rb new file mode 100644 index 00000000..d0265993 --- /dev/null +++ b/.github/workflows/milestone_close.rb @@ -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]}" diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100755 index 00000000..4f3b95d4 --- /dev/null +++ b/DEPLOY.md @@ -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 ' 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 + +### 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 + 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 ::::::::::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 + 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 ] +