feat: add ability to create pre-release tags (#17)
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>
This commit was merged in pull request #17.
This commit is contained in:
2025-09-30 17:29:47 +02:00
committed by t.behrendt
parent 5b2f1fe9d4
commit 0e6aab82a5
6 changed files with 231 additions and 50 deletions

View File

@@ -149,6 +149,16 @@ describe("TagService", () => {
const result = tagService.renderTag(tag, "v");
expect(result).toBe("v1.2.3");
});
it("should render tag with prerelease suffix", () => {
const result = tagService.renderTag(tag, "", "-rc-abc123");
expect(result).toBe("1.2.3-rc-abc123");
});
it("should render tag with prefix and prerelease suffix", () => {
const result = tagService.renderTag(tag, "v", "-rc-abc123");
expect(result).toBe("v1.2.3-rc-abc123");
});
});
describe("parseTag", () => {
@@ -221,7 +231,7 @@ describe("TagIncrementer", () => {
const result = await tagIncrementer.getBaseTag(undefined);
expect(result).toBe("v2.0.0");
expect(mockCoreService.getLogs()).toContain("INFO: Using latest tag: v2.0.0");
expect(mockCoreService.getLogs()).toContain("INFO: Using latest real tag: v2.0.0");
});
it("should default to 0.0.0 when no tags found", async () => {
@@ -234,6 +244,34 @@ describe("TagIncrementer", () => {
"WARNING: No tags found in repository, defaulting to 0.0.0"
);
});
it("should skip pre-release tags and use latest real tag", async () => {
mockGitService.setMockTags([
{ name: "v2.0.0-rc-26035c6e13" },
{ name: "v1.2.3" },
{ name: "v1.1.4-rc-aa512edb16" },
{ name: "v1.0.0" },
]);
const result = await tagIncrementer.getBaseTag(undefined);
expect(result).toBe("v1.2.3");
expect(mockCoreService.getLogs()).toContain("INFO: Using latest real tag: v1.2.3");
});
it("should default to 0.0.0 when only pre-release tags found", async () => {
mockGitService.setMockTags([
{ name: "v2.0.0-rc-26035c6e13" },
{ name: "v1.1.4-rc-aa512edb16" },
]);
const result = await tagIncrementer.getBaseTag(undefined);
expect(result).toBe("0.0.0");
expect(mockCoreService.getWarnings()).toContain(
"WARNING: No real tags found (only pre-release tags), defaulting to 0.0.0"
);
});
});
describe("determineIncrementType", () => {
@@ -302,6 +340,28 @@ describe("TagIncrementer", () => {
expect(result).toBe("2.1.1");
});
it("should create prerelease tag when prerelease is true", async () => {
mockGitService.setMockTags([{ name: "v1.2.3" }]);
mockGitService.setMockCommit({
commit: { message: "feat: new feature" },
});
const result = await tagIncrementer.incrementTag(undefined, "ref", 50, true, "abc123");
expect(result).toBe("1.3.0-rc-abc123");
});
it("should not create prerelease tag when prerelease is false", async () => {
mockGitService.setMockTags([{ name: "v1.2.3" }]);
mockGitService.setMockCommit({
commit: { message: "feat: new feature" },
});
const result = await tagIncrementer.incrementTag(undefined, "ref", 50, false, "abc123");
expect(result).toBe("1.3.0");
});
});
});
@@ -315,14 +375,18 @@ describe("LocalGitService", () => {
describe("listTags", () => {
it("should call git command to list tags", async () => {
mockExecSync.mockReturnValue("v1.0.0\nv1.1.0\n" as any);
mockExecSync.mockReturnValue("v1.0.0\n1.1.0\n2.0.0-rc-26035c6e13\n" as any);
const result = await localGitService.listTags(5);
expect(mockExecSync).toHaveBeenCalledWith("git tag --sort=-version:refname", {
encoding: "utf8",
});
expect(result).toEqual([{ name: "v1.0.0" }, { name: "v1.1.0" }]);
expect(result).toEqual([
{ name: "v1.0.0" },
{ name: "1.1.0" },
{ name: "2.0.0-rc-26035c6e13" },
]);
});
it("should return empty array when no tags exist", async () => {
@@ -391,9 +455,12 @@ describe("LocalCoreService", () => {
// Integration test example
describe("TagIncrementer Integration", () => {
it("should handle complete tag increment flow", async () => {
const mockGitService = new MockGitService([{ name: "v1.2.3" }], {
commit: { message: "feat: new feature" },
});
const mockGitService = new MockGitService(
[{ name: "v2.0.0-rc-26035c6e13" }, { name: "v1.1.4-rc-aa512edb16" }, { name: "v1.2.3" }],
{
commit: { message: "feat: new feature" },
}
);
const mockCoreService = new MockCoreService();
const commitAnalyzer = new ConventionalCommitAnalyzer();
const tagService = new TagService();
@@ -410,5 +477,23 @@ describe("TagIncrementer Integration", () => {
expect(result).toBe("1.3.0");
expect(mockCoreService.getLogs()).toContain("INFO: Determined increment type: minor");
expect(mockCoreService.getLogs()).toContain("INFO: New tag: 1.3.0");
expect(mockCoreService.getLogs()).toContain("INFO: Using latest real tag: v1.2.3");
});
});
describe("Prerelease Validation", () => {
it("should validate prerelease configuration correctly", () => {
// Test cases for prerelease validation logic
const testCases = [
{ prerelease: true, githubSha: "abc123", shouldFail: false },
{ prerelease: true, githubSha: "", shouldFail: true },
{ prerelease: false, githubSha: "abc123", shouldFail: false },
{ prerelease: false, githubSha: "", shouldFail: false },
];
testCases.forEach(({ prerelease, githubSha, shouldFail }) => {
const result = prerelease && !githubSha;
expect(result).toBe(shouldFail);
});
});
});

View File

@@ -73,8 +73,8 @@ export class TagService {
}
}
renderTag(tag: Tag, prefix: string = ""): string {
return `${prefix}${tag.major}.${tag.minor}.${tag.patch}`;
renderTag(tag: Tag, prefix: string = "", prereleaseSuffix: string = ""): string {
return `${prefix}${tag.major}.${tag.minor}.${tag.patch}${prereleaseSuffix}`;
}
parseTag(tagString: string): Tag {
@@ -103,22 +103,29 @@ export class TagIncrementer {
private tagService: TagService
) {}
async getBaseTag(lastTag: string | undefined): Promise<string> {
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 tags from local repository");
const tags = await this.gitService.listTags(1);
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 baseTag = tags[0].name;
this.coreService.info(`Using latest tag: ${baseTag}`);
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;
}
@@ -155,10 +162,16 @@ export class TagIncrementer {
}
}
async incrementTag(lastTag: string | undefined, ref: string): Promise<string> {
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);
const baseTag = await this.getBaseTag(lastTag, maxTags);
const parsedTag = this.tagService.parseTag(baseTag);
this.coreService.info(
@@ -168,7 +181,9 @@ export class TagIncrementer {
const incrementType = await this.determineIncrementType(ref);
const newTag = this.tagService.bumpTag(parsedTag, incrementType);
const renderedTag = this.tagService.renderTag(newTag);
const prereleaseSuffix = prerelease && githubSha ? `-rc-${githubSha}` : "";
const renderedTag = this.tagService.renderTag(newTag, "", prereleaseSuffix);
this.coreService.info(`New tag: ${renderedTag}`);
this.coreService.info(
@@ -242,12 +257,27 @@ export async function run(): Promise<void> {
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();
@@ -261,7 +291,13 @@ export async function run(): Promise<void> {
await gitService.testConnection();
coreService.info("Git repository connection successful");
const newTag = await tagIncrementer.incrementTag(lastTag || undefined, ref);
const newTag = await tagIncrementer.incrementTag(
lastTag || undefined,
ref,
maxTags,
prerelease,
githubSha
);
coreService.info(`Process completed successfully. New tag: ${newTag}`);
coreService.setOutput("new-tag", newTag);