Compare commits

..

4 Commits

Author SHA1 Message Date
Cursor Agent
ac2f04c34b [AI] Clean up comment to remove reference to previous implementation
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-05-09 18:13:43 +00:00
Matiss Janis Aboltins
0b2dba60cd Change category to Maintenance in release notes
Updated category from 'Enhancements' to 'Maintenance'.
2026-05-09 18:00:34 +01:00
github-actions[bot]
f624ae9701 Add release notes for PR #7780 2026-05-09 17:59:35 +01:00
github-actions[bot]
732a6a107b [AI] Stabilize size-compare job by pinning downloads to run_id
The compare job in .github/workflows/size-compare.yml was flaky because
fountainhead/action-wait-for-check matched a check by name from any run
on the branch, while dawidd6/action-download-artifact with branch:/pr:
filters and workflow_conclusion: '' resolved to the latest run regardless
of completion. When a new master build started in the seconds between
waiting and downloading, the action picked up the in-progress run and
failed with "artifact not found".

Replaces the eight wait-for-check steps with one actions/github-script
step that polls listWorkflowRuns for a successful build.yml run on
master and the PR head SHA in parallel via Promise.all, then pins all
eight downloads to those run_ids.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 17:52:24 +01:00
22 changed files with 82 additions and 265 deletions

View File

@@ -1,7 +1,7 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose
{
"name": "Actual Devcontainer",
"name": "Actual development",
"dockerComposeFile": ["../docker-compose.yml", "docker-compose.yml"],
// Alternatively:
// "image": "mcr.microsoft.com/devcontainers/typescript-node:0-16",

View File

@@ -44,7 +44,6 @@ CLP
CMCIFRPAXXX
COBADEFF
CODEOWNERS
Codespaces
COEP
commerzbank
Copiar

View File

@@ -33,6 +33,7 @@ jobs:
permissions:
pull-requests: write
contents: read
actions: read
steps:
- name: Checkout base branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -44,140 +45,120 @@ jobs:
with:
download-translations: 'false'
- name: Wait for ${{github.base_ref}} web build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: master-web-build
# Resolve one successful `build.yml` run for each side (master and PR
# head) up front, then pin every download below to its `run_id`. This
# ensures artifact downloads are consistent and prevents race conditions.
- name: Resolve build runs
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
id: build-runs
env:
BASE_REF: ${{ github.base_ref }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
with:
token: ${{ secrets.GITHUB_TOKEN }}
checkName: web
ref: ${{github.base_ref}}
- name: Wait for ${{github.base_ref}} API build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: master-api-build
with:
token: ${{ secrets.GITHUB_TOKEN }}
checkName: api
ref: ${{github.base_ref}}
- name: Wait for ${{github.base_ref}} CLI build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: master-cli-build
with:
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}}
script: |
const TIMEOUT_MS = 30 * 60 * 1000;
const SLEEP_MS = 15000;
- name: Wait for PR build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: wait-for-web-build
with:
token: ${{ secrets.GITHUB_TOKEN }}
checkName: web
ref: ${{github.event.pull_request.head.sha}}
- name: Wait for API PR build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: wait-for-api-build
with:
token: ${{ secrets.GITHUB_TOKEN }}
checkName: api
ref: ${{github.event.pull_request.head.sha}}
- name: Wait for CLI PR build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: wait-for-cli-build
with:
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}}
async function resolveRun({ label, filter, notFoundHint }) {
const deadline = Date.now() + TIMEOUT_MS;
while (true) {
const { data } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'build.yml',
...filter,
status: 'success',
per_page: 1,
});
if (data.workflow_runs.length > 0) {
const run = data.workflow_runs[0];
core.info(`Found ${label} build run ${run.id} (${run.html_url})`);
return run.id;
}
if (Date.now() > deadline) {
throw new Error(
`No successful build.yml run found for ${label} within 30 min — ${notFoundHint}.`,
);
}
core.info(`No successful ${label} build run yet — sleeping 15s.`);
await new Promise(r => setTimeout(r, SLEEP_MS));
}
}
- 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' || steps.wait-for-crdt-build.outputs.conclusion == 'failure'
run: |
echo "Build failed on PR branch or ${GITHUB_BASE_REF}"
exit 1
const baseRef = process.env.BASE_REF;
const headSha = process.env.HEAD_SHA;
const [masterRunId, headRunId] = await Promise.all([
resolveRun({
label: baseRef,
filter: { branch: baseRef },
notFoundHint: `${baseRef} may be broken`,
}),
resolveRun({
label: `PR head ${headSha}`,
filter: { head_sha: headSha },
notFoundHint:
'build may still be running, have failed, or the branch may have been force-pushed',
}),
]);
core.setOutput('master_run_id', masterRunId);
core.setOutput('head_run_id', headRunId);
- name: Download web build artifact from ${{github.base_ref}}
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
id: pr-web-build
with:
branch: ${{github.base_ref}}
run_id: ${{ steps.build-runs.outputs.master_run_id }}
workflow: build.yml
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: build-stats
path: base
- name: Download API build artifact from ${{github.base_ref}}
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
id: pr-api-build
with:
branch: ${{github.base_ref}}
run_id: ${{ steps.build-runs.outputs.master_run_id }}
workflow: build.yml
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: api-build-stats
path: base
- name: Download build stats from PR
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
with:
pr: ${{github.event.pull_request.number}}
run_id: ${{ steps.build-runs.outputs.head_run_id }}
workflow: build.yml
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: build-stats
path: head
allow_forks: true
- name: Download API stats from PR
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
with:
pr: ${{github.event.pull_request.number}}
run_id: ${{ steps.build-runs.outputs.head_run_id }}
workflow: build.yml
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: api-build-stats
path: head
allow_forks: true
- name: Download CLI build artifact from ${{github.base_ref}}
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
with:
branch: ${{github.base_ref}}
run_id: ${{ steps.build-runs.outputs.master_run_id }}
workflow: build.yml
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: cli-build-stats
path: base
- name: Download CLI stats from PR
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
with:
pr: ${{github.event.pull_request.number}}
run_id: ${{ steps.build-runs.outputs.head_run_id }}
workflow: build.yml
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
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}}
run_id: ${{ steps.build-runs.outputs.master_run_id }}
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}}
run_id: ${{ steps.build-runs.outputs.head_run_id }}
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

View File

@@ -516,29 +516,6 @@ describe('API CRUD operations', () => {
);
});
// apis: getNote, updateNote
test('Notes: successfully get and update note', async () => {
const categories = await api.getCategories();
const categoryId = categories[0].id;
// No note exists initially
const initial = await api.getNote(categoryId);
expect(initial).toBeNull();
// Set a note
await api.updateNote(categoryId, 'Test note content');
const afterSet = await api.getNote(categoryId);
expect(afterSet).toEqual({ id: categoryId, note: 'Test note content' });
// Update the note
await api.updateNote(categoryId, 'Updated note content');
const afterUpdate = await api.getNote(categoryId);
expect(afterUpdate).toEqual({
id: categoryId,
note: 'Updated note content',
});
});
// apis: getRules, getPayeeRules, createRule, updateRule, deleteRule
test('Rules: successfully update rules', async () => {
await api.createPayee({ name: 'test-payee' });

View File

@@ -13,7 +13,6 @@ import type { ImportTransactionsOpts } from '@actual-app/core/types/api-handlers
import type { Handlers } from '@actual-app/core/types/handlers';
import type {
ImportTransactionEntity,
NoteEntity,
RuleEntity,
TransactionEntity,
} from '@actual-app/core/types/models';
@@ -248,14 +247,6 @@ export function deleteCategory(
return send('api/category-delete', { id, transferCategoryId });
}
export function getNote(id: NoteEntity['id']) {
return send('api/note-get', { id });
}
export function updateNote(id: NoteEntity['id'], note: NoteEntity['note']) {
return send('api/note-update', { id, note });
}
export function getCommonPayees() {
return send('api/common-payees-get');
}

View File

@@ -590,8 +590,6 @@ export function useSyncAccountsMutation() {
accountIdsToSync = accountIdsToSync.filter(
id => !simpleFinAccounts.find(sfa => sfa.id === id),
);
dispatch(setAccountsSyncing({ ids: accountIdsToSync }));
}
// Loop through the accounts and perform sync operation.. one by one

View File

@@ -81,6 +81,7 @@ export const Modal = ({
inset: 0,
zIndex: MODAL_Z_INDEX,
fontSize: 14,
willChange: 'transform',
// on mobile, we disable the blurred background for performance reasons
...(isNarrowWidth
? {

View File

@@ -466,7 +466,6 @@ const AccountList = forwardRef<HTMLDivElement, AccountListProps>(
<ListBox
aria-label={ariaLabel}
items={accounts}
dependencies={[syncingAccountIds, failedAccounts, updatedAccounts]}
dragAndDropHooks={dragAndDropHooks}
ref={ref}
style={{

View File

@@ -1,24 +1,6 @@
import { describe, expect, it } from 'vitest';
import {
calculateSpendingReportTimeRange,
calculateTimeRange,
} from './reportRanges';
// In test mode, monthUtils.currentMonth() returns '2017-01'
describe('calculateTimeRange', () => {
it('keeps last month as a live time range when restoring a saved widget', () => {
const [start, end, mode] = calculateTimeRange({
start: '2016-11',
end: '2016-11',
mode: 'lastMonth',
});
expect(start).toBe('2016-12');
expect(end).toBe('2016-12');
expect(mode).toBe('lastMonth');
});
});
import { calculateSpendingReportTimeRange } from './reportRanges';
// In test mode, monthUtils.currentMonth() returns '2017-01'
describe('calculateSpendingReportTimeRange', () => {

View File

@@ -212,10 +212,6 @@ export function calculateTimeRange(
return getLatestRange(offset);
}
if (mode === 'lastMonth') {
const lastMonth = monthUtils.subMonths(monthUtils.currentMonth(), 1);
return [lastMonth, lastMonth, 'lastMonth'] as const;
}
if (mode === 'lastYear') {
return [
monthUtils.getYearStart(monthUtils.prevYear(monthUtils.currentMonth())),

View File

@@ -87,11 +87,6 @@ import APIList from './APIList';
"deleteSchedule"
]} />
<APIList title="Notes" sections={[
"getNote",
"updateNote"
]} />
<APIList title="Misc" sections={[
"BudgetFile",
"initConfig",
@@ -735,22 +730,6 @@ Update fields of a rule. `fields` can specify any field described in [`Schedule`
<Method name="deleteSchedule" args={[{ name: 'id', type: 'id' }]} returns="Promise<null>" />
## Notes
Notes can be attached to any entity (categories, budget months, etc.) by ID. They are also used to define budget templates and savings goals (e.g. `#template 250`, `#goal 1000`).
#### `getNote`
<Method name="getNote" args={[{ name: 'id', type: 'id' }]} returns="Promise<Note | null>" />
Returns the note for the given entity ID, or `null` if no note has been set.
#### `updateNote`
<Method name="updateNote" args={[{ name: 'id', type: 'id' }, { name: 'note', type: 'string' }]} returns="Promise<void>" />
Sets the note on the entity with the given ID. Pass an empty string to clear the note.
## Misc
#### BudgetFile

View File

@@ -6,10 +6,6 @@ This guide will help you set up your development environment for contributing to
## Prerequisites
:::tip
If you prefer not to install Node and Yarn locally, you can use the [Dev Container](#dev-container) or run [Docker Compose](#docker-compose) directly.
:::
Before you begin, ensure you have the following installed:
- **Node.js**: Version 22 or greater. You can download it from the [Node.js website](https://nodejs.org/en/download) (we recommend the LTS version).
@@ -43,34 +39,6 @@ Before you begin, ensure you have the following installed:
yarn typecheck
```
## Dev Container
The repo includes a [`.devcontainer/`](https://github.com/actualbudget/actual/tree/master/.devcontainer) configuration that follows the [Dev Containers spec](https://containers.dev/). Any tool that supports the spec can use it — for example VS Code or Cursor (with the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)), JetBrains IDEs (via Gateway), GitHub Codespaces, or the [`@devcontainers/cli`](https://github.com/devcontainers/cli).
In an editor that supports the spec, open the cloned repo and accept the **Reopen in Container** prompt (or run the equivalent command from your editor's command palette). The container will build, `yarn install` will run automatically via `postCreateCommand`, and you'll be dropped into a shell with the toolchain ready.
To start the dev server, open a terminal inside the container and run:
```bash
yarn start
```
The dev server will be available at `http://localhost:3001/`. Most editors automatically forward the port from the container to your host.
## Docker Compose
For other editors, run from the repo root:
```bash
docker compose up --build
```
This starts a container that runs `yarn start:browser` on port 3001. Open `http://localhost:3001/` in your browser.
:::note
The container mounts your repo at `/app`. If you've already run `yarn install` on your host, the native modules (`better-sqlite3`, `bcrypt`, `electron`, `sharp`) will be compiled for your host OS and won't work inside the Linux container. Either delete `node_modules/` first and let the container reinstall, or run the dev container path above (which rebuilds them automatically).
:::
## Essential Development Commands
All commands should be run from the **root directory** of the repository. Never run yarn commands from child workspace directories.

View File

@@ -707,16 +707,6 @@ handlers['api/category-delete'] = withMutation(async function ({
});
});
handlers['api/note-get'] = async function ({ id }) {
checkFileOpen();
return handlers['notes-get']({ id });
};
handlers['api/note-update'] = withMutation(async function ({ id, note }) {
checkFileOpen();
return handlers['notes-save']({ id, note });
});
handlers['api/common-payees-get'] = async function () {
checkFileOpen();
const payees = await handlers['common-payees-get']();

View File

@@ -7,20 +7,12 @@ import type { NoteEntity } from '#types/models';
export type NotesHandlers = {
'notes-save': typeof updateNotes;
'notes-save-undoable': typeof updateNotes;
'notes-get': (arg: Pick<NoteEntity, 'id'>) => Promise<NoteEntity | null>;
};
export const app = createApp<NotesHandlers>();
app.method('notes-save', updateNotes);
app.method('notes-save-undoable', mutator(undoable(updateNotes)));
app.method('notes-get', getNote);
async function updateNotes({ id, note }: NoteEntity) {
await db.update('notes', { id, note });
}
async function getNote({
id,
}: Pick<NoteEntity, 'id'>): Promise<NoteEntity | null> {
return db.first<NoteEntity>('SELECT id, note FROM notes WHERE id = ?', [id]);
}

View File

@@ -19,7 +19,6 @@ import type {
ImportTransactionEntity,
NearbyPayeeEntity,
NewRuleEntity,
NoteEntity,
PayeeEntity,
PayeeLocationEntity,
RuleEntity,
@@ -196,10 +195,6 @@ export type ApiHandlers = {
transferCategoryId?: APICategoryEntity['id'];
}) => Promise<void>;
'api/note-get': (arg: Pick<NoteEntity, 'id'>) => Promise<NoteEntity | null>;
'api/note-update': (arg: NoteEntity) => Promise<void>;
'api/payees-get': () => Promise<APIPayeeEntity[]>;
'api/common-payees-get': () => Promise<APIPayeeEntity[]>;

View File

@@ -1,6 +0,0 @@
---
category: Maintenance
authors: [nikhilweee]
---
Document the Dev Container and Docker Compose options as alternatives to local Node and Yarn setup in the contributor development-setup guide.

View File

@@ -1,6 +0,0 @@
---
category: Bugfixes
authors: [totallynotjon]
---
Fix sporadic text blur in modals by removing unnecessary `will-change: transform` on the modal overlay.

View File

@@ -1,6 +0,0 @@
---
category: Bugfixes
authors: [ADGJSD]
---
Fix dashboard report widgets saved with the "Last month" live range restoring as static.

View File

@@ -1,6 +0,0 @@
---
category: Enhancements
authors: [whlapinel]
---
Add `getNote` and `updateNote` to the public `@actual-app/api`, enabling programmatic read/write of category notes (templates, goals, etc.) without internal API access.

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Stabilize size comparison workflow by pinning artifact downloads to specific run IDs.

View File

@@ -1,6 +0,0 @@
---
category: Bugfixes
authors: [MatissJanis]
---
Fix mobile bank sync indicators not updating live during sync.

View File

@@ -791,7 +791,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-module-transforms@npm:^7.27.1, @babel/helper-module-transforms@npm:^7.28.6":
"@babel/helper-module-transforms@npm:^7.27.1, @babel/helper-module-transforms@npm:^7.28.3, @babel/helper-module-transforms@npm:^7.28.6":
version: 7.28.6
resolution: "@babel/helper-module-transforms@npm:7.28.6"
dependencies:
@@ -820,13 +820,6 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-plugin-utils@npm:^7.28.6":
version: 7.28.6
resolution: "@babel/helper-plugin-utils@npm:7.28.6"
checksum: 10/21c853bbc13dbdddf03309c9a0477270124ad48989e1ad6524b83e83a77524b333f92edd2caae645c5a7ecf264ec6d04a9ebe15aeb54c7f33c037b71ec521e4a
languageName: node
linkType: hard
"@babel/helper-remap-async-to-generator@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/helper-remap-async-to-generator@npm:7.27.1"
@@ -1359,16 +1352,16 @@ __metadata:
linkType: hard
"@babel/plugin-transform-modules-systemjs@npm:^7.28.5":
version: 7.29.4
resolution: "@babel/plugin-transform-modules-systemjs@npm:7.29.4"
version: 7.28.5
resolution: "@babel/plugin-transform-modules-systemjs@npm:7.28.5"
dependencies:
"@babel/helper-module-transforms": "npm:^7.28.6"
"@babel/helper-plugin-utils": "npm:^7.28.6"
"@babel/helper-module-transforms": "npm:^7.28.3"
"@babel/helper-plugin-utils": "npm:^7.27.1"
"@babel/helper-validator-identifier": "npm:^7.28.5"
"@babel/traverse": "npm:^7.29.0"
"@babel/traverse": "npm:^7.28.5"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 10/79269e6ec8ec831bb63bf1c7cc1a980e28da785e92b36d42612f0139e4044499b99aa109fca849e1a156c092aabf6c24d145f4cabf2ac9ea84ef468852fe4c03
checksum: 10/1b91b4848845eaf6e21663d97a2a6c896553b127deaf3c2e9a2a4f041249277d13ebf71fd42d0ecbc4385e9f76093eff592fe0da0dcf1401b3f38c1615d8c539
languageName: node
linkType: hard
@@ -16286,9 +16279,9 @@ __metadata:
linkType: hard
"fast-uri@npm:^3.0.1":
version: 3.1.2
resolution: "fast-uri@npm:3.1.2"
checksum: 10/1dff04865b2a38d3e0659deadfbf72efdf83a776bfbf9667e4aa9e5a3ec31bc341cda9622136b32b7652a857c8ba11896794186e8f876f8b2b72731fce8622f6
version: 3.1.0
resolution: "fast-uri@npm:3.1.0"
checksum: 10/818b2c96dc913bcf8511d844c3d2420e2c70b325c0653633f51821e4e29013c2015387944435cd0ef5322c36c9beecc31e44f71b257aeb8e0b333c1d62bb17c2
languageName: node
linkType: hard