Compare commits

...

172 Commits

Author SHA1 Message Date
Julian
4c62e2a75d Empty commit to bump CI 2026-05-03 03:39:27 +00:00
Julian Dominguez-Schatz
922f0b8a53 Update docs release date 2026-05-02 13:47:55 -04:00
Julian Dominguez-Schatz
cfd527b446 Fix shared worker resumption after tab suspend (#7656)
* [AI] Fix SharedWorker tab resume recovery

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* [AI] Fix SharedWorker reload readiness

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add release notes

* Update packages/desktop-client/src/shared-browser-server-core.ts

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
2026-05-02 13:46:31 -04:00
Julian Dominguez-Schatz
56ea6cba68 Update author
Updated author information in the release notes.
2026-04-27 23:38:40 -04:00
github-actions[bot]
60c0796a92 Generate release notes for v26.5.0 2026-04-27 21:52:09 +00:00
Matt Fiddaman
7b0460d7e9 fix infinite loop when remainder is impossible to solve (#7623)
* fix infinite loop when remainder is impossible to solve

* note
2026-04-27 22:51:20 +01:00
Matt Fiddaman
46ba63f370 increase test coverage for budget templates (#7620)
* [AI] cover existing template engine logic with regression tests

Adds tests for goal template behavior that predates this PR so the
suite can be cherry-picked onto master to confirm no regressions. No
production code changes.

Covers:
- init() validation: schedule names, by/schedule priority match, past
  by-target with and without annual/repeat, percentage source not
  found, special source aliases, duplicate limit/spend/goal
  directives, weekly limit missing start date, invalid limit period,
  unrecognized periodic period
- runRemainder cap clamping and hideDecimal fraction removal
- Income-category branch in runTemplatesForPriority
- getLimitExcess against an aggregate weekly cap
- Past by-target rolling forward via the annual period
- runSchedule full=true (no sinking accumulation), percent and fixed
  adjustments, completed-schedule filtering, past-date error for
  non-repeating schedules, monthly/weekly/daily sinking contribution
  branches when interval exceeds the pay-month-of cap, surplus
  absorption when last-month balance exceeds the target, and
  tracking-budget mode forcing all schedules pay-month-of
- applyMultipleCategoryTemplates orchestration: per-category writes,
  cross-category priority clamping when funds run out, error
  notification path
- applyTemplate force=false skipping already-budgeted categories

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* note

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 22:51:20 +01:00
github-actions[bot]
4926cd5d76 Generate release notes for v26.5.0 2026-04-27 21:48:23 +00:00
Emil Tveden Bjerglund
05efe9ceee Fix Sankey income bug, when payee it not set (#7632)
* Ensure income categories are shown correct, even if payee is not set

* Add release note
2026-04-27 22:47:35 +01:00
Matt Fiddaman
da522f3e3e add release note highlights 2026-04-27 22:35:34 +01:00
github-actions[bot]
8636591ddf Generate release notes for v26.5.0 2026-04-27 21:30:22 +00:00
Matt Fiddaman
b5d59c7428 fix lint (#7643) 2026-04-27 22:29:28 +01:00
Matt Fiddaman
c02e308739 fix cherrypicked commits not being respected and lint race in release note generation workflow (#7640)
* fix cherrypicked commits not being respected and lint race

* note

* coderabbit suggestions

* fix lint

* make double restore possibility safe
2026-04-27 21:59:57 +01:00
Matt Fiddaman
a5e80edd32 fix release note generation script (#7635)
* fix release note generation script

* note
2026-04-27 20:48:40 +01:00
github-merge-queue
9beeae54e0 🔖 (26.5.0) 2026-04-25 17:13:14 +00:00
Matt Fiddaman
eb922fd191 sankey card should follow report settings (#7619)
* sankey card should follow report rules

* note
2026-04-25 16:17:45 +00:00
Julian Dominguez-Schatz
2b584e1ad0 Make double ctrl-f trigger browser-native search (#7605)
* Make double Ctrl-f trigger browser find

* Add release notes

* Invert condition

* Rename release note
2026-04-25 00:53:25 +00:00
dependabot[bot]
4f1bc3fcdd Bump postcss from 8.5.8 to 8.5.10 (#7613)
* Bump postcss from 8.5.8 to 8.5.10

Bumps [postcss](https://github.com/postcss/postcss) from 8.5.8 to 8.5.10.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.8...8.5.10)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Add release notes for postcss version bump

Updated postcss version for maintenance.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Julian Dominguez-Schatz <julian.dominguezschatz@gmail.com>
2026-04-25 00:23:54 +00:00
Julian Dominguez-Schatz
ea50db524b Enable stricter electron build options (#7609)
* Enable stricter electron build options

* Add release notes

* Fix build signatures when no signing credentials are provided

* Attempt fix again
2026-04-24 23:59:28 +00:00
Julian Dominguez-Schatz
da1d0a94b9 Migrate file service to TypeScript (#7606)
* Migrate file service to TypeScript

* Add release notes

* Rabbit

* Stricter types
2026-04-24 22:50:43 +00:00
Alec Bakholdin
daab7f737e Import Transactions - Persist and set reimport deleted transactions config to true (#7610)
* fix: restored default functionality from v26.3.0 for reimport deleted transactions. import-reimport-deleted is now a synced pref that persists between imports

* release notes

* release note update

---------

Co-authored-by: Alec Bakholdin <alecbakholdin.com>
2026-04-24 18:21:45 +00:00
Emil Tveden Bjerglund
686f10247d Enhance Sankey chart datamodel, show income and allow layer filtering (#7582)
* Refactor to use directed, weighted graph as datamodel

* Fix percentage labels

* Reimplement sorting and topN handling

* Fix typing. Show toBudget on graph.

* Implement better DAG model

* Fix Other-grouping with new datamodel

* Add global sorting

* Reorder spreadsheet code for clarity

* Add percentageLabels back

* Fix all sorting modes

* Better color handling

* Handle if overbudgeted

* Fix filtering issue related to hidden nodes for Spent report

* Implement enums for special names

* Linting and typechecking

* Add layer selectors

* Trim SankeyCard

* Fix issue with empty nodes making the graph unreadable

* Add release note

* Update release note

* Reorder code

* Address coderabbit comments

* Ensure that layer-from and layer-to cannot be equal

* Update layer selectors to match selected view mode

* Fix wrong graph object reference

* Cap regex length

* Fixed wrong layer assignment for budget income categories

* Make translation not optional in createSpreadsheet

* Use predefined suffix for 'Other'

* Avoid invalid layer selection for Budgeted

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #7582

* Import translation in spreadsheet, instead of passing as argument

* Remove all non-null assertions and handle safely

* Fix most uses of 'as'

* Fix issues hiding Other categories and giving wrong toBudget value

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-24 18:20:15 +00:00
Julian Dominguez-Schatz
227c995155 Disallow reconfiguring OpenID after initialization (#7608)
* Disallow reconfiguring OpenID after initialization

* Add release notes
2026-04-24 15:12:01 +00:00
dependabot[bot]
c8224d24be Bump @xmldom/xmldom from 0.8.12 to 0.8.13 (#7596)
Bumps [@xmldom/xmldom](https://github.com/xmldom/xmldom) from 0.8.12 to 0.8.13.
- [Release notes](https://github.com/xmldom/xmldom/releases)
- [Changelog](https://github.com/xmldom/xmldom/blob/master/CHANGELOG.md)
- [Commits](https://github.com/xmldom/xmldom/compare/0.8.12...0.8.13)

---
updated-dependencies:
- dependency-name: "@xmldom/xmldom"
  dependency-version: 0.8.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-23 20:06:30 +00:00
Dakyne
29a06b23ea Add Gruvbox Light and Dark themes to custom theme catalog (#7571)
* Add Gruvbox Light and Dark custom themes to catalog

* Add release note for PR #7571

* [autofix.ci] apply automated fixes

* Update packages/desktop-client/src/data/customThemeCatalog.json

Co-authored-by: Joel Jeremy Marquez <joeljeremy.marquez@gmail.com>

---------

Co-authored-by: Dakyne <fawn_salable_73@icloud.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Joel Jeremy Marquez <joeljeremy.marquez@gmail.com>
2026-04-23 14:25:05 +00:00
Matiss Janis Aboltins
aeb28d3b87 Add Discord notification for nightly theme catalog scan failures (#7595)
* [AI] Notify Discord when nightly theme catalog scan fails

Adds an if: failure() step to the validate-theme-catalog job that posts a
minimal alert to the DISCORD_WEBHOOK_URL webhook with a link back to the
failing workflow run. Fires on both theme validation failures (script exits
1) and earlier step failures (checkout/setup), so infrastructure breakage
is also surfaced. nofail: true keeps a Discord outage from cascading into
a red job.

* [AI] Drop setup comment from Discord notify step

* [AI] Move Discord notify to its own job gated by an environment

Splits the notify step into a separate notify-failure job that depends on
validate-theme-catalog and runs only on failure. The new job binds to the
nightly-alerts GitHub Environment so the DISCORD_WEBHOOK_URL secret is
scoped to a dedicated environment rather than inherited at the repo level
(zizmor secrets-without-environment).

* [AI] Add release notes for 7595

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 06:58:21 +00:00
dependabot[bot]
bd1da27404 Bump dompurify from 3.3.2 to 3.4.1 (#7591)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.3.2 to 3.4.1.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.3.2...3.4.1)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
2026-04-22 20:51:12 +00:00
Matiss Janis Aboltins
7501674613 [AI] Fix Docker build for workspace:* dependencies (#7564)
* [AI] Fix Docker build for workspace:* dependencies

Since @actual-app/crdt became a workspace:* dep, `yarn workspaces focus
--production` creates relative symlinks in node_modules that dangle when
only node_modules is copied into the prod image, breaking local Docker
builds with ERR_MODULE_NOT_FOUND: @actual-app/crdt.

Dereference yarn's workspace symlinks in the builder stage with `cp -RL`
so the prod stage can copy a self-contained node_modules without needing
to enumerate which workspace:* deps exist. Adding a new workspace:* dep
now requires zero Dockerfile changes.

Also move the sync-server .dockerignore to the repo root (and drop stray
local node_modules / .git / .yarn caches from the build context), since
docker builds use the repo root as context — the old sync-server-level
file was no longer being applied.

Fixes #7561.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [AI] Strip dev-only dirs from dereferenced workspace packages

The generic `cp -RL` step copies full workspace package trees into the
image (src/, e2e/, tests, build-stats, etc.). Remove them after the
dereference — they're not needed at runtime, and skipping them recovers
~67MB from the final image on both alpine and ubuntu variants.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [AI] Rephrase 7564 release note to be user-facing

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:20:51 +00:00
Jaime R Calzada
c3717e7036 [AI] Add per-schedule custom upcoming length override (#7434)
* [AI] Add per-schedule custom upcoming length override

* [AI] Add release notes for PR #7434

* [AI] Add custom length option to per-schedule upcoming length selector

* [AI] Deduplicate preset values and guard against malformed custom upcoming length

* [autofix.ci] apply automated fixes

* [AI] Retrigger CI (flaky accounts E2E test)

* [AI] Improve schedule editor layout for upcoming length field

Restructure the custom upcoming length from a checkbox + conditional
dropdown into a proper FormField with a single Select that includes
"Use global default" as the null option. Place it in a responsive
side-by-side row with the auto-post checkbox, consistent with the
form's existing layout patterns.

* [AI] Address CodeRabbit review feedback

Tighten custom upcoming length validation with a proper regex
instead of a loose hyphen check. Replace fixed height with minHeight
on the auto-post checkbox row to avoid clipping translated labels.

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #7434

* [AI] Fix custom_upcoming_length not persisting on mobile

The mobile schedule save handler was missing the custom_upcoming_length
field from the payload sent to schedule/create and schedule/update.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-04-22 16:33:21 +00:00
Matt Fiddaman
9d91da77ec move from tsgo dev preview to beta (#7587)
* move from tsgo dev preview to beta

* note
2026-04-22 15:22:14 +00:00
Copilot
1c97388654 [AI] Consolidate npm release and nightly publishing into one workflow (#7583)
* [AI] Unify npm release and nightly publish workflows

Agent-Logs-Url: https://github.com/actualbudget/actual/sessions/3f8de051-a9a7-4527-88d8-5c44bc06a562

Co-authored-by: jfdoming <9922514+jfdoming@users.noreply.github.com>

* [AI] Harden unified npm publish workflow conditionals

Agent-Logs-Url: https://github.com/actualbudget/actual/sessions/3f8de051-a9a7-4527-88d8-5c44bc06a562

Co-authored-by: jfdoming <9922514+jfdoming@users.noreply.github.com>

* [AI] Clarify nightly install step and add concise release note

Agent-Logs-Url: https://github.com/actualbudget/actual/sessions/af3d68aa-d217-47be-addb-1b40b08f533b

Co-authored-by: jfdoming <9922514+jfdoming@users.noreply.github.com>

* [AI] Revert release note edit and make npm publish workflow ACT-compatible (#7584)

* Initial plan

* [AI] Revert release note edit and validate workflow with act

Agent-Logs-Url: https://github.com/actualbudget/actual/sessions/df98a192-197a-4df4-a804-80b69116f742

Co-authored-by: jfdoming <9922514+jfdoming@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jfdoming <9922514+jfdoming@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jfdoming <9922514+jfdoming@users.noreply.github.com>
2026-04-22 15:18:29 +00:00
Julian Dominguez-Schatz
8323a7d27c Reduce permissions in stale workflow (#7555)
* Restrict permissions on stale workflow

* Add release notes for reducing permissions in stale workflow
2026-04-22 15:17:35 +00:00
Matiss Janis Aboltins
7d4e28041c [AI] Export API models as separate entry point (#7581)
* [AI] Expose API entity types via @actual-app/api/models

Adds a new `./models` subpath export on `@actual-app/api` that re-exports
the public API entity types (`APIAccountEntity`, `APICategoryEntity`,
`APICategoryGroupEntity`, `APIFileEntity`, `APIPayeeEntity`,
`APIScheduleEntity`, `APITagEntity`, `AmountOPType`) from
`@actual-app/core/server/api-models`. Consumers can now import these types
from a stable public entry point instead of reaching into core internals:

    import type {
      APICategoryEntity,
      APICategoryGroupEntity,
    } from '@actual-app/api/models';

Uses `export type *` so the compiled `dist/models.js` is empty and no
runtime code is added. The Vite lib config is expanded to a multi-entry
map (`index`, `models`) so both bundles are produced, and tsgo already
emits `@types/models.d.ts` via the existing `declarationDir` setup.

* Add release notes for PR #7581

* Modify release notes for API model exports

Updated category from 'Features' to 'Enhancements' and added API export details.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-22 10:45:54 +00:00
Matiss Janis Aboltins
3c77b3d0d5 [AI] Add enforce-boundaries ESLint rule for architectural boundaries (#7467)
* [AI] Add enforce-boundaries ESLint rule for architectural boundaries

Disallow tsconfig compilerOptions.paths, Vite resolve.alias, and
backtracked imports (../../) to enforce the use of package.json
subpath imports (#path) as the canonical aliasing mechanism.

The rule is enabled globally as an error without autofixes.

https://claude.ai/code/session_01T7VCnq5Kid7co9vBDPHWmR

* [AI] Fix enforce-boundaries lint violations

Replace backtracked imports (../../) with subpath imports (#path):
- migrations.ts: use #migrations/* mapping
- Formula.tsx: use #components/* mapping
- TransactionsTable.test.tsx: use #mocks/* mapping

Suppress unavoidable violations with oxlint-disable comments:
- preview.tsx: cross-package theme imports (pre-existing TODO)
- vite.desktop.config.ts: handlebars resolve.alias (types require root entry)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Clean up enforce-boundaries rule: remove redundant comments, optimize Property visitor, add edge-case tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add release notes for PR #7467

* Update category for release notes

Changed category from Enhancements to Maintenance.

* [AI] Fix JSON syntax error after merge

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* [AI] Merge master and fix lint errors in enforce-boundaries rule

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-04-21 21:18:42 +00:00
Matiss Janis Aboltins
846b6a6b7a [AI] Add nightly CI scan for custom theme catalog (#7566)
* [AI] Add nightly CI scan for custom theme catalog

Adds a scheduled GitHub Actions workflow that fetches `actual.css` from
every repo in `customThemeCatalog.json` and runs it through the same
`embedThemeFonts` + `validateThemeCss` pipeline the app uses at install
time. Failing themes fail the job so maintainers get an alert when a
third-party repo introduces a regression.

The scan treats fetched CSS as opaque text: never executed, never
injected into a DOM, size-capped at 512 KB per file, 15s per fetch,
restricted to raw.githubusercontent.com with redirects disabled, and
run with `contents: read` permissions only. Each catalog `repo` is
schema-checked against `owner/repo` before being interpolated into
the URL.

* [AI] Simplify theme catalog scan

- Reuse `CatalogTheme` type from customThemes instead of duplicating as
  `CatalogEntry` in the script.
- Hoist `appendFileSync` to the static `node:fs` import; drop the dynamic
  import inside `writeStepSummary`.
- Drop the narrative header docstring and the trailing `// ...` comments
  that just restated constant names.
- Drop the redundant URL-prefix re-check inside the CSS fetch helper;
  the single call site constructs the URL from a pinned literal.
- Drop the 250 ms inter-request delay (GitHub Raw rate limits are not
  relevant for 21 requests, and the trailing delay was idle wall-clock
  against the 10-min job budget).
- Give each font fetch inside `embedThemeFonts` its own 15 s timeout
  via `AbortSignal.any`, instead of sharing one signal across every
  font in a theme. Drop the now-unnecessary caller-supplied signal
  from the CI call site.

* [AI] Fix lint on theme catalog scan imports
2026-04-21 21:18:21 +00:00
Julian Dominguez-Schatz
07c71154c9 Enable trusted publishing for releases (#7579)
* Enable trusted publishing for releases

* Add release notes for PR #7579

* Update 7579.md

* [autofix.ci] apply automated fixes

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-21 18:34:38 +00:00
Matt Fiddaman
2f49c5c400 add repository details to package.json files (#7578)
* add package URLs

* note
2026-04-21 17:40:11 +00:00
Matt Fiddaman
4b28a8146e fix error when clearing the payee field of a transaction (#7532)
* treat undefined values as missing in updates

* note
2026-04-21 17:30:45 +00:00
Matiss Janis Aboltins
362d8d60e4 [AI] Optimize CI e2e tests with pre-built bundle serving (#7503)
* [AI] Speed up and stabilize Playwright e2e tests

- Serve the prebuilt browser bundle via bin/serve-build.mjs in CI to
  skip per-shard Vite startup; 3-shard matrix with 4 workers each.
- Disable CSS animations in non-VRT runs via a fixture-level init
  script; bump expect timeout to 10s for AutoSizer-bound assertions.
- Use page.evaluate() for React Aria button clicks and a native value
  setter + single input event for controlled-input fills to eliminate
  React Aria re-render races in createAccount and Payee/Category
  autocompletes.
- Click the matching option directly (instead of Enter on a not-yet-
  highlighted list) in mobile transaction and schedule autocompletes.
- FocusableAmountInput.applyText reads the DOM input value so the
  typed amount survives a blur that fires before React flushes the
  onChange state update under CPU contention.
- MobileTransactionEntryPage.fillAmount waits for the outer display
  button (reads parent props.value) so async rules-run completes
  before the next fillField snapshots the transaction.
- MobileNavigation dispatches nav link clicks through evaluate() to
  bypass Playwright's viewport-stability check against the navbar's
  react-spring transforms.
- MobileBudgetPage summary-button lookups use locator.or().waitFor()
  instead of an isVisible() cascade.
- ConfigurationPage.startFresh/createTestFile wait for the account
  header / budget table to mount before returning.
- Workflow hardening: persist-credentials=false on all actions/checkout
  and top-level permissions: contents: read (zizmor findings).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [AI] Apply animation-disable init script to browser.newPage pages

The previous implementation extended the test-scoped `page` fixture,
but every test creates its own page via `browser.newPage()` and never
uses the fixture-provided page — so the init script was a no-op in
every test.

Move the wrap to the worker-scoped `browser` fixture: intercept
`browser.newPage` so each page created that way has `addInitScript`
applied before the caller can navigate to it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:03:56 +00:00
Julian Dominguez-Schatz
664cfdf244 Force node version 24 for trusted publishing (#7577)
* Force node version 24 for trusted publishing

* Add release notes for PR #7577

* Enable check-latest for npm setup action

* Update nightly package publishing workflow to Node.js 24

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-21 16:44:03 +00:00
Stephen Brown II
880bb67423 [AI] Fix transaction row drag breaking inline text edits (#7572)
* [AI] fix: disable transaction row drag while editing input cells

Row-level useDrag competed with notes/payee/amount fields (GH #7567).
Disable reorder drag when the row is in edit mode except for select/cleared columns.

* Add release notes
2026-04-21 16:04:17 +00:00
Julian Dominguez-Schatz
3e35d3b6f5 fix: trusted publishing requires npm version >= 11.5.1 (#7574)
* fix: trusted publishing requires npm version >= 11.5.1

* Add release notes for PR #7574

* Update .github/workflows/publish-nightly-npm-packages.yml

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>

* Update release notes for trusted publishing fix

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-04-21 16:03:37 +00:00
Matiss Janis Aboltins
75da8f1851 [AI] fix: ensure crdt builds before loot-core is packed (#7565)
The `Publish nightly npm packages` workflow started failing at the
"Pack the core package" step with:

    Cannot find module '@actual-app/crdt' or its corresponding type declarations.

PR #7541 switched `@actual-app/crdt`'s package.json to conditional
exports (`types` → `./dist/index.d.ts`). `yarn pack` for
`@actual-app/core` triggers a prepack that runs `tsgo -b`, which now
resolves `@actual-app/crdt` via the `types` condition and expects
`packages/crdt/dist/index.d.ts`. Nothing was building crdt first
because loot-core's tsconfig didn't declare it as a project
reference.

Fix: declare the project reference so `tsgo -b` walks the graph and
builds crdt before loot-core. Sibling packages already do this.

Also adopt `@monorepo-utils/workspaces-to-typescript-project-references`
to keep each package's tsconfig `references` in sync with its
`workspace:*` deps, and wire it into a new `yarn check:tsconfig-references`
step in the `check` CI job plus lint-staged. Running the tool added
`../desktop-client` references to sync-server and desktop-electron
(both declare `@actual-app/web` as a workspace dep even though they
only use it at runtime via `require.resolve`); the extra references
are harmless — in CI the corresponding build is already cached by
earlier steps.

https://claude.ai/code/session_01AA2gEMqX24GWeq5BovNmaz
2026-04-20 22:07:27 +00:00
Julian Dominguez-Schatz
29275a573d Run zizmor auto-fix tool (#7533)
* Run `zizmor` auto-fix tool

* Add release notes

* Enable credential persistence for string extraction

Updated workflow to allow pushing extracted strings.

* Enable credential persistence for release notes

Enable credential persistence to allow committing release notes.
2026-04-20 19:40:04 +00:00
Copilot
ead1b8e39d Remove redundant inline type import guideline (#7553)
* Initial plan

* [AI] Remove inline type import guideline (handled by oxfmt/oxlint)

Agent-Logs-Url: https://github.com/actualbudget/actual/sessions/7891fb33-668f-444e-bd69-5806181dcecd

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Add release notes for PR #7553

* Update author and remove redundant TypeScript guidance

Updated author credit in release notes and removed outdated guidance.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
2026-04-20 19:38:57 +00:00
Matiss Janis Aboltins
3c361fdabf [AI] Add AI Usage Policy for contributors (#7548)
* [AI] Add AI Usage Policy for contributors

Add a contributor-facing AI Usage Policy page modeled on ESLint's version,
covering disclosure, human-only interaction with maintainers, and author
responsibility. Wire it into the docs sidebar, link it from the contributing
index and the root CONTRIBUTING.md.

https://claude.ai/code/session_012RspFcLedoUjbEYknJYPiL

* [AI] Unwrap AI policy paragraphs, renumber release note

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-20 19:37:44 +00:00
James Skinner
8691766fb8 Fix reconciled value of children of split transactions (#7453)
* Fix reconciled value of children of split transactions

* Update release note

* Set mock transactions to include reconciled field
2026-04-20 17:14:26 +00:00
Julian Dominguez-Schatz
e896ce408a Enable trusted publishing for nightly npm packages (#7556)
* Enable trusted publishing for nightly `npm` packages

Ref: https://docs.npmjs.com/trusted-publishers

* Add release notes for PR #7556

* Change category to Maintenance and update description

* Fix formatting of id-token permission comment

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-20 14:38:06 +00:00
Matiss Janis Aboltins
3373154b40 Refactor CI workflows to use shared setup job (#7551)
* [AI] Run setup once per workflow and fan out via needs

Add a prep `setup` job at the top of `check.yml` and `build.yml`, and
make every other job in those workflows declare `needs: setup`.

The composite action in `.github/actions/setup` caches `node_modules`
keyed on `yarn.lock`. When that hash changes (dep-bump PRs, master after
a merge), the cache is cold and every fan-out job races to run
`yarn --immutable` in parallel — one wins the cache save, the rest do
redundant work. Serialising through a single `setup` job warms the
cache once so downstream jobs restore instantly and skip yarn install
via the existing `if: steps.cache.outputs.cache-hit != 'true'` guard.

No changes to the composite action or cache keys. `e2e-test.yml` is
intentionally left alone.

* [AI] Harden setup jobs and add release note

Address zizmor code-scanning findings on the new `setup` jobs added in
the previous commit:

- Scope `permissions: contents: read` so the job no longer inherits
  workflow-default write permissions.
- Pass `persist-credentials: false` to `actions/checkout` so the GitHub
  token isn't left on disk for later steps that don't need it.

Add `upcoming-release-notes/7551.md` to satisfy the release-notes PR
check.

* [AI] Disable credential persistence on build.yml checkouts

Each of `api`, `crdt`, `web`, `cli`, `server` in build.yml does
`actions/checkout` (which writes the GitHub token into `.git/config`)
and then uploads build artifacts in the same job. Zizmor flags this as
"credential persistence through GitHub Actions artifacts" because a
misconfigured upload path could capture `.git/config` and leak the
token.

None of these jobs push or write to git, so drop the credential
persistence via `persist-credentials: false` on the checkout.

* [AI] Disable credential persistence on check.yml checkouts

None of the jobs in check.yml (`constraints`, `lint`, `typecheck`,
`validate-cli`, `test`, `migrations`) push or write to git, so pass
`persist-credentials: false` to their `actions/checkout` calls to
resolve the zizmor "credential persistence" finding. Mirrors the fix
just applied to build.yml.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-19 21:22:49 +00:00
Matiss Janis Aboltins
f85627dcf6 [AI] Disable bundle minification for readable error messages (#7538)
* [AI] Disable bundle minification for readable production error messages

The desktop-client had dead terserOptions (no `minify: 'terser'` was set, so
Vite's default esbuild minifier ran with name mangling). The loot-core and
plugins-service workers used Terser with mangle:false but still compressed.
Set `minify: false` across all three browser build configs so production
stack traces are human-readable.

https://claude.ai/code/session_01VEywxebiNYAgJia35fygQx

* [AI] Rename release note to match PR number

https://claude.ai/code/session_01VEywxebiNYAgJia35fygQx

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-19 20:35:26 +00:00
Matt Fiddaman
695fd0e7e0 fix bank sync account linking modal being disabled when relinking existing accounts (#7487)
* fix link account modal button disabled

* note
2026-04-18 22:25:28 +00:00
Matiss Janis Aboltins
9682f6d8c9 ci: disable fail-fast for Electron build workflows (#7547)
* [AI] Disable fail-fast for Electron build matrices

Prevents cancellation of in-progress platform builds when one fails, so
Windows/macOS/Linux results are all visible on a single run.

* Add release notes for PR #7547

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-18 21:32:21 +00:00
Matiss Janis Aboltins
4b940423ee [AI] Persist custom CSS override across theme changes (#7495)
* [AI] Persist custom CSS overrides as a standalone global pref

Moves the custom CSS override out of the InstalledTheme JSON blob into
a dedicated customCssOverride global pref so that overrides survive
switching themes, clearing installed themes, or toggling auto/light/dark
mode. Includes a one-time migration that lifts the legacy overrideCss
field out of installedCustomLightTheme / installedCustomDarkTheme JSON.

- Add customCssOverride global pref (loot-core types + server defaults)
- Inject the override as a trailing style layer in CustomThemeStyle so
  it layers on top of any installed custom theme
- Drop overrideCss from the InstalledTheme type; extractLegacyOverride
  + migrateLegacyOverride handle the one-time lift with whitespace trim
- Run the migration from CustomThemeStyle with an idempotent effect that
  re-runs safely once prefs hydrate
- Bind the ThemeInstaller textarea directly to the new pref
- Add a "Custom CSS is active" indicator button next to the theme
  selector that opens the installer for editing the override without
  flipping auto mode to light
- Pre-switch out of auto when the user picks "Custom theme" from the
  main selector, so the flag that used to distinguish entry points goes
  away and handleInstall collapses to a pure slot dispatch
- Tests: hermetic Themes settings tests, expanded customThemes unit
  tests covering extraction/migration/trim edge cases, updated
  ThemeInstaller tests for the new pref binding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Address review feedback for custom CSS override installer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Defer auto-mode theme switch and guard stale installer callbacks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 15:29:32 +00:00
Matiss Janis Aboltins
9fb6876f3b [AI] sync-server: use workspace reference for @actual-app/crdt (#7541)
* [AI] sync-server: use workspace reference for @actual-app/crdt

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Update build script in sync-server package to use TypeScript's build mode

* Package electron

* [AI] crdt: add conditional exports so Node can load the built bundle

Before this change the root exports entry pointed at `./src/index.ts`, so
any pure-Node consumer (notably the sync server that Electron forks as a
utility process) failed to import `@actual-app/crdt` — Node can't execute
TypeScript source directly. Sync-server had been masking this by pulling
`@actual-app/crdt@npm:2.1.0` where `publishConfig.exports` resolves to
`./dist/index.js`; once sync-server switched to `workspace:*`, the
Functional Desktop App CI job timed out waiting for the sync server to
boot.

Switch to conditional exports in the same shape `@actual-app/api` already
uses:

- `types` → `./dist/index.d.ts` for TypeScript tooling
- `development` → `./src/index.ts` for Vite/Vitest (HMR, fast feedback)
- `default` → `./dist/index.js` for Node runtime

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:27:34 +00:00
kenkuo
210422f61a [AI] Add 'Last 30 days' date range option to custom reports (#7217)
* Add 'Last 30 days' date range option to custom reports

Add a rolling 30-day date range to the Live mode date filter in custom
reports. The option appears between 'Last month' and 'Last 3 months'
and is available for Daily, Weekly, and Monthly intervals.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* [autofix.ci] apply automated fixes

* Fix release note filename and author to match PR #7217

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-04-18 15:10:35 +00:00
Aadhith
6941cc9e01 Add Ilavenil theme to custom theme catalog (#7543)
* Add theme Ilavenil to custom catalog

* Add release notes
2026-04-18 14:38:08 +00:00
tempiz
13abe0cb00 Addition of scoped ErrorBoundarys per #7391 (#7497)
* Addition of scoped ErrorBoundarys per 7391

* Adjusted to use FeatureErrorfallback from #7437
2026-04-17 20:52:57 +00:00
Matt Fiddaman
a4e401bc8b fix build error with typescript v6 (#7524)
* fix tsconfig error

* note

* fix strict mode violation

* fix another type issue
2026-04-17 20:52:44 +00:00
Matiss Janis Aboltins
ff7f81ac06 [AI] Emit bundle stats from the crdt package (#7537)
* [AI] Emit bundle stats from the crdt package

The crdt package was the only published library without a stats.json
artifact. Migrate its build to Vite (mirroring the api/cli setup), wire
in rollup-plugin-visualizer to emit dist/stats.json, and upload it from
the CRDT CI job. Declarations are still produced by tsgo via
--emitDeclarationOnly.

https://claude.ai/code/session_01CDVAGLGu49q5YMHsRLkYLQ

* Add release notes for PR #7537

* [AI] crdt: drop redundant rm -rf dist from build script

Vite's build.emptyOutDir: true already clears the output directory
before writing, so the leading rm -rf dist is unnecessary.

https://claude.ai/code/session_01CDVAGLGu49q5YMHsRLkYLQ

* [AI] Include crdt in the size-compare bundle stats table

Wait for the crdt build check on both the base branch and the PR,
download the crdt-build-stats artifact for each, and pass it to
bundle-stats-comment.mjs so the summary table rendered on the PR
includes a row for the crdt package alongside desktop-client,
loot-core, api, and cli.

https://claude.ai/code/session_01CDVAGLGu49q5YMHsRLkYLQ

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-17 20:38:06 +00:00
Michael Clark
5bf160463c 🎨 Update storybook fonts and brand image (#7536)
* update storybook fonts and brand image

* release notes
2026-04-17 19:26:24 +00:00
Matiss Janis Aboltins
598bf81da1 [AI] crdt: typecheck test files and clean up lint issues (#7534)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:33:02 +00:00
Julian Dominguez-Schatz
995670476e Add GitHub Actions check step via zizmor (#7465)
* Add GitHub Actions check step via `zizmor`

* Add security-events permissions to check-gh-actions

Added permissions for security events in GitHub Actions.

* Add persist-credentials option to checkout action

* Add release notes for PR #7465

* Change category to Maintenance and update action step

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-17 11:55:51 +00:00
Matt Fiddaman
711939f71c replace uuid and fs-extra with builtins (#7529)
* fs-extra

* uuid

* note
2026-04-16 20:42:27 +00:00
Matt Fiddaman
8486dca33a clean up more unused dependencies (#7528)
* remove unused deps

* note

* remove babel

* fix lint
2026-04-16 20:23:13 +00:00
Matt Fiddaman
359c1fe9ce consolidate internal naming patterns used for budget types (#7527)
* consolidate budget naming pattern

* note

* coderabbit feedback
2026-04-16 17:37:21 +00:00
Matt Fiddaman
7a4b43e7a4 fix some more GitHub code quality issues (#7520)
* Apply suggested fix to packages/desktop-client/src/components/modals/EditFieldModal.tsx from Copilot Autofix

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* Apply suggested fix to packages/desktop-client/src/components/modals/EditFieldModal.tsx from Copilot Autofix

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* note

* fix typo in note

* remove useless conditions

---------

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-04-16 17:07:22 +00:00
sk10727-a11y
94ea408303 Fix filter operator switching behavior (#7304)
* [autofix.ci] apply automated fixes

* Fix render condition (code rabbit)

* [autofix.ci] apply automated fixes

* Fix filter operator switching behavior

updated to allow conversion from multi-ID to text if only one array value and to handle no-value operators for account filter

final fixes before upstream sync
Fix UUID leakage when switching filter operators
Fix UUID leakage when switching filter operators#

* fixed merge conflicts

* [autofix.ci] apply automated fixes

* Fix Account filter input not rendering due to incorrect conditional logic

* [autofix.ci] apply automated fixes

* Rename isPayeeIdOp to isIdOp for clarity

* [autofix.ci] apply automated fixes

* Trigger CI rerun

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-16 16:47:28 +00:00
Matt Fiddaman
dc8694cc3a fix potentially inconsistent state updates (#7523)
* fix potentially inconsistent state updates

* note
2026-04-16 09:41:00 +00:00
Matt Fiddaman
89b442bc74 fix runImport failing when ACTUAL_DATA_DIR environment variable is not set (#7522)
* Fix ACTUAL_DATA_DIR being used instead of fetching it from config

* note
2026-04-16 09:40:43 +00:00
Matiss Janis Aboltins
3ca0bcbc13 Remove inactive community repository links from docs (#7500)
* [AI] Remove outdated community project links from docs

* Add release notes for PR #7500

* [AI] Add back actualplaid link to community repos

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* Delete 7500.md

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-04-16 07:35:20 +00:00
Matt Fiddaman
0e5e641aae ⬆️ eslint v10 & migrate eslint plugins to oxc performant API (#7508)
* eslint (^9.39.3 -> ^10.2.0)

* migrate plugins to oxc alternative API

* note
2026-04-15 17:57:04 +00:00
Matt Fiddaman
8c47374b9d ⬆️ mid-month dependency bump (#7506)
* typescript (^5.9.3 -> ^6.0.2)

* eslint-plugin-perfectionist (^5.6.0 → ^5.8.0)

* @types/node (^22.19.15 → ^22.19.17)

* @typescript/native-preview (^7.0.0-dev.20260309.1 → ^7.0.0-dev.20260404.1)

* eslint (^9.39.3 → ^9.39.4)

* lage (^2.14.19 → ^2.15.5)

* lint-staged (^16.3.2 → ^16.4.0)

* minimatch (^10.2.4 → ^10.2.5)

* vitest (^4.1.0 → ^4.1.2)

* better-sqlite3 (^12.6.2 → ^12.8.0)

* commander (^13.0.0 → ^13.1.0)

* cosmiconfig (^9.0.0 → ^9.0.1)

* @chromatic-com/storybook (^5.0.1 → ^5.1.1)

* @storybook/addon-a11y (^10.2.16 → ^10.3.4)

* @storybook/addon-docs (^10.2.16 → ^10.3.4)

* @storybook/react-vite (^10.2.16 → ^10.3.4)

* eslint-plugin-storybook (^10.2.16 → ^10.3.4)

* storybook (^10.2.16 → ^10.3.4)

* @codemirror/language (^6.12.2 → ^6.12.3)

* @react-aria/interactions (^3.27.0 → ^3.27.1)

* @swc/core (^1.15.18 → ^1.15.24)

* @swc/helpers (^0.5.19 → ^0.5.21)

* @tanstack/react-query (^5.90.21 → ^5.96.2)

* @uiw/react-codemirror (^4.25.7 → ^4.25.9)

* @vitejs/plugin-basic-ssl (^2.2.0 → ^2.3.0)

* i18next (^25.8.14 → ^25.10.10)

* lru-cache (^11.2.6 → ^11.2.7)

* react-grid-layout (^2.2.2 → ^2.2.3)

* react-i18next (^16.5.6 → ^16.6.6)

* rolldown (^1.0.0-rc.12 → ^1.0.0-rc.13)

* sass (^1.97.3 → ^1.99.0)

* adm-zip (^0.5.16 → ^0.5.17)

* csv-parse (^6.1.0 → ^6.2.1)

* csv-stringify (^6.6.0 → ^6.7.0)

* jest-diff (^30.2.0 → ^30.3.0)

* express-rate-limit (^8.3.0 → ^8.3.2)

* upgrade yarn to 4.13.0

* react-aria-components (^1.15.1 → ^1.16.0)

* @vitejs/plugin-react (^6.0.0 → ^6.0.1)

* @codemirror/state (^6.5.4 → ^6.6.0), @codemirror/view (^6.38.7 → ^6.41.0)

* react-aria (^3.46.0 → ^3.47.0)

* react-error-boundary (^6.0.3 → ^6.1.1)

* recharts (^3.7.0 → ^3.8.1)

* fast-check (4.5.3 → ^4.6.0)

* rollup-plugin-visualizer (^6.0.11 → ^7.0.1)

* commander (^13.1.0 → ^14.0.3)

* note

* coderabbit feedback, and a test for good measure

* typescript (^5.9.3 -> ^6.0.2)

* @playwright/test (1.58.2 -> 1.59.1)

* yarn dedupe
2026-04-15 17:05:26 +00:00
Stephen Brown II
f8d5d38d0a Skip release notes generation for docs-only PRs (#6815) 2026-04-15 14:52:16 +00:00
Matt Fiddaman
023f34814c ⬆️ bump gh actions (#7507)
* upload-artifact

* codeql-action

* create-pr

* docker

* github-script

* sticky-pull-request-comment

* action-download-artifact

* note
2026-04-15 14:36:28 +00:00
JasmineLCY
2aa2e49df9 [AI] Fix regex checkbox label contrast in notes modal (#7515)
* [AI] Fix regex checkbox label contrast in notes modal (#7514)

Apply theme.menuAutoCompleteText color to the "Use Regular Expressions"
checkbox label in the find-and-replace tab so it contrasts with the
menuAutoCompleteBackground modal background.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: rename release note file to match PR number

The CI expects the release note file to be named after the PR number
(7515), not the issue number (7514).

---------

Co-authored-by: liuren.lcy <liuren.lcy@antgroup.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 13:25:41 +00:00
Aadhith
c9f5f6deb2 Add Nord theme to custom theme catalog (#7513)
* Add nord theme to catalog

* Add release notes
2026-04-15 10:08:21 +00:00
Matiss Janis Aboltins
e109d652b4 [AI] Refactor IS_GENERIC_BROWSER env var to --mode=browser (#7466)
* [AI] Refactor IS_GENERIC_BROWSER env var to --mode=browser

Replace the IS_GENERIC_BROWSER environment variable with Vite's built-in
--mode=browser flag to distinguish browser builds from Electron builds.
This aligns with the existing --mode=desktop pattern used for Electron
production builds.

Also fix build-shims.js to derive NODE_ENV from import.meta.env.DEV
instead of import.meta.env.MODE, so custom modes don't leak into
process.env.NODE_ENV.

https://claude.ai/code/session_014HvkpR59Ke4eUoiUzsUruv

* Add release notes for PR #7466

* [AI] Fix COOP/COEP headers not set with --mode=browser

The server.headers config was gated on mode === 'development', which
excluded --mode=browser. Since server.headers only applies during
vite serve (not builds), always set the COOP/COEP headers. These are
required for SharedArrayBuffer support used by the SQLite backend.

https://claude.ai/code/session_014HvkpR59Ke4eUoiUzsUruv

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-15 07:39:31 +00:00
tempiz
4878c0f333 Add maketransfer to uncategorized transactions (#7496)
* Add maketransfer to uncategorized transactions

Add maketransfer to uncategorized transactions

* retrigger checks

* retrigger checks 2
2026-04-14 23:52:34 +00:00
dependabot[bot]
18886fe166 Bump tar from 7.5.1 to 7.5.13 (#7505)
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.1 to 7.5.13.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.1...v7.5.13)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 22:32:49 +00:00
dependabot[bot]
081b43cf99 Bump jws from 3.2.2 to 3.2.3 (#7504)
Bumps [jws](https://github.com/brianloveswords/node-jws) from 3.2.2 to 3.2.3.
- [Release notes](https://github.com/brianloveswords/node-jws/releases)
- [Changelog](https://github.com/auth0/node-jws/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianloveswords/node-jws/compare/v3.2.2...v3.2.3)

---
updated-dependencies:
- dependency-name: jws
  dependency-version: 3.2.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 22:17:18 +00:00
dependabot[bot]
29827b9be8 Bump electron from 39.8.4 to 39.8.5 (#7413)
Bumps [electron](https://github.com/electron/electron) from 39.8.4 to 39.8.5.
- [Release notes](https://github.com/electron/electron/releases)
- [Commits](https://github.com/electron/electron/compare/v39.8.4...v39.8.5)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 39.8.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 21:57:49 +00:00
dependabot[bot]
b5b422f4c8 Bump lodash from 4.17.21 to 4.18.1 (#7502)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.18.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.18.1)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 21:40:42 +00:00
dependabot[bot]
21408f1e66 Bump vite from 8.0.0 to 8.0.8 (#7501)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.0 to 8.0.8.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.8/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 21:27:47 +00:00
Trevin Chow
c77b4cc220 Add scoped ErrorBoundary to Rules page to contain rendering crashes (#7437)
* [AI] Add scoped ErrorBoundary to Rules page to contain rendering crashes

* [autofix.ci] apply automated fixes

* docs: add release notes for ErrorBoundary PR

* fix(rules): add resetKeys to ErrorBoundary for route navigation reset

In react-error-boundary v6, the boundary does not auto-reset when the
user navigates away and back. Adding resetKeys={[location.pathname]}
ensures the error state clears on route changes.

This contribution was developed with AI assistance (Claude Code).

* fix(rules): show error message in fallback UI and log to console

Addresses reviewer feedback on #7437: surface error.message in the
FeatureErrorFallback so users can copy-paste when reporting issues,
and log the error to the console via useEffect so the error isn't
swallowed by the boundary.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-14 21:06:32 +00:00
Matiss Janis Aboltins
eac26fa4ef Remove duplicate excludes.txt entry (#7499)
* Remove duplicate excludes.txt entry

Remove package-lock.json from spelling exclusions

* Add release notes for PR #7499

* Fix duplicate exclusion in spelling checks

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-14 20:48:32 +00:00
dependabot[bot]
d27db77164 Bump follow-redirects from 1.15.11 to 1.16.0 (#7498)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.11 to 1.16.0.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.11...v1.16.0)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-version: 1.16.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 20:35:48 +00:00
Matiss Janis Aboltins
c7876a58cb Revert "[AI] Add project-level hook to enforce PR creation rules" (#7491)
* Revert "[AI] Add project-level hook to enforce PR creation rules (#7471)"

This reverts commit d6253c86eb.

* Add release notes for PR #7491

* Delete upcoming-release-notes/7491.md

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-14 17:12:53 +00:00
Stephen Brown II
54a26ae199 [AI] Add BALANCE_OF() function to fetch balance of an arbitrary account (#7335)
* Add BALANCE_OF() function to fetch balance of an arbitrary account

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #7335

* Adjust docs

* [autofix.ci] apply automated fixes

* Update tests to verify sidebar balances

* Add comments to schedule-template.ts

* Update index.ts to recompute sidebar balances

* [autofix.ci] apply automated fixes

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #7335

* Address CodeRabbit comments

* [AI] Implement BALANCE_OF as HyperFormula custom function via context

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* Remove protective if statement when adding _balanceOfPrefetched

* [autofix.ci] apply automated fixes

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-14 13:40:12 +00:00
Michael Clark
bf513ad11c :electron: Desktop app tests for exporting budget file (#7494)
* tests for exporting budget file

* release notes

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #7494

* ditch the vrt, it's done in the cdesktop-client

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-14 07:51:45 +00:00
Matiss Janis Aboltins
4bc8ec876a [AI] Add publishConfig.imports sync validator with pre-commit integration (#7469)
* [AI] Add publishConfig.imports sync validator with pre-commit integration

Add a TypeScript script that validates publishConfig.imports stays in sync
with imports in all packages/*/package.json files. Runs automatically in
the pre-commit hook via lint-staged with --fix mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Add release notes for #7469

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Guard main() with require.main and respect lint-staged file args

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Handle non-string imports targets in derivePublishImports

- Add type guard to check for non-string values in imports
- Throw descriptive error when conditional imports are encountered
- Update type signature to accept Record<string, string | object>
- Add test case for non-string imports error handling

This prevents TypeError when packages have conditional imports (e.g., #browser-preload in desktop-client)

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* [AI] Limit resolvePackageJsonPaths scope to packages directory

- Add packagesRoot constant to restrict path resolution
- Update while loop condition to only traverse within packages directory
- Add additional check to ensure candidate paths are under packages/
- Prevents resolution to repo root package.json for missing/deleted files

This ensures the validator only processes package.json files under packages/* and avoids accidentally targeting the monorepo root manifest.

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-04-14 07:40:36 +00:00
Matiss Janis Aboltins
c4f3fb0b93 [AI] Fix type errors for API consumers by shipping .d.ts declarations (#7468)
* [AI] Fix type errors for API consumers by shipping .d.ts declarations from loot-core

Downstream consumers of @actual-app/api with strict: true get type errors
because @actual-app/core exports raw .ts source files. Consumers' tsc
follows the import chain into core's source (compiled with strict: false),
and skipLibCheck doesn't help since it only skips .d.ts files.

Add "types" conditions to all imports/exports entries in loot-core's
package.json, pointing to the pre-built declarations in lib-dist/decl/.
Add .npmignore to include lib-dist/decl/ in the published package.

Fixes #7410

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Use prepack/postpack scripts instead of inflating package.json

Replace the inline "types" conditions in imports/exports with a prepack
script that adds them at pack/publish time. This keeps the checked-in
package.json clean while still shipping .d.ts declarations to npm
consumers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Convert prepack/postpack scripts to TypeScript

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Add release notes for #7468

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Fix recursive ExportValue type and remove redundant comment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Rename scripts to .mts and inline types conditions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Make backup/restore scripts safer

- Check if backup exists before creating it in prepack
- Make restore idempotent by checking if backup exists in postpack
- Prevents overwriting existing backups from interrupted runs
- Addresses CodeRabbit review feedback

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* [AI] Type api-handlers.ts fields to drop implicit any

The `fields` / export-args slots in the ApiHandlers contract were
untyped, surfacing as TS7008 errors in strict consumers. Replace them
with the `Partial<APIXxxEntity>` shapes the `@actual-app/api` wrappers
already pass, and annotate the matching call sites in `api.ts` with
`@ts-expect-error` where the legacy helpers still declare full-entity
parameters despite accepting partial updates at runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Replace vite-plugin-dts with tsgo for api types

Drops vite-plugin-dts in favor of running tsgo --emitDeclarationOnly
after the vite bundle, eliminating a heavy dev dependency tree
(api-extractor, volar, vue language-core) from the api package build.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Add build script to loot-core to emit declarations via lage

`yarn build:cli` failed in CI with TS6305 because api's
`tsgo --emitDeclarationOnly` depends on loot-core's pre-built
`lib-dist/decl/*.d.ts`, but loot-core had no `build` script, so lage's
`^build` cascade silently skipped it. Add `"build": "tsgo -b"` so loot-core
slots into the dependency chain; its tsconfig already has
`emitDeclarationOnly: true`, so the output is declarations only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Simplify API build

* [AI] Document TypeScript moduleResolution requirement for @actual-app/api

The published declarations rely on package.json exports conditions, which
classic node / node10 resolvers don't honor. Document the supported modes
(bundler / nodenext / node16) in the package README and in the Getting
Started section of the API docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Type-guard default value in add-types-conditions prepack

`value.default` is typed `ExportValue | undefined`, which allows nested
conditional objects. The previous truthy check fell through to
`shouldSkip(defaultValue)` and would crash on `.endsWith()` if that shape
ever appeared. Replace with a `typeof === 'string'` narrowing and drop a
now-redundant "Insert types as the first key" comment.

No runtime change on current package.json — no nested `default` values
exist today — but the script is not covered by loot-core's tsconfig
include, so the latent type issue was silent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Allow "nodenext" in docs spellcheck expect list

Referenced in the new TypeScript moduleResolution note in the API docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Move loot-core declarations to @types and whitelist publish with files

Relocate loot-core's composite TypeScript output from lib-dist/decl to the
top-level @types directory, matching the api package's convention. Replace
the old .npmignore blacklist with an explicit package.json files whitelist.

- tsconfig.json: outDir @types, exclude test/mock dirs from decl emission
- scripts/add-types-conditions.mts: rewrite paths to ./@types/src/...
- package.json: files whitelist shipping only src, @types, migrations,
  typings, default-db.sqlite; drop legacy typesVersions (docs now require
  moduleResolution bundler/nodenext/node16, so the classic-resolution
  fallback is unused)
- .gitignore: ignore the new @types build artifact
- lage.config.js: factor outputGlob into a shared BUILD_OUTPUT_GLOBS
  constant and add @types/** so lage caches loot-core's decl output
- root tsconfig.json: tighten exclude from packages/api/@types to
  packages/*/@types to cover both api and loot-core
- delete .npmignore entirely

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Build loot-core declarations inside prepack

yarn workspace @actual-app/core pack is the first non-setup step in the
publish workflow, running before any build. Without a build chained into
prepack the @types/ tree is empty at pack time, so the tarball shipped a
transformed package.json pointing at ./@types/src/... paths that didn't
exist. npm publish doesn't re-run hooks on a pre-packed tarball, so the
frozen snapshot must be self-contained; prepack now runs yarn build first
to populate @types/ before add-types-conditions rewrites the exports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-04-14 07:40:17 +00:00
youngcw
69cd4fc13a Update sync error message (#7493)
* format

* note

* update test

* change message

* note

* fix
2026-04-13 19:07:22 +00:00
Juulz
35a48cbf15 🎨 You Need A - Theme Dark (#7447)
* Update customThemeCatalog.json

* Update customThemeCatalog.json

* [autofix.ci] apply automated fixes

* Update customThemeCatalog.json

* 🎨 You Need A Theme Dark, based on 2026 nYNAB dark

* YNA Theme Dark

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-13 15:17:53 +00:00
Emil Tveden Bjerglund
213185661f [AI] Add Sankey report toggle to view values as percentages (#7476)
* Add Options button. Add toggle for showing values as percentages.

* Add release note

* [autofix.ci] apply automated fixes

* Ensure 0's are also reported as percentages for consistency

* Shorten release note

* Remove reduncant type assertions

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-13 14:26:26 +00:00
Michael Clark
4fa658b91e :electron: Playwright test to verify sync server starts (#7484)
* test for opening sync server

* urelease note

* lint

* remove console logs from test

* wait on navigation

* works locally... Pipelines failing

* testing

* more

* rebuild electron before testing

* jeeezoo

* ipv4 issue?

* [autofix.ci] apply automated fixes

* add rebuild step to vrt

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #7484

---------

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-13 09:58:20 +00:00
Michael Clark
95b2925be6 :electron: Fix desktop app playwright vrt setup (#7490)
* fix desktop app playwright vrt tests

* release notes

* add bcrypt in as well

* install build tools for bcrypt....
2026-04-13 08:04:15 +00:00
Matt Fiddaman
db72948d7c change release process to use a branch and add automation (#7418)
* release automation

* note

* harden tokens for release notes workflow

* fix yarn install on release notes check

* amend release docs

* address coderabbit

* change back to auto version resolution

* more coderabbit

* clarify a poorly worded comment

* drop cherry pick workflows

* fix docker tag

* coderabbit

* drop `v` from release branch name

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>

* simplify release note workflows

---------

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
2026-04-12 22:17:14 +00:00
Matt Fiddaman
2c4d64eaac fix docker tags in docs (#7486) 2026-04-12 20:06:08 +00:00
Matiss Janis Aboltins
78b1fc2713 [AI] Add no-extraneous-dependencies lint rule (#7480)
* [AI] Add no-extraneous-dependencies lint rule to prevent transitive dependency usage

Closes #7479. Adds a custom ESLint rule that flags imports of packages not
explicitly listed in the workspace's dependencies or devDependencies. Also
fixes all existing violations by adding missing deps and removes unused
deps (@reduxjs/toolkit, @rschedule/json-tools) from loot-core.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Fix builtin subpath detection and improve cache in no-extraneous-dependencies

Fix false positives for Node.js builtin subpaths (fs/promises, path/posix)
by checking the package name portion against builtins. Also cache all
visited directories during walk-up, not just the starting directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Release notes

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 16:11:26 +00:00
Juulz
d2475ebb02 [Docs] Update tags doc for clarity - fixes #7475 (#7485)
* Update tags.md

* [autofix.ci] apply automated fixes

* Update tags.md

* Update tags.md

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-12 13:46:47 +00:00
Michael Clark
78dad7c91b Docs - fonts (#7477)
* making the docs fonts match the app fonts

* quote the blink mac sys font
2026-04-12 08:43:14 +00:00
Matiss Janis Aboltins
410658009e [AI] Fix sync-server build not resolving subpath imports (#7478)
* [AI] Fix sync-server build not resolving subpath imports

The add-import-extensions build script only handled relative imports
(./  ../), leaving #-prefixed subpath imports unresolved in the build
output. At runtime Node.js resolved them via package.json's imports
map back to source files, which have extensionless imports that fail
in ESM.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Add release notes for sync-server subpath imports fix

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Simplify subpath import resolution using publishConfig.imports

Use publishConfig.imports which already has ./build/src/ paths with .js
extensions, eliminating manual src->build and .ts->.js conversions.
Also sort wildcard patterns by specificity and extract shared helper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Use replaceAll for wildcard substitution to satisfy CodeQL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 05:36:55 +00:00
Matiss Janis Aboltins
a69e33bd7b [AI] Upgrade oxlint, oxfmt, and oxlint-tsgolint to latest versions (#7463)
- oxlint ^1.51.0 → ^1.59.0
- oxfmt ^0.32.0 → ^0.44.0
- oxlint-tsgolint ^0.13.0 → ^0.20.0
- Remove rules dropped by oxlint: import/no-unresolved, import/no-unused-modules,
  import/no-useless-path-segments, react/no-unstable-nested-components
- Rename eslint/no-use-isnan → eslint/use-isnan
- Remove deprecated moduleResolution: "node10" from crdt and desktop-electron tsconfigs

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 05:36:29 +00:00
Juulz
0025010382 [Docs] Update text and image in restore doc to Switch file from Close file. (#7473)
* Update restore.md

* Add files via upload
2026-04-11 22:57:59 +00:00
Matiss Janis Aboltins
d6253c86eb [AI] Add project-level hook to enforce PR creation rules (#7471)
* [AI] Add project-level hook to enforce PR creation rules

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Fix hook JSON output format to use hookSpecificOutput

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Rename 7470.md to 7471.md

* Update upcoming-release-notes/7471.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-11 15:37:26 +00:00
Matiss Janis Aboltins
5220dff75b Prevent path traversal in file upload by using basename (#7428)
* [AI] Fix path traversal vulnerability in uploadFileWeb

Sanitize user-supplied filename with fs.basename() to strip directory
components (e.g. ../../) before writing to /uploads/, preventing
arbitrary file writes outside the intended directory.

https://claude.ai/code/session_01UgQANWBxqkqVT7xGyWNAXB

* [AI] Harden path traversal fix and correct broken basename in web fs

The browser fs.basename implementation was returning the directory part
instead of the filename, making the previous fs.basename() fix
ineffective on the web platform. Replace with inline sanitization that
works regardless of platform: split on path separators, strip null
bytes, reject . and .., and use fs.join for safe path construction.

Also fix the browser fs.basename to actually return the last path
segment, matching the behavior of Node's path.basename.

https://claude.ai/code/session_01UgQANWBxqkqVT7xGyWNAXB

* [AI] Revert browser fs.basename change per user request

The browser fs.basename implementation is restored to its original
behavior. The path traversal fix in uploadFileWeb does not depend on
fs.basename.

https://claude.ai/code/session_01UgQANWBxqkqVT7xGyWNAXB

* [AI] Add release notes for path traversal fix (#7428)

https://claude.ai/code/session_01UgQANWBxqkqVT7xGyWNAXB

* [AI] Suppress no-control-regex lint for null byte sanitization

The \0 regex is intentional to strip null bytes from filenames as part
of the path traversal fix.

https://claude.ai/code/session_01UgQANWBxqkqVT7xGyWNAXB

* [AI] Use replaceAll for null-byte stripping instead of regex

Replace /\0/g regex with replaceAll('\0', '') to avoid triggering
the no-control-regex ESLint rule, removing the need for the
eslint-disable comment.

https://claude.ai/code/session_01UgQANWBxqkqVT7xGyWNAXB

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-11 11:28:13 +00:00
Matiss Janis Aboltins
65b154151f [AI] Enable subpath imports across all packages (#7462)
* [AI] Enable subpath imports across all packages

Generalize the prefer-subpath-imports ESLint rule to work with any
package (not just loot-core) and enable it globally. Add subpath import
mappings to cli, component-library, and sync-server package.json files.
Auto-fix all backtracked relative imports to use #-prefixed subpath
imports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Add release notes for #7462

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Fix mock specifiers in accounts.test.ts to use aliased imports

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* [AI] Fix mock specifiers in query.test.ts to use aliased imports

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* [AI] Fix ESLint rule to properly validate src/ directory paths

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* [AI] Add publishConfig.imports for sync-server to remap aliases to build directory

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-04-11 08:56:49 +00:00
Michael Clark
4aa6ac76b4 Update docs logo and cleanup some unused files (#7444)
* update docs logo and cleanup some unused files

* remove icon.png

* keep old logo around in archived
2026-04-11 08:03:37 +00:00
Kennedy242
a87676bd84 Fix bug where total selected balance is not shown when it is zero (#7460)
* Write a test to catch this case

* Handle falsey values better in 'Selected balance' logic
2026-04-10 22:49:54 +00:00
Matiss Janis Aboltins
e322b0319c Update pre-commit hook configuration (#7461)
* [AI] Set .husky/pre-commit hook as executable

The pre-commit hook was being ignored by git because it lacked the
executable permission bit, producing a warning on every commit attempt.

https://claude.ai/code/session_016jLmTo6L5PxMKK8wJMptCP

* Add release notes for PR #7461

* Update 7461.md

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-10 22:46:51 +00:00
Matiss Janis Aboltins
4960363de6 [AI] Stop using .browser extension; removing "resolve.extensions" - prefer conditions via package.json (#7254)
* [AI] Consolidate loot-core connection: default web path, electron split, drop .browser

* [autofix.ci] apply automated fixes

* [AI] Replace browser-preload .browser extension with package.json subpath imports

Use the imports field in desktop-client/package.json with conditional
resolution (electron → empty stub, default → real implementation) to
eliminate the last .browser file extension from the codebase.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Refactor connection imports to use @actual-app/core

* Implement connection mock for desktop-client tests and update import path

* [AI] Fix formatting and update imports after master merge

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Fix connection mock in TransactionsTable tests and use electron-renderer condition

Wire up the manual connection mock for TransactionsTable tests since the
__mocks__ directory was removed, and restore electron-renderer condition
in loot-core package.json exports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [autofix.ci] apply automated fixes

* [AI] Remove redundant resolveExtensions from vite configs

These arrays were identical to Vite's built-in default and served no purpose.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Remove remaining resolveExtensions from vite/vitest configs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Fix build failures: update browser-preload import path and condition

- Change loot-core/shared/platform to @actual-app/core/shared/platform
- Use electron-renderer condition for #browser-preload to match vite resolve

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Remove redundant resolveExtensions from api and loot-core desktop configs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove '*.browser.ts' extension and alias resolutions

Removed the special '*.browser.ts' file extension and file resolutions via alias, preferring conditions.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:32:51 +00:00
Matiss Janis Aboltins
d76d7d3204 Security hardening: validate release notes and workflow inputs (#7448)
* [AI] Harden GitHub Actions workflows against low-severity security issues

- generate-release-pr.yml: replace `eval` with an associative array for
  per-package version tracking. The version input was already moved to an
  env var in #7433, so this removes the remaining defense-in-depth concern
  of `eval`ing subshell output.
- create-release-notes-file.js: validate the OpenAI-returned category
  against the known allow-list (Features, Bugfixes, Enhancements,
  Maintenance), validate the author against the GitHub username regex,
  and collapse the summary to a single line before embedding it in the
  markdown body. Prevents indirect prompt-injection via CodeRabbit
  comments from producing malformed YAML frontmatter.
- generate-summary.js: stop logging the full CodeRabbit comment body to
  CI logs.
- netlify-release.yml, i18n-string-extract-master.yml: pass secrets via
  `env:` blocks rather than as CLI arguments, so they do not appear in
  argv / process listings.

https://claude.ai/code/session_012pZSkUBbabmmuaxbwysW33

* Add release notes for PR #7448

* [AI] Address review feedback on security hardening

- create-release-notes-file.js: stop logging the full fileContent body.
  Only log the target filename plus the (already-validated) category and
  author metadata, so the model-generated release-note text doesn't end
  up in CI logs.
- create-release-notes-file.js: validate summaryData.prNumber as a
  positive integer before using it in the file path or commit message,
  and switch both usages to the validated numeric value.
- i18n-string-extract-master.yml: write the Weblate API key into
  ~/.config/weblate under a [keys] section in a new "Configure Weblate
  API credentials" step, then drop the per-step env blocks and the
  --key CLI flag from every wlc invocation so the secret is no longer
  visible in process listings at all.

https://claude.ai/code/session_012pZSkUBbabmmuaxbwysW33

* [AI] Remove debug console.log statements for category in release notes script

Remove the four "Debug - ..." console.log calls that printed the raw
category env var (value/type/JSON-stringified form) plus the cleanCategory
value. They were clutter in CI logs; the existing info-level
"Creating release notes file: ... (category: ..., author: ...)" log
already surfaces the sanitized category.

https://claude.ai/code/session_012pZSkUBbabmmuaxbwysW33

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-10 17:11:52 +00:00
Matiss Janis Aboltins
d8317c44b7 [AI] Migrate desktop-client to subpath imports (#7446)
* [AI] Migrate desktop-client to subpath imports

Replace the `@desktop-client/*` path alias with Node.js subpath
imports (`#*`) across packages/desktop-client:

- Declare the full `imports` map in packages/desktop-client/package.json
  (bare index entries, root-level files, and per-subdirectory wildcards
  with explicit extension overrides where `.ts` and `.tsx` mix).
- Update all source files to import from `#...` specifiers.
- Drop the `@desktop-client` group from .oxfmtrc.json.
- Enable `actual/prefer-subpath-imports` for desktop-client in
  .oxlintrc.json so future code keeps using the subpath form.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Drop legacy desktop-client aliases

Remove the `@desktop-client/*` and `loot-core/*` path aliases from
vite.config.ts and tsconfig.json now that every desktop-client source
file imports via subpath imports / `@actual-app/core`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Release notes

* [AI] Use electron-renderer condition for renderer-only exports

Desktop-client's Vite build used the `electron` resolve condition, which
overlapped with loot-core exports where `electron` means the Node/main
variant (e.g. `shared/platform.electron.ts` using `os`,
`platform/server/asyncStorage/index.electron.ts` using `fs`). Once the
`loot-core` Vite alias was removed, the renderer bundle started pulling
those Node variants and crashed at runtime with
`It.default.platform is not a function` inside `platform.electron.ts`.

Introduce a distinct `electron-renderer` condition used only by
desktop-client's Vite config, and rename the `electron` key to
`electron-renderer` on the sole loot-core export whose `electron` branch
is the Electron renderer variant (`#/./platform/client/connection`, the
IPC `global.Actual.ipcConnect` file). Every other `electron`-conditioned
export keeps its Node semantics and is still matched by loot-core's own
`vite.desktop.config.ts` (`conditions: ['electron']`).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Drop .electron.* extensions from loot-core desktop resolver

Now that every Node/main variant is selected via the `electron` subpath
import condition in `packages/loot-core/package.json`, Vite's
`resolveExtensions` list no longer needs the `.electron.js`,
`.electron.ts`, `.electron.tsx` entries. Remove them to keep resolution
explicit and avoid implicit extension picking.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Align desktop-client TS resolution with Vite

- Set `customConditions: ["electron-renderer"]` in
  `packages/desktop-client/tsconfig.json` so TypeScript resolves
  conditional imports (notably `@actual-app/core/platform/client/connection`)
  to the same file Vite picks at runtime. Today the surfaces happen to
  match because both variants import from a shared `index-types.ts`,
  but the alignment prevents a latent drift bug.
- Fix typo in the release note (`Standartise` -> `Standardise`).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:03:45 +00:00
Michael Clark
c2c6d49afa Upgrade docusaurus (#7445)
* upgrade docusaurus

* release notes
2026-04-10 08:06:31 +00:00
Emil Tveden Bjerglund
6a96231c1a Add more filtering options for transactions in the Sankey report (#7442)
* Allow more filters in Spent mode

* Add release note

* Add translation do displayed fields. Fix typo.
2026-04-09 18:59:17 +00:00
Juulz
085355b467 🎨 Add You Need A (YNA) Theme Light (#7441)
* Update customThemeCatalog.json

Add YNA Theme Light

* [autofix.ci] apply automated fixes

* 🎨 You Need A Theme Light

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-09 18:55:21 +00:00
Juulz
23313b3ac5 Change formatting of reconcile form for clarity and ease of use (#7423)
* Update Reconcile.tsx

* Update Reconcile.tsx

* Update Reconcile.tsx

* Update Reconcile.tsx

* Update Reconcile.tsx

* Update Reconcile.tsx

* Update Reconcile.tsx

* [autofix.ci] apply automated fixes

* Update Reconcile.tsx

* Update Reconcile.tsx

* [autofix.ci] apply automated fixes

* Change formatting of reconcile form for clarity and ease of use.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-09 14:09:58 +00:00
Matiss Janis Aboltins
f364d5a9d8 [AI] Add subpath import wildcard patterns to loot-core (#7429)
* [AI] Add subpath import wildcard patterns and explicit directory entries to loot-core

Extend the package.json imports field with prefix-specific wildcard patterns
(#server/*, #shared/*, #types/*, #mocks/*, #platform/*) and explicit entries
for directory imports (#server/db, #server/sync, etc.) to support the ongoing
migration from relative imports to subpath imports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Add release notes for #7429

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-04-09 08:02:25 +00:00
Tamás Szelei
e6bd684812 [AI] Update Age of Money Report discussion link to issue #7006 (#7421) 2026-04-08 23:05:56 +00:00
Matiss Janis Aboltins
4d0f0f740d Update browserlist (caniuse-lite) (#7431)
* [AI] Update browserslist caniuse-lite database

Update caniuse-lite from 1.0.30001751 to 1.0.30001787 and
baseline-browser-mapping from 2.9.14 to 2.10.16.

https://claude.ai/code/session_01Q4x7bPh2tkKDKcbM4kfHNo

* [AI] Add release notes for browserslist update

https://claude.ai/code/session_01Q4x7bPh2tkKDKcbM4kfHNo

* Update 7431.md

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-08 22:35:21 +00:00
Matiss Janis Aboltins
20ba076a51 Add rate limiting to authentication endpoints (#7432)
* [AI] Add rate limiting to authentication endpoints

Add strict rate limiting (5 attempts per 15 minutes) to /account/login,
/account/bootstrap, and /account/change-password endpoints to prevent
brute-force password attacks. Uses express-rate-limit as route-level
middleware on auth-sensitive routes only.

https://claude.ai/code/session_017SHnNCn93RzxpvEEPJAZUZ

* [AI] Add release notes and remove rate limit from /change-password

Add upcoming release notes file for the auth rate limiting feature.
Remove rate limiting from /change-password since it already requires
a valid admin session token.

https://claude.ai/code/session_017SHnNCn93RzxpvEEPJAZUZ

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-08 22:21:04 +00:00
Matiss Janis Aboltins
4efa8bba04 Fix script injection patterns in GitHub Actions workflows (#7433)
* [AI] Fix script injection in vrt-update-apply.yml workflow

Use environment variables instead of direct expression interpolation
in the github-script step to prevent potential script injection via
artifact-sourced values (steps.apply.outputs.error and
steps.metadata.outputs.pr_number).

https://claude.ai/code/session_01V28NTQAXTvSfwyoDhWpWo9

* [AI] Fix script injection in generate-release-pr.yml workflow

Use environment variable instead of direct expression interpolation
for github.event.inputs.version in the shell script context to
prevent potential command injection.

https://claude.ai/code/session_01V28NTQAXTvSfwyoDhWpWo9

* [AI] Add release notes for #7433

https://claude.ai/code/session_01V28NTQAXTvSfwyoDhWpWo9

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-08 22:19:45 +00:00
Matiss Janis Aboltins
1f3b4e613d Pin check-spelling action versions to specific commits (#7430)
* [AI] Pin check-spelling actions to commit SHAs in docs-spelling.yml

Pin check-spelling/check-spelling@main and check-spelling/spell-check-this@prerelease
to specific commit SHAs to prevent supply chain attacks. The update job is especially
sensitive as it has contents:write, pull-requests:write permissions and access to the
CHECK_SPELLING SSH deploy key.

https://claude.ai/code/session_01FK9KT4VRxvm24bb18Q9rvM

* [AI] Fix spell-check-this prerelease pin to correct commit SHA

https://claude.ai/code/session_01FK9KT4VRxvm24bb18Q9rvM

* [AI] Add release notes for pinning check-spelling action versions

https://claude.ai/code/session_01FK9KT4VRxvm24bb18Q9rvM

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-08 21:38:07 +00:00
youngcw
926f7193f9 fix formula date variable in rules (#7373)
* format

* note

* update test
2026-04-08 15:47:19 +00:00
Eduardo Pio
092b85e075 Fix/5840 escape search wildcards (#7270)
* Escape LIKE wildcards; support ? in unicodeLike

* add release notes

* [autofix.ci] apply automated fixes

* Revert "[autofix.ci] apply automated fixes"

This reverts commit d8a2843a5b.

* [autofix.ci] apply automated fixes

* fix: address bot review feedback for escapes

* Support escaped backslash

* Add tests for escaped characters in unicodeLike

* [autofix.ci] apply automated fixes

* Remove underscore from escaped search regex

* Refactor regex escaping to use REGEX_SPECIAL constant

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-08 15:37:41 +00:00
gust0717
2bbcbaee73 Fixes query for tag starting in $ (#7324)
* Fixes query for tag starting in $

* Update packages/loot-core/src/server/transactions/transaction-rules.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply coderabbitai suggestion

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-08 14:45:00 +00:00
tabedzki
446fde6cd9 Add Category Group filtering in Budget Analysis Report (#7116)
* feat: add FilterInclude to match FilterExclude

* feat: enhance category filtering to support category groups in budget analysis

* add release notes

* fix: applied corabbit's suggestion

* applied coderabbit's fixes

* removed duped code

* applied more coderabbit comments

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* fix: include docs about include parameter

* fix: contains and matches now properly filter category names instead of UUID

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-04-08 14:34:26 +00:00
James Skinner
7ce44c2e56 Migrate add-attribute plugin to TypeScript (#7417)
* Migrate add-attribute plugin to TypeScript

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-08 14:02:45 +00:00
Matiss Janis Aboltins
e0772e24cd [AI] Add ErrorBoundary around dashboard widgets (#7382)
* [AI] Add ErrorBoundary around dashboard widgets (#7273)

Wraps each dashboard widget in an ErrorBoundary so a faulty widget
degrades to an error card instead of crashing the entire Reports page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add release notes for PR #7382

* [autofix.ci] apply automated fixes

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-08 06:41:31 +00:00
Emil Tveden Bjerglund
3d5881ea57 Fix Net Worth graph not showing correct number of intervals (#7296)
* Fix Net Worth graph showing N-1 intervals, resulting in inconsistent totalChange value.

* Fix starting date being wrong for 'daily' and 'weekly'

* Linting

* Add release note

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #7296

* Remove manipulation of startDate for 'yearly'

The result was not consistent with the reports previous behavior when yearly was selected (it went back too far).

* Remove empty datapoint at beginning when start equals earliest transaction

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-08 02:40:28 +00:00
Matiss Janis Aboltins
2295e6d464 [AI] Add electron conditions to loot-core platform/server exports (#7383)
* trim down some unused/unnecessary dependencies (#7350)

* fix github actions inconsistencies

* fix pinning of transitive deps in eslint-plugin

* drop use of node-fetch in api

* drop md5 dependency in favour of node:crypto

* drop slash

* drop unused top level packages

* add note about node-polyfills warning

* remove unused deps from desktop-client

* drop pegjs types

* note

* drop node-jq

* [Doc] More tour image (mostly) updates & a hotkey fix (#7328)

* Fix keyboard shortcut Mac key for undo operations

Updated keyboard shortcut instructions for Mac & make consistent.

* Add files via upload

* Fix undo shortcut from 'K' to 'Z'

Updated keyboard shortcut for undo operation in payees guide. COFFEE!

* Revise budget section for clarity and consistency

Updated category descriptions and improved Markdown support details.

* Add files via upload

* Fix grammatical error in budget.md

* Fix typo and clarify Markdown description in budget.md

Corrected a typo in the documentation regarding the chevrons and clarified the description of rendered Markdown.

* Fix spelling error in budget documentation

Corrected the spelling of 'cheverons' to 'chevrons'.

* Add files via upload

* Remove redundant text in budget.md

* Fix formatting issues in payees.md

* count points script should fetch the release note from the PR directly (#7309)

* get pr release note from PR, not top of master

* note

* [AI] Mobile: Post transaction today on global account lists (#7311) (#7322)

* [AI] Mobile: pass today for Post transaction today on global account lists (#7311)

All Accounts, On budget, and Off budget transaction lists now forward the
today flag to schedule/post-transaction, matching single-account mobile
and desktop behavior.

Made-with: Cursor

* [AI] Add release note for PR 7322 (#7311)

Made-with: Cursor

* [AI] Tighten release note wording for PR 7322 (imperative)

Made-with: Cursor

---------

Co-authored-by: Pranay Mac M1 <pranayseela@yahoo.com>

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
Co-authored-by: Pranay S <pranayritvik@gmail.com>
Co-authored-by: Pranay Mac M1 <pranayseela@yahoo.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>

* Implement Sankey report for spent and budgeted money (#7220)

* Implement Sankey graph report

* Add release notes

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6068

* Remove local debug settings

* [autofix.ci] apply automated fixes

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6068

* Improve graphs from comments

* Fix lints

* coderabit fixes

* Fix filtering and UI enhancements

* remove pngs

* Fix typecheck

* Another type issue

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6068

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6068

* Fix strict typing issues

* Update report page

Now better conforms with components from other reports, e.g. by reusing Header
Makes it possible to display a period longer than one month.

* Change view description order

* Formatting and cleanup

* Removed difference section, as it will be difficult to get a reliable view across months

* Introduce the Timeframe param, similar to Spending report, to allow saving a Live sliding window.

* Allow filtering just the last month

* Fix linting errors

* Remove all information about income

* Remove debugging statement

* Sort categories and subcategories by amount

* Move compact mode to spreadsheet to fix Card view more easily

* Update tests file

* Add release notes

* Rename release notes to match PR#

* Fix autofix.ci issues

* Update packages/desktop-client/e2e/sankey.test.ts

Enable experimental feature fall all tests, pr. coderabbit recommendation

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Add sankey-card to isWidgetType

* Gate Sankey routes to prevent direct URL bypass

* Fix typo

* Change node transformation to work by key instead of name, to remove risk of duplicate issues

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Prevent false-positive pass in month-change test.

* Translate mode to a proper label

* Fix message for empty data

* Enabled LoadingIndicator until data is ready

* Change card default mode

* More robust filtering

* Fixed issue with budgeted spreadsheet not using 'end' date

* Allow copying SankeyCard to dashboard

* Fix typing and linting issues

* Remove e2e tests

I cannot currently get them to pass, because I dont fully understand playwright and how they are supposed to work. I can see that they don't exist for other reports. We can add them later if required.

* Remove unecessary sankey reference

* Refactor spreadsheet

* Remove dead code from SankeyGraph

* Collect to Other if too many subcategories

* Edit wrong comment

* Linting and typechecking

* Show remaining amount to budget

* Hide description on narrow device

* Add visual clue if 'To budget' is larger than 'Budgeted' and would extend below the edge of the graph

* Add colors to the links

* Fix report card showing subcategories instead of main categories

* Add tooltip info to Other on SankeyCard

* Create globalOther flag and implement greedy category reduction algorithm

* Allow user to select between Global or Per category Other

* Allow user to choose number of subcategories to show

* Allow user to select how subcategories are sorted

* Fix budget filtering

* [autofix.ci] apply automated fixes

* Condense sorting and Other-grouping to one option

* Implement Sort as budget option

* Dynamically adjust topN based on SankeyCard height

* Remove old feature flags from previous PR

---------

Co-authored-by: andrewhumble <43395285+andrewhumble@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Fix yarn generate:icons command (#7281)

* fix icon templates with `module.exports` to `export default`

* Add `@svgr/babel-plugin-add-jsx-attribute` to dependencies

* Run `yarn generate:icons`, and set prettier singleQuote to reduce changes

* Add release note

* Add temporary fix for `SvgChartArea`

* Add `ChartArea` svg from the existing tsx

* CI rerun

* Standardise ledger scrolling when using keyboard shortcuts (#7283)

* Standardise table keyboard navigation by preventing browser scroll with arrow keys

* Add release note

* Apply the preventDefault() in specific cases so that it is not applied to default

---------

Co-authored-by: youngcw <calebyoung94@gmail.com>

* Fix updateTransaction corrupting split parents with partial updates (#7242)

* [AI] Fix updateTransaction corrupting split parents with partial updates

When `api.updateTransaction(id, { notes: '...' })` is called on a split
parent, the `updateTransaction` helper replaces the parent with the
sparse update object (`{ id, notes }`) instead of merging it with
the existing transaction data.  This causes `recalculateSplit` to see
`amount` as `undefined` (→ 0), which doesn't match the children's
total and sets a `SplitTransactionError` on the parent.  `makeChild`
also inherits undefined `account`, `date`, and `cleared` values,
potentially creating broken child rows.

Fix: merge the incoming partial fields (`{ ...trans, ...transaction }`)
so all existing properties are preserved.

Add a test that performs a notes-only update on a split parent and
asserts no error is set and the amount stays intact.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* [AI] Add release notes for PR #7242

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address review feedback: remove verbose comment and simplify release note

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: L. Warren Thompson <lwarrenthompson@Warren-MBP.local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* [AI] Add electron conditions to loot-core platform/server exports and fix imports

- Add "electron" condition to platform/server exports (asyncStorage,
  connection, fetch, fs, sqlite) so they resolve to .electron.ts files
  when using the electron export condition
- Remove broken ./client/platform export referencing non-existent files
- Convert deep relative imports in electron files to subpath imports
  (#types/prefs, #server/errors, #server/mutators)

https://claude.ai/code/session_01FPpKnozt42Mf79YHAT6ytM

* [AI] Convert remaining relative imports to subpath imports in electron files

- Convert ../fs, ../log, ../../exceptions to subpath imports
  (#platform/server/fs, #platform/server/log, #platform/exceptions)
- Add electron-conditional entries to the imports field in package.json
  for all 5 platform/server modules with electron variants
- Add resolve.conditions: ['electron'] to vite.desktop.config.ts so the
  electron condition is recognized during desktop builds

https://claude.ai/code/session_01FPpKnozt42Mf79YHAT6ytM

* Add release notes for PR #7383

* [AI] Fix API build and test failures from conditional exports

- Add "api" condition to all 5 platform/server exports and imports
  entries so the API build resolves to .api.ts variants correctly
- Add resolve.conditions and ssr.resolve.conditions: ['api'] to
  packages/api/vite.config.ts
- Add explicit #platform/server/log and #platform/exceptions entries
  to the imports field (array fallback in #* wildcard doesn't work for
  directory modules)
- Revert #platform/server/fs back to relative ../fs import in
  asyncStorage/index.electron.ts — subpath imports for platform modules
  with electron variants don't work in vitest because the test runner
  doesn't propagate resolve.conditions to Node's import resolution

https://claude.ai/code/session_01FPpKnozt42Mf79YHAT6ytM

* fix: apply CodeRabbit auto-fixes

Fixed 2 file(s) based on 2 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

* [autofix.ci] apply automated fixes

* Enhance package.json and Vite configurations for Electron support; refactor imports to use new path aliases. Added new paths for server and shared modules, updated SSR settings, and improved test configurations for better module resolution.

* [AI] Merge electron condition with default Vite conditions in vitest config

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* [AI] Move @ts-strict-ignore comment to first line in reset.ts

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
Co-authored-by: Juulz <julesmcn@gmail.com>
Co-authored-by: Pranay S <pranayritvik@gmail.com>
Co-authored-by: Pranay Mac M1 <pranayseela@yahoo.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
Co-authored-by: Emil Tveden Bjerglund <emilbp@gmail.com>
Co-authored-by: andrewhumble <43395285+andrewhumble@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: James Skinner <56730344+JSkinnerUK@users.noreply.github.com>
Co-authored-by: L. Warren Thompson <warren.thompson@zuirail.com>
Co-authored-by: L. Warren Thompson <lwarrenthompson@Warren-MBP.local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-04-07 22:17:43 +00:00
Matt Fiddaman
1e8ad9a89f fix loot-core version bump in generate script (#7414) 2026-04-07 22:17:18 +00:00
Matt Fiddaman
5009f01218 generate docs from release notes directly (#7408)
* move workflows to use the local actions

* generate docs from release notes directly

* note

* fix create PR workflow from SHA
2026-04-07 21:00:32 +00:00
Michael Clark
6decd9d0f6 Move view styles into view component (#7412)
* move view styles into view component

* release notes

* use viewstyles on autocomplete
2026-04-07 20:39:37 +00:00
JkBoyo
5809292579 Capture sync time for initial account linking Fixes #7282 (#7347)
* Capture sync time for initial account linking

* Fix id to point to correct id

* Add release notes

* fix indenting
2026-04-07 20:31:00 +00:00
Matiss Janis Aboltins
edc0242203 [AI] Fix CLI accounts list: compute balances, hide closed, sort by group (#7378)
* [AI] Fix accounts list: compute balances, hide closed, sort by budget group

- Replace empty `balance_current` (bank-sync field) with computed `balance`
  from transaction history via `getAccountBalance`
- Filter out closed accounts by default; add `--include-closed` flag
- Stable-sort on-budget accounts before off-budget
- Add `balance_current` to AMOUNT_FIELDS for table/csv formatting
- Update docs and tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Extract duplicate isRecord type guard to shared utils

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [AI] Add types condition to api package exports for tsc resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update packages/cli/src/commands/query.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* [AI] Add balance_available/balance_limit to AMOUNT_FIELDS, validate query result.data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-04-07 19:28:46 +00:00
Matiss Janis Aboltins
4fe4421ab7 [AI] Fix crash viewing account ledger with expired recurring schedules (#7381)
* [AI] Fix crash viewing account ledger with expired recurring schedules

Guard against null return from getNextDate() when a recurring schedule
has an end date in the past and no future occurrences exist.

Fixes #7285

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add release notes for PR #7381

* [AI] Address code review feedback for PR #7381

Revert schedule-template.ts changes and fix test names/assertions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Simplify bugfix description for account ledger crash

Removed redundant information about null checks in the bugfix description.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-07 18:14:28 +00:00
Matiss Janis Aboltins
78739b926b [AI] Clarify that E2E encryption does not cover bank sync tokens (#7392)
* [AI] Clarify that E2E encryption does not cover bank sync tokens (#5550)

Update docs and in-app text to make clear that end-to-end encryption
only applies to budget data, not bank sync tokens stored on the server.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add release notes for PR #7392

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-04-07 17:16:19 +00:00
Matt Fiddaman
d42f6c7437 improve release notes actions (#7407)
* consolidate find-comment, post-comment into single action

* use release notes in current branch for both generation and deletion

* note
2026-04-07 17:16:14 +00:00
Matt Fiddaman
ba780514f6 prevent messages being dropped after closing last budget (#7411)
* prevent messages being dropped after closing last budget

* note
2026-04-07 17:14:28 +00:00
Trevin Chow
a84fb3dae1 Fix custom report editor retaining unsaved settings (#7356)
* Fix custom report editor retaining unsaved settings

The session storage clear condition only fired when navigating from
the /reports dashboard. Since the URL tracking runs inside the report
component, the stored URL always pointed to the last report path, so
revisiting the same report never triggered the clear.

Changed the condition to clear session storage whenever the stored URL
differs from the current path. This handles navigating from the
dashboard, from another report, or any other page.

Fixes #7332

* Add release notes for PR #7356
2026-04-07 16:37:43 +00:00
Tamás Szelei
e3dd3d1d5a Add Age of Money report (#2994) (#6685) 2026-04-07 15:05:45 +00:00
James Skinner
477b1873e2 Extend normalisation utility for non-latin diacritic character handling (#7284)
* Extend normalisation utility for non-latin diacritic character handling

* Add release note

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-07 14:54:32 +00:00
dependabot[bot]
59192c9b02 Bump lodash from 4.17.23 to 4.18.1 (#7380)
* trim down some unused/unnecessary dependencies (#7350)

* fix github actions inconsistencies

* fix pinning of transitive deps in eslint-plugin

* drop use of node-fetch in api

* drop md5 dependency in favour of node:crypto

* drop slash

* drop unused top level packages

* add note about node-polyfills warning

* remove unused deps from desktop-client

* drop pegjs types

* note

* drop node-jq

* [Doc] More tour image (mostly) updates & a hotkey fix (#7328)

* Fix keyboard shortcut Mac key for undo operations

Updated keyboard shortcut instructions for Mac & make consistent.

* Add files via upload

* Fix undo shortcut from 'K' to 'Z'

Updated keyboard shortcut for undo operation in payees guide. COFFEE!

* Revise budget section for clarity and consistency

Updated category descriptions and improved Markdown support details.

* Add files via upload

* Fix grammatical error in budget.md

* Fix typo and clarify Markdown description in budget.md

Corrected a typo in the documentation regarding the chevrons and clarified the description of rendered Markdown.

* Fix spelling error in budget documentation

Corrected the spelling of 'cheverons' to 'chevrons'.

* Add files via upload

* Remove redundant text in budget.md

* Fix formatting issues in payees.md

* count points script should fetch the release note from the PR directly (#7309)

* get pr release note from PR, not top of master

* note

* [AI] Mobile: Post transaction today on global account lists (#7311) (#7322)

* [AI] Mobile: pass today for Post transaction today on global account lists (#7311)

All Accounts, On budget, and Off budget transaction lists now forward the
today flag to schedule/post-transaction, matching single-account mobile
and desktop behavior.

Made-with: Cursor

* [AI] Add release note for PR 7322 (#7311)

Made-with: Cursor

* [AI] Tighten release note wording for PR 7322 (imperative)

Made-with: Cursor

---------

Co-authored-by: Pranay Mac M1 <pranayseela@yahoo.com>

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
Co-authored-by: Pranay S <pranayritvik@gmail.com>
Co-authored-by: Pranay Mac M1 <pranayseela@yahoo.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>

* Implement Sankey report for spent and budgeted money (#7220)

* Implement Sankey graph report

* Add release notes

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6068

* Remove local debug settings

* [autofix.ci] apply automated fixes

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6068

* Improve graphs from comments

* Fix lints

* coderabit fixes

* Fix filtering and UI enhancements

* remove pngs

* Fix typecheck

* Another type issue

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6068

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6068

* Fix strict typing issues

* Update report page

Now better conforms with components from other reports, e.g. by reusing Header
Makes it possible to display a period longer than one month.

* Change view description order

* Formatting and cleanup

* Removed difference section, as it will be difficult to get a reliable view across months

* Introduce the Timeframe param, similar to Spending report, to allow saving a Live sliding window.

* Allow filtering just the last month

* Fix linting errors

* Remove all information about income

* Remove debugging statement

* Sort categories and subcategories by amount

* Move compact mode to spreadsheet to fix Card view more easily

* Update tests file

* Add release notes

* Rename release notes to match PR#

* Fix autofix.ci issues

* Update packages/desktop-client/e2e/sankey.test.ts

Enable experimental feature fall all tests, pr. coderabbit recommendation

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Add sankey-card to isWidgetType

* Gate Sankey routes to prevent direct URL bypass

* Fix typo

* Change node transformation to work by key instead of name, to remove risk of duplicate issues

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Prevent false-positive pass in month-change test.

* Translate mode to a proper label

* Fix message for empty data

* Enabled LoadingIndicator until data is ready

* Change card default mode

* More robust filtering

* Fixed issue with budgeted spreadsheet not using 'end' date

* Allow copying SankeyCard to dashboard

* Fix typing and linting issues

* Remove e2e tests

I cannot currently get them to pass, because I dont fully understand playwright and how they are supposed to work. I can see that they don't exist for other reports. We can add them later if required.

* Remove unecessary sankey reference

* Refactor spreadsheet

* Remove dead code from SankeyGraph

* Collect to Other if too many subcategories

* Edit wrong comment

* Linting and typechecking

* Show remaining amount to budget

* Hide description on narrow device

* Add visual clue if 'To budget' is larger than 'Budgeted' and would extend below the edge of the graph

* Add colors to the links

* Fix report card showing subcategories instead of main categories

* Add tooltip info to Other on SankeyCard

* Create globalOther flag and implement greedy category reduction algorithm

* Allow user to select between Global or Per category Other

* Allow user to choose number of subcategories to show

* Allow user to select how subcategories are sorted

* Fix budget filtering

* [autofix.ci] apply automated fixes

* Condense sorting and Other-grouping to one option

* Implement Sort as budget option

* Dynamically adjust topN based on SankeyCard height

* Remove old feature flags from previous PR

---------

Co-authored-by: andrewhumble <43395285+andrewhumble@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Fix yarn generate:icons command (#7281)

* fix icon templates with `module.exports` to `export default`

* Add `@svgr/babel-plugin-add-jsx-attribute` to dependencies

* Run `yarn generate:icons`, and set prettier singleQuote to reduce changes

* Add release note

* Add temporary fix for `SvgChartArea`

* Add `ChartArea` svg from the existing tsx

* CI rerun

* Standardise ledger scrolling when using keyboard shortcuts (#7283)

* Standardise table keyboard navigation by preventing browser scroll with arrow keys

* Add release note

* Apply the preventDefault() in specific cases so that it is not applied to default

---------

Co-authored-by: youngcw <calebyoung94@gmail.com>

* Bump lodash from 4.17.23 to 4.18.1

Bumps [lodash](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* note

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
Co-authored-by: Juulz <julesmcn@gmail.com>
Co-authored-by: Pranay S <pranayritvik@gmail.com>
Co-authored-by: Pranay Mac M1 <pranayseela@yahoo.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
Co-authored-by: Emil Tveden Bjerglund <emilbp@gmail.com>
Co-authored-by: andrewhumble <43395285+andrewhumble@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: James Skinner <56730344+JSkinnerUK@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
2026-04-07 10:55:46 +00:00
Matt Fiddaman
8511687da4 replace nordigen-node with our own GoCardless implementation (#7352)
* implement our own GoCardless api class

* switch the service to use the new api

* drop deps

* note

* guard against request forgery

* strip empty params from the request body, add error logging

* coderabbit suggestions

* fix test, make institution nullable
2026-04-07 10:33:04 +00:00
Matt Fiddaman
a394aa1a57 remove useless assignments to local variables (#7354)
* fixes

* note

* enforce with lint

* identical operands

* unused state

* fix unused imports in new sankey report
2026-04-07 10:32:30 +00:00
dependabot[bot]
5eaf0be744 Bump vite from 8.0.0 to 8.0.5 (#7398)
* Bump vite from 8.0.0 to 8.0.5

Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.0 to 8.0.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* note

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-04-07 10:28:57 +00:00
Matt Fiddaman
bb7d7275a6 migrate actualbudget/actions to the main repo (#7406)
* migrate release note actions

* move workflows to use the local actions

* note

* fix failing cleanup in release notes action

* fix codeQL violation
2026-04-07 10:28:30 +00:00
Walter
4fe79e890b Issue 6856 crypt missing due to protocol isn't marked as secure (#7368)
* crypt missing due to protocol isn't marked as secure

* [autofix.ci] apply automated fixes

* release notes

* remove binary files

* Update upcoming-release-notes/7368.md

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>

---------

Co-authored-by: Walter <walter@72:bf:48:9d:a9:b6.home>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-04-07 08:04:43 +00:00
Matiss Janis Aboltins
7af0910d4e [AI] Improve theme catalog responsive layout in ThemeInstaller (#7253)
* [AI] Improve theme catalog responsive layout in ThemeInstaller

* Refactor ColorPalette and ThemeInstaller components for improved layout and responsiveness

* Fix typo in Custom Reports description

* Correct category name from 'Reports' to 'Themes'

* [AI] Fix theme catalog scrollbar overlapping content

Reserve space for the scrollbar by adding right padding to catalog rows
so items don't get clipped when the list overflows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:41:48 +00:00
Matiss Janis Aboltins
093d869bba [AI] Add .claude/worktrees directory to .gitignore (#7344)
* [AI] Add .claude/ directory to .gitignore

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Modify .gitignore for Claude worktrees

Update .gitignore to include Claude worktrees and exclude settings.json

* Update .gitignore to include claude worktree folder

* Add .claude/settings.local.json to .gitignore

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:41:33 +00:00
Matiss Janis Aboltins
fc5f598098 docs: Add guide for self-signed SSL certificates in CLI (#7360)
* [AI] Add self-signed SSL certificate documentation to CLI docs

Add a section explaining how to use NODE_TLS_REJECT_UNAUTHORIZED=0
to allow the CLI to connect to servers with self-signed SSL certificates,
with a security caution about the implications.

Closes #7327

https://claude.ai/code/session_01Mwsuc9By67uzSiMLxvPsMq

* Add release notes for PR #7360

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-07 07:41:22 +00:00
Matiss Janis Aboltins
799db6c496 [AI] Improve post-checkout hook to handle git worktree creation (#7393)
* [AI] Add worktree creation hook to run yarn install

Add a WorktreeCreate hook in .claude/settings.json that runs a setup
script after creating a new git worktree. The script creates a detached
worktree and runs yarn install to ensure dependencies are available.

https://claude.ai/code/session_01MjuJLWcxNU6nbWQDrgGpPr

* [AI] Move worktree setup hook to husky post-checkout for universal support

Instead of a Claude Code-specific hook, detect new worktree creation in
the existing husky post-checkout hook by checking for missing node_modules.
This works with any tool that creates git worktrees (Claude Code, Cursor,
CLI, etc.) since git runs post-checkout after worktree creation.

https://claude.ai/code/session_01MjuJLWcxNU6nbWQDrgGpPr

* Add release notes for PR #7393

* Update 7393.md

* [AI] Fix post-checkout hook to propagate yarn install failures

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-04-07 07:40:47 +00:00
Kennedy242
477fed1607 Add documentation for self-signed cert healthchecks (#7397)
* Add documentation for self-signed cert healthchecks

Adds instruction to set NODE_EXTRA_CA_CERTS in docker-compose healthcheck commands so Node.js can trust the certificate.

* [autofix.ci] apply automated fixes

* fixup! Add documentation for self-signed cert healthchecks

* [autofix.ci] apply automated fixes

* Update Docker health checks with self-signed certs

If using self signed certs, comment the first test line and uncomment
the second test line.

* fixup! Update Docker health checks with self-signed certs

* fixup! Add documentation for self-signed cert healthchecks

* fixup! Update Docker health checks with self-signed certs

* [autofix.ci] apply automated fixes

* fixup! Add documentation for self-signed cert healthchecks

* fixup! Update Docker health checks with self-signed certs

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-07 00:09:35 +00:00
Juulz
64b2d9b31a [Docs] Tour: Update reports, payees, schedules and rules pages. (#7405)
* Improve clarity of reports documentation

Revised descriptions for built-in and custom reports for clarity and conciseness.

* Revise schedules documentation for clarity and conciseness

* Update payees management documentation for clarity

* Update wording and formatting in rules documentation

* Fix formatting issue in schedules.md

* Clarify Payees management overview

Revised the description to clarify the overview of Payees management.

* [autofix.ci] apply automated fixes

* Fix hyphenation in reports documentation

* [autofix.ci] apply automated fixes

* Update packages/docs/docs/tour/reports.md

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>

* Apply suggestions from code review

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>

* Update transaction rule example in documentation

* Apply suggestions from code review

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* [autofix.ci] apply automated fixes

* Add 'PAYPAL' to spelling expectations

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-06 22:30:38 +00:00
Juulz
5511d508ba [Docs] Tour: Move the Tour to website top level and add a sidebar for the tour (#7399)
* Add tourSidebar with various tour documentation

Added a new sidebar for the tour section with multiple entries.

* Add 'Tour Actual' sidebar to Docusaurus config

* Remove 'A Tour of Actual' from docs sidebar

Removed 'A Tour of Actual' category and its items from the sidebar.

* Rename 'Tour Actual' to 'Tour' in sidebar
2026-04-06 22:25:53 +00:00
Victor
59839f83e3 Proportional distribute for split transaction (#7257)
* Add button to proportionally distribute remaining amount of split transaction among child transactions

* Added release note

* Increased min width for split error popover so all buttons are visible

* Updated release note

* Merge proportional distribution into even distribution button

* Added docs on split transactions

* [autofix.ci] apply automated fixes

* Fixed spelling

* [autofix.ci] apply automated fixes

* Change split transaction popover hack to use resize event

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-04-06 21:51:18 +00:00
Matt Fiddaman
3ec6eeabb1 add confirmation dialog when changing half-reconciled transfers (#7269)
* check paired transfer transactions for reconciliation status

* add confirm messages

* note

* bulk edit

* improve types

* add checks to individual transaction edits

* add to mobile

* wabbit
2026-04-06 21:15:23 +00:00
Lucas Alvarez
ceaf13f271 feat: Add CLP currency (#7346) 2026-04-06 21:14:54 +00:00
dependabot[bot]
8e1d4a8b27 Bump electron from 39.2.7 to 39.8.4 (#7367)
* Bump electron from 39.2.7 to 39.8.4

Bumps [electron](https://github.com/electron/electron) from 39.2.7 to 39.8.4.
- [Release notes](https://github.com/electron/electron/releases)
- [Commits](https://github.com/electron/electron/compare/v39.2.7...v39.8.4)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 39.8.4
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* note

* tidy up electron dependencies

* note

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-04-06 21:23:27 +00:00
Matt Fiddaman
3bbcb60fe6 pin minimatch versions to resolve vulnerability reports (#7355)
* pin minimatch versions

* note
2026-04-06 21:14:38 +00:00
Juulz
c2319cdcb5 [Docs] Tour: update tour budgeting page for clarity (#7403)
* Improve clarity and formatting in budget.md

* Fix grammatical error in budget documentation

* Update packages/docs/docs/tour/budget.md

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>

* Update packages/docs/docs/tour/budget.md

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-04-06 21:14:32 +00:00
Juulz
d3895042bb Update account documentation for clarity and accuracy (#7404) 2026-04-06 20:51:54 +00:00
Juulz
533cbed106 [Docs] Tour: Update user interface page for clarity (#7402)
* Refine user interface documentation for clarity

Updated text for clarity and corrected typos in the user interface documentation.

* Clarify server status and sync icon details

Updated descriptions for server status and sync icon interactions.

* Clarify server status and sidebar account display

Updated server status descriptions for clarity and improved wording in the sidebar section.
2026-04-06 20:49:23 +00:00
Juulz
221a57e218 [Docs] Tour: remove old files (#7400)
* Delete packages/docs/docs/tour/settings.md

NO longer in use.

* Delete packages/docs/docs/tour/sidebar.md

No longer in use.
2026-04-06 20:48:53 +00:00
Juulz
23bad279a0 [Docs] Tour: Update tour landing page for clarity and accuracy (#7401)
* Update tour documentation for clarity and accuracy

* Fix formatting and phrasing in tour documentation

* Fix link to documentation in tour index
2026-04-06 20:47:06 +00:00
L. Warren Thompson
d262f7d8b2 Fix updateTransaction corrupting split parents with partial updates (#7242)
* [AI] Fix updateTransaction corrupting split parents with partial updates

When `api.updateTransaction(id, { notes: '...' })` is called on a split
parent, the `updateTransaction` helper replaces the parent with the
sparse update object (`{ id, notes }`) instead of merging it with
the existing transaction data.  This causes `recalculateSplit` to see
`amount` as `undefined` (→ 0), which doesn't match the children's
total and sets a `SplitTransactionError` on the parent.  `makeChild`
also inherits undefined `account`, `date`, and `cleared` values,
potentially creating broken child rows.

Fix: merge the incoming partial fields (`{ ...trans, ...transaction }`)
so all existing properties are preserved.

Add a test that performs a notes-only update on a split parent and
asserts no error is set and the amount stays intact.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* [AI] Add release notes for PR #7242

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address review feedback: remove verbose comment and simplify release note

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: L. Warren Thompson <lwarrenthompson@Warren-MBP.local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 18:12:51 +01:00
James Skinner
c75a94e8b0 Standardise ledger scrolling when using keyboard shortcuts (#7283)
* Standardise table keyboard navigation by preventing browser scroll with arrow keys

* Add release note

* Apply the preventDefault() in specific cases so that it is not applied to default

---------

Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-04-05 18:12:51 +01:00
James Skinner
cb50930d0b Fix yarn generate:icons command (#7281)
* fix icon templates with `module.exports` to `export default`

* Add `@svgr/babel-plugin-add-jsx-attribute` to dependencies

* Run `yarn generate:icons`, and set prettier singleQuote to reduce changes

* Add release note

* Add temporary fix for `SvgChartArea`

* Add `ChartArea` svg from the existing tsx

* CI rerun
2026-04-05 18:12:51 +01:00
Emil Tveden Bjerglund
10b7385ad4 Implement Sankey report for spent and budgeted money (#7220)
* Implement Sankey graph report

* Add release notes

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6068

* Remove local debug settings

* [autofix.ci] apply automated fixes

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6068

* Improve graphs from comments

* Fix lints

* coderabit fixes

* Fix filtering and UI enhancements

* remove pngs

* Fix typecheck

* Another type issue

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6068

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6068

* Fix strict typing issues

* Update report page

Now better conforms with components from other reports, e.g. by reusing Header
Makes it possible to display a period longer than one month.

* Change view description order

* Formatting and cleanup

* Removed difference section, as it will be difficult to get a reliable view across months

* Introduce the Timeframe param, similar to Spending report, to allow saving a Live sliding window.

* Allow filtering just the last month

* Fix linting errors

* Remove all information about income

* Remove debugging statement

* Sort categories and subcategories by amount

* Move compact mode to spreadsheet to fix Card view more easily

* Update tests file

* Add release notes

* Rename release notes to match PR#

* Fix autofix.ci issues

* Update packages/desktop-client/e2e/sankey.test.ts

Enable experimental feature fall all tests, pr. coderabbit recommendation

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Add sankey-card to isWidgetType

* Gate Sankey routes to prevent direct URL bypass

* Fix typo

* Change node transformation to work by key instead of name, to remove risk of duplicate issues

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Prevent false-positive pass in month-change test.

* Translate mode to a proper label

* Fix message for empty data

* Enabled LoadingIndicator until data is ready

* Change card default mode

* More robust filtering

* Fixed issue with budgeted spreadsheet not using 'end' date

* Allow copying SankeyCard to dashboard

* Fix typing and linting issues

* Remove e2e tests

I cannot currently get them to pass, because I dont fully understand playwright and how they are supposed to work. I can see that they don't exist for other reports. We can add them later if required.

* Remove unecessary sankey reference

* Refactor spreadsheet

* Remove dead code from SankeyGraph

* Collect to Other if too many subcategories

* Edit wrong comment

* Linting and typechecking

* Show remaining amount to budget

* Hide description on narrow device

* Add visual clue if 'To budget' is larger than 'Budgeted' and would extend below the edge of the graph

* Add colors to the links

* Fix report card showing subcategories instead of main categories

* Add tooltip info to Other on SankeyCard

* Create globalOther flag and implement greedy category reduction algorithm

* Allow user to select between Global or Per category Other

* Allow user to choose number of subcategories to show

* Allow user to select how subcategories are sorted

* Fix budget filtering

* [autofix.ci] apply automated fixes

* Condense sorting and Other-grouping to one option

* Implement Sort as budget option

* Dynamically adjust topN based on SankeyCard height

* Remove old feature flags from previous PR

---------

Co-authored-by: andrewhumble <43395285+andrewhumble@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-05 18:12:51 +01:00
Juulz
78ce7da1b4 [Doc] More tour image (mostly) updates & a hotkey fix (#7328)
* Fix keyboard shortcut Mac key for undo operations

Updated keyboard shortcut instructions for Mac & make consistent.

* Add files via upload

* Fix undo shortcut from 'K' to 'Z'

Updated keyboard shortcut for undo operation in payees guide. COFFEE!

* Revise budget section for clarity and consistency

Updated category descriptions and improved Markdown support details.

* Add files via upload

* Fix grammatical error in budget.md

* Fix typo and clarify Markdown description in budget.md

Corrected a typo in the documentation regarding the chevrons and clarified the description of rendered Markdown.

* Fix spelling error in budget documentation

Corrected the spelling of 'cheverons' to 'chevrons'.

* Add files via upload

* Remove redundant text in budget.md

* Fix formatting issues in payees.md

* count points script should fetch the release note from the PR directly (#7309)

* get pr release note from PR, not top of master

* note

* [AI] Mobile: Post transaction today on global account lists (#7311) (#7322)

* [AI] Mobile: pass today for Post transaction today on global account lists (#7311)

All Accounts, On budget, and Off budget transaction lists now forward the
today flag to schedule/post-transaction, matching single-account mobile
and desktop behavior.

Made-with: Cursor

* [AI] Add release note for PR 7322 (#7311)

Made-with: Cursor

* [AI] Tighten release note wording for PR 7322 (imperative)

Made-with: Cursor

---------

Co-authored-by: Pranay Mac M1 <pranayseela@yahoo.com>

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
Co-authored-by: Pranay S <pranayritvik@gmail.com>
Co-authored-by: Pranay Mac M1 <pranayseela@yahoo.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-04-05 18:12:51 +01:00
Matt Fiddaman
b03080b246 trim down some unused/unnecessary dependencies (#7350)
* fix github actions inconsistencies

* fix pinning of transitive deps in eslint-plugin

* drop use of node-fetch in api

* drop md5 dependency in favour of node:crypto

* drop slash

* drop unused top level packages

* add note about node-polyfills warning

* remove unused deps from desktop-client

* drop pegjs types

* note

* drop node-jq
2026-04-05 18:12:51 +01:00
1476 changed files with 26165 additions and 15482 deletions

View File

@@ -1,8 +1,6 @@
node_modules
user-files
server-files
# Yarn
**/node_modules
.git
.lage
.pnp.*
.yarn/*
!.yarn/patches

View File

@@ -16,14 +16,19 @@ if (!token || !repo || !issueNumber || !summaryDataJson || !category) {
const [owner, repoName] = repo.split('/');
const octokit = new Octokit({ auth: token });
const VALID_CATEGORIES = [
'Features',
'Bugfixes',
'Enhancements',
'Maintenance',
];
const GITHUB_USERNAME_RE =
/^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/;
async function createReleaseNotesFile() {
try {
const summaryData = JSON.parse(summaryDataJson);
console.log('Debug - Category value:', category);
console.log('Debug - Category type:', typeof category);
console.log('Debug - Category JSON stringified:', JSON.stringify(category));
if (!summaryData) {
console.log('No summary data available, cannot create file');
return;
@@ -34,26 +39,62 @@ async function createReleaseNotesFile() {
return;
}
// Create file content - ensure category is not quoted
// Normalize category - strip surrounding quotes and validate against allow-list
const cleanCategory =
typeof category === 'string'
? category.replace(/^["']|["']$/g, '')
: category;
console.log('Debug - Clean category:', cleanCategory);
if (!VALID_CATEGORIES.includes(cleanCategory)) {
console.log(
`Invalid category "${cleanCategory}". Must be one of: ${VALID_CATEGORIES.join(', ')}`,
);
return;
}
// Validate author is a plausible GitHub username
const author = String(summaryData.author || '');
if (!GITHUB_USERNAME_RE.test(author)) {
console.log(
`Invalid author "${author}", aborting release notes creation`,
);
return;
}
// Normalize summary: collapse whitespace to a single line so it cannot
// introduce extra YAML frontmatter or break the markdown structure.
const cleanSummary = String(summaryData.summary || '')
.replace(/\s+/g, ' ')
.trim();
if (!cleanSummary) {
console.log('Empty summary, aborting release notes creation');
return;
}
// Validate PR number - must be a positive integer. The value comes from
// the GitHub API, but we harden it because it's used to build a file path
// and a commit message.
const validatedPrNumber = Number(summaryData.prNumber);
if (!Number.isInteger(validatedPrNumber) || validatedPrNumber <= 0) {
console.log(
`Invalid PR number "${summaryData.prNumber}", aborting release notes creation`,
);
return;
}
const fileContent = `---
category: ${cleanCategory}
authors: [${summaryData.author}]
authors: [${author}]
---
${summaryData.summary}
${cleanSummary}
`;
const fileName = `upcoming-release-notes/${summaryData.prNumber}.md`;
const fileName = `upcoming-release-notes/${validatedPrNumber}.md`;
console.log(`Creating release notes file: ${fileName}`);
console.log('File content:');
console.log(fileContent);
console.log(
`Creating release notes file: ${fileName} (category: ${cleanCategory}, author: ${author})`,
);
// Get PR info
const { data: pr } = await octokit.rest.pulls.get({
@@ -75,7 +116,7 @@ ${summaryData.summary}
owner: headOwner,
repo: headRepo,
path: fileName,
message: `Add release notes for PR #${summaryData.prNumber}`,
message: `Add release notes for PR #${validatedPrNumber}`,
content: Buffer.from(fileContent).toString('base64'),
branch: prBranch,
committer: {

View File

@@ -25,8 +25,6 @@ try {
process.exit(0);
}
console.log('CodeRabbit comment body:', commentBody);
const data = JSON.stringify({
model: 'gpt-4o-mini',
messages: [

View File

@@ -39,6 +39,22 @@ async function getPRDetails() {
console.log('- Base Branch:', pr.base.ref);
console.log('- Head Branch:', pr.head.ref);
// Fetch all changed files to detect docs-only PRs
const files = await octokit.paginate(octokit.rest.pulls.listFiles, {
owner,
repo: repoName,
pull_number: issueNumber,
per_page: 100,
});
const changedFiles = files.map(f => f.filename);
const isDocsOnly =
changedFiles.length > 0 &&
changedFiles.every(file => file.startsWith('packages/docs/'));
console.log('- Changed Files:', changedFiles.length);
console.log('- Is Docs Only:', isDocsOnly);
const result = {
number: pr.number,
author: pr.user.login,
@@ -47,11 +63,31 @@ async function getPRDetails() {
headBranch: pr.head.ref,
};
let eligible = true;
if (pr.base.ref !== 'master') {
console.log(
'PR does not target master branch, skipping release notes generation',
);
eligible = false;
} else if (pr.head.ref.startsWith('release/')) {
console.log(
'PR head branch is a release branch, skipping release notes generation',
);
eligible = false;
} else if (isDocsOnly) {
console.log(
'PR only changes documentation, skipping release notes generation',
);
eligible = false;
}
setOutput('result', JSON.stringify(result));
setOutput('eligible', JSON.stringify(eligible));
} catch (error) {
console.log('Error getting PR details:', error.message);
console.log('Stack:', error.stack);
setOutput('result', 'null');
setOutput('eligible', 'false');
process.exit(1);
}
}
@@ -60,5 +96,6 @@ getPRDetails().catch(error => {
console.log('Unhandled error:', error.message);
console.log('Stack:', error.stack);
setOutput('result', 'null');
setOutput('eligible', 'false');
process.exit(1);
});

View File

@@ -68,7 +68,6 @@ ignore$
^\Qsrc/\E$
^\Qstatic/\E$
^\Q.github/\E$
(?:^|/)package(?:-lock|)\.json$
(?:^|/)yarn\.lock$
(?:^|/)(?i)docusaurus.config.js
(?:^|/)(?i)README.md

View File

@@ -126,6 +126,7 @@ Moldovan
murmurhash
NETWORKDAYS
nginx
nodenext
OIDC
Okabe
overbudgeted
@@ -133,6 +134,7 @@ overbudgeting
oxc
Paribas
passwordless
PAYPAL
picomatch
pluggyai
Poste

View File

@@ -0,0 +1,17 @@
name: Check release notes
description: Validate that a PR includes a properly formatted release note file
runs:
using: composite
steps:
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
- name: Install dependencies
shell: bash
run: yarn workspaces focus @actual-app/ci-actions
- name: Check release notes
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
shell: bash
run: node packages/ci-actions/bin/release-notes-check.mjs

View File

@@ -0,0 +1,17 @@
name: Generate release notes
description: Generate release documentation from release note files
runs:
using: composite
steps:
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
- name: Install dependencies
shell: bash
run: yarn workspaces focus actual @actual-app/ci-actions
- name: Generate release notes
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
run: node packages/ci-actions/bin/release-notes-generate.mjs

View File

@@ -52,8 +52,9 @@ runs:
with:
repository: actualbudget/translations
path: ${{ inputs.working-directory }}/packages/desktop-client/locale
if: ${{ inputs.download-translations == 'true' }}
persist-credentials: false
if: ${{ inputs.download-translations == 'true' && !env.ACT }}
- name: Remove untranslated languages
run: packages/desktop-client/bin/remove-untranslated-languages
shell: bash
if: ${{ inputs.download-translations == 'true' }}
if: ${{ inputs.download-translations == 'true' && !env.ACT }}

View File

@@ -18,6 +18,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -42,11 +44,7 @@ jobs:
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
- name: Check if release notes file already exists
if: >-
steps.check-first-comment.outputs.result == 'true' &&
steps.pr-details.outputs.result != 'null' &&
fromJSON(steps.pr-details.outputs.result).baseBranch == 'master' &&
!startsWith(fromJSON(steps.pr-details.outputs.result).headBranch, 'release/')
if: steps.pr-details.outputs.eligible == 'true'
id: check-release-notes-exists
run: node .github/actions/ai-generated-release-notes/check-release-notes-exists.js
env:
@@ -56,7 +54,7 @@ jobs:
PR_DETAILS: ${{ steps.pr-details.outputs.result }}
- name: Generate summary with OpenAI
if: steps.check-first-comment.outputs.result == 'true' && steps.check-release-notes-exists.outputs.result == 'false'
if: steps.check-release-notes-exists.outputs.result == 'false'
id: generate-summary
run: node .github/actions/ai-generated-release-notes/generate-summary.js
env:
@@ -65,7 +63,7 @@ jobs:
PR_DETAILS: ${{ steps.pr-details.outputs.result }}
- name: Determine category with OpenAI
if: steps.check-first-comment.outputs.result == 'true' && steps.check-release-notes-exists.outputs.result == 'false' && steps.generate-summary.outputs.result != 'null'
if: steps.generate-summary.outputs.result != 'null' && steps.generate-summary.outputs.result != ''
id: determine-category
run: node .github/actions/ai-generated-release-notes/determine-category.js
env:
@@ -75,7 +73,7 @@ jobs:
SUMMARY_DATA: ${{ steps.generate-summary.outputs.result }}
- name: Create and commit release notes file via GitHub API
if: steps.check-first-comment.outputs.result == 'true' && steps.check-release-notes-exists.outputs.result == 'false' && steps.generate-summary.outputs.result != 'null' && steps.determine-category.outputs.result != 'null' && steps.determine-category.outputs.result != ''
if: steps.determine-category.outputs.result != 'null' && steps.determine-category.outputs.result != ''
run: node .github/actions/ai-generated-release-notes/create-release-notes-file.js
env:
GITHUB_TOKEN: ${{ secrets.ACTIONS_UPDATE_TOKEN }}
@@ -85,7 +83,7 @@ jobs:
CATEGORY: ${{ steps.determine-category.outputs.result }}
- name: Comment on PR
if: steps.check-first-comment.outputs.result == 'true' && steps.check-release-notes-exists.outputs.result == 'false' && steps.generate-summary.outputs.result != 'null' && steps.determine-category.outputs.result != 'null' && steps.determine-category.outputs.result != ''
if: steps.determine-category.outputs.result != 'null' && steps.determine-category.outputs.result != ''
run: node .github/actions/ai-generated-release-notes/comment-on-pr.js
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -16,6 +16,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:

View File

@@ -19,35 +19,54 @@ concurrency:
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
jobs:
setup:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
api:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Build API
run: cd packages/api && yarn build
run: yarn build:api
- name: Create package tgz
run: cd packages/api && yarn pack && mv package.tgz actual-api.tgz
- name: Prepare bundle stats artifact
run: cp packages/api/app/stats.json api-stats.json
- name: Upload Build
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: actual-api
path: packages/api/actual-api.tgz
- name: Upload API bundle stats
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: api-build-stats
path: api-stats.json
crdt:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -56,35 +75,48 @@ jobs:
run: cd packages/crdt && yarn build
- name: Create package tgz
run: cd packages/crdt && yarn pack && mv package.tgz actual-crdt.tgz
- name: Prepare bundle stats artifact
run: cp packages/crdt/dist/stats.json crdt-stats.json
- name: Upload Build
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: actual-crdt
path: packages/crdt/actual-crdt.tgz
- name: Upload CRDT bundle stats
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: crdt-build-stats
path: crdt-stats.json
web:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Web
run: yarn build:browser
- name: Upload Build
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: actual-web
path: packages/desktop-client/build
- name: Upload Build Stats
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build-stats
path: packages/desktop-client/build-stats
cli:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -96,20 +128,23 @@ jobs:
- name: Prepare bundle stats artifact
run: cp packages/cli/dist/stats.json cli-stats.json
- name: Upload Build
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: actual-cli
path: packages/cli/actual-cli.tgz
- name: Upload CLI bundle stats
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cli-build-stats
path: cli-stats.json
server:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -117,7 +152,7 @@ jobs:
- name: Build Server
run: yarn workspace @actual-app/sync-server build
- name: Upload Build
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sync-server
path: packages/sync-server/build

View File

@@ -12,20 +12,40 @@ concurrency:
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
jobs:
setup:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
constraints:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Check dependency version consistency
run: yarn constraints
- name: Check tsconfig project references are in sync
run: yarn check:tsconfig-references
lint:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -33,9 +53,12 @@ jobs:
- name: Lint
run: yarn lint
typecheck:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -43,9 +66,12 @@ jobs:
- name: Typecheck
run: yarn typecheck
validate-cli:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -55,21 +81,36 @@ jobs:
- name: Check that the built CLI works
run: node packages/sync-server/build/bin/actual-server.js --version
test:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Test
run: yarn test
check-gh-actions:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
migrations:
needs: setup
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:

View File

@@ -23,13 +23,15 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
languages: javascript
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
category: '/language:javascript'

View File

@@ -17,6 +17,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:

View File

@@ -1,6 +1,9 @@
name: Generate release PR
name: Cut release branch
on:
schedule:
# 17:00 UTC on the 25th of each month
- cron: '0 17 25 * *'
workflow_dispatch:
inputs:
ref:
@@ -11,22 +14,35 @@ on:
description: 'Version number for the release (optional)'
required: false
default: ''
release-date:
description: 'Expected release date, YYYY-MM-DD (optional)'
required: false
default: ''
permissions:
contents: write
pull-requests: write
jobs:
generate-release-pr:
cut-release-branch:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.inputs.ref }}
ref: ${{ github.event.inputs.ref || 'master' }}
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Bump package versions
id: bump_package_versions
shell: bash
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
declare -A packages=(
[web]="desktop-client"
@@ -34,16 +50,17 @@ jobs:
[sync]="sync-server"
[api]="api"
[cli]="cli"
[core]="core"
[core]="loot-core"
)
declare -A new_versions
for key in "${!packages[@]}"; do
pkg="${packages[$key]}"
if [[ -n "${{ github.event.inputs.version }}" ]]; then
if [[ -n "$INPUT_VERSION" ]]; then
version=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts \
--package-json "./packages/$pkg/package.json" \
--version "${{ github.event.inputs.version }}" \
--version "$INPUT_VERSION" \
--update)
else
version=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts \
@@ -52,15 +69,33 @@ jobs:
--update)
fi
eval "NEW_${key^^}_VERSION=\"$version\""
new_versions[$key]="$version"
done
echo "version=$NEW_WEB_VERSION" >> "$GITHUB_OUTPUT"
- name: Create PR
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
echo "version=${new_versions[web]}" >> "$GITHUB_OUTPUT"
- name: Compute release date
id: release_date
shell: bash
env:
INPUT_DATE: ${{ github.event.inputs['release-date'] }}
run: |
if [[ -n "$INPUT_DATE" ]]; then
echo "date=$INPUT_DATE" >> "$GITHUB_OUTPUT"
else
# default to the 1st of next month
echo "date=$(date -d '+1 month' '+%Y-%m-01')" >> "$GITHUB_OUTPUT"
fi
- name: Create release branch and PR
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.ACTIONS_UPDATE_TOKEN }}
commit-message: '🔖 (${{ steps.bump_package_versions.outputs.version }})'
title: '🔖 (${{ steps.bump_package_versions.outputs.version }})'
body: 'Generated by [generate-release-pr.yml](../tree/master/.github/workflows/generate-release-pr.yml)'
branch: 'release/v${{ steps.bump_package_versions.outputs.version }}'
body: |
Generated by [cut-release-branch.yml](../tree/master/.github/workflows/cut-release-branch.yml)
<!-- release-date:${{ steps.release_date.outputs.date }} -->
branch: 'release/${{ steps.bump_package_versions.outputs.version }}'
base: master

View File

@@ -37,6 +37,8 @@ jobs:
os: [ubuntu, alpine]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
@@ -54,14 +56,14 @@ jobs:
tags: ${{ env.TAGS }}
- name: Login to Docker Hub
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
if: github.event_name != 'pull_request' && !github.event.repository.fork
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
@@ -76,7 +78,7 @@ jobs:
run: yarn build:server
- name: Build image for testing
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
push: false
@@ -93,7 +95,7 @@ jobs:
# This will use the cache from the earlier build step and not rebuild the image
# https://docs.docker.com/build/ci/github-actions/test-before-push/
- name: Build and push images
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}

View File

@@ -29,6 +29,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
@@ -58,13 +60,13 @@ jobs:
tags: ${{ env.TAGS }}
- name: Login to Docker Hub
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -78,7 +80,7 @@ jobs:
run: yarn build:server
- name: Build and push ubuntu image
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
push: true
@@ -87,7 +89,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
- name: Build and push alpine image
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
push: true

View File

@@ -79,12 +79,12 @@ jobs:
steps:
- name: check-spelling
id: spelling
uses: check-spelling/check-spelling@main
uses: check-spelling/check-spelling@cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c # main
with:
suppress_push_for_open_pull_request: 1
checkout: true
check_file_names: 1
spell_check_this: check-spelling/spell-check-this@prerelease
spell_check_this: check-spelling/spell-check-this@92453f59cac8985482dfc301a8ece4c6d7de8a0c # prerelease
post_comment: 0
use_magic_file: 1
experimental_apply_changes_via_bot: 1
@@ -114,10 +114,10 @@ jobs:
if: (success() || failure()) && needs.spelling.outputs.followup && github.event_name == 'push'
steps:
- name: comment
uses: check-spelling/check-spelling@main
uses: check-spelling/check-spelling@cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c # main
with:
checkout: true
spell_check_this: check-spelling/spell-check-this@prerelease
spell_check_this: check-spelling/spell-check-this@92453f59cac8985482dfc301a8ece4c6d7de8a0c # prerelease
task: ${{ needs.spelling.outputs.followup }}
config: .github/actions/docs-spelling
@@ -131,10 +131,10 @@ jobs:
if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request')
steps:
- name: comment
uses: check-spelling/check-spelling@main
uses: check-spelling/check-spelling@cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c # main
with:
checkout: true
spell_check_this: check-spelling/spell-check-this@prerelease
spell_check_this: check-spelling/spell-check-this@92453f59cac8985482dfc301a8ece4c6d7de8a0c # prerelease
task: ${{ needs.spelling.outputs.followup }}
experimental_apply_changes_via_bot: 1
config: .github/actions/docs-spelling
@@ -156,7 +156,7 @@ jobs:
cancel-in-progress: false
steps:
- name: apply spelling updates
uses: check-spelling/check-spelling@main
uses: check-spelling/check-spelling@cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c # main
with:
experimental_apply_changes_via_bot: 1
checkout: true

View File

@@ -17,32 +17,81 @@ on:
env:
GITHUB_PR_NUMBER: ${{github.event.pull_request.number}}
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
functional:
name: Functional (shard ${{ matrix.shard }}/5)
build-web:
name: Build web bundle
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5]
container:
image: mcr.microsoft.com/playwright:v1.58.2-jammy
image: mcr.microsoft.com/playwright:v1.59.1-jammy
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Trust the repository directory
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Build browser bundle
# REACT_APP_NETLIFY=true flips isNonProductionEnvironment() on in the
# bundle so the "Create test file" button (used by every e2e beforeEach
# via ConfigurationPage.createTestFile()) is still rendered in a
# production build. Without it, e2e tests would time out waiting for
# a button that was tree-shaken out.
env:
REACT_APP_NETLIFY: 'true'
run: |
yarn workspace plugins-service build
yarn workspace @actual-app/crdt build
yarn workspace @actual-app/core build:browser
yarn workspace @actual-app/web build:browser
- name: Upload build artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: desktop-client-build
path: packages/desktop-client/build/
retention-days: 1
overwrite: true
functional:
name: Functional (shard ${{ matrix.shard }}/3)
runs-on: ubuntu-latest
needs: build-web
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
container:
image: mcr.microsoft.com/playwright:v1.59.1-jammy
env:
E2E_USE_BUILD: '1'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Trust the repository directory
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Download web build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: desktop-client-build
path: packages/desktop-client/build/
- name: Run E2E Tests
run: yarn e2e --shard=${{ matrix.shard }}/5
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: always()
run: yarn e2e --shard=${{ matrix.shard }}/3
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure()
with:
name: desktop-client-test-results-shard-${{ matrix.shard }}
path: packages/desktop-client/test-results/
@@ -53,19 +102,27 @@ jobs:
name: Functional Desktop App
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.58.2-jammy
image: mcr.microsoft.com/playwright:v1.59.1-jammy
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Trust the repository directory
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
# Build tools are needed to rebuild native modules like better-sqlite3 used by the Desktop app, which is required to run E2E tests on the Desktop app.
- name: Install build tools
run: apt-get update && apt-get install -y build-essential python3
- name: Run Desktop app E2E Tests
run: |
yarn rebuild-electron
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: desktop-app-test-results
@@ -74,23 +131,35 @@ jobs:
overwrite: true
vrt:
name: Visual regression (shard ${{ matrix.shard }}/5)
name: Visual regression (shard ${{ matrix.shard }}/3)
runs-on: ubuntu-latest
needs: build-web
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5]
shard: [1, 2, 3]
container:
image: mcr.microsoft.com/playwright:v1.58.2-jammy
image: mcr.microsoft.com/playwright:v1.59.1-jammy
env:
E2E_USE_BUILD: '1'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Trust the repository directory
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Download web build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: desktop-client-build
path: packages/desktop-client/build/
- name: Run VRT Tests
run: yarn vrt --shard=${{ matrix.shard }}/5
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
run: yarn vrt --shard=${{ matrix.shard }}/3
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: vrt-blob-report-${{ matrix.shard }}
@@ -104,9 +173,11 @@ jobs:
runs-on: ubuntu-latest
if: ${{ !cancelled() }}
container:
image: mcr.microsoft.com/playwright:v1.58.2-jammy
image: mcr.microsoft.com/playwright:v1.59.1-jammy
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
- name: Download all blob reports
@@ -118,7 +189,7 @@ jobs:
- name: Merge reports
id: merge-reports
run: yarn workspace @actual-app/web run playwright merge-reports --reporter html ./all-blob-reports
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
id: playwright-report-vrt
with:
name: html-report--attempt-${{ github.run_attempt }}
@@ -131,10 +202,12 @@ jobs:
mkdir -p vrt-metadata
echo "${{ github.event.pull_request.number }}" > vrt-metadata/pr-number.txt
echo "${{ needs.vrt.result }}" > vrt-metadata/vrt-result.txt
echo "${{ steps.playwright-report-vrt.outputs.artifact-url }}" > vrt-metadata/artifact-url.txt
echo "${STEPS_PLAYWRIGHT_REPORT_VRT_OUTPUTS_ARTIFACT_URL}" > vrt-metadata/artifact-url.txt
env:
STEPS_PLAYWRIGHT_REPORT_VRT_OUTPUTS_ARTIFACT_URL: ${{ steps.playwright-report-vrt.outputs.artifact-url }}
- name: Upload VRT metadata
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: vrt-comment-metadata
path: vrt-metadata/

View File

@@ -53,7 +53,7 @@ jobs:
- name: Comment on PR with VRT report link
if: steps.metadata.outputs.should_comment == 'true'
uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3.0.2
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
with:
number: ${{ steps.metadata.outputs.pr_number }}
header: vrt-comment

View File

@@ -22,6 +22,7 @@ jobs:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
os:
- ubuntu-22.04
@@ -30,6 +31,8 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- if: ${{ startsWith(matrix.os, 'windows') }}
run: pip.exe install setuptools
- if: ${{ ! startsWith(matrix.os, 'windows') }}
@@ -56,9 +59,11 @@ jobs:
METAINFO_FILE="packages/desktop-electron/extra-resources/linux/com.actualbudget.actual.metainfo.xml"
TODAY=$(date +%Y-%m-%d)
VERSION=${{ steps.process_version.outputs.version }}
VERSION=${STEPS_PROCESS_VERSION_OUTPUTS_VERSION}
sed -i "s/%RELEASE_VERSION%/$VERSION/g; s/%RELEASE_DATE%/$TODAY/g" "$METAINFO_FILE"
flatpak run --command=flatpak-builder-lint org.flatpak.Builder appstream "$METAINFO_FILE"
env:
STEPS_PROCESS_VERSION_OUTPUTS_VERSION: ${{ steps.process_version.outputs.version }}
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Electron for Mac
@@ -74,7 +79,7 @@ jobs:
if: ${{ ! startsWith(matrix.os, 'macos') }}
run: ./bin/package-electron
- name: Upload Build
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: actual-electron-${{ matrix.os }}
path: |
@@ -85,7 +90,7 @@ jobs:
packages/desktop-electron/dist/*.flatpak
- name: Upload Windows Store Build
if: ${{ startsWith(matrix.os, 'windows') }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: actual-electron-${{ matrix.os }}-appx
path: |

View File

@@ -26,6 +26,7 @@ concurrency:
jobs:
build:
strategy:
fail-fast: false
matrix:
os:
- ubuntu-22.04
@@ -34,6 +35,8 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- if: ${{ startsWith(matrix.os, 'windows') }}
run: pip.exe install setuptools
- if: ${{ ! startsWith(matrix.os, 'windows') }}
@@ -65,56 +68,56 @@ jobs:
run: ./bin/package-electron
- name: Upload Linux x64 AppImage
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Actual-linux-x86_64.AppImage
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-linux-x86_64.AppImage
- name: Upload Linux arm64 AppImage
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Actual-linux-arm64.AppImage
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-linux-arm64.AppImage
- name: Upload Linux x64 flatpak
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Actual-linux-x86_64.flatpak
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-linux-x86_64.flatpak
- name: Upload Windows x32 exe
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Actual-windows-ia32.exe
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-windows-ia32.exe
- name: Upload Windows x64 exe
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Actual-windows-x64.exe
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-windows-x64.exe
- name: Upload Windows arm64 exe
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Actual-windows-arm64.exe
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-windows-arm64.exe
- name: Upload Mac x64 dmg
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Actual-mac-x64.dmg
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-mac-x64.dmg
- name: Upload Mac arm64 dmg
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Actual-mac-arm64.dmg
if-no-files-found: ignore
@@ -122,7 +125,7 @@ jobs:
- name: Upload Windows Store Build
if: ${{ startsWith(matrix.os, 'windows') }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: actual-electron-${{ matrix.os }}-appx
path: |

View File

@@ -25,7 +25,7 @@ jobs:
if: github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Post welcome comment
uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3.0.2
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
number: ${{ github.event.pull_request.number }}

View File

@@ -15,6 +15,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
path: actual
persist-credentials: false
- name: Set up environment
uses: ./actual/.github/actions/setup
with:
@@ -27,12 +28,23 @@ jobs:
- name: Configure i18n client
run: |
pip install wlc
- name: Configure Weblate API credentials
env:
WEBLATE_API_KEY: ${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}
run: |
# Write the API key to wlc's config file instead of passing it on
# the command line, so the secret doesn't appear in process listings.
mkdir -p "$HOME/.config"
umask 077
cat > "$HOME/.config/weblate" <<EOF
[keys]
https://hosted.weblate.org/api/ = ${WEBLATE_API_KEY}
EOF
- name: Lock translations
run: |
wlc \
--url https://hosted.weblate.org/api/ \
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
lock \
actualbudget/actual
@@ -40,7 +52,6 @@ jobs:
run: |
wlc \
--url https://hosted.weblate.org/api/ \
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
push \
actualbudget/actual
- name: Check out updated translations
@@ -49,6 +60,8 @@ jobs:
ssh-key: ${{ secrets.STRING_IMPORT_DEPLOY_KEY }}
repository: actualbudget/translations
path: translations
# Need to be able to push back extracted strings
persist-credentials: true
- name: Generate i18n strings
working-directory: actual
run: |
@@ -73,7 +86,6 @@ jobs:
run: |
wlc \
--url https://hosted.weblate.org/api/ \
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
pull \
actualbudget/actual
@@ -82,6 +94,5 @@ jobs:
run: |
wlc \
--url https://hosted.weblate.org/api/ \
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
unlock \
actualbudget/actual

View File

@@ -25,6 +25,8 @@ jobs:
steps:
# This is not a security concern because we have approved & merged the PR
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22

View File

@@ -22,6 +22,8 @@ jobs:
steps:
- name: Repository Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
@@ -34,10 +36,11 @@ jobs:
- name: Deploy to Netlify
id: netlify_deploy
env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_API_TOKEN }}
run: |
netlify deploy \
--dir packages/desktop-client/build \
--site ${{ secrets.NETLIFY_SITE_ID }} \
--auth ${{ secrets.NETLIFY_API_TOKEN }} \
--filter @actual-app/web \
--prod

View File

@@ -0,0 +1,47 @@
name: Nightly theme catalog scan
on:
schedule:
# 05:15 UTC daily — runs after the i18n extract job (04:00) and well
# before the nightly Electron/npm publishes (00:00 UTC the next day).
- cron: '15 5 * * *'
workflow_dispatch:
permissions:
contents: read
jobs:
validate-theme-catalog:
name: Validate custom theme catalog
runs-on: ubuntu-latest
if: github.repository == 'actualbudget/actual'
timeout-minutes: 10
steps:
- name: Check out repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Validate themes
run: yarn workspace @actual-app/web validate:theme-catalog
notify-failure:
name: Notify Discord on failure
needs: validate-theme-catalog
if: failure() && github.repository == 'actualbudget/actual'
runs-on: ubuntu-latest
environment: nightly-alerts
timeout-minutes: 5
steps:
- name: Notify Discord
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_URL }}
status: Failure
title: Nightly theme catalog scan failed
description: The nightly scan failed. One or more themes may be broken, or the scan itself did not complete.
username: Actual Nightly
nofail: true

View File

@@ -54,8 +54,9 @@ jobs:
- name: Verify release assets exist
env:
GH_TOKEN: ${{ github.token }}
STEPS_RESOLVE_VERSION_OUTPUTS_TAG: ${{ steps.resolve_version.outputs.tag }}
run: |
TAG="${{ steps.resolve_version.outputs.tag }}"
TAG="${STEPS_RESOLVE_VERSION_OUTPUTS_TAG}"
echo "Checking release assets for tag $TAG..."
ASSETS=$(gh api "repos/${{ github.repository }}/releases/tags/$TAG" --jq '.assets[].name')
@@ -77,7 +78,7 @@ jobs:
- name: Calculate AppImage SHA256 (streamed)
run: |
VERSION="${{ steps.resolve_version.outputs.version }}"
VERSION="${STEPS_RESOLVE_VERSION_OUTPUTS_VERSION}"
BASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}"
echo "Streaming x86_64 AppImage to compute SHA256..."
@@ -90,30 +91,35 @@ jobs:
echo "APPIMAGE_X64_SHA256=$APPIMAGE_X64_SHA256" >> "$GITHUB_ENV"
echo "APPIMAGE_ARM64_SHA256=$APPIMAGE_ARM64_SHA256" >> "$GITHUB_ENV"
env:
STEPS_RESOLVE_VERSION_OUTPUTS_VERSION: ${{ steps.resolve_version.outputs.version }}
- name: Checkout Flathub repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: flathub/com.actualbudget.actual
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
persist-credentials: false
- name: Update manifest with new version
run: |
VERSION="${{ steps.resolve_version.outputs.version }}"
VERSION="${STEPS_RESOLVE_VERSION_OUTPUTS_VERSION}"
# Replace x86_64 entry
sed -i "/x86_64.AppImage/{n;s|sha256:.*|sha256: ${{ env.APPIMAGE_X64_SHA256 }}|}" com.actualbudget.actual.yml
sed -i "/x86_64.AppImage/{n;s|sha256:.*|sha256: ${APPIMAGE_X64_SHA256}|}" com.actualbudget.actual.yml
sed -i "/x86_64.AppImage/s|url:.*|url: https://github.com/actualbudget/actual/releases/download/v${VERSION}/Actual-linux-x86_64.AppImage|" com.actualbudget.actual.yml
# Replace arm64 entry
sed -i "/arm64.AppImage/{n;s|sha256:.*|sha256: ${{ env.APPIMAGE_ARM64_SHA256 }}|}" com.actualbudget.actual.yml
sed -i "/arm64.AppImage/{n;s|sha256:.*|sha256: ${APPIMAGE_ARM64_SHA256}|}" com.actualbudget.actual.yml
sed -i "/arm64.AppImage/s|url:.*|url: https://github.com/actualbudget/actual/releases/download/v${VERSION}/Actual-linux-arm64.AppImage|" com.actualbudget.actual.yml
echo "Updated manifest:"
cat com.actualbudget.actual.yml
env:
STEPS_RESOLVE_VERSION_OUTPUTS_VERSION: ${{ steps.resolve_version.outputs.version }}
- name: Create PR in Flathub repo
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
commit-message: 'Update Actual flatpak to version ${{ steps.resolve_version.outputs.version }}'

View File

@@ -20,6 +20,7 @@ concurrency:
jobs:
build:
strategy:
fail-fast: false
matrix:
os:
- ubuntu-22.04
@@ -29,6 +30,8 @@ jobs:
if: github.event.repository.fork == false
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- if: ${{ startsWith(matrix.os, 'windows') }}
run: pip.exe install setuptools
@@ -83,49 +86,49 @@ jobs:
run: ./bin/package-electron
- name: Upload Linux x64 AppImage
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Actual-linux-x86_64.AppImage
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-linux-x86_64.AppImage
- name: Upload Linux arm64 AppImage
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Actual-linux-arm64.AppImage
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-linux-arm64.AppImage
- name: Upload Windows x32 exe
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Actual-windows-ia32.exe
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-windows-ia32.exe
- name: Upload Windows x64 exe
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Actual-windows-x64.exe
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-windows-x64.exe
- name: Upload Windows arm64 exe
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Actual-windows-arm64.exe
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-windows-arm64.exe
- name: Upload Mac x64 dmg
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Actual-mac-x64.dmg
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-mac-x64.dmg
- name: Upload Mac arm64 dmg
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Actual-mac-arm64.dmg
if-no-files-found: ignore
@@ -133,7 +136,7 @@ jobs:
- name: Upload Windows Store Build
if: ${{ startsWith(matrix.os, 'windows') }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: actual-electron-${{ matrix.os }}-appx
path: |

View File

@@ -1,124 +0,0 @@
name: Publish nightly npm packages
# Nightly npm packages are built daily at midnight UTC
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
build-and-pack:
runs-on: ubuntu-latest
name: Build and pack npm packages
if: github.event.repository.fork == false
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
- name: Update package versions
run: |
# Get new nightly versions
NEW_CORE_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/loot-core/package.json --type nightly)
NEW_WEB_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/desktop-client/package.json --type nightly)
NEW_SYNC_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/sync-server/package.json --type nightly)
NEW_API_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/api/package.json --type nightly)
NEW_CLI_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/cli/package.json --type nightly)
# Set package versions
npm version $NEW_CORE_VERSION --no-git-tag-version --workspace=@actual-app/core --no-workspaces-update
npm version $NEW_WEB_VERSION --no-git-tag-version --workspace=@actual-app/web --no-workspaces-update
npm version $NEW_SYNC_VERSION --no-git-tag-version --workspace=@actual-app/sync-server --no-workspaces-update
npm version $NEW_API_VERSION --no-git-tag-version --workspace=@actual-app/api --no-workspaces-update
npm version $NEW_CLI_VERSION --no-git-tag-version --workspace=@actual-app/cli --no-workspaces-update
- name: Yarn install
run: |
yarn install
- name: Pack the core package
run: |
yarn workspace @actual-app/core pack --filename @actual-app/core.tgz
- name: Build Server & Web
run: yarn build:server
- name: Pack the web and server packages
run: |
yarn workspace @actual-app/web pack --filename @actual-app/web.tgz
yarn workspace @actual-app/sync-server pack --filename @actual-app/sync-server.tgz
- name: Build API
run: yarn build:api
- name: Pack the api package
run: |
yarn workspace @actual-app/api pack --filename @actual-app/api.tgz
- name: Build CLI
run: yarn workspace @actual-app/cli build
- name: Pack the cli package
run: |
yarn workspace @actual-app/cli pack --filename @actual-app/cli.tgz
- name: Upload package artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: npm-packages
path: |
packages/loot-core/@actual-app/core.tgz
packages/desktop-client/@actual-app/web.tgz
packages/sync-server/@actual-app/sync-server.tgz
packages/api/@actual-app/api.tgz
packages/cli/@actual-app/cli.tgz
publish:
runs-on: ubuntu-latest
name: Publish Nightly npm packages
needs: build-and-pack
permissions:
contents: read
packages: write
steps:
- name: Download the artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: npm-packages
- name: Setup node and npm registry
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
- name: Publish Core
run: |
npm publish loot-core/@actual-app/core.tgz --access public --tag nightly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish Web
run: |
npm publish desktop-client/@actual-app/web.tgz --access public --tag nightly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish Sync-Server
run: |
npm publish sync-server/@actual-app/sync-server.tgz --access public --tag nightly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish API
run: |
npm publish api/@actual-app/api.tgz --access public --tag nightly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish CLI
run: |
npm publish cli/@actual-app/cli.tgz --access public --tag nightly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,26 +1,55 @@
name: Publish npm packages
# # Npm packages are published for every new tag
# Npm packages are published for every new tag and nightly schedule
on:
push:
tags:
- 'v*.*.*'
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
build-and-pack:
runs-on: ubuntu-latest
name: Build and pack npm packages
if: github.event_name == 'push' || (github.event.repository.fork == false)
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
- name: Update package versions
if: github.event_name != 'push'
run: |
# Get new nightly versions
NEW_CORE_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/loot-core/package.json --type nightly)
NEW_WEB_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/desktop-client/package.json --type nightly)
NEW_SYNC_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/sync-server/package.json --type nightly)
NEW_API_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/api/package.json --type nightly)
NEW_CLI_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/cli/package.json --type nightly)
# Set package versions
npm version $NEW_CORE_VERSION --no-git-tag-version --workspace=@actual-app/core --no-workspaces-update
npm version $NEW_WEB_VERSION --no-git-tag-version --workspace=@actual-app/web --no-workspaces-update
npm version $NEW_SYNC_VERSION --no-git-tag-version --workspace=@actual-app/sync-server --no-workspaces-update
npm version $NEW_API_VERSION --no-git-tag-version --workspace=@actual-app/api --no-workspaces-update
npm version $NEW_CLI_VERSION --no-git-tag-version --workspace=@actual-app/cli --no-workspaces-update
- name: Yarn install
if: github.event_name != 'push'
run: |
# Required after nightly `npm version` updates workspace manifests.
yarn install
- name: Pack the core package
run: |
yarn workspace @actual-app/core pack --filename @actual-app/core.tgz
- name: Build Web
- name: Build Server & Web
run: yarn build:server
- name: Pack the web and server packages
@@ -43,7 +72,8 @@ jobs:
yarn workspace @actual-app/cli pack --filename @actual-app/cli.tgz
- name: Upload package artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ !env.ACT }}
with:
name: npm-packages
path: |
@@ -60,6 +90,9 @@ jobs:
permissions:
contents: read
packages: write
id-token: write # Required for OIDC
env:
NPM_DIST_TAG: ${{ github.event_name != 'push' && 'nightly' || '' }}
steps:
- name: Download the artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
@@ -69,35 +102,26 @@ jobs:
- name: Setup node and npm registry
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
node-version: 24
check-latest: true
registry-url: 'https://registry.npmjs.org'
- name: Publish Core
run: |
npm publish loot-core/@actual-app/core.tgz --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
npm publish loot-core/@actual-app/core.tgz --access public ${NPM_DIST_TAG:+--tag "$NPM_DIST_TAG"}
- name: Publish Web
run: |
npm publish desktop-client/@actual-app/web.tgz --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
npm publish desktop-client/@actual-app/web.tgz --access public ${NPM_DIST_TAG:+--tag "$NPM_DIST_TAG"}
- name: Publish Sync-Server
run: |
npm publish sync-server/@actual-app/sync-server.tgz --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
npm publish sync-server/@actual-app/sync-server.tgz --access public ${NPM_DIST_TAG:+--tag "$NPM_DIST_TAG"}
- name: Publish API
run: |
npm publish api/@actual-app/api.tgz --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
npm publish api/@actual-app/api.tgz --access public ${NPM_DIST_TAG:+--tag "$NPM_DIST_TAG"}
- name: Publish CLI
run: |
npm publish cli/@actual-app/cli.tgz --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
npm publish cli/@actual-app/cli.tgz --access public ${NPM_DIST_TAG:+--tag "$NPM_DIST_TAG"}

View File

@@ -3,6 +3,10 @@ name: Release notes
on:
pull_request:
permissions:
contents: write
pull-requests: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
@@ -11,15 +15,37 @@ jobs:
release-notes:
runs-on: ubuntu-latest
steps:
- name: Check if triggered by bot
id: bot-check
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { data: commit } = await github.rest.git.getCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: context.payload.pull_request.head.sha,
});
const skip = commit.author.name === 'github-actions[bot]'
&& commit.message.startsWith('Generate release notes');
console.log(`Head commit by "${commit.author.name}": ${commit.message.split('\n')[0]}`);
console.log(`Skip: ${skip}`);
core.setOutput('skip', String(skip));
- name: Checkout
if: steps.bot-check.outputs.skip != 'true'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
token: ${{ secrets.ACTIONS_UPDATE_TOKEN || github.token }}
# Need to be able to commit release notes after generation
persist-credentials: true
- name: Get changed files
if: steps.bot-check.outputs.skip != 'true'
id: changed-files
run: |
git fetch origin ${{ github.base_ref }}
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
git fetch origin ${GITHUB_BASE_REF}
CHANGED_FILES=$(git diff --name-only origin/${GITHUB_BASE_REF}...HEAD)
NON_DOCS_FILES=$(echo "$CHANGED_FILES" | grep -v -e "^packages/docs/" -e "^\.github/actions/docs-spelling/" || true)
if [ -z "$NON_DOCS_FILES" ] && [ -n "$CHANGED_FILES" ]; then
@@ -28,9 +54,17 @@ jobs:
else
echo "only_docs=false" >> $GITHUB_OUTPUT
fi
- name: Check release notes
if: startsWith(github.head_ref, 'release/') == false && steps.changed-files.outputs.only_docs != 'true'
uses: actualbudget/actions/release-notes/check@main
if: >-
steps.bot-check.outputs.skip != 'true'
&& startsWith(github.head_ref, 'release/') == false
&& steps.changed-files.outputs.only_docs != 'true'
uses: ./.github/actions/release-notes/check
- name: Generate release notes
if: startsWith(github.head_ref, 'release/') == true
uses: actualbudget/actions/release-notes/generate@main
if: >-
steps.bot-check.outputs.skip != 'true'
&& startsWith(github.head_ref, 'release/') == true
&& github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name
uses: ./.github/actions/release-notes/generate

View File

@@ -38,6 +38,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.base_ref }}
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -64,6 +65,13 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
checkName: cli
ref: ${{github.base_ref}}
- name: Wait for ${{github.base_ref}} CRDT build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: master-crdt-build
with:
token: ${{ secrets.GITHUB_TOKEN }}
checkName: crdt
ref: ${{github.base_ref}}
- name: Wait for PR build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
@@ -86,15 +94,22 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
checkName: cli
ref: ${{github.event.pull_request.head.sha}}
- name: Wait for CRDT PR build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: wait-for-crdt-build
with:
token: ${{ secrets.GITHUB_TOKEN }}
checkName: crdt
ref: ${{github.event.pull_request.head.sha}}
- name: Report build failure
if: steps.wait-for-web-build.outputs.conclusion == 'failure' || steps.wait-for-api-build.outputs.conclusion == 'failure' || steps.wait-for-cli-build.outputs.conclusion == 'failure'
if: steps.wait-for-web-build.outputs.conclusion == 'failure' || steps.wait-for-api-build.outputs.conclusion == 'failure' || steps.wait-for-cli-build.outputs.conclusion == 'failure' || steps.wait-for-crdt-build.outputs.conclusion == 'failure'
run: |
echo "Build failed on PR branch or ${{github.base_ref}}"
echo "Build failed on PR branch or ${GITHUB_BASE_REF}"
exit 1
- name: Download web build artifact from ${{github.base_ref}}
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
id: pr-web-build
with:
branch: ${{github.base_ref}}
@@ -103,7 +118,7 @@ jobs:
name: build-stats
path: base
- name: Download API build artifact from ${{github.base_ref}}
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
id: pr-api-build
with:
branch: ${{github.base_ref}}
@@ -112,7 +127,7 @@ jobs:
name: api-build-stats
path: base
- name: Download build stats from PR
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
with:
pr: ${{github.event.pull_request.number}}
workflow: build.yml
@@ -121,7 +136,7 @@ jobs:
path: head
allow_forks: true
- name: Download API stats from PR
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
with:
pr: ${{github.event.pull_request.number}}
workflow: build.yml
@@ -130,7 +145,7 @@ jobs:
path: head
allow_forks: true
- name: Download CLI build artifact from ${{github.base_ref}}
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
with:
branch: ${{github.base_ref}}
workflow: build.yml
@@ -138,7 +153,7 @@ jobs:
name: cli-build-stats
path: base
- name: Download CLI stats from PR
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
with:
pr: ${{github.event.pull_request.number}}
workflow: build.yml
@@ -146,6 +161,23 @@ jobs:
name: cli-build-stats
path: head
allow_forks: true
- name: Download CRDT build artifact from ${{github.base_ref}}
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
with:
branch: ${{github.base_ref}}
workflow: build.yml
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: crdt-build-stats
path: base
- name: Download CRDT stats from PR
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
with:
pr: ${{github.event.pull_request.number}}
workflow: build.yml
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: crdt-build-stats
path: head
allow_forks: true
- name: Strip content hashes from stats files
run: |
if [ -f ./head/web-stats.json ]; then
@@ -168,10 +200,12 @@ jobs:
--base loot-core=./base/loot-core-stats.json \
--base api=./base/api-stats.json \
--base cli=./base/cli-stats.json \
--base crdt=./base/crdt-stats.json \
--head desktop-client=./head/web-stats.json \
--head loot-core=./head/loot-core-stats.json \
--head api=./head/api-stats.json \
--head cli=./head/cli-stats.json \
--head crdt=./head/crdt-stats.json \
--identifier combined \
--format pr-body > bundle-stats-comment.md
- name: Post combined bundle stats comment

View File

@@ -3,9 +3,12 @@ on:
schedule:
- cron: '30 1 * * *'
workflow_dispatch: # Allow manual triggering
permissions: {}
jobs:
stale:
permissions:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
@@ -16,6 +19,8 @@ jobs:
days-before-close: 5
days-before-issue-stale: -1
stale-wip:
permissions:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
@@ -27,6 +32,8 @@ jobs:
days-before-issue-stale: -1
stale-needs-info:
permissions:
issues: write
runs-on: ubuntu-latest
steps:
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0

View File

@@ -107,7 +107,7 @@ jobs:
fi
# Commit
git commit -m "Update VRT screenshots" -m "Auto-generated by VRT workflow" -m "PR: #${{ steps.metadata.outputs.pr_number }}"
git commit -m "Update VRT screenshots" -m "Auto-generated by VRT workflow" -m "PR: #${STEPS_METADATA_OUTPUTS_PR_NUMBER}"
echo "applied=true" >> "$GITHUB_OUTPUT"
else
@@ -116,6 +116,8 @@ jobs:
echo "error=Patch conflicts with current branch state" >> "$GITHUB_OUTPUT"
exit 1
fi
env:
STEPS_METADATA_OUTPUTS_PR_NUMBER: ${{ steps.metadata.outputs.pr_number }}
- name: Push changes
if: steps.apply.outputs.applied == 'true'
@@ -133,12 +135,15 @@ jobs:
- name: Comment on PR - Failure
if: failure() && steps.metadata.outputs.pr_number != ''
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
APPLY_ERROR: ${{ steps.apply.outputs.error }}
PR_NUMBER: ${{ steps.metadata.outputs.pr_number }}
with:
script: |
const error = `${{ steps.apply.outputs.error }}` || 'Unknown error occurred';
const error = process.env.APPLY_ERROR || 'Unknown error occurred';
await github.rest.issues.createComment({
issue_number: ${{ steps.metadata.outputs.pr_number }},
issue_number: parseInt(process.env.PR_NUMBER, 10),
owner: context.repo.owner,
repo: context.repo.repo,
body: `❌ Failed to apply VRT updates: ${error}\n\nPlease check the workflow logs for details.`

View File

@@ -26,7 +26,7 @@ jobs:
pull-requests: write
steps:
- name: Add 👀 reaction to comment
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
await github.rest.reactions.createForIssueComment({
@@ -44,11 +44,11 @@ jobs:
github.event.issue.pull_request &&
startsWith(github.event.comment.body, '/update-vrt')
container:
image: mcr.microsoft.com/playwright:v1.58.2-jammy
image: mcr.microsoft.com/playwright:v1.59.1-jammy
steps:
- name: Get PR details
id: pr
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { data: pr } = await github.rest.pulls.get({
@@ -63,15 +63,21 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ steps.pr.outputs.head_sha }}
persist-credentials: false
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
# Build tools are needed to rebuild native modules like better-sqlite3 used by the Desktop app, which is required to run VRT tests on the Desktop app and generate updated snapshots.
- name: Install build tools
run: apt-get update && apt-get install -y build-essential python3
- name: Run VRT Tests on Desktop app
continue-on-error: true
run: |
yarn rebuild-electron
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop --update-snapshots
- name: Run VRT Tests
@@ -113,7 +119,7 @@ jobs:
- name: Upload patch artifact
if: steps.create-patch.outputs.has_changes == 'true'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: vrt-patch-${{ github.event.issue.number }}
path: vrt-update.patch
@@ -124,12 +130,15 @@ jobs:
run: |
mkdir -p pr-metadata
echo "${{ github.event.issue.number }}" > pr-metadata/pr-number.txt
echo "${{ steps.pr.outputs.head_ref }}" > pr-metadata/head-ref.txt
echo "${{ steps.pr.outputs.head_repo }}" > pr-metadata/head-repo.txt
echo "${STEPS_PR_OUTPUTS_HEAD_REF}" > pr-metadata/head-ref.txt
echo "${STEPS_PR_OUTPUTS_HEAD_REPO}" > pr-metadata/head-repo.txt
env:
STEPS_PR_OUTPUTS_HEAD_REF: ${{ steps.pr.outputs.head_ref }}
STEPS_PR_OUTPUTS_HEAD_REPO: ${{ steps.pr.outputs.head_repo }}
- name: Upload PR metadata
if: steps.create-patch.outputs.has_changes == 'true'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: vrt-metadata-${{ github.event.issue.number }}
path: pr-metadata/

4
.gitignore vendored
View File

@@ -58,6 +58,10 @@ bundle.mobile.js.map
# IntelliJ IDEA
.idea
# Claude Code
.claude/worktrees/*
.claude/settings.local.json
# Misc
.#*

View File

@@ -1,11 +1,19 @@
#!/bin/sh
# Run yarn install when switching branches (if yarn.lock changed)
# or when creating a new worktree (node_modules won't exist yet)
# $3 is 1 for branch checkout, 0 for file checkout
if [ "$3" != "1" ]; then
exit 0
fi
# Worktree creation: node_modules doesn't exist yet, always install
if [ ! -d "node_modules" ]; then
echo "New worktree detected — running yarn install..."
yarn install || exit 1
exit 0
fi
# Check if yarn.lock changed between the old and new HEAD
if git diff --name-only "$1" "$2" | grep -q "^yarn.lock$"; then
echo "yarn.lock changed — running yarn install..."

0
.husky/pre-commit Normal file → Executable file
View File

View File

@@ -9,24 +9,14 @@
"react",
"builtin",
"external",
"loot-core",
["parent", "subpath"],
"sibling",
"index",
"desktop-client"
"index"
],
"customGroups": [
{
"groupName": "react",
"elementNamePattern": ["react", "react-dom/*", "react-*"]
},
{
"groupName": "loot-core",
"elementNamePattern": ["loot-core/**", "@actual-app/core/**"]
},
{
"groupName": "desktop-client",
"elementNamePattern": ["@desktop-client/**"]
}
],
"newlinesBetween": true

View File

@@ -36,6 +36,9 @@
"actual/prefer-const": "error",
"actual/no-anchor-tag": "error",
"actual/no-react-default-import": "error",
"actual/prefer-subpath-imports": "error",
"actual/enforce-boundaries": "error",
"actual/no-extraneous-dependencies": "error",
// JSX A11y rules
"jsx-a11y/no-autofocus": [
@@ -120,9 +123,6 @@
"import/no-amd": "error",
"import/no-default-export": "error",
"import/no-webpack-loader-syntax": "error",
"import/no-useless-path-segments": "error",
"import/no-unresolved": "error",
"import/no-unused-modules": "error",
"import/no-duplicates": [
"error",
{
@@ -160,7 +160,6 @@
"react/no-danger-with-children": "error",
"react/no-direct-mutation-state": "error",
"react/no-is-mounted": "error",
"react/no-unstable-nested-components": "error",
"react/require-render-return": "error",
"react/rules-of-hooks": "error",
"react/self-closing-comp": "error",
@@ -234,7 +233,7 @@
"eslint/require-yield": "error",
"eslint/getter-return": "error",
"eslint/unicode-bom": ["error", "never"],
"eslint/no-use-isnan": "error",
"eslint/use-isnan": "error",
"eslint/valid-typeof": "error",
"eslint/no-useless-rename": [
"error",
@@ -335,14 +334,9 @@
],
"patterns": [
{
"group": ["**/*.api", "**/*.web", "**/*.electron"],
"group": ["**/*.api", "**/*.electron"],
"message": "Don't directly reference imports from other platforms"
},
{
"group": ["uuid"],
"importNames": ["*"],
"message": "Use `import { v4 as uuidv4 } from 'uuid'` instead"
},
{
"group": ["**/style", "**/colors"],
"importNames": ["colors"],
@@ -361,7 +355,9 @@
],
"eslint/no-useless-constructor": "error",
"eslint/no-undef": "error",
"eslint/no-unused-expressions": "error"
"eslint/no-unused-expressions": "error",
"eslint/no-return-assign": "error",
"eslint/no-unused-vars": "error"
},
"overrides": [
{
@@ -377,6 +373,12 @@
"actual/prefer-logger-over-console": "off"
}
},
{
"files": ["packages/eslint-plugin-actual/lib/rules/__tests__/**/*"],
"rules": {
"actual/enforce-boundaries": "off"
}
},
{
"files": [
"packages/api/migrations/*",
@@ -421,6 +423,16 @@
"rules": {
"eslint/no-empty-function": "off"
}
},
// crdt enforces the repo's "TODO: enable this" typescript rules as errors
{
"files": ["packages/crdt/**/*"],
"rules": {
"typescript/no-misused-spread": "error",
"typescript/no-base-to-string": "error",
"typescript/no-unsafe-unary-minus": "error",
"typescript/no-unsafe-type-assertion": "error"
}
}
]
}

File diff suppressed because one or more lines are too long

940
.yarn/releases/yarn-4.13.0.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ enableTransparentWorkspaces: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.10.3.cjs
yarnPath: .yarn/releases/yarn-4.13.0.cjs

View File

@@ -281,7 +281,6 @@ Always run `yarn typecheck` before committing.
- Avoid `any` or `unknown` unless absolutely necessary
- Look for existing type definitions in the codebase
- Avoid type assertions (`as`, `!`) - prefer `satisfies`
- Use inline type imports: `import { type MyType } from '...'`
**Naming:**
@@ -331,7 +330,7 @@ Always maintain newlines between import groups.
### Platform-Specific Code
- Don't directly reference platform-specific imports (`.api`, `.web`, `.electron`)
- Don't directly reference platform-specific imports (`.api`, `.electron`)
- Use conditional exports in `loot-core` for platform-specific code
- Platform resolution happens at build time via package.json exports
@@ -501,7 +500,7 @@ Icons in `packages/component-library/src/icons/` are auto-generated. Don't manua
1. Check `tsconfig.json` for path mappings
2. Check package.json `exports` field (especially for loot-core)
3. Verify platform-specific imports (`.web`, `.electron`, `.api`)
3. Verify platform-specific imports (`.electron`, `.api`)
4. Use absolute imports in `desktop-client` (enforced by ESLint)
### Build Failures

View File

@@ -1 +1,3 @@
Please review the contributing documentation on our website: https://actualbudget.org/docs/contributing/
If you plan to use AI tools when contributing, please also read our [AI Usage Policy](https://actualbudget.org/docs/contributing/ai-usage-policy).

View File

@@ -0,0 +1,218 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
derivePublishImports,
validatePackage,
} from '../validate-publish-imports.js';
describe('derivePublishImports', () => {
it('prepends ./build/ to .js paths', () => {
const imports = {
'#account-db': './src/account-db.js',
};
expect(derivePublishImports(imports)).toEqual({
'#account-db': './build/src/account-db.js',
});
});
it('converts .ts extension to .js and prepends ./build/', () => {
const imports = {
'#migrations': './src/migrations.ts',
};
expect(derivePublishImports(imports)).toEqual({
'#migrations': './build/src/migrations.js',
});
});
it('converts .tsx extension to .js and prepends ./build/', () => {
const imports = {
'#component': './src/component.tsx',
};
expect(derivePublishImports(imports)).toEqual({
'#component': './build/src/component.js',
});
});
it('preserves wildcard patterns', () => {
const imports = {
'#accounts/*': './src/accounts/*.js',
'#services/*': './src/app-gocardless/services/*.ts',
};
expect(derivePublishImports(imports)).toEqual({
'#accounts/*': './build/src/accounts/*.js',
'#services/*': './build/src/app-gocardless/services/*.js',
});
});
it('handles multiple entries with mixed extensions', () => {
const imports = {
'#account-db': './src/account-db.js',
'#migrations': './src/migrations.ts',
'#app-gocardless/errors': './src/app-gocardless/errors.ts',
'#util/*': './src/util/*.ts',
'#scripts/*': './src/scripts/*.js',
};
expect(derivePublishImports(imports)).toEqual({
'#account-db': './build/src/account-db.js',
'#migrations': './build/src/migrations.js',
'#app-gocardless/errors': './build/src/app-gocardless/errors.js',
'#util/*': './build/src/util/*.js',
'#scripts/*': './build/src/scripts/*.js',
});
});
it('returns empty object for empty imports', () => {
expect(derivePublishImports({})).toEqual({});
});
it('throws error for non-string imports values', () => {
const imports = {
'#foo': './src/foo.js',
'#conditional': {
browser: './src/browser.js',
node: './src/node.js',
},
};
expect(() => derivePublishImports(imports)).toThrow(
'Unsupported imports target for "#conditional". Expected a string path.',
);
});
it('handles paths with /index.js suffix', () => {
const imports = {
'#util/title': './src/util/title/index.js',
};
expect(derivePublishImports(imports)).toEqual({
'#util/title': './build/src/util/title/index.js',
});
});
});
describe('validatePackage', () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'validate-imports-'));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
function writePackageJson(content: Record<string, unknown>) {
const filePath = path.join(tmpDir, 'package.json');
fs.writeFileSync(filePath, JSON.stringify(content, null, 2) + '\n');
return filePath;
}
it('skips packages with no publishConfig', () => {
const filePath = writePackageJson({
name: 'test-pkg',
imports: { '#foo': './src/foo.js' },
});
const { result, warnings } = validatePackage(filePath);
expect(result).toBeNull();
expect(warnings).toEqual([]);
});
it('skips packages with publishConfig but no publishConfig.imports', () => {
const filePath = writePackageJson({
name: 'test-pkg',
imports: { '#foo': './src/foo.js' },
publishConfig: { access: 'public' },
});
const { result, warnings } = validatePackage(filePath);
expect(result).toBeNull();
expect(warnings).toEqual([]);
});
it('warns when publishConfig.imports exists but imports does not', () => {
const filePath = writePackageJson({
name: 'test-pkg',
publishConfig: {
imports: { '#foo': './build/src/foo.js' },
},
});
const { result, warnings } = validatePackage(filePath);
expect(result).toBeNull();
expect(warnings).toHaveLength(1);
expect(warnings[0]).toContain('orphaned');
});
it('returns no errors when publishConfig.imports matches', () => {
const filePath = writePackageJson({
name: 'test-pkg',
imports: {
'#foo': './src/foo.js',
'#bar': './src/bar.ts',
},
publishConfig: {
imports: {
'#foo': './build/src/foo.js',
'#bar': './build/src/bar.js',
},
},
});
const { result } = validatePackage(filePath);
expect(result).not.toBeNull();
expect(result!.missingKeys).toEqual([]);
expect(result!.extraKeys).toEqual([]);
expect(result!.wrongValues).toEqual([]);
});
it('detects missing keys in publishConfig.imports', () => {
const filePath = writePackageJson({
name: 'test-pkg',
imports: {
'#foo': './src/foo.js',
'#bar': './src/bar.ts',
},
publishConfig: {
imports: {
'#foo': './build/src/foo.js',
},
},
});
const { result } = validatePackage(filePath);
expect(result!.missingKeys).toEqual(['#bar']);
});
it('detects extra keys in publishConfig.imports', () => {
const filePath = writePackageJson({
name: 'test-pkg',
imports: {
'#foo': './src/foo.js',
},
publishConfig: {
imports: {
'#foo': './build/src/foo.js',
'#orphan': './build/src/orphan.js',
},
},
});
const { result } = validatePackage(filePath);
expect(result!.extraKeys).toEqual(['#orphan']);
});
it('detects wrong values in publishConfig.imports', () => {
const filePath = writePackageJson({
name: 'test-pkg',
imports: {
'#foo': './src/foo.ts',
},
publishConfig: {
imports: {
'#foo': './src/foo.ts',
},
},
});
const { result } = validatePackage(filePath);
expect(result!.wrongValues).toEqual([
{ key: '#foo', expected: './build/src/foo.js', actual: './src/foo.ts' },
]);
});
});

View File

@@ -16,6 +16,7 @@ packages/desktop-client/bin/remove-untranslated-languages
export NODE_OPTIONS="--max-old-space-size=4096"
yarn workspace @actual-app/crdt build
yarn workspace plugins-service build
yarn workspace @actual-app/core build:browser
yarn workspace @actual-app/web build:browser

View File

@@ -43,6 +43,7 @@ if [ $SKIP_TRANSLATIONS == false ]; then
pushd packages/desktop-client/locale > /dev/null
git checkout .
git pull
popd > /dev/null
packages/desktop-client/bin/remove-untranslated-languages
@@ -50,6 +51,7 @@ fi
export NODE_OPTIONS="--max-old-space-size=4096"
yarn workspace @actual-app/crdt build
yarn workspace plugins-service build
yarn workspace @actual-app/core build:node
yarn workspace @actual-app/web build --mode=desktop # electron specific build

View File

@@ -28,5 +28,5 @@ echo "Running VRT tests with the following parameters:"
echo "E2E_START_URL: $E2E_START_URL"
echo "VRT_ARGS: $VRT_ARGS"
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.58.2-jammy /bin/bash \
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.59.1-jammy /bin/bash \
-c "E2E_START_URL=$E2E_START_URL yarn vrt $VRT_ARGS"

View File

@@ -0,0 +1,216 @@
import fs from 'node:fs';
import path from 'node:path';
/**
* Derives publishConfig.imports from imports by:
* 1. Prepending ./build/ to each value path
* 2. Replacing .ts/.tsx extensions with .js
*/
export function derivePublishImports(
imports: Record<string, string | object>,
): Record<string, string> {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(imports)) {
if (typeof value !== 'string') {
throw new Error(
`Unsupported imports target for "${key}". Expected a string path.`,
);
}
const withBuildPrefix = value.replace(/^\.\//, './build/');
const withJsExtension = withBuildPrefix.replace(/\.tsx?$/, '.js');
result[key] = withJsExtension;
}
return result;
}
export type ValidationResult = {
packagePath: string;
packageName: string;
missingKeys: string[];
extraKeys: string[];
wrongValues: Array<{ key: string; expected: string; actual: string }>;
};
/**
* Validates publishConfig.imports against imports for a single package.json.
* Returns null if the package should be skipped (no publishConfig.imports).
* Returns a ValidationResult if the package has both fields.
*/
export function validatePackage(packageJsonPath: string): {
result: ValidationResult | null;
warnings: string[];
} {
const warnings: string[] = [];
const content = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
const packageName: string = content.name ?? packageJsonPath;
const imports: Record<string, string | object> | undefined = content.imports;
const publishImports: Record<string, string> | undefined =
content.publishConfig?.imports;
// No publishConfig.imports → skip
if (!publishImports) {
return { result: null, warnings };
}
// Has publishConfig.imports but no imports → warn
if (!imports) {
warnings.push(
`${packageName}: orphaned publishConfig.imports (no imports field)`,
);
return { result: null, warnings };
}
const expected = derivePublishImports(imports);
const expectedKeys = new Set(Object.keys(expected));
const actualKeys = new Set(Object.keys(publishImports));
const missingKeys = [...expectedKeys].filter(k => !actualKeys.has(k));
const extraKeys = [...actualKeys].filter(k => !expectedKeys.has(k));
const wrongValues: ValidationResult['wrongValues'] = [];
for (const key of expectedKeys) {
if (actualKeys.has(key) && publishImports[key] !== expected[key]) {
wrongValues.push({
key,
expected: expected[key],
actual: publishImports[key],
});
}
}
return {
result: {
packagePath: packageJsonPath,
packageName,
missingKeys,
extraKeys,
wrongValues,
},
warnings,
};
}
export function fixPackage(packageJsonPath: string): boolean {
const raw = fs.readFileSync(packageJsonPath, 'utf-8');
const content = JSON.parse(raw);
if (!content.imports || !content.publishConfig?.imports) {
return false;
}
const expected = derivePublishImports(content.imports);
// Check if already correct
if (
JSON.stringify(content.publishConfig.imports) === JSON.stringify(expected)
) {
return false;
}
content.publishConfig.imports = expected;
fs.writeFileSync(packageJsonPath, JSON.stringify(content, null, 2) + '\n');
return true;
}
function findPackageJsonFiles(): string[] {
const packagesDir = path.resolve(__dirname, '..', 'packages');
const entries = fs.readdirSync(packagesDir, { withFileTypes: true });
const results: string[] = [];
for (const entry of entries) {
if (entry.isDirectory()) {
const pkgPath = path.join(packagesDir, entry.name, 'package.json');
if (fs.existsSync(pkgPath)) {
results.push(pkgPath);
}
}
}
return results;
}
function resolvePackageJsonPaths(filePaths: string[]): string[] {
const packagesRoot = path.resolve(__dirname, '..', 'packages');
const seen = new Set<string>();
for (const filePath of filePaths) {
const resolvedPath = path.resolve(filePath);
let dir = path.dirname(resolvedPath);
while (dir.startsWith(packagesRoot + path.sep)) {
const candidate = path.join(dir, 'package.json');
if (
fs.existsSync(candidate) &&
candidate.startsWith(packagesRoot + path.sep)
) {
seen.add(candidate);
break;
}
dir = path.dirname(dir);
}
}
return [...seen];
}
function main() {
const args = process.argv.slice(2);
const fixMode = args.includes('--fix');
const filePaths = args.filter(arg => !arg.startsWith('--'));
const packageJsonFiles =
filePaths.length > 0
? resolvePackageJsonPaths(filePaths)
: findPackageJsonFiles();
let hasErrors = false;
const allWarnings: string[] = [];
for (const pkgPath of packageJsonFiles) {
if (fixMode) {
const fixed = fixPackage(pkgPath);
if (fixed) {
const name = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).name;
console.log(`Fixed publishConfig.imports in ${name}`);
}
} else {
const { result, warnings } = validatePackage(pkgPath);
allWarnings.push(...warnings);
if (result) {
const hasIssues =
result.missingKeys.length > 0 ||
result.extraKeys.length > 0 ||
result.wrongValues.length > 0;
if (hasIssues) {
hasErrors = true;
console.error(`\n${result.packageName}:`);
for (const key of result.missingKeys) {
console.error(` Missing key: ${key}`);
}
for (const key of result.extraKeys) {
console.error(` Extra key: ${key}`);
}
for (const { key, expected, actual } of result.wrongValues) {
console.error(` Wrong value for ${key}:`);
console.error(` expected: ${expected}`);
console.error(` actual: ${actual}`);
}
}
}
}
}
for (const warning of allWarnings) {
console.warn(`Warning: ${warning}`);
}
if (hasErrors) {
console.error(
'\npublishConfig.imports is out of sync. Run with --fix to auto-fix.',
);
process.exit(1);
}
}
if (require.main === module) {
main();
}

9
bin/vitest.config.mts Normal file
View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
include: ['__tests__/**/*.test.ts'],
environment: 'node',
},
});

View File

@@ -1,3 +1,5 @@
const BUILD_OUTPUT_GLOBS = ['lib-dist/**', 'dist/**', 'build/**', '@types/**'];
/** @type {import('lage').ConfigOptions} */
module.exports = {
pipeline: {
@@ -20,14 +22,14 @@ module.exports = {
dependsOn: ['^build'],
cache: true,
options: {
outputGlob: ['lib-dist/**', 'dist/**', 'build/**'],
outputGlob: BUILD_OUTPUT_GLOBS,
},
},
},
cacheOptions: {
cacheStorageConfig: {
provider: 'local',
outputGlob: ['lib-dist/**', 'dist/**', 'build/**'],
outputGlob: BUILD_OUTPUT_GLOBS,
},
},
npmClient: 'yarn',

View File

@@ -40,7 +40,7 @@
"build:browser": "./bin/package-browser",
"build:desktop": "./bin/package-electron",
"build:plugins-service": "yarn workspace plugins-service build",
"build:api": "yarn workspace @actual-app/api build",
"build:api": "yarn build --scope=@actual-app/api",
"build:cli": "yarn build --scope=@actual-app/cli",
"build:docs": "yarn workspace docs build",
"build:storybook": "yarn workspace @actual-app/components build:storybook",
@@ -54,50 +54,57 @@
"playwright": "yarn workspace @actual-app/web run playwright",
"vrt": "yarn workspace @actual-app/web run vrt",
"vrt:docker": "./bin/run-vrt",
"rebuild-electron": "./node_modules/.bin/electron-rebuild -m ./packages/loot-core",
"rebuild-electron": "./node_modules/.bin/electron-rebuild -m ./packages/loot-core && ./node_modules/.bin/electron-rebuild -m ./packages/desktop-electron -o better-sqlite3,bcrypt",
"rebuild-node": "yarn workspace @actual-app/core rebuild",
"lint": "oxfmt --check . && oxlint --type-aware --quiet",
"lint:fix": "oxfmt . && oxlint --fix --type-aware --quiet",
"install:server": "yarn workspaces focus @actual-app/sync-server --production",
"constraints": "yarn constraints",
"typecheck": "tsgo -p tsconfig.root.json --noEmit && lage typecheck",
"jq": "./node_modules/node-jq/bin/jq",
"check:tsconfig-references": "workspaces-to-typescript-project-references --check",
"sync:tsconfig-references": "workspaces-to-typescript-project-references",
"prepare": "husky"
},
"devDependencies": {
"@monorepo-utils/workspaces-to-typescript-project-references": "^2.10.3",
"@octokit/rest": "^22.0.1",
"@types/node": "^22.19.15",
"@types/node": "^22.19.17",
"@types/prompts": "^2.4.9",
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
"@typescript/native-preview": "beta",
"@yarnpkg/types": "^4.0.1",
"baseline-browser-mapping": "^2.10.0",
"cross-env": "^10.1.0",
"eslint": "^9.39.3",
"eslint-plugin-perfectionist": "^5.6.0",
"eslint": "^10.2.0",
"eslint-plugin-perfectionist": "^5.8.0",
"eslint-plugin-typescript-paths": "^0.0.33",
"html-to-image": "^1.11.13",
"husky": "^9.1.7",
"lage": "^2.14.19",
"lint-staged": "^16.3.2",
"minimatch": "^10.2.4",
"node-jq": "^6.3.1",
"lage": "^2.15.5",
"lint-staged": "^16.4.0",
"minimatch": "^10.2.5",
"npm-run-all": "^4.1.5",
"oxfmt": "^0.32.0",
"oxlint": "^1.51.0",
"oxlint-tsgolint": "^0.13.0",
"oxfmt": "^0.44.0",
"oxlint": "^1.59.0",
"oxlint-tsgolint": "^0.20.0",
"p-limit": "^7.3.0",
"prompts": "^2.4.2",
"source-map-support": "^0.5.21",
"ts-node": "^10.9.2",
"typescript": "^5.9.3"
"typescript": "^6.0.2",
"vitest": "^4.1.2"
},
"resolutions": {
"adm-zip": "patch:adm-zip@npm%3A0.5.16#~/.yarn/patches/adm-zip-npm-0.5.16-4556fea098.patch",
"axios": "1.14.0",
"minimatch@10.2.1": "10.2.5",
"minimatch@3.1.2": "3.1.5",
"minimatch@>=10.0.0 <11.0.0": "10.2.5",
"minimatch@>=3.0.0 <4.0.0": "3.1.5",
"minimatch@>=5.0.0 <6.0.0": "5.1.9",
"minimatch@>=9.0.0 <10.0.0": "9.0.9",
"rollup": "4.40.1",
"socks": ">=2.8.3"
},
"lint-staged": {
"packages/*/{package.json,tsconfig.json}": [
"ts-node ./bin/validate-publish-imports.ts --fix",
"yarn sync:tsconfig-references"
],
"*.{js,mjs,jsx,ts,tsx,md,json,yml,yaml}": [
"oxfmt --no-error-on-unmatched-pattern"
],
@@ -113,5 +120,5 @@
"node": ">=22",
"yarn": "^4.9.1"
},
"packageManager": "yarn@4.10.3"
"packageManager": "yarn@4.13.0"
}

View File

@@ -3,3 +3,7 @@ npm install @actual-app/api
```
View docs here: https://actualbudget.org/docs/api/
## TypeScript
`@actual-app/api` publishes TypeScript declarations. Consumers using TypeScript must set `moduleResolution` to `"bundler"`, `"nodenext"`, or `"node16"` in their `tsconfig.json`. Legacy `"node"` / `"node10"` / `"classic"` resolution is not supported in strict mode — the published declarations rely on package.json `exports` conditions that older resolvers don't honor.

View File

@@ -1,5 +1,5 @@
class Query {
/** @type {import('loot-core/shared/query').QueryState} */
/** @type {import('@actual-app/core/shared/query').QueryState} */
state;
constructor(state) {

View File

@@ -1,8 +1,3 @@
import type {
RequestInfo as FetchInfo,
RequestInit as FetchInit,
} from 'node-fetch';
import { init as initLootCore } from '@actual-app/core/server/main';
import type { InitConfig, lib } from '@actual-app/core/server/main';
@@ -17,14 +12,6 @@ export let internal: typeof lib | null = null;
export async function init(config: InitConfig = {}) {
validateNodeVersion();
if (!globalThis.fetch) {
globalThis.fetch = (url: URL | RequestInfo, init?: RequestInit) => {
return import('node-fetch').then(({ default: fetch }) =>
fetch(url as unknown as FetchInfo, init as unknown as FetchInit),
) as unknown as Promise<Response>;
};
}
internal = await initLootCore(config);
return internal;
}

View File

@@ -1,9 +1,8 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import { vi } from 'vitest';
import type { RuleEntity } from '@actual-app/core/types/models';
import { vi } from 'vitest';
import * as api from './index';

1
packages/api/models.ts Normal file
View File

@@ -0,0 +1 @@
export type * from '@actual-app/core/server/api-models';

View File

@@ -1,8 +1,13 @@
{
"name": "@actual-app/api",
"version": "26.4.0",
"version": "26.5.0",
"description": "An API for Actual",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/actualbudget/actual.git",
"directory": "packages/api"
},
"files": [
"@types",
"dist"
@@ -11,8 +16,14 @@
"types": "@types/index.d.ts",
"exports": {
".": {
"types": "./@types/index.d.ts",
"development": "./index.ts",
"default": "./dist/index.js"
},
"./models": {
"types": "./@types/models.d.ts",
"development": "./models.ts",
"default": "./dist/models.js"
}
},
"publishConfig": {
@@ -20,30 +31,31 @@
".": {
"types": "./@types/index.d.ts",
"default": "./dist/index.js"
},
"./models": {
"types": "./@types/models.d.ts",
"default": "./dist/models.js"
}
}
},
"scripts": {
"build": "vite build",
"build": "vite build && tsgo --emitDeclarationOnly",
"test": "vitest --run",
"typecheck": "tsgo -b && tsc-strict"
},
"dependencies": {
"@actual-app/core": "workspace:*",
"@actual-app/crdt": "workspace:*",
"better-sqlite3": "^12.6.2",
"compare-versions": "^6.1.1",
"node-fetch": "^3.3.2",
"uuid": "^13.0.0"
"better-sqlite3": "^12.8.0",
"compare-versions": "^6.1.1"
},
"devDependencies": {
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
"rollup-plugin-visualizer": "^6.0.11",
"@typescript/native-preview": "beta",
"rollup-plugin-visualizer": "^7.0.1",
"typescript-strict-plugin": "^2.4.4",
"vite": "^8.0.0",
"vite-plugin-dts": "^4.5.4",
"vite": "^8.0.5",
"vite-plugin-peggy-loader": "^2.0.1",
"vitest": "^4.1.0"
"vitest": "^4.1.2"
},
"engines": {
"node": ">=20"

View File

@@ -7,6 +7,7 @@
"target": "ES2021",
"module": "es2022",
"moduleResolution": "bundler",
"customConditions": ["api"],
"noEmit": false,
"declaration": true,
"declarationMap": true,
@@ -14,9 +15,28 @@
"rootDir": ".",
"declarationDir": "@types",
"tsBuildInfoFile": "dist/.tsbuildinfo",
"plugins": [{ "name": "typescript-strict-plugin", "paths": ["."] }]
"plugins": [
{
"name": "typescript-strict-plugin",
"paths": ["."]
}
]
},
"references": [{ "path": "../crdt" }, { "path": "../loot-core" }],
"references": [
{
"path": "../loot-core"
},
{
"path": "../crdt"
}
],
"include": ["."],
"exclude": ["**/node_modules/*", "dist", "@types", "*.test.ts", "*.config.ts"]
"exclude": [
"**/node_modules/*",
"dist",
"@types",
"*.test.ts",
"*.config.ts",
"*.config.mts"
]
}

View File

@@ -3,7 +3,6 @@ import path from 'path';
import { visualizer } from 'rollup-plugin-visualizer';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import peggyLoader from 'vite-plugin-peggy-loader';
const lootCoreRoot = path.resolve(__dirname, '../loot-core');
@@ -55,7 +54,11 @@ function copyMigrationsAndDefaultDb() {
}
export default defineConfig({
ssr: { noExternal: true, external: ['better-sqlite3'] },
ssr: {
noExternal: true,
external: ['better-sqlite3'],
resolve: { conditions: ['api'] },
},
build: {
ssr: true,
target: 'node20',
@@ -63,24 +66,22 @@ export default defineConfig({
emptyOutDir: true,
sourcemap: true,
lib: {
entry: path.resolve(__dirname, 'index.ts'),
entry: {
index: path.resolve(__dirname, 'index.ts'),
models: path.resolve(__dirname, 'models.ts'),
},
formats: ['cjs'],
fileName: () => 'index.js',
fileName: (_format, entryName) => `${entryName}.js`,
},
},
plugins: [
cleanOutputDirs(),
peggyLoader(),
dts({
tsconfigPath: path.resolve(__dirname, 'tsconfig.json'),
outDir: path.resolve(__dirname, '@types'),
rollupTypes: true,
}),
copyMigrationsAndDefaultDb(),
visualizer({ template: 'raw-data', filename: 'app/stats.json' }),
],
resolve: {
extensions: ['.api.ts', '.js', '.ts', '.tsx', '.json'],
conditions: ['api'],
},
test: {
globals: true,

View File

@@ -0,0 +1,68 @@
import * as fs from 'node:fs';
import matter from 'gray-matter';
import {
categoryAutocorrections,
categoryOrder,
} from '../src/release-notes/util.mjs';
console.log('Looking in ' + fs.realpathSync('upcoming-release-notes'));
const expectedPath = `upcoming-release-notes/${process.env.PR_NUMBER}.md`;
function reportError(message) {
console.log(`::error::${message}`);
process.stdout.write('::notice::');
fs.createReadStream('upcoming-release-notes/README.md').pipe(process.stdout);
fs.createReadStream('upcoming-release-notes/README.md')
.pipe(fs.createWriteStream(process.env.GITHUB_STEP_SUMMARY))
.on('close', () => {
process.exit(1);
});
}
(() => {
if (!fs.existsSync(expectedPath)) {
reportError(`Release note file ${expectedPath} not found`);
return;
}
const { data, content } = matter(fs.readFileSync(expectedPath, 'utf-8'));
if (!data.category) {
reportError(`Release note is missing a category.`);
return;
}
if (categoryAutocorrections[data.category]) {
data.category = categoryAutocorrections[data.category];
}
if (!categoryOrder.includes(data.category)) {
reportError(
`Release note category "${data.category}" is not one of ${categoryOrder
.map(JSON.stringify)
.join(', ')}`,
);
return;
}
if (!data.authors) {
reportError(`Release note is missing authors.`);
return;
}
if (!Array.isArray(data.authors)) {
reportError(`Release note authors should be a list.`);
return;
}
if (content.trim().split('\n').length !== 1) {
reportError(
`Release note file ${expectedPath} body should contain exactly one line`,
);
return;
}
console.log('Everything looks good! \u{1f389}');
})();

View File

@@ -0,0 +1,312 @@
import * as childProcess from 'node:child_process';
import * as fs from 'node:fs/promises';
import { join } from 'node:path';
import { inspect, promisify } from 'node:util';
import matter from 'gray-matter';
import listify from 'listify';
import {
categoryAutocorrections,
categoryOrder,
} from '../src/release-notes/util.mjs';
const exec = promisify(childProcess.exec);
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');
const apiResult = await fetch('https://api.github.com/graphql', {
method: 'POST',
headers: {
Authorization: `bearer ${process.env.GITHUB_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: /* GraphQL */ `
query GetPRMetadata(
$name: String!
$owner: String!
$headRefName: String!
) {
repository(name: $name, owner: $owner) {
pullRequests(headRefName: $headRefName, first: 1) {
edges {
node {
number
headRefName
body
}
}
}
}
}
`,
variables: {
name: repo,
owner,
headRefName: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME,
},
}),
}).then(res => res.json());
await collapsedLog('API Response', apiResult);
const prData = apiResult.data.repository.pullRequests.edges[0].node;
const version = prData.headRefName.split('/')[1].replace(/^v/, '');
const slug = version.replace(/\./g, '-');
const author = process.env.GITHUB_ACTOR || 'TODO';
const commitMessage = `Generate release notes for v${version}`;
const releaseDateMatch = (prData.body || '').match(
/<!-- release-date:(\d{4}-\d{2}-\d{2}) -->/,
);
const releaseDate = releaseDateMatch ? releaseDateMatch[1] : 'TODO';
const botName = 'github-actions[bot]';
const botEmail = '41898282+github-actions[bot]@users.noreply.github.com';
await exec(`git config user.name '${botName}'`);
await exec(`git config user.email '${botEmail}'`);
const AUTOGEN_MARKER = '<!-- release-notes:auto-generated -->';
await group('Prepare branch', async () => {
if (process.env.GITHUB_HEAD_REF) {
await exec(`git fetch origin ${process.env.GITHUB_HEAD_REF}`, {
stdio: 'inherit',
});
await exec(`git checkout ${process.env.GITHUB_HEAD_REF}`, {
stdio: 'inherit',
});
}
// recover deleted release note files from previous generation commits
const baseRef = process.env.GITHUB_BASE_REF || 'master';
await exec(`git fetch origin ${baseRef}`, { stdio: 'inherit' });
const { stdout: mergeBase } = await exec(
`git merge-base HEAD origin/${baseRef}`,
);
const base = mergeBase.trim();
const { stdout: genLog } = await exec(
`git log --grep='${commitMessage}' --format=%H ${base}..HEAD`,
);
const genCommits = genLog.split('\n').filter(Boolean);
console.log(
`Reversing upcoming-release-notes deletions from ${genCommits.length} prior generation commit(s)`,
);
const tmpDir = process.env.RUNNER_TEMP || '/tmp';
for (const sha of genCommits) {
const patchPath = join(tmpDir, `revert-${sha}.patch`);
try {
await exec(
`git diff --diff-filter=D ${sha}~1..${sha} -- upcoming-release-notes > ${patchPath}`,
);
const { size } = await fs.stat(patchPath);
if (size > 0) {
await exec(`git apply -R --3way ${patchPath}`, { stdio: 'inherit' });
}
} finally {
await fs.unlink(patchPath).catch(() => undefined);
}
}
});
const { notesByCategory, files } = await parseReleaseNotes(
'upcoming-release-notes',
);
const categorizedNotes = formatNotes(notesByCategory);
await collapsedLog('Release Notes', categorizedNotes);
if (files.length === 0) {
console.log('No release notes found, nothing to generate');
process.exit(0);
}
const highlights = '- TODO: Add release highlights';
const blogPath = join(
'packages/docs/blog',
`${releaseDate}-release-${slug}.md`,
);
const releasesPath = 'packages/docs/docs/releases.md';
await group('Generate blog post', async () => {
const template = `---
title: Release ${version}
description: New release of Actual.
date: ${releaseDate}T10:00
slug: release-${version}
tags: [announcement, release]
hide_table_of_contents: false
authors: ${author}
---
${highlights}
<!--truncate-->
**Docker Tag: ${version}**
${AUTOGEN_MARKER}
${categorizedNotes}
`;
let blogContent;
try {
const existing = await fs.readFile(blogPath, 'utf-8');
const idx = existing.indexOf(AUTOGEN_MARKER);
if (idx === -1) {
console.log(
`WARNING: ${blogPath} missing ${AUTOGEN_MARKER}, rewriting from template`,
);
blogContent = template;
} else {
blogContent =
existing.slice(0, idx + AUTOGEN_MARKER.length) +
'\n' +
categorizedNotes +
'\n';
}
} catch (e) {
if (e.code !== 'ENOENT') throw e;
blogContent = template;
}
await fs.writeFile(blogPath, blogContent);
console.log(`Wrote ${blogPath}`);
});
await group('Update releases.md', async () => {
const existing = await fs.readFile(releasesPath, 'utf-8');
const sectionRe = new RegExp(
`(^|\\n)## ${escapeRegExp(version)}\\n[\\s\\S]*?(?=\\n## |$)`,
);
const match = existing.match(sectionRe);
let updated;
if (match) {
const section = match[0];
const idx = section.indexOf(AUTOGEN_MARKER);
if (idx === -1) {
console.log(
`WARNING: section for ${version} in ${releasesPath} missing ${AUTOGEN_MARKER}, leaving as-is`,
);
updated = existing;
} else {
const newSection =
section.slice(0, idx + AUTOGEN_MARKER.length) + '\n' + categorizedNotes;
updated = existing.replace(section, newSection);
}
} else {
const newSection = `## ${version}
Release date: ${releaseDate}
${highlights}
**Docker Tag: ${version}**
${AUTOGEN_MARKER}
${categorizedNotes}`;
updated = existing.replace(
'# Release Notes\n',
`# Release Notes\n\n${newSection}\n`,
);
}
await fs.writeFile(releasesPath, updated);
console.log(`Updated ${releasesPath}`);
});
await group('Remove used release notes', async () => {
await Promise.all(
files.map(f => fs.unlink(join('upcoming-release-notes', f))),
);
});
await group('Format generated files', async () => {
await exec(`yarn exec oxfmt ${blogPath} ${releasesPath}`, {
stdio: 'inherit',
});
});
await group('Commit and push', async () => {
await exec(
'git add upcoming-release-notes packages/docs/blog packages/docs/docs/releases.md',
{ stdio: 'inherit' },
);
try {
await exec('git diff --cached --quiet');
console.log('No changes to commit');
return;
} catch {
// there are staged changes
}
await exec(`git commit -m '${commitMessage}'`);
await exec('git push origin', { stdio: 'inherit' });
});
async function parseReleaseNotes(dir) {
const files = (await fs.readdir(dir)).filter(f => f.match(/^\d+\.md$/));
const notes = files.map(async name => {
const content = await fs.readFile(join(dir, name), 'utf-8');
const { data, content: body } = matter(content);
const number = name.replace('.md', '');
const authors = listify(
data.authors.map(a => `@${a}`),
{ finalWord: '&' },
);
return {
category: categoryAutocorrections[data.category] ?? data.category,
value: `- [#${number}](https://github.com/actualbudget/${repo}/pull/${number}) ${body.trim()} — thanks ${authors}`,
};
});
const notesByCategory = (await Promise.all(notes)).reduce(
(acc, note) => {
if (!acc[note.category]) {
console.log(`WARNING: Unrecognized category "${note.category}"`);
acc[note.category] = [];
}
acc[note.category].push(note.value);
return acc;
},
Object.fromEntries(categoryOrder.map(c => [c, []])),
);
return { notesByCategory, files };
}
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function formatNotes(notes) {
return Object.entries(notes)
.filter(([_, values]) => values.length > 0)
.map(([category, values]) => `#### ${category}\n\n${values.join('\n')}`)
.join('\n\n');
}
async function collapsedLog(name, value) {
await group(name, () => {
if (typeof value === 'string') {
console.log(value);
} else {
console.log(inspect(value, { depth: null }));
}
});
}
async function group(name, cb) {
console.log(`::group::${name}`);
await cb();
console.log('::endgroup::');
}

View File

@@ -8,9 +8,12 @@
"typecheck": "tsgo -b"
},
"devDependencies": {
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
"@octokit/rest": "^22.0.1",
"@typescript/native-preview": "beta",
"extensionless": "^2.0.6",
"vitest": "^4.1.0"
"gray-matter": "^4.0.3",
"listify": "^1.0.3",
"vitest": "^4.1.2"
},
"extensionless": {
"lookFor": [

View File

@@ -0,0 +1,12 @@
export const categoryAutocorrections = {
Feature: 'Features',
Enhancement: 'Enhancements',
Bugfix: 'Bugfixes',
};
export const categoryOrder = [
'Features',
'Enhancements',
'Bugfixes',
'Maintenance',
];

View File

@@ -60,7 +60,7 @@ function resolveType(
currentDate.getFullYear() === 2000 + versionYear &&
currentDate.getMonth() + 1 === versionMonth;
if (inPatchMonth && currentDate.getDate() <= 25) {
if (inPatchMonth && currentDate.getDate() < 25) {
return 'hotfix';
}

View File

@@ -98,8 +98,8 @@ Run `actual <command> --help` for subcommands and options.
### Examples
```bash
# List all accounts (as a table)
actual accounts list --format table
# List all accounts (as a table; excludes closed by default)
actual accounts list [--include-closed] --format table
# Find an entity ID by name
actual server get-id --type accounts --name "Checking"
@@ -122,13 +122,35 @@ actual query run --table transactions \
### Amount Convention
All monetary amounts are **integer cents**:
All monetary amounts are **integer cents** when passed as input (flags, JSON):
| CLI Value | Dollar Amount |
| --------- | ------------- |
| `5000` | $50.00 |
| `-12350` | -$123.50 |
**Output formatting:** Table (`--format table`) and CSV (`--format csv`) output automatically converts cent values to decimal (e.g. `1665.00` instead of `166500`). JSON output always returns raw cents for programmatic use.
### Tips & Common Pitfalls
- **Split transactions:** When summing or counting transactions, filter `"is_parent": false` to avoid double-counting. A split parent holds the total amount, and its children hold the individual parts — including both would count the total twice.
- **Avoid rapid sequential requests:** Each CLI invocation opens a new server connection. Running queries in a tight loop (e.g. one per month) may trigger rate limiting or authentication failures. Instead, fetch all data in a single query with a date range filter and process locally:
```bash
# Good: single query for the full year
actual query run --table transactions \
--filter '{"$and":[{"date":{"$gte":"2025-01-01"}},{"date":{"$lte":"2025-12-31"}}]}' \
--limit 5000
# Bad: one query per month in a loop (may fail with auth errors)
for month in 01 02 03 ...; do actual query run ...; done
```
- **Uncategorized transactions:** `category.name` is `null` for transactions without a category. Account for this when filtering or grouping by category.
- **No date sub-fields in AQL:** `date.month`, `date.year`, etc. are not supported as query fields. To group by month, fetch raw transactions with a date range filter and aggregate locally in a script.
## Running Locally (Development)
If you're working on the CLI within the monorepo:

View File

@@ -1,8 +1,13 @@
{
"name": "@actual-app/cli",
"version": "26.4.0",
"version": "26.5.0",
"description": "CLI for Actual Budget",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/actualbudget/actual.git",
"directory": "packages/cli"
},
"bin": {
"actual": "./dist/cli.js",
"actual-cli": "./dist/cli.js"
@@ -11,6 +16,14 @@
"dist"
],
"type": "module",
"imports": {
"#commands/*": "./src/commands/*.ts",
"#config": "./src/config.ts",
"#connection": "./src/connection.ts",
"#input": "./src/input.ts",
"#output": "./src/output.ts",
"#utils": "./src/utils.ts"
},
"scripts": {
"build": "vite build",
"test": "vitest --run",
@@ -19,15 +32,15 @@
"dependencies": {
"@actual-app/api": "workspace:*",
"cli-table3": "^0.6.5",
"commander": "^13.0.0",
"cosmiconfig": "^9.0.0"
"commander": "^14.0.3",
"cosmiconfig": "^9.0.1"
},
"devDependencies": {
"@types/node": "^22.19.15",
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
"rollup-plugin-visualizer": "^6.0.11",
"vite": "^8.0.0",
"vitest": "^4.1.0"
"@types/node": "^22.19.17",
"@typescript/native-preview": "beta",
"rollup-plugin-visualizer": "^7.0.1",
"vite": "^8.0.5",
"vitest": "^4.1.2"
},
"engines": {
"node": ">=22"

View File

@@ -1,7 +1,7 @@
import * as api from '@actual-app/api';
import { Command } from 'commander';
import { printOutput } from '../output';
import { printOutput } from '#output';
import { registerAccountsCommand } from './accounts';
@@ -15,11 +15,11 @@ vi.mock('@actual-app/api', () => ({
getAccountBalance: vi.fn().mockResolvedValue(10000),
}));
vi.mock('../connection', () => ({
vi.mock('#connection', () => ({
withConnection: vi.fn((_opts, fn) => fn()),
}));
vi.mock('../output', () => ({
vi.mock('#output', () => ({
printOutput: vi.fn(),
}));
@@ -62,14 +62,28 @@ describe('accounts commands', () => {
});
describe('list', () => {
it('calls api.getAccounts and prints result', async () => {
const accounts = [{ id: '1', name: 'Checking' }];
it('calls api.getAccounts and prints result with computed balance', async () => {
const accounts = [
{ id: '1', name: 'Checking', offbudget: false, closed: false },
];
vi.mocked(api.getAccounts).mockResolvedValue(accounts);
await run(['accounts', 'list']);
expect(api.getAccounts).toHaveBeenCalled();
expect(printOutput).toHaveBeenCalledWith(accounts, undefined);
expect(api.getAccountBalance).toHaveBeenCalledWith('1');
expect(printOutput).toHaveBeenCalledWith(
[
{
id: '1',
name: 'Checking',
offbudget: false,
closed: false,
balance: 10000,
},
],
undefined,
);
});
it('passes format option to printOutput', async () => {
@@ -79,6 +93,59 @@ describe('accounts commands', () => {
expect(printOutput).toHaveBeenCalledWith([], 'csv');
});
it('filters out closed accounts by default', async () => {
vi.mocked(api.getAccounts).mockResolvedValue([
{ id: '1', name: 'Open', offbudget: false, closed: false },
{ id: '2', name: 'Closed', offbudget: false, closed: true },
]);
await run(['accounts', 'list']);
expect(printOutput).toHaveBeenCalledWith(
[
{
id: '1',
name: 'Open',
offbudget: false,
closed: false,
balance: 10000,
},
],
undefined,
);
});
it('includes closed accounts when --include-closed is passed', async () => {
vi.mocked(api.getAccounts).mockResolvedValue([
{ id: '1', name: 'Open', offbudget: false, closed: false },
{ id: '2', name: 'Closed', offbudget: false, closed: true },
]);
await run(['accounts', 'list', '--include-closed']);
expect(printOutput).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({ id: '2', closed: true }),
]),
undefined,
);
});
it('sorts on-budget accounts before off-budget', async () => {
vi.mocked(api.getAccounts).mockResolvedValue([
{ id: '1', name: 'OffBudget', offbudget: true, closed: false },
{ id: '2', name: 'OnBudget', offbudget: false, closed: false },
]);
await run(['accounts', 'list']);
const output = vi.mocked(printOutput).mock.calls[0][0] as Array<{
id: string;
}>;
expect(output[0].id).toBe('2'); // on-budget first
expect(output[1].id).toBe('1'); // off-budget second
});
});
describe('create', () => {

View File

@@ -1,9 +1,9 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { printOutput } from '../output';
import { parseBoolFlag, parseIntFlag } from '../utils';
import { withConnection } from '#connection';
import { printOutput } from '#output';
import { parseBoolFlag, parseIntFlag } from '#utils';
export function registerAccountsCommand(program: Command) {
const accounts = program.command('accounts').description('Manage accounts');
@@ -11,11 +11,28 @@ export function registerAccountsCommand(program: Command) {
accounts
.command('list')
.description('List all accounts')
.action(async () => {
.option('--include-closed', 'Include closed accounts', false)
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getAccounts();
printOutput(result, opts.format);
const allAccounts = await api.getAccounts();
const accounts = allAccounts.filter(
a => cmdOpts.includeClosed || !a.closed,
);
// Stable sort: on-budget first, off-budget second
// (preserves API sort_order within each group)
accounts.sort((a, b) => Number(a.offbudget) - Number(b.offbudget));
const balances = await Promise.all(
accounts.map(a => api.getAccountBalance(a.id)),
);
const output = accounts.map((a, i) => ({
id: a.id,
name: a.name,
offbudget: a.offbudget,
closed: a.closed,
balance: balances[i],
}));
printOutput(output, opts.format);
});
});
@@ -24,7 +41,11 @@ export function registerAccountsCommand(program: Command) {
.description('Create a new account')
.requiredOption('--name <name>', 'Account name')
.option('--offbudget', 'Create as off-budget account', false)
.option('--balance <amount>', 'Initial balance in cents', '0')
.option(
'--balance <amount>',
'Initial balance in cents (e.g. 50000 = 500.00)',
'0',
)
.action(async cmdOpts => {
const balance = parseIntFlag(cmdOpts.balance, '--balance');
const opts = program.opts();

View File

@@ -1,10 +1,10 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { resolveConfig } from '../config';
import { withConnection } from '../connection';
import { printOutput } from '../output';
import { parseBoolFlag, parseIntFlag } from '../utils';
import { resolveConfig } from '#config';
import { withConnection } from '#connection';
import { printOutput } from '#output';
import { parseBoolFlag, parseIntFlag } from '#utils';
export function registerBudgetsCommand(program: Command) {
const budgets = program.command('budgets').description('Manage budgets');
@@ -82,7 +82,10 @@ export function registerBudgetsCommand(program: Command) {
.description('Set budget amount for a category in a month')
.requiredOption('--month <month>', 'Budget month (YYYY-MM)')
.requiredOption('--category <id>', 'Category ID')
.requiredOption('--amount <amount>', 'Amount in cents')
.requiredOption(
'--amount <amount>',
'Amount in cents (e.g. 50000 = 500.00)',
)
.action(async cmdOpts => {
const amount = parseIntFlag(cmdOpts.amount, '--amount');
const opts = program.opts();
@@ -111,7 +114,10 @@ export function registerBudgetsCommand(program: Command) {
.command('hold-next-month')
.description('Hold budget amount for next month')
.requiredOption('--month <month>', 'Budget month (YYYY-MM)')
.requiredOption('--amount <amount>', 'Amount in cents')
.requiredOption(
'--amount <amount>',
'Amount in cents (e.g. 50000 = 500.00)',
)
.action(async cmdOpts => {
const parsedAmount = parseIntFlag(cmdOpts.amount, '--amount');
const opts = program.opts();

View File

@@ -1,9 +1,9 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { printOutput } from '../output';
import { parseBoolFlag } from '../utils';
import { withConnection } from '#connection';
import { printOutput } from '#output';
import { parseBoolFlag } from '#utils';
export function registerCategoriesCommand(program: Command) {
const categories = program

View File

@@ -1,9 +1,9 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { printOutput } from '../output';
import { parseBoolFlag } from '../utils';
import { withConnection } from '#connection';
import { printOutput } from '#output';
import { parseBoolFlag } from '#utils';
export function registerCategoryGroupsCommand(program: Command) {
const groups = program

View File

@@ -1,8 +1,8 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { printOutput } from '../output';
import { withConnection } from '#connection';
import { printOutput } from '#output';
export function registerPayeesCommand(program: Command) {
const payees = program.command('payees').description('Manage payees');

View File

@@ -1,7 +1,7 @@
import * as api from '@actual-app/api';
import { Command } from 'commander';
import { printOutput } from '../output';
import { printOutput } from '#output';
import { parseOrderBy, registerQueryCommand } from './query';
@@ -21,11 +21,11 @@ vi.mock('@actual-app/api', () => {
};
});
vi.mock('../connection', () => ({
vi.mock('#connection', () => ({
withConnection: vi.fn((_opts, fn) => fn()),
}));
vi.mock('../output', () => ({
vi.mock('#output', () => ({
printOutput: vi.fn(),
}));
@@ -145,6 +145,25 @@ describe('query commands', () => {
]);
});
it('outputs unwrapped data array (not the full result envelope)', async () => {
const mockData = [{ id: '1', amount: -500 }];
vi.mocked(api.aqlQuery).mockResolvedValueOnce({
data: mockData,
dependencies: [],
});
await run([
'query',
'run',
'--table',
'transactions',
'--select',
'id,amount',
]);
expect(printOutput).toHaveBeenCalledWith(mockData, undefined);
});
it('passes --filter as JSON', async () => {
await run([
'query',

View File

@@ -1,14 +1,10 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { readJsonInput } from '../input';
import { printOutput } from '../output';
import { parseIntFlag } from '../utils';
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
import { withConnection } from '#connection';
import { readJsonInput } from '#input';
import { printOutput } from '#output';
import { isRecord, parseIntFlag } from '#utils';
/**
* Parse order-by strings like "date:desc,amount:asc,id" into
@@ -253,7 +249,17 @@ Available tables: ${AVAILABLE_TABLES}
Use "actual query tables" and "actual query fields <table>" for schema info.
Common filter operators: $eq, $ne, $lt, $lte, $gt, $gte, $like, $and, $or
See ActualQL docs for full reference: https://actualbudget.org/docs/api/actual-ql/`;
See ActualQL docs for full reference: https://actualbudget.org/docs/api/actual-ql/
Tips:
- Amounts are stored as integer cents (e.g. 166500 = 1665.00).
Table and CSV output auto-formats these as decimals; JSON keeps raw cents.
- Filter "is_parent": false to avoid double-counting split transactions.
- Fetch all data in a single query with a date range instead of running
one query per month — rapid sequential requests may cause auth failures.
- date.month, date.year etc. are not supported as fields in AQL.
To group by month, fetch raw transactions with a date range filter
and aggregate locally (e.g. in a script).`;
export function registerQueryCommand(program: Command) {
const query = program
@@ -306,10 +312,14 @@ export function registerQueryCommand(program: Command) {
const result = await api.aqlQuery(queryObj);
if (!isRecord(result) || !('data' in result)) {
throw new Error('Query result missing data');
}
if (cmdOpts.count) {
printOutput({ count: result.data }, opts.format);
} else {
printOutput(result, opts.format);
printOutput(result.data, opts.format);
}
});
});

View File

@@ -1,9 +1,9 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { readJsonInput } from '../input';
import { printOutput } from '../output';
import { withConnection } from '#connection';
import { readJsonInput } from '#input';
import { printOutput } from '#output';
export function registerRulesCommand(program: Command) {
const rules = program

View File

@@ -1,9 +1,9 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { readJsonInput } from '../input';
import { printOutput } from '../output';
import { withConnection } from '#connection';
import { readJsonInput } from '#input';
import { printOutput } from '#output';
export function registerSchedulesCommand(program: Command) {
const schedules = program

View File

@@ -2,8 +2,8 @@ import * as api from '@actual-app/api';
import { Option } from 'commander';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { printOutput } from '../output';
import { withConnection } from '#connection';
import { printOutput } from '#output';
export function registerServerCommand(program: Command) {
const server = program.command('server').description('Server utilities');

View File

@@ -1,8 +1,8 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { printOutput } from '../output';
import { withConnection } from '#connection';
import { printOutput } from '#output';
export function registerTagsCommand(program: Command) {
const tags = program.command('tags').description('Manage tags');

View File

@@ -1,9 +1,9 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { readJsonInput } from '../input';
import { printOutput } from '../output';
import { withConnection } from '#connection';
import { readJsonInput } from '#input';
import { printOutput } from '#output';
export function registerTransactionsCommand(program: Command) {
const transactions = program

View File

@@ -3,6 +3,8 @@ import { join } from 'path';
import { cosmiconfig } from 'cosmiconfig';
import { isRecord } from './utils';
export type CliConfig = {
serverUrl: string;
password?: string;
@@ -41,10 +43,6 @@ const configFileKeys: readonly string[] = [
'encryptionPassword',
];
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function validateConfigFileContent(value: unknown): ConfigFileContent {
if (!isRecord(value)) {
throw new Error(

View File

@@ -60,6 +60,33 @@ describe('formatOutput', () => {
expect(result).toContain('a');
expect(result).toContain('b');
});
it('formats amount fields as decimal values', () => {
const data = [{ name: 'Groceries', amount: -250000 }];
const result = formatOutput(data, 'table');
expect(result).toContain('-2500.00');
expect(result).not.toContain('-250000');
});
it('formats balance fields as decimal values', () => {
const data = [{ id: 'acc1', balance: 166500 }];
const result = formatOutput(data, 'table');
expect(result).toContain('1665.00');
});
it('formats budgeted and spent fields as decimal values', () => {
const data = [{ budgeted: 50000, spent: -32150 }];
const result = formatOutput(data, 'table');
expect(result).toContain('500.00');
expect(result).toContain('-321.50');
});
it('does not format non-amount numeric fields', () => {
const data = [{ id: 12345, sort_order: 100 }];
const result = formatOutput(data, 'table');
expect(result).toContain('12345');
expect(result).toContain('100');
});
});
describe('csv', () => {
@@ -112,6 +139,21 @@ describe('formatOutput', () => {
const lines = result.split('\n');
expect(lines[0]).toBe('a,b');
});
it('formats amount fields as decimal values', () => {
const data = [{ name: 'Coffee', amount: -2500 }];
const result = formatOutput(data, 'csv');
const lines = result.split('\n');
expect(lines[0]).toBe('name,amount');
expect(lines[1]).toBe('Coffee,-25.00');
});
it('does not format amount fields in json output', () => {
const data = [{ amount: 166500 }];
const result = formatOutput(data, 'json');
expect(result).toContain('166500');
expect(result).not.toContain('1665.00');
});
});
});

View File

@@ -2,6 +2,29 @@ import Table from 'cli-table3';
export type OutputFormat = 'json' | 'table' | 'csv';
// Fields containing integer-cent values, auto-formatted as decimals in table/csv output.
const AMOUNT_FIELDS = new Set([
'amount',
'balance',
'balance_available',
'balance_current',
'balance_limit',
'budgeted',
'spent',
'carryover',
]);
function isAmountValue(key: string, value: unknown): value is number {
return AMOUNT_FIELDS.has(key) && typeof value === 'number';
}
function formatCellValue(key: string, value: unknown): string {
if (isAmountValue(key, value)) {
return (value / 100).toFixed(2);
}
return String(value ?? '');
}
export function formatOutput(
data: unknown,
format: OutputFormat = 'json',
@@ -23,7 +46,7 @@ function formatTable(data: unknown): string {
if (data && typeof data === 'object') {
const table = new Table();
for (const [key, value] of Object.entries(data)) {
table.push({ [key]: String(value) });
table.push({ [key]: formatCellValue(key, value) });
}
return table.toString();
}
@@ -39,7 +62,7 @@ function formatTable(data: unknown): string {
for (const row of data) {
const r = row as Record<string, unknown>;
table.push(keys.map(k => String(r[k] ?? '')));
table.push(keys.map(k => formatCellValue(k, r[k])));
}
return table.toString();
@@ -50,7 +73,9 @@ function formatCsv(data: unknown): string {
if (data && typeof data === 'object') {
const entries = Object.entries(data);
const header = entries.map(([k]) => escapeCsv(k)).join(',');
const values = entries.map(([, v]) => escapeCsv(String(v))).join(',');
const values = entries
.map(([k, v]) => escapeCsv(formatCellValue(k, v)))
.join(',');
return header + '\n' + values;
}
return String(data);
@@ -64,7 +89,7 @@ function formatCsv(data: unknown): string {
const header = keys.map(k => escapeCsv(k)).join(',');
const rows = data.map(row => {
const r = row as Record<string, unknown>;
return keys.map(k => escapeCsv(String(r[k] ?? ''))).join(',');
return keys.map(k => escapeCsv(formatCellValue(k, r[k]))).join(',');
});
return [header, ...rows].join('\n');

View File

@@ -1,3 +1,7 @@
export function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
export function parseBoolFlag(value: string, flagName: string): boolean {
if (value !== 'true' && value !== 'false') {
throw new Error(

View File

@@ -45,6 +45,27 @@
-->
<style>
/* Show logo icon next to brand title text */
.sidebar-header a[href='https://actualbudget.org'] {
display: flex;
align-items: center;
gap: 8px;
text-decoration: none;
font-weight: 700;
font-size: 16px;
}
.sidebar-header a[href='https://actualbudget.org']::before {
content: '';
display: inline-block;
width: 32px;
height: 32px;
min-width: 32px;
background-image: url('https://actualbudget.org/img/logo.webp');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
#storybook-explorer-searchfield {
font-weight: 400 !important;
font-size: 14px !important;

View File

@@ -16,7 +16,6 @@ const theme = create({
base: 'light',
brandTitle: 'Actual Budget',
brandUrl: 'https://actualbudget.org',
brandImage: 'https://actualbudget.org/img/actual.webp',
brandTarget: '_blank',
// UI colors
@@ -32,7 +31,7 @@ const theme = create({
// Fonts
fontBase:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
'"Inter Variable", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
fontCode: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace',
// Text colors

View File

@@ -4,8 +4,11 @@ import type { Preview } from '@storybook/react-vite';
// Not ideal to import from desktop-client, but we need a source of truth for theme variables
// TODO: this needs refactoring
// oxlint-disable-next-line actual/enforce-boundaries
import * as darkTheme from '../../desktop-client/src/style/themes/dark';
// oxlint-disable-next-line actual/enforce-boundaries
import * as lightTheme from '../../desktop-client/src/style/themes/light';
// oxlint-disable-next-line actual/enforce-boundaries
import * as midnightTheme from '../../desktop-client/src/style/themes/midnight';
const THEMES = {

View File

@@ -2,6 +2,9 @@
"name": "@actual-app/components",
"version": "0.0.1",
"license": "MIT",
"imports": {
"#tokens": "./src/tokens.ts"
},
"exports": {
"./hooks/*": "./src/hooks/*.ts",
"./icons/logo": "./src/icons/logo/index.ts",
@@ -44,24 +47,25 @@
},
"dependencies": {
"@emotion/css": "^11.13.5",
"react-aria-components": "^1.15.1",
"react-aria-components": "^1.16.0",
"usehooks-ts": "^3.1.1"
},
"devDependencies": {
"@chromatic-com/storybook": "^5.0.1",
"@storybook/addon-a11y": "^10.2.16",
"@storybook/addon-docs": "^10.2.16",
"@storybook/react-vite": "^10.2.16",
"@chromatic-com/storybook": "^5.1.1",
"@storybook/addon-a11y": "^10.3.4",
"@storybook/addon-docs": "^10.3.4",
"@storybook/react-vite": "^10.3.4",
"@svgr/babel-plugin-add-jsx-attribute": "^8.0.0",
"@svgr/cli": "^8.1.0",
"@types/react": "^19.2.14",
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
"@vitejs/plugin-react": "^6.0.0",
"eslint-plugin-storybook": "^10.2.16",
"@typescript/native-preview": "beta",
"@vitejs/plugin-react": "^6.0.1",
"eslint-plugin-storybook": "^10.3.4",
"react": "19.2.4",
"react-dom": "19.2.4",
"storybook": "^10.2.16",
"vite": "^8.0.0",
"vitest": "^4.1.0"
"storybook": "^10.3.4",
"vite": "^8.0.5",
"vitest": "^4.1.2"
},
"peerDependencies": {
"react": ">=19.2",

View File

@@ -5,6 +5,21 @@ import { css, cx } from '@emotion/css';
import type { CSSProperties } from './styles';
export const viewStyles = css({
alignItems: 'stretch',
borderWidth: 0,
borderStyle: 'solid',
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'column',
margin: 0,
padding: 0,
position: 'relative',
/* fix flexbox bugs */
minHeight: 0,
minWidth: 0,
});
type ViewProps = Omit<HTMLProps<HTMLDivElement>, 'style'> & {
className?: string;
style?: CSSProperties;
@@ -13,19 +28,15 @@ type ViewProps = Omit<HTMLProps<HTMLDivElement>, 'style'> & {
};
export const View = forwardRef<HTMLDivElement, ViewProps>((props, ref) => {
// The default styles are special-cased and pulled out into static
// styles, and hardcode the class name here. View is used almost
// everywhere and we can avoid any perf penalty that glamor would
// incur.
const { className = '', style, nativeStyle, innerRef, ...restProps } = props;
return (
<div
{...restProps}
ref={innerRef ?? ref}
style={nativeStyle}
className={cx(
'view',
viewStyles,
className,
style && Object.keys(style).length > 0 ? css(style) : undefined,
)}

View File

@@ -1,6 +1,6 @@
import { useWindowSize } from 'usehooks-ts';
import { breakpoints } from '../tokens';
import { breakpoints } from '#tokens';
export function useResponsive() {
const { height, width } = useWindowSize({

View File

@@ -1,4 +1,8 @@
module.exports = {
prettier: true,
prettierConfig: {
singleQuote: true,
},
svgoConfig: {
plugins: [
{
@@ -14,7 +18,7 @@ module.exports = {
babelConfig: {
plugins: [
[
'./add-attribute',
'./add-attribute.ts',
{
elements: ['path', 'Path', 'rect', 'Rect'],
attributes: [

View File

@@ -1,10 +1,26 @@
import type BabelTemplate from '@babel/template';
import type { NodePath } from '@babel/traverse';
import type * as BabelTypes from '@babel/types';
import type { Attribute, Options } from '@svgr/babel-plugin-add-jsx-attribute';
type PluginAPI = {
types: typeof BabelTypes;
template: typeof BabelTemplate;
};
const positionMethod = {
start: 'unshiftContainer',
end: 'pushContainer',
};
} as const;
const addJSXAttribute = ({ types: t, template }, opts) => {
function getAttributeValue({ literal, value }) {
const addJSXAttribute = ({ types: t, template }: PluginAPI, opts: Options) => {
function getAttributeValue({
literal,
value,
}: Pick<Attribute, 'literal' | 'value'>):
| BabelTypes.JSXExpressionContainer
| BabelTypes.StringLiteral
| null {
if (typeof value === 'boolean') {
return t.jsxExpressionContainer(t.booleanLiteral(value));
}
@@ -14,7 +30,7 @@ const addJSXAttribute = ({ types: t, template }, opts) => {
}
if (typeof value === 'string' && literal) {
return t.jsxExpressionContainer(template.ast(value).expression);
return t.jsxExpressionContainer(template.expression.ast(value));
}
if (typeof value === 'string') {
@@ -24,7 +40,7 @@ const addJSXAttribute = ({ types: t, template }, opts) => {
return null;
}
function getAttribute({ spread, name, value, literal }) {
function getAttribute({ spread, name, value, literal }: Attribute) {
if (spread) {
return t.jsxSpreadAttribute(t.identifier(name));
}
@@ -37,7 +53,8 @@ const addJSXAttribute = ({ types: t, template }, opts) => {
return {
visitor: {
JSXOpeningElement(path) {
JSXOpeningElement(path: NodePath<BabelTypes.JSXOpeningElement>) {
if (!t.isJSXIdentifier(path.node.name)) return;
if (!opts.elements.includes(path.node.name.name)) return;
opts.attributes.forEach(
@@ -52,7 +69,11 @@ const addJSXAttribute = ({ types: t, template }, opts) => {
const newAttribute = getAttribute({ spread, name, value, literal });
const attributes = path.get('attributes');
const isEqualAttribute = attribute => {
const isEqualAttribute = (
attribute: NodePath<
BabelTypes.JSXAttribute | BabelTypes.JSXSpreadAttribute
>,
) => {
if (spread) {
return attribute.get('argument').isIdentifier({ name });
}
@@ -67,7 +88,11 @@ const addJSXAttribute = ({ types: t, template }, opts) => {
// Only add the color if it doesn't explicitly say no
// color
if (attribute.get('value').node.value !== 'none') {
const attrValue = attribute.get('value');
if (
!attrValue.isStringLiteral() ||
attrValue.node.value !== 'none'
) {
attribute.replaceWith(newAttribute);
}
@@ -84,4 +109,4 @@ const addJSXAttribute = ({ types: t, template }, opts) => {
};
};
module.exports = addJSXAttribute;
export default addJSXAttribute;

View File

@@ -9,4 +9,4 @@ function indexTemplate(filePaths: { path: string }[]) {
return exportEntries.join('\n');
}
module.exports = indexTemplate;
export default indexTemplate;

View File

@@ -13,11 +13,11 @@ export const SvgLogo = (props: SVGProps<SVGSVGElement>) => (
>
<path
fill="currentColor"
d="m1.138 30.423 13.8-29.309a.32.32 0 0 1 .289-.184h.605a.32.32 0 0 1 .287.18l8.903 18.29 2.791-1.074a.32.32 0 0 1 .414.184l.742 1.932a.32.32 0 0 1-.183.413l-2.574.99 3.175 6.524a.32.32 0 0 1-.147.428l-1.861.905a.32.32 0 0 1-.428-.147l-3.277-6.733-21.98 8.453a.32.32 0 0 1-.415-.189l-.152-.418a.32.32 0 0 1 .01-.245ZM15.56 6.152 5.85 26.774l16.634-6.398L15.56 6.152Z"
d="m1.138 30.423 13.8-29.309a.32.32 0 0 1 .289-.184h.605a.32.32 0 0 1 .287.18l8.903 18.29 2.791-1.074a.32.32 0 0 1 .414.184l.742 1.932a.32.32 0 0 1-.183.413l-2.574.99 3.175 6.524a.32.32 0 0 1-.147.428l-1.861.905a.32.32 0 0 1-.428-.147l-3.277-6.733-21.98 8.453a.32.32 0 0 1-.415-.189l-.152-.418a.32.32 0 0 1 .01-.245M15.56 6.152 5.85 26.774l16.634-6.398z"
/>
<path
fill="currentColor"
d="m21.777 14.568.932 2.544-21.203 7.775a.32.32 0 0 1-.41-.19l-.713-1.944a.32.32 0 0 1 .19-.41l21.204-7.775Z"
d="m21.777 14.568.932 2.544-21.203 7.775a.32.32 0 0 1-.41-.19l-.713-1.944a.32.32 0 0 1 .19-.41z"
/>
</svg>
);

View File

@@ -15,4 +15,4 @@ export const ${componentName} = (${props}) => (
`;
};
module.exports = tmpl;
export default tmpl;

View File

@@ -11,11 +11,11 @@ export const SvgAdd = (props: SVGProps<SVGSVGElement>) => (
}}
>
<path
d="M23 11.5a1.5 1.5 0 0 1-1.5 1.5h-20a1.5 1.5 0 0 1 0-3h20a1.5 1.5 0 0 1 1.5 1.5Z"
d="M23 11.5a1.5 1.5 0 0 1-1.5 1.5h-20a1.5 1.5 0 0 1 0-3h20a1.5 1.5 0 0 1 1.5 1.5"
fill="currentColor"
/>
<path
d="M11.5 23a1.5 1.5 0 0 1-1.5-1.5v-20a1.5 1.5 0 0 1 3 0v20a1.5 1.5 0 0 1-1.5 1.5Z"
d="M11.5 23a1.5 1.5 0 0 1-1.5-1.5v-20a1.5 1.5 0 0 1 3 0v20a1.5 1.5 0 0 1-1.5 1.5"
fill="currentColor"
/>
</svg>

View File

@@ -12,7 +12,7 @@ export const SvgExpandArrow = (props: SVGProps<SVGSVGElement>) => (
>
<path
fill="currentColor"
d="M24.483.576c-.309-.327-.674-.49-1.097-.49H1.56C1.137.086.771.25.463.576A1.635 1.635 0 0 0 0 1.737c0 .448.154.834.463 1.161l10.913 11.558c.31.327.675.49 1.097.49.422 0 .788-.163 1.096-.49L24.483 2.898c.308-.327.463-.713.463-1.16 0-.448-.155-.835-.463-1.162Z"
d="M24.483.576q-.463-.49-1.097-.49H1.56q-.633 0-1.096.49A1.64 1.64 0 0 0 0 1.737q0 .671.463 1.161l10.913 11.558q.465.49 1.097.49.633 0 1.096-.49L24.483 2.898q.462-.49.463-1.16 0-.672-.463-1.162"
/>
</svg>
);

Some files were not shown because too many files have changed in this diff Show More