All checks were successful
CD / Release (push) Successful in 40s
Reviewed-on: #17 Co-authored-by: Timo Behrendt <t.behrendt@t00n.de> Co-committed-by: Timo Behrendt <t.behrendt@t00n.de>
318 lines
9.5 KiB
TypeScript
318 lines
9.5 KiB
TypeScript
import * as core from "@actions/core";
|
|
import * as ccp from "conventional-commits-parser";
|
|
import type { Commit } from "conventional-commits-parser";
|
|
import { execSync } from "child_process";
|
|
|
|
export enum IncrementType {
|
|
MAJOR = "major",
|
|
MINOR = "minor",
|
|
PATCH = "patch",
|
|
}
|
|
|
|
export type Tag = {
|
|
major: number;
|
|
minor: number;
|
|
patch: number;
|
|
};
|
|
|
|
export interface GitService {
|
|
listTags(perPage: number): Promise<{ name: string }[]>;
|
|
getCommit(ref: string): Promise<{ commit: { message: string } }>;
|
|
testConnection(): Promise<void>;
|
|
}
|
|
|
|
export interface CoreService {
|
|
getInput(name: string): string;
|
|
info(message: string): void;
|
|
warning(message: string): void;
|
|
setOutput(name: string, value: string): void;
|
|
setFailed(message: string): void;
|
|
}
|
|
|
|
export class ConventionalCommitAnalyzer {
|
|
analyzeCommit(message: string): Commit {
|
|
return ccp.sync(message);
|
|
}
|
|
|
|
determineIncrementType(analyzedCommit: Commit): IncrementType {
|
|
if (
|
|
analyzedCommit.breaking ||
|
|
analyzedCommit.header?.includes("!") ||
|
|
analyzedCommit.notes.some((note: any) => note.title === "BREAKING CHANGE")
|
|
) {
|
|
return IncrementType.MAJOR;
|
|
} else if (analyzedCommit.type === "feat") {
|
|
return IncrementType.MINOR;
|
|
} else {
|
|
return IncrementType.PATCH;
|
|
}
|
|
}
|
|
}
|
|
|
|
export class TagService {
|
|
bumpTag(tag: Tag, incrementType: IncrementType): Tag {
|
|
switch (incrementType) {
|
|
case IncrementType.MAJOR:
|
|
return {
|
|
major: tag.major + 1,
|
|
minor: 0,
|
|
patch: 0,
|
|
};
|
|
case IncrementType.MINOR:
|
|
return {
|
|
major: tag.major,
|
|
minor: tag.minor + 1,
|
|
patch: 0,
|
|
};
|
|
case IncrementType.PATCH:
|
|
return {
|
|
major: tag.major,
|
|
minor: tag.minor,
|
|
patch: tag.patch + 1,
|
|
};
|
|
}
|
|
}
|
|
|
|
renderTag(tag: Tag, prefix: string = "", prereleaseSuffix: string = ""): string {
|
|
return `${prefix}${tag.major}.${tag.minor}.${tag.patch}${prereleaseSuffix}`;
|
|
}
|
|
|
|
parseTag(tagString: string): Tag {
|
|
if (!/^v?\d+\.\d+\.\d+$/.test(tagString)) {
|
|
throw new Error(`Invalid tag format: ${tagString}. Expected semantic version (e.g., 1.2.3)`);
|
|
}
|
|
|
|
const versionMatch = tagString.match(/^v?(\d+)\.(\d+)\.(\d+)$/);
|
|
if (!versionMatch) {
|
|
throw new Error(`Could not parse version from tag: ${tagString}`);
|
|
}
|
|
|
|
return {
|
|
major: parseInt(versionMatch[1]),
|
|
minor: parseInt(versionMatch[2]),
|
|
patch: parseInt(versionMatch[3]),
|
|
};
|
|
}
|
|
}
|
|
|
|
export class TagIncrementer {
|
|
constructor(
|
|
private gitService: GitService,
|
|
private coreService: CoreService,
|
|
private commitAnalyzer: ConventionalCommitAnalyzer,
|
|
private tagService: TagService
|
|
) {}
|
|
|
|
async getBaseTag(lastTag: string | undefined, maxTags: number = 50): Promise<string> {
|
|
if (lastTag) {
|
|
this.coreService.info(`Using provided tag: ${lastTag}`);
|
|
return lastTag;
|
|
}
|
|
|
|
this.coreService.info(`Fetching up to ${maxTags} tags from local repository`);
|
|
const tags = await this.gitService.listTags(maxTags);
|
|
|
|
if (tags.length === 0) {
|
|
this.coreService.warning("No tags found in repository, defaulting to 0.0.0");
|
|
return "0.0.0";
|
|
}
|
|
|
|
const realTags = tags.filter((tag) => !tag.name.includes("-rc-"));
|
|
|
|
if (realTags.length === 0) {
|
|
this.coreService.warning("No real tags found (only pre-release tags), defaulting to 0.0.0");
|
|
return "0.0.0";
|
|
}
|
|
|
|
const baseTag = realTags[0].name;
|
|
this.coreService.info(`Using latest real tag: ${baseTag}`);
|
|
return baseTag;
|
|
}
|
|
|
|
async determineIncrementType(ref: string): Promise<IncrementType> {
|
|
this.coreService.info(`Fetching commit ${ref}`);
|
|
|
|
try {
|
|
const { commit } = await this.gitService.getCommit(ref);
|
|
this.coreService.info(`Successfully fetched commit: ${commit.message.substring(0, 100)}...`);
|
|
|
|
const analyzedCommit = this.commitAnalyzer.analyzeCommit(commit.message);
|
|
this.coreService.info(
|
|
`Analyzed commit - type: ${analyzedCommit.type}, breaking: ${analyzedCommit.breaking}`
|
|
);
|
|
|
|
const incrementType = this.commitAnalyzer.determineIncrementType(analyzedCommit);
|
|
this.coreService.info(`Determined increment type: ${incrementType}`);
|
|
|
|
return incrementType;
|
|
} catch (error) {
|
|
this.coreService.info(
|
|
`Error fetching commit ${ref}: ${error instanceof Error ? error.message : String(error)}`
|
|
);
|
|
|
|
// Additional debugging for commit-related errors
|
|
if (error instanceof Error && error.message.includes("404")) {
|
|
this.coreService.info(`404 error suggests commit ${ref} was not found in repository`);
|
|
this.coreService.info(
|
|
`This could indicate: 1) SHA doesn't exist, 2) Repository access issue, 3) API endpoint mismatch`
|
|
);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async incrementTag(
|
|
lastTag: string | undefined,
|
|
ref: string,
|
|
maxTags: number = 50,
|
|
prerelease: boolean = false,
|
|
githubSha: string = ""
|
|
): Promise<string> {
|
|
this.coreService.info(`Starting tag increment process at ref ${ref}`);
|
|
|
|
const baseTag = await this.getBaseTag(lastTag, maxTags);
|
|
const parsedTag = this.tagService.parseTag(baseTag);
|
|
|
|
this.coreService.info(
|
|
`Parsed version: ${parsedTag.major}.${parsedTag.minor}.${parsedTag.patch}`
|
|
);
|
|
|
|
const incrementType = await this.determineIncrementType(ref);
|
|
|
|
const newTag = this.tagService.bumpTag(parsedTag, incrementType);
|
|
|
|
const prereleaseSuffix = prerelease && githubSha ? `-rc-${githubSha}` : "";
|
|
const renderedTag = this.tagService.renderTag(newTag, "", prereleaseSuffix);
|
|
|
|
this.coreService.info(`New tag: ${renderedTag}`);
|
|
this.coreService.info(
|
|
`Successfully determined new tag: ${renderedTag} (${incrementType} increment)`
|
|
);
|
|
|
|
return renderedTag;
|
|
}
|
|
}
|
|
|
|
export class LocalGitService implements GitService {
|
|
async listTags(perPage: number): Promise<{ name: string }[]> {
|
|
try {
|
|
const tags = execSync("git tag --sort=-version:refname", { encoding: "utf8" })
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.slice(0, perPage);
|
|
|
|
return tags.map((name) => ({ name }));
|
|
} catch (error) {
|
|
// If no tags exist, return empty array
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async getCommit(ref: string): Promise<{ commit: { message: string } }> {
|
|
try {
|
|
const message = execSync(`git log -1 --pretty=%B ${ref}`, { encoding: "utf8" }).trim();
|
|
return { commit: { message } };
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Failed to get commit ${ref}: ${error instanceof Error ? error.message : String(error)}`
|
|
);
|
|
}
|
|
}
|
|
|
|
async testConnection(): Promise<void> {
|
|
try {
|
|
// Test if we're in a git repository
|
|
execSync("git rev-parse --git-dir", { stdio: "pipe" });
|
|
} catch (error) {
|
|
throw new Error("Not in a git repository. Please ensure this action runs after checkout.");
|
|
}
|
|
}
|
|
}
|
|
|
|
export class LocalCoreService implements CoreService {
|
|
getInput(name: string): string {
|
|
return core.getInput(name);
|
|
}
|
|
|
|
info(message: string): void {
|
|
core.info(message);
|
|
}
|
|
|
|
warning(message: string): void {
|
|
core.warning(message);
|
|
}
|
|
|
|
setOutput(name: string, value: string): void {
|
|
core.setOutput(name, value);
|
|
}
|
|
|
|
setFailed(message: string): void {
|
|
core.setFailed(message);
|
|
}
|
|
}
|
|
|
|
export async function run(): Promise<void> {
|
|
try {
|
|
const coreService = new LocalCoreService();
|
|
|
|
const lastTag = coreService.getInput("last-tag");
|
|
const maxTagsInput = coreService.getInput("max-tags");
|
|
const maxTags = parseInt(maxTagsInput || "50", 10);
|
|
const prereleaseInput = coreService.getInput("prerelease");
|
|
const prerelease = prereleaseInput === "true";
|
|
const githubSha = process.env.GITHUB_SHA || "";
|
|
const ref = "HEAD";
|
|
|
|
if (prerelease && !githubSha) {
|
|
const errorMessage =
|
|
"Prerelease is enabled but GITHUB_SHA environment variable is not available. This is required for prerelease tags.";
|
|
coreService.setFailed(errorMessage);
|
|
return;
|
|
}
|
|
|
|
// Log context information for debugging
|
|
coreService.info(`Working with local git repository`);
|
|
coreService.info(`Commit ref: ${ref}`);
|
|
coreService.info(`Last tag input: ${lastTag || "undefined"}`);
|
|
coreService.info(`Max tags limit: ${maxTags}`);
|
|
coreService.info(`Prerelease: ${prerelease}`);
|
|
coreService.info(`GitHub SHA: ${githubSha || "undefined"}`);
|
|
|
|
const gitService = new LocalGitService();
|
|
const commitAnalyzer = new ConventionalCommitAnalyzer();
|
|
const tagService = new TagService();
|
|
const tagIncrementer = new TagIncrementer(gitService, coreService, commitAnalyzer, tagService);
|
|
|
|
coreService.info("Starting tag increment process...");
|
|
|
|
// Test the connection first
|
|
coreService.info("Testing git repository connection...");
|
|
await gitService.testConnection();
|
|
coreService.info("Git repository connection successful");
|
|
|
|
const newTag = await tagIncrementer.incrementTag(
|
|
lastTag || undefined,
|
|
ref,
|
|
maxTags,
|
|
prerelease,
|
|
githubSha
|
|
);
|
|
|
|
coreService.info(`Process completed successfully. New tag: ${newTag}`);
|
|
coreService.setOutput("new-tag", newTag);
|
|
} catch (error) {
|
|
const coreService = new LocalCoreService();
|
|
|
|
// Enhanced error logging
|
|
if (error instanceof Error) {
|
|
coreService.info(`Error details: ${error.message}`);
|
|
coreService.info(`Error stack: ${error.stack}`);
|
|
coreService.setFailed(error.message);
|
|
} else {
|
|
coreService.info(`Unknown error: ${String(error)}`);
|
|
coreService.setFailed("An unknown error occurred");
|
|
}
|
|
}
|
|
}
|