diff --git a/.github/workflows/peribolos-drift.yml b/.github/workflows/peribolos-drift.yml new file mode 100644 index 0000000..0d21a5a --- /dev/null +++ b/.github/workflows/peribolos-drift.yml @@ -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 + 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 + 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 + 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 diff --git a/.github/workflows/peribolos-sync.yml b/.github/workflows/peribolos-sync.yml index 7375e31..84a870e 100644 --- a/.github/workflows/peribolos-sync.yml +++ b/.github/workflows/peribolos-sync.yml @@ -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(" | ")' diff --git a/.github/workflows/peribolos-validate.yml b/.github/workflows/peribolos-validate.yml new file mode 100644 index 0000000..a1d0781 --- /dev/null +++ b/.github/workflows/peribolos-validate.yml @@ -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 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d755a71 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/unbound-force/.github + +go 1.24.0 diff --git a/org/config.yaml b/org/config.yaml index 0750f5d..4fc15b8 100644 --- a/org/config.yaml +++ b/org/config.yaml @@ -13,41 +13,18 @@ orgs: - 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