Minimal Maintenance: Bump Go

Welcome!

  • Review: Minimal Maintenance: Bump Go

    Héctor Hurtado & pancho horrillo

Introducing GitHub Actions

Keeping the Go compiler up to date

  • Keeping the Go compiler up to date

Step #0: Make the version you use explicit by storing it into a file

file: .github/versions/go

1.14
  • When building the project, honor the version stored in the file

file: .workflows/build.yml

...
- name: Load Go version
  id: go-version
  run: |
    echo ::set-output name=go-version::$(<.github/versions/go)

- uses: actions/setup-go@v2
  id: setup-go
  with:
    go-version: ${{ steps.go-version.outputs.go-version }}
...

Keeping the Go compiler up to date

  • Keeping the Go compiler up to date

Step #1: Periodically check if an updated version of Go is available (and create a PR with the updated version file)

Naive approach to Step #1:

  • Naive approach to Step #1

file: .workflows/github/bump-go-inline.yml (edited)

name: Bump Go - inline
on:
  schedule:
    - cron: 00 9 * * *
jobs:
  bump-go:
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/checkout@v2
      - name: Ensure we are using the latest Go
        id: bump-go
        run: |
          go_version_filepath=.github/versions/go

          curl --silent --fail 'https://golang.org/dl/?mode=json'         \
          | jq --raw-output --exit-status '.[0].version | sub("^go"; "")' \
          > $go_version_filepath

          echo ::set-output name=go-version::$(< $go_version_filepath)

      - name: Create pull request
        uses: peter-evans/create-pull-request@v3
        with:
          title: Bump Go version
          commit-message: Bump Go version to ${{ steps.bump-go.outputs.go-version }}
          branch: bump-go-inline/patch

Pros and cons of the naive approach

  • Pros and Cons of the naive approach
  • Pro: straightforward to implement

just copy and paste the workflow into your project

  • Pro: great for prototyping
  • Con: when updates to the workflow are needed,

the changes have to be distributed to every project that uses it (ahem…)

Native approach to Step #1

  • Native approach to Step #1
  • Package the workflow as a GitHub Action
  • Two flavors available (at the time of writing):
    • Docker <- maybe overkill for our use case
    • JavaScript / TypeScript + node.js <- Shiny! Let’s try this one!

Native approach to Step #1

  • Native approach to Step #1 - TypeScript version
  • Start with https://github.com/actions/typescript-action as a template
  • Customize action.yml, package.json, __tests__/*, src/* and README.md
  • Optionally publish the action into the marketplace

Native approach to Step #1 - TypeScript version

  • Native approach to Step #1 - TypeScript version

Our implementation: https://github.com/BBVA/bump-go/tree/typescript

file: action.yml (edited)

name: 'Bump Go'
description: 'Bump the Go compiler version to the most up-to-date release'
inputs:
  go-version-filepath:
    description: 'Path to the file containing the Go version to build your project'
    required: false
    default: '.github/versions/go'
outputs:
  go-version:
    description: 'Latest current Go version'
runs:
  using: 'node12'
  main: 'dist/index.js'

Native approach to Step #1 - TypeScript version

  • Native approach to Step #1 - TypeScript version

file: src/main.ts (edited)

import {run} from './bump-go'

run()

Native approach to Step #1 - TypeScript version

  • Native approach to Step #1 - TypeScript version

file: src/bump-go.ts (edited)

import * as core from '@actions/core'
import * as gover from './go-version'
import {promises as fs} from 'fs'

export async function run(): Promise<void> {
  try {
    const goVersionFilePath = core.getInput('go-version-filepath')
    const currentGoVersion = await gover.getCurrent()
    await fs.writeFile(goVersionFilePath, `${currentGoVersion}\n`, 'utf8')
    core.setOutput('go-version', currentGoVersion)
  } catch (error) {
    core.setFailed(error.message)
  }
}

Native approach to Step #1 - TypeScript version

  • Native approach to Step #1 - TypeScript version

file: src/go-version.ts (edited)

import * as httpm from '@actions/http-client'

interface IGoVersion {
  version: string
}

const dlUrl = 'https://golang.org/dl/?mode=json'

export async function getCurrent(): Promise<string> {
  const http: httpm.HttpClient = new httpm.HttpClient('Bump Go')
  const request = await http.getJson<IGoVersion[]>(dlUrl)

  if (!request.result) {
    throw new Error(`Go download URL did not yield any results`)
  }

  try {
    // First result is current Go release.  Drop 'go' prefix from version.
    return request.result[0].version.substr(2)
  } catch (error) {
    throw new Error(`Error extracting current Go version: ${error.message}`)
  }
}

Native approach to Step #1 - TypeScript version - building and testing

  • Native approach to Step #1 - TypeScript version - building and testing
  • Run ’npm install && npm run all’ to install deps, test and build
  • Output is stored on dist/index.js, which matches what action.yml expects:

file: action.yml (edited)

...
runs:
  using: 'node12'
  main: 'dist/index.js'
  • dist/index.js must be added to the repo, it’s not built by the Actions engine automatically
  • Note that the go-version file must exist prior to running this action in your project
  • To test locally:
echo 1.0 > go-version
env INPUT_GO-VERSION=./go-version node dist/index.js
::set-output name=go-version::1.15.2

Native approach to Step #1 - TypeScript version - notes

  • Native approach to Step #1 - TypeScript version - notes
  • NOTE: Creating the PR is handled by a different action, e.g: peter-evans/create-pull-request, that must be explicitly added to the workflow
  • A big shout out to pixeliko and CesarGallego for helping us understand how TypeScript works

Native approach to Step #1 - TypeScript version - Pros and Cons

  • Native approach to Step #1 - TypeScript version - Pros and Cons
  • Pro: Native solution
  • Con: Surprisingly high maintenance
    • Over the course of three months, dependabot detected a number of security issues with the dependencies
    • dependabot provided straightforward PRs for most updates, but not all
    • using ’npm audit’ to fix the rest revealed a bunch of high risk vulnerabilities, as well as a high volume (~1K) of low risk vulnerabilities in the dependencies

Native approach to Step #1 - A New Hope

Native approach to Step #1 - A New Hope

  • Native approach to Step #1 - A New Hope
  • Quickly reimplemented bump-go with this flavor:

file: action.yml (edited)

name: 'Bump Go'
description: 'Ensure Go is up-to-date'
inputs:
  go-version-filepath:
    description: 'Path to the file containing the Go version to build your project'
    required: false
    default: '.github/versions/go'
outputs:
  go-version:
    description: 'Latest current Go version'
    value: ${{ steps.bump-go.outputs.go-version }}
runs:
  using: "composite"
  steps:
    - name: Ensure Go is up-to-date
      id: bump-go
      shell: bash
      run: |
        go_version_filepath="${{ inputs.go-version-filepath }}"
        curl --silent --fail 'https://golang.org/dl/?mode=json'         \
        | jq --raw-output --exit-status '.[0].version | sub("^go"; "")' \
        > $go_version_filepath
        echo ::set-output name=go-version::$(< $go_version_filepath)

Native approach to Step #1 - Hope is Crushed

  • Native approach to Step #1 - Hope is Crushed
  • While trying to simplify the integration with actions/setup-go, I came across this:

    actions/setup-go#23 (comment)

    hazcod: I extract the Go version to use out of my Docker containers, […] It works with dependabot for automatic updates that way”

  • OMFG, this guy is subverting dependabot’s support for Dockerfiles to get the job done. Genius!
  • No need for a GitHub Action after all
  • Bye bye, Bump Go. Thanks for all the fish!

Leveraging dependabot to do the work for us

  • Leveraging dependabot to do the work for us

file: .github/go/Dockerfile

FROM golang:1.12

file: .github/dependabot.yml

version: 2
updates:
  - package-ecosystem: "docker"
    directory: "/.github/go"
    schedule:
      interval: "daily"

file: .github/workflows/build.yml

...
  - name: Load Go version
    id: go-version
    run: |
      echo ::set-output name=go-version::$(sed 's/^.*://' .github/go/Dockerfile)

  - uses: actions/setup-go@v2
    with:
      go-version: ${{ steps.go-version.outputs.go-version }}
...

Dependabot also updates Go dependencies

  • Dependabot also updates Go dependencies

file: .github/dependabot.yml

version: 2
updates:
  - package-ecosystem: "gomod"
    directory: "/"
    schedule:
      interval: "daily"
  • SemVer stability score

https://dependabot.com/compatibility-score/

images/semver-badge.png

“Dependabot has updated uglifier between SemVer compatible versions 5423 times across 1279 projects so far. 98% of those updates passed CI.”

Thanks to nilp0inter for finding about this!

Dependabot also updates GitHub Actions

  • Dependabot also updates GitHub Actions

file: .github/dependabot.yml

version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "daily"

Next Steps

  • Next Steps
  • Minimal Maintenance lives on!

    In progress:

    • sign-and-go GitHub Action for signing releases automatically
    • procedure to minimize the work to produce release notes for the releases, leveraging semantic commit messages (thanks, pixeliko!)
  • Possible experiment: deploy dependabot on premise

    https://github.com/dependabot/dependabot-core

The End

  • The End

Thanks for coming!