Live-API smoke #26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Live-API smoke | |
| # Runs the smoke test against real public APIs (DexScreener, GoPlus, OpenOcean, | |
| # Across, Hyperliquid, Polymarket, Morpho, Pendle, Drift, etc.) on a schedule | |
| # and on-demand. Unit tests mock these APIs; this catches drift between our | |
| # wiring and what the upstream services actually return. | |
| # | |
| # This intentionally runs separately from the standard CI workflow so a | |
| # transient upstream outage doesn't block PRs. | |
| # | |
| # Alerting model: ONE rolling issue (label: smoke-failure). A failing run | |
| # creates it if absent, otherwise appends a comment. A green run closes it | |
| # with a recovery comment. This keeps the signal at exactly one open issue | |
| # instead of one new issue per day (issues #49–#68 taught us that an alert | |
| # nobody can keep up with is the same as no alert). | |
| on: | |
| schedule: | |
| # Daily at 09:00 UTC. Picks up endpoint shape changes within a day. | |
| - cron: '0 9 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| ref: | |
| description: 'Git ref to test (default: main)' | |
| required: false | |
| default: 'main' | |
| jobs: | |
| smoke: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| issues: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.inputs.ref || 'main' }} | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| cache-dependency-path: mcp-server/package-lock.json | |
| - name: Install deps | |
| run: cd mcp-server && npm ci | |
| - name: Compile TypeScript | |
| run: cd mcp-server && node node_modules/typescript/bin/tsc | |
| - name: Run live-API smoke | |
| id: smoke | |
| env: | |
| # CHAINGPT_API_KEY is required for the server to boot, but the | |
| # read-only smoke cases never hit the ChainGPT plugin API itself. | |
| CHAINGPT_API_KEY: ${{ secrets.CHAINGPT_API_KEY || 'smoke-test' }} | |
| MORALIS_API_KEY: ${{ secrets.MORALIS_API_KEY }} | |
| ONEINCH_API_KEY: ${{ secrets.ONEINCH_API_KEY }} | |
| ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} | |
| run: | | |
| cd mcp-server | |
| set +e | |
| node dist/smoke-test.js 2>&1 | tee smoke-output.txt | |
| code=${PIPESTATUS[0]} | |
| # Strip ANSI colors and keep the failure tail for the issue body | |
| tail -c 4000 smoke-output.txt | sed 's/\x1b\[[0-9;]*m//g' > smoke-tail.txt | |
| exit $code | |
| - name: Update rolling issue (failure → create/comment, success → close) | |
| if: ${{ always() && github.event_name == 'schedule' }} | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const failed = '${{ steps.smoke.outcome }}' === 'failure'; | |
| const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const today = new Date().toISOString().slice(0, 10); | |
| let tail = ''; | |
| try { tail = fs.readFileSync('mcp-server/smoke-tail.txt', 'utf8'); } catch {} | |
| const { data: open } = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| labels: 'smoke-failure', | |
| per_page: 50, | |
| }); | |
| const rolling = open.find((i) => i.title.startsWith('[smoke] Live-API smoke failing')); | |
| if (failed) { | |
| const update = [ | |
| `**${today} — still failing.** [Run log](${runUrl})`, | |
| '', | |
| '```', | |
| tail.slice(-3000), | |
| '```', | |
| ].join('\n'); | |
| if (rolling) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| issue_number: rolling.number, body: update, | |
| }); | |
| core.info(`Commented on rolling issue #${rolling.number}.`); | |
| } else { | |
| await github.rest.issues.create({ | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| title: '[smoke] Live-API smoke failing', | |
| body: [ | |
| 'The scheduled live-API smoke is failing. This issue stays open (with one', | |
| 'comment per failing day) until the first green run auto-closes it.', | |
| '', | |
| 'Likely causes: an upstream API changed shape, a rate limit, or a moved endpoint.', | |
| '', | |
| update, | |
| ].join('\n'), | |
| labels: ['smoke-failure', 'live-api'], | |
| }); | |
| core.info('Opened new rolling smoke-failure issue.'); | |
| } | |
| } else if ('${{ steps.smoke.outcome }}' === 'success' && rolling) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| issue_number: rolling.number, | |
| body: `**${today} — recovered.** Smoke is green again: [run log](${runUrl}). Auto-closing.`, | |
| }); | |
| await github.rest.issues.update({ | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| issue_number: rolling.number, state: 'closed', | |
| }); | |
| core.info(`Closed rolling issue #${rolling.number} after green run.`); | |
| } else if (rolling) { | |
| // cancelled / skipped — inconclusive. Keep the issue open but | |
| // don't let the timeline silently skip a day. | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| issue_number: rolling.number, | |
| body: `**${today} — inconclusive** (run outcome: ${{ toJSON(steps.smoke.outcome) }}). [Run log](${runUrl}). Issue stays open.`, | |
| }); | |
| } else { | |
| core.info('No open rolling issue — nothing to do.'); | |
| } |