ci / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 46s
ci / ts (packages + web) (push) Successful in 38s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (apps/web/Dockerfile, web, ) (push) Successful in 44s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (services/api/Dockerfile, storage-agent, storage-agent) (push) Successful in 51s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (services/recommender/Dockerfile, recommender, ) (push) Successful in 29s
ci / mobile (Metro-бандл Android) (push) Successful in 51s
ci / api (push) Successful in 1m23s
ci / recommender (push) Successful in 37s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (services/api/Dockerfile, api, api) (push) Successful in 1m33s
release / release (заметки) (push) Failing after 5s
release / android (APK в релиз) (push) Skipped
release / verify (push) Successful in 1m19s
The release job now creates the Gitea release right after verification with generated notes: Android build status, changes grouped by Conventional Commit type with commit links, server update steps and APK install/check notes. The android job attaches the APK and .sha256 and rewrites only the status block (building / built with size, versionCode and SHA-256 / failed with a log link). scripts/release.mjs is idempotent on re-runs and unit-tested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
112 lines
4.5 KiB
JavaScript
112 lines
4.5 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { test } from 'node:test';
|
|
|
|
import {
|
|
groupCommits,
|
|
renderAndroidStatus,
|
|
renderNotes,
|
|
replaceAndroidStatus,
|
|
STATUS_END,
|
|
STATUS_START,
|
|
} from './release.mjs';
|
|
|
|
const sha = (n) => String(n).repeat(40).slice(0, 40);
|
|
const repoUrl = 'https://git.example/owner/pipu-music';
|
|
|
|
test('groups conventional commits by section in a fixed order', () => {
|
|
const groups = groupCommits([
|
|
{ sha: sha(1), subject: 'fix(api): handle empty token' },
|
|
{ sha: sha(2), subject: 'feat: add wave' },
|
|
{ sha: sha(3), subject: 'ci(release): build arm64 only' },
|
|
{ sha: sha(4), subject: 'build(deploy): pin images' },
|
|
{ sha: sha(5), subject: 'Update README' },
|
|
{ sha: sha(6), subject: 'chore: bump deps' },
|
|
]);
|
|
assert.deepEqual(
|
|
groups.sections.map(([title, items]) => [title, items.map((i) => i.text)]),
|
|
[
|
|
['Новое', ['add wave']],
|
|
['Исправления', ['handle empty token']],
|
|
['Сборка и CI', ['build arm64 only', 'pin images']],
|
|
['Прочее', ['Update README', 'bump deps']],
|
|
],
|
|
);
|
|
assert.equal(groups.sections[1][1][0].scope, 'api');
|
|
assert.deepEqual(groups.breaking, []);
|
|
});
|
|
|
|
test('marks breaking changes', () => {
|
|
const groups = groupCommits([{ sha: sha(1), subject: 'feat(api)!: drop v0 endpoints' }]);
|
|
assert.equal(groups.breaking.length, 1);
|
|
assert.equal(groups.sections[0][0], 'Новое');
|
|
});
|
|
|
|
test('android status blocks', () => {
|
|
const building = renderAndroidStatus({ status: 'building', runUrl: 'https://run' });
|
|
assert.ok(building.startsWith(STATUS_START) && building.endsWith(STATUS_END));
|
|
assert.match(building, /собирается — \[ход сборки\]\(https:\/\/run\)/);
|
|
|
|
const ok = renderAndroidStatus({
|
|
status: 'success',
|
|
versionCode: '10299',
|
|
signing: 'release',
|
|
apk: { name: 'pipu-music-0.1.2.apk', size: 25 * 1024 * 1024 + 300_000, sha256: 'ab'.repeat(32) },
|
|
});
|
|
assert.match(ok, /`pipu-music-0\.1\.2\.apk` \(25,3 МБ\), versionCode `10299`/);
|
|
assert.doesNotMatch(ok, /отладочным/);
|
|
|
|
const debug = renderAndroidStatus({ status: 'success', signing: 'debug', apk: { name: 'a.apk', size: 1, sha256: 'x' } });
|
|
assert.match(debug, /отладочным ключом/);
|
|
|
|
assert.match(renderAndroidStatus({ status: 'failure', runUrl: 'https://run' }), /не удалась — \[лог сборки\]/);
|
|
});
|
|
|
|
test('replaces only the status block and is idempotent', () => {
|
|
const body = `intro\n\n${renderAndroidStatus({ status: 'building', runUrl: 'u' })}\n\n## Что изменилось\n`;
|
|
const failed = renderAndroidStatus({ status: 'failure', runUrl: 'u' });
|
|
const once = replaceAndroidStatus(body, failed);
|
|
assert.equal(replaceAndroidStatus(once, failed), once);
|
|
assert.ok(once.startsWith('intro\n\n') && once.endsWith('\n\n## Что изменилось\n'));
|
|
assert.doesNotMatch(once, /собирается/);
|
|
assert.equal(once.split(STATUS_START).length, 2);
|
|
});
|
|
|
|
test('adds a status block when the body has none', () => {
|
|
const block = renderAndroidStatus({ status: 'failure', runUrl: 'u' });
|
|
assert.ok(replaceAndroidStatus('hand-written notes', block).startsWith(block));
|
|
});
|
|
|
|
test('release notes', () => {
|
|
const notes = renderNotes({
|
|
version: '0.2.0',
|
|
tag: 'v0.2.0',
|
|
prevTag: 'v0.1.7',
|
|
prerelease: false,
|
|
repoUrl,
|
|
groups: groupCommits([{ sha: sha(1), subject: 'feat(web): library screen' }]),
|
|
androidStatus: renderAndroidStatus({ status: 'building', runUrl: 'https://run' }),
|
|
});
|
|
assert.match(notes, /### Новое\n\n- \*\*web:\*\* library screen \(\[1111111\]\(https:\/\/git\.example\/owner\/pipu-music\/commit\/1{40}\)\)/);
|
|
assert.match(notes, /\[v0\.1\.7\.\.\.v0\.2\.0\]\(https:\/\/git\.example\/owner\/pipu-music\/compare\/v0\.1\.7\.\.\.v0\.2\.0\)/);
|
|
assert.match(notes, /\.\/update\.sh v0\.2\.0/);
|
|
assert.match(notes, /"version":"0\.2\.0"/);
|
|
assert.match(notes, /pipu-music-0\.2\.0\.apk/);
|
|
assert.match(notes, /src\/tag\/v0\.2\.0\/docs\/install-guide\.md/);
|
|
assert.doesNotMatch(notes, /Предварительная версия/);
|
|
});
|
|
|
|
test('release notes for a first prerelease without changes', () => {
|
|
const notes = renderNotes({
|
|
version: '0.1.0-rc.1',
|
|
tag: 'v0.1.0-rc.1',
|
|
prevTag: null,
|
|
prerelease: true,
|
|
repoUrl,
|
|
groups: groupCommits([]),
|
|
androidStatus: renderAndroidStatus({ status: 'building', runUrl: 'u' }),
|
|
});
|
|
assert.match(notes, /Предварительная версия/);
|
|
assert.match(notes, /Изменений с прошлой версии нет/);
|
|
assert.match(notes, /commits\/tag\/v0\.1\.0-rc\.1/);
|
|
});
|