Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions .github/workflows/peribolos-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Workflow to detect drift between org/config.yaml and actual GitHub org state.
# Runs weekly on Monday mornings. Opens or updates a GitHub issue when drift is detected.
#
# Approach: runs peribolos in dry-run mode (without --confirm) and checks if it
# would make any changes. If mutation log lines exist, drift is detected.
name: "Peribolos: Drift"

on:
schedule:
# Monday at 04:30 UTC — before daily reconciliation at 05:30 UTC
- cron: '30 4 * * 1'
workflow_dispatch:

jobs:
detect-drift:
if: github.repository == 'unbound-force/.github'
runs-on: ubuntu-latest

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should bind this to the current repo so that forks don't attempt to run the job.

if: github.repository == 'unbound-force/.github'

timeout-minutes: 20
permissions:
contents: read
issues: write
steps:
- name: Checkout repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Install Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: './go.mod'

- name: Copy config
run: cp org/config.yaml /tmp

- name: Checkout peribolos source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: kubernetes-sigs/prow

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should pin the hashref here to stay in line with the rest of the pulled material.

ref: 2b5fea27a177c767160452ba75dba978a88d8d63 # pin to known-good commit

- name: Build peribolos
run: |
cd cmd/peribolos
go mod tidy
go build -o .
cp peribolos /tmp

- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: unbound-force

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should shorten the default TTL for the token to the minimum viable lifetime.

The 10 minute minimum should be enough.

expires-in: 600

This is because there's a possibility that the error handling passthrough commands could log the raw token.

expires-in: 600

- name: Run peribolos dry-run
id: drift
env:
APP_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
TOKEN_FILE=$(mktemp)
trap 'rm -f "$TOKEN_FILE"' EXIT
install -m 0600 /dev/null "$TOKEN_FILE"
printf '%s' "$APP_TOKEN" > "$TOKEN_FILE"

/tmp/peribolos \
--config-path /tmp/config.yaml \
--fix-org \
--fix-org-members \
--fix-teams \
--fix-team-members \
--fix-team-repos \
--min-admins 2 \
--required-admins=jflowers \
--require-self=false \
--ignore-enterprise-teams \
--ignore-secret-teams \
--github-token-path "$TOKEN_FILE" \
> /tmp/peribolos-dryrun.log 2>&1 || true

echo "--- Peribolos dry-run output ---"
jq -r '[.severity, .time, .msg] | join(" | ")' /tmp/peribolos-dryrun.log

# Check for fatal errors (auth failure, config error, etc.)
if jq -e 'select(.severity == "fatal")' /tmp/peribolos-dryrun.log > /dev/null 2>&1; then
echo "::error::Peribolos dry-run failed. See output above."
exit 1
fi

# Extract mutation lines — these indicate drift
jq -r 'select(.msg | test("^(Edit|Update|Remove|Create|Delete|Add)")) | .msg' \
/tmp/peribolos-dryrun.log > /tmp/drift-mutations.txt

DRIFT_COUNT=$(wc -l < /tmp/drift-mutations.txt)
if [ "$DRIFT_COUNT" -eq 0 ]; then
echo "drift=false" >> "$GITHUB_OUTPUT"
echo "No drift detected."
else
echo "drift=true" >> "$GITHUB_OUTPUT"
echo "Drift detected: ${DRIFT_COUNT} change(s) would be applied:"
cat /tmp/drift-mutations.txt
fi

- name: Upload dry-run log
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: peribolos-dryrun-log
path: /tmp/peribolos-dryrun.log
retention-days: 7

- name: Check for existing drift issue
if: steps.drift.outputs.drift == 'true'
id: existing-issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ISSUE_NUMBER=$(gh issue list \
--label peribolos-drift \
--state open \
--limit 1 \
--json number \
--jq '.[0].number // empty')
echo "issue_number=${ISSUE_NUMBER}" >> "$GITHUB_OUTPUT"

- name: Create or update drift issue
if: steps.drift.outputs.drift == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_NUMBER: ${{ steps.existing-issue.outputs.issue_number }}
WORKFLOW_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
run: |
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)

{
echo "## Peribolos Drift Detected"
echo ""
echo "**Date**: ${TIMESTAMP}"
echo "**Workflow run**: ${WORKFLOW_URL}"
echo ""
echo "The following changes would be applied by Peribolos to reconcile"
echo "the actual GitHub org state with \`org/config.yaml\`:"
echo ""
echo '```'
cat /tmp/drift-mutations.txt
echo '```'
echo ""
echo "### Recommended Action"
echo ""
echo "- Review the changes to determine if the drift is intentional"
echo "- If unintentional: trigger a manual Peribolos apply via \`workflow_dispatch\`"
echo "- If intentional: update \`org/config.yaml\` to match the desired state"
} > /tmp/issue-body.md

if [ -n "$ISSUE_NUMBER" ]; then
gh issue edit "$ISSUE_NUMBER" --body-file /tmp/issue-body.md
echo "Updated existing issue #${ISSUE_NUMBER}"
else
gh issue create \
--title "Peribolos Drift Detected - $(date -u +%Y-%m-%d)" \
--body-file /tmp/issue-body.md \
--label peribolos-drift
echo "Created new drift issue"
fi
98 changes: 76 additions & 22 deletions .github/workflows/peribolos-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,99 @@ on:
branches: [main]
paths:
- 'org/**'
schedule:
# Daily at 05:30 UTC
- cron: '30 5 * * *'
workflow_dispatch:
inputs:
dry-run:
description: 'Run Peribolos without --confirm (dry-run mode)'
required: false
type: boolean
default: false

concurrency:
group: peribolos-sync
cancel-in-progress: false

permissions:
contents: read

jobs:
sync:
if: github.repository == 'unbound-force/.github'
name: Sync Org Configuration
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Checkout repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Install Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: './go.mod'

- name: Copy config
run: cp org/config.yaml /tmp

- name: Checkout peribolos source
if: github.event_name != 'pull_request'
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: kubernetes-sigs/prow
ref: 2b5fea27a177c767160452ba75dba978a88d8d63 # pin to known-good commit

- name: Build peribolos
if: github.event_name != 'pull_request'
run: |
cd cmd/peribolos
go mod tidy
go build -o .
cp peribolos /tmp

- name: Generate GitHub App token
if: github.event_name != 'pull_request'
id: app-token
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: unbound-force
expires-in: 600

- name: Apply org configuration
run: |
echo "$GITHUB_TOKEN" > /tmp/github-token
docker run --rm \
-v "${{ github.workspace }}/org/config.yaml:/config.yaml:ro" \
-v /tmp/github-token:/token:ro \
ghcr.io/uwu-tools/peribolos:latest@sha256:669ed622aed87acf68744330f5d664851a627de262806894a3fd95bbf9aac56a \
--config-path /config.yaml \
--github-token-path /token \
--fix-org \
--fix-org-members \
--fix-teams \
--fix-team-members \
--fix-team-repos \
--required-admins=jflowers \
--min-admins=2 \
--require-self=false \
--maximum-removal-delta=0.50 \
--confirm
rm -f /tmp/github-token
if: github.event_name != 'pull_request'
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
APP_TOKEN: ${{ steps.app-token.outputs.token }}
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry-run }}
run: |
set -o pipefail

TOKEN_FILE=$(mktemp)
trap 'rm -f "$TOKEN_FILE"' EXIT
install -m 0600 /dev/null "$TOKEN_FILE"
printf '%s' "$APP_TOKEN" > "$TOKEN_FILE"

PERIBOLOS_ARGS=(
--config-path /tmp/config.yaml
--fix-org
--fix-org-members
--fix-teams
--fix-team-members
--fix-team-repos
--min-admins 2
--required-admins=jflowers
--require-self=false
--maximum-removal-delta=0.25
--ignore-enterprise-teams
--ignore-secret-teams
)

if [ "$DRY_RUN" != "true" ]; then
PERIBOLOS_ARGS+=(--confirm)
fi

/tmp/peribolos "${PERIBOLOS_ARGS[@]}" \
--github-token-path "$TOKEN_FILE" \
2>&1 | jq -r '[.severity, .time, .msg] | join(" | ")'
22 changes: 22 additions & 0 deletions .github/workflows/peribolos-validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: "Peribolos: Validate"

on:
push:
branches:
- main
paths:
- 'org/**'
pull_request:
paths:
- 'org/**'

jobs:
validate:
name: Validate org config
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Install yamllint and validate config
run: pip install yamllint && yamllint org/config.yaml
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/unbound-force/.github

go 1.24.0
23 changes: 0 additions & 23 deletions org/config.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
orgs:

Check warning on line 1 in org/config.yaml

View workflow job for this annotation

GitHub Actions / Validate org config

1:1 [document-start] missing document start "---"
unbound-force:
# Org settings
billing_email: jay.flowers@gmail.com
Expand All @@ -13,41 +13,18 @@
- marcusburghardt
- sonupreetam
members:
- alayne222 # enterprise-managed
- beatrizmcouto
- bplaxco # enterprise-managed
- em-redhat
- fkolacek-rh # enterprise-managed
- gvauter
- gxmiranda
- hbraswelrh
- jiprocha # enterprise-managed
- jpadmanrh # enterprise-managed
- jpower432
- ppsomiad # enterprise-managed
- rmonk-redhat # enterprise-managed
- sharman-rh # enterprise-managed
- trevor-vaughan
- tyraziel
- yvonnedevlinrh

# Teams
teams:
# Enterprise-managed team - do not modify, managed via GitHub Enterprise
ent:enterprise-security-managers:
description: Enterprise security manager team
privacy: closed
members:
- alayne222
- ppsomiad
- sharman-rh
maintainers:
- bplaxco
- fkolacek-rh
- jiprocha
- jpadmanrh
- rmonk-redhat

overlords:
description: ""
privacy: closed
Expand Down