Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions errors/concurrency-timing/ct-105.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
id: ct-105
title: 'Environment Protection Approval Gate Holds `cancel-in-progress: false` Concurrency Slot — Intermediate Pushes Silently Dropped'
category: concurrency-timing
severity: silent-failure
tags:
- concurrency
- cancel-in-progress
- environment-protection
- approval-gate
- human-review
- starvation
- deployment
- silent-failure
patterns:
- regex: 'cancel-in-progress:\s*false'
flags: 'i'
- regex: 'environment:\s*\n\s+name:\s*\w+'
flags: 'im'
- regex: 'Canceling since a higher priority waiting request'
flags: 'i'
error_messages:
- 'Canceling since a higher priority waiting request for'
- 'This run was waiting for a concurrent run to finish'
- 'Run was cancelled'
root_cause: |
When a deployment workflow uses `concurrency.cancel-in-progress: false` at the
workflow or job level AND the workflow targets an environment with manual approval
protection rules (required reviewers), the first pending run occupies the single
available "pending" slot in the concurrency queue indefinitely while awaiting
human review.

GitHub Actions concurrency with `cancel-in-progress: false` allows exactly one
run to be executing and at most one run to be pending — a maximum of two active
runs per group. Any third or subsequent run that arrives while the queue is full
(one running, one pending-approval) is immediately cancelled with "Canceling since
a higher priority waiting request exists."

Unlike a wait-timer (which resolves automatically after a configured delay),
an approval gate can remain unresolved for hours or days. Every push to the
protected branch during that approval window is silently discarded. When the
approver finally reviews, they approve the OLDEST queued run, not the most
recent code — potentially deploying a version that is now out of date.
fix: |
Choose between two strategies depending on whether you want the latest code or
ordered deployments:

**Option A (deploy latest, cancel stale):** Use `cancel-in-progress: true`.
Each new push cancels the previously pending approval, so approvers always
review the current code. The trade-off is that the approver must re-approve
after every new push.

**Option B (ordered deployments, no drops):** Split the workflow into two
separate jobs. Use `cancel-in-progress: true` on a pre-deployment job (lint,
test, artifact build) and a separate concurrency group with `cancel-in-progress:
false` only on the final deployment job that targets the environment. The test
jobs race and only the winner enters the deployment queue.

**Option C (awareness only):** Keep `cancel-in-progress: false` but add a
notification step that alerts on cancellation so the team knows intermediate
commits were skipped.
fix_code:
- language: yaml
label: 'Option A — cancel-in-progress: true so approvers always see latest code'
code: |
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true # pending approval is cancelled when newer push arrives

jobs:
deploy:
environment: production # has required reviewers
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh
- language: yaml
label: 'Option B — split jobs: CI races, deployment queues in order'
code: |
jobs:
build:
runs-on: ubuntu-latest
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true # only the latest build matters
steps:
- run: ./build.sh
- uses: actions/upload-artifact@v4
with:
name: artifact-${{ github.sha }}
path: dist/

deploy:
needs: build
runs-on: ubuntu-latest
environment: production # approval gate lives here
concurrency:
group: deploy-production # single slot; ordered, no drops
cancel-in-progress: false
steps:
- uses: actions/download-artifact@v4
with:
name: artifact-${{ github.sha }}
- run: ./deploy.sh
prevention:
- 'Never combine `cancel-in-progress: false` with an environment that has human approval rules unless you explicitly want ordered deployment of every commit'
- 'Document in the workflow file which combination of concurrency strategy and environment protection is in use and why'
- 'Add a Slack/Teams notification on run cancellation so the team knows commits were skipped and can manually trigger a deployment if needed'
- 'Consider `cancel-in-progress: true` for non-production environments (staging) where you always want the latest code, and only use `cancel-in-progress: false` for production where you want ordered deploys'
docs:
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-concurrency'
label: 'GitHub Actions — Using concurrency'
- url: 'https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment'
label: 'GitHub Actions — Managing environments for deployment'
- url: 'https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/reviewing-deployments'
label: 'GitHub Actions — Reviewing deployments'
106 changes: 106 additions & 0 deletions errors/triggers/tr-119.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
id: tr-119
title: '`repository_dispatch` `client_payload` Property Names Containing Hyphens or Special Characters Are Inaccessible via Expression Syntax'
category: triggers
severity: silent-failure
tags:
- repository_dispatch
- client_payload
- expression
- property-access
- hyphen
- dot-notation
- silent-failure
- api
patterns:
- regex: 'github\.event\.client_payload\.[a-z0-9_]*-[a-z0-9_]'
flags: 'i'
- regex: 'client_payload.*[''"][a-z0-9_-]*-[a-z0-9_-]*[''"]'
flags: 'i'
error_messages:
- 'Unrecognized named-value'
- 'Expected ''}''. Found ''-'''
- 'Variable named ''build'' is not defined'
root_cause: |
GitHub Actions expression syntax supports only dot-notation property access
(`object.property`) and does not support bracket notation (`object['key']`).
When a `repository_dispatch` event is sent with a `client_payload` that contains
JSON keys using hyphens, dots, spaces, or other characters that are not valid
identifiers, those properties cannot be accessed in a workflow expression.

For example, dispatching with:
client_payload: {"build-type": "release", "target.env": "prod"}

means that `${{ github.event.client_payload.build-type }}` is a parse error:
the `-` is interpreted as a subtraction operator, producing "build" minus "type",
and the expression either errors or evaluates to a nonsensical number.
`${{ github.event.client_payload.target.env }}` silently evaluates as a nested
property access on an undefined sub-object and returns an empty string.

There is no bracket-notation workaround within a `${{ }}` expression. The
`fromJSON()` / `toJSON()` functions do not help because they do not provide
bracket-style access within the expression engine.

The only reliable fix is to restructure `client_payload` to use snake_case or
camelCase keys, or to parse the raw payload in a shell step via `jq`.
fix: |
Use only snake_case or camelCase property names in `client_payload` so they
are valid identifier tokens accessible via dot notation.

If you need to parse an existing payload with non-identifier keys, write the
payload to a file and parse it with `jq` in a `run:` step — the expression
engine is bypassed and all key names work.
fix_code:
- language: yaml
label: 'Use snake_case keys in client_payload (API caller side)'
code: |
# When triggering via GitHub REST API, use snake_case keys:
# POST /repos/{owner}/{repo}/dispatches
# {
# "event_type": "deploy",
# "client_payload": {
# "build_type": "release", <-- was: "build-type"
# "target_env": "production" <-- was: "target.env"
# }
# }

# Workflow can now access these safely:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Read dispatch inputs
run: |
echo "Build type: ${{ github.event.client_payload.build_type }}"
echo "Target env: ${{ github.event.client_payload.target_env }}"
- language: yaml
label: 'Parse non-identifier keys with jq in a run step'
code: |
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Parse client_payload with jq
id: payload
run: |
# Write payload to file to use jq (bypasses expression engine)
echo '${{ toJSON(github.event.client_payload) }}' > payload.json
BUILD_TYPE=$(jq -r '.["build-type"]' payload.json)
TARGET_ENV=$(jq -r '.["target.env"]' payload.json)
echo "build_type=$BUILD_TYPE" >> $GITHUB_OUTPUT
echo "target_env=$TARGET_ENV" >> $GITHUB_OUTPUT

- name: Use parsed values
run: |
echo "Building ${{ steps.payload.outputs.build_type }} for ${{ steps.payload.outputs.target_env }}"
prevention:
- 'Always use snake_case or camelCase for all `client_payload` keys in `repository_dispatch` calls — never use hyphens, dots, spaces, or other non-identifier characters'
- 'Document your `client_payload` schema in the workflow file using a comment so callers know which key format is expected'
- 'Add input validation in the workflow to fail-fast if expected payload keys resolve to empty strings'
- 'Use `actionlint` with `--format` checking — it will flag expression syntax errors caused by hyphenated property access'
docs:
- url: 'https://docs.github.com/en/rest/repos/repos#create-a-repository-dispatch-event'
label: 'GitHub REST API — Create a repository dispatch event'
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions'
label: 'GitHub Actions — Evaluate expressions in workflows and actions'
- url: 'https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet'
label: 'GitHub Actions — Workflow syntax: on.repository_dispatch'
107 changes: 107 additions & 0 deletions errors/yaml-syntax/ys-121.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
id: ys-121
title: '`uses:` Field Does Not Evaluate `${{ }}` Expressions — Dynamic Action or Reusable Workflow Versions Are Not Possible'
category: yaml-syntax
severity: error
tags:
- uses
- expressions
- dynamic-version
- action-version
- reusable-workflow
- vars-context
- limitation
- parse-error
patterns:
- regex: 'uses:\s+[a-zA-Z0-9_./-]+@\$\{\{'
flags: 'i'
- regex: 'uses:\s+\$\{\{.*\}\}'
flags: 'i'
- regex: 'uses:\s+[a-zA-Z0-9_./-]*\$\{\{'
flags: 'i'
error_messages:
- 'The uses clause must not contain a string template'
- 'Expected format''{owner}/{repo}@{ref}'''
- 'Unrecognized named-value'
- 'Invalid format for ''uses'''
root_cause: |
The `uses:` field in both step definitions (`jobs.<id>.steps[*].uses`) and
reusable workflow job calls (`jobs.<id>.uses`) is evaluated at workflow
**parse time**, before the expression engine runs. GitHub Actions does not
substitute `${{ }}` expressions in `uses:` values — the literal string
(including the dollar-brace syntax) is passed to the action/workflow resolver,
which fails to find a valid reference.

This is a deliberate security constraint: allowing expressions in `uses:` would
enable repository variables, secrets, or environment-dependent values to redirect
execution to an attacker-controlled action or workflow version, creating a
supply-chain attack vector. The parser enforces this by rejecting any `uses:`
value containing `${{`.

Common patterns that fail:
uses: actions/checkout@${{ vars.CHECKOUT_VERSION }}
uses: ${{ vars.MY_ORG }}/custom-action@v2
uses: ./.github/actions/${{ matrix.action_name }}

All of these produce a parse error or a resolution failure before the workflow run starts.
fix: |
Pin `uses:` values to literal strings. Manage version consistency using tooling
rather than runtime expressions:

- Use **Dependabot** (`dependabot.yml`) to automatically create PRs that bump
action versions across all workflow files when a new release is published.
- Use **Renovate Bot** for finer-grained version control policies.
- For local composite actions, reference them with a literal relative path:
`uses: ./.github/actions/my-action`
- For SHA pinning (strongest supply-chain guarantee), pin to the full commit SHA:
`uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683`

If you need to conditionally run different actions, use an `if:` condition on
separate steps rather than a dynamic `uses:` reference.
fix_code:
- language: yaml
label: 'Use literal action version strings — no expressions in uses:'
code: |
steps:
# BAD: expressions are not evaluated in uses: — parse error
# - uses: actions/checkout@${{ vars.CHECKOUT_VERSION }}
# - uses: ${{ vars.CUSTOM_ORG }}/deploy-action@v3
# - uses: ./.github/actions/${{ matrix.action }}

# GOOD: literal version pins
- uses: actions/checkout@v4.2.2
- uses: actions/setup-node@v4.1.0

# GOOD: conditional execution via if: instead of dynamic uses:
- uses: my-org/deploy-staging@v2
if: ${{ vars.ENVIRONMENT == 'staging' }}
- uses: my-org/deploy-production@v2
if: ${{ vars.ENVIRONMENT == 'production' }}
- language: yaml
label: 'Dependabot config to auto-update action versions'
code: |
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
github-actions:
patterns:
- "actions/*"
- "github/*"
prevention:
- 'Run `actionlint` in CI — it detects `${{ }}` expressions in `uses:` fields and fails the pipeline before the workflow reaches production'
- 'Configure Dependabot or Renovate to automate action version bumps so you never need to parameterize versions at runtime'
- 'Use SHA-pinning for critical workflows to freeze the exact action version regardless of tag mutations'
- 'For local reusable actions, use relative paths (`uses: ./.github/actions/name`) which are always resolved from the repository root without expressions'
docs:
- url: 'https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsuses'
label: 'GitHub Actions — jobs.<job_id>.steps[*].uses syntax'
- url: 'https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_iduses'
label: 'GitHub Actions — jobs.<job_id>.uses (reusable workflow)'
- url: 'https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot'
label: 'Keeping your actions up to date with Dependabot'
- url: 'https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#using-third-party-actions'
label: 'Security hardening for GitHub Actions — using third-party actions'
Loading