Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion apps/desktop/scripts/ci/plan-r2-release-asset-prune.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
githubRepositoryFromProduct,
isDownloadAssetName,
relativeUpdatePath,
versionFromAssetName,
} from '../update-release-assets.mjs';

const GITHUB_API_BASE_URL = 'https://api.github.com';
Expand Down Expand Up @@ -55,6 +56,11 @@ function githubHeaders(token) {
return headers;
}

function releaseVersionFromTag(tagName) {
const value = String(tagName ?? '');
return /^v\d/u.test(value) ? value.slice(1) : null;
}

function cloudflareHeaders(token) {
return new Headers({
accept: 'application/json',
Expand Down Expand Up @@ -222,9 +228,14 @@ function staleReleaseAssetKeys(releases, keepVersions, updatePath, channel) {
const keys = [];

for (const release of channelReleases.slice(keepVersions)) {
const releaseVersion = releaseVersionFromTag(release?.tag_name);
for (const asset of release.assets ?? []) {
const fileName = asset?.name;
if (!isDownloadAssetName(fileName) || channelFromAssetName(fileName) !== channel) {
if (
!isDownloadAssetName(fileName) ||
channelFromAssetName(fileName) !== channel ||
versionFromAssetName(fileName) !== releaseVersion
) {
continue;
}
keys.push(`${updatePath}/${fileName}`);
Expand Down
58 changes: 58 additions & 0 deletions apps/desktop/tests/ci/plan-r2-release-asset-prune.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,20 @@ function notFoundResponse() {
return new Response(null, { status: 404 });
}

const tagAssetBoundaries = [
['v1.2.3', 'TouchAI-1.2.3-windows-full.nupkg', true],
['v1.2.3', 'TouchAI-beta-1.2.3-beta.1-windows-full.nupkg', false],
['v1.2.3-beta.1', 'TouchAI-beta-1.2.3-beta.1-windows-full.nupkg', true],
['v1.2.3-beta.1', 'TouchAI-1.2.3-windows-full.nupkg', false],
[
'v1.2.3-nightly.20260728.1',
'TouchAI-nightly-1.2.3-nightly.20260728.1-windows-full.nupkg',
true,
],
['release-1.2.3', 'TouchAI-1.2.3-windows-full.nupkg', false],
['1.2.3', 'TouchAI-1.2.3-windows-full.nupkg', false],
] as const;

describe('planR2ReleaseAssetPrune', () => {
it('plans deletion for old channel assets that already exist on GitHub Releases', async () => {
const planner = await loadPlanner();
Expand All @@ -88,6 +102,7 @@ describe('planR2ReleaseAssetPrune', () => {
release('v0.2.0-beta.2', '2026-05-22T00:00:00Z', [
'TouchAI-beta-0.2.0-beta.2-windows-full.nupkg',
'TouchAI-beta-0.2.0-beta.2-windows-delta.nupkg',
'TouchAI-beta-0.2.0-beta.20-windows-full.nupkg',
'release-notes.md',
]),
release('v0.2.0', '2026-05-21T00:00:00Z', ['TouchAI-0.2.0-windows-full.nupkg']),
Expand Down Expand Up @@ -289,6 +304,49 @@ describe('planR2ReleaseAssetPrune', () => {
}
});

it.each(tagAssetBoundaries)(
'matches release tag %s to asset %s: %s',
async (tagName, assetName, shouldDelete) => {
const planner = await loadPlanner();
const product = productWithRetention();
const channel = tagName.includes('-nightly.')
? 'nightly'
: tagName.includes('-beta.')
? 'beta'
: 'stable';
product.services.updates.deployment.r2HotAssetVersions[channel] = 1;
const root = await createFixture(product);
const retainedTag =
channel === 'nightly'
? 'v9.9.9-nightly.20990101.1'
: channel === 'beta'
? 'v9.9.9-beta.1'
: 'v9.9.9';
const fetchMock = vi.fn<typeof fetch>(async (input) => {
if (new URL(input.toString()).hostname === 'api.github.com') {
return githubReleasesResponse([
release(retainedTag, '2026-07-29T00:00:00Z', []),
release(tagName, '2026-07-28T00:00:00Z', [assetName]),
]);
}

return notFoundResponse();
});

try {
expect(planner?.planR2ReleaseAssetPrune).toBeTypeOf('function');
await expect(
planner!.planR2ReleaseAssetPrune(root, channel, {
fetch: fetchMock as unknown as typeof fetch,
token: 'token',
})
).resolves.toEqual(shouldDelete ? [`touchai-app/v1/${assetName}`] : []);
} finally {
await rm(root, { recursive: true, force: true });
}
}
);

it('rejects unsupported channels before querying GitHub releases', async () => {
const planner = await loadPlanner();
const product = productWithRetention();
Expand Down
Loading