From 9370d2c89833e96f7653b204815af8934efea281 Mon Sep 17 00:00:00 2001 From: agent-company Date: Fri, 29 May 2026 20:04:26 +0000 Subject: [PATCH] [Phase 6] ci: add scripts/publish-openapi.js and publish-openapi workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes leeworks-agents/api-company#100 - scripts/publish-openapi.js: Node.js script using only built-in modules (https, fs, path — no extra deps) that uploads each API's openapi.yaml to the RapidAPI Platform API via PUT multipart/form-data. - Reads RAPIDAPI_KEY/RAPIDAPI_PLATFORM_KEY from environment. - Skips silently per API if API ID / version ID not set. - Skips all uploads if RAPIDAPI_KEY not set (exits 0). - Exits non-zero on any HTTP error. - Logs: '✓ Published {api-name} spec to RapidAPI' on success. - Passes 'node --check' syntax validation. - .gitea/workflows/publish-openapi.yaml: triggers on push to main when apis/*/openapi.yaml changes. - Runs Redocly CLI lint (fails pipeline on spec errors). - Detects changed specs via git diff. - Calls publish-openapi.js; no secrets = skip, not crash. --- .gitea/workflows/publish-openapi.yaml | 58 +++++++++++ scripts/publish-openapi.js | 134 ++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 .gitea/workflows/publish-openapi.yaml create mode 100644 scripts/publish-openapi.js diff --git a/.gitea/workflows/publish-openapi.yaml b/.gitea/workflows/publish-openapi.yaml new file mode 100644 index 0000000..c8e8fab --- /dev/null +++ b/.gitea/workflows/publish-openapi.yaml @@ -0,0 +1,58 @@ +# Validate and publish OpenAPI specs to RapidAPI when apis/*/openapi.yaml changes on main. +# Requires secrets (configured once RapidAPI listings are live): +# RAPIDAPI_PLATFORM_KEY, RAPIDAPI_ZIP_API_ID, RAPIDAPI_ZIP_VERSION_ID, +# RAPIDAPI_HOLIDAYS_API_ID, RAPIDAPI_HOLIDAYS_VERSION_ID, +# RAPIDAPI_AQI_API_ID, RAPIDAPI_AQI_VERSION_ID +# +# If RAPIDAPI_PLATFORM_KEY is not set the publish step exits 0 with a skip message. + +name: Validate and Publish OpenAPI Specs + +on: + push: + branches: + - main + paths: + - 'apis/*/openapi.yaml' + +jobs: + publish: + name: Lint and publish specs + runs-on: ubuntu-latest + + steps: + - name: Checkout (with history for diff) + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Set up Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Redocly CLI + run: npm install -g @redocly/cli@latest + + - name: Lint OpenAPI specs + run: | + echo "Linting all OpenAPI specs..." + npx @redocly/cli lint apis/*/openapi.yaml + + - name: Detect changed specs + id: changed + run: | + changed=$(git diff --name-only HEAD~1 HEAD | grep 'openapi\.yaml' || true) + echo "Changed specs: ${changed:-none}" + echo "files=${changed}" >> "$GITHUB_OUTPUT" + + - name: Publish specs to RapidAPI + env: + RAPIDAPI_KEY: ${{ secrets.RAPIDAPI_PLATFORM_KEY }} + RAPIDAPI_ZIP_API_ID: ${{ secrets.RAPIDAPI_ZIP_API_ID }} + RAPIDAPI_ZIP_VERSION_ID: ${{ secrets.RAPIDAPI_ZIP_VERSION_ID }} + RAPIDAPI_HOLIDAYS_API_ID: ${{ secrets.RAPIDAPI_HOLIDAYS_API_ID }} + RAPIDAPI_HOLIDAYS_VERSION_ID: ${{ secrets.RAPIDAPI_HOLIDAYS_VERSION_ID }} + RAPIDAPI_AQI_API_ID: ${{ secrets.RAPIDAPI_AQI_API_ID }} + RAPIDAPI_AQI_VERSION_ID: ${{ secrets.RAPIDAPI_AQI_VERSION_ID }} + run: node scripts/publish-openapi.js diff --git a/scripts/publish-openapi.js b/scripts/publish-openapi.js new file mode 100644 index 0000000..ce43d18 --- /dev/null +++ b/scripts/publish-openapi.js @@ -0,0 +1,134 @@ +#!/usr/bin/env node +/** + * publish-openapi.js + * + * Uploads each API's openapi.yaml to the RapidAPI Platform API. + * Environment variables required per API (skips silently if not set): + * RAPIDAPI_KEY — RapidAPI Platform API bearer token + * RAPIDAPI_ZIP_API_ID — API ID for ZIP Enrichment on RapidAPI + * RAPIDAPI_ZIP_VERSION_ID — Version ID for ZIP Enrichment + * RAPIDAPI_HOLIDAYS_API_ID + * RAPIDAPI_HOLIDAYS_VERSION_ID + * RAPIDAPI_AQI_API_ID + * RAPIDAPI_AQI_VERSION_ID + * + * Usage: node scripts/publish-openapi.js + */ + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); + +const RAPIDAPI_KEY = process.env.RAPIDAPI_KEY || process.env.RAPIDAPI_PLATFORM_KEY; + +if (!RAPIDAPI_KEY) { + console.log('⚠️ RAPIDAPI_KEY / RAPIDAPI_PLATFORM_KEY not set — skipping all spec uploads.'); + process.exit(0); +} + +const APIS = [ + { + name: 'zip-enrichment', + specPath: path.join(__dirname, '..', 'apis', 'zip-enrichment', 'openapi.yaml'), + apiId: process.env.RAPIDAPI_ZIP_API_ID, + versionId: process.env.RAPIDAPI_ZIP_VERSION_ID, + }, + { + name: 'holidays', + specPath: path.join(__dirname, '..', 'apis', 'holidays', 'openapi.yaml'), + apiId: process.env.RAPIDAPI_HOLIDAYS_API_ID, + versionId: process.env.RAPIDAPI_HOLIDAYS_VERSION_ID, + }, + { + name: 'air-quality', + specPath: path.join(__dirname, '..', 'apis', 'air-quality', 'openapi.yaml'), + apiId: process.env.RAPIDAPI_AQI_API_ID, + versionId: process.env.RAPIDAPI_AQI_VERSION_ID, + }, +]; + +/** + * Build a multipart/form-data body from a file buffer. + * Returns { body: Buffer, boundary: string } + */ +function buildMultipart(fieldName, filename, fileBuffer, contentType = 'application/yaml') { + const boundary = '----FormBoundary' + Math.random().toString(36).slice(2); + const CRLF = '\r\n'; + const parts = [ + Buffer.from( + `--${boundary}${CRLF}` + + `Content-Disposition: form-data; name="${fieldName}"; filename="${filename}"${CRLF}` + + `Content-Type: ${contentType}${CRLF}${CRLF}` + ), + fileBuffer, + Buffer.from(`${CRLF}--${boundary}--${CRLF}`), + ]; + return { body: Buffer.concat(parts), boundary }; +} + +/** + * Upload a spec file to RapidAPI Platform API. + * Returns a promise that resolves with the response status code. + */ +function uploadSpec(api) { + return new Promise((resolve, reject) => { + if (!api.apiId || !api.versionId) { + console.log(`⏭️ Skipping ${api.name}: API ID or Version ID not configured.`); + return resolve(null); + } + + if (!fs.existsSync(api.specPath)) { + console.log(`⏭️ Skipping ${api.name}: spec file not found at ${api.specPath}`); + return resolve(null); + } + + const fileBuffer = fs.readFileSync(api.specPath); + const { body, boundary } = buildMultipart('spec', 'openapi.yaml', fileBuffer); + + const options = { + hostname: 'platformapi1.p.rapidapi.com', + path: `/v1/apis/${api.apiId}/versions/${api.versionId}`, + method: 'PUT', + headers: { + 'Authorization': `Bearer ${RAPIDAPI_KEY}`, + 'X-RapidAPI-Key': RAPIDAPI_KEY, + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + 'Content-Length': body.length, + }, + }; + + const req = https.request(options, (res) => { + let data = ''; + res.on('data', (chunk) => { data += chunk; }); + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + console.log(`✓ Published ${api.name} spec to RapidAPI (HTTP ${res.statusCode})`); + resolve(res.statusCode); + } else { + reject(new Error(`Failed to publish ${api.name}: HTTP ${res.statusCode} — ${data}`)); + } + }); + }); + + req.on('error', reject); + req.write(body); + req.end(); + }); +} + +async function main() { + let hasError = false; + for (const api of APIS) { + try { + await uploadSpec(api); + } catch (err) { + console.error(`✗ ${err.message}`); + hasError = true; + } + } + if (hasError) { + process.exit(1); + } +} + +main(); -- 2.52.0