Fix version bump logic to work if the month has rolled over (#5662)

* Fix version bump logic to work if the month has rolled over

* Refactor script to be more testable

* Add tests for regression

* Move tests to dedicated package

* Add release notes

* Coderabbit
This commit is contained in:
Julian Dominguez-Schatz
2025-09-05 07:31:53 -04:00
committed by GitHub
parent 5459b8baca
commit 4df05aa37c
10 changed files with 268 additions and 121 deletions

View File

@@ -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);
}

View File

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

View File

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

View 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);
}

View File

@@ -0,0 +1,11 @@
{
"name": "@actual-app/ci-actions",
"private": true,
"type": "module",
"devDependencies": {
"vitest": "^3.2.4"
},
"scripts": {
"test": "vitest"
}
}

View 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”.',
);
}
}

View File

@@ -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/);
});
});

View 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',
},
});

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [jfdoming]
---
Fix version bump logic to work if the month has rolled over

View File

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