diff --git a/.github/actions/get-next-package-version.js b/.github/actions/get-next-package-version.js deleted file mode 100644 index 16e22f7880..0000000000 --- a/.github/actions/get-next-package-version.js +++ /dev/null @@ -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); -} diff --git a/.github/workflows/generate-release-pr.yml b/.github/workflows/generate-release-pr.yml index 61fb5e9191..89d0a147b1 100644 --- a/.github/workflows/generate-release-pr.yml +++ b/.github/workflows/generate-release-pr.yml @@ -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) diff --git a/.github/workflows/publish-nightly-npm-packages.yml b/.github/workflows/publish-nightly-npm-packages.yml index 9c7b47c1d4..33b514f6a3 100644 --- a/.github/workflows/publish-nightly-npm-packages.yml +++ b/.github/workflows/publish-nightly-npm-packages.yml @@ -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 diff --git a/packages/ci-actions/bin/get-next-package-version.js b/packages/ci-actions/bin/get-next-package-version.js new file mode 100755 index 0000000000..e2e5436dc7 --- /dev/null +++ b/packages/ci-actions/bin/get-next-package-version.js @@ -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); +} diff --git a/packages/ci-actions/package.json b/packages/ci-actions/package.json new file mode 100644 index 0000000000..48c5e68a2b --- /dev/null +++ b/packages/ci-actions/package.json @@ -0,0 +1,11 @@ +{ + "name": "@actual-app/ci-actions", + "private": true, + "type": "module", + "devDependencies": { + "vitest": "^3.2.4" + }, + "scripts": { + "test": "vitest" + } +} diff --git a/packages/ci-actions/src/versions/get-next-package-version.js b/packages/ci-actions/src/versions/get-next-package-version.js new file mode 100644 index 0000000000..eb6dfc27a4 --- /dev/null +++ b/packages/ci-actions/src/versions/get-next-package-version.js @@ -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”.', + ); + } +} diff --git a/packages/ci-actions/src/versions/get-next-package-version.test.js b/packages/ci-actions/src/versions/get-next-package-version.test.js new file mode 100644 index 0000000000..031019e298 --- /dev/null +++ b/packages/ci-actions/src/versions/get-next-package-version.test.js @@ -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/); + }); +}); diff --git a/packages/ci-actions/vitest.config.mts b/packages/ci-actions/vitest.config.mts new file mode 100644 index 0000000000..81a9c30fa1 --- /dev/null +++ b/packages/ci-actions/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + include: ['src/**/*.test.(js|jsx|ts|tsx)'], + environment: 'node', + }, +}); diff --git a/upcoming-release-notes/5662.md b/upcoming-release-notes/5662.md new file mode 100644 index 0000000000..2f41839953 --- /dev/null +++ b/upcoming-release-notes/5662.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [jfdoming] +--- + +Fix version bump logic to work if the month has rolled over diff --git a/yarn.lock b/yarn.lock index 9eca68198b..a7bf6a07bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,6 +34,14 @@ __metadata: languageName: unknown linkType: soft +"@actual-app/ci-actions@workspace:packages/ci-actions": + version: 0.0.0-use.local + resolution: "@actual-app/ci-actions@workspace:packages/ci-actions" + dependencies: + vitest: "npm:^3.2.4" + languageName: unknown + linkType: soft + "@actual-app/components@workspace:*, @actual-app/components@workspace:packages/component-library": version: 0.0.0-use.local resolution: "@actual-app/components@workspace:packages/component-library"