Computed values in GitHub actions
TIL: How to setup the configuration of a GitHub actions workflow so that steps can refer to values computed during the action.
In a couple of workflows, I’ve wanted to be able to read some configuration file or otherwise compute a value and then have that value available in the job that runs. AFAICT, the input of one step can’t read the output of another step (in the same job), so how can we do this?
Today,I didn’t actually learn it today, but I had to find my notes and re-learn it today, so I’m trying to make some better notes. I wanted to write a workflow to build DocBook: The Definitive Guide. In order to know where to put the output, I have to know the DocBook version being documented.
The canonical place to look for that is in the gradle.properties
file:
docbookBaseVersion=5.2
(There are other things in there, but they aren’t relevant.)
The document being formatted is, by definition, documenting that version of DocBook.
The trick in the GitHub workflow is to use two jobs. You can declare that the output of one job is available in subsequent jobs. Here’s the workflow:
name: build-specs
on: push
jobs:
load_config:
runs-on: ubuntu-latest
outputs:
dbversion: ${{ steps.load.outputs.version }}
The first job is named load_config
and has an
outputs
section where we declare what names this job will output.
I’m declaring that this job will produce a value called dbversion
and that
the value will come from the version
output of the step named load
in this job.
steps:
- name: Checkout
uses: actions/checkout@v3
I have to checkout the branch to read the property file.
- name: Load the configuration
id: load
run: |
echo version=`cat gradle.properties \
| grep "^docbookBaseVersion" \
| cut -f2 -d=` >> $GITHUB_OUTPUT
And here’s the magic. This step has the id load
(that’s the “load”
part of steps.load.outputs.version
above). When it runs, it writes
name/value pairs by appending them to the magic file
$GITHUB_OUTPUT
. You can output as many pairs as you want, but you
must declare them all in the outputs:
section.
That’s the end of this job. Now the next job:
build-and-deploy:
runs-on: ubuntu-latest
needs: load_config
We begin by saying this job “needs” the load_config
job. In this job,
we can refer to the version number computed by the load_config
job using the
special expression needs.load_config.outputs.dbversion
.
env:
DBVERSION: ${{ needs.load_config.outputs.dbversion }}
I’m copying the computed value into an environment variable. This isn’t
strictly necessary, but it’s easier to type env.DBVERSION
than
needs.load_config.outputs.dbversion
and having these kinds of
computed values in environment variables is conceptually familiar.
steps:
- name: Checkout the specifications
uses: actions/checkout@v3
This job checks out the sources too, though I’m not actually using them in this example.
- name: What version is this?
run: |
echo DocBook version ${{ env.DBVERSION }}
Finally, this task prints the DocBook version, demonstrating that we computed it successfully!
Here’s the complete workflow, for reference:
name: build-specs
on: push
jobs:
load_config:
runs-on: ubuntu-latest
outputs:
dbversion: ${{ steps.load.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Load the configuration
id: load
run: |
echo version=`cat gradle.properties \
| grep "^docbookBaseVersion" \
| cut -f2 -d=` >> $GITHUB_OUTPUT
build-and-deploy:
runs-on: ubuntu-latest
needs: load_config
env:
DBVERSION: ${{ needs.load_config.outputs.dbversion }}
steps:
- name: Checkout the specifications
uses: actions/checkout@v3
- name: What version is this?
run: |
echo DocBook version ${{ env.DBVERSION }}