GitHub Actions' required properties aren't always required

Earlier today on Renovate we noticed that our documentation site had not been deploying for a couple of days.
The root cause was down to a quirk of GitHub Actions, where a required property isn't actually always enforced to be required π
To explain this, let's look at the following composite action:
# .github/actions/setup-node/action.yml
name: 'Setup Node and install dependencies'
description: 'Setup Node and install dependencies using cache'
inputs:
node-version:
description: 'Node version'
required: true
os:
description: 'Composite actions do not support `runner.os`, so it must be passed in as an input'
required: true
save-cache:
description: 'Save cache when needed'
required: false
default: 'false'
runs:
using: 'composite'
steps:
- name: Calculate `CACHE_KEY`
shell: bash
run: |
echo 'CACHE_KEY=node_modules-${{
inputs.os
}}-${{
inputs.node-version
}}-${{
hashFiles('pnpm-lock.yaml', 'package.json')
}}' >> "$GITHUB_ENV"
# ...
This is then used as part of our build:
# .github/workflows/build.yml
build-deploy-docs-site:
needs: release
permissions: # ...
environment: # ...
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
show-progress: false
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
node-version: ${{ needs.setup-build.outputs.node-version }}
os: ${{ runner.os }}
Can you spot what's wrong here?
You may notice this line:
node-version: ${{ needs.setup-build.outputs.node-version }}
We're referencing a job, but that wasn't found in our needs definition.
In this case, GitHub Actions is therefore evaluating this statement as the empty string, resolving as node-version: "".
Because there is technically something passed to the node-version argument, required: true isn't flagging any issues.
In my opinion, required: true should error if it's receiving empty input, but maybe this is instead a slightly painful gotcha, but one I'm now going to going to remember going forwards.
We've since added a check in our composite action to fail the build if either required: true options are unset.
It looks like actionlint would have caught this too - so that's on my TODO list to add to our CI pipelines now, too!