diff --git a/.gitignore b/.gitignore index f71a535..028c65c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,11 @@ dist-ssr test-results *.local +# OpenAPI Configuration (may contain sensitive URLs) +.openapi.config.json +src/api/specs/*.yml +src/api/specs/*.yaml + # Editor directories and files !.vscode/extensions.json .idea diff --git a/.openapi.config.example.json b/.openapi.config.example.json new file mode 100644 index 0000000..92f1586 --- /dev/null +++ b/.openapi.config.example.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OpenAPI Download Configuration", + "description": "Konfiguration für das Herunterladen von OpenAPI-Spezifikationen aus Nexus", + "nexusUrl": "https://nexus.example.com", + "repository": "raw-hosted", + "outputDir": "src/api/specs", + "apis": [ + { + "name": "user-management-api", + "version": "1.0.0", + "filename": "user-api.yml", + "description": "User Management API Specification" + }, + { + "name": "product-catalog-api", + "version": "2.3.1", + "filename": "product-api.yml", + "description": "Product Catalog API Specification" + } + ] +} diff --git a/OPENAPI_QUICKSTART.md b/OPENAPI_QUICKSTART.md new file mode 100644 index 0000000..93628e7 --- /dev/null +++ b/OPENAPI_QUICKSTART.md @@ -0,0 +1,176 @@ +# 🚀 OpenAPI Integration Quickstart + +Schnellstart-Anleitung für die OS-unabhängige OpenAPI-Integration in dein React-Projekt. + +## 📋 Voraussetzungen + +- Node.js 18 oder höher +- Zugriff auf Nexus Repository (optional: mit Authentifizierung) +- OpenAPI-Spezifikationen im Nexus veröffentlicht (via Jenkins Shared Library) + +## ⚡ Schnellstart + +### 1. Konfiguration einrichten + +```bash +# Unix/macOS/Linux +cp .openapi.config.example.json .openapi.config.json + +# Windows (PowerShell) +Copy-Item .openapi.config.example.json .openapi.config.json +``` + +Bearbeite `.openapi.config.json` und passe die Werte an: + +```json +{ + "nexusUrl": "https://dein-nexus.example.com", + "repository": "raw-hosted", + "outputDir": "src/api/specs", + "apis": [ + { + "name": "user-management-api", + "version": "1.0.0", + "filename": "user-api.yml" + } + ] +} +``` + +### 2. OpenAPI-Specs herunterladen + +```bash +npm run download:openapi +``` + +### 3. (Optional) Mit Authentifizierung + +```bash +# Unix/macOS/Linux +export NEXUS_USERNAME="dein-username" +export NEXUS_PASSWORD="dein-password" +npm run download:openapi + +# Windows (PowerShell) +$env:NEXUS_USERNAME="dein-username" +$env:NEXUS_PASSWORD="dein-password" +npm run download:openapi +``` + +### 4. Build mit automatischem Download + +```bash +npm run build +``` + +Die Specs werden automatisch vor dem Build heruntergeladen (via `prebuild` Hook). + +## 📁 Dateien + +| Datei | Beschreibung | Git | +|-------|--------------|-----| +| `scripts/download-openapi.js` | Download-Script | ✅ Committen | +| `.openapi.config.example.json` | Beispiel-Konfiguration | ✅ Committen | +| `.openapi.config.json` | Deine Konfiguration | ❌ Nicht committen | +| `src/api/specs/*.yml` | Heruntergeladene Specs | ❌ Nicht committen | + +## 🔗 Zusammenhang mit Shared Library + +1. **Jenkins Shared Library** (`shared-library/`) veröffentlicht OpenAPI-Specs nach Nexus +2. **Download-Script** (`scripts/download-openapi.js`) lädt sie in dein React-Projekt +3. **Build-Process** nutzt die Specs zur Code-Generierung oder Validierung + +### Workflow-Übersicht + +``` +┌─────────────────────┐ +│ Git Repository │ +│ (openapi.yml) │ +└──────────┬──────────┘ + │ + ▼ +┌─────────────────────┐ +│ Jenkins Pipeline │ +│ (Shared Library) │ +│ - Validate │ +│ - Upload to Nexus │ +└──────────┬──────────┘ + │ + ▼ +┌─────────────────────┐ +│ Nexus Repository │ +│ raw-hosted/ │ +│ api-name/version/ │ +└──────────┬──────────┘ + │ + ▼ +┌─────────────────────┐ +│ React Project │ +│ npm run │ +│ download:openapi │ +└─────────────────────┘ +``` + +## 📚 Vollständige Dokumentation + +Für detaillierte Informationen siehe: + +- **[OPENAPI_DOWNLOAD.md](docs/OPENAPI_DOWNLOAD.md)** - Vollständige Anleitung mit CI/CD, Troubleshooting, etc. +- **[shared-library/README.md](shared-library/README.md)** - Jenkins Shared Library Dokumentation + +## 🛠️ NPM Scripts + +| Script | Beschreibung | +|--------|--------------| +| `npm run download:openapi` | Lädt OpenAPI-Specs aus Nexus | +| `npm run build` | Build mit automatischem Download (via `prebuild`) | +| `npm run dev` | Development-Server (ohne Auto-Download) | + +## ❓ Troubleshooting + +### Fehler: "Konfigurationsdatei nicht gefunden" + +```bash +cp .openapi.config.example.json .openapi.config.json +``` + +### Fehler: "HTTP 401 Unauthorized" + +Setze Credentials als Umgebungsvariablen (siehe oben). + +### Fehler: "HTTP 404 Not Found" + +Prüfe: +1. Nexus-URL korrekt? +2. API-Name und Version richtig geschrieben? +3. API in Nexus veröffentlicht? + +Teste URL manuell: +``` +https://nexus.example.com/repository/raw-hosted/api-name/1.0.0/api-name-1.0.0.yml +``` + +## 🎯 Best Practices + +✅ **DO** +- Verwende spezifische Versionen (`1.0.0`) +- Nutze Umgebungsvariablen für Credentials +- Committe `.openapi.config.example.json` + +❌ **DON'T** +- Committe niemals `.openapi.config.json` (enthält sensible URLs) +- Committe niemals heruntergeladene Specs (`src/api/specs/*.yml`) +- Hardcode niemals Credentials + +## 💡 Nächste Schritte + +1. **Code-Generierung**: Generiere TypeScript-Typen aus OpenAPI + ```bash + npm install --save-dev @openapitools/openapi-generator-cli + ``` + +2. **Validierung**: Validiere API-Responses gegen die Spec + +3. **Mock-Server**: Nutze die Spec für lokales Mocking + +4. **CI/CD**: Integriere in deine Build-Pipeline (siehe [OPENAPI_DOWNLOAD.md](docs/OPENAPI_DOWNLOAD.md)) diff --git a/docs/OPENAPI_DOWNLOAD.md b/docs/OPENAPI_DOWNLOAD.md new file mode 100644 index 0000000..d99fa54 --- /dev/null +++ b/docs/OPENAPI_DOWNLOAD.md @@ -0,0 +1,452 @@ +# OpenAPI Download - OS-unabhängige Integration + +Diese Anleitung zeigt, wie du OpenAPI-Spezifikationen aus Nexus in dein React-Projekt herunterladen kannst - vollständig OS-unabhängig (Windows, macOS, Linux). + +## Inhaltsverzeichnis + +1. [Schnellstart](#schnellstart) +2. [Konfiguration](#konfiguration) +3. [Verwendung](#verwendung) +4. [Authentifizierung](#authentifizierung) +5. [Integration in CI/CD](#integration-in-cicd) +6. [Erweiterte Nutzung](#erweiterte-nutzung) +7. [Troubleshooting](#troubleshooting) + +## Schnellstart + +### 1. Konfiguration erstellen + +Kopiere `.openapi.config.example.json` zu `.openapi.config.json` und passe die Werte an: + +```bash +# Unix/macOS/Linux +cp .openapi.config.example.json .openapi.config.json + +# Windows (PowerShell) +Copy-Item .openapi.config.example.json .openapi.config.json + +# Windows (CMD) +copy .openapi.config.example.json .openapi.config.json +``` + +### 2. OpenAPI-Specs herunterladen + +```bash +npm run download:openapi +``` + +Die Dateien werden nach `src/api/specs/` heruntergeladen. + +### 3. Automatischer Download beim Build + +Beim Build werden die Specs automatisch heruntergeladen (via `prebuild` hook): + +```bash +npm run build +``` + +## Konfiguration + +Die Datei `.openapi.config.json` steuert, welche APIs heruntergeladen werden: + +```json +{ + "nexusUrl": "https://nexus.example.com", + "repository": "raw-hosted", + "outputDir": "src/api/specs", + "apis": [ + { + "name": "user-management-api", + "version": "1.0.0", + "filename": "user-api.yml" + }, + { + "name": "product-catalog-api", + "version": "2.3.1", + "filename": "product-api.yml" + } + ] +} +``` + +### Konfigurationsoptionen + +| Feld | Typ | Beschreibung | +|------|-----|--------------| +| `nexusUrl` | String | Basis-URL deines Nexus-Servers | +| `repository` | String | Name des Nexus Raw Repository | +| `outputDir` | String | Zielverzeichnis relativ zum Projektroot | +| `apis` | Array | Liste der herunterzuladenden APIs | +| `apis[].name` | String | API-Name (muss mit Jenkins Pipeline übereinstimmen) | +| `apis[].version` | String | API-Version | +| `apis[].filename` | String | Lokaler Dateiname (optional, default: `{name}.yml`) | + +### URL-Format + +Die OpenAPI-Dateien werden von folgender URL geladen: + +``` +{nexusUrl}/repository/{repository}/{name}/{version}/{name}-{version}.yml +``` + +Beispiel: +``` +https://nexus.example.com/repository/raw-hosted/user-management-api/1.0.0/user-management-api-1.0.0.yml +``` + +## Verwendung + +### Manueller Download + +```bash +# NPM +npm run download:openapi + +# PNPM +pnpm download:openapi + +# Yarn +yarn download:openapi +``` + +### Direkter Aufruf + +```bash +node scripts/download-openapi.js +``` + +### Automatischer Download + +Der Download erfolgt automatisch vor jedem Build durch den `prebuild` Hook in `package.json`: + +```json +{ + "scripts": { + "prebuild": "npm run download:openapi", + "build": "tsc -b && vite build" + } +} +``` + +## Authentifizierung + +### Mit Umgebungsvariablen + +Setze die Credentials als Umgebungsvariablen: + +```bash +# Unix/macOS/Linux +export NEXUS_USERNAME="dein-username" +export NEXUS_PASSWORD="dein-password" +npm run download:openapi + +# Windows (PowerShell) +$env:NEXUS_USERNAME="dein-username" +$env:NEXUS_PASSWORD="dein-password" +npm run download:openapi + +# Windows (CMD) +set NEXUS_USERNAME=dein-username +set NEXUS_PASSWORD=dein-password +npm run download:openapi +``` + +### Mit .env-Datei (optional) + +Wenn du ein Tool wie `dotenv` verwendest: + +1. Installiere `dotenv`: + ```bash + npm install --save-dev dotenv + ``` + +2. Erstelle `.env`: + ```env + NEXUS_USERNAME=dein-username + NEXUS_PASSWORD=dein-password + ``` + +3. Erweitere das Script (erste Zeile in `download-openapi.js`): + ```javascript + import 'dotenv/config'; + ``` + +### Ohne Authentifizierung + +Wenn dein Nexus öffentlich zugänglich ist, funktioniert das Script ohne Credentials. + +## Integration in CI/CD + +### GitHub Actions + +```yaml +name: Build + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci + + - name: Download OpenAPI Specs + env: + NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }} + NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} + run: npm run download:openapi + + - name: Build + run: npm run build +``` + +### GitLab CI + +```yaml +build: + image: node:20 + script: + - npm ci + - npm run download:openapi + - npm run build + variables: + NEXUS_USERNAME: $NEXUS_USERNAME + NEXUS_PASSWORD: $NEXUS_PASSWORD +``` + +### Jenkins + +```groovy +pipeline { + agent any + + environment { + NEXUS_CREDS = credentials('nexus-credentials') + } + + stages { + stage('Install') { + steps { + sh 'npm ci' + } + } + + stage('Download OpenAPI') { + steps { + withCredentials([usernamePassword( + credentialsId: 'nexus-credentials', + usernameVariable: 'NEXUS_USERNAME', + passwordVariable: 'NEXUS_PASSWORD' + )]) { + sh 'npm run download:openapi' + } + } + } + + stage('Build') { + steps { + sh 'npm run build' + } + } + } +} +``` + +## Erweiterte Nutzung + +### Verschiedene Umgebungen + +Erstelle umgebungsspezifische Konfigurationen: + +```bash +.openapi.config.dev.json +.openapi.config.staging.json +.openapi.config.prod.json +``` + +Passe das Script an (in `download-openapi.js`): + +```javascript +const ENV = process.env.NODE_ENV || 'development'; +const CONFIG_FILE = path.join( + process.cwd(), + `.openapi.config.${ENV}.json` +); +``` + +Verwendung: + +```bash +NODE_ENV=production npm run download:openapi +``` + +### Versionierung mit Package-Managern + +Wenn du spezifische Versionen tracken willst: + +```json +{ + "apis": [ + { + "name": "user-management-api", + "version": "latest", + "filename": "user-api.yml" + } + ] +} +``` + +Erweitere das Script um `latest`-Unterstützung (Nexus REST API). + +### Code-Generierung aus OpenAPI + +Nach dem Download kannst du automatisch TypeScript-Typen generieren: + +```bash +npm install --save-dev @openapitools/openapi-generator-cli +``` + +In `package.json`: + +```json +{ + "scripts": { + "download:openapi": "node scripts/download-openapi.js", + "generate:api": "openapi-generator-cli generate -i src/api/specs/user-api.yml -g typescript-fetch -o src/api/generated", + "prebuild": "npm run download:openapi && npm run generate:api" + } +} +``` + +## Troubleshooting + +### Fehler: "Konfigurationsdatei nicht gefunden" + +**Problem:** `.openapi.config.json` existiert nicht. + +**Lösung:** +```bash +cp .openapi.config.example.json .openapi.config.json +``` + +### Fehler: "HTTP 401 Unauthorized" + +**Problem:** Authentifizierung fehlgeschlagen. + +**Lösung:** +1. Prüfe Umgebungsvariablen: + ```bash + # Unix/macOS/Linux + echo $NEXUS_USERNAME + echo $NEXUS_PASSWORD + + # Windows (PowerShell) + echo $env:NEXUS_USERNAME + echo $env:NEXUS_PASSWORD + ``` + +2. Credentials in Jenkins/CI prüfen + +### Fehler: "HTTP 404 Not Found" + +**Problem:** API-Datei existiert nicht in Nexus. + +**Lösung:** +1. Prüfe URL manuell im Browser: + ``` + {nexusUrl}/repository/{repository}/{name}/{version}/{name}-{version}.yml + ``` + +2. Prüfe Schreibweise von `name` und `version` + +3. Prüfe ob die API in Nexus veröffentlicht wurde (Jenkins Pipeline) + +### Fehler: "ENOTFOUND" oder "ECONNREFUSED" + +**Problem:** Netzwerkfehler oder falscher Nexus-URL. + +**Lösung:** +1. Prüfe Nexus-URL: + ```bash + curl https://nexus.example.com + ``` + +2. Prüfe Proxy-Einstellungen: + ```bash + npm config get proxy + npm config get https-proxy + ``` + +3. Prüfe Firewall-Regeln + +### Spec-Dateien werden nicht gefunden im Build + +**Problem:** TypeScript/Build kann die heruntergeladenen Dateien nicht finden. + +**Lösung:** +1. Prüfe `outputDir` in `.openapi.config.json` + +2. Stelle sicher, dass das Verzeichnis im `src`-Ordner liegt + +3. Füge zu `tsconfig.json` hinzu: + ```json + { + "compilerOptions": { + "resolveJsonModule": true + }, + "include": ["src/**/*", "src/api/specs/*.yml"] + } + ``` + +### Script funktioniert nicht unter Windows + +**Problem:** Pfad-Separatoren oder Encoding-Probleme. + +**Lösung:** Das Script verwendet `path.join()`, was automatisch OS-spezifische Pfade erstellt. Wenn Probleme auftreten: + +1. Prüfe, dass Node.js korrekt installiert ist: + ```bash + node --version + ``` + +2. Verwende PowerShell statt CMD + +3. Prüfe Encoding der Konfigurationsdatei (sollte UTF-8 sein) + +## Best Practices + +### 1. Versionierung + +- ✅ Verwende spezifische Versionen (`1.0.0`) +- ❌ Vermeide vage Versionen (`latest`, `*`) + +### 2. Credentials + +- ✅ Nutze Umgebungsvariablen oder CI-Secrets +- ❌ Hardcode niemals Credentials in Dateien + +### 3. Caching + +- ✅ Überlege, ob du Specs in CI/CD cachen willst +- ✅ Nutze den `prebuild` Hook für automatischen Download + +### 4. Git + +- ✅ Committe `.openapi.config.example.json` +- ❌ Committe niemals `.openapi.config.json` (enthält evtl. sensible URLs) +- ❌ Committe niemals heruntergeladene Specs + +## Support + +Bei Fragen oder Problemen: + +1. Prüfe dieses Dokument +2. Prüfe die Nexus-URL im Browser +3. Kontaktiere das DevOps-Team diff --git a/package.json b/package.json index ff37379..eedc4d1 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "lint": "eslint .", "preview": "vite preview", "test:e2e": "playwright test", - "update": "ncu -u" + "update": "ncu -u", + "download:openapi": "node scripts/download-openapi.js", + "prebuild": "npm run download:openapi" }, "dependencies": { "react": "19.2.0", diff --git a/scripts/download-openapi.js b/scripts/download-openapi.js new file mode 100644 index 0000000..20885e3 --- /dev/null +++ b/scripts/download-openapi.js @@ -0,0 +1,190 @@ +#!/usr/bin/env node + +/** + * OpenAPI Downloader + * + * OS-unabhängiges Script zum Herunterladen von OpenAPI-Spezifikationen aus Nexus. + * Unterstützt Authentifizierung und mehrere API-Quellen. + * + * Verwendung: + * node scripts/download-openapi.js + * npm run download:openapi + */ + +import fs from 'fs'; +import path from 'path'; +import https from 'https'; +import http from 'http'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Konfiguration laden +const CONFIG_FILE = path.join(process.cwd(), '.openapi.config.json'); +const DEFAULT_OUTPUT_DIR = path.join(process.cwd(), 'src', 'api', 'specs'); + +/** + * Lädt die Konfiguration aus .openapi.config.json + */ +function loadConfig() { + if (!fs.existsSync(CONFIG_FILE)) { + console.error(`❌ Konfigurationsdatei nicht gefunden: ${CONFIG_FILE}`); + console.log('Erstelle eine .openapi.config.json mit folgendem Inhalt:'); + console.log(JSON.stringify({ + nexusUrl: "https://nexus.example.com", + repository: "raw-hosted", + outputDir: "src/api/specs", + apis: [ + { + name: "user-management-api", + version: "1.0.0", + filename: "user-api.yml" + } + ] + }, null, 2)); + process.exit(1); + } + + return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); +} + +/** + * Erstellt das Ausgabeverzeichnis, falls es nicht existiert + */ +function ensureOutputDir(outputDir) { + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + console.log(`📁 Verzeichnis erstellt: ${outputDir}`); + } +} + +/** + * Lädt Credentials aus Umgebungsvariablen + */ +function getCredentials() { + const username = process.env.NEXUS_USERNAME; + const password = process.env.NEXUS_PASSWORD; + + if (username && password) { + return Buffer.from(`${username}:${password}`).toString('base64'); + } + + return null; +} + +/** + * Lädt eine Datei von einer URL herunter + */ +function downloadFile(url, outputPath, authHeader) { + return new Promise((resolve, reject) => { + const protocol = url.startsWith('https') ? https : http; + const options = { + headers: {} + }; + + if (authHeader) { + options.headers['Authorization'] = `Basic ${authHeader}`; + } + + console.log(`📥 Lade herunter: ${url}`); + + const request = protocol.get(url, options, (response) => { + if (response.statusCode === 301 || response.statusCode === 302) { + // Redirect folgen + const redirectUrl = response.headers.location; + console.log(`↪️ Redirect zu: ${redirectUrl}`); + return downloadFile(redirectUrl, outputPath, authHeader) + .then(resolve) + .catch(reject); + } + + if (response.statusCode !== 200) { + reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`)); + return; + } + + const fileStream = fs.createWriteStream(outputPath); + response.pipe(fileStream); + + fileStream.on('finish', () => { + fileStream.close(); + console.log(`✅ Gespeichert: ${outputPath}`); + resolve(); + }); + + fileStream.on('error', (err) => { + fs.unlinkSync(outputPath); + reject(err); + }); + }); + + request.on('error', reject); + request.end(); + }); +} + +/** + * Hauptfunktion + */ +async function main() { + console.log('🚀 OpenAPI Downloader\n'); + + // Konfiguration laden + const config = loadConfig(); + const outputDir = path.join(process.cwd(), config.outputDir || DEFAULT_OUTPUT_DIR); + const credentials = getCredentials(); + + // Ausgabeverzeichnis erstellen + ensureOutputDir(outputDir); + + // Credentials-Info + if (credentials) { + console.log('🔐 Authentifizierung: aktiviert (via NEXUS_USERNAME/NEXUS_PASSWORD)\n'); + } else { + console.log('⚠️ Authentifizierung: keine Credentials gefunden\n'); + } + + // Alle APIs herunterladen + const results = { + success: [], + failed: [] + }; + + for (const api of config.apis) { + const url = `${config.nexusUrl}/repository/${config.repository}/${api.name}/${api.version}/${api.name}-${api.version}.yml`; + const outputPath = path.join(outputDir, api.filename || `${api.name}.yml`); + + try { + await downloadFile(url, outputPath, credentials); + results.success.push(api.name); + } catch (error) { + console.error(`❌ Fehler bei ${api.name}: ${error.message}`); + results.failed.push({ name: api.name, error: error.message }); + } + + console.log(''); // Leerzeile zwischen Downloads + } + + // Zusammenfassung + console.log('═══════════════════════════════════════'); + console.log(`✅ Erfolgreich: ${results.success.length}`); + console.log(`❌ Fehlgeschlagen: ${results.failed.length}`); + console.log('═══════════════════════════════════════'); + + if (results.failed.length > 0) { + console.log('\n❌ Fehlgeschlagene Downloads:'); + results.failed.forEach(f => { + console.log(` - ${f.name}: ${f.error}`); + }); + process.exit(1); + } + + console.log('\n🎉 Alle OpenAPI-Spezifikationen erfolgreich heruntergeladen!'); +} + +// Script ausführen +main().catch(error => { + console.error('❌ Unerwarteter Fehler:', error); + process.exit(1); +}); diff --git a/shared-library/.gitignore b/shared-library/.gitignore new file mode 100644 index 0000000..763cea9 --- /dev/null +++ b/shared-library/.gitignore @@ -0,0 +1,17 @@ +# Node.js +node_modules/ +npm-debug.log* +package-lock.json + +# Test files +*.test.js +test/ + +# IDE +.idea/ +*.iml +.vscode/ + +# OS +.DS_Store +Thumbs.db diff --git a/shared-library/README.md b/shared-library/README.md new file mode 100644 index 0000000..151c5c7 --- /dev/null +++ b/shared-library/README.md @@ -0,0 +1,264 @@ +# OpenAPI Jenkins Shared Library + +Eine optimierte Jenkins Shared Library für die Validierung und Veröffentlichung von OpenAPI-Spezifikationen in Nexus. + +## Features + +- **Sparse Git Checkout** - Lädt nur die benötigte OpenAPI-Datei für optimale Performance +- **Node.js-basierte Validierung** - YAML-Syntax und OpenAPI-Struktur-Prüfung +- **Strict-Mode** - Optionale erweiterte Validierung (deprecated Operations, Metadaten) +- **Nexus Upload** - Versionierter Upload in Raw Repositories +- **Retry-Logik** - Automatische Wiederholungsversuche bei Netzwerkfehlern +- **Reproduzierbare Builds** - Gepinnte Dependencies in `package.json` + +## Verzeichnisstruktur + +``` +shared-library/ +├── vars/ +│ ├── openapiPublish.groovy # Haupt-Pipeline +│ ├── openapiCheckout.groovy # Git Sparse Checkout +│ ├── openapiValidate.groovy # YAML/OpenAPI Validierung +│ └── openapiUploadNexus.groovy # Nexus Upload +└── resources/ + └── de/ + └── mycompany/ + └── openapi/ + ├── package.json # Node.js Dependencies + └── validate.js # Validierungsskript +``` + +## Installation + +### 1. Shared Library in Jenkins einrichten + +Gehe zu **Manage Jenkins → System → Global Pipeline Libraries** und füge hinzu: + +| Feld | Wert | +|------|------| +| **Name** | `openapi-shared-library` | +| **Default version** | `main` | +| **Retrieval method** | Modern SCM → Git | +| **Project Repository** | URL deines Library-Repos | + +### 2. Credentials in Jenkins anlegen + +Erstelle folgende Credentials in Jenkins: + +- `git-credentials` - Username/Password oder SSH für Git +- `nexus-credentials` - Username/Password für Nexus + +## Verwendung + +### Variante 1: Komplette Pipeline mit Parametern + +```groovy +@Library('openapi-shared-library') _ + +openapiPublish( + apiName: 'user-management-api', + gitRepoUrl: 'https://github.com/myorg/my-api.git', + gitBranch: 'main', + nexusUrl: 'https://nexus.example.com', + nodeImage: 'node:20-alpine', + strict: true +) +``` + +### Variante 2: Mit Jenkins Pipeline-Parametern + +```groovy +@Library('openapi-shared-library') _ + +properties([ + parameters([ + string(name: 'API_NAME', description: 'API-Name in Kebab-Case'), + string(name: 'GIT_REPO_URL', description: 'Git Repository URL'), + string(name: 'GIT_BRANCH', defaultValue: 'main', description: 'Git Branch'), + string(name: 'NEXUS_URL', defaultValue: 'https://nexus.example.com'), + booleanParam(name: 'STRICT', defaultValue: false, description: 'Strict-Mode aktivieren') + ]) +]) + +openapiPublish() +``` + +### Variante 3: Einzelne Steps verwenden + +```groovy +@Library('openapi-shared-library') _ + +node { + stage('Checkout') { + openapiCheckout( + repoUrl: 'https://github.com/myorg/my-api.git', + branch: 'main', + filePath: 'openapi.yml' + ) + } + + stage('Validate') { + openapiValidate( + file: 'openapi.yml', + nodeImage: 'node:20-alpine', + strict: true + ) + } + + stage('Upload') { + openapiUploadNexus( + file: 'openapi.yml', + apiName: 'my-api', + apiVersion: '1.0.0', + nexusUrl: 'https://nexus.example.com', + repository: 'raw-hosted' + ) + } +} +``` + +## Parameter-Referenz + +### openapiPublish + +| Parameter | Typ | Default | Beschreibung | +|-----------|-----|---------|--------------| +| `apiName` | String | (pflicht) | API-Name in kebab-case (z.B. `user-management-api`) | +| `gitRepoUrl` | String | (pflicht) | Git Repository URL | +| `gitBranch` | String | `'main'` | Git Branch | +| `gitCredentialsId` | String | `'git-credentials'` | Jenkins Credentials ID für Git | +| `nexusUrl` | String | `'https://nexus.example.com'` | Nexus Server URL | +| `nexusRepository` | String | `'raw-hosted'` | Nexus Repository Name | +| `nexusCredentialsId` | String | `'nexus-credentials'` | Jenkins Credentials ID für Nexus | +| `nodeImage` | String | `'node:20-alpine'` | Docker Image für Node.js | +| `openapiFile` | String | `'openapi.yml'` | Pfad zur OpenAPI-Datei im Repo | +| `strict` | Boolean | `false` | Strict-Mode für erweiterte Validierung | + +**Rückgabewert:** Map mit `apiName`, `apiVersion`, `artifactUrl` + +### openapiCheckout + +| Parameter | Typ | Default | Beschreibung | +|-----------|-----|---------|--------------| +| `repoUrl` | String | (pflicht) | Git Repository URL | +| `branch` | String | `'main'` | Git Branch | +| `credentialsId` | String | `'git-credentials'` | Jenkins Credentials ID | +| `filePath` | String | `'openapi.yml'` | Pfad zur Datei im Repository | + +### openapiValidate + +| Parameter | Typ | Default | Beschreibung | +|-----------|-----|---------|--------------| +| `file` | String | `'openapi.yml'` | Pfad zur lokalen OpenAPI-Datei | +| `nodeImage` | String | `'node:20-alpine'` | Docker Image für Node.js | +| `strict` | Boolean | `false` | Strict-Mode aktivieren | + +**Strict-Mode prüft:** +- Keine deprecated Operations +- `servers`-Definition vorhanden +- Contact-Email in `info.contact` definiert +- API-Beschreibung in `info.description` vorhanden + +### openapiUploadNexus + +| Parameter | Typ | Default | Beschreibung | +|-----------|-----|---------|--------------| +| `file` | String | `'openapi.yml'` | Pfad zur lokalen Datei | +| `apiName` | String | (pflicht) | API-Name | +| `apiVersion` | String | (pflicht) | API-Version | +| `nexusUrl` | String | (pflicht) | Nexus Server URL | +| `repository` | String | `'raw-hosted'` | Nexus Repository Name | +| `credentialsId` | String | `'nexus-credentials'` | Jenkins Credentials ID | + +**Rückgabewert:** String mit der Artefakt-URL + +## Optimierungen + +Diese Library implementiert folgende Optimierungen: + +### 1. Reproduzierbare Builds +- Gepinnte Versionen in `package.json` (keine `^` oder `~`) +- Verwendung von `npm ci` wenn möglich + +### 2. Performance +- Git Sparse Checkout (lädt nur benötigte Datei) +- Shallow Clone (Tiefe 1, keine Tags) +- Docker-Container-Wiederverwendung + +### 3. Robustheit +- Retry-Logik für Git und Nexus (3 Versuche) +- curl mit `--retry` für Nexus-Upload +- Explizite Fehlerbehandlung mit sprechenden Fehlermeldungen + +### 4. Validierung +- YAML-Syntax-Prüfung +- OpenAPI-Struktur-Validierung +- Optionaler Strict-Mode für Best Practices + +## Weitere Optimierungsmöglichkeiten + +### Eigenes Docker-Image für schnellere Builds + +Erstelle ein eigenes Image mit vorinstallierten Dependencies: + +```dockerfile +FROM node:20-alpine +WORKDIR /workspace +COPY package.json package-lock.json ./ +RUN npm ci --production +COPY validate.js ./ +``` + +Dann in `openapiValidate.groovy`: + +```groovy +def nodeImage = config.nodeImage ?: 'mycompany/openapi-validator:latest' +``` + +### Integration mit Spectral für erweiterte Linting + +Ergänze in `package.json`: + +```json +"dependencies": { + "js-yaml": "4.1.0", + "@apidevtools/swagger-parser": "10.1.0", + "@stoplight/spectral-cli": "6.11.0" +} +``` + +## Troubleshooting + +### "Konnte keine Version aus info.version extrahieren" + +Stelle sicher, dass deine OpenAPI-Datei eine gültige Version enthält: + +```yaml +openapi: 3.0.0 +info: + title: My API + version: 1.0.0 # Diese Zeile ist erforderlich +``` + +### "Nexus-Upload fehlgeschlagen (HTTP 401)" + +Überprüfe die Nexus-Credentials: +- Korrekte Credentials ID in Jenkins +- Benutzer hat Upload-Rechte für das Repository + +### "YAML-Syntax-Fehler" + +Validiere deine YAML-Datei lokal: + +```bash +npm install js-yaml +node -e "console.log(require('js-yaml').load(require('fs').readFileSync('openapi.yml', 'utf8')))" +``` + +## Lizenz + +Intern verwendete Library - keine externe Lizenz. + +## Kontakt + +Bei Fragen wende dich an das DevOps-Team. diff --git a/shared-library/examples/Jenkinsfile.custom b/shared-library/examples/Jenkinsfile.custom new file mode 100644 index 0000000..828c963 --- /dev/null +++ b/shared-library/examples/Jenkinsfile.custom @@ -0,0 +1,68 @@ +// Beispiel 3: Custom Pipeline mit einzelnen Steps +@Library('openapi-shared-library') _ + +def apiName = 'my-custom-api' +def apiVersion = '' +def artifactUrl = '' + +node { + try { + stage('Checkout OpenAPI') { + openapiCheckout( + repoUrl: 'https://github.com/myorg/my-api.git', + branch: 'develop', + credentialsId: 'git-credentials', + filePath: 'specs/openapi.yaml' + ) + } + + stage('Extract Version') { + // Manuelle Version-Extraktion + def data = readYaml file: 'specs/openapi.yaml' + apiVersion = data.info.version + echo "API Version: ${apiVersion}" + } + + stage('Run Validation') { + openapiValidate( + file: 'specs/openapi.yaml', + nodeImage: 'node:20-alpine', + strict: true + ) + } + + stage('Custom Processing') { + // Zusätzliche Custom-Verarbeitung + echo "Führe zusätzliche Checks aus..." + // z.B. Linting, zusätzliche Tests, etc. + } + + stage('Upload to Nexus') { + artifactUrl = openapiUploadNexus( + file: 'specs/openapi.yaml', + apiName: apiName, + apiVersion: apiVersion, + nexusUrl: 'https://nexus.example.com', + repository: 'openapi-specs', + credentialsId: 'nexus-credentials' + ) + } + + stage('Notify') { + // Custom Notification + echo """ + ======================================== + OpenAPI erfolgreich veröffentlicht! + API: ${apiName} + Version: ${apiVersion} + URL: ${artifactUrl} + ======================================== + """ + } + + } catch (Exception e) { + currentBuild.result = 'FAILURE' + echo "Pipeline fehlgeschlagen: ${e.message}" + throw e + } +} diff --git a/shared-library/examples/Jenkinsfile.example b/shared-library/examples/Jenkinsfile.example new file mode 100644 index 0000000..f7452d0 --- /dev/null +++ b/shared-library/examples/Jenkinsfile.example @@ -0,0 +1,15 @@ +// Beispiel 1: Vollständige Pipeline mit direkten Parametern +@Library('openapi-shared-library') _ + +openapiPublish( + apiName: 'user-management-api', + gitRepoUrl: 'https://github.com/myorg/my-api.git', + gitBranch: 'main', + gitCredentialsId: 'git-credentials', + nexusUrl: 'https://nexus.example.com', + nexusRepository: 'raw-hosted', + nexusCredentialsId: 'nexus-credentials', + nodeImage: 'node:20-alpine', + openapiFile: 'openapi.yml', + strict: true +) diff --git a/shared-library/examples/Jenkinsfile.params b/shared-library/examples/Jenkinsfile.params new file mode 100644 index 0000000..d7c534d --- /dev/null +++ b/shared-library/examples/Jenkinsfile.params @@ -0,0 +1,45 @@ +// Beispiel 2: Pipeline mit Jenkins-Parametern +@Library('openapi-shared-library') _ + +properties([ + parameters([ + string( + name: 'API_NAME', + description: 'API-Name in Kebab-Case (z.B. user-management-api)', + defaultValue: '' + ), + string( + name: 'GIT_REPO_URL', + description: 'Git Repository URL', + defaultValue: '' + ), + string( + name: 'GIT_BRANCH', + description: 'Git Branch', + defaultValue: 'main' + ), + string( + name: 'NEXUS_URL', + description: 'Nexus Server URL', + defaultValue: 'https://nexus.example.com' + ), + string( + name: 'NEXUS_REPOSITORY', + description: 'Nexus Repository Name', + defaultValue: 'raw-hosted' + ), + string( + name: 'OPENAPI_FILE', + description: 'Pfad zur OpenAPI-Datei im Repository', + defaultValue: 'openapi.yml' + ), + booleanParam( + name: 'STRICT', + description: 'Strict-Mode für erweiterte Validierung aktivieren', + defaultValue: false + ) + ]) +]) + +// Pipeline nutzt automatisch die Parameter +openapiPublish() diff --git a/shared-library/resources/de/mycompany/openapi/package.json b/shared-library/resources/de/mycompany/openapi/package.json new file mode 100644 index 0000000..90f522e --- /dev/null +++ b/shared-library/resources/de/mycompany/openapi/package.json @@ -0,0 +1,13 @@ +{ + "name": "openapi-validator", + "version": "1.0.0", + "description": "OpenAPI/YAML validation toolkit for Jenkins pipelines", + "private": true, + "dependencies": { + "js-yaml": "4.1.0", + "@apidevtools/swagger-parser": "10.1.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/shared-library/resources/de/mycompany/openapi/validate.js b/shared-library/resources/de/mycompany/openapi/validate.js new file mode 100644 index 0000000..26a6ea1 --- /dev/null +++ b/shared-library/resources/de/mycompany/openapi/validate.js @@ -0,0 +1,88 @@ +const fs = require('fs'); +const yaml = require('js-yaml'); +const SwaggerParser = require('@apidevtools/swagger-parser'); + +const filePath = process.argv[2] || 'openapi.yml'; +const strict = process.argv.includes('--strict'); + +async function validate() { + console.log('=== YAML-Validierung ==='); + + // 1. Prüfen ob Datei existiert + if (!fs.existsSync(filePath)) { + console.error('FEHLER: Datei nicht gefunden:', filePath); + process.exit(1); + } + + // 2. YAML-Syntax validieren + let content; + try { + const fileContent = fs.readFileSync(filePath, 'utf8'); + content = yaml.load(fileContent); + console.log('✓ YAML-Syntax ist valide'); + } catch (e) { + console.error('✗ YAML-Syntax-Fehler:', e.message); + process.exit(1); + } + + // 3. OpenAPI-Struktur validieren + console.log('\n=== OpenAPI-Validierung ==='); + try { + const api = await SwaggerParser.validate(filePath); + console.log('✓ OpenAPI-Spezifikation ist valide'); + console.log(' - Title:', api.info.title); + console.log(' - Version:', api.info.version); + console.log(' - OpenAPI-Version:', api.openapi || api.swagger); + + // 4. Strict-Mode Validierungen + if (strict) { + console.log('\n=== Strict-Mode Prüfungen ==='); + + // Prüfe auf deprecated Operationen + let hasDeprecated = false; + if (api.paths) { + for (const [path, pathItem] of Object.entries(api.paths)) { + for (const [method, operation] of Object.entries(pathItem)) { + if (typeof operation === 'object' && operation.deprecated) { + console.warn(`⚠ Deprecated Operation gefunden: ${method.toUpperCase()} ${path}`); + hasDeprecated = true; + } + } + } + } + + // Prüfe auf servers-Definition + if (!api.servers || api.servers.length === 0) { + console.warn('⚠ Keine servers-Definition gefunden'); + } + + // Prüfe auf Contact-Info + if (!api.info.contact || !api.info.contact.email) { + console.warn('⚠ Keine Contact-Email in info.contact definiert'); + } + + // Prüfe auf Description + if (!api.info.description) { + console.warn('⚠ Keine API-Beschreibung (info.description) vorhanden'); + } + + if (hasDeprecated) { + console.log('\n✗ Strict-Mode: Deprecated Operationen sind nicht erlaubt'); + process.exit(2); + } + + console.log('✓ Strict-Mode Prüfungen bestanden'); + } + + } catch (e) { + console.error('✗ OpenAPI-Validierungsfehler:', e.message); + process.exit(1); + } + + console.log('\n=== Validierung erfolgreich abgeschlossen ==='); +} + +validate().catch(err => { + console.error('Unerwarteter Fehler:', err); + process.exit(1); +}); diff --git a/shared-library/vars/openapiCheckout.groovy b/shared-library/vars/openapiCheckout.groovy new file mode 100644 index 0000000..c60be15 --- /dev/null +++ b/shared-library/vars/openapiCheckout.groovy @@ -0,0 +1,51 @@ +#!/usr/bin/env groovy + +/** + * OpenAPI Sparse Checkout + * + * Checkt nur die benötigte OpenAPI-Datei aus dem Repository aus + * mittels Git Sparse Checkout für optimale Performance. + * + * @param config Map mit folgenden Parametern: + * - repoUrl: Git Repository URL (pflicht) + * - branch: Git Branch (default: 'main') + * - credentialsId: Jenkins Credentials ID (default: 'git-credentials') + * - filePath: Pfad zur Datei im Repo (default: 'openapi.yml') + */ +def call(Map config = [:]) { + def repoUrl = config.repoUrl + def branch = config.branch ?: 'main' + def credentialsId = config.credentialsId ?: 'git-credentials' + def filePath = config.filePath ?: 'openapi.yml' + + if (!repoUrl) { + error "repoUrl ist erforderlich für openapiCheckout" + } + + echo "Starte Sparse Checkout: ${filePath} aus ${repoUrl} (Branch: ${branch})" + + // Workspace bereinigen vor Checkout + deleteDir() + + checkout([ + $class: 'GitSCM', + branches: [[name: "*/${branch}"]], + extensions: [ + [$class: 'SparseCheckoutPaths', sparseCheckoutPaths: [ + [$class: 'SparseCheckoutPath', path: filePath] + ]], + [$class: 'CloneOption', depth: 1, shallow: true, noTags: true], + [$class: 'CleanBeforeCheckout'] + ], + userRemoteConfigs: [[ + url: repoUrl, + credentialsId: credentialsId + ]] + ]) + + if (!fileExists(filePath)) { + error "${filePath} wurde im Repository nicht gefunden" + } + + echo "✓ Datei ${filePath} erfolgreich ausgecheckt" +} diff --git a/shared-library/vars/openapiPublish.groovy b/shared-library/vars/openapiPublish.groovy new file mode 100644 index 0000000..f36d220 --- /dev/null +++ b/shared-library/vars/openapiPublish.groovy @@ -0,0 +1,144 @@ +#!/usr/bin/env groovy + +/** + * OpenAPI Publish Pipeline + * + * Haupteinstiegspunkt für die OpenAPI-Publishing-Pipeline. + * Koordiniert Checkout, Validierung und Upload nach Nexus. + * + * @param config Map mit folgenden optionalen Parametern: + * - apiName: Name der API in kebab-case (pflicht) + * - gitRepoUrl: Git Repository URL (pflicht) + * - gitBranch: Git Branch (default: 'main') + * - gitCredentialsId: Jenkins Credentials ID für Git (default: 'git-credentials') + * - nexusUrl: Nexus URL (default: 'https://nexus.example.com') + * - nexusRepository: Nexus Repository Name (default: 'raw-hosted') + * - nexusCredentialsId: Jenkins Credentials ID für Nexus (default: 'nexus-credentials') + * - nodeImage: Docker Image für Node.js (default: 'node:20-alpine') + * - openapiFile: Pfad zur OpenAPI-Datei (default: 'openapi.yml') + * - strict: Strict-Mode für Validierung (default: false) + * + * @return Map mit apiName, apiVersion und artifactUrl + */ +def call(Map config = [:]) { + // Parameter aus config oder Pipeline-Parametern + def apiName = config.apiName ?: params.API_NAME + def gitRepoUrl = config.gitRepoUrl ?: params.GIT_REPO_URL + def gitBranch = config.gitBranch ?: params.GIT_BRANCH ?: 'main' + def gitCredentialsId = config.gitCredentialsId ?: params.GIT_CREDENTIALS_ID ?: 'git-credentials' + def nexusUrl = config.nexusUrl ?: params.NEXUS_URL ?: 'https://nexus.example.com' + def nexusRepository = config.nexusRepository ?: params.NEXUS_REPOSITORY ?: 'raw-hosted' + def nexusCredentialsId = config.nexusCredentialsId ?: params.NEXUS_CREDENTIALS_ID ?: 'nexus-credentials' + def nodeImage = config.nodeImage ?: params.NODE_IMAGE ?: 'node:20-alpine' + def openapiFile = config.openapiFile ?: params.OPENAPI_FILE ?: 'openapi.yml' + def strict = config.strict ?: params.STRICT ?: false + + // Validierung + validateParameters(apiName, gitRepoUrl) + + def apiVersion = '' + def artifactUrl = '' + + node { + try { + stage('OpenAPI auschecken') { + retry(3) { + openapiCheckout( + repoUrl: gitRepoUrl, + branch: gitBranch, + credentialsId: gitCredentialsId, + filePath: openapiFile + ) + } + } + + stage('Version auslesen') { + apiVersion = extractVersion(openapiFile) + echo "API: ${apiName}, Version: ${apiVersion}" + } + + stage('YAML validieren') { + openapiValidate( + file: openapiFile, + nodeImage: nodeImage, + strict: strict + ) + } + + stage('Nach Nexus hochladen') { + retry(3) { + artifactUrl = openapiUploadNexus( + file: openapiFile, + apiName: apiName, + apiVersion: apiVersion, + nexusUrl: nexusUrl, + repository: nexusRepository, + credentialsId: nexusCredentialsId + ) + } + } + + stage('Zusammenfassung') { + printSummary(apiName, apiVersion, artifactUrl) + } + + } finally { + // Cleanup / Logging auch bei Fehlern + echo "Pipeline beendet. API=${apiName}, Version=${apiVersion ?: 'unbekannt'}, Artifact=${artifactUrl ?: 'kein Upload'}" + } + } + + return [ + apiName: apiName, + apiVersion: apiVersion, + artifactUrl: artifactUrl + ] +} + +/** + * Validiert die Pflichtparameter + */ +private void validateParameters(String apiName, String gitRepoUrl) { + if (!apiName?.trim()) { + error "apiName darf nicht leer sein" + } + + if (!apiName.matches('^[a-z0-9]+(-[a-z0-9]+)*$')) { + error "apiName muss in Kebab-Case sein (z.B. my-api-name). Erhalten: '${apiName}'" + } + + if (!gitRepoUrl?.trim()) { + error "gitRepoUrl darf nicht leer sein" + } +} + +/** + * Extrahiert die Version aus der OpenAPI-Datei mittels readYaml + */ +private String extractVersion(String openapiFile) { + def data = readYaml file: openapiFile + + def version = data?.info?.version + if (!version) { + error "Konnte keine Version aus info.version in ${openapiFile} extrahieren" + } + + return version.toString() +} + +/** + * Gibt eine formatierte Zusammenfassung aus + */ +private void printSummary(String apiName, String apiVersion, String artifactUrl) { + echo """ + ╔═══════════════════════════════════════════════════════╗ + ║ Pipeline erfolgreich abgeschlossen ║ + ╠═══════════════════════════════════════════════════════╣ + ║ API-Name: ${apiName} + ║ Version: ${apiVersion} + ╠═══════════════════════════════════════════════════════╣ + ║ Artefakt: + ║ ${artifactUrl} + ╚═══════════════════════════════════════════════════════╝ + """.stripIndent() +} diff --git a/shared-library/vars/openapiUploadNexus.groovy b/shared-library/vars/openapiUploadNexus.groovy new file mode 100644 index 0000000..6141874 --- /dev/null +++ b/shared-library/vars/openapiUploadNexus.groovy @@ -0,0 +1,71 @@ +#!/usr/bin/env groovy + +/** + * OpenAPI Nexus Upload + * + * Lädt die validierte OpenAPI-Datei in ein Nexus Raw Repository hoch. + * Unterstützt automatisches Retry bei Netzwerkfehlern. + * + * @param config Map mit folgenden Parametern: + * - file: Pfad zur lokalen Datei (default: 'openapi.yml') + * - apiName: Name der API (pflicht) + * - apiVersion: Version der API (pflicht) + * - nexusUrl: Nexus URL (pflicht) + * - repository: Nexus Repository Name (default: 'raw-hosted') + * - credentialsId: Jenkins Credentials ID (default: 'nexus-credentials') + * + * @return String mit der vollständigen Artefakt-URL + */ +def call(Map config = [:]) { + def file = config.file ?: 'openapi.yml' + def apiName = config.apiName + def apiVersion = config.apiVersion + def nexusUrl = config.nexusUrl + def repository = config.repository ?: 'raw-hosted' + def credentialsId = config.credentialsId ?: 'nexus-credentials' + + // Validierung + if (!apiName) { + error "apiName ist erforderlich für openapiUploadNexus" + } + if (!apiVersion) { + error "apiVersion ist erforderlich für openapiUploadNexus" + } + if (!nexusUrl) { + error "nexusUrl ist erforderlich für openapiUploadNexus" + } + + // Artefakt-Pfad mit aussagekräftigem Dateinamen + def filename = "${apiName}-${apiVersion}.yml" + def artifactPath = "${apiName}/${apiVersion}/${filename}" + def uploadUrl = "${nexusUrl}/repository/${repository}/${artifactPath}" + + echo "Starte Upload nach Nexus: ${uploadUrl}" + + withCredentials([usernamePassword( + credentialsId: credentialsId, + usernameVariable: 'NEXUS_USER', + passwordVariable: 'NEXUS_PASS' + )]) { + def httpStatus = sh( + script: """ + curl --silent --fail --show-error \\ + --retry 3 --retry-delay 5 \\ + -u "\${NEXUS_USER}:\${NEXUS_PASS}" \\ + --upload-file '${file}' \\ + '${uploadUrl}' \\ + -w '%{http_code}' \\ + -o /dev/null + """, + returnStdout: true + ).trim() + + if (httpStatus.toInteger() >= 400) { + error "Nexus-Upload fehlgeschlagen (HTTP ${httpStatus})" + } + + echo "✓ Upload erfolgreich: ${uploadUrl}" + } + + return uploadUrl +} diff --git a/shared-library/vars/openapiValidate.groovy b/shared-library/vars/openapiValidate.groovy new file mode 100644 index 0000000..824630b --- /dev/null +++ b/shared-library/vars/openapiValidate.groovy @@ -0,0 +1,49 @@ +#!/usr/bin/env groovy + +/** + * OpenAPI Validation + * + * Validiert YAML-Syntax und OpenAPI-Spezifikation mittels Node.js. + * Nutzt package.json und validate.js aus den Library Resources. + * + * @param config Map mit folgenden Parametern: + * - file: Pfad zur OpenAPI-Datei (default: 'openapi.yml') + * - nodeImage: Docker Image für Node.js (default: 'node:20-alpine') + * - strict: Strict-Mode aktivieren (default: false) + */ +def call(Map config = [:]) { + def file = config.file ?: 'openapi.yml' + def nodeImage = config.nodeImage ?: 'node:20-alpine' + def strict = config.strict ?: false + + echo "Starte Validierung von ${file} (Strict-Mode: ${strict})" + + docker.image(nodeImage).inside { + // Ressourcen aus der Shared Library ins Workspace schreiben + writeFile file: 'package.json', text: libraryResource('de/mycompany/openapi/package.json') + writeFile file: 'validate.js', text: libraryResource('de/mycompany/openapi/validate.js') + + // Dependencies installieren (npm ci bevorzugt, fallback zu npm install) + sh ''' + echo "Installiere Abhängigkeiten..." + npm ci --silent --no-progress 2>/dev/null || npm install --silent --no-progress 2>/dev/null + ''' + + // Validierung ausführen + def strictFlag = strict ? '--strict' : '' + def exitCode = sh( + script: "node validate.js '${file}' ${strictFlag}", + returnStatus: true + ) + + if (exitCode == 1) { + error "Validierung fehlgeschlagen: YAML-Syntax oder OpenAPI-Strukturfehler" + } else if (exitCode == 2) { + error "Strict-Mode Validierung fehlgeschlagen: Deprecated Operationen oder fehlende Metadaten" + } else if (exitCode != 0) { + error "Validierung fehlgeschlagen mit Exit-Code ${exitCode}" + } + } + + echo "✓ Validierung für ${file} erfolgreich" +}