Menjaga visibilitas progres sprint antartim sering kali memakan waktu jika dilakukan secara manual melalui standup harian atau pesan manual. GitHub Projects (v2) menyediakan GraphQL API yang fleksibel untuk mengekstrak data item pekerjaan, status, dan target sprint. Dengan mengintegrasikannya ke GitHub Actions dan Slack Incoming Webhooks, laporan status sprint mingguan dapat dikirimkan otomatis secara konsisten tanpa overhead dependensi tambahan.
Arsitektur Integrasi
Solusi ini terdiri dari tiga komponen utama:
- GitHub Actions Cron Workflow: Memicu eksekusi script sesuai jadwal (misalnya, setiap Senin pukul 09:00 UTC).
- Node.js Runner Script: Memanfaatkan API standar bawaan Node.js (
fetch) untuk query GraphQL GitHub, memproses status pekerjaan, dan memformat payload JSON. - Slack Incoming Webhook: Menerima payload Block Kit dan merendernya sebagai pesan terstruktur di channel tujuan.
Alternatif paling ringkas: Gunakan aplikasi GitHub Marketplace siap pakai jika tidak membutuhkan kustomisasi field atau agregasi metriks tertentu.
1. Konfigurasi Token dan Hak Akses
GitHub Projects v2 memerlukan scope token khusus yang tidak dicakup oleh GITHUB_TOKEN default pada repo biasa jika project berada di level organisasi. Siapkan Personal Access Token (PAT) atau GitHub App token dengan izin:
read:projectatauproject (read)repo (read)
Simpan token tersebut di repository settings di bawah Settings > Secrets and variables > Actions dengan nama GH_PROJECTS_TOKEN. Buat juga secret SLACK_WEBHOOK_URL yang berisi URL webhook Slack dari Slack App Anda.
2. Query Data via GitHub Projects GraphQL API
Data field kustom pada Projects v2 diakses melalui union type ProjectV2ItemFieldValue. Simpan script berikut sebagai scripts/sync-sprint.mjs. Script ini menggunakan Node.js 18+ tanpa pustaka eksternal.
// scripts/sync-sprint.mjs
const GH_TOKEN = process.env.GH_PROJECTS_TOKEN;
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL;
const ORG_LOGIN = process.env.ORG_LOGIN;
const PROJECT_NUMBER = parseInt(process.env.PROJECT_NUMBER, 10);
if (!GH_TOKEN || !SLACK_WEBHOOK || !ORG_LOGIN || isNaN(PROJECT_NUMBER)) {
console.error("Environment variables required: GH_PROJECTS_TOKEN, SLACK_WEBHOOK_URL, ORG_LOGIN, PROJECT_NUMBER");
process.exit(1);
}
const query = `
query getProjectData($org: String!, $number: Int!) {
organization(login: $org) {
projectV2(number: $number) {
title
items(first: 50) {
nodes {
fieldValues(first: 10) {
nodes {
... on ProjectV2ItemFieldSingleSelectValue {
name
field { ... on ProjectV2FieldCommon { name } }
}
... on ProjectV2ItemFieldIterationValue {
title
field { ... on ProjectV2FieldCommon { name } }
}
}
}
content {
... on Issue { title state url }
... on PullRequest { title state url }
}
}
}
}
}
}`;
async function run() {
const ghRes = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${GH_TOKEN}`,
"Content-Type": "application/json",
"User-Agent": "node-fetch-sprint-sync"
},
body: JSON.stringify({ query, variables: { org: ORG_LOGIN, number: PROJECT_NUMBER } })
});
const remaining = ghRes.headers.get("x-ratelimit-remaining");
if (remaining && parseInt(remaining, 10) < 10) {
console.warn(`Peringatan: GitHub API rate limit tersisa ${remaining}`);
}
const { data, errors } = await ghRes.json();
if (errors) {
throw new Error(`GraphQL error: ${JSON.stringify(errors)}`);
}
const items = data.organization.projectV2.items.nodes;
const stats = { todo: 0, inProgress: 0, done: 0 };
for (const item of items) {
if (!item.content) continue;
const statusVal = item.fieldValues.nodes.find(
(f) => f.field && f.field.name === "Status"
);
const status = statusVal ? statusVal.name.toLowerCase() : "todo";
if (status.includes("done") || status.includes("closed")) stats.done++;
else if (status.includes("progress")) stats.inProgress++;
else stats.todo++;
}
// Format Payload Slack Block Kit
const payload = {
blocks: [
{
type: "header",
text: { type: "plain_text", text: `Status Sprint: ${data.organization.projectV2.title}` }
},
{
type: "section",
fields: [
{ type: "mrkdwn", text: `*To Do:*
${stats.todo}` },
{ type: "mrkdwn", text: `*In Progress:*
${stats.inProgress}` },
{ type: "mrkdwn", text: `*Done:*
${stats.done}` },
{ type: "mrkdwn", text: `*Total Items:*
${items.length}` }
]
}
]
};
const slackRes = await fetch(SLACK_WEBHOOK, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!slackRes.ok) {
const errorBody = await slackRes.text();
throw new Error(`Slack post failed (${slackRes.status}): ${errorBody}`);
}
console.log("Berhasil sinkronisasi status sprint ke Slack.");
}
// ponytail: basic error catching and execution
run().catch((err) => {
console.error(err);
process.exit(1);
});
3. Implementasi GitHub Actions Workflow
Konfigurasikan workflow YAML pada .github/workflows/sprint-sync.yml. Alur kerja ini menggunakan cron trigger mingguan dan event workflow_dispatch agar pengujian dapat dijalankan manual.
name: Sprint Goal Sync
on:
schedule:
- cron: '0 2 * * 1' # Setiap Senin pukul 02:00 UTC
workflow_dispatch:
jobs:
sync-to-slack:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js runtime
uses: actions/setup-node@v4
with:
node-version: 20
- name: Run sync script
env:
GH_PROJECTS_TOKEN: ${{ secrets.GH_PROJECTS_TOKEN }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
ORG_LOGIN: "organisasi-anda"
PROJECT_NUMBER: 1
run: node scripts/sync-sprint.mjs
4. Rate Limiting dan Validasi Payload
GraphQL API GitHub menerapkan limit konsumsi biaya kueri (query point rate limits). Dalam script di atas, header x-ratelimit-remaining diperiksa langsung setelah eksekusi. Jangan memuat iterasi field yang tidak digunakan agar bobot GraphQL point tetap rendah.
Untuk format Slack, pastikan panjang karakter per elemen mrkdwn tidak melebihi 2000 karakter, dan batasi array blocks maksimal 50 objek sesuai batas spesifikasi Slack API.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!