Skip to content

Commit 61a5853

Browse files
committed
fix(sisyphus): use GitHub API for release PR creation in workflows
1 parent 535a923 commit 61a5853

3 files changed

Lines changed: 72 additions & 86 deletions

File tree

packages/sisyphus/src/commands/actions/release-pr.ts

Lines changed: 31 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { appendFileSync } from "node:fs";
12
import { args, color, Exit, log, spinner } from "@r5n/cli-core";
23
import { BaseCommand, type Ctx } from "../../base-command";
34
import { Package, Stone } from "../../domain";
@@ -6,8 +7,6 @@ import { ChangelogGenerator, PackageUpdater, StoneManager, WorkspaceScanner } fr
67

78
const RELEASE_BRANCH = "sisyphus/release";
89
const RELEASE_LABEL = "sisyphus-release";
9-
const RELEASE_LABEL_DESCRIPTION = "Sisyphus release PR";
10-
const RELEASE_LABEL_COLOR = "6f42c1";
1110
const PR_TITLE_PREFIX = "chore(release):";
1211

1312
const releasePrArgs = args({
@@ -18,7 +17,7 @@ type ReleasePrCtx = Ctx<typeof releasePrArgs>;
1817

1918
export class ActionsReleasePrCommand extends BaseCommand {
2019
name = "release-pr";
21-
description = "Create or update a release PR from pending stones";
20+
description = "Prepare release branch for PR creation";
2221
args = releasePrArgs;
2322

2423
private provider: GitProvider | null = null;
@@ -37,6 +36,7 @@ export class ActionsReleasePrCommand extends BaseCommand {
3736

3837
if (stones.length === 0) {
3938
log.info(color.dim("No pending stones found, skipping release PR"));
39+
this.setOutput("skipped", "true");
4040
return;
4141
}
4242

@@ -55,38 +55,26 @@ export class ActionsReleasePrCommand extends BaseCommand {
5555
log.info(`${color.dim("Packages:")} ${updatedPackages.map((p) => p.name).join(", ")}`);
5656

5757
if (ctx.args.dryRun) {
58-
log.info(color.yellow("\n[dry-run] Would create/update release PR"));
58+
log.info(color.yellow("\n[dry-run] Would prepare release branch"));
5959
log.info(color.dim("\nPR Body preview:"));
6060
log.info(prBody);
6161
return;
6262
}
6363

6464
const s = spinner();
6565

66-
const existingPr = await this.findExistingReleasePr();
67-
6866
try {
69-
if (existingPr) {
70-
s.start("Updating release branch...");
71-
await this.updateReleaseBranch(ctx, stones, updatedPackages);
72-
s.stop("Release branch updated");
73-
74-
s.start("Updating PR...");
75-
await this.updatePr(existingPr.number, prTitle, prBody);
76-
s.stop(`PR #${existingPr.number} updated`);
77-
78-
log.info(`\n${color.green("Release PR updated:")} ${existingPr.url}`);
79-
} else {
80-
s.start("Creating release branch...");
81-
await this.createReleaseBranch(ctx, stones, updatedPackages);
82-
s.stop("Release branch created");
83-
84-
s.start("Creating PR...");
85-
const pr = await this.createPr(prTitle, prBody);
86-
s.stop(`PR #${pr.number} created`);
87-
88-
log.info(`\n${color.green("Release PR created:")} ${pr.url}`);
89-
}
67+
s.start("Preparing release branch...");
68+
await this.prepareReleaseBranch(ctx, stones, updatedPackages);
69+
s.stop("Release branch ready");
70+
71+
this.setOutput("skipped", "false");
72+
this.setOutput("branch", RELEASE_BRANCH);
73+
this.setOutput("title", prTitle);
74+
this.setOutput("label", RELEASE_LABEL);
75+
this.setOutputMultiline("body", prBody);
76+
77+
log.info(`\n${color.green("Release branch prepared:")} ${RELEASE_BRANCH}`);
9078
} catch (error) {
9179
s.stop("Failed");
9280
await this.restoreMainBranch();
@@ -134,13 +122,7 @@ export class ActionsReleasePrCommand extends BaseCommand {
134122
return lines.join("\n");
135123
}
136124

137-
private async findExistingReleasePr(): Promise<{ number: number; url: string } | null> {
138-
const provider = await this.getProvider();
139-
const pr = await provider.findPr({ head: RELEASE_BRANCH, label: RELEASE_LABEL });
140-
return pr ? { number: pr.number, url: pr.url } : null;
141-
}
142-
143-
private async createReleaseBranch(ctx: ReleasePrCtx, stones: Stone[], packages: Package[]) {
125+
private async prepareReleaseBranch(ctx: ReleasePrCtx, stones: Stone[], packages: Package[]) {
144126
const provider = await this.getProvider();
145127
const baseBranch = await provider.getDefaultBranch();
146128

@@ -155,28 +137,6 @@ export class ActionsReleasePrCommand extends BaseCommand {
155137
await Bun.$`git checkout ${baseBranch}`.quiet();
156138
}
157139

158-
private async updateReleaseBranch(ctx: ReleasePrCtx, stones: Stone[], packages: Package[]) {
159-
const provider = await this.getProvider();
160-
const baseBranch = await provider.getDefaultBranch();
161-
162-
await Bun.$`git fetch origin ${baseBranch}`.quiet();
163-
await Bun.$`git checkout ${RELEASE_BRANCH}`.quiet();
164-
await Bun.$`git reset --hard origin/${baseBranch}`.quiet();
165-
166-
await this.applyReleaseChanges(ctx, stones, packages);
167-
168-
await Bun.$`git add -A`.quiet();
169-
170-
const hasChanges = await Bun.$`git diff --cached --quiet`.nothrow().quiet();
171-
if (hasChanges.exitCode !== 0) {
172-
await Bun.$`git commit -m ${`${PR_TITLE_PREFIX} prepare release`}`.quiet();
173-
}
174-
175-
await Bun.$`git push origin ${RELEASE_BRANCH} --force`.quiet();
176-
177-
await Bun.$`git checkout ${baseBranch}`.quiet();
178-
}
179-
180140
private async applyReleaseChanges(ctx: ReleasePrCtx, stones: Stone[], packages: Package[]) {
181141
const changelogConfig = ctx.config.get("changelog");
182142
const generator = new ChangelogGenerator(changelogConfig);
@@ -201,36 +161,26 @@ export class ActionsReleasePrCommand extends BaseCommand {
201161
ctx.config.set("currentRelease", { packages: packageVersions, stoneIds: stones.map((s) => s.id), timestamp });
202162
}
203163

204-
private async createPr(title: string, body: string): Promise<{ number: number; url: string }> {
205-
const provider = await this.getProvider();
206-
const baseBranch = await provider.getDefaultBranch();
207-
208-
await provider.ensureLabelExists(RELEASE_LABEL, {
209-
color: RELEASE_LABEL_COLOR,
210-
description: RELEASE_LABEL_DESCRIPTION,
211-
});
212-
213-
const pr = await provider.createPr({
214-
base: baseBranch,
215-
body,
216-
head: RELEASE_BRANCH,
217-
labels: [RELEASE_LABEL],
218-
title,
219-
});
220-
221-
return { number: pr.number, url: pr.url };
222-
}
223-
224-
private async updatePr(prNumber: number, title: string, body: string) {
225-
const provider = await this.getProvider();
226-
await provider.updatePr(prNumber, { body, title });
227-
}
228-
229164
private async restoreMainBranch() {
230165
const provider = await this.getProvider();
231166
const baseBranch = await provider.getDefaultBranch();
232167
try {
233168
await Bun.$`git checkout ${baseBranch}`.quiet();
234169
} catch {}
235170
}
171+
172+
private setOutput(name: string, value: string) {
173+
const outputFile = process.env.GITHUB_OUTPUT;
174+
if (outputFile) {
175+
appendFileSync(outputFile, `${name}=${value}\n`);
176+
}
177+
}
178+
179+
private setOutputMultiline(name: string, value: string) {
180+
const outputFile = process.env.GITHUB_OUTPUT;
181+
if (outputFile) {
182+
const delimiter = `EOF_${Date.now()}`;
183+
appendFileSync(outputFile, `${name}<<${delimiter}\n${value}\n${delimiter}\n`);
184+
}
185+
}
236186
}

packages/sisyphus/src/commands/actions/templates/github/sis-create-stone.yml

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,42 @@ jobs:
4949
git add .sisyphus/stones/
5050
git diff --cached --quiet || (git commit -m "chore: add stone from PR #${{ github.event.pull_request.number }}" && git push)
5151
52-
- name: Create or update release PR
52+
- name: Prepare release branch
53+
id: release
5354
if: steps.stone.outputs.skipped == 'false'
5455
run: bunx @r5n/sisyphus actions release-pr
5556
env:
5657
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58+
59+
- name: Create or update release PR
60+
if: steps.stone.outputs.skipped == 'false' && steps.release.outputs.skipped != 'true'
61+
env:
62+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
63+
run: |
64+
# Check if PR already exists
65+
EXISTING_PR=$(gh pr list --head "${{ steps.release.outputs.branch }}" --json number --jq '.[0].number' 2>/dev/null || echo "")
66+
67+
if [ -n "$EXISTING_PR" ]; then
68+
echo "Updating existing PR #$EXISTING_PR"
69+
gh pr edit "$EXISTING_PR" \
70+
--title "${{ steps.release.outputs.title }}" \
71+
--body "${{ steps.release.outputs.body }}"
72+
else
73+
echo "Creating new PR"
74+
# Create PR via API (more reliable than gh pr create)
75+
PR_URL=$(gh api repos/${{ github.repository }}/pulls \
76+
--method POST \
77+
-f head="${{ steps.release.outputs.branch }}" \
78+
-f base="main" \
79+
-f title="${{ steps.release.outputs.title }}" \
80+
-f body="${{ steps.release.outputs.body }}" \
81+
--jq '.html_url')
82+
echo "Created PR: $PR_URL"
83+
84+
# Extract PR number and add label
85+
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
86+
gh api repos/${{ github.repository }}/issues/$PR_NUMBER/labels \
87+
--method POST \
88+
-f "labels[]=${{ steps.release.outputs.label }}" > /dev/null
89+
echo "Added label: ${{ steps.release.outputs.label }}"
90+
fi

packages/sisyphus/src/providers/GitHubProvider.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,16 @@ export class GitHubProvider extends GitProvider {
5959
const result =
6060
await Bun.$`gh pr create --head ${options.head} --base ${options.base} --title ${options.title} --body ${options.body}`.nothrow();
6161

62-
const url = result.stdout.toString().trim();
62+
const stdout = result.stdout.toString().trim();
63+
const stderr = result.stderr.toString().trim();
64+
const combined = `${stdout}\n${stderr}`;
6365

64-
if (!url.includes("/pull/")) {
65-
const stderr = result.stderr.toString().trim();
66+
const urlMatch = combined.match(/https:\/\/github\.com\/[^\s]+\/pull\/\d+/);
67+
if (!urlMatch) {
6668
throw new Error(stderr || "Failed to create PR");
6769
}
6870

69-
const number = this.extractPrNumber(url);
71+
const number = this.extractPrNumber(urlMatch[0]);
7072

7173
if (options.labels && options.labels.length > 0) {
7274
await this.addLabels(number, options.labels);

0 commit comments

Comments
 (0)