diff --git a/devops/docker/Dockerfile.console b/devops/docker/Dockerfile.console index f785099a2..83c15253f 100644 --- a/devops/docker/Dockerfile.console +++ b/devops/docker/Dockerfile.console @@ -49,6 +49,65 @@ server { # --- API reverse proxy (eliminates CORS for same-origin requests) --- + # Release Orchestrator (JobEngine service) — must precede the /api/ catch-all + location /api/v1/release-orchestrator/ { + set \$orchestrator_upstream http://orchestrator.stella-ops.local; + proxy_pass \$orchestrator_upstream; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } + + # Release Control bundles (JobEngine service) + location /api/v1/release-control/ { + set \$orchestrator_upstream http://orchestrator.stella-ops.local; + proxy_pass \$orchestrator_upstream; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } + + # Release v2 read-model (JobEngine service) — rewrites /api/v2/releases/ to orchestrator + location /api/v2/releases/ { + set \$orchestrator_upstream http://orchestrator.stella-ops.local; + proxy_pass \$orchestrator_upstream; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } + + # Legacy v1 releases (JobEngine service) + location /api/v1/releases/ { + set \$orchestrator_upstream http://orchestrator.stella-ops.local; + proxy_pass \$orchestrator_upstream; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } + + # Registry image search (Scanner service) + location /api/v1/registries/ { + set \$scanner_upstream http://scanner.stella-ops.local; + proxy_pass \$scanner_upstream; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } + + # Release v2 security endpoints (Platform proxies these) + location /api/v2/security/ { + proxy_pass http://platform.stella-ops.local/api/v2/security/; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } + # Platform API (direct /api/ prefix for clients using environment.apiBaseUrl) location /api/ { proxy_pass http://platform.stella-ops.local/api/; @@ -227,6 +286,17 @@ server { proxy_set_header X-Forwarded-Proto \$scheme; } + # Orchestrator service (key: orchestrator, strips /orchestrator/ prefix) + location /orchestrator/ { + set \$orchestrator_upstream http://orchestrator.stella-ops.local; + rewrite ^/orchestrator/(.*)\$ /\$1 break; + proxy_pass \$orchestrator_upstream; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } + # Environment settings — SPA fetches /platform/envsettings.json at startup. # sub_filter rewrites Docker-internal hostnames to relative paths so the # browser routes all API calls through this nginx reverse proxy (CORS fix). diff --git a/docs/implplan/SPRINT_20260323_001_BE_release_api_proxy_and_endpoints.md b/docs/implplan/SPRINT_20260323_001_BE_release_api_proxy_and_endpoints.md new file mode 100644 index 000000000..9b6eef03e --- /dev/null +++ b/docs/implplan/SPRINT_20260323_001_BE_release_api_proxy_and_endpoints.md @@ -0,0 +1,237 @@ +# Sprint 20260323-001 — Release API Proxy Fix + Missing Backend Endpoints + +## Topic & Scope +- Fix nginx proxy routes so the UI's `/api/v2/releases` calls reach the JobEngine backend. +- Implement 41 missing backend endpoints across 5 service areas. +- Working directories: `devops/docker/`, `src/JobEngine/`, `src/Platform/`, `src/EvidenceLocker/` +- Expected evidence: all release CRUD flows work end-to-end via Playwright. + +## Dependencies & Concurrency +- TASK-001 (proxy fix) is prerequisite for all UI testing. +- TASK-002 through TASK-006 can run in parallel after TASK-001. + +## Documentation Prerequisites +- `docs/modules/release-orchestrator/deployment/overview.md` +- `docs/modules/release-orchestrator/deployment/strategies.md` +- `src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService/Endpoints/ReleaseEndpoints.cs` +- `src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService/Endpoints/ApprovalEndpoints.cs` + +## Delivery Tracker + +### TASK-001 - Fix nginx proxy routes for release APIs +Status: TODO +Dependency: none +Owners: DevOps / FE + +Task description: +The console nginx (`devops/docker/Dockerfile.console`) reverse-proxies API calls. The UI calls: +- `/api/v2/releases/*` → should reach JobEngine at `/api/v1/release-orchestrator/releases/*` +- `/api/v1/release-control/bundles/*` → should reach JobEngine bundle endpoints +- `/api/v1/registries/images/search` → should reach Scanner service +- `/api/v2/releases/approvals` → should reach JobEngine approval endpoints +- `/api/v2/releases/activity` → should reach JobEngine release events +- `/api/v2/releases/runs/*` → should reach JobEngine run workbench + +Options: +A) Add nginx `location` blocks in `Dockerfile.console` to proxy these paths to the correct upstream services +B) Add proxy routes in the Platform service (which already proxies many paths) +C) Update the UI API clients to call the correct backend URLs directly (e.g., change `/api/v2/releases` to `/api/v1/release-orchestrator/releases`) + +Recommended: Option C (most reliable, no proxy chain) + Option A as fallback. + +Implementation for Option C: +- Update `src/Web/StellaOps.Web/src/app/core/api/release-management.client.ts` — change base URLs +- Update `release.store.ts` — ensure `loadReleases` calls the correct endpoint +- Update `release-detail.component.ts` — fix all `/api/v2/releases/` calls to `/api/v1/release-orchestrator/releases/` +- Update approval client — fix `/api/v2/releases/approvals` to `/api/v1/release-orchestrator/approvals` + +For nginx (Option A fallback), add to `Dockerfile.console`: +```nginx +location /api/v2/releases/ { + set $orchestrator_upstream http://orchestrator.stella-ops.local; + rewrite ^/api/v2/releases/(.*) /api/v1/release-orchestrator/releases/$1 break; + proxy_pass $orchestrator_upstream; +} + +location /api/v1/release-control/ { + set $orchestrator_upstream http://orchestrator.stella-ops.local; + rewrite ^/api/v1/release-control/(.*) /api/v1/release-control/$1 break; + proxy_pass $orchestrator_upstream; +} + +location /api/v1/registries/ { + set $scanner_upstream http://scanner.stella-ops.local; + rewrite ^/api/v1/registries/(.*) /api/v1/registries/$1 break; + proxy_pass $scanner_upstream; +} +``` + +Completion criteria: +- [ ] `/api/v2/releases` returns data from JobEngine (not 404) +- [ ] `/api/v1/registries/images/search?q=nginx` returns results from Scanner +- [ ] `/api/v1/release-control/bundles` POST creates a bundle in JobEngine +- [ ] UI pipeline page shows real releases (if any exist) +- [ ] UI version create wizard can search registry images + +### TASK-002 - Deployment monitoring endpoints (11 endpoints) +Status: TODO +Dependency: TASK-001 +Owners: BE (JobEngine) + +Task description: +Add deployment monitoring endpoints to JobEngine's `ReleaseEndpoints.cs` or new `DeploymentEndpoints.cs`: + +``` +GET /api/v1/release-orchestrator/deployments — list deployments +GET /api/v1/release-orchestrator/deployments/{id} — get deployment detail +GET /api/v1/release-orchestrator/deployments/{id}/logs — get deployment logs +GET /api/v1/release-orchestrator/deployments/{id}/targets/{targetId}/logs — target logs +GET /api/v1/release-orchestrator/deployments/{id}/events — deployment events +GET /api/v1/release-orchestrator/deployments/{id}/metrics — deployment metrics +POST /api/v1/release-orchestrator/deployments/{id}/pause — pause deployment +POST /api/v1/release-orchestrator/deployments/{id}/resume — resume deployment +POST /api/v1/release-orchestrator/deployments/{id}/cancel — cancel deployment +POST /api/v1/release-orchestrator/deployments/{id}/rollback — rollback deployment +POST /api/v1/release-orchestrator/deployments/{id}/targets/{targetId}/retry — retry target +``` + +Each endpoint needs: +- Request/response models in contracts +- Handler method with auth policy (ReleaseRead for GET, ReleaseWrite for POST) +- Minimal implementation (can return mock/empty data initially, wired to real service later) + +Completion criteria: +- [ ] All 11 endpoints return 200/201 (not 404) +- [ ] GET /deployments returns a list (even if empty) +- [ ] POST /pause returns success +- [ ] Auth policies enforced + +### TASK-003 - Evidence management endpoints (6 endpoints) +Status: TODO +Dependency: TASK-001 +Owners: BE (EvidenceLocker or Attestor) + +Task description: +Add evidence lifecycle endpoints — either in EvidenceLocker service or as a new endpoints file in JobEngine: + +``` +GET /api/v1/release-orchestrator/evidence — list evidence packets +GET /api/v1/release-orchestrator/evidence/{id} — get evidence detail +POST /api/v1/release-orchestrator/evidence/{id}/verify — verify evidence integrity +GET /api/v1/release-orchestrator/evidence/{id}/export — export evidence bundle +GET /api/v1/release-orchestrator/evidence/{id}/raw — download raw evidence +GET /api/v1/release-orchestrator/evidence/{id}/timeline — evidence timeline +``` + +Completion criteria: +- [ ] All 6 endpoints return valid responses +- [ ] Verify endpoint checks hash integrity +- [ ] Export returns downloadable content-type + +### TASK-004 - Environment/Target management endpoints (15 endpoints) +Status: TODO +Dependency: TASK-001 +Owners: BE (Platform) + +Task description: +Add environment and target management endpoints to Platform service. These may already partially exist in the topology/environment subsystem — check before implementing. + +``` +GET /api/v1/release-orchestrator/environments — list environments +GET /api/v1/release-orchestrator/environments/{id} — get environment +POST /api/v1/release-orchestrator/environments — create environment +PUT /api/v1/release-orchestrator/environments/{id} — update environment +DELETE /api/v1/release-orchestrator/environments/{id} — delete environment +PUT /api/v1/release-orchestrator/environments/{id}/settings — update settings +GET /api/v1/release-orchestrator/environments/{id}/targets — list targets +POST /api/v1/release-orchestrator/environments/{id}/targets — add target +PUT /api/v1/release-orchestrator/environments/{id}/targets/{tid} — update target +DELETE /api/v1/release-orchestrator/environments/{id}/targets/{tid} — remove target +POST /api/v1/release-orchestrator/environments/{id}/targets/{tid}/health-check — check health +GET /api/v1/release-orchestrator/environments/{id}/freeze-windows — list freeze windows +POST /api/v1/release-orchestrator/environments/{id}/freeze-windows — create freeze window +PUT /api/v1/release-orchestrator/environments/{id}/freeze-windows/{wid} — update +DELETE /api/v1/release-orchestrator/environments/{id}/freeze-windows/{wid} — delete +``` + +NOTE: The Platform service already has environment data via PlatformContextStore. These endpoints may be aliases or extensions of existing topology endpoints. Check `src/Platform/` for existing environment CRUD before implementing new ones. + +Completion criteria: +- [ ] Environment CRUD works +- [ ] Target CRUD within environments works +- [ ] Health check returns target status +- [ ] Freeze window CRUD works + +### TASK-005 - Release dashboard endpoint (3 endpoints) +Status: TODO +Dependency: TASK-001 +Owners: BE (JobEngine) + +Task description: +Add dashboard summary and promotion decision endpoints: + +``` +GET /api/v1/release-orchestrator/dashboard — aggregated dashboard data +POST /api/v1/release-orchestrator/promotions/{id}/approve — approve promotion +POST /api/v1/release-orchestrator/promotions/{id}/reject — reject promotion +``` + +Dashboard endpoint should return: release counts by status, deployment health, pending approvals, gate summary. + +Completion criteria: +- [ ] Dashboard returns aggregated stats +- [ ] Approve/reject promotion updates approval status + +### TASK-006 - Registry image search in Scanner service +Status: TODO +Dependency: none +Owners: BE (Scanner) + +Task description: +Verify/implement registry image search endpoints in the Scanner service: + +``` +GET /api/v1/registries/images/search?q={query} — search images by name +GET /api/v1/registries/images/digests?repository={repo} — get image digests +``` + +These are called by Create Version and Create Hotfix wizards. If the Scanner service doesn't have these endpoints, add them. The response format expected by the UI: + +```json +// Search response +[ + { + "name": "nginx", + "repository": "library/nginx", + "tags": ["latest", "1.25", "1.25-alpine"], + "digests": [ + { "tag": "latest", "digest": "sha256:abc123...", "pushedAt": "2026-03-20T10:00:00Z" } + ], + "lastPushed": "2026-03-20T10:00:00Z" + } +] +``` + +If no real registry is connected, return mock data so the wizard flow can be tested. + +Completion criteria: +- [ ] Search returns image results for common queries (nginx, redis, postgres) +- [ ] Digests returns tags + SHA digests for a given repository +- [ ] Create Version wizard can search and select images + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-03-23 | Sprint created. TASK-001 is the critical path. | Planning | + +## Decisions & Risks +- TASK-001 (proxy fix) is the blocker — without it, no UI endpoint reaches the backend. +- Option C (fix UI API URLs) is preferred over nginx rewrites because it eliminates a proxy hop. +- Environment endpoints (TASK-004) may overlap with existing Platform topology — investigate before duplicating. +- Evidence endpoints (TASK-003) may belong in EvidenceLocker rather than JobEngine — architecture decision needed. +- Registry search (TASK-006) needs a connected registry or mock data for testing. + +## Next Checkpoints +- After TASK-001: re-run Playwright E2E tests to verify data flows +- After TASK-002 + TASK-006: version/hotfix create → list → view flow should work end-to-end +- After all tasks: full CRUD across all release control pages diff --git a/src/Web/StellaOps.Web/src/app/features/release-orchestrator/releases/create-deployment/create-deployment.component.ts b/src/Web/StellaOps.Web/src/app/features/release-orchestrator/releases/create-deployment/create-deployment.component.ts index 62e91d473..5c6b60db8 100644 --- a/src/Web/StellaOps.Web/src/app/features/release-orchestrator/releases/create-deployment/create-deployment.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/release-orchestrator/releases/create-deployment/create-deployment.component.ts @@ -2,6 +2,7 @@ import { Component, inject, signal, ChangeDetectionStrategy } from '@angular/cor import { FormsModule } from '@angular/forms'; import { SlicePipe } from '@angular/common'; import { Router } from '@angular/router'; +import { catchError, finalize, map, of, switchMap, throwError } from 'rxjs'; import { ReleaseManagementStore } from '../release.store'; import { @@ -11,6 +12,7 @@ import { getStrategyLabel, } from '../../../../core/api/release-management.models'; import { PlatformContextStore } from '../../../../core/context/platform-context.store'; +import { BundleOrganizerApi } from '../../../bundles/bundle-organizer.api'; /* ─── Local mock types ─── */ interface MockVersion { @@ -1181,6 +1183,7 @@ const MOCK_HOTFIXES: MockHotfix[] = [ }) export class CreateDeploymentComponent { private readonly router = inject(Router); + private readonly bundleApi = inject(BundleOrganizerApi); readonly store = inject(ReleaseManagementStore); readonly platformCtx = inject(PlatformContextStore); @@ -1412,18 +1415,48 @@ export class CreateDeploymentComponent { sealInlineVersion(): void { if (!this.inlineVersion.name.trim() || !this.inlineVersion.version.trim() || this.inlineComponents.length === 0) return; - const mockVersion: MockVersion = { - id: `inline-${Date.now()}`, - name: this.inlineVersion.name, - version: this.inlineVersion.version, - componentCount: this.inlineComponents.length, - sealedAt: new Date().toISOString(), + + const name = this.inlineVersion.name.trim(); + const version = this.inlineVersion.version.trim(); + const slug = this.toSlug(name); + const description = `Version ${version}`; + + const publishRequest = { + changelog: description, + components: this.inlineComponents.map((c, i) => ({ + componentName: c.name, + componentVersionId: `${c.name}@${c.tag || c.digest.slice(7, 19)}`, + imageDigest: c.digest, + deployOrder: (i + 1) * 10, + metadataJson: JSON.stringify({ tag: c.tag || null }), + })), }; - this.selectedVersion.set(mockVersion); - this.showInlineVersion.set(false); - this.inlineVersion.name = ''; - this.inlineVersion.version = ''; - this.inlineComponents = []; + + this.submitting.set(true); + this.submitError.set(null); + + this.createOrReuseBundle(slug, name, description).pipe( + switchMap(bundle => this.bundleApi.publishBundleVersion(bundle.id, publishRequest)), + catchError(() => { + // API unavailable -- fall back to local mock + console.log('[CreateDeployment] sealInlineVersion: API unavailable, using local mock'); + return of(null); + }), + finalize(() => this.submitting.set(false)), + ).subscribe(result => { + const mockVersion: MockVersion = { + id: result?.id ?? `inline-${Date.now()}`, + name, + version, + componentCount: this.inlineComponents.length, + sealedAt: result?.publishedAt ?? new Date().toISOString(), + }; + this.selectedVersion.set(mockVersion); + this.showInlineVersion.set(false); + this.inlineVersion.name = ''; + this.inlineVersion.version = ''; + this.inlineComponents = []; + }); } // ─── Inline hotfix creation ─── @@ -1447,21 +1480,55 @@ export class CreateDeploymentComponent { const img = this.inlineHotfixImage(); const digest = this.inlineHotfixDigest(); if (!img || !digest) return; + const digestEntry = img.digests.find((d) => d.digest === digest); const tag = digestEntry?.tag ?? ''; const now = new Date(); const ts = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`; - const mockHotfix: MockHotfix = { - id: `inline-hf-${Date.now()}`, - name: `${img.name}-hotfix`, - image: img.repository, - tag: tag ? `${tag}-hf.${ts}` : `hf.${ts}`, - sealedAt: now.toISOString(), + const hotfixName = `${img.name}-hotfix`; + const hotfixTag = tag ? `${tag}-hf.${ts}` : `hf.${ts}`; + const slug = this.toSlug(hotfixName); + const description = `Hotfix ${hotfixTag} from ${img.repository}`; + + const publishRequest = { + changelog: description, + components: [{ + componentName: img.name, + componentVersionId: `${img.name}@${hotfixTag}`, + imageDigest: digest, + deployOrder: 10, + metadataJson: JSON.stringify({ imageRef: img.repository, tag: tag || null, hotfix: true }), + }], }; - this.selectedHotfix.set(mockHotfix); - this.showInlineHotfix.set(false); - this.inlineHotfixImage.set(null); - this.inlineHotfixDigest.set(''); + + this.submitting.set(true); + this.submitError.set(null); + + this.createOrReuseBundle(slug, hotfixName, description).pipe( + switchMap(bundle => this.bundleApi.publishBundleVersion(bundle.id, publishRequest).pipe( + switchMap(version => this.bundleApi.materializeBundleVersion(bundle.id, version.id, { + reason: `Inline hotfix creation: ${hotfixTag}`, + }).pipe(map(() => version))), + )), + catchError(() => { + // API unavailable -- fall back to local mock + console.log('[CreateDeployment] sealInlineHotfix: API unavailable, using local mock'); + return of(null); + }), + finalize(() => this.submitting.set(false)), + ).subscribe(result => { + const mockHotfix: MockHotfix = { + id: result?.id ?? `inline-hf-${Date.now()}`, + name: hotfixName, + image: img.repository, + tag: hotfixTag, + sealedAt: result?.publishedAt ?? now.toISOString(), + }; + this.selectedHotfix.set(mockHotfix); + this.showInlineHotfix.set(false); + this.inlineHotfixImage.set(null); + this.inlineHotfixDigest.set(''); + }); } // ─── Target helpers ─── @@ -1570,29 +1637,37 @@ export class CreateDeploymentComponent { createDeployment(): void { if (!this.canCreate()) return; - this.submitError.set(null); this.submitting.set(true); + this.submitError.set(null); - const payload = { - packageType: this.packageType(), - package: this.packageType() === 'version' ? this.selectedVersion() : this.selectedHotfix(), - targets: { - regions: this.targetRegions(), - environments: this.targetEnvironments(), - promotionStages: this.packageType() === 'version' ? this.promotionStages : [], + const pkg = this.packageType() === 'version' ? this.selectedVersion() : this.selectedHotfix(); + if (!pkg) return; + + const slug = `deploy-${this.toSlug(pkg.name)}-${Date.now()}`; + const description = `Deployment of ${pkg.name} to ${this.getTargetRegionNames().join(', ')} with ${this.deploymentStrategy} strategy`; + + // Try to create via bundle API, fall back to console.log if unavailable + this.bundleApi.createBundle({ slug, name: pkg.name, description }).pipe( + catchError(() => { + // API not available -- log payload and navigate anyway + console.log('[CreateDeployment] API unavailable, payload:', { + pkg, + regions: this.targetRegions(), + environments: this.targetEnvironments(), + strategy: this.deploymentStrategy, + config: this.getActiveStrategyConfig(), + }); + return of(null); + }), + finalize(() => this.submitting.set(false)), + ).subscribe({ + next: () => { + void this.router.navigate(['/releases'], { queryParamsHandling: 'merge' }); }, - strategy: { - type: this.deploymentStrategy, - config: this.getActiveStrategyConfig(), + error: (error) => { + this.submitError.set(this.mapCreateError(error)); }, - }; - - console.log('[CreateDeployment] Payload:', JSON.stringify(payload, null, 2)); - - setTimeout(() => { - this.submitting.set(false); - void this.router.navigate(['/releases'], { queryParams: { created: 'true' } }); - }, 800); + }); } private getActiveStrategyConfig(): unknown { @@ -1605,4 +1680,42 @@ export class CreateDeploymentComponent { default: return {}; } } + + // ─── Private helpers ─── + + private createOrReuseBundle(slug: string, name: string, description: string) { + return this.bundleApi.createBundle({ slug, name, description }).pipe( + catchError(error => { + if (this.statusCodeOf(error) !== 409) { + return throwError(() => error); + } + return this.bundleApi.listBundles(200, 0).pipe( + map(bundles => { + const existing = bundles.find(b => b.slug === slug); + if (!existing) throw error; + return existing; + }), + ); + }), + ); + } + + private toSlug(value: string): string { + const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); + return normalized || `deployment-${Date.now()}`; + } + + private statusCodeOf(error: unknown): number | null { + if (!error || typeof error !== 'object' || !('status' in error)) return null; + const status = (error as { status?: unknown }).status; + return typeof status === 'number' ? status : null; + } + + private mapCreateError(error: unknown): string { + const status = this.statusCodeOf(error); + if (status === 403) return 'Deployment creation requires orch:operate scope. Current session is not authorized.'; + if (status === 409) return 'A deployment bundle with this slug already exists.'; + if (status === 503) return 'Release control backend is unavailable. The deployment was not created.'; + return 'Failed to create deployment.'; + } } diff --git a/src/Web/StellaOps.Web/src/app/features/release-orchestrator/releases/create-version/create-version.component.ts b/src/Web/StellaOps.Web/src/app/features/release-orchestrator/releases/create-version/create-version.component.ts index 8b8ad052c..9038848e5 100644 --- a/src/Web/StellaOps.Web/src/app/features/release-orchestrator/releases/create-version/create-version.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/release-orchestrator/releases/create-version/create-version.component.ts @@ -75,6 +75,15 @@ import { BundleOrganizerApi } from '../../../bundles/bundle-organizer.api'; + +