* feat(desktop): scaffold Tauri 2 macOS shell with empty window
* feat(desktop): server store, URL normalization, and health-check helpers
* feat(desktop): IPC commands for server list/add/remove/health + active-server state
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* feat(desktop): native onboarding server picker with health check and remembered servers
* feat(desktop): vibrancy background, overlay titlebar, and inset traffic lights
* feat(desktop): native menu bar with standard shortcuts and menu events
* feat(desktop): webview→Rust bridge with native notifications
* fix(desktop): gate bridge injection on PageLoadEvent::Finished
Prevents double-injecting the bridge IIFE (once on Started, once on
Finished), which was duplicating every native notification.
* feat(desktop): Dock badge driven by webview attention count
* feat(desktop): launch-at-login autostart commands
* feat(desktop): sure:// deep link scheme with parse tests and navigation
* feat(desktop): preferences window with server switcher and launch-at-login
* docs(desktop): README for dev, release, signing/notarization, and deferred widget
* fix(desktop): remove dead New Window menu item
* fix(desktop): correct login route to /sessions/new
Rails uses `resources :sessions` (plural), so the login page is
/sessions/new, not the /session/new the plan assumed. Fixes an
immediate 404 when connecting to a server.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* feat(desktop): Sure-styled onboarding, draggable titlebar, and app content offset
- Restyle onboarding + prefs to match Sure's auth page: solid surface
background, centered logomark, .form-field-style inputs, inverse primary
button; theme-aware via prefers-color-scheme (design-system tokens).
- Add a draggable titlebar strip on bundled pages and inject one into the
remote page so the window drags from the top everywhere.
- Inject a top offset on the logged-in app-layout root so the sidebar logo
clears the macOS traffic lights.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* fix(desktop): de-dupe login navigation to prevent CSRF token/session race
connect() navigated to /sessions/new directly AND via the
active-server-changed event, which is also handled by a second listener
injected into the page by bridge.js. One connect fired multiple concurrent
GET /sessions/new requests, each minting a fresh session + CSRF token; the
form shown and the _sure_session finally stored could come from different
GETs, so the login POST failed 'Can't verify CSRF token authenticity'
intermittently. Route all navigation through a single window-level guard so
only the first request per server wins.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* fix(desktop): enable window drag permission; offset only the icon rail
- Add core:window:allow-start-dragging (+ show/set-focus, event emit/listen) to
capabilities so data-tauri-drag-region actually drags the window on macOS.
- Offset only the 84px left icon rail (logomark) to clear the traffic lights
instead of pushing the entire app-layout down; keep main content full-height.
- Drag strip z-index lowered below Sure's sticky headers so its controls stay
clickable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* feat(desktop): persist active server and resume session on launch
- Persist the active server to the Keychain in set_active_server; active_server
falls back to it so a relaunch knows where to go.
- On launch, auto-resume straight to the last server instead of showing the
picker every time.
- Navigate to the server root (not /sessions/new): Rails serves the dashboard
when the session cookie is still valid, or redirects to login when not — so a
persisted session no longer forces a re-login.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* feat: desktop SSO via system browser with PKCE code exchange
Passkeys/WebAuthn don't work in an embedded WKWebView, so SSO now runs in
the system browser and hands a session back to the app securely.
Server (Rails):
- GET /auth/desktop/:provider — stashes a PKCE S256 challenge, hands off to
OmniAuth (reusing the mobile auto-submit form). Passkeys work (real browser).
- openid_connect — for a linked identity in a desktop flow, mints a single-use,
2-min, PKCE-bound one-time code and redirects to sure://sso/callback?code=...
(unlinked identities are sent back with an error).
- GET /sessions/desktop_exchange — verifies the code + PKCE verifier
(secure_compare), single-use (cache delete), then create_session_for; MFA is
enforced at exchange time. Sets the normal web session cookie in the webview.
- Tests: happy path + single-use, wrong-verifier rejection, missing challenge.
Desktop (Tauri):
- start_sso command: generates PKCE, opens the browser, stores the verifier.
- sure://sso/callback deep link -> webview navigates to desktop_exchange with
the verifier (never sent through the deep link, so an intercepted code is
useless).
- bridge.ts intercepts SSO provider form submits and routes them to start_sso;
password login stays in the webview.
- remote.json capability: minimal IPC (drag, event bridge, prefs window,
start_sso) for the remote Sure origin — no fs/shell/http.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* fix(desktop): correct remote IPC capability + handle menu in Rust + drag fallback
Root cause of prefs/switch-server/SSO/drag doing nothing on the logged-in
page: the remote-bridge capability's remote.urls ('https://*') did not match
the server origin, so all IPC (event listen, invoke, drag command) was denied.
Per Tauri v2, window.__TAURI__ is injected on remote pages only with
withGlobalTauri (set) AND a matching remote.urls; patterns need a path
wildcard.
- remote.json: urls -> https://*/**, http://*/** (+ bare host) so any server
origin matches.
- menu.rs: Preferences and Switch Server now show the prefs window directly in
Rust (no dependency on remote-page IPC); Switch Server moved from Window to
the App menu.
- bridge.ts: drops the menu-event listeners (Rust owns them), adds a
startDragging mousedown fallback for the drag strip, logs diagnostics, and
reports start_sso success/failure to the console.
- main.ts: drops the now-unused menu listeners.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* fix(desktop): SSO via event (remote can't invoke commands), disk-backed server store
Diagnostics confirmed window.__TAURI__ + IPC work on the remote page, but a
remote origin cannot invoke custom commands ('start_sso not allowed. Plugin
not found'). Events are permitted, so SSO now goes through an event.
- SSO: bridge emits 'sure://start-sso'; Rust listens and runs begin_sso
(opens the system browser). start_sso command kept for local use.
- servers: mirror the server list + active server to a JSON file in
Application Support as a fallback — Keychain items don't persist for
unsigned builds, which was wiping the saved server on relaunch.
- remote.json: add notification:default (Sure's PWA was requesting it and
erroring).
- menu: log whether the prefs window is present when Preferences/Switch
Server fire, to diagnose the no-op.
- main: log the persisted active server on boot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* fix(desktop): drag the top band on every page via mousedown, not a z-indexed strip
The fixed drag strip sat below Sure's sticky headers (z-10) so it worked only
on pages without a top header. Replace it with a document-level mousedown in the
top ~34px that starts a window drag unless the target is an interactive element
— so dragging works on all pages, Sure's titlebar controls stay clickable, and
main content isn't pushed down.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* ci(desktop): tag-triggered GitHub Actions release for universal unsigned .dmg
- .github/workflows/desktop-release.yml: on a 'desktop-v*' tag, build the
universal (Apple Silicon + Intel) .dmg on a macOS runner via tauri-action and
publish it to a GitHub Release with unsigned-install instructions.
- README: universal build command, the tag-based release process, and the
Gatekeeper 'Open Anyway' / xattr steps for end users.
- Drop the unused iOS/Android icon sets (macOS build only needs icon.icns).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* ci(desktop): rolling desktop-latest build on desktop/ changes, not manual tags
Replaces the manual desktop-v* tag release with a path-filtered workflow that
builds only when desktop/ changes on main and publishes to a single rolling
'desktop-latest' prerelease with a stable Sure.dmg filename — one permanent
download URL, and the file changes only when the desktop code does.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* ci(desktop): tag-driven versioned releases; tag is the single source of version
Revert to manual version tags (desktop-v*) for explicit version control, but
derive the app/.dmg version from the tag so package.json + tauri.conf.json are
synced automatically in CI — no manual version-file edits. Each tag produces its
own versioned GitHub Release with the universal unsigned .dmg.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* ci(desktop): release entirely from GitHub via workflow_dispatch version input
Make the GitHub Action the single tool to version + deploy the desktop app:
Run workflow -> enter a version -> it syncs the version, builds the universal
unsigned .dmg, and creates the desktop-v<version> tag + Release. Refuses to
re-release an existing version; marks pre-release versions accordingly. Tag
push (desktop-v*) still works as a secondary trigger.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* ci(desktop): publish releases with make_latest:false so they don't hijack the repo's Latest badge
Desktop is a secondary artifact, not the main product. Build with tauri-action,
then publish via action-gh-release with make_latest:false so the repo's 'Latest
release' badge stays on the main app's v* release. Separate desktop-v* tag
namespace already keeps it out of the v* publish workflow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* fix(desktop): address PR review feedback (security + correctness)
Security:
- workflow: pass workflow_dispatch version via env (no shell injection); pin all
third-party actions to commit SHAs.
- SSO: gate deep-link navigation and begin_sso to servers the user has saved
(is_known_server), so a rogue page/deep link can't drive them.
- desktop_exchange is now POST (verifier in body, not URL/logs); CSRF skipped
since the single-use PKCE code is the protection.
- desktop_sso_start validates the code_challenge is a 43-char base64url digest.
- desktop_exchange claims the one-time code atomically (delete-and-check) to
close the read/delete TOCTOU.
- failure: return desktop SSO errors to the app via sure://sso/callback?error.
Correctness / stability:
- prefs window hides on close instead of being destroyed, so the menu can
reopen it.
- servers.rs: on-disk store is authoritative (file-first read), atomic writes
(temp + rename).
- main.ts/prefs.ts: try/catch around add/set/remove/active_server and boot; add
a shared serverErrorMessage helper (no duplicated substring checks).
- vite.config.ts: derive dir from import.meta.url (ESM has no __dirname).
- bridge.ts: coalesce MutationObserver scans to one per frame.
- README: notarization example uses the universal .dmg name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* fix(desktop): scope remote IPC to server origins at runtime, drop wildcard capability
Resolves the remaining security finding: the static remote.json granted Tauri
IPC to any http(s) origin (https://*). Remove it and instead add a capability
scoped to each server's exact origin at runtime (CapabilityBuilder +
add_capability), granting only the minimal permissions the bridge needs, for
saved/active servers on startup and for the target in set_active_server. No
origin outside the user's configured servers can access IPC.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* fix(desktop): add runtime per-origin IPC capability (grant_server_capability)
Implements the runtime-scoped capability that replaces the removed wildcard
remote.json: CapabilityBuilder scoped to each server's exact origin, added via
add_capability for saved/active servers at startup and in set_active_server.
(Split from the previous commit, which only recorded the remote.json removal.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* ci(desktop): harden release workflow (no shared caches, environment gate)
Address two release-workflow security findings:
- Remove cache: npm and the swatinem/rust-cache step so a poisoned Actions
cache written by another workflow can't flow into a published .dmg (P0).
- Add 'environment: release' to the build job so publishing can require manual
approval and scope secrets to release runs (P1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf
* fix(desktop): remove transparency code and fix relaunch behavior
* fix(desktop): fix PR review findings; adjust app notarization path, use Sure theme tokens instead of hardcoding values
* ci(desktop): switch release from independant versioning to using Sure's publishing workflow, releasing and versioning with every main app release
* fix(desktop): restrict CSP as much as possible while maintaining functionality; allow bundled scripts, Tauri IPC, inline styles; deny wildcards
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Bump version to next iteration after v0.7.3-alpha.4 release
* Use GitHub token for automated version bump PRs
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Fix N+1 queries on categories index by batching lookups and removing partial fallbacks
* resolve Codex review suggestion about Guard subcategory against missing parents
* resolve coderabbitai review suggestion - Guard against empty categories when computing category_ids_with_transactions
* resolve coderabbitai review suggestion - Redundant pb-4 makes the conditional dead code
* resolve coderabbitai caution on PR review
* resolve sure-design review - DS Drift Patrol
* fix conflicting hidden/flex classes
* resolve jjmata review suggestion - schema.rb noise
* fix conflict and adjust related parts
* Ignore Brakeman EOLRails warning for Rails 7.2
Restore ignore entry lost during merge from main; documents upgrade
tracking and matches brakeman 7.1.2 in Gemfile.lock.
* db migration executed
* Remove deprecated focus-ring override from goals color picker.
The summary_class override was reintroduced during merge conflict
resolution and clobbered DS::Disclosure's canonical focus styling.
* fix merge conflict on disclosure.rb
* Restore parent-based semantics while keeping the performance win for root categories
* fix(ci): skip scheduled preview cleanup on forks
Only run the hourly Cloudflare preview cleanup on we-promise/sure,
where the required secrets exist.
* Revert schema.rb dump noise unrelated to categories N+1 fix
Restore db/schema.rb from main so PostgreSQL check-constraint and
column-order churn does not obscure the real PR changes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Remove obsolete Rails 7.2 EOLRails Brakeman ignore
Main is already on Rails 8.1, so the 7.2.3.1 ignore entry is no longer needed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address review: bare disclosure docs, category picker aria label
* fix(test): wait for async exchange rate updates in system test
* fix(test): retry account edit submit around Turbo morph races
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Bump version to next iteration after v0.7.3-alpha.3 release
* Fix automated version bump pull requests
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
- Remove .github/workflows/ci.yml and .github/workflows/llm-evals.yml from
pipelock exclude-paths — both already have inline # pipelock:ignore markers
- Keep Dockerfile.preview with inline explanation: uses local dev creds
(rails/rails) and Dockerfile syntax can't use # pipelock:ignore markers
* feat(mobile): standardize money typography and semantic amount color
Add a brightness-aware SureColors theme extension and a MoneyText/SureMoney
primitive (semantic success/destructive/subdued tokens + tabular figures for
column-aligned digits), then migrate the transaction lists and balance cards
off raw Colors.green/red/grey.
Step 2 of the mobile design-system sequence (#2235), after #2237's theme
foundation. Primitive-first: screens consume shared tokens/typography.
* fix: review feedback — brightness-aware token fallback, de-flake Setting tests, Pipelock localhost FP
- SureColors.of falls back to the palette matching the active brightness (not
always light) when the extension is missing, so dark surfaces stay correct.
- Clear the rails-settings-cached cache before each test; its in-memory cache
survives the per-test transaction rollback, leaking Setting.* across tests and
flaking Settings::HostingsControllerTest (stale empty string vs nil).
Full unit suite: 4952 runs, 0 failures.
- Suppress the localhost test-DB DATABASE_URL false positive with line-level
`# pipelock:ignore` in ci.yml + llm-evals.yml instead of excluding whole files,
so those workflows stay scanned for real secrets.
* fix(preview): bind :3000 instantly and bound diagnostics posts
The trusted preview deploy chain now succeeds end to end (after #2124,
#2207, #2217), but the preview container itself dies on Cloudflare with
"Container crashed while checking for ports" and no entrypoint
diagnostics ever arrive (run 27186150190).
Two compounding causes, both reproduced/measured locally against the
pinned @cloudflare/containers 0.3.3 behavior:
1. The port window is unwinnable. The library waits a hardcoded ~20s for
the container port, but the entrypoint only binds :3000 after redis,
postgres, and rails db:prepare complete -- measured at 69s under a
basic instance's 1/4 vCPU. Fix: bind :3000 within ~1s via a tiny Ruby
placeholder responder (static 503 + meta-refresh, input ignored),
verified with a TCP connect poll, and released just before the real
server starts. The worker still gates previewReady on the real Rails
/up probe and sample data, so readiness semantics are unchanged.
2. Diagnostics could stall boot and never deliver. emit_status used curl
with no timeout against the worker, whose Durable Object can be
unresponsive while it waits for this container's port (observed as
15s status timeouts in the failed run) -- so boot could deadlock
against the port check, and failure detail (#2217) never reached the
diagnostics artifact. Fix: --connect-timeout 2 --max-time 5 on all
posts, progress events fire-and-forget in the background, and failure
paths flush synchronously before exit.
Validated locally at Cloudflare basic limits (1 GiB / 0.25 vCPU): first
:3000 response in 1s, full boot to Rails /up=200 at ~76s with no OOM,
and a forced postgres failure exits 1 in 4s with the failure event
flushed. With the port window satisfied, the next real failure will
finally surface postgres detail in _container_status and CI artifacts.
* fix(preview): never read from placeholder clients
Superagent flagged on PR #2286 that the single-threaded :3000 placeholder
blocked on client.readpartial, so one connection that sends nothing -- such
as a bare TCP port probe, which is exactly what the Cloudflare port check
performs -- would wedge the accept loop and starve every later probe.
The response is static, so drop the read entirely: each connection is
written the 503 warming page and closed immediately, making the loop
effectively non-blocking per client.
Regression-tested by holding three idle TCP connections open while HTTP
probes still answered 503 immediately; full boot under basic-instance
limits (1 GiB / 0.25 vCPU) still reaches Rails /up=200 with the placeholder
answering at 1s.
* fix(preview): stop postgres crashing on Cloudflare's small /dev/shm
This is the actual root cause of the preview container dying on Cloudflare
with "Container crashed while checking for ports" (run 27341808838) and the
maintainer's "dies immediately after postgres-start" (#2217).
Reproduced locally: running the preview image with a tiny /dev/shm and a
WRITABLE root (the realistic Cloudflare model) crashes postgres on startup
with:
FATAL: could not resize shared memory segment "/PostgreSQL..." to
1048576 bytes: No space left on device
PostgreSQL 17 defaults to dynamic_shared_memory_type = posix, which
allocates dynamic shared memory in /dev/shm. Cloudflare Containers provide
only a tiny /dev/shm, so postgres FATALs before the entrypoint can bind a
port, and the container exits -> the supervisor reports it crashed during
the port check. Local Docker hid this because its default /dev/shm is 64MB.
Memory was ruled out (boots fine at -m 512m); it is specifically /dev/shm.
Fix: set dynamic_shared_memory_type = mmap so DSM is file-backed in the
data directory instead of /dev/shm. The build comments out the default
posix line, appends mmap, verifies the result, and fails hard if
postgresql.conf is missing so this critical setting cannot silently regress.
Also log fail_preview reasons to stderr so the real failure is captured by
Cloudflare container observability even when the HTTP diagnostics channel is
blocked by the worker's port wait.
Verified at Cloudflare basic-equivalent limits (1 GiB / 0.25 vCPU,
/dev/shm 64k): before, exit 1 right after "Starting PostgreSQL..."; after,
"PostgreSQL is ready" -> Rails /up=200 (~97s), placeholder answering :3000
within 1s. Normal-resource boot still reaches /up=200 in ~9s.
* fix(preview): use standard-1 instance and widen CI readiness poll
Two changes the preview needs to actually deploy and be reported ready on
Cloudflare, both validated by deploying to a real CF account.
- instance_type basic -> standard-1. The container runs postgres + redis +
puma AND generates the full demo dataset (Demo::Generator, ~12 years of
transactions), which peaks just over basic's 1 GiB and OOM-kills the
container (exit 137) before demo-data-ready. standard-1 (1/2 vCPU, 4 GiB)
completes it. Measured on real Cloudflare: rails ready ~46s, demo data
~149s, peak well under 4 GiB, no OOM.
- "Collect preview diagnostics" poll budget 40 -> 100 (~128s -> ~350s), with
the matching guard in bin/preview_deploy_security_check.rb. A real CF
standard-1 run reached previewReady at ~195s, so the old 40-poll budget
would have failed a working preview before it finished warming up. The
loop still breaks early on previewReady/previewFailed.
* fix(preview): apply DSM override to the cluster the entrypoint starts
The build selected postgresql.conf via `find ... | head -1`, which picks an
arbitrary cluster, while the entrypoint starts the highest-version cluster
(ls /etc/postgresql | sort -V | tail -1). Today only PG17 is installed so they
coincide, but if a second major version were ever present the override could
land on a cluster that never runs, silently reintroducing the /dev/shm crash.
Derive PG_CONF from the same highest-version cluster the entrypoint starts so
the dynamic_shared_memory_type=mmap override always applies to the active
cluster. Verified: build edits /etc/postgresql/17/main/postgresql.conf, and at
runtime under a tiny /dev/shm postgres starts with SHOW
dynamic_shared_memory_type = mmap.
* fix(preview): apply pg_hba trust rules to the cluster the entrypoint starts
Same latent issue as the dynamic_shared_memory_type override: the build wrote
the local `trust` rules to `find ... | head -1` (arbitrary cluster), while the
entrypoint starts the highest-version cluster. With a single PG17 they coincide,
but a second major version would send the trust rules to a cluster that never
runs, breaking the entrypoint's trust-auth db setup.
Derive PG_HBA from the same highest-version cluster (ls /etc/postgresql |
sort -V | tail -1) and fail the build if it's missing. Verified under a tiny
/dev/shm: postgres starts and CREATE ROLE/CREATE DATABASE succeed via trust.
* Fix prerelease version bump workflow
* Fix prerelease version-bump job: add PR fallback for protected branches
### Motivation
- The prerelease version bump job was failing when it could not push to protected release branches, causing the workflow to error instead of progressing.
- The job needs permission and robust push logic so prerelease automation can update `.sure-version` and `charts/sure/Chart.yaml` even when direct pushes are blocked.
### Description
- Grant the `bump-pre_release-version` job `pull-requests: write` permission so it may open an automated PR when direct pushes are disallowed.
- Harden the bump step: enable `set -euo pipefail`, pass `GH_TOKEN` into the job environment, and use fully-qualified branch refs for pushes.
- Add retry + rebase logic for direct pushes and a fallback path that pushes the changes to an `automation/bump-version-after-...` branch and opens a PR with `gh pr create` when direct push attempts fail.
- Preserve existing validations that ensure `.sure-version` and `charts/sure/Chart.yaml` exist and the prerelease portion is parsed before writing the bump.
### Testing
- Verified workflow YAML parses with `ruby -e 'require "yaml"; YAML.load_file(".github/workflows/publish.yml"); puts "YAML OK"'` and it succeeded.
- Confirmed the bump job has the expected permission with `ruby -e 'require "yaml"; workflow=YAML.load_file(".github/workflows/publish.yml"); abort("missing bump job") unless workflow.dig("jobs", "bump-pre_release-version", "permissions", "pull-requests") == "write"; puts "bump permissions OK"'` and it succeeded.
- Ran ActionLint via `go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.8 .github/workflows/publish.yml` and received no actionable errors.
- Ran `git diff --check` to ensure no whitespace/format issues and it succeeded.
* Harden pre-release bump job: validation, retries and PR fallback
### Motivation
- Make the `bump-pre_release-version` job more robust by validating prerelease version formats and failing early on unexpected inputs.
- Handle protected branches gracefully by attempting direct pushes with retries and falling back to creating a pull request when direct push is blocked.
- Ensure the workflow has the permissions required to open PRs when needed.
### Description
- Added `pull-requests: write` to the job `permissions` so the workflow can create PRs when required.
- Hardened the bump script with `set -euo pipefail` and a strict regex that validates and extracts `BASE_VERSION`, prerelease tag, and number using `BASH_REMATCH` before incrementing the prerelease counter.
- Simplified and made file reads safer by using input redirection for `tr` and improved error messages for missing or malformed version files.
- Reworked commit/push logic to set a commit message variable, attempt direct pushes with exponential backoff and detection of branch-protection errors, and when rejected create a dedicated bump branch and open a PR via `gh pr create`.
### Testing
- Validated the modified workflow YAML with a YAML linter; no syntax errors were reported.
- Executed the updated bump logic in a CI dry-run (workflow syntax validation on GitHub Actions) and the workflow file was accepted by the Actions syntax checks.
* Add manual workflow triggers and robust pre-release bump with protected-branch fallback
### Motivation
- Allow manual invocation of PR and chart CI workflows via `workflow_dispatch` for on-demand runs.
- Make the pre-release version bump process more robust and reliable when `refs/tags/v*` releases are published.
- Ensure the bump can proceed even when the target branch is protected by creating an automated pull request and running PR checks.
### Description
- Added `workflow_dispatch` to `.github/workflows/pr.yml` and `.github/workflows/chart-ci.yml` to support manual runs.
- Extended `bump-pre_release-version` job in `.github/workflows/publish.yml` to request `actions: write` and `pull-requests: write` permissions.
- Rewrote the bump script to use `set -euo pipefail` and a single regex validation to parse and increment prerelease versions (e.g. `1.2.3-alpha.4`), update `.sure-version`, and update `charts/sure/Chart.yaml` reliably.
- Improved commit and push flow by adding `GH_TOKEN`, using a consistent commit message variable, attempting direct pushes with retry and rebase, detecting branch-protection failures, and falling back to creating a bumped branch and an automated PR via `gh pr create`; the workflow also dispatches `pr.yml` and `chart-ci.yml` runs for the created branch.
### Testing
- No automated tests were run as part of this change; behavior will be exercised when the workflow creates a bump PR or when workflows are manually triggered via the new `workflow_dispatch` events.
* fix(helm): normalize appVersion to strip leading v (#2050)
Releases triggered on a tag like `v0.7.1-rc.1` end up writing
`appVersion: "v0.7.1-rc.1"` into Chart.yaml / the published
index.yaml, but the Docker image is pushed to GHCR without the leading
`v` (`ghcr.io/we-promise/sure:0.7.1-rc.1`). Flux CD / any consumer
that pulls the chart then fails with `ImagePullBackoff` against
`v0.7.1-rc.1` (a tag that doesn't exist).
`normalize_version` is already applied to `CHART_VERSION`; route the
two tag-derived `APP_VERSION` paths through the same helper so the
appVersion matches the published image tag.
Closes#2050
* chore(ci): bind helm-publish version inputs to step env (#2050)
@coderabbitai (zizmor) flagged that the version-resolve step expanded
${{ inputs.chart_version }} and ${{ inputs.app_version }} directly
into bash, which is a template-injection vector — a malicious caller
of this reusable workflow could inject shell via an input like
'; rm -rf … #'.
Bind both inputs to step env (CHART_VERSION_INPUT,
APP_VERSION_INPUT) and reference them as shell variables in the
conditionals. Behaviour is unchanged; the values just arrive through
the env table instead of the runner's template pass.
---------
Co-authored-by: jeffrey701 <jeffrey701@users.noreply.github.com>
* ci(preview): rewrite image config before registry push
Point the trusted preview deploy config at the loaded CI image before Wrangler validates the worker config for the Cloudflare registry push. This keeps the existing trusted deploy boundary intact while fixing the post-2062 image-push ordering regression.
* ci(preview): require trusted readiness diagnostics
* ci(preview): use nonce for diagnostics events
* ci(preview): retain diagnostics timing anchors
* chore(ci): pin GitHub Actions to commit SHAs (#1811)
Follow-up to #1810. The Node-24 upgrade left every workflow on mutable
tag refs (`actions/checkout@v5`, `actions/download-artifact@v7`, etc.)
which superagent-security[bot] flagged on the ci.yml + publish.yml
reviews.
Pin all 18 external actions to the commit SHA they currently resolve to
and add a trailing `# vMAJOR.MINOR.PATCH` comment so reviewers can see
the version. Local reusable-workflow refs (`uses: ./.github/...`) are
left alone — pinning those would defeat the point.
Closes#1811
* chore(ci): address review — persist-credentials + setup-node consistency (#1811)
Two pieces of follow-up feedback on the SHA-pinning PR:
- @coderabbitai (P1 nitpicks) + @JSONbored: add 'persist-credentials:
false' to checkout steps in jobs that don't perform authenticated git
operations. Adds the line to 17 read-only checkouts across 9
workflows (chart-ci, ci, flutter-build, helm-publish, ios-testflight,
llm-evals, preview-cleanup, preview-deploy, publish:build).
Checkouts inside jobs that 'git push' (chart-release, mobile-build,
mobile-release, helm-publish:second-checkout, publish:bump-pre_release)
are intentionally left alone so they keep their token.
- @jjmata: preview-deploy.yml was the only workflow on
actions/setup-node v6.4.0; everywhere else pinned v5.0.0. Standardise
on v5.0.0 to match.
Dependabot config already has a github-actions ecosystem entry with a
weekly schedule, so no addition needed for that point.
* chore(ci): document intentional setup-node v6→5 normalization (#1811)
@superagent-security flagged the v6.4.0 -> v5.0.0 change in
preview-deploy.yml as a possible unintended downgrade. The downgrade
was deliberate, per @jjmata's review request to normalize setup-node
across all workflows. Add an inline YAML comment next to the line so
future scans don't re-flag it.
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: jeffrey701 <jeffrey701@users.noreply.github.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
* ci(preview): split PR image builds from trusted deploys
* ci(preview): harden preview artifact handoff
Move the preview image artifact into the trusted preview workflow as a no-secret build job, gate deployment on base-trusted workflow definitions, and keep Cloudflare credentials isolated to the deploy-only job.
Also fail closed when the pushed image reference is not written into wrangler.toml and expand the preview deploy guard to enforce the same-run artifact and permission boundaries.
* ci(preview): move preview builds out of privileged trigger
* ci(preview): avoid secret-shaped wrangler env assignments
* ci(preview): keep wrangler credential env explicit
* ci(preview): isolate deployment tooling
Keep PR preview source separate from the deployment toolchain by building a temporary deploy workspace from base-revision preview metadata and PR-owned source.
Add a focused CI guard so future preview workflow edits preserve the trusted tooling split.
* ci(preview): harden workflow guard checks
Address CodeRabbit feedback by making the preview deploy guard assertions collision-proof and more resilient to equivalent GitHub Actions expression and workspace path forms.
* ci(preview): normalize workflow guard paths
* ci(preview): defer workflow guard validation
* revert(preview): restore workflow guard validation
* ci(preview): gate preview deployments
* feat: add Cloudflare Containers PR preview deployments
Add GitHub workflows to automatically deploy PRs to Cloudflare
Containers after tests pass, with automatic cleanup after 24 hours.
Components:
- workers/preview/: Cloudflare Worker entry point that routes
traffic to the Rails container
- preview-deploy.yml: Deploys PRs after CI passes, comments
preview URL on PR
- preview-cleanup.yml: Cleans up previews on PR close or after
24 hours via scheduled job
The container sleeps after 30 minutes of inactivity and wakes
automatically on the next request.
Required secrets:
- CLOUDFLARE_API_TOKEN
- CLOUDFLARE_ACCOUNT_ID
- CLOUDFLARE_WORKERS_SUBDOMAIN
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: use development environment with embedded PostgreSQL for previews
- Add preview-specific Dockerfile with PostgreSQL server included
- Add docker-entrypoint.sh to start PostgreSQL and run migrations
- Change RAILS_ENV from production to development
- Auto-generate SECRET_KEY_BASE and DATABASE_URL for self-contained previews
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* feat: add Redis to preview container
- Install redis-server in the preview Dockerfile
- Start Redis in the entrypoint before PostgreSQL
- Auto-configure REDIS_URL for Sidekiq background jobs
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: mark GitHub deployment inactive on manual PR cleanup
When using workflow_dispatch with a specific pr_number, the workflow
now also marks the associated GitHub deployment as inactive, mirroring
the behavior of the batch cleanup path.
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: remove npm cache config that requires missing lockfile
The setup-node action's cache feature requires a package-lock.json
which doesn't exist in workers/preview/. Remove the cache configuration
to fix the workflow.
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: only update deployment status when deployment ID exists
Add condition to check steps.deployment.outputs.result exists before
attempting to update deployment status. This prevents a JavaScript
syntax error when the deployment step fails and no ID is available.
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: quote shell variables to fix SC2086 shellcheck warning
Quote the --var argument and GITHUB_OUTPUT redirection to prevent
word splitting issues.
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: add permissions for deployment status operations
Add deployments: write permission to the cleanup workflow so the
GITHUB_TOKEN can list and update deployment statuses.
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: specify build context for Dockerfile in wrangler config
Use object syntax for image config to set build context to repository
root, allowing the Dockerfile to reference files from both the root
(Gemfile, .ruby-version) and workers/preview/ (docker-entrypoint.sh).
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: run wrangler from repo root for correct build context
- Update workflow to run wrangler with --config flag from repo root
- Update wrangler.toml paths (main, image) to be relative to repo root
- Embed entrypoint script directly in Dockerfile using heredoc
- Remove separate docker-entrypoint.sh file
This ensures the Docker build context includes Gemfile, .ruby-version,
and other files at the repo root.
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: move preview Dockerfile to repo root for correct build context
Wrangler resolves paths relative to the config file, not the current
directory. Moving Dockerfile.preview to repo root ensures:
- Build context is the repo root (where Gemfile, .ruby-version are)
- Path in wrangler.toml is ../../Dockerfile.preview (relative to config)
- Worker runs from workers/preview/ directory again
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: use find to locate pg_hba.conf instead of glob in redirection
Shell glob patterns don't work with redirection operators. Use find
to locate the actual pg_hba.conf path before writing to it.
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: enable workers_dev for preview deployments
Add workers_dev = true to make the preview worker accessible via
the workers.dev subdomain.
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* feat: enable observability for container logs
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix preview container boot path
* fix: set preview container startup command explicitly
* fix: update preview worker compatibility date
* chore: expose preview container diagnostics
* fix: recover from stale preview container state
* fix: harden preview container startup paths
* chore: report preview startup stages
* fix: bypass stale container helper state during recovery
* fix: allow longer preview container startup
* fix: upgrade preview container runtime
* fix: use supported node version for preview deploy
* fix: use public container startup flow
* fix: simplify preview container startup
* chore: retain preview container diagnostic history
* fix: bypass systemctl redirect for postgres startup
* chore: probe rails readiness from inside preview container
* chore: capture rails process and port diagnostics
* chore: capture rails startup logs on preview timeout
* fix: align preview bind behavior with ipv6 startup model
* chore: capture preview socket state on rails timeout
* chore: capture rails wait state and child processes
* fix: launch preview with puma directly
* fix: run preview in production mode
* chore: probe preview app boot before puma
* fix: disable lookbook routes in production preview
* chore: capture ruby backtrace from hung boot probe
* fix: disable bootsnap in preview runtime
* fix: disable sidekiq web routes in production preview
* chore: trace hung preview boot probe with strace
* fix: json-escape preview telemetry payloads
* fix: pass preview telemetry env vars correctly
* chore: signal ruby child for preview boot backtrace
* fix: allow longer preview cold-start budget
* fix: skip sidekiq web requires in production preview
* chore: deploy hello world preview container
* fix(preview): restore rails image without redundant warmup
* feat(preview): seed demo dataset on boot
* ci(preview): require preview-cf label
* ci(preview): reuse pr workflow checks
* fix(preview): avoid clearing demo data in production boot
* fix(preview): tolerate already-running postgres on boot
* fix(preview): check demo user via psql during boot
* fix(preview): defer heavy demo seed until after boot
* fix(preview): move demo-user creation after rails boot
* fix(preview): fail fast on container lifecycle errors
* fix(preview): validate manual cleanup pr input
* fix(preview): parameterize preview pr number
* ci(preview): use setup-node v6
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: KiloClaw <kiloclaw@openclaw.ai>
* Extract version to .sure-version file and add Sentry release tracking
Move the hardcoded version string to a `.sure-version` file at the repo
root so it can be read by both the Rails version initializer and other
tooling. Configure `config.release` in the Sentry initializer to tag
errors with the app version.
https://claude.ai/code/session_01KfUgF42B3exoU2vpErqJyW
* Use .sure-version as single source of truth in Helm CI workflows
Update chart-ci, chart-release, and publish workflows to read the app
version from .sure-version instead of regex-parsing version.rb. The
pre-release bump job now writes directly to .sure-version and stages it
for commit.
https://claude.ai/code/session_01KfUgF42B3exoU2vpErqJyW
* Guard empty .sure-version fallback
* fix: sync Helm chart version with .sure-version
* Moving on to `v0.7.1-alpha.*` with this
* Defensive rescue
* Getting fancy with versions now
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: SureBot <sure-bot@we-promise.com>
Co-authored-by: sure-admin <sure-admin@splashblot.com>
* Add Google Play upload workflow for mobile releases
* Fix Google Play workflow reuse and track input
* Gate Play upload on AAB artifact availability
* Harden Play release notes input handling
* feat(ci): improve LLM eval visibility in GitHub Actions
- Add step summary output for each eval run (shows in GH UI)
- Add new 'summarize_evals' job that aggregates results from all matrix runs
- Generate markdown table with accuracy, cost, and duration for all evals
- Add threshold checking (fails workflow if accuracy < 70%)
- Include status icons (✅/❌) for quick visual assessment
- Show overall pass/fail status at the end of summary
* Fix LLM eval workflow summary
---------
Co-authored-by: SureBot <sure-bot@we-promise.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
* feat: add Binance support (Items, Accounts, Importers, Processor, and Sync)
* refactor: deduplicate 'stablecoins' constant and push stale_rate filter to SQL
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>