Skip to content

Commit 32916fb

Browse files
authored
[r5n-0] gitlab support, actions command improvements/fixes (#14)
1 parent 5d3f8bc commit 32916fb

18 files changed

Lines changed: 294 additions & 150 deletions

packages/core/src/config-manager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ export class ConfigManager<T extends object> {
2323
this.save();
2424
}
2525

26+
public delete<K extends keyof T>(key: K): void {
27+
delete this.config[key];
28+
this.save();
29+
}
30+
2631
public exists(): boolean {
2732
return fs.existsSync(this.configPath);
2833
}

packages/sisyphus/src/commands/actions/init.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { dirname } from "node:path";
44
import { args, color, confirm, Exit, log, multiselect, note } from "@r5n/cli-core";
55
import { BaseCommand, type Ctx } from "../../base-command";
66
import { detectProvider } from "../../providers";
7-
import { createCiGenerator, GitLabCiGenerator, WORKFLOW_CONFIGS, type WorkflowType } from "./CiGenerator";
7+
import { createCiGenerator, GitHubCiGenerator, GitLabCiGenerator, WORKFLOW_CONFIGS, type WorkflowType } from "./CiGenerator";
88

99
const PROVIDER_LABELS = { bitbucket: "Bitbucket Pipelines", github: "GitHub Actions", gitlab: "GitLab CI" };
1010

@@ -71,6 +71,19 @@ export class ActionsInitCommand extends BaseCommand {
7171
log.info(`\nAdd this to your ${color.bold(".gitlab-ci.yml")}:\n`);
7272
log.info(color.cyan(generator.getIncludeInstruction()));
7373
log.info("");
74+
log.info(
75+
`${color.yellow("Note:")} Add a ${color.bold("GITLAB_TOKEN")} CI/CD variable (Personal Access Token with ${color.bold("api")} + ${color.bold("write_repository")} scopes)`,
76+
);
77+
log.info(color.dim("Required for creating release PRs. Settings → CI/CD → Variables"));
78+
log.info("");
79+
}
80+
81+
if (generator instanceof GitHubCiGenerator) {
82+
log.info(
83+
`\n${color.yellow("Note:")} Enable ${color.bold("\"Allow GitHub Actions to create and approve pull requests\"")}`,
84+
);
85+
log.info(color.dim("Settings → Actions → General → Workflow permissions"));
86+
log.info("");
7487
}
7588

7689
log.info(color.dim("Commit and push these files to enable the workflows."));

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

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ export class ActionsReleasePrCommand extends BaseCommand {
2727
private async getProvider(): Promise<GitProvider> {
2828
if (!this.provider) {
2929
this.provider = await createGitProvider();
30-
await this.provider.ensureAvailable();
3130
}
3231
return this.provider;
3332
}
@@ -40,7 +39,6 @@ export class ActionsReleasePrCommand extends BaseCommand {
4039

4140
if (stones.length === 0) {
4241
log.info(color.dim("No pending stones found, skipping release PR"));
43-
await Bun.$`git checkout ${baseBranch}`;
4442
return;
4543
}
4644

@@ -54,16 +52,13 @@ export class ActionsReleasePrCommand extends BaseCommand {
5452
log.info(color.yellow("\n[dry-run] Would create/update release PR"));
5553
log.info(color.dim("\nPR Body preview:"));
5654
log.info(prBody);
57-
await Bun.$`git checkout ${baseBranch}`;
5855
return;
5956
}
6057

6158
await this.commitAndPushChanges(ctx, stones, packages);
6259
await this.createOrUpdatePr(prTitle, prBody);
63-
await Bun.$`git checkout ${baseBranch}`;
64-
} catch (error) {
65-
await this.restoreMainBranch();
66-
throw error;
60+
} finally {
61+
await this.restoreMainBranch(baseBranch);
6762
}
6863
}
6964

@@ -102,7 +97,9 @@ export class ActionsReleasePrCommand extends BaseCommand {
10297
s.start("Applying release changes...");
10398

10499
const changedFiles = await this.applyReleaseChanges(ctx, stones, packages);
105-
await Bun.$`git add ${changedFiles}`;
100+
for (const file of changedFiles) {
101+
await Bun.$`git add ${file}`.nothrow();
102+
}
106103

107104
const hasChanges = await Bun.$`git diff --cached --quiet`.nothrow();
108105
if (hasChanges.exitCode !== 0) {
@@ -313,9 +310,7 @@ export class ActionsReleasePrCommand extends BaseCommand {
313310
await provider.updatePr(prNumber, { body, title });
314311
}
315312

316-
private async restoreMainBranch() {
317-
const provider = await this.getProvider();
318-
const baseBranch = await provider.getDefaultBranch();
313+
private async restoreMainBranch(baseBranch: string) {
319314
try {
320315
await Bun.$`git checkout ${baseBranch}`.quiet();
321316
} catch (error) {

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

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
name: Sisyphus - Handle PR Merge
22

33
on:
4+
push:
5+
branches: [main]
46
pull_request:
57
types: [closed]
68
branches: [main]
79

10+
concurrency:
11+
group: sisyphus-${{ github.ref }}
12+
cancel-in-progress: false
13+
814
jobs:
915
handle-merge:
10-
if: github.event.pull_request.merged == true && !contains(github.event.pull_request.labels.*.name, 'sisyphus-release')
16+
if: github.event_name == 'pull_request' && github.event.pull_request.merged == true && !contains(github.event.pull_request.labels.*.name, 'sisyphus-release')
1117
runs-on: ubuntu-latest
1218
permissions:
1319
contents: write
@@ -54,3 +60,29 @@ jobs:
5460
run: bunx @r5n/sisyphus actions release-pr
5561
env:
5662
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
63+
64+
handle-push:
65+
if: github.event_name == 'push' && github.event.head_commit.committer.username != 'web-flow'
66+
runs-on: ubuntu-latest
67+
permissions:
68+
contents: write
69+
pull-requests: write
70+
steps:
71+
- uses: actions/checkout@v4
72+
with:
73+
fetch-depth: 0
74+
75+
- uses: oven-sh/setup-bun@v2
76+
77+
- name: Install dependencies
78+
run: bun install
79+
80+
- name: Configure git
81+
run: |
82+
git config user.name "r5n-bot[bot]"
83+
git config user.email "r5n-bot[bot]@users.noreply.github.com"
84+
85+
- name: Update release PR
86+
run: bunx @r5n/sisyphus actions release-pr
87+
env:
88+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
handle-merge:
22
stage: sisyphus
3-
image: oven/bun:latest
3+
image: oven/bun:alpine
4+
before_script:
5+
- apk add --no-cache git
46
rules:
5-
- if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_STATE == "merged" && $CI_MERGE_REQUEST_LABELS !~ /sisyphus-release/
7+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_COMMIT_TITLE =~ /^Merge branch/ && $CI_COMMIT_TITLE !~ /sisyphus\/release/
68
script:
9+
- |
10+
MR_IID=$(echo "$CI_COMMIT_DESCRIPTION" | grep -o '![0-9]*' | grep -o '[0-9]*' | head -1)
11+
if [ -z "$MR_IID" ]; then
12+
echo "No MR IID found in commit description, skipping"
13+
exit 0
14+
fi
15+
echo "Detected MR !$MR_IID"
716
- git fetch --unshallow || true
817
- git config user.name "r5n-bot[bot]"
918
- git config user.email "r5n-bot[bot]@users.noreply.gitlab.com"
1019
- bun install
1120
- |
12-
OUTPUT=$(bunx @r5n/sisyphus pr --url $CI_MERGE_REQUEST_PROJECT_URL/-/merge_requests/$CI_MERGE_REQUEST_IID --yes 2>&1) && EXIT_CODE=0 || EXIT_CODE=$?
21+
GIT_TOKEN="${GITLAB_TOKEN:-$CI_JOB_TOKEN}"
22+
GIT_USER=$([ -n "$GITLAB_TOKEN" ] && echo "oauth2" || echo "gitlab-ci-token")
23+
OUTPUT=$(bunx @r5n/sisyphus pr --url $CI_PROJECT_URL/-/merge_requests/$MR_IID --yes 2>&1) && EXIT_CODE=0 || EXIT_CODE=$?
1324
echo "$OUTPUT"
1425
if echo "$OUTPUT" | grep -q "Skipping:"; then
1526
echo "Stone creation skipped"
@@ -19,8 +30,20 @@ handle-merge:
1930
exit $EXIT_CODE
2031
fi
2132
git add .sisyphus/stones/
22-
git diff --cached --quiet || git commit -m 'chore: add stone from MR !'"$CI_MERGE_REQUEST_IID"
23-
git push https://oauth2:$GITLAB_TOKEN@$CI_SERVER_HOST/$CI_PROJECT_PATH.git HEAD:$CI_DEFAULT_BRANCH
33+
git diff --cached --quiet || git commit -m 'chore: add stone from MR !'"$MR_IID"
34+
git push "https://${GIT_USER}:${GIT_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" HEAD:$CI_DEFAULT_BRANCH
2435
bunx @r5n/sisyphus actions release-pr
25-
variables:
26-
GITLAB_TOKEN: $GITLAB_TOKEN
36+
37+
handle-push:
38+
stage: sisyphus
39+
image: oven/bun:alpine
40+
before_script:
41+
- apk add --no-cache git
42+
rules:
43+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_COMMIT_TITLE !~ /^Merge branch/
44+
script:
45+
- git fetch --unshallow || true
46+
- git config user.name "r5n-bot[bot]"
47+
- git config user.email "r5n-bot[bot]@users.noreply.gitlab.com"
48+
- bun install
49+
- bunx @r5n/sisyphus actions release-pr
Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
release:
22
stage: sisyphus
3-
image: oven/bun:latest
3+
image: oven/bun:alpine
4+
before_script:
5+
- apk add --no-cache git
46
rules:
57
- if: $CI_PIPELINE_SOURCE == "web"
68
when: manual
7-
- if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_STATE == "merged" && $CI_MERGE_REQUEST_LABELS =~ /sisyphus-release/
9+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_COMMIT_TITLE =~ /^Merge branch.*sisyphus\/release/
810
script:
911
- git fetch --unshallow || true
1012
- bun install
1113
- bunx @r5n/sisyphus roll --publish-only --yes
12-
variables:
13-
GITLAB_TOKEN: $GITLAB_TOKEN
14-
NPM_TOKEN: $NPM_TOKEN

packages/sisyphus/src/commands/init.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export class InitCommand extends BaseCommand {
3333

3434
if (useDefault) {
3535
ctx.config.save(SISYPHUS_DEFAULT_CONFIG);
36+
await this.setLastStoneToHead(ctx);
3637
this.displaySuccessNote(ctx.cli.version);
3738
return;
3839
}
@@ -49,6 +50,7 @@ export class InitCommand extends BaseCommand {
4950
const mergedConfig = deepMerge<SisyphusConfig>({ ...currentConfig }, newConfig);
5051

5152
ctx.config.save(mergedConfig);
53+
await this.setLastStoneToHead(ctx);
5254
this.displaySuccessNote(ctx.cli.version);
5355
}
5456

@@ -187,6 +189,16 @@ export class InitCommand extends BaseCommand {
187189
} as InitFormValues;
188190
}
189191

192+
private async setLastStoneToHead(ctx: InitCtx) {
193+
try {
194+
const result = await Bun.$`git rev-parse HEAD`.quiet();
195+
const commit = result.stdout.toString().trim();
196+
if (commit) {
197+
ctx.config.set("lastStone", { commit, date: new Date().toISOString() });
198+
}
199+
} catch {}
200+
}
201+
190202
private displaySuccessNote(version?: string) {
191203
note(
192204
`${color.magenta(`${color.bold("Sisyphus ")}${color.underline(color.italic(`v${version ?? "unknown"}`))}`)}${color.green(" is setup to roll stones.")}

packages/sisyphus/src/commands/roll.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,6 @@ export class RollCommand extends BaseCommand {
345345
s.stop("Release created");
346346
}
347347

348-
ctx.config.set("currentRelease", undefined);
349348
ctx.config.set("lastStone", { commit: await this.getCurrentCommit(), date: new Date().toISOString() });
350349

351350
note(
@@ -360,7 +359,7 @@ export class RollCommand extends BaseCommand {
360359
}
361360

362361
private createFallbackStone(packages: Package[]): Stone {
363-
const message = `Release ${packages.map((p) => `${p.name}@${p.version}`).join(", ")}`;
362+
const message = `Release ${packages.map((p) => `${p.name}@${p.newVersion ?? p.version}`).join(", ")}`;
364363
return Stone.create({ message }, 0);
365364
}
366365
}

packages/sisyphus/src/domain/Commit.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,21 +120,26 @@ export class Commit {
120120
}
121121

122122
private static async fetchFromRange(range: string): Promise<Commit[]> {
123-
const result = await Bun.$`git log ${range} --pretty=format:"%H%x1f%s%x1f%an" --no-merges`.quiet();
123+
const format = "%x00%H%x1f%s%x1f%an%x1f%b%x00";
124+
const result = await Bun.$`git log ${range} --pretty=format:${format} --name-only --no-merges`.quiet();
124125
const output = result.stdout.toString().trim();
125126

126127
if (!output) return [];
127128

129+
const segments = output.split("\x00").filter(Boolean);
128130
const commits: Commit[] = [];
129131

130-
for (const line of output.split("\n")) {
131-
const parts = line.split(FIELD_SEPARATOR);
132-
if (parts.length < 3) continue;
133-
const [hash, subject, author] = parts;
132+
for (let i = 0; i < segments.length; i += 2) {
133+
const fields = segments[i]!.split(FIELD_SEPARATOR);
134+
if (fields.length < 3) continue;
135+
136+
const [hash, subject, author] = fields;
134137
if (!hash || !subject || !author) continue;
135138

136-
const commit = await Commit.hydrate(hash, subject, author);
137-
commits.push(commit);
139+
const body = fields.slice(3).join(FIELD_SEPARATOR).trim() || undefined;
140+
const files = (segments[i + 1] ?? "").split("\n").filter(Boolean);
141+
142+
commits.push(Commit.parse(hash, subject, author).withFiles(files).withBody(body));
138143
}
139144

140145
return commits;

packages/sisyphus/src/providers/GitHubProvider.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ type GitHubCliPrResponse = {
4040
export class GitHubProvider extends GitProvider {
4141
readonly name = "github" as const;
4242

43+
private get apiPath(): string {
44+
return `repos/${encodeURIComponent(this.owner)}/${encodeURIComponent(this.repo)}`;
45+
}
46+
4347
async ensureAvailable(): Promise<void> {
4448
try {
4549
await Bun.$`which gh`.quiet();
@@ -83,14 +87,14 @@ export class GitHubProvider extends GitProvider {
8387

8488
async createPr(options: CreatePrOptions): Promise<PullRequest> {
8589
const result =
86-
await Bun.$`gh api repos/${this.owner}/${this.repo}/pulls --method POST -f head=${options.head} -f base=${options.base} -f title=${options.title} -f body=${options.body}`;
90+
await Bun.$`gh api ${this.apiPath}/pulls --method POST -f head=${options.head} -f base=${options.base} -f title=${options.title} -f body=${options.body}`;
8791

8892
const data = JSON.parse(result.stdout.toString());
8993
const prNumber = data.number as number;
9094

9195
if (options.labels && options.labels.length > 0) {
9296
const labelArgs = options.labels.flatMap((l) => ["-f", `labels[]=${l}`]);
93-
await Bun.$`gh api repos/${this.owner}/${this.repo}/issues/${prNumber}/labels --method POST ${labelArgs}`;
97+
await Bun.$`gh api ${this.apiPath}/issues/${prNumber}/labels --method POST ${labelArgs}`;
9498
}
9599

96100
return this.getPr(prNumber);
@@ -106,7 +110,7 @@ export class GitHubProvider extends GitProvider {
106110

107111
async getPr(number: number): Promise<PullRequest> {
108112
try {
109-
const result = await Bun.$`gh api repos/${this.owner}/${this.repo}/pulls/${number}`.quiet();
113+
const result = await Bun.$`gh api ${this.apiPath}/pulls/${number}`.quiet();
110114
const data = JSON.parse(result.stdout.toString());
111115

112116
return this.mapRestApiResponse(data);
@@ -130,7 +134,7 @@ export class GitHubProvider extends GitProvider {
130134
async getPrCommits(number: number): Promise<string[]> {
131135
try {
132136
const result =
133-
await Bun.$`gh api repos/${this.owner}/${this.repo}/pulls/${number}/commits --jq '.[].sha'`.quiet();
137+
await Bun.$`gh api ${this.apiPath}/pulls/${number}/commits --jq '.[].sha'`.quiet();
134138
return result.stdout.toString().trim().split("\n").filter(Boolean);
135139
} catch {
136140
return [];
@@ -140,7 +144,7 @@ export class GitHubProvider extends GitProvider {
140144
async getPrFiles(number: number): Promise<string[]> {
141145
try {
142146
const result =
143-
await Bun.$`gh api repos/${this.owner}/${this.repo}/pulls/${number}/files --jq '.[].filename'`.quiet();
147+
await Bun.$`gh api ${this.apiPath}/pulls/${number}/files --jq '.[].filename'`.quiet();
144148
return result.stdout.toString().trim().split("\n").filter(Boolean);
145149
} catch {
146150
return [];

0 commit comments

Comments
 (0)