bytelyst-devops-tools/scripts/tracker-seed/seed-tracker-items.mjs
saravanakumardb1 eb4e755c5f feat(tracker-seed): seed script + payloads for engineering-review work items
Files the ENGINEERING_REVIEW_SCORECARD.md P0-P3 action plan as tracker items
(one per affected product) via the platform-service POST /api/items API.
Dependency-free Node seeder mints an HS256 token from $JWT_SECRET, dedupes by
title, and supports --dry-run. No live writes performed (stack is down); run
the script once the platform stack is up.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-05-30 21:14:12 -07:00

139 lines
5.0 KiB
JavaScript

#!/usr/bin/env node
/**
* Seed ByteLyst tracker items from a JSON payload file.
*
* Creates feature/bug/task items via the platform-service tracker API
* (POST /api/items — the same endpoint tracker-web proxies to). Items are
* scoped per `productId`. Use this to file the ENGINEERING_REVIEW_SCORECARD.md
* P0-P3 work once the platform stack is running.
*
* Auth: mints a short-lived HS256 access token signed with $JWT_SECRET
* (same secret platform-service verifies with). No external deps — uses
* node:crypto only.
*
* Env:
* PLATFORM_API_URL Base URL of platform-service (default http://localhost:4003)
* JWT_SECRET Shared JWT secret (required unless --dry-run)
* SEED_SUB Token subject / reportedBy (default "eng-review-bot")
* SEED_EMAIL Token email claim (default "eng-review-bot@bytelyst.local")
* SEED_ROLE Token role claim (default "admin")
* ITEMS_FILE Payload file (default ./engineering-review-items.json)
*
* Flags:
* --dry-run Print what would be created; no token, no network calls.
* --force Skip the dedupe-by-title check.
*
* Examples:
* node seed-tracker-items.mjs --dry-run
* JWT_SECRET=... PLATFORM_API_URL=http://localhost:4003 node seed-tracker-items.mjs
*/
import { readFileSync } from 'node:fs';
import { createHmac } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const args = new Set(process.argv.slice(2));
const DRY_RUN = args.has('--dry-run');
const FORCE = args.has('--force');
const API = (process.env.PLATFORM_API_URL || 'http://localhost:4003').replace(/\/$/, '');
const SECRET = process.env.JWT_SECRET || '';
const SUB = process.env.SEED_SUB || 'eng-review-bot';
const EMAIL = process.env.SEED_EMAIL || 'eng-review-bot@bytelyst.local';
const ROLE = process.env.SEED_ROLE || 'admin';
const ITEMS_FILE = resolve(__dirname, process.env.ITEMS_FILE || 'engineering-review-items.json');
function b64url(buf) {
return Buffer.from(buf).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
/** Mint an HS256 access token compatible with @bytelyst/auth extractAuth(). */
function mintToken() {
if (!SECRET) throw new Error('JWT_SECRET must be set (or use --dry-run)');
const now = Math.floor(Date.now() / 1000);
const header = { alg: 'HS256', typ: 'JWT' };
const payload = { sub: SUB, email: EMAIL, role: ROLE, type: 'access', iat: now, exp: now + 3600 };
const head = b64url(JSON.stringify(header));
const body = b64url(JSON.stringify(payload));
const sig = b64url(createHmac('sha256', SECRET).update(`${head}.${body}`).digest());
return `${head}.${body}.${sig}`;
}
async function listTitles(token, productId) {
const url = `${API}/api/items?productId=${encodeURIComponent(productId)}&limit=500`;
const res = await fetch(url, {
headers: { authorization: `Bearer ${token}`, 'x-product-id': productId },
});
if (!res.ok) throw new Error(`list failed (${res.status})`);
const data = await res.json();
return new Set((data.items || []).map((i) => i.title));
}
async function createItem(token, item) {
const res = await fetch(`${API}/api/items`, {
method: 'POST',
headers: {
authorization: `Bearer ${token}`,
'content-type': 'application/json',
'x-product-id': item.productId,
},
body: JSON.stringify(item),
});
const text = await res.text();
if (!res.ok) throw new Error(`${res.status} ${text.slice(0, 300)}`);
return JSON.parse(text);
}
async function main() {
const raw = JSON.parse(readFileSync(ITEMS_FILE, 'utf8'));
const items = raw.items || raw;
console.log(`Loaded ${items.length} item(s) from ${ITEMS_FILE}`);
console.log(`Target: ${API}/api/items (dry-run=${DRY_RUN}, force=${FORCE})\n`);
if (DRY_RUN) {
for (const it of items) {
console.log(` [DRY] ${it.productId.padEnd(16)} ${it.type}/${it.priority} ${it.title}`);
}
console.log(`\nDry run only — nothing sent. ${items.length} item(s) would be created.`);
return;
}
const token = mintToken();
const titleCache = new Map();
let created = 0;
let skipped = 0;
let failed = 0;
for (const it of items) {
try {
if (!FORCE) {
if (!titleCache.has(it.productId)) {
titleCache.set(it.productId, await listTitles(token, it.productId).catch(() => new Set()));
}
if (titleCache.get(it.productId).has(it.title)) {
console.log(` SKIP ${it.productId.padEnd(16)} ${it.title} (already exists)`);
skipped++;
continue;
}
}
const res = await createItem(token, it);
console.log(` OK ${it.productId.padEnd(16)} ${res.id} ${it.title}`);
created++;
} catch (err) {
console.error(` FAIL ${it.productId.padEnd(16)} ${it.title}\n ${err.message}`);
failed++;
}
}
console.log(`\nDone. created=${created} skipped=${skipped} failed=${failed}`);
if (failed > 0) process.exit(1);
}
main().catch((err) => {
console.error(`Fatal: ${err.message}`);
process.exit(1);
});