diff --git a/run-e2e.sh b/run-e2e.sh index 7d6aba74a..d356bfc76 100755 --- a/run-e2e.sh +++ b/run-e2e.sh @@ -194,8 +194,20 @@ for ws in "${E2E_WORKSPACES[@]}"; do WS_DIR="workspaces/${ws}/e2e-tests" WS_CONFIG="${WS_DIR}/playwright.config.ts" - # Extract content between "projects: [" and "]," (the project objects) - PROJECTS_BLOCK=$(sed -n '/projects: \[/,/^\s*\],/{ /projects: \[/d; /^\s*\],/d; p; }' "$WS_CONFIG") + # Extract content between "projects: [" and its matching "]". + # Uses awk with bracket-depth tracking so nested arrays (e.g. testMatch: [...]) + # don't prematurely terminate the extraction. + PROJECTS_BLOCK=$(awk ' + /projects:[[:space:]]*\[/ { inside=1; depth=1; next } + inside { + for (i=1; i<=length($0); i++) { + c = substr($0, i, 1) + if (c == "[") depth++ + if (c == "]") { depth--; if (depth == 0) { inside=0; next } } + } + if (inside) print + } + ' "$WS_CONFIG") if [[ -z "$PROJECTS_BLOCK" ]]; then echo "[WARN] No projects found in $WS_CONFIG, skipping" diff --git a/workspaces/orchestrator/e2e-tests/.yarnrc.yml b/workspaces/orchestrator/e2e-tests/.yarnrc.yml new file mode 100644 index 000000000..e1643b310 --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/.yarnrc.yml @@ -0,0 +1,2 @@ +nodeLinker: node-modules +npmRegistryServer: https://registry.npmjs.org diff --git a/workspaces/orchestrator/e2e-tests/eslint.config.js b/workspaces/orchestrator/e2e-tests/eslint.config.js new file mode 100644 index 000000000..d8d041826 --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/eslint.config.js @@ -0,0 +1,3 @@ +import { createEslintConfig } from "@red-hat-developer-hub/e2e-test-utils/eslint"; + +export default createEslintConfig(import.meta.dirname); diff --git a/workspaces/orchestrator/e2e-tests/package.json b/workspaces/orchestrator/e2e-tests/package.json new file mode 100644 index 000000000..82aa0d546 --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/package.json @@ -0,0 +1,39 @@ +{ + "name": "orchestrator-e2e-tests", + "version": "1.0.0", + "private": true, + "type": "module", + "engines": { + "node": ">=22", + "yarn": ">=3" + }, + "packageManager": "yarn@3.8.7", + "description": "E2E tests for Orchestrator plugin", + "scripts": { + "test": "playwright test", + "report": "playwright show-report", + "test:ui": "playwright test --ui", + "test:headed": "playwright test --headed", + "lint:check": "eslint .", + "lint:fix": "eslint . --fix", + "prettier:check": "prettier --check .", + "prettier:fix": "prettier --write .", + "tsc:check": "tsc --noEmit", + "check": "yarn tsc:check && yarn lint:check && yarn prettier:check" + }, + "devDependencies": { + "@eslint/js": "^9.39.2", + "@playwright/test": "1.57.0", + "@types/node": "^24.10.1", + "dotenv": "^16.4.7", + "eslint": "^9.39.2", + "eslint-plugin-check-file": "^3.3.1", + "eslint-plugin-playwright": "^2.4.0", + "prettier": "^3.7.4", + "typescript": "^5.9.3", + "typescript-eslint": "^8.50.0" + }, + "dependencies": { + "@red-hat-developer-hub/e2e-test-utils": "redhat-developer/rhdh-e2e-test-utils#main" + } +} diff --git a/workspaces/orchestrator/e2e-tests/playwright.config.ts b/workspaces/orchestrator/e2e-tests/playwright.config.ts new file mode 100644 index 000000000..66a867ca4 --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/playwright.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "@red-hat-developer-hub/e2e-test-utils/playwright-config"; +import dotenv from "dotenv"; + +dotenv.config({ path: `${import.meta.dirname}/.env` }); +/** + * Orchestrator plugin e2e test configuration. + * Extends the base config from rhdh-e2e-test-utils. + */ +export default defineConfig({ + projects: [ + { + name: "orchestrator", + }, + ], +}); diff --git a/workspaces/orchestrator/e2e-tests/tests/config/app-config-rhdh.yaml b/workspaces/orchestrator/e2e-tests/tests/config/app-config-rhdh.yaml new file mode 100644 index 000000000..f29dfc294 --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tests/config/app-config-rhdh.yaml @@ -0,0 +1,28 @@ +# rhdh app config file +# this file is used to merge with the default values of the rhdh app config + +app: + title: RHDH Orchestrator Test Instance +backend: + reading: + allow: + - host: github.com +permission: + enabled: true + rbac: + pluginsWithPermission: + - orchestrator + admin: + users: + - name: user:default/test1 +orchestrator: + dataIndexService: + url: ${SONATAFLOW_DATA_INDEX_URL} +catalog: + rules: + - allow: [Template, Component, System, Group, Resource, Location, API, User] + locations: + - type: url + target: https://github.com/testetson22/greeting_54mjks/blob/main/templates/greeting/greeting.yaml + - type: url + target: https://github.com/testetson22/greeting_54mjks/blob/main/templates/greeting/greeting_w_component.yaml diff --git a/workspaces/orchestrator/e2e-tests/tests/config/rhdh-secrets.yaml b/workspaces/orchestrator/e2e-tests/tests/config/rhdh-secrets.yaml new file mode 100644 index 000000000..4bcd010b5 --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tests/config/rhdh-secrets.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: rhdh-secrets +type: Opaque +stringData: + SONATAFLOW_DATA_INDEX_URL: $SONATAFLOW_DATA_INDEX_URL + LOKI_BASE_URL: http://localhost:3100 + # Required by orchestrator-backend-module-loki at startup; e2e does not deploy Loki. + LOKI_TOKEN: e2e-ci-placeholder diff --git a/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator-entity-workflow.tests.ts b/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator-entity-workflow.tests.ts new file mode 100644 index 000000000..30650d1de --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator-entity-workflow.tests.ts @@ -0,0 +1,118 @@ +import { test, expect } from "@red-hat-developer-hub/e2e-test-utils/test"; +import { cleanupGreetingComponentEntity } from "../support/utils/catalog-helpers.js"; +import { ensureDataIndexOrSkip } from "../support/utils/cluster-helpers.js"; +import { OrchestratorUiPage } from "../support/pages/orchestrator-po.js"; + +/** + * RHIDP-11833 through RHIDP-11838: Entity-Workflow Integration Tests + * + * These tests verify the integration between RHDH catalog entities and + * Orchestrator workflows, including: + * - EntityPicker-based entity association + * - orchestrator.io/workflows annotation behavior + * - Workflows tab visibility on entity pages + * - Catalog <-> Workflows breadcrumb navigation + * - Template execution -> workflow run linkage + * + * Templates used (from testetson22/greeting_54mjks on GitHub): + * - greeting.yaml: name=greeting, title="Greeting workflow" - NO orchestrator.io/workflows annotation + * - greeting_w_component.yaml: name=greetingComponent, title="Greeting Test Picker" - HAS annotation + * + * These are scaffolder templates that use the orchestrator:workflow:run action + * to trigger the "greeting" SonataFlow workflow deployed by CI. + */ +export function registerOrchestratorEntityWorkflowTests(): void { + test.describe("Entity-Workflow Integration", () => { + let orchestratorUi: OrchestratorUiPage; + + test.beforeEach(async ({ page, loginHelper }, testInfo) => { + orchestratorUi = new OrchestratorUiPage(page); + await loginHelper.loginAsKeycloakUser(); + await ensureDataIndexOrSkip(testInfo.project.name, test); + }); + + test.afterAll(async () => { + await cleanupGreetingComponentEntity(); + }); + + test("RHIDP-11833: Run workflow with existing entity", async ({ + uiHelper, + }) => { + await orchestratorUi.launchGreetingTemplateFromSelfService(uiHelper, { + entityName: `test-entity-${Date.now()}`, + selectEnglishIfVisible: true, + }); + await expect( + orchestratorUi.workflowLink(/Greeting workflow/i), + ).toBeVisible(); + await orchestratorUi.expectWorkflowHeading(/Greeting workflow/i); + }); + + test("RHIDP-11834: Show workflow for annotated template", async ({ + uiHelper, + }) => { + await orchestratorUi.openGreetingTemplateFromCatalog(uiHelper); + await orchestratorUi.clickWorkflowsTab(); + await expect( + orchestratorUi.workflowLink("Greeting workflow"), + ).toBeVisible(); + await orchestratorUi.expectWorkflowVisible("Greeting workflow"); + }); + + test("RHIDP-11835: Hide workflow for unannotated template", async ({ + uiHelper, + }) => { + await orchestratorUi.openTemplateFromCatalog( + uiHelper, + /Greeting workflow/i, + ); + await orchestratorUi.expectWorkflowMissingInEntityTab( + "Greeting workflow", + ); + await expect( + orchestratorUi.workflowLink("Greeting workflow"), + ).toHaveCount(0); + }); + + test("RHIDP-11836: Navigate Catalog and Workflows via breadcrumb", async ({ + uiHelper, + page, + }) => { + await orchestratorUi.openGreetingTemplateFromCatalog(uiHelper); + await orchestratorUi.clickWorkflowsTab(); + await orchestratorUi.openGreetingWorkflowFromEntityTab(); + await orchestratorUi.openEntityFromBreadcrumb( + "greetingComponent", + /Greeting Test Picker/i, + ); + await expect(page).toHaveURL( + /\/catalog\/default\/template\/greetingcomponent(?:\/workflows)?/i, + ); + }); + + test("RHIDP-11837: Show workflow runs after template run", async ({ + uiHelper, + }) => { + await orchestratorUi.launchGreetingTemplateFromSelfService(uiHelper, { + entityName: `test-entity-${Date.now()}`, + selectEnglishIfVisible: true, + }); + await orchestratorUi.gotoWorkflows(uiHelper, false); + await expect( + orchestratorUi.workflowLink(/Greeting workflow/i), + ).toBeVisible(); + await orchestratorUi.expectWorkflowVisible(/Greeting workflow/i); + }); + + test("RHIDP-11838: Show Workflows tab with dynamic plugin config", async ({ + uiHelper, + }) => { + await orchestratorUi.openGreetingTemplateFromCatalog(uiHelper); + await orchestratorUi.clickWorkflowsTab(); + await expect( + orchestratorUi.workflowLink("Greeting workflow"), + ).toBeVisible(); + await orchestratorUi.expectWorkflowVisible("Greeting workflow"); + }); + }); +} diff --git a/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator-rbac-instance-access.tests.ts b/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator-rbac-instance-access.tests.ts new file mode 100644 index 000000000..335019051 --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator-rbac-instance-access.tests.ts @@ -0,0 +1,341 @@ +import { test, expect, Page } from "@red-hat-developer-hub/e2e-test-utils/test"; +import { + LoginHelper, + UIhelper, + AuthApiHelper, + RbacApiHelper, +} from "@red-hat-developer-hub/e2e-test-utils/helpers"; +import { + setupAuthenticatedPage, + deleteRoleAndPolicies, + buildPolicies, + roleApiName, + PRIMARY_USER, + SECONDARY_USER, +} from "./rbac-helpers.js"; +import { OrchestratorUiPage } from "../support/pages/orchestrator-po.js"; +import { + LONG_WAIT_TIMEOUT_MS, + RBAC_MEMBERSHIP_UPDATE_TIMEOUT_MS, +} from "./timeouts.js"; + +export function registerOrchestratorRbacInstanceAccess(): void { + test.describe.serial("RBAC instance access: initiator and admin", () => { + test.describe.configure({ retries: 0 }); + let loginHelper: LoginHelper; + let uiHelper: UIhelper; + let page: Page; + let apiToken: string; + let workflowInstanceId: string; + let workflowUserRoleName: string; + let workflowAdminRoleName: string; + + test.beforeAll(async ({ browser }, testInfo) => { + ({ page, uiHelper, loginHelper, apiToken } = await setupAuthenticatedPage( + browser, + testInfo, + )); + + const rbacApi = await RbacApiHelper.build(apiToken); + try { + const rolesResponse = await rbacApi.getRoles(); + if (rolesResponse.ok()) { + const roles = await rolesResponse.json(); + const lingeringRoles = roles.filter( + (role: { name: string }) => + role.name.includes("workflowUser") || + role.name.includes("workflowAdmin"), + ); + + for (const role of lingeringRoles) { + await deleteRoleAndPolicies(apiToken, role.name); + } + } + } catch { + /* best-effort cleanup */ + } + + workflowUserRoleName = "role:default/workflowUser"; + await deleteRoleAndPolicies(apiToken, workflowUserRoleName); + + const rolePostResponse = await rbacApi.createRoles({ + memberReferences: [PRIMARY_USER, SECONDARY_USER], + name: workflowUserRoleName, + }); + const policyPostResponse = await rbacApi.createPolicies( + buildPolicies(workflowUserRoleName, [ + { + permission: "orchestrator.workflow.greeting", + policy: "read", + effect: "allow", + }, + { + permission: "orchestrator.workflow.use.greeting", + policy: "update", + effect: "allow", + }, + ]), + ); + + expect(rolePostResponse.ok()).toBeTruthy(); + expect(policyPostResponse.ok()).toBeTruthy(); + + const rolesResponse = await rbacApi.getRoles(); + expect(rolesResponse.ok()).toBeTruthy(); + const roles = await rolesResponse.json(); + const workflowRole = roles.find( + (role: { name: string; memberReferences: string[] }) => + role.name === workflowUserRoleName, + ); + expect(workflowRole).toBeDefined(); + expect(workflowRole?.memberReferences).toContain(PRIMARY_USER); + expect(workflowRole?.memberReferences).toContain(SECONDARY_USER); + + const policiesResponse = await rbacApi.getPoliciesByRole( + roleApiName(workflowUserRoleName), + ); + expect(policiesResponse.ok()).toBeTruthy(); + const policies = await policiesResponse.json(); + expect(policies).toHaveLength(2); + }); + + test.afterAll(async () => { + try { + if (!page) return; + await page.goto("/"); + await page.context().clearCookies(); + + try { + await loginHelper.loginAsKeycloakUser(); + apiToken = await new AuthApiHelper(page).getToken(); + } catch { + return; + } + + if (workflowUserRoleName) { + await deleteRoleAndPolicies(apiToken, workflowUserRoleName); + } + if (workflowAdminRoleName) { + await deleteRoleAndPolicies(apiToken, workflowAdminRoleName); + } + } catch (error) { + console.error("Error during cleanup in afterAll:", error); + } + }); + + test("Workflow instance access: initiator-only then admin override", async () => { + await test.step("Primary user runs greeting workflow and captures instance ID", async () => { + const orchestratorUi = new OrchestratorUiPage(page); + await orchestratorUi.gotoWorkflows(uiHelper); + await orchestratorUi.expectWorkflowVisible("Greeting workflow"); + await orchestratorUi.openWorkflow("Greeting workflow"); + await orchestratorUi.expectWorkflowHeading("Greeting workflow"); + await orchestratorUi.clickRunButton(); + await orchestratorUi.runCurrentWorkflowFromWizard(); + + await page.waitForURL(/\/orchestrator\/instances\/[a-f0-9-]+/); + const url = page.url(); + const match = url.match(/\/orchestrator\/instances\/([a-f0-9-]+)/); + expect(match).not.toBeNull(); + workflowInstanceId = match![1]; + + await expect(page.getByText(/Run completed at/i)).toBeVisible({ + timeout: LONG_WAIT_TIMEOUT_MS, + }); + }); + + await test.step("Primary user can access the workflow instance", async () => { + await uiHelper.goToPageUrl( + `/orchestrator/instances/${workflowInstanceId}`, + ); + await page.waitForLoadState("load"); + await expect(page.getByText("Completed", { exact: true })).toBeVisible({ + timeout: LONG_WAIT_TIMEOUT_MS, + }); + }); + + await test.step("Secondary user cannot access primary user's workflow instance", async () => { + await page.context().clearCookies(); + await page.goto("/"); + await page.waitForLoadState("load"); + + try { + await loginHelper.loginAsKeycloakUser( + process.env.GH_USER2_ID || "test2", + process.env.GH_USER2_PASS || "test2@123", + ); + } catch { + /* proceed to explicit access check */ + } + + await uiHelper.goToPageUrl( + `/orchestrator/instances/${workflowInstanceId}`, + ); + await page.waitForLoadState("load"); + + const pageContent = await page.locator("body").textContent(); + + const hasAccessDenied = + pageContent?.includes("not found") || + pageContent?.includes("Not Found") || + pageContent?.includes("denied") || + pageContent?.includes("unauthorized") || + pageContent?.includes("Unauthorized") || + !pageContent?.includes("Completed"); + + expect(hasAccessDenied).toBe(true); + }); + + await test.step("Grant instanceAdminView to secondary user via workflowAdmin role", async () => { + workflowUserRoleName = "role:default/workflowUser"; + workflowAdminRoleName = "role:default/workflowAdmin"; + await deleteRoleAndPolicies(apiToken, workflowAdminRoleName); + + await page.context().clearCookies(); + await page.goto("/"); + await page.waitForLoadState("load"); + + await loginHelper.loginAsKeycloakUser(); + apiToken = await new AuthApiHelper(page).getToken(); + + const rbacApi = await RbacApiHelper.build(apiToken); + + try { + await rbacApi.createRoles({ + memberReferences: [PRIMARY_USER, SECONDARY_USER], + name: workflowUserRoleName, + }); + await rbacApi.createPolicies( + buildPolicies(workflowUserRoleName, [ + { + permission: "orchestrator.workflow.greeting", + policy: "read", + effect: "allow", + }, + { + permission: "orchestrator.workflow.use.greeting", + policy: "update", + effect: "allow", + }, + ]), + ); + } catch { + /* role can already exist from previous serial runs */ + } + + const rolePostResponse = await rbacApi.createRoles({ + memberReferences: [SECONDARY_USER], + name: workflowAdminRoleName, + }); + const policyPostResponse = await rbacApi.createPolicies( + buildPolicies(workflowAdminRoleName, [ + { + permission: "orchestrator.workflow", + policy: "read", + effect: "allow", + }, + { + permission: "orchestrator.workflow.use", + policy: "update", + effect: "allow", + }, + { + permission: "orchestrator.instanceAdminView", + policy: "read", + effect: "allow", + }, + ]), + ); + + expect(rolePostResponse.ok()).toBeTruthy(); + expect(policyPostResponse.ok()).toBeTruthy(); + + const oldWorkflowUserRole = { + memberReferences: [PRIMARY_USER, SECONDARY_USER], + name: workflowUserRoleName, + }; + const updatedWorkflowUserRole = { + memberReferences: [PRIMARY_USER], + name: workflowUserRoleName, + }; + + const roleUpdateResponse = await rbacApi.updateRole( + roleApiName(workflowUserRoleName), + oldWorkflowUserRole, + updatedWorkflowUserRole, + ); + + expect(roleUpdateResponse.ok()).toBeTruthy(); + + await expect + .poll( + async () => { + const rolesResponse = await rbacApi.getRoles(); + if (!rolesResponse.ok()) return null; + const roles = await rolesResponse.json(); + const workflowUserRole = roles.find( + (role: { name: string; memberReferences: string[] }) => + role.name === workflowUserRoleName, + ); + return workflowUserRole?.memberReferences ?? null; + }, + { timeout: RBAC_MEMBERSHIP_UPDATE_TIMEOUT_MS }, + ) + .not.toContain(SECONDARY_USER); + }); + + await test.step("Verify admin role membership and policy state", async () => { + const rbacApi = await RbacApiHelper.build(apiToken); + + const rolesResponse = await rbacApi.getRoles(); + expect(rolesResponse.ok()).toBeTruthy(); + + const roles = await rolesResponse.json(); + const adminRole = roles.find( + (role: { name: string; memberReferences: string[] }) => + role.name === workflowAdminRoleName, + ); + expect(adminRole).toBeDefined(); + expect(adminRole?.memberReferences).toContain(SECONDARY_USER); + + const policiesResponse = await rbacApi.getPoliciesByRole( + roleApiName(workflowAdminRoleName), + ); + expect(policiesResponse.ok()).toBeTruthy(); + + const policies = await policiesResponse.json(); + expect(policies).toHaveLength(3); + + const workflowUserRole = roles.find( + (role: { name: string; memberReferences: string[] }) => + role.name === workflowUserRoleName, + ); + expect(workflowUserRole).toBeDefined(); + expect(workflowUserRole?.memberReferences).toContain(PRIMARY_USER); + expect(workflowUserRole?.memberReferences).not.toContain( + SECONDARY_USER, + ); + }); + + await test.step("Secondary user can now access the primary user's instance", async () => { + await page.context().clearCookies(); + await page.goto("/"); + await page.waitForLoadState("load"); + + await loginHelper.loginAsKeycloakUser( + process.env.GH_USER2_ID || "test2", + process.env.GH_USER2_PASS || "test2@123", + ); + + await uiHelper.goToPageUrl( + `/orchestrator/instances/${workflowInstanceId}`, + ); + await page.waitForLoadState("load"); + + await expect(page.getByText("Completed", { exact: true })).toBeVisible({ + timeout: LONG_WAIT_TIMEOUT_MS, + }); + }); + }); + }); +} diff --git a/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator-rbac-matrix.tests.ts b/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator-rbac-matrix.tests.ts new file mode 100644 index 000000000..e81982895 --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator-rbac-matrix.tests.ts @@ -0,0 +1,162 @@ +import { test, expect, Page } from "@red-hat-developer-hub/e2e-test-utils/test"; +import { UIhelper } from "@red-hat-developer-hub/e2e-test-utils/helpers"; +import { + type PolicySpec, + setupAuthenticatedPage, + deleteRoleAndPolicies, + createRoleWithPolicies, + verifyRoleWithPolicies, + globalWorkflowPolicies, + greetingWorkflowPolicies, + PRIMARY_USER, +} from "./rbac-helpers.js"; +import { OrchestratorUiPage } from "../support/pages/orchestrator-po.js"; + +const RBAC_RETRIES = process.env.CI ? 1 : 0; + +type RbacMatrixScenario = { + name: string; + roleName: string; + policies: PolicySpec[]; + visibleWorkflows: string[]; + hiddenWorkflows: string[]; + openWorkflowForRunState?: string; + runState?: "enabled" | "disabled"; + triggerRun?: boolean; + expectEmptyTable?: boolean; +}; + +async function validateScenarioWorkflows( + orchestratorUi: OrchestratorUiPage, + uiHelper: UIhelper, + scenario: RbacMatrixScenario, +): Promise { + if (scenario.expectEmptyTable) { + await uiHelper.verifyTableIsEmpty(); + } + + for (const workflow of scenario.visibleWorkflows) { + await orchestratorUi.expectWorkflowVisible(workflow); + } + for (const workflow of scenario.hiddenWorkflows) { + await orchestratorUi.expectWorkflowAbsent(workflow); + } + + if (!(scenario.openWorkflowForRunState && scenario.runState)) { + return; + } + + await orchestratorUi.openWorkflow(scenario.openWorkflowForRunState); + await orchestratorUi.expectWorkflowHeading(scenario.openWorkflowForRunState); + + if (scenario.runState === "enabled") { + await orchestratorUi.expectRunButtonEnabled(); + if (scenario.triggerRun) { + await orchestratorUi.clickRunButton(); + } + return; + } + + await orchestratorUi.expectRunButtonDisabledOrAbsent(); +} + +const RBAC_MATRIX_SCENARIOS: RbacMatrixScenario[] = [ + { + name: "Global Workflow Access", + roleName: "role:default/workflowReadwrite", + policies: globalWorkflowPolicies("allow", "allow"), + visibleWorkflows: ["Greeting workflow"], + hiddenWorkflows: [], + openWorkflowForRunState: "Greeting workflow", + runState: "enabled", + triggerRun: true, + }, + { + name: "Global Workflow Read-Only Access", + roleName: "role:default/workflowReadonly", + policies: globalWorkflowPolicies("allow", "deny"), + visibleWorkflows: ["Greeting workflow"], + hiddenWorkflows: [], + openWorkflowForRunState: "Greeting workflow", + runState: "disabled", + }, + { + name: "Global Workflow Denied Access", + roleName: "role:default/workflowDenied", + policies: globalWorkflowPolicies("deny", "deny"), + visibleWorkflows: [], + hiddenWorkflows: ["Greeting workflow"], + expectEmptyTable: true, + }, + { + name: "Individual Workflow Denied Access", + roleName: "role:default/workflowGreetingDenied", + policies: greetingWorkflowPolicies("deny", "deny"), + visibleWorkflows: [], + hiddenWorkflows: ["Greeting workflow", "User Onboarding"], + expectEmptyTable: true, + }, + { + name: "Individual Workflow Read-Write Access", + roleName: "role:default/workflowGreetingReadwrite", + policies: greetingWorkflowPolicies("allow", "allow"), + visibleWorkflows: ["Greeting workflow"], + hiddenWorkflows: ["User Onboarding"], + openWorkflowForRunState: "Greeting workflow", + runState: "enabled", + triggerRun: true, + }, + { + name: "Individual Workflow Read-Only Access", + roleName: "role:default/workflowGreetingReadonly", + policies: greetingWorkflowPolicies("allow", "deny"), + visibleWorkflows: ["Greeting workflow"], + hiddenWorkflows: ["User Onboarding"], + openWorkflowForRunState: "Greeting workflow", + runState: "disabled", + }, +]; + +export function registerOrchestratorRbacMatrix(): void { + for (const scenario of RBAC_MATRIX_SCENARIOS) { + test.describe(`RBAC matrix: ${scenario.name}`, () => { + test.describe.configure({ retries: RBAC_RETRIES }); + + let uiHelper: UIhelper; + let page: Page; + let apiToken: string; + + test.beforeAll(async ({ browser }, testInfo) => { + ({ page, uiHelper, apiToken } = await setupAuthenticatedPage( + browser, + testInfo, + )); + await createRoleWithPolicies( + apiToken, + scenario.roleName, + [PRIMARY_USER], + scenario.policies, + ); + await verifyRoleWithPolicies( + apiToken, + scenario.roleName, + [PRIMARY_USER], + scenario.policies, + ); + }); + + test.afterAll(async () => { + await deleteRoleAndPolicies(apiToken, scenario.roleName); + }); + + test(scenario.name, async () => { + const orchestratorUi = new OrchestratorUiPage(page); + await orchestratorUi.gotoWorkflows(uiHelper); + await expect( + page.getByRole("heading", { name: /workflows/i }), + ).toBeVisible(); + await validateScenarioWorkflows(orchestratorUi, uiHelper, scenario); + }); + }); + } +} diff --git a/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator-rbac-template-run.tests.ts b/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator-rbac-template-run.tests.ts new file mode 100644 index 000000000..2d0c5ed0e --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator-rbac-template-run.tests.ts @@ -0,0 +1,262 @@ +import { test, expect, Page } from "@red-hat-developer-hub/e2e-test-utils/test"; +import { + UIhelper, + RbacApiHelper, +} from "@red-hat-developer-hub/e2e-test-utils/helpers"; +import { + setupAuthenticatedPage, + deleteRoleAndPolicies, + buildPolicies, + PRIMARY_USER, +} from "./rbac-helpers.js"; +import { cleanupGreetingComponentEntity } from "../support/utils/catalog-helpers.js"; +import { OrchestratorUiPage } from "../support/pages/orchestrator-po.js"; +import { + RBAC_ROLE_PROPAGATION_INTERVAL_MS, + RBAC_ROLE_PROPAGATION_TIMEOUT_MS, + TEMPLATE_SUBMIT_TIMEOUT_FIRST_MS, + TEMPLATE_SUBMIT_TIMEOUT_RETRY_MS, + TEMPLATE_RUN_TIMEOUT_MS, +} from "./timeouts.js"; + +const RBAC_RETRIES = process.env.CI ? 1 : 0; + +/** + * RHIDP-11839, RHIDP-11840: Entity-Workflow RBAC Tests + * + * Templates used in the tests (from catalog locations): + * - greeting_w_component.yaml: name=greetingComponent, title="Greeting Test Picker" - HAS annotation + */ +async function submitGreetingTemplateWithRetry( + page: Page, + orchestratorUi: OrchestratorUiPage, + uiHelper: UIhelper, +): Promise { + for (let attempt = 1; attempt <= 2; attempt++) { + await orchestratorUi.openGreetingTemplateFromSelfService(uiHelper); + + try { + await orchestratorUi.submitTemplate( + attempt === 1 + ? TEMPLATE_SUBMIT_TIMEOUT_FIRST_MS + : TEMPLATE_SUBMIT_TIMEOUT_RETRY_MS, + ); + return; + } catch (error) { + if (attempt === 2) { + throw error; + } + await page.goto("/"); + await page.waitForLoadState("domcontentloaded"); + } + } +} + +export function registerOrchestratorRbacTemplateRun(): void { + test.describe("RHIDP-11839: Template run WITHOUT workflow permissions", () => { + test.describe.configure({ retries: RBAC_RETRIES }); + let uiHelper: UIhelper; + let page: Page; + let apiToken: string; + const roleName = "role:default/catalogSuperuserNoWorkflowTest"; + + test.beforeAll(async ({ browser }, testInfo) => { + ({ page, uiHelper, apiToken } = await setupAuthenticatedPage( + browser, + testInfo, + )); + await cleanupGreetingComponentEntity(); + const rbacApi = await RbacApiHelper.build(apiToken); + const rolePostResponse = await rbacApi.createRoles({ + memberReferences: [PRIMARY_USER], + name: roleName, + }); + const policyPostResponse = await rbacApi.createPolicies( + buildPolicies(roleName, [ + { permission: "catalog-entity", policy: "read", effect: "allow" }, + { + permission: "catalog.entity.create", + policy: "create", + effect: "allow", + }, + { + permission: "catalog.location.read", + policy: "read", + effect: "allow", + }, + { + permission: "catalog.location.create", + policy: "create", + effect: "allow", + }, + { + permission: "scaffolder.action.execute", + policy: "use", + effect: "allow", + }, + { + permission: "scaffolder.task.create", + policy: "create", + effect: "allow", + }, + { + permission: "scaffolder.task.read", + policy: "read", + effect: "allow", + }, + // Explicitly DENY orchestrator permissions + { + permission: "orchestrator.workflow", + policy: "read", + effect: "deny", + }, + { + permission: "orchestrator.workflow.use", + policy: "update", + effect: "deny", + }, + ]), + ); + expect(rolePostResponse.ok()).toBeTruthy(); + expect(policyPostResponse.ok()).toBeTruthy(); + }); + + test.afterAll(async () => { + await cleanupGreetingComponentEntity(); + await deleteRoleAndPolicies(apiToken, roleName); + }); + + test("Open orchestrator-tagged template in Catalog", async () => { + const orchestratorUi = new OrchestratorUiPage(page); + await orchestratorUi.openGreetingTemplateFromCatalog(uiHelper); + await expect(page.getByRole("heading").first()).toBeVisible(); + }); + + test("Template launch is denied without workflow permissions", async () => { + const orchestratorUi = new OrchestratorUiPage(page); + await orchestratorUi.launchGreetingTemplateFromSelfService(uiHelper, { + selectEnglishIfVisible: false, + }); + + // Template execution should succeed, but workflow execution should be denied. + await expect + .poll( + async () => { + if (await orchestratorUi.hasUnauthorizedIndicator()) { + return true; + } + await orchestratorUi.gotoWorkflows(uiHelper, false); + return ( + (await orchestratorUi + .workflowLink("Greeting workflow") + .count()) === 0 + ); + }, + { + timeout: RBAC_ROLE_PROPAGATION_TIMEOUT_MS, + intervals: [RBAC_ROLE_PROPAGATION_INTERVAL_MS], + }, + ) + .toBe(true); + }); + }); + + test.describe("RHIDP-11840: Template run WITH workflow permissions", () => { + test.describe.configure({ retries: RBAC_RETRIES }); + let uiHelper: UIhelper; + let page: Page; + let apiToken: string; + const roleName = "role:default/catalogSuperuserWithWorkflowTest"; + + test.beforeAll(async ({ browser }, testInfo) => { + ({ page, uiHelper, apiToken } = await setupAuthenticatedPage( + browser, + testInfo, + )); + await cleanupGreetingComponentEntity(); + const rbacApi = await RbacApiHelper.build(apiToken); + const rolePostResponse = await rbacApi.createRoles({ + memberReferences: [PRIMARY_USER], + name: roleName, + }); + const policyPostResponse = await rbacApi.createPolicies( + buildPolicies(roleName, [ + { permission: "catalog-entity", policy: "read", effect: "allow" }, + { + permission: "catalog.entity.create", + policy: "create", + effect: "allow", + }, + { + permission: "catalog.location.read", + policy: "read", + effect: "allow", + }, + { + permission: "catalog.location.create", + policy: "create", + effect: "allow", + }, + { + permission: "scaffolder.action.execute", + policy: "use", + effect: "allow", + }, + { + permission: "scaffolder.task.create", + policy: "create", + effect: "allow", + }, + { + permission: "scaffolder.task.read", + policy: "read", + effect: "allow", + }, + // Orchestrator permissions - ALLOW + { + permission: "orchestrator.workflow", + policy: "read", + effect: "allow", + }, + { + permission: "orchestrator.workflow.use", + policy: "update", + effect: "allow", + }, + ]), + ); + expect(rolePostResponse.ok()).toBeTruthy(); + expect(policyPostResponse.ok()).toBeTruthy(); + }); + + test.afterAll(async () => { + await cleanupGreetingComponentEntity(); + await deleteRoleAndPolicies(apiToken, roleName); + }); + + test("Open orchestrator-tagged template in Catalog", async () => { + const orchestratorUi = new OrchestratorUiPage(page); + await orchestratorUi.openGreetingTemplateFromCatalog(uiHelper); + await expect(page.getByRole("heading").first()).toBeVisible(); + }); + + test("Launch template and run workflow", async () => { + test.setTimeout(TEMPLATE_RUN_TIMEOUT_MS); + const orchestratorUi = new OrchestratorUiPage(page); + await submitGreetingTemplateWithRetry(page, orchestratorUi, uiHelper); + await expect(page).toHaveURL(/\/orchestrator\/instances\/[a-f0-9-]+/); + }); + + test("Show workflow run in Orchestrator", async () => { + const orchestratorUi = new OrchestratorUiPage(page); + await orchestratorUi.gotoWorkflows(uiHelper, false); + await expect( + orchestratorUi.workflowLink(/Greeting workflow/i), + ).toBeVisible(); + await orchestratorUi.expectWorkflowVisible(/Greeting workflow/i); + await orchestratorUi.openWorkflow(/Greeting workflow/i); + await orchestratorUi.expectWorkflowHeading(/Greeting workflow/i); + await orchestratorUi.expectRunButtonEnabled(); + }); + }); +} diff --git a/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator-rbac.tests.ts b/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator-rbac.tests.ts new file mode 100644 index 000000000..f5d429c58 --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator-rbac.tests.ts @@ -0,0 +1,19 @@ +import { test } from "@red-hat-developer-hub/e2e-test-utils/test"; +import { removeBaselineRole } from "./rbac-helpers.js"; +import { registerOrchestratorRbacMatrix } from "./orchestrator-rbac-matrix.tests.js"; +import { registerOrchestratorRbacInstanceAccess } from "./orchestrator-rbac-instance-access.tests.js"; +import { registerOrchestratorRbacTemplateRun } from "./orchestrator-rbac-template-run.tests.js"; +import { SUITE_SETUP_TIMEOUT_MS } from "./timeouts.js"; + +export function rbacTests(): void { + test.describe("RBAC", () => { + test.beforeAll(async ({ browser }, testInfo) => { + test.setTimeout(SUITE_SETUP_TIMEOUT_MS); + await removeBaselineRole(browser, testInfo); + }); + + registerOrchestratorRbacMatrix(); + registerOrchestratorRbacInstanceAccess(); + registerOrchestratorRbacTemplateRun(); + }); +} diff --git a/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator-workflow.tests.ts b/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator-workflow.tests.ts new file mode 100644 index 000000000..42ca8fd74 --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator-workflow.tests.ts @@ -0,0 +1,434 @@ +import { + test, + expect, + type Page, +} from "@red-hat-developer-hub/e2e-test-utils/test"; +import { AuthApiHelper } from "@red-hat-developer-hub/e2e-test-utils/helpers"; +import { OrchestratorPage } from "@red-hat-developer-hub/e2e-test-utils/pages"; +import { ensureBaselineRole, runOc } from "./rbac-helpers.js"; +import { + type WorkflowNode, + type WorkflowInstance, + ensureDataIndexOrSkip, + patchHttpbin, + restartAndWait, + cleanupAfterTest, +} from "../support/utils/cluster-helpers.js"; +import { + requireEnvVar, + getEnvVarDefault, +} from "../support/utils/env-helpers.js"; +import { registerOrchestratorEntityWorkflowTests } from "./orchestrator-entity-workflow.tests.js"; +import { OrchestratorUiPage } from "../support/pages/orchestrator-po.js"; +import { + FAILSWITCH_RERUN_TIMEOUT_MS, + FAILSWITCH_TEST_TIMEOUT_MS, + LONG_WAIT_TIMEOUT_MS, + SUITE_SETUP_TIMEOUT_MS, + TOKEN_PROPAGATION_TIMEOUT_MS, + WORKFLOW_ALL_RUNS_RETRY_TIMEOUT_MS, + WORKFLOW_POLL_INTERVAL_MS, + WORKFLOW_POLL_MAX_ATTEMPTS, + WORKFLOW_RUN_TIMEOUT_MS, +} from "./timeouts.js"; + +async function assertApiResponseOk( + response: { + ok(): boolean; + status(): number; + text(): Promise; + }, + operation: string, +): Promise { + if (response.ok()) return; + throw new Error( + `${operation} failed with ${response.status()}: ${await response.text()}`, + ); +} + +async function pollWorkflowInstanceStatus( + page: Page, + backstageToken: string, + instanceId: string, + timeoutMs: number, + pollIntervalMs: number, +): Promise { + let statusBody: WorkflowInstance | undefined; + await expect + .poll( + async () => { + const statusResponse = await page.request.get( + `/api/orchestrator/v2/workflows/instances/${instanceId}`, + { + headers: { + Authorization: `Bearer ${backstageToken}`, + }, + }, + ); + expect(statusResponse.ok()).toBeTruthy(); + const currentStatus = (await statusResponse.json()) as WorkflowInstance; + statusBody = currentStatus; + if (currentStatus.state === "ERROR") { + throw new Error( + `Workflow failed with ERROR state: ${JSON.stringify(currentStatus)}`, + ); + } + return currentStatus.state; + }, + { timeout: timeoutMs, intervals: [pollIntervalMs] }, + ) + .toBe("COMPLETED"); + + return statusBody as WorkflowInstance; +} + +function assertExpectedWorkflowNodes(statusBody: WorkflowInstance): void { + const nodes = statusBody.nodes; + const expectedNodes = [ + "getWithBearerTokenSecurityScheme", + "getWithOtherBearerTokenSecurityScheme", + "getWithSimpleBearerTokenSecurityScheme", + "extractUser", + ]; + + for (const nodeName of expectedNodes) { + const node = nodes.find((n: WorkflowNode) => n.name === nodeName); + expect(node, `Node '${nodeName}' should exist`).toBeDefined(); + if (!node) continue; + + expect( + node.errorMessage, + `Node '${nodeName}' should have no error`, + ).toBeNull(); + expect( + node.exit, + `Node '${nodeName}' should have completed`, + ).not.toBeNull(); + } +} + +function assertOpenShiftSampleServerLogs(statusBody: WorkflowInstance): void { + if (process.env.IS_OPENSHIFT !== "true") return; + + const serviceUrl = statusBody.serviceUrl || ""; + const nsMatch = /token-propagation\.([^:/]+)/.exec(serviceUrl); + const namespace = nsMatch?.[1] || getEnvVarDefault("NAME_SPACE", ""); + if (!namespace) return; + + if (!/^[a-z0-9-]+$/.test(namespace)) { + throw new Error( + `Invalid namespace format: "${namespace}". Must contain only lowercase alphanumeric characters and hyphens.`, + ); + } + + const sampleServerLogs = runOc( + ["logs", "-l", "app=sample-server", "-n", namespace, "--tail=200"], + LONG_WAIT_TIMEOUT_MS, + ); + + expect( + sampleServerLogs, + "Sample-server should log /first endpoint request", + ).toContain("Headers for first"); + expect( + sampleServerLogs, + "Sample-server should log /other endpoint request", + ).toContain("Headers for other"); + expect( + sampleServerLogs, + "Sample-server should log /simple endpoint request", + ).toContain("Headers for simple"); +} + +export function workflowTests(): void { + test.describe("Workflow Execution", () => { + test.beforeAll(async ({ browser }, testInfo) => { + test.setTimeout(SUITE_SETUP_TIMEOUT_MS); + await ensureBaselineRole(browser, testInfo); + }); + + test.describe("Greeting workflow", () => { + let orchestrator: OrchestratorPage; + let orchestratorUi: OrchestratorUiPage; + + test.beforeEach(async ({ page, loginHelper }, testInfo) => { + orchestrator = new OrchestratorPage(page); + orchestratorUi = new OrchestratorUiPage(page); + await loginHelper.loginAsKeycloakUser(); + await ensureDataIndexOrSkip(testInfo.project.name, test); + }); + + test("Greeting workflow appears in Workflows tab", async ({ + uiHelper, + }) => { + test.setTimeout(WORKFLOW_RUN_TIMEOUT_MS); + await orchestratorUi.gotoWorkflows(uiHelper, false); + await expect( + orchestratorUi.workflowLink(/Greeting workflow/i), + ).toBeVisible(); + await orchestrator.selectGreetingWorkflowItem(); + await orchestrator.runGreetingWorkflow(); + await orchestratorUi.gotoWorkflows(uiHelper, false); + await expect( + orchestratorUi.workflowLink(/Greeting workflow/i), + ).toBeVisible(); + await orchestrator.selectGreetingWorkflowItem(); + await orchestratorUi.expectRunButtonEnabled(); + }); + + test("Greeting workflow run details", async ({ uiHelper }) => { + test.setTimeout(WORKFLOW_RUN_TIMEOUT_MS); + await orchestratorUi.gotoWorkflows(uiHelper, false); + await expect( + orchestratorUi.workflowLink(/Greeting workflow/i), + ).toBeVisible(); + await orchestrator.selectGreetingWorkflowItem(); + await orchestrator.runGreetingWorkflow(); + await orchestrator.reRunGreetingWorkflow(); + await orchestrator.validateWorkflowRunsDetails(); + }); + }); + + test.describe("Failswitch workflow", () => { + let orchestrator: OrchestratorPage; + let orchestratorUi: OrchestratorUiPage; + + test.beforeEach(async ({ page, loginHelper }, testInfo) => { + orchestrator = new OrchestratorPage(page); + orchestratorUi = new OrchestratorUiPage(page); + await loginHelper.loginAsKeycloakUser(); + await ensureDataIndexOrSkip(testInfo.project.name, test); + }); + + test("Run failswitch and validate workflow runs", async ({ + uiHelper, + }) => { + test.setTimeout(FAILSWITCH_TEST_TIMEOUT_MS); + await orchestratorUi.gotoWorkflows(uiHelper, false); + await orchestrator.selectFailSwitchWorkflowItem(); + await orchestrator.runFailSwitchWorkflow("OK"); + await orchestrator.validateCurrentWorkflowStatus("Completed"); + await orchestrator.reRunFailSwitchWorkflow("Wait"); + await orchestrator.abortWorkflow(); + await orchestrator.reRunFailSwitchWorkflow("KO"); + await orchestrator.validateCurrentWorkflowStatus("Failed"); + await orchestratorUi.gotoWorkflows(uiHelper, false); + await orchestrator.selectFailSwitchWorkflowItem(); + await orchestrator.runFailSwitchWorkflow("Wait"); + await orchestrator.validateCurrentWorkflowStatus("Running"); + await orchestratorUi.gotoWorkflows(uiHelper, false); + await expect + .poll( + async () => { + try { + await orchestrator.validateWorkflowAllRuns(); + return true; + } catch { + await orchestratorUi.gotoWorkflows(uiHelper, false); + return false; + } + }, + { + timeout: WORKFLOW_ALL_RUNS_RETRY_TIMEOUT_MS, + intervals: [WORKFLOW_POLL_INTERVAL_MS], + }, + ) + .toBe(true); + await orchestrator.validateWorkflowAllRunsStatusIcons(); + }); + + test("Abort workflow", async ({ uiHelper }) => { + test.setTimeout(FAILSWITCH_TEST_TIMEOUT_MS); + await orchestratorUi.gotoWorkflows(uiHelper, false); + await expect(orchestratorUi.workflowLink(/Failswitch/i)).toBeVisible(); + await orchestrator.selectFailSwitchWorkflowItem(); + await orchestrator.runFailSwitchWorkflow("Wait"); + await orchestrator.abortWorkflow(); + }); + + test("Validate Running status details", async ({ uiHelper }) => { + test.setTimeout(FAILSWITCH_TEST_TIMEOUT_MS); + await orchestratorUi.gotoWorkflows(uiHelper, false); + await expect(orchestratorUi.workflowLink(/Failswitch/i)).toBeVisible(); + await orchestrator.selectFailSwitchWorkflowItem(); + await orchestrator.runFailSwitchWorkflow("Wait"); + await orchestrator.validateWorkflowStatusDetails("Running"); + }); + + test("Validate Failed status details", async ({ uiHelper }) => { + test.setTimeout(FAILSWITCH_TEST_TIMEOUT_MS); + await orchestratorUi.gotoWorkflows(uiHelper, false); + await expect(orchestratorUi.workflowLink(/Failswitch/i)).toBeVisible(); + await orchestrator.selectFailSwitchWorkflowItem(); + await orchestrator.runFailSwitchWorkflow("KO"); + await orchestrator.validateWorkflowStatusDetails("Failed"); + }); + + test("Validate Completed status details", async ({ uiHelper }) => { + test.setTimeout(FAILSWITCH_TEST_TIMEOUT_MS); + await orchestratorUi.gotoWorkflows(uiHelper, false); + await expect(orchestratorUi.workflowLink(/Failswitch/i)).toBeVisible(); + await orchestrator.selectFailSwitchWorkflowItem(); + await orchestrator.runFailSwitchWorkflow("OK"); + await orchestrator.validateWorkflowStatusDetails("Completed"); + }); + + test("Rerun failswitch from failure point", async ({ + uiHelper, + }, testInfo) => { + test.setTimeout(FAILSWITCH_RERUN_TIMEOUT_MS); + const ns = testInfo.project.name; + + test.skip(!ns, "NAME_SPACE not set"); + + const originalHttpbin = "https://httpbin.org/"; + try { + patchHttpbin(ns!, "https://foobar.org/"); + restartAndWait(ns!); + + await orchestratorUi.gotoWorkflows(uiHelper, false); + await expect( + orchestratorUi.workflowLink(/Failswitch/i), + ).toBeVisible(); + await orchestrator.selectFailSwitchWorkflowItem(); + await orchestrator.runFailSwitchWorkflow("Wait"); + await orchestrator.validateCurrentWorkflowStatus("Failed"); + + patchHttpbin(ns!, originalHttpbin); + restartAndWait(ns!); + + await orchestrator.reRunOnFailure("From failure point"); + await orchestrator.validateCurrentWorkflowStatus("Completed"); + } catch (e) { + console.error(`[rerun-failure] Test failed: ${e}`); + testInfo.annotations.push({ + type: "test-error", + description: String(e), + }); + throw e; + } finally { + try { + cleanupAfterTest(ns!, originalHttpbin); + } catch (cleanupErr) { + testInfo.annotations.push({ + type: "cleanup-error", + description: String(cleanupErr), + }); + } + } + }); + + test("Open suggested workflow from failswitch", async ({ uiHelper }) => { + test.setTimeout(FAILSWITCH_TEST_TIMEOUT_MS); + await orchestratorUi.gotoWorkflows(uiHelper, false); + await expect(orchestratorUi.workflowLink(/Failswitch/i)).toBeVisible(); + await orchestrator.selectFailSwitchWorkflowItem(); + await orchestrator.runFailSwitchWorkflow("OK"); + await orchestratorUi.openSuggestedWorkflowAndRun(/greeting/i); + await orchestratorUi.expectWorkflowHeading("Greeting workflow"); + await orchestratorUi.expectWorkflowWizardVisible(); + }); + }); + + test.describe("Token propagation workflow API", () => { + test("Execute token propagation workflow via API", async ({ + page, + loginHelper, + }) => { + test.setTimeout(TOKEN_PROPAGATION_TIMEOUT_MS); + + await loginHelper.loginAsKeycloakUser(); + + const backstageToken = await new AuthApiHelper(page).getToken(); + + const kcBaseUrl = requireEnvVar("KEYCLOAK_BASE_URL"); + const kcRealm = requireEnvVar("KEYCLOAK_REALM"); + const kcClientId = requireEnvVar("KEYCLOAK_CLIENT_ID"); + const kcClientSecret = requireEnvVar("KEYCLOAK_CLIENT_SECRET"); + const username = getEnvVarDefault("GH_USER_ID", "test1"); + const password = getEnvVarDefault("GH_USER_PASS", "test1@123"); + + const tokenUrl = `${kcBaseUrl}/realms/${kcRealm}/protocol/openid-connect/token`; + const tokenRequestForm: Record = { + username, + password, + scope: "openid", + }; + tokenRequestForm["grant_type"] = "password"; + tokenRequestForm["client_id"] = kcClientId; + tokenRequestForm["client_secret"] = kcClientSecret; + + const tokenResponse = await page.request.post(tokenUrl, { + form: tokenRequestForm, + }); + await assertApiResponseOk(tokenResponse, "Keycloak token request"); + const tokenBody = await tokenResponse.json(); + const oidcToken = tokenBody.access_token; + expect(oidcToken).toBeTruthy(); + + const executeResponse = await page.request.post( + `/api/orchestrator/v2/workflows/token-propagation/execute`, + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${backstageToken}`, + }, + data: { + inputData: {}, + authTokens: [ + { provider: "OAuth2", token: oidcToken }, + { + provider: "SimpleBearerToken", + token: "test-simple-bearer-token-value", + }, + ], + }, + }, + ); + await assertApiResponseOk(executeResponse, "Workflow execution"); + const { id: instanceId } = await executeResponse.json(); + expect(instanceId).toBeTruthy(); + + const pollTimeoutMs = + WORKFLOW_POLL_MAX_ATTEMPTS * WORKFLOW_POLL_INTERVAL_MS; + const statusBody = await pollWorkflowInstanceStatus( + page, + backstageToken, + instanceId, + pollTimeoutMs, + WORKFLOW_POLL_INTERVAL_MS, + ); + expect(statusBody.state).toBe("COMPLETED"); + + expect(statusBody.workflowdata.result.completedWith).toBe("success"); + expect(statusBody.workflowdata.result.message).toContain( + "Token propagated", + ); + + assertExpectedWorkflowNodes(statusBody); + assertOpenShiftSampleServerLogs(statusBody); + }); + }); + + test.describe("Workflow all runs", () => { + let orchestrator: OrchestratorPage; + let orchestratorUi: OrchestratorUiPage; + + test.beforeEach(async ({ page, loginHelper }) => { + orchestrator = new OrchestratorPage(page); + orchestratorUi = new OrchestratorUiPage(page); + await loginHelper.loginAsKeycloakUser(); + }); + + test("Validate Workflow All Runs", async ({ uiHelper }) => { + await orchestratorUi.gotoWorkflows(uiHelper, false); + await expect( + orchestratorUi.workflowLink(/workflow/i).first(), + ).toBeVisible(); + await orchestrator.validateWorkflowAllRuns(); + }); + }); + + registerOrchestratorEntityWorkflowTests(); + }); +} diff --git a/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator.spec.ts b/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator.spec.ts new file mode 100644 index 000000000..7dec4dc55 --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tests/specs/orchestrator.spec.ts @@ -0,0 +1,33 @@ +import { test } from "@red-hat-developer-hub/e2e-test-utils/test"; +import { + deploySonataflow, + logOrchestratorDeployFailureDiagnostics, +} from "./rbac-helpers.js"; +import { workflowTests } from "./orchestrator-workflow.tests.js"; +import { rbacTests } from "./orchestrator-rbac.tests.js"; + +test.describe("Orchestrator", () => { + test.beforeAll(async ({ rhdh }, testInfo) => { + test.setTimeout(40 * 60 * 1000); + await test.runOnce("orchestrator-setup", async () => { + const project = rhdh.deploymentConfig.namespace; + await rhdh.configure({ auth: "keycloak" }); + await deploySonataflow(project); + process.env.SONATAFLOW_DATA_INDEX_URL = + "http://sonataflow-platform-data-index-service.orchestrator.svc.cluster.local"; + try { + await rhdh.deploy({ timeout: 900_000 }); + } catch (err) { + logOrchestratorDeployFailureDiagnostics(project); + throw err; + } + }); + testInfo.annotations.push({ + type: "component", + description: "orchestrator", + }); + }); + + workflowTests(); + rbacTests(); +}); diff --git a/workspaces/orchestrator/e2e-tests/tests/specs/rbac-helpers.ts b/workspaces/orchestrator/e2e-tests/tests/specs/rbac-helpers.ts new file mode 100644 index 000000000..64baf108c --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tests/specs/rbac-helpers.ts @@ -0,0 +1,313 @@ +import { + test, + expect, + Browser, + TestInfo, + Page, +} from "@red-hat-developer-hub/e2e-test-utils/test"; +import { + setupBrowser, + LoginHelper, + UIhelper, + AuthApiHelper, + RbacApiHelper, + Policy, + Response, +} from "@red-hat-developer-hub/e2e-test-utils/helpers"; +export { + deploySonataflow, + runOc, + logOrchestratorDeployFailureDiagnostics, +} from "../support/utils/deployment-helpers.js"; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +export const PRIMARY_USER = `user:default/${process.env.PRIMARY_TEST_USER || "test1"}`; +export const SECONDARY_USER = `user:default/${process.env.SECONDARY_TEST_USER || "test2"}`; + +export const BASELINE_ROLE_NAME = "role:default/orchestrator-baseline"; + +// --------------------------------------------------------------------------- +// RBAC helpers +// --------------------------------------------------------------------------- + +export type PolicySpec = { + permission: string; + policy: string; + effect: string; +}; + +export function globalWorkflowPolicies( + readEffect: "allow" | "deny", + useEffect: "allow" | "deny", +): PolicySpec[] { + return [ + { + permission: "orchestrator.workflow", + policy: "read", + effect: readEffect, + }, + { + permission: "orchestrator.workflow.use", + policy: "update", + effect: useEffect, + }, + ]; +} + +export function greetingWorkflowPolicies( + readEffect: "allow" | "deny", + useEffect: "allow" | "deny", +): PolicySpec[] { + return [ + { + permission: "orchestrator.workflow.greeting", + policy: "read", + effect: readEffect, + }, + { + permission: "orchestrator.workflow.use.greeting", + policy: "update", + effect: useEffect, + }, + ]; +} + +export function roleApiName(roleName: string): string { + return roleName.replace("role:", "").replace("default/", ""); +} + +export function buildPolicies(roleName: string, specs: PolicySpec[]) { + return specs.map((spec) => ({ entityReference: roleName, ...spec })); +} + +export async function createRoleWithPolicies( + apiToken: string, + roleName: string, + memberReferences: string[], + policySpecs: PolicySpec[], +): Promise { + const rbacApi = await RbacApiHelper.build(apiToken); + const rolePostResponse = await rbacApi.createRoles({ + memberReferences, + name: roleName, + }); + const policyPostResponse = await rbacApi.createPolicies( + buildPolicies(roleName, policySpecs), + ); + expect(rolePostResponse.ok()).toBeTruthy(); + expect(policyPostResponse.ok()).toBeTruthy(); +} + +export async function verifyRoleWithPolicies( + apiToken: string, + roleName: string, + expectedMembers: string[], + expectedPolicies: PolicySpec[], +): Promise { + const rbacApi = await RbacApiHelper.build(apiToken); + + const rolesResponse = await rbacApi.getRoles(); + expect(rolesResponse.ok()).toBeTruthy(); + + const roles = await rolesResponse.json(); + const workflowRole = roles.find( + (role: { name: string; memberReferences: string[] }) => + role.name === roleName, + ); + expect(workflowRole).toBeDefined(); + for (const member of expectedMembers) { + expect(workflowRole?.memberReferences).toContain(member); + } + + const policiesResponse = await rbacApi.getPoliciesByRole( + roleApiName(roleName), + ); + expect(policiesResponse.ok()).toBeTruthy(); + + const policies = await policiesResponse.json(); + expect(policies).toHaveLength(expectedPolicies.length); + + for (const expectedPolicy of expectedPolicies) { + const actualPolicy = policies.find( + (policy: { permission: string; policy: string; effect: string }) => + policy.permission === expectedPolicy.permission && + policy.policy === expectedPolicy.policy, + ); + expect(actualPolicy).toBeDefined(); + expect(actualPolicy.effect).toBe(expectedPolicy.effect); + } +} + +const BASELINE_POLICIES = buildPolicies(BASELINE_ROLE_NAME, [ + { permission: "orchestrator.workflow", policy: "read", effect: "allow" }, + { + permission: "orchestrator.workflow.use", + policy: "update", + effect: "allow", + }, + { permission: "catalog-entity", policy: "read", effect: "allow" }, + { permission: "catalog.entity.create", policy: "create", effect: "allow" }, + { permission: "catalog.location.read", policy: "read", effect: "allow" }, + { permission: "catalog.location.create", policy: "create", effect: "allow" }, + { + permission: "scaffolder.action.execute", + policy: "use", + effect: "allow", + }, + { permission: "scaffolder.task.create", policy: "create", effect: "allow" }, + { permission: "scaffolder.task.read", policy: "read", effect: "allow" }, + { + permission: "scaffolder.template.parameter.read", + policy: "read", + effect: "allow", + }, + { + permission: "scaffolder.template.step.read", + policy: "read", + effect: "allow", + }, +]); + +async function withTempPage( + browser: Browser, + fn: (page: Awaited>) => Promise, +): Promise { + const context = await browser.newContext({ + baseURL: process.env.RHDH_BASE_URL, + ignoreHTTPSErrors: true, + }); + const page = await context.newPage(); + try { + await fn(page); + } finally { + await context.close(); + } +} + +export async function setupAuthenticatedPage( + browser: Browser, + testInfo: TestInfo, +): Promise<{ + page: Page; + uiHelper: UIhelper; + loginHelper: LoginHelper; + apiToken: string; +}> { + const { page } = await setupBrowser(browser, testInfo); + const uiHelper = new UIhelper(page); + const loginHelper = new LoginHelper(page); + + // #region agent log + fetch("http://127.0.0.1:7243/ingest/6da3078a-a822-462b-a16f-4189672bc261", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Debug-Session-Id": "eb40fb", + }, + body: JSON.stringify({ + sessionId: "eb40fb", + runId: "pre-fix", + hypothesisId: "H3", + location: "rbac-helpers.ts:setupAuthenticatedPage:attempt-start", + message: "starting original auth setup flow", + data: { pageClosed: page.isClosed(), url: page.url() }, + timestamp: Date.now(), + }), + }).catch(() => {}); + // #endregion + + await loginHelper.loginAsKeycloakUser(); + const apiToken = await new AuthApiHelper(page).getToken(); + + // #region agent log + fetch("http://127.0.0.1:7243/ingest/6da3078a-a822-462b-a16f-4189672bc261", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Debug-Session-Id": "eb40fb", + }, + body: JSON.stringify({ + sessionId: "eb40fb", + runId: "pre-fix", + hypothesisId: "H3", + location: "rbac-helpers.ts:setupAuthenticatedPage:token-ready", + message: "original auth setup flow completed", + data: { tokenPresent: Boolean(apiToken), pageClosed: page.isClosed() }, + timestamp: Date.now(), + }), + }).catch(() => {}); + // #endregion + + return { page, uiHelper, loginHelper, apiToken }; +} + +export async function deleteRoleAndPolicies( + apiToken: string, + roleName: string, +): Promise { + const rbacApi = await RbacApiHelper.build(apiToken); + const apiName = roleApiName(roleName); + try { + const policiesResponse = await rbacApi.getPoliciesByRole(apiName); + if (policiesResponse.ok()) { + const policies = + await Response.removeMetadataFromResponse(policiesResponse); + await rbacApi.deletePolicy(apiName, policies as Policy[]); + } + await rbacApi.deleteRole(apiName); + } catch { + // role may not exist yet + } +} + +export async function ensureBaselineRole( + browser: Browser, + _testInfo: TestInfo, +): Promise { + await test.runOnce("rbac-baseline-setup", async () => { + await withTempPage(browser, async (page) => { + const loginHelper = new LoginHelper(page); + await loginHelper.loginAsKeycloakUser(); + const token = await new AuthApiHelper(page).getToken(); + const rbacApi = await RbacApiHelper.build(token); + + await rbacApi.createRoles({ + memberReferences: [PRIMARY_USER], + name: BASELINE_ROLE_NAME, + }); + + await rbacApi.createPolicies(BASELINE_POLICIES); + }); + }); +} + +export async function removeBaselineRole( + browser: Browser, + _testInfo: TestInfo, +): Promise { + await test.runOnce("rbac-baseline-cleanup", async () => { + await withTempPage(browser, async (page) => { + const loginHelper = new LoginHelper(page); + await loginHelper.loginAsKeycloakUser(); + const token = await new AuthApiHelper(page).getToken(); + await deleteRoleAndPolicies(token, BASELINE_ROLE_NAME); + + const rbacApi = await RbacApiHelper.build(token); + const verifyResponse = await rbacApi.getRoles(); + if (verifyResponse.ok()) { + const roles = await verifyResponse.json(); + const found = roles.find( + (r: { name: string }) => r.name === BASELINE_ROLE_NAME, + ); + if (found) { + console.warn( + "[rbac-baseline] WARNING: Baseline role was NOT removed successfully!", + ); + } + } + }); + }); +} diff --git a/workspaces/orchestrator/e2e-tests/tests/specs/timeouts.ts b/workspaces/orchestrator/e2e-tests/tests/specs/timeouts.ts new file mode 100644 index 000000000..0f675b581 --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tests/specs/timeouts.ts @@ -0,0 +1,18 @@ +export const SUITE_SETUP_TIMEOUT_MS = 40 * 60 * 1000; +export const WORKFLOW_RUN_TIMEOUT_MS = 150 * 1000; +export const FAILSWITCH_TEST_TIMEOUT_MS = 180 * 1000; +export const FAILSWITCH_RERUN_TIMEOUT_MS = 240 * 1000; +export const TOKEN_PROPAGATION_TIMEOUT_MS = 5 * 60 * 1000; + +export const WORKFLOW_POLL_INTERVAL_MS = 5 * 1000; +export const WORKFLOW_POLL_MAX_ATTEMPTS = 30; +export const WORKFLOW_ALL_RUNS_RETRY_TIMEOUT_MS = 15 * 1000; + +export const RBAC_ROLE_PROPAGATION_TIMEOUT_MS = 10 * 1000; +export const RBAC_ROLE_PROPAGATION_INTERVAL_MS = 1 * 1000; +export const RBAC_MEMBERSHIP_UPDATE_TIMEOUT_MS = 10 * 1000; + +export const LONG_WAIT_TIMEOUT_MS = 30 * 1000; +export const TEMPLATE_RUN_TIMEOUT_MS = 240 * 1000; +export const TEMPLATE_SUBMIT_TIMEOUT_FIRST_MS = 90 * 1000; +export const TEMPLATE_SUBMIT_TIMEOUT_RETRY_MS = 120 * 1000; diff --git a/workspaces/orchestrator/e2e-tests/tests/support/pages/orchestrator-obj.ts b/workspaces/orchestrator/e2e-tests/tests/support/pages/orchestrator-obj.ts new file mode 100644 index 000000000..fca06095c --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tests/support/pages/orchestrator-obj.ts @@ -0,0 +1,75 @@ +import { + type Locator, + type Page, +} from "@red-hat-developer-hub/e2e-test-utils/test"; + +export const ORCHESTRATOR_COMPONENTS = { + getWorkflowsHeading: (page: Page): Locator => + page.getByRole("heading", { name: "Workflows" }), + + getWorkflowLink: (page: Page, workflowName: string | RegExp): Locator => + page.getByRole("link", { name: workflowName }), + + getRunButton: (page: Page): Locator => + page.getByRole("button", { name: "Run" }), + + getNextButton: (page: Page): Locator => + page.getByRole("button", { name: "Next" }), + + getWorkflowsTab: (page: Page): Locator => + page.getByRole("tab", { name: "Workflows" }), + + getBreadcrumbNav: (page: Page): Locator => + page.getByRole("navigation", { name: /breadcrumb/i }), + + getSuggestedNextWorkflowHeading: (page: Page): Locator => + page.getByRole("heading", { name: /suggested next workflow/i }), + + getWorkflowSuggestionDialog: ( + page: Page, + workflowName: string | RegExp, + ): Locator => page.getByRole("dialog", { name: workflowName }), + + getRunWorkflowButton: (page: Page): Locator => + page.getByRole("button", { name: /run workflow/i }), + + getUnauthorizedIndicators: (page: Page): Locator[] => [ + page.getByText(/unauthorized|not authorized/i), + page.getByText(/access denied|permission denied/i), + page.getByText(/forbidden/i), + ], +}; + +export const CATALOG_TEMPLATE_COMPONENTS = { + getTemplateLink: (page: Page, templateName: string | RegExp): Locator => + page.getByRole("link", { name: templateName }).first(), +}; + +export const SELFSERVICE_TEMPLATE_COMPONENTS = { + getSelfServiceLink: (page: Page): Locator => + page.getByRole("link", { name: "Self-service", exact: true }), + + getChooseButtonInCard: (page: Page, templateTitle: string): Locator => + page + .locator("article") + .filter({ + has: page.getByRole("heading", { + name: new RegExp(templateTitle, "i"), + }), + }) + .getByRole("button", { name: "Choose", exact: true }) + .first(), + + getNameField: (page: Page): Locator => page.getByLabel("Name"), + + getLanguageField: (page: Page): Locator => page.getByLabel("Language"), + + getEnglishOption: (page: Page): Locator => + page.getByRole("option", { name: "English" }), + + getReviewButton: (page: Page): Locator => + page.getByRole("button", { name: /Review/i }), + + getCreateButton: (page: Page): Locator => + page.getByRole("button", { name: /Create/i }), +}; diff --git a/workspaces/orchestrator/e2e-tests/tests/support/pages/orchestrator-po.ts b/workspaces/orchestrator/e2e-tests/tests/support/pages/orchestrator-po.ts new file mode 100644 index 000000000..4e8ce4699 --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tests/support/pages/orchestrator-po.ts @@ -0,0 +1,540 @@ +import { expect, Page } from "@red-hat-developer-hub/e2e-test-utils/test"; +import { UIhelper } from "@red-hat-developer-hub/e2e-test-utils/helpers"; +import { OrchestratorPage } from "@red-hat-developer-hub/e2e-test-utils/pages"; +import { + CATALOG_TEMPLATE_COMPONENTS, + ORCHESTRATOR_COMPONENTS, + SELFSERVICE_TEMPLATE_COMPONENTS, +} from "./orchestrator-obj.js"; +import { ORCHESTRATOR_PAGE } from "./orchestrator.js"; +import { waitForScaffolderTerminalState } from "../utils/scaffolder-helpers.js"; + +type LaunchTemplateOptions = { + entityName?: string; + selectEnglishIfVisible?: boolean; + timeoutMs?: number; +}; + +type RunButtonState = "enabled" | "disabled" | "absent" | "disabledOrAbsent"; + +export class OrchestratorUiPage { + constructor(private readonly page: Page) {} + + runButton() { + return ORCHESTRATOR_COMPONENTS.getRunButton(this.page); + } + + workflowLink(name: string | RegExp) { + return ORCHESTRATOR_COMPONENTS.getWorkflowLink(this.page, name); + } + + async gotoWorkflows(uiHelper: UIhelper, reload = true): Promise { + if (reload) { + await this.page.reload(); + } + await uiHelper.goToPageUrl("/orchestrator"); + await uiHelper.verifyHeading(ORCHESTRATOR_PAGE.workflowsHeading); + } + + async selectGreetingWorkflow( + orchestratorPage: OrchestratorPage, + ): Promise { + await orchestratorPage.selectGreetingWorkflowItem(); + await this.expectWorkflowHeading(ORCHESTRATOR_PAGE.greetingWorkflowName); + } + + async expectWorkflowHeading(name: string | RegExp): Promise { + await expect(this.page.getByRole("heading", { name })).toBeVisible(); + } + + async openWorkflow(name: string | RegExp): Promise { + const workflow = this.workflowLink(name); + await expect(workflow).toBeVisible({ timeout: 30_000 }); + await workflow.click(); + } + + async expectWorkflowVisible(name: string | RegExp): Promise { + await expect(this.workflowLink(name)).toBeVisible({ timeout: 30_000 }); + } + + async expectWorkflowAbsent(name: string | RegExp): Promise { + await expect(this.workflowLink(name)).toHaveCount(0); + } + + async verifyRunButtonState(state: RunButtonState): Promise { + const runButton = this.runButton(); + const buttonCount = await runButton.count(); + + if (state === "absent") { + await expect(runButton).toHaveCount(0); + return; + } + + if (state === "disabledOrAbsent") { + if (buttonCount === 0) return; + await expect(runButton).toBeDisabled(); + return; + } + + await expect(runButton).toBeVisible(); + + if (state === "enabled") { + await expect(runButton).toBeEnabled(); + return; + } + + await expect(runButton).toBeDisabled(); + } + + async expectRunButtonEnabled(): Promise { + await this.verifyRunButtonState("enabled"); + } + + async expectRunButtonDisabled(): Promise { + await this.verifyRunButtonState("disabled"); + } + + async expectRunButtonAbsent(): Promise { + await this.verifyRunButtonState("absent"); + } + + async expectRunButtonDisabledOrAbsent(): Promise { + await this.verifyRunButtonState("disabledOrAbsent"); + } + + async clickRunButton(): Promise { + await this.verifyRunButtonState("enabled"); + await this.runButton().click(); + } + + async runCurrentWorkflowFromWizard(): Promise { + await ORCHESTRATOR_COMPONENTS.getNextButton(this.page).click(); + await this.clickRunButton(); + } + + async openTemplateFromCatalog( + uiHelper: UIhelper, + templateName: string | RegExp, + maxAttempts = 3, + ): Promise { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + await uiHelper.openSidebar("Catalog"); + await uiHelper.verifyHeading(/Catalog|My Org Catalog|All/); + await uiHelper.selectMuiBox("Kind", "Template"); + + const templateLink = CATALOG_TEMPLATE_COMPONENTS.getTemplateLink( + this.page, + templateName, + ); + const visible = await templateLink + .isVisible({ timeout: attempt === maxAttempts ? 30_000 : 10_000 }) + .catch(() => false); + + if (visible) { + await templateLink.click(); + await this.page.waitForLoadState("domcontentloaded"); + return; + } + + await this.page.reload({ waitUntil: "domcontentloaded" }); + } + + throw new Error( + `Template link ${templateName.toString()} was not visible in Catalog after ${maxAttempts} attempts`, + ); + } + + async openGreetingTemplateFromCatalog(uiHelper: UIhelper): Promise { + await this.openTemplateFromCatalog( + uiHelper, + ORCHESTRATOR_PAGE.greetingTemplateName, + ); + } + + async openTemplateFromSelfService( + uiHelper: UIhelper, + templateTitle: string | RegExp = ORCHESTRATOR_PAGE.greetingTemplateName, + ): Promise { + await SELFSERVICE_TEMPLATE_COMPONENTS.getSelfServiceLink(this.page).click(); + await uiHelper.verifyHeading(ORCHESTRATOR_PAGE.selfServiceHeading); + await this.page.waitForLoadState("domcontentloaded"); + + const templateTitleText = + templateTitle instanceof RegExp ? templateTitle.source : templateTitle; + const escapedTemplateTitle = templateTitleText.replace( + /[.*+?^${}()|[\]\\]/g, + "\\$&", + ); + const exactTemplateTitleRegex = new RegExp( + `^\\s*${escapedTemplateTitle}\\s*$`, + "i", + ); + + const exactHeading = this.page.getByRole("heading", { + name: exactTemplateTitleRegex, + }); + const exactHeadingCount = await exactHeading.count(); + + const matchingCards = this.page.locator("article").filter({ + has: this.page.getByRole("heading", { + name: new RegExp(templateTitleText, "i"), + }), + }); + const matchingCardCount = await matchingCards.count(); + const chooseInMatchingCards = matchingCards.getByRole("button", { + name: "Choose", + exact: true, + }); + const chooseCountInMatchingCards = await chooseInMatchingCards.count(); + const cardHeadingTexts = await matchingCards + .locator("h1, h2, h3, h4, h5, h6") + .allInnerTexts(); + const cardCount = await this.page.locator("article").count(); + let chosenCardIndex = -1; + for (let i = 0; i < cardCount; i++) { + const card = this.page.locator("article").nth(i); + const cardHeadings = await card + .locator("h1, h2, h3, h4, h5, h6") + .allInnerTexts(); + const hasExactHeading = cardHeadings.some( + (heading) => + heading.trim().toLowerCase() === templateTitleText.toLowerCase(), + ); + const hasChoose = + (await card + .getByRole("button", { name: "Choose", exact: true }) + .count()) > 0; + if (hasExactHeading && hasChoose) { + chosenCardIndex = i; + break; + } + } + console.warn("[debug-eb40fb][H2] pre-click self-service cards", { + templateTitleText, + matchingCardCount, + chooseCountInMatchingCards, + exactHeadingCount, + chosenCardIndex, + cardHeadingTexts, + url: this.page.url(), + }); + // #region agent log + fetch("http://127.0.0.1:7243/ingest/6da3078a-a822-462b-a16f-4189672bc261", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Debug-Session-Id": "eb40fb", + }, + body: JSON.stringify({ + sessionId: "eb40fb", + runId: "pre-fix", + hypothesisId: "H2", + location: "orchestrator-po.ts:openTemplateFromSelfService:pre-click", + message: "self-service card and choose counts before click", + data: { + templateTitleText, + matchingCardCount, + chooseCountInMatchingCards, + url: this.page.url(), + }, + timestamp: Date.now(), + }), + }).catch(() => {}); + // #endregion + + if (chosenCardIndex >= 0) { + await this.page + .locator("article") + .nth(chosenCardIndex) + .getByRole("button", { name: "Choose", exact: true }) + .first() + .click(); + } else { + await SELFSERVICE_TEMPLATE_COMPONENTS.getChooseButtonInCard( + this.page, + templateTitleText, + ).click(); + } + // #region agent log + fetch("http://127.0.0.1:7243/ingest/6da3078a-a822-462b-a16f-4189672bc261", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Debug-Session-Id": "eb40fb", + }, + body: JSON.stringify({ + sessionId: "eb40fb", + runId: "pre-fix", + hypothesisId: "H1", + location: "orchestrator-po.ts:openTemplateFromSelfService:post-click", + message: "self-service choose click completed", + data: { templateTitleText, url: this.page.url() }, + timestamp: Date.now(), + }), + }).catch(() => {}); + // #endregion + const templateHeading = this.page + .locator("h1, h2, h3, h4, h5, h6") + .filter({ hasText: new RegExp(templateTitleText, "i") }) + .first(); + const templateHeadingVisible = await templateHeading + .isVisible({ timeout: 5_000 }) + .catch(() => false); + + if (templateHeadingVisible) { + // #region agent log + fetch( + "http://127.0.0.1:7243/ingest/6da3078a-a822-462b-a16f-4189672bc261", + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Debug-Session-Id": "eb40fb", + }, + body: JSON.stringify({ + sessionId: "eb40fb", + runId: "pre-fix", + hypothesisId: "H1", + location: + "orchestrator-po.ts:openTemplateFromSelfService:heading-success", + message: "template heading verification succeeded", + data: { + templateTitleText, + url: this.page.url(), + matchedHeading: "templateTitle", + }, + timestamp: Date.now(), + }), + }, + ).catch(() => {}); + // #endregion + return; + } + + try { + await expect( + this.page.getByRole("heading", { name: /Greeting workflow/i }), + ).toBeVisible({ timeout: 30_000 }); + // #region agent log + fetch( + "http://127.0.0.1:7243/ingest/6da3078a-a822-462b-a16f-4189672bc261", + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Debug-Session-Id": "eb40fb", + }, + body: JSON.stringify({ + sessionId: "eb40fb", + runId: "pre-fix", + hypothesisId: "H1", + location: + "orchestrator-po.ts:openTemplateFromSelfService:heading-fallback", + message: "fallback heading verification succeeded", + data: { + templateTitleText, + url: this.page.url(), + matchedHeading: "greetingWorkflow", + }, + timestamp: Date.now(), + }), + }, + ).catch(() => {}); + // #endregion + } catch (error) { + const headingCount = await this.page + .locator("h1, h2, h3, h4, h5, h6") + .filter({ hasText: new RegExp(templateTitleText, "i") }) + .count(); + const visibleHeadings = await this.page + .locator("h1, h2, h3, h4, h5, h6") + .allInnerTexts(); + const selfServiceHeadingCount = await this.page + .getByRole("heading", { name: /self-service/i }) + .count(); + console.warn("[debug-eb40fb][H1] heading verify failed after choose", { + templateTitleText, + url: this.page.url(), + headingCount, + selfServiceHeadingCount, + visibleHeadings, + }); + // #region agent log + fetch( + "http://127.0.0.1:7243/ingest/6da3078a-a822-462b-a16f-4189672bc261", + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Debug-Session-Id": "eb40fb", + }, + body: JSON.stringify({ + sessionId: "eb40fb", + runId: "pre-fix", + hypothesisId: "H1", + location: + "orchestrator-po.ts:openTemplateFromSelfService:heading-failure", + message: "template heading verification failed", + data: { + templateTitleText, + url: this.page.url(), + headingCount, + errorName: error instanceof Error ? error.name : "UnknownError", + errorMessage: + error instanceof Error ? error.message : String(error), + }, + timestamp: Date.now(), + }), + }, + ).catch(() => {}); + // #endregion + throw error; + } + } + + async openGreetingTemplateFromSelfService(uiHelper: UIhelper): Promise { + await this.openTemplateFromSelfService( + uiHelper, + ORCHESTRATOR_PAGE.greetingTemplateName, + ); + } + + async launchGreetingTemplateFromSelfService( + uiHelper: UIhelper, + options: LaunchTemplateOptions = {}, + ): Promise { + await this.openGreetingTemplateFromSelfService(uiHelper); + await this.fillGreetingTemplateForm(options); + await this.submitTemplate(options.timeoutMs); + } + + async fillGreetingTemplateForm( + options: Omit = {}, + ): Promise { + const { entityName, selectEnglishIfVisible = true } = options; + if (entityName) { + const nameField = SELFSERVICE_TEMPLATE_COMPONENTS.getNameField(this.page); + const hasNameField = (await nameField.count()) > 0; + if (hasNameField) { + await expect(nameField).toBeVisible({ timeout: 10_000 }); + await nameField.fill(entityName); + } + } + + if (!selectEnglishIfVisible) return; + const languageField = SELFSERVICE_TEMPLATE_COMPONENTS.getLanguageField( + this.page, + ); + const hasLanguageField = (await languageField.count()) > 0; + if (!hasLanguageField) return; + + const isVisible = await languageField + .first() + .isVisible({ timeout: 5_000 }) + .catch(() => false); + if (!isVisible) return; + + await languageField.click(); + await SELFSERVICE_TEMPLATE_COMPONENTS.getEnglishOption(this.page).click(); + } + + async submitTemplate(timeoutMs?: number): Promise { + const reviewButton = SELFSERVICE_TEMPLATE_COMPONENTS.getReviewButton( + this.page, + ); + const hasReview = (await reviewButton.count()) > 0; + if (hasReview) { + const reviewVisible = await reviewButton + .first() + .isVisible({ timeout: 5_000 }) + .catch(() => false); + if (reviewVisible) { + await reviewButton.click(); + await this.page.waitForLoadState("domcontentloaded"); + } + } + const createButton = SELFSERVICE_TEMPLATE_COMPONENTS.getCreateButton( + this.page, + ); + await createButton.waitFor({ state: "visible", timeout: 10_000 }); + await createButton.click(); + await waitForScaffolderTerminalState(this.page, timeoutMs ?? 120_000); + } + + async openGreetingWorkflowFromEntityTab(): Promise { + const workflowLink = this.workflowLink( + ORCHESTRATOR_PAGE.greetingWorkflowName, + ); + await expect(workflowLink).toBeVisible({ timeout: 10_000 }); + await workflowLink.click(); + await this.expectWorkflowHeading(ORCHESTRATOR_PAGE.greetingWorkflowName); + } + + async clickWorkflowsTab(): Promise { + await ORCHESTRATOR_COMPONENTS.getWorkflowsTab(this.page).click(); + await this.page.waitForLoadState("load"); + } + + async expectWorkflowMissingInEntityTab(name: string | RegExp): Promise { + const workflowsTab = ORCHESTRATOR_COMPONENTS.getWorkflowsTab(this.page); + if ((await workflowsTab.count()) === 0) return; + await workflowsTab.click(); + await this.page.waitForLoadState("domcontentloaded"); + await expect(this.workflowLink(name)).toHaveCount(0); + } + + async hasUnauthorizedIndicator(): Promise { + const indicators = ORCHESTRATOR_COMPONENTS.getUnauthorizedIndicators( + this.page, + ); + for (const indicator of indicators) { + if ((await indicator.count()) > 0) { + return true; + } + } + return false; + } + + async openSuggestedWorkflowAndRun( + workflowName: string | RegExp, + ): Promise { + await expect( + ORCHESTRATOR_COMPONENTS.getSuggestedNextWorkflowHeading(this.page), + ).toBeVisible(); + await this.openWorkflow(workflowName); + await expect( + ORCHESTRATOR_COMPONENTS.getWorkflowSuggestionDialog( + this.page, + workflowName, + ), + ).toBeVisible(); + const runWorkflowButton = ORCHESTRATOR_COMPONENTS.getRunWorkflowButton( + this.page, + ); + await expect(runWorkflowButton).toBeVisible(); + await runWorkflowButton.click(); + } + + async expectWorkflowWizardVisible(): Promise { + await expect( + ORCHESTRATOR_COMPONENTS.getNextButton(this.page), + ).toBeVisible(); + } + + async openEntityFromBreadcrumb( + entityName: string, + expectedHeading: string | RegExp, + ): Promise { + const breadcrumb = ORCHESTRATOR_COMPONENTS.getBreadcrumbNav(this.page); + await expect(breadcrumb).toBeVisible(); + + const entityBreadcrumb = breadcrumb.getByText(entityName); + await expect(entityBreadcrumb).toBeVisible(); + + await entityBreadcrumb.click(); + await this.page.waitForLoadState("load"); + await expect( + this.page.getByRole("heading", { name: expectedHeading }), + ).toBeVisible(); + } +} diff --git a/workspaces/orchestrator/e2e-tests/tests/support/pages/orchestrator.ts b/workspaces/orchestrator/e2e-tests/tests/support/pages/orchestrator.ts new file mode 100644 index 000000000..0858175d9 --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tests/support/pages/orchestrator.ts @@ -0,0 +1,6 @@ +export const ORCHESTRATOR_PAGE = { + workflowsHeading: "Workflows", + greetingWorkflowName: "Greeting workflow", + greetingTemplateName: /Greeting Test Picker/i, + selfServiceHeading: "Self-service", +} as const; diff --git a/workspaces/orchestrator/e2e-tests/tests/support/utils/catalog-helpers.ts b/workspaces/orchestrator/e2e-tests/tests/support/utils/catalog-helpers.ts new file mode 100644 index 000000000..0f68cdaa3 --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tests/support/utils/catalog-helpers.ts @@ -0,0 +1,17 @@ +import { APIHelper } from "@red-hat-developer-hub/e2e-test-utils/helpers"; + +const GREETING_COMPONENT_LOCATION = + "https://github.com/testetson22/greeting_54mjks/blob/main/templates/greeting/skeleton/catalog-info.yaml"; + +export async function cleanupGreetingComponentEntity(): Promise { + try { + const locationId = await APIHelper.getLocationIdByTarget( + GREETING_COMPONENT_LOCATION, + ); + if (locationId) { + await APIHelper.deleteEntityLocationById(locationId); + } + } catch (e) { + console.warn("Cleanup of greeting-test-component location failed:", e); + } +} diff --git a/workspaces/orchestrator/e2e-tests/tests/support/utils/cluster-helpers.ts b/workspaces/orchestrator/e2e-tests/tests/support/utils/cluster-helpers.ts new file mode 100644 index 000000000..3bb60640c --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tests/support/utils/cluster-helpers.ts @@ -0,0 +1,200 @@ +import { execFileSync } from "node:child_process"; +import { runOc } from "./deployment-helpers.js"; + +export interface WorkflowNode { + name: string; + errorMessage: string | null; + exit: string | null; +} + +export interface WorkflowInstance { + state: string; + workflowdata: { + result: { + completedWith: string; + message: string; + }; + }; + nodes: WorkflowNode[]; + serviceUrl?: string; +} + +function isDataIndexHealthy(ns: string): boolean { + try { + const health = runOc( + [ + "exec", + "-n", + ns, + "deploy/sonataflow-platform-data-index-service", + "--", + "curl", + "-s", + "--max-time", + "5", + "http://localhost:8080/q/health/ready", + ], + 15_000, + ); + const parsed = JSON.parse(health); + return parsed.status === "UP"; + } catch { + return false; + } +} + +async function recoverDataIndex(ns: string): Promise { + try { + runOc( + [ + "rollout", + "restart", + "deploy/sonataflow-platform-data-index-service", + "-n", + ns, + ], + 15_000, + ); + runOc( + [ + "rollout", + "status", + "deploy/sonataflow-platform-data-index-service", + "-n", + ns, + "--timeout=120s", + ], + 130_000, + ); + for (let attempt = 0; attempt < 6; attempt++) { + await new Promise((resolve) => setTimeout(resolve, 5_000)); + if (isDataIndexHealthy(ns)) { + return true; + } + } + return false; + } catch { + return false; + } +} + +let dataIndexRecoveryFailed = false; + +export async function ensureDataIndexOrSkip( + ns: string, + testParam: { skip: (condition: boolean, reason: string) => void }, +): Promise { + if (dataIndexRecoveryFailed) { + testParam.skip( + true, + "Data-index recovery already failed earlier — skipping", + ); + return; + } + if (isDataIndexHealthy(ns)) return; + const recovered = await recoverDataIndex(ns); + if (!recovered) { + dataIndexRecoveryFailed = true; + } + testParam.skip( + !recovered, + "Data-index is unhealthy and could not be recovered — skipping workflow execution test", + ); +} + +export function getHttpbinValue(ns: string): string | undefined { + try { + const result = execFileSync( + "oc", + [ + "-n", + ns, + "get", + "sonataflow", + "failswitch", + "-o", + `jsonpath={.spec.podTemplate.container.env[?(@.name=='HTTPBIN')].value}`, + ], + { encoding: "utf-8", timeout: 30_000 }, + ); + const value = result.trim() || undefined; + return value; + } catch { + return undefined; + } +} + +export function patchHttpbin(ns: string, value: string): void { + let existing: Array<{ name: string; value: string }> = []; + try { + const raw = execFileSync( + "oc", + [ + "-n", + ns, + "get", + "sonataflow", + "failswitch", + "-o", + "jsonpath={.spec.podTemplate.container.env}", + ], + { encoding: "utf-8", timeout: 30_000 }, + ).trim(); + if (raw && raw !== "null" && raw !== "") { + existing = JSON.parse(raw); + } + } catch { + // ignore + } + const idx = existing.findIndex((e: { name: string }) => e.name === "HTTPBIN"); + if (idx >= 0) existing[idx] = { name: "HTTPBIN", value }; + else existing.push({ name: "HTTPBIN", value }); + const patch = JSON.stringify({ + spec: { podTemplate: { container: { env: existing } } }, + }); + execFileSync( + "oc", + [ + "-n", + ns, + "patch", + "sonataflow", + "failswitch", + "--type", + "merge", + "-p", + patch, + ], + { encoding: "utf-8", timeout: 30_000 }, + ); +} + +export function restartAndWait(ns: string): void { + execFileSync( + "oc", + ["-n", ns, "rollout", "restart", "deployment", "failswitch"], + { encoding: "utf-8", timeout: 30_000 }, + ); + + execFileSync( + "oc", + [ + "-n", + ns, + "rollout", + "status", + "deployment", + "failswitch", + "--timeout=60s", + ], + { encoding: "utf-8", timeout: 90_000 }, + ); +} + +export function cleanupAfterTest(ns: string, originalHttpbin: string): void { + const currentHttpbin = getHttpbinValue(ns); + if (currentHttpbin !== originalHttpbin) { + patchHttpbin(ns, originalHttpbin); + restartAndWait(ns); + } +} diff --git a/workspaces/orchestrator/e2e-tests/tests/support/utils/deployment-helpers.ts b/workspaces/orchestrator/e2e-tests/tests/support/utils/deployment-helpers.ts new file mode 100644 index 000000000..93c72c2c3 --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tests/support/utils/deployment-helpers.ts @@ -0,0 +1,604 @@ +import { execFileSync } from "node:child_process"; +import { readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { installOrchestrator } from "@red-hat-developer-hub/e2e-test-utils/orchestrator"; +import { $ } from "@red-hat-developer-hub/e2e-test-utils/utils"; +import { getEnvVarDefault } from "./env-helpers.js"; + +const WORKFLOW_REPO = + "https://github.com/rhdhorchestrator/serverless-workflows.git"; +const DEMO_WORKFLOW_REPO = + "https://github.com/rhdhorchestrator/orchestrator-demo.git"; +const WORKFLOW_REPO_REF = + getEnvVarDefault("SERVERLESS_WORKFLOWS_REF", "") || + "daeeee8dec16beab6d96a81774ef500081a2c2b0"; + +const MANIFEST_DIRS = [ + "workflows/greeting/manifests", + "workflows/fail-switch/src/main/resources/manifests", +]; + +const WORKFLOWS = ["greeting", "failswitch"]; + +/** Default SonataFlow operator Postgres secret; e2e uses `backstage-psql-secret` instead. */ +const UPSTREAM_WORKFLOW_PG_SECRET = "sonataflow-psql-postgresql"; +const E2E_WORKFLOW_PG_SECRET = "backstage-psql-secret"; + +export async function deploySonataflow(namespace: string): Promise { + await installOrchestrator(namespace); + + const oslFullVersion = detectOperatorVersion( + "operators.coreos.com/logic-operator.openshift-operators", + "operators.coreos.com/logic-operator-rhel8.openshift-operators", + ); + const oslMajorMinor = oslFullVersion.replace(/^(\d+\.\d+).*/, "$1") || ""; + + const osFullVersion = detectOperatorVersion( + "operators.coreos.com/serverless-operator.openshift-operators", + ); + const osMajorMinor = osFullVersion.replace(/^(\d+\.\d+).*/, "$1") || ""; + + if (oslMajorMinor && osMajorMinor && oslMajorMinor !== osMajorMinor) { + console.warn( + `[deploy-sonataflow] WARNING: OS (${osMajorMinor}) and OSL (${oslMajorMinor}) major.minor versions differ — this may cause Knative API incompatibilities`, + ); + } + + hardenSonataFlowPlatform(namespace); + + const workflowDir = `/tmp/serverless-workflows-${process.pid}`; + try { + await $`git clone --depth=1 ${WORKFLOW_REPO} ${workflowDir}`; + await $`git -C ${workflowDir} fetch --depth=1 origin ${WORKFLOW_REPO_REF}`; + await $`git -C ${workflowDir} checkout --detach ${WORKFLOW_REPO_REF}`; + + for (const rel of MANIFEST_DIRS) { + const fullPath = join(workflowDir, rel); + await $`oc apply -n ${namespace} -f ${fullPath}`; + } + } finally { + await $`rm -rf ${workflowDir}`.catch(() => {}); + } + + await waitForCRs(namespace); + + // Patch persistence before image alignment so the operator never materializes + // ReplicaSets that still reference upstream `sonataflow-psql-postgresql` (missing). + for (const workflow of WORKFLOWS) { + patchWorkflowPostgres(namespace, workflow); + } + + alignWorkflowImages(namespace, oslMajorMinor); + + // Image patch can trigger another reconcile; re-apply persistence for safety. + for (const workflow of WORKFLOWS) { + patchWorkflowPostgres(namespace, workflow); + } + + const postgresAlignTimeoutMs = 120_000; + for (const workflow of WORKFLOWS) { + await waitForWorkflowPostgresDeploymentAligned( + namespace, + workflow, + postgresAlignTimeoutMs, + ); + runOc( + ["rollout", "restart", `deployment/${workflow}`, "-n", namespace], + 60_000, + ); + await sleep(2_000); + runOc( + [ + "rollout", + "status", + `deployment/${workflow}`, + "-n", + namespace, + "--timeout=600s", + ], + 610_000, + ); + } + + await deployTokenPropagationWorkflow(namespace, postgresAlignTimeoutMs); +} + +function patchWorkflowPostgres(namespace: string, workflow: string): string { + const patch = JSON.stringify({ + spec: { + persistence: { + postgresql: { + secretRef: { + name: "backstage-psql-secret", + userKey: "POSTGRES_USER", + passwordKey: "POSTGRES_PASSWORD", + }, + serviceRef: { + name: "backstage-psql", + namespace, + databaseName: "backstage_plugin_orchestrator", + }, + }, + }, + }, + }); + return runOc([ + "-n", + namespace, + "patch", + "sonataflow", + workflow, + "--type", + "merge", + "-p", + patch, + ]); +} + +function parseOcJson( + args: string[], + timeoutMs: number, +): T | undefined { + try { + return JSON.parse(runOc(args, timeoutMs)) as T; + } catch { + return undefined; + } +} + +function sonataFlowUsesE2ePostgresSecret(cr: Record): boolean { + const spec = cr.spec as Record | undefined; + const persistence = spec?.persistence as Record | undefined; + const pg = persistence?.postgresql as Record | undefined; + const secretRef = pg?.secretRef as Record | undefined; + return secretRef?.name === E2E_WORKFLOW_PG_SECRET; +} + +/** True if the live Deployment pod template still references the operator default secret. */ +function deploymentPodTemplateReferencesUpstreamPgSecret( + deployment: Record, +): boolean { + const spec = deployment.spec as Record | undefined; + const template = spec?.template as Record | undefined; + if (!template) return false; + return JSON.stringify(template).includes(UPSTREAM_WORKFLOW_PG_SECRET); +} + +/** + * Wait until the SonataFlow CR and workflow Deployment both reflect the e2e Postgres + * wiring, re-applying the merge patch when the operator lags. Does not restart the rollout. + */ +async function waitForWorkflowPostgresDeploymentAligned( + namespace: string, + workflow: string, + timeoutMs: number, +): Promise { + const deadline = Date.now() + timeoutMs; + let attempt = 0; + while (Date.now() < deadline) { + attempt++; + const cr = parseOcJson>( + ["get", "sonataflow", workflow, "-n", namespace, "-o", "json"], + 15_000, + ); + const deployment = parseOcJson>( + ["get", "deployment", workflow, "-n", namespace, "-o", "json"], + 15_000, + ); + const crOk = cr && sonataFlowUsesE2ePostgresSecret(cr); + const depOk = + deployment && + !deploymentPodTemplateReferencesUpstreamPgSecret(deployment); + if (crOk && depOk) { + return; + } + patchWorkflowPostgres(namespace, workflow); + await sleep(2_000); + } + console.warn( + `[deploy-sonataflow] TIMEOUT (${timeoutMs}ms): workflow "${workflow}" not aligned on ${E2E_WORKFLOW_PG_SECRET} (SonataFlow CR + Deployment template; attempts=${attempt})`, + ); +} + +async function waitForCRs(namespace: string): Promise { + const deadline = Date.now() + 60_000; + let attempt = 0; + while (Date.now() < deadline) { + attempt++; + try { + const out = runOc(["get", "sonataflow", "-n", namespace, "--no-headers"]); + const found = out.split("\n").filter(Boolean).length; + if (found >= WORKFLOWS.length) { + return; + } + } catch { + // not available yet + } + await sleep(5_000); + } + console.warn( + `[deploy-sonataflow] TIMEOUT: Only found fewer than ${WORKFLOWS.length} SonataFlow CRs after ${attempt} attempts`, + ); +} + +function hardenSonataFlowPlatform(namespace: string): void { + try { + const sfpPatch = JSON.stringify({ + spec: { + services: { + dataIndex: { + podTemplate: { + container: { + resources: { + requests: { memory: "64Mi", cpu: "250m" }, + limits: { memory: "1Gi", cpu: "500m" }, + }, + livenessProbe: { + failureThreshold: 200, + httpGet: { + path: "/q/health/live", + port: 8080, + scheme: "HTTP", + }, + periodSeconds: 10, + timeoutSeconds: 10, + }, + readinessProbe: { + failureThreshold: 200, + httpGet: { + path: "/q/health/ready", + port: 8080, + scheme: "HTTP", + }, + periodSeconds: 10, + timeoutSeconds: 10, + }, + }, + }, + }, + jobService: { + podTemplate: { + container: { + resources: { + requests: { memory: "64Mi", cpu: "250m" }, + limits: { memory: "1Gi", cpu: "500m" }, + }, + }, + }, + }, + }, + }, + }); + runOc([ + "-n", + namespace, + "patch", + "sonataflowplatform", + "sonataflow-platform", + "--type", + "merge", + "-p", + sfpPatch, + ]); + runOc( + [ + "rollout", + "status", + "deployment/sonataflow-platform-data-index-service", + "-n", + namespace, + "--timeout=300s", + ], + 310_000, + ); + runOc( + [ + "rollout", + "status", + "deployment/sonataflow-platform-jobs-service", + "-n", + namespace, + "--timeout=300s", + ], + 310_000, + ); + } catch { + /* SFP patch non-fatal */ + } +} + +function alignWorkflowImages(namespace: string, oslMajorMinor: string): void { + if (!oslMajorMinor || oslMajorMinor === "1.37") return; + + const oslTag = `osl_${oslMajorMinor.replace(".", "_")}`; + const imageMap: Record = { + greeting: `quay.io/orchestrator/serverless-workflow-greeting:${oslTag}`, + failswitch: `quay.io/orchestrator/fail-switch:${oslTag}`, + }; + for (const wf of WORKFLOWS) { + const image = imageMap[wf]; + if (!image) continue; + try { + const imgPatch = JSON.stringify({ + spec: { podTemplate: { container: { image } } }, + }); + runOc([ + "-n", + namespace, + "patch", + "sonataflow", + wf, + "--type", + "merge", + "-p", + imgPatch, + ]); + } catch { + /* ignore per-workflow patch failure */ + } + } +} + +async function deployTokenPropagationWorkflow( + namespace: string, + postgresAlignTimeoutMs: number, +): Promise { + const kcBaseUrl = process.env.KEYCLOAK_BASE_URL; + const kcRealm = process.env.KEYCLOAK_REALM; + const kcClientId = process.env.KEYCLOAK_CLIENT_ID; + const kcClientSecret = process.env.KEYCLOAK_CLIENT_SECRET; + if (!kcBaseUrl || !kcRealm || !kcClientId || !kcClientSecret) { + throw new Error( + "KEYCLOAK_BASE_URL, KEYCLOAK_REALM, KEYCLOAK_CLIENT_ID and KEYCLOAK_CLIENT_SECRET must be set", + ); + } + + const authServerUrl = `${kcBaseUrl}/realms/${kcRealm}`; + const tokenUrl = `${authServerUrl}/protocol/openid-connect/token`; + const sampleServerUrl = `http://sample-server-service.${namespace}:8080`; + const demoDir = `/tmp/orchestrator-demo-${process.pid}`; + const manifestsDir = join(demoDir, "09_token_propagation/manifests"); + const propsCm = join( + manifestsDir, + "01-configmap_token-propagation-props.yaml", + ); + const specsCm = join( + manifestsDir, + "03-configmap_02-token-propagation-resources-specs.yaml", + ); + + try { + await $`git clone --depth=1 ${DEMO_WORKFLOW_REPO} ${demoDir}`; + + const propsData = readFileSync(propsCm, "utf-8") + .replaceAll( + "http://example-kc-service.keycloak:8080/realms/quarkus", + authServerUrl, + ) + .replaceAll("client-id=quarkus-app", `client-id=${kcClientId}`) + .replaceAll( + "client-secret=lVGSvdaoDUem7lqeAnqXn1F92dCPbQea", + `client-secret=${kcClientSecret}`, + ) + .replaceAll( + "http://sample-server-service.rhdh-operator", + sampleServerUrl, + ); + writeFileSync(propsCm, propsData, "utf-8"); + + const specsData = readFileSync(specsCm, "utf-8").replaceAll( + "http://example-kc-service.keycloak:8080/realms/quarkus/protocol/openid-connect/token", + tokenUrl, + ); + writeFileSync(specsCm, specsData, "utf-8"); + + await $`oc apply -n ${namespace} -f - <<'EOF' +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sample-server + labels: + app: sample-server +spec: + replicas: 1 + selector: + matchLabels: + app: sample-server + template: + metadata: + labels: + app: sample-server + spec: + containers: + - name: sample-server + image: quay.io/orchestrator/sample-server:latest + ports: + - containerPort: 8080 + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 15 + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + name: sample-server-service + labels: + app: sample-server +spec: + selector: + app: sample-server + ports: + - port: 8080 + targetPort: 8080 + protocol: TCP +EOF`; + + runOc( + [ + "wait", + "deployment/sample-server", + "-n", + namespace, + "--for=condition=Available", + "--timeout=120s", + ], + 130_000, + ); + + await $`oc apply -n ${namespace} -f ${manifestsDir}`; + + patchWorkflowPostgres(namespace, "token-propagation"); + await waitForWorkflowPostgresDeploymentAligned( + namespace, + "token-propagation", + postgresAlignTimeoutMs, + ); + runOc( + ["rollout", "restart", "deployment/token-propagation", "-n", namespace], + 60_000, + ); + await sleep(2_000); + runOc( + [ + "rollout", + "status", + "deployment/token-propagation", + "-n", + namespace, + "--timeout=600s", + ], + 610_000, + ); + } finally { + await $`rm -rf ${demoDir}`.catch(() => {}); + } +} + +export function runOc(args: string[], timeoutMs = 30_000): string { + return execFileSync("oc", args, { + encoding: "utf-8", + timeout: timeoutMs, + maxBuffer: 32 * 1024 * 1024, + }).trim(); +} + +function formatOcFailure(err: unknown): string { + if (err instanceof Error) { + const m = err.message.trim(); + return m.includes("\n") ? (m.split("\n")[0] ?? m) : m; + } + return String(err); +} + +/** + * Best-effort snapshot when `rhdh.deploy()` fails (pods, hub describe/logs, recent events). + */ +export function logOrchestratorDeployFailureDiagnostics( + namespace: string, +): void { + const banner = (title: string) => { + console.error(`\n===== [orchestrator-e2e deploy failure] ${title} =====\n`); + }; + + const safeOc = (args: string[], timeoutMs = 120_000): string | undefined => { + try { + return runOc(args, timeoutMs); + } catch (err) { + console.error( + `[orchestrator-e2e deploy failure] oc ${args.join(" ")} failed: ${formatOcFailure(err)}`, + ); + return undefined; + } + }; + + const dumpOc = (out: string | undefined, emptyHint: string) => { + if (out === undefined) return; + if (out.trim().length > 0) { + console.error(out); + } else { + console.error(emptyHint); + } + }; + + banner(`namespace=${namespace}`); + + dumpOc( + safeOc(["get", "pods", "-n", namespace, "-o", "wide"], 60_000), + "(get pods — empty stdout)", + ); + + const hubPod = safeOc([ + "get", + "pods", + "-n", + namespace, + "-l", + "app.kubernetes.io/instance=redhat-developer-hub", + "-o", + "jsonpath={.items[0].metadata.name}", + ])?.trim(); + + if (hubPod) { + banner(`redhat-developer-hub pod describe (${hubPod})`); + dumpOc( + safeOc(["describe", "pod", "-n", namespace, hubPod], 120_000), + "(describe produced no stdout)", + ); + banner(`redhat-developer-hub pod logs (${hubPod}) --all-containers`); + dumpOc( + safeOc( + ["logs", "-n", namespace, hubPod, "--all-containers", "--tail=300"], + 120_000, + ), + "(no container logs on stdout)", + ); + } else { + banner("redhat-developer-hub pod not found via label selector"); + } + + banner("recent namespace events (last 40 lines)"); + const events = safeOc( + ["get", "events", "-n", namespace, "--sort-by=.lastTimestamp"], + 60_000, + ); + if (events?.trim()) { + const lines = events.trim().split("\n"); + console.error(lines.slice(-40).join("\n")); + } else { + console.error("(no events or oc get events failed)"); + } +} + +function detectOperatorVersion(...labels: string[]): string { + for (const label of labels) { + try { + const version = runOc([ + "get", + "csv", + "-n", + "openshift-operators", + "-o", + "jsonpath={.items[0].spec.version}", + "-l", + label, + ]); + if (version) return version; + } catch { + /* try next candidate */ + } + } + return ""; +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/workspaces/orchestrator/e2e-tests/tests/support/utils/env-helpers.ts b/workspaces/orchestrator/e2e-tests/tests/support/utils/env-helpers.ts new file mode 100644 index 000000000..9b459822f --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tests/support/utils/env-helpers.ts @@ -0,0 +1,13 @@ +export function requireEnvVar(name: string): string { + const value = process.env[name]; + if (!value?.trim()) { + throw new Error(`Environment variable ${name} is not set`); + } + return value; +} + +export function getEnvVarDefault(name: string, fallback: string): string { + const value = process.env[name]; + if (!value) return fallback; + return value; +} diff --git a/workspaces/orchestrator/e2e-tests/tests/support/utils/scaffolder-helpers.ts b/workspaces/orchestrator/e2e-tests/tests/support/utils/scaffolder-helpers.ts new file mode 100644 index 000000000..b39a4b75b --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tests/support/utils/scaffolder-helpers.ts @@ -0,0 +1,67 @@ +import { Page } from "@red-hat-developer-hub/e2e-test-utils/test"; +import { UIhelper } from "@red-hat-developer-hub/e2e-test-utils/helpers"; + +export async function launchGreetingTemplateFromSelfService( + page: Page, + uiHelper: UIhelper, +): Promise { + await uiHelper.clickLink({ ariaLabel: "Self-service" }); + await uiHelper.verifyHeading("Self-service"); + await page.waitForLoadState("domcontentloaded"); + await uiHelper.clickBtnInCard("Greeting Test Picker", "Choose"); + await uiHelper.verifyHeading(/Greeting Test Picker/i, 30_000); +} + +export async function openTemplateFromCatalog( + page: Page, + uiHelper: UIhelper, + templateName: RegExp, + maxAttempts = 3, +): Promise { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + await uiHelper.openSidebar("Catalog"); + await uiHelper.verifyHeading(/Catalog|My Org Catalog|All/); + await uiHelper.selectMuiBox("Kind", "Template"); + + const templateLink = page.getByRole("link", { name: templateName }).first(); + const visible = await templateLink + .isVisible({ timeout: attempt === maxAttempts ? 30_000 : 10_000 }) + .catch(() => false); + + if (visible) { + await templateLink.click(); + await page.waitForLoadState("domcontentloaded"); + return; + } + + await page.reload({ waitUntil: "domcontentloaded" }); + } + + throw new Error( + `Template link ${templateName.toString()} was not visible in Catalog after ${maxAttempts} attempts`, + ); +} + +export async function waitForScaffolderTerminalState( + page: Page, + timeoutMs = 120_000, +): Promise { + const completed = page.getByText(/Completed|succeeded|finished/i); + const conflictError = page.getByText(/409 Conflict/i); + const startOver = page.getByRole("button", { name: "Start Over" }); + await completed + .or(conflictError) + .or(startOver) + .first() + .waitFor({ state: "visible", timeout: timeoutMs }); +} + +export async function clickCreateAndWaitForScaffolderTerminalState( + page: Page, + timeoutMs = 120_000, +): Promise { + const createButton = page.getByRole("button", { name: /Create/i }); + await createButton.waitFor({ state: "visible", timeout: 10_000 }); + await createButton.click(); + await waitForScaffolderTerminalState(page, timeoutMs); +} diff --git a/workspaces/orchestrator/e2e-tests/tsconfig.json b/workspaces/orchestrator/e2e-tests/tsconfig.json new file mode 100644 index 000000000..ede55abc0 --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@red-hat-developer-hub/e2e-test-utils/tsconfig", + "include": ["**/*.ts"] +} diff --git a/workspaces/orchestrator/e2e-tests/yarn.lock b/workspaces/orchestrator/e2e-tests/yarn.lock new file mode 100644 index 000000000..b3a1da16d --- /dev/null +++ b/workspaces/orchestrator/e2e-tests/yarn.lock @@ -0,0 +1,2619 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 6 + cacheKey: 8 + +"@axe-core/playwright@npm:4.11.1": + version: 4.11.1 + resolution: "@axe-core/playwright@npm:4.11.1" + dependencies: + axe-core: ~4.11.1 + peerDependencies: + playwright-core: ">= 1.0.0" + checksum: 311f107688bfd930ee201dbab9c75048a6cdd39be7af0d8fdf05e5d01b76ef8d57ca687954102839eefa14ec23f96b6a2833d4bef82c4b280e43b7f4e15a39b3 + languageName: node + linkType: hard + +"@backstage-community/plugin-rbac-common@npm:1.26.0": + version: 1.26.0 + resolution: "@backstage-community/plugin-rbac-common@npm:1.26.0" + peerDependencies: + "@backstage/errors": ^1.2.7 + "@backstage/plugin-permission-common": ^0.9.7 + checksum: 690ea138f77595396a51ad99982d574d3de32b4900f5aba93d0b2ac893e4b918b967efa77c5a9bd7514f8c42e2af1fc6a244debb0253649620ce7cf613599752 + languageName: node + linkType: hard + +"@eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.1": + version: 4.9.1 + resolution: "@eslint-community/eslint-utils@npm:4.9.1" + dependencies: + eslint-visitor-keys: ^3.4.3 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: 0a27c2d676c4be6b329ebb5dd8f6c5ef5fae9a019ff575655306d72874bb26f3ab20e0b241a5f086464bb1f2511ca26a29ff6f80c1e2b0b02eca4686b4dfe1b5 + languageName: node + linkType: hard + +"@eslint-community/regexpp@npm:^4.12.1, @eslint-community/regexpp@npm:^4.12.2": + version: 4.12.2 + resolution: "@eslint-community/regexpp@npm:4.12.2" + checksum: 1770bc81f676a72f65c7200b5675ff7a349786521f30e66125faaf767fde1ba1c19c3790e16ba8508a62a3933afcfc806a893858b3b5906faf693d862b9e4120 + languageName: node + linkType: hard + +"@eslint/config-array@npm:^0.21.2": + version: 0.21.2 + resolution: "@eslint/config-array@npm:0.21.2" + dependencies: + "@eslint/object-schema": ^2.1.7 + debug: ^4.3.1 + minimatch: ^3.1.5 + checksum: f3d6ba56d6a3dfc5400575011fb4ae5ac189c96b6ca4920adb6da2d084f9eaa28583fa0aa55e123c42baa2bd31f85228ee35a05c8a395b58fb8d976e16482697 + languageName: node + linkType: hard + +"@eslint/config-array@npm:^0.23.4": + version: 0.23.5 + resolution: "@eslint/config-array@npm:0.23.5" + dependencies: + "@eslint/object-schema": ^3.0.5 + debug: ^4.3.1 + minimatch: ^10.2.4 + checksum: 2cb8c3d3450f2b1c91dcc21109bfee58356915cbfa1429b9e82efc04c2acf7ccdf12ef20734989afdb1e676b8bf5f2e10548405efc6b8b2c89bbd9e89e5a8e49 + languageName: node + linkType: hard + +"@eslint/config-helpers@npm:^0.4.2": + version: 0.4.2 + resolution: "@eslint/config-helpers@npm:0.4.2" + dependencies: + "@eslint/core": ^0.17.0 + checksum: 63ff6a0730c9fff2edb80c89b39b15b28d6a635a1c3f32cf0d7eb3e2625f2efbc373c5531ae84e420ae36d6e37016dd40c365b6e5dee6938478e9907aaadae0b + languageName: node + linkType: hard + +"@eslint/config-helpers@npm:^0.5.4": + version: 0.5.5 + resolution: "@eslint/config-helpers@npm:0.5.5" + dependencies: + "@eslint/core": ^1.2.1 + checksum: 2442c0e5281b0a0733942a439fc3fd18b38bd69c2f49a284ec7cac8658f287c4356f6e83a513efd377c2d9e55c4624d47fa28461fa7d5431eef1f5f0d14f23d0 + languageName: node + linkType: hard + +"@eslint/core@npm:^0.17.0": + version: 0.17.0 + resolution: "@eslint/core@npm:0.17.0" + dependencies: + "@types/json-schema": ^7.0.15 + checksum: ff9b5b4987f0bae4f2a4cfcdc7ae584ad3b0cb58526ca562fb281d6837700a04c7f3c86862e95126462318f33f60bf38e1cb07ed0e2449532d4b91cd5f4ab1f2 + languageName: node + linkType: hard + +"@eslint/core@npm:^1.2.0, @eslint/core@npm:^1.2.1": + version: 1.2.1 + resolution: "@eslint/core@npm:1.2.1" + dependencies: + "@types/json-schema": ^7.0.15 + checksum: 430f53c5c6bcfabe54d7232d6b74bf9f6f62b0337f73ca0db70a0a0dbe4843243ce24577df61619fcbc0ef45cc6e2872074bed3295538acd72361b69f3b5eb47 + languageName: node + linkType: hard + +"@eslint/eslintrc@npm:^3.3.5": + version: 3.3.5 + resolution: "@eslint/eslintrc@npm:3.3.5" + dependencies: + ajv: ^6.14.0 + debug: ^4.3.2 + espree: ^10.0.1 + globals: ^14.0.0 + ignore: ^5.2.0 + import-fresh: ^3.2.1 + js-yaml: ^4.1.1 + minimatch: ^3.1.5 + strip-json-comments: ^3.1.1 + checksum: b1c0ac8938891f47a92ef662c790cc599f6562b06562f4035efd075f99c2b62eb4960ee0e2021d424942c8d1084665b581f3799d863c67979b269a8ccda48364 + languageName: node + linkType: hard + +"@eslint/js@npm:10.0.1": + version: 10.0.1 + resolution: "@eslint/js@npm:10.0.1" + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + checksum: 5e60b80ec48d303c9273d5c2803ae3fe12fd5335d57d889d4f9df9249910a97e2921118403765bff26a00b734182437f91a0f6f552654cf12ad73bb49995e22e + languageName: node + linkType: hard + +"@eslint/js@npm:9.39.4, @eslint/js@npm:^9.39.2": + version: 9.39.4 + resolution: "@eslint/js@npm:9.39.4" + checksum: 5b1cd1e6c13bc119c92911e6cef7cf886d942c9e047db0c923bbdd539ed6b9820d986b4559be1f2e24836de7fbad95bbfe268b2bf3d1fef76de37bdc8fae19d8 + languageName: node + linkType: hard + +"@eslint/object-schema@npm:^2.1.7": + version: 2.1.7 + resolution: "@eslint/object-schema@npm:2.1.7" + checksum: fc5708f192476956544def13455d60fd1bafbf8f062d1e05ec5c06dd470b02078eaf721e696a8b31c1c45d2056723a514b941ae5eea1398cc7e38eba6711a775 + languageName: node + linkType: hard + +"@eslint/object-schema@npm:^3.0.5": + version: 3.0.5 + resolution: "@eslint/object-schema@npm:3.0.5" + checksum: 4e9aee969d73a5c12c06dcf9e3a7903d441cdc946b3768099dba1937f2af58bd8ed4b1bcf34bbc54432440cdd00dfab970edd5ce2b4fb1afd2d0e6018c87aa0b + languageName: node + linkType: hard + +"@eslint/plugin-kit@npm:^0.4.1": + version: 0.4.1 + resolution: "@eslint/plugin-kit@npm:0.4.1" + dependencies: + "@eslint/core": ^0.17.0 + levn: ^0.4.1 + checksum: 3f4492e02a3620e05d46126c5cfeff5f651ecf33466c8f88efb4812ae69db5f005e8c13373afabc070ecca7becd319b656d6670ad5093f05ca63c2a8841d99ba + languageName: node + linkType: hard + +"@eslint/plugin-kit@npm:^0.7.0": + version: 0.7.1 + resolution: "@eslint/plugin-kit@npm:0.7.1" + dependencies: + "@eslint/core": ^1.2.1 + levn: ^0.4.1 + checksum: 4d6c0cc823fb5cca2fa5a7a4fdd32340a5e3c755d639addcb0b53fd8edf94e1b1dbf3aa284e504cc04289980e4c6be726e997dee2ec44c27fa35717a48eafacd + languageName: node + linkType: hard + +"@gar/promise-retry@npm:^1.0.0": + version: 1.0.3 + resolution: "@gar/promise-retry@npm:1.0.3" + checksum: 0d13ea3bb1025755e055648f6e290d2a7e0c87affaf552218f09f66b3fcd9ea9d5c9cc5fe2aa6e285e1530437768e40f9448fe9a86f4f3417b216dcf488d3d1a + languageName: node + linkType: hard + +"@humanfs/core@npm:^0.19.1": + version: 0.19.1 + resolution: "@humanfs/core@npm:0.19.1" + checksum: 611e0545146f55ddfdd5c20239cfb7911f9d0e28258787c4fc1a1f6214250830c9367aaaeace0096ed90b6739bee1e9c52ad5ba8adaf74ab8b449119303babfe + languageName: node + linkType: hard + +"@humanfs/node@npm:^0.16.6": + version: 0.16.7 + resolution: "@humanfs/node@npm:0.16.7" + dependencies: + "@humanfs/core": ^0.19.1 + "@humanwhocodes/retry": ^0.4.0 + checksum: 7d2a396a94d80158ce320c0fd7df9aebb82edb8b667e5aaf8f87f4ca50518d0941ca494e0cd68e06b061e777ce5f7d26c45f93ac3fa9f7b11fd1ff26e3cd1440 + languageName: node + linkType: hard + +"@humanwhocodes/module-importer@npm:^1.0.1": + version: 1.0.1 + resolution: "@humanwhocodes/module-importer@npm:1.0.1" + checksum: 0fd22007db8034a2cdf2c764b140d37d9020bbfce8a49d3ec5c05290e77d4b0263b1b972b752df8c89e5eaa94073408f2b7d977aed131faf6cf396ebb5d7fb61 + languageName: node + linkType: hard + +"@humanwhocodes/retry@npm:^0.4.0, @humanwhocodes/retry@npm:^0.4.2": + version: 0.4.3 + resolution: "@humanwhocodes/retry@npm:0.4.3" + checksum: d423455b9d53cf01f778603404512a4246fb19b83e74fe3e28c70d9a80e9d4ae147d2411628907ca983e91a855a52535859a8bb218050bc3f6dbd7a553b7b442 + languageName: node + linkType: hard + +"@isaacs/fs-minipass@npm:^4.0.0": + version: 4.0.1 + resolution: "@isaacs/fs-minipass@npm:4.0.1" + dependencies: + minipass: ^7.0.4 + checksum: 5d36d289960e886484362d9eb6a51d1ea28baed5f5d0140bbe62b99bac52eaf06cc01c2bc0d3575977962f84f6b2c4387b043ee632216643d4787b0999465bf2 + languageName: node + linkType: hard + +"@jsep-plugin/assignment@npm:^1.3.0": + version: 1.3.0 + resolution: "@jsep-plugin/assignment@npm:1.3.0" + peerDependencies: + jsep: ^0.4.0||^1.0.0 + checksum: 5549497d403a6c00969d61202a6d3dafc5a349929d190a524363dcfacb3436dbda3d9f88b2ec1330247a594ad3c5f1c17b0997769d0b206802281bad6cf9a410 + languageName: node + linkType: hard + +"@jsep-plugin/regex@npm:^1.0.4": + version: 1.0.4 + resolution: "@jsep-plugin/regex@npm:1.0.4" + peerDependencies: + jsep: ^0.4.0||^1.0.0 + checksum: 78ef01554535ac6c108851a2a6d86377bce10de01a263ad7b31f9b37c8aa9fc6c49f24b753e5da7d771c5921c913e43c1c33e0bc0fa5d02562d906c83a237836 + languageName: node + linkType: hard + +"@keycloak/keycloak-admin-client@npm:26.5.6": + version: 26.5.6 + resolution: "@keycloak/keycloak-admin-client@npm:26.5.6" + dependencies: + camelize-ts: ^3.0.0 + url-template: ^3.1.1 + checksum: 1e996590de8f191dbe616622102895b358c4e31274a7770b3bba2039fc2e64ce9a67ca6cbf6e823d39e061891c25c29b4342b199f20cc3e04937509cf3975843 + languageName: node + linkType: hard + +"@kubernetes/client-node@npm:1.4.0": + version: 1.4.0 + resolution: "@kubernetes/client-node@npm:1.4.0" + dependencies: + "@types/js-yaml": ^4.0.1 + "@types/node": ^24.0.0 + "@types/node-fetch": ^2.6.13 + "@types/stream-buffers": ^3.0.3 + form-data: ^4.0.0 + hpagent: ^1.2.0 + isomorphic-ws: ^5.0.0 + js-yaml: ^4.1.0 + jsonpath-plus: ^10.3.0 + node-fetch: ^2.7.0 + openid-client: ^6.1.3 + rfc4648: ^1.3.0 + socks-proxy-agent: ^8.0.4 + stream-buffers: ^3.0.2 + tar-fs: ^3.0.9 + ws: ^8.18.2 + checksum: d62b22db84d1832c904ca1489ca2de029b1698ae111cf0fb1dac96ae9e5a68414e1c128adf9ffdd0fd75c937aef831a3e39c48c1b43850c62765d6e61ce25efd + languageName: node + linkType: hard + +"@npmcli/agent@npm:^4.0.0": + version: 4.0.0 + resolution: "@npmcli/agent@npm:4.0.0" + dependencies: + agent-base: ^7.1.0 + http-proxy-agent: ^7.0.0 + https-proxy-agent: ^7.0.1 + lru-cache: ^11.2.1 + socks-proxy-agent: ^8.0.3 + checksum: 89ae20b44859ff8d4de56ade319d8ceaa267a0742d6f7345fe98aa5cd8614ced7db85ea4dc5bfbd6614dbb200a10b134e087143582534c939e8a02219e8665c8 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^5.0.0": + version: 5.0.0 + resolution: "@npmcli/fs@npm:5.0.0" + dependencies: + semver: ^7.3.5 + checksum: 897dac32eb37e011800112d406b9ea2ebd96f1dab01bb8fbeb59191b86f6825dffed6a89f3b6c824753d10f8735b76d630927bd7610e9e123b129ef2e5f02cb5 + languageName: node + linkType: hard + +"@npmcli/redact@npm:^4.0.0": + version: 4.0.0 + resolution: "@npmcli/redact@npm:4.0.0" + checksum: 4e029c44a8593304bb1aa5a8f1559cb8f37b4dc3880c589ce546da0b8cfa741d16a054db38ee309e81c2120c148ba33edbb3252f97a78b3234ba9ab3fa3e176c + languageName: node + linkType: hard + +"@otplib/core@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/core@npm:12.0.1" + checksum: b3c34bc20b31bc3f49cc0dc3c0eb070491c0101e8c1efa83cec48ca94158bd736aaca8187df667fc0c4a239d4ac52076bc44084bee04a50c80c3630caf77affa + languageName: node + linkType: hard + +"@otplib/plugin-crypto@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/plugin-crypto@npm:12.0.1" + dependencies: + "@otplib/core": ^12.0.1 + checksum: 6867c74ee8aca6c2db9670362cf51e44f3648602c39318bf537421242e33f0012a172acd43bbed9a21d706e535dc4c66aff965380673391e9fd74cf685b5b13a + languageName: node + linkType: hard + +"@otplib/plugin-thirty-two@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/plugin-thirty-two@npm:12.0.1" + dependencies: + "@otplib/core": ^12.0.1 + thirty-two: ^1.0.2 + checksum: 920099e40d3e8c2941291c84c70064c2d86d0d1ed17230d650445d5463340e406bc413ddf2e40c374ddc4ee988ef1e3facacab9b5248b1ff361fd13df52bf88f + languageName: node + linkType: hard + +"@otplib/preset-default@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/preset-default@npm:12.0.1" + dependencies: + "@otplib/core": ^12.0.1 + "@otplib/plugin-crypto": ^12.0.1 + "@otplib/plugin-thirty-two": ^12.0.1 + checksum: 8133231384f6277f77eb8e42ef83bc32a8b01059bef147d1c358d9e9bfd292e1c239f581fe008367a48489dd68952b7ac0948e6c41412fc06079da2c91b71d16 + languageName: node + linkType: hard + +"@otplib/preset-v11@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/preset-v11@npm:12.0.1" + dependencies: + "@otplib/core": ^12.0.1 + "@otplib/plugin-crypto": ^12.0.1 + "@otplib/plugin-thirty-two": ^12.0.1 + checksum: 367cb09397e617c21ec748d54e920ab43f1c5dfba70cbfd88edf73aecca399cf0c09fefe32518f79c7ee8a06e7058d14b200da378cc7d46af3cac4e22a153e2f + languageName: node + linkType: hard + +"@playwright/test@npm:1.57.0": + version: 1.57.0 + resolution: "@playwright/test@npm:1.57.0" + dependencies: + playwright: 1.57.0 + bin: + playwright: cli.js + checksum: 1a84783a240d69c2c8081a127b446f812a8dc86fe6f60a9511dd501cc0e6229cbec7e7753972678f3f063ad2bebb2cedbe9caebc5faa41014aebed35773ea242 + languageName: node + linkType: hard + +"@red-hat-developer-hub/e2e-test-utils@redhat-developer/rhdh-e2e-test-utils#main": + version: 1.1.31 + resolution: "@red-hat-developer-hub/e2e-test-utils@https://github.com/redhat-developer/rhdh-e2e-test-utils.git#commit=6da74afc383ec84ddba217b70c63d886845ad002" + dependencies: + "@axe-core/playwright": 4.11.1 + "@backstage-community/plugin-rbac-common": 1.26.0 + "@eslint/js": 10.0.1 + "@keycloak/keycloak-admin-client": 26.5.6 + "@kubernetes/client-node": 1.4.0 + eslint: 10.2.0 + eslint-plugin-check-file: 3.3.1 + eslint-plugin-playwright: 2.10.1 + fs-extra: 11.3.4 + js-yaml: 4.1.1 + lodash.clonedeepwith: 4.5.0 + lodash.mergewith: 4.6.2 + otplib: 12.0.1 + prettier: 3.8.1 + proper-lockfile: 4.1.2 + typescript: 6.0.2 + typescript-eslint: 8.58.1 + zx: 8.8.5 + peerDependencies: + "@playwright/test": ^1.57.0 + checksum: 5d75609a2c021e602fdd89ea85b89571eb66bd9f7b5534189fc0850b5bfd8a8f5f2ac6e6f5a9c74af5c87f0e0643bbb8a1e4574863bdb208b3f0bcbbf8a051ab + languageName: node + linkType: hard + +"@types/esrecurse@npm:^4.3.1": + version: 4.3.1 + resolution: "@types/esrecurse@npm:4.3.1" + checksum: ada5798554b76ac466e90fff26a769b65f905666f32988dcd1b6cf8288896e0fb53080843fd644bf731d16719a6e09b155d623ce36545b75abdd99bb6dcec114 + languageName: node + linkType: hard + +"@types/estree@npm:^1.0.6, @types/estree@npm:^1.0.8": + version: 1.0.8 + resolution: "@types/estree@npm:1.0.8" + checksum: bd93e2e415b6f182ec4da1074e1f36c480f1d26add3e696d54fb30c09bc470897e41361c8fd957bf0985024f8fbf1e6e2aff977d79352ef7eb93a5c6dcff6c11 + languageName: node + linkType: hard + +"@types/js-yaml@npm:^4.0.1": + version: 4.0.9 + resolution: "@types/js-yaml@npm:4.0.9" + checksum: e5e5e49b5789a29fdb1f7d204f82de11cb9e8f6cb24ab064c616da5d6e1b3ccfbf95aa5d1498a9fbd3b9e745564e69b4a20b6c530b5a8bbb2d4eb830cda9bc69 + languageName: node + linkType: hard + +"@types/json-schema@npm:^7.0.15": + version: 7.0.15 + resolution: "@types/json-schema@npm:7.0.15" + checksum: 97ed0cb44d4070aecea772b7b2e2ed971e10c81ec87dd4ecc160322ffa55ff330dace1793489540e3e318d90942064bb697cc0f8989391797792d919737b3b98 + languageName: node + linkType: hard + +"@types/node-fetch@npm:^2.6.13": + version: 2.6.13 + resolution: "@types/node-fetch@npm:2.6.13" + dependencies: + "@types/node": "*" + form-data: ^4.0.4 + checksum: e4b4db3a8c23309dadf0beb87e88882af1157f0c08b7b76027ac40add6ed363c924e2fa275f42ae45eacf776b25ed439d14400d9d6372eb39634dd4c7e7e1ad8 + languageName: node + linkType: hard + +"@types/node@npm:*": + version: 25.5.2 + resolution: "@types/node@npm:25.5.2" + dependencies: + undici-types: ~7.18.0 + checksum: d2edd36f3326862872f23e8cefec4ce220e0ea4dd71ee491c33d2d334335a419a53a1321c522012243eab0d51f19330a5c882454141f5be2b5692e0edd534dd1 + languageName: node + linkType: hard + +"@types/node@npm:^24.0.0, @types/node@npm:^24.10.1": + version: 24.12.2 + resolution: "@types/node@npm:24.12.2" + dependencies: + undici-types: ~7.16.0 + checksum: 81819134cba7708abf6b969c6dbd37a863f378076f51b93c0031ce93060378914d60ea5a208dfd6a7dd368b30af9061dc7e78d5e3e492151a5df61f8268b628e + languageName: node + linkType: hard + +"@types/stream-buffers@npm:^3.0.3": + version: 3.0.8 + resolution: "@types/stream-buffers@npm:3.0.8" + dependencies: + "@types/node": "*" + checksum: 2e269491769f3c529236cdd743a505d1dca52d14b3672714730d8940000d948bdf9468b2e54609b9d591d31e51e8e043eb44a830a6189bc727bd9dfc4dd70cdf + languageName: node + linkType: hard + +"@typescript-eslint/eslint-plugin@npm:8.58.1": + version: 8.58.1 + resolution: "@typescript-eslint/eslint-plugin@npm:8.58.1" + dependencies: + "@eslint-community/regexpp": ^4.12.2 + "@typescript-eslint/scope-manager": 8.58.1 + "@typescript-eslint/type-utils": 8.58.1 + "@typescript-eslint/utils": 8.58.1 + "@typescript-eslint/visitor-keys": 8.58.1 + ignore: ^7.0.5 + natural-compare: ^1.4.0 + ts-api-utils: ^2.5.0 + peerDependencies: + "@typescript-eslint/parser": ^8.58.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: d1e858f9a8c07e1c61000d2af9ec8b5e3d7c252ff9239447cdf4a9665b8df4fd47a8f2af0d27fcd99088ec5e35f58cc240a9ac7f4de4f94b835ee1915b1ea1b3 + languageName: node + linkType: hard + +"@typescript-eslint/parser@npm:8.58.1": + version: 8.58.1 + resolution: "@typescript-eslint/parser@npm:8.58.1" + dependencies: + "@typescript-eslint/scope-manager": 8.58.1 + "@typescript-eslint/types": 8.58.1 + "@typescript-eslint/typescript-estree": 8.58.1 + "@typescript-eslint/visitor-keys": 8.58.1 + debug: ^4.4.3 + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 820c13575f5479c1021f0bb7a51d1f42cc1602f2f2606d7f5ddd7f5f54f8814d364c8aa96fe477f2c6eb49f4829ba603840da09885837f70140957d409f8a203 + languageName: node + linkType: hard + +"@typescript-eslint/project-service@npm:8.58.1": + version: 8.58.1 + resolution: "@typescript-eslint/project-service@npm:8.58.1" + dependencies: + "@typescript-eslint/tsconfig-utils": ^8.58.1 + "@typescript-eslint/types": ^8.58.1 + debug: ^4.4.3 + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: c83fbf0e4c948f39e063b822133b637e43d08445b718782f84dee9e58b11261421ee9fa05a2d879715270ccde1de9eda39be4caaa691334b537e928d155b3260 + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:8.58.1": + version: 8.58.1 + resolution: "@typescript-eslint/scope-manager@npm:8.58.1" + dependencies: + "@typescript-eslint/types": 8.58.1 + "@typescript-eslint/visitor-keys": 8.58.1 + checksum: f5815899048063b949b97b7c3f756e531a9d67496f21504c7a590ee97aee857a88cef191c91add443cbfa68d081441a08ea650deca6386ed6e1b97654c96a87a + languageName: node + linkType: hard + +"@typescript-eslint/tsconfig-utils@npm:8.58.1, @typescript-eslint/tsconfig-utils@npm:^8.58.1": + version: 8.58.1 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.58.1" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: e2a6b78ab07e4ac8c5afeefc8dc3658d9529fb57e8913360e1e0b250fefefabc8593954e50ae57a0b53d8d0f0ad8d3d6eeda588e6867e2ef3fb09f8f34979309 + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:8.58.1": + version: 8.58.1 + resolution: "@typescript-eslint/type-utils@npm:8.58.1" + dependencies: + "@typescript-eslint/types": 8.58.1 + "@typescript-eslint/typescript-estree": 8.58.1 + "@typescript-eslint/utils": 8.58.1 + debug: ^4.4.3 + ts-api-utils: ^2.5.0 + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: ab0ac8adf6e4edc777f1a2bbc4c8e5242d0cf3699ce70549a355d5b09ff024dd1669578b8696b963e1c103d4b9939522d34cb1a53fc36a1e1e449090230fcf3a + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:8.58.1, @typescript-eslint/types@npm:^8.58.1": + version: 8.58.1 + resolution: "@typescript-eslint/types@npm:8.58.1" + checksum: 96abd6c72b82885fb83cf2ef1921802b92b82efa228323f2f65acb4c926bc04878d7bf816396d25aaf81159251b9e77344c09ecc9790116d546f206c9d9a8ac6 + languageName: node + linkType: hard + +"@typescript-eslint/typescript-estree@npm:8.58.1": + version: 8.58.1 + resolution: "@typescript-eslint/typescript-estree@npm:8.58.1" + dependencies: + "@typescript-eslint/project-service": 8.58.1 + "@typescript-eslint/tsconfig-utils": 8.58.1 + "@typescript-eslint/types": 8.58.1 + "@typescript-eslint/visitor-keys": 8.58.1 + debug: ^4.4.3 + minimatch: ^10.2.2 + semver: ^7.7.3 + tinyglobby: ^0.2.15 + ts-api-utils: ^2.5.0 + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 6fcaa5995d641b745d581a08f2305a931bfdc293120ced2b604784b2009ab394eda1ed9463d32a5fffcdc66435fbb012588bfe62a18a5e7af0ae62741a86d563 + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:8.58.1": + version: 8.58.1 + resolution: "@typescript-eslint/utils@npm:8.58.1" + dependencies: + "@eslint-community/eslint-utils": ^4.9.1 + "@typescript-eslint/scope-manager": 8.58.1 + "@typescript-eslint/types": 8.58.1 + "@typescript-eslint/typescript-estree": 8.58.1 + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: baf40d0dc1c0d36d8d46e4e1a664ea3ebd84d37abd33cd858bd7d8271dd0ba2078b3f3097ec9059c575025e8871889d6796155b1b077234ba644bd598199031b + languageName: node + linkType: hard + +"@typescript-eslint/visitor-keys@npm:8.58.1": + version: 8.58.1 + resolution: "@typescript-eslint/visitor-keys@npm:8.58.1" + dependencies: + "@typescript-eslint/types": 8.58.1 + eslint-visitor-keys: ^5.0.0 + checksum: b028a9fb3f14aa96d350e819ab63b6ca98d5cbb7f40eb4901b13ae7f63c3bf42c24eb52506c27098b94940ea5e515678b83c030cff35bca22b476b76785b0d0a + languageName: node + linkType: hard + +"abbrev@npm:^4.0.0": + version: 4.0.0 + resolution: "abbrev@npm:4.0.0" + checksum: d0344b63d28e763f259b4898c41bdc92c08e9d06d0da5617d0bbe4d78244e46daea88c510a2f9472af59b031d9060ec1a999653144e793fd029a59dae2f56dc8 + languageName: node + linkType: hard + +"acorn-jsx@npm:^5.3.2": + version: 5.3.2 + resolution: "acorn-jsx@npm:5.3.2" + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: c3d3b2a89c9a056b205b69530a37b972b404ee46ec8e5b341666f9513d3163e2a4f214a71f4dfc7370f5a9c07472d2fd1c11c91c3f03d093e37637d95da98950 + languageName: node + linkType: hard + +"acorn@npm:^8.15.0, acorn@npm:^8.16.0": + version: 8.16.0 + resolution: "acorn@npm:8.16.0" + bin: + acorn: bin/acorn + checksum: bbfa466cd0dbd18b4460a85e9d0fc2f35db999380892403c573261beda91f23836db2aa71fd3ae65e94424ad14ff8e2b7bd37c7a2624278fd89137cd6e448c41 + languageName: node + linkType: hard + +"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": + version: 7.1.4 + resolution: "agent-base@npm:7.1.4" + checksum: 86a7f542af277cfbd77dd61e7df8422f90bac512953709003a1c530171a9d019d072e2400eab2b59f84b49ab9dd237be44315ca663ac73e82b3922d10ea5eafa + languageName: node + linkType: hard + +"ajv@npm:^6.14.0": + version: 6.14.0 + resolution: "ajv@npm:6.14.0" + dependencies: + fast-deep-equal: ^3.1.1 + fast-json-stable-stringify: ^2.0.0 + json-schema-traverse: ^0.4.1 + uri-js: ^4.2.2 + checksum: 7bb3ea97bb8af52521589079f427e799b6561acaa94f50e13410cb87588c51df8db1afe1157b3e48f1a829269adaa11116e0c2cafe2b998add1523789809a3c5 + languageName: node + linkType: hard + +"ansi-styles@npm:^4.1.0": + version: 4.3.0 + resolution: "ansi-styles@npm:4.3.0" + dependencies: + color-convert: ^2.0.1 + checksum: 513b44c3b2105dd14cc42a19271e80f386466c4be574bccf60b627432f9198571ebf4ab1e4c3ba17347658f4ee1711c163d574248c0c1cdc2d5917a0ad582ec4 + languageName: node + linkType: hard + +"argparse@npm:^2.0.1": + version: 2.0.1 + resolution: "argparse@npm:2.0.1" + checksum: 83644b56493e89a254bae05702abf3a1101b4fa4d0ca31df1c9985275a5a5bd47b3c27b7fa0b71098d41114d8ca000e6ed90cad764b306f8a503665e4d517ced + languageName: node + linkType: hard + +"async-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-function@npm:1.0.0" + checksum: 9102e246d1ed9b37ac36f57f0a6ca55226876553251a31fc80677e71471f463a54c872dc78d5d7f80740c8ba624395cccbe8b60f7b690c4418f487d8e9fd1106 + languageName: node + linkType: hard + +"async-generator-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-generator-function@npm:1.0.0" + checksum: 74a71a4a2dd7afd06ebb612f6d612c7f4766a351bedffde466023bf6dae629e46b0d2cd38786239e0fbf245de0c7df76035465e16d1213774a0efb22fec0d713 + languageName: node + linkType: hard + +"asynckit@npm:^0.4.0": + version: 0.4.0 + resolution: "asynckit@npm:0.4.0" + checksum: 7b78c451df768adba04e2d02e63e2d0bf3b07adcd6e42b4cf665cb7ce899bedd344c69a1dcbce355b5f972d597b25aaa1c1742b52cffd9caccb22f348114f6be + languageName: node + linkType: hard + +"axe-core@npm:~4.11.1": + version: 4.11.2 + resolution: "axe-core@npm:4.11.2" + checksum: 9d156e714990ace407814235d823eb996a6b419162cfa80e9d85ac79b42b4ca9aaa2adf29698618b389ec5e2bd967f57464aeb84928fa261122b46b49f7fb513 + languageName: node + linkType: hard + +"b4a@npm:^1.6.4": + version: 1.8.0 + resolution: "b4a@npm:1.8.0" + peerDependencies: + react-native-b4a: "*" + peerDependenciesMeta: + react-native-b4a: + optional: true + checksum: 92a3addf120a69c26c8dfcda1537eb63e10e682fb44a7f2d78f3347fe12c61b4022152b5a7f16bd71872f34eda84c4b8ce441cbe02a07e9a7cd7c1a3cb0ecf07 + languageName: node + linkType: hard + +"balanced-match@npm:^1.0.0": + version: 1.0.2 + resolution: "balanced-match@npm:1.0.2" + checksum: 9706c088a283058a8a99e0bf91b0a2f75497f185980d9ffa8b304de1d9e58ebda7c72c07ebf01dadedaac5b2907b2c6f566f660d62bd336c3468e960403b9d65 + languageName: node + linkType: hard + +"balanced-match@npm:^4.0.2": + version: 4.0.4 + resolution: "balanced-match@npm:4.0.4" + checksum: fb07bb66a0959c2843fc055838047e2a95ccebb837c519614afb067ebfdf2fa967ca8d712c35ced07f2cd26fc6f07964230b094891315ad74f11eba3d53178a0 + languageName: node + linkType: hard + +"bare-events@npm:^2.5.4, bare-events@npm:^2.7.0": + version: 2.8.2 + resolution: "bare-events@npm:2.8.2" + peerDependencies: + bare-abort-controller: "*" + peerDependenciesMeta: + bare-abort-controller: + optional: true + checksum: 97e6fc825bc984363a1f695c9a962b1b9f0ea467baa95d7059565a0aa31f4b5fc0f5eb71c087456ae3a436f39fce14a6147a12dd63aac0ff1d20cc5ad64cd780 + languageName: node + linkType: hard + +"bare-fs@npm:^4.0.1, bare-fs@npm:^4.5.5": + version: 4.6.0 + resolution: "bare-fs@npm:4.6.0" + dependencies: + bare-events: ^2.5.4 + bare-path: ^3.0.0 + bare-stream: ^2.6.4 + bare-url: ^2.2.2 + fast-fifo: ^1.3.2 + peerDependencies: + bare-buffer: "*" + peerDependenciesMeta: + bare-buffer: + optional: true + checksum: 24ecefcd9bc2bf67884127d77f22023334d6bbffe4e9fbb6287e6377688695ea292137280aa1ab258a5ba2f1899ea5a95e0b04df94c6645b641db3cfa51852b0 + languageName: node + linkType: hard + +"bare-os@npm:^3.0.1": + version: 3.8.7 + resolution: "bare-os@npm:3.8.7" + checksum: 5315748e353063507e8c2bb750592f8c029ec0b6303f20a9add95179c64090e19dbad6ef93dd2b97124bbb4015130f3e59d7fd81ed25a1d0b66484520e286275 + languageName: node + linkType: hard + +"bare-path@npm:^3.0.0": + version: 3.0.0 + resolution: "bare-path@npm:3.0.0" + dependencies: + bare-os: ^3.0.1 + checksum: 51d559515f332f62cf9c37c38f2640c1b84b5e8c9de454b70baf029f806058cf94c51d6a0dfec0025cc7760f2069dc3e16c82f0d24f4a9ddb18c829bf9c0206d + languageName: node + linkType: hard + +"bare-stream@npm:^2.6.4": + version: 2.12.0 + resolution: "bare-stream@npm:2.12.0" + dependencies: + streamx: ^2.25.0 + teex: ^1.0.1 + peerDependencies: + bare-abort-controller: "*" + bare-buffer: "*" + bare-events: "*" + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + checksum: 1e8d9b7cfd41e3806e6739e677291d0504f7cd6f8b3858fcf6b4962386a1b978134d294184920bef790512cf29970f04b9a21c7a18c1984280e1d390a22e5af0 + languageName: node + linkType: hard + +"bare-url@npm:^2.2.2": + version: 2.4.0 + resolution: "bare-url@npm:2.4.0" + dependencies: + bare-path: ^3.0.0 + checksum: 7ad8b147650b296890f070feeccb5d2b8e1e833d3a0537206da4dc7051c72bceea99d7fa9732bb851ad30827b22b3a2268ef9abf19dc88f540a7314cb4188993 + languageName: node + linkType: hard + +"brace-expansion@npm:^1.1.7": + version: 1.1.13 + resolution: "brace-expansion@npm:1.1.13" + dependencies: + balanced-match: ^1.0.0 + concat-map: 0.0.1 + checksum: b5f4329fdbe9d2e25fa250c8f866ebd054ba946179426e99b86dcccddabdb1d481f0e40ee5430032e62a7d0a6c2837605ace6783d015aa1d65d85ca72154d936 + languageName: node + linkType: hard + +"brace-expansion@npm:^5.0.5": + version: 5.0.5 + resolution: "brace-expansion@npm:5.0.5" + dependencies: + balanced-match: ^4.0.2 + checksum: 4481b7ffa467b34c14e258167dbd8d9485a2d31d03060e8e8b38142dcde32cdc89c8f55b04d3ae7aae9304fa7eac1dfafd602787cf09c019cc45de3bb6950ffc + languageName: node + linkType: hard + +"braces@npm:^3.0.3": + version: 3.0.3 + resolution: "braces@npm:3.0.3" + dependencies: + fill-range: ^7.1.1 + checksum: b95aa0b3bd909f6cd1720ffcf031aeaf46154dd88b4da01f9a1d3f7ea866a79eba76a6d01cbc3c422b2ee5cdc39a4f02491058d5df0d7bf6e6a162a832df1f69 + languageName: node + linkType: hard + +"cacache@npm:^20.0.1": + version: 20.0.4 + resolution: "cacache@npm:20.0.4" + dependencies: + "@npmcli/fs": ^5.0.0 + fs-minipass: ^3.0.0 + glob: ^13.0.0 + lru-cache: ^11.1.0 + minipass: ^7.0.3 + minipass-collect: ^2.0.1 + minipass-flush: ^1.0.5 + minipass-pipeline: ^1.2.4 + p-map: ^7.0.2 + ssri: ^13.0.0 + checksum: b368523e60f3105167cbeec633c19ee773588439b32754f63aa8767fc6ea293ad2d92a765882f0f088037411ef2cffecff7c33496bfc13b2ec3a4f0f6e45bfe2 + languageName: node + linkType: hard + +"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": + version: 1.0.2 + resolution: "call-bind-apply-helpers@npm:1.0.2" + dependencies: + es-errors: ^1.3.0 + function-bind: ^1.1.2 + checksum: b2863d74fcf2a6948221f65d95b91b4b2d90cfe8927650b506141e669f7d5de65cea191bf788838bc40d13846b7886c5bc5c84ab96c3adbcf88ad69a72fcdc6b + languageName: node + linkType: hard + +"callsites@npm:^3.0.0": + version: 3.1.0 + resolution: "callsites@npm:3.1.0" + checksum: 072d17b6abb459c2ba96598918b55868af677154bec7e73d222ef95a8fdb9bbf7dae96a8421085cdad8cd190d86653b5b6dc55a4484f2e5b2e27d5e0c3fc15b3 + languageName: node + linkType: hard + +"camelize-ts@npm:^3.0.0": + version: 3.0.0 + resolution: "camelize-ts@npm:3.0.0" + checksum: 835f7f79ddec6e6e0364c6a8294ce82586bca5d9443001f28077169181801cb126d8bc608c85504aa6c877de6fe5f7c9533f80996dc81117d865ff92c676d680 + languageName: node + linkType: hard + +"chalk@npm:^4.0.0": + version: 4.1.2 + resolution: "chalk@npm:4.1.2" + dependencies: + ansi-styles: ^4.1.0 + supports-color: ^7.1.0 + checksum: fe75c9d5c76a7a98d45495b91b2172fa3b7a09e0cc9370e5c8feb1c567b85c4288e2b3fded7cfdd7359ac28d6b3844feb8b82b8686842e93d23c827c417e83fc + languageName: node + linkType: hard + +"chownr@npm:^3.0.0": + version: 3.0.0 + resolution: "chownr@npm:3.0.0" + checksum: fd73a4bab48b79e66903fe1cafbdc208956f41ea4f856df883d0c7277b7ab29fd33ee65f93b2ec9192fc0169238f2f8307b7735d27c155821d886b84aa97aa8d + languageName: node + linkType: hard + +"color-convert@npm:^2.0.1": + version: 2.0.1 + resolution: "color-convert@npm:2.0.1" + dependencies: + color-name: ~1.1.4 + checksum: 79e6bdb9fd479a205c71d89574fccfb22bd9053bd98c6c4d870d65c132e5e904e6034978e55b43d69fcaa7433af2016ee203ce76eeba9cfa554b373e7f7db336 + languageName: node + linkType: hard + +"color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610 + languageName: node + linkType: hard + +"combined-stream@npm:^1.0.8": + version: 1.0.8 + resolution: "combined-stream@npm:1.0.8" + dependencies: + delayed-stream: ~1.0.0 + checksum: 49fa4aeb4916567e33ea81d088f6584749fc90c7abec76fd516bf1c5aa5c79f3584b5ba3de6b86d26ddd64bae5329c4c7479343250cfe71c75bb366eae53bb7c + languageName: node + linkType: hard + +"concat-map@npm:0.0.1": + version: 0.0.1 + resolution: "concat-map@npm:0.0.1" + checksum: 902a9f5d8967a3e2faf138d5cb784b9979bad2e6db5357c5b21c568df4ebe62bcb15108af1b2253744844eb964fc023fbd9afbbbb6ddd0bcc204c6fb5b7bf3af + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.6": + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" + dependencies: + path-key: ^3.1.0 + shebang-command: ^2.0.0 + which: ^2.0.1 + checksum: 8d306efacaf6f3f60e0224c287664093fa9185680b2d195852ba9a863f85d02dcc737094c6e512175f8ee0161f9b87c73c6826034c2422e39de7d6569cf4503b + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.4.3": + version: 4.4.3 + resolution: "debug@npm:4.4.3" + dependencies: + ms: ^2.1.3 + peerDependenciesMeta: + supports-color: + optional: true + checksum: 4805abd570e601acdca85b6aa3757186084a45cff9b2fa6eee1f3b173caa776b45f478b2a71a572d616d2010cea9211d0ac4a02a610e4c18ac4324bde3760834 + languageName: node + linkType: hard + +"deep-is@npm:^0.1.3": + version: 0.1.4 + resolution: "deep-is@npm:0.1.4" + checksum: edb65dd0d7d1b9c40b2f50219aef30e116cedd6fc79290e740972c132c09106d2e80aa0bc8826673dd5a00222d4179c84b36a790eef63a4c4bca75a37ef90804 + languageName: node + linkType: hard + +"delayed-stream@npm:~1.0.0": + version: 1.0.0 + resolution: "delayed-stream@npm:1.0.0" + checksum: 46fe6e83e2cb1d85ba50bd52803c68be9bd953282fa7096f51fc29edd5d67ff84ff753c51966061e5ba7cb5e47ef6d36a91924eddb7f3f3483b1c560f77a0020 + languageName: node + linkType: hard + +"dotenv@npm:^16.4.7": + version: 16.6.1 + resolution: "dotenv@npm:16.6.1" + checksum: e8bd63c9a37f57934f7938a9cf35de698097fadf980cb6edb61d33b3e424ceccfe4d10f37130b904a973b9038627c2646a3365a904b4406514ea94d7f1816b69 + languageName: node + linkType: hard + +"dunder-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "dunder-proto@npm:1.0.1" + dependencies: + call-bind-apply-helpers: ^1.0.1 + es-errors: ^1.3.0 + gopd: ^1.2.0 + checksum: 149207e36f07bd4941921b0ca929e3a28f1da7bd6b6ff8ff7f4e2f2e460675af4576eeba359c635723dc189b64cdd4787e0255897d5b135ccc5d15cb8685fc90 + languageName: node + linkType: hard + +"end-of-stream@npm:^1.1.0": + version: 1.4.5 + resolution: "end-of-stream@npm:1.4.5" + dependencies: + once: ^1.4.0 + checksum: 1e0cfa6e7f49887544e03314f9dfc56a8cb6dde910cbb445983ecc2ff426fc05946df9d75d8a21a3a64f2cecfe1bf88f773952029f46756b2ed64a24e95b1fb8 + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e + languageName: node + linkType: hard + +"es-define-property@npm:^1.0.1": + version: 1.0.1 + resolution: "es-define-property@npm:1.0.1" + checksum: 0512f4e5d564021c9e3a644437b0155af2679d10d80f21adaf868e64d30efdfbd321631956f20f42d655fedb2e3a027da479fad3fa6048f768eb453a80a5f80a + languageName: node + linkType: hard + +"es-errors@npm:^1.3.0": + version: 1.3.0 + resolution: "es-errors@npm:1.3.0" + checksum: ec1414527a0ccacd7f15f4a3bc66e215f04f595ba23ca75cdae0927af099b5ec865f9f4d33e9d7e86f512f252876ac77d4281a7871531a50678132429b1271b5 + languageName: node + linkType: hard + +"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": + version: 1.1.1 + resolution: "es-object-atoms@npm:1.1.1" + dependencies: + es-errors: ^1.3.0 + checksum: 214d3767287b12f36d3d7267ef342bbbe1e89f899cfd67040309fc65032372a8e60201410a99a1645f2f90c1912c8c49c8668066f6bdd954bcd614dda2e3da97 + languageName: node + linkType: hard + +"es-set-tostringtag@npm:^2.1.0": + version: 2.1.0 + resolution: "es-set-tostringtag@npm:2.1.0" + dependencies: + es-errors: ^1.3.0 + get-intrinsic: ^1.2.6 + has-tostringtag: ^1.0.2 + hasown: ^2.0.2 + checksum: 789f35de4be3dc8d11fdcb91bc26af4ae3e6d602caa93299a8c45cf05d36cc5081454ae2a6d3afa09cceca214b76c046e4f8151e092e6fc7feeb5efb9e794fc6 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^4.0.0": + version: 4.0.0 + resolution: "escape-string-regexp@npm:4.0.0" + checksum: 98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 + languageName: node + linkType: hard + +"eslint-plugin-check-file@npm:3.3.1, eslint-plugin-check-file@npm:^3.3.1": + version: 3.3.1 + resolution: "eslint-plugin-check-file@npm:3.3.1" + dependencies: + is-glob: ^4.0.3 + micromatch: ^4.0.8 + peerDependencies: + eslint: ">=9.0.0" + checksum: 32b5c3954d4710901b0365308996432596ae1373e678255e016522bbefe19dfe5510dc9b661db937eeb8f3ebb11efa58b2d39c6554a31ae8e5edd2b864c7a79c + languageName: node + linkType: hard + +"eslint-plugin-playwright@npm:2.10.1, eslint-plugin-playwright@npm:^2.4.0": + version: 2.10.1 + resolution: "eslint-plugin-playwright@npm:2.10.1" + dependencies: + globals: ^17.3.0 + peerDependencies: + eslint: ">=8.40.0" + checksum: f738f0cd60e0b3a783b2b022b2b90d5e29a9138671e439a7bbe62a647f8a524f4b040f83f2d606d5eeafb33f602d25ac0c6caf8a017f60c2f70d11040b2902c5 + languageName: node + linkType: hard + +"eslint-scope@npm:^8.4.0": + version: 8.4.0 + resolution: "eslint-scope@npm:8.4.0" + dependencies: + esrecurse: ^4.3.0 + estraverse: ^5.2.0 + checksum: cf88f42cd5e81490d549dc6d350fe01e6fe420f9d9ea34f134bb359b030e3c4ef888d36667632e448937fe52449f7181501df48c08200e3d3b0fee250d05364e + languageName: node + linkType: hard + +"eslint-scope@npm:^9.1.2": + version: 9.1.2 + resolution: "eslint-scope@npm:9.1.2" + dependencies: + "@types/esrecurse": ^4.3.1 + "@types/estree": ^1.0.8 + esrecurse: ^4.3.0 + estraverse: ^5.2.0 + checksum: ea1a4333f5912e1ec83328ecf8103b0bb9628beca10d5efc17ce63a825ed3ab0b68c036c2dbd3127cf71f51cc04fb4685a27aac082d55c2faf134391d06443af + languageName: node + linkType: hard + +"eslint-visitor-keys@npm:^3.4.3": + version: 3.4.3 + resolution: "eslint-visitor-keys@npm:3.4.3" + checksum: 36e9ef87fca698b6fd7ca5ca35d7b2b6eeaaf106572e2f7fd31c12d3bfdaccdb587bba6d3621067e5aece31c8c3a348b93922ab8f7b2cbc6aaab5e1d89040c60 + languageName: node + linkType: hard + +"eslint-visitor-keys@npm:^4.2.1": + version: 4.2.1 + resolution: "eslint-visitor-keys@npm:4.2.1" + checksum: 3a77e3f99a49109f6fb2c5b7784bc78f9743b834d238cdba4d66c602c6b52f19ed7bcd0a5c5dbbeae3a8689fd785e76c001799f53d2228b278282cf9f699fff5 + languageName: node + linkType: hard + +"eslint-visitor-keys@npm:^5.0.0, eslint-visitor-keys@npm:^5.0.1": + version: 5.0.1 + resolution: "eslint-visitor-keys@npm:5.0.1" + checksum: d6cc6830536ab4a808f25325686c2c27862f27aab0c1ffed39627293b06cee05d95187da113cafd366314ea5be803b456115de71ad625e365020f20e2a6af89b + languageName: node + linkType: hard + +"eslint@npm:10.2.0": + version: 10.2.0 + resolution: "eslint@npm:10.2.0" + dependencies: + "@eslint-community/eslint-utils": ^4.8.0 + "@eslint-community/regexpp": ^4.12.2 + "@eslint/config-array": ^0.23.4 + "@eslint/config-helpers": ^0.5.4 + "@eslint/core": ^1.2.0 + "@eslint/plugin-kit": ^0.7.0 + "@humanfs/node": ^0.16.6 + "@humanwhocodes/module-importer": ^1.0.1 + "@humanwhocodes/retry": ^0.4.2 + "@types/estree": ^1.0.6 + ajv: ^6.14.0 + cross-spawn: ^7.0.6 + debug: ^4.3.2 + escape-string-regexp: ^4.0.0 + eslint-scope: ^9.1.2 + eslint-visitor-keys: ^5.0.1 + espree: ^11.2.0 + esquery: ^1.7.0 + esutils: ^2.0.2 + fast-deep-equal: ^3.1.3 + file-entry-cache: ^8.0.0 + find-up: ^5.0.0 + glob-parent: ^6.0.2 + ignore: ^5.2.0 + imurmurhash: ^0.1.4 + is-glob: ^4.0.0 + json-stable-stringify-without-jsonify: ^1.0.1 + minimatch: ^10.2.4 + natural-compare: ^1.4.0 + optionator: ^0.9.3 + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true + bin: + eslint: bin/eslint.js + checksum: b63e89221bd0c05e91378420b501a2de8b192d0fcd307453e72eca5ebf9d8e933c42187e636096d8971ffde61ac7eab4736f441f9c569c2d22d9d5a275077532 + languageName: node + linkType: hard + +"eslint@npm:^9.39.2": + version: 9.39.4 + resolution: "eslint@npm:9.39.4" + dependencies: + "@eslint-community/eslint-utils": ^4.8.0 + "@eslint-community/regexpp": ^4.12.1 + "@eslint/config-array": ^0.21.2 + "@eslint/config-helpers": ^0.4.2 + "@eslint/core": ^0.17.0 + "@eslint/eslintrc": ^3.3.5 + "@eslint/js": 9.39.4 + "@eslint/plugin-kit": ^0.4.1 + "@humanfs/node": ^0.16.6 + "@humanwhocodes/module-importer": ^1.0.1 + "@humanwhocodes/retry": ^0.4.2 + "@types/estree": ^1.0.6 + ajv: ^6.14.0 + chalk: ^4.0.0 + cross-spawn: ^7.0.6 + debug: ^4.3.2 + escape-string-regexp: ^4.0.0 + eslint-scope: ^8.4.0 + eslint-visitor-keys: ^4.2.1 + espree: ^10.4.0 + esquery: ^1.5.0 + esutils: ^2.0.2 + fast-deep-equal: ^3.1.3 + file-entry-cache: ^8.0.0 + find-up: ^5.0.0 + glob-parent: ^6.0.2 + ignore: ^5.2.0 + imurmurhash: ^0.1.4 + is-glob: ^4.0.0 + json-stable-stringify-without-jsonify: ^1.0.1 + lodash.merge: ^4.6.2 + minimatch: ^3.1.5 + natural-compare: ^1.4.0 + optionator: ^0.9.3 + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true + bin: + eslint: bin/eslint.js + checksum: 474550582ab15ca0863c4624bea1978567434cc907097f0cf12e05fcb18f10e96be408da33c2e0195c037162a8b0f2dbf1bc37622509f6a2e221dcdc52ce68fe + languageName: node + linkType: hard + +"espree@npm:^10.0.1, espree@npm:^10.4.0": + version: 10.4.0 + resolution: "espree@npm:10.4.0" + dependencies: + acorn: ^8.15.0 + acorn-jsx: ^5.3.2 + eslint-visitor-keys: ^4.2.1 + checksum: 5f9d0d7c81c1bca4bfd29a55270067ff9d575adb8c729a5d7f779c2c7b910bfc68ccf8ec19b29844b707440fc159a83868f22c8e87bbf7cbcb225ed067df6c85 + languageName: node + linkType: hard + +"espree@npm:^11.2.0": + version: 11.2.0 + resolution: "espree@npm:11.2.0" + dependencies: + acorn: ^8.16.0 + acorn-jsx: ^5.3.2 + eslint-visitor-keys: ^5.0.1 + checksum: 7545dc501ab5cff558af1aa290c7e586d7d2a83c9ecdcb5f2c8ba7ee6634b70f4083d1bed198ec17ddf11d3265751aa78e315b4d4c7506711066a4ef38c1084a + languageName: node + linkType: hard + +"esquery@npm:^1.5.0, esquery@npm:^1.7.0": + version: 1.7.0 + resolution: "esquery@npm:1.7.0" + dependencies: + estraverse: ^5.1.0 + checksum: 3239792b68cf39fe18966d0ca01549bb15556734f0144308fd213739b0f153671ae916013fce0bca032044a4dbcda98b43c1c667f20c20a54dec3597ac0d7c27 + languageName: node + linkType: hard + +"esrecurse@npm:^4.3.0": + version: 4.3.0 + resolution: "esrecurse@npm:4.3.0" + dependencies: + estraverse: ^5.2.0 + checksum: ebc17b1a33c51cef46fdc28b958994b1dc43cd2e86237515cbc3b4e5d2be6a811b2315d0a1a4d9d340b6d2308b15322f5c8291059521cc5f4802f65e7ec32837 + languageName: node + linkType: hard + +"estraverse@npm:^5.1.0, estraverse@npm:^5.2.0": + version: 5.3.0 + resolution: "estraverse@npm:5.3.0" + checksum: 072780882dc8416ad144f8fe199628d2b3e7bbc9989d9ed43795d2c90309a2047e6bc5979d7e2322a341163d22cfad9e21f4110597fe487519697389497e4e2b + languageName: node + linkType: hard + +"esutils@npm:^2.0.2": + version: 2.0.3 + resolution: "esutils@npm:2.0.3" + checksum: 22b5b08f74737379a840b8ed2036a5fb35826c709ab000683b092d9054e5c2a82c27818f12604bfc2a9a76b90b6834ef081edbc1c7ae30d1627012e067c6ec87 + languageName: node + linkType: hard + +"events-universal@npm:^1.0.0": + version: 1.0.1 + resolution: "events-universal@npm:1.0.1" + dependencies: + bare-events: ^2.7.0 + checksum: fb8451c98535bde30585004303a368d55c38e5bc3ed6aa9b5d29fecaabaf8ec276a33ff77dcc1d1c05eecf83b8161f184cabc9a03b76a06c10e9a4ce827a6abc + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.3 + resolution: "exponential-backoff@npm:3.1.3" + checksum: 471fdb70fd3d2c08a74a026973bdd4105b7832911f610ca67bbb74e39279411c1eed2f2a110c9d41c2edd89459ba58fdaba1c174beed73e7a42d773882dcff82 + languageName: node + linkType: hard + +"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": + version: 3.1.3 + resolution: "fast-deep-equal@npm:3.1.3" + checksum: e21a9d8d84f53493b6aa15efc9cfd53dd5b714a1f23f67fb5dc8f574af80df889b3bce25dc081887c6d25457cce704e636395333abad896ccdec03abaf1f3f9d + languageName: node + linkType: hard + +"fast-fifo@npm:^1.2.0, fast-fifo@npm:^1.3.2": + version: 1.3.2 + resolution: "fast-fifo@npm:1.3.2" + checksum: 6bfcba3e4df5af7be3332703b69a7898a8ed7020837ec4395bb341bd96cc3a6d86c3f6071dd98da289618cf2234c70d84b2a6f09a33dd6f988b1ff60d8e54275 + languageName: node + linkType: hard + +"fast-json-stable-stringify@npm:^2.0.0": + version: 2.1.0 + resolution: "fast-json-stable-stringify@npm:2.1.0" + checksum: b191531e36c607977e5b1c47811158733c34ccb3bfde92c44798929e9b4154884378536d26ad90dfecd32e1ffc09c545d23535ad91b3161a27ddbb8ebe0cbecb + languageName: node + linkType: hard + +"fast-levenshtein@npm:^2.0.6": + version: 2.0.6 + resolution: "fast-levenshtein@npm:2.0.6" + checksum: 92cfec0a8dfafd9c7a15fba8f2cc29cd0b62b85f056d99ce448bbcd9f708e18ab2764bda4dd5158364f4145a7c72788538994f0d1787b956ef0d1062b0f7c24c + languageName: node + linkType: hard + +"fdir@npm:^6.5.0": + version: 6.5.0 + resolution: "fdir@npm:6.5.0" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: bd537daa9d3cd53887eed35efa0eab2dbb1ca408790e10e024120e7a36c6e9ae2b33710cb8381e35def01bc9c1d7eaba746f886338413e68ff6ebaee07b9a6e8 + languageName: node + linkType: hard + +"file-entry-cache@npm:^8.0.0": + version: 8.0.0 + resolution: "file-entry-cache@npm:8.0.0" + dependencies: + flat-cache: ^4.0.0 + checksum: f67802d3334809048c69b3d458f672e1b6d26daefda701761c81f203b80149c35dea04d78ea4238969dd617678e530876722a0634c43031a0957f10cc3ed190f + languageName: node + linkType: hard + +"fill-range@npm:^7.1.1": + version: 7.1.1 + resolution: "fill-range@npm:7.1.1" + dependencies: + to-regex-range: ^5.0.1 + checksum: b4abfbca3839a3d55e4ae5ec62e131e2e356bf4859ce8480c64c4876100f4df292a63e5bb1618e1d7460282ca2b305653064f01654474aa35c68000980f17798 + languageName: node + linkType: hard + +"find-up@npm:^5.0.0": + version: 5.0.0 + resolution: "find-up@npm:5.0.0" + dependencies: + locate-path: ^6.0.0 + path-exists: ^4.0.0 + checksum: 07955e357348f34660bde7920783204ff5a26ac2cafcaa28bace494027158a97b9f56faaf2d89a6106211a8174db650dd9f503f9c0d526b1202d5554a00b9095 + languageName: node + linkType: hard + +"flat-cache@npm:^4.0.0": + version: 4.0.1 + resolution: "flat-cache@npm:4.0.1" + dependencies: + flatted: ^3.2.9 + keyv: ^4.5.4 + checksum: 899fc86bf6df093547d76e7bfaeb900824b869d7d457d02e9b8aae24836f0a99fbad79328cfd6415ee8908f180699bf259dc7614f793447cb14f707caf5996f6 + languageName: node + linkType: hard + +"flatted@npm:^3.2.9": + version: 3.4.2 + resolution: "flatted@npm:3.4.2" + checksum: 1b2536fccbbf75d67a823dea67819f764c19266ad5e4aca6b47f6bf84d3b5e1c15eb5862f7dec1fb87129b60741524933192051286de52baddbc97129896380d + languageName: node + linkType: hard + +"form-data@npm:^4.0.0, form-data@npm:^4.0.4": + version: 4.0.5 + resolution: "form-data@npm:4.0.5" + dependencies: + asynckit: ^0.4.0 + combined-stream: ^1.0.8 + es-set-tostringtag: ^2.1.0 + hasown: ^2.0.2 + mime-types: ^2.1.12 + checksum: af8328413c16d0cded5fccc975a44d227c5120fd46a9e81de8acf619d43ed838414cc6d7792195b30b248f76a65246949a129a4dadd148721948f90cd6d4fb69 + languageName: node + linkType: hard + +"fs-extra@npm:11.3.4": + version: 11.3.4 + resolution: "fs-extra@npm:11.3.4" + dependencies: + graceful-fs: ^4.2.0 + jsonfile: ^6.0.1 + universalify: ^2.0.0 + checksum: 3d1453db564b20ad58adf7c9583d0821546bd05e158865407fa2767af774ad8353418118655005f48c5bd0dbda19ea4bd053f8a5dcef2ce2e97580f7a039a221 + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: ^7.0.3 + checksum: 8722a41109130851d979222d3ec88aabaceeaaf8f57b2a8f744ef8bd2d1ce95453b04a61daa0078822bc5cd21e008814f06fe6586f56fef511e71b8d2394d802 + languageName: node + linkType: hard + +"fsevents@npm:2.3.2": + version: 2.3.2 + resolution: "fsevents@npm:2.3.2" + dependencies: + node-gyp: latest + checksum: 97ade64e75091afee5265e6956cb72ba34db7819b4c3e94c431d4be2b19b8bb7a2d4116da417950c3425f17c8fe693d25e20212cac583ac1521ad066b77ae31f + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@2.3.2#~builtin": + version: 2.3.2 + resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin::version=2.3.2&hash=df0bf1" + dependencies: + node-gyp: latest + conditions: os=darwin + languageName: node + linkType: hard + +"function-bind@npm:^1.1.2": + version: 1.1.2 + resolution: "function-bind@npm:1.1.2" + checksum: 2b0ff4ce708d99715ad14a6d1f894e2a83242e4a52ccfcefaee5e40050562e5f6dafc1adbb4ce2d4ab47279a45dc736ab91ea5042d843c3c092820dfe032efb1 + languageName: node + linkType: hard + +"generator-function@npm:^2.0.0": + version: 2.0.1 + resolution: "generator-function@npm:2.0.1" + checksum: 3bf87f7b0230de5d74529677e6c3ceb3b7b5d9618b5a22d92b45ce3876defbaf5a77791b25a61b0fa7d13f95675b5ff67a7769f3b9af33f096e34653519e873d + languageName: node + linkType: hard + +"get-intrinsic@npm:^1.2.6": + version: 1.3.1 + resolution: "get-intrinsic@npm:1.3.1" + dependencies: + async-function: ^1.0.0 + async-generator-function: ^1.0.0 + call-bind-apply-helpers: ^1.0.2 + es-define-property: ^1.0.1 + es-errors: ^1.3.0 + es-object-atoms: ^1.1.1 + function-bind: ^1.1.2 + generator-function: ^2.0.0 + get-proto: ^1.0.1 + gopd: ^1.2.0 + has-symbols: ^1.1.0 + hasown: ^2.0.2 + math-intrinsics: ^1.1.0 + checksum: c02b3b6a445f9cd53e14896303794ac60f9751f58a69099127248abdb0251957174c6524245fc68579dc8e6a35161d3d94c93e665f808274716f4248b269436a + languageName: node + linkType: hard + +"get-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "get-proto@npm:1.0.1" + dependencies: + dunder-proto: ^1.0.1 + es-object-atoms: ^1.0.0 + checksum: 4fc96afdb58ced9a67558698b91433e6b037aaa6f1493af77498d7c85b141382cf223c0e5946f334fb328ee85dfe6edd06d218eaf09556f4bc4ec6005d7f5f7b + languageName: node + linkType: hard + +"glob-parent@npm:^6.0.2": + version: 6.0.2 + resolution: "glob-parent@npm:6.0.2" + dependencies: + is-glob: ^4.0.3 + checksum: c13ee97978bef4f55106b71e66428eb1512e71a7466ba49025fc2aec59a5bfb0954d5abd58fc5ee6c9b076eef4e1f6d3375c2e964b88466ca390da4419a786a8 + languageName: node + linkType: hard + +"glob@npm:^13.0.0": + version: 13.0.6 + resolution: "glob@npm:13.0.6" + dependencies: + minimatch: ^10.2.2 + minipass: ^7.1.3 + path-scurry: ^2.0.2 + checksum: 1eb421c696c66af3c26e4845dbdd222d3b982ede17448456b49272722d872e9a91741b50e4e827370c57d17a39a69790061f45033523f085c076d8fcc0f69d2b + languageName: node + linkType: hard + +"globals@npm:^14.0.0": + version: 14.0.0 + resolution: "globals@npm:14.0.0" + checksum: 534b8216736a5425737f59f6e6a5c7f386254560c9f41d24a9227d60ee3ad4a9e82c5b85def0e212e9d92162f83a92544be4c7fd4c902cb913736c10e08237ac + languageName: node + linkType: hard + +"globals@npm:^17.3.0": + version: 17.4.0 + resolution: "globals@npm:17.4.0" + checksum: 456f0d844ef7d502c41ae7ae07e602491fb7ac5e8b37c7c2fcf1419974321e27a35b94249168a7e521fcf09105b32b8f85cedc63b6b90cc8a594d1c281c2f5cd + languageName: node + linkType: hard + +"gopd@npm:^1.2.0": + version: 1.2.0 + resolution: "gopd@npm:1.2.0" + checksum: cc6d8e655e360955bdccaca51a12a474268f95bb793fc3e1f2bdadb075f28bfd1fd988dab872daf77a61d78cbaf13744bc8727a17cfb1d150d76047d805375f3 + languageName: node + linkType: hard + +"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7 + languageName: node + linkType: hard + +"has-flag@npm:^4.0.0": + version: 4.0.0 + resolution: "has-flag@npm:4.0.0" + checksum: 261a1357037ead75e338156b1f9452c016a37dcd3283a972a30d9e4a87441ba372c8b81f818cd0fbcd9c0354b4ae7e18b9e1afa1971164aef6d18c2b6095a8ad + languageName: node + linkType: hard + +"has-symbols@npm:^1.0.3, has-symbols@npm:^1.1.0": + version: 1.1.0 + resolution: "has-symbols@npm:1.1.0" + checksum: b2316c7302a0e8ba3aaba215f834e96c22c86f192e7310bdf689dd0e6999510c89b00fbc5742571507cebf25764d68c988b3a0da217369a73596191ac0ce694b + languageName: node + linkType: hard + +"has-tostringtag@npm:^1.0.2": + version: 1.0.2 + resolution: "has-tostringtag@npm:1.0.2" + dependencies: + has-symbols: ^1.0.3 + checksum: 999d60bb753ad714356b2c6c87b7fb74f32463b8426e159397da4bde5bca7e598ab1073f4d8d4deafac297f2eb311484cd177af242776bf05f0d11565680468d + languageName: node + linkType: hard + +"hasown@npm:^2.0.2": + version: 2.0.2 + resolution: "hasown@npm:2.0.2" + dependencies: + function-bind: ^1.1.2 + checksum: e8516f776a15149ca6c6ed2ae3110c417a00b62260e222590e54aa367cbcd6ed99122020b37b7fbdf05748df57b265e70095d7bf35a47660587619b15ffb93db + languageName: node + linkType: hard + +"hpagent@npm:^1.2.0": + version: 1.2.0 + resolution: "hpagent@npm:1.2.0" + checksum: b029da695edae438cee4da2a437386f9db4ac27b3ceb7306d02e1b586c9c194741ed2e943c8a222e0cfefaf27ee3f863aca7ba1721b0950a2a19bf25bc0d85e2 + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.1.1": + version: 4.2.0 + resolution: "http-cache-semantics@npm:4.2.0" + checksum: 7a7246ddfce629f96832791176fd643589d954e6f3b49548dadb4290451961237fab8fcea41cd2008fe819d95b41c1e8b97f47d088afc0a1c81705287b4ddbcc + languageName: node + linkType: hard + +"http-proxy-agent@npm:^7.0.0": + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" + dependencies: + agent-base: ^7.1.0 + debug: ^4.3.4 + checksum: 670858c8f8f3146db5889e1fa117630910101db601fff7d5a8aa637da0abedf68c899f03d3451cac2f83bcc4c3d2dabf339b3aa00ff8080571cceb02c3ce02f3 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^7.0.1": + version: 7.0.6 + resolution: "https-proxy-agent@npm:7.0.6" + dependencies: + agent-base: ^7.1.2 + debug: 4 + checksum: b882377a120aa0544846172e5db021fa8afbf83fea2a897d397bd2ddd8095ab268c24bc462f40a15f2a8c600bf4aa05ce52927f70038d4014e68aefecfa94e8d + languageName: node + linkType: hard + +"iconv-lite@npm:^0.7.2": + version: 0.7.2 + resolution: "iconv-lite@npm:0.7.2" + dependencies: + safer-buffer: ">= 2.1.2 < 3.0.0" + checksum: faf884c1f631a5d676e3e64054bed891c7c5f616b790082d99ccfbfd017c661a39db8009160268fd65fae57c9154d4d491ebc9c301f3446a078460ef114dc4b8 + languageName: node + linkType: hard + +"ignore@npm:^5.2.0": + version: 5.3.2 + resolution: "ignore@npm:5.3.2" + checksum: 2acfd32a573260ea522ea0bfeff880af426d68f6831f973129e2ba7363f422923cf53aab62f8369cbf4667c7b25b6f8a3761b34ecdb284ea18e87a5262a865be + languageName: node + linkType: hard + +"ignore@npm:^7.0.5": + version: 7.0.5 + resolution: "ignore@npm:7.0.5" + checksum: d0862bf64d3d58bf34d5fb0a9f725bec9ca5ce8cd1aecc8f28034269e8f69b8009ffd79ca3eda96962a6a444687781cd5efdb8c7c8ddc0a6996e36d31c217f14 + languageName: node + linkType: hard + +"import-fresh@npm:^3.2.1": + version: 3.3.1 + resolution: "import-fresh@npm:3.3.1" + dependencies: + parent-module: ^1.0.0 + resolve-from: ^4.0.0 + checksum: a06b19461b4879cc654d46f8a6244eb55eb053437afd4cbb6613cad6be203811849ed3e4ea038783092879487299fda24af932b86bdfff67c9055ba3612b8c87 + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 7cae75c8cd9a50f57dadd77482359f659eaebac0319dd9368bcd1714f55e65badd6929ca58569da2b6494ef13fdd5598cd700b1eba23f8b79c5f19d195a3ecf7 + languageName: node + linkType: hard + +"ip-address@npm:^10.0.1": + version: 10.1.0 + resolution: "ip-address@npm:10.1.0" + checksum: 76b1abcdf52a32e2e05ca1f202f3a8ab8547e5651a9233781b330271bd7f1a741067748d71c4cbb9d9906d9f1fa69e7ddc8b4a11130db4534fdab0e908c84e0d + languageName: node + linkType: hard + +"is-extglob@npm:^2.1.1": + version: 2.1.1 + resolution: "is-extglob@npm:2.1.1" + checksum: df033653d06d0eb567461e58a7a8c9f940bd8c22274b94bf7671ab36df5719791aae15eef6d83bbb5e23283967f2f984b8914559d4449efda578c775c4be6f85 + languageName: node + linkType: hard + +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.3": + version: 4.0.3 + resolution: "is-glob@npm:4.0.3" + dependencies: + is-extglob: ^2.1.1 + checksum: d381c1319fcb69d341cc6e6c7cd588e17cd94722d9a32dbd60660b993c4fb7d0f19438674e68dfec686d09b7c73139c9166b47597f846af387450224a8101ab4 + languageName: node + linkType: hard + +"is-number@npm:^7.0.0": + version: 7.0.0 + resolution: "is-number@npm:7.0.0" + checksum: 456ac6f8e0f3111ed34668a624e45315201dff921e5ac181f8ec24923b99e9f32ca1a194912dc79d539c97d33dba17dc635202ff0b2cf98326f608323276d27a + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 26bf6c5480dda5161c820c5b5c751ae1e766c587b1f951ea3fcfc973bafb7831ae5b54a31a69bd670220e42e99ec154475025a468eae58ea262f813fdc8d1c62 + languageName: node + linkType: hard + +"isexe@npm:^4.0.0": + version: 4.0.0 + resolution: "isexe@npm:4.0.0" + checksum: 2ead327ef596042ef9c9ec5f236b316acfaedb87f4bb61b3c3d574fb2e9c8a04b67305e04733bde52c24d9622fdebd3270aadb632adfbf9cadef88fe30f479e5 + languageName: node + linkType: hard + +"isomorphic-ws@npm:^5.0.0": + version: 5.0.0 + resolution: "isomorphic-ws@npm:5.0.0" + peerDependencies: + ws: "*" + checksum: e20eb2aee09ba96247465fda40c6d22c1153394c0144fa34fe6609f341af4c8c564f60ea3ba762335a7a9c306809349f9b863c8beedf2beea09b299834ad5398 + languageName: node + linkType: hard + +"jose@npm:^6.1.3": + version: 6.2.2 + resolution: "jose@npm:6.2.2" + checksum: 37838e1654b2f76f939678d0528c82100156e12eb717bbce2c6e9fd136f83e8c960f9ffb3b675dcb768b6cdd56d5c806bf56f468f8edd8fb57b165c3f18f0902 + languageName: node + linkType: hard + +"js-yaml@npm:4.1.1, js-yaml@npm:^4.1.0, js-yaml@npm:^4.1.1": + version: 4.1.1 + resolution: "js-yaml@npm:4.1.1" + dependencies: + argparse: ^2.0.1 + bin: + js-yaml: bin/js-yaml.js + checksum: ea2339c6930fe048ec31b007b3c90be2714ab3e7defcc2c27ebf30c74fd940358f29070b4345af0019ef151875bf3bc3f8644bea1bab0372652b5044813ac02d + languageName: node + linkType: hard + +"jsep@npm:^1.4.0": + version: 1.4.0 + resolution: "jsep@npm:1.4.0" + checksum: 8e7af5ecb91483b227092b87a3e85b5df3e848dbe6f201b19efcb18047567530d21dfeecb0978e09d1f66554fcfaed84176819eeacdfc86f61dc05c40c18f824 + languageName: node + linkType: hard + +"json-buffer@npm:3.0.1": + version: 3.0.1 + resolution: "json-buffer@npm:3.0.1" + checksum: 9026b03edc2847eefa2e37646c579300a1f3a4586cfb62bf857832b60c852042d0d6ae55d1afb8926163fa54c2b01d83ae24705f34990348bdac6273a29d4581 + languageName: node + linkType: hard + +"json-schema-traverse@npm:^0.4.1": + version: 0.4.1 + resolution: "json-schema-traverse@npm:0.4.1" + checksum: 7486074d3ba247769fda17d5181b345c9fb7d12e0da98b22d1d71a5db9698d8b4bd900a3ec1a4ffdd60846fc2556274a5c894d0c48795f14cb03aeae7b55260b + languageName: node + linkType: hard + +"json-stable-stringify-without-jsonify@npm:^1.0.1": + version: 1.0.1 + resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" + checksum: cff44156ddce9c67c44386ad5cddf91925fe06b1d217f2da9c4910d01f358c6e3989c4d5a02683c7a5667f9727ff05831f7aa8ae66c8ff691c556f0884d49215 + languageName: node + linkType: hard + +"jsonfile@npm:^6.0.1": + version: 6.2.0 + resolution: "jsonfile@npm:6.2.0" + dependencies: + graceful-fs: ^4.1.6 + universalify: ^2.0.0 + dependenciesMeta: + graceful-fs: + optional: true + checksum: c3028ec5c770bb41290c9bb9ca04bdd0a1b698ddbdf6517c9453d3f90fc9e000c9675959fb46891d317690a93c62de03ff1735d8dbe02be83e51168ce85815d3 + languageName: node + linkType: hard + +"jsonpath-plus@npm:^10.3.0": + version: 10.4.0 + resolution: "jsonpath-plus@npm:10.4.0" + dependencies: + "@jsep-plugin/assignment": ^1.3.0 + "@jsep-plugin/regex": ^1.0.4 + jsep: ^1.4.0 + bin: + jsonpath: bin/jsonpath-cli.js + jsonpath-plus: bin/jsonpath-cli.js + checksum: c736fb09a477f98eb8ceb05a88b71b426ae636d6f9454da32f2bde802c5c0c2f5927f186786024ff9b3360543467b5423d6849d66f42864cc6e8d1ec16017cfb + languageName: node + linkType: hard + +"keyv@npm:^4.5.4": + version: 4.5.4 + resolution: "keyv@npm:4.5.4" + dependencies: + json-buffer: 3.0.1 + checksum: 74a24395b1c34bd44ad5cb2b49140d087553e170625240b86755a6604cd65aa16efdbdeae5cdb17ba1284a0fbb25ad06263755dbc71b8d8b06f74232ce3cdd72 + languageName: node + linkType: hard + +"levn@npm:^0.4.1": + version: 0.4.1 + resolution: "levn@npm:0.4.1" + dependencies: + prelude-ls: ^1.2.1 + type-check: ~0.4.0 + checksum: 12c5021c859bd0f5248561bf139121f0358285ec545ebf48bb3d346820d5c61a4309535c7f387ed7d84361cf821e124ce346c6b7cef8ee09a67c1473b46d0fc4 + languageName: node + linkType: hard + +"locate-path@npm:^6.0.0": + version: 6.0.0 + resolution: "locate-path@npm:6.0.0" + dependencies: + p-locate: ^5.0.0 + checksum: 72eb661788a0368c099a184c59d2fee760b3831c9c1c33955e8a19ae4a21b4116e53fa736dc086cdeb9fce9f7cc508f2f92d2d3aae516f133e16a2bb59a39f5a + languageName: node + linkType: hard + +"lodash.clonedeepwith@npm:4.5.0": + version: 4.5.0 + resolution: "lodash.clonedeepwith@npm:4.5.0" + checksum: 9fbf4ebfa04b381df226a2298eba680327bea3d0d5d19c5118de7ae218fd219186e30e9fd0d33b13729f34ffbc83c1cf09cb27aff265ba94cb602b8a2b1e71c9 + languageName: node + linkType: hard + +"lodash.merge@npm:^4.6.2": + version: 4.6.2 + resolution: "lodash.merge@npm:4.6.2" + checksum: ad580b4bdbb7ca1f7abf7e1bce63a9a0b98e370cf40194b03380a46b4ed799c9573029599caebc1b14e3f24b111aef72b96674a56cfa105e0f5ac70546cdc005 + languageName: node + linkType: hard + +"lodash.mergewith@npm:4.6.2": + version: 4.6.2 + resolution: "lodash.mergewith@npm:4.6.2" + checksum: a6db2a9339752411f21b956908c404ec1e088e783a65c8b29e30ae5b3b6384f82517662d6f425cc97c2070b546cc2c7daaa8d33f78db7b6e9be06cd834abdeb8 + languageName: node + linkType: hard + +"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1": + version: 11.3.3 + resolution: "lru-cache@npm:11.3.3" + checksum: 35d2d2565e7ac35c90b7b57dacddb2728c8f071640ca9bc3d83f5641a7dd1ca1ac2a0aa23eb72c6fd23653f49a5da04262cc28a19f14946869ff69dcc2e75746 + languageName: node + linkType: hard + +"make-fetch-happen@npm:^15.0.0": + version: 15.0.5 + resolution: "make-fetch-happen@npm:15.0.5" + dependencies: + "@gar/promise-retry": ^1.0.0 + "@npmcli/agent": ^4.0.0 + "@npmcli/redact": ^4.0.0 + cacache: ^20.0.1 + http-cache-semantics: ^4.1.1 + minipass: ^7.0.2 + minipass-fetch: ^5.0.0 + minipass-flush: ^1.0.5 + minipass-pipeline: ^1.2.4 + negotiator: ^1.0.0 + proc-log: ^6.0.0 + ssri: ^13.0.0 + checksum: e43195c1e98d37acb4358eb574e6b4a591cd46624cbb4800ab2988bd21a3e7b4a26f94b561af119637643b87144e2adf03808909fefa4f88122ee1b3af7e9400 + languageName: node + linkType: hard + +"math-intrinsics@npm:^1.1.0": + version: 1.1.0 + resolution: "math-intrinsics@npm:1.1.0" + checksum: 0e513b29d120f478c85a70f49da0b8b19bc638975eca466f2eeae0071f3ad00454c621bf66e16dd435896c208e719fc91ad79bbfba4e400fe0b372e7c1c9c9a2 + languageName: node + linkType: hard + +"micromatch@npm:^4.0.8": + version: 4.0.8 + resolution: "micromatch@npm:4.0.8" + dependencies: + braces: ^3.0.3 + picomatch: ^2.3.1 + checksum: 79920eb634e6f400b464a954fcfa589c4e7c7143209488e44baf627f9affc8b1e306f41f4f0deedde97e69cb725920879462d3e750ab3bd3c1aed675bb3a8966 + languageName: node + linkType: hard + +"mime-db@npm:1.52.0": + version: 1.52.0 + resolution: "mime-db@npm:1.52.0" + checksum: 0d99a03585f8b39d68182803b12ac601d9c01abfa28ec56204fa330bc9f3d1c5e14beb049bafadb3dbdf646dfb94b87e24d4ec7b31b7279ef906a8ea9b6a513f + languageName: node + linkType: hard + +"mime-types@npm:^2.1.12": + version: 2.1.35 + resolution: "mime-types@npm:2.1.35" + dependencies: + mime-db: 1.52.0 + checksum: 89a5b7f1def9f3af5dad6496c5ed50191ae4331cc5389d7c521c8ad28d5fdad2d06fd81baf38fed813dc4e46bb55c8145bb0ff406330818c9cf712fb2e9b3836 + languageName: node + linkType: hard + +"minimatch@npm:^10.2.2, minimatch@npm:^10.2.4": + version: 10.2.5 + resolution: "minimatch@npm:10.2.5" + dependencies: + brace-expansion: ^5.0.5 + checksum: 000423875fecbc7da1d74bf63c9081363a71291ef2588c376c45647ac004582cb5bc8cc09ef84420b26bfb490f4d0818d328e78569c6228e20d90271283f73ba + languageName: node + linkType: hard + +"minimatch@npm:^3.1.5": + version: 3.1.5 + resolution: "minimatch@npm:3.1.5" + dependencies: + brace-expansion: ^1.1.7 + checksum: 47ef6f412c08be045a7291d11b1c40777925accf7252dc6d3caa39b1bfbb3a7ea390ba7aba464d762d783265c644143d2c8a204e6b5763145024d52ee65a1941 + languageName: node + linkType: hard + +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" + dependencies: + minipass: ^7.0.3 + checksum: b251bceea62090f67a6cced7a446a36f4cd61ee2d5cea9aee7fff79ba8030e416327a1c5aa2908dc22629d06214b46d88fdab8c51ac76bacbf5703851b5ad342 + languageName: node + linkType: hard + +"minipass-fetch@npm:^5.0.0": + version: 5.0.2 + resolution: "minipass-fetch@npm:5.0.2" + dependencies: + iconv-lite: ^0.7.2 + minipass: ^7.0.3 + minipass-sized: ^2.0.0 + minizlib: ^3.0.1 + dependenciesMeta: + iconv-lite: + optional: true + checksum: d4dfdd9700fc8aba445834f75f2abaf9e5d404c10eda06e2db4a8ba89fc66a26956d19703d0edf9be864cb30dec22356d343509ad0a105446516c0ead4330328 + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.7 + resolution: "minipass-flush@npm:1.0.7" + dependencies: + minipass: ^3.0.0 + checksum: dc43fd1644aaea31b6ba88281d928a136b9fcd5425c718791e1007db15cf2cd41c75d073548d2f46088f90971833b3bd86752d2a2612bf8256122dedf5b7f3db + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: ^3.0.0 + checksum: b14240dac0d29823c3d5911c286069e36d0b81173d7bdf07a7e4a91ecdef92cdff4baaf31ea3746f1c61e0957f652e641223970870e2353593f382112257971b + languageName: node + linkType: hard + +"minipass-sized@npm:^2.0.0": + version: 2.0.0 + resolution: "minipass-sized@npm:2.0.0" + dependencies: + minipass: ^7.1.2 + checksum: 1a1fd251aef4e24050a04ea03fdc0514960f7304a374fd01f352bfdb72c0a2c084ad05d63e76011c181cadfb38dbf487f8782e1e778337f6a099ac2da26b6d5d + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: ^4.0.0 + checksum: a30d083c8054cee83cdcdc97f97e4641a3f58ae743970457b1489ce38ee1167b3aaf7d815cd39ec7a99b9c40397fd4f686e83750e73e652b21cb516f6d845e48 + languageName: node + linkType: hard + +"minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2, minipass@npm:^7.1.3": + version: 7.1.3 + resolution: "minipass@npm:7.1.3" + checksum: 2ede17c0bf8fec499be3360fd07f0ec7666189e3907320a9b653f1530cf84af98928c5b12d80bfb75f321833bf2e97785b940540213ebdafe97a5f10327e664d + languageName: node + linkType: hard + +"minizlib@npm:^3.0.1, minizlib@npm:^3.1.0": + version: 3.1.0 + resolution: "minizlib@npm:3.1.0" + dependencies: + minipass: ^7.1.2 + checksum: a15e6f0128f514b7d41a1c68ce531155447f4669e32d279bba1c1c071ef6c2abd7e4d4579bb59ccc2ed1531346749665968fdd7be8d83eb6b6ae2fe1f3d370a7 + languageName: node + linkType: hard + +"ms@npm:^2.1.3": + version: 2.1.3 + resolution: "ms@npm:2.1.3" + checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d + languageName: node + linkType: hard + +"natural-compare@npm:^1.4.0": + version: 1.4.0 + resolution: "natural-compare@npm:1.4.0" + checksum: 23ad088b08f898fc9b53011d7bb78ec48e79de7627e01ab5518e806033861bef68d5b0cd0e2205c2f36690ac9571ff6bcb05eb777ced2eeda8d4ac5b44592c3d + languageName: node + linkType: hard + +"negotiator@npm:^1.0.0": + version: 1.0.0 + resolution: "negotiator@npm:1.0.0" + checksum: 20ebfe79b2d2e7cf9cbc8239a72662b584f71164096e6e8896c8325055497c96f6b80cd22c258e8a2f2aa382a787795ec3ee8b37b422a302c7d4381b0d5ecfbb + languageName: node + linkType: hard + +"node-fetch@npm:^2.7.0": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: ^5.0.0 + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: d76d2f5edb451a3f05b15115ec89fc6be39de37c6089f1b6368df03b91e1633fd379a7e01b7ab05089a25034b2023d959b47e59759cb38d88341b2459e89d6e5 + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 12.2.0 + resolution: "node-gyp@npm:12.2.0" + dependencies: + env-paths: ^2.2.0 + exponential-backoff: ^3.1.1 + graceful-fs: ^4.2.6 + make-fetch-happen: ^15.0.0 + nopt: ^9.0.0 + proc-log: ^6.0.0 + semver: ^7.3.5 + tar: ^7.5.4 + tinyglobby: ^0.2.12 + which: ^6.0.0 + bin: + node-gyp: bin/node-gyp.js + checksum: d4ce0acd08bd41004f45e10cef468f4bd15eaafb3acc388a0c567416e1746dc005cc080b8a3495e4e2ae2eed170a2123ff622c2d6614062f4a839837dcf1dd9d + languageName: node + linkType: hard + +"nopt@npm:^9.0.0": + version: 9.0.0 + resolution: "nopt@npm:9.0.0" + dependencies: + abbrev: ^4.0.0 + bin: + nopt: bin/nopt.js + checksum: 7a5d9ab0629eaec1944a95438cc4efa6418ed2834aa8eb21a1bea579a7d8ac3e30120131855376a96ef59ab0e23ad8e0bc94d3349770a95e5cb7119339f7c7fb + languageName: node + linkType: hard + +"oauth4webapi@npm:^3.8.4": + version: 3.8.5 + resolution: "oauth4webapi@npm:3.8.5" + checksum: 4e4915e38e12e1807f84b185676b2a7bd79ebdeb46b35dc772d4abf0b0ef0a690613558298db6453006bc350ca883ff6bdb22717fe3d7e5101155562b30b0df1 + languageName: node + linkType: hard + +"once@npm:^1.3.1, once@npm:^1.4.0": + version: 1.4.0 + resolution: "once@npm:1.4.0" + dependencies: + wrappy: 1 + checksum: cd0a88501333edd640d95f0d2700fbde6bff20b3d4d9bdc521bdd31af0656b5706570d6c6afe532045a20bb8dc0849f8332d6f2a416e0ba6d3d3b98806c7db68 + languageName: node + linkType: hard + +"openid-client@npm:^6.1.3": + version: 6.8.2 + resolution: "openid-client@npm:6.8.2" + dependencies: + jose: ^6.1.3 + oauth4webapi: ^3.8.4 + checksum: 35f76a03f33a1d87f8ebeaebd6b6263eee17905823ae448f590e82e5a9168e3fec98e0788e541d251352b17560c7a20ce90acee798ecae907cea6fb675a4eab8 + languageName: node + linkType: hard + +"optionator@npm:^0.9.3": + version: 0.9.4 + resolution: "optionator@npm:0.9.4" + dependencies: + deep-is: ^0.1.3 + fast-levenshtein: ^2.0.6 + levn: ^0.4.1 + prelude-ls: ^1.2.1 + type-check: ^0.4.0 + word-wrap: ^1.2.5 + checksum: ecbd010e3dc73e05d239976422d9ef54a82a13f37c11ca5911dff41c98a6c7f0f163b27f922c37e7f8340af9d36febd3b6e9cef508f3339d4c393d7276d716bb + languageName: node + linkType: hard + +"orchestrator-e2e-tests@workspace:.": + version: 0.0.0-use.local + resolution: "orchestrator-e2e-tests@workspace:." + dependencies: + "@eslint/js": ^9.39.2 + "@playwright/test": 1.57.0 + "@red-hat-developer-hub/e2e-test-utils": "redhat-developer/rhdh-e2e-test-utils#main" + "@types/node": ^24.10.1 + dotenv: ^16.4.7 + eslint: ^9.39.2 + eslint-plugin-check-file: ^3.3.1 + eslint-plugin-playwright: ^2.4.0 + prettier: ^3.7.4 + typescript: ^5.9.3 + typescript-eslint: ^8.50.0 + languageName: unknown + linkType: soft + +"otplib@npm:12.0.1": + version: 12.0.1 + resolution: "otplib@npm:12.0.1" + dependencies: + "@otplib/core": ^12.0.1 + "@otplib/preset-default": ^12.0.1 + "@otplib/preset-v11": ^12.0.1 + checksum: 4a1b91cf1b8e920b50ad4bac2ef2a89126630c62daf68e9b32ff15106b2551db905d3b979955cf5f8f114da0a8883cec3d636901d65e793c1745bb4174e2a572 + languageName: node + linkType: hard + +"p-limit@npm:^3.0.2": + version: 3.1.0 + resolution: "p-limit@npm:3.1.0" + dependencies: + yocto-queue: ^0.1.0 + checksum: 7c3690c4dbf62ef625671e20b7bdf1cbc9534e83352a2780f165b0d3ceba21907e77ad63401708145ca4e25bfc51636588d89a8c0aeb715e6c37d1c066430360 + languageName: node + linkType: hard + +"p-locate@npm:^5.0.0": + version: 5.0.0 + resolution: "p-locate@npm:5.0.0" + dependencies: + p-limit: ^3.0.2 + checksum: 1623088f36cf1cbca58e9b61c4e62bf0c60a07af5ae1ca99a720837356b5b6c5ba3eb1b2127e47a06865fee59dd0453cad7cc844cda9d5a62ac1a5a51b7c86d3 + languageName: node + linkType: hard + +"p-map@npm:^7.0.2": + version: 7.0.4 + resolution: "p-map@npm:7.0.4" + checksum: 4be2097e942f2fd3a4f4b0c6585c721f23851de8ad6484d20c472b3ea4937d5cd9a59914c832b1bceac7bf9d149001938036b82a52de0bc381f61ff2d35d26a5 + languageName: node + linkType: hard + +"parent-module@npm:^1.0.0": + version: 1.0.1 + resolution: "parent-module@npm:1.0.1" + dependencies: + callsites: ^3.0.0 + checksum: 6ba8b255145cae9470cf5551eb74be2d22281587af787a2626683a6c20fbb464978784661478dd2a3f1dad74d1e802d403e1b03c1a31fab310259eec8ac560ff + languageName: node + linkType: hard + +"path-exists@npm:^4.0.0": + version: 4.0.0 + resolution: "path-exists@npm:4.0.0" + checksum: 505807199dfb7c50737b057dd8d351b82c033029ab94cb10a657609e00c1bc53b951cfdbccab8de04c5584d5eff31128ce6afd3db79281874a5ef2adbba55ed1 + languageName: node + linkType: hard + +"path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 55cd7a9dd4b343412a8386a743f9c746ef196e57c823d90ca3ab917f90ab9f13dd0ded27252ba49dbdfcab2b091d998bc446f6220cd3cea65db407502a740020 + languageName: node + linkType: hard + +"path-scurry@npm:^2.0.2": + version: 2.0.2 + resolution: "path-scurry@npm:2.0.2" + dependencies: + lru-cache: ^11.0.0 + minipass: ^7.1.2 + checksum: a723afe86e342e19dd1b49ce4f5b64a9a84b1e2e07ffc62f051c11623ecd461b1bf1599eee1ecacfce03dda8b6bb866a5df80c0ded45375d258ff22f631920a7 + languageName: node + linkType: hard + +"picomatch@npm:^2.3.1": + version: 2.3.2 + resolution: "picomatch@npm:2.3.2" + checksum: 0a3f5b9ff28faf022e1429b66e47c122e19e7b31cbd098095d29e949684e7ff1d9b83a2133d931326a53ec6ec11c7c59b1850c27fde2f26ca1d5f35861e9701a + languageName: node + linkType: hard + +"picomatch@npm:^4.0.4": + version: 4.0.4 + resolution: "picomatch@npm:4.0.4" + checksum: 76b387b5157951422fa6049a96bdd1695e39dd126cd99df34d343638dc5cdb8bcdc83fff288c23eddcf7c26657c35e3173d4d5f488c4f28b889b314472e0a662 + languageName: node + linkType: hard + +"playwright-core@npm:1.57.0": + version: 1.57.0 + resolution: "playwright-core@npm:1.57.0" + bin: + playwright-core: cli.js + checksum: 960e80d6ec06305b11a3ca9e78e8e4201cc17f37dd37279cb6fece4df43d74bf589833f4f94535fadd284b427f98c5f1cf09368e22f0f00b6a9477571ce6b03b + languageName: node + linkType: hard + +"playwright@npm:1.57.0": + version: 1.57.0 + resolution: "playwright@npm:1.57.0" + dependencies: + fsevents: 2.3.2 + playwright-core: 1.57.0 + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 176fd9fd890f390e0aa00d42697b70072d534243b15467d9430f3af329e77b3225b67a0afa12ea76fb440300dabd92d4cf040baf5edceee8eeff0ee1590ae5b7 + languageName: node + linkType: hard + +"prelude-ls@npm:^1.2.1": + version: 1.2.1 + resolution: "prelude-ls@npm:1.2.1" + checksum: cd192ec0d0a8e4c6da3bb80e4f62afe336df3f76271ac6deb0e6a36187133b6073a19e9727a1ff108cd8b9982e4768850d413baa71214dd80c7979617dca827a + languageName: node + linkType: hard + +"prettier@npm:3.8.1, prettier@npm:^3.7.4": + version: 3.8.1 + resolution: "prettier@npm:3.8.1" + bin: + prettier: bin/prettier.cjs + checksum: 36fe4ecd95751aa17fea70b48afd5086e88002988238112fc1be30a5307af6983e1833be790b0cc1c54702b71f73b12bfec12c05166d7619e3151ab221654297 + languageName: node + linkType: hard + +"proc-log@npm:^6.0.0": + version: 6.1.0 + resolution: "proc-log@npm:6.1.0" + checksum: ac450ff8244e95b0c9935b52d629fef92ae69b7e39aea19972a8234259614d644402dd62ce9cb094f4a637d8a4514cba90c1456ad785a40ad5b64d502875a817 + languageName: node + linkType: hard + +"proper-lockfile@npm:4.1.2": + version: 4.1.2 + resolution: "proper-lockfile@npm:4.1.2" + dependencies: + graceful-fs: ^4.2.4 + retry: ^0.12.0 + signal-exit: ^3.0.2 + checksum: 00078ee6a61c216a56a6140c7d2a98c6c733b3678503002dc073ab8beca5d50ca271de4c85fca13b9b8ee2ff546c36674d1850509b84a04a5d0363bcb8638939 + languageName: node + linkType: hard + +"pump@npm:^3.0.0": + version: 3.0.4 + resolution: "pump@npm:3.0.4" + dependencies: + end-of-stream: ^1.1.0 + once: ^1.3.1 + checksum: d043c3e710c56ffd280711e98a94e863ab334f79ea43cee0fb70e1349b2355ffd2ff287c7522e4c960a247699d5b7825f00fa090b85d6179c973be13f78a6c49 + languageName: node + linkType: hard + +"punycode@npm:^2.1.0": + version: 2.3.1 + resolution: "punycode@npm:2.3.1" + checksum: bb0a0ceedca4c3c57a9b981b90601579058903c62be23c5e8e843d2c2d4148a3ecf029d5133486fb0e1822b098ba8bba09e89d6b21742d02fa26bda6441a6fb2 + languageName: node + linkType: hard + +"resolve-from@npm:^4.0.0": + version: 4.0.0 + resolution: "resolve-from@npm:4.0.0" + checksum: f4ba0b8494846a5066328ad33ef8ac173801a51739eb4d63408c847da9a2e1c1de1e6cbbf72699211f3d13f8fc1325648b169bd15eb7da35688e30a5fb0e4a7f + languageName: node + linkType: hard + +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 623bd7d2e5119467ba66202d733ec3c2e2e26568074923bc0585b6b99db14f357e79bdedb63cab56cec47491c4a0da7e6021a7465ca6dc4f481d3898fdd3158c + languageName: node + linkType: hard + +"rfc4648@npm:^1.3.0": + version: 1.5.4 + resolution: "rfc4648@npm:1.5.4" + checksum: 4bfd555f16b8ed1bceb3cf4d79242cf3253a2600de51f45ac19549f366b4b8edc0d9b8feaae778059c137f1d10ebd87e125fc839636f6267a7b6badaa95a9f00 + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3.0.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: cab8f25ae6f1434abee8d80023d7e72b598cf1327164ddab31003c51215526801e40b66c5e65d658a0af1e9d6478cadcb4c745f4bd6751f97d8644786c0978b0 + languageName: node + linkType: hard + +"semver@npm:^7.3.5, semver@npm:^7.7.3": + version: 7.7.4 + resolution: "semver@npm:7.7.4" + bin: + semver: bin/semver.js + checksum: 9b4a6a58e98b9723fafcafa393c9d4e8edefaa60b8dfbe39e30892a3604cf1f45f52df9cfb1ae1a22b44c8b3d57fec8a9bb7b3e1645431587cb272399ede152e + languageName: node + linkType: hard + +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: ^3.0.0 + checksum: 6b52fe87271c12968f6a054e60f6bde5f0f3d2db483a1e5c3e12d657c488a15474121a1d55cd958f6df026a54374ec38a4a963988c213b7570e1d51575cea7fa + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 1a2bcae50de99034fcd92ad4212d8e01eedf52c7ec7830eedcf886622804fe36884278f2be8be0ea5fde3fd1c23911643a4e0f726c8685b61871c8908af01222 + languageName: node + linkType: hard + +"signal-exit@npm:^3.0.2": + version: 3.0.7 + resolution: "signal-exit@npm:3.0.7" + checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: b5167a7142c1da704c0e3af85c402002b597081dd9575031a90b4f229ca5678e9a36e8a374f1814c8156a725d17008ae3bde63b92f9cfd132526379e580bec8b + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^8.0.3, socks-proxy-agent@npm:^8.0.4": + version: 8.0.5 + resolution: "socks-proxy-agent@npm:8.0.5" + dependencies: + agent-base: ^7.1.2 + debug: ^4.3.4 + socks: ^2.8.3 + checksum: b4fbcdb7ad2d6eec445926e255a1fb95c975db0020543fbac8dfa6c47aecc6b3b619b7fb9c60a3f82c9b2969912a5e7e174a056ae4d98cb5322f3524d6036e1d + languageName: node + linkType: hard + +"socks@npm:^2.8.3": + version: 2.8.7 + resolution: "socks@npm:2.8.7" + dependencies: + ip-address: ^10.0.1 + smart-buffer: ^4.2.0 + checksum: 4bbe2c88cf0eeaf49f94b7f11564a99b2571bde6fd1e714ff95b38f89e1f97858c19e0ab0e6d39eb7f6a984fa67366825895383ed563fe59962a1d57a1d55318 + languageName: node + linkType: hard + +"ssri@npm:^13.0.0": + version: 13.0.1 + resolution: "ssri@npm:13.0.1" + dependencies: + minipass: ^7.0.3 + checksum: 42acbdbd485e9a5a198de2198b6fd474d1e84bff6bea5d95aa0a8aa26ea78ce44f2097ac481e767f0406de7ceccfa4669584116d4fcf2d4e2dba7034d7c34930 + languageName: node + linkType: hard + +"stream-buffers@npm:^3.0.2": + version: 3.0.3 + resolution: "stream-buffers@npm:3.0.3" + checksum: 3f0bdc4b1fd3ff370cef5a2103dd930b8981d42d97741eeb087a660771e27f0fc35fa8a351bb36e15bbbbce0eea00fefed60d6cdff4c6c3f527580529f183807 + languageName: node + linkType: hard + +"streamx@npm:^2.12.5, streamx@npm:^2.15.0, streamx@npm:^2.25.0": + version: 2.25.0 + resolution: "streamx@npm:2.25.0" + dependencies: + events-universal: ^1.0.0 + fast-fifo: ^1.3.2 + text-decoder: ^1.1.0 + checksum: eb38bcf6724cc65ba953e03199055be42d2eb2c1695aca613c46b5ef8a2e9dcfb7ef2ff2ea5eeceae26db9896ce6e29918967211a6ad4311577554fb14db99f8 + languageName: node + linkType: hard + +"strip-json-comments@npm:^3.1.1": + version: 3.1.1 + resolution: "strip-json-comments@npm:3.1.1" + checksum: 492f73e27268f9b1c122733f28ecb0e7e8d8a531a6662efbd08e22cccb3f9475e90a1b82cab06a392f6afae6d2de636f977e231296400d0ec5304ba70f166443 + languageName: node + linkType: hard + +"supports-color@npm:^7.1.0": + version: 7.2.0 + resolution: "supports-color@npm:7.2.0" + dependencies: + has-flag: ^4.0.0 + checksum: 3dda818de06ebbe5b9653e07842d9479f3555ebc77e9a0280caf5a14fb877ffee9ed57007c3b78f5a6324b8dbeec648d9e97a24e2ed9fdb81ddc69ea07100f4a + languageName: node + linkType: hard + +"tar-fs@npm:^3.0.9": + version: 3.1.2 + resolution: "tar-fs@npm:3.1.2" + dependencies: + bare-fs: ^4.0.1 + bare-path: ^3.0.0 + pump: ^3.0.0 + tar-stream: ^3.1.5 + dependenciesMeta: + bare-fs: + optional: true + bare-path: + optional: true + checksum: 2c591863823bfe90b9a1fe75dd53a340d90621390873cc8c313a5d6755a986af42562eaa2d6c2e5fc557c76c1882f4566d653e46453d5b93d96a80217d6c9711 + languageName: node + linkType: hard + +"tar-stream@npm:^3.1.5": + version: 3.1.8 + resolution: "tar-stream@npm:3.1.8" + dependencies: + b4a: ^1.6.4 + bare-fs: ^4.5.5 + fast-fifo: ^1.2.0 + streamx: ^2.15.0 + checksum: 2064263433bd2ab93cecc6048101f9a104fdc402d5e15571354fcbf93b8a91c72d94b11113d2292c8dc93e0f0a34c5b57ea39418fda61cb3aaa75a21f9faecfb + languageName: node + linkType: hard + +"tar@npm:^7.5.4": + version: 7.5.13 + resolution: "tar@npm:7.5.13" + dependencies: + "@isaacs/fs-minipass": ^4.0.0 + chownr: ^3.0.0 + minipass: ^7.1.2 + minizlib: ^3.1.0 + yallist: ^5.0.0 + checksum: adcc2a9179dab1b36ecb26575e698d2df8491a1df2cc83e2a0fdd8eaefd076da60dd2e20383a37760b5790bee34e9291aa2b2a9b3deef37ff03c1046219e5df7 + languageName: node + linkType: hard + +"teex@npm:^1.0.1": + version: 1.0.1 + resolution: "teex@npm:1.0.1" + dependencies: + streamx: ^2.12.5 + checksum: 36bf7ce8bb5eb428ad7b14b695ee7fb0a02f09c1a9d8181cc42531208543a920b299d711bf78dad4ff9bcf36ac437ae8e138053734746076e3e0e7d6d76eef64 + languageName: node + linkType: hard + +"text-decoder@npm:^1.1.0": + version: 1.2.7 + resolution: "text-decoder@npm:1.2.7" + dependencies: + b4a: ^1.6.4 + checksum: a544f8490806675986e9703cda2d0809d7bd010adf0cc19ac9975791912ea9e6998cd4696d7d7e9392c5648e660111a865c983e8adfeb29699fab471548f82a3 + languageName: node + linkType: hard + +"thirty-two@npm:^1.0.2": + version: 1.0.2 + resolution: "thirty-two@npm:1.0.2" + checksum: f6700b31d16ef942fdc0d14daed8a2f69ea8b60b0e85db8b83adf58d84bbeafe95a17d343ab55efaae571bb5148b62fc0ee12b04781323bf7af7d7e9693eec76 + languageName: node + linkType: hard + +"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15": + version: 0.2.16 + resolution: "tinyglobby@npm:0.2.16" + dependencies: + fdir: ^6.5.0 + picomatch: ^4.0.4 + checksum: db9d22ce1deb1095720a683c492cd5e80da0f71fed21ed697e2752f6f298edd8a1249dab197c86a26f001c180594a81bf532400fe519791ed2a2cb57b03bc337 + languageName: node + linkType: hard + +"to-regex-range@npm:^5.0.1": + version: 5.0.1 + resolution: "to-regex-range@npm:5.0.1" + dependencies: + is-number: ^7.0.0 + checksum: f76fa01b3d5be85db6a2a143e24df9f60dd047d151062d0ba3df62953f2f697b16fe5dad9b0ac6191c7efc7b1d9dcaa4b768174b7b29da89d4428e64bc0a20ed + languageName: node + linkType: hard + +"tr46@npm:~0.0.3": + version: 0.0.3 + resolution: "tr46@npm:0.0.3" + checksum: 726321c5eaf41b5002e17ffbd1fb7245999a073e8979085dacd47c4b4e8068ff5777142fc6726d6ca1fd2ff16921b48788b87225cbc57c72636f6efa8efbffe3 + languageName: node + linkType: hard + +"ts-api-utils@npm:^2.5.0": + version: 2.5.0 + resolution: "ts-api-utils@npm:2.5.0" + peerDependencies: + typescript: ">=4.8.4" + checksum: 5b2a2db7aa041d60b040df691ee5e73d534fb4cb3cf4fd6d2c27c584a32836a7ca8272fb23d865e673559ea639fdba35f8623249bf931df22188f0aaef7f0075 + languageName: node + linkType: hard + +"type-check@npm:^0.4.0, type-check@npm:~0.4.0": + version: 0.4.0 + resolution: "type-check@npm:0.4.0" + dependencies: + prelude-ls: ^1.2.1 + checksum: ec688ebfc9c45d0c30412e41ca9c0cdbd704580eb3a9ccf07b9b576094d7b86a012baebc95681999dd38f4f444afd28504cb3a89f2ef16b31d4ab61a0739025a + languageName: node + linkType: hard + +"typescript-eslint@npm:8.58.1, typescript-eslint@npm:^8.50.0": + version: 8.58.1 + resolution: "typescript-eslint@npm:8.58.1" + dependencies: + "@typescript-eslint/eslint-plugin": 8.58.1 + "@typescript-eslint/parser": 8.58.1 + "@typescript-eslint/typescript-estree": 8.58.1 + "@typescript-eslint/utils": 8.58.1 + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: fba919623d3ea7a4c10289a653f4387542ae4a0b2a15823bb4cf2ca5ce7b4ff0fcbc5868b11129bbd0d7117c4c1a70988e9fdc36ff4e166955e13f404c5fcf45 + languageName: node + linkType: hard + +"typescript@npm:6.0.2": + version: 6.0.2 + resolution: "typescript@npm:6.0.2" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: a95632e5e4f7b9e7d9e9e9b79b4f147b45181258e7cae4c9f9f2a24176aff688d25e798cddf78cc39cb95423c8c63b36fd4ed5ac4028d37667dcc62a1e89e9e7 + languageName: node + linkType: hard + +"typescript@npm:^5.9.3": + version: 5.9.3 + resolution: "typescript@npm:5.9.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 0d0ffb84f2cd072c3e164c79a2e5a1a1f4f168e84cb2882ff8967b92afe1def6c2a91f6838fb58b168428f9458c57a2ba06a6737711fdd87a256bbe83e9a217f + languageName: node + linkType: hard + +"typescript@patch:typescript@6.0.2#~builtin": + version: 6.0.2 + resolution: "typescript@patch:typescript@npm%3A6.0.2#~builtin::version=6.0.2&hash=5786d5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 9f29a9339aa66f5e13485c596c8d4d086fb95960f8be8a23ea6ef33afe61f7c465d330a6f77911bb8613f231c2ebf131763a82d277cefa61147b24af2073f0c4 + languageName: node + linkType: hard + +"typescript@patch:typescript@^5.9.3#~builtin": + version: 5.9.3 + resolution: "typescript@patch:typescript@npm%3A5.9.3#~builtin::version=5.9.3&hash=5786d5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: a5a6dc399d3761ded54192031f11d3ad5df8001c7febe3fbbc8098efcb552cdf8f2f402b3618c56dafcd04fef63dee005f4900f608e185404caedc46480539ed + languageName: node + linkType: hard + +"undici-types@npm:~7.16.0": + version: 7.16.0 + resolution: "undici-types@npm:7.16.0" + checksum: 1ef68fc6c5bad200c8b6f17de8e5bc5cfdcadc164ba8d7208cd087cfa8583d922d8316a7fd76c9a658c22b4123d3ff847429185094484fbc65377d695c905857 + languageName: node + linkType: hard + +"undici-types@npm:~7.18.0": + version: 7.18.2 + resolution: "undici-types@npm:7.18.2" + checksum: 23da306c8366574adec305b06a8519ab5c7d09e3f5d16c1a98709a34fae17da09ec95198f30f86c00055e02efa8bfcc843e84e8aebeb9b8d6bb3e06afccae07a + languageName: node + linkType: hard + +"universalify@npm:^2.0.0": + version: 2.0.1 + resolution: "universalify@npm:2.0.1" + checksum: ecd8469fe0db28e7de9e5289d32bd1b6ba8f7183db34f3bfc4ca53c49891c2d6aa05f3fb3936a81285a905cc509fb641a0c3fc131ec786167eff41236ae32e60 + languageName: node + linkType: hard + +"uri-js@npm:^4.2.2": + version: 4.4.1 + resolution: "uri-js@npm:4.4.1" + dependencies: + punycode: ^2.1.0 + checksum: 7167432de6817fe8e9e0c9684f1d2de2bb688c94388f7569f7dbdb1587c9f4ca2a77962f134ec90be0cc4d004c939ff0d05acc9f34a0db39a3c797dada262633 + languageName: node + linkType: hard + +"url-template@npm:^3.1.1": + version: 3.1.1 + resolution: "url-template@npm:3.1.1" + checksum: ac09daaeaec55a6b070b838ed161d66b050a46fc12ac251cb2db1ce356e786cfb117ee4391d943aaaa757971c509a0142b3cd83dfd8cc3d7b6d90a99d001a5f9 + languageName: node + linkType: hard + +"webidl-conversions@npm:^3.0.0": + version: 3.0.1 + resolution: "webidl-conversions@npm:3.0.1" + checksum: c92a0a6ab95314bde9c32e1d0a6dfac83b578f8fa5f21e675bc2706ed6981bc26b7eb7e6a1fab158e5ce4adf9caa4a0aee49a52505d4d13c7be545f15021b17c + languageName: node + linkType: hard + +"whatwg-url@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-url@npm:5.0.0" + dependencies: + tr46: ~0.0.3 + webidl-conversions: ^3.0.0 + checksum: b8daed4ad3356cc4899048a15b2c143a9aed0dfae1f611ebd55073310c7b910f522ad75d727346ad64203d7e6c79ef25eafd465f4d12775ca44b90fa82ed9e2c + languageName: node + linkType: hard + +"which@npm:^2.0.1": + version: 2.0.2 + resolution: "which@npm:2.0.2" + dependencies: + isexe: ^2.0.0 + bin: + node-which: ./bin/node-which + checksum: 1a5c563d3c1b52d5f893c8b61afe11abc3bab4afac492e8da5bde69d550de701cf9806235f20a47b5c8fa8a1d6a9135841de2596535e998027a54589000e66d1 + languageName: node + linkType: hard + +"which@npm:^6.0.0": + version: 6.0.1 + resolution: "which@npm:6.0.1" + dependencies: + isexe: ^4.0.0 + bin: + node-which: bin/which.js + checksum: dbea77c7d3058bf6c78bf9659d2dce4d2b57d39a15b826b2af6ac2e5a219b99dc8a831b79fdbc453c0598adb4f3f84cf9c2491fd52beb9f5d2dececcad117f68 + languageName: node + linkType: hard + +"word-wrap@npm:^1.2.5": + version: 1.2.5 + resolution: "word-wrap@npm:1.2.5" + checksum: f93ba3586fc181f94afdaff3a6fef27920b4b6d9eaefed0f428f8e07adea2a7f54a5f2830ce59406c8416f033f86902b91eb824072354645eea687dff3691ccb + languageName: node + linkType: hard + +"wrappy@npm:1": + version: 1.0.2 + resolution: "wrappy@npm:1.0.2" + checksum: 159da4805f7e84a3d003d8841557196034155008f817172d4e986bd591f74aa82aa7db55929a54222309e01079a65a92a9e6414da5a6aa4b01ee44a511ac3ee5 + languageName: node + linkType: hard + +"ws@npm:^8.18.2": + version: 8.20.0 + resolution: "ws@npm:8.20.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 2b31d24a53690770564a033c21ea48390f84d23fbc5abc14b2bbec4e112846f2f3ca66caee769a73fb8bc89ba16b452a6911a553e9742bbc75bccb79e203953e + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5 + languageName: node + linkType: hard + +"yallist@npm:^5.0.0": + version: 5.0.0 + resolution: "yallist@npm:5.0.0" + checksum: eba51182400b9f35b017daa7f419f434424410691bbc5de4f4240cc830fdef906b504424992700dc047f16b4d99100a6f8b8b11175c193f38008e9c96322b6a5 + languageName: node + linkType: hard + +"yocto-queue@npm:^0.1.0": + version: 0.1.0 + resolution: "yocto-queue@npm:0.1.0" + checksum: f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700 + languageName: node + linkType: hard + +"zx@npm:8.8.5": + version: 8.8.5 + resolution: "zx@npm:8.8.5" + bin: + zx: build/cli.js + checksum: bf3bb23ae0d0a1198ea53a7ac7e341900b33251aeb4ed657dcbfcc76ed7b0411bd47cc6bdaf67f7b2b79f83339e25d1e47bbf359a8ededfdc33792fb22a92c4c + languageName: node + linkType: hard diff --git a/workspaces/orchestrator/metadata/redhat-backstage-plugin-orchestrator-backend-module-loki.yaml b/workspaces/orchestrator/metadata/redhat-backstage-plugin-orchestrator-backend-module-loki.yaml index 9272b607e..0f35e7346 100644 --- a/workspaces/orchestrator/metadata/redhat-backstage-plugin-orchestrator-backend-module-loki.yaml +++ b/workspaces/orchestrator/metadata/redhat-backstage-plugin-orchestrator-backend-module-loki.yaml @@ -35,3 +35,4 @@ spec: workflowLogProvider: loki: baseUrl: ${LOKI_BASE_URL} + token: ${LOKI_TOKEN} diff --git a/workspaces/orchestrator/smoke-tests/test.env b/workspaces/orchestrator/smoke-tests/test.env index 0e0c4d317..1f9e87854 100644 --- a/workspaces/orchestrator/smoke-tests/test.env +++ b/workspaces/orchestrator/smoke-tests/test.env @@ -1 +1,2 @@ -LOKI_BASE_URL=https://example_url \ No newline at end of file +LOKI_BASE_URL=https://example_url +LOKI_TOKEN=e2e-ci-placeholder \ No newline at end of file