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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: package.json
node-version: "24"
- run: npm ci
- run: npm run test
- run: npm run check
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ inputs:
default: "false"

runs:
using: "node20"
using: "node24"
main: "lib/main.js"
branding:
icon: "git-merge"
Expand Down
6 changes: 5 additions & 1 deletion lib/action.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,20 @@ function main() {
const currentBranch = (0, utils_1.getBranchFromRef)(GITHUB_REF);
const isReleaseBranch = releaseBranches
.split(',')
.map((b) => b.trim())
.filter(Boolean)
.some((branch) => currentBranch.match(branch));
const isPreReleaseBranch = preReleaseBranches
.split(',')
.map((b) => b.trim())
.filter(Boolean)
.some((branch) => currentBranch.match(branch));
const isPrerelease = !isReleaseBranch && isPreReleaseBranch;
// Sanitize identifier according to
// https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions
const identifier = (appendToPreReleaseTag ? appendToPreReleaseTag : currentBranch).replace(/[^a-zA-Z0-9-]/g, '-');
const prefixRegex = new RegExp(`^${tagPrefix}`);
const validTags = yield (0, utils_1.getValidTags)(prefixRegex, /true/i.test(shouldFetchAllTags));
const validTags = yield (0, utils_1.getValidTags)(prefixRegex, /true/i.test(shouldFetchAllTags), tagPrefix);
const latestTag = (0, utils_1.getLatestTag)(validTags, prefixRegex, tagPrefix);
const latestPrereleaseTag = (0, utils_1.getLatestPrereleaseTag)(validTags, identifier, prefixRegex);
let commits;
Expand Down
110 changes: 110 additions & 0 deletions lib/git.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getCommitsFromGit = exports.listTagsFromGit = exports.hasGitRepository = void 0;
const exec_1 = require("@actions/exec");
const fs = __importStar(require("fs"));
function hasGitRepository(cwd = process.cwd()) {
return fs.existsSync(`${cwd}/.git`);
}
exports.hasGitRepository = hasGitRepository;
function listTagsFromGit(tagPrefix) {
return __awaiter(this, void 0, void 0, function* () {
const output = [];
const exitCode = yield (0, exec_1.exec)('git', [
'for-each-ref',
`refs/tags/${tagPrefix}*`,
'--sort=-version:refname',
'--format=%(refname:short)%09%(objectname)',
], {
silent: true,
ignoreReturnCode: true,
listeners: {
stdout: (data) => {
output.push(data.toString());
},
},
});
if (exitCode !== 0) {
throw new Error(`git for-each-ref failed with exit code ${exitCode}`);
}
return output
.join('')
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const tabIndex = line.indexOf('\t');
const name = line.slice(0, tabIndex);
const sha = line.slice(tabIndex + 1);
return {
name,
commit: { sha, url: '' },
zipball_url: '',
tarball_url: '',
node_id: '',
};
});
});
}
exports.listTagsFromGit = listTagsFromGit;
function getCommitsFromGit(baseRef, headRef) {
return __awaiter(this, void 0, void 0, function* () {
if (baseRef === 'HEAD') {
return [{ message: 'chore: initial release', hash: headRef }];
}
const output = [];
const exitCode = yield (0, exec_1.exec)('git', ['log', '--format=%H%x09%s', `${baseRef}..${headRef}`], {
silent: true,
ignoreReturnCode: true,
listeners: {
stdout: (data) => {
output.push(data.toString());
},
},
});
if (exitCode !== 0) {
throw new Error(`git log failed for range ${baseRef}..${headRef} (exit ${exitCode})`);
}
return output
.join('')
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const tabIndex = line.indexOf('\t');
const hash = line.slice(0, tabIndex);
const message = line.slice(tabIndex + 1).trim();
return { hash, message };
})
.filter((commit) => !!commit.message);
});
}
exports.getCommitsFromGit = getCommitsFromGit;
30 changes: 28 additions & 2 deletions lib/github.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,30 @@ exports.getOctokitSingleton = getOctokitSingleton;
/**
* Fetch all tags for a given repository recursively
*/
function formatGithubApiError(error, operation) {
var _a, _b, _c;
if (error && typeof error === 'object') {
const apiError = error;
const status = (_a = apiError.status) !== null && _a !== void 0 ? _a : 'unknown';
const message = String((_b = apiError.message) !== null && _b !== void 0 ? _b : error);
const url = ((_c = apiError.response) === null || _c === void 0 ? void 0 : _c.url) ? ` (${apiError.response.url})` : '';
const hint = message.includes('<!DOCTYPE html>')
? ' GitHub returned an HTML error page — verify GITHUB_API_URL and network access from self-hosted runners.'
: '';
return new Error(`${operation} failed (${status})${url}: ${message.slice(0, 500)}${hint}`);
}
return new Error(`${operation} failed: ${String(error)}`);
}
function listTags(shouldFetchAllTags = false, fetchedTags = [], page = 1) {
return __awaiter(this, void 0, void 0, function* () {
const octokit = getOctokitSingleton();
const tags = yield octokit.repos.listTags(Object.assign(Object.assign({}, github_1.context.repo), { per_page: 100, page }));
let tags;
try {
tags = yield octokit.repos.listTags(Object.assign(Object.assign({}, github_1.context.repo), { per_page: 100, page }));
}
catch (error) {
throw formatGithubApiError(error, `listTags page ${page}`);
}
if (tags.data.length < 100 || shouldFetchAllTags === false) {
return [...fetchedTags, ...tags.data];
}
Expand All @@ -64,7 +84,13 @@ function compareCommits(baseRef, headRef) {
return __awaiter(this, void 0, void 0, function* () {
const octokit = getOctokitSingleton();
core.debug(`Comparing commits (${baseRef}...${headRef})`);
const commits = yield octokit.repos.compareCommits(Object.assign(Object.assign({}, github_1.context.repo), { base: baseRef, head: headRef }));
let commits;
try {
commits = yield octokit.repos.compareCommits(Object.assign(Object.assign({}, github_1.context.repo), { base: baseRef, head: headRef }));
}
catch (error) {
throw formatGithubApiError(error, `compareCommits (${baseRef}...${headRef})`);
}
return commits.data.commits;
});
}
Expand Down
61 changes: 55 additions & 6 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,25 @@ const semver_1 = require("semver");
// @ts-ignore
const default_release_types_1 = __importDefault(require("@semantic-release/commit-analyzer/lib/default-release-types"));
const github_1 = require("./github");
const git_1 = require("./git");
const defaults_1 = require("./defaults");
function getValidTags(prefixRegex, shouldFetchAllTags) {
function getValidTags(prefixRegex, shouldFetchAllTags, tagPrefix) {
return __awaiter(this, void 0, void 0, function* () {
const tags = yield (0, github_1.listTags)(shouldFetchAllTags);
let tags;
if (tagPrefix && (0, git_1.hasGitRepository)()) {
try {
tags = yield (0, git_1.listTagsFromGit)(tagPrefix);
core.debug(`Listed ${tags.length} tags from local git.`);
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
core.warning(`Failed to list tags from git (${message}). Falling back to GitHub API.`);
tags = yield (0, github_1.listTags)(shouldFetchAllTags);
}
}
else {
tags = yield (0, github_1.listTags)(shouldFetchAllTags);
}
const invalidTags = tags.filter((tag) => !prefixRegex.test(tag.name) || !(0, semver_1.valid)(tag.name.replace(prefixRegex, '')));
invalidTags.forEach((name) => core.debug(`Found Invalid Tag: ${name}.`));
const validTags = tags
Expand All @@ -53,6 +68,22 @@ function getValidTags(prefixRegex, shouldFetchAllTags) {
exports.getValidTags = getValidTags;
function getCommits(baseRef, headRef) {
return __awaiter(this, void 0, void 0, function* () {
// Fallback tag uses sha "HEAD", which is not valid for the compare API.
if (baseRef === 'HEAD') {
core.debug('No prior tag commit found; skipping compare API and treating all commits as new.');
return [{ message: 'chore: initial release', hash: headRef }];
}
if ((0, git_1.hasGitRepository)()) {
try {
const commits = yield (0, git_1.getCommitsFromGit)(baseRef, headRef);
core.debug(`Listed ${commits.length} commits from local git.`);
return commits;
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
core.warning(`Failed to list commits from git (${message}). Falling back to GitHub API.`);
}
}
const commits = yield (0, github_1.compareCommits)(baseRef, headRef);
return commits
.filter((commit) => !!commit.commit.message)
Expand All @@ -64,21 +95,39 @@ function getCommits(baseRef, headRef) {
}
exports.getCommits = getCommits;
function getBranchFromRef(ref) {
return ref.replace('refs/heads/', '');
if (ref.startsWith('refs/heads/')) {
return ref.slice('refs/heads/'.length);
}
// GitHub Actions on pull_request events set GITHUB_REF to `refs/pull/<num>/{merge,head}`.
// Returning the raw ref leaks slashes into the "branch" identifier, which later becomes part
// of the prerelease identifier and can produce surprising versions like
// `1.2.3-refs-pull-123-merge.0`. Provide a stable, semver-safe identifier instead.
const prMatch = ref.match(/^refs\/pull\/(\d+)\/(merge|head)$/);
if (prMatch) {
return `pr-${prMatch[1]}`;
}
return ref;
}
exports.getBranchFromRef = getBranchFromRef;
function isPr(ref) {
return ref.includes('refs/pull/');
}
exports.isPr = isPr;
function getLatestTag(tags, prefixRegex, tagPrefix) {
return (tags.find((tag) => prefixRegex.test(tag.name) &&
!(0, semver_1.prerelease)(tag.name.replace(prefixRegex, ''))) || {
const stable = tags.find((tag) => prefixRegex.test(tag.name) &&
!(0, semver_1.prerelease)(tag.name.replace(prefixRegex, '')));
if (stable) {
return stable;
}
if (tags.length > 0) {
return tags[0];
}
return {
name: `${tagPrefix}0.0.0`,
commit: {
sha: 'HEAD',
},
});
};
}
exports.getLatestTag = getLatestTag;
function getLatestPrereleaseTag(tags, identifier, prefixRegex) {
Expand Down
Loading
Loading