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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ __tests__/runner/*
# but its recommended not to check these in https://github.com/actions/toolkit/blob/master/docs/action-versioning.md#recommendations
node_modules
coverage
.env
61 changes: 32 additions & 29 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
# https://help.github.com/en/articles/metadata-syntax-for-github-actions
name: "GH Release"
description: "Github Action for creating Github Releases"
author: "softprops"
name: 'GH Release'
description: 'Github Action for creating Github Releases'
author: 'softprops'
inputs:
body:
description: "Note-worthy description of changes in release"
description: 'Note-worthy description of changes in release'
required: false
body_path:
description: "Path to load note-worthy description of changes in release from"
description: 'Path to load note-worthy description of changes in release from'
required: false
name:
description: "Gives the release a custom name. Defaults to tag name"
description: 'Gives the release a custom name. Defaults to tag name'
required: false
tag_name:
description: "Gives a tag name. Defaults to github.ref_name. refs/tags/<name> values are normalized to <name>."
description: 'Gives a tag name. Defaults to github.ref_name. refs/tags/<name> values are normalized to <name>.'
required: false
draft:
description: "Keeps the release as a draft. Defaults to false. When reusing an existing draft release, set this to true to keep it draft; omit it to publish after upload. On immutable-release repositories, use this for prereleases that upload assets and publish the draft later."
description: 'Keeps the release as a draft. Defaults to false. When reusing an existing draft release, set this to true to keep it draft; omit it to publish after upload. On immutable-release repositories, use this for prereleases that upload assets and publish the draft later.'
required: false
prerelease:
description: "Identify the release as a prerelease. Defaults to false"
description: 'Identify the release as a prerelease. Defaults to false'
required: false
preserve_order:
description: "Upload artifacts sequentially in the provided order. This does not control the final display order GitHub uses for release assets."
description: 'Upload artifacts sequentially in the provided order. This does not control the final display order GitHub uses for release assets.'
required: false
files:
description: "Newline-delimited list of path globs for asset files to upload. Escape glob metacharacters when matching literal filenames that contain them. `~/...` expands to the runner home directory. On Windows, both \\ and / path separators are accepted. GitHub may normalize raw asset filenames that contain special characters; the action restores the asset label when possible, but the final download name remains GitHub-controlled."
Expand All @@ -31,52 +31,55 @@ inputs:
description: "Base directory to resolve 'files' globs against. Defaults to the workspace root used by the action step."
required: false
overwrite_files:
description: "Overwrite existing files with the same name. Defaults to true"
description: 'Overwrite existing files with the same name. Defaults to true'
required: false
default: 'true'
fail_on_unmatched_files:
description: "Fails if any of the `files` globs match nothing. Defaults to false"
description: 'Fails if any of the `files` globs match nothing. Defaults to false'
required: false
repository:
description: "Repository to make releases against, in <owner>/<repo> format"
description: 'Repository to make releases against, in <owner>/<repo> format'
required: false
token:
description: "Authorized GitHub token or PAT. Defaults to github.token when omitted. A non-empty explicit token overrides GITHUB_TOKEN. Passing an empty string treats the token as unset."
description: 'Authorized GitHub token or PAT. Defaults to github.token when omitted. A non-empty explicit token overrides GITHUB_TOKEN. Passing an empty string treats the token as unset.'
required: false
default: ${{ github.token }}
target_commitish:
description: "Commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. When creating a new tag for an older commit, `github.token` may not have permission to create the ref; use a PAT or another token with sufficient contents permissions if you hit 403 `Resource not accessible by integration`."
description: 'Commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. When creating a new tag for an older commit, `github.token` may not have permission to create the ref; use a PAT or another token with sufficient contents permissions if you hit 403 `Resource not accessible by integration`.'
required: false
discussion_category_name:
description: "If specified, a discussion of the specified category is created and linked to the release. The value must be a category that already exists in the repository. If there is already a discussion linked to the release, this parameter is ignored."
description: 'If specified, a discussion of the specified category is created and linked to the release. The value must be a category that already exists in the repository. If there is already a discussion linked to the release, this parameter is ignored.'
required: false
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
previous_tag:
description: "Optional. When generate_release_notes is enabled, use this tag as GitHub's previous_tag_name comparison base. If omitted, GitHub chooses the comparison base automatically."
required: false
default: ""
default: ''
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
make_latest:
description: "Specifies whether this release should be set as the latest release for the repository. Drafts and prereleases cannot be set as latest. Can be `true`, `false`, or `legacy`. Uses GitHub api default if not provided"
description: 'Specifies whether this release should be set as the latest release for the repository. Drafts and prereleases cannot be set as latest. Can be `true`, `false`, or `legacy`. Uses GitHub api default if not provided'
required: false
latest:
description: 'Update the latest release. Default is false.'
required: false
env:
GITHUB_TOKEN: "As provided by Github Actions"
GITHUB_TOKEN: 'As provided by Github Actions'
outputs:
url:
description: "URL to the Release HTML Page"
description: 'URL to the Release HTML Page'
id:
description: "Release ID"
description: 'Release ID'
upload_url:
description: "URL for uploading assets to the release"
description: 'URL for uploading assets to the release'
assets:
description: "JSON array containing information about each uploaded asset, in the format given [here](https://docs.github.com/en/rest/reference/repos#upload-a-release-asset--code-samples) (minus the `uploader` field)"
description: 'JSON array containing information about each uploaded asset, in the format given [here](https://docs.github.com/en/rest/reference/repos#upload-a-release-asset--code-samples) (minus the `uploader` field)'
runs:
using: "node24"
main: "dist/index.js"
using: 'node24'
main: 'dist/index.js'
branding:
color: "green"
icon: "package"
color: 'green'
icon: 'package'
43 changes: 41 additions & 2 deletions src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ type ReleaseMutationParams = {
};

export interface Releaser {
getLatestRelease(params: { owner: string; repo: string }): Promise<{ data: Release }>;

getReleaseByTag(params: { owner: string; repo: string; tag: string }): Promise<{ data: Release }>;

createRelease(params: ReleaseMutationParams): Promise<{ data: Release }>;
Expand Down Expand Up @@ -109,6 +111,10 @@ export class GitHubReleaser implements Releaser {
this.github = github;
}

getLatestRelease(params: { owner: string; repo: string }): Promise<{ data: Release }> {
return this.github.rest.repos.getLatestRelease(params);
}

getReleaseByTag(params: {
owner: string;
repo: string;
Expand Down Expand Up @@ -506,7 +512,7 @@ export const release = async (
}

const [owner, repo] = config.github_repository.split('/');
const tag =
let tag =
normalizeTagName(config.input_tag_name) ||
(isTag(config.github_ref) ? config.github_ref.replace('refs/tags/', '') : '');

Expand All @@ -518,7 +524,15 @@ export const release = async (
console.log(`📝 Generating release notes using previous tag ${previous_tag_name}`);
}
try {
const _release: Release | undefined = await findTagFromReleases(releaser, owner, repo, tag);
let _release: Release | undefined;
if (config.input_latest) {
_release = await findLatestRelease(releaser, owner, repo);
if (_release !== undefined) {
tag = _release.tag_name;
}
} else {
_release = await findTagFromReleases(releaser, owner, repo, tag);
}

if (_release === undefined) {
return await createRelease(
Expand Down Expand Up @@ -716,6 +730,31 @@ export const listReleaseAssets = async (
}
};

/**
* Find the latest release.
*
* @param releaser - The GitHub API wrapper for release operations
* @param owner - The owner of the repository
* @param repo - The name of the repository
* @returns The release with the given tag name, or undefined if no release with that tag name is found
*/
export async function findLatestRelease(
releaser: Releaser,
owner: string,
repo: string,
): Promise<Release | undefined> {
try {
const { data: release } = await releaser.getLatestRelease({ owner, repo });
return release;
} catch (error) {
// Release not found (404) or other error - return undefined to allow creation
if (error.status === 404) {
return undefined;
}
// Re-throw unexpected errors
throw error;
}
}
/**
* Finds a release by tag name.
*
Expand Down
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* This file is the entrypoint for the action
*/
import { run } from './main';

// It calls the actual logic of the action
run();
11 changes: 7 additions & 4 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ import { isTag, parseConfig, paths, unmatchedPatterns, uploadUrl } from './util'

import { env } from 'process';

async function run() {
export async function run() {
try {
const config = parseConfig(env);
if (!config.input_tag_name && !isTag(config.github_ref) && !config.input_draft) {
if (
!config.input_latest &&
!config.input_tag_name &&
!isTag(config.github_ref) &&
!config.input_draft
) {
throw new Error(`⚠️ GitHub Releases requires a tag`);
}
if (config.input_files) {
Expand Down Expand Up @@ -110,5 +115,3 @@ async function run() {
setFailed(error.message);
}
}

run();
2 changes: 2 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface Config {
input_previous_tag?: string;
input_append_body?: boolean;
input_make_latest: 'true' | 'false' | 'legacy' | undefined;
input_latest?: boolean;
}

export const uploadUrl = (url: string): string => {
Expand Down Expand Up @@ -118,6 +119,7 @@ export const parseConfig = (env: Env): Config => {
input_previous_tag: env.INPUT_PREVIOUS_TAG?.trim() || undefined,
input_append_body: env.INPUT_APPEND_BODY == 'true',
input_make_latest: parseMakeLatest(env.INPUT_MAKE_LATEST),
input_latest: env.INPUT_LATEST == 'true',
};
};

Expand Down