chore: publish from staged

This commit is contained in:
github-actions[bot]
2026-06-09 05:54:09 +00:00
parent fb5e0f561f
commit b25823ace2
3 changed files with 615 additions and 0 deletions
@@ -0,0 +1,580 @@
import { createServer } from "node:http";
import { execFileSync, spawnSync, execSync } from "node:child_process";
import { dirname } from "node:path";
import { createRequire } from "node:module";
import { joinSession, createCanvas } from "@github/copilot-sdk/extension";
const require = createRequire(import.meta.url);
const { marked } = require("marked");
const servers = new Map();
let workspacePath = null;
let lastError = null;
// Fetch live issues from GitHub REST API instead of gh CLI subprocess
async function fetchLiveIssues(cwd) {
try {
// Use GitHub REST API to fetch issues
// This avoids the subprocess execution restriction
const owner = "github";
const repo = "awesome-copilot";
const label = "external-plugin";
// Get authentication token from environment or use public access
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
const headers = {
"Accept": "application/vnd.github.v3+json"
};
if (token) {
headers["Authorization"] = `token ${token}`;
}
// Fetch issues with external-plugin label
const response = await fetch(
`https://api.github.com/repos/${owner}/${repo}/issues?labels=${label}&state=open&per_page=100`,
{ headers }
);
if (!response.ok) {
const error = await response.text();
throw new Error(`GitHub API error ${response.status}: ${error.substring(0, 200)}`);
}
const issues = await response.json();
// Filter to only external-plugin labeled issues and map to our format
return issues
.filter(issue => issue.labels && issue.labels.some(l => l.name === label))
.map(issue => ({
number: issue.number,
title: issue.title,
body: issue.body || "",
bodyHtml: marked.parse(issue.body || ""),
labels: (issue.labels || []).map(l => ({ name: l.name })),
pr_url: issue.body?.match(/\[Generated PR\]\(([^)]+)\)/)?.[1],
created_at: issue.created_at,
updated_at: issue.updated_at
}));
} catch (err) {
lastError = err.message;
throw err;
}
}
function renderHtml() {
return `<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>External Plugins Board</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--background-color-default, #ffffff);
color: var(--text-color-default, #1f2328);
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
font-size: var(--text-body-medium, 14px);
line-height: var(--leading-body-medium, 20px);
padding: 1.5rem;
}
h1 {
font-size: var(--text-title-medium, 20px);
font-weight: var(--font-weight-semibold, 600);
margin-bottom: 1.5rem;
}
.board {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1.5rem;
}
.column {
background: var(--background-color-secondary, #f6f8fa);
border: 1px solid var(--border-color-default, #d0d7de);
border-radius: 6px;
padding: 1rem;
}
.column-header {
font-weight: 600;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--border-color-default, #d0d7de);
}
.column-header.requires { color: #cf222e; }
.column-header.ready { color: #0969da; }
.column-header.approved { color: #1a7f34; }
.column-header.rejected { color: var(--text-color-muted, #656d76); }
.issues { display: flex; flex-direction: column; gap: 0.75rem; }
.issue-card {
background: white;
border: 1px solid var(--border-color-default, #d0d7de);
border-radius: 6px;
padding: 0.75rem;
cursor: grab;
transition: all 0.2s;
}
.issue-card:hover {
border-color: #0969da;
box-shadow: 0 0 8px rgba(9, 105, 218, 0.2);
}
.issue-card.dragging { opacity: 0.5; }
.issue-number { font-weight: 600; color: var(--text-color-muted, #656d76); font-size: 12px; }
.issue-title { margin: 0.25rem 0; font-size: 12px; }
.issue-link { color: #0969da; text-decoration: none; margin-top: 0.5rem; font-size: 12px; }
.issue-link:hover { text-decoration: underline; }
.loading { text-align: center; padding: 2rem; color: var(--text-color-muted, #656d76); }
.error {
color: #cf222e;
padding: 1.5rem;
background: var(--background-color-secondary, #f6f8fa);
border: 1px solid #da3633;
border-radius: 6px;
white-space: pre-wrap;
font-family: var(--font-mono, monospace);
font-size: 12px;
line-height: 1.5;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.open {
display: flex;
}
.modal-content {
background: var(--background-color-default, #ffffff);
border: 1px solid var(--border-color-default, #d0d7de);
border-radius: 8px;
padding: 1.5rem;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
position: relative;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.modal-title {
font-size: var(--text-title-medium, 20px);
font-weight: var(--font-weight-semibold, 600);
}
.modal-number {
color: var(--text-color-muted, #656d76);
font-size: 12px;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--text-color-default, #1f2328);
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.close-btn:hover {
background: var(--background-color-secondary, #f6f8fa);
}
.modal-body {
margin-bottom: 1.5rem;
}
.modal-description {
color: var(--text-color-default, #1f2328);
margin-bottom: 1rem;
line-height: var(--leading-body-medium, 20px);
}
.modal-description h1, .modal-description h2, .modal-description h3 {
font-weight: var(--font-weight-semibold, 600);
margin: 1rem 0 0.5rem;
border-bottom: 1px solid var(--border-color-default, #d1d9e0);
padding-bottom: 0.25rem;
}
.modal-description h1 { font-size: 1.2em; }
.modal-description h2 { font-size: 1.1em; }
.modal-description h3 { font-size: 1em; }
.modal-description p { margin: 0.5rem 0; }
.modal-description ul, .modal-description ol {
padding-left: 1.5rem;
margin: 0.5rem 0;
}
.modal-description li { margin: 0.25rem 0; }
.modal-description a {
color: var(--color-accent-fg, #0969da);
text-decoration: none;
}
.modal-description a:hover { text-decoration: underline; }
.modal-description code {
font-family: var(--font-mono, monospace);
font-size: var(--text-code-inline, 12px);
background: var(--background-color-secondary, #f6f8fa);
padding: 0.1em 0.3em;
border-radius: 3px;
}
.modal-description pre {
background: var(--background-color-secondary, #f6f8fa);
padding: 0.75rem;
border-radius: 6px;
overflow-x: auto;
margin: 0.5rem 0;
}
.modal-description pre code {
background: none;
padding: 0;
}
.modal-description blockquote {
border-left: 3px solid var(--border-color-default, #d1d9e0);
padding-left: 0.75rem;
margin: 0.5rem 0;
color: var(--text-color-muted, #656d76);
}
.modal-description hr {
border: none;
border-top: 1px solid var(--border-color-default, #d1d9e0);
margin: 0.75rem 0;
}
.modal-description input[type="checkbox"] { margin-right: 0.3em; }
.modal-meta {
display: flex;
gap: 1rem;
font-size: 12px;
color: var(--text-color-muted, #656d76);
margin-bottom: 1rem;
}
.modal-meta-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.modal-labels {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.label-badge {
background: #f0f6ff;
color: #0969da;
padding: 0.25rem 0.5rem;
border-radius: 3px;
font-size: 11px;
font-weight: 500;
}
.label-badge.requires { background: #fef2f1; color: #cf222e; }
.label-badge.ready { background: #f0f6ff; color: #0969da; }
.label-badge.approved { background: #f0f6ff; color: #1a7f34; }
.label-badge.rejected { background: #f6f8fa; color: var(--text-color-muted, #656d76); }
.modal-pr {
border-top: 1px solid var(--border-color-default, #d0d7de);
padding-top: 1rem;
}
.pr-link {
color: #0969da;
text-decoration: none;
font-weight: 500;
}
.pr-link:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<h1>External Plugins Board</h1>
<div id="content"><div class="loading">Loading issues...</div></div>
<div id="modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<div>
<div class="modal-number" id="modalNumber"></div>
<div class="modal-title" id="modalTitle"></div>
</div>
<button class="close-btn" onclick="closeModal()">×</button>
</div>
<div class="modal-body">
<div class="modal-description" id="modalDescription"></div>
<div class="modal-meta" id="modalMeta"></div>
<div class="modal-labels" id="modalLabels"></div>
<div class="modal-pr" id="modalPR" style="display: none;"></div>
</div>
</div>
</div>
<script>
const STATES = [
{ key: 'requires-submitter-fixes', label: 'Requires Submitter Fixes' },
{ key: 'ready-for-review', label: 'Ready for Review' },
{ key: 'approved', label: 'Approved' },
{ key: 'rejected', label: 'Rejected' }
];
let draggedIssue = null;
let allIssues = [];
function formatDate(dateStr) {
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
}
function getLabelClass(labelName) {
if (labelName.includes('requires')) return 'requires';
if (labelName.includes('ready')) return 'ready';
if (labelName.includes('approved')) return 'approved';
if (labelName.includes('rejected')) return 'rejected';
return '';
}
function showIssueModal(issueNumber) {
const issue = allIssues.find(i => i.number === issueNumber);
if (!issue) return;
document.getElementById('modalNumber').textContent = '#' + issue.number;
document.getElementById('modalTitle').textContent = issue.title;
document.getElementById('modalDescription').innerHTML = issue.bodyHtml || '';
const metaHtml = \`
<div class="modal-meta-item">
<span style="color: var(--text-color-muted);">Created</span>
<span>\${formatDate(issue.created_at)}</span>
</div>
<div class="modal-meta-item">
<span style="color: var(--text-color-muted);">Updated</span>
<span>\${formatDate(issue.updated_at)}</span>
</div>
\`;
document.getElementById('modalMeta').innerHTML = metaHtml;
const labelsHtml = (issue.labels || []).map(l =>
\`<span class="label-badge \${getLabelClass(l.name)}">\${l.name.replace(/-/g, ' ')}</span>\`
).join('');
document.getElementById('modalLabels').innerHTML = labelsHtml;
const prDiv = document.getElementById('modalPR');
if (issue.pr_url) {
prDiv.innerHTML = \`<a href="\${issue.pr_url}" target="_blank" class="pr-link">View generated PR →</a>\`;
prDiv.style.display = 'block';
} else {
prDiv.style.display = 'none';
}
document.getElementById('modal').classList.add('open');
}
function closeModal() {
document.getElementById('modal').classList.remove('open');
}
async function loadIssues() {
try {
const response = await fetch('/api/issues');
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'Failed to load');
allIssues = data;
render(data);
} catch (err) {
document.getElementById('content').innerHTML = '<div class="error">Error: ' + err.message + '</div>';
}
}
function render(issues) {
const board = document.createElement('div');
board.className = 'board';
STATES.forEach(state => {
const column = document.createElement('div');
column.className = 'column';
column.dataset.state = state.key;
const header = document.createElement('div');
header.className = 'column-header ' + state.key.split('-')[0];
header.textContent = state.label;
column.appendChild(header);
const issuesContainer = document.createElement('div');
issuesContainer.className = 'issues';
const stateIssues = issues.filter(issue => {
return (issue.labels || []).some(l => l.name === state.key);
});
stateIssues.forEach(issue => {
const card = document.createElement('div');
card.className = 'issue-card';
card.draggable = true;
card.dataset.issue = issue.number;
const num = document.createElement('div');
num.className = 'issue-number';
num.textContent = '#' + issue.number;
card.appendChild(num);
const title = document.createElement('div');
title.className = 'issue-title';
title.textContent = issue.title;
card.appendChild(title);
if (issue.pr_url) {
const link = document.createElement('a');
link.className = 'issue-link';
link.href = issue.pr_url;
link.target = '_blank';
link.textContent = 'View PR →';
card.appendChild(link);
}
card.addEventListener('click', (e) => {
if (e.target.tagName !== 'A') {
showIssueModal(issue.number);
}
});
card.addEventListener('dragstart', () => {
draggedIssue = issue.number;
card.classList.add('dragging');
});
card.addEventListener('dragend', () => {
card.classList.remove('dragging');
});
issuesContainer.appendChild(card);
});
column.appendChild(issuesContainer);
board.appendChild(column);
});
document.getElementById('content').innerHTML = '';
document.getElementById('content').appendChild(board);
}
document.addEventListener('dragover', e => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
document.addEventListener('drop', async e => {
e.preventDefault();
const column = e.target.closest('.column');
if (column && draggedIssue) {
try {
const response = await fetch('/api/issues/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ issueNumber: draggedIssue, newState: column.dataset.state })
});
if (response.ok) await loadIssues();
} catch (err) {
console.error('Error:', err);
}
draggedIssue = null;
}
});
// Close modal when clicking outside
document.getElementById('modal').addEventListener('click', (e) => {
if (e.target.id === 'modal') closeModal();
});
loadIssues();
</script>
</body>
</html>`;
}
async function startServer(instanceId, cwd) {
const server = createServer(async (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
if (req.url === "/" && req.method === "GET") {
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(renderHtml());
} else if (req.url === "/api/issues" && req.method === "GET") {
try {
const issues = await fetchLiveIssues(cwd);
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(issues || []));
} catch (err) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: err.message }));
}
} else if (req.url === "/api/issues/update" && req.method === "POST") {
let body = "";
req.on("data", chunk => { body += chunk; });
req.on("end", async () => {
try {
const { issueNumber, newState } = JSON.parse(body);
const labels = ['requires-submitter-fixes', 'ready-for-review', 'approved', 'rejected'];
for (const label of labels.filter(l => l !== newState)) {
try {
spawnSync("gh", [
"issue", "edit", issueNumber.toString(),
"--remove-label", label
], { cwd, shell: true });
} catch (e) {}
}
spawnSync("gh", [
"issue", "edit", issueNumber.toString(),
"--add-label", newState
], { cwd, shell: true });
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ ok: true }));
} catch (err) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: err.message }));
}
});
} else {
res.writeHead(404);
res.end("Not found");
}
});
await new Promise(resolve => server.listen(0, "127.0.0.1", resolve));
const port = server.address().port;
return { server, url: `http://127.0.0.1:${port}/` };
}
const session = await joinSession({
canvases: [
createCanvas({
id: "external-plugins-board",
displayName: "External Plugins Board",
description: "Kanban board for managing external plugin submission issues",
open: async (ctx) => {
let entry = servers.get(ctx.instanceId);
if (!entry) {
if (!workspacePath) {
const filePath = import.meta.url.replace(/^file:\/\//, '').replace(/\//g, '\\');
workspacePath = dirname(dirname(dirname(filePath)));
}
entry = await startServer(ctx.instanceId, workspacePath);
servers.set(ctx.instanceId, entry);
}
return { title: "External Plugins Board", url: entry.url };
},
onClose: async (ctx) => {
const entry = servers.get(ctx.instanceId);
if (entry) {
servers.delete(ctx.instanceId);
await new Promise(resolve => entry.server.close(() => resolve()));
}
},
}),
],
});
@@ -0,0 +1,27 @@
{
"name": "external-plugins-board",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "external-plugins-board",
"version": "1.0.0",
"dependencies": {
"marked": "^15.0.0"
}
},
"node_modules/marked": {
"version": "15.0.12",
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
}
}
}
@@ -0,0 +1,8 @@
{
"name": "external-plugins-board",
"version": "1.0.0",
"type": "module",
"dependencies": {
"marked": "^15.0.0"
}
}