Automating GitHub Profile Updates with GitHub Actions
Tags: github, githubactions
Published: 2023-05-01 15:00:00

Introduced in 2020, the GitHub user profile README allow individuals to give a long-form introduction. This multi-part tutorial explains how I setup my own profile to create dynamic content to aid discovery of my projects:

- [with the Liquid template engine and Shields](https://dev.to/j12y/setup-a-dynamic-github-profile-readme-3lbm) (Part 1 of 4)
- [using GitHub's GraphQL API to query dynamic data about all my repos](https://dev.to/j12y/setup-a-dynamic-github-profile-readme-3lbm) (Part 2 of 4)
- [fetching RSS blog post details from third-party sites](https://dev.to/j12y/fetching-blog-rss-feeds-for-building-a-github-profile-i31) (Part 3 of 4)
- **automating updates with GitHub Actions** (keep reading below)

You can visit [github.com/j12y](https://github.com/j12y) to see the final result of what I came up with for my own profile page.

![My GitHub Profile](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hlg6ws0r5hwxel0ii84h.png)


## Dynamic Updates in Response to Changes

The problem to solve is how to dynamically update my [GitHub Profile](https://github.com/j12y) if there is a [content change in a template](https://dev.to/j12y/setup-a-dynamic-github-profile-readme-3lbm), an update to [the metadata of a repos](https://dev.to/j12y/setup-a-dynamic-github-profile-readme-3lbm), or a new [blog post](https://dev.to/j12y/fetching-blog-rss-feeds-for-building-a-github-profile-i31). Conveniently GitHub Actions provides a solution for continuous integration (CI) / continuous delivery (CD).

A GitHub Action can be triggered:
* manually
* when a commit or branch is merged
* at a scheduled interval

One big advantage is that by running directly on the GitHub platform it becomes convenient for storing secrets and enabling changes in repositories by utilizing an existing authorization.

## Getting Started with GitHub Actions

The [Learn GitHub Actions](https://docs.github.com/en/actions) documentation provides a good introduction to terminology and for understanding how it works. I don't want to repeat those details and focus on how I applied it, so reference the docs if anything is unclear.

The configuration file for my project is  [.github/workflows/build-readme.yml](https://github.com/j12y/j12y/blob/main/.github/workflows/build-readme.yml). The folder naming convention is critical, but the YAML file can be named to suit the purpose and there may be multiple workflows kept in separate configuration files.

I found the easiest way to get started was with a manual trigger to make sure the job behaves in the way I expect before fully automating it. This is done by specifying a `workflow_dispatch` configuration.

```yaml
name: Update README.md

on:
  workflow_dispatch:
```

From the **Actions** menu in the repo, you can select the *Update README.md* workflow and click the `Run workflow` button to trigger the job to run at will.

![Run workflow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e52ilxzji4db5ym32b48.png)

## GitHub Actions Jobs and Build Steps

What happens when the button is clicked depends on how the job is configured. Here's the first few steps of my **build** job.

```yaml
jobs:
  build:
    runs-on: ubuntu-latest
    environment: buildenv
    steps:
      - name: Trigger event
        run: echo "Triggered by ${{ github.event_name }}"
      - name: Checkout repo
        uses: actions/checkout@v3
      - name: Install dependencies
        run: |
          npm install
```

The `runs-on` configures the type of machine to run the job. There are a number of different types of [runners](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on) but a github hosted **ubuntu-latest** is versatile enough for my needs.

We'll come back to the `environment` in the next section.

These first few steps demonstrate how you can run commands like `npm install` or import other workflows such as how it `uses` the [actions/checkout](https://github.com/actions/checkout) to copy the contents of the repository into a working directory on the runner host. Read [Reusable workflows](https://docs.github.com/en/actions/using-workflows/reusing-workflows#calling-a-reusable-workflow) for more about the syntax for referencing them.

## Using Environment Variables with Actions

The previous section referenced the `buildenv` environment. This enables variables or secrets that you want to be able to reference but not expose in code to be stored.

![Environment settings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zlazi3i2tepfgc9p1e8k.png)

The environment distinguishes between **secrets** (which will be redacted from logs) and **vars** which may be captured in output for debugging and can be modified without changing any code. There are also some universally available **github** values that can be used.

```yaml
      - name: Build source
        env:
          TOKEN: ${{ secrets.TOKEN }}
          REPO: ${{ github.repository_owner }}
          USERNAME: ${{ github.repository_owner }}
          MEDIUM_ID: ${{ vars.MEDIUM_ID }}
          DEVTO_ID: ${{ vars.DEVTO_ID }}
          DOLBYIO_ID: ${{ vars.DOLBYIO_ID }}
        run: |
          npm start
```

There is a bit of redundancy to this configuration that is not ideal. The variable names need to be mapped between the GitHub environment and the runner environment. I asked the community for tricks on [Passing All GitHub Environment Variables and Secrets in a GitHub Actions Workflow](https://stackoverflow.com/questions/75646046/passing-all-github-environment-variables-and-secrets-in-a-github-actions-workflo) but it seems this may be a common feature request.

One workaround would be to take all the secrets and variables, encode as JSON, pass that as a variable, and then unpack it again. Alternatively, could just store the JSON structure as the environment variable itself. In my case, since there were only a few variables I didn't add the extra complexity.

```yaml
run: |
  echo "$ALL_VARS"
env:
  ALL_VARS: ${{ toJSON(vars) }}
```

The `npm start` in my project generates the README.md output and is the primary build output from this workflow.

Next, I need to do something with the artifact.

## Conditional in a GitHub Action Workflow

If there are no template changes, no new blog posts, and no repo updates it would be unnecessary to commit changes to the README.md. Worse, that can cause the job to fail since the commit would fail with no differences.

To solve for this, you can run a step to run a `git diff` and store the result as an environment variable.

```yaml
      - name: Check for changes
        run: |-
          if git diff --exit-code; then
            echo "README_CHANGED=0" >> "$GITHUB_ENV"
          else
            echo "README_CHANGED=1" >> "$GITHUB_ENV"
            git diff
          fi
```

The variable can then be checked with the `if` syntax which means this step will be optional depending on the value of the environment variable.

```yaml
      - name: Commit change
        if: env.README_CHANGED != 0
        run: |-
          git config user.name "GitHub Action: ${{ github.workflow }} #${{ github.run_number }}"
          git config user.email "<>"
          git add README.md
          git commit -m 'GitHub Action: ${{ github.workflow }} #${{ github.run_number }}'
          git push
```

If there are meaningful changes, these steps will set the git configuration for checkin to the repository with a meaningful commit message that references the GitHub Action's job number.

## Actions to Manage Scheduled Jobs Like Cron

There are many ways to trigger an action to execute. One clever thing I observed others do was to have something like a webhook from a third-party service create an issue and then use the creation of an issue to trigger the action to run.

Given my use case is primarily about metadata updates and new blog content, a daily update is sufficient. With GitHub Actions you can also configure scheduled jobs that run like a cron job would at regular intervals.

```yaml
on:
  schedule:
    - cron: '55 7 * * 1-5'        # Weekdays at 7:55
```

Every weekday morning since that's when I'm more likely to have updates the workflow will run and update the README with any new changes.

## Actions to Manage Merges

This last trigger is pretty straightforward, any time I change the content of the templates used to generate the README, I want that `push` and merge to trigger an update right away.

The action can run `on` that trigger as well:

```yaml
on:
  push:
```

## Take Action on Your Profile

I hope this overview helps you get acquainted with GitHub Actions, perhaps using your own GitHub Profile as a way to get familiar with a low-stakes project while building your portfolio.

If you missed an earlier part of the series, check them out for specific dives into the various tools used.

- [with the Liquid template engine and Shields](https://dev.to/j12y/setup-a-dynamic-github-profile-readme-3lbm) (Part 1 of 4)
- [using GitHub's GraphQL API to query dynamic data about all my repos](https://dev.to/j12y/setup-a-dynamic-github-profile-readme-3lbm) (Part 2 of 4)
- [fetching RSS blog post details from third-party sites](https://dev.to/j12y/fetching-blog-rss-feeds-for-building-a-github-profile-i31) (Part 3 of 4)

Let me know if you have any questions or were able to use this on your own profile or similar project.