Compare commits

..

2 Commits

Author SHA1 Message Date
Chris Farhood 629261625c Remove antipattern content from UAT skill
Strip "What UAT Is NOT" section per board feedback on PRI-1487.
Focus skill on positive guidance only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 05:09:28 +00:00
Chris Farhood 3abfe53a1e Refactor UAT skill: delegate to UAT_PLAYBOOK.md per plugin
- Rewrite skills/uat/SKILL.md to be a general UAT overview that
  delegates per-plugin test procedures to UAT_PLAYBOOK.md in each
  plugin repository (authoritative source of truth)
- Add "What UAT Is NOT" section to explicitly prevent GitHub-browsing
  anti-pattern
- Update SDLC skill to require UAT_PLAYBOOK.md as a required deliverable
  alongside code changes affecting plugin testing
- Create UAT_PLAYBOOK.md in sealed-secrets and rook plugin repos

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 05:07:28 +00:00
9 changed files with 62 additions and 579 deletions
-56
View File
@@ -1,56 +0,0 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install linters
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends shellcheck yamllint
- name: Lint Markdown
uses: DavidAnson/markdownlint-cli2-action@v19
with:
globs: "**/*.md"
- name: Lint YAML
run: yamllint .
- name: Shellcheck
run: shellcheck scripts/*.sh
- name: Validate skill frontmatter
run: |
set -e
fail=0
for f in skills/*/SKILL.md; do
fm=$(awk 'BEGIN{c=0} /^---$/{c++; next} c==1{print} c>=2{exit}' "$f")
for key in name description; do
if ! printf '%s\n' "$fm" | grep -qE "^${key}:[[:space:]]"; then
echo "::error file=${f}::missing '${key}' in YAML frontmatter"
fail=1
fi
done
done
exit $fail
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate JSON files
run: |
find . -name "*.json" -not -path "./.git/*" | while read -r f; do
python3 -m json.tool "$f" > /dev/null || { echo "::error file=$f::Invalid JSON"; exit 1; }
done
echo "All JSON files valid"
-24
View File
@@ -1,24 +0,0 @@
name: Promotion Gate
on:
pull_request:
branches: [main]
jobs:
promotion_gate:
name: Promotion Gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate skills directory structure
run: |
set -e
fail=0
for dir in skills/*/; do
if [ ! -f "${dir}SKILL.md" ]; then
echo "::error::Missing SKILL.md in ${dir}"
fail=1
fi
done
exit $fail
+17
View File
@@ -0,0 +1,17 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Lint Markdown
uses: DavidAnson/markdownlint-cli2-action@v19
with:
globs: "**/*.md"
-439
View File
@@ -1,439 +0,0 @@
name: Plugin Release
on:
workflow_call:
inputs:
version:
description: 'Release version (e.g. 1.0.0)'
required: true
type: string
node-version:
description: 'Node.js version to use'
required: false
type: string
default: '22'
upstream-repo:
description: 'Upstream repo to fetch appVersion from (e.g. fenio/tns-csi). Leave empty to skip.'
required: false
type: string
default: ''
secrets:
RELEASE_APP_ID:
description: 'GitHub App ID for creating PRs (org blocks GITHUB_TOKEN from creating PRs)'
required: true
RELEASE_APP_PRIVATE_KEY:
description: 'GitHub App private key (PEM format)'
required: true
permissions:
contents: write
pull-requests: write
concurrency:
group: release
cancel-in-progress: false
jobs:
check-secrets:
runs-on: ubuntu-latest
outputs:
ready: ${{ steps.check.outputs.ready }}
steps:
- name: Verify RELEASE_APP_ID is configured
id: check
env:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
run: |
if [ -z "$RELEASE_APP_ID" ]; then
echo "::notice::RELEASE_APP_ID org secret is not configured (see PRI-380). Release skipped — no artifacts will be created."
echo "ready=false" >> $GITHUB_OUTPUT
else
echo "ready=true" >> $GITHUB_OUTPUT
fi
ci:
needs: check-secrets
if: needs.check-secrets.outputs.ready == 'true'
uses: ./.github/workflows/plugin-ci.yaml
with:
node-version: ${{ inputs.node-version }}
check-token-permissions:
needs: check-secrets
if: needs.check-secrets.outputs.ready == 'true'
runs-on: ubuntu-latest
outputs:
has_write: ${{ steps.check.outputs.has_write }}
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
- name: Check write permissions via API
id: check
run: |
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST \
-H "Authorization: Bearer ${{ steps.app-token.outputs.token }}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/git/refs" \
-d '{"ref":"refs/heads/_release_check","sha":"${{ github.sha }}"}')
if [ "$HTTP_CODE" = "201" ]; then
echo "::notice::Token has write permission — cleaning up test ref."
curl -s -o /dev/null -w "%{http_code}" \
-X DELETE \
-H "Authorization: Bearer ${{ steps.app-token.outputs.token }}" \
"https://api.github.com/repos/${{ github.repository }}/git/refs/heads/_release_check"
echo "has_write=true" >> $GITHUB_OUTPUT
elif [ "$HTTP_CODE" = "403" ]; then
echo "::error::Token lacks write permission. Release cannot push tags or branches."
echo "has_write=false" >> $GITHUB_OUTPUT
exit 1
else
echo "::warning::Unexpected response ($HTTP_CODE) when checking write permission."
echo "has_write=false" >> $GITHUB_OUTPUT
exit 1
fi
check-tag:
needs: check-secrets
if: needs.check-secrets.outputs.ready == 'true'
runs-on: ubuntu-latest
outputs:
skip: ${{ steps.check.outputs.skip }}
steps:
- name: Check if tag already exists
id: check
run: |
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer ${{ github.token }}" \
"https://api.github.com/repos/${{ github.repository }}/git/refs/tags/v${{ inputs.version }}")
if [ "$HTTP_CODE" = "200" ]; then
echo "::notice::Tag v${{ inputs.version }} already exists. Release skipped (not an error)."
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
release:
needs: [ci, check-tag, check-secrets, check-token-permissions]
if: needs.check-secrets.outputs.ready == 'true' && needs.check-tag.outputs.skip != 'true' && needs.check-token-permissions.outputs.has_write == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Validate version format
run: |
if [[ ! "${{ inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Version must be in X.Y.Z format"
exit 1
fi
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Detect package manager
id: pkg-manager
run: |
if [ -f "pnpm-lock.yaml" ]; then
echo "manager=pnpm" >> $GITHUB_OUTPUT
echo "lockfile=pnpm-lock.yaml" >> $GITHUB_OUTPUT
# Check for packageManager field in package.json (Corepack pinning).
# pnpm/action-setup@v5 errors when packageManager is absent and no version
# is specified, so use Corepack for repos that have the field pinned and
# fall back to pnpm/action-setup with version: latest for repos that don't.
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 "lockfile=package-lock.json" >> $GITHUB_OUTPUT
echo "has_package_manager=false" >> $GITHUB_OUTPUT
fi
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
# Only enable built-in npm caching here; pnpm caching is handled below
# after pnpm is installed (corepack is not available before setup-node).
cache: ${{ steps.pkg-manager.outputs.manager == 'npm' && 'npm' || '' }}
- name: Setup pnpm (via Corepack, reads version from 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 install
- name: Setup pnpm (version latest)
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: Configure Git
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Update version in package.json
run: |
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
pnpm version ${{ inputs.version }} --no-git-tag-version --allow-same-version
else
npm version ${{ inputs.version }} --no-git-tag-version --allow-same-version
fi
- name: Update artifacthub-pkg.yml
run: |
VERSION="${{ inputs.version }}"
if [ -f artifacthub-pkg.yml ]; then
PKG_NAME=$(grep '^name:' artifacthub-pkg.yml | cut -d: -f2 | tr -d ' "')
else
PKG_NAME=$(jq -r .name package.json | sed 's|^@[^/]*/||')
fi
RELEASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}/${PKG_NAME}-${VERSION}.tar.gz"
sed -i "s/^version:.*/version: \"${VERSION}\"/" artifacthub-pkg.yml
sed -i "s|headlamp/plugin/archive-url:.*|headlamp/plugin/archive-url: \"${RELEASE_URL}\"|" artifacthub-pkg.yml
- name: Update appVersion from upstream release
if: inputs.upstream-repo != ''
run: |
APP_VERSION=$(curl -sf "https://api.github.com/repos/${{ inputs.upstream-repo }}/releases/latest" | jq -r '.tag_name | ltrimstr("v")')
if [ -z "$APP_VERSION" ] || [ "$APP_VERSION" = "null" ]; then
echo "::warning::Could not fetch latest upstream release, skipping appVersion update"
else
sed -i "s|^appVersion:.*|appVersion: \"${APP_VERSION}\"|" artifacthub-pkg.yml
echo "appVersion set to ${APP_VERSION}"
fi
- name: Install dependencies
run: |
max_attempts=3
attempt=1
while [ $attempt -le $max_attempts ]; do
echo "Attempt $attempt of $max_attempts"
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
pnpm install --frozen-lockfile && break
else
npm ci && break
fi
if [ $attempt -lt $max_attempts ]; then
echo "::warning::Install step failed on attempt $attempt. Retrying in 5 seconds..."
sleep 5
fi
attempt=$((attempt + 1))
done
if [ $attempt -gt $max_attempts ]; then
echo "::error::Install step failed after $max_attempts attempts."
exit 1
fi
- name: Build plugin
run: npx @kinvolk/headlamp-plugin build
- name: Package plugin
run: npx @kinvolk/headlamp-plugin package
- name: Prepare release tarball
run: |
VERSION="${{ inputs.version }}"
# headlamp-plugin strips the @org/ prefix when naming tarballs.
# e.g. @privilegedescalation/headlamp-argocd-plugin -> headlamp-argocd-plugin
if [ -f artifacthub-pkg.yml ]; then
PKG_NAME=$(grep '^name:' artifacthub-pkg.yml | cut -d: -f2 | tr -d ' "')
else
PKG_NAME=$(jq -r .name package.json | sed 's|^@[^/]*/||')
fi
TARBALL="${PKG_NAME}-${VERSION}.tar.gz"
for f in *.tar.gz; do
[ "$f" != "$TARBALL" ] && mv "$f" "$TARBALL"
done
if [ ! -f "$TARBALL" ]; then
echo "Error: Expected tarball $TARBALL not found"
ls -la *.tar.gz 2>/dev/null || echo "No .tar.gz files found"
exit 1
fi
echo "TARBALL=$TARBALL" >> $GITHUB_ENV
echo "PKG_NAME=$PKG_NAME" >> $GITHUB_ENV
- name: Validate tarball
run: |
echo "Tarball: ${{ env.TARBALL }}"
ls -lh "${{ env.TARBALL }}"
tar -tzf "${{ env.TARBALL }}" | head -20
tar -tzf "${{ env.TARBALL }}" | grep -q "main.js" || { echo "Error: main.js not found in tarball"; exit 1; }
- name: Compute checksum
run: |
CHECKSUM=$(sha256sum "${{ env.TARBALL }}" | awk '{print $1}')
echo "CHECKSUM=$CHECKSUM" >> $GITHUB_ENV
sed -i "s|headlamp/plugin/archive-checksum:.*|headlamp/plugin/archive-checksum: sha256:${CHECKSUM}|" artifacthub-pkg.yml
- name: Commit and tag
run: |
VERSION="${{ inputs.version }}"
BRANCH="release/v${VERSION}"
# If the release branch already exists (e.g. from a failed prior run),
# delete it so the re-trigger can proceed cleanly. The check-tag job
# above already skips when the tag exists, so we only reach here when
# the tag does NOT exist yet — safe to remove a stale branch.
if git ls-remote --exit-code origin "refs/heads/$BRANCH" 2>/dev/null; then
echo "::notice::Branch $BRANCH already exists — deleting for clean re-trigger."
git push origin --delete "$BRANCH"
fi
git checkout -b "$BRANCH"
git add package.json "${{ steps.pkg-manager.outputs.lockfile }}" artifacthub-pkg.yml
git commit -m "release: v${VERSION}"
git tag "v${VERSION}"
git push origin "$BRANCH"
git push origin "refs/tags/v${VERSION}"
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: "v${{ inputs.version }}"
files: ${{ env.TARBALL }}
fail_on_unmatched_files: false
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Install GitHub CLI
run: |
if ! command -v gh &>/dev/null; then
GH_VERSION="2.74.0"
curl -fsSL "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" -o /tmp/gh.tar.gz
tar -xzf /tmp/gh.tar.gz -C /tmp
mkdir -p "$HOME/.local/bin"
mv "/tmp/gh_${GH_VERSION}_linux_amd64/bin/gh" "$HOME/.local/bin/gh"
rm -rf /tmp/gh.tar.gz "/tmp/gh_${GH_VERSION}_linux_amd64"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
"$HOME/.local/bin/gh" --version
fi
- name: Create PR for version bump
run: |
set -o pipefail
VERSION="${{ inputs.version }}"
BODY=$(printf "Automated version bump and checksum update for v%s.\n\ncc @cpfarhood" "${VERSION}")
# Create PR only if an OPEN one doesn't already exist.
# Note: gh pr view also finds MERGED PRs; we must check for open ones explicitly
# so that a re-trigger after a stale-branch delete creates a fresh PR.
OPEN_PR=$(gh pr list --base main --head "release/v${VERSION}" --state open --json number --jq '.[0].number' 2>/dev/null)
if [ -z "$OPEN_PR" ]; then
gh pr create \
--title "release: v${VERSION}" \
--body "$BODY" \
--base main \
--head "release/v${VERSION}"
# Pull the number again to handle both create and pre-existing cases
OPEN_PR=$(gh pr list --base main --head "release/v${VERSION}" --state open --json number --jq '.[0].number' 2>/dev/null)
else
echo "::notice::Open PR #${OPEN_PR} for release/v${VERSION} already exists — skipping creation."
fi
# Guard: ensure we have a PR number before proceeding
if [ -z "$OPEN_PR" ]; then
echo "::error::Could not determine PR number for release/v${VERSION}."
exit 1
fi
echo "::notice::Working with PR #${OPEN_PR}"
# Check if PR was already merged (idempotency — safe to re-trigger after a stale branch)
MERGED_CHECK=$(gh pr view "$OPEN_PR" --json state --jq '.state' 2>/dev/null)
if [ "$MERGED_CHECK" = "MERGED" ]; then
echo "::notice::PR #${OPEN_PR} was already merged. Nothing to do."
exit 0
fi
# Determine whether to use --auto or not based on current status.
# Retry the status check up to 3 times with exponential back-off when
# GitHub is still computing the merge state (UNKNOWN state).
MAX_RETRIES=3
BACKOFF=3
MERGE_STATE=""
for i in $(seq 1 $MAX_RETRIES); do
MERGE_STATE=$(gh pr view "$OPEN_PR" --json mergeStateStatus --jq '.mergeStateStatus' 2>/dev/null)
if [ "$MERGE_STATE" != "UNKNOWN" ]; then
break
fi
if [ $i -lt $MAX_RETRIES ]; then
echo "PR merge state is UNKNOWN (GitHub still computing). Retry ${i}/${MAX_RETRIES} in ${BACKOFF}s..."
sleep $BACKOFF
BACKOFF=$((BACKOFF * 2))
fi
done
if [ "$MERGE_STATE" = "BLOCKED" ] || [ "$MERGE_STATE" = "UNKNOWN" ]; then
echo "PR is $MERGE_STATE — attempting auto-merge (safe fallback, waits for branch protection checks)."
if gh pr merge "$OPEN_PR" --auto --squash --delete-branch 2>&1; then
echo "Auto-merge initiated successfully."
else
AUTO_MERGE_ERR=$?
# If --auto failed because auto-merge is disabled for this repo
# (autoMergeAllowed: false), fall back to --admin which merges
# regardless of branch protection rules. --admin requires GitHub
# App token, not GITHUB_TOKEN, so GH_TOKEN is already correct.
if gh pr merge "$OPEN_PR" --admin --squash --delete-branch 2>&1; then
echo "Auto-merge unavailable (autoMergeAllowed: false) — merged via --admin."
else
echo "::error::Both --auto and --admin merge failed. Exiting."
exit 1
fi
fi
else
echo "PR is $MERGE_STATE — merging directly."
gh pr merge "$OPEN_PR" --squash --delete-branch
fi
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Verify checksums are consistent (main == tag == tarball)
run: |
VERSION="${{ inputs.version }}"
TARBALL_CS=$(sha256sum "${{ env.TARBALL }}" | awk '{print $1}')
# Checksum recorded in the tag's artifacthub-pkg.yml
TAG_CS=$(git show "v${VERSION}:artifacthub-pkg.yml" 2>/dev/null | grep "archive-checksum" | awk '{print $2}' | sed 's/sha256://')
# Checksum now on main (after PR merge)
MAIN_CS=$(git fetch origin main 2>/dev/null; git show "origin/main:artifacthub-pkg.yml" | grep "archive-checksum" | awk '{print $2}' | sed 's/sha256://')
echo "Tarball SHA256 : $TARBALL_CS"
echo "Tag artifacthub: $TAG_CS"
echo "Main artifacthub: $MAIN_CS"
FAIL=0
[ "$TARBALL_CS" != "$TAG_CS" ] && echo "ERROR: tag checksum mismatch!" && FAIL=1
[ "$TARBALL_CS" != "$MAIN_CS" ] && echo "ERROR: main checksum mismatch!" && FAIL=1
[ "$FAIL" = "1" ] && exit 1
echo "All checksums consistent — ArtifactHub will index correctly."
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
-7
View File
@@ -1,7 +0,0 @@
extends: default
rules:
line-length: disable
document-start: disable
truthy:
check-keys: false
-6
View File
@@ -1,6 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"local>privilegedescalation/.github:renovate-config"
]
}
+7 -5
View File
@@ -10,7 +10,9 @@ description: >
## GitHub Authentication
Access to GitHub is done via token in your env **Never** run `gh auth login` directly — it hangs headless agents.
**Invoke the `github-app-token` skill** before any GitHub operation. It generates a short-lived installation token and sets `GH_TOKEN`. **Never** run `gh auth login` directly — it hangs headless agents.
Token expires after ~1 hour. Re-invoke the skill to regenerate if needed.
## GitHub Issues — Board Approval Required
@@ -67,13 +69,15 @@ Each promotion is a PR reviewed and merged by its gate owner:
2. **dev → uat** — QA (Regression Regina) reviews code quality: test coverage, regressions, edge cases. QA merges to `uat` after approval.
3. **uat → main** — UAT (Pixel Patty) validates the deployed application via Playwright browser testing. UAT merges to `main` after validation passes. For detailed UAT testing procedures, see the `uat` company skill.
**Playbook maintenance:** When plugin testing procedures change (new features, changed UI, different access paths), the engineer must update the plugin's `UAT_PLAYBOOK.md` in the same PR. This is a required deliverable alongside the code change — not optional cleanup.
**Each gate owner has merge authority.** No separate merge step by another role. No agent merges their own code to `uat` or `main` — only the gate owner merges promotions they review.
## Pipeline
### Pipeline A: Plugin/Feature Changes
```text
```
Engineer → PR to dev → self-merge → deploys to dev
→ Engineer validates on dev
→ PR from dev → uat → QA reviews → QA merges
@@ -84,11 +88,9 @@ Engineer → PR to dev → self-merge → deploys to dev
Applies to changes in `headlamp-*-plugin/` repos (plugin code, features, bug fixes).
**UAT_PLAYBOOK.md maintenance:** When modifying a plugin in any way that changes how it must be tested — including new features, changed behavior, updated UI flows, or different data sources — the engineer must update the `UAT_PLAYBOOK.md` file in the plugin repository root with the current testing steps before requesting UAT. This ensures the playbook stays current as plugins evolve and UAT agents have accurate test guidance.
### Pipeline B: Infrastructure Changes (No UI Impact)
```text
```
Engineer → PR to main → CI passes → QA reviews → QA merges
→ Production
```
+1
View File
@@ -132,3 +132,4 @@ flowchart LR
class UATB uat
class MAIN prod
```
+37 -42
View File
@@ -1,20 +1,40 @@
---
name: uat
description: >
Functional UAT procedures for Privileged Escalation Headlamp plugins. General
behavior, acceptance criteria, artifact requirements, and reference to
plugin-specific test steps in UAT_PLAYBOOK.md.
Functional UAT procedures for Privileged Escalation Headlamp plugins. Delegates per-plugin testing procedures to UAT_PLAYBOOK.md in each plugin repository.
---
# UAT Procedures
## Purpose
This skill defines **functional User Acceptance Testing** for all Privileged Escalation Headlamp plugins. UAT validates that plugins work correctly in the deployed environment — by exercising plugin features in a running Headlamp instance, not by reviewing code or CI results.
This skill defines **functional User Acceptance Testing** for all Privileged Escalation Headlamp plugins. UAT validates that plugins work correctly in the deployed environment — by loading plugins in a running Headlamp instance and exercising their features.
## UAT Environment
## Pipeline Stage
The UAT Headlamp instance runs in the `headlamp-uat` Kubernetes namespace. Navigate to the Headlamp UAT URL using your Playwright browser. The plugin under test must be deployed to UAT before testing begins.
UAT is the final promotion gate before production: `uat → main`. Pixel Patty (UAT agent) validates the deployed application via Playwright browser testing before merging.
## Per-Plugin Testing Procedures
Detailed, up-to-date testing procedures for each plugin are maintained in the plugin's own repository at `UAT_PLAYBOOK.md`. This is the authoritative source — do not copy procedure details into this skill.
| Plugin | Repository |
|--------|------------|
| headlamp-polaris-plugin | `polaris` |
| headlamp-sealed-secrets-plugin | `sealed-secrets` |
| headlamp-intel-gpu-plugin | `intel-gpu` |
| headlamp-kube-vip-plugin | `kube-vip` |
| headlamp-tns-csi-plugin | `tns-csi` |
| headlamp-rook-plugin | `rook` |
| headlamp-argocd-plugin | `argocd` |
Each plugin's `UAT_PLAYBOOK.md` contains:
- Access path (sidebar entry or app bar location)
- Step-by-step test actions
- Expected results for each step
- Pass/fail criteria
- Artifact requirements (screenshots, console errors, environment info)
## General Process
@@ -22,48 +42,23 @@ For every `uat→main` promotion:
1. Open the Headlamp UAT instance in the browser
2. Confirm the plugin appears in the sidebar or app bar
3. Read the plugin's `UAT_PLAYBOOK.md` for the specific test steps to run
4. Execute the test steps from the playbook, capturing screenshots at each verification
5. Check the browser console for errors throughout
6. Post a structured test report (see Artifacts section)
3. Retrieve the plugin's `UAT_PLAYBOOK.md` from its repository
4. Execute the test steps in the playbook
5. Capture screenshots of the **running plugin** at each verification step
6. Check the browser console for errors
7. Post a structured test report (see Artifacts section)
## Acceptance Criteria
A plugin passes UAT when:
- **Plugin loads** — sidebar entry or app bar action is visible and accessible
- **Features work** — all core features in the playbook execute without errors
- **No console errors** — browser console shows no errors during normal operation
- **Data matches cluster state** — plugin data is consistent with `kubectl` queries against the cluster
A plugin fails UAT when:
- Plugin does not load or renders only an error state
- Any core feature is inaccessible or produces errors
- Console errors are present and not explainable as unrelated noise
- Displayed data contradicts known cluster state
## Artifact Requirements
## UAT Artifacts
For each plugin tested, the UAT report must include:
1. **Screenshots** of the plugin running in Headlamp — sidebar entry visible, main view loaded, at least one detail view
2. **Test checklist** — each step from `UAT_PLAYBOOK.md` marked pass/fail
2. **Test checklist** — each step from the playbook marked pass/fail
3. **Console errors** — any browser console errors observed (attach screenshot if present)
4. **Environment info** — Headlamp version, plugin version, browser used, namespace context
## Reading UAT_PLAYBOOK.md
Each plugin repository contains a `UAT_PLAYBOOK.md` in its root directory. That file contains the canonical test steps for that specific plugin. Before running UAT, read the relevant playbook to know:
- Which features to exercise
- What the expected results are
- What screenshots to capture at each step
If `UAT_PLAYBOOK.md` does not exist for a plugin, treat that as a gap — report it in the UAT findings and flag it as a documentation issue.
4. **Environment** — Headlamp version, plugin version, browser used
## Decision Criteria
- **Approve** the `uat→main` promotion when all applicable test steps from the playbook pass and no console errors are present
- **Request changes** when any test step fails — include specific failing steps, observed results vs. expected results, and failure screenshots
- **Block** if the plugin fails to load entirely — escalate to CTO as a deployment issue requiring immediate resolution
- **Approve** the `uat→main` PR when all applicable test steps pass
- **Request changes** with specific failing steps and failure screenshots
- **Block** if the plugin fails to load entirely — escalate to CTO as a deployment issue