From e85e21e6dd371166c829fcc016d87eec5b12dcf1 Mon Sep 17 00:00:00 2001 From: Hector Flores Date: Sat, 13 Jun 2026 09:20:49 -0500 Subject: [PATCH] feat: add 3 new error entries (concurrency-timing x1, triggers x1, yaml-syntax x1) --- ...ped-concurrency-cross-branch-collision.yml | 130 +++++++++++++++++ ...ed-type-fires-before-upstream-executes.yml | 131 ++++++++++++++++++ ...h-contains-case-insensitive-expression.yml | 101 ++++++++++++++ 3 files changed, 362 insertions(+) create mode 100644 errors/concurrency-timing/environment-deployment-branch-scoped-concurrency-cross-branch-collision.yml create mode 100644 errors/triggers/workflow-run-requested-type-fires-before-upstream-executes.yml create mode 100644 errors/yaml-syntax/startswith-endswith-contains-case-insensitive-expression.yml diff --git a/errors/concurrency-timing/environment-deployment-branch-scoped-concurrency-cross-branch-collision.yml b/errors/concurrency-timing/environment-deployment-branch-scoped-concurrency-cross-branch-collision.yml new file mode 100644 index 0000000..bd02803 --- /dev/null +++ b/errors/concurrency-timing/environment-deployment-branch-scoped-concurrency-cross-branch-collision.yml @@ -0,0 +1,130 @@ +id: ct-104 +title: 'Branch-Scoped Concurrency Group Allows Simultaneous Deployments to the Same Environment from Different Branches' +category: concurrency-timing +severity: silent-failure +tags: + - concurrency + - environment + - deployment + - branch + - github-ref + - cross-branch + - concurrent-deploy + - job-environment +patterns: + - regex: 'group:\s*[''"]?[^''"\n]*\$\{\{\s*github\.ref[^}]*\}\}[^''"\n]*' + flags: 'i' + - regex: 'group:\s*[''"]?[^''"\n]*\$\{\{\s*github\.ref_name[^}]*\}\}[^''"\n]*' + flags: 'i' + - regex: 'group:\s*[''"]?[^''"\n]*\$\{\{\s*github\.head_ref[^}]*\}\}[^''"\n]*' + flags: 'i' +error_messages: + - "# No error — two branches deploy to the same environment simultaneously; may cause partial state or deployment races" +root_cause: | + Workflows that include `github.ref`, `github.ref_name`, or `github.head_ref` in + their concurrency group key create **separate concurrency groups per branch**. When + two branches push to a workflow that deploys to the same environment, they get + different concurrency groups and do NOT queue or cancel each other. + + Example: + - Branch `feature/alpha` pushes → group: `deploy-staging-refs/heads/feature/alpha` + - Branch `main` pushes → group: `deploy-staging-refs/heads/main` + - Groups are different → both jobs run simultaneously → both deploy to `staging` + + This pattern is correct for CI workflows (each branch's tests should run + independently), but incorrect for shared deployment environments where only one + deployment should be active at a time. + + The confusion is common because teams copy a per-branch concurrency pattern from + CI into deploy workflows without adjusting the key. Environment protection rules + (required reviewers, wait timers) gate each individual job but do NOT prevent + multiple simultaneous deployments from different concurrency groups. + + GitHub Actions provides the `job.environment` context — the environment name string + for the current job — which creates a stable, per-environment concurrency key that + applies across all branches deploying to that environment. +fix: | + Key the concurrency group on the environment name, not the branch ref, so all + branches deploying to the same environment share one concurrency slot: + + concurrency: + group: deploy-${{ job.environment }} + cancel-in-progress: false # queue; do not discard deploys + + Important: `job.environment` is only populated inside a job that declares + `environment:`. Set the concurrency group at the **job level**, not at the + workflow level, when using `job.environment`. + + Use `cancel-in-progress: false` for deployments to ensure every triggered deploy + runs in order rather than being silently dropped. +fix_code: + - language: yaml + label: 'WRONG — branch-scoped group; feature/ and main can deploy to staging simultaneously' + code: | + on: [push] + + jobs: + deploy: + runs-on: ubuntu-latest + environment: staging + concurrency: + # BAD: different branches get different concurrency slots + # feature/alpha and main can both deploy to staging at the same time + group: deploy-staging-${{ github.ref }} + cancel-in-progress: true + steps: + - run: ./deploy.sh staging + - language: yaml + label: 'CORRECT — environment-scoped group; only one deploy to staging at a time' + code: | + on: [push] + + jobs: + deploy: + runs-on: ubuntu-latest + environment: staging + concurrency: + # GOOD: all branches deploying to staging share one concurrency slot + # job.environment is the environment name ("staging") + group: deploy-${{ job.environment }} + cancel-in-progress: false # queue — do not skip any deploys + steps: + - run: ./deploy.sh staging + - language: yaml + label: 'MULTI-ENV — staging and production each get their own independent slot' + code: | + on: + push: + branches: [main] + + jobs: + deploy-staging: + runs-on: ubuntu-latest + environment: staging + concurrency: + group: deploy-${{ job.environment }} # "staging" slot + cancel-in-progress: false + steps: + - run: ./deploy.sh staging + + deploy-production: + needs: deploy-staging + runs-on: ubuntu-latest + environment: production + concurrency: + group: deploy-${{ job.environment }} # "production" slot (separate) + cancel-in-progress: false + steps: + - run: ./deploy.sh production +prevention: + - 'For deployment workflows, key concurrency groups on the environment name (`job.environment`), not on the branch ref.' + - 'Use `cancel-in-progress: false` for deployment jobs — silently dropping a deploy means a commit never reaches the environment.' + - 'Apply per-branch concurrency groups to CI jobs only; apply per-environment concurrency groups to deploy jobs.' + - 'Set job-level `concurrency:` (not workflow-level) when using `job.environment`, since that context is only available within a job.' +docs: + - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-concurrency' + label: 'GitHub Docs — Using concurrency' + - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#job-context' + label: 'GitHub Docs — job context (job.environment)' + - url: 'https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment' + label: 'GitHub Docs — Managing environments for deployment' diff --git a/errors/triggers/workflow-run-requested-type-fires-before-upstream-executes.yml b/errors/triggers/workflow-run-requested-type-fires-before-upstream-executes.yml new file mode 100644 index 0000000..841da6e --- /dev/null +++ b/errors/triggers/workflow-run-requested-type-fires-before-upstream-executes.yml @@ -0,0 +1,131 @@ +id: tr-118 +title: '`workflow_run` `requested` Activity Type Fires When Upstream Is Queued — Before Execution, Artifacts Unavailable' +category: triggers +severity: silent-failure +tags: + - workflow_run + - requested + - activity-types + - artifacts + - default-branch + - timing + - pre-execution +patterns: + - regex: 'types:\s*\[.*\brequested\b' + flags: 'si' + - regex: 'types:\s*\n(\s+- [^\n]+\n)*\s+- requested' + flags: 'si' +error_messages: + - "Unable to find artifact" + - "Error: Artifact not found for associated workflow run" + - "No artifact found with name" +root_cause: | + The `workflow_run` event supports three activity types: `completed`, `in_progress`, + and `requested`. The `requested` type fires when the upstream workflow run is + **queued** — before any steps have executed. + + Developers who use `types: [requested]` expecting to access upstream workflow + results (artifacts, job outputs, step conclusions) will find nothing is available. + The upstream workflow has not run yet. + + Key behaviors of the `requested` type: + + 1. **No artifacts** — The upstream workflow has not uploaded anything yet. + 2. **Null conclusion** — `github.event.workflow_run.conclusion` is `null` at + request time; the run has not completed. + 3. **Fires for subsequently cancelled runs** — If the upstream workflow is cancelled + before execution (e.g., superseded by a concurrency group), the downstream + `requested`-triggered workflow still runs to completion, wasting runner minutes + processing a run that never produced output. + 4. **Default-branch requirement** — Like ALL `workflow_run` triggers, the listening + workflow must be on the repository's default branch. Even if the upstream runs + on a feature branch, the downstream always executes from the default branch. + 5. **No upstream-blocking capability** — `workflow_run` is downstream-only. A + `requested` listener cannot pause or influence the upstream workflow. + + The docs say `requested` fires "when a workflow run is requested", which developers + interpret as "just before my CI runs — time to prepare". In practice, it fires the + moment the run is queued, with zero upstream data available. +fix: | + Use `completed` to access upstream artifacts, outputs, and conclusion: + + on: + workflow_run: + workflows: ["CI"] + types: [completed] + + Use `in_progress` to react as soon as the upstream workflow begins executing + (artifacts still unavailable but the run is confirmed started and not cancelled). + + Use `requested` only for pure notification/audit workflows that need no upstream + results — for example, posting "CI run queued" to a webhook using only metadata + fields like `github.event.workflow_run.html_url` and `github.event.workflow_run.id`. + + If you need to set up resources BEFORE an upstream workflow runs, `workflow_run` + is not the right tool — it cannot block the upstream. Use job-dependency ordering + within a single workflow, or a `repository_dispatch` from a setup job. +fix_code: + - language: yaml + label: 'WRONG — requested type; artifacts unavailable; upstream may be queued but not run' + code: | + on: + workflow_run: + workflows: ["Build"] + types: [requested] # fires when Build is QUEUED, not when it finishes + + jobs: + process: + runs-on: ubuntu-latest + steps: + # FAILS: No artifact exists — Build has not executed yet + - uses: actions/download-artifact@v4 + with: + run-id: ${{ github.event.workflow_run.id }} + name: build-output + - language: yaml + label: 'CORRECT — use completed type and guard on conclusion' + code: | + on: + workflow_run: + workflows: ["Build"] + types: [completed] # fires AFTER Build finishes (artifacts available) + + jobs: + process: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + run-id: ${{ github.event.workflow_run.id }} + name: build-output + - run: echo "Processing build artifact..." + - language: yaml + label: 'VALID use of requested — notification only (no upstream artifacts needed)' + code: | + on: + workflow_run: + workflows: ["CI"] + types: [requested] # appropriate: notification only, no artifacts needed + + jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Post CI started notification + run: | + curl -s -X POST "$WEBHOOK_URL" \ + -d "CI run queued: ${{ github.event.workflow_run.html_url }}" + env: + WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} +prevention: + - 'Use `completed` type when your downstream workflow needs upstream artifacts, outputs, or conclusion.' + - 'Use `in_progress` to react as soon as the upstream workflow begins executing (no artifacts yet, but run is confirmed started).' + - '`requested` is only appropriate for workflows that need NO upstream results — pure notifications or audit logs.' + - 'Always guard `completed`-triggered jobs with `if: github.event.workflow_run.conclusion == ''success''` to skip failed upstream runs.' + - 'Remember: all `workflow_run` listeners execute from the default branch regardless of the upstream workflow''s branch.' +docs: + - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_run' + label: 'GitHub Docs — workflow_run event and activity types' + - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#using-data-from-the-triggering-workflow' + label: 'GitHub Docs — Using data from the triggering workflow' diff --git a/errors/yaml-syntax/startswith-endswith-contains-case-insensitive-expression.yml b/errors/yaml-syntax/startswith-endswith-contains-case-insensitive-expression.yml new file mode 100644 index 0000000..21e135a --- /dev/null +++ b/errors/yaml-syntax/startswith-endswith-contains-case-insensitive-expression.yml @@ -0,0 +1,101 @@ +id: ys-120 +title: '`startsWith()`, `endsWith()`, and `contains()` Are Case-Insensitive in GitHub Actions Expressions' +category: yaml-syntax +severity: silent-failure +tags: + - expressions + - startsWith + - endsWith + - contains + - case-insensitive + - if-condition +patterns: + - regex: '\$\{\{[^}]*\bstartsWith\s*\([^}]*\}\}' + flags: 'i' + - regex: '\$\{\{[^}]*\bendsWith\s*\([^}]*\}\}' + flags: 'i' + - regex: '\$\{\{[^}]*\bcontains\s*\([^}]*\}\}' + flags: 'i' +error_messages: + - "# No error message — case-insensitive match succeeds silently when developer expects case-sensitive behavior" +root_cause: | + The `startsWith()`, `endsWith()`, and `contains()` functions in GitHub Actions + expressions are **case-insensitive** — they match regardless of letter casing. This + is documented but commonly overlooked, because the same functions in most programming + languages (JavaScript, Python, Go) perform case-sensitive comparisons. + + Example: `startsWith('refs/heads/Release/1.0', 'refs/heads/release/')` evaluates + to `true` despite the capital `R` in `Release`, because the comparison ignores case. + + This causes two categories of problems: + + 1. **Unintended matches** — A condition meant to fire only for lowercase `release/` + branches also fires for `RELEASE/`, `Release/`, or any mixed-case variant. Branch + protection gates, deploy guards, and environment filters may activate unexpectedly. + + 2. **Misleading specificity** — Developers write precise-looking conditions such as + `startsWith(github.ref, 'refs/heads/Hotfix/')` assuming only that exact casing + matches, but the condition matches `hotfix/`, `HOTFIX/`, and every other variant. + + GitHub Actions expressions use `System.String.StartsWith` with `OrdinalIgnoreCase` + internally, making these three functions behave differently from most languages. +fix: | + To perform a **case-sensitive** comparison, use the `==` operator directly, or + normalize casing with `toLower()` / `toUpper()` before comparing: + + - Exact case-sensitive equality: `github.ref == 'refs/heads/release/1.0'` + - Explicit case-insensitive prefix: `startsWith(toLower(github.ref), 'refs/heads/release/')` + (use `toLower()` to document the intent explicitly, even though `startsWith` is + already case-insensitive without it) + + If you WANT case-insensitive matching, no change is needed — just add a comment + to document that the behavior is intentionally case-insensitive so future + maintainers do not "fix" it. +fix_code: + - language: yaml + label: 'SURPRISING — startsWith matches Release/ even though pattern is lowercase release/' + code: | + on: [push] + jobs: + deploy: + runs-on: ubuntu-latest + # Developer assumes this only matches refs/heads/release/* (lowercase) + # ACTUAL: also matches refs/heads/Release/*, refs/heads/RELEASE/*, etc. + if: ${{ startsWith(github.ref, 'refs/heads/release/') }} + steps: + - run: echo "Deploying release branch" + - language: yaml + label: 'EXPLICIT — toLower() makes case-insensitive intent obvious' + code: | + on: [push] + jobs: + deploy: + runs-on: ubuntu-latest + # toLower() is redundant (startsWith is already case-insensitive) + # but documents the intent clearly for future maintainers + if: ${{ startsWith(toLower(github.ref), 'refs/heads/release/') }} + steps: + - run: echo "Deploying release branch (any casing)" + - language: yaml + label: 'CASE-SENSITIVE — use == for exact case-sensitive matches' + code: | + on: [push] + jobs: + deploy-exact: + runs-on: ubuntu-latest + # Only matches exact lowercase ref — use == not startsWith + if: ${{ github.ref == 'refs/heads/release/1.0' }} + steps: + - run: echo "Deploying exactly refs/heads/release/1.0" +prevention: + - 'Treat `startsWith()`, `endsWith()`, and `contains()` as case-insensitive in GitHub Actions — document this in workflow comments when the behavior matters.' + - 'Use `toLower()` before comparisons when you want to make case-normalization visible to future readers.' + - 'Use `==` for exact case-sensitive equality checks instead of `startsWith`/`endsWith` when casing must be preserved.' + - 'Test branch-name filters with uppercase and mixed-case branch names to confirm the intended match behavior.' +docs: + - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#startswith' + label: 'GitHub Docs — startsWith function (case-insensitive note)' + - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#endswith' + label: 'GitHub Docs — endsWith function (case-insensitive note)' + - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#contains' + label: 'GitHub Docs — contains function (case-insensitive note)'