Daniel Doubrovkine bio photo

Daniel Doubrovkine

aka dB., @awscloud, former CTO @artsy, +@vestris, NYC

Email Twitter LinkedIn Github Strava
Creative Commons License

The slack-ruby-client generates code from an API reference scraped from the Slack documentation website. Until now, the update process was a manual operation involving checking out the code, running a rake task, updating a CHANGELOG.md, and making a pull request, e.g. slack-ruby-client#455.

Let’s automate this using GitHub Actions (GHA)! We’ll need some advanced token-fu to auto-trigger CI.

A basic job that runs on cron, daily at 11:15PM.

name: Update API
    - cron: "15 23 * * *"
    runs-on: ubuntu-latest

Scope permissions to r/w access to repo contents and pull requests.

  contents: write
  pull-requests: write

Check-out the code, and run the rake task that updates the API.

  - uses: actions/checkout@v3
      submodules: recursive
  - name: Set up Ruby
    uses: ruby/setup-ruby@v1
      ruby-version: "3.2"
      bundler-cache: true
  - name: Update API from slack-api-ref
    run: bundle exec rake slack:api:update

Create a pull request with the changes.

- name: Create pull request
  id: cpr
  uses: peter-evans/create-pull-request@v4
    token: ${{ secrets.GITHUB_TOKEN }}
    commit-message: Update API from slack-api-ref
    title: Update API from slack-api-ref
    body: |
      Update API from slack-api-ref.
    branch: automated-api-update
    base: master

This works, but does not trigger CI. This is by design, because GITHUB_TOKEN is not allowed to.

To trigger CI we need a different token. You can create a personal access token (PAT), but that would run CI under your account, which may exclude you from approving PRs because of branch protection rules. A better solution is to use a token from an org-owned GitHub app. I created one called “Slack Ruby CI Bot”, and gave it r/w permissions for “Contents” and “Pull Requests”, then installed it in the slack-ruby GitHub org and noted the installation ID. I also generated a new private key from the bottom of the app settings page and set two repo secrets: CI_APP_ID to the value of the app ID, and CI_APP_PRIVATE_KEY for the contents of the private key from the .pem file downloaded from GitHub.

Get the the app token in GHA.

- name: GitHub App token
  id: github_app_token
  uses: tibdex/github-app-token@v1.6.0
    app_id: ${{ secrets.CI_APP_ID }}
    private_key: ${{ secrets.CI_APP_PRIVATE_KEY }}
    installation_id: 36985419

Use it in the pull request GHA, with a fallback to GITHUB_TOKEN for testing the GHA in my fork.

- name: Create pull request
  id: cpr
  uses: peter-evans/create-pull-request@v4
    token: ${{ steps.github_app_token.outputs.token || secrets.GITHUB_TOKEN }}

Now that PRs trigger CI, and commits are made by slack-ruby-ci-bot, let’s update CHANGELOG.md with the PR number output by the create-pull-request action. A text search-and-replace will do.

- uses: jacobtomlinson/gha-find-replace@v3
  if: ${{ steps.cpr.outputs.pull-request-number != '' }}
    include: CHANGELOG.md
    find: "\\* Your contribution here."
    replace: "* [#${{steps.cpr.outputs.pull-request-number}}] ...\n* Your contribution here."

We can amend the previous pull request and force-push the change back to GitHub. To authenticate to GitHub using the above-mentioned token we generate a base64-encoded BASIC auth x-access-token:token header, then stuff it into all HTTP requests made by git. This is what the create-pull-request action actually does in code, too.

- name: Commit and Push
  run: |
    git config --local user.name 'slack-ruby-ci-bot'
    git config --local user.email 'noreply@github.com'
    git config --local --unset-all http.https://github.com/.extraheader || true
    AUTH=$(echo -n "x-access-token:${{ steps.github_app_token.outputs.token || secrets.GITHUB_TOKEN }}" | base64)
    echo "::add-mask::${AUTH}"
    git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic ${AUTH}"
    git add CHANGELOG.md
    git commit --amend --no-edit
    git push origin automated-api-update -f

Bonus features include getting the current date and the git commit of the updated submodule that contains the API reference to make the CHANGELOG and the commit messages pretty.

- name: Get current date
  id: date
  run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
- name: Get slack-api-ref ref
  id: api-ref
  run: echo "::set-output name=api-ref::$(git rev-parse --short HEAD:lib/slack/web/api/slack-api-ref)"

The final result is here and you can see it in action in slack-ruby-client#465.