feat(api): add /version endpoint and increment version to 0.1.25
This commit is contained in:
175
.claude/commands/deploy-stg.md
Normal file
175
.claude/commands/deploy-stg.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# deploy-stg
|
||||
|
||||
Deploy ke staging environment secara penuh: version bump, cek migrasi, commit, push, trigger GitHub workflow (publish + re-pull), dan verifikasi versi.
|
||||
|
||||
**Repo GitHub:** `bipproductbali/desa-darmasaba`
|
||||
**Branch stg:** `stg`
|
||||
**STG URL:** `https://desa-darmasaba-stg.wibudev.com`
|
||||
|
||||
---
|
||||
|
||||
## Alur Eksekusi
|
||||
|
||||
### Langkah 0 — Cek /api/version
|
||||
Sebelum apapun, pastikan endpoint `/api/version` sudah ada:
|
||||
```bash
|
||||
grep -n '"/version"' src/app/api/\[\[...slugs\]\]/route.ts
|
||||
```
|
||||
Jika belum ada, tambahkan langsung ke main API group (referensi dari package.json via `fs.readFile`).
|
||||
|
||||
---
|
||||
|
||||
### Langkah 1 — Version Bump
|
||||
|
||||
Baca versi saat ini dari `package.json`, bump patch version (+1), lalu tulis ulang:
|
||||
```bash
|
||||
# Baca versi saat ini
|
||||
node -e "const p=require('./package.json'); const [maj,min,pat]=p.version.split('.').map(Number); console.log(maj+'.'+min+'.'+(pat+1))"
|
||||
```
|
||||
Update `package.json` dengan versi baru. Simpan versi baru sebagai `$NEW_VERSION`.
|
||||
|
||||
---
|
||||
|
||||
### Langkah 2 — Cek Prisma Migration
|
||||
|
||||
```bash
|
||||
bunx prisma migrate status
|
||||
```
|
||||
|
||||
- Jika output mengandung kata `pending` atau `drift` → buat migrasi baru:
|
||||
```bash
|
||||
bunx prisma migrate dev --name bump-stg-<new_version>
|
||||
```
|
||||
- Jika sudah up-to-date → lanjut ke langkah berikutnya.
|
||||
|
||||
---
|
||||
|
||||
### Langkah 3 — Build Check
|
||||
|
||||
```bash
|
||||
bun run build
|
||||
```
|
||||
|
||||
Jika build gagal, **stop** dan perbaiki error dulu sebelum melanjutkan deploy.
|
||||
|
||||
---
|
||||
|
||||
### Langkah 4 — Commit
|
||||
|
||||
Stage semua perubahan dan commit:
|
||||
```bash
|
||||
git add package.json
|
||||
git add prisma/migrations/ # jika ada migrasi baru
|
||||
git commit -m "chore: bump version to $NEW_VERSION for stg deploy"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Langkah 5 — Push ke origin/stg
|
||||
|
||||
```bash
|
||||
git push origin HEAD:stg
|
||||
```
|
||||
|
||||
Tunggu push selesai sebelum trigger workflow.
|
||||
|
||||
---
|
||||
|
||||
### Langkah 6 — Trigger publish.yml
|
||||
|
||||
**Input 1:** `stack_env` = `stg`
|
||||
**Input 2:** `tag` = versi dari package.json (contoh: `0.1.25`)
|
||||
|
||||
```bash
|
||||
gh workflow run publish.yml \
|
||||
--repo bipprojectbali/desa-darmasaba \
|
||||
--ref stg \
|
||||
-f stack_env=stg \
|
||||
-f tag=$NEW_VERSION
|
||||
```
|
||||
|
||||
Tunggu 5 detik lalu dapatkan run ID:
|
||||
```bash
|
||||
sleep 5
|
||||
RUN_ID=$(gh run list \
|
||||
--workflow=publish.yml \
|
||||
--repo bipprojectbali/desa-darmasaba \
|
||||
--limit 1 \
|
||||
--json databaseId \
|
||||
-q '.[0].databaseId')
|
||||
```
|
||||
|
||||
Monitor sampai selesai:
|
||||
```bash
|
||||
gh run watch $RUN_ID --repo bipprojectbali/desa-darmasaba
|
||||
```
|
||||
|
||||
Jika publish **gagal** → **stop**, jangan lanjut ke re-pull.
|
||||
|
||||
---
|
||||
|
||||
### Langkah 7 — Trigger re-pull.yml
|
||||
|
||||
Setelah publish berhasil, trigger re-pull:
|
||||
|
||||
**Input 1:** `stack_env` = `stg`
|
||||
**Input 2:** `stack_name` = `desa-darmasaba` → stack yang di-deploy: `desa-darmasaba-stg`
|
||||
|
||||
```bash
|
||||
gh workflow run re-pull.yml \
|
||||
--repo bipprojectbali/desa-darmasaba \
|
||||
--ref main \
|
||||
-f stack_name=desa-darmasaba \
|
||||
-f stack_env=stg
|
||||
```
|
||||
|
||||
Tunggu 5 detik lalu monitor:
|
||||
```bash
|
||||
sleep 5
|
||||
REPULL_ID=$(gh run list \
|
||||
--workflow=re-pull.yml \
|
||||
--repo bipprojectbali/desa-darmasaba \
|
||||
--limit 1 \
|
||||
--json databaseId \
|
||||
-q '.[0].databaseId')
|
||||
gh run watch $REPULL_ID --repo bipprojectbali/desa-darmasaba
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Langkah 8 — Verifikasi Versi
|
||||
|
||||
Bandingkan versi di stg dengan versi lokal:
|
||||
|
||||
```bash
|
||||
# Versi lokal
|
||||
LOCAL_VER=$(node -e "console.log(require('./package.json').version)")
|
||||
|
||||
# Versi di STG (tunggu container siap ~30 detik)
|
||||
sleep 30
|
||||
STG_VER=$(curl -s https://desa-darmasaba-stg.wibudev.com/api/version | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).version))")
|
||||
|
||||
echo "Local : $LOCAL_VER"
|
||||
echo "STG : $STG_VER"
|
||||
```
|
||||
|
||||
- Jika `LOCAL_VER == STG_VER` → **Deploy berhasil!**
|
||||
- Jika berbeda → cek logs container di Portainer atau jalankan `gh run view $REPULL_ID --repo bipprojectbali/desa-darmasaba --log`
|
||||
|
||||
---
|
||||
|
||||
## Ringkasan Workflow Inputs
|
||||
|
||||
| Workflow | Input | Value |
|
||||
|----------|-------|-------|
|
||||
| `publish.yml` | `stack_env` | `stg` |
|
||||
| `publish.yml` | `tag` | versi dari `package.json` (e.g. `0.1.25`) |
|
||||
| `re-pull.yml` | `stack_name` | `desa-darmasaba` |
|
||||
| `re-pull.yml` | `stack_env` | `stg` |
|
||||
|
||||
## Catatan
|
||||
|
||||
- Jangan jalankan `re-pull.yml` jika `publish.yml` belum selesai/berhasil.
|
||||
- Verifikasi versi dilakukan via `/api/version` (bukan `/api/utils/version`).
|
||||
- Jika `gh` belum login: `gh auth login`.
|
||||
- Untuk cek status workflow manual: `gh run list --repo bipprojectbali/desa-darmasaba`.
|
||||
258
.claude/mcp/github-actions.mjs
Normal file
258
.claude/mcp/github-actions.mjs
Normal file
@@ -0,0 +1,258 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* MCP Server: GitHub Actions Workflow Tools
|
||||
* Tools: trigger_publish, trigger_repull, get_workflow_runs, watch_workflow_run
|
||||
*/
|
||||
|
||||
import { execSync, spawnSync } from "child_process";
|
||||
|
||||
const REPO = "bipprojectbali/desa-darmasaba";
|
||||
|
||||
// --- MCP Protocol Helpers ---
|
||||
|
||||
function send(obj) {
|
||||
process.stdout.write(JSON.stringify(obj) + "\n");
|
||||
}
|
||||
|
||||
function respond(id, result) {
|
||||
send({ jsonrpc: "2.0", id, result });
|
||||
}
|
||||
|
||||
function respondError(id, code, message) {
|
||||
send({ jsonrpc: "2.0", id, error: { code, message } });
|
||||
}
|
||||
|
||||
// --- Shell Helper ---
|
||||
|
||||
function runCmd(cmd) {
|
||||
const r = spawnSync("sh", ["-c", cmd], { encoding: "utf-8" });
|
||||
if (r.error) return { ok: false, out: r.error.message };
|
||||
if (r.status !== 0) return { ok: false, out: (r.stderr || r.stdout || "").trim() };
|
||||
return { ok: true, out: (r.stdout || "").trim() };
|
||||
}
|
||||
|
||||
function getLatestRunId(workflow, delaySecs = 3) {
|
||||
const r = runCmd(
|
||||
`sleep ${delaySecs} && gh run list --workflow=${workflow} --repo ${REPO} --limit 1 --json databaseId -q '.[0].databaseId'`
|
||||
);
|
||||
return r.ok ? r.out.trim() : null;
|
||||
}
|
||||
|
||||
// --- Tool Definitions ---
|
||||
|
||||
const TOOLS = [
|
||||
{
|
||||
name: "trigger_publish",
|
||||
description:
|
||||
"Trigger publish.yml workflow: build & push Docker image to GHCR. Returns run ID.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
stack_env: {
|
||||
type: "string",
|
||||
enum: ["dev", "stg", "prod"],
|
||||
description: "Target environment (branch dengan nama ini akan di-checkout)",
|
||||
},
|
||||
tag: {
|
||||
type: "string",
|
||||
description: "Image tag, biasanya versi dari package.json (e.g. 0.1.25)",
|
||||
},
|
||||
},
|
||||
required: ["stack_env", "tag"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "trigger_repull",
|
||||
description:
|
||||
"Trigger re-pull.yml workflow: redeploy stack di Portainer. Hanya jalankan setelah publish berhasil.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
stack_name: {
|
||||
type: "string",
|
||||
description: "Nama stack (e.g. desa-darmasaba). Stack yang di-deploy: <stack_name>-<stack_env>",
|
||||
},
|
||||
stack_env: {
|
||||
type: "string",
|
||||
enum: ["dev", "stg", "prod"],
|
||||
description: "Target environment",
|
||||
},
|
||||
},
|
||||
required: ["stack_name", "stack_env"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_workflow_runs",
|
||||
description: "List run terbaru dari suatu workflow beserta status dan conclusion-nya.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
workflow: {
|
||||
type: "string",
|
||||
description: "Nama file workflow (e.g. publish.yml, re-pull.yml)",
|
||||
},
|
||||
limit: {
|
||||
type: "number",
|
||||
description: "Jumlah run yang ditampilkan (default: 5)",
|
||||
},
|
||||
},
|
||||
required: ["workflow"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "watch_workflow_run",
|
||||
description:
|
||||
"Lihat status detail sebuah workflow run: apakah in_progress, success, atau failure.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
run_id: {
|
||||
type: "number",
|
||||
description: "ID workflow run (dapat dari get_workflow_runs)",
|
||||
},
|
||||
},
|
||||
required: ["run_id"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// --- Tool Handlers ---
|
||||
|
||||
function handleTriggerPublish(args) {
|
||||
const { stack_env, tag } = args;
|
||||
const triggerCmd = `gh workflow run publish.yml --repo ${REPO} --ref ${stack_env} -f stack_env=${stack_env} -f tag=${tag}`;
|
||||
const r = runCmd(triggerCmd);
|
||||
if (!r.ok) return { ok: false, error: r.out };
|
||||
|
||||
const runId = getLatestRunId("publish.yml");
|
||||
return {
|
||||
ok: true,
|
||||
message: `Workflow publish.yml berhasil di-trigger (env=${stack_env}, tag=${tag})`,
|
||||
run_id: runId ? Number(runId) : null,
|
||||
monitor_hint: runId
|
||||
? `Gunakan watch_workflow_run dengan run_id: ${runId}`
|
||||
: "Gunakan get_workflow_runs untuk mendapatkan run_id terbaru",
|
||||
};
|
||||
}
|
||||
|
||||
function handleTriggerRepull(args) {
|
||||
const { stack_name, stack_env } = args;
|
||||
const triggerCmd = `gh workflow run re-pull.yml --repo ${REPO} --ref main -f stack_name=${stack_name} -f stack_env=${stack_env}`;
|
||||
const r = runCmd(triggerCmd);
|
||||
if (!r.ok) return { ok: false, error: r.out };
|
||||
|
||||
const runId = getLatestRunId("re-pull.yml");
|
||||
return {
|
||||
ok: true,
|
||||
message: `Workflow re-pull.yml berhasil di-trigger (stack=${stack_name}-${stack_env})`,
|
||||
run_id: runId ? Number(runId) : null,
|
||||
monitor_hint: runId
|
||||
? `Gunakan watch_workflow_run dengan run_id: ${runId}`
|
||||
: "Gunakan get_workflow_runs untuk mendapatkan run_id terbaru",
|
||||
};
|
||||
}
|
||||
|
||||
function handleGetWorkflowRuns(args) {
|
||||
const limit = args.limit || 5;
|
||||
const r = runCmd(
|
||||
`gh run list --workflow=${args.workflow} --repo ${REPO} --limit ${limit} --json databaseId,status,conclusion,displayTitle,createdAt,headBranch`
|
||||
);
|
||||
if (!r.ok) return { ok: false, error: r.out };
|
||||
try {
|
||||
return { ok: true, runs: JSON.parse(r.out) };
|
||||
} catch {
|
||||
return { ok: false, error: "Gagal parse output", raw: r.out };
|
||||
}
|
||||
}
|
||||
|
||||
function handleWatchWorkflowRun(args) {
|
||||
const r = runCmd(
|
||||
`gh run view ${args.run_id} --repo ${REPO} --json status,conclusion,displayTitle,headBranch,createdAt,updatedAt,jobs`
|
||||
);
|
||||
if (!r.ok) return { ok: false, error: r.out };
|
||||
try {
|
||||
const data = JSON.parse(r.out);
|
||||
return {
|
||||
ok: true,
|
||||
run_id: args.run_id,
|
||||
status: data.status,
|
||||
conclusion: data.conclusion,
|
||||
title: data.displayTitle,
|
||||
branch: data.headBranch,
|
||||
jobs: (data.jobs || []).map((j) => ({
|
||||
name: j.name,
|
||||
status: j.status,
|
||||
conclusion: j.conclusion,
|
||||
})),
|
||||
};
|
||||
} catch {
|
||||
return { ok: false, error: "Gagal parse output", raw: r.out };
|
||||
}
|
||||
}
|
||||
|
||||
// --- Request Router ---
|
||||
|
||||
function handleMessage(msg) {
|
||||
const { id, method, params } = msg;
|
||||
|
||||
if (method === "initialize") {
|
||||
respond(id, {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: { name: "github-actions", version: "1.0.0" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === "notifications/initialized") return;
|
||||
|
||||
if (method === "ping") {
|
||||
respond(id, {});
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === "tools/list") {
|
||||
respond(id, { tools: TOOLS });
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === "tools/call") {
|
||||
const { name, arguments: args = {} } = params;
|
||||
let result;
|
||||
|
||||
if (name === "trigger_publish") result = handleTriggerPublish(args);
|
||||
else if (name === "trigger_repull") result = handleTriggerRepull(args);
|
||||
else if (name === "get_workflow_runs") result = handleGetWorkflowRuns(args);
|
||||
else if (name === "watch_workflow_run") result = handleWatchWorkflowRun(args);
|
||||
else {
|
||||
respondError(id, -32601, `Tool tidak ditemukan: ${name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
respond(id, {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
respondError(id, -32601, `Method tidak dikenal: ${method}`);
|
||||
}
|
||||
|
||||
// --- Stdin Loop ---
|
||||
|
||||
let buf = "";
|
||||
process.stdin.setEncoding("utf-8");
|
||||
process.stdin.on("data", (chunk) => {
|
||||
buf += chunk;
|
||||
const lines = buf.split("\n");
|
||||
buf = lines.pop();
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
try {
|
||||
handleMessage(JSON.parse(trimmed));
|
||||
} catch {
|
||||
// ignore malformed JSON
|
||||
}
|
||||
}
|
||||
});
|
||||
15
.mcp.json
Normal file
15
.mcp.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"command": "sh",
|
||||
"args": [
|
||||
"-c",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN=$(gh auth token) npx -y @modelcontextprotocol/server-github"
|
||||
]
|
||||
},
|
||||
"github-actions": {
|
||||
"command": "node",
|
||||
"args": [".claude/mcp/github-actions.mjs"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "desa-darmasaba",
|
||||
"version": "0.1.24",
|
||||
"version": "0.1.25",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -87,6 +87,13 @@ const ApiServer = new Elysia()
|
||||
.group("/api", (app) =>
|
||||
app
|
||||
.use(Utils)
|
||||
.get("/version", async () => {
|
||||
const packageJson = await fs.readFile(
|
||||
path.join(ROOT, "package.json"),
|
||||
"utf-8",
|
||||
);
|
||||
return { version: JSON.parse(packageJson).version };
|
||||
})
|
||||
.use(FileStorage)
|
||||
.use(LandingPage)
|
||||
.use(PPID)
|
||||
|
||||
Reference in New Issue
Block a user