Compare commits

..

5 Commits

Author SHA1 Message Date
Matiss Janis Aboltins
82673ecd50 [AI] Use bash for /update-vrt merge step (#7783)
The Merge VRT Patches job runs inside the Playwright container where
the default GitHub Actions shell is `sh -e {0}`, not bash. The merge
step uses bash-only constructs (`shopt -s nullglob`, array literals,
`${#patches[@]}`, `"${patches[@]}"`), so every /update-vrt run that
reaches the merge stage now exits 127 with `shopt: not found` (e.g.
run 25609625260).

Pin this step to `shell: bash` to match the explicit `shell: bash` we
already use elsewhere in the workflow. The sibling shard-patch creation
steps stay on the default sh because they only use POSIX features.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:56:50 +00:00
Matiss Janis Aboltins
18c704b3ba [AI] Sync server: harden CORS proxy method validation (#7788)
* [AI] Sync server: harden CORS proxy method validation

The CORS proxy validated `method` against a fallback-normalized value but
forwarded the raw client-supplied value to fetch(), letting a non-string
input (e.g. ["POST"]) bypass the GET/HEAD allowlist via undici's String()
coercion. Reject non-string method, pass the validated normalized method
to fetch(), and drop the unreachable body-forwarding branch.

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

* [AI] Polish release notes wording

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

* Rename 7787.md to 7788.md

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:11:40 +00:00
Michael Clark
b05c207123 :electron: Publish to Microsoft store after release is published (#7757)
* move desktop app microsoft store publish to after the release is published

* release notes
2026-05-09 21:16:13 +00:00
Matiss Janis Aboltins
b9ab3e7bc6 [AI] Fix /update-vrt build step after lage browser-build refactor (#7781)
The build-web job in vrt-update-generate.yml invoked
`yarn workspace @actual-app/core build:browser`, but #7602 removed that
script when it routed the browser pipeline through
`lage build:browser --to=@actual-app/web` (orchestrated by
bin/package-browser). The recent /update-vrt parallelization (#7641)
preserved the now-stale per-workspace invocations, so every comment
trigger fails with "Couldn't find a script named build:browser".

Match the working e2e-test.yml build-web step exactly:
`yarn build:browser --skip-translations`. lage's `^build` edge handles
the upstream graph (crdt, plugins-service, loot-core) automatically, and
`--skip-translations` keeps the captured snapshots aligned with regular
VRT runs (which also strip Weblate locale chunks for determinism).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:16:42 +00:00
Matiss Janis Aboltins
4f40defe9e [AI] Mobile: live value tracking (#7774)
* [AI] Update mobile budget value color live as user types

The mobile FocusableAmountInput's color was computed from the saved
`value` prop, so it stayed in the gray "zero" state until blur. Track
the in-progress edited value via the existing `onChangeValue` callback
and feed it to `makeAmountFullStyle` so the color reflects what the
user is currently typing.

* Add release notes for PR #7774

* Change category from Features to Bugfix

* [AI] Reapply sign when computing live amount color

liveValue holds the absolute value (the input field has no sign — the
+/- toggle controls it separately), so passing it directly to
makeAmountFullStyle picked positiveColor for amounts the user intends
as negative. Pass maybeApplyNegative(liveValue, isNegative) so the
color matches the signed value.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-09 18:08:58 +00:00
14 changed files with 222 additions and 75 deletions

View File

@@ -117,49 +117,7 @@ jobs:
!packages/desktop-electron/dist/Actual-windows.exe
packages/desktop-electron/dist/*.AppImage
packages/desktop-electron/dist/*.flatpak
packages/desktop-electron/dist/*.appx
outputs:
version: ${{ steps.process_version.outputs.version }}
publish-microsoft-store:
needs: build
runs-on: windows-latest
environment: release
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
steps:
- name: Install StoreBroker
shell: powershell
run: |
Install-Module -Name StoreBroker -AcceptLicense -Force -Scope CurrentUser -Verbose
- name: Download Microsoft Store artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: actual-electron-windows-latest-appx
- name: Submit to Microsoft Store
shell: powershell
run: |
# Disable telemetry
$global:SBDisableTelemetry = $true
# Authenticate against the store
$pass = ConvertTo-SecureString -String '${{ secrets.MICROSOFT_STORE_CLIENT_SECRET }}' -AsPlainText -Force
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ${{ secrets.MICROSOFT_STORE_CLIENT_ID }},$pass
Set-StoreBrokerAuthentication -TenantId '${{ secrets.MICROSOFT_STORE_TENANT_ID }}' -Credential $cred
# Zip and create metadata files
$artifacts = Get-ChildItem -Path . -Filter *.appx | Select-Object -ExpandProperty FullName
New-StoreBrokerConfigFile -Path "$PWD/config.json" -AppId ${{ secrets.MICROSOFT_STORE_PRODUCT_ID }}
New-SubmissionPackage -ConfigPath "$PWD/config.json" -DisableAutoPackageNameFormatting -AppxPath $artifacts -OutPath "$PWD" -OutName submission
# Submit the app
# See https://github.com/microsoft/StoreBroker/blob/master/Documentation/USAGE.md#the-easy-way
Update-ApplicationSubmission `
-AppId ${{ secrets.MICROSOFT_STORE_PRODUCT_ID }} `
-SubmissionDataPath "submission.json" `
-PackagePath "submission.zip" `
-ReplacePackages `
-NoStatus `
-AutoCommit `
-Force

View File

@@ -0,0 +1,113 @@
name: Publish Microsoft Store
defaults:
run:
shell: bash
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Release tag (e.g. v25.3.0)'
required: true
type: string
concurrency:
group: publish-microsoft-store
cancel-in-progress: false
jobs:
publish-microsoft-store:
runs-on: windows-latest
environment: release
steps:
- name: Resolve version
id: resolve_version
env:
EVENT_NAME: ${{ github.event_name }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
INPUT_TAG: ${{ inputs.tag }}
run: |
if [[ "$EVENT_NAME" == "release" ]]; then
TAG="$RELEASE_TAG"
else
TAG="$INPUT_TAG"
fi
if [[ -z "$TAG" ]]; then
echo "::error::No tag provided"
exit 1
fi
# Validate tag format (v-prefixed semver, e.g. v25.3.0 or v1.2.3-beta.1)
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "::error::Invalid tag format: $TAG (expected v-prefixed semver, e.g. v25.3.0)"
exit 1
fi
VERSION="${TAG#v}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Resolved tag=$TAG version=$VERSION"
- 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}"
echo "Checking release assets for tag $TAG..."
ASSETS=$(gh api "repos/${{ github.repository }}/releases/tags/$TAG" --jq '.assets[].name')
echo "Found assets:"
echo "$ASSETS"
if ! echo "$ASSETS" | grep -q "\.appx$"; then
echo "::error::No .appx assets found in release $TAG"
exit 1
fi
echo "Required .appx assets found."
- name: Download Microsoft Store artifacts
env:
GH_TOKEN: ${{ github.token }}
STEPS_RESOLVE_VERSION_OUTPUTS_TAG: ${{ steps.resolve_version.outputs.tag }}
run: |
TAG="${STEPS_RESOLVE_VERSION_OUTPUTS_TAG}"
gh release download "$TAG" --repo "${{ github.repository }}" --pattern "*.appx"
- name: Install StoreBroker
shell: powershell
run: |
Install-Module -Name StoreBroker -AcceptLicense -Force -Scope CurrentUser -Verbose
- name: Submit to Microsoft Store
shell: powershell
run: |
# Disable telemetry
$global:SBDisableTelemetry = $true
# Authenticate against the store
$pass = ConvertTo-SecureString -String '${{ secrets.MICROSOFT_STORE_CLIENT_SECRET }}' -AsPlainText -Force
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ${{ secrets.MICROSOFT_STORE_CLIENT_ID }},$pass
Set-StoreBrokerAuthentication -TenantId '${{ secrets.MICROSOFT_STORE_TENANT_ID }}' -Credential $cred
# Zip and create metadata files
$artifacts = Get-ChildItem -Path . -Filter *.appx | Select-Object -ExpandProperty FullName
New-StoreBrokerConfigFile -Path "$PWD/config.json" -AppId ${{ secrets.MICROSOFT_STORE_PRODUCT_ID }}
New-SubmissionPackage -ConfigPath "$PWD/config.json" -DisableAutoPackageNameFormatting -AppxPath $artifacts -OutPath "$PWD" -OutName submission
# Submit the app
# See https://github.com/microsoft/StoreBroker/blob/master/Documentation/USAGE.md#the-easy-way
Update-ApplicationSubmission `
-AppId ${{ secrets.MICROSOFT_STORE_PRODUCT_ID }} `
-SubmissionDataPath "submission.json" `
-PackagePath "submission.zip" `
-ReplacePackages `
-NoStatus `
-AutoCommit `
-Force

View File

@@ -82,16 +82,17 @@ jobs:
with:
download-translations: 'false'
- name: Build browser bundle
# REACT_APP_NETLIFY=true keeps the "Create test file" button in the
# production bundle — every VRT test's beforeEach relies on it via
# ConfigurationPage.createTestFile().
# 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.
# --skip-translations keeps VRT screenshots deterministic by rendering
# source-code English instead of upstream Weblate en.json (which can
# drift between snapshot capture and test runs).
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
run: yarn build:browser --skip-translations
- name: Upload build artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
@@ -257,6 +258,7 @@ jobs:
- name: Merge shard patches
id: create-patch
shell: bash
run: |
git config --global --add safe.directory "$GITHUB_WORKSPACE"
git config --global user.name "github-actions[bot]"

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

@@ -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

@@ -190,9 +190,11 @@ export const FocusableAmountInput = memo(function FocusableAmountInput({
buttonProps,
onFocus,
onBlur,
onChangeValue,
...props
}: FocusableAmountInputProps) {
const [isNegative, setIsNegative] = useState(true);
const [liveValue, setLiveValue] = useState(Math.abs(value));
const maybeApplyNegative = (amount: number, negative: boolean) => {
const absValue = Math.abs(amount);
@@ -203,6 +205,15 @@ export const FocusableAmountInput = memo(function FocusableAmountInput({
props.onUpdateAmount?.(maybeApplyNegative(amount, negative));
};
const handleChangeValue = (text: string) => {
setLiveValue(currencyToAmount(text) || 0);
onChangeValue?.(text);
};
useEffect(() => {
setLiveValue(Math.abs(value));
}, [value]);
useEffect(() => {
if (sign) {
setIsNegative(sign === '-');
@@ -227,10 +238,11 @@ export const FocusableAmountInput = memo(function FocusableAmountInput({
value={value}
onFocus={onFocus}
onBlur={onBlur}
onChangeValue={handleChangeValue}
onUpdateAmount={amount => onUpdateAmount(amount, isNegative)}
focused={focused && !disabled}
style={{
...makeAmountFullStyle(value, {
...makeAmountFullStyle(maybeApplyNegative(liveValue, isNegative), {
zeroColor: isNegative ? theme.numberNegative : theme.numberNeutral,
positiveColor: theme.numberPositive,
negativeColor: theme.numberNegative,

View File

@@ -177,15 +177,12 @@ app.use('/', async (req, res) => {
}
try {
// Extract method, body, and headers from the request body (sent by loot-core)
const {
method = 'GET',
body,
headers: customHeaders = {},
} = req.body || {};
const { method = 'GET', headers: customHeaders = {} } = req.body || {};
const methodNormalized =
typeof method === 'string' ? method.toUpperCase() : 'GET';
if (typeof method !== 'string') {
return res.status(400).json({ error: 'Invalid method parameter' });
}
const methodNormalized = method.toUpperCase();
if (!['GET', 'HEAD'].includes(methodNormalized)) {
return res.status(405).json({ error: 'Method not allowed' });
}
@@ -218,13 +215,8 @@ app.use('/', async (req, res) => {
}
const response = await fetch(url.href, {
method,
method: methodNormalized,
headers: requestHeaders,
body: ['GET', 'HEAD'].includes(method)
? undefined
: typeof body === 'string'
? body
: JSON.stringify(body),
});
const contentType =

View File

@@ -414,6 +414,55 @@ describe('app-cors-proxy', () => {
expect(res.statusCode).toBe(405);
expect(res.body.error).toBe('Method not allowed');
});
it('should reject non-string method (array bypass)', async () => {
global.fetch.mockClear();
const res = await request(app)
.get('/')
.send({ method: ['POST'], body: { evil: true } })
.query({ url: 'https://api.github.com/repos/user/repo1' });
expect(res.statusCode).toBe(400);
expect(res.body.error).toBe('Invalid method parameter');
const proxyCalls = global.fetch.mock.calls.filter(
([url]) => url === 'https://api.github.com/repos/user/repo1',
);
expect(proxyCalls).toHaveLength(0);
});
it('should reject non-string method (object bypass)', async () => {
global.fetch.mockClear();
const res = await request(app)
.get('/')
.send({ method: { toString: () => 'POST' } })
.query({ url: 'https://github.com/user/repo1' });
expect(res.statusCode).toBe(400);
expect(res.body.error).toBe('Invalid method parameter');
const proxyCalls = global.fetch.mock.calls.filter(
([url]) => url === 'https://github.com/user/repo1',
);
expect(proxyCalls).toHaveLength(0);
});
it('should forward the validated method to fetch, not the raw input', async () => {
global.fetch.mockClear();
await request(app)
.get('/')
.send({ method: 'get' })
.query({ url: 'https://github.com/user/repo1' });
const proxyCall = global.fetch.mock.calls.find(
([url]) => url === 'https://github.com/user/repo1',
);
expect(proxyCall).toBeDefined();
expect(proxyCall[1].method).toBe('GET');
});
});
describe('GitHub authentication', () => {

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MikesGlitch]
---
Alter desktop app publish workflow to publish to Microsoft store after release is published

View File

@@ -0,0 +1,6 @@
---
category: Bugfix
authors: [MatissJanis]
---
Mobile: add live value tracking for user input in mobile transactions.

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Fix update-vrt workflow

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Fix /update-vrt merge step failing on Playwright container with `shopt: not found`

View File

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

View File

@@ -0,0 +1,6 @@
---
category: Bugfixes
authors: [MatissJanis]
---
Fix an issue where the CORS proxy could be bypassed.