Skip to content

Commit 5388d91

Browse files
committed
ci: add daily sandbox health check (read + write round-trip)
Add `npm run integration`: a sandbox-only check that reads account/quota and does a campaign create+delete round-trip, hard-guarded to refuse running outside the sandbox. New health.yml runs it on push to main, daily (cron) and on demand — never on pull_request, with the token scoped to a single step.
1 parent a1c8697 commit 5388d91

3 files changed

Lines changed: 106 additions & 0 deletions

File tree

.github/workflows/health.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Sandbox health check
2+
3+
# Read + write round-trip against the API sandbox (synthetic data, no real money).
4+
# Runs on push to main and daily; never on pull_request, so the token secret is
5+
# not exposed to PRs or forks.
6+
on:
7+
schedule:
8+
- cron: "0 6 * * *"
9+
push:
10+
branches: [main]
11+
workflow_dispatch:
12+
13+
jobs:
14+
integration:
15+
runs-on: ubuntu-latest
16+
if: github.repository == 'gistrec/mcp-yandex-direct'
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: actions/setup-node@v4
20+
with:
21+
node-version: 20.x
22+
cache: npm
23+
- run: npm ci
24+
- name: Sandbox integration check (read + write round-trip)
25+
run: npm run integration
26+
env:
27+
YANDEX_DIRECT_SANDBOX: "true"
28+
YANDEX_DIRECT_TOKEN: ${{ secrets.YANDEX_DIRECT_TOKEN }}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"dev": "tsx watch src/index.ts",
1919
"typecheck": "tsc -p tsconfig.check.json",
2020
"smoke": "node --import tsx src/smoke.ts",
21+
"integration": "node --import tsx src/integration.ts",
2122
"test": "node --import tsx --test $(find src -name '*.test.ts')",
2223
"prepare": "npm run build",
2324
"prepublishOnly": "npm run typecheck && npm test"

src/integration.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env node
2+
// Sandbox integration check: a read pass plus a create/delete write round-trip.
3+
// Hard-guarded to the sandbox so it can never write to a real account.
4+
import { YandexDirectClient } from "./client.js";
5+
import { loadConfig } from "./config.js";
6+
7+
interface ObjectResult {
8+
Id?: number;
9+
Errors?: { Message?: string }[];
10+
}
11+
12+
function today(): string {
13+
return new Date().toISOString().slice(0, 10);
14+
}
15+
16+
function firstError(result?: ObjectResult): string {
17+
return result?.Errors?.map((e) => e.Message).join("; ") ?? "unknown error";
18+
}
19+
20+
async function main(): Promise<void> {
21+
const config = loadConfig();
22+
if (!config.sandbox) {
23+
console.error("Refusing to run: integration writes require the sandbox (set YANDEX_DIRECT_SANDBOX=true).");
24+
process.exit(1);
25+
}
26+
const client = new YandexDirectClient(config);
27+
console.log("Yandex Direct sandbox integration check\n");
28+
29+
// 1. Read: account info (also carries the Units quota header).
30+
const account = await client.call<{ Clients?: { Login?: string; Currency?: string }[] }>(
31+
"clients",
32+
"get",
33+
{ FieldNames: ["Login", "Currency"] },
34+
);
35+
const c = account.Clients?.[0];
36+
console.log(`account: ${c?.Login ?? "?"} (${c?.Currency ?? "?"})`);
37+
const units = client.units;
38+
if (units) console.log(`quota: ${units.spent} spent / ${units.rest} left / ${units.limit} limit`);
39+
40+
// 2. Write round-trip: create a campaign, then delete it (leaves the sandbox clean).
41+
const name = `ci-healthcheck-${Date.now()}`;
42+
const add = await client.call<{ AddResults?: ObjectResult[] }>("campaigns", "add", {
43+
Campaigns: [
44+
{
45+
Name: name,
46+
StartDate: today(),
47+
TextCampaign: {
48+
BiddingStrategy: {
49+
Search: { BiddingStrategyType: "HIGHEST_POSITION" },
50+
Network: { BiddingStrategyType: "SERVING_OFF" },
51+
},
52+
},
53+
},
54+
],
55+
});
56+
const created = add.AddResults?.[0];
57+
if (!created?.Id) {
58+
throw new Error(`campaign create failed: ${firstError(created)}`);
59+
}
60+
console.log(`created campaign ${created.Id}`);
61+
62+
const del = await client.call<{ DeleteResults?: ObjectResult[] }>("campaigns", "delete", {
63+
SelectionCriteria: { Ids: [created.Id] },
64+
});
65+
const deleted = del.DeleteResults?.[0];
66+
if (deleted?.Errors?.length) {
67+
throw new Error(`campaign delete failed: ${firstError(deleted)}`);
68+
}
69+
console.log(`deleted campaign ${created.Id}`);
70+
71+
console.log("\nIntegration check passed.");
72+
}
73+
74+
main().catch((err) => {
75+
console.error(`\nIntegration check FAILED: ${err instanceof Error ? err.message : String(err)}`);
76+
process.exit(1);
77+
});

0 commit comments

Comments
 (0)