Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
cc017e1
feat(sisyphus): add actions command for GitHub Actions integration
deralaxo Mar 11, 2026
c0a6d5b
refactor(sisyphus): add GitProvider abstraction and rename github to
deralaxo Mar 12, 2026
3d18eb6
feat(sisyphus): implement GitLabProvider for glab CLI integration
deralaxo Mar 11, 2026
a979a0d
feat(sisyphus): add GitLab CI support to actions init command
deralaxo Mar 11, 2026
55ba5da
fix(sisyphus): include root files when analyzing PRs in single-package
deralaxo Mar 11, 2026
db970db
fix(sisyphus): use consistent field separator in Commit.fromHash
deralaxo Mar 12, 2026
999cf03
feat(sisyphus): add two-stage release flow with stone archiving and
deralaxo Mar 12, 2026
681f60e
refactor(sisyphus): merge create-stone and release-pr workflows to avoid
deralaxo Mar 12, 2026
d714285
fix(sisyphus): remove .quiet() from git providers shell commands to
deralaxo Mar 12, 2026
ab43eb6
fix(sisyphus): properly propagate errors in workflow templates while
deralaxo Mar 12, 2026
3d2564d
fix(cli-core): exit with code 1 on errors
deralaxo Mar 12, 2026
face1fe
fix(sisyphus): support publish-only mode in ReleaseOrchestrator
deralaxo Mar 12, 2026
0eb0593
fix(sisyphus): use REST API for PR creation to avoid GraphQL permission
deralaxo Mar 13, 2026
a70a456
fix(sisyphus): separate --all and --yes flags in actions init
deralaxo Mar 13, 2026
2ca1ea2
fix(sisyphus): show shell errors for critical git and GitHub API
deralaxo Mar 13, 2026
a248b83
fix(sisyphus): stash changes before switching branches in release-pr
deralaxo Mar 13, 2026
f61cbcf
fix(sisyphus): only stage release-related files in release-pr
deralaxo Mar 13, 2026
af12fa3
feat(sisyphus): add workflow_dispatch to release workflow template
deralaxo Mar 13, 2026
ae9aa4a
fix(sisyphus): store old version in currentRelease for release notes
deralaxo Mar 13, 2026
b844179
fix(sisyphus): show new version in publish-only log output
deralaxo Mar 13, 2026
620c76c
fix(sisyphus): improve GitLab workflow templates
deralaxo Mar 16, 2026
234a387
feat(sisyphus): explicitly detect and handle squash vs merge PRs
deralaxo Mar 16, 2026
c991d7b
refactor(sisyphus): improve release PR body format
deralaxo Mar 16, 2026
3e05410
fix(sisyphus): filter release notes by package and use collapsible
deralaxo Mar 16, 2026
52ee71d
feat(sisyphus): add configurable changelog headers
deralaxo Mar 16, 2026
699cb41
fix(sisyphus): use commit.author config for release commits
deralaxo Mar 16, 2026
a03b93f
feat(sisyphus): exclude PR commits from --fromCommits by default
deralaxo Mar 18, 2026
310d0f1
fix: handle invalid regex patterns and simplify Package version methods
deralaxo Mar 18, 2026
6d804a4
feat(sisyphus): add commit filtering to skip bot and release commits
deralaxo Mar 18, 2026
096dcf9
feat(sisyphus): auto-generate stones from commits in release-pr flow
deralaxo Mar 18, 2026
3f41d67
fix(sisyphus): show commit details in release PR body
deralaxo Mar 18, 2026
b39114f
fix(sisyphus): update lastStone after generating stones from commits
deralaxo Mar 18, 2026
85fcf51
fix(sisyphus): set lastStone from all released stones, not just
deralaxo Mar 18, 2026
56f4ca2
fix(sisyphus): generate stones on release branch in release-pr flow
deralaxo Mar 18, 2026
1e239e4
fix(sisyphus): add logging to error recovery catch blocks
deralaxo Mar 19, 2026
bc8505a
chore(sisyphus): use r5n-bot[bot] for CI commit author
deralaxo Mar 19, 2026
7e7e653
refactor(sisyphus): feedback
deralaxo Mar 19, 2026
87c3093
refactor(sisyphus): extract constants and improve type safety
deralaxo Mar 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions packages/core/src/abstract-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,19 +128,22 @@ export abstract class AbstractCLI {
private handleError(error: unknown) {
if (this.metadata.onError?.(error)) return;

if (error instanceof Cancel) return;
if (error instanceof Cancel) {
process.exit(0);
}

if (error instanceof Exit) {
log.warn(color.yellow(error.message));
if (error.hint) log.info(color.dim(error.hint));
return;
process.exit(0);
}

if (error instanceof Error) {
log.error(color.red(error.message));
return;
process.exit(1);
}

console.error(color.red("Error:"), error);
process.exit(1);
}
}
3 changes: 3 additions & 0 deletions packages/sisyphus/build.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { cp } from "node:fs/promises";
import { bunPackageBuilder } from "@r5n/tools/builder";

await bunPackageBuilder({ banner: "#!/usr/bin/env bun", maxSize: 150, packages: "bundle", target: "bun", type: "cli" });

await cp("src/commands/actions/templates", "dist/templates", { recursive: true });
2 changes: 2 additions & 0 deletions packages/sisyphus/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AbstractCLI, ConfigManager } from "@r5n/cli-core";
import { version } from "../package.json";
import {
ActionsCommand,
CheckCommand,
InitCommand,
MigrateCommand,
Expand All @@ -27,6 +28,7 @@ class SisyphusCLI extends AbstractCLI {

init() {
this.registerCommands([
new ActionsCommand(),
new CheckCommand(),
new InitCommand(),
new MigrateCommand(),
Expand Down
106 changes: 106 additions & 0 deletions packages/sisyphus/src/commands/actions/CiGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import type { Provider } from "../../providers";

export type WorkflowType = "create-stone" | "release";

export type WorkflowFile = { path: string; content: string };

export type WorkflowConfig = { name: string; description: string };

export const WORKFLOW_CONFIGS: Record<WorkflowType, WorkflowConfig> = {
"create-stone": {
description: "When a PR is merged, create stone and update release PR",
name: "Handle PR merge (stone + release PR)",
},
release: { description: "When the release PR is merged, publish packages", name: "Publish release" },
};

const TEMPLATES_DIR = join(dirname(fileURLToPath(import.meta.url)), "templates");

const TEMPLATE_FILENAMES: Record<WorkflowType, string> = {
"create-stone": "sis-create-stone.yml",
release: "sis-release.yml",
};

export abstract class CiGenerator {
abstract readonly provider: Provider;
protected abstract readonly outputDir: string;

async generate(workflows: WorkflowType[]): Promise<WorkflowFile[]> {
const files: WorkflowFile[] = [];

for (const type of workflows) {
const content = await this.readTemplate(type);
const path = this.getOutputPath(type);
files.push({ content, path });
}

return files;
}

getExistingFiles(workflows: WorkflowType[]): string[] {
return workflows.map((type) => this.getOutputPath(type)).filter((path) => existsSync(path));
}

protected async readTemplate(type: WorkflowType): Promise<string> {
const templatePath = join(TEMPLATES_DIR, this.provider, TEMPLATE_FILENAMES[type]);
return readFile(templatePath, "utf-8");
}

protected abstract getOutputPath(type: WorkflowType): string;
}

export class GitHubCiGenerator extends CiGenerator {
readonly provider = "github" as const;
protected readonly outputDir = ".github/workflows";

protected getOutputPath(type: WorkflowType): string {
return join(this.outputDir, TEMPLATE_FILENAMES[type]);
}
}

const GITLAB_SISYPHUS_FILE = ".gitlab-ci-sisyphus.yml";

export class GitLabCiGenerator extends CiGenerator {
readonly provider = "gitlab" as const;
protected readonly outputDir = ".";

async generate(workflows: WorkflowType[]): Promise<WorkflowFile[]> {
const sections: string[] = [];

sections.push("stages:\n - sisyphus\n");

for (const type of workflows) {
const content = await this.readTemplate(type);
sections.push(content);
}

return [{ content: sections.join("\n"), path: GITLAB_SISYPHUS_FILE }];
}

getExistingFiles(_workflows: WorkflowType[]): string[] {
return existsSync(GITLAB_SISYPHUS_FILE) ? [GITLAB_SISYPHUS_FILE] : [];
}

getIncludeInstruction(): string {
return `include:\n - local: '${GITLAB_SISYPHUS_FILE}'`;
}

protected getOutputPath(_type: WorkflowType): string {
return GITLAB_SISYPHUS_FILE;
}
}

export function createCiGenerator(provider: Provider): CiGenerator {
switch (provider) {
case "github":
return new GitHubCiGenerator();
case "gitlab":
return new GitLabCiGenerator();
default:
throw new Error(`CI generation is not supported for ${provider}`);
}
}
12 changes: 12 additions & 0 deletions packages/sisyphus/src/commands/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { BaseCommand } from "../../base-command";
import { ActionsInitCommand } from "./init";
import { ActionsReleasePrCommand } from "./release-pr";

export class ActionsCommand extends BaseCommand {
name = "actions";
description = "CI/CD integration";

init() {
this.registerSubcommands([new ActionsInitCommand(), new ActionsReleasePrCommand()]);
}
}
107 changes: 107 additions & 0 deletions packages/sisyphus/src/commands/actions/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { existsSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import { dirname } from "node:path";
import { args, color, confirm, Exit, log, multiselect, note } from "@r5n/cli-core";
import { BaseCommand, type Ctx } from "../../base-command";
import { detectProvider } from "../../providers";
import { createCiGenerator, GitLabCiGenerator, WORKFLOW_CONFIGS, type WorkflowType } from "./CiGenerator";

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

const initArgs = args({
all: { alias: "a", default: false, description: "Install all workflows", type: "boolean" },
createStone: { default: false, description: "Install create-stone workflow", type: "boolean" },
dryRun: { alias: "d", default: false, description: "Preview without writing files", type: "boolean" },
release: { default: false, description: "Install release workflow", type: "boolean" },
yes: { alias: "y", default: false, description: "Skip confirmation prompts", type: "boolean" },
});

type InitCtx = Ctx<typeof initArgs>;

export class ActionsInitCommand extends BaseCommand {
name = "init";
description = "Set up CI workflows for automated releases";
args = initArgs;
prompts = true;

async execute(ctx: InitCtx) {
const provider = await detectProvider();
const generator = createCiGenerator(provider);

log.info(`Detected ${color.bold(PROVIDER_LABELS[provider])} repository\n`);

const selected = await this.selectWorkflows(ctx);

if (selected.length === 0) {
log.info(color.dim("No workflows selected"));
return;
}

const existing = generator.getExistingFiles(selected);
if (existing.length > 0 && !ctx.args.yes) {
if (!ctx.interactive) {
throw new Exit(`Workflows already exist: ${existing.join(", ")}`, "Use --yes to overwrite existing workflows");
}
log.warn(`The following files already exist:\n${existing.map((f) => ` ${f}`).join("\n")}`);
const overwrite = await confirm({ initialValue: false, message: "Overwrite existing files?" });
if (!overwrite) return;
}

const files = await generator.generate(selected);

if (ctx.args.dryRun) {
for (const file of files) {
log.info(`\n${color.bold(file.path)}:`);
log.info(color.dim(file.content));
}
return;
}

for (const file of files) {
const dir = dirname(file.path);
if (dir !== "." && !existsSync(dir)) {
await mkdir(dir, { recursive: true });
}
await writeFile(file.path, file.content, "utf-8");
}

note(files.map((f) => ` ${color.green("+")} ${f.path}`).join("\n"), color.green("CI workflows created"));

if (generator instanceof GitLabCiGenerator) {
log.info(`\nAdd this to your ${color.bold(".gitlab-ci.yml")}:\n`);
log.info(color.cyan(generator.getIncludeInstruction()));
log.info("");
}

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

private async selectWorkflows(ctx: InitCtx): Promise<WorkflowType[]> {
const fromFlags = this.getWorkflowsFromFlags(ctx);
if (fromFlags.length > 0) return fromFlags;

if (!ctx.interactive) {
throw new Exit("No workflows specified", "Use --all or specify workflows with --createStone, --release");
}

return multiselect({
initialValues: Object.keys(WORKFLOW_CONFIGS) as WorkflowType[],
message: "Select workflows to create",
options: Object.entries(WORKFLOW_CONFIGS).map(([key, config]) => ({
hint: config.description,
label: config.name,
value: key as WorkflowType,
})),
required: false,
});
}

private getWorkflowsFromFlags(ctx: InitCtx): WorkflowType[] {
if (ctx.args.all) return Object.keys(WORKFLOW_CONFIGS) as WorkflowType[];

const selected: WorkflowType[] = [];
if (ctx.args.createStone) selected.push("create-stone");
if (ctx.args.release) selected.push("release");
return selected;
}
}
Loading
Loading