Compare commits

...

7 Commits

Author SHA1 Message Date
Matt Fiddaman
36e5cb17f5 fix healthcheck script (#7840)
* fix healthcheck script

* note

* test release docker image
2026-05-14 21:39:10 +00:00
Matiss Janis Aboltins
2c9e0af3e4 Fix OpenID auth test flakiness (#7847)
* [AI] Fix flaky openid /config test from cross-worker auth race

Vitest runs sync-server test files in parallel workers that share
account.sqlite. Other files (e.g. app-account.test.js) insert 'openid'
auth rows, and auth.method is a PRIMARY KEY, so a concurrent INSERT in
app-openid.test.ts can hit UNIQUE constraint failed: auth.method.

Use INSERT OR REPLACE in the helper and clear the auth table in
beforeEach for a clean start.

* Add release notes for PR #7847

* Change category from Bugfixes to Maintenance

Fix OpenID authentication test flakiness by ensuring test isolation with INSERT OR REPLACE.

* [AI] Disable file parallelism for sync-server tests

The previous fix only patched insertOpenIdAuth in app-openid.test.ts,
but app-account.test.js's insertAuthRow helper also does plain
INSERT INTO auth ... 'openid' ... (lines 197, 203, 210, 229, 245).
With maxWorkers: 2 and a shared account.sqlite, either file's INSERT
can race the other's and hit UNIQUE constraint failed: auth.method.

Disable cross-file parallelism so test files run sequentially against
the shared DB. Within-file tests still run sequentially by default.
Test suite goes from ~20s to ~36s; trades some speed for stability.

* [AI] Revert openid test changes, reword release note

The fileParallelism: false change in vitest.config.ts already prevents
the auth.method UNIQUE-constraint race across files, so the INSERT OR
REPLACE and extra beforeEach cleanup in app-openid.test.ts are no longer
needed. Revert that file back to its original state and reword the
release note to describe the actual fix.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-14 21:30:33 +00:00
Matt Fiddaman
e04924810d clean up GoCardless bank factory loading process (#7809)
* remove TLA in bank-factory

* note
2026-05-14 21:04:20 +00:00
Matiss Janis Aboltins
872a40f829 Add explicit permissions to GitHub Actions workflows (#7846)
* [AI] Add explicit permissions blocks to GitHub Actions workflows

Resolves zizmor "excessive-permissions" findings by declaring minimal
`permissions:` blocks for workflows that previously relied on the default
GITHUB_TOKEN permissions.

https://claude.ai/code/session_01FsyCaLEqb3C4egMPUoAFRg

* Add release notes for PR #7846

* Update category for release notes

Changed category from Enhancements to Maintenance.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-14 20:17:50 +00:00
Matiss Janis Aboltins
fd01bd855c [AI] Stabilize size-compare job by pinning downloads to run_id (#7780)
* [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>

* Add release notes for PR #7780

* Change category to Maintenance in release notes

Updated category from 'Enhancements' to 'Maintenance'.

* [AI] Clean up comment to remove reference to previous implementation

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

---------

Co-authored-by: Claude Opus 4.7 (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-05-14 19:41:13 +00:00
Matiss Janis Aboltins
7e0f024c97 Add repository metadata to @actual-app/crdt package.json (#7845)
* [AI] Fix npm provenance for @actual-app/crdt and bump to 3.0.1

Add the missing repository field to packages/crdt/package.json so the npm
provenance bundle can validate the source against
https://github.com/actualbudget/actual. Without it, publishing fails with
"Error verifying sigstore provenance bundle: repository.url is \"\"".

* Add release notes for PR #7845

* [AI] Revert @actual-app/crdt version back to 3.0.0

* Fix metadata formatting in package.json for crdt

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-14 19:34:39 +00:00
Matiss Janis Aboltins
2b7bc80f4e release crdt v3.0.0 (#7818)
* release crdt v3.0.0

* Test

* Bump version

* Revert readme
2026-05-14 19:08:51 +00:00
28 changed files with 212 additions and 100 deletions

View File

@@ -14,6 +14,9 @@ on:
pull_request:
merge_group:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}

View File

@@ -7,6 +7,9 @@ on:
pull_request:
merge_group:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}

View File

@@ -11,6 +11,11 @@ on:
required: true
type: string
permissions:
contents: read
issues: read
pull-requests: read
jobs:
count-points:
runs-on: ubuntu-latest

View File

@@ -91,10 +91,15 @@ jobs:
tags: actualbudget/actual-server-testing
- name: Test that the docker image boots
timeout-minutes: 1
run: |
docker run --detach --network=host actualbudget/actual-server-testing
sleep 10
curl --fail -sS -LI -w '%{http_code}\n' --retry 20 --retry-delay 1 --retry-connrefused localhost:5006
docker run --detach --network=host --name actual-server actualbudget/actual-server-testing
HEALTHCMD=$(yq -r '.services.actual_server.healthcheck.test[1]' packages/sync-server/docker-compose.yml)
until docker exec actual-server sh -c "$HEALTHCMD"; do sleep 1; done
- name: Dump container logs on failure
if: failure()
run: docker logs actual-server || true
# 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/

View File

@@ -23,6 +23,10 @@ env:
TAGS: |
type=semver,pattern={{version}}
permissions:
contents: read
packages: write
jobs:
build:
name: Build Docker image
@@ -83,6 +87,23 @@ jobs:
- name: Build Web
run: yarn build:server
- name: Build ubuntu image for testing
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
push: false
load: true
file: packages/sync-server/docker/ubuntu.Dockerfile
tags: actualbudget/actual-server-testing
- name: Test that the ubuntu image boots
timeout-minutes: 1
run: |
docker rm -f actual-server 2>/dev/null || true
docker run --detach --network=host --name actual-server actualbudget/actual-server-testing
HEALTHCMD=$(yq -r '.services.actual_server.healthcheck.test[1]' packages/sync-server/docker-compose.yml)
until docker exec actual-server sh -c "$HEALTHCMD"; do sleep 1; done
- name: Build and push ubuntu image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
@@ -92,6 +113,23 @@ jobs:
platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: ${{ steps.meta.outputs.tags }}
- name: Build alpine image for testing
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
push: false
load: true
file: packages/sync-server/docker/alpine.Dockerfile
tags: actualbudget/actual-server-testing
- name: Test that the alpine image boots
timeout-minutes: 1
run: |
docker rm -f actual-server 2>/dev/null || true
docker run --detach --network=host --name actual-server actualbudget/actual-server-testing
HEALTHCMD=$(yq -r '.services.actual_server.healthcheck.test[1]' packages/sync-server/docker-compose.yml)
until docker exec actual-server sh -c "$HEALTHCMD"; do sleep 1; done
- name: Build and push alpine image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
@@ -100,3 +138,7 @@ jobs:
file: packages/sync-server/docker/alpine.Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
tags: ${{ steps.alpine-meta.outputs.tags }}
- name: Dump container logs on failure
if: failure()
run: docker logs actual-server || true

View File

@@ -19,6 +19,9 @@ on:
- '!packages/docs/**' # Docs changes don't affect Electron
- '!packages/eslint-plugin-actual/**' # Eslint plugin changes don't affect Electron
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

View File

@@ -6,6 +6,9 @@ on:
- cron: '0 4 * * *'
workflow_dispatch:
permissions:
contents: read
jobs:
extract-and-upload-i18n-strings:
runs-on: ubuntu-latest

View File

@@ -4,6 +4,9 @@ on:
issues:
types: [labeled]
permissions:
issues: write
jobs:
needs-votes:
if: ${{ github.event.label.name == 'feature' }}

View File

@@ -4,6 +4,9 @@ on:
issues:
types: [labeled]
permissions:
issues: write
jobs:
tech-support:
if: ${{ github.event.label.name == 'tech-support' }}

View File

@@ -4,6 +4,9 @@ on:
issues:
types: [closed]
permissions:
issues: write
jobs:
remove-help-wanted:
if: ${{ !contains(github.event.issue.labels.*.name, 'feature') && contains(github.event.issue.labels.*.name, 'help wanted') }}

View File

@@ -12,6 +12,9 @@ on:
tags:
- v**
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

View File

@@ -18,6 +18,9 @@ concurrency:
group: publish-flathub
cancel-in-progress: false
permissions:
contents: read
jobs:
publish-flathub:
runs-on: ubuntu-22.04

View File

@@ -18,6 +18,9 @@ concurrency:
group: publish-microsoft-store
cancel-in-progress: false
permissions:
contents: read
jobs:
publish-microsoft-store:
runs-on: windows-latest

View File

@@ -13,6 +13,9 @@ defaults:
env:
CI: true
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

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

@@ -1,8 +1,13 @@
{
"name": "@actual-app/crdt",
"version": "2.1.0",
"version": "3.0.0",
"description": "CRDT layer of Actual",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/actualbudget/actual.git",
"directory": "packages/crdt"
},
"files": [
"dist",
"!dist/**/*.test.d.ts",

View File

@@ -21,9 +21,9 @@ services:
- ./actual-data:/data
healthcheck:
# Enable health check for the instance
test: ['CMD-SHELL', 'node src/scripts/health-check.js']
test: ['CMD-SHELL', 'node scripts/health-check.js']
# health check using self signed certs
# test: ['CMD-SHELL', 'NODE_EXTRA_CA_CERTS=/data/selfhost.crt node src/scripts/health-check.js']
# test: ['CMD-SHELL', 'NODE_EXTRA_CA_CERTS=/data/selfhost.crt node scripts/health-check.js']
interval: 60s
timeout: 10s
retries: 3

View File

@@ -46,6 +46,9 @@ COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/packages/sync-server/package.json ./
COPY --from=builder /app/packages/sync-server/build ./
# script dir changed when we swapped build method, add the legacy dir in for compatibility
RUN mkdir -p src && ln -s ../scripts src/scripts
ENTRYPOINT ["/sbin/tini","-g", "--"]
EXPOSE 5006
CMD ["node", "app.js"]

View File

@@ -46,6 +46,9 @@ COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/packages/sync-server/package.json ./
COPY --from=builder /app/packages/sync-server/build ./
# script dir changed when we swapped build method, add the legacy dir in for compatibility
RUN mkdir -p src && ln -s ../scripts src/scripts
ENTRYPOINT ["/usr/bin/tini","-g", "--"]
EXPOSE 5006
CMD ["node", "app.js"]

View File

@@ -2,19 +2,9 @@ import IntegrationBank from './banks/integration-bank';
// Filename convention: <name>_<bic>.{ts,js} (skips bank.interface,
// integration-bank, and any other helper without an underscore).
const bankLoaders = import.meta.glob('./banks/*_*.{ts,js}');
const bankModules = import.meta.glob('./banks/*_*.{ts,js}', { eager: true });
async function loadBanks() {
const imports = await Promise.all(
Object.values(bankLoaders).map(loader =>
loader().then(handler => handler.default),
),
);
return imports;
}
export const banks = await loadBanks();
export const banks = Object.values(bankModules).map(m => m.default);
export function BankFactory(institutionId) {
return (

View File

@@ -7,5 +7,8 @@ export default {
enabled: false,
},
maxWorkers: 2,
// All test files share account.sqlite. Running files in parallel races on
// the auth table's PRIMARY KEY (e.g. UNIQUE constraint failed: auth.method).
fileParallelism: false,
},
};

View File

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

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [matt-fidd]
---
Clean up GoCardless bank factory loading process

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
CRDT: release v3.0.0

View File

@@ -0,0 +1,6 @@
---
category: Bugfixes
authors: [matt-fidd]
---
Fix docker healthcheck script and add backwards compatibility

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Update package.json for @actual-app/crdt with missing metadata.

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Add explicit permissions to GitHub Actions workflows for enhanced security and token scope control.

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Run sync-server test files sequentially to avoid races on the shared account.sqlite database.