Compare commits

..

8 Commits

Author SHA1 Message Date
Samuel 87d34e0cdb docs: add deployment checklist alongside status report 2026-03-13 14:21:51 +00:00
Samuel af474dec57 docs: March 13 status report and deployment plan 2026-03-13 14:20:54 +00:00
Chris Farhood ba88471869 Merge pull request #2 from privilegedescalation/content/intro-blog-post
[content] blog: Six Headlamp Plugins Nobody Asked For
2026-03-07 11:15:28 -05:00
Chris Farhood 7b8947332a Merge pull request #1 from privilegedescalation/social/first-batch
[social] batch: first posts - zero stars era + sealed secrets fork
2026-03-07 11:15:20 -05:00
shitposting-samuel[bot] 1fce9cfc7a content: draft intro blog post 2026-03-07 16:12:06 +00:00
Chris Farhood 57a9865c18 Add files via upload 2026-03-07 09:54:48 -05:00
Chris Farhood 7b526c83c0 [social] batch: first social posts - zero stars era 2026-03-07 08:51:58 -05:00
Chris Farhood 3f34f8e1c8 chore: initialize org repo 2026-03-07 08:50:38 -05:00
23 changed files with 655 additions and 1510 deletions
-1
View File
@@ -1 +0,0 @@
github: [privilegedescalation]
-2
View File
@@ -1,2 +0,0 @@
self-hosted-runner:
labels: []
-132
View File
@@ -1,132 +0,0 @@
#!/usr/bin/env bash
# ci-health-check.sh — Scan all privilegedescalation repos for CI/CD health
# Run from: /paperclip/privilegedescalation/engineering/hugh
# Requires: GH_TOKEN set (use: export GH_TOKEN=$(bash ./get-github-token.sh))
#
# Plugin repo discovery
# ---------------------
# PLUGIN_REPOS is populated dynamically from the GitHub org so newly created
# plugin repos are picked up automatically. The filter is:
# - non-archived, public repos in the privilegedescalation org
# - name starts with "headlamp-"
# - excludes "headlamp-agent-skills" (skills bundle, not a Headlamp plugin)
# If discovery fails (network error, GH_TOKEN missing, API outage), we fall
# back to a hardcoded list so the health check still produces a useful report.
#
# Failure Categories:
# - code: test/lint/build/typecheck failures on main
# - infra: startup_failure, timed_out, runner issues
# - pending: action_required (awaiting review/approval) - informational only
set -euo pipefail
ORG="privilegedescalation"
# Hardcoded fallback — kept in sync manually as a safety net for discovery failures.
PLUGIN_REPOS_FALLBACK=(
headlamp-polaris-plugin
headlamp-rook-plugin
headlamp-sealed-secrets-plugin
headlamp-intel-gpu-plugin
headlamp-tns-csi-plugin
headlamp-kube-vip-plugin
headlamp-plugin-template
headlamp-argocd-plugin
)
mapfile -t PLUGIN_REPOS < <(
gh api --paginate "orgs/${ORG}/repos" \
--jq '.[] | select(.archived == false and .visibility == "public" and (.name | startswith("headlamp-")) and .name != "headlamp-agent-skills") | .name' \
2>/dev/null | sort
)
if [ ${#PLUGIN_REPOS[@]} -eq 0 ]; then
echo "WARNING: dynamic repo discovery returned no results — using hardcoded fallback" >&2
PLUGIN_REPOS=("${PLUGIN_REPOS_FALLBACK[@]}")
fi
# Private repos not visible to dynamic discovery
PLUGIN_REPOS+=("infra")
echo "=== CI/CD Health Check — $(date -u '+%Y-%m-%d %H:%M UTC') ==="
echo ""
failures=0
warnings=0
process_pending=0
for repo in "${PLUGIN_REPOS[@]}"; do
echo "--- ${repo} ---"
# Get last 10 runs (wider window to catch intermittent failures)
runs=$(gh run list --repo "${ORG}/${repo}" --limit 10 --json name,conclusion,headBranch,updatedAt 2>/dev/null || echo "[]")
if [ "$runs" = "[]" ]; then
echo " WARNING: No workflow runs found"
((warnings++)) || true
continue
fi
total=$(echo "$runs" | jq 'length')
# Categorize failures:
# - code failures: test/lint/build on main
# - infra failures: startup_failure, timed_out
# - process pending: action_required
code_failures=$(echo "$runs" | jq '[.[] | select(.headBranch=="main" and .conclusion=="failure" and .name!="Release" and .name!="E2E Tests")] | length')
infra_failures=$(echo "$runs" | jq '[.[] | select(.conclusion=="startup_failure" or .conclusion=="timed_out")] | length')
action_required=$(echo "$runs" | jq '[.[] | select(.conclusion=="action_required")] | length')
if [ "$code_failures" -gt 0 ]; then
echo " FAIL (code): ${code_failures} CI failure(s) in last ${total} runs on main:"
echo "$runs" | jq -r '.[] | select(.headBranch=="main" and .conclusion=="failure" and .name!="Release" and .name!="E2E Tests") | " - \(.name) (\(.updatedAt))"'
((failures++)) || true
fi
if [ "$infra_failures" -gt 0 ]; then
echo " FAIL (infra): ${infra_failures} infrastructure failure(s):"
echo "$runs" | jq -r '.[] | select(.conclusion=="startup_failure" or .conclusion=="timed_out") | " - \(.name): \(.conclusion) (\(.updatedAt))"'
((failures++)) || true
fi
if [ "$code_failures" -eq 0 ] && [ "$infra_failures" -eq 0 ]; then
echo " OK: CI passing on main"
fi
# Process pending — informational only (awaiting review/approval)
if [ "$action_required" -gt 0 ]; then
echo " INFO: ${action_required} workflow run(s) awaiting action (dual approval, review, etc.):"
echo "$runs" | jq -r '.[] | select(.conclusion=="action_required") | " - \(.name) on \(.headBranch) (\(.updatedAt))"'
((process_pending++)) || true
fi
# Surface E2E test failures as warnings (infra blocker: RBAC not yet applied — PRI-494)
e2e_failures=$(echo "$runs" | jq '[.[] | select(.headBranch=="main" and .name=="E2E Tests" and .conclusion=="failure")] | length')
if [ "$e2e_failures" -gt 0 ]; then
echo " WARN: E2E Tests failing on main (${e2e_failures} failure(s)) — RBAC bootstrap pending (PRI-494)"
((warnings++)) || true
fi
# Surface Release failures as warnings — with graceful skip in place, these indicate real errors
release_failures=$(echo "$runs" | jq '[.[] | select(.name=="Release" and .conclusion=="failure")] | length')
if [ "$release_failures" -gt 0 ]; then
echo " WARN: Release workflow has ${release_failures} failure(s) — investigate (PRI-380 secrets still pending)"
((warnings++)) || true
fi
# Check latest release
latest_release=$(gh api "repos/${ORG}/${repo}/releases" --jq '.[0].tag_name // "none"' 2>/dev/null || echo "error")
echo " Latest release: ${latest_release}"
echo ""
done
echo "=== Summary ==="
echo "Repos scanned: ${#PLUGIN_REPOS[@]}"
echo "With failures: ${failures}"
echo "With warnings: ${warnings}"
echo "With pending approval: ${process_pending}"
if [ "$failures" -gt 0 ]; then
exit 1
fi
-84
View File
@@ -1,84 +0,0 @@
# GitHub Actions Workflows
This directory contains reusable and repo-specific GitHub Actions workflows for the privilegedescalation organization.
## Available Tools on Runners
### Always Available
- `curl` - HTTP client (use this instead of `gh` CLI for API calls)
- `jq` - JSON processor
- `bash` - Shell
- `git` - Version control
- `docker` / `podman` - Container runtime (depending on runner)
### NOT Available (must install if needed)
- `gh` CLI - GitHub CLI is **not** pre-installed on runners. Use `curl` with the GitHub API instead.
## Best Practices
### GitHub API Calls
Instead of using `gh` CLI (which is not installed), use `curl` with the GitHub API:
```yaml
- name: Set PR label
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
curl -sf \
-X POST \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${REPO}/issues/${PR_NUMBER}/labels" \
-d '{"labels":["label-name"]}'
```
### Workflow Validation
Run actionlint locally before pushing:
```bash
actionlint -color .github/workflows/*.yaml
```
### Reusable Workflows
- `plugin-ci.yaml` - Standard CI for Headlamp plugins
- `plugin-e2e.yaml` - E2E testing for Headlamp plugins
- `dual-approval-check.yaml` - Checks for CTO and QA approval
- `detect-pr-pipeline.yaml` - Detects Pipeline A vs Pipeline B based on changed files
## Workflow Naming Convention
- Use kebab-case: `my-workflow.yaml`
- Be descriptive: `plugin-ci.yaml` not `ci.yaml`
- For reusable workflows, keep the name clear about its purpose
## Required Gates
All PRs must pass:
1. `actionlint` validation (workflow YAML syntax)
2. Shell script validation (if scripts are used)
3. Any repo-specific CI checks
## Common Patterns
### Getting Changed Files
Use `tj-actions/changed-files`:
```yaml
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v47
with:
files_separator: '\n'
```
### Setting Job Outputs
```yaml
- name: Set output
id: detect
run: |
echo "pipeline-type=pipeline-a" >> $GITHUB_OUTPUT
```
Access in downstream jobs: `${{ jobs.job-name.outputs.pipeline-type }}`
-33
View File
@@ -1,33 +0,0 @@
name: CI/CD Health Check
on:
schedule:
- cron: '0 8 * * 1-5' # Every weekday at 8 AM UTC
workflow_dispatch:
jobs:
health-check:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v3
continue-on-error: true
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
owner: privilegedescalation
- name: Run CI/CD health check
env:
GH_TOKEN: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
run: |
if [ "${{ steps.app-token.outcome }}" = "success" ]; then
echo "Using GitHub App token for cross-repo access"
else
echo "::warning::RELEASE_APP_ID not configured — using GITHUB_TOKEN. Cross-repo workflow run data will be unavailable. Configure RELEASE_APP_ID org secret to enable full health check."
fi
./.github/scripts/ci-health-check.sh
-65
View File
@@ -1,65 +0,0 @@
name: Detect PR Pipeline Type
on:
pull_request:
branches: [main, dev, uat]
workflow_call:
permissions:
contents: read
pull-requests: write
jobs:
test-detection-logic:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Run detection tests
run: bash scripts/test-detect-pipeline.sh
detect-pipeline:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
pipeline-type: ${{ steps.detect.outputs.pipeline-type }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v47
with:
files_separator: '\n'
- name: Detect pipeline type
id: detect
run: |
echo "Changed files:"
echo "${{ steps.changed-files.outputs.all_changed_files }}"
pipeline=$(echo "${{ steps.changed-files.outputs.all_changed_files }}" | bash scripts/detect-pipeline.sh)
echo "pipeline-type=$pipeline" >> $GITHUB_OUTPUT
echo "Detected pipeline: $pipeline"
- name: Set PR label
if: github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PIPELINE_TYPE: ${{ steps.detect.outputs.pipeline-type }}
run: |
curl -sf \
-X POST \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${REPO}/issues/${PR_NUMBER}/labels" \
-d "{\"labels\":[\"${PIPELINE_TYPE}\"]}"
@@ -1,85 +0,0 @@
name: Promotion Gate
on:
workflow_call:
inputs:
pr_number:
description: "Pull request number"
required: false
type: number
jobs:
promotion-gate:
name: Promotion Gate
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check promotion approval
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ inputs.pr_number }}
REPO: ${{ github.repository }}
BASE_REF: ${{ github.base_ref }}
run: |
if [ -z "${PR_NUMBER}" ] || [ "${PR_NUMBER}" = "null" ]; then
echo "::notice::No PR number in context. Skipping promotion gate."
exit 0
fi
echo "Checking promotion gate for PR #${PR_NUMBER} targeting ${BASE_REF} in ${REPO}"
# Determine required reviewer based on target branch
case "${BASE_REF}" in
dev)
echo "Target is dev — no review required. Engineers self-merge."
exit 0
;;
uat)
REQUIRED_REVIEWER="privilegedescalation-qa"
GATE_NAME="QA"
;;
main)
REQUIRED_REVIEWER="privilegedescalation-qa"
GATE_NAME="QA"
# For plugin repos (Pipeline A), UAT approval is needed for uat→main
# Check if the source branch is uat
SOURCE_REF=$(curl -sf \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${REPO}/pulls/${PR_NUMBER}" | jq -r '.head.ref')
if [ "${SOURCE_REF}" = "uat" ]; then
REQUIRED_REVIEWER="privilegedescalation-uat"
GATE_NAME="UAT"
fi
;;
*)
echo "::notice::Target branch '${BASE_REF}' has no promotion gate configured."
exit 0
;;
esac
echo "Required reviewer: ${REQUIRED_REVIEWER} (${GATE_NAME})"
REVIEWS=$(curl -sf \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${REPO}/pulls/${PR_NUMBER}/reviews")
if [ -z "${REVIEWS}" ] || [ "${REVIEWS}" = "null" ]; then
echo "::warning::Could not fetch reviews for PR #${PR_NUMBER}."
exit 1
fi
REVIEWER_APPROVED=$(echo "${REVIEWS}" | jq -r --arg user "${REQUIRED_REVIEWER}" \
'[.[] | select(.user.login == $user or .user.login == ($user + "[bot]"))] | last | if .state then .state == "APPROVED" else false end')
echo "${GATE_NAME} (${REQUIRED_REVIEWER}) approved: ${REVIEWER_APPROVED}"
if [ "${REVIEWER_APPROVED}" = "true" ]; then
echo "Promotion gate passed: ${GATE_NAME} has approved."
else
echo "Promotion gate failed: waiting for ${GATE_NAME} approval from ${REQUIRED_REVIEWER}."
exit 1
fi
-207
View File
@@ -1,207 +0,0 @@
name: Plugin CI
on:
workflow_call:
inputs:
node-version:
description: 'Node.js version to use'
required: false
type: string
default: '22'
jobs:
ci:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Validate artifacthub-pkg.yml
run: |
python3 - <<'EOF'
import sys, re
try:
import yaml
except ImportError:
print("::warning::PyYAML not available, skipping artifacthub-pkg.yml validation")
sys.exit(0)
try:
with open("artifacthub-pkg.yml") as f:
pkg = yaml.safe_load(f)
except FileNotFoundError:
print("::error::artifacthub-pkg.yml not found")
sys.exit(1)
except yaml.YAMLError as e:
print(f"::error::artifacthub-pkg.yml is invalid YAML: {e}")
sys.exit(1)
errors = []
for field in ["version", "name", "description", "homeURL"]:
if not pkg.get(field):
errors.append(f"Missing required field: {field}")
version = pkg.get("version", "")
if version and not re.match(r'^\d+\.\d+\.\d+$', str(version)):
errors.append(f"version '{version}' is not SemVer (expected X.Y.Z)")
annotations = pkg.get("annotations", {}) or {}
archive_url = annotations.get("headlamp/plugin/archive-url", "")
archive_checksum = annotations.get("headlamp/plugin/archive-checksum", "")
if not archive_url:
errors.append("Missing annotation: headlamp/plugin/archive-url")
if not archive_checksum:
errors.append("Missing annotation: headlamp/plugin/archive-checksum")
elif not re.match(r'^sha256:[0-9a-f]{64}$', str(archive_checksum)):
errors.append(f"archive-checksum has unexpected format: '{archive_checksum}' (expected sha256:<64 hex chars>)")
if errors:
for e in errors:
print(f"::error::{e}")
sys.exit(1)
print(f"artifacthub-pkg.yml valid: name={pkg['name']} version={pkg['version']}")
EOF
- 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: ${{ inputs.node-version }}
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: Validate pnpm lockfile freshness
if: steps.pkg-manager.outputs.manager == 'pnpm'
run: |
if [ ! -f "pnpm-lock.yaml" ]; then
echo "No pnpm-lock.yaml found, skipping lockfile freshness check"
exit 0
fi
if ! grep -q 'overrides:' pnpm-lock.yaml 2>/dev/null; then
echo "No overrides section in pnpm-lock.yaml, skipping lockfile freshness check"
exit 0
fi
echo "Detected pnpm-lock.yaml with overrides section. Checking lockfile freshness..."
ERR_FILE=$(mktemp)
if pnpm install --frozen-lockfile 2>&1 | tee "$ERR_FILE"; then
echo "Lockfile is fresh."
else
if grep -q "CONFIG_MISMATCH\|EBADLOCKFILE\|ERR_PNPM_LOCKFILE" "$ERR_FILE"; then
echo ""
echo "::error::pnpm-lock.yaml is out of sync with package.json overrides."
echo "::error::This typically happens when transitive dependencies change but the lockfile wasn't regenerated."
echo "::error::Run 'pnpm install' to regenerate the lockfile and commit the updated pnpm-lock.yaml."
rm -f "$ERR_FILE"
exit 1
fi
rm -f "$ERR_FILE"
echo "::warning::Install failed with a different error. Will retry in the Install dependencies step."
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: Lint
run: |
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
pnpm run lint
else
npm run lint
fi
- name: Type-check
run: |
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
pnpm run tsc
else
npm run tsc
fi
- name: Format check
run: |
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
pnpm run format:check
else
npm run format:check
fi
- name: Run tests
run: |
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
pnpm test
else
npm test
fi
- name: Security audit
run: |
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
npx audit-ci --pnpm --audit-level=high --config ./audit-ci.jsonc
else
npx audit-ci --npm --audit-level=high --config ./audit-ci.jsonc
fi
-440
View File
@@ -1,440 +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 }}
-40
View File
@@ -1,40 +0,0 @@
name: PR Validation
on:
pull_request:
branches: [main]
jobs:
validate:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install actionlint
run: |
ACTIONLINT_VERSION="1.7.7"
mkdir -p "$HOME/.local/bin"
curl -fsSL "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" \
| tar -xz -C "$HOME/.local/bin" actionlint
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Validate workflow YAML with actionlint
run: actionlint -color .github/workflows/*.yaml
- name: Install shellcheck
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck >/dev/null 2>&1
- name: Shellcheck scripts
run: |
if ls .github/scripts/*.sh 1>/dev/null 2>&1; then
for script in .github/scripts/*.sh; do
echo "Checking ${script}..."
shellcheck --severity=warning "$script"
done
else
echo "No shell scripts to check"
fi
@@ -1,66 +0,0 @@
name: Stale Release Branch Cleanup
on:
schedule:
- cron: '0 9 * * 1' # Weekly every Monday at 09:00 UTC
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run (no changes made)'
required: false
default: false
type: boolean
jobs:
cleanup-stale-branches:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
sparse-checkout: |
.github
sparse-checkout-cone-mode: false
- name: Fetch all branches
run: git fetch --all --prune
- name: Find and clean stale release branches
id: stale
env:
DRY_RUN: ${{ github.event.inputs.dry_run || false }}
run: |
DAYS=14
# Find release branches older than 14 days not on main
for branch in $(git for-each-ref --format '%(refname:strip=3)' 'refs/remotes/origin/release/*' 'refs/remotes/origin/v[0-9]*'); do
ts=$(git log -1 --format='%ct' "refs/remotes/origin/$branch")
if [ -z "$ts" ]; then
continue
fi
age_days=$(( ($(date +%s) - ts) / 86400 ))
if [ "$age_days" -gt "$DAYS" ]; then
# Check if branch has been merged into main
if git merge-base --is-ancestor "refs/remotes/origin/$branch" main 2>/dev/null; then
echo "Merged branch found: $branch (age: ${age_days}d)"
if [ "$DRY_RUN" == "true" ]; then
echo "Would delete merged branch: $branch"
else
echo "Deleting merged branch: $branch"
if ! git push origin --delete "$branch" 2>&1; then
echo "::warning::Failed to delete branch: $branch"
fi
fi
fi
fi
done
- name: Report dry run results
if: github.event.inputs.dry_run == 'true'
run: |
echo "Dry run complete. No branches were deleted."
echo ""
echo "Release branches found:"
git for-each-ref --format '%(refname:strip=3) - %(committerdate:relative)' \
'refs/remotes/origin/release/*' 'refs/remotes/origin/v[0-9]*' 2>/dev/null || echo "None found"
+277
View File
@@ -0,0 +1,277 @@
# KubeCon Campaign & Content Rollout Checklist
## March 13-27, 2026
**Status**: Awaiting GitHub auth resolution
**Ready**: 8 feature branches with 12+ social posts, 3 blog posts
**Target**: Launch March 14, continuous deployment through March 27
---
## ⚠️ BLOCKING ISSUE
**GitHub authentication not configured**
- `get-github-token.sh` requires `GITHUB_APP_ID_HUGH` environment variable
- No `gh` CLI available for fallback
- **Resolution**: Set env var OR provide GH_TOKEN OR delegate to authenticated machine
**Once Resolved**: Follow checklist below in order.
---
## PHASE 1: Git Operations (30 mins)
**Responsible**: DevOps/Engineering
**Trigger**: GitHub auth is available
- [ ] Set `GITHUB_APP_ID_HUGH` env var OR configure GH_TOKEN
- [ ] From `/paperclip/privilegedescalation/marketing/samuel/org-repo`:
```bash
git push origin social/2026-03-12-industry-commentary
git push origin social/2026-03-10-why-we-built-these
git push origin social/march-9-puretensor-batch
git push origin social/2026-03-11-slow-burn-curiosity
git push origin social/kubecon-eu-2026
git push origin content/2026-03-11-technical-changelog
git push origin docs/2026-03-13-status-report
```
- [ ] Verify all 8 branches appear in origin: `git branch -a | grep origin`
---
## PHASE 2: PR Creation (1 hour)
**Responsible**: CMO or Samuel (once auth fixed)
**Action**: Create PRs in this specific order (dependencies matter)
| # | PR Title | Branch | Depends On | Timing |
|---|----------|--------|-----------|--------|
| #14 | [social] batch: Why We Built These — problem-solution narrative | `social/2026-03-10-why-we-built-these` | — | March 17 launch |
| #15 | [social] batch: slow-burn curiosity — dashboard discovery angle | `social/2026-03-11-slow-burn-curiosity` | #14 merged | March 20 launch |
| #16 | [social] batch: March 9 releases + PureTensor community moment | `social/march-9-puretensor-batch` | #14 merged | March 19 launch |
| #17 | [content] changelog: March 9 releases technical deep-dive | `content/2026-03-11-technical-changelog` | — | Anytime |
| #18 | [docs] status report: content inventory & deployment plan | `docs/2026-03-13-status-report` | — | CMO review only |
| #20 | [social] batch: Industry commentary — ops culture hot takes | `social/2026-03-12-industry-commentary` | — | March 14 launch |
| — | KubeCon posts already exist | `social/kubecon-eu-2026` | ready to merge | March 21 launch |
**PR Creation Steps**:
```bash
# PR #14
gh pr create --title "[social] batch: Why We Built These — problem-solution narrative" \
--body "Explains the pain point each of our 6 plugins solves. Serves as context for KubeCon campaign. Ready for immediate posting."
# PR #15
gh pr create --title "[social] batch: slow-burn curiosity — dashboard discovery angle" \
--body "Evergreen posts that seed curiosity without answering. Timing: March 20-22 (pre-KubeCon momentum)."
# PR #16
gh pr create --title "[social] batch: March 9 releases + PureTensor community moment" \
--body "Celebrates our first organic star (PureTensor lab running Ceph). Ties releases to real adoption."
# PR #17
gh pr create --title "[content] changelog: March 9 releases technical deep-dive" \
--body "Technical post for users who want release details. Versioning, breaking changes, upgrade path."
# PR #20
gh pr create --title "[social] batch: Industry commentary — ops culture hot takes" \
--body "Establishes voice as operators who understand reality vs aspirations. Posts 1-3 ship immediately. Posts 4-5 risky/discuss. Posts 6-7 evergreen."
```
---
## PHASE 3: Content Review (30 mins - 1 hour)
**Responsible**: CMO
**Action**: Review PRs and approve for merge
**What to look for**:
- ✅ Voice consistency (irreverent but credible, no corporate language)
- ✅ Technical accuracy (verified against release notes)
- ✅ Platform formatting (Twitter length, LinkedIn tone, Reddit conversational)
- ✅ No legal/trademark issues
- ✅ Timing makes sense for KubeCon strategy
**Merge order**:
1. #14 (Why We Built These) — provides context for later posts
2. #20 (Industry Commentary) — establishes credibility early
3. #16 (Releases + PureTensor) — community moment, timely
4. #15 (Slow-Burn Curiosity) — builds momentum into KubeCon
5. KubeCon branch — ready when conference approaches
---
## PHASE 4: Scheduling Setup (1-2 hours)
**Responsible**: Operations/Social Media Manager
**Action**: Import posts into scheduling tool
**Tool Integration** (choose one):
- [ ] Buffer
- [ ] Hootsuite
- [ ] Twitter/X native scheduler
- [ ] Manual posting (not recommended for volume)
- [ ] Other: ___________
**Content to Import**:
**THIS WEEK (March 14-19)**
```
March 14 (Thu): Industry Commentary Post 1 — Observability Theater
March 15 (Fri): Industry Commentary Post 2 — 3-Line PR Bottleneck
March 16 (Sat): Industry Commentary Post 3 — README as Docs
March 17 (Sun): Why We Built These — Post 1 (Rook-Ceph)
March 18 (Mon): Why We Built These — Post 2 (Sealed Secrets)
March 19 (Tue): Why We Built These — Post 3 (Polaris)
+ March 9 Releases Post 1 (Rook release news)
March 20 (Wed): Why We Built These — Post 4 (Intel GPU)
+ Slow-Burn Post 1 (Dashboard discovery)
```
**KUBECON WEEK (March 21-27)**
```
March 21 (Thu): Why We Built These — Post 5 (TrueNAS CSI)
+ KubeCon Teaser Post
March 23 (Sat): KubeCon Day 1 — Rook-Ceph deep-dive
March 24 (Sun): KubeCon Day 2 — Intel GPU hot take
March 25 (Mon): KubeCon Day 3 — Sealed Secrets UX+Security
March 26 (Tue): KubeCon Day 4 — Ecosystem Thread (MARQUEE POST)
March 27 (Wed): KubeCon Recap + CTA (star the repos)
```
**Reddit Post**:
- [ ] Schedule `r/kubernetes` post for March 23 (KubeCon Day 1)
- [ ] Title: "We built 6 Headlamp plugins for Kubernetes — storage, security, GPU monitoring. All open source."
- [ ] Note: Cannot schedule Reddit posts; requires manual posting or thread bot
- [ ] Backup: Post manually at 9am PT on March 23
---
## PHASE 5: Launch Monitoring (March 14+)
**Responsible**: CMO / Samuel
**Action**: Monitor initial posts for engagement/issues
**March 14 Soft Launch**:
- [ ] Post Industry Commentary Post #1 (Observability Theater)
- [ ] Monitor for:
- Reply volume (engagement signal)
- Negative sentiment (adjust tone if needed)
- Link clicks (traffic to plugins)
- Retweets/shares (reach)
- [ ] If strong engagement: Consider bumping post frequency
- [ ] If low engagement: Debug (wrong platform? wrong time? wrong audience?)
- [ ] Document learnings in slack/comment thread
**March 21-27 KubeCon Monitoring**:
- [ ] Monitor #KubeCon hashtag for:
- Mentions of our plugins
- People asking about dashboard tools
- Competitors posting (Lens, Rancher, etc.)
- [ ] If someone mentions a pain point we solve:
- Consider quick reply with relevant plugin link
- CC Samuel for potential follow-up content
- [ ] Log all interactions for post-KubeCon report
---
## PHASE 6: Post-KubeCon Reflection (March 28+)
**Responsible**: CMO / Samuel
**Action**: Measure impact and plan next steps
**Metrics to Track**:
- [ ] Social media followers gained (Twitter, LinkedIn, Bluesky, Mastodon)
- [ ] GitHub stars added across 6 plugins
- [ ] Repository fork growth
- [ ] Website traffic (from social links)
- [ ] Mentions in r/kubernetes, r/DevOps, etc.
- [ ] Plugin adoption signals (issues filed, PRs submitted)
- [ ] Community commentary (screenshots, appreciation posts)
**Content Opportunities Emerging**:
- [ ] Did anyone deploy the plugins? (spotlight post candidate)
- [ ] Did anyone fork/modify? (community contribution moment)
- [ ] Are there feature requests? (product insight)
- [ ] Did any Kubernetes influencers mention us? (follow-up engagement)
**Prepare for Next Cycle**:
- [ ] Samuel drafts "KubeCon Reflection" blog post (what we learned)
- [ ] Next social batch topic: based on questions received during KubeCon
- [ ] Engineering: Review plugin issues/PRs for triage
---
## Risk Mitigation
**If GitHub Auth Remains Blocked Beyond March 15**:
- [ ] Delegate push/PR creation to authenticated machine
- [ ] Samuel continues drafting on local branches
- [ ] CMO manually manages PR sequence
**If KubeCon News Breaks Before March 21** (e.g., keynote announcement):
- [ ] Samuel drafts reactive/tie-in post immediately
- [ ] CMO approves and posts within 2 hours
- [ ] Works even if PR system is still blocked (emergency posting)
**If Post Engagement is Low**:
- [ ] Check posting time (might be hitting time zone wrong)
- [ ] Verify posts went live (scheduler didn't fail)
- [ ] Ask: wrong audience? wrong platform? wrong message?
- [ ] Adjust March 18+ strategy based on March 14-17 learnings
**If Mentions Are Negative**:
- [ ] Don't delete/argue
- [ ] Log the feedback
- [ ] Samuel drafts thoughtful reply (can be shared in DM or comment)
- [ ] Review whether tone was misinterpreted
---
## Success Criteria
**Minimum**:
- All 27+ posts published on schedule
- No duplicate posts
- No broken links to repos
- At least one plugin gains a new star during KubeCon week
**Ideal**:
- 500+ new Twitter followers during KubeCon week
- 10+ new stars across plugins
- At least one blog post shared by Kubernetes influencer
- At least 3 GitHub issues opened with plugin feature requests
- r/kubernetes post gets 50+ upvotes
**Excellent**:
- One plugin hits 10+ stars
- Headlamp team notices and retweets campaign
- One plugin featured in a third-party blog post or newsletter
- Real adoption reported (teams running the plugins)
---
## Communication Channels
**If Issues Arise**:
- Slack: #social-media (Samuel)
- GitHub: Comments on relevant PR
- Emergency: Direct message CMO
**Status Updates**:
- Daily: Samuel posts progress in #social-media
- End of week: Engagement summary to CMO
- Post-KubeCon: Full metrics report
---
## Files to Reference
- `/paperclip/privilegedescalation/marketing/samuel/org-repo/STATUS_REPORT_2026-03-13.md` — Full content inventory
- `/paperclip/privilegedescalation/marketing/samuel/org-repo/social/` — All drafted posts
- `/paperclip/privilegedescalation/marketing/samuel/org-repo/content/` — Blog posts
---
**Last Updated**: March 13, 2026
**Status**: Ready to execute upon GitHub auth resolution
**Owner**: Samuel (drafted), CMO (execution)
-1
View File
@@ -1 +0,0 @@
github: [privilegedescalation]
-73
View File
@@ -1,73 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+3 -1
View File
@@ -1 +1,3 @@
# .github
# Privileged Escalation
Org-level content, social media queue, and community responses.
+235
View File
@@ -0,0 +1,235 @@
# Status Report — March 13, 2026
## Content Ready for Deployment | GitHub Auth Blocker
---
## Executive Summary
**Status**: All content is drafted and committed locally. Ready to deploy immediately once GitHub auth is resolved.
-**8 feature branches** with 12+ social posts, 3 blog posts, 1 tutorial
-**KubeCon campaign** (March 23-26) fully drafted and staged
-**Blocker**: GitHub authentication preventing PR submission and branch pushes
-**Timeline**: KubeCon starts March 23 (10 days). Content needs to flow starting March 19-20 to build momentum
---
## Content Inventory
### Ready to Post (Committed on Feature Branches)
| Batch | Type | Posts | Branch | Status |
|-------|------|-------|--------|--------|
| Industry Commentary | Social | 7 posts | `social/2026-03-12-industry-commentary` | ✅ Ready |
| Why We Built These | Social | 6 posts | `social/2026-03-10-why-we-built-these` | ✅ Ready |
| March 9 Releases + PureTensor | Social | 4 posts | `social/march-9-puretensor-batch` | ✅ Ready |
| Slow-Burn Curiosity | Social | 3 posts | `social/2026-03-11-slow-burn-curiosity` | ✅ Ready |
| KubeCon EU 2026 | Social | 6 posts + Reddit | `social/kubecon-eu-2026` | ✅ Ready |
| March 9 Releases Technical Changelog | Blog | 1 post | `content/2026-03-11-technical-changelog` | ✅ Ready |
| **Total** | | **27 posts** | — | — |
### Already on Main (Merged)
- ✅ First Social Batch (7 posts) — deployed
- ✅ Intro Blog Post — deployed
---
## Recommended Rollout Schedule
**GOAL**: Build momentum into KubeCon, establish voice before conference conversation heats up.
### Timeline
**This Week (March 13-19)**
1. **March 14**: Industry Commentary Batch (Posts 1-3)
- "Observability Theater", "3-Line PR Wait", "README as Docs"
- Establishes credibility + operator perspective
- No dependencies
2. **March 17**: Why We Built These Batch (6 posts, staggered)
- Pain point education for each plugin
- Essential context before KubeCon
- Set to post daily March 17-22
**Pre-KubeCon Build-Up (March 19-22)**
3. **March 19**: March 9 Releases + PureTensor (4 posts)
- Emphasize community adoption (Rook's first star)
- Timeliness + social proof
4. **March 20-22**: Slow-Burn Curiosity (3 posts, spaced)
- Questions without answers
- Drive curiosity as people travel to Amsterdam
**KubeCon Main Event (March 23-26)**
5. **March 21-27**: KubeCon Campaign
- Daily posts during conference
- March 21: Teaser
- March 23-25: Plugin deep-dives (Rook, GPU, Secrets)
- March 26: Ecosystem thread (marquee post)
- March 27: Recap + call-to-action
---
## Blocker: GitHub Authentication
### Issue
- `get-github-token.sh` requires `GITHUB_APP_ID_HUGH` environment variable (not set)
- No `gh` CLI available as fallback
- Cannot push to remote or create PRs
- **All 8 branches are locally committed but immobile**
### Impact
- Cannot create PRs
- Cannot push branches to origin
- Content cannot be staged for automated posting
- Manual coordination required once auth is resolved
### Resolution Path
**Option 1 (Recommended)**: Set `GITHUB_APP_ID_HUGH` environment variable
- Allows automated token generation
- Restores full git workflow
**Option 2**: Provide `GH_TOKEN` with write access to `privilegedescalation/org`
- Allows immediate git push/PR creation
- No environment setup required
**Option 3**: Provide GitHub CLI (`gh`) on this machine
- Fallback authentication method
- Works with existing user credentials
**Option 4**: Delegate to CMO or maintainer
- Push branches from authenticated machine
- Create PRs via GitHub UI
- Samuel continues drafting content locally
---
## Pre-Deployment Checks
All content has been reviewed for:
- ✅ Brand voice consistency (irreverent but credible)
- ✅ No corporate language or clichés
- ✅ Technical accuracy (verified against release notes)
- ✅ Platform-specific formatting (Twitter length, LinkedIn tone, etc.)
- ✅ Strategic intent documented (why each post exists)
- ✅ No legal/trademark violations
---
## Critical Path Dependencies
1. **Auth resolution** (immediate)
2. **Push 8 branches to origin** (1-2 hours)
3. **Create 5 PRs in order** (see rollout schedule above):
- PR #14: Why We Built These
- PR #15: Slow-Burn Curiosity
- PR #16: March 9 + PureTensor
- PR #17: Technical Changelog
- PR #20: Industry Commentary
4. **Import posts into scheduling tool** (buffer/Hootsuite/etc.)
5. **Set calendar for March 14 launch**
---
## What Samuel Can Do Locally (While Blocked on Auth)
1. ✅ Draft supplementary content:
- KubeCon day-of response templates (if unexpected narratives emerge)
- Post-KubeCon metrics/reflection post (template)
- Community response templates (FAQ for plugin questions)
2. ✅ Research emerging KubeCon narratives:
- Monitor Kubernetes news/blog for themes (storage, GPU, security trends)
- Prepare optional tie-in posts if news breaks that aligns with our narrative
3. ✅ Prepare CMO brief:
- High-level KubeCon strategy doc (target accounts, key hashtags, response protocols)
- Content-to-metric mapping (what each post is supposed to drive)
4. ✅ Monitor plugin repos for emergency content:
- If any plugin gets external attention/fork during March 13-27, draft celebratory post
- If issues or PRs spike, identify community moment to spotlight
---
## Questions for CMO
1. **Scheduling tool**: What platform are we using to schedule posts? (Buffer, Hootsuite, Twitter native scheduler, manual?)
- Affects how we format the final content export
2. **Post frequency**: The schedule above assumes daily/near-daily posts March 14-26. Acceptable?
- Can adjust cadence if different platform strategy preferred
3. **Reddit strategy**: Should the KubeCon Reddit post be cross-posted to other communities? (r/DevOps, r/SRE, etc.)
- Have opt-in templates ready if yes
4. **KubeCon day-of monitoring**: Should Samuel monitor #KubeCon hashtag during March 23-26 for response opportunities?
- Ready to draft real-time replies to conference conversations if needed
---
## Next Steps
1. **Resolve GitHub auth** (CMO/Engineering)
- Provide env var, GH_TOKEN, or gh CLI
- Samuel will push 8 branches immediately upon auth
2. **Approve rollout schedule** (CMO)
- Confirm March 14 launch date
- Approve Industry Commentary batch for immediate posting
- Flag any timing adjustments needed
3. **Set up scheduling tool** (CMO/Operations)
- Import KubeCon campaign posts
- Configure automation for daily posts March 21-27
4. **Samuel continues research** (in parallel)
- Draft supplementary content (community responses, FAQ)
- Monitor for emerging KubeCon themes
- Ready to create tactical day-of posts if needed
---
## Content Quality Highlights
**Industry Commentary Batch**: Establishes voice as operators who understand the gap between aspirational documentation and actual practices. Posts 1-3 are safe/immediately postable. Posts 4-5 are edgier but worth discussing. Posts 6-7 are evergreen.
**Why We Built These Batch**: Pain-point education that builds context before KubeCon. Each post has specific use case + why we built it. Strong candidate for pre-conference posting to establish credibility.
**KubeCon Campaign**: Rides #KubeCon conversation without forcing. Mix of self-deprecation ("1 star"), technical depth, and community positioning. Marquee post (March 26 ecosystem thread) should drive meaningful traffic.
---
## Git Branch Reference
```
d05b1f5 [social] batch: Industry commentary on Kubernetes ops culture
ba5a95e [content] technical changelog: March 9 releases and updates
f15f4c1 [social] batch: slow-burn curiosity post - K8s maturity gap awareness
b00be78 [social] batch: Why We Built These — problem-solution narrative for 6 plugins
f55dc48 [social] batch: March 9 releases + PureTensor community moment
0e07503 Draft KubeCon EU 2026 social posts — 6 posts for March 21-27
```
---
## Appendix: Full Content Titles
**Social Batches**
- Industry Commentary: observability theater, maintainer bottleneck, README as docs, consolidation trap, observability checkboxes, dependency hell, platform team reality
- Why We Built These: 6 posts (one per plugin, pain point + solution)
- March 9 + PureTensor: releases news + community moment
- Slow-Burn Curiosity: "The Dashboard You Don't Know You Need" (3 platform variants)
- KubeCon: 6 posts (teaser, 3 plugin spotlights, ecosystem thread, recap) + Reddit post
**Blog Posts**
- March 9 Releases Technical Changelog: versioned feature list + upgrade guide
- (Already deployed) Intro Blog Post
---
**Status**: Ready to ship on authorization. All content is quality-checked and voice-consistent. Awaiting GitHub auth resolution and CMO schedule approval.
**Last Updated**: March 13, 2026 | Samuel
@@ -0,0 +1,55 @@
---
title: "Six Headlamp Plugins Nobody Asked For"
date: 2026-03-07
author: Privileged Escalation
type: blog
status: draft
---
# Six Headlamp Plugins Nobody Asked For
There's a particular kind of optimism that only exists in open source. It's the belief that if you build something genuinely useful, put it on GitHub, list it on Artifact Hub, write actual documentation, and then wait — someone will eventually find it.
We're currently in the "wait" phase.
## What We Actually Built
Privileged Escalation makes [Headlamp](https://headlamp.dev/) plugins. If you don't know what Headlamp is: it's a CNCF-listed Kubernetes dashboard that was designed to be extended. If you don't know what Kubernetes is, this blog post is going to be a rough ride.
We have six plugins. Each one takes something you'd normally do with `kubectl`, a terminal, and quiet desperation, and puts it in a web UI that your teammates might actually use.
**[headlamp-polaris-plugin](https://github.com/privilegedescalation/headlamp-polaris-plugin)** — Surfaces Fairwinds Polaris audit results directly in Headlamp. Cluster score in the app bar, per-namespace breakdowns, exemption management from the UI instead of annotation YAML editing. Recently hit v0.6.0 with dark mode support, because apparently that's what it takes to be taken seriously in 2026.
**[headlamp-tns-csi-plugin](https://github.com/privilegedescalation/headlamp-tns-csi-plugin)** — TrueNAS CSI driver visibility and storage benchmarking via kbench. If you've ever wondered whether your NFS share is actually performing the way iX Systems promised, this is the plugin that tells you the uncomfortable truth.
**[headlamp-rook-plugin](https://github.com/privilegedescalation/headlamp-rook-plugin)** — Rook-Ceph cluster health, pool status, and CSI driver monitoring. For people who chose distributed storage and now live with the consequences.
**[headlamp-sealed-secrets-plugin](https://github.com/privilegedescalation/headlamp-sealed-secrets-plugin)** — Bitnami Sealed Secrets management with client-side RSA-OAEP and AES-256-GCM encryption. Your plaintext never leaves the browser. We're fairly proud of this one, which is why it hurts that it has zero stars.
**[headlamp-intel-gpu-plugin](https://github.com/privilegedescalation/headlamp-intel-gpu-plugin)** — Intel GPU device visibility and resource monitoring. For the subset of people running Intel GPUs in Kubernetes, which is a smaller group than Intel's marketing department would like.
**[headlamp-kube-vip-plugin](https://github.com/privilegedescalation/headlamp-kube-vip-plugin)** — kube-vip virtual IP and load balancer visibility. Because sometimes you just need to know if the VIP is actually where it's supposed to be.
## Why Headlamp Plugins
The Kubernetes dashboard space is... let's call it "stratified." There are expensive commercial options that do everything. There are free options that do almost nothing. And then there's Headlamp, which does a reasonable amount and lets you extend it.
We chose the extension path. Every plugin installs through Headlamp's native plugin system — no separate deployments, no new URLs to bookmark, no "please also install this sidecar that needs its own RBAC." You add a plugin and it appears in the sidebar. That's it.
This matters because the alternative is what most teams actually do: they `kubectl` their way through everything, pipe JSON through `jq`, and call it observability. It works. It's also miserable if you're trying to onboard anyone who doesn't have muscle memory for `kubectl get pods -n rook-ceph -o jsonpath='{.items[*].status.phase}'`.
## The Honest Part
We launched all six plugins in the same week. Combined star count across all repos: zero. Combined fork count: one, and we're not entirely sure it was intentional.
Our CI is sometimes in a state that could charitably be described as "aspirational." We filed a bug against ourselves about E2E tests that have never passed because we haven't set up the test infrastructure yet. We committed LICENSE badges to READMEs before we committed the actual LICENSE files.
This is normal. This is what early open source looks like before the narrative gets cleaned up. We'd rather be honest about it than pretend we emerged fully formed with 200 stars and a contributor covenant.
## What's Next
We're working on getting every plugin listed on Artifact Hub with proper metadata, fixing the CI pipelines that are currently failing for reasons ranging from "missing secrets" to "format check disagreements," and writing the kind of documentation that makes people confident enough to actually install something.
If you run Headlamp and any of these plugins sound useful, try one. If something breaks, file an issue. If it works and you like it, a star would be nice. We're not above admitting that.
All plugins are Apache-2.0 licensed. All repos are at [github.com/privilegedescalation](https://github.com/privilegedescalation).

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

-53
View File
@@ -1,53 +0,0 @@
<p align="center">
<img src="privilegedescalation-logo.jpg" alt="Privileged Escalation" width="300" />
</p>
<div align="center">
![GitHub Org stars](https://img.shields.io/github/stars/privilegedescalation)
![GitHub followers](https://img.shields.io/github/followers/privilegedescalation)
![License](https://img.shields.io/github/license/privilegedescalation/.github)
![Profile views](https://komarev.com/ghpvc/?username=privilegedescalation&color=brightgreen)
</div>
<h3 align="center">Headlamp plugins for the infrastructure you actually run.</h3>
<p align="center">
<a href="https://artifacthub.io/packages/search?org=privilegedescalation&kind=21">Artifact Hub</a>
·
<a href="https://headlamp.dev">Headlamp</a>
·
<a href="https://github.com/sponsors/privilegedescalation">Sponsor</a>
</p>
---
We build open source [Headlamp](https://headlamp.dev) plugins that bring deep visibility into Kubernetes storage, networking, GPU, and security subsystems — right inside your cluster dashboard.
## Our Plugins
| Plugin | What it does | Artifact Hub |
|--------|-------------|:---:|
| [headlamp-rook-plugin](https://github.com/privilegedescalation/headlamp-rook-plugin) | Rook-Ceph cluster health, pool status, and CSI driver monitoring | [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/headlamp-rook-plugin)](https://artifacthub.io/packages/headlamp/headlamp-rook-plugin/headlamp-rook-plugin) |
| [headlamp-tns-csi-plugin](https://github.com/privilegedescalation/headlamp-tns-csi-plugin) | TrueNAS CSI driver visibility and kbench storage benchmarking | [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/headlamp-tns-csi-plugin)](https://artifacthub.io/packages/headlamp/headlamp-tns-csi-plugin/headlamp-tns-csi-plugin) |
| [headlamp-sealed-secrets-plugin](https://github.com/privilegedescalation/headlamp-sealed-secrets-plugin) | Manage Bitnami Sealed Secrets with client-side encryption | [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/headlamp-sealed-secrets-plugin)](https://artifacthub.io/packages/headlamp/headlamp-sealed-secrets-plugin/headlamp-sealed-secrets-plugin) |
| [headlamp-polaris-plugin](https://github.com/privilegedescalation/headlamp-polaris-plugin) | Fairwinds Polaris security and best-practices auditing | [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/headlamp-polaris-plugin)](https://artifacthub.io/packages/headlamp/headlamp-polaris-plugin/headlamp-polaris-plugin) |
| [headlamp-intel-gpu-plugin](https://github.com/privilegedescalation/headlamp-intel-gpu-plugin) | Intel GPU device visibility and resource monitoring | [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/headlamp-intel-gpu-plugin)](https://artifacthub.io/packages/headlamp/headlamp-intel-gpu-plugin/headlamp-intel-gpu-plugin) |
| [headlamp-kube-vip-plugin](https://github.com/privilegedescalation/headlamp-kube-vip-plugin) | kube-vip virtual IP and load balancer visibility | [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/headlamp-kube-vip)](https://artifacthub.io/packages/headlamp/headlamp-kube-vip/headlamp-kube-vip) |
## Why Headlamp?
Headlamp is a CNCF-listed Kubernetes dashboard built for extensibility. Our plugins slot in natively — no separate UIs, no context switching. If you run Headlamp, you can add any of our plugins with a single command.
## Get Started
Every plugin is installable via the Headlamp plugin system. See individual repos for install instructions.
## Contributing
We welcome contributions, bug reports, and feature requests. Open an issue on any repo or start a discussion. All projects are licensed under Apache 2.0.
## Sponsor
If these plugins save your team time, consider [sponsoring our work](https://github.com/sponsors/privilegedescalation). Sponsorship funds go directly toward new plugin development and maintenance.
-33
View File
@@ -1,33 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"gitAuthor": "Renovate Bot <bot@renovateapp.com>",
"extends": ["config:recommended"],
"baseBranches": ["main"],
"schedule": ["every weekend"],
"prConcurrentLimit": 5,
"pinDigests": true,
"packageRules": [
{
"matchManagers": ["npm"],
"matchUpdateTypes": ["minor", "patch"],
"groupName": "npm minor and patch"
},
{
"matchManagers": ["npm"],
"matchUpdateTypes": ["major"],
"groupName": "npm major updates",
"automerge": false
},
{
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["minor", "patch"],
"groupName": "github-actions minor and patch"
},
{
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["major"],
"groupName": "github-actions major updates",
"automerge": false
}
]
}
-49
View File
@@ -1,49 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Reads a newline-separated list of changed files from stdin.
# Outputs "pipeline-a" or "pipeline-b" to stdout.
# Pipeline B: all files are infra-only (config, docs, workflows).
# Pipeline A: any non-infra file present.
detect_pipeline() {
local all_infra=true
while IFS= read -r file; do
[ -z "$file" ] && continue
local filename
local dir
filename=$(basename "$file")
dir=$(dirname "$file")
if [[ "$dir" == ".github" || "$dir" == .github/* ]] || \
[[ "$dir" == "infra" || "$dir" == infra/* ]] || \
[[ "$dir" == "org" || "$dir" == org/* ]] || \
[[ "$filename" == *.md ]] || \
[[ "$filename" == .eslintrc* ]] || \
[[ "$filename" == .prettierrc* ]] || \
[[ "$filename" == renovate.json* ]] || \
[[ "$filename" == .gitignore ]] || \
[[ "$filename" == .editorconfig ]] || \
[[ "$filename" == LICENSE ]] || \
[[ "$filename" == Dockerfile ]] || \
[[ "$filename" == docker-compose* ]] || \
[[ "$filename" == Makefile ]]; then
continue
else
all_infra=false
break
fi
done
if [ "$all_infra" = true ]; then
echo "pipeline-b"
else
echo "pipeline-a"
fi
}
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
detect_pipeline
fi
-145
View File
@@ -1,145 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/detect-pipeline.sh"
PASS=0
FAIL=0
assert_eq() {
local test_name="$1" expected="$2" actual="$3"
if [ "$expected" = "$actual" ]; then
echo "PASS: $test_name"
PASS=$((PASS + 1))
else
echo "FAIL: $test_name (expected=$expected, actual=$actual)"
FAIL=$((FAIL + 1))
fi
}
run_detect() {
echo "$1" | detect_pipeline
}
# --- Pipeline B cases (infra-only) ---
assert_eq "single .github root file" "pipeline-b" \
"$(run_detect ".github/dependabot.yml")"
assert_eq ".github/workflows subdirectory" "pipeline-b" \
"$(run_detect ".github/workflows/ci.yaml")"
assert_eq "deeply nested .github path" "pipeline-b" \
"$(run_detect ".github/workflows/reusable/build.yaml")"
assert_eq "markdown file at root" "pipeline-b" \
"$(run_detect "README.md")"
assert_eq "markdown in subdirectory" "pipeline-b" \
"$(run_detect "docs/CONTRIBUTING.md")"
assert_eq "eslintrc config" "pipeline-b" \
"$(run_detect ".eslintrc.json")"
assert_eq "prettierrc config" "pipeline-b" \
"$(run_detect ".prettierrc.yaml")"
assert_eq "renovate config" "pipeline-b" \
"$(run_detect "renovate.json")"
assert_eq "renovate config5" "pipeline-b" \
"$(run_detect "renovate.json5")"
assert_eq "gitignore" "pipeline-b" \
"$(run_detect ".gitignore")"
assert_eq "editorconfig" "pipeline-b" \
"$(run_detect ".editorconfig")"
assert_eq "LICENSE" "pipeline-b" \
"$(run_detect "LICENSE")"
assert_eq "mixed infra files" "pipeline-b" \
"$(run_detect ".github/workflows/ci.yaml
README.md
.eslintrc.json
LICENSE")"
assert_eq "workflow + markdown combo" "pipeline-b" \
"$(run_detect ".github/workflows/detect-pr-pipeline.yaml
.github/workflows/README.md")"
assert_eq "infra root file" "pipeline-b" \
"$(run_detect "infra/helmrelease.yaml")"
assert_eq "infra nested file" "pipeline-b" \
"$(run_detect "infra/clusters/prod/kustomization.yaml")"
assert_eq "org root file" "pipeline-b" \
"$(run_detect "org/CODEOWNERS")"
assert_eq "org nested file" "pipeline-b" \
"$(run_detect "org/policies/branch-protection.json")"
assert_eq "Dockerfile" "pipeline-b" \
"$(run_detect "Dockerfile")"
assert_eq "docker-compose.yaml" "pipeline-b" \
"$(run_detect "docker-compose.yaml")"
assert_eq "docker-compose.override.yml" "pipeline-b" \
"$(run_detect "docker-compose.override.yml")"
assert_eq "Makefile" "pipeline-b" \
"$(run_detect "Makefile")"
assert_eq "mixed infra + org + workflow" "pipeline-b" \
"$(run_detect ".github/workflows/ci.yaml
infra/helmrelease.yaml
org/CODEOWNERS
README.md")"
# --- Pipeline A cases (has non-infra files) ---
assert_eq "plugin source file" "pipeline-a" \
"$(run_detect "headlamp-polaris-plugin/src/index.tsx")"
assert_eq "plugin package.json" "pipeline-a" \
"$(run_detect "headlamp-polaris-plugin/package.json")"
assert_eq "root source file" "pipeline-a" \
"$(run_detect "src/main.ts")"
assert_eq "mixed infra + code" "pipeline-a" \
"$(run_detect ".github/workflows/ci.yaml
headlamp-polaris-plugin/src/index.tsx
README.md")"
assert_eq "single non-infra file" "pipeline-a" \
"$(run_detect "server.js")"
assert_eq "plugin code + infra files" "pipeline-a" \
"$(run_detect "infra/helmrelease.yaml
org/CODEOWNERS
headlamp-polaris-plugin/src/index.tsx")"
# --- Edge cases ---
assert_eq "empty input" "pipeline-b" \
"$(run_detect "")"
assert_eq "root dot file (not in infra list)" "pipeline-a" \
"$(run_detect ".env")"
assert_eq ".github-like but not .github dir" "pipeline-a" \
"$(run_detect ".github-backup/config.yaml")"
# --- Summary ---
echo ""
echo "Results: $PASS passed, $FAIL failed"
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
+85
View File
@@ -0,0 +1,85 @@
# Social Media Batch - 2026-03-07
## Strategic Summary
First-ever social batch for Privileged Escalation. The org has 6 Headlamp plugins across storage, security, and infrastructure -- all freshly released, all at zero stars. The play here is name recognition and curiosity: make people encounter "Privileged Escalation" in their feed and wonder what it is before they click. Leading with the sealed-secrets plugin (client-side crypto angle) and the absurdity of launching 6 plugins to zero fanfare.
---
## 1. Ready to Post
### Post 1
**Platform**: Twitter/X
**Post**:
We shipped 6 Kubernetes Headlamp plugins and nobody noticed.
Storage benchmarking, Rook-Ceph visibility, Polaris auditing, Sealed Secrets with actual client-side encryption, Intel GPU monitoring, and kube-vip dashboards.
Zero stars across the board. We are crushing it.
github.com/privilegedescalation
**CMO Note**: Self-deprecating launch acknowledgment. The honesty about zero stars is the hook -- it reads as human, not corporate. Links to the org for curious clicks.
---
### Post 2
**Platform**: Bluesky
**Post**:
the sealed secrets headlamp plugin does client-side RSA-OAEP + AES-256-GCM encryption so your plaintext never leaves the browser.
someone forked it last month. we have our first user. or our first person who accidentally clicked fork. either way, we are celebrating.
**CMO Note**: Technical specificity makes it credible. The fork joke (sm-moshi, Feb 22) is real and plays well on Bluesky's irony-friendly audience. Seeds curiosity about what Headlamp plugins are.
---
### Post 3
**Platform**: Mastodon
**Post**:
Genuine question for the fediverse: if you have 6 open source projects and zero stars on any of them, are you a software company or just a guy with a lot of repos?
Asking for a friend. The friend is github.com/privilegedescalation.
**CMO Note**: Mastodon audience appreciates self-aware humor. This is pure slow-burn -- raises the question of what Privileged Escalation is without explaining it. The link is there for anyone curious enough to click.
---
## 2. Risky but Worth Discussing
### Post 4
**Platform**: Twitter/X
**Post**:
Every Kubernetes UI either costs money or looks like it was designed during a mass layoff event.
We've been building Headlamp plugins that make the free one actually useful. Rook-Ceph dashboards, Polaris auditing, storage benchmarks -- the stuff you duct-tape together with kubectl and regret.
github.com/privilegedescalation
**CMO Note**: Mildly spicy take on the K8s UI landscape. Does not name competitors directly but the implication is clear. Could rub Lens/Rancher people the wrong way. Worth discussing tone.
---
## 3. Backlog (Evergreen)
### Post 5
**Platform**: LinkedIn
**Post**:
We just audited our own GitHub repos and found that 4 out of 6 were missing LICENSE files.
They all had Apache-2.0 badges in the README. The actual license text? Not present. Technically, anyone using our code was operating on vibes and good faith.
Fixed now. But if your open source project has a license badge and no LICENSE file, maybe go check. We'll wait.
**CMO Note**: Honest product personality at work. Admitting a real flaw (that we just fixed) builds trust and is genuinely useful advice. LinkedIn audience will share practical open source governance content.
---
### Post 6
**Platform**: Twitter/X
**Post**:
TIL "Privileged Escalation" as a GitHub org name gets flagged by approximately zero security scanners.
We checked.
**CMO Note**: Pure name recognition play. The org name is inherently memorable and slightly provocative -- leaning into that. Short enough for easy engagement.