Skip to content

Commit 5bcad60

Browse files
authored
ci(sdk): autonomous agent-PR merge gated on tests + API-surface contract test (#3)
Two pieces that together let agent/* PRs land without a human: - src/api-surface.test.ts: pins the public contract (exports, RektRadar methods, canonical api.rektradar.io REST+WS host). This is the test that would have caught the docs shipping `rr.stream()` (a method the SDK never had) and the base URL pointing at app. instead of api. Renaming or removing a public member now fails CI here instead of in a developer's bot. - .github/workflows/auto-merge.yml: on green CI for an agent/* or security/* branch, squash-merge the PR via the built-in GITHUB_TOKEN. No org secrets, no Copilot pipeline (overkill for a non-deployed npm package) — the test suite is the gate. Publishing stays manual: npm only releases on a v* tag.
1 parent 03e026d commit 5bcad60

2 files changed

Lines changed: 126 additions & 0 deletions

File tree

.github/workflows/auto-merge.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Autonomous merge for agent-authored PRs.
2+
#
3+
# The SDK does not deploy and has no Copilot-review pipeline; here the test suite
4+
# IS the quality gate. When CI ("Build & Test" + the publish gate) passes on a
5+
# PR whose branch is agent/* or security/*, squash-merge it automatically. Any
6+
# other branch prefix is ignored, so human PRs still merge by hand.
7+
#
8+
# Uses the built-in GITHUB_TOKEN (main is unprotected) -> no org secrets needed.
9+
# Publishing stays manual: npm only releases on a pushed v* tag, never on merge.
10+
name: Auto-merge agent PRs
11+
12+
on:
13+
workflow_run:
14+
workflows: ["CI"]
15+
types: [completed]
16+
17+
permissions:
18+
contents: write
19+
pull-requests: write
20+
21+
jobs:
22+
auto-merge:
23+
if: >-
24+
github.event.workflow_run.conclusion == 'success' &&
25+
github.event.workflow_run.event == 'pull_request' &&
26+
(startsWith(github.event.workflow_run.head_branch, 'agent/') ||
27+
startsWith(github.event.workflow_run.head_branch, 'security/'))
28+
runs-on: ubuntu-latest
29+
steps:
30+
- name: Squash-merge the green PR
31+
env:
32+
GH_TOKEN: ${{ github.token }}
33+
BRANCH: ${{ github.event.workflow_run.head_branch }}
34+
run: |
35+
PR=$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$BRANCH" \
36+
--state open --json number,isDraft \
37+
--jq '[.[] | select(.isDraft == false)][0].number')
38+
if [ -z "$PR" ]; then
39+
echo "No open non-draft PR for $BRANCH — nothing to merge."
40+
exit 0
41+
fi
42+
echo "Auto-merging PR #$PR ($BRANCH) after green CI."
43+
gh pr merge "$PR" --repo "$GITHUB_REPOSITORY" --squash --delete-branch

src/api-surface.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { describe, it, expect } from "vitest";
2+
import * as sdk from "./index.js";
3+
import {
4+
RektRadar,
5+
DEFAULT_BASE_URL,
6+
connectStream,
7+
streamUrl,
8+
verifyWebhook,
9+
} from "./index.js";
10+
import type { FetchLike } from "./types.js";
11+
12+
// The README, the rektradar.io/developers page and the dev.to article all
13+
// reference this surface by hand. When a public name is renamed or removed,
14+
// nothing in those hand-written examples fails until a developer copy-pastes
15+
// them and gets a runtime error (this is exactly how `rr.stream()` shipped in
16+
// the docs while the SDK only ever exposed `connectStream`). These tests pin
17+
// the contract so a breaking change fails CI here instead of in someone's bot.
18+
19+
const noopFetch: FetchLike = async () => ({
20+
ok: true,
21+
status: 200,
22+
json: async () => ({}),
23+
headers: { get: () => null },
24+
});
25+
26+
describe("public API surface", () => {
27+
it("exports exactly the documented runtime members", () => {
28+
expect(Object.keys(sdk).sort()).toEqual(
29+
[
30+
"DEFAULT_BASE_URL",
31+
"RektRadar",
32+
"RektRadarError",
33+
"connectStream",
34+
"streamUrl",
35+
"verifyWebhook",
36+
].sort(),
37+
);
38+
});
39+
40+
it("exposes the documented RektRadar methods", () => {
41+
const rr = new RektRadar({ apiKey: "rr_test", fetch: noopFetch });
42+
const surface = rr as unknown as Record<string, unknown>;
43+
for (const method of ["token", "tokenFull", "rugs", "recent", "topDeployers"]) {
44+
expect(typeof surface[method]).toBe("function");
45+
}
46+
});
47+
48+
it("has no streaming method on the client (streaming is connectStream)", () => {
49+
// The docs once wrote `rr.stream(...)`, which never existed. Streaming is a
50+
// standalone `connectStream()` export. Guard against the mistake returning.
51+
const rr = new RektRadar({ apiKey: "rr_test", fetch: noopFetch });
52+
expect((rr as unknown as Record<string, unknown>).stream).toBeUndefined();
53+
expect(typeof connectStream).toBe("function");
54+
expect(typeof streamUrl).toBe("function");
55+
expect(typeof verifyWebhook).toBe("function");
56+
});
57+
58+
it("defaults to the canonical api.rektradar.io host, never app.", () => {
59+
expect(DEFAULT_BASE_URL).toBe("https://api.rektradar.io");
60+
expect(DEFAULT_BASE_URL).not.toContain("app.rektradar.io");
61+
62+
// The WebSocket origin must also be api. (the base-URL bug pointed both the
63+
// REST client and the stream at app.rektradar.io). connectStream defaults to
64+
// wss://api.rektradar.io when no baseUrl is passed.
65+
let openedUrl = "";
66+
class CaptureSocket {
67+
constructor(url: string) {
68+
openedUrl = url;
69+
}
70+
addEventListener() {}
71+
removeEventListener() {}
72+
close() {}
73+
}
74+
connectStream({
75+
apiKey: "rr_test",
76+
events: ["rug"],
77+
onMessage: () => {},
78+
WebSocket: CaptureSocket as unknown as never,
79+
});
80+
expect(openedUrl.startsWith("wss://api.rektradar.io")).toBe(true);
81+
expect(openedUrl).not.toContain("app.rektradar.io");
82+
});
83+
});

0 commit comments

Comments
 (0)