Merge pull request #174 from privilegedescalation/hugh/pr-pipeline-detection

feat: add PR pipeline type detection workflow
This commit is contained in:
2026-05-11 15:51:17 -07:00
committed by GitHub
4 changed files with 343 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
# 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 }}`
+65
View File
@@ -0,0 +1,65 @@
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: runners-privilegedescalation
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: runners-privilegedescalation
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}\"]}"
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
# Reads a newline-separated list of changed files from stdin.
# Outputs "pipeline-a" or "pipeline-b" to stdout.
# Pipeline B: all files are infra-only (config, docs, workflows).
# Pipeline A: any non-infra file present.
detect_pipeline() {
local all_infra=true
while IFS= read -r file; do
[ -z "$file" ] && continue
local filename
local dir
filename=$(basename "$file")
dir=$(dirname "$file")
if [[ "$dir" == ".github" || "$dir" == .github/* ]] || \
[[ "$dir" == "infra" || "$dir" == infra/* ]] || \
[[ "$dir" == "org" || "$dir" == org/* ]] || \
[[ "$filename" == *.md ]] || \
[[ "$filename" == .eslintrc* ]] || \
[[ "$filename" == .prettierrc* ]] || \
[[ "$filename" == renovate.json* ]] || \
[[ "$filename" == .gitignore ]] || \
[[ "$filename" == .editorconfig ]] || \
[[ "$filename" == LICENSE ]] || \
[[ "$filename" == Dockerfile ]] || \
[[ "$filename" == docker-compose* ]] || \
[[ "$filename" == Makefile ]]; then
continue
else
all_infra=false
break
fi
done
if [ "$all_infra" = true ]; then
echo "pipeline-b"
else
echo "pipeline-a"
fi
}
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
detect_pipeline
fi
+145
View File
@@ -0,0 +1,145 @@
#!/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