{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreifymg3sfu7qk2ocbtswq27z56n4m7xaectp5yi5ay4tpp7u6qdf7a",
    "uri": "at://did:plc:wszrgoqdwy3i2dfeub2mt3wf/app.bsky.feed.post/3mf4yknh6nhz2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreiep5dsa2ip64fanl23ckaegh7pnbzyisteupyynsmg2humj63gwc4"
    },
    "mimeType": "image/png",
    "size": 13001
  },
  "description": "A gotcha with how `required: true` allows an empty string as valid input.",
  "path": "/posts/2026/02/18/github-actions-required/",
  "publishedAt": "2026-02-18T11:28:31.000Z",
  "site": "https://www.jvt.me",
  "tags": [
    "blogumentation",
    "github",
    "github-actions",
    "Renovate",
    "actionlint"
  ],
  "textContent": "Earlier today on Renovate we noticed that our documentation site had not been deploying for a couple of days.\n\nThe root cause was down to a quirk of GitHub Actions, where a `required` property isn't actually always enforced to be required 😅\n\nTo explain this, let's look at the following composite action:\n\n\n    # .github/actions/setup-node/action.yml\n    name: 'Setup Node and install dependencies'\n    description: 'Setup Node and install dependencies using cache'\n    inputs:\n      node-version:\n        description: 'Node version'\n        required: true\n      os:\n        description: 'Composite actions do not support `runner.os`, so it must be passed in as an input'\n        required: true\n      save-cache:\n        description: 'Save cache when needed'\n        required: false\n        default: 'false'\n    runs:\n      using: 'composite'\n      steps:\n        - name: Calculate `CACHE_KEY`\n          shell: bash\n          run: |\n            echo 'CACHE_KEY=node_modules-${{\n              inputs.os\n            }}-${{\n              inputs.node-version\n            }}-${{\n              hashFiles('pnpm-lock.yaml', 'package.json')\n            }}' >> \"$GITHUB_ENV\"\n\n         # ...\n\n\nThis is then used as part of our build:\n\n\n    # .github/workflows/build.yml\n      build-deploy-docs-site:\n        needs: release\n\n        permissions: # ...\n\n        environment: # ...\n\n        runs-on: ubuntu-latest\n        steps:\n          - name: Checkout code\n            uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n            with:\n              show-progress: false\n\n          - name: Setup Node.js\n            uses: ./.github/actions/setup-node\n            with:\n              node-version: ${{ needs.setup-build.outputs.node-version }}\n              os: ${{ runner.os }}\n\n\nCan you spot what's wrong here?\n\nYou may notice this line:\n\n\n              node-version: ${{ needs.setup-build.outputs.node-version }}\n\n\nWe're referencing a job, but that wasn't found in our `needs` definition.\n\nIn this case, GitHub Actions is therefore evaluating this statement as the empty string, resolving as `node-version: \"\"`.\n\nBecause there is _technically_ something passed to the `node-version` argument, `required: true` isn't flagging any issues.\n\nIn 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.\n\nWe've since added a check in our composite action to fail the build if either `required: true` options are unset.\n\nIt looks like actionlint would have caught this too - so that's on my TODO list to add to our CI pipelines now, too!",
  "title": "GitHub Actions' required properties aren't always required",
  "updatedAt": "2026-02-18T11:28:31.000Z"
}