Skip to content

Commit e0c27e0

Browse files
committed
feat: add post-merge releases, pre-release support, fix ESM build
- Add post-merge release: git tags, GitHub Releases, spec attachments - Add pre-release-tag input for beta/rc versions - Add new inputs: create-releases, tag-prefix, attach-specs - Add new outputs: bumped-versions, release-urls, created-tags - Fix ESM build (format: esm in tsup.config.ts) - Use shared diffContracts from CLI instead of duplicated logic - Integrate VersionManager for actual versioning
1 parent d5522cf commit e0c27e0

12 files changed

Lines changed: 75187 additions & 26052 deletions

File tree

action.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@ inputs:
3232
description: 'Branch name for Version Contracts PR'
3333
required: false
3434
default: 'contractual/version-contracts'
35+
pre-release-tag:
36+
description: 'Pre-release tag (e.g., "alpha", "beta", "rc") - creates versions like 2.0.0-beta.0'
37+
required: false
38+
create-releases:
39+
description: 'Create GitHub Releases when Version PR merges'
40+
required: false
41+
default: 'true'
42+
tag-prefix:
43+
description: 'Tag format: "contract" for orders-api@1.0.0, "v" for v1.0.0, "none" for 1.0.0'
44+
required: false
45+
default: 'contract'
46+
attach-specs:
47+
description: 'Attach spec files to GitHub Releases as assets'
48+
required: false
49+
default: 'true'
3550

3651
outputs:
3752
has-breaking:
@@ -42,6 +57,12 @@ outputs:
4257
description: 'Whether a changeset was auto-created'
4358
version-pr-url:
4459
description: 'URL of the Version Contracts PR (release mode)'
60+
bumped-versions:
61+
description: 'JSON of bumped versions (release mode), e.g. {"orders-api": "2.0.0", "order-schema": "1.1.0"}'
62+
release-urls:
63+
description: 'JSON array of created GitHub Release URLs (post-merge)'
64+
created-tags:
65+
description: 'JSON array of created git tags (post-merge)'
4566

4667
runs:
4768
using: 'node20'

dist/index.js

Lines changed: 74542 additions & 25932 deletions
Large diffs are not rendered by default.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
},
2222
"dependencies": {
2323
"@actions/core": "^1.10.1",
24+
"@actions/exec": "^3.0.0",
2425
"@actions/github": "^6.0.0",
2526
"@octokit/rest": "^20.1.1"
2627
},

pnpm-lock.yaml

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/github/releases.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/**
2+
* GitHub Release utilities
3+
*/
4+
5+
import * as core from '@actions/core';
6+
import * as github from '@actions/github';
7+
import * as exec from '@actions/exec';
8+
import { readFileSync } from 'node:fs';
9+
import { basename } from 'node:path';
10+
import type { TagPrefix } from '../types.js';
11+
12+
type Octokit = ReturnType<typeof github.getOctokit>;
13+
14+
export interface ReleaseOptions {
15+
/** Git tag name (e.g., "orders-api@2.0.0") */
16+
tagName: string;
17+
/** Release title (e.g., "orders-api v2.0.0") */
18+
releaseName: string;
19+
/** Release body (markdown) */
20+
body: string;
21+
/** Whether this is a pre-release */
22+
prerelease: boolean;
23+
}
24+
25+
export interface ReleaseResult {
26+
/** GitHub Release URL */
27+
url: string;
28+
/** GitHub Release ID (for uploading assets) */
29+
id: number;
30+
}
31+
32+
export interface AssetInfo {
33+
/** Display name for the asset */
34+
name: string;
35+
/** Path to the file to upload */
36+
path: string;
37+
}
38+
39+
/**
40+
* Format a git tag based on the tag prefix setting
41+
*
42+
* @param contractName - Contract name (e.g., "orders-api")
43+
* @param version - Version string (e.g., "2.0.0")
44+
* @param prefix - Tag prefix format
45+
* @returns Formatted tag name
46+
*
47+
* @example
48+
* formatTag("orders-api", "2.0.0", "contract") // "orders-api@2.0.0"
49+
* formatTag("orders-api", "2.0.0", "v") // "v2.0.0"
50+
* formatTag("orders-api", "2.0.0", "none") // "2.0.0"
51+
*/
52+
export function formatTag(contractName: string, version: string, prefix: TagPrefix): string {
53+
switch (prefix) {
54+
case 'contract':
55+
return `${contractName}@${version}`;
56+
case 'v':
57+
return `v${version}`;
58+
case 'none':
59+
return version;
60+
}
61+
}
62+
63+
/**
64+
* Check if a version is a pre-release (contains hyphen)
65+
*
66+
* @example
67+
* isPrerelease("2.0.0") // false
68+
* isPrerelease("2.0.0-beta.0") // true
69+
*/
70+
export function isPrerelease(version: string): boolean {
71+
return version.includes('-');
72+
}
73+
74+
/**
75+
* Create a git tag using git CLI
76+
*
77+
* @param tagName - The tag name to create
78+
* @param message - Tag message
79+
*/
80+
export async function createGitTag(tagName: string, message: string): Promise<void> {
81+
core.info(`Creating git tag: ${tagName}`);
82+
83+
// Create annotated tag
84+
await exec.exec('git', ['tag', '-a', tagName, '-m', message]);
85+
86+
// Push tag to remote
87+
await exec.exec('git', ['push', 'origin', tagName]);
88+
89+
core.info(`Tag ${tagName} created and pushed`);
90+
}
91+
92+
/**
93+
* Create a GitHub Release
94+
*
95+
* @param octokit - Authenticated Octokit instance
96+
* @param context - GitHub context
97+
* @param options - Release options
98+
* @returns Release URL and ID
99+
*/
100+
export async function createRelease(
101+
octokit: Octokit,
102+
context: typeof github.context,
103+
options: ReleaseOptions
104+
): Promise<ReleaseResult> {
105+
core.info(`Creating GitHub Release: ${options.releaseName}`);
106+
107+
const { data: release } = await octokit.rest.repos.createRelease({
108+
owner: context.repo.owner,
109+
repo: context.repo.repo,
110+
tag_name: options.tagName,
111+
name: options.releaseName,
112+
body: options.body,
113+
prerelease: options.prerelease,
114+
});
115+
116+
core.info(`Release created: ${release.html_url}`);
117+
118+
return {
119+
url: release.html_url,
120+
id: release.id,
121+
};
122+
}
123+
124+
/**
125+
* Upload an asset to an existing GitHub Release
126+
*
127+
* @param octokit - Authenticated Octokit instance
128+
* @param context - GitHub context
129+
* @param releaseId - The release ID to upload to
130+
* @param asset - Asset info (name and path)
131+
*/
132+
export async function uploadReleaseAsset(
133+
octokit: Octokit,
134+
context: typeof github.context,
135+
releaseId: number,
136+
asset: AssetInfo
137+
): Promise<void> {
138+
core.info(`Uploading asset: ${asset.name}`);
139+
140+
const fileContent = readFileSync(asset.path);
141+
142+
await octokit.rest.repos.uploadReleaseAsset({
143+
owner: context.repo.owner,
144+
repo: context.repo.repo,
145+
release_id: releaseId,
146+
name: asset.name,
147+
data: fileContent as unknown as string,
148+
});
149+
150+
core.info(`Asset uploaded: ${asset.name}`);
151+
}
152+
153+
/**
154+
* Check if a tag already exists
155+
*
156+
* @param octokit - Authenticated Octokit instance
157+
* @param context - GitHub context
158+
* @param tagName - Tag name to check
159+
* @returns True if tag exists
160+
*/
161+
export async function tagExists(
162+
octokit: Octokit,
163+
context: typeof github.context,
164+
tagName: string
165+
): Promise<boolean> {
166+
try {
167+
await octokit.rest.git.getRef({
168+
owner: context.repo.owner,
169+
repo: context.repo.repo,
170+
ref: `tags/${tagName}`,
171+
});
172+
return true;
173+
} catch (error) {
174+
// 404 means tag doesn't exist
175+
return false;
176+
}
177+
}

src/main.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as core from '@actions/core';
22
import { runPRCheck } from './modes/pr-check.js';
33
import { runRelease } from './modes/release.js';
4-
import type { ActionInputs } from './types.js';
4+
import type { ActionInputs, TagPrefix } from './types.js';
55

66
/**
77
* Parse and validate action inputs
@@ -18,6 +18,12 @@ function getInputs(): ActionInputs {
1818
throw new Error('github-token is required');
1919
}
2020

21+
// Parse tag-prefix with validation
22+
const tagPrefixInput = core.getInput('tag-prefix') || 'contract';
23+
if (!['contract', 'v', 'none'].includes(tagPrefixInput)) {
24+
throw new Error(`Invalid tag-prefix: "${tagPrefixInput}". Must be 'contract', 'v', or 'none'.`);
25+
}
26+
2127
return {
2228
mode,
2329
githubToken,
@@ -26,6 +32,10 @@ function getInputs(): ActionInputs {
2632
autoChangeset: core.getInput('auto-changeset') === 'true',
2733
versionPrTitle: core.getInput('version-pr-title') || 'Version Contracts',
2834
versionPrBranch: core.getInput('version-pr-branch') || 'contractual/version-contracts',
35+
preReleaseTag: core.getInput('pre-release-tag') || undefined,
36+
createReleases: core.getInput('create-releases') !== 'false',
37+
tagPrefix: tagPrefixInput as TagPrefix,
38+
attachSpecs: core.getInput('attach-specs') !== 'false',
2939
};
3040
}
3141

0 commit comments

Comments
 (0)