This commit is contained in:
Paulo Cesar 2025-06-11 15:37:31 +00:00 committed by GitHub
commit 262a313453
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 605 additions and 412 deletions

View file

@ -61,6 +61,7 @@ describe('util', () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: undefined, input_make_latest: undefined,
input_previous_tag: undefined,
}), }),
); );
}); });
@ -84,6 +85,7 @@ describe('util', () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: undefined, input_make_latest: undefined,
input_previous_tag: undefined,
}), }),
); );
}); });
@ -107,6 +109,7 @@ describe('util', () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: undefined, input_make_latest: undefined,
input_previous_tag: undefined,
}), }),
); );
}); });
@ -143,6 +146,7 @@ describe('util', () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: undefined, input_make_latest: undefined,
input_previous_tag: undefined,
}, },
); );
}); });
@ -171,6 +175,7 @@ describe('util', () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: undefined, input_make_latest: undefined,
input_previous_tag: undefined,
}, },
); );
}); });
@ -198,6 +203,7 @@ describe('util', () => {
input_discussion_category_name: 'releases', input_discussion_category_name: 'releases',
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: undefined, input_make_latest: undefined,
input_previous_tag: undefined,
}, },
); );
}); });
@ -226,6 +232,7 @@ describe('util', () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: true, input_generate_release_notes: true,
input_make_latest: undefined, input_make_latest: undefined,
input_previous_tag: undefined,
}, },
); );
}); });
@ -258,6 +265,7 @@ describe('util', () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: undefined, input_make_latest: undefined,
input_previous_tag: undefined,
}, },
); );
}); });
@ -287,6 +295,7 @@ describe('util', () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: undefined, input_make_latest: undefined,
input_previous_tag: undefined,
}, },
); );
}); });
@ -306,6 +315,7 @@ describe('util', () => {
input_draft: false, input_draft: false,
input_prerelease: true, input_prerelease: true,
input_preserve_order: undefined, input_preserve_order: undefined,
input_previous_tag: undefined,
input_files: [], input_files: [],
input_overwrite_files: undefined, input_overwrite_files: undefined,
input_name: undefined, input_name: undefined,
@ -342,6 +352,7 @@ describe('util', () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: 'false', input_make_latest: 'false',
input_previous_tag: undefined,
}, },
); );
}); });
@ -369,6 +380,7 @@ describe('util', () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: undefined, input_make_latest: undefined,
input_previous_tag: undefined,
}, },
); );
}); });

View file

@ -50,6 +50,10 @@ inputs:
generate_release_notes: generate_release_notes:
description: "Whether to automatically generate the name and body for this release. If name is specified, the specified name will be used; otherwise, a name will be automatically generated. If body is specified, the body will be pre-pended to the automatically generated notes." description: "Whether to automatically generate the name and body for this release. If name is specified, the specified name will be used; otherwise, a name will be automatically generated. If body is specified, the body will be pre-pended to the automatically generated notes."
required: false required: false
previous_tag:
description: "The tag name of the previous release. If not specified, the previous tag will be detected automatically."
required: false
default: ""
append_body: append_body:
description: "Append to existing body instead of overwriting it. Default is false." description: "Append to existing body instead of overwriting it. Default is false."
required: false required: false

2
dist/index.js vendored

File diff suppressed because one or more lines are too long

772
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,12 @@ export interface ReleaseAsset {
size: number; size: number;
} }
export type GenerateReleaseNotesParams = Partial<
Parameters<GitHub['rest']['repos']['generateReleaseNotes']['defaults']>[0]
>;
export type CreateReleaseParams = Partial<Parameters<GitHub['rest']['repos']['createRelease']>[0]>;
export type UpdateReleaseParams = Partial<Parameters<GitHub['rest']['repos']['updateRelease']>[0]>;
export interface Release { export interface Release {
id: number; id: number;
upload_url: string; upload_url: string;
@ -29,36 +35,15 @@ export interface Release {
export interface Releaser { export interface Releaser {
getReleaseByTag(params: { owner: string; repo: string; tag: string }): Promise<{ data: Release }>; getReleaseByTag(params: { owner: string; repo: string; tag: string }): Promise<{ data: Release }>;
createRelease(params: { createRelease(params: CreateReleaseParams): Promise<{ data: Release }>;
owner: string;
repo: string;
tag_name: string;
name: string;
body: string | undefined;
draft: boolean | undefined;
prerelease: boolean | undefined;
target_commitish: string | undefined;
discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
make_latest: 'true' | 'false' | 'legacy' | undefined;
}): Promise<{ data: Release }>;
updateRelease(params: { updateRelease(params: UpdateReleaseParams): Promise<{ data: Release }>;
owner: string;
repo: string;
release_id: number;
tag_name: string;
target_commitish: string;
name: string;
body: string | undefined;
draft: boolean | undefined;
prerelease: boolean | undefined;
discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
make_latest: 'true' | 'false' | 'legacy' | undefined;
}): Promise<{ data: Release }>;
allReleases(params: { owner: string; repo: string }): AsyncIterableIterator<{ data: Release[] }>; allReleases(params: { owner: string; repo: string }): AsyncIterableIterator<{ data: Release[] }>;
getLatestTag(params: { owner: string; repo: string }): Promise<undefined | string>;
generateReleaseBody(params: GenerateReleaseNotesParams): Promise<string>;
} }
export class GitHubReleaser implements Releaser { export class GitHubReleaser implements Releaser {
@ -72,62 +57,59 @@ export class GitHubReleaser implements Releaser {
repo: string; repo: string;
tag: string; tag: string;
}): Promise<{ data: Release }> { }): Promise<{ data: Release }> {
return this.github.rest.repos.getReleaseByTag(params); return this.github.rest.repos.getReleaseByTag(params as any);
} }
createRelease(params: { createRelease(params: CreateReleaseParams): Promise<{ data: Release }> {
owner: string; return this.github.rest.repos.createRelease({
repo: string; ...params,
tag_name: string; generate_release_notes: false,
name: string; } as any);
body: string | undefined;
draft: boolean | undefined;
prerelease: boolean | undefined;
target_commitish: string | undefined;
discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
make_latest: 'true' | 'false' | 'legacy' | undefined;
}): Promise<{ data: Release }> {
if (
typeof params.make_latest === 'string' &&
!['true', 'false', 'legacy'].includes(params.make_latest)
) {
params.make_latest = undefined;
} }
return this.github.rest.repos.createRelease(params); updateRelease(params: UpdateReleaseParams): Promise<{ data: Release }> {
} return this.github.rest.repos.updateRelease({
...params,
updateRelease(params: { generate_release_notes: false,
owner: string; } as any);
repo: string;
release_id: number;
tag_name: string;
target_commitish: string;
name: string;
body: string | undefined;
draft: boolean | undefined;
prerelease: boolean | undefined;
discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined;
make_latest: 'true' | 'false' | 'legacy' | undefined;
}): Promise<{ data: Release }> {
if (
typeof params.make_latest === 'string' &&
!['true', 'false', 'legacy'].includes(params.make_latest)
) {
params.make_latest = undefined;
}
return this.github.rest.repos.updateRelease(params);
} }
allReleases(params: { owner: string; repo: string }): AsyncIterableIterator<{ data: Release[] }> { allReleases(params: { owner: string; repo: string }): AsyncIterableIterator<{ data: Release[] }> {
const updatedParams = { per_page: 100, ...params }; const updatedParams = { per_page: 100, ...params };
return this.github.paginate.iterator( return this.github.paginate.iterator(
this.github.rest.repos.listReleases.endpoint.merge(updatedParams), this.github.rest.repos.listReleases.endpoint.merge(updatedParams as any),
); );
} }
async getLatestTag(params: { owner: string; repo: string }): Promise<undefined | string> {
try {
const release = await this.github.rest.repos.getLatestRelease(params as any);
if (!release?.data) {
return;
}
return release.data.tag_name;
} catch (e) {
console.error(e);
return;
}
}
async generateReleaseBody(params: GenerateReleaseNotesParams): Promise<string> {
try {
const { data } = await this.github.rest.repos.generateReleaseNotes(params as any);
if (!data.body) {
throw new Error('No release body generated');
}
return data.body;
} catch (e) {
throw e;
}
}
} }
export const asset = (path: string): ReleaseAsset => { export const asset = (path: string): ReleaseAsset => {
@ -215,8 +197,40 @@ export const release = async (
config.input_tag_name || config.input_tag_name ||
(isTag(config.github_ref) ? config.github_ref.replace('refs/tags/', '') : ''); (isTag(config.github_ref) ? config.github_ref.replace('refs/tags/', '') : '');
const previous_tag = config.input_previous_tag;
const discussion_category_name = config.input_discussion_category_name; const discussion_category_name = config.input_discussion_category_name;
const generate_release_notes = config.input_generate_release_notes; const generate_release_notes = config.input_generate_release_notes;
const latestTag: string | undefined = !previous_tag
? await releaser.getLatestTag({
owner,
repo,
})
: undefined;
if (latestTag) {
console.log(`🏷️ Latest tag related to a release is ${latestTag}`);
} else if (previous_tag) {
console.log(`🏷️ Previous tag is ${previous_tag}`);
}
const tag_name = tag;
let body: string = generate_release_notes
? await releaser.generateReleaseBody({
owner,
repo,
tag_name,
previous_tag_name: previous_tag || latestTag,
} as GenerateReleaseNotesParams)
: '';
if ((generate_release_notes && previous_tag) || latestTag) {
console.log(`Will generate release notes using ${previous_tag || latestTag} as previous tag`);
}
body = body ? `${body}\n` : '';
try { try {
const _release: Release | undefined = await findTagFromReleases(releaser, owner, repo, tag); const _release: Release | undefined = await findTagFromReleases(releaser, owner, repo, tag);
@ -250,7 +264,6 @@ export const release = async (
target_commitish = existingRelease.target_commitish; target_commitish = existingRelease.target_commitish;
} }
const tag_name = tag;
const name = config.input_name || existingRelease.name || tag; const name = config.input_name || existingRelease.name || tag;
// revisit: support a new body-concat-strategy input for accumulating // revisit: support a new body-concat-strategy input for accumulating
// body parts as a release gets updated. some users will likely want this while // body parts as a release gets updated. some users will likely want this while
@ -258,18 +271,20 @@ export const release = async (
// no one wants // no one wants
const workflowBody = releaseBody(config) || ''; const workflowBody = releaseBody(config) || '';
const existingReleaseBody = existingRelease.body || ''; const existingReleaseBody = existingRelease.body || '';
let body: string;
if (config.input_append_body && workflowBody && existingReleaseBody) { if (config.input_append_body && workflowBody && existingReleaseBody) {
body = existingReleaseBody + '\n' + workflowBody; console.log(' Appending existing release body');
body = body + existingReleaseBody + '\n' + workflowBody;
} else { } else {
body = workflowBody || existingReleaseBody; console.log(` Using ${workflowBody ? 'workflow body' : 'existing release body'}`);
body = body + (workflowBody || existingReleaseBody);
} }
const draft = config.input_draft !== undefined ? config.input_draft : existingRelease.draft; const draft = config.input_draft !== undefined ? config.input_draft : existingRelease.draft;
const prerelease = const prerelease =
config.input_prerelease !== undefined ? config.input_prerelease : existingRelease.prerelease; config.input_prerelease !== undefined ? config.input_prerelease : existingRelease.prerelease;
const make_latest = config.input_make_latest; const make_latest = config.input_make_latest!;
const release = await releaser.updateRelease({ const release = await releaser.updateRelease({
owner, owner,
@ -282,12 +297,55 @@ export const release = async (
draft, draft,
prerelease, prerelease,
discussion_category_name, discussion_category_name,
generate_release_notes,
make_latest, make_latest,
}); } as UpdateReleaseParams);
return release.data; return release.data;
} catch (error) { } catch (error) {
if (error.status !== 404) { if (error.status === 404) {
const tag_name = tag;
const name = config.input_name || tag;
const workflowBody = releaseBody(config) || '';
if (config.input_append_body && workflowBody) {
console.log(' Appending existing release body');
body = body + workflowBody;
}
const draft = config.input_draft;
const prerelease = config.input_prerelease;
const target_commitish = config.input_target_commitish;
const make_latest = config.input_make_latest!;
let commitMessage: string = '';
if (target_commitish) {
commitMessage = ` using commit "${target_commitish}"`;
}
console.log(`👩‍🏭 Creating new GitHub release for tag ${tag_name}${commitMessage}...`);
try {
let release = await releaser.createRelease({
owner,
repo,
tag_name,
name,
body,
draft,
prerelease,
target_commitish,
discussion_category_name,
make_latest,
} as CreateReleaseParams);
return release.data;
} catch (error) {
// presume a race with competing metrix runs
console.log(
`⚠️ GitHub release failed with status: ${
error.status
}\n${JSON.stringify(error.response.data.errors)}\nretrying... (${
maxRetries - 1
} retries remaining)`,
);
return release(config, releaser, maxRetries - 1);
}
} else {
console.log( console.log(
`⚠️ Unexpected error fetching GitHub release for tag ${config.github_ref}: ${error}`, `⚠️ Unexpected error fetching GitHub release for tag ${config.github_ref}: ${error}`,
); );
@ -350,7 +408,7 @@ async function createRelease(
const draft = config.input_draft; const draft = config.input_draft;
const prerelease = config.input_prerelease; const prerelease = config.input_prerelease;
const target_commitish = config.input_target_commitish; const target_commitish = config.input_target_commitish;
const make_latest = config.input_make_latest; const make_latest = config.input_make_latest!;
let commitMessage: string = ''; let commitMessage: string = '';
if (target_commitish) { if (target_commitish) {
commitMessage = ` using commit "${target_commitish}"`; commitMessage = ` using commit "${target_commitish}"`;
@ -367,9 +425,8 @@ async function createRelease(
prerelease, prerelease,
target_commitish, target_commitish,
discussion_category_name, discussion_category_name,
generate_release_notes,
make_latest, make_latest,
}); } as CreateReleaseParams);
return release.data; return release.data;
} catch (error) { } catch (error) {
// presume a race with competing matrix runs // presume a race with competing matrix runs

View file

@ -1,5 +1,5 @@
import { readFileSync, statSync } from 'fs';
import * as glob from 'glob'; import * as glob from 'glob';
import { statSync, readFileSync } from 'fs';
export interface Config { export interface Config {
github_token: string; github_token: string;
@ -22,6 +22,7 @@ export interface Config {
input_generate_release_notes?: boolean; input_generate_release_notes?: boolean;
input_append_body?: boolean; input_append_body?: boolean;
input_make_latest: 'true' | 'false' | 'legacy' | undefined; input_make_latest: 'true' | 'false' | 'legacy' | undefined;
input_previous_tag?: string;
} }
export const uploadUrl = (url: string): string => { export const uploadUrl = (url: string): string => {
@ -74,6 +75,7 @@ export const parseConfig = (env: Env): Config => {
input_generate_release_notes: env.INPUT_GENERATE_RELEASE_NOTES == 'true', input_generate_release_notes: env.INPUT_GENERATE_RELEASE_NOTES == 'true',
input_append_body: env.INPUT_APPEND_BODY == 'true', input_append_body: env.INPUT_APPEND_BODY == 'true',
input_make_latest: parseMakeLatest(env.INPUT_MAKE_LATEST), input_make_latest: parseMakeLatest(env.INPUT_MAKE_LATEST),
input_previous_tag: env.INPUT_PREVIOUS_TAG?.trim() || undefined,
}; };
}; };