Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 69 additions & 9 deletions integration-docs/scripts/upload-openapi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packageRoot = path.resolve(__dirname, '..');
const repoRoot = path.resolve(packageRoot, '..');
const defaultApiReferenceDir = path.join(repoRoot, 'api-reference');
const docsBaseUrl = 'https://docs.flashcat.cloud';
const httpMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'];

export const requiredEnv = [
'CDN_ACCESS_KEY',
Expand Down Expand Up @@ -46,6 +48,36 @@ export function validateJsonFile(filePath) {
JSON.parse(fs.readFileSync(filePath, 'utf8'));
}

export function buildOpenapiReferenceIndex(openapi) {
const result = {};

for (const [apiPath, pathItem] of Object.entries(openapi.paths ?? {})) {
const operation = httpMethods
.map((method) => pathItem?.[method])
.find(Boolean);
const href = operation?.['x-mint']?.href;
const label = operation?.summary || operation?.['x-mint']?.metadata?.sidebarTitle || operation?.operationId;

if (href && label) {
result[apiPath] = {
label,
url: new URL(href, docsBaseUrl).toString()
};
}
}

return result;
}

function buildJsonUploadOptions() {
return {
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Cache-Control': 'public, max-age=300'
}
};
}

async function createOssClient(env = process.env) {
const { default: OSS } = await import('ali-oss');
return new OSS({
Expand Down Expand Up @@ -77,6 +109,20 @@ async function refreshCdnCache(cdnRuntime, url) {
console.log(`Refreshed CDN cache: ${url}`);
}

async function uploadJsonAsset({
env,
ossClient,
cdnRuntime,
ossFilePath,
payload,
label
}) {
const result = await ossClient.put(ossFilePath, payload, buildJsonUploadOptions());
const cdnUrl = buildCdnUrl(result.url, env.CDN_ENDPOINT, env.CDN_URL);
console.log(`Uploaded ${label} -> ${cdnUrl}`);
await refreshCdnCache(cdnRuntime, cdnUrl);
}

export async function uploadOpenapiJsonFiles({
apiReferenceDir = defaultApiReferenceDir,
env = process.env,
Expand All @@ -103,18 +149,32 @@ export async function uploadOpenapiJsonFiles({
for (const file of files) {
const localFilePath = path.join(apiReferenceDir, file);
const ossFilePath = buildOssFilePath(env.CDN_DIR, file);
const result = await resolvedOssClient.put(ossFilePath, localFilePath, {
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Cache-Control': 'public, max-age=300'
}
const uploadPayload = Buffer.from(`${JSON.stringify(
buildOpenapiReferenceIndex(JSON.parse(fs.readFileSync(localFilePath, 'utf8'))),
null,
2
)}\n`);
await uploadJsonAsset({
env,
ossClient: resolvedOssClient,
cdnRuntime: resolvedCdnRuntime,
ossFilePath,
payload: uploadPayload,
label: file
});
const cdnUrl = buildCdnUrl(result.url, env.CDN_ENDPOINT, env.CDN_URL);
console.log(`Uploaded ${file} -> ${cdnUrl}`);
await refreshCdnCache(resolvedCdnRuntime, cdnUrl);
}

console.log(`Uploaded ${files.length} OpenAPI JSON files from ${apiReferenceDir}`);
const manifestPayload = Buffer.from(`${JSON.stringify({ files }, null, 2)}\n`);
await uploadJsonAsset({
env,
ossClient: resolvedOssClient,
cdnRuntime: resolvedCdnRuntime,
ossFilePath: buildOssFilePath(env.CDN_DIR, 'manifest.json'),
payload: manifestPayload,
label: 'manifest.json'
});

console.log(`Uploaded ${files.length} OpenAPI JSON files and manifest from ${apiReferenceDir}`);
}

async function loadDotenvIfAvailable() {
Expand Down
88 changes: 83 additions & 5 deletions integration-docs/scripts/upload-openapi.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import path from 'node:path';
import test from 'node:test';

import {
buildOpenapiReferenceIndex,
buildOssFilePath,
listOpenapiJsonFiles,
uploadOpenapiJsonFiles,
Expand Down Expand Up @@ -50,12 +51,71 @@ test('validateRequiredEnv reports every missing upload credential', () => {
]);
});

test('uploadOpenapiJsonFiles uploads every JSON file and refreshes each CDN URL', async () => {
test('buildOpenapiReferenceIndex keeps only path label and docs URL', () => {
assert.deepEqual(buildOpenapiReferenceIndex({
paths: {
'/alert/list': {
post: {
summary: '查询告警列表',
'x-mint': {
href: '/zh/api-reference/on-call/alerts/alert-read-list'
}
}
},
'/alert/ignored': {
post: {
summary: '缺少文档链接'
}
}
}
}), {
'/alert/list': {
label: '查询告警列表',
url: 'https://docs.flashcat.cloud/zh/api-reference/on-call/alerts/alert-read-list'
}
});
});

test('uploadOpenapiJsonFiles uploads every JSON file, a manifest, and refreshes each CDN URL', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openapi-upload-'));
const apiReferenceDir = path.join(tempDir, 'api-reference');
fs.mkdirSync(apiReferenceDir);
fs.writeFileSync(path.join(apiReferenceDir, 'openapi.en.json'), '{"openapi":"3.1.0"}\n');
fs.writeFileSync(path.join(apiReferenceDir, 'openapi.zh.json'), '{"openapi":"3.1.0"}\n');
fs.writeFileSync(path.join(apiReferenceDir, 'openapi.en.json'), JSON.stringify({
openapi: '3.1.0',
paths: {
'/alert/list': {
post: {
summary: 'List alerts',
'x-mint': {
href: '/en/api-reference/on-call/alerts/alert-read-list'
},
responses: {
200: {
description: 'Success'
}
}
}
}
}
}));
fs.writeFileSync(path.join(apiReferenceDir, 'openapi.zh.json'), JSON.stringify({
openapi: '3.1.0',
paths: {
'/alert/list': {
post: {
summary: '查询告警列表',
'x-mint': {
href: '/zh/api-reference/on-call/alerts/alert-read-list'
},
responses: {
200: {
description: '成功'
}
}
}
}
}
}));
fs.writeFileSync(path.join(apiReferenceDir, 'ignored.txt'), 'not json\n');

const uploaded = [];
Expand Down Expand Up @@ -92,15 +152,33 @@ test('uploadOpenapiJsonFiles uploads every JSON file and refreshes each CDN URL'
uploaded.map((item) => item.ossFilePath),
[
'/docs/api-reference/openapi.en.json',
'/docs/api-reference/openapi.zh.json'
'/docs/api-reference/openapi.zh.json',
'/docs/api-reference/manifest.json'
]
);
assert.deepEqual(
refreshed,
[
'https://docs-cdn.flashcat.cloud/docs/api-reference/openapi.en.json',
'https://docs-cdn.flashcat.cloud/docs/api-reference/openapi.zh.json'
'https://docs-cdn.flashcat.cloud/docs/api-reference/openapi.zh.json',
'https://docs-cdn.flashcat.cloud/docs/api-reference/manifest.json'
]
);
assert.equal(uploaded[0].options.headers['Content-Type'], 'application/json; charset=utf-8');
const zhUpload = uploaded.find((item) => item.ossFilePath.endsWith('/openapi.zh.json'));
assert.ok(zhUpload);
assert.deepEqual(JSON.parse(zhUpload.localFilePath.toString('utf8')), {
'/alert/list': {
label: '查询告警列表',
url: 'https://docs.flashcat.cloud/zh/api-reference/on-call/alerts/alert-read-list'
}
});
const manifestUpload = uploaded.find((item) => item.ossFilePath.endsWith('/manifest.json'));
assert.ok(manifestUpload);
assert.deepEqual(JSON.parse(manifestUpload.localFilePath.toString('utf8')), {
files: [
'openapi.en.json',
'openapi.zh.json'
]
});
});