Claude-code-plugins miro-ci-integration
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/miro-pack/skills/miro-ci-integration" ~/.claude/skills/jeremylongshore-claude-code-plugins-miro-ci-integration && rm -rf "$T"
manifest:
plugins/saas-packs/miro-pack/skills/miro-ci-integration/SKILL.mdsource content
Miro CI Integration
Overview
Set up CI/CD pipelines for Miro REST API v2 integrations with isolated test boards, proper secret handling, and API validation in GitHub Actions.
Prerequisites
- GitHub repository with Actions enabled
- Miro app with test credentials (separate from production)
- A dedicated test board ID for integration tests
GitHub Actions Workflow
# .github/workflows/miro-integration.yml name: Miro Integration Tests on: push: branches: [main] pull_request: branches: [main] jobs: unit-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - run: npm ci - run: npm test -- --coverage - name: Upload coverage if: always() uses: actions/upload-artifact@v4 with: name: coverage path: coverage/ integration-tests: runs-on: ubuntu-latest needs: unit-tests # Only run on main branch or when explicitly requested if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'run-integration') env: MIRO_ACCESS_TOKEN: ${{ secrets.MIRO_ACCESS_TOKEN_TEST }} MIRO_TEST_BOARD_ID: ${{ secrets.MIRO_TEST_BOARD_ID }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - run: npm ci - name: Verify Miro API connectivity run: | STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ -H "Authorization: Bearer $MIRO_ACCESS_TOKEN" \ "https://api.miro.com/v2/boards?limit=1") if [ "$STATUS" != "200" ]; then echo "::error::Miro API returned $STATUS — check MIRO_ACCESS_TOKEN_TEST secret" exit 1 fi echo "Miro API connectivity verified (HTTP $STATUS)" - name: Run integration tests run: npm run test:integration timeout-minutes: 5 - name: Cleanup test board items if: always() run: | # Delete items created during test run curl -s "https://api.miro.com/v2/boards/$MIRO_TEST_BOARD_ID/items?limit=50" \ -H "Authorization: Bearer $MIRO_ACCESS_TOKEN" | \ jq -r '.data[].id' | \ while read -r ITEM_ID; do curl -s -X DELETE \ "https://api.miro.com/v2/boards/$MIRO_TEST_BOARD_ID/items/$ITEM_ID" \ -H "Authorization: Bearer $MIRO_ACCESS_TOKEN" done echo "Test board cleaned"
Configuring Secrets
# Store test credentials as GitHub secrets gh secret set MIRO_ACCESS_TOKEN_TEST --body "your_test_access_token" gh secret set MIRO_TEST_BOARD_ID --body "uXjVN1234567890" # For OAuth refresh in CI (long-lived tokens) gh secret set MIRO_CLIENT_ID --body "your_client_id" gh secret set MIRO_CLIENT_SECRET --body "your_client_secret" gh secret set MIRO_REFRESH_TOKEN --body "your_refresh_token"
Integration Test Examples
// tests/integration/miro-boards.test.ts import { describe, it, expect, afterAll } from 'vitest'; const TOKEN = process.env.MIRO_ACCESS_TOKEN!; const BOARD_ID = process.env.MIRO_TEST_BOARD_ID!; const BASE = 'https://api.miro.com/v2'; const createdIds: string[] = []; const miroFetch = async (path: string, method = 'GET', body?: unknown) => { const response = await fetch(`${BASE}${path}`, { method, headers: { 'Authorization': `Bearer ${TOKEN}`, 'Content-Type': 'application/json', }, ...(body ? { body: JSON.stringify(body) } : {}), }); return { status: response.status, data: await response.json() }; }; describe('Miro REST API v2 Integration', () => { it.skipIf(!TOKEN)('should read test board', async () => { const { status, data } = await miroFetch(`/boards/${BOARD_ID}`); expect(status).toBe(200); expect(data.type).toBe('board'); expect(data.id).toBe(BOARD_ID); }); it.skipIf(!TOKEN)('should create and delete a sticky note', async () => { // Create const { status: createStatus, data: note } = await miroFetch( `/boards/${BOARD_ID}/sticky_notes`, 'POST', { data: { content: `CI test: ${Date.now()}`, shape: 'square' }, style: { fillColor: 'light_yellow' }, position: { x: 0, y: 0 }, } ); expect(createStatus).toBe(201); expect(note.type).toBe('sticky_note'); createdIds.push(note.id); // Delete const { status: deleteStatus } = await miroFetch( `/boards/${BOARD_ID}/items/${note.id}`, 'DELETE' ); expect(deleteStatus).toBe(204); }); it.skipIf(!TOKEN)('should list items with pagination', async () => { const { status, data } = await miroFetch( `/boards/${BOARD_ID}/items?limit=10` ); expect(status).toBe(200); expect(Array.isArray(data.data)).toBe(true); }); afterAll(async () => { // Clean up any items that weren't deleted in tests for (const id of createdIds) { await miroFetch(`/boards/${BOARD_ID}/items/${id}`, 'DELETE').catch(() => {}); } }); });
Token Refresh in CI
Miro access tokens expire in ~1 hour. For CI pipelines that run infrequently, automate refresh:
refresh-token: runs-on: ubuntu-latest steps: - name: Refresh Miro access token run: | RESPONSE=$(curl -s -X POST https://api.miro.com/v1/oauth/token \ -d "grant_type=refresh_token" \ -d "client_id=${{ secrets.MIRO_CLIENT_ID }}" \ -d "client_secret=${{ secrets.MIRO_CLIENT_SECRET }}" \ -d "refresh_token=${{ secrets.MIRO_REFRESH_TOKEN }}") NEW_TOKEN=$(echo "$RESPONSE" | jq -r '.access_token') if [ "$NEW_TOKEN" = "null" ] || [ -z "$NEW_TOKEN" ]; then echo "::error::Token refresh failed" exit 1 fi echo "::add-mask::$NEW_TOKEN" echo "MIRO_ACCESS_TOKEN=$NEW_TOKEN" >> "$GITHUB_ENV"
Error Handling
| CI Issue | Cause | Solution |
|---|---|---|
| Token expired in CI | Long time between runs | Add token refresh step |
| Rate limited in CI | Parallel test runs | Run integration tests serially |
| Test board full | No cleanup | Add cleanup step |
| Flaky tests | Miro API latency | Add retries + increase timeout |
| Secret not found | Missing GitHub secret | Run commands above |
Resources
Next Steps
For deployment patterns, see
miro-deploy-integration.