From 8bcf552def01b1cf835af586d30ae8f7d035e71f Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Wed, 6 May 2026 13:19:12 +0000 Subject: [PATCH 1/5] chore(ci): add audit-ci allowlist for inherited @kinvolk/headlamp-plugin CVEs (PRI-855) CTO decision (PRI-854): high-severity vulns are dev/build-time only and acceptable risk with explicit allowlist. Co-Authored-By: Paperclip --- audit-ci.jsonc | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 audit-ci.jsonc diff --git a/audit-ci.jsonc b/audit-ci.jsonc new file mode 100644 index 0000000..c5cd425 --- /dev/null +++ b/audit-ci.jsonc @@ -0,0 +1,20 @@ +{ + // Allowlist for inherited dev-dependency CVEs from @kinvolk/headlamp-plugin + // CTO decision (PRI-854): these high-severity vulns are dev/build-time only, + // trace to @kinvolk/headlamp-plugin transitive deps (Picomatch, Vite, lodash), + // and do NOT ship in production plugin artifacts. + "allowlist": [ + { + "id": "GHSA-hhpm-516h-p3p6", + "reason": "Picomatch ReDoS: devDependency only, does not ship in production plugin bundle" + }, + { + "id": "GHSA-36xf-7xpp-53w5", + "reason": "Vite arbitrary file read: devDependency only, does not ship in production plugin bundle" + }, + { + "id": "GHSA-jf8v-p3pp-93qh", + "reason": "lodash code injection via _.template: devDependency only, does not ship in production plugin bundle" + } + ] +} -- 2.52.0 From cc9b0c40428c5aa810dbbb8796eb18274975f812 Mon Sep 17 00:00:00 2001 From: "privilegedescalation-ceo[bot]" <269721483+privilegedescalation-ceo[bot]@users.noreply.github.com> Date: Sun, 10 May 2026 21:35:01 +0000 Subject: [PATCH 2/5] docs: replace hardcoded namespace with placeholder Users choose their own namespace for Headlamp. Replace the hardcoded `headlamp` namespace in installation docs with so users substitute their own value. Refs: PRI-435 Co-authored-by: Chris Farhood Co-authored-by: Paperclip --- docs/getting-started/installation.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 7ffae1a..e543d02 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -121,7 +121,7 @@ For Headlamp running in Kubernetes: kubectl create configmap headlamp-sealed-secrets-plugin \ --from-file=main.js=dist/main.js \ --from-file=package.json=package.json \ - -n headlamp + -n ``` 2. **Update Headlamp deployment**: @@ -130,7 +130,7 @@ For Headlamp running in Kubernetes: kind: Deployment metadata: name: headlamp - namespace: headlamp + namespace: spec: template: spec: @@ -149,7 +149,7 @@ For Headlamp running in Kubernetes: 3. **Apply and restart**: ```bash kubectl apply -f headlamp-deployment.yaml - kubectl rollout restart deployment/headlamp -n headlamp + kubectl rollout restart deployment/headlamp -n ``` ## Verification -- 2.52.0 From 04942a64c09076c4f332a55c788f864ce23c6c21 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Mon, 11 May 2026 13:49:04 +0000 Subject: [PATCH 3/5] chore: remove E2E testing infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete e2e/ directory (auth.setup.ts, sealed-secrets.spec.ts, .auth/.gitkeep) - Delete playwright.config.ts - Delete scripts/deploy-e2e-headlamp.sh - Delete scripts/teardown-e2e-headlamp.sh - Delete .github/workflows/e2e.yaml - Remove e2e and e2e:headed scripts from package.json - Remove @playwright/test dependency from package.json Context: [PRI-1133](/PRI/issues/PRI-1133) — full E2E purge across org. Co-Authored-By: Paperclip --- .github/workflows/e2e.yaml | 23 ---- e2e/.auth/.gitkeep | 0 e2e/auth.setup.ts | 81 ------------ e2e/sealed-secrets.spec.ts | 88 ------------- package.json | 3 - playwright.config.ts | 27 ---- scripts/deploy-e2e-headlamp.sh | 207 ------------------------------- scripts/teardown-e2e-headlamp.sh | 41 ------ 8 files changed, 470 deletions(-) delete mode 100644 .github/workflows/e2e.yaml delete mode 100644 e2e/.auth/.gitkeep delete mode 100644 e2e/auth.setup.ts delete mode 100644 e2e/sealed-secrets.spec.ts delete mode 100644 playwright.config.ts delete mode 100755 scripts/deploy-e2e-headlamp.sh delete mode 100755 scripts/teardown-e2e-headlamp.sh diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml deleted file mode 100644 index 0363889..0000000 --- a/.github/workflows/e2e.yaml +++ /dev/null @@ -1,23 +0,0 @@ -name: E2E Tests - -on: - push: - branches: [main] - pull_request: - branches: [main] - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: e2e-${{ github.repository }} - cancel-in-progress: false - -jobs: - e2e: - uses: privilegedescalation/.github/.github/workflows/plugin-e2e.yaml@main - with: - node-version: "22" - headlamp-version: v0.40.1 - e2e-namespace: headlamp-dev diff --git a/e2e/.auth/.gitkeep b/e2e/.auth/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/e2e/auth.setup.ts b/e2e/auth.setup.ts deleted file mode 100644 index 1823f81..0000000 --- a/e2e/auth.setup.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { test as setup, expect, Page } from '@playwright/test'; - -const AUTH_STATE_PATH = 'e2e/.auth/state.json'; - -async function authenticateWithOIDC(page: Page, username: string, password: string): Promise { - // Navigate to login — Headlamp redirects / to /c/main/login - await page.goto('/'); - await page.waitForURL('**/login'); - - // Click "Sign In" and capture the Authentik popup - const popupPromise = page.waitForEvent('popup'); - await page.getByRole('button', { name: /sign in/i }).click(); - const popup = await popupPromise; - - // Wait for the Authentik popup to fully load before interacting - await popup.waitForLoadState('domcontentloaded'); - await popup.waitForLoadState('networkidle'); - - // Authentik step 1: fill username — wait for the form to render - const usernameField = popup.getByRole('textbox', { name: /email or username/i }); - await usernameField.waitFor({ state: 'visible', timeout: 15_000 }); - await usernameField.fill(username); - await popup.getByRole('button', { name: /log in/i }).click(); - - // Authentik step 2: fill password — wait for the next step to load - await popup.waitForLoadState('networkidle'); - const passwordField = popup.getByRole('textbox', { name: /password/i }); - await passwordField.waitFor({ state: 'visible', timeout: 15_000 }); - await passwordField.fill(password); - await popup.getByRole('button', { name: /continue|log in/i }).click(); - - // Wait for the popup to close (Authentik redirects back, Headlamp processes callback) - await popup.waitForEvent('close', { timeout: 15_000 }); - - // Original page should now be authenticated — wait for sidebar - await expect(page.getByRole('navigation', { name: 'Navigation' })).toBeVisible({ - timeout: 15_000, - }); -} - -async function authenticateWithToken(page: Page, token: string): Promise { - await page.goto('/'); - // Headlamp goes to /token directly when no OIDC is configured, - // or through /login when OIDC is configured - await page.waitForURL(/\/(login|token)$/); - - if (page.url().includes('/login')) { - // OIDC login page — click "use a token" to reach token auth. - const useTokenBtn = page.getByRole('button', { name: /use a token/i }); - await useTokenBtn.waitFor({ state: 'visible', timeout: 15_000 }); - await useTokenBtn.click(); - await page.waitForURL('**/token'); - } - - // Fill the "ID token" field and submit - await page.getByRole('textbox', { name: /id token/i }).fill(token); - await page.getByRole('button', { name: /authenticate/i }).click(); - - // Wait for the main UI to load - await expect(page.getByRole('navigation', { name: 'Navigation' })).toBeVisible({ - timeout: 15_000, - }); -} - -setup('authenticate with Headlamp', async ({ page }) => { - const username = process.env.AUTHENTIK_USERNAME; - const password = process.env.AUTHENTIK_PASSWORD; - const token = process.env.HEADLAMP_TOKEN; - - if (username && password) { - await authenticateWithOIDC(page, username, password); - } else if (token) { - await authenticateWithToken(page, token); - } else { - throw new Error( - 'Set AUTHENTIK_USERNAME + AUTHENTIK_PASSWORD for OIDC auth, or HEADLAMP_TOKEN for token auth' - ); - } - - await page.context().storageState({ path: AUTH_STATE_PATH }); -}); diff --git a/e2e/sealed-secrets.spec.ts b/e2e/sealed-secrets.spec.ts deleted file mode 100644 index 2af5e91..0000000 --- a/e2e/sealed-secrets.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('Sealed Secrets plugin smoke tests', () => { - test('sidebar contains sealed-secrets entry', async ({ page }) => { - await page.goto('/'); - const sidebar = page.getByRole('navigation', { name: 'Navigation' }); - await expect(sidebar).toBeVisible({ timeout: 15_000 }); - await expect(sidebar.getByRole('button', { name: /sealed.secrets/i })).toBeVisible(); - }); - - test('sidebar sealed-secrets entry is clickable and navigates to list view', async ({ page }) => { - await page.goto('/'); - const sidebar = page.getByRole('navigation', { name: 'Navigation' }); - await expect(sidebar).toBeVisible({ timeout: 15_000 }); - - const sealedSecretsEntry = sidebar.getByRole('button', { name: /sealed.secrets/i }); - await expect(sealedSecretsEntry).toBeVisible(); - await sealedSecretsEntry.click(); - - await expect(page).toHaveURL(/\/sealedsecrets/); - await expect(page.getByRole('heading', { name: /sealed.secrets/i })).toBeVisible(); - }); - - test('sealed secrets list page renders table or empty state', async ({ page }) => { - await page.goto('/c/main/sealedsecrets'); - - await expect(page.getByRole('heading', { name: /sealed.secrets/i })).toBeVisible({ - timeout: 15_000, - }); - - // Either a populated table or an empty-state indicator must be visible - const hasTable = await page.locator('table').first().isVisible().catch(() => false); - const hasEmptyState = await page - .locator('text=/no.*sealed|no.*secret|0 item|empty/i') - .first() - .isVisible() - .catch(() => false); - expect(hasTable || hasEmptyState).toBe(true); - }); - - test('sealing keys page renders table or empty state', async ({ page }) => { - await page.goto('/c/main/sealedsecrets/keys'); - - await expect(page.getByRole('heading', { name: /sealing.key/i })).toBeVisible({ - timeout: 15_000, - }); - - const hasTable = await page.locator('table').first().isVisible().catch(() => false); - const hasEmptyState = await page - .locator('text=/no.*key|0 item|empty/i') - .first() - .isVisible() - .catch(() => false); - expect(hasTable || hasEmptyState).toBe(true); - }); - - test('navigation between sealed-secrets views works', async ({ page }) => { - await page.goto('/c/main/sealedsecrets'); - await expect(page.getByRole('heading', { name: /sealed.secrets/i })).toBeVisible({ - timeout: 15_000, - }); - - // Navigate to Sealing Keys via sidebar - const sidebar = page.getByRole('navigation', { name: 'Navigation' }); - const keysLink = sidebar.getByRole('link', { name: /sealing.key/i }); - await expect(keysLink).toBeVisible(); - await keysLink.click(); - - await expect(page).toHaveURL(/\/sealedsecrets\/keys$/); - await expect(page.getByRole('heading', { name: /sealing.key/i })).toBeVisible(); - - // Navigate back to All Sealed Secrets - const allSecretsLink = sidebar.getByRole('link', { name: /all sealed secrets/i }); - await expect(allSecretsLink).toBeVisible(); - await allSecretsLink.click(); - - await expect(page).toHaveURL(/\/sealedsecrets(?!\/keys)/); - await expect(page.getByRole('heading', { name: /sealed.secrets/i })).toBeVisible(); - }); - - test('plugin settings page shows sealed-secrets plugin entry', async ({ page }) => { - await page.goto('/settings/plugins'); - - // Wait for plugin list to load — plugin scripts load asynchronously - const pluginEntry = page.locator('text=sealed-secrets').first(); - await expect(pluginEntry).toBeVisible({ timeout: 30_000 }); - }); -}); diff --git a/package.json b/package.json index 941f81d..2e46804 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,6 @@ "format:check": "prettier --check src/", "test": "vitest run", "test:watch": "vitest", - "e2e": "playwright test", - "e2e:headed": "playwright test --headed", "storybook": "headlamp-plugin storybook", "storybook-build": "headlamp-plugin storybook-build", "i18n": "headlamp-plugin i18n", @@ -61,7 +59,6 @@ }, "devDependencies": { "@headlamp-k8s/eslint-config": "^0.6.0", - "@playwright/test": "^1.58.2", "@iconify/react": "^6.0.2", "@kinvolk/headlamp-plugin": "^0.13.0", "@mui/material": "^5.15.14", diff --git a/playwright.config.ts b/playwright.config.ts deleted file mode 100644 index b916edf..0000000 --- a/playwright.config.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { defineConfig, devices } from '@playwright/test'; - -export default defineConfig({ - testDir: './e2e', - timeout: 30_000, - expect: { timeout: 10_000 }, - fullyParallel: false, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 1 : 0, - reporter: 'list', - use: { - baseURL: process.env.HEADLAMP_URL || (() => { throw new Error('HEADLAMP_URL is required — run scripts/deploy-e2e-headlamp.sh first'); })(), - trace: 'on-first-retry', - screenshot: 'only-on-failure', - }, - projects: [ - { name: 'setup', testMatch: /auth\.setup\.ts/, timeout: 60_000 }, - { - name: 'chromium', - use: { - ...devices['Desktop Chrome'], - storageState: 'e2e/.auth/state.json', - }, - dependencies: ['setup'], - }, - ], -}); diff --git a/scripts/deploy-e2e-headlamp.sh b/scripts/deploy-e2e-headlamp.sh deleted file mode 100755 index 288f2f8..0000000 --- a/scripts/deploy-e2e-headlamp.sh +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env bash -# deploy-e2e-headlamp.sh -# -# Deploys a stock Headlamp instance with the sealed-secrets plugin loaded via -# a ConfigMap volume mount. No custom Docker images — the plugin is built -# in CI and injected as a ConfigMap. -# -# E2E resources are deployed to the `privilegedescalation-dev` namespace. Nothing -# persists beyond the test run — teardown cleans up all created resources. -# -# Prerequisites: -# - Plugin built (dist/ exists with plugin-main.js + package.json) -# - kubectl configured with cluster access -# RBAC is managed via Flux from privilegedescalation/infra/base/rbac/e2e-ci-runner-headlamp-rbac.yaml. -# The infra repo is the source of truth — do not apply this file directly. -# Apply RBAC first: kubectl apply -f privilegedescalation/infra/base/rbac/e2e-ci-runner-headlamp-rbac.yaml -# -# Environment: -# E2E_NAMESPACE — namespace for E2E Headlamp (default: privilegedescalation-dev) -# E2E_RELEASE — release/resource name prefix (default: headlamp-e2e) -# HEADLAMP_VERSION — Headlamp image tag (default: latest) -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -DIST_DIR="$REPO_ROOT/dist" - -E2E_NAMESPACE="${E2E_NAMESPACE:-privilegedescalation-dev}" -E2E_RELEASE="${E2E_RELEASE:-headlamp-e2e}" -HEADLAMP_VERSION="${HEADLAMP_VERSION:-latest}" - -if [ ! -d "$DIST_DIR" ]; then - echo "ERROR: dist/ not found. Run 'pnpm build' first." >&2 - exit 1 -fi - -# --- Preflight: verify RBAC before touching the cluster --- -echo "Checking RBAC permissions in namespace '${E2E_NAMESPACE}'..." -if ! kubectl auth can-i delete configmaps -n "$E2E_NAMESPACE" --quiet 2>/dev/null; then - echo "ERROR: Missing RBAC — cannot delete configmaps in namespace '${E2E_NAMESPACE}'." >&2 - echo " Apply RBAC first: kubectl apply -f privilegedescalation/infra/base/rbac/e2e-ci-runner-headlamp-rbac.yaml" >&2 - exit 1 -fi - -echo "=== E2E Headlamp Deployment ===" -echo " Image: ghcr.io/headlamp-k8s/headlamp:${HEADLAMP_VERSION}" -echo " Namespace: $E2E_NAMESPACE" -echo " Release: $E2E_RELEASE" - -# --- Create ConfigMap from built plugin --- -echo "" -echo "Creating ConfigMap with plugin files..." - -# Delete existing ConfigMap if present (idempotent redeploy) -kubectl delete configmap headlamp-sealed-secrets-plugin \ - -n "$E2E_NAMESPACE" --ignore-not-found - -# Create ConfigMap from dist/ contents and package.json -kubectl create configmap headlamp-sealed-secrets-plugin \ - -n "$E2E_NAMESPACE" \ - --from-file="$DIST_DIR" \ - --from-file=package.json="$REPO_ROOT/package.json" - -# --- Tear down any existing E2E deployment for a clean start --- -echo "" -echo "Removing any existing E2E deployment (clean-start)..." -kubectl delete deployment "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found --wait -kubectl delete service "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found --wait -kubectl delete serviceaccount "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found --wait - -# --- Deploy Headlamp via kubectl apply --- -echo "" -echo "Deploying Headlamp E2E instance..." - -kubectl apply -f - </dev/null; do - ATTEMPTS=$((ATTEMPTS + 1)) - if [ "$ATTEMPTS" -ge "$MAX_ATTEMPTS" ]; then - echo "ERROR: ${SVC_URL} not reachable after $((MAX_ATTEMPTS * 5))s" >&2 - exit 1 - fi - echo " [${ATTEMPTS}/${MAX_ATTEMPTS}] not yet reachable, retrying in 5s..." - sleep 5 -done -echo "" -echo "E2E Headlamp is ready at: ${SVC_URL}" -echo " export HEADLAMP_URL=${SVC_URL}" - -# --- Generate a token for test auth --- -echo "" -echo "Creating service account token for E2E auth..." -kubectl create serviceaccount headlamp-e2e-test \ - -n "$E2E_NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - - -TOKEN=$(kubectl create token headlamp-e2e-test -n "$E2E_NAMESPACE" --duration=1h 2>/dev/null || echo "") -if [ -n "$TOKEN" ]; then - echo " export HEADLAMP_TOKEN=" - echo "" - echo "HEADLAMP_URL=${SVC_URL}" > "$REPO_ROOT/.env.e2e" - echo "HEADLAMP_TOKEN=${TOKEN}" >> "$REPO_ROOT/.env.e2e" - echo "Wrote .env.e2e with HEADLAMP_URL and HEADLAMP_TOKEN" -else - echo " WARNING: Could not generate token. Set HEADLAMP_TOKEN manually or use OIDC." -fi - -echo "" -echo "E2E deployment complete." diff --git a/scripts/teardown-e2e-headlamp.sh b/scripts/teardown-e2e-headlamp.sh deleted file mode 100755 index d59bb7e..0000000 --- a/scripts/teardown-e2e-headlamp.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash -# teardown-e2e-headlamp.sh -# -# Tears down the dedicated E2E Headlamp instance deployed by deploy-e2e-headlamp.sh. -# -# RBAC is managed via Flux from privilegedescalation/infra/base/rbac/e2e-ci-runner-headlamp-rbac.yaml. -# The infra repo is the source of truth — do not apply this file directly. -# -# Environment: -# E2E_NAMESPACE — namespace to clean up (default: privilegedescalation-dev) -# E2E_RELEASE — release/resource name prefix (default: headlamp-e2e) -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" - -E2E_NAMESPACE="${E2E_NAMESPACE:-privilegedescalation-dev}" -E2E_RELEASE="${E2E_RELEASE:-headlamp-e2e}" - -echo "=== E2E Headlamp Teardown ===" -echo " Namespace: $E2E_NAMESPACE" -echo " Release: $E2E_RELEASE" - -echo "Removing Headlamp Deployment, Service, and ServiceAccount..." -kubectl delete deployment "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found -kubectl delete service "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found -kubectl delete serviceaccount "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found - -echo "Cleaning up ConfigMap..." -kubectl delete configmap headlamp-sealed-secrets-plugin -n "$E2E_NAMESPACE" --ignore-not-found - -echo "Cleaning up test service account..." -kubectl delete serviceaccount headlamp-e2e-test -n "$E2E_NAMESPACE" --ignore-not-found - -# Clean up .env.e2e if present -if [ -f "$REPO_ROOT/.env.e2e" ]; then - rm "$REPO_ROOT/.env.e2e" - echo "Removed .env.e2e" -fi - -echo "" -echo "E2E teardown complete." -- 2.52.0 From 195efc44b32fe859191e9a130a05bfde5282d442 Mon Sep 17 00:00:00 2001 From: "privilegedescalation-engineer[bot]" <269729446+privilegedescalation-engineer[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 20:11:55 +0000 Subject: [PATCH 4/5] Fix pnpm-lock.yaml after E2E deletion (#82) Regenerate lockfile after @playwright/test removal from package.json in commit 943d901. Fixes CI failure on main branch. Co-authored-by: Chris Farhood Co-authored-by: Paperclip --- pnpm-lock.yaml | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08a06ea..ba8b2b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,9 +24,6 @@ importers: '@mui/material': specifier: ^5.15.14 version: 5.18.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@playwright/test': - specifier: ^1.58.2 - version: 1.59.1 '@testing-library/jest-dom': specifier: ^6.4.8 version: 6.9.1 @@ -1015,11 +1012,6 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@playwright/test@1.59.1': - resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} - engines: {node: '>=18'} - hasBin: true - '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -3102,11 +3094,6 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -4260,16 +4247,6 @@ packages: resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} engines: {node: '>=10'} - playwright-core@1.59.1: - resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} - engines: {node: '>=18'} - hasBin: true - - playwright@1.59.1: - resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} - engines: {node: '>=18'} - hasBin: true - possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -6643,10 +6620,6 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/test@1.59.1': - dependencies: - playwright: 1.59.1 - '@popperjs/core@2.11.8': {} '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@18.3.28)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': @@ -9186,9 +9159,6 @@ snapshots: fs.realpath@1.0.0: {} - fsevents@2.3.2: - optional: true - fsevents@2.3.3: optional: true @@ -10588,14 +10558,6 @@ snapshots: dependencies: find-up: 5.0.0 - playwright-core@1.59.1: {} - - playwright@1.59.1: - dependencies: - playwright-core: 1.59.1 - optionalDependencies: - fsevents: 2.3.2 - possible-typed-array-names@1.1.0: {} postcss-modules-extract-imports@3.1.0(postcss@8.5.13): -- 2.52.0 From 36a5d2a72a2ea2c6839bf9e2895492d13ea27a6e Mon Sep 17 00:00:00 2001 From: "privilegedescalation-ceo[bot]" <269721483+privilegedescalation-ceo[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 21:40:02 +0000 Subject: [PATCH 5/5] Update CI and approval workflows for three-branch SDLC (#83) CI triggers on dev/uat/main. Promotion gate replaces dual-approval. Co-authored-by: Chris Farhood Co-authored-by: Paperclip --- .github/workflows/ci.yaml | 4 ++-- .github/workflows/dual-approval.yaml | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 899f2b1..654169d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [main] + branches: [main, dev, uat] pull_request: - branches: [main] + branches: [main, dev, uat] workflow_dispatch: workflow_call: diff --git a/.github/workflows/dual-approval.yaml b/.github/workflows/dual-approval.yaml index c4a96cf..9552ee4 100644 --- a/.github/workflows/dual-approval.yaml +++ b/.github/workflows/dual-approval.yaml @@ -1,20 +1,21 @@ -name: Dual Approval (CTO + QA) +name: Promotion Gate -# Calls the shared dual-approval-check workflow. -# Passes when both privilegedescalation-cto and privilegedescalation-qa -# have approved the PR. Add "Dual Approval (CTO + QA)" to required_status_checks -# in branch protection to enforce this gate. +# Calls the shared promotion gate workflow. +# dev PRs: no gate (engineer self-merges). +# uat PRs: QA approval required. +# main PRs: UAT approval required (uat→main promotions). on: pull_request_review: types: [submitted, dismissed] pull_request: - branches: [main] + branches: [uat, main] types: [opened, reopened, synchronize] jobs: - dual-approval: + promotion-gate: uses: privilegedescalation/.github/.github/workflows/dual-approval-check.yaml@main secrets: inherit with: pr_number: ${{ github.event.pull_request.number }} + -- 2.52.0