CI/CD Debugging Scenarios
Practical debugging playbooks for GitHub Actions, ArgoCD, and Jenkins.
1. GitHub Actions: OIDC Auth to AWS Fails
Symptom: Error assuming role or Token is not valid when using aws-actions/configure-aws-credentials.
flowchart TD
classDef err fill:#e74c3c,stroke:#c0392b,color:#fff
classDef decision fill:#f39c12,stroke:#ba6018,color:#fff
classDef fix fill:#27ae60,stroke:#1e8449,color:#fff
classDef verify fill:#3498db,stroke:#2471a3,color:#fff
A["OIDC auth fails:<br/>Error assuming role /<br/>Token is not valid"]:::err --> B
B{"Audience claim<br/>matches trust policy?"}:::decision
B -- No --> B1["Set audience to<br/>sts.amazonaws.com"]:::fix
subgraph TRUST["Trust policy verification"]
B -- Yes --> C{"Subject condition<br/>in trust policy?"}:::decision
C -- Mismatch --> C1["Fix repo/branch in<br/>the Condition block"]:::fix
C -- OK --> D{"OIDC provider<br/>thumbprint valid?"}:::decision
D -- Stale --> D1["Update thumbprint in<br/>the IAM OIDC provider"]:::fix
end
D -- OK --> E{"Region in<br/>role ARN correct?"}:::decision
E -- Wrong --> E1["Fix the region<br/>segment of the ARN"]:::fix
E -- OK --> F["Auth succeeds"]:::verify
audience field in the workflow's configure-aws-credentials step must exactly match what the trust policy expects — almost always sts.amazonaws.com. A mismatch here fails before AWS ever gets to evaluating the subject.
token.actions.githubusercontent.com:sub condition encodes exactly which repo and ref (branch or tag) is allowed to assume the role — e.g. repo:org/repo:ref:refs/heads/main. A PR from a different branch, or a fork, won't match and the assume-role call is rejected outright.
aws iam get-open-id-connect-provider shows the thumbprint IAM trusts for GitHub's certificate chain. GitHub rotates its intermediate certs occasionally; a stale thumbprint in IAM breaks every workflow using that provider at once, not just one repo.
role-to-assume is a fully-qualified ARN — a typo'd or leftover region segment from a copy-pasted ARN fails the exact same way as a genuine trust-policy problem, so rule it out before digging further into IAM.
aws sts get-caller-identity right after the credentials step. It confirms the whole chain — audience, subject, thumbprint, and region — resolved correctly, in one command.
Checklist & commands:
# workflow snippet — audience must match
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/MyRole
aws-region: us-east-1
audience: sts.amazonaws.com # must match trust policy
// IAM trust policy — subject must match branch/repo
{
"Condition": {
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:org/repo:ref:refs/heads/main",
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
}
}
}
# verify OIDC provider thumbprint
aws iam list-open-id-connect-providers
aws iam get-open-id-connect-provider \
--open-id-connect-provider-arn arn:aws:iam::ACCOUNT:oidc-provider/token.actions.githubusercontent.com
# test assume-role manually with a debug token
aws sts get-caller-identity
Prevention: Pin the aws-actions/configure-aws-credentials version (@v4 not @main). Store the role ARN in a repository variable not hardcoded in the workflow. Add a CI test workflow that fires on every trust-policy change PR and asserts aws sts get-caller-identity succeeds.
A GitHub Actions job fails with "Error assuming role" even though the IAM role has every S3 permission the job needs. Where's the actual problem, and where do you look first?
audience matches what the trust policy expects, and whether the sub condition's repo/branch matches exactly. A role with a flawless S3 policy still can't be assumed if the trust policy's Condition block doesn't match the token GitHub is presenting.2. GitHub Actions: Job Hangs Indefinitely
Symptom: A step runs forever with no output; job never completes.
flowchart TD
classDef err fill:#e74c3c,stroke:#c0392b,color:#fff
classDef decision fill:#f39c12,stroke:#ba6018,color:#fff
classDef fix fill:#27ae60,stroke:#1e8449,color:#fff
classDef verify fill:#3498db,stroke:#2471a3,color:#fff
A["Job hangs:<br/>step runs forever,<br/>no output"]:::err --> B{"Interactive<br/>prompt?"}:::decision
B -- Yes --> B1["Add -y / --no-input<br/>flag to the command"]:::fix
subgraph WAIT["Things silently blocking"]
B -- No --> C{"sudo without<br/>NOPASSWD?"}:::decision
C -- Yes --> C1["Add NOPASSWD in<br/>sudoers, or avoid sudo"]:::fix
C -- No --> D{"Test waiting<br/>for a port?"}:::decision
D -- Yes --> D1["Add a timeout or<br/>wait-on utility"]:::fix
D -- No --> E{"Unreachable<br/>network host?"}:::decision
E -- Yes --> E1["Mock or skip<br/>the network call"]:::fix
end
E -- No --> F{"job timeout-minutes<br/>not set?"}:::decision
F -- Missing --> F1["Add timeout-minutes<br/>to the job"]:::fix
F -- Set --> G["Cancel the job,<br/>check the last log line"]:::verify
Commands:
jobs:
build:
timeout-minutes: 15 # always set a ceiling
steps:
- run: apt-get install -y curl # -y prevents prompt
# cancel a stuck run via CLI
gh run list --limit 5
gh run cancel <run-id>
# stream logs to find the last line before hang
gh run view <run-id> --log | tail -40
apt-get install curl without -y silently blocks on a confirmation prompt no one is there to answer — CI has no TTY to type "yes" into, so the process just sits there until something else (a job timeout, if you set one) kills it. Fix: pass the non-interactive flag every tool supports (-y, --no-input, DEBIAN_FRONTEND=noninteractive).
sudo call in a step whose user isn't configured with NOPASSWD in sudoers blocks waiting for a password that will never arrive. Fix: add a NOPASSWD entry for the CI user, or restructure the step so it doesn't need sudo at all.
wait-on-style utility with an explicit timeout, not a raw unbounded loop.
Prevention: Set timeout-minutes on every job and every step. Use continue-on-error: false (default) and add cancel-in-progress: true to concurrency groups so a new push cancels stuck old runs. Add a job_timeout metric via GitHub API to alert if any job exceeds 30 minutes.
You set timeout-minutes: 15 on a job that's prone to hanging. Is that alone enough to stop a bad push from piling up stuck runs?
timeout-minutes only bounds how long a single run can hang before it's killed. A new push on the same branch still queues or races behind however many previous runs are still ticking down their own 15 minutes. Pair it with cancel-in-progress: true on the workflow's concurrency group so a new push on the same ref actively cancels the old, still-hanging job instead of leaving it to time out on its own.3. ArgoCD: App Out of Sync But Won't Sync
Symptom: App shows OutOfSync in the UI but sync does nothing or errors immediately.
flowchart TD
classDef err fill:#e74c3c,stroke:#c0392b,color:#fff
classDef decision fill:#f39c12,stroke:#ba6018,color:#fff
classDef fix fill:#27ae60,stroke:#1e8449,color:#fff
classDef verify fill:#3498db,stroke:#2471a3,color:#fff
A["App shows OutOfSync,<br/>sync does nothing<br/>or errors immediately"]:::err --> B["Check diff<br/>in ArgoCD UI"]:::verify
B --> C{"Resource managed<br/>by another tool?"}:::decision
C -- Yes --> C1["Remove Helm/kubectl<br/>annotations, or adopt<br/>the resource into ArgoCD"]:::fix
subgraph SYNC["Sync configuration"]
C -- No --> D{"Auto-sync<br/>disabled?"}:::decision
D -- Yes --> D1["Enable auto-sync,<br/>or trigger manually"]:::fix
D -- No --> E{"Sync error<br/>in k8s Events?"}:::decision
E -- Yes --> E1["Fix the validation<br/>webhook error"]:::fix
end
E -- No --> F["Force sync<br/>with --force"]:::fix
argocd app diff my-app shows exactly which fields ArgoCD thinks are different between desired and live state — this is the fastest way to tell genuine drift apart from a sync mechanism that's actually broken.
kubectl apply instead of ArgoCD's own tracking annotation, ArgoCD reports perpetual drift because another tool keeps changing the object out from under it. Remove the foreign annotations, or formally adopt the resource into the ArgoCD Application.
argocd app get my-app shows the sync policy. A disabled auto-sync means ArgoCD is faithfully reporting drift and just waiting for a human to click Sync — that's expected behavior, not a bug to chase.
kubectl get events surfaces admission/validation-webhook rejections that would otherwise look, from the ArgoCD UI alone, like sync is silently doing nothing.
argocd app sync my-app --force overwrites live state unconditionally — reach for it after the diff and events have told you what's actually wrong, not as the first move.
Commands:
# inspect what ArgoCD thinks is different
argocd app diff my-app
# check sync status and last error
argocd app get my-app
# enable sync if disabled
argocd app set my-app --sync-policy automated
# force sync (overwrites live state)
argocd app sync my-app --force
# check for webhook rejections in k8s events
kubectl get events -n my-namespace --sort-by='.lastTimestamp' | tail -20
Prevention: Enable ArgoCD auto-sync with selfHeal: true and prune: true in non-prod environments so drift is corrected automatically. Use syncPolicy.syncOptions: [CreateNamespace=true] to avoid manual namespace creation. Add argocd app wait in the CD pipeline — fails the deploy pipeline if ArgoCD doesn't reach Synced+Healthy within the timeout.
Should you turn on ArgoCD's selfHeal: true and prune: true in production the same way you'd enable them in staging?
selfHeal and prune in non-prod environments so drift self-corrects automatically. In production, a controller that auto-heals and auto-prunes will just as happily revert or delete something an engineer changed live for a legitimate reason — an emergency scale-up, a break-glass hotfix — which is a much larger blast radius than the same behavior in staging. Treat it as an environment-scoped setting, not a global default.4. ArgoCD: ImagePullBackOff After Deploy
Symptom: ArgoCD reports Synced/Healthy but pods stay in ImagePullBackOff.
flowchart TD
classDef err fill:#e74c3c,stroke:#c0392b,color:#fff
classDef decision fill:#f39c12,stroke:#ba6018,color:#fff
classDef fix fill:#27ae60,stroke:#1e8449,color:#fff
classDef verify fill:#3498db,stroke:#2471a3,color:#fff
A["ArgoCD reports<br/>Synced/Healthy but pods<br/>stay ImagePullBackOff"]:::err --> B{"Tag exists<br/>in registry?"}:::decision
B -- No --> B1["Push the correct tag,<br/>or fix the image ref"]:::fix
B -- Yes --> C{"imagePullSecret<br/>in namespace?"}:::decision
C -- Missing --> C1["Create the secret and<br/>add it to the serviceAccount"]:::fix
subgraph AUTH["Registry auth"]
C -- Present --> D{"ECR token<br/>expired?"}:::decision
D -- Yes --> D1["Switch to IRSA,<br/>remove static creds"]:::fix
D -- No --> E{"Image updater<br/>in use?"}:::decision
E -- Yes --> E1["Check updater logs<br/>and annotation config"]:::fix
end
E -- No --> F["Check kubelet<br/>pull error details"]:::verify
Commands:
# check pod events for pull error details
kubectl describe pod <pod-name> -n <ns> | grep -A 10 Events
# verify image tag exists in ECR
aws ecr describe-images --repository-name my-repo \
--image-ids imageTag=v1.2.3
# create ECR pull secret (short-term fix)
kubectl create secret docker-registry ecr-creds \
--docker-server=<account>.dkr.ecr.<region>.amazonaws.com \
--docker-username=AWS \
--docker-password=$(aws ecr get-login-password)
# check argocd-image-updater logs
kubectl logs -n argocd deploy/argocd-image-updater | tail -50
image: myapp:latest (or any mutable tag like :v1.2.3 that gets re-pushed) is a moving pointer — the tag that existed when ArgoCD last synced can point at a different digest by the time a node actually pulls it. That race is exactly how a `Synced/Healthy` app ends up with pods that can't pull: the tag ArgoCD recorded and the tag currently in the registry disagree.
image: myapp@sha256:... pins to one immutable content hash. There's no "the tag changed underneath me" race to debug at all — whatever digest ArgoCD synced is byte-for-byte what every node pulls, forever. This is the fix the Prevention note below calls out directly.
Prevention: Use image digest pinning in ArgoCD (image: myapp@sha256:...) — eliminates tag races. Store imagePullSecrets as a sealed secret or External Secrets Operator resource, not a manually-created secret. Add a registry reachability check to the CD pipeline before deploying.
ArgoCD shows the app as Synced/Healthy, and the manifest still references image: myapp:v1.2.3 — that tag is deployed everywhere it's supposed to be. Can pods still land in ImagePullBackOff?
v1.2.3 gets re-pushed to point at a different digest after ArgoCD last synced, the tag ArgoCD recorded and the tag currently in the registry can disagree by the time a node actually pulls it, and the pull fails with no manifest change involved at all. ArgoCD's Synced/Healthy status only reflects the desired manifest matching the live object's spec — it has no way to know the tag it's pointing at silently changed underneath it. Pinning to a digest (image: myapp@sha256:...) removes the race entirely, since a digest can't be re-pushed to mean something else.5. Jenkins Pipeline: Docker Build Fails in Agent
Symptom: docker: command not found or permission denied /var/run/docker.sock.
flowchart TD
classDef err fill:#e74c3c,stroke:#c0392b,color:#fff
classDef decision fill:#f39c12,stroke:#ba6018,color:#fff
classDef fix fill:#27ae60,stroke:#1e8449,color:#fff
A["Docker build fails:<br/>command not found /<br/>permission denied on socket"]:::err --> B{"docker binary<br/>present on agent?"}:::decision
B -- No --> B1["Install Docker on<br/>the agent, or use DinD"]:::fix
B -- Yes --> C{"jenkins user in<br/>docker group?"}:::decision
C -- No --> C1["usermod -aG docker jenkins,<br/>restart the agent"]:::fix
C -- Yes --> D{"Socket mounted<br/>in the agent pod?"}:::decision
D -- No --> D1["Mount /var/run/docker.sock<br/>in the podTemplate"]:::fix
subgraph APPROACH["Which build approach?"]
D -- Yes --> E{"DinD sidecar<br/>or socket mount?"}:::decision
E -- DinD --> E1["Use a privileged DinD<br/>sidecar container"]:::fix
E -- "Socket mount" --> E2["Check socket perms:<br/>chmod 666 or group membership"]:::fix
end
jenkins user needs to be in the host's docker group to talk to the socket without root. usermod -aG docker jenkins plus an agent restart is required to pick up the new group — reloading the Jenkins service alone isn't enough, since group membership is resolved at session/login time, not by the app.
/var/run/docker.sock has to be an explicit hostPath volume mount, or the container never sees the host's Docker daemon regardless of user/group settings.
ls -la /var/run/docker.sock should show srw-rw---- owned by the docker group. Wrong permissions here produce the exact same "permission denied" error as a missing group membership, so check both rather than assuming which one it is.
Pipeline snippet & commands:
// Jenkinsfile — socket mount approach
pipeline {
agent {
kubernetes {
yaml """
apiVersion: v1
kind: Pod
spec:
containers:
- name: docker
image: docker:24-cli
command: [sleep, infinity]
volumeMounts:
- name: docker-sock
mountPath: /var/run/docker.sock
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
"""
}
}
stages {
stage('Build') {
steps {
container('docker') {
sh 'docker build -t my-image .'
}
}
}
}
}
Shares the host's Docker daemon directly. Fastest option and no extra container to boot, but any build step that can reach the socket has an effective path to host root — treat the mount as equivalent to running the build step privileged.
// Jenkinsfile — Docker-in-Docker sidecar approach
pipeline {
agent {
kubernetes {
yaml """
apiVersion: v1
kind: Pod
spec:
containers:
- name: docker
image: docker:24-cli
command: [sleep, infinity]
env:
- name: DOCKER_HOST
value: tcp://localhost:2375
- name: dind
image: docker:24-dind
securityContext:
privileged: true
env:
- name: DOCKER_TLS_CERTDIR
value: ""
"""
}
}
stages {
stage('Build') {
steps {
container('docker') {
sh 'docker build -t my-image .'
}
}
}
}
}
Runs an isolated daemon per pipeline in a dind sidecar, so no build shares the node's actual Docker socket. Trade-off: the sidecar itself must run privileged: true, so the risk moves from "shared host socket" to "one more privileged container per build" rather than disappearing.
# on the agent node — add jenkins to docker group
sudo usermod -aG docker jenkins
sudo systemctl restart jenkins
# verify socket permissions
ls -la /var/run/docker.sock # should be srw-rw---- docker group
Prevention: Use rootless Docker or Kaniko/Buildah in CI agents — eliminates the docker.sock privilege escalation risk entirely. If Docker-in-Docker is required, use --privileged only in isolated ephemeral agents, never on long-lived shared agents.
The socket-mount fix for Jenkins' docker.sock permission error works — the build goes green. Is that the recommended long-term fix?
docker.sock can launch a privileged container and effectively become root on the host. Both socket-mount and DinD carry this risk in different shapes. The recommended fix is to move off Docker entirely for CI builds in favor of rootless Docker or Kaniko/Buildah, which eliminates the privilege-escalation path rather than just permissioning around it. DinD with --privileged is acceptable only as a fallback, and only on isolated, ephemeral agents — never long-lived shared ones.6. Pipeline Deploys to Wrong Environment
Symptom: A staging branch push triggers a production deployment.
flowchart TD
classDef err fill:#e74c3c,stroke:#c0392b,color:#fff
classDef decision fill:#f39c12,stroke:#ba6018,color:#fff
classDef fix fill:#27ae60,stroke:#1e8449,color:#fff
classDef verify fill:#3498db,stroke:#2471a3,color:#fff
A["Wrong env deployed:<br/>staging push triggers<br/>a production deploy"]:::err --> B{"Branch protection<br/>configured?"}:::decision
B -- No --> B1["Add branch rules<br/>in GitHub/GitLab"]:::fix
subgraph CONFIG["Environment wiring — check even after branch rules pass"]
B -- Yes --> C{"Env var set<br/>correctly?"}:::decision
C -- Wrong --> C1["Fix the ENV var in<br/>the workflow/Jenkinsfile"]:::fix
C -- OK --> D{"Env name matches<br/>in workflow?"}:::decision
D -- Mismatch --> D1["Align environment:<br/>name in the workflow"]:::fix
D -- OK --> E{"ArgoCD app targets<br/>correct cluster/ns?"}:::decision
E -- Wrong --> E1["Fix destination in<br/>the ArgoCD Application CR"]:::fix
E -- OK --> F{"Correct Helm<br/>values file used?"}:::decision
F -- Wrong --> F1["Fix the -f values<br/>path in sync config"]:::fix
end
F -- OK --> G["Add a manual<br/>approval gate for prod"]:::verify
Concrete fixes:
# GitHub Actions — gate production on branch + approval
jobs:
deploy-prod:
if: github.ref == 'refs/heads/main'
environment: production # requires manual approval in repo settings
steps:
- run: helm upgrade --install my-app ./chart -f values/prod.yaml
# verify ArgoCD app destination
argocd app get my-app -o json | jq '.spec.destination'
# check which values file ArgoCD is using
argocd app get my-app -o json | jq '.spec.source.helm'
Prevention: Use ArgoCD ApplicationSets with environment-specific values files and project RBAC — engineers can't promote to prod without approval. Add a diff step to the CD pipeline that shows what will change in each environment before applying. Use Helm --atomic flag so failed upgrades auto-rollback.
Branch protection is correctly configured so only main can trigger the production deploy job. Does that fully prevent a wrong-environment deploy?
environment: name, the ArgoCD Application's destination cluster/namespace, or the Helm values file path actually point at production. Any one of those can be misconfigured — a copy-pasted destination, a stale values path — and silently deploy the right branch to the wrong place. That's exactly why the diagnostic flow keeps checking env var, environment name, ArgoCD destination, and values file even after branch protection has already passed.7. Container Image Build Passes but App Crashes in Staging
Symptom: docker build succeeds in CI, but container exits immediately in staging.
flowchart TD
classDef err fill:#e74c3c,stroke:#c0392b,color:#fff
classDef decision fill:#f39c12,stroke:#ba6018,color:#fff
classDef fix fill:#27ae60,stroke:#1e8449,color:#fff
classDef verify fill:#3498db,stroke:#2471a3,color:#fff
A["docker build succeeds<br/>in CI, container exits<br/>immediately in staging"]:::err --> B{"Env vars<br/>missing?"}:::decision
B -- Yes --> B1["Add vars to CI<br/>environment secrets"]:::fix
B -- No --> C{"Secrets<br/>injected?"}:::decision
C -- No --> C1["Mount secret, or use<br/>a secrets manager"]:::fix
C -- Yes --> D{"Wrong base<br/>image arch?"}:::decision
D -- "arm/amd mismatch" --> D1["Use a multi-arch build:<br/>docker buildx"]:::fix
D -- OK --> E{"Health check<br/>path wrong?"}:::decision
E -- Yes --> E1["Fix HEALTHCHECK or<br/>readinessProbe path"]:::fix
E -- No --> F{"DB migration<br/>not run?"}:::decision
F -- Yes --> F1["Add an init container,<br/>or a pre-deploy hook"]:::fix
F -- No --> G["Check container<br/>logs in staging"]:::verify
Commands:
# pull and inspect the exact CI-built image locally
docker pull <registry>/my-app:<ci-tag>
docker run --rm -e ENV=staging <registry>/my-app:<ci-tag>
# check arch of built image
docker inspect <image> | jq '.[].Architecture'
# multi-arch build
docker buildx build --platform linux/amd64,linux/arm64 -t my-app:latest --push .
# check staging pod logs
kubectl logs -n staging deploy/my-app --previous
kubectl logs -n staging deploy/my-app -f
# describe pod for crash reason
kubectl describe pod -n staging -l app=my-app | grep -A 5 "Last State"
Prevention: Add a post-deploy smoke test step in the CD pipeline: kubectl rollout status deployment/my-app -n staging --timeout=120s && curl -f https://staging.example.com/health. If it fails, auto-rollback with kubectl rollout undo. Use progressDeadlineSeconds: 120 on Deployments so rollouts auto-fail fast.
A container image builds and runs fine on the CI runner, but crash-loops the instant it's deployed to staging. What's a likely cause the build step wouldn't have caught at all?
docker build produces a single-arch image, and it "succeeds" on CI because CI's architecture happens to match the image it just built — the mismatch only surfaces once the same image lands on different hardware. Fix with a multi-arch build (docker buildx build --platform linux/amd64,linux/arm64), not by re-running the same single-arch build and hoping.8. Flaky Tests Blocking Pipeline
Symptom: Tests pass locally and sometimes in CI, but fail intermittently and block merges.
flowchart TD
classDef err fill:#e74c3c,stroke:#c0392b,color:#fff
classDef decision fill:#f39c12,stroke:#ba6018,color:#fff
classDef fix fill:#27ae60,stroke:#1e8449,color:#fff
classDef verify fill:#3498db,stroke:#2471a3,color:#fff
A["Flaky test in CI:<br/>passes locally, fails<br/>intermittently, blocks merges"]:::err --> B
subgraph CAUSES["Check causes in order"]
B{"Race<br/>condition?"}:::decision
B -- Yes --> B1["Run go test -race,<br/>fix the data race"]:::fix
B -- No --> C{"Timing<br/>dependent?"}:::decision
C -- Yes --> C1["Add retry or fix<br/>the deterministic wait"]:::fix
C -- No --> D{"External service<br/>dependency?"}:::decision
D -- Yes --> D1["Mock the service<br/>in tests"]:::fix
D -- No --> E{"Parallel job<br/>resource contention?"}:::decision
E -- Yes --> E1["Limit concurrency,<br/>or isolate resources"]:::fix
end
E -- No --> F["Increase test timeout:<br/>-timeout flag"]:::verify
go test -race to have the race detector instrument every memory access and flag the exact unsynchronized read/write, then fix the underlying data race, not the symptom.
Commands:
# detect races
go test -race ./...
# run with explicit timeout
go test -timeout 120s ./...
# re-run flaky test N times locally
for i in $(seq 1 10); do go test -run TestMyFlaky ./pkg/...; done
# run only failed tests from last run (requires gotestsum)
gotestsum --rerun-fails=3 --packages ./...
# GitHub Actions — retry flaky step
- name: Test
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: go test -race -timeout 90s ./...
Prevention: Track flakiness rate per test in CI metrics — quarantine any test with >5% flakiness until fixed (don't just retry). Use go test -count=1 to disable test caching and always run fresh. For integration tests: use testcontainers-go to spin up real dependencies instead of mocks — eliminates a whole class of flakiness from mock state pollution.
A flaky test gets wrapped in nick-fields/retry with max_attempts: 3, and the pipeline goes green again. Does that count as fixing the flaky test?
Quick Reference
| Symptom | First command |
|---|---|
| OIDC auth fails | aws sts get-caller-identity |
| Job hangs | gh run cancel <id> + check last log |
| ArgoCD won't sync | argocd app diff my-app |
| ImagePullBackOff | kubectl describe pod → check Events |
| Docker not found in Jenkins | ls -la /var/run/docker.sock |
| Wrong env deployed | argocd app get my-app -o json | jq .spec.destination |
| App crashes in staging | kubectl logs deploy/my-app --previous |
| Flaky tests | go test -race ./... |