Compare commits
97 Commits
jfdoming/a
...
release
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36e5cb17f5 | ||
|
|
2c9e0af3e4 | ||
|
|
e04924810d | ||
|
|
872a40f829 | ||
|
|
fd01bd855c | ||
|
|
7e0f024c97 | ||
|
|
2b7bc80f4e | ||
|
|
6c1699f0b0 | ||
|
|
4b1b68a353 | ||
|
|
dbf5d7c079 | ||
|
|
6bfc299d28 | ||
|
|
a7e22b023c | ||
|
|
d3e7c1ee87 | ||
|
|
a7e100276e | ||
|
|
ee82a16026 | ||
|
|
b61732e20e | ||
|
|
83073b3ee0 | ||
|
|
78234102fa | ||
|
|
2c7f3c7a3d | ||
|
|
a4cb6dac37 | ||
|
|
d1f9f8aecf | ||
|
|
3b927e754c | ||
|
|
f2f3a5aa6d | ||
|
|
8fbad7d64f | ||
|
|
44a3013772 | ||
|
|
70716a59da | ||
|
|
e83567216f | ||
|
|
92d4f82b66 | ||
|
|
50feba1afb | ||
|
|
8263e58eb2 | ||
|
|
d015858e4a | ||
|
|
f3a9c1a02c | ||
|
|
daa698e7d2 | ||
|
|
e0536b593d | ||
|
|
d500057494 | ||
|
|
0086f805f8 | ||
|
|
911d8371cc | ||
|
|
a95c0ad9b0 | ||
|
|
d9308c1474 | ||
|
|
425db2d94d | ||
|
|
5d270340a5 | ||
|
|
070144f182 | ||
|
|
e479c84898 | ||
|
|
1306da27c5 | ||
|
|
0fd510a1d4 | ||
|
|
82673ecd50 | ||
|
|
18c704b3ba | ||
|
|
b05c207123 | ||
|
|
b9ab3e7bc6 | ||
|
|
4f40defe9e | ||
|
|
3799b587ec | ||
|
|
8e1f27f316 | ||
|
|
fb95d4c92d | ||
|
|
2782d464ab | ||
|
|
b63f5dd303 | ||
|
|
35a01b0fa6 | ||
|
|
3104503a8a | ||
|
|
db38565524 | ||
|
|
9e9cf45641 | ||
|
|
8ab8277429 | ||
|
|
d9fb66422b | ||
|
|
345c99be4d | ||
|
|
e3b42b51a3 | ||
|
|
bc08ed97e9 | ||
|
|
d2b50adf30 | ||
|
|
1f4f706c4a | ||
|
|
852b95524b | ||
|
|
6fb73786d5 | ||
|
|
e3952d2a24 | ||
|
|
fea36466d2 | ||
|
|
1fadfa4e9b | ||
|
|
6ead7ea42c | ||
|
|
d6fc3212b9 | ||
|
|
071611fcc5 | ||
|
|
263358b5cf | ||
|
|
44fc959ed8 | ||
|
|
d787d0ce43 | ||
|
|
2c3e2a34fd | ||
|
|
78d533c800 | ||
|
|
49f6b21f2c | ||
|
|
9f05207fe8 | ||
|
|
8366c442a2 | ||
|
|
4b73fd7e45 | ||
|
|
c593bda145 | ||
|
|
1b86bba2cd | ||
|
|
6c2c96e826 | ||
|
|
6298f6a324 | ||
|
|
1afe7c9a1e | ||
|
|
24279264da | ||
|
|
4a5ee9c2dc | ||
|
|
a8eb204ce7 | ||
|
|
f68e4fbb2a | ||
|
|
dd3b1144d1 | ||
|
|
ff0f5bdb35 | ||
|
|
11ce29e7fd | ||
|
|
d58c9a9a07 | ||
|
|
598e3ec9d8 |
@@ -1,6 +1,6 @@
|
||||
issue_enrichment:
|
||||
auto_enrich:
|
||||
enabled: false
|
||||
enabled: true
|
||||
reviews:
|
||||
request_changes_workflow: true
|
||||
review_status: false
|
||||
|
||||
@@ -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 development",
|
||||
"name": "Actual Devcontainer",
|
||||
"dockerComposeFile": ["../docker-compose.yml", "docker-compose.yml"],
|
||||
// Alternatively:
|
||||
// "image": "mcr.microsoft.com/devcontainers/typescript-node:0-16",
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -3,9 +3,6 @@ contact_links:
|
||||
- name: Bank-sync issues
|
||||
url: https://discord.gg/pRYNYr4W5A
|
||||
about: Is bank-sync not working? Returning too much or too few information? Reach out to the community on Discord.
|
||||
- name: Support
|
||||
url: https://discord.gg/pRYNYr4W5A
|
||||
about: Need help with something? Having troubles setting up? Or perhaps issues using the API? Reach out to the community on Discord.
|
||||
- name: Translations
|
||||
url: https://hosted.weblate.org/projects/actualbudget/actual/
|
||||
about: Found a string that needs a better translation? Add your suggestion or upvote an existing one in Weblate.
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/tech-support.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Tech Support
|
||||
description: Need help with something? Having troubles setting up? Or perhaps issues using the API?
|
||||
title: '[Support]: '
|
||||
labels: ['tech-support']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
> ⚠️ **Tech support tickets opened here are automatically closed.** GitHub Issues are reserved for bug reports and feature requests. The fastest way to get help is to ask the community on [Discord](https://discord.gg/pRYNYr4W5A) — that's where most of the community lives and can help you in real time.
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Describe your problem
|
||||
description: Please describe, in as much detail as you can, what you need help with.
|
||||
placeholder: I'm trying to [...] but [...]
|
||||
validations:
|
||||
required: true
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,4 +1,4 @@
|
||||
<!-- Thank you for submitting a pull request! Make sure to follow the instructions to write release notes for your PR — it should only take a minute or two: https://github.com/actualbudget/docs#writing-good-release-notes. Try running yarn generate:release-notes *before* pushing your PR for an interactive experience. -->
|
||||
<!-- Thank you for submitting a pull request! Make sure to follow the instructions to write release notes for your PR — it should only take a minute or two: https://actualbudget.org/docs/contributing/#writing-good-release-notes. Try running yarn generate:release-notes *before* pushing your PR for an interactive experience. -->
|
||||
|
||||
## Description
|
||||
|
||||
|
||||
17
.github/actions/docs-spelling/excludes.txt
vendored
@@ -1,13 +1,16 @@
|
||||
# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-excludes
|
||||
(?:^|/)(?i).nojekyll
|
||||
(?:^|/)(?i)COPYRIGHT
|
||||
(?:^|/)(?i)docusaurus.config.js
|
||||
(?:^|/)(?i)LICEN[CS]E
|
||||
(?:^|/)(?i)README.md
|
||||
(?:^|/)3rdparty/
|
||||
(?:^|/)go\.sum$
|
||||
(?:^|/)package(?:-lock|)\.json$
|
||||
(?:^|/)pyproject.toml
|
||||
(?:^|/)requirements(?:-dev|-doc|-test|)\.txt$
|
||||
(?:^|/)vendor/
|
||||
ignore$
|
||||
(?:^|/)yarn\.lock$
|
||||
\.a$
|
||||
\.ai$
|
||||
\.avi$
|
||||
@@ -53,6 +56,7 @@ ignore$
|
||||
\.svgz?$
|
||||
\.tar$
|
||||
\.tiff?$
|
||||
\.tsx$
|
||||
\.ttf$
|
||||
\.wav$
|
||||
\.webm$
|
||||
@@ -62,15 +66,12 @@ ignore$
|
||||
\.zip$
|
||||
^\.github/actions/spelling/
|
||||
^\.github/ISSUE_TEMPLATE/
|
||||
^\Q.github/workflows/spelling.yml\E$
|
||||
^\.yarn/
|
||||
^\Q.github/\E$
|
||||
^\Q.github/workflows/spelling.yml\E$
|
||||
^\Qnode_modules/\E$
|
||||
^\Qsrc/\E$
|
||||
^\Qstatic/\E$
|
||||
^\Q.github/\E$
|
||||
(?:^|/)yarn\.lock$
|
||||
(?:^|/)(?i)docusaurus.config.js
|
||||
(?:^|/)(?i)README.md
|
||||
(?:^|/)(?i).nojekyll
|
||||
^\static/
|
||||
\.tsx$
|
||||
^packages/docs/docs/releases\.md$
|
||||
ignore$
|
||||
|
||||
14
.github/actions/docs-spelling/expect.txt
vendored
@@ -38,10 +38,13 @@ Cetelem
|
||||
cimode
|
||||
Citi
|
||||
Citibank
|
||||
claude
|
||||
Cloudflare
|
||||
CLP
|
||||
CMCIFRPAXXX
|
||||
COBADEFF
|
||||
CODEOWNERS
|
||||
Codespaces
|
||||
COEP
|
||||
commerzbank
|
||||
Copiar
|
||||
@@ -53,6 +56,7 @@ crt
|
||||
CZK
|
||||
Danske
|
||||
datadir
|
||||
datamodel
|
||||
DATEDIF
|
||||
Depositos
|
||||
deselection
|
||||
@@ -82,6 +86,7 @@ Globecard
|
||||
GLS
|
||||
gocardless
|
||||
Grafana
|
||||
Gruvbox
|
||||
HABAL
|
||||
Hampel
|
||||
HELADEF
|
||||
@@ -89,6 +94,7 @@ HLOOKUP
|
||||
HUF
|
||||
IFERROR
|
||||
IFNA
|
||||
Ilavenil
|
||||
INDUSTRIEL
|
||||
INGBPLPW
|
||||
Ingo
|
||||
@@ -127,6 +133,7 @@ murmurhash
|
||||
NETWORKDAYS
|
||||
nginx
|
||||
nodenext
|
||||
nord
|
||||
OIDC
|
||||
Okabe
|
||||
overbudgeted
|
||||
@@ -140,14 +147,13 @@ pluggyai
|
||||
Poste
|
||||
PPABPLPK
|
||||
prefs
|
||||
Primoco
|
||||
Priotecs
|
||||
proactively
|
||||
Qatari
|
||||
QNTOFRP
|
||||
QONTO
|
||||
Raiffeisen
|
||||
REGEXREPLACE
|
||||
relinking
|
||||
revolut
|
||||
RIED
|
||||
RSchedule
|
||||
@@ -172,7 +178,6 @@ SWEDBANK
|
||||
SWEDNOKK
|
||||
Synology
|
||||
systemctl
|
||||
tada
|
||||
taskbar
|
||||
templating
|
||||
THB
|
||||
@@ -180,6 +185,7 @@ TIMEFRAME
|
||||
touchscreen
|
||||
triaging
|
||||
tsgo
|
||||
tsgolint
|
||||
TWD
|
||||
UAH
|
||||
ubuntu
|
||||
@@ -195,4 +201,6 @@ websecure
|
||||
WEEKNUM
|
||||
Widiba
|
||||
WOR
|
||||
worktree
|
||||
youngcw
|
||||
zizmor
|
||||
|
||||
7
.github/actions/setup/action.yml
vendored
@@ -10,6 +10,10 @@ inputs:
|
||||
description: 'Whether to download translations as part of setup, default true'
|
||||
required: false
|
||||
default: 'true'
|
||||
cache:
|
||||
description: 'Whether to restore and save dependency and Lage caches, default true'
|
||||
required: false
|
||||
default: 'true'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
@@ -18,6 +22,7 @@ runs:
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 22
|
||||
package-manager-cache: ${{ inputs.cache }}
|
||||
- name: Install yarn
|
||||
run: npm install -g yarn
|
||||
shell: bash
|
||||
@@ -28,6 +33,7 @@ runs:
|
||||
shell: bash
|
||||
- name: Cache
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
if: ${{ inputs.cache == 'true' }}
|
||||
id: cache
|
||||
with:
|
||||
path: ${{ format('{0}/**/node_modules', inputs.working-directory) }}
|
||||
@@ -37,6 +43,7 @@ runs:
|
||||
shell: bash
|
||||
- name: Cache Lage
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
if: ${{ inputs.cache == 'true' }}
|
||||
with:
|
||||
path: ${{ format('{0}/.lage', inputs.working-directory) }}
|
||||
key: lage-${{ runner.os }}-${{ github.sha }}
|
||||
|
||||
27
.github/workflows/ai-generated-label.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Add 'AI generated' label to '[AI]' PRs
|
||||
|
||||
##########################################################################################
|
||||
# This workflow uses the 'pull_request_target' event so it has a token that can add a #
|
||||
# label to PRs from forks. It does NOT check out or execute any code from the PR, so it #
|
||||
# is not vulnerable to the usual 'pull_request_target' code-injection concerns. Keep it #
|
||||
# that way - do not add a checkout step or run any PR-provided scripts here. #
|
||||
##########################################################################################
|
||||
|
||||
on:
|
||||
# This workflow never checks out or runs PR code; it only reads the PR title and adds a label.
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||
types: [opened, reopened, edited]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
add-ai-generated-label:
|
||||
name: Add 'AI generated' label
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.event.pull_request.title, '[AI]')
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-add-labels@bd52874380e3909a1ac983768df6976535ece7f8 # v1.1.0
|
||||
with:
|
||||
labels: AI generated
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
3
.github/workflows/build.yml
vendored
@@ -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' }}
|
||||
|
||||
3
.github/workflows/check.yml
vendored
@@ -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' }}
|
||||
|
||||
5
.github/workflows/count-points.yml
vendored
@@ -11,6 +11,11 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
count-points:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
48
.github/workflows/crdt-version-check.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: CRDT version bump check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'packages/crdt/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-version-bump:
|
||||
runs-on: ubuntu-latest
|
||||
name: Ensure @actual-app/crdt version is bumped
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Verify version bump
|
||||
env:
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if ! git cat-file -e "origin/${BASE_REF}:packages/crdt/package.json" 2>/dev/null; then
|
||||
echo "packages/crdt/package.json does not exist on the base branch; skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
BASE_VERSION=$(git show "origin/${BASE_REF}:packages/crdt/package.json" | jq -r .version)
|
||||
HEAD_VERSION=$(jq -r .version packages/crdt/package.json)
|
||||
echo "Base version: $BASE_VERSION"
|
||||
echo "Head version: $HEAD_VERSION"
|
||||
|
||||
if [ "$BASE_VERSION" = "$HEAD_VERSION" ]; then
|
||||
echo "::error file=packages/crdt/package.json::Files in packages/crdt/ were modified but the @actual-app/crdt version was not bumped. Please update the \"version\" field in packages/crdt/package.json."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
HIGHEST=$(printf '%s\n%s\n' "$BASE_VERSION" "$HEAD_VERSION" | sort -V | tail -n1)
|
||||
if [ "$HIGHEST" != "$HEAD_VERSION" ]; then
|
||||
echo "::error file=packages/crdt/package.json::The @actual-app/crdt version ($HEAD_VERSION) must be greater than the base version ($BASE_VERSION)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Version bumped from $BASE_VERSION to $HEAD_VERSION."
|
||||
3
.github/workflows/cut-release-branch.yml
vendored
@@ -26,6 +26,7 @@ permissions:
|
||||
jobs:
|
||||
cut-release-branch:
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -36,6 +37,8 @@ jobs:
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
cache: 'false'
|
||||
download-translations: 'false'
|
||||
|
||||
- name: Bump package versions
|
||||
|
||||
15
.github/workflows/docker-edge.yml
vendored
@@ -32,6 +32,7 @@ jobs:
|
||||
if: github.event_name == 'workflow_dispatch' || !github.event.repository.fork
|
||||
name: Build Docker image
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu, alpine]
|
||||
@@ -74,6 +75,9 @@ jobs:
|
||||
# This is faster and avoids yarn memory issues
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
cache: 'false'
|
||||
- name: Build Web
|
||||
run: yarn build:server
|
||||
|
||||
@@ -87,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/
|
||||
|
||||
46
.github/workflows/docker-release.yml
vendored
@@ -23,10 +23,15 @@ env:
|
||||
TAGS: |
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docker image
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
@@ -76,9 +81,29 @@ jobs:
|
||||
# This is faster and avoids yarn memory issues
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
cache: 'false'
|
||||
- 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:
|
||||
@@ -88,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:
|
||||
@@ -96,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
|
||||
|
||||
15
.github/workflows/e2e-test.yml
vendored
@@ -46,13 +46,12 @@ jobs:
|
||||
# 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:
|
||||
@@ -200,11 +199,13 @@ jobs:
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
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 "${PR_NUMBER}" > vrt-metadata/pr-number.txt
|
||||
echo "${VRT_RESULT}" > vrt-metadata/vrt-result.txt
|
||||
echo "${STEPS_PLAYWRIGHT_REPORT_VRT_OUTPUTS_ARTIFACT_URL}" > vrt-metadata/artifact-url.txt
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
STEPS_PLAYWRIGHT_REPORT_VRT_OUTPUTS_ARTIFACT_URL: ${{ steps.playwright-report-vrt.outputs.artifact-url }}
|
||||
VRT_RESULT: ${{ needs.vrt.result }}
|
||||
- name: Upload VRT metadata
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
|
||||
47
.github/workflows/electron-master.yml
vendored
@@ -21,6 +21,7 @@ jobs:
|
||||
# this is so the assets can be added to the release
|
||||
permissions:
|
||||
contents: write
|
||||
environment: release
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -66,6 +67,9 @@ jobs:
|
||||
STEPS_PROCESS_VERSION_OUTPUTS_VERSION: ${{ steps.process_version.outputs.version }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
cache: 'false'
|
||||
- name: Build Electron for Mac
|
||||
if: ${{ startsWith(matrix.os, 'macos') }}
|
||||
run: ./bin/package-electron
|
||||
@@ -116,48 +120,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
|
||||
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
|
||||
|
||||
3
.github/workflows/electron-pr.yml
vendored
@@ -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
|
||||
|
||||
@@ -6,6 +6,9 @@ on:
|
||||
- cron: '0 4 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
extract-and-upload-i18n-strings:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -4,6 +4,9 @@ on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
needs-votes:
|
||||
if: ${{ github.event.label.name == 'feature' }}
|
||||
|
||||
26
.github/workflows/issues-close-tech-support.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Close tech support issues with automated message
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
tech-support:
|
||||
if: ${{ github.event.label.name == 'tech-support' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create comment and close issue
|
||||
run: |
|
||||
gh issue comment "$ISSUE_URL" --body ":wave: Thanks for reaching out!
|
||||
|
||||
GitHub Issues are reserved for bug reports and feature requests, so tech support tickets are automatically closed. The fastest way to get help is to ask the community on [Discord](https://discord.gg/pRYNYr4W5A) — that's where most of the community lives and can help you in real time.
|
||||
|
||||
<!-- tech-support-auto-close-comment -->"
|
||||
|
||||
gh issue close "$ISSUE_URL"
|
||||
env:
|
||||
ISSUE_URL: https://github.com/actualbudget/actual/issues/${{ github.event.issue.number }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -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') }}
|
||||
|
||||
37
.github/workflows/merge-freeze-unfreeze.yml
vendored
@@ -1,37 +0,0 @@
|
||||
# When the "unfreeze" label is added to a PR, add that PR to Merge Freeze's unblocked list
|
||||
# so it can be merged during a freeze. Uses pull_request_target so the workflow runs in
|
||||
# the base repo and has access to MERGEFREEZE_ACCESS_TOKEN for fork PRs; it does not
|
||||
# checkout or run any PR code. Requires MERGEFREEZE_ACCESS_TOKEN repo secret
|
||||
# (project-specific token from Merge Freeze Web API panel for actualbudget/actual / master).
|
||||
# See: https://docs.mergefreeze.com/web-api#post-freeze-status
|
||||
|
||||
name: Merge Freeze – add PR to unblocked list
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
unfreeze:
|
||||
if: ${{ github.event.label.name == 'unfreeze' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions: {}
|
||||
concurrency:
|
||||
group: merge-freeze-unfreeze-${{ github.ref }}-labels
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: POST to Merge Freeze – add PR to unblocked list
|
||||
env:
|
||||
MERGEFREEZE_ACCESS_TOKEN: ${{ secrets.MERGEFREEZE_ACCESS_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
USER_NAME: ${{ github.actor }}
|
||||
run: |
|
||||
set -e
|
||||
if [ -z "$MERGEFREEZE_ACCESS_TOKEN" ]; then
|
||||
echo "::error::MERGEFREEZE_ACCESS_TOKEN secret is not set"
|
||||
exit 1
|
||||
fi
|
||||
url="https://www.mergefreeze.com/api/branches/actualbudget/actual/master/?access_token=${MERGEFREEZE_ACCESS_TOKEN}"
|
||||
payload=$(jq -n --arg user_name "$USER_NAME" --argjson pr "$PR_NUMBER" '{frozen: true, user_name: $user_name, unblocked_prs: [$pr]}')
|
||||
curl -sf -X POST "$url" -H "Content-Type: application/json" -d "$payload"
|
||||
echo "Merge Freeze updated: PR #$PR_NUMBER added to unblocked list."
|
||||
7
.github/workflows/netlify-release.yml
vendored
@@ -12,6 +12,9 @@ on:
|
||||
tags:
|
||||
- v**
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
@@ -19,6 +22,7 @@ concurrency:
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
steps:
|
||||
- name: Repository Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -27,6 +31,9 @@ jobs:
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
cache: 'false'
|
||||
|
||||
- name: Install Netlify
|
||||
run: npm install netlify-cli@17.10.1 -g
|
||||
|
||||
86
.github/workflows/publish-crdt.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
name: Publish @actual-app/crdt
|
||||
|
||||
# Automatically publishes @actual-app/crdt when its package.json version
|
||||
# changes on master (typically via a merged PR that bumped the version).
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'packages/crdt/package.json'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: publish-crdt
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
name: Check if publish is needed
|
||||
outputs:
|
||||
should-publish: ${{ steps.check.outputs.should-publish }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Compare local version against npm registry
|
||||
id: check
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
LOCAL_VERSION=$(jq -r .version packages/crdt/package.json)
|
||||
echo "Local version: $LOCAL_VERSION"
|
||||
|
||||
PUBLISHED_VERSION=$(npm view @actual-app/crdt version 2>/dev/null || echo "")
|
||||
echo "Published version: ${PUBLISHED_VERSION:-<none>}"
|
||||
|
||||
if [ "$LOCAL_VERSION" = "$PUBLISHED_VERSION" ]; then
|
||||
echo "Versions match - nothing to publish."
|
||||
echo "should-publish=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Version changed - publish required."
|
||||
echo "should-publish=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
publish:
|
||||
needs: check-version
|
||||
if: needs.check-version.outputs.should-publish == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
name: Publish @actual-app/crdt to npm
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # Required for npm OIDC provenance
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
cache: 'false'
|
||||
download-translations: 'false'
|
||||
|
||||
- name: Build @actual-app/crdt
|
||||
run: yarn workspace @actual-app/crdt build
|
||||
|
||||
- name: Pack @actual-app/crdt
|
||||
run: yarn workspace @actual-app/crdt pack --filename @actual-app/crdt.tgz
|
||||
|
||||
- name: Setup node and npm registry
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 24
|
||||
check-latest: true
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
package-manager-cache: false
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Publish to npm
|
||||
run: npm publish packages/crdt/@actual-app/crdt.tgz --access public --provenance
|
||||
4
.github/workflows/publish-flathub.yml
vendored
@@ -18,9 +18,13 @@ concurrency:
|
||||
group: publish-flathub
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
publish-flathub:
|
||||
runs-on: ubuntu-22.04
|
||||
environment: release
|
||||
steps:
|
||||
- name: Resolve version
|
||||
id: resolve_version
|
||||
|
||||
116
.github/workflows/publish-microsoft-store.yml
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
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
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
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
|
||||
@@ -13,6 +13,9 @@ defaults:
|
||||
env:
|
||||
CI: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
@@ -27,6 +30,7 @@ jobs:
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
environment: release
|
||||
if: github.event.repository.fork == false
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -44,6 +48,9 @@ jobs:
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
cache: 'false'
|
||||
|
||||
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
|
||||
name: Setup Flatpak dependencies
|
||||
|
||||
9
.github/workflows/publish-npm-packages.yml
vendored
@@ -9,6 +9,9 @@ on:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-and-pack:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -21,6 +24,9 @@ jobs:
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
cache: 'false'
|
||||
|
||||
- name: Update package versions
|
||||
if: github.event_name != 'push'
|
||||
@@ -87,6 +93,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Publish npm packages
|
||||
needs: build-and-pack
|
||||
environment: release
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -104,6 +111,8 @@ jobs:
|
||||
with:
|
||||
node-version: 24
|
||||
check-latest: true
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
package-manager-cache: false
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Publish Core
|
||||
|
||||
145
.github/workflows/size-compare.yml
vendored
@@ -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
|
||||
|
||||
9
.github/workflows/vrt-update-apply.yml
vendored
@@ -75,9 +75,12 @@ jobs:
|
||||
|
||||
echo "Found patch file: $PATCH_FILE"
|
||||
|
||||
# Validate patch only contains PNG files
|
||||
# Validate patch only contains PNG files. `git format-patch` emits a
|
||||
# `GIT binary patch` block for PNGs (no +++/--- lines), so check
|
||||
# `diff --git` headers — those are present for both text and binary.
|
||||
echo "Validating patch contains only PNG files..."
|
||||
if grep -E '^(\+\+\+|---) [ab]/' "$PATCH_FILE" | grep -v '\.png$'; then
|
||||
if grep -E '^diff --git ' "$PATCH_FILE" \
|
||||
| grep -vE '^diff --git a/[^[:space:]]+\.png b/[^[:space:]]+\.png$'; then
|
||||
echo "ERROR: Patch contains non-PNG files! Rejecting for security."
|
||||
echo "applied=false" >> "$GITHUB_OUTPUT"
|
||||
echo "error=Patch validation failed: contains non-PNG files" >> "$GITHUB_OUTPUT"
|
||||
@@ -85,7 +88,7 @@ jobs:
|
||||
fi
|
||||
|
||||
# Extract file list for verification
|
||||
FILES_CHANGED=$(grep -E '^\+\+\+ b/' "$PATCH_FILE" | sed 's/^+++ b\///' | wc -l)
|
||||
FILES_CHANGED=$(grep -cE '^diff --git ' "$PATCH_FILE")
|
||||
echo "Patch modifies $FILES_CHANGED PNG file(s)"
|
||||
|
||||
# Configure git
|
||||
|
||||
256
.github/workflows/vrt-update-generate.yml
vendored
@@ -36,15 +36,16 @@ jobs:
|
||||
content: 'eyes'
|
||||
});
|
||||
|
||||
generate-vrt-updates:
|
||||
name: Generate VRT Updates
|
||||
get-pr:
|
||||
name: Resolve PR details
|
||||
runs-on: ubuntu-latest
|
||||
# Only run on PR comments containing /update-vrt
|
||||
if: >
|
||||
github.event.issue.pull_request &&
|
||||
startsWith(github.event.comment.body, '/update-vrt')
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.59.1-jammy
|
||||
outputs:
|
||||
head_sha: ${{ steps.pr.outputs.head_sha }}
|
||||
head_ref: ${{ steps.pr.outputs.head_ref }}
|
||||
head_repo: ${{ steps.pr.outputs.head_repo }}
|
||||
steps:
|
||||
- name: Get PR details
|
||||
id: pr
|
||||
@@ -60,11 +61,132 @@ jobs:
|
||||
core.setOutput('head_ref', pr.head.ref);
|
||||
core.setOutput('head_repo', pr.head.repo.full_name);
|
||||
|
||||
build-web:
|
||||
name: Build web bundle
|
||||
runs-on: ubuntu-latest
|
||||
needs: get-pr
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.59.1-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ steps.pr.outputs.head_sha }}
|
||||
ref: ${{ needs.get-pr.outputs.head_sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Trust workspace directory
|
||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
shell: bash
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- 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.
|
||||
# --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 build:browser --skip-translations
|
||||
- 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
|
||||
|
||||
browser-vrt:
|
||||
name: Browser VRT (shard ${{ matrix.shard }}/3)
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get-pr, 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:
|
||||
ref: ${{ needs.get-pr.outputs.head_sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Trust workspace directory
|
||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
shell: bash
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- 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
|
||||
continue-on-error: true
|
||||
run: yarn vrt --update-snapshots --shard=${{ matrix.shard }}/3
|
||||
- name: Create shard patch with PNG changes only
|
||||
id: create-patch
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
git add "**/*.png"
|
||||
|
||||
if git diff --staged --quiet; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No VRT changes in this shard"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
git commit -m "Update VRT screenshots (browser shard ${{ matrix.shard }})"
|
||||
git format-patch -1 HEAD --stdout > vrt-shard.patch
|
||||
|
||||
# Validate patch only contains PNG files. `git format-patch` emits a
|
||||
# `GIT binary patch` block for PNGs (no +++/--- lines), so check
|
||||
# `diff --git` headers — those are present for both text and binary.
|
||||
if grep -E '^diff --git ' vrt-shard.patch \
|
||||
| grep -vE '^diff --git a/[^[:space:]]+\.png b/[^[:space:]]+\.png$'; then
|
||||
echo "ERROR: Shard patch contains non-PNG files!"
|
||||
exit 1
|
||||
fi
|
||||
- name: Upload shard patch
|
||||
if: steps.create-patch.outputs.has_changes == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: vrt-shard-browser-${{ matrix.shard }}
|
||||
path: vrt-shard.patch
|
||||
retention-days: 1
|
||||
overwrite: true
|
||||
|
||||
desktop-vrt:
|
||||
name: Desktop VRT
|
||||
runs-on: ubuntu-latest
|
||||
needs: get-pr
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.59.1-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ needs.get-pr.outputs.head_sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Trust workspace directory
|
||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
shell: bash
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -74,48 +196,124 @@ jobs:
|
||||
- name: Install build tools
|
||||
run: apt-get update && apt-get install -y build-essential python3
|
||||
|
||||
- name: Run VRT Tests on Desktop app
|
||||
- name: Run Desktop VRT Tests
|
||||
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
|
||||
continue-on-error: true
|
||||
run: yarn vrt --update-snapshots
|
||||
|
||||
- name: Create patch with PNG changes only
|
||||
- name: Create shard patch with PNG changes only
|
||||
id: create-patch
|
||||
run: |
|
||||
# Trust the repository directory (required for container environments)
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Stage only PNG files
|
||||
git add "**/*.png"
|
||||
|
||||
# Check if there are any changes
|
||||
if git diff --staged --quiet; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No VRT changes to commit"
|
||||
echo "No VRT changes in desktop shard"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Create commit and patch
|
||||
git commit -m "Update VRT screenshots"
|
||||
git format-patch -1 HEAD --stdout > vrt-update.patch
|
||||
git commit -m "Update VRT screenshots (desktop)"
|
||||
git format-patch -1 HEAD --stdout > vrt-shard.patch
|
||||
|
||||
# Validate patch only contains PNG files
|
||||
if grep -E '^(\+\+\+|---) [ab]/' vrt-update.patch | grep -v '\.png$'; then
|
||||
echo "ERROR: Patch contains non-PNG files!"
|
||||
# See validation note in browser-vrt above.
|
||||
if grep -E '^diff --git ' vrt-shard.patch \
|
||||
| grep -vE '^diff --git a/[^[:space:]]+\.png b/[^[:space:]]+\.png$'; then
|
||||
echo "ERROR: Desktop shard patch contains non-PNG files!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Patch created successfully with PNG changes only"
|
||||
- name: Upload shard patch
|
||||
if: steps.create-patch.outputs.has_changes == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: vrt-shard-desktop
|
||||
path: vrt-shard.patch
|
||||
retention-days: 1
|
||||
overwrite: true
|
||||
|
||||
merge-patch:
|
||||
name: Merge VRT Patches
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get-pr, browser-vrt, desktop-vrt]
|
||||
if: ${{ !cancelled() && needs.get-pr.result == 'success' }}
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.59.1-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ needs.get-pr.outputs.head_sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download all shard patches
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: /tmp/shard-patches
|
||||
pattern: vrt-shard-*
|
||||
|
||||
- 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]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# actions/download-artifact puts a lone matched artifact directly in
|
||||
# `path` but gives each of several its own `path/<name>/` subdir, so
|
||||
# recurse instead of globbing `*/vrt-shard.patch` (which would miss
|
||||
# the common single-shard case).
|
||||
mapfile -t patches < <(find /tmp/shard-patches -type f -name 'vrt-shard.patch' | sort)
|
||||
|
||||
if [ ${#patches[@]} -eq 0 ]; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No shard patches to merge"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Defense in depth: re-validate every shard patch before applying.
|
||||
# See validation note in browser-vrt above for why we match
|
||||
# `diff --git` headers instead of +++/--- lines.
|
||||
for patch in "${patches[@]}"; do
|
||||
echo "Validating $patch"
|
||||
if grep -E '^diff --git ' "$patch" \
|
||||
| grep -vE '^diff --git a/[^[:space:]]+\.png b/[^[:space:]]+\.png$'; then
|
||||
echo "ERROR: $patch contains non-PNG files!"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Apply each shard patch. Shards touch disjoint PNG files so
|
||||
# order does not matter. --index stages the applied changes.
|
||||
for patch in "${patches[@]}"; do
|
||||
echo "Applying $patch"
|
||||
git apply --index "$patch"
|
||||
done
|
||||
|
||||
if git diff --staged --quiet; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No VRT changes after merge"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
git commit -m "Update VRT screenshots"
|
||||
git format-patch -1 HEAD --stdout > vrt-update.patch
|
||||
|
||||
# Final guard on the combined patch.
|
||||
if grep -E '^diff --git ' vrt-update.patch \
|
||||
| grep -vE '^diff --git a/[^[:space:]]+\.png b/[^[:space:]]+\.png$'; then
|
||||
echo "ERROR: Merged patch contains non-PNG files!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Merged patch created successfully with PNG changes only"
|
||||
|
||||
- name: Upload patch artifact
|
||||
if: steps.create-patch.outputs.has_changes == 'true'
|
||||
@@ -130,11 +328,11 @@ 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 "${NEEDS_GET_PR_OUTPUTS_HEAD_REF}" > pr-metadata/head-ref.txt
|
||||
echo "${NEEDS_GET_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 }}
|
||||
NEEDS_GET_PR_OUTPUTS_HEAD_REF: ${{ needs.get-pr.outputs.head_ref }}
|
||||
NEEDS_GET_PR_OUTPUTS_HEAD_REPO: ${{ needs.get-pr.outputs.head_repo }}
|
||||
|
||||
- name: Upload PR metadata
|
||||
if: steps.create-patch.outputs.has_changes == 'true'
|
||||
|
||||
3
.gitignore
vendored
@@ -42,6 +42,9 @@ bundle.desktop.js.map
|
||||
bundle.mobile.js
|
||||
bundle.mobile.js.map
|
||||
|
||||
# Python virtualenv (Electron CI provisions one at the repo root for setuptools)
|
||||
.venv/
|
||||
|
||||
# Yarn
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"vi": "readonly",
|
||||
"backend": "readonly",
|
||||
"importScripts": "readonly",
|
||||
"FS": "readonly"
|
||||
"FS": "readonly",
|
||||
"__APP_VERSION__": "readonly"
|
||||
},
|
||||
"rules": {
|
||||
// Import sorting
|
||||
@@ -337,6 +338,11 @@
|
||||
"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"],
|
||||
@@ -370,7 +376,8 @@
|
||||
"files": ["**/*.test.{js,ts,jsx,tsx}", "packages/docs/**/*"],
|
||||
"rules": {
|
||||
"actual/no-untranslated-strings": "off",
|
||||
"actual/prefer-logger-over-console": "off"
|
||||
"actual/prefer-logger-over-console": "off",
|
||||
"typescript/unbound-method": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,3 +7,7 @@ enableTransparentWorkspaces: false
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.13.0.cjs
|
||||
|
||||
# Secure default: don't run postinstall scripts.
|
||||
# If a new package requires them, add it to dependenciesMeta in package.json.
|
||||
enableScripts: false
|
||||
|
||||
@@ -4,21 +4,30 @@ ROOT=`dirname $0`
|
||||
|
||||
cd "$ROOT/.."
|
||||
|
||||
echo "Updating translations..."
|
||||
if ! [ -d packages/desktop-client/locale ]; then
|
||||
git clone https://github.com/actualbudget/translations packages/desktop-client/locale
|
||||
SKIP_TRANSLATIONS=false
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--skip-translations)
|
||||
SKIP_TRANSLATIONS=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$SKIP_TRANSLATIONS" = false ]; then
|
||||
echo "Updating translations..."
|
||||
if ! [ -d packages/desktop-client/locale ]; then
|
||||
git clone https://github.com/actualbudget/translations packages/desktop-client/locale
|
||||
fi
|
||||
pushd packages/desktop-client/locale > /dev/null
|
||||
git checkout .
|
||||
git pull
|
||||
popd > /dev/null
|
||||
packages/desktop-client/bin/remove-untranslated-languages
|
||||
fi
|
||||
pushd packages/desktop-client/locale > /dev/null
|
||||
git checkout .
|
||||
git pull
|
||||
popd > /dev/null
|
||||
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
|
||||
|
||||
echo "packages/desktop-client/build"
|
||||
lage build:browser --to=@actual-app/web
|
||||
|
||||
@@ -57,8 +57,7 @@ yarn workspace @actual-app/core build:node
|
||||
yarn workspace @actual-app/web build --mode=desktop # electron specific build
|
||||
|
||||
# required for running the sync-server server
|
||||
yarn workspace @actual-app/core build:browser
|
||||
yarn workspace @actual-app/web build:browser
|
||||
yarn build:browser
|
||||
yarn workspace @actual-app/sync-server build
|
||||
|
||||
# Emit @actual-app/core declarations so desktop-electron (which includes typings/window.ts) can build
|
||||
|
||||
@@ -25,6 +25,14 @@ module.exports = {
|
||||
outputGlob: BUILD_OUTPUT_GLOBS,
|
||||
},
|
||||
},
|
||||
// Not cached: the script stages files into public/ and build-stats/ that
|
||||
// fall outside BUILD_OUTPUT_GLOBS, so a cache hit would skip the side
|
||||
// effects.
|
||||
'build:browser': {
|
||||
type: 'npmScript',
|
||||
dependsOn: ['^build'],
|
||||
cache: false,
|
||||
},
|
||||
},
|
||||
cacheOptions: {
|
||||
cacheStorageConfig: {
|
||||
|
||||
25
package.json
@@ -24,18 +24,16 @@
|
||||
"start:server-dev": "NODE_ENV=development BROWSER_OPEN=localhost:5006 yarn npm-run-all --parallel 'start:server-monitor' 'start'",
|
||||
"start:desktop": "yarn desktop-dependencies && npm-run-all --parallel 'start:desktop-*'",
|
||||
"start:docs": "yarn workspace docs start",
|
||||
"desktop-dependencies": "npm-run-all --parallel rebuild-electron build:browser-backend build:plugins-service",
|
||||
"desktop-dependencies": "npm-run-all --parallel rebuild-electron build:plugins-service",
|
||||
"start:desktop-node": "yarn workspace @actual-app/core watch:node",
|
||||
"start:desktop-client": "yarn workspace @actual-app/web watch",
|
||||
"start:desktop-server-client": "yarn workspace @actual-app/web build:browser",
|
||||
"start:desktop-electron": "yarn workspace desktop-electron watch",
|
||||
"start:browser": "yarn workspace plugins-service build-dev && npm-run-all --parallel 'start:browser-*'",
|
||||
"start:browser": "npm-run-all --parallel 'start:browser-*' 'start:service-plugins'",
|
||||
"start:service-plugins": "yarn workspace plugins-service watch",
|
||||
"start:browser-backend": "yarn workspace @actual-app/core watch:browser",
|
||||
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
|
||||
"start:storybook": "yarn workspace @actual-app/components start:storybook",
|
||||
"build": "lage build",
|
||||
"build:browser-backend": "yarn workspace @actual-app/core build:browser",
|
||||
"build:server": "yarn build:browser && yarn workspace @actual-app/sync-server build",
|
||||
"build:browser": "./bin/package-browser",
|
||||
"build:desktop": "./bin/package-electron",
|
||||
@@ -54,7 +52,7 @@
|
||||
"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 && ./node_modules/.bin/electron-rebuild -m ./packages/desktop-electron -o better-sqlite3,bcrypt",
|
||||
"rebuild-electron": "./node_modules/.bin/electron-rebuild -m ./packages/desktop-electron -o better-sqlite3,bcrypt --build-from-source -f",
|
||||
"rebuild-node": "yarn workspace @actual-app/core rebuild",
|
||||
"lint": "oxfmt --check . && oxlint --type-aware --quiet",
|
||||
"lint:fix": "oxfmt . && oxlint --fix --type-aware --quiet",
|
||||
@@ -89,6 +87,23 @@
|
||||
"typescript": "^6.0.2",
|
||||
"vitest": "^4.1.2"
|
||||
},
|
||||
"dependenciesMeta": {
|
||||
"bcrypt": {
|
||||
"built": true
|
||||
},
|
||||
"better-sqlite3": {
|
||||
"built": true
|
||||
},
|
||||
"electron": {
|
||||
"built": true
|
||||
},
|
||||
"esbuild": {
|
||||
"built": true
|
||||
},
|
||||
"sharp": {
|
||||
"built": true
|
||||
}
|
||||
},
|
||||
"resolutions": {
|
||||
"adm-zip": "patch:adm-zip@npm%3A0.5.16#~/.yarn/patches/adm-zip-npm-0.5.16-4556fea098.patch",
|
||||
"minimatch@10.2.1": "10.2.5",
|
||||
|
||||
@@ -6,6 +6,11 @@ import { vi } from 'vitest';
|
||||
|
||||
import * as api from './index';
|
||||
|
||||
declare global {
|
||||
var IS_TESTING: boolean;
|
||||
var currentMonth: string | null;
|
||||
}
|
||||
|
||||
// In tests we run from source; loot-core's API fs uses __dirname (for the built dist/).
|
||||
// Mock the fs so path constants point at loot-core package root where migrations live.
|
||||
vi.mock(
|
||||
@@ -511,6 +516,29 @@ 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' });
|
||||
|
||||
@@ -13,6 +13,7 @@ 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';
|
||||
@@ -203,8 +204,8 @@ export function getAccountBalance(id: APIAccountEntity['id'], cutoff?: Date) {
|
||||
return send('api/account-balance', { id, cutoff });
|
||||
}
|
||||
|
||||
export function getCategoryGroups() {
|
||||
return send('api/category-groups-get');
|
||||
export function getCategoryGroups(options: { hidden?: boolean } = {}) {
|
||||
return send('api/category-groups-get', options);
|
||||
}
|
||||
|
||||
export function createCategoryGroup(group: Omit<APICategoryGroupEntity, 'id'>) {
|
||||
@@ -225,8 +226,8 @@ export function deleteCategoryGroup(
|
||||
return send('api/category-group-delete', { id, transferCategoryId });
|
||||
}
|
||||
|
||||
export function getCategories() {
|
||||
return send('api/categories-get', { grouped: false });
|
||||
export function getCategories(options: { hidden?: boolean } = {}) {
|
||||
return send('api/categories-get', { grouped: false, ...options });
|
||||
}
|
||||
|
||||
export function createCategory(category: Omit<APICategoryEntity, 'id'>) {
|
||||
@@ -247,6 +248,14 @@ 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');
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "26.4.0",
|
||||
"version": "26.5.2",
|
||||
"description": "An API for Actual",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@@ -10,7 +10,9 @@
|
||||
},
|
||||
"files": [
|
||||
"@types",
|
||||
"dist"
|
||||
"dist",
|
||||
"!@types/**/*.test.d.ts",
|
||||
"!@types/**/*.test.d.ts.map"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"types": "@types/index.d.ts",
|
||||
@@ -47,7 +49,8 @@
|
||||
"@actual-app/core": "workspace:*",
|
||||
"@actual-app/crdt": "workspace:*",
|
||||
"better-sqlite3": "^12.8.0",
|
||||
"compare-versions": "^6.1.1"
|
||||
"compare-versions": "^6.1.1",
|
||||
"uuid": "^14.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript/native-preview": "beta",
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
"**/node_modules/*",
|
||||
"dist",
|
||||
"@types",
|
||||
"*.test.ts",
|
||||
"*.config.ts",
|
||||
"*.config.mts"
|
||||
]
|
||||
|
||||
@@ -85,6 +85,12 @@ export default defineConfig({
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
// Each test loads a budget file and runs all DB migrations, which can be
|
||||
// slow on busy CI runners; the default 5s timeout is too tight and causes
|
||||
// flaky timeouts (and a cascade of unhandled rejections from in-flight work
|
||||
// continuing after teardown).
|
||||
testTimeout: 20_000,
|
||||
hookTimeout: 20_000,
|
||||
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
|
||||
// print only console.error
|
||||
return type === 'stderr';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actual-app/cli",
|
||||
"version": "26.4.0",
|
||||
"version": "26.5.2",
|
||||
"description": "CLI for Actual Budget",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
131
packages/cli/src/commands/categories.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import * as api from '@actual-app/api';
|
||||
import { Command } from 'commander';
|
||||
|
||||
import { printOutput } from '#output';
|
||||
|
||||
import { registerCategoriesCommand } from './categories';
|
||||
import { registerCategoryGroupsCommand } from './category-groups';
|
||||
|
||||
vi.mock('@actual-app/api', () => ({
|
||||
getCategories: vi.fn().mockResolvedValue([]),
|
||||
createCategory: vi.fn().mockResolvedValue('new-id'),
|
||||
updateCategory: vi.fn().mockResolvedValue(undefined),
|
||||
deleteCategory: vi.fn().mockResolvedValue(undefined),
|
||||
getCategoryGroups: vi.fn().mockResolvedValue([]),
|
||||
createCategoryGroup: vi.fn().mockResolvedValue('new-group-id'),
|
||||
updateCategoryGroup: vi.fn().mockResolvedValue(undefined),
|
||||
deleteCategoryGroup: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock('#connection', () => ({
|
||||
withConnection: vi.fn((_opts, fn) => fn()),
|
||||
}));
|
||||
|
||||
vi.mock('#output', () => ({
|
||||
printOutput: vi.fn(),
|
||||
}));
|
||||
|
||||
function createProgram(): Command {
|
||||
const program = new Command();
|
||||
program.option('--format <format>');
|
||||
program.option('--server-url <url>');
|
||||
program.option('--password <pw>');
|
||||
program.option('--session-token <token>');
|
||||
program.option('--sync-id <id>');
|
||||
program.option('--data-dir <dir>');
|
||||
program.option('--verbose');
|
||||
program.exitOverride();
|
||||
registerCategoriesCommand(program);
|
||||
registerCategoryGroupsCommand(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
async function run(args: string[]) {
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', ...args]);
|
||||
}
|
||||
|
||||
describe('categories commands', () => {
|
||||
let stderrSpy: ReturnType<typeof vi.spyOn>;
|
||||
let stdoutSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
stderrSpy = vi
|
||||
.spyOn(process.stderr, 'write')
|
||||
.mockImplementation(() => true);
|
||||
stdoutSpy = vi
|
||||
.spyOn(process.stdout, 'write')
|
||||
.mockImplementation(() => true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stderrSpy.mockRestore();
|
||||
stdoutSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('categories list', () => {
|
||||
it('asks the API to exclude hidden categories by default', async () => {
|
||||
await run(['categories', 'list']);
|
||||
|
||||
expect(api.getCategories).toHaveBeenCalledWith({ hidden: false });
|
||||
});
|
||||
|
||||
it('asks the API for all categories when --include-hidden is passed', async () => {
|
||||
await run(['categories', 'list', '--include-hidden']);
|
||||
|
||||
expect(api.getCategories).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
it('prints whatever the API returns', async () => {
|
||||
const visible = {
|
||||
id: '1',
|
||||
name: 'Visible',
|
||||
group_id: 'g1',
|
||||
hidden: false,
|
||||
};
|
||||
vi.mocked(api.getCategories).mockResolvedValue([visible]);
|
||||
|
||||
await run(['categories', 'list']);
|
||||
|
||||
expect(printOutput).toHaveBeenCalledWith([visible], undefined);
|
||||
});
|
||||
|
||||
it('passes format option to printOutput', async () => {
|
||||
vi.mocked(api.getCategories).mockResolvedValue([]);
|
||||
|
||||
await run(['--format', 'csv', 'categories', 'list']);
|
||||
|
||||
expect(printOutput).toHaveBeenCalledWith([], 'csv');
|
||||
});
|
||||
});
|
||||
|
||||
describe('category-groups list', () => {
|
||||
it('asks the API to exclude hidden groups by default', async () => {
|
||||
await run(['category-groups', 'list']);
|
||||
|
||||
expect(api.getCategoryGroups).toHaveBeenCalledWith({ hidden: false });
|
||||
});
|
||||
|
||||
it('asks the API for all groups when --include-hidden is passed', async () => {
|
||||
await run(['category-groups', 'list', '--include-hidden']);
|
||||
|
||||
expect(api.getCategoryGroups).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
it('prints whatever the API returns', async () => {
|
||||
const group = {
|
||||
id: 'g1',
|
||||
name: 'Group',
|
||||
is_income: false,
|
||||
hidden: false,
|
||||
categories: [{ id: 'c1', name: 'Cat', group_id: 'g1', hidden: false }],
|
||||
};
|
||||
vi.mocked(api.getCategoryGroups).mockResolvedValue([group]);
|
||||
|
||||
await run(['category-groups', 'list']);
|
||||
|
||||
expect(printOutput).toHaveBeenCalledWith([group], undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,13 +12,16 @@ export function registerCategoriesCommand(program: Command) {
|
||||
|
||||
categories
|
||||
.command('list')
|
||||
.description('List all categories')
|
||||
.action(async () => {
|
||||
.description('List categories (excludes hidden by default)')
|
||||
.option('--include-hidden', 'Include hidden categories', false)
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(
|
||||
opts,
|
||||
async () => {
|
||||
const result = await api.getCategories();
|
||||
const result = await api.getCategories(
|
||||
cmdOpts.includeHidden ? {} : { hidden: false },
|
||||
);
|
||||
printOutput(result, opts.format);
|
||||
},
|
||||
{ mutates: false },
|
||||
|
||||
@@ -12,13 +12,16 @@ export function registerCategoryGroupsCommand(program: Command) {
|
||||
|
||||
groups
|
||||
.command('list')
|
||||
.description('List all category groups')
|
||||
.action(async () => {
|
||||
.description('List category groups (excludes hidden by default)')
|
||||
.option('--include-hidden', 'Include hidden groups and categories', false)
|
||||
.action(async cmdOpts => {
|
||||
const opts = program.opts();
|
||||
await withConnection(
|
||||
opts,
|
||||
async () => {
|
||||
const result = await api.getCategoryGroups();
|
||||
const result = await api.getCategoryGroups(
|
||||
cmdOpts.includeHidden ? {} : { hidden: false },
|
||||
);
|
||||
printOutput(result, opts.format);
|
||||
},
|
||||
{ mutates: false },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$(dirname "$0")")"
|
||||
|
||||
@@ -7,20 +8,10 @@ if ! [ -x "$(command -v protoc)" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export PATH="$PWD/bin:$PATH"
|
||||
|
||||
protoc --plugin="protoc-gen-ts=../../node_modules/.bin/protoc-gen-ts" \
|
||||
--ts_opt=esModuleInterop=true \
|
||||
--ts_out="src/proto" \
|
||||
--js_out=import_style=commonjs,binary:src/proto \
|
||||
protoc --plugin="protoc-gen-es=../../node_modules/.bin/protoc-gen-es" \
|
||||
--es_opt=target=ts \
|
||||
--es_out="src/proto" \
|
||||
--proto_path=src/proto \
|
||||
sync.proto
|
||||
|
||||
../../node_modules/.bin/oxfmt src/proto/*.d.ts
|
||||
|
||||
for file in src/proto/*.d.ts; do
|
||||
{ echo "/* oxlint-disable typescript/no-namespace */"; sed 's/export class/export declare class/g' "$file"; } > "${file%.d.ts}.ts"
|
||||
rm "$file"
|
||||
done
|
||||
|
||||
echo 'One more step! Find the `var global = ...` declaration in src/proto/sync_pb.js and change it to `var global = globalThis;`'
|
||||
../../node_modules/.bin/oxfmt src/proto/*.ts
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
{
|
||||
"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",
|
||||
"!dist/**/*.test.d.ts",
|
||||
"!dist/**/*.test.d.ts.map",
|
||||
"!dist/**/*.spec.d.ts",
|
||||
"!dist/**/*.spec.d.ts.map"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
@@ -21,25 +27,26 @@
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build:node": "vite build",
|
||||
"proto:generate": "./bin/generate-proto",
|
||||
"build": "yarn run build:node && tsgo -p tsconfig.build.json --emitDeclarationOnly",
|
||||
"build": "yarn run build:node && tsgo -b",
|
||||
"test": "vitest --run",
|
||||
"typecheck": "tsgo -b"
|
||||
},
|
||||
"dependencies": {
|
||||
"google-protobuf": "^3.21.4",
|
||||
"murmurhash": "^2.0.1"
|
||||
"@bufbuild/protobuf": "^2.11.0",
|
||||
"murmurhash": "^2.0.1",
|
||||
"uuid": "^14.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/google-protobuf": "3.15.12",
|
||||
"@bufbuild/protoc-gen-es": "^2.11.0",
|
||||
"@typescript/native-preview": "beta",
|
||||
"protoc-gen-js": "3.21.4-4",
|
||||
"rollup-plugin-visualizer": "^7.0.1",
|
||||
"ts-protoc-gen": "0.15.0",
|
||||
"vite": "^8.0.5",
|
||||
"vitest": "^4.1.2"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import murmurhash from 'murmurhash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import type { TrieNode } from './merkle';
|
||||
|
||||
@@ -76,7 +77,7 @@ export function deserializeClock(clock: string): Clock {
|
||||
}
|
||||
|
||||
export function makeClientId() {
|
||||
return crypto.randomUUID().replace(/-/g, '').slice(-16);
|
||||
return uuidv4().replace(/-/g, '').slice(-16);
|
||||
}
|
||||
|
||||
const config = {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import './proto/sync_pb.js'; // Import for side effects
|
||||
import type * as SyncPb from './proto/sync_pb';
|
||||
|
||||
export {
|
||||
merkle,
|
||||
getClock,
|
||||
@@ -13,16 +10,17 @@ export {
|
||||
Timestamp,
|
||||
} from './crdt';
|
||||
|
||||
declare global {
|
||||
var proto: typeof SyncPb;
|
||||
}
|
||||
export {
|
||||
type EncryptedData,
|
||||
type Message,
|
||||
type MessageEnvelope,
|
||||
type SyncRequest,
|
||||
type SyncResponse,
|
||||
EncryptedDataSchema,
|
||||
MessageSchema,
|
||||
MessageEnvelopeSchema,
|
||||
SyncRequestSchema,
|
||||
SyncResponseSchema,
|
||||
} from './proto/sync_pb';
|
||||
|
||||
const { proto } = globalThis;
|
||||
|
||||
export const SyncRequest = proto.SyncRequest;
|
||||
export const SyncResponse = proto.SyncResponse;
|
||||
export const Message = proto.Message;
|
||||
export const MessageEnvelope = proto.MessageEnvelope;
|
||||
export const EncryptedData = proto.EncryptedData;
|
||||
|
||||
export const SyncProtoBuf = proto;
|
||||
export { create, fromBinary, toBinary } from '@bufbuild/protobuf';
|
||||
|
||||
@@ -21,6 +21,7 @@ message MessageEnvelope {
|
||||
}
|
||||
|
||||
message SyncRequest {
|
||||
reserved 4;
|
||||
repeated MessageEnvelope messages = 1;
|
||||
string fileId = 2;
|
||||
string groupId = 3;
|
||||
|
||||
@@ -1,217 +1,161 @@
|
||||
/* oxlint-disable typescript/no-namespace */
|
||||
// package:
|
||||
// file: sync.proto
|
||||
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
|
||||
// @generated from file sync.proto (syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import * as jspb from 'google-protobuf';
|
||||
import type { Message as Message$1 } from '@bufbuild/protobuf';
|
||||
import type { GenFile, GenMessage } from '@bufbuild/protobuf/codegenv2';
|
||||
import { fileDesc, messageDesc } from '@bufbuild/protobuf/codegenv2';
|
||||
|
||||
export declare class EncryptedData extends jspb.Message {
|
||||
getIv(): Uint8Array | string;
|
||||
getIv_asU8(): Uint8Array;
|
||||
getIv_asB64(): string;
|
||||
setIv(value: Uint8Array | string): void;
|
||||
/**
|
||||
* Describes the file sync.proto.
|
||||
*/
|
||||
export const file_sync: GenFile /*@__PURE__*/ = fileDesc(
|
||||
'CgpzeW5jLnByb3RvIjoKDUVuY3J5cHRlZERhdGESCgoCaXYYASABKAwSDwoHYXV0aFRhZxgCIAEoDBIMCgRkYXRhGAMgASgMIkYKB01lc3NhZ2USDwoHZGF0YXNldBgBIAEoCRILCgNyb3cYAiABKAkSDgoGY29sdW1uGAMgASgJEg0KBXZhbHVlGAQgASgJIkoKD01lc3NhZ2VFbnZlbG9wZRIRCgl0aW1lc3RhbXAYASABKAkSEwoLaXNFbmNyeXB0ZWQYAiABKAgSDwoHY29udGVudBgDIAEoDCJ2CgtTeW5jUmVxdWVzdBIiCghtZXNzYWdlcxgBIAMoCzIQLk1lc3NhZ2VFbnZlbG9wZRIOCgZmaWxlSWQYAiABKAkSDwoHZ3JvdXBJZBgDIAEoCRINCgVrZXlJZBgFIAEoCRINCgVzaW5jZRgGIAEoCUoECAQQBSJCCgxTeW5jUmVzcG9uc2USIgoIbWVzc2FnZXMYASADKAsyEC5NZXNzYWdlRW52ZWxvcGUSDgoGbWVya2xlGAIgASgJYgZwcm90bzM',
|
||||
);
|
||||
|
||||
getAuthtag(): Uint8Array | string;
|
||||
getAuthtag_asU8(): Uint8Array;
|
||||
getAuthtag_asB64(): string;
|
||||
setAuthtag(value: Uint8Array | string): void;
|
||||
/**
|
||||
* @generated from message EncryptedData
|
||||
*/
|
||||
export type EncryptedData = Message$1<'EncryptedData'> & {
|
||||
/**
|
||||
* @generated from field: bytes iv = 1;
|
||||
*/
|
||||
iv: Uint8Array;
|
||||
|
||||
getData(): Uint8Array | string;
|
||||
getData_asU8(): Uint8Array;
|
||||
getData_asB64(): string;
|
||||
setData(value: Uint8Array | string): void;
|
||||
/**
|
||||
* @generated from field: bytes authTag = 2;
|
||||
*/
|
||||
authTag: Uint8Array;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): EncryptedData.AsObject;
|
||||
static toObject(
|
||||
includeInstance: boolean,
|
||||
msg: EncryptedData,
|
||||
): EncryptedData.AsObject;
|
||||
static extensions: { [key: number]: jspb.ExtensionFieldInfo<jspb.Message> };
|
||||
static extensionsBinary: {
|
||||
[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>;
|
||||
};
|
||||
static serializeBinaryToWriter(
|
||||
message: EncryptedData,
|
||||
writer: jspb.BinaryWriter,
|
||||
): void;
|
||||
static deserializeBinary(bytes: Uint8Array): EncryptedData;
|
||||
static deserializeBinaryFromReader(
|
||||
message: EncryptedData,
|
||||
reader: jspb.BinaryReader,
|
||||
): EncryptedData;
|
||||
}
|
||||
/**
|
||||
* @generated from field: bytes data = 3;
|
||||
*/
|
||||
data: Uint8Array;
|
||||
};
|
||||
|
||||
export namespace EncryptedData {
|
||||
export type AsObject = {
|
||||
iv: Uint8Array | string;
|
||||
authtag: Uint8Array | string;
|
||||
data: Uint8Array | string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Describes the message EncryptedData.
|
||||
* Use `create(EncryptedDataSchema)` to create a new message.
|
||||
*/
|
||||
export const EncryptedDataSchema: GenMessage<EncryptedData> /*@__PURE__*/ =
|
||||
messageDesc(file_sync, 0);
|
||||
|
||||
export declare class Message extends jspb.Message {
|
||||
getDataset(): string;
|
||||
setDataset(value: string): void;
|
||||
/**
|
||||
* @generated from message Message
|
||||
*/
|
||||
export type Message = Message$1<'Message'> & {
|
||||
/**
|
||||
* @generated from field: string dataset = 1;
|
||||
*/
|
||||
dataset: string;
|
||||
|
||||
getRow(): string;
|
||||
setRow(value: string): void;
|
||||
/**
|
||||
* @generated from field: string row = 2;
|
||||
*/
|
||||
row: string;
|
||||
|
||||
getColumn(): string;
|
||||
setColumn(value: string): void;
|
||||
/**
|
||||
* @generated from field: string column = 3;
|
||||
*/
|
||||
column: string;
|
||||
|
||||
getValue(): string;
|
||||
setValue(value: string): void;
|
||||
/**
|
||||
* @generated from field: string value = 4;
|
||||
*/
|
||||
value: string;
|
||||
};
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): Message.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: Message): Message.AsObject;
|
||||
static extensions: { [key: number]: jspb.ExtensionFieldInfo<jspb.Message> };
|
||||
static extensionsBinary: {
|
||||
[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>;
|
||||
};
|
||||
static serializeBinaryToWriter(
|
||||
message: Message,
|
||||
writer: jspb.BinaryWriter,
|
||||
): void;
|
||||
static deserializeBinary(bytes: Uint8Array): Message;
|
||||
static deserializeBinaryFromReader(
|
||||
message: Message,
|
||||
reader: jspb.BinaryReader,
|
||||
): Message;
|
||||
}
|
||||
/**
|
||||
* Describes the message Message.
|
||||
* Use `create(MessageSchema)` to create a new message.
|
||||
*/
|
||||
export const MessageSchema: GenMessage<Message> /*@__PURE__*/ = messageDesc(
|
||||
file_sync,
|
||||
1,
|
||||
);
|
||||
|
||||
export namespace Message {
|
||||
export type AsObject = {
|
||||
dataset: string;
|
||||
row: string;
|
||||
column: string;
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* @generated from message MessageEnvelope
|
||||
*/
|
||||
export type MessageEnvelope = Message$1<'MessageEnvelope'> & {
|
||||
/**
|
||||
* @generated from field: string timestamp = 1;
|
||||
*/
|
||||
timestamp: string;
|
||||
|
||||
export declare class MessageEnvelope extends jspb.Message {
|
||||
getTimestamp(): string;
|
||||
setTimestamp(value: string): void;
|
||||
/**
|
||||
* @generated from field: bool isEncrypted = 2;
|
||||
*/
|
||||
isEncrypted: boolean;
|
||||
|
||||
getIsencrypted(): boolean;
|
||||
setIsencrypted(value: boolean): void;
|
||||
/**
|
||||
* @generated from field: bytes content = 3;
|
||||
*/
|
||||
content: Uint8Array;
|
||||
};
|
||||
|
||||
getContent(): Uint8Array | string;
|
||||
getContent_asU8(): Uint8Array;
|
||||
getContent_asB64(): string;
|
||||
setContent(value: Uint8Array | string): void;
|
||||
/**
|
||||
* Describes the message MessageEnvelope.
|
||||
* Use `create(MessageEnvelopeSchema)` to create a new message.
|
||||
*/
|
||||
export const MessageEnvelopeSchema: GenMessage<MessageEnvelope> /*@__PURE__*/ =
|
||||
messageDesc(file_sync, 2);
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): MessageEnvelope.AsObject;
|
||||
static toObject(
|
||||
includeInstance: boolean,
|
||||
msg: MessageEnvelope,
|
||||
): MessageEnvelope.AsObject;
|
||||
static extensions: { [key: number]: jspb.ExtensionFieldInfo<jspb.Message> };
|
||||
static extensionsBinary: {
|
||||
[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>;
|
||||
};
|
||||
static serializeBinaryToWriter(
|
||||
message: MessageEnvelope,
|
||||
writer: jspb.BinaryWriter,
|
||||
): void;
|
||||
static deserializeBinary(bytes: Uint8Array): MessageEnvelope;
|
||||
static deserializeBinaryFromReader(
|
||||
message: MessageEnvelope,
|
||||
reader: jspb.BinaryReader,
|
||||
): MessageEnvelope;
|
||||
}
|
||||
/**
|
||||
* @generated from message SyncRequest
|
||||
*/
|
||||
export type SyncRequest = Message$1<'SyncRequest'> & {
|
||||
/**
|
||||
* @generated from field: repeated MessageEnvelope messages = 1;
|
||||
*/
|
||||
messages: MessageEnvelope[];
|
||||
|
||||
export namespace MessageEnvelope {
|
||||
export type AsObject = {
|
||||
timestamp: string;
|
||||
isencrypted: boolean;
|
||||
content: Uint8Array | string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* @generated from field: string fileId = 2;
|
||||
*/
|
||||
fileId: string;
|
||||
|
||||
export declare class SyncRequest extends jspb.Message {
|
||||
clearMessagesList(): void;
|
||||
getMessagesList(): Array<MessageEnvelope>;
|
||||
setMessagesList(value: Array<MessageEnvelope>): void;
|
||||
addMessages(value?: MessageEnvelope, index?: number): MessageEnvelope;
|
||||
/**
|
||||
* @generated from field: string groupId = 3;
|
||||
*/
|
||||
groupId: string;
|
||||
|
||||
getFileid(): string;
|
||||
setFileid(value: string): void;
|
||||
/**
|
||||
* @generated from field: string keyId = 5;
|
||||
*/
|
||||
keyId: string;
|
||||
|
||||
getGroupid(): string;
|
||||
setGroupid(value: string): void;
|
||||
/**
|
||||
* @generated from field: string since = 6;
|
||||
*/
|
||||
since: string;
|
||||
};
|
||||
|
||||
getKeyid(): string;
|
||||
setKeyid(value: string): void;
|
||||
/**
|
||||
* Describes the message SyncRequest.
|
||||
* Use `create(SyncRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const SyncRequestSchema: GenMessage<SyncRequest> /*@__PURE__*/ =
|
||||
messageDesc(file_sync, 3);
|
||||
|
||||
getSince(): string;
|
||||
setSince(value: string): void;
|
||||
/**
|
||||
* @generated from message SyncResponse
|
||||
*/
|
||||
export type SyncResponse = Message$1<'SyncResponse'> & {
|
||||
/**
|
||||
* @generated from field: repeated MessageEnvelope messages = 1;
|
||||
*/
|
||||
messages: MessageEnvelope[];
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): SyncRequest.AsObject;
|
||||
static toObject(
|
||||
includeInstance: boolean,
|
||||
msg: SyncRequest,
|
||||
): SyncRequest.AsObject;
|
||||
static extensions: { [key: number]: jspb.ExtensionFieldInfo<jspb.Message> };
|
||||
static extensionsBinary: {
|
||||
[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>;
|
||||
};
|
||||
static serializeBinaryToWriter(
|
||||
message: SyncRequest,
|
||||
writer: jspb.BinaryWriter,
|
||||
): void;
|
||||
static deserializeBinary(bytes: Uint8Array): SyncRequest;
|
||||
static deserializeBinaryFromReader(
|
||||
message: SyncRequest,
|
||||
reader: jspb.BinaryReader,
|
||||
): SyncRequest;
|
||||
}
|
||||
/**
|
||||
* @generated from field: string merkle = 2;
|
||||
*/
|
||||
merkle: string;
|
||||
};
|
||||
|
||||
export namespace SyncRequest {
|
||||
export type AsObject = {
|
||||
messagesList: Array<MessageEnvelope.AsObject>;
|
||||
fileid: string;
|
||||
groupid: string;
|
||||
keyid: string;
|
||||
since: string;
|
||||
};
|
||||
}
|
||||
|
||||
export declare class SyncResponse extends jspb.Message {
|
||||
clearMessagesList(): void;
|
||||
getMessagesList(): Array<MessageEnvelope>;
|
||||
setMessagesList(value: Array<MessageEnvelope>): void;
|
||||
addMessages(value?: MessageEnvelope, index?: number): MessageEnvelope;
|
||||
|
||||
getMerkle(): string;
|
||||
setMerkle(value: string): void;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): SyncResponse.AsObject;
|
||||
static toObject(
|
||||
includeInstance: boolean,
|
||||
msg: SyncResponse,
|
||||
): SyncResponse.AsObject;
|
||||
static extensions: { [key: number]: jspb.ExtensionFieldInfo<jspb.Message> };
|
||||
static extensionsBinary: {
|
||||
[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>;
|
||||
};
|
||||
static serializeBinaryToWriter(
|
||||
message: SyncResponse,
|
||||
writer: jspb.BinaryWriter,
|
||||
): void;
|
||||
static deserializeBinary(bytes: Uint8Array): SyncResponse;
|
||||
static deserializeBinaryFromReader(
|
||||
message: SyncResponse,
|
||||
reader: jspb.BinaryReader,
|
||||
): SyncResponse;
|
||||
}
|
||||
|
||||
export namespace SyncResponse {
|
||||
export type AsObject = {
|
||||
messagesList: Array<MessageEnvelope.AsObject>;
|
||||
merkle: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Describes the message SyncResponse.
|
||||
* Use `create(SyncResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const SyncResponseSchema: GenMessage<SyncResponse> /*@__PURE__*/ =
|
||||
messageDesc(file_sync, 4);
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"emitDeclarationOnly": false
|
||||
},
|
||||
"exclude": ["**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
@@ -4,8 +4,8 @@
|
||||
"rootDir": "./src",
|
||||
"composite": true,
|
||||
"target": "ES2021",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"noEmit": false,
|
||||
"emitDeclarationOnly": true,
|
||||
"declaration": true,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { defineConfig } from 'vite';
|
||||
export default defineConfig({
|
||||
ssr: {
|
||||
noExternal: true,
|
||||
external: ['google-protobuf', 'murmurhash'],
|
||||
external: ['@bufbuild/protobuf', 'murmurhash'],
|
||||
},
|
||||
build: {
|
||||
ssr: true,
|
||||
@@ -16,7 +16,7 @@ export default defineConfig({
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, 'src/index.ts'),
|
||||
formats: ['cjs'],
|
||||
formats: ['es'],
|
||||
fileName: () => 'index.js',
|
||||
},
|
||||
},
|
||||
|
||||
1
packages/desktop-client/.gitignore
vendored
@@ -8,6 +8,7 @@ coverage
|
||||
test-results
|
||||
playwright-report
|
||||
blob-report
|
||||
.playwright-cli
|
||||
|
||||
# production
|
||||
build
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/sh -ex
|
||||
|
||||
ROOT=`dirname $0`
|
||||
cd "$ROOT/.."
|
||||
|
||||
echo "Building the browser..."
|
||||
|
||||
rm -fr build
|
||||
|
||||
export REACT_APP_BACKEND_WORKER_HASH=`ls "$ROOT"/../public/kcab/kcab.worker.*.js | sed 's/.*kcab\.worker\.\(.*\)\.js/\1/'`
|
||||
|
||||
yarn build --mode=browser
|
||||
|
||||
rm -fr build-stats
|
||||
mkdir build-stats
|
||||
mv build/kcab/stats.json build-stats/loot-core-stats.json
|
||||
mv ./stats.json build-stats/web-stats.json
|
||||
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 81 KiB |
@@ -1,5 +1,5 @@
|
||||
import { test as base, expect as baseExpect } from '@playwright/test';
|
||||
import type { Browser, Locator } from '@playwright/test';
|
||||
import type { Browser, Locator, Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Disable CSS transitions and animations globally in e2e (non-VRT) runs.
|
||||
@@ -51,7 +51,7 @@ export const test = process.env.VRT
|
||||
});
|
||||
|
||||
export const expect = baseExpect.extend({
|
||||
async toMatchThemeScreenshots(locator: Locator) {
|
||||
async toMatchThemeScreenshots(target: Locator | Page) {
|
||||
// Disable screenshot assertions in regular e2e tests;
|
||||
// only enable them when doing VRT tests
|
||||
if (!process.env.VRT) {
|
||||
@@ -62,38 +62,33 @@ export const expect = baseExpect.extend({
|
||||
}
|
||||
|
||||
const config = {
|
||||
mask: [locator.locator('[data-vrt-mask="true"]')],
|
||||
mask: [target.locator('[data-vrt-mask="true"]')],
|
||||
maxDiffPixels: 5,
|
||||
};
|
||||
|
||||
// Get the data-theme attribute from page.
|
||||
// If there is a page() function, it means that the locator
|
||||
// is not a page object but a locator object.
|
||||
const dataThemeLocator =
|
||||
typeof locator.page === 'function'
|
||||
? locator.page().locator('[data-theme]')
|
||||
: locator.locator('[data-theme]');
|
||||
const page: Page = 'page' in target ? target.page() : target;
|
||||
const dataThemeLocator = page.locator('[data-theme]');
|
||||
|
||||
// Check lightmode
|
||||
await locator.evaluate(() => window.Actual.setTheme('auto'));
|
||||
await page.evaluate(() => window.Actual.setTheme('auto'));
|
||||
await baseExpect(dataThemeLocator).toHaveAttribute('data-theme', 'auto');
|
||||
await baseExpect(locator).toHaveScreenshot(config);
|
||||
await baseExpect(target).toHaveScreenshot(config);
|
||||
|
||||
// Switch to darkmode and check
|
||||
await locator.evaluate(() => window.Actual.setTheme('dark'));
|
||||
await page.evaluate(() => window.Actual.setTheme('dark'));
|
||||
await baseExpect(dataThemeLocator).toHaveAttribute('data-theme', 'dark');
|
||||
await baseExpect(locator).toHaveScreenshot(config);
|
||||
await baseExpect(target).toHaveScreenshot(config);
|
||||
|
||||
// Switch to midnight theme and check
|
||||
await locator.evaluate(() => window.Actual.setTheme('midnight'));
|
||||
await page.evaluate(() => window.Actual.setTheme('midnight'));
|
||||
await baseExpect(dataThemeLocator).toHaveAttribute(
|
||||
'data-theme',
|
||||
'midnight',
|
||||
);
|
||||
await baseExpect(locator).toHaveScreenshot(config);
|
||||
await baseExpect(target).toHaveScreenshot(config);
|
||||
|
||||
// Switch back to lightmode
|
||||
await locator.evaluate(() => window.Actual.setTheme('auto'));
|
||||
await page.evaluate(() => window.Actual.setTheme('auto'));
|
||||
return {
|
||||
message: () => 'pass',
|
||||
pass: true,
|
||||
|
||||
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 22 KiB |