Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d05b1f5113 | |||
| ba5a95e938 | |||
| f15f4c1577 | |||
| ba88471869 | |||
| 7b8947332a | |||
| 1fce9cfc7a | |||
| 57a9865c18 | |||
| 7b526c83c0 | |||
| 3f34f8e1c8 |
@@ -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,22 +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: Run CI/CD health check
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
echo "::warning::GITEA_TOKEN not configured — health check may have limited data."
|
||||
fi
|
||||
./.github/scripts/ci-health-check.sh
|
||||
@@ -1,82 +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
|
||||
|
||||
env:
|
||||
HEAD_REF: ${{ github.head_ref }}
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
git clone --depth=1 "https://x-access-token:${{ secrets.GITEA_TOKEN }}@git.farh.net/${{ github.repository }}.git" .
|
||||
git fetch origin "$BASE_REF" --depth=1
|
||||
git fetch origin +refs/pull/*/head:refs/pull/*/head --depth=1
|
||||
git checkout "${{ github.sha }}"
|
||||
|
||||
- name: Run detection tests
|
||||
run: bash scripts/test-detect-pipeline.sh
|
||||
|
||||
detect-pipeline:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
env:
|
||||
HEAD_REF: ${{ github.head_ref }}
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
|
||||
outputs:
|
||||
pipeline-type: ${{ steps.detect.outputs.pipeline-type }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
git clone --depth=1 "https://x-access-token:${{ secrets.GITEA_TOKEN }}@git.farh.net/${{ github.repository }}.git" .
|
||||
git fetch origin "$BASE_REF" --depth=1
|
||||
git fetch origin +refs/pull/*/head:refs/pull/*/head --depth=1
|
||||
git checkout "${{ github.sha }}"
|
||||
|
||||
- name: Get changed files
|
||||
run: |
|
||||
mkdir -p /tmp/pr-detect
|
||||
git fetch origin "$BASE_REF" --depth=1 2>/dev/null
|
||||
git fetch origin +refs/pull/*/head:refs/pull/*/head --depth=1 2>/dev/null
|
||||
git diff --name-only "origin/$BASE_REF" HEAD > /tmp/pr-detect/changed_files.txt
|
||||
echo "Files found: $(wc -l < /tmp/pr-detect/changed_files.txt)"
|
||||
cat /tmp/pr-detect/changed_files.txt
|
||||
|
||||
- name: Detect pipeline type
|
||||
id: detect
|
||||
run: |
|
||||
pipeline=$(bash scripts/detect-pipeline.sh < /tmp/pr-detect/changed_files.txt)
|
||||
|
||||
echo "pipeline-type=$pipeline" >> $GITHUB_OUTPUT
|
||||
echo "Detected pipeline: $pipeline"
|
||||
|
||||
- name: Set PR label
|
||||
if: github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
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,52 +0,0 @@
|
||||
name: PR Validation
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
env:
|
||||
HEAD_REF: ${{ github.head_ref }}
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
run: |
|
||||
git clone --depth=1 "https://x-access-token:${{ secrets.GITEA_TOKEN }}@git.farh.net/${{ github.repository }}.git" .
|
||||
git fetch origin "$BASE_REF" --depth=1
|
||||
git fetch origin +refs/pull/*/head:refs/pull/*/head --depth=1
|
||||
git checkout "${{ github.sha }}"
|
||||
|
||||
- name: Install actionlint
|
||||
run: |
|
||||
ACTIONLINT_VERSION="1.7.7"
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
apt-get install -y wget -qq >/dev/null 2>&1 || true
|
||||
wget -qO- "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 -shellcheck="" -color .github/workflows/*.yaml
|
||||
|
||||
- name: Install shellcheck
|
||||
run: |
|
||||
SC_VERSION="v0.10.0"
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
wget -qO- "https://github.com/koalaman/shellcheck/releases/download/${SC_VERSION}/shellcheck-${SC_VERSION}.linux.x86_64.tar.xz" \
|
||||
| tar -xJ --strip-components=1 -C "$HOME/.local/bin" "shellcheck-${SC_VERSION}/shellcheck"
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- 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,27 +0,0 @@
|
||||
name: Renovate
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * 6' # Saturday 2:00 UTC — aligns with "every weekend" in renovate-config.json
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Run Renovate
|
||||
env:
|
||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||
RENOVATE_PLATFORM: gitea
|
||||
RENOVATE_ENDPOINT: https://git.farh.net
|
||||
RENOVATE_AUTODISCOVER: "true"
|
||||
LOG_LEVEL: debug
|
||||
run: |
|
||||
npx renovate \
|
||||
--token="${RENOVATE_TOKEN}" \
|
||||
--platform=gitea \
|
||||
--endpoint=https://git.farh.net \
|
||||
--configurationFile=renovate-config.json
|
||||
@@ -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.
|
||||
@@ -1 +1,3 @@
|
||||
# .github
|
||||
# Privileged Escalation
|
||||
|
||||
Org-level content, social media queue, and community responses.
|
||||
|
||||
@@ -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).
|
||||
@@ -0,0 +1,142 @@
|
||||
# Changelog: March 9 Release Roundup
|
||||
|
||||
**Posted**: March 11, 2026 | **Applies to**: Headlamp Plugins (all versions)
|
||||
|
||||
Five days after the March 4 release cycle, four more plugins shipped point releases. This post covers what changed, why it matters, and what broke along the way (spoiler: not much).
|
||||
|
||||
---
|
||||
|
||||
## The Releases
|
||||
|
||||
### Rook v0.2.7
|
||||
|
||||
**What changed**:
|
||||
- Improved OSD status visibility across distributed Ceph clusters
|
||||
- Better handling of pool rebalancing edge cases
|
||||
- Fixed a rendering bug when a cluster had >32 OSDs
|
||||
- Updated Ceph API compatibility to 0.94.x range
|
||||
|
||||
**Why it matters**:
|
||||
If you're running Ceph at any real scale, you've hit the "how many OSDs are rebalancing right now?" question. The dashboard now shows OSD-level state transitions in the UI, so you're not digging through `ceph status` output looking for the one node that's resynchronizing. The >32 OSD fix addresses the fact that someone actually had 48 OSDs and reported it in the issue.
|
||||
|
||||
**Backwards compatibility**: ✅ Full. Existing deployments see UI improvements immediately, no config changes needed.
|
||||
|
||||
---
|
||||
|
||||
### Sealed Secrets v0.2.23
|
||||
|
||||
**What changed**:
|
||||
- Updated to sealed-secrets upstream v0.27.0
|
||||
- Improved secret rotation workflow UX (showing cleartext preview warning)
|
||||
- Fixed a bug where the secret values list didn't sort properly by age
|
||||
- Added copy-to-clipboard for encrypted values (for debugging)
|
||||
|
||||
**Why it matters**:
|
||||
The main win is the upstream sync. Sealed Secrets v0.27.0 fixed a subtle bug where RSA key rotation could leave orphaned secrets. The UX improvements matter because operators rotating secrets now see a clear warning before they preview plaintext — "this is temporary for debugging," not "let me read passwords." The copy-to-clipboard thing is tiny, but debugging encrypted values at scale is annoying enough that people asked for it.
|
||||
|
||||
**Backwards compatibility**: ✅ Full. Keys from v0.2.22 work unchanged.
|
||||
|
||||
---
|
||||
|
||||
### Intel GPU v0.4.2
|
||||
|
||||
**What changed**:
|
||||
- Fixed node-level GPU memory tracking (was under-reporting available VRAM on some driver versions)
|
||||
- Added per-workload GPU utilization chart (alpha, feedback welcome)
|
||||
- Improved support for mixed-generation GPU clusters (Xe + Arc)
|
||||
- Better error messages when Intel GPU drivers are misconfigured
|
||||
|
||||
**Why it matters**:
|
||||
The memory tracking fix is the critical one. If your scheduler wasn't placing workloads on GPU nodes, there's a decent chance your available VRAM metric was wrong and workloads were failing silently. The per-workload chart is alpha because we're still figuring out what's useful here (people want different things: power draw vs. FLOP utilization vs. memory pressure). The error messaging helps catch GPU driver issues at the dashboard level instead of as Kubernetes scheduler logs you won't read.
|
||||
|
||||
**Backwards compatibility**: ✅ Full. Existing dashboards update silently. The new chart is opt-in via the plugin settings.
|
||||
|
||||
---
|
||||
|
||||
### TrueNAS CSI v0.2.6
|
||||
|
||||
**What changed**:
|
||||
- Fixed a race condition in the storage pool detail view under high-frequency metric updates
|
||||
- Added historical IOPS trend (last 7 days, if your monitoring retention supports it)
|
||||
- Improved error handling when the CSI driver's API is temporarily unavailable
|
||||
- Updated deployment docs for TrueNAS SCALE 24.04
|
||||
|
||||
**Why it matters**:
|
||||
The race condition was rare but nasty—under sustained I/O load, the pool view would flicker or show stale metrics. This is now fixed. The 7-day trend is useful for "is my throughput degrading" analysis without requiring external tools. The API timeout handling means if your TrueNAS box restarts, the dashboard degrades gracefully instead of erroring.
|
||||
|
||||
**Backwards compatibility**: ✅ Full. Config unchanged, UX improvements automatic.
|
||||
|
||||
---
|
||||
|
||||
## What Wasn't Shipped
|
||||
|
||||
A few things were on the table but didn't make March 9:
|
||||
|
||||
- **Kube-vip v0.2.0** (major refactor) — held for more testing, targeting early April
|
||||
- **Polaris v0.7.0** (policy templates) — still in review, no timeline yet
|
||||
- **Multi-cluster federation** (experimental feature) — code is there, docs aren't, holding until docs are done right
|
||||
|
||||
These things will ship when they're done, not before.
|
||||
|
||||
---
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
None. All four plugins maintain backwards compatibility.
|
||||
|
||||
---
|
||||
|
||||
## How to Upgrade
|
||||
|
||||
For each plugin, in your Headlamp installation:
|
||||
|
||||
```bash
|
||||
# 1. Check your current version
|
||||
helm list -n headlamp | grep plugin-name
|
||||
|
||||
# 2. Update the plugin
|
||||
helm upgrade plugin-name headlamp/plugin-name \
|
||||
--repo https://artifacthub.io/packages/helm \
|
||||
--namespace headlamp
|
||||
|
||||
# 3. Verify
|
||||
kubectl rollout status deployment/headlamp-plugin-name -n headlamp
|
||||
```
|
||||
|
||||
If you're not using Helm, download the latest manifest from each plugin's GitHub release page.
|
||||
|
||||
---
|
||||
|
||||
## Known Issues
|
||||
|
||||
**TrueNAS CSI**: If your CSI driver is on an older API version (pre-24.02), some metrics may not appear. This is logged as a warning. Upgrade the driver if you need the features.
|
||||
|
||||
**Intel GPU**: Multi-node GPU scheduling still requires manual node labeling. A future release will handle label discovery automatically.
|
||||
|
||||
**Rook**: The OSD visualization can take 30 seconds to update on first load in very large clusters (>64 OSDs). We know. We're working on it.
|
||||
|
||||
---
|
||||
|
||||
## What's Next
|
||||
|
||||
- April 9: Kube-vip v0.2.0 (major refactor)
|
||||
- Ongoing: Polaris v0.7.0 (no date yet, serious scope)
|
||||
- Ongoing: Community feedback on Intel GPU utilization charts (please file issues if the metrics aren't useful)
|
||||
|
||||
---
|
||||
|
||||
## Feedback
|
||||
|
||||
Found a bug? File an issue in the relevant repo:
|
||||
- [github.com/privilegedescalation/headlamp-rook-plugin](https://github.com/privilegedescalation/headlamp-rook-plugin)
|
||||
- [github.com/privilegedescalation/headlamp-sealed-secrets-plugin](https://github.com/privilegedescalation/headlamp-sealed-secrets-plugin)
|
||||
- [github.com/privilegedescalation/headlamp-intel-gpu-plugin](https://github.com/privilegedescalation/headlamp-intel-gpu-plugin)
|
||||
- [github.com/privilegedescalation/headlamp-tns-csi-plugin](https://github.com/privilegedescalation/headlamp-tns-csi-plugin)
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
|
||||
Thanks to everyone who reported issues between March 4 and March 9. You're the reason these releases matter.
|
||||
|
||||
Special shout-out to @puretensor for running these plugins in production and telling us what actually breaks.
|
||||
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
@@ -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.
|
||||
@@ -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,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.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Slow Burn Post - 2026-03-11
|
||||
|
||||
## Strategic Summary
|
||||
|
||||
Plant a question that makes people curious about Kubernetes observability and operational maturity without revealing the answer. The goal is to get people wondering "what's the right way to do this" before they know what Headlamp is. Works as a callback to the "Why We Built These" educational batch posted days earlier, reminding operators why those pain points matter.
|
||||
|
||||
---
|
||||
|
||||
## 1. Ready to Post
|
||||
|
||||
### Post: "The Dashboard You Don't Know You Need"
|
||||
|
||||
**Platform**: Twitter/X
|
||||
|
||||
**Post**:
|
||||
|
||||
Every mature Kubernetes environment has a moment: someone asks a question about the cluster, and everyone agrees it's a good question, and nobody knows how to answer it quickly.
|
||||
|
||||
Usually because the answer lives in four different CLI tools, three different dashboards, and someone's grep history.
|
||||
|
||||
**CMO Note**: This post plants the seed that there's a maturity gap: most K8s teams have experienced the "good question, no quick answer" moment. It doesn't pitch a solution; it just names the problem that serious operators have. Works well 1-2 weeks after the "Why We Built These" batch posts as a cooling period reminder. Tone is acknowledgment + dry acceptance, not mocking.
|
||||
|
||||
---
|
||||
|
||||
### Post: Bluesky Variant
|
||||
|
||||
**Platform**: Bluesky
|
||||
|
||||
**Post**:
|
||||
|
||||
You have a really good question about your cluster. Everyone agrees it's good. Nobody can answer it in under 2 minutes without splitting a terminal into four panes and running grep until they find it.
|
||||
|
||||
That's not a flaw in your question. That's a flaw in how K8s visibility tools are designed.
|
||||
|
||||
**CMO Note**: Slightly longer and more conversational for Bluesky's format. Same core message—empathy + problem naming—but with more room to breathe. This version gets slightly more pointed at the tooling itself.
|
||||
|
||||
---
|
||||
|
||||
### Post: Mastodon Variant
|
||||
|
||||
**Platform**: Mastodon
|
||||
|
||||
**Post**:
|
||||
|
||||
The hardest part of Kubernetes maturity isn't resource management or networking or pod placement. It's the moment when you realize you have observability *data* everywhere, but visibility nowhere — and combining them requires knowing which three tools to juggle.
|
||||
|
||||
There's a better way to design this.
|
||||
|
||||
**CMO Note**: Most technical framing, appeals to operators who think about K8s maturity as a journey. "Data vs. visibility" distinction is specific enough to resonate. Concluding line is subtle: suggests a different approach without naming the product.
|
||||
|
||||
---
|
||||
|
||||
## 2. Risky but Worth Discussing
|
||||
|
||||
None for this batch — slow-burn posts are inherently low-risk since they're empathy-first, not pitch-first.
|
||||
|
||||
---
|
||||
|
||||
## 3. Backlog (Evergreen)
|
||||
|
||||
This post works evergreen (true anytime after the "Why We Built" batch has gone out) and can be part of a longer narrative arc.
|
||||
|
||||
Suggested follow-up posts (future):
|
||||
- "3 ways teams solve this today" — comparative without favoring ours
|
||||
- "Observation vs. visibility: what's the difference?" — educational explainer
|
||||
- "What does the right K8s dashboard look like?" — leading question that sets up product reveal
|
||||
|
||||
---
|
||||
|
||||
## Why This Works
|
||||
|
||||
1. **No product mention** — Pure empathy for the operator experience
|
||||
2. **Specific frustration** — Not "we make things better," but "this common moment sucks"
|
||||
3. **Opens door for follow-up** — The next post can be "here's one solution," but this one just raises the question
|
||||
4. **Sets up narrative arc** — "Why We Built These" explains pain points; this post reminds people why those pain points matter
|
||||
5. **Conversational tone** — Not a complaint, not a pitch, just "yeah, that moment is real"
|
||||
|
||||
---
|
||||
|
||||
## Timing Notes
|
||||
|
||||
- Post 1-2 weeks after "Why We Built These" batch (suggest March 18-25 timeframe)
|
||||
- Evergreen content that can be scheduled anytime
|
||||
- Ideal as part of the slow-burn curiosity campaign (PR #15) before the KubeCon push
|
||||
@@ -0,0 +1,170 @@
|
||||
# Social Media Batch — Industry Commentary on Kubernetes Operations Culture
|
||||
|
||||
## Strategic Summary
|
||||
|
||||
Hot takes on the absurdities, failures, and genuine problems in how teams actually operate Kubernetes. Not product pitches. Not tutorials. Just observations about the gap between "Kubernetes best practices" and "what we're doing at 2am when the cluster's on fire." These posts position Privileged Escalation as operators who understand the actual pain, not consultants selling the dream.
|
||||
|
||||
Timing: Evergreen content, works anytime. Some posts pair well with technical releases; others stand alone. This batch establishes credibility and voice before KubeCon campaign.
|
||||
|
||||
---
|
||||
|
||||
## 1. Ready to Post
|
||||
|
||||
### Post 1: The Observability Checklist Theater
|
||||
|
||||
**Platform**: Twitter/X
|
||||
|
||||
**Post**:
|
||||
"We have observability," she said, pointing to three separate dashboards, four different APIs, and a grep script she's afraid to touch.
|
||||
|
||||
Observability isn't having data. It's knowing what to do with it when you're 30 seconds into an incident.
|
||||
|
||||
Most Kubernetes deployments have the first part. Nobody has the second.
|
||||
|
||||
**CMO Note**: Identifies a real gap (data vs visibility vs actionability) without mocking the teams experiencing it. Dry acknowledgment of a universal pain point. Sets up future posts about "what good observability looks like." No product mention needed — the empathy does the work.
|
||||
|
||||
---
|
||||
|
||||
### Post 2: The 3-Line PR That Ate 6 Weeks
|
||||
|
||||
**Platform**: Bluesky
|
||||
|
||||
**Post**:
|
||||
Your platform team has a 3-line PR waiting for maintainer approval. It's been 42 days.
|
||||
|
||||
Not because the code is bad. Not because they're thinking about it. They're just... busy.
|
||||
|
||||
This is why every infrastructure team ends up maintaining their own forks of things. Not because they wanted to. Because the alternative was waiting forever.
|
||||
|
||||
**CMO Note**: Speaks directly to the maintainer bottleneck that creates fragmentation in the ecosystem. Bluesky audience (more irony-literate) appreciates the "42 days" specificity. The "own forks" insight is a callback to our sealed-secrets fork acknowledgment from earlier batches.
|
||||
|
||||
---
|
||||
|
||||
### Post 3: The README That Became the Docs
|
||||
|
||||
**Platform**: Mastodon
|
||||
|
||||
**Post**:
|
||||
Kubernetes documentation is a weird thing. The official docs are great. The best practices docs are aspirational. The actual documentation for how to run your thing is 40 lines in a README written by someone at 11pm because they're shipping in the morning.
|
||||
|
||||
Three years later, that README is the only source of truth. It hasn't been updated in two years. Someone just hired reads it and wonders why nothing matches.
|
||||
|
||||
This is fine.
|
||||
|
||||
**CMO Note**: Honest observation about documentation drift and pragmatism in ops. "This is fine" ending is dark humor that Mastodon audience gets. Not judgmental, just realistic. Positions us as people who understand that perfect documentation is a fantasy.
|
||||
|
||||
---
|
||||
|
||||
## 2. Risky but Worth Discussing
|
||||
|
||||
### Post 4: The Consolidation Trap (Skip for Safety)
|
||||
|
||||
**Platform**: Twitter/X
|
||||
|
||||
**Post**:
|
||||
Every infrastructure company eventually ships one tool that tries to do everything.
|
||||
|
||||
It is always bloated. It is always slow. It is always worse at specific things than the 6 single-purpose tools it replaced.
|
||||
|
||||
We had the opportunity to do this. We didn't. We're weird that way.
|
||||
|
||||
**CMO Note**: RISKY. This is a soft dig at competitors (Lens, Rancher, etc.) and might read as salty if not careful. However, it's grounded in our actual decision (6 plugins instead of one). Could land well with operators who are exhausted by consolidation fantasies. Recommend getting CMO sign-off before posting. If approved, schedule it after "Why We Built These" batch so context is fresh.
|
||||
|
||||
---
|
||||
|
||||
### Post 5: Observability Theater: The Security Checkbox
|
||||
|
||||
**Platform**: Bluesky
|
||||
|
||||
**Post**:
|
||||
"We have visibility into our supply chain."
|
||||
|
||||
Translation: We run a scanner once a quarter and it generates a report nobody reads.
|
||||
|
||||
"We monitor resource usage."
|
||||
|
||||
Translation: Prometheus metrics exist. We haven't looked at them in a year but they're technically there.
|
||||
|
||||
Real observability isn't a checkbox. It's a practice. It takes work. Most teams don't do it.
|
||||
|
||||
**CMO Note**: MILDLY RISKY. Could read as too critical of teams trying their best. However, it's honest about the gap between aspirational and actual practices. Strong with engineering leaders who are frustrated with their own observability theater. Recommend pairing with a constructive follow-up post about "what real visibility looks like."
|
||||
|
||||
---
|
||||
|
||||
## 3. Backlog (Evergreen, Lower Urgency)
|
||||
|
||||
### Post 6: The Dependency Management Hellscape
|
||||
|
||||
**Platform**: Twitter/X
|
||||
|
||||
**Post**:
|
||||
You have 1,247 transitive dependencies in your Kubernetes cluster.
|
||||
|
||||
You know what 14 of them do.
|
||||
|
||||
Nobody knows who maintains the other 1,233. Nobody knows what version of OpenSSL they actually use. If one of them breaks, you have a 2-week-long blame game ahead of you.
|
||||
|
||||
This is the cloud native era.
|
||||
|
||||
**CMO Note**: BACKLOG. Evergreen tech-industry grumbling. Node_modules meme energy. Good for reaching people who are deep in dependency hell and looking for commiseration. Can post anytime without losing relevance. Low risk, high relatability.
|
||||
|
||||
---
|
||||
|
||||
### Post 7: The Platform Team as Glorified Operators
|
||||
|
||||
**Platform**: LinkedIn
|
||||
|
||||
**Post**:
|
||||
The job listing said "Platform Engineer."
|
||||
|
||||
What they meant: "You will spend 60% of your time un-breaking things that broke automatically, 30% fighting with vendors, and 10% actually building the platform you were hired to build."
|
||||
|
||||
Platform engineering is good work. But we're not honest about what it is yet. It's operations wearing a different hat.
|
||||
|
||||
**CMO Note**: BACKLOG. Professional tone for LinkedIn. Speaks to platform engineering leaders who are exhausted. The honesty about role mismatch will resonate with your actual audience. Low controversy, high empathy. Works anytime. Could be paired with recruitment/community posts about "what good platform engineering support looks like."
|
||||
|
||||
---
|
||||
|
||||
## Post Selection Recommendation
|
||||
|
||||
**For This Week** (Pre-KubeCon): Posts 1, 2, 3
|
||||
- Establish credibility as operators who get it
|
||||
- Set tone before educational batches ("Why We Built These")
|
||||
- Build audience affinity
|
||||
|
||||
**For Next Week** (During KubeCon): Hold — focus on KubeCon campaign posts
|
||||
|
||||
**For Later** (March 28+): Posts 4, 5, 6, 7
|
||||
- Post 4 only if CMO approves and timing feels right
|
||||
- Posts 5, 6, 7 are genuinely evergreen — use as filler/buffer content
|
||||
|
||||
---
|
||||
|
||||
## Voice Check
|
||||
|
||||
✅ Dry observations, not punchlines
|
||||
✅ Mild grievances, not venting
|
||||
✅ Credibility through specificity ("42 days," "1,247 dependencies," "11pm")
|
||||
✅ Empathy for teams, not mockery
|
||||
✅ Operator perspective (not vendor, not consultant)
|
||||
✅ No corporate language, no "exciting to announce"
|
||||
✅ Each post stands alone; no threading needed
|
||||
|
||||
---
|
||||
|
||||
## Tags & Platform Consistency
|
||||
|
||||
- Twitter/X: Short, punchy, no threads
|
||||
- Bluesky: Slightly longer, conversation-friendly
|
||||
- LinkedIn: Professional tone, slightly longer form
|
||||
- Mastodon: More technical, darker humor accepted
|
||||
|
||||
All posts include implicit "this resonates because you've lived it" angle rather than "you should agree with us."
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Posts 1-3: Self-contained, can post anytime
|
||||
- Post 4: Requires "Why We Built These" context (posted 1+ week prior)
|
||||
- Posts 5-7: Evergreen, zero dependencies
|
||||
Reference in New Issue
Block a user