feat(ci): add RAML 1.0 package generation pipeline
Build and Push Docker Image / build (push) Successful in 1m27s
CI / Security audit (push) Successful in 1m43s
CI / Swagger Validation & Coverage (push) Failing after 1m56s
CI / Tests & coverage (push) Failing after 1m56s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 53s

- Add generate:openapi, generate:raml, package:raml scripts to package.json
- Add archiver dependency for creating tar.gz archives
- Create scripts/generate-openapi.js to fetch merged OpenAPI spec from running server
- Create scripts/package-raml.js to build versioned RAML tar.gz archive
- Create .spectral.yml with minimal OpenAPI linting rules
- Add /api/swagger.json endpoint to server/app.js for serving merged spec
- Extend swagger job in ci.yml with RAML generation steps
- Upload raml-package artifact to CI with 14-day retention
This commit is contained in:
2026-05-21 14:04:26 +01:00
parent afa6ebc3c7
commit 1a4ff73067
7 changed files with 1253 additions and 3 deletions
+108
View File
@@ -0,0 +1,108 @@
// Copyright (c) 2026 Gordon Bolton. MIT License.
/**
* Generates the merged OpenAPI spec by bootstrapping the Express app
* and fetching the spec from /api/swagger.json.
*
* This ensures the generated spec matches exactly what users see in production.
*/
const { createApp } = require('../server/app.js');
const http = require('http');
const fs = require('fs');
const path = require('path');
const PORT = 34567; // Use a different port to avoid conflicts
const OUTPUT_DIR = path.join(process.cwd(), 'dist');
const OUTPUT_FILE = path.join(OUTPUT_DIR, 'openapi-merged.json');
async function generateOpenApiSpec() {
// Ensure output directory exists
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
console.log('Bootstrapping Express app in test mode...');
const app = createApp({ skipRateLimits: true });
return new Promise((resolve, reject) => {
const server = http.createServer(app);
server.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
// Fetch the merged spec
const options = {
hostname: 'localhost',
port: PORT,
path: '/api/swagger.json',
method: 'GET'
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const spec = JSON.parse(data);
// Validate it's a proper OpenAPI spec
if (!spec.openapi || !spec.info) {
throw new Error('Invalid OpenAPI spec: missing openapi or info field');
}
// Write to file
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(spec, null, 2));
console.log(`✓ OpenAPI spec written to ${OUTPUT_FILE}`);
console.log(` Version: ${spec.openapi}`);
console.log(` Title: ${spec.info.title}`);
server.close(() => {
resolve();
});
} catch (error) {
console.error('Error processing OpenAPI spec:', error.message);
server.close(() => {
reject(error);
});
}
});
});
req.on('error', (error) => {
console.error('Error fetching spec:', error.message);
server.close(() => {
reject(error);
});
});
req.end();
});
server.on('error', (error) => {
if (error.code === 'EADDRINUSE') {
reject(new Error(`Port ${PORT} is already in use`));
} else {
reject(error);
}
});
});
}
// Run if executed directly
if (require.main === module) {
generateOpenApiSpec()
.then(() => {
console.log('OpenAPI spec generation complete');
process.exit(0);
})
.catch((error) => {
console.error('Failed to generate OpenAPI spec:', error);
process.exit(1);
});
}
module.exports = { generateOpenApiSpec };