diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 8033b4c..400417c 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -63,3 +63,41 @@ jobs: fi echo "New tag: ${{ steps.increment-tag.outputs.new-tag }}" + + prerelease-test: + name: Prerelease Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - uses: actions/setup-node@v5 + with: + node-version-file: .nvmrc + - run: npm ci + - name: Test prerelease tag generation + id: prerelease-tag + uses: ./ + with: + token: ${{ secrets.GITEA_TOKEN }} + prerelease: "true" + - name: Verify prerelease tag format + run: | + new_tag="${{ steps.prerelease-tag.outputs.new-tag }}" + echo "Generated prerelease tag: $new_tag" + + # Check if tag contains -rc- prefix + if [[ "$new_tag" == *"-rc-"* ]]; then + echo "✓ Prerelease tag format is correct" + else + echo "✗ Prerelease tag format is incorrect. Expected format: X.Y.Z-rc-" + exit 1 + fi + + # Check if tag ends with a hash-like string (at least 7 characters) + if [[ "$new_tag" =~ -rc-[a-f0-9]{7,}$ ]]; then + echo "✓ Prerelease tag contains valid SHA suffix" + else + echo "✗ Prerelease tag SHA suffix is invalid" + exit 1 + fi diff --git a/README.md b/README.md index afa9670..02b23ef 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,15 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} ``` +### With prerelease tags + +```yaml +- uses: tbehrendt/conventional-semantic-git-tag-increment@v1 + with: + prerelease: "true" + token: ${{ secrets.GITHUB_TOKEN }} +``` + ## Examples | Commit Message | Current Tag | New Tag | Reason | @@ -52,11 +61,20 @@ jobs: | `feat!: change API response format` | `v1.1.1` | `v2.0.0` | Breaking change | | `docs: update README` | `v2.0.0` | `v2.0.1` | Documentation update | +### Prerelease Examples + +| Commit Message | Current Tag | New Tag (prerelease) | Reason | +| ------------------------------- | ----------- | -------------------- | ----------- | +| `feat: add user authentication` | `v1.0.0` | `1.1.0-rc-abc123` | New feature | +| `fix: resolve login bug` | `v1.1.0` | `1.1.1-rc-def456` | Bug fix | + ## Inputs - `last-tag` (optional): Starting tag to increment from. If not provided, uses the latest tag in the repository. - `token` (required): GitHub token for repository access. Use `${{ github.token }}` for public repos or a PAT for private repos. +- `prerelease` (optional): Whether to create a prerelease tag with `-rc-` suffix. Defaults to `false`. +- `max-tags` (optional): Maximum number of tags to fetch when looking for the latest non-pre-release tag. Defaults to `50`. ## Outputs -- `new-tag`: The incremented semantic version tag (e.g., `1.2.3`) +- `new-tag`: The incremented semantic version tag (e.g., `1.2.3` or `1.2.3-rc-abc123` for prerelease) diff --git a/action.yml b/action.yml index e3389c7..0a435a9 100644 --- a/action.yml +++ b/action.yml @@ -11,6 +11,14 @@ inputs: description: "Token for repository access" required: true default: "${{ github.token }}" + prerelease: + description: "Whether to create a prerelease tag" + required: false + default: "false" + max-tags: + description: "Maximum number of tags to fetch when looking for the latest non-pre-release tag" + required: false + default: "50" outputs: new-tag: diff --git a/src/main.spec.ts b/src/main.spec.ts index 2686db0..1c7f1ee 100644 --- a/src/main.spec.ts +++ b/src/main.spec.ts @@ -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); + }); }); }); diff --git a/src/main.ts b/src/main.ts index 01f3ad9..158ebbc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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 { + async getBaseTag(lastTag: string | undefined, maxTags: number = 50): Promise { 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 { + async incrementTag( + lastTag: string | undefined, + ref: string, + maxTags: number = 50, + prerelease: boolean = false, + githubSha: string = "" + ): Promise { 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 { 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 { 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);