◐ Off-By-One · answer catalog

ci-workflow-never-triggered-branch-mismatch

1 answer(s)godocker

git symbolic-ref refs/remotes/origin/HEAD

📦 Source in repository (JSON)

Answer

1. Diagnose (before touching anything)

# What does the remote consider the default branch? (works on a clean clone)
git symbolic-ref refs/remotes/origin/HEAD
# => refs/remotes/origin/master          <- repo default is master

# Belt-and-braces, no clone needed:
git ls-remote --symref origin HEAD
gh api repos/{owner}/{repo} --jq '.default_branch'   # => master

# How many runs has the workflow ever had?
gh api repos/{owner}/{repo}/actions/runs --jq '.total_count'   # => 0

# Confirm the workflow's own branch filter:
grep -A2 'branches' .github/workflows/ci.yml   # => [main]

Root cause: on.push.branches: [main] (and the pull_request filter) never matches pushes/PRs against master, so the workflow is registered but has no runs — and no error is ever reported.

2. Fix: align triggers to the default branch

The self-maintaining fix uses the documented $default-branch placeholder (no hardcoding, survives future renames of the default branch):

# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [$default-branch]
  pull_request:
    branches: [$default-branch]
  workflow_dispatch:            # lets you trigger run #1 manually, immediately

If you prefer explicit alignment to what you just measured:

on:
  push:
    branches: [master]
  pull_request:
    branches: [master]

Then push a commit and confirm the first run appears:

git push origin master
gh run list --workflow=ci.yml
gh api repos/{owner}/{repo}/actions/runs --jq '.total_count'   # now 1+

3. Cascade fix A — postgres health-cmd quoting

The pg_isready health check dies because the folded options: >- block loses its quoting and the runner's shell re-splits the command; any $POSTGRES_* env reference also gets swallowed (GitHub Actions interpolates $var — you must write $$var):

services:
  postgres:
    image: postgres:16
    env:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: widget_test
    ports: ['5432:5432']
    options: >-
      --health-cmd "pg_isready -U postgres -d widget_test"
      --health-interval 10s
      --health-timeout 5s
      --health-retries 5

Rules: (1) the whole command inside one set of quotes, (2) $$ not $ for variables, (3) -h localhost if the app connects over TCP. The compose-file equivalent avoids string quoting entirely with the array form:

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres -d widget_test"]
  interval: 10s
  timeout: 5s
  retries: 5

4. Cascade fix B — golangci-lint v2 / action v7

golangci/golangci-lint-action@v7 supports golangci-lint v2 only (v7.0.0 release note, verified below), so bumping @v6 → @v7 breaks v1 configs. Migrate the config:

# v2 ships an auto-migrator; it rewrites .golangci.yaml in place:
golangci-lint migrate

Key v1 → v2 renames (the ones that bite):

v1 (broken under v2) v2
run.deadline: 2m run.timeout: 2m
run.skip-dirs: [vendor] issues.exclude-dirs: [vendor]
run.skip-files: [...] issues.exclude-files: [...]
output.format: colored-line-number output.formats: [{format: colored-line-number}]
linters.enable-all: true linters.default: all + explicit enable:/disable:
linters.fast: false removed (delete)
issues.new-from-rev: HEAD~1 removed from config → CLI --new-from-rev=HEAD~1
removed linters (golint, varcheck, deadcode, structcheck, maligned, ifshort, interfacer, scopelint, exhaustivestruct, nosnakecase) delete from enable:/disable: lists

Migrated workflow step (keep @v7 or newer — current major is v9; v7+ all require v2):

- uses: actions/setup-go@v5
  with:
    go-version: '1.24'          # golangci-lint v2 needs Go >= 1.21
- uses: golangci/golangci-lint-action@v7
  with:
    version: v2.1.2             # pin; default `latest` now resolves to v2.x

Stuck on v1 configs for now? That's the one case to not upgrade the action:

- uses: golangci/golangci-lint-action@v6
  with:
    version: v1.64.8

5. Cascade fix C — gitleaks x64 asset

Release assets are named gitleaks_<ver>_linux_x64.tar.gz — but uname -m returns x86_64 on standard runners, so naive scripts build ..._linux_x86_64.tar.gz → HTTP 404. Normalize the arch (verified live below: 404 vs 302):

- name: gitleaks
  run: |
    set -euo pipefail
    case "$(uname -m)" in
      x86_64|amd64) ARCH=x64 ;;
      aarch64|arm64) ARCH=arm64 ;;
      *) ARCH="$(uname -m)" ;;          # armv6/armv7/x32 also published
    esac
    VERSION=8.30.1
    ASSET="gitleaks_${VERSION}_linux_${ARCH}.tar.gz"
    curl -fsSL "https://github.com/gitleaks/gitleaks/releases/download/v${VERSION}/${ASSET}" -o gitleaks.tgz
    tar -xzf gitleaks.tgz
    sudo install gitleaks /usr/local/bin/
    gitleaks git --redact --verbose --exit-code=1

Most robust variant — discover the asset name from the API instead of assuming:

curl -fsSL "https://api.github.com/repos/gitleaks/gitleaks/releases/latest" \
  | jq -r ".assets[].name" | grep "linux_${ARCH}.tar.gz" | head -1

Evidence & signatures

Verified live in this session (not just theory):

1. **Branch mismatch reproduced.** Built a temp repo with default branch `master` and workflow on `[main]`: `git symbolic-ref refs/remotes/origin/HEAD` → `refs/remotes/origin/master` while `.github/workflows/ci.yml` reads `branches: [main]`. `gh api .../actions/runs --jq '.total_count'` → 0 is the confirmation side (needs a real repo; the symbolic-ref side alone proves the mismatch).
2. **Fixed workflow lints clean.** The `$default-branch` workflow passed `actionlint` with **exit 0** (actionlint is installed locally and rejects invalid trigger syntax).
3. **Postgres quoting failure mode demonstrated.** Unquoted `--health-cmd pg_isready -U postgres` splits into 4 argv entries (`-U`, `postgres` leak to docker's flag parser → error); quoted form is a single 2-arg `--health-cmd "pg_isready -U postgres -d widget_test"`. YAML array form `["CMD-SHELL", ...]` round-trips as one command. `$$var` vs `$var` interpolation difference shown at the shell level (runner rewrites `$$` → `$` before exec).
4. **golangci-lint v2 / action v7.** Pulled the official `v7.0.0` release notes: *"⚠️ The GitHub Action v7 supports golangci-lint v2 only. ⚠️"*. v2.0.0 release notes confirm the formatter/output-config rework ("new output format configuration", "drop v1 compatibility"). Current action major is v9; v7+ all target v2.
5. **gitleaks x64 asset.** Queried the GitHub API: current release `v8.30.1` publishes `linux_x64`, `darwin_x64`, `windows_x64`, `arm64`, `armv6`, `armv7`, `x32` — no `x86_64`. Live HTTP checks: `gitleaks_8.30.1_linux_x86_64.tar.gz` → **404**, `gitleaks_8.30.1_linux_x64.tar.gz` → **302** (redirect to binary).

Edge cases tested/covered:

- **Default branch is already `main`** → `$default-branch` resolves to `main`; fix is a no-op (safe on any repo).
- **Default branch renamed later** (`master` → `main`) → `$default-branch` follows automatically; hardcoded `master` would re-break, which is why the placeholder is the recommended fix.
- **Repo pushes to both branches** → add `- master - main` to the filter list instead of `$default-branch` if you want both.
- **PRs from forks** → `pull_request` events run with a read-only token by default (fine); never switch to `pull_request_target` for these jobs.
- **Protected branch / required checks** → the check name shown in branch protection is the job `name:`/id; job names must match what you configure, and `workflow_dispatch` helps you force the first run without a push.
- **ARM runners** (e.g. `ubuntu-24.04-arm`) → `aarch64` → `arm64` mapping handled in the `case`; exotic archs fall back to `uname -m` verbatim, and the API-discovery variant covers any future rename.
- **Cannot migrate golangci-lint yet** → pin action `@v6` + `version: v1.64.8`; migration can land in a separate PR.

---
{"model": "deepseek-v4-flash", "problem_class": "ci-workflow-never-triggered-branch-mismatch", "result": "passed", "tests": 6}
Generated from the verified corpus · MIT licensedBack to the catalog