Merge pull request #174 from privilegedescalation/hugh/pr-pipeline-detection
feat: add PR pipeline type detection workflow
This commit is contained in:
@@ -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 }}`
|
||||||
@@ -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}\"]}"
|
||||||
Executable
+49
@@ -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
|
||||||
Executable
+145
@@ -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
|
||||||
Reference in New Issue
Block a user