mirror of
https://github.com/softprops/action-gh-release.git
synced 2026-03-15 09:20:54 -04:00
fix: restore dotfile asset labels (#749)
Signed-off-by: Rui Chen <rui@chenrui.dev>
This commit is contained in:
@@ -6,8 +6,12 @@ import {
|
|||||||
release,
|
release,
|
||||||
Release,
|
Release,
|
||||||
Releaser,
|
Releaser,
|
||||||
|
upload,
|
||||||
} from '../src/github';
|
} from '../src/github';
|
||||||
|
|
||||||
|
import { mkdtempSync, rmSync, writeFileSync } from 'fs';
|
||||||
|
import { tmpdir } from 'os';
|
||||||
|
import { join } from 'path';
|
||||||
import { assert, describe, expect, it, vi } from 'vitest';
|
import { assert, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
describe('github', () => {
|
describe('github', () => {
|
||||||
@@ -78,6 +82,7 @@ describe('github', () => {
|
|||||||
listReleaseAssets: () => Promise.reject('Not implemented'),
|
listReleaseAssets: () => Promise.reject('Not implemented'),
|
||||||
deleteReleaseAsset: () => Promise.reject('Not implemented'),
|
deleteReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
deleteRelease: () => Promise.reject('Not implemented'),
|
deleteRelease: () => Promise.reject('Not implemented'),
|
||||||
|
updateReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
uploadReleaseAsset: () => Promise.reject('Not implemented'),
|
uploadReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -201,6 +206,7 @@ describe('github', () => {
|
|||||||
listReleaseAssets: () => Promise.reject('Not implemented'),
|
listReleaseAssets: () => Promise.reject('Not implemented'),
|
||||||
deleteReleaseAsset: () => Promise.reject('Not implemented'),
|
deleteReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
deleteRelease: () => Promise.reject('Not implemented'),
|
deleteRelease: () => Promise.reject('Not implemented'),
|
||||||
|
updateReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
uploadReleaseAsset: () => Promise.reject('Not implemented'),
|
uploadReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -258,6 +264,7 @@ describe('github', () => {
|
|||||||
listReleaseAssets: () => Promise.reject('Not implemented'),
|
listReleaseAssets: () => Promise.reject('Not implemented'),
|
||||||
deleteReleaseAsset: () => Promise.reject('Not implemented'),
|
deleteReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
deleteRelease: () => Promise.reject('Not implemented'),
|
deleteRelease: () => Promise.reject('Not implemented'),
|
||||||
|
updateReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
uploadReleaseAsset: () => Promise.reject('Not implemented'),
|
uploadReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -377,6 +384,7 @@ describe('github', () => {
|
|||||||
listReleaseAssets: () => Promise.reject('Not implemented'),
|
listReleaseAssets: () => Promise.reject('Not implemented'),
|
||||||
deleteReleaseAsset: () => Promise.reject('Not implemented'),
|
deleteReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
deleteRelease: deleteReleaseSpy,
|
deleteRelease: deleteReleaseSpy,
|
||||||
|
updateReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
uploadReleaseAsset: () => Promise.reject('Not implemented'),
|
uploadReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -428,6 +436,7 @@ describe('github', () => {
|
|||||||
listReleaseAssets: () => Promise.reject('Not implemented'),
|
listReleaseAssets: () => Promise.reject('Not implemented'),
|
||||||
deleteReleaseAsset: () => Promise.reject('Not implemented'),
|
deleteReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
deleteRelease: deleteReleaseSpy,
|
deleteRelease: deleteReleaseSpy,
|
||||||
|
updateReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
uploadReleaseAsset: () => Promise.reject('Not implemented'),
|
uploadReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -441,4 +450,122 @@ describe('github', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('upload', () => {
|
||||||
|
it('restores a dotfile label when GitHub normalizes the uploaded asset name', async () => {
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'gh-release-dotfile-'));
|
||||||
|
const dotfilePath = join(tempDir, '.config');
|
||||||
|
writeFileSync(dotfilePath, 'config');
|
||||||
|
|
||||||
|
const updateReleaseAssetSpy = vi.fn(async () => ({
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
name: 'default.config',
|
||||||
|
label: '.config',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
const releaser: Releaser = {
|
||||||
|
getReleaseByTag: () => Promise.reject('Not implemented'),
|
||||||
|
createRelease: () => Promise.reject('Not implemented'),
|
||||||
|
updateRelease: () => Promise.reject('Not implemented'),
|
||||||
|
finalizeRelease: () => Promise.reject('Not implemented'),
|
||||||
|
allReleases: async function* () {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
},
|
||||||
|
listReleaseAssets: () => Promise.reject('Not implemented'),
|
||||||
|
deleteReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
|
deleteRelease: () => Promise.reject('Not implemented'),
|
||||||
|
updateReleaseAsset: updateReleaseAssetSpy,
|
||||||
|
uploadReleaseAsset: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
status: 201,
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
name: 'default.config',
|
||||||
|
label: '',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await upload(
|
||||||
|
config,
|
||||||
|
releaser,
|
||||||
|
'https://uploads.example.test/assets',
|
||||||
|
dotfilePath,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updateReleaseAssetSpy).toHaveBeenCalledWith({
|
||||||
|
owner: 'owner',
|
||||||
|
repo: 'repo',
|
||||||
|
asset_id: 1,
|
||||||
|
name: 'default.config',
|
||||||
|
label: '.config',
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: 1,
|
||||||
|
name: 'default.config',
|
||||||
|
label: '.config',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches an existing asset by label when overwriting a dotfile', async () => {
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'gh-release-dotfile-'));
|
||||||
|
const dotfilePath = join(tempDir, '.config');
|
||||||
|
writeFileSync(dotfilePath, 'config');
|
||||||
|
|
||||||
|
const deleteReleaseAssetSpy = vi.fn(async () => undefined);
|
||||||
|
const releaser: Releaser = {
|
||||||
|
getReleaseByTag: () => Promise.reject('Not implemented'),
|
||||||
|
createRelease: () => Promise.reject('Not implemented'),
|
||||||
|
updateRelease: () => Promise.reject('Not implemented'),
|
||||||
|
finalizeRelease: () => Promise.reject('Not implemented'),
|
||||||
|
allReleases: async function* () {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
},
|
||||||
|
listReleaseAssets: () => Promise.reject('Not implemented'),
|
||||||
|
deleteReleaseAsset: deleteReleaseAssetSpy,
|
||||||
|
deleteRelease: () => Promise.reject('Not implemented'),
|
||||||
|
updateReleaseAsset: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: {
|
||||||
|
id: 2,
|
||||||
|
name: 'default.config',
|
||||||
|
label: '.config',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
uploadReleaseAsset: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
status: 201,
|
||||||
|
data: {
|
||||||
|
id: 2,
|
||||||
|
name: 'default.config',
|
||||||
|
label: '',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await upload(config, releaser, 'https://uploads.example.test/assets', dotfilePath, [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'default.config',
|
||||||
|
label: '.config',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(deleteReleaseAssetSpy).toHaveBeenCalledWith({
|
||||||
|
asset_id: 1,
|
||||||
|
owner: 'owner',
|
||||||
|
repo: 'repo',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
4
dist/index.js
vendored
4
dist/index.js
vendored
File diff suppressed because one or more lines are too long
@@ -23,7 +23,7 @@ export interface Release {
|
|||||||
target_commitish: string;
|
target_commitish: string;
|
||||||
draft: boolean;
|
draft: boolean;
|
||||||
prerelease: boolean;
|
prerelease: boolean;
|
||||||
assets: Array<{ id: number; name: string }>;
|
assets: Array<{ id: number; name: string; label?: string | null }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Releaser {
|
export interface Releaser {
|
||||||
@@ -71,12 +71,20 @@ export interface Releaser {
|
|||||||
owner: string;
|
owner: string;
|
||||||
repo: string;
|
repo: string;
|
||||||
release_id: number;
|
release_id: number;
|
||||||
}): Promise<Array<{ id: number; name: string; [key: string]: any }>>;
|
}): Promise<Array<{ id: number; name: string; label?: string | null; [key: string]: any }>>;
|
||||||
|
|
||||||
deleteReleaseAsset(params: { owner: string; repo: string; asset_id: number }): Promise<void>;
|
deleteReleaseAsset(params: { owner: string; repo: string; asset_id: number }): Promise<void>;
|
||||||
|
|
||||||
deleteRelease(params: { owner: string; repo: string; release_id: number }): Promise<void>;
|
deleteRelease(params: { owner: string; repo: string; release_id: number }): Promise<void>;
|
||||||
|
|
||||||
|
updateReleaseAsset(params: {
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
asset_id: number;
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
}): Promise<{ data: any }>;
|
||||||
|
|
||||||
uploadReleaseAsset(params: {
|
uploadReleaseAsset(params: {
|
||||||
url: string;
|
url: string;
|
||||||
size: number;
|
size: number;
|
||||||
@@ -211,7 +219,7 @@ export class GitHubReleaser implements Releaser {
|
|||||||
owner: string;
|
owner: string;
|
||||||
repo: string;
|
repo: string;
|
||||||
release_id: number;
|
release_id: number;
|
||||||
}): Promise<Array<{ id: number; name: string; [key: string]: any }>> {
|
}): Promise<Array<{ id: number; name: string; label?: string | null; [key: string]: any }>> {
|
||||||
return this.github.paginate(this.github.rest.repos.listReleaseAssets, {
|
return this.github.paginate(this.github.rest.repos.listReleaseAssets, {
|
||||||
...params,
|
...params,
|
||||||
per_page: 100,
|
per_page: 100,
|
||||||
@@ -230,6 +238,16 @@ export class GitHubReleaser implements Releaser {
|
|||||||
await this.github.rest.repos.deleteRelease(params);
|
await this.github.rest.repos.deleteRelease(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateReleaseAsset(params: {
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
asset_id: number;
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
}): Promise<{ data: any }> {
|
||||||
|
return await this.github.rest.repos.updateReleaseAsset(params);
|
||||||
|
}
|
||||||
|
|
||||||
async uploadReleaseAsset(params: {
|
async uploadReleaseAsset(params: {
|
||||||
url: string;
|
url: string;
|
||||||
size: number;
|
size: number;
|
||||||
@@ -267,7 +285,7 @@ export const upload = async (
|
|||||||
releaser: Releaser,
|
releaser: Releaser,
|
||||||
url: string,
|
url: string,
|
||||||
path: string,
|
path: string,
|
||||||
currentAssets: Array<{ id: number; name: string }>,
|
currentAssets: Array<{ id: number; name: string; label?: string | null }>,
|
||||||
): Promise<any> => {
|
): Promise<any> => {
|
||||||
const [owner, repo] = config.github_repository.split('/');
|
const [owner, repo] = config.github_repository.split('/');
|
||||||
const { name, mime, size } = asset(path);
|
const { name, mime, size } = asset(path);
|
||||||
@@ -275,7 +293,8 @@ export const upload = async (
|
|||||||
// note: GitHub renames asset filenames that have special characters, non-alphanumeric characters, and leading or trailing periods. The "List release assets" endpoint lists the renamed filenames.
|
// note: GitHub renames asset filenames that have special characters, non-alphanumeric characters, and leading or trailing periods. The "List release assets" endpoint lists the renamed filenames.
|
||||||
// due to this renaming we need to be mindful when we compare the file name we're uploading with a name github may already have rewritten for logical comparison
|
// due to this renaming we need to be mindful when we compare the file name we're uploading with a name github may already have rewritten for logical comparison
|
||||||
// see https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#upload-a-release-asset
|
// see https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#upload-a-release-asset
|
||||||
({ name: currentName }) => currentName == alignAssetName(name),
|
({ name: currentName, label: currentLabel }) =>
|
||||||
|
currentName === name || currentName === alignAssetName(name) || currentLabel === name,
|
||||||
);
|
);
|
||||||
if (currentAsset) {
|
if (currentAsset) {
|
||||||
if (config.input_overwrite_files === false) {
|
if (config.input_overwrite_files === false) {
|
||||||
@@ -310,6 +329,22 @@ export const upload = async (
|
|||||||
}\n${json.message}\n${JSON.stringify(json.errors)}`,
|
}\n${json.message}\n${JSON.stringify(json.errors)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (json.name && json.name !== name && json.id) {
|
||||||
|
console.log(`✏️ Restoring asset label to ${name}...`);
|
||||||
|
try {
|
||||||
|
const { data } = await releaser.updateReleaseAsset({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
asset_id: json.id,
|
||||||
|
name: json.name,
|
||||||
|
label: name,
|
||||||
|
});
|
||||||
|
console.log(`✅ Uploaded ${name}`);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`error updating release asset label for ${name}: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
console.log(`✅ Uploaded ${name}`);
|
console.log(`✅ Uploaded ${name}`);
|
||||||
return json;
|
return json;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user