Compare commits
82 Commits
show-total
...
v25.10.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7846d2e787 | ||
|
|
ca6d80461a | ||
|
|
fa14cbb697 | ||
|
|
1210a74b4a | ||
|
|
534c1e6680 | ||
|
|
14d436712a | ||
|
|
e9f3925124 | ||
|
|
f28229be99 | ||
|
|
1fc922c672 | ||
|
|
c712217a7c | ||
|
|
3559b2df3a | ||
|
|
6365a8f4bb | ||
|
|
14426b64fd | ||
|
|
65790d4b9c | ||
|
|
9af4ba4d07 | ||
|
|
28caf8eaf9 | ||
|
|
81160256bc | ||
|
|
ca5378c0e8 | ||
|
|
08b5b7fdc7 | ||
|
|
67c0b6911b | ||
|
|
4e9e153989 | ||
|
|
b0321ee265 | ||
|
|
753a105b3d | ||
|
|
5a888d44b9 | ||
|
|
7a4799de94 | ||
|
|
4ad369cd8f | ||
|
|
2c9a66cec6 | ||
|
|
6e96b81799 | ||
|
|
f89d4fd13d | ||
|
|
cc0812113a | ||
|
|
59724d445f | ||
|
|
6b99497d5d | ||
|
|
5f5457b226 | ||
|
|
4bdcb27573 | ||
|
|
8ae070ab12 | ||
|
|
0ca5bec094 | ||
|
|
988bc21818 | ||
|
|
f4419b96de | ||
|
|
e30a38ced8 | ||
|
|
98b91cfb8d | ||
|
|
942d3ea4d5 | ||
|
|
3c9b70df79 | ||
|
|
5c18b53888 | ||
|
|
413398531c | ||
|
|
e4c3d4e12a | ||
|
|
91b838c539 | ||
|
|
9eb0e04c6a | ||
|
|
14bf3d611c | ||
|
|
34b6599da3 | ||
|
|
bc1cd9023c | ||
|
|
5ae9176f5e | ||
|
|
2ed908aff4 | ||
|
|
3318dd56e9 | ||
|
|
00ab11cc40 | ||
|
|
25c83eb64d | ||
|
|
7a420b79f2 | ||
|
|
d2cfedf5e4 | ||
|
|
00a4cfcabf | ||
|
|
a18a05f55a | ||
|
|
b399f290a6 | ||
|
|
7c07295448 | ||
|
|
510dd31de6 | ||
|
|
8e5a88bc55 | ||
|
|
bbf91ccbca | ||
|
|
58bc14e1b3 | ||
|
|
de2966a06c | ||
|
|
90b859fd74 | ||
|
|
fafcee071d | ||
|
|
ed40901534 | ||
|
|
338093836b | ||
|
|
4df05aa37c | ||
|
|
5459b8baca | ||
|
|
073d91a7b7 | ||
|
|
58a638cee2 | ||
|
|
23f1bae7db | ||
|
|
57240284a3 | ||
|
|
6c6d8931bb | ||
|
|
cae8fa4e6f | ||
|
|
48ae371ecc | ||
|
|
e8d93fb797 | ||
|
|
6790f99de2 | ||
|
|
68f0b05aed |
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description:
|
||||
description:
|
||||
globs: *.ts,*.tsx
|
||||
alwaysApply: false
|
||||
---
|
||||
@@ -13,6 +13,7 @@ Code Style and Structure
|
||||
- Prefer iteration and modularization over code duplication.
|
||||
- Use descriptive variable names with auxiliary verbs (e.g., isLoaded, hasError).
|
||||
- Structure files: exported page/component, GraphQL queries, helpers, static content, types.
|
||||
- When creating a new component, place it in its own file rather than grouping multiple components in a single file.
|
||||
|
||||
Naming Conventions
|
||||
|
||||
@@ -20,7 +21,7 @@ Naming Conventions
|
||||
|
||||
TypeScript Usage
|
||||
|
||||
- Use TypeScript for all code; prefer interfaces over types.
|
||||
- Use TypeScript for all code; prefer types over interfaces.
|
||||
- Avoid enums; use objects or maps instead.
|
||||
- Avoid using `any` or `unknown` unless absolutely necessary. Look for type definitions in the codebase instead.
|
||||
- Avoid type assertions with `as` or `!`; prefer using `satisfies`.
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -2,6 +2,7 @@ name: Bug Report
|
||||
description: File a bug report also known as an issue or problem.
|
||||
title: '[Bug]: '
|
||||
labels: ['needs triage', 'bug']
|
||||
type: Bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -2,6 +2,7 @@ name: Feature request
|
||||
description: Request a missing feature
|
||||
title: '[Feature] '
|
||||
labels: ['feature']
|
||||
type: Feature
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
117
.github/actions/get-next-package-version.js
vendored
@@ -1,117 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// This script is used in GitHub Actions to get the next version based on the current package.json version.
|
||||
// It supports three types of versioning: nightly, hotfix, and monthly.
|
||||
|
||||
const { parseArgs } = require('node:util');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const args = process.argv;
|
||||
|
||||
const options = {
|
||||
'package-json': {
|
||||
type: 'string',
|
||||
short: 'p',
|
||||
},
|
||||
type: {
|
||||
type: 'string', // nightly, hotfix, monthly, auto
|
||||
short: 't',
|
||||
},
|
||||
update: {
|
||||
type: 'boolean',
|
||||
short: 'u',
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
|
||||
const { values } = parseArgs({
|
||||
args,
|
||||
options,
|
||||
allowPositionals: true,
|
||||
});
|
||||
|
||||
if (!values['package-json']) {
|
||||
console.error(
|
||||
'Please specify the path to package.json using --package-json or -p option.',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const packageJsonPath = values['package-json'];
|
||||
|
||||
// Read and parse package.json
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
const currentVersion = packageJson.version;
|
||||
|
||||
// Parse year and month from version (e.g. 25.5.1 -> year=2025, month=5)
|
||||
const versionParts = currentVersion.split('.');
|
||||
const versionYear = parseInt(versionParts[0]);
|
||||
const versionMonth = parseInt(versionParts[1]);
|
||||
const versionHotfix = parseInt(versionParts[2]);
|
||||
|
||||
// Create date and add 1 month
|
||||
const versionDate = new Date(2000 + versionYear, versionMonth - 1, 1); // month is 0-indexed
|
||||
const nextVersionMonthDate = new Date(
|
||||
versionDate.getFullYear(),
|
||||
versionDate.getMonth() + 1,
|
||||
1,
|
||||
);
|
||||
|
||||
// Format back to YY.M format
|
||||
const nextVersionYear = nextVersionMonthDate
|
||||
.getFullYear()
|
||||
.toString()
|
||||
.slice(nextVersionMonthDate.getFullYear() < 2100 ? -2 : -3);
|
||||
const nextVersionMonth = nextVersionMonthDate.getMonth() + 1; // Convert back to 1-indexed
|
||||
|
||||
// Get current date string
|
||||
const currentDate = new Date();
|
||||
const currentDateString = currentDate
|
||||
.toISOString()
|
||||
.split('T')[0]
|
||||
.replaceAll('-', '');
|
||||
|
||||
if (values.type === 'auto') {
|
||||
if (currentDate.getDate() <= 25) {
|
||||
values.type = 'hotfix';
|
||||
} else {
|
||||
values.type = 'monthly';
|
||||
}
|
||||
}
|
||||
|
||||
let newVersion;
|
||||
switch (values.type) {
|
||||
case 'nightly': {
|
||||
newVersion = `${nextVersionYear}.${nextVersionMonth}.0-nightly.${currentDateString}`;
|
||||
break;
|
||||
}
|
||||
case 'hotfix': {
|
||||
newVersion = `${versionYear}.${versionMonth}.${versionHotfix + 1}`;
|
||||
break;
|
||||
}
|
||||
case 'monthly': {
|
||||
newVersion = `${nextVersionYear}.${nextVersionMonth}.0`;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.error(
|
||||
'Invalid type specified. Use "auto", "nightly", "hotfix", or "monthly".',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.stdout.write(newVersion); // return the new version to stdout
|
||||
|
||||
if (values.update) {
|
||||
packageJson.version = newVersion;
|
||||
fs.writeFileSync(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2) + '\n',
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
6
.github/scripts/count-points.mjs
vendored
@@ -2,7 +2,7 @@ import { Octokit } from '@octokit/rest';
|
||||
import { minimatch } from 'minimatch';
|
||||
import pLimit from 'p-limit';
|
||||
|
||||
const limit = pLimit(30);
|
||||
const limit = pLimit(50);
|
||||
|
||||
/** Repository-specific configuration for points calculation */
|
||||
const REPOSITORY_CONFIG = new Map([
|
||||
@@ -129,13 +129,13 @@ async function countContributorPoints(repo) {
|
||||
// Get all PRs using search
|
||||
const searchQuery = `repo:${owner}/${repo} is:pr is:merged merged:${since.toISOString()}..${until.toISOString()}`;
|
||||
const recentPRs = await octokit.paginate(
|
||||
octokit.search.issuesAndPullRequests,
|
||||
'GET /search/issues',
|
||||
{
|
||||
q: searchQuery,
|
||||
per_page: 100,
|
||||
advanced_search: true,
|
||||
},
|
||||
response => response.data,
|
||||
response => response.data.filter(pr => pr.number),
|
||||
);
|
||||
|
||||
// Get reviews and PR details for each PR
|
||||
|
||||
2
.github/workflows/electron-master.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- ubuntu-22.04
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
2
.github/workflows/electron-pr.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- ubuntu-22.04
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
2
.github/workflows/generate-release-pr.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
if [[ -n "${{ github.event.inputs.version }}" ]]; then
|
||||
version="${{ github.event.inputs.version }}"
|
||||
else
|
||||
version=$(node ./.github/actions/get-next-package-version.js \
|
||||
version=$(node ./packages/ci-actions/bin/get-next-package-version.js \
|
||||
--package-json "./packages/$pkg/package.json" \
|
||||
--type auto \
|
||||
--update)
|
||||
|
||||
@@ -20,9 +20,9 @@ jobs:
|
||||
- name: Update package versions
|
||||
run: |
|
||||
# Get new nightly versions
|
||||
NEW_WEB_VERSION=$(node ./.github/actions/get-next-package-version.js --package-json ./packages/desktop-client/package.json --type nightly)
|
||||
NEW_SYNC_VERSION=$(node ./.github/actions/get-next-package-version.js --package-json ./packages/sync-server/package.json --type nightly)
|
||||
NEW_API_VERSION=$(node ./.github/actions/get-next-package-version.js --package-json ./packages/api/package.json --type nightly)
|
||||
NEW_WEB_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/desktop-client/package.json --type nightly)
|
||||
NEW_SYNC_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/sync-server/package.json --type nightly)
|
||||
NEW_API_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/api/package.json --type nightly)
|
||||
|
||||
# Set package versions
|
||||
npm version $NEW_WEB_VERSION --no-git-tag-version --workspace=@actual-app/web --no-workspaces-update
|
||||
|
||||
@@ -14,6 +14,8 @@ git pull
|
||||
popd > /dev/null
|
||||
packages/desktop-client/bin/remove-untranslated-languages
|
||||
|
||||
export NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
yarn workspace loot-core build:browser
|
||||
yarn workspace @actual-app/web build:browser
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ git pull
|
||||
popd > /dev/null
|
||||
packages/desktop-client/bin/remove-untranslated-languages
|
||||
|
||||
export NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
yarn workspace loot-core build:node
|
||||
yarn workspace @actual-app/web build --mode=desktop # electron specific build
|
||||
|
||||
|
||||
@@ -154,9 +154,6 @@ export default pluginTypescript.config(
|
||||
{
|
||||
plugins: {
|
||||
actual: pluginActual,
|
||||
'react-hooks': pluginReactHooks,
|
||||
'jsx-a11y': pluginJSXA11y,
|
||||
'typescript-paths': pluginTypescriptPaths,
|
||||
},
|
||||
rules: {
|
||||
'actual/no-untranslated-strings': 'error',
|
||||
@@ -165,6 +162,10 @@ export default pluginTypescript.config(
|
||||
},
|
||||
{
|
||||
files: ['**/*.{js,ts,jsx,tsx}'],
|
||||
plugins: {
|
||||
'jsx-a11y': pluginJSXA11y,
|
||||
'react-hooks': pluginReactHooks,
|
||||
},
|
||||
rules: {
|
||||
// http://eslint.org/docs/rules/
|
||||
'array-callback-return': 'warn',
|
||||
@@ -450,6 +451,7 @@ export default pluginTypescript.config(
|
||||
|
||||
'actual/typography': 'warn',
|
||||
'actual/prefer-if-statement': 'warn',
|
||||
'actual/prefer-logger-over-console': 'error',
|
||||
|
||||
// Note: base rule explicitly disabled in favor of the TS one
|
||||
'no-unused-vars': 'off',
|
||||
@@ -630,6 +632,9 @@ export default pluginTypescript.config(
|
||||
},
|
||||
{
|
||||
files: ['packages/desktop-client/**/*.{js,ts,jsx,tsx}'],
|
||||
plugins: {
|
||||
'typescript-paths': pluginTypescriptPaths,
|
||||
},
|
||||
rules: {
|
||||
'typescript-paths/absolute-parent-import': [
|
||||
'error',
|
||||
@@ -771,6 +776,7 @@ export default pluginTypescript.config(
|
||||
rules: {
|
||||
'actual/typography': 'off',
|
||||
'actual/no-untranslated-strings': 'off',
|
||||
'actual/prefer-logger-over-console': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
10
package.json
@@ -57,15 +57,15 @@
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@types/node": "^22.17.0",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"@typescript-eslint/parser": "^8.42.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-import-resolver-typescript": "^4.3.5",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-hooks": "^6.0.0-rc.2",
|
||||
"eslint-plugin-typescript-paths": "^0.0.33",
|
||||
"globals": "^15.15.0",
|
||||
"html-to-image": "^1.11.13",
|
||||
@@ -80,7 +80,7 @@
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.32.1",
|
||||
"typescript-eslint": "^8.42.0",
|
||||
"typescript-strict-plugin": "^2.4.4"
|
||||
},
|
||||
"resolutions": {
|
||||
@@ -99,7 +99,7 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.1",
|
||||
"browserslist": [
|
||||
"electron 24.0",
|
||||
"electron >= 35.0",
|
||||
"defaults"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -42,7 +42,11 @@ export async function init(config: InitConfig = {}) {
|
||||
|
||||
export async function shutdown() {
|
||||
if (actualApp) {
|
||||
await actualApp.send('sync');
|
||||
try {
|
||||
await actualApp.send('sync');
|
||||
} catch (e) {
|
||||
// most likely that no budget is loaded, so the sync failed
|
||||
}
|
||||
await actualApp.send('close-budget');
|
||||
actualApp = null;
|
||||
}
|
||||
|
||||
@@ -740,3 +740,122 @@ describe('API CRUD operations', () => {
|
||||
expect(transactions[0].notes).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
//apis: createSchedule, getSchedules, updateSchedule, deleteSchedule
|
||||
test('Schedules: successfully complete schedules operations', async () => {
|
||||
await api.loadBudget(budgetName);
|
||||
//test a schedule with a recuring configuration
|
||||
const ScheduleId1 = await api.createSchedule({
|
||||
name: 'test-schedule 1',
|
||||
posts_transaction: true,
|
||||
// amount: -5000,
|
||||
amountOp: 'is',
|
||||
date: {
|
||||
frequency: 'monthly',
|
||||
interval: 1,
|
||||
start: '2025-06-13',
|
||||
patterns: [],
|
||||
skipWeekend: false,
|
||||
weekendSolveMode: 'after',
|
||||
endMode: 'never',
|
||||
},
|
||||
});
|
||||
//test the creation of non recurring schedule
|
||||
const ScheduleId2 = await api.createSchedule({
|
||||
name: 'test-schedule 2',
|
||||
posts_transaction: false,
|
||||
amount: 4000,
|
||||
amountOp: 'is',
|
||||
date: '2025-06-13',
|
||||
});
|
||||
let schedules = await api.getSchedules();
|
||||
|
||||
// Schedules successfully created
|
||||
expect(schedules).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'test-schedule 1',
|
||||
posts_transaction: true,
|
||||
// amount: -5000,
|
||||
amountOp: 'is',
|
||||
date: {
|
||||
frequency: 'monthly',
|
||||
interval: 1,
|
||||
start: '2025-06-13',
|
||||
patterns: [],
|
||||
skipWeekend: false,
|
||||
weekendSolveMode: 'after',
|
||||
endMode: 'never',
|
||||
},
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: 'test-schedule 2',
|
||||
posts_transaction: false,
|
||||
amount: 4000,
|
||||
amountOp: 'is',
|
||||
date: '2025-06-13',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
//check getIDByName works on schedules
|
||||
expect(await api.getIDByName('schedules', 'test-schedule 1')).toEqual(
|
||||
ScheduleId1,
|
||||
);
|
||||
expect(await api.getIDByName('schedules', 'test-schedule 2')).toEqual(
|
||||
ScheduleId2,
|
||||
);
|
||||
|
||||
//check getIDByName works on accounts
|
||||
const schedAccountId1 = await api.createAccount(
|
||||
{ name: 'sched-test-account1', offbudget: true },
|
||||
1000,
|
||||
);
|
||||
|
||||
expect(await api.getIDByName('accounts', 'sched-test-account1')).toEqual(
|
||||
schedAccountId1,
|
||||
);
|
||||
|
||||
//check getIDByName works on payees
|
||||
const schedPayeeId1 = await api.createPayee({ name: 'sched-test-payee1' });
|
||||
|
||||
expect(await api.getIDByName('payees', 'sched-test-payee1')).toEqual(
|
||||
schedPayeeId1,
|
||||
);
|
||||
await api.updateSchedule(ScheduleId1, {
|
||||
amount: -10000,
|
||||
account: schedAccountId1,
|
||||
});
|
||||
await api.deleteSchedule(ScheduleId2);
|
||||
|
||||
// schedules successfully updated, and one of them deleted
|
||||
await api.updateSchedule(ScheduleId1, {
|
||||
amount: -10000,
|
||||
account: schedAccountId1,
|
||||
payee: schedPayeeId1,
|
||||
});
|
||||
await api.deleteSchedule(ScheduleId2);
|
||||
|
||||
schedules = await api.getSchedules();
|
||||
expect(schedules).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: ScheduleId1,
|
||||
posts_transaction: true,
|
||||
amount: -10000,
|
||||
account: schedAccountId1,
|
||||
payee: schedPayeeId1,
|
||||
amountOp: 'is',
|
||||
date: {
|
||||
frequency: 'monthly',
|
||||
interval: 1,
|
||||
start: '2025-06-13',
|
||||
patterns: [],
|
||||
skipWeekend: false,
|
||||
weekendSolveMode: 'after',
|
||||
endMode: 'never',
|
||||
},
|
||||
}),
|
||||
expect.not.objectContaining({ id: ScheduleId2 }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -96,6 +96,7 @@ export function addTransactions(
|
||||
|
||||
export interface ImportTransactionsOpts {
|
||||
defaultCleared?: boolean;
|
||||
dryRun?: boolean;
|
||||
}
|
||||
|
||||
export function importTransactions(
|
||||
@@ -103,11 +104,13 @@ export function importTransactions(
|
||||
transactions: ImportTransactionEntity[],
|
||||
opts: ImportTransactionsOpts = {
|
||||
defaultCleared: true,
|
||||
dryRun: false,
|
||||
},
|
||||
) {
|
||||
return send('api/transactions-import', {
|
||||
accountId,
|
||||
transactions,
|
||||
isPreview: opts.dryRun,
|
||||
opts,
|
||||
});
|
||||
}
|
||||
@@ -239,3 +242,31 @@ export function holdBudgetForNextMonth(month, amount) {
|
||||
export function resetBudgetHold(month) {
|
||||
return send('api/budget-reset-hold', { month });
|
||||
}
|
||||
|
||||
export function createSchedule(schedule) {
|
||||
return send('api/schedule-create', schedule);
|
||||
}
|
||||
|
||||
export function updateSchedule(id, fields, resetNextDate?: boolean) {
|
||||
return send('api/schedule-update', {
|
||||
id,
|
||||
fields,
|
||||
resetNextDate,
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteSchedule(scheduleId) {
|
||||
return send('api/schedule-delete', scheduleId);
|
||||
}
|
||||
|
||||
export function getSchedules() {
|
||||
return send('api/schedules-get');
|
||||
}
|
||||
|
||||
export function getIDByName(type, name) {
|
||||
return send('api/get-id-by-name', { type, name });
|
||||
}
|
||||
|
||||
export function getServerVersion() {
|
||||
return send('api/get-server-version');
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "25.8.0",
|
||||
"version": "25.10.0",
|
||||
"license": "MIT",
|
||||
"description": "An API for Actual",
|
||||
"engines": {
|
||||
|
||||
73
packages/ci-actions/bin/get-next-package-version.js
Executable file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// This script is used in GitHub Actions to get the next version based on the current package.json version.
|
||||
// It supports three types of versioning: nightly, hotfix, and monthly.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import { parseArgs } from 'node:util';
|
||||
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { getNextVersion } from '../src/versions/get-next-package-version.js';
|
||||
|
||||
const args = process.argv;
|
||||
|
||||
const options = {
|
||||
'package-json': {
|
||||
type: 'string',
|
||||
short: 'p',
|
||||
},
|
||||
type: {
|
||||
type: 'string', // nightly, hotfix, monthly, auto
|
||||
short: 't',
|
||||
},
|
||||
update: {
|
||||
type: 'boolean',
|
||||
short: 'u',
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
|
||||
const { values } = parseArgs({
|
||||
args,
|
||||
options,
|
||||
allowPositionals: true,
|
||||
});
|
||||
|
||||
if (!values['package-json']) {
|
||||
console.error(
|
||||
'Please specify the path to package.json using --package-json or -p option.',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const packageJsonPath = values['package-json'];
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
const currentVersion = packageJson.version;
|
||||
|
||||
let newVersion;
|
||||
try {
|
||||
newVersion = getNextVersion({
|
||||
currentVersion,
|
||||
type: values.type,
|
||||
currentDate: new Date(),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.stdout.write(newVersion);
|
||||
|
||||
if (values.update) {
|
||||
packageJson.version = newVersion;
|
||||
fs.writeFileSync(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2) + '\n',
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
11
packages/ci-actions/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@actual-app/ci-actions",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest"
|
||||
}
|
||||
}
|
||||
72
packages/ci-actions/src/versions/get-next-package-version.js
Normal file
@@ -0,0 +1,72 @@
|
||||
function parseVersion(version) {
|
||||
const [y, m, p] = version.split('.');
|
||||
return {
|
||||
versionYear: parseInt(y, 10),
|
||||
versionMonth: parseInt(m, 10),
|
||||
versionHotfix: parseInt(p, 10),
|
||||
};
|
||||
}
|
||||
|
||||
function computeNextMonth(versionYear, versionMonth) {
|
||||
// Create date and add 1 month
|
||||
const versionDate = new Date(2000 + versionYear, versionMonth - 1, 1); // month is 0-indexed
|
||||
const nextVersionMonthDate = new Date(
|
||||
versionDate.getFullYear(),
|
||||
versionDate.getMonth() + 1,
|
||||
1,
|
||||
);
|
||||
|
||||
// Format back to YY.M format
|
||||
const fullYear = nextVersionMonthDate.getFullYear();
|
||||
const nextVersionYear = fullYear.toString().slice(fullYear < 2100 ? -2 : -3);
|
||||
const nextVersionMonth = nextVersionMonthDate.getMonth() + 1; // Convert back to 1-indexed
|
||||
return { nextVersionYear, nextVersionMonth };
|
||||
}
|
||||
|
||||
// Determine logical type from 'auto' based on the current date and version
|
||||
function resolveType(type, currentDate, versionYear, versionMonth) {
|
||||
if (type !== 'auto') return type;
|
||||
const inPatchMonth =
|
||||
currentDate.getFullYear() === 2000 + versionYear &&
|
||||
currentDate.getMonth() + 1 === versionMonth;
|
||||
if (inPatchMonth && currentDate.getDate() <= 25) return 'hotfix';
|
||||
return 'monthly';
|
||||
}
|
||||
|
||||
export function getNextVersion({
|
||||
currentVersion,
|
||||
type,
|
||||
currentDate = new Date(),
|
||||
}) {
|
||||
const { versionYear, versionMonth, versionHotfix } =
|
||||
parseVersion(currentVersion);
|
||||
const { nextVersionYear, nextVersionMonth } = computeNextMonth(
|
||||
versionYear,
|
||||
versionMonth,
|
||||
);
|
||||
const resolvedType = resolveType(
|
||||
type,
|
||||
currentDate,
|
||||
versionYear,
|
||||
versionMonth,
|
||||
);
|
||||
|
||||
// Format date stamp once for nightly
|
||||
const currentDateString = currentDate
|
||||
.toISOString()
|
||||
.split('T')[0]
|
||||
.replaceAll('-', '');
|
||||
|
||||
switch (resolvedType) {
|
||||
case 'nightly':
|
||||
return `${nextVersionYear}.${nextVersionMonth}.0-nightly.${currentDateString}`;
|
||||
case 'hotfix':
|
||||
return `${versionYear}.${versionMonth}.${versionHotfix + 1}`;
|
||||
case 'monthly':
|
||||
return `${nextVersionYear}.${nextVersionMonth}.0`;
|
||||
default:
|
||||
throw new Error(
|
||||
'Invalid type specified. Use “auto”, “nightly”, “hotfix”, or “monthly”.',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { getNextVersion } from './get-next-package-version';
|
||||
|
||||
describe('getNextVersion (lib)', () => {
|
||||
it('hotfix increments patch', () => {
|
||||
expect(
|
||||
getNextVersion({
|
||||
currentVersion: '25.8.1',
|
||||
type: 'hotfix',
|
||||
currentDate: new Date('2025-08-10'),
|
||||
}),
|
||||
).toBe('25.8.2');
|
||||
});
|
||||
|
||||
it('monthly advances month same year', () => {
|
||||
expect(
|
||||
getNextVersion({
|
||||
currentVersion: '25.8.3',
|
||||
type: 'monthly',
|
||||
currentDate: new Date('2025-08-15'),
|
||||
}),
|
||||
).toBe('25.9.0');
|
||||
});
|
||||
|
||||
it('monthly wraps year December -> January', () => {
|
||||
expect(
|
||||
getNextVersion({
|
||||
currentVersion: '25.12.3',
|
||||
type: 'monthly',
|
||||
currentDate: new Date('2025-12-05'),
|
||||
}),
|
||||
).toBe('26.1.0');
|
||||
});
|
||||
|
||||
it('nightly format with date stamp', () => {
|
||||
expect(
|
||||
getNextVersion({
|
||||
currentVersion: '25.8.1',
|
||||
type: 'nightly',
|
||||
currentDate: new Date('2025-08-22'),
|
||||
}),
|
||||
).toBe('25.9.0-nightly.20250822');
|
||||
});
|
||||
|
||||
it('auto before 25th -> hotfix', () => {
|
||||
expect(
|
||||
getNextVersion({
|
||||
currentVersion: '25.8.4',
|
||||
type: 'auto',
|
||||
currentDate: new Date('2025-08-20'),
|
||||
}),
|
||||
).toBe('25.8.5');
|
||||
});
|
||||
|
||||
it('auto after 25th (same month) -> monthly', () => {
|
||||
expect(
|
||||
getNextVersion({
|
||||
currentVersion: '25.8.4',
|
||||
type: 'auto',
|
||||
currentDate: new Date('2025-08-27'),
|
||||
}),
|
||||
).toBe('25.9.0');
|
||||
});
|
||||
|
||||
it('auto after 25th (next month) -> monthly', () => {
|
||||
expect(
|
||||
getNextVersion({
|
||||
currentVersion: '25.8.4',
|
||||
type: 'auto',
|
||||
currentDate: new Date('2025-09-02'),
|
||||
}),
|
||||
).toBe('25.9.0');
|
||||
});
|
||||
|
||||
it('invalid type throws', () => {
|
||||
expect(() =>
|
||||
getNextVersion({
|
||||
currentVersion: '25.8.4',
|
||||
type: 'unknown',
|
||||
currentDate: new Date('2025-08-10'),
|
||||
}),
|
||||
).toThrow(/Invalid type/);
|
||||
});
|
||||
});
|
||||
9
packages/ci-actions/vitest.config.mts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
include: ['src/**/*.test.(js|jsx|ts|tsx)'],
|
||||
environment: 'node',
|
||||
},
|
||||
});
|
||||
@@ -13,9 +13,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/cli": "^8.1.0",
|
||||
"@types/react": "^19.1.4",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"@types/react": "^19.1.12",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"exports": {
|
||||
|
||||
@@ -154,4 +154,10 @@ export const styles: Record<string, any> = {
|
||||
borderRadius: 4,
|
||||
padding: '3px 5px',
|
||||
},
|
||||
mobileListItem: {
|
||||
borderBottom: `1px solid ${theme.tableBorder}`,
|
||||
backgroundColor: theme.tableBackground,
|
||||
padding: 16,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
};
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |