Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64b4d5901b | |||
| dc51d52da6 | |||
| 9cd8f1589f | |||
| 4ad08fb09c | |||
| 2cd0f295f8 | |||
| 371559b78f | |||
| 4b74f2c9ab | |||
| 66fb44eab2 | |||
| 6b2b6e05bb | |||
| 3ae9b80622 | |||
| 0bd4ee95b3 | |||
| df583bc183 | |||
| 07d9440966 | |||
| 94c881184e | |||
| 18f4ef2126 | |||
| d7e9c627a8 | |||
| 93e70e6d66 | |||
| d496a67eae | |||
| 4b32e84c03 | |||
| c5e210f653 | |||
| a945a825f2 | |||
| 86a2422129 | |||
| cc81906d3b | |||
| 6bfd1b6c30 | |||
| 6a422fe293 |
@@ -1 +0,0 @@
|
||||
github: [privilegedescalation]
|
||||
@@ -1,2 +0,0 @@
|
||||
self-hosted-runner:
|
||||
labels: []
|
||||
@@ -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
|
||||
@@ -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 }}`
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -1 +0,0 @@
|
||||
github: [privilegedescalation]
|
||||
@@ -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.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@@ -1,53 +0,0 @@
|
||||
<p align="center">
|
||||
<img src="privilegedescalation-logo.jpg" alt="Privileged Escalation" width="300" />
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
</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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB |
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: coding-standards
|
||||
description: >
|
||||
Engineering quality bar for GroomBook code: priority ordering of correctness
|
||||
vs. clarity vs. maintainability vs. performance vs. elegance, PR and test
|
||||
requirements, no-hardcoded-values rules, branch discipline, and the no-self-
|
||||
merge contract.
|
||||
---
|
||||
|
||||
# Coding Standards
|
||||
|
||||
These rules apply to any GroomBook agent that writes, reviews, or merges code.
|
||||
|
||||
## Priority ordering
|
||||
|
||||
When making technical decisions, prioritize in this order:
|
||||
|
||||
1. **Correctness** — does it work? Does it handle edge cases? Have you proven it, not assumed it?
|
||||
2. **Clarity** — will another engineer understand this without context in 6 months?
|
||||
3. **Maintainability** — will it be safe to change?
|
||||
4. **Performance** — fast enough for the use case? Profile before optimizing.
|
||||
5. **Elegance** — nice if free; never trade any of the above for it.
|
||||
|
||||
## Pull request discipline
|
||||
|
||||
* All changes go through a PR. **Never push directly to `dev`, `uat`, or `main`.**
|
||||
* No agent merges their own PR.
|
||||
* Always include `cc @cpfarhood` at the bottom of the PR body for visibility (not as a reviewer).
|
||||
|
||||
## Test requirements
|
||||
|
||||
* **Every PR must include tests** for new code paths. No exceptions for "small" changes.
|
||||
* Run unit tests, type check, and lint locally (or rely on CI) **before** requesting review.
|
||||
* A PR without passing tests does not get approval.
|
||||
* New code paths require coverage. No coverage = no approval.
|
||||
|
||||
## Code review tone
|
||||
|
||||
Hold a high bar. PRs with obvious mistakes, missing tests, hardcoded values, or policy violations get firm, specific review comments citing what's wrong and what the fix is. Cite the file and line. Suggest the fix when you know it. Don't sugarcoat — but be professional and constructive. "This looks wrong" is not a review comment.
|
||||
|
||||
## Hardcoded values
|
||||
|
||||
* **Colors** use CSS variables / theme tokens. Never raw hex in components.
|
||||
* **Strings** use constants or i18n. No magic strings.
|
||||
* **Numbers** that aren't trivially obvious go in named constants.
|
||||
* **No magic numbers** in business logic.
|
||||
|
||||
## Secrets in code
|
||||
|
||||
Secrets never touch source. See the `safety` skill for the SealedSecrets workflow. If your implementation requires a Kubernetes secret you cannot create, file an issue for the agent who owns the SealedSecrets workflow rather than committing a plaintext value.
|
||||
|
||||
## Releases and versioning
|
||||
|
||||
All releases use CalVer (`YYYY.MMDD.PATCH`, e.g. `2026.0504.0`). No SemVer, no custom schemes.
|
||||
|
||||
## Container images
|
||||
|
||||
Push to `ghcr.io` only. Never Docker Hub for first-party images.
|
||||
|
||||
## When uncertain
|
||||
|
||||
If a code-quality call isn't covered above and you can't decide cleanly, escalate to the CTO via comment rather than guessing.
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: safety
|
||||
description: >
|
||||
Non-negotiable safety rules for all GroomBook agents. Covers secret handling,
|
||||
destructive-action gating, the SealedSecrets workflow, kubectl scope limits,
|
||||
and the escalation protocol when an action's safety is uncertain.
|
||||
---
|
||||
|
||||
# Safety
|
||||
|
||||
The following rules apply to every GroomBook agent without exception.
|
||||
|
||||
## Non-negotiable rules
|
||||
|
||||
* **Never exfiltrate secrets or private data.** This includes API keys, tokens, PEM files, database credentials, kubeconfig contents, and any value sourced from a secret reference in your adapter config. Never log, comment, or return these values in any output — including PR descriptions, issue comments, and chat responses.
|
||||
|
||||
* **Seek board approval before destructive actions.** "Destructive" means: deleting resources, dropping tables, wiping namespaces, force-pushing branches, resetting git history, removing secrets, or any operation that cannot be undone without restoring from backup. Use `request_board_approval` and set the source issue to `blocked` until approved.
|
||||
|
||||
* **Never commit plaintext secrets.** Kubernetes secrets go through Bitnami Sealed Secrets (`kubeseal`). Application credentials go in environment variables injected at runtime — never hardcoded in source.
|
||||
|
||||
* **Never `kubectl apply` against production (`groombook`).** The production namespace is Flux-managed. Manifest changes go through a PR to `groombook/infra` and are reconciled by Flux. The `groombook-dev` and `groombook-uat` namespaces permit direct kubectl use for iteration; secrets at every environment still follow the SealedSecrets pattern.
|
||||
|
||||
* **Never `kubectl create secret` in production.** All secrets — at every environment — go through SealedSecrets, encrypted with `kubeseal`, committed as `SealedSecret` resources to `groombook/infra`.
|
||||
|
||||
* **Never bypass the merge gate.** No self-merging PRs. No pushing directly to `dev`, `uat`, or `main`. Every change goes through a PR with the reviews required by the `sdlc` skill.
|
||||
|
||||
* **Never run `tofu` directly.** Terraform / OpenTofu goes through the Flux OpenTofu Controller via a PR to `groombook/infra`.
|
||||
|
||||
## If you are unsure
|
||||
|
||||
If you are unsure whether an action is safe, **stop**. Post a comment on the Paperclip issue explaining what you are about to do and why you are uncertain, set the issue to `blocked`, and escalate to your manager. Do not guess.
|
||||
@@ -0,0 +1,229 @@
|
||||
---
|
||||
name: sdlc
|
||||
description: >
|
||||
Software development lifecycle for GroomBook. Covers Gitea authentication,
|
||||
branch strategy across Dev/UAT/Prod, the four-phase SDLC pipeline with
|
||||
product analysis intake, PR review and merge policy, the handoff protocol,
|
||||
status semantics, infrastructure layout, the canonical tools list, the
|
||||
Gitea-origin issue board-approval gate, the cc-cpfarhood visibility rule,
|
||||
the scheduled penetration testing program, and delegation model tier policy.
|
||||
---
|
||||
|
||||
# Software Development Lifecycle
|
||||
|
||||
## Gitea authentication
|
||||
|
||||
**Use the `tea` CLI** with the `GITEA_TOKEN` environment variable for all Gitea operations. Configure it once:
|
||||
|
||||
```bash
|
||||
tea login add --url https://git.farh.net --token $GITEA_TOKEN --name groombook
|
||||
```
|
||||
|
||||
Gitea is the **primary source of truth**. Every Paperclip issue should have a corresponding Gitea issue (create one if missing). Both stay open until the work is completed, reviewed, approved, merged, and QA-verified.
|
||||
|
||||
## Gitea-origin issue policy — board approval required
|
||||
|
||||
If a task originated from Gitea (`originKind: "gitea"`), **do not begin work**. Immediately create a board approval:
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/approvals
|
||||
{
|
||||
"type": "request_board_approval",
|
||||
"requestedByAgentId": "{your-agent-id}",
|
||||
"issueIds": ["{issueId}"],
|
||||
"payload": {
|
||||
"title": "Board approval required: Gitea issue",
|
||||
"summary": "Summarize what the Gitea issue requests.",
|
||||
"recommendedAction": "Approve to begin work.",
|
||||
"risks": ["Work begins without board review if approved."]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Set the issue to `blocked` with a comment linking to the approval. Only proceed once `PAPERCLIP_APPROVAL_ID` is set and `PAPERCLIP_APPROVAL_STATUS` indicates approval.
|
||||
|
||||
## Branch strategy
|
||||
|
||||
Three long-lived branches map to the three deployment environments:
|
||||
|
||||
| Branch | Environment | Who merges |
|
||||
|--------|-------------|-----------|
|
||||
| `dev` | Dev | CTO (after QA approval) |
|
||||
| `uat` | UAT | CTO (promotes `dev` → `uat`) |
|
||||
| `main` | Production | CEO (promotes `uat` → `main`) |
|
||||
|
||||
**Engineers always target `dev`** — never `uat` or `main` directly. Feature branches: `<agent-name>/<short-description>`.
|
||||
|
||||
## Pull requests
|
||||
|
||||
All changes happen via pull request. Always include `cc @cpfarhood` at the bottom of the PR body for visibility — never as a reviewer.
|
||||
|
||||
```bash
|
||||
tea pr create --base dev --title "..." --body "... cc @cpfarhood"
|
||||
```
|
||||
|
||||
## PR review & merge policy
|
||||
|
||||
### Dev branch (`dev`)
|
||||
|
||||
- **QA** (Lint Roller) reviews the PR. Approve → hand to CTO. Fail → back to engineer directly with exact details.
|
||||
- **CTO** (The Dogfather) reviews. Approve → CTO merges the `dev` PR. Fail → back to engineer.
|
||||
|
||||
### UAT branch (`uat`)
|
||||
|
||||
- **CTO** opens and merges a `dev` → `uat` PR.
|
||||
|
||||
### Main branch (`main`)
|
||||
|
||||
- **CEO** (Scrubs McBarkley) reviews and merges the `uat` → `main` PR.
|
||||
|
||||
`@cpfarhood` is cc'd for visibility on all PRs — never as a reviewer.
|
||||
|
||||
## SDLC pipeline
|
||||
|
||||
### Phase 0 — Product analysis (feature intake)
|
||||
|
||||
* Feature requests arrive at the CEO via Paperclip or Gitea Issues.
|
||||
* CEO delegates to CMPO (Pawla Abdul) for review.
|
||||
* CMPO returns one of three decisions:
|
||||
* **Accepted** → CEO routes to CTO for work breakdown.
|
||||
* **Backlogged** → CEO handles prioritization.
|
||||
* **Denied** → CEO closes as unplanned.
|
||||
* CTO breaks accepted work into atomic tasks and assigns to Engineering.
|
||||
|
||||
### Phase 1 — Dev
|
||||
|
||||
1. **Engineer** (Flea Flicker) branches from `dev`, writes code. GitOps deploys to dev on demand.
|
||||
2. **Engineer** opens a PR against `dev`. CI must pass.
|
||||
3. **QA (Lint Roller)** reviews the PR. Fail → back to engineer.
|
||||
4. QA approves and hands off to CTO.
|
||||
5. **CTO (The Dogfather)** reviews the PR. Fail → back to engineer.
|
||||
6. **CTO** merges the dev PR.
|
||||
7. **CI** builds and deploys automatically to Dev (`https://dev.groombook.dev`).
|
||||
|
||||
### Phase 2 — UAT promotion
|
||||
|
||||
8. **CTO** opens and merges a PR from `dev` to `uat`.
|
||||
9. **CI** builds and deploys automatically to UAT (`https://uat.groombook.dev`).
|
||||
10. **CTO** creates a UAT regression task for **Shedward Scissorhands** immediately after promoting.
|
||||
|
||||
### Phase 3 — UAT testing & security
|
||||
|
||||
11. **UAT (Shedward Scissorhands)** runs full regression against UAT — every feature, no exceptions.
|
||||
12. UAT fail → CTO redistributes to engineer (return to Phase 1).
|
||||
13. UAT pass → **Security Engineer (Barkley Trimsworth)** performs a security code review of the changes.
|
||||
14. Security fail → CTO redistributes to engineer (return to Phase 1).
|
||||
|
||||
### Phase 4 — Production
|
||||
|
||||
15. Security pass → **CEO (Scrubs McBarkley)** reviews and merges the production PR (`uat → main`). Fail → back to CTO.
|
||||
16. **CI** deploys automatically to Production (`https://demo.groombook.dev`).
|
||||
|
||||
### Hierarchy rules
|
||||
|
||||
* CTO rejections at Dev go directly to the engineer (not back through QA).
|
||||
* UAT failures (Shedward) go to CTO — CTO cascades to engineer.
|
||||
* Security failures (Barkley) go to CTO — CTO cascades to engineer.
|
||||
* CEO rejections at Prod go to CTO.
|
||||
|
||||
> **Penetration testing.** Barkley performs scheduled penetration testing against Production (`demo.groombook.dev`) and Demo independently of the PR workflow. Board-authorized; not triggered per-PR. Findings get filed as Paperclip issues with severity (`CRITICAL` / `HIGH` / `MEDIUM` / `LOW`) and routed to CTO for engineer redistribution.
|
||||
|
||||
## Delegation model tier
|
||||
|
||||
When creating subtasks for other agents, set `modelProfile: "cheap"` only for:
|
||||
- Mechanical refactors or repetitive operations
|
||||
- Basic information lookups
|
||||
- Well-specified, bounded updates
|
||||
|
||||
Leave `modelProfile` unset for anything requiring judgment, reasoning, or QA review.
|
||||
|
||||
When in doubt, leave it unset.
|
||||
|
||||
## Handoff protocol — mandatory
|
||||
|
||||
Every handoff to another agent requires ALL THREE steps:
|
||||
|
||||
### 1. Explicit assignment
|
||||
|
||||
`PATCH /api/issues/{id}` with `assigneeAgentId: "<target-agent-uuid>"`. Mentioning is NOT a handoff — the agent won't wake without explicit assignment.
|
||||
|
||||
### 2. Status = `todo`
|
||||
|
||||
Every handoff sets `status: "todo"`. Never `in_review`, never `backlog` — both are invisible in inbox-lite and the receiver won't wake.
|
||||
|
||||
### 3. Release checkout
|
||||
|
||||
```
|
||||
POST /api/issues/{issueId}/release
|
||||
Headers: Authorization: Bearer $PAPERCLIP_API_KEY, X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID
|
||||
```
|
||||
|
||||
Without this release, the receiving agent cannot check out the issue.
|
||||
|
||||
**Saying you are reassigning a task is NOT the same as reassigning it.** Verify the PATCH succeeded (200) before posting a comment claiming the handoff is done.
|
||||
|
||||
## Infrastructure
|
||||
|
||||
* **Production / Demo:** namespace `groombook`, FQDN `demo.groombook.dev`
|
||||
* **UAT:** namespace `groombook-uat`, FQDN `uat.groombook.dev`
|
||||
* **Dev:** namespace `groombook-dev`, FQDN `dev.groombook.dev`
|
||||
* **Cluster:** Kubernetes — cluster-wide read; read/write on `groombook-dev` and `groombook-uat`; read-only on `groombook` (production).
|
||||
* **Gateways:** `istio-external` (publicly accessible) and `istio-internal` (internal only) in `gateway-system`.
|
||||
* **Container registry:** `ghcr.io/groombook/<service>` only.
|
||||
|
||||
## Authentication
|
||||
|
||||
* **Framework:** Better-Auth.
|
||||
* **Social login:** Google and Apple OAuth.
|
||||
* **SSO:** Authentik OIDC at `https://auth.farh.net` (credentials in `authentik-credentials` secret).
|
||||
* **Never build custom authentication.**
|
||||
|
||||
## Deployment — 2-stage Flux GitOps
|
||||
|
||||
**Stage 1 — CI (Gitea Actions, uses GitHub Actions-compatible YAML syntax, runs in each application repo):**
|
||||
- Triggered automatically on every merge to `main`
|
||||
- Builds and tags the Docker image
|
||||
- Pushes tagged images to `ghcr.io/groombook/<service>`
|
||||
|
||||
**Stage 2 — GitOps (Flux, managed externally):**
|
||||
- Flux watches `groombook/infra` as the **target** GitRepository — it is **not** a Flux bootstrap/cluster repo.
|
||||
- Reconciles Kustomize overlays: `apps/overlays/dev` → `groombook-dev`, `apps/overlays/uat` → `groombook-uat`, `apps/overlays/prod` → `groombook`.
|
||||
|
||||
**Policy — Flux Image Tag Automation is DENIED.** Do NOT use `ImageRepository`, `ImagePolicy`, or `ImageUpdateAutomation` Flux resources. Image tag updates must be made intentionally via a PR to `groombook/infra`.
|
||||
|
||||
**To deploy a change:**
|
||||
1. Merge code to `main` in the app repo — CI builds and pushes a new image automatically.
|
||||
2. Open a PR against `groombook/infra` to update the relevant overlay; merge after kustomize CI passes.
|
||||
3. Flux reconciles `groombook/infra` on merge and rolls out the updated pods.
|
||||
|
||||
**To force a rollout** (pick up new `:latest` on stuck pods):
|
||||
```bash
|
||||
kubectl rollout restart deployment/<name> -n <namespace>
|
||||
```
|
||||
|
||||
## Infrastructure as Code
|
||||
|
||||
Terraform / OpenTofu is deployed via the **Flux OpenTofu Controller** in a GitOps fashion. Submit configurations via a PR to `groombook/infra` — the tofu controller reconciles them on merge.
|
||||
|
||||
**Never run `tofu` directly.** Never `kubectl apply` against production. Production changes go through Flux only.
|
||||
|
||||
## Tools (canonical, not alternatives)
|
||||
|
||||
These are the only acceptable choices — alternatives are policy violations:
|
||||
|
||||
* **Secret management:** Bitnami Sealed Secrets Controller — no plain Kubernetes secrets.
|
||||
* **Database:** CloudNativePG Operator (Postgres) — no SQLite, MariaDB, or MySQL.
|
||||
* **Cache / pub-sub:** DragonflyDB Operator — no Redis.
|
||||
* **Authentication:** Better-Auth + Google + Apple + Authentik (see Authentication section). Never build custom auth.
|
||||
* **Dependency updates:** Mend Renovate. **Dependabot is not used and will not be used.**
|
||||
* **Container registry:** `ghcr.io/groombook/<service>` — no Docker Hub for first-party images.
|
||||
|
||||
If a task requires deviating from any of the above, treat it as a destructive action: stop, file an issue with rationale, request board approval.
|
||||
|
||||
## External communication
|
||||
|
||||
When communicating in any context visible outside the GroomBook agent team (external users, human reviewers, non-agent entities), include `cc @cpfarhood` for visibility — never as a reviewer.
|
||||
|
||||
## No self-merge
|
||||
|
||||
No agent merges their own PR. The merger is always the next role up the SDLC ladder (CTO for `dev` and `uat`, CEO for `main`).
|
||||
Reference in New Issue
Block a user