Skip to content

Commit

Permalink
feat: add glab-status cli util
Browse files Browse the repository at this point in the history
  • Loading branch information
JiLiZART committed Sep 25, 2024
1 parent 302ac2f commit b59f89f
Show file tree
Hide file tree
Showing 8 changed files with 360 additions and 17 deletions.
Binary file modified bun.lockb
Binary file not shown.
22 changes: 16 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"name": "npm-bun-tsup-library",
"description": "My Library template",
"name": "glab-status",
"description": "GitLab CLI rich status display tool",
"type": "module",
"bin": {
"glab-status": "dist/cli.cjs"
},
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
Expand All @@ -25,8 +28,11 @@
"./package.json": "./package.json"
},
"keywords": [
"cool",
"package"
"gitlab",
"merge request",
"pipeline",
"status",
"cli"
],
"devDependencies": {
"@changesets/cli": "^2.26.2",
Expand All @@ -37,5 +43,9 @@
"peerDependencies": {
"typescript": "^5.0.0"
},
"license": "MIT"
}
"license": "MIT",
"dependencies": {
"date-fns": "4.1.0",
"picocolors": "1.1.0"
}
}
47 changes: 47 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env bun
import colors from "picocolors";
import { Git } from "./git";
import { GitLab } from "./gitlab";
import {
Display,
DisplayCommit,
DisplayEnvironment,
DisplayMergeRequest,
DisplayPipelineJobs,
} from "./display";

if (!process.env.GITLAB_TOKEN) {
console.error("Please set the GITLAB_TOKEN environment variable.");
console.error("You can generate a new token here:");
console.error(
colors.blue("> https://gitlab.com/-/user_settings/personal_access_tokens")
);
process.exit(1);
}

try {
const git = new Git();
const gitlab = new GitLab(process.env.GITLAB_TOKEN);
const myProjects = await gitlab.myProjectsByName(await git.repoName());
const branch = await git.branch();

for (const project of myProjects) {
const pipeline = await project.pipelineBy(branch);

if (pipeline) {
new Display(
new DisplayCommit(await project.commitBy(pipeline.sha), pipeline),
new DisplayMergeRequest(await project.mergeRequestBy(branch)),
new DisplayEnvironment(await project.environmentsBy(branch)),
new DisplayPipelineJobs(await project.pipelineJobsBy(pipeline.id))
);
} else {
console.error("No pipelines found for this project.");
}
}
} catch (error: unknown) {
if (error instanceof Error) {
console.error("Error fetching pipeline status:", error.message);
}
console.error(error);
}
151 changes: 151 additions & 0 deletions src/display.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import colors from "picocolors";
import { parseISO, formatRelative } from "date-fns";

export class Display {
rows: string[] = [];
constructor(...args: unknown[]) {
const rows = args[0];

if (rows instanceof Display) {
(args as Display[]).forEach((display) => display.display());
} else if (Array.isArray(rows)) {
this.rows = rows;
}
}

display() {
this.rows.forEach((row) => console.log(row));
}
}

export class DisplayEnvironment extends Display {
constructor(environment: { external_url: string }) {
super([
colors.bold("Environment"),
`> ${colors.blue(environment.external_url)}`,
"",
]);
}
}

export class DisplayMergeRequest extends Display {
constructor(mr: {
merged_at: string;
state: string;
title: string;
iid: string;
web_url: string;
}) {
const mrid = colors.red(`#${mr.iid}`);
const date = colors.green(
`(${formatRelative(parseISO(mr.merged_at), new Date())})`
);
const statusEmojis = {
opened: colors.greenBright("✅ Opened"),
closed: colors.redBright("❌ Closed"),
merged: colors.greenBright("✅ Merged"),
} as Record<string, string>;

const status = statusEmojis[mr.state] || "";

super([
colors.bold("Merge Request"),
`${mrid} - ${mr.title} - ${status} ${date}`,
`> ${colors.blue(mr.web_url)}`,
"",
]);
}
}

export class DisplayCommit extends Display {
static statusEmojis = {
created: colors.whiteBright("⚙️ Created"),
waiting_for_resource: colors.yellowBright("⏳ Waiting for resource"),
preparing: colors.yellowBright("🔄 Preparing"),
pending: colors.yellowBright("💤 Pending"),
running: colors.whiteBright("🚀 Running"),
success: colors.greenBright("✅ Success"),
failed: colors.redBright("❌ Failed"),
canceled: colors.redBright("🛑 Canceled"),
skipped: colors.yellowBright("⏭️ Skipped"),
manual: colors.blueBright("👤 Manual"),
scheduled: colors.blueBright("📅 Scheduled"),
} as Record<string, string>;

constructor(
commit: {
created_at: string;
short_id: string;
title: string;
author_name: string;
},
pipeline: { status: string; web_url: string }
) {
const sha = colors.red(`${commit.short_id}`);
const author = colors.blueBright(`<${commit.author_name}>`);
const date = colors.green(
`(${formatRelative(parseISO(commit.created_at), new Date())})`
);
const title = colors.whiteBright(commit.title);

super([
colors.bold("Commit"),
`${sha} - ${title} ${date} ${author} - ${
DisplayCommit.statusEmojis[pipeline.status]
}`,
`> ${colors.blue(pipeline.web_url)}`,
"",
]);
}
}

type PipelineJob = {
status: string;
stage: string;
duration: number;
name: string;
};

export class DisplayPipelineJobs extends Display {
static statusMap = {
success: "🟢",
failed: "🔴",
canceled: "🟠",
skipped: "🟡",
pending: "🟣",
running: "🔵",
created: "⚪",
} as Record<string, string>;

constructor(pipelineJobs: PipelineJob[] = []) {
const stages = pipelineJobs
.filter((item) => item.status !== "manual")
.reduce((acc, item) => {
const items = (acc[item.stage] = acc[item.stage] || []);
items.push(item);
return acc;
}, {} as Record<string, PipelineJob[]>);

super(
Object.entries(stages)
.map(([stage, stages]) => {
return [
colors.bold(stage),
stages
.map((stage) => {
const secs = colors.gray(`(${Math.round(stage.duration)}s)`);
const status =
DisplayPipelineJobs.statusMap[stage.status] ||
stage.status ||
"";

return String(`${status} ${stage.name} ${secs}`).trim();
})
.join(" "),
"",
].flat();
})
.flat()
);
}
}
33 changes: 33 additions & 0 deletions src/git.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { $ } from "bun";

export class Git {
cwd: string;

constructor(cwd = process.cwd()) {
this.cwd = cwd;
}

async branch() {
const stdout = await $`git rev-parse --abbrev-ref HEAD`
.cwd(this.cwd)
.text();

return stdout.trim();
}

async remoteUrl() {
const stdout = await $`git ls-remote --get-url`.cwd(this.cwd).text();

return stdout
.trim()
.replace(/^git@(.*?):/, "https://$1/")
.replace(/[A-z0-9\-]+@/, "")
.replace(/\.git$/, "");
}

async repoName() {
const projectName = (remoteUrl: string) => remoteUrl.split("/").pop();

return projectName(await this.remoteUrl());
}
}
102 changes: 102 additions & 0 deletions src/gitlab.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
export class GitlabProject {
GITLAB_API_PIPELINES_URL = `/projects/:id/pipelines`;
GITLAB_API_PIPELINE_JOBS_URL = `/projects/:id/pipelines/:pipeline_id/jobs`;
GITLAB_API_COMMIT_URL = `/projects/:id/repository/commits/:sha`;
GITLAB_API_PROJECT_MRS_URL = `/projects/:id/merge_requests`;
GITLAB_API_ENVS_URL = `/projects/:id/environments`;

constructor(gitlab, projectId) {
this.gitlab = gitlab;
this.projectId = projectId;
}

async commitBy(sha) {
return this.gitlab._get(
this.GITLAB_API_COMMIT_URL.replace(":id", this.projectId).replace(
":sha",
sha
)
);
}

async pipelineBy(branch) {
return this.gitlab
._get(this.GITLAB_API_PIPELINES_URL.replace(":id", this.projectId), {
ref: branch,
per_page: 1,
})
.then((pipelines) => pipelines[0]);
}

async mergeRequestBy(branch) {
return this.gitlab
._get(this.GITLAB_API_PROJECT_MRS_URL.replace(":id", this.projectId), {
source_branch: branch,
})
.then((mrs) => mrs[0]);
}

async environmentsBy(branch) {
return this.gitlab
._get(this.GITLAB_API_ENVS_URL.replace(":id", this.projectId), {
search: branch,
})
.then((envs) => envs[0]);
}

async pipelineJobsBy(pipelineId) {
return this.gitlab._get(
this.GITLAB_API_PIPELINE_JOBS_URL.replace(":id", this.projectId).replace(
":pipeline_id",
pipelineId
)
);
}
}

export class GitLab {
BASE_URL = "https://gitlab.com/api/v4";
GITLAB_API_PROJECTS_URL = `/projects`;

constructor(token) {
this.token = token;
}

async _get(url, params = {}) {
return fetch(
`${this.BASE_URL}${url}?${new URLSearchParams(params).toString()}`,
{
headers: { "Private-Token": this.token },
params,
}
).then((res) => res.json());
}

async projectsByName(name) {
return this._get(this.GITLAB_API_PROJECTS_URL, {
search: name,
per_page: 100,
}).then((projects) =>
projects.map((item) => ({
id: item.id,
name: item.name,
ssh_url_to_repo: item.ssh_url_to_repo,
http_url_to_repo: item.http_url_to_repo,
web_url: item.web_url,
}))
);
}

async myProjectsByName(name) {
const filterProjects = (projects, name) =>
projects.filter(
(project) =>
project.ssh_url_to_repo.includes(name) ||
project.http_url_to_repo.includes(name)
);

return filterProjects(await this.projectsByName(name), name).map(
(project) => new GitlabProject(this, project.id)
);
}
}
6 changes: 3 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export default function hello() {
return 'world'
}
export * from "./display";
export * from "./git";
export * from "./gitlab";
Loading

0 comments on commit b59f89f

Please sign in to comment.