diff --git a/errors/concurrency-timing/ct-105.yml b/errors/concurrency-timing/ct-105.yml new file mode 100644 index 0000000..8e49369 --- /dev/null +++ b/errors/concurrency-timing/ct-105.yml @@ -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' diff --git a/errors/triggers/tr-119.yml b/errors/triggers/tr-119.yml new file mode 100644 index 0000000..68b8001 --- /dev/null +++ b/errors/triggers/tr-119.yml @@ -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' diff --git a/errors/yaml-syntax/ys-121.yml b/errors/yaml-syntax/ys-121.yml new file mode 100644 index 0000000..aadc1d0 --- /dev/null +++ b/errors/yaml-syntax/ys-121.yml @@ -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..steps[*].uses`) and + reusable workflow job calls (`jobs..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..steps[*].uses syntax' + - url: 'https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_iduses' + label: 'GitHub Actions — jobs..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'