stack-gotchas
Overview
Diagnose-and-recover recipes for failures I've actually hit. Each is symptom → what it really means → fix — not a tutorial. Match the symptom, apply the recipe.
release-please: "Resource not accessible by integration"
- Symptom: release-please fails at "Creating N releases…" despite correct token perms.
- Means: GitHub secondary rate-limit (abuse detection) on the
github-actions[bot]token, not a permissions bug. Tell-tale: a release succeeded seconds earlier;gh api rate_limitcore looks healthy. Usually triggered by a burst (dependabot opening ~12 PRs). - Fix: (1) create the missing release with your user token, full 40-char SHA:
gh release create vX.Y.Z --target <full-sha> --title vX.Y.Z --notes "<CHANGELOG section>" --latest. (2) Swap the PR labelautorelease: pending→autorelease: taggedvia REST (gh api), or it retries forever. (3) Let the bot cooldown clear (~minutes–1h) with no more content-creation calls. Code usually deployed anyway (Pages runs on push independently).
release-please: infinite auto-merge loop
- Symptom: the release PR never merges; the workflow re-dispatches itself every ~90s.
- Means: the bot's PR (made with
GITHUB_TOKEN) gets no CI run → the requiredverifycheck never appears → auto-merge can't complete; it lands only via a fragile fast-forward that breaks ifmainadvanced by hand in parallel. - Fix: create the tag/release yourself at
mainHEAD (gh release create vX.Y.Z --target <main-sha>) → no pending release → no re-dispatch → loop stops. Replicate the bump inpackage.json+.release-please-manifest.json+CHANGELOG.mdvia a normal user PR so CI runs. Don't hand-mergemainmid-release. Durable fix: give release-please a PAT (its PRs then trigger CI) and drop the unconditional self-redispatch.
release-please: "GitHub Actions is not permitted to create or approve pull requests" (new repo)
- Symptom: on a freshly created repo, the Release workflow's release-please step
fails almost immediately (~15s) with
release-please failed: GitHub Actions is not permitted to create or approve pull requests.The CI workflow is fine; only release PR creation fails. - Means: new repos default the repo Actions setting to
default_workflow_permissions=read+can_approve_pull_request_reviews=false. release-please must open its release PR, and the repo/org toggle overrides the workflow-levelpermissions: pull-requests: write— so the in-workflow grant is not enough on its own. - Fix: enable it once (Settings → Actions → General → "Allow GitHub Actions to create
and approve pull requests"), via API:
Do this right aftergh api -X PUT repos/<owner>/<repo>/actions/permissions/workflow \ -f default_workflow_permissions=write -F can_approve_pull_request_reviews=true gh run rerun <failed-release-run-id>gh repo createfor any new FAP / sister app — it's a one-time per-repo setting, not a code or token problem. (Distinct from the secondary rate-limit and auto-merge-loop failures above.)
GitHub Pages: deploy rejected with empty logs after master→main rename
- Symptom: the deploy job fails with no steps and empty logs.
- Means: rejected at the environment gate — the
github-pagesenv's deployment-branch-policy still lists only the old branch (master). - Fix:
Set the default branch before the first deploy; ensure Pagesgh api -X POST repos/<owner>/<repo>/environments/github-pages/deployment-branch-policies -f name=main gh api repos/<owner>/<repo>/environments/github-pages/deployment-branch-policies --jq '.branch_policies[]' gh api -X DELETE repos/<owner>/<repo>/environments/github-pages/deployment-branch-policies/<id>build_type=workflow.
GitHub Pages: deployment_queued timeout, then every retry says "Deployment cancelled"
- Symptom: two distinct failures, usually in that order. First, deploy runs sit at
Current status: deployment_queuedfor the whole timeout and abort with##[error]Timeout reached, aborting!— logs are full and the build step succeeded (unlike the empty-log branch-policy failure above). Then every re-run or re-dispatch of that same commit fails within seconds with##[error]Deployment cancelled. - Means: two different things, and conflating them wastes an hour.
- The timeout is GitHub-side; githubstatus can read "All Systems Operational" while it happens, and it is intermittent — the same commit may deploy fine minutes later. No local cause was ever established.
- The sticky cancellation is deterministic and the important one. The Pages deployment ID is the commit SHA. Aborting cancels the deployment for that SHA, and the cancelled state is permanent, so every later attempt at the same commit is rejected outright. Retrying can never work.
- Never press "Re-run". The first attempt already uploaded the
github-pagesartifact; the re-run uploads a second with the same name into the same run, anddeploy-pagesrefuses withMultiple artifacts named "github-pages" were unexpectedly found for this workflow run. Artifact count is 2.A third dead end for the same commit.
- Fix: all three routes are closed for a jammed commit — re-run, re-dispatch and waiting
each fail by a different mechanism. Land a new commit so the deploy gets a fresh SHA
and a clean run. Prefer a real pending change over an empty commit. Then set
concurrency: {group: pages, cancel-in-progress: false}: cancelling does not stop the deployment it already queued, it just poisons that SHA, sotruemanufactures this failure on every burst of merges. GitHub's own Pages starter workflow says the same — "do NOT cancel in-progress runs, as we want to allow these production deployments to complete." - Before diagnosing a bundle mismatch,
git pull. Comparing a localdist/against the live page after a release, without pulling the release commit, shows a hash mismatch that is purely a stalepackage.jsonversion — not a failed deploy. Verify withcurl -s <url> | grep -oE 'index-[A-Za-z0-9_-]+\.js'against a fresh build of the pulled default branch.
Verify mobile/responsive rendering in WSL (no sudo)
- Symptom: you need to see/measure real mobile widths but there's no Linux browser;
Windows Edge headless clamps window width to ~500px (screenshots <450px render a wider
layout cropped — misleading), and puppeteer can't drive the Windows
.exefrom WSL (stdio pipe breaks; its CDP debug port is unreachable over NAT). - Means: you need a native Linux Chromium, but the one puppeteer downloads is missing
libnss3/libnspr4/libasound2and there's no passwordless sudo. - Fix (all without root):
Drive vianpx -y @puppeteer/browsers install chrome@stable # native linux chromium apt-get download libnss3 libnspr4 libasound2t64 # download .deb, no sudo for d in *.deb; do dpkg -x "$d" root; done # extract libs locally # run with: LD_LIBRARY_PATH=$PWD/root/usr/lib/x86_64-linux-gnu <chrome> …puppeteer-core(executablePath= that chrome;LD_LIBRARY_PATHin the env). ThensetViewport({ width, deviceScaleFactor: 2, isMobile: true })at any width, measurescrollWidth/getBoundingClientRect, bisect overflow by togglingdisplay:noneper child, and confirm a fix by injecting the style and re-measuring before editing. Keep the env until done — don't re-download the ~150MB browser mid-task. - Before re-debugging a "still broken" UI report: confirm the user isn't on a stale
deploy or cached PWA (check the live tag + that
autoUpdateactivated). Much of this class of confusion is version lag, not a bug.
Supabase: egress blown (not DB size)
- Symptom: Free-tier limit hit while DB size is tiny.
- Means: the binding limit is egress ≈
blob_size × updates × connected_clients. Usually an unbounded field in a JSON blob growing quadratically (history/trash snapshots). - Fix: cap growing fields hard; make history an audit-log (no full snapshots); broadcast
only
{version}over Realtime and version-gate full fetches; write-through the local cache. Seeeskills:perf-bar. Next lever: split the growing field into its own table.
Supabase: open-write RLS
- Symptom: anyone with the anon key can overwrite a row via PostgREST.
- Means: RLS is
SELECT true+INSERT/UPDATE true; client-side PIN/rate-limit give no server protection (the anon key ships in the client). - Fix: restrict direct writes; enforce ownership/PIN in an RPC with RLS. See
eskills:security-bar.
Supabase: stale-client blob wipe
- Symptom: a newly-added field (e.g. subgroups) silently disappears after another user edits.
- Means: whole-blob rewrite + Zod stripping unknown keys + a manual-update PWA → a stale
cached client (running old code) drops fields its schema doesn't know on its next write.
The optimistic
versioncheck doesn't protect (same-version-lineage overwrite). - Fix: stamp
_schemaVersionon every write + a Postgres BEFORE UPDATE trigger rejecting writes whose version < stored (→ HTTP 426 → "please update" prompt); flip the PWA toautoUpdate. Operational rule: bumpSCHEMA_VERSIONwhenever the snapshot shape changes, or the guard won't protect the new field.
Flipper FAP: release-please leaves application.fam un-bumped
- Symptom: one leg of the triad stays behind on release — e.g.
application.fam'sfap_version, ORinclude/version.h's constant — while the others bump → catalog/CI mismatch, and an in-app "version" string shows the stale number. - Means: the
genericupdater only replaces a version on a line that itself carries thex-release-please-versionmarker. The marker must be INLINE on the same line as the value —fap_version="x.y.z" # x-release-please-versionand#define APP_VERSION "x.y.z" // x-release-please-version. A marker on a separate line above the#define(or a missing/edited marker) makes the updater silently no-op that file. (Hit on flipper-tutu: version.h had the marker on its own line and stuck at 0.1.0 while fam/manifest advanced.) - Fix: put the marker inline on each version-bearing line; sync the lagging file to the
current version once by hand; keep the triad
application.fam↔include/version.h↔.release-please-manifest.jsonaligned. Seestacks/references/flipper/release-please.md.
Flipper FAP: catalog submission rejected on version/commit
- Symptom: the official catalog CI fails the entry.
- Means: the manifest
commit_shapoints at a later fix commit instead of the tagged release commit, or the entry version ≠fap_version. - Fix: point
commit_shaat the release commit; make the catalog version equalfap_version. (Catalog-update-flow memory +stacks→ flipper.)
Flipper FAP: host test won't compile (furi symbols)
- Symptom:
make testfails to compile with undefined furi references. - Means: a file under test (transitively) includes
furi— furi isn't available on the host gcc build; the layering leaked. - Fix: keep
domain/pure C and move the furi-touching code behind a plain-C signature inplatform/. Only testdomain/. Seestacks/references/flipper/architecture.md.
Flipper FAP: cppcheck flags entry points as unusedFunction
- Symptom:
make lintererrors onunusedFunctionformain.c/*_app.c/ port files. - Means: those functions are called by the firmware, not within the TU — a cppcheck false positive, not dead code.
- Fix: add a path-scoped
--suppress=unusedFunction:<path>(or inline// cppcheck-suppress), never disable the check globally. Seestacks/references/flipper/formatting.md.
Flipper FAP: blank/stale UI when launched from favourites or quick-buttons
- Symptom: the screen is blank or frozen (UI never refreshes) when the app is opened from a desktop favourite or a quick-press button shortcut — but it works fine when opened from the Apps menu. (Reported by catalog users on two of my apps.)
- Means: the app only calls
view_port_update()in reaction to input — the main loop blocks onfuri_message_queue_get(..., FuriWaitForever)and paints nothing until a key is pressed. The Apps menu masks it: the loader's background hourglass animation forces a GUI redraw, so the first frame appears. Favourites/quick-buttons skip that loader → nothing triggers the draw. The app was leaning on an external redraw ("a bug"), not driving its own. - Fix: force the redraw from the app itself. (1) Draw once immediately after
gui_add_view_port(...)withview_port_update(view_port). (2) Don't depend on input to paint: either give the queue a finite timeout (furi_message_queue_get(q, &e, 50)) and callview_port_update()every loop tick, or call it after every state change (including the initial state). Same fix for aViewDispatcher/SceneManagerapp: ensure the first scene transition happens before the loop blocks. Seestacks/references/flipper/architecture.md.
GDPR: open-RLS exposes personal data
- Symptom: any anon key holder can read/update/delete rows that contain names, email, or financial data (e.g. EventSplit's events blob).
- Means: open-write RLS (
SELECT/INSERT/UPDATE true) isn't just a security bug — it's a personal-data exposure / breach risk under GDPR (unauthorized access to identifiable people, including third parties who never consented). - Fix: restrict direct writes / enforce ownership in an RPC (the security fix →
eskills:security-bar); then in the privacy notice, only claim protections you actually have. Don't document "your data is protected" while RLS is open. Seeeskills:gdpr.
GDPR: undisclosed international transfer to an AI provider
- Symptom: the app sends user audio/text to OpenAI/Anthropic/Google/Azure, but the privacy notice (if any) doesn't mention it (mintza).
- Means: that's a processor + international transfer (US) with no disclosure and no transfer mechanism named — a GDPR gap, and possibly special-category data (voice).
- Fix: disclose every AI provider as a processor + name the transfer basis (SCCs/DPF) in
the notice; confirm no-training-on-API-data + retention; send the model the minimum
content, no needless identifiers. See
eskills:gdpr(AI-processor posture).