Skip to content

Commit b2112a3

Browse files
greenpoloclaude
andcommitted
ci: weekly upstream-watch — opens issue assigned to Copilot Coding Agent on new releases
Polls latest releases for the five upstream projects this plugin depends on (ACP spec, Gemini CLI, Codex CLI, GitHub Copilot CLI, Qwen Code) every Monday 14:00 UTC. When any of them ship a new tag, opens an issue assigned to copilot-swe-agent (the Copilot Coding Agent identity) with: - Excerpts of the new release notes - Why each upstream matters for which adapter - Pointers at the most-likely-affected files in this repo - A focused prompt asking Copilot to either open a PR with the minimal fix, comment "no plugin updates needed" with a reason, or ask for clarification if ambiguous Cursor's CLI isn't on GitHub and its changelog is JS-rendered, so it's listed as a manual-reference source inside the issue body. Copilot is asked to research it alongside the GitHub-hosted upstreams. State tracked in .github/upstream-state.json. Manual trigger via workflow_dispatch (Actions tab) for ad-hoc runs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a10fe5a commit b2112a3

3 files changed

Lines changed: 301 additions & 0 deletions

File tree

.github/scripts/upstream-watch.mjs

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Polls upstream projects this plugin depends on, diffs against
4+
* .github/upstream-state.json, and (if anything changed) emits:
5+
* - an updated state file
6+
* - upstream-issue-body.md — body for an issue to assign to copilot-swe-agent
7+
* - upstream-issue-title.txt — issue title
8+
* - has_changes=true on $GITHUB_OUTPUT
9+
*
10+
* Cursor's CLI isn't on GitHub and its changelog is JS-rendered — too brittle to
11+
* diff reliably from a workflow. We list it as a manual-reference source in the
12+
* issue body and let Copilot do the actual research.
13+
*/
14+
15+
import fs from "node:fs";
16+
import path from "node:path";
17+
import process from "node:process";
18+
19+
const STATE_PATH = path.join(".github", "upstream-state.json");
20+
const ISSUE_BODY_PATH = "upstream-issue-body.md";
21+
const ISSUE_TITLE_PATH = "upstream-issue-title.txt";
22+
23+
const SOURCES = [
24+
{
25+
id: "acp-spec",
26+
label: "ACP spec",
27+
repo: "agentclientprotocol/agent-client-protocol",
28+
relevance: "Defines the JSON-RPC methods the plugin's `acp-client.mjs` implements. Spec changes can introduce new methods we should handle (e.g. `terminal/*`, `session/request_permission`, `cursor/ask_question`)."
29+
},
30+
{
31+
id: "gemini-cli",
32+
label: "Gemini CLI",
33+
repo: "google-gemini/gemini-cli",
34+
relevance: "Drives `plugins/multi/scripts/lib/adapters/gemini.mjs`. Watch for changes to ACP handshake, model alias resolution, MCP support, or new approval modes."
35+
},
36+
{
37+
id: "codex-cli",
38+
label: "Codex CLI (App Server Protocol)",
39+
repo: "openai/codex",
40+
relevance: "Drives `plugins/multi/scripts/lib/adapters/codex.mjs` and `app-server.mjs`. Watch for sandbox mode changes, new approval policies, or app-server protocol updates."
41+
},
42+
{
43+
id: "copilot-cli",
44+
label: "GitHub Copilot CLI",
45+
repo: "github/copilot-cli",
46+
relevance: "Drives `plugins/multi/scripts/lib/adapters/copilot.mjs`. Watch for ACP changes, slash-command additions/removals, or auth flow changes."
47+
},
48+
{
49+
id: "qwen-code",
50+
label: "Qwen Code",
51+
repo: "QwenLM/qwen-code",
52+
relevance: "Drives `plugins/multi/scripts/lib/adapters/qwen.mjs`. Watch for ACP support changes (the `--acp` flag graduated from `--experimental-acp` recently)."
53+
}
54+
];
55+
56+
// Manual-reference sources the workflow can't reliably auto-diff. Copilot is
57+
// asked to research these inside the issue.
58+
const MANUAL_SOURCES = [
59+
{
60+
label: "Cursor agent CLI",
61+
changelog: "https://cursor.com/changelog",
62+
forum: "https://forum.cursor.com/c/bug-report/6",
63+
relevance: "Drives `plugins/multi/scripts/lib/adapters/cursor.mjs`. The plugin currently works around the 2026.04.17-787b533 ACP regression (see `maybeWarnAboutCursorVersion` and `ensureCursorAllowlist`). When Cursor ships a fix, both workarounds can likely be simplified or removed."
64+
}
65+
];
66+
67+
const PLUGIN_FILES_OF_INTEREST = [
68+
"plugins/multi/scripts/lib/acp-client.mjs (shared ACP JSON-RPC client; `buildAutoApproveRequestHandler`)",
69+
"plugins/multi/scripts/lib/acp-terminals.mjs (client-side terminal services)",
70+
"plugins/multi/scripts/lib/mcp-servers.mjs (MCP wiring for ACP `session/new`)",
71+
"plugins/multi/scripts/lib/adapters/{codex,gemini,cursor,copilot,qwen}.mjs (per-CLI adapters)",
72+
"plugins/multi/scripts/multi-cli-companion.mjs (companion runtime + dispatch)"
73+
];
74+
75+
function readState() {
76+
try {
77+
return JSON.parse(fs.readFileSync(STATE_PATH, "utf8"));
78+
} catch {
79+
return {};
80+
}
81+
}
82+
83+
async function fetchLatestRelease(repo) {
84+
const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
85+
headers: {
86+
"User-Agent": "cc-multi-cli-plugin-upstream-watch",
87+
"Accept": "application/vnd.github+json",
88+
...(process.env.GITHUB_TOKEN ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } : {})
89+
}
90+
});
91+
if (res.status === 404) return null; // repo has no releases yet
92+
if (!res.ok) throw new Error(`GitHub API ${res.status} for ${repo}`);
93+
const data = await res.json();
94+
return {
95+
tag: data.tag_name,
96+
name: data.name,
97+
publishedAt: data.published_at,
98+
htmlUrl: data.html_url,
99+
body: typeof data.body === "string" ? data.body : ""
100+
};
101+
}
102+
103+
function trimReleaseNotes(body, maxLines = 30) {
104+
if (!body) return "_(no release notes provided)_";
105+
const lines = body.replace(/\r/g, "").split("\n").filter(Boolean);
106+
if (lines.length <= maxLines) return lines.join("\n");
107+
return lines.slice(0, maxLines).join("\n") + "\n\n_…(release notes truncated; click the link above for the full text)_";
108+
}
109+
110+
function appendOutput(key, value) {
111+
if (!process.env.GITHUB_OUTPUT) return;
112+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value}\n`);
113+
}
114+
115+
function renderIssueBody(changes) {
116+
const today = new Date().toISOString().slice(0, 10);
117+
const sections = changes.map((c) => `### ${c.source.label}${c.previous ?? "(none)"} → \`${c.latest.tag}\`
118+
119+
- **Release**: [${c.latest.name ?? c.latest.tag}](${c.latest.htmlUrl}) (published ${c.latest.publishedAt?.slice(0, 10) ?? "?"})
120+
- **Why this matters for the plugin**: ${c.source.relevance}
121+
122+
<details><summary>Release notes excerpt</summary>
123+
124+
${trimReleaseNotes(c.latest.body)}
125+
126+
</details>`);
127+
128+
const manualSection = MANUAL_SOURCES.map((s) => `- **${s.label}** — changelog: ${s.changelog} · forum (bug reports): ${s.forum}
129+
- ${s.relevance}`).join("\n");
130+
131+
const filesSection = PLUGIN_FILES_OF_INTEREST.map((f) => `- \`${f}\``).join("\n");
132+
133+
return `_This issue was opened automatically by [\`upstream-watch.yml\`](../actions/workflows/upstream-watch.yml) on ${today} because at least one upstream project this plugin depends on shipped a new release._
134+
135+
## Detected changes
136+
137+
${sections.join("\n\n")}
138+
139+
## Also worth checking (manual — Copilot, please research these too)
140+
141+
${manualSection}
142+
143+
## Plugin files most likely to need updates
144+
145+
${filesSection}
146+
147+
## What I'd like you to do, @copilot
148+
149+
1. **Read the linked release notes** for each detected change above, plus the manual-reference changelogs.
150+
2. **Compare what changed against the relevant adapter / shared code** in this repo. Look specifically for:
151+
- New ACP methods we should handle in \`acp-client.mjs\`'s \`buildAutoApproveRequestHandler\`
152+
- Renamed / deprecated CLI flags or model IDs hardcoded in any adapter
153+
- New CLI capabilities that obsolete a workaround we currently ship (e.g., the Cursor 2026.04.17 regression workaround in \`cursor.mjs\` — check if a newer Cursor release fixes it, and if so, propose removing \`maybeWarnAboutCursorVersion\` and the allowlist-injection or scoping it tighter)
154+
- Breaking changes that would silently break the plugin
155+
3. **For each change that warrants action**, open a focused PR against \`master\` with the minimal fix. Reference the upstream release / commit / forum thread in the PR description.
156+
4. **If a detected change does NOT need any plugin update**, reply on this issue with a short note saying "no plugin updates needed for X — reason: …" and close it.
157+
5. **If something is ambiguous** (you can't tell from release notes whether the plugin is affected), ask in a comment rather than guessing.
158+
159+
You may use \`ACP_TRACE=1\` and the rest of the diagnostic patterns documented in \`plugins/multi/skills/customize/SKILL.md\` if you want to verify behavior empirically.
160+
161+
---
162+
163+
_State tracked in \`.github/upstream-state.json\` — bumped by this same workflow. If you want to suppress a noisy upstream from this watch, edit \`.github/scripts/upstream-watch.mjs\`._
164+
`;
165+
}
166+
167+
async function main() {
168+
const state = readState();
169+
const newState = { ...state };
170+
const changes = [];
171+
172+
for (const src of SOURCES) {
173+
try {
174+
const latest = await fetchLatestRelease(src.repo);
175+
if (!latest || !latest.tag) {
176+
console.error(`[${src.id}] no latest release found`);
177+
continue;
178+
}
179+
const previousTag = state[src.id]?.latest;
180+
if (previousTag !== latest.tag) {
181+
console.log(`[${src.id}] CHANGED: ${previousTag ?? "(none)"} -> ${latest.tag}`);
182+
changes.push({ source: src, previous: previousTag, latest });
183+
// Only update state when the tag actually changed. Avoids weekly
184+
// commit noise from pure `checkedAt` bumps. Look at the Actions tab
185+
// for last-run-time visibility instead.
186+
newState[src.id] = { latest: latest.tag, checkedAt: new Date().toISOString() };
187+
} else {
188+
console.log(`[${src.id}] unchanged at ${latest.tag}`);
189+
// Preserve existing entry verbatim — don't touch `checkedAt`.
190+
}
191+
} catch (err) {
192+
console.error(`[${src.id}] failed:`, err.message);
193+
// Preserve previous state for this source; don't drop it.
194+
if (state[src.id]) newState[src.id] = state[src.id];
195+
}
196+
}
197+
198+
// Preserve the leading _comment field on rewrite.
199+
if (state._comment) newState._comment = state._comment;
200+
201+
fs.writeFileSync(STATE_PATH, JSON.stringify(newState, null, 2) + "\n");
202+
console.log(`State written: ${STATE_PATH}`);
203+
204+
if (changes.length === 0) {
205+
console.log("No upstream changes detected.");
206+
appendOutput("has_changes", "false");
207+
return;
208+
}
209+
210+
const labels = changes.map((c) => c.source.label).join(", ");
211+
const title = `Upstream changes detected: ${labels}`;
212+
fs.writeFileSync(ISSUE_TITLE_PATH, title);
213+
fs.writeFileSync(ISSUE_BODY_PATH, renderIssueBody(changes));
214+
console.log(`Issue title: ${title}`);
215+
console.log(`Issue body written to ${ISSUE_BODY_PATH}`);
216+
appendOutput("has_changes", "true");
217+
appendOutput("issue_title", title);
218+
}
219+
220+
main().catch((err) => {
221+
console.error("upstream-watch failed:", err);
222+
process.exit(1);
223+
});

.github/upstream-state.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"_comment": "Last-seen versions of upstream projects this plugin depends on. Updated by .github/workflows/upstream-watch.yml. When a polled upstream's latest tag changes vs. what's recorded here, the workflow opens an issue assigned to copilot-swe-agent and updates this file.",
3+
"acp-spec": { "latest": "v0.12.2", "checkedAt": "2026-04-26T00:00:00Z" },
4+
"gemini-cli": { "latest": "v0.39.1", "checkedAt": "2026-04-26T00:00:00Z" },
5+
"codex-cli": { "latest": "rust-v0.125.0", "checkedAt": "2026-04-26T00:00:00Z" },
6+
"copilot-cli": { "latest": "v1.0.36", "checkedAt": "2026-04-26T00:00:00Z" },
7+
"qwen-code": { "latest": "v0.15.3", "checkedAt": "2026-04-26T00:00:00Z" }
8+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Upstream watch
2+
3+
# Polls upstream projects this plugin depends on (ACP spec + the five CLIs)
4+
# once a week. When any of them ship a new release, opens an issue assigned to
5+
# the Copilot Coding Agent (`copilot-swe-agent`) so Copilot can research and
6+
# propose plugin updates.
7+
#
8+
# Manual trigger via the Actions tab is also supported (workflow_dispatch).
9+
10+
on:
11+
schedule:
12+
# Mondays at 14:00 UTC (~10am Eastern, ~7am Pacific). Weekly is enough —
13+
# upstream changes don't ship on hourly cadence and we'd rather under-fire
14+
# than spam the Copilot agent.
15+
- cron: "0 14 * * 1"
16+
workflow_dispatch: {}
17+
18+
permissions:
19+
contents: write # to commit .github/upstream-state.json updates
20+
issues: write # to open the upstream-changes issue
21+
pull-requests: write # so the Copilot agent has the scope it needs once assigned
22+
23+
concurrency:
24+
# If the cron and a manual run collide, let the manual run win.
25+
group: upstream-watch
26+
cancel-in-progress: false
27+
28+
jobs:
29+
watch:
30+
runs-on: ubuntu-latest
31+
steps:
32+
- name: Checkout
33+
uses: actions/checkout@v5
34+
35+
- name: Setup Node
36+
uses: actions/setup-node@v5
37+
with:
38+
node-version: "20"
39+
40+
- name: Poll upstream sources
41+
id: poll
42+
env:
43+
# Auth the GitHub API call — avoids the 60/hr unauthenticated limit.
44+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45+
run: node .github/scripts/upstream-watch.mjs
46+
47+
- name: Commit updated state file
48+
if: always()
49+
run: |
50+
if git diff --quiet -- .github/upstream-state.json; then
51+
echo "No state changes to commit."
52+
exit 0
53+
fi
54+
git config user.name "github-actions[bot]"
55+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
56+
git add .github/upstream-state.json
57+
git commit -m "chore(upstream-watch): bump state $(date -u +%Y-%m-%d)"
58+
git push
59+
60+
- name: Open issue for Copilot
61+
if: steps.poll.outputs.has_changes == 'true'
62+
env:
63+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
64+
ISSUE_TITLE: ${{ steps.poll.outputs.issue_title }}
65+
run: |
66+
gh issue create \
67+
--title "$ISSUE_TITLE" \
68+
--body-file upstream-issue-body.md \
69+
--assignee copilot-swe-agent \
70+
--label upstream-watch

0 commit comments

Comments
 (0)