diff --git a/docs/implplan/SPRINT_20260310_025_FE_releases_deployment_evidence_scope_preservation.md b/docs/implplan/SPRINT_20260310_025_FE_releases_deployment_evidence_scope_preservation.md new file mode 100644 index 000000000..cb3f53b40 --- /dev/null +++ b/docs/implplan/SPRINT_20260310_025_FE_releases_deployment_evidence_scope_preservation.md @@ -0,0 +1,47 @@ +# Sprint 20260310-025 - FE Releases Deployment Evidence Scope Preservation + +## Topic & Scope +- Preserve active tenant/region/environment/time-window scope through deployment-detail evidence and replay actions so release operators do not fall into stale evidence context after drilling into a deployment. +- Tighten the existing live releases deployment Playwright harness so this class of scope regression fails explicitly in future scratch iterations. +- Working directory: `src/Web/StellaOps.Web`. +- Allowed coordination edits: `docs/implplan/SPRINT_20260310_025_FE_releases_deployment_evidence_scope_preservation.md`. +- Expected evidence: focused Angular scope tests and a live releases deployment Playwright run with scoped evidence/replay links. + +## Dependencies & Concurrency +- Depends on the authenticated local stack being reachable through `https://stella-ops.local`. +- Safe parallelism: avoid unrelated search or topology edits while this release-detail iteration is in progress. + +## Documentation Prerequisites +- `AGENTS.md` +- `docs/qa/feature-checks/FLOW.md` +- `docs/implplan/SPRINT_20260308_026_FE_live_releases_deployments_route_and_action_repair.md` + +## Delivery Tracker + +### FE-DEPLOY-SCOPE-001 - Preserve scoped evidence and replay handoffs +Status: DONE +Dependency: none +Owners: QA, Developer, Architect +Task description: +- Update deployment-detail navigation so back-navigation, replay verify, evidence workspace, and proof-chain links all preserve the active shell scope instead of falling back to stale global context. +- Add focused regression tests and harden the live Playwright check so scope drift becomes a failing result. + +Completion criteria: +- [x] Deployment detail evidence and proof links merge active query scope. +- [x] `returnTo` emitted by deployment detail preserves the active context query parameters. +- [x] Live releases deployment Playwright evidence/replay checks fail on scope drift and pass on the fixed stack. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-03-10 | Sprint created after the live releases deployment sweep showed deployment evidence and proof-chain actions reopening under `eu-west/eu-stage/7d` instead of the active `us-east/stage` scope. | Developer | +| 2026-03-10 | Added focused deployment-detail scope regressions (`2/2` passing), rebuilt the web shell, synced the live bundle, and reran the Playwright releases deployment sweep cleanly with scoped replay/evidence/proof URLs and no remaining scope issues. | Developer | + +## Decisions & Risks +- Decision: preserve scope at the deployment-detail source by emitting scoped `returnTo` URLs and merged query params rather than relying on downstream evidence pages to guess the active context. +- Risk: similar release-detail surfaces may still encode incomplete `returnTo` state. +- Mitigation: strengthen each live action harness to assert scoped downstream URLs, not just page reachability. + +## Next Checkpoints +- Run focused deployment-detail scope tests. +- Rerun the live releases deployment Playwright check and commit the fix. diff --git a/src/Web/StellaOps.Web/scripts/live-releases-deployments-check.mjs b/src/Web/StellaOps.Web/scripts/live-releases-deployments-check.mjs index 2d266b77e..0a5e88204 100644 --- a/src/Web/StellaOps.Web/scripts/live-releases-deployments-check.mjs +++ b/src/Web/StellaOps.Web/scripts/live-releases-deployments-check.mjs @@ -19,6 +19,36 @@ const DETAIL_URL = `${BASE_URL}/releases/deployments/DEP-2026-050?tenant=demo-pr const STATE_PATH = path.join(outputDirectory, 'live-frontdoor-auth-state.json'); const REPORT_PATH = path.join(outputDirectory, 'live-frontdoor-auth-report.json'); const RESULT_PATH = path.join(outputDirectory, 'live-releases-deployments-check.json'); +const EXPECTED_SCOPE = { + tenant: 'demo-prod', + regions: 'us-east', + environments: 'stage', +}; + +function collectScopeIssues(url, expectedScope, label) { + const issues = []; + const parsed = new URL(url); + + for (const [key, expectedValue] of Object.entries(expectedScope)) { + const actualValue = parsed.searchParams.get(key); + if (actualValue !== expectedValue) { + issues.push(`${label} expected ${key}=${expectedValue} but got ${actualValue ?? '(missing)'}`); + } + } + + const returnTo = parsed.searchParams.get('returnTo'); + if (returnTo) { + const parsedReturnTo = new URL(returnTo, BASE_URL); + for (const [key, expectedValue] of Object.entries(expectedScope)) { + const actualValue = parsedReturnTo.searchParams.get(key); + if (actualValue !== expectedValue) { + issues.push(`returnTo from ${label} expected ${key}=${expectedValue} but got ${actualValue ?? '(missing)'}`); + } + } + } + + return issues; +} async function seedAuthenticatedPage(browser, authReport) { const context = await createAuthenticatedContext(browser, authReport, { @@ -72,6 +102,7 @@ async function main() { artifactDownloadSuggestedFilename: '', logsDownloadSuggestedFilename: '', detailActionStatus: '', + scopeIssues: [], }; const headerActions = page.locator('.deployment-detail .header-actions button'); @@ -107,6 +138,11 @@ async function main() { result.proofChainsHref = (await page.locator('.rekor-link').getAttribute('href')) ?? ''; await page.goto(new URL(result.proofChainsHref, BASE_URL).toString(), { waitUntil: 'networkidle', timeout: 30_000 }); result.proofChainsUrl = page.url(); + result.scopeIssues.push( + ...collectScopeIssues(result.replayUrl, EXPECTED_SCOPE, 'replayUrl'), + ...collectScopeIssues(result.evidenceWorkspaceUrl, EXPECTED_SCOPE, 'evidenceWorkspaceUrl'), + ...collectScopeIssues(result.proofChainsUrl, EXPECTED_SCOPE, 'proofChainsUrl'), + ); await page.goto(result.detailUrl, { waitUntil: 'networkidle', timeout: 30_000 }); await page.getByRole('button', { name: 'Artifacts' }).click(); @@ -140,6 +176,10 @@ async function main() { writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, 'utf8'); await context.close(); process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + + if (result.scopeIssues.length > 0) { + throw new Error(result.scopeIssues.join('; ')); + } } finally { await browser.close(); } diff --git a/src/Web/StellaOps.Web/src/app/core/testing/deployment-detail-scope-links.component.spec.ts b/src/Web/StellaOps.Web/src/app/core/testing/deployment-detail-scope-links.component.spec.ts new file mode 100644 index 000000000..cadaea8a1 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/core/testing/deployment-detail-scope-links.component.spec.ts @@ -0,0 +1,75 @@ +import { TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { ActivatedRoute, Router, RouterLink, convertToParamMap, provideRouter } from '@angular/router'; +import { of } from 'rxjs'; + +import { DeploymentDetailPageComponent } from '../../features/deployments/deployment-detail-page.component'; + +describe('Deployment detail scope-preserving links', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [DeploymentDetailPageComponent], + providers: [ + provideRouter([]), + { + provide: ActivatedRoute, + useValue: { + params: of({ deploymentId: 'DEP-2026-050' }), + snapshot: { + queryParamMap: convertToParamMap({ + tenant: 'demo-prod', + regions: 'us-east', + region: 'us-east', + environments: 'stage', + environment: 'stage', + timeWindow: '7d', + }), + }, + }, + }, + ], + }); + }); + + it('marks deployment evidence links to merge the active query scope', () => { + const fixture = TestBed.createComponent(DeploymentDetailPageComponent); + const component = fixture.componentInstance; + component.activeTab.set('evidence'); + fixture.detectChanges(); + + const backLink = fixture.debugElement.query(By.css('.back-link'))?.injector.get(RouterLink) ?? null; + const evidenceLink = fixture.debugElement.query(By.css('.evidence-info a'))?.injector.get(RouterLink) ?? null; + const proofLink = fixture.debugElement.query(By.css('.rekor-link'))?.injector.get(RouterLink) ?? null; + + expect(backLink).toBeTruthy(); + expect(evidenceLink).toBeTruthy(); + expect(proofLink).toBeTruthy(); + expect([backLink, evidenceLink, proofLink].every((link) => link?.queryParamsHandling === 'merge')).toBeTrue(); + + expect(evidenceLink?.queryParams?.['returnTo']).toContain('tenant=demo-prod'); + expect(evidenceLink?.queryParams?.['returnTo']).toContain('regions=us-east'); + expect(evidenceLink?.queryParams?.['returnTo']).toContain('environments=stage'); + expect(evidenceLink?.queryParams?.['returnTo']).toContain('timeWindow=7d'); + }); + + it('preserves scope when opening replay verification', () => { + const fixture = TestBed.createComponent(DeploymentDetailPageComponent); + const component = fixture.componentInstance; + const router = TestBed.inject(Router); + const navigateSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + fixture.detectChanges(); + + component.replayVerify(); + + expect(navigateSpy).toHaveBeenCalledWith( + ['/evidence/verify-replay'], + jasmine.objectContaining({ + queryParamsHandling: 'merge', + queryParams: jasmine.objectContaining({ + releaseId: 'v1.2.5', + returnTo: jasmine.stringMatching(/tenant=demo-prod/), + }), + }), + ); + }); +}); diff --git a/src/Web/StellaOps.Web/src/app/features/deployments/deployment-detail-page.component.ts b/src/Web/StellaOps.Web/src/app/features/deployments/deployment-detail-page.component.ts index af4ac2312..07e02d9cf 100644 --- a/src/Web/StellaOps.Web/src/app/features/deployments/deployment-detail-page.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/deployments/deployment-detail-page.component.ts @@ -9,6 +9,7 @@ import { Component, ChangeDetectionStrategy, signal, computed, inject, OnInit, E import { CommonModule } from '@angular/common'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { FormsModule } from '@angular/forms'; +import { buildContextReturnTo } from '../../shared/ui/context-route-state/context-route-state'; interface WorkflowStep { id: string; @@ -36,7 +37,7 @@ interface DeploymentArtifact { template: `