Compare commits

..

3 Commits

Author SHA1 Message Date
Chris Farhood 75cf41ef4d fix: sync pnpm-lock.yaml after removing tar and undici deps
The pnpm-lock.yaml was out of sync with package.json after tar and undici
were removed. Regenerated to resolve pnpm install failure in CI.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-04 15:19:26 +00:00
Chris Farhood a324ee621b fix: add markdownlint config to resolve CI failures
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-04 05:24:26 +00:00
Chris Farhood 0c521be1a1 Remove duplicate tar/undici from devDependencies (already in pnpm.overrides)
Consolidates dual override blocks by removing the duplicate entries
from devDependencies. These packages are already pinned via pnpm.overrides
and should not appear in devDependencies.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-04 02:21:10 +00:00
13 changed files with 11 additions and 545 deletions
+2 -6
View File
@@ -2,13 +2,9 @@ name: CI
on:
push:
branches: ['**']
branches: [main]
pull_request:
branches: [main, dev]
workflow_dispatch:
permissions:
contents: read
branches: [main]
jobs:
ci:
-218
View File
@@ -1,218 +0,0 @@
name: E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
# Only one E2E run at a time — the shared E2E_RELEASE in headlamp-dev cannot
# be shared across concurrent runs. cancel-in-progress: false queues rather
# than cancels to avoid skipping the teardown step.
concurrency:
group: e2e-${{ github.repository }}
cancel-in-progress: false
env:
E2E_NAMESPACE: headlamp-dev
E2E_RELEASE: headlamp-e2e-argocd
HEADLAMP_VERSION: v0.40.1
jobs:
e2e:
runs-on: runners-privilegedescalation
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Detect package manager
id: pkg-manager
run: |
if [ -f "pnpm-lock.yaml" ]; then
echo "manager=pnpm" >> $GITHUB_OUTPUT
PM=$(python3 -c "import json,sys; d=json.load(open('package.json')); print('true' if d.get('packageManager','').startswith('pnpm@') else 'false')" 2>/dev/null || echo "false")
echo "has_package_manager=$PM" >> $GITHUB_OUTPUT
else
echo "manager=npm" >> $GITHUB_OUTPUT
echo "has_package_manager=false" >> $GITHUB_OUTPUT
fi
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: '22'
cache: ${{ steps.pkg-manager.outputs.manager == 'npm' && 'npm' || '' }}
- name: Setup pnpm (Corepack, respects packageManager field)
if: steps.pkg-manager.outputs.manager == 'pnpm' && steps.pkg-manager.outputs.has_package_manager == 'true'
run: |
npm install -g corepack
corepack enable pnpm
corepack prepare $(node -p "require('./package.json').packageManager") --activate
- name: Setup pnpm (version latest, no packageManager field)
if: steps.pkg-manager.outputs.manager == 'pnpm' && steps.pkg-manager.outputs.has_package_manager == 'false'
uses: pnpm/action-setup@v5
with:
run_install: false
version: latest
- name: Get pnpm store directory
id: pnpm-store
if: steps.pkg-manager.outputs.manager == 'pnpm'
run: echo "dir=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm store
if: steps.pkg-manager.outputs.manager == 'pnpm'
uses: actions/cache@v5
with:
path: ${{ steps.pnpm-store.outputs.dir }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Setup kubectl
uses: azure/setup-kubectl@v4
with:
version: 'latest'
- name: Get kubeconfig
run: |
set -euo pipefail
echo "=== Runner kubeconfig diagnostic ==="
echo "KUBECONFIG=${KUBECONFIG:-}"
for path in /runner/config /home/runner/.kube/config "${HOME:-}/.kube/config"; do
if [ -f "$path" ]; then
echo "FOUND kubeconfig at: $path"
fi
done
echo ""
echo "=== In-cluster service account check ==="
in_cluster=false
if [ -f /var/run/secrets/kubernetes.io/serviceaccount/token ]; then
echo "Service account token present — in-cluster mode available"
in_cluster=true
fi
if [ -f /runner/config ]; then
echo "KUBECONFIG=/runner/config" >> "$GITHUB_ENV"
elif [ -f /home/runner/.kube/config ]; then
echo "KUBECONFIG=/home/runner/.kube/config" >> "$GITHUB_ENV"
elif [ -f "${HOME:-}/.kube/config" ]; then
echo "KUBECONFIG=${HOME:-}/.kube/config" >> "$GITHUB_ENV"
elif [ "$in_cluster" = true ]; then
echo "No static kubeconfig found — generating in-cluster kubeconfig"
KUBECFG_DIR="${HOME:-}/.kube"
mkdir -p "$KUBECFG_DIR"
kubectl config set-cluster in-cluster \
--server="https://${KUBERNETES_SERVICE_HOST:-kubernetes.default.svc}:${KUBERNETES_SERVICE_PORT:-443}" \
--certificate-authority=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
--embed-certs=true \
--kubeconfig="$KUBECFG_DIR/config"
kubectl config set-credentials in-cluster \
--token="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
--kubeconfig="$KUBECFG_DIR/config"
kubectl config set-context in-cluster \
--cluster=in-cluster \
--user=in-cluster \
--kubeconfig="$KUBECFG_DIR/config"
kubectl config use-context in-cluster \
--kubeconfig="$KUBECFG_DIR/config"
echo "KUBECONFIG=$KUBECFG_DIR/config" >> "$GITHUB_ENV"
else
echo "::error::No kubeconfig found"
exit 1
fi
- name: Apply RBAC for E2E pipeline
run: |
set -x
kubectl apply -f deployment/e2e-ci-runner-rbac.yaml
echo "Waiting for RBAC propagation..."
sleep 5
kubectl get role e2e-ci-runner -n headlamp-dev
kubectl get rolebinding e2e-ci-runner-binding -n headlamp-dev 2>&1 | tail -3 || true
set +x
- name: Install dependencies
run: |
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
pnpm install --frozen-lockfile
else
npm ci
fi
- name: Build plugin
run: npx @kinvolk/headlamp-plugin build
- name: Deploy E2E Headlamp instance
run: scripts/deploy-e2e-headlamp.sh
- name: Load E2E environment
run: |
if [ -f .env.e2e ]; then
cat .env.e2e >> "$GITHUB_ENV"
else
echo "::error::deploy-e2e-headlamp.sh did not produce .env.e2e"
exit 1
fi
- name: Install Playwright browsers
run: |
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
pnpm exec playwright install --with-deps chromium
else
npx playwright install --with-deps chromium
fi
- name: Run E2E tests
run: |
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
pnpm run e2e
else
npm run e2e
fi
env:
HEADLAMP_URL: ${{ env.HEADLAMP_URL }}
HEADLAMP_TOKEN: ${{ env.HEADLAMP_TOKEN }}
- name: Collect deployment diagnostics on failure
if: failure()
run: |
echo "=== Pod state ==="
kubectl get pods -n "$E2E_NAMESPACE" -l "app.kubernetes.io/instance=$E2E_RELEASE" 2>&1 || true
echo "=== Pod describe ==="
kubectl describe pods -n "$E2E_NAMESPACE" -l "app.kubernetes.io/instance=$E2E_RELEASE" 2>&1 || true
echo "=== Container logs (current) ==="
kubectl logs -n "$E2E_NAMESPACE" -l "app.kubernetes.io/instance=$E2E_RELEASE" \
--tail=100 2>&1 || true
echo "=== Container logs (previous, if crashed) ==="
kubectl logs -n "$E2E_NAMESPACE" -l "app.kubernetes.io/instance=$E2E_RELEASE" \
--previous --tail=100 2>&1 || true
echo "=== Recent namespace events ==="
kubectl get events -n "$E2E_NAMESPACE" --sort-by='.lastTimestamp' 2>&1 | tail -20 || true
- name: Teardown E2E instance
if: always()
run: scripts/teardown-e2e-headlamp.sh
- name: Upload Playwright report
uses: actions/upload-artifact@v7
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
- name: Upload test results
uses: actions/upload-artifact@v7
if: failure()
with:
name: test-results
path: test-results/
retention-days: 7
-1
View File
@@ -32,4 +32,3 @@ gh workflow run Release --field version=0.1.0
## License
Apache-2.0
+3 -3
View File
@@ -1,4 +1,4 @@
version: "0.1.2"
version: "0.1.0"
name: headlamp-argocd
displayName: ArgoCD Headlamp Plugin
createdAt: "2026-04-21T00:00:00Z"
@@ -26,8 +26,8 @@ maintainers:
provider:
name: privilegedescalation
annotations:
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-argocd-plugin/releases/download/v0.1.2/privilegedescalation-headlamp-argocd-plugin-0.1.2.tar.gz"
headlamp/plugin/archive-checksum: sha256:e71f84913eed1fd7e2d074912e3bfa668c4b1fefcbb069731a4e4277a998ca28
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-argocd-plugin/releases/download/v0.1.0/headlamp-argocd-0.1.0.tar.gz"
headlamp/plugin/archive-checksum: "sha256:1f4df43f79b795bdf4f70e1e3aa5bacadf689ea5584fdadf92fb677faab21c2c"
headlamp/plugin/version-compat: ">=0.26"
headlamp/plugin/distro-compat: "in-cluster"
changes:
-33
View File
@@ -1,33 +0,0 @@
---
# RBAC for the GitHub Actions CI runner to manage the E2E Headlamp instance.
# CI-only test fixture — NOT for production use.
#
# Grants the ARC runner service account permissions in the headlamp-dev
# namespace to deploy and tear down a dedicated Headlamp instance.
# E2E resources run in `headlamp-dev` — nothing persists beyond a test run.
#
# Plugin is loaded via ConfigMap volume mount — no custom Docker images.
#
# Note: This RBAC is mirrored in privilegedescalation/infra (base/rbac/)
# and managed by Flux GitOps. The infra repo is the source of truth.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: e2e-ci-runner
namespace: headlamp-dev
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "create", "update", "patch", "delete", "watch"]
- apiGroups: [""]
resources: ["services", "serviceaccounts", "configmaps", "secrets", "events"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["serviceaccounts/token"]
verbs: ["create"]
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["roles", "rolebindings"]
verbs: ["get", "list", "create", "update", "patch", "delete"]
-18
View File
@@ -1,18 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('ArgoCD plugin smoke tests', () => {
test('sidebar contains ArgoCD 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: 'ArgoCD' })).toBeVisible();
});
test('applications list page loads', async ({ page }) => {
await page.goto('/c/main/argocd');
await expect(
page.getByRole('heading', { name: /argo.*cd/i })
).toBeVisible({ timeout: 15_000 });
});
});
-34
View File
@@ -1,34 +0,0 @@
import { test as setup, expect, Page } from '@playwright/test';
const AUTH_STATE_PATH = 'e2e/.auth/state.json';
async function authenticateWithToken(page: Page, token: string): Promise<void> {
await page.goto('/');
await page.waitForURL(/\/(login|token)$/);
if (page.url().includes('/login')) {
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');
}
await page.getByRole('textbox', { name: /id token/i }).fill(token);
await page.getByRole('button', { name: /authenticate/i }).click();
await expect(page.getByRole('navigation', { name: 'Navigation' })).toBeVisible({
timeout: 15_000,
});
}
setup('authenticate with Headlamp', async ({ page }) => {
const token = process.env.HEADLAMP_TOKEN;
if (!token) {
throw new Error('Set HEADLAMP_TOKEN for token auth');
}
await authenticateWithToken(page, token);
await page.context().storageState({ path: AUTH_STATE_PATH });
});
+4 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@privilegedescalation/headlamp-argocd-plugin",
"version": "0.1.2",
"version": "0.1.0",
"description": "Headlamp plugin for ArgoCD visibility — monitors ArgoCD Applications, Rollouts, and health status",
"repository": {
"type": "git",
@@ -23,9 +23,7 @@
"format": "prettier --write src/",
"format:check": "prettier --check src/",
"test": "vitest run",
"test:watch": "vitest",
"e2e": "playwright test",
"e2e:headed": "playwright test --headed"
"test:watch": "vitest"
},
"peerDependencies": {
"react": "^18.0.0",
@@ -35,12 +33,10 @@
"overrides": {
"tar": "^7.5.11",
"undici": "^7.24.3",
"flatted": "^3.4.2",
"elliptic": ">=6.6.1"
"flatted": "^3.4.2"
}
},
"devDependencies": {
"@playwright/test": "^1.58.2",
"@kinvolk/headlamp-plugin": "^0.13.0",
"@mui/material": "^5.15.14",
"@testing-library/jest-dom": "^6.4.8",
@@ -59,4 +55,4 @@
"typescript": "~5.6.2",
"vitest": "^3.0.5"
}
}
}
-27
View File
@@ -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 || 'http://headlamp-e2e-argocd.headlamp-dev.svc.cluster.local',
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'],
},
],
});
+2 -3
View File
@@ -8,7 +8,6 @@ overrides:
tar: ^7.5.11
undici: ^7.24.3
flatted: ^3.4.2
elliptic: '>=6.6.1'
importers:
@@ -6230,7 +6229,7 @@ snapshots:
jsdom: 24.1.3
jsonpath-plus: 10.4.0
lodash: 4.18.1
material-react-table: 2.13.3(0078ddeddc9e779fa84c03996c1db10e)
material-react-table: 2.13.3(330725fe5432f245d076f0c0dda1a7a7)
monaco-editor: 0.52.2
msw: 2.4.9(typescript@5.6.2)
msw-storybook-addon: 2.0.3(msw@2.4.9(typescript@5.6.3))
@@ -9932,7 +9931,7 @@ snapshots:
'@types/minimatch': 3.0.5
minimatch: 3.1.5
material-react-table@2.13.3(0078ddeddc9e779fa84c03996c1db10e):
material-react-table@2.13.3(330725fe5432f245d076f0c0dda1a7a7):
dependencies:
'@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@19.2.14)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)
-4
View File
@@ -1,4 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>privilegedescalation/.github:renovate-config"]
}
-173
View File
@@ -1,173 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DIST_DIR="$REPO_ROOT/dist"
E2E_NAMESPACE="${E2E_NAMESPACE:-headlamp-dev}"
E2E_RELEASE="${E2E_RELEASE:-headlamp-e2e-argocd}"
HEADLAMP_VERSION="${HEADLAMP_VERSION:-v0.40.1}"
if [ ! -d "$DIST_DIR" ]; then
echo "ERROR: dist/ not found. Run 'npm run build' first." >&2
exit 1
fi
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
exit 1
fi
echo "=== E2E Headlamp Deployment ==="
echo " Image: ghcr.io/headlamp-k8s/headlamp:${HEADLAMP_VERSION}"
echo " Namespace: $E2E_NAMESPACE"
echo " Release: $E2E_RELEASE"
echo ""
echo "Creating ConfigMap with plugin files..."
kubectl delete configmap headlamp-argocd-plugin \
-n "$E2E_NAMESPACE" --ignore-not-found
kubectl create configmap headlamp-argocd-plugin \
-n "$E2E_NAMESPACE" \
--from-file="$DIST_DIR" \
--from-file=package.json="$REPO_ROOT/package.json"
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
echo ""
echo "Deploying Headlamp E2E instance..."
kubectl apply -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: ${E2E_RELEASE}
namespace: ${E2E_NAMESPACE}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${E2E_RELEASE}
namespace: ${E2E_NAMESPACE}
labels:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: ${E2E_RELEASE}
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: ${E2E_RELEASE}
template:
metadata:
labels:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: ${E2E_RELEASE}
spec:
serviceAccountName: ${E2E_RELEASE}
automountServiceAccountToken: true
securityContext: {}
containers:
- name: headlamp
image: ghcr.io/headlamp-k8s/headlamp:${HEADLAMP_VERSION}
imagePullPolicy: IfNotPresent
securityContext:
runAsNonRoot: true
privileged: false
runAsUser: 100
runAsGroup: 101
args:
- "-in-cluster"
- "-in-cluster-context-name=main"
- "-plugins-dir=/headlamp/plugins"
ports:
- name: http
containerPort: 4466
protocol: TCP
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 6
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 10
periodSeconds: 10
volumeMounts:
- name: argocd-plugin
mountPath: /headlamp/plugins/headlamp-argocd
readOnly: true
volumes:
- name: argocd-plugin
configMap:
name: headlamp-argocd-plugin
---
apiVersion: v1
kind: Service
metadata:
name: ${E2E_RELEASE}
namespace: ${E2E_NAMESPACE}
labels:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: ${E2E_RELEASE}
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: ${E2E_RELEASE}
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
EOF
echo "Waiting for rollout..."
kubectl rollout status "deployment/${E2E_RELEASE}" \
-n "$E2E_NAMESPACE" --timeout=120s
SVC_URL="http://${E2E_RELEASE}.${E2E_NAMESPACE}.svc.cluster.local"
echo ""
echo "Waiting for ${SVC_URL} to be reachable..."
ATTEMPTS=0
MAX_ATTEMPTS=24
until curl -sf --max-time 5 "${SVC_URL}" -o /dev/null 2>/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 ""
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 "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."
fi
echo ""
echo "E2E deployment complete."
-17
View File
@@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
E2E_NAMESPACE="${E2E_NAMESPACE:-headlamp-dev}"
E2E_RELEASE="${E2E_RELEASE:-headlamp-e2e-argocd}"
echo "=== E2E Teardown ==="
echo " Namespace: $E2E_NAMESPACE"
echo " Release: $E2E_RELEASE"
kubectl delete deployment "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found || true
kubectl delete service "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found || true
kubectl delete serviceaccount "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found || true
kubectl delete serviceaccount headlamp-e2e-test -n "$E2E_NAMESPACE" --ignore-not-found || true
kubectl delete configmap headlamp-argocd-plugin -n "$E2E_NAMESPACE" --ignore-not-found || true
echo "Teardown complete."