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
59 changes: 54 additions & 5 deletions .github/workflows/apply.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,65 @@ jobs:
GITHUB_APP_PEM_FILE: ${{ secrets.RO_GITHUB_APP_PEM_FILE }}
run: node lib/actions/find-sha-for-plan.js
working-directory: scripts
apply:
classify:
needs: [prepare]
if: needs.prepare.outputs.sha != '' && needs.prepare.outputs.workspaces != ''
permissions:
contents: read
name: Classify
runs-on: ubuntu-latest
environment: read
outputs:
matrix: ${{ steps.classify.outputs.matrix }}
env:
TF_IN_AUTOMATION: 1
TF_INPUT: 0
AWS_ACCESS_KEY_ID: ${{ secrets.RO_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.RO_AWS_SECRET_ACCESS_KEY }}
WORKSPACES: ${{ needs.prepare.outputs.workspaces }}
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup terraform
uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0
with:
terraform_version: 1.12.0
terraform_wrapper: false
- name: Initialize terraform
run: terraform init
working-directory: terraform
- name: Install pnpm
uses: pnpm/action-setup@91ab88e2619ed1f46221f0ba42d1492c02baf788 # v6.0.6
with:
version: 10
- name: Use Node.js lts/*
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: lts/*
cache: ''
- run: pnpm install --frozen-lockfile && pnpm run build
working-directory: scripts
- name: Classify workspaces
id: classify
env:
MODE: write
run: node lib/actions/classify-allow-destroy.js
working-directory: scripts
apply:
needs: [prepare, classify]
if: needs.prepare.outputs.sha != '' && needs.prepare.outputs.workspaces != ''
permissions:
actions: read
contents: read
strategy:
fail-fast: false
matrix:
workspace: ${{ fromJson(needs.prepare.outputs.workspaces) }}
matrix: ${{ fromJson(needs.classify.outputs.matrix) }}
name: Apply
runs-on: ubuntu-latest
environment: write
environment: ${{ matrix.environment }}
env:
TF_IN_AUTOMATION: 1
TF_INPUT: 0
Expand All @@ -84,14 +130,17 @@ jobs:
terraform_wrapper: false
- name: Initialize terraform
run: terraform init
- name: Allow destroy in guarded environment
if: matrix.environment == 'write-allow-destroy'
run: cp allow_destroy_override.tf.disabled allow_destroy_override.tf
- name: Download reviewed terraform plan
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SHA: ${{ needs.prepare.outputs.sha }}
run: gh run download -n "${TF_WORKSPACE}_${SHA}.tfplan" --repo "${GITHUB_REPOSITORY}"
- name: Replan merged commit
run: |
terraform show -json > $TF_WORKSPACE.tfstate.json
terraform show -json > "$TF_WORKSPACE.tfstate.json"
terraform plan -refresh=false -lock=false -out="${TF_WORKSPACE}.merged.tfplan" -no-color
- name: Compare reviewed and merged plans
run: |
Expand Down
65 changes: 60 additions & 5 deletions .github/workflows/plan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,69 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 10
plan:
classify:
needs: [prepare]
permissions:
contents: read
pull-requests: read
name: Classify
runs-on: ubuntu-latest
environment: read
outputs:
matrix: ${{ steps.classify.outputs.matrix }}
env:
TF_IN_AUTOMATION: 1
TF_INPUT: 0
AWS_ACCESS_KEY_ID: ${{ secrets.RO_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.RO_AWS_SECRET_ACCESS_KEY }}
WORKSPACES: ${{ needs.prepare.outputs.workspaces }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- if: github.event_name == 'pull_request_target'
env:
NUMBER: ${{ github.event.pull_request.number }}
SHA: ${{ github.event.pull_request.head.sha }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git fetch origin "pull/${NUMBER}/head"
rm -rf github && git checkout "${SHA}" -- github
- name: Setup terraform
uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0
with:
terraform_version: 1.12.0
terraform_wrapper: false
- name: Initialize terraform
run: terraform init
working-directory: terraform
- name: Install pnpm
uses: pnpm/action-setup@91ab88e2619ed1f46221f0ba42d1492c02baf788 # v6.0.6
with:
version: 10
- name: Use Node.js lts/*
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: lts/*
cache: ''
- run: pnpm install --frozen-lockfile && pnpm run build
working-directory: scripts
- name: Classify workspaces
id: classify
env:
MODE: read
run: node lib/actions/classify-allow-destroy.js
working-directory: scripts
plan:
needs: [prepare, classify]
permissions:
contents: read
pull-requests: read
strategy:
fail-fast: false
matrix:
workspace: ${{ fromJson(needs.prepare.outputs.workspaces || '[]') }}
matrix: ${{ fromJson(needs.classify.outputs.matrix) }}
name: Plan
runs-on: ubuntu-latest
environment: read
environment: ${{ matrix.environment }}
env:
TF_IN_AUTOMATION: 1
TF_INPUT: 0
Expand Down Expand Up @@ -88,9 +139,13 @@ jobs:
- name: Initialize terraform
run: terraform init
working-directory: terraform
- name: Allow destroy in guarded environment
if: matrix.environment == 'read-allow-destroy'
run: cp allow_destroy_override.tf.disabled allow_destroy_override.tf
working-directory: terraform
- name: Plan terraform
run: |
terraform show -json > $TF_WORKSPACE.tfstate.json
terraform show -json > "$TF_WORKSPACE.tfstate.json"
terraform plan -refresh=false -lock=false -out="${TF_WORKSPACE}.tfplan" -no-color
working-directory: terraform
- name: Upload terraform plan
Expand Down
130 changes: 130 additions & 0 deletions .github/workflows/update-members.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
name: Update Members

on:
workflow_dispatch:
inputs:
organization:
description: Organization config to update
required: true
cutoff-date:
description: Inactive before this date (YYYY-MM-DD)
required: false
limit:
description: Remove up to this many longest-inactive members
required: false
ignore:
description: Comma, space, or newline separated usernames to ignore
required: false
only:
description: Comma, space, or newline separated usernames to target
required: false
public-repo-access:
description: Whether to retain effective access to public repositories
required: true
default: retain
type: choice
options:
- retain
- remove
organization-membership:
description: Whether selected users remain organization members
required: true
default: keep
type: choice
options:
- keep
- remove

defaults:
run:
shell: bash

jobs:
update:
permissions:
contents: write
pull-requests: write
name: Update members
runs-on: ubuntu-latest
environment: push
env:
GITHUB_APP_ID: ${{ secrets.RO_GITHUB_APP_ID }}
GITHUB_APP_INSTALLATION_ID: ${{ secrets[format('RO_GITHUB_APP_INSTALLATION_ID_{0}', github.event.inputs.organization)] || secrets.RO_GITHUB_APP_INSTALLATION_ID }}
GITHUB_APP_PEM_FILE: ${{ secrets.RO_GITHUB_APP_PEM_FILE }}
TF_WORKSPACE: ${{ github.event.inputs.organization }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install pnpm
uses: pnpm/action-setup@91ab88e2619ed1f46221f0ba42d1492c02baf788 # v6.0.6
with:
version: 10
- name: Use Node.js lts/*
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: lts/*
cache: ""
- name: Initialize scripts
run: pnpm install --frozen-lockfile && pnpm run build
working-directory: scripts
- name: Update members
id: update
run: node lib/actions/update-members.js
working-directory: scripts
env:
CUTOFF_DATE: ${{ github.event.inputs['cutoff-date'] }}
LIMIT: ${{ github.event.inputs.limit }}
IGNORE: ${{ github.event.inputs.ignore }}
ONLY: ${{ github.event.inputs.only }}
PUBLIC_REPO_ACCESS: ${{ github.event.inputs['public-repo-access'] }}
ORGANIZATION_MEMBERSHIP: ${{ github.event.inputs['organization-membership'] }}
- name: Check if organization config was modified
id: config-modified
env:
ORGANIZATION: ${{ github.event.inputs.organization }}
run: |
if [ -z "$(git status --porcelain -- "github/${ORGANIZATION}.yml")" ]; then
echo "this=false" >> $GITHUB_OUTPUT
else
echo "this=true" >> $GITHUB_OUTPUT
fi
- uses: ./.github/actions/git-config-user
if: steps.config-modified.outputs.this == 'true'
- name: Create draft pull request
if: steps.config-modified.outputs.this == 'true'
env:
ORGANIZATION: ${{ github.event.inputs.organization }}
CUTOFF_DATE: ${{ github.event.inputs['cutoff-date'] }}
LIMIT: ${{ github.event.inputs.limit }}
IGNORE: ${{ github.event.inputs.ignore }}
ONLY: ${{ github.event.inputs.only }}
PUBLIC_REPO_ACCESS: ${{ github.event.inputs['public-repo-access'] }}
ORGANIZATION_MEMBERSHIP: ${{ github.event.inputs['organization-membership'] }}
AFFECTED_USERS: ${{ steps.update.outputs.affected-users }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
branch="update-members-${ORGANIZATION}-${GITHUB_RUN_ID}"
body="$(mktemp)"
{
echo 'The changes in this PR were made by a bot. Please review carefully.'
echo
echo "Organization: ${ORGANIZATION}"
echo "Cutoff date: ${CUTOFF_DATE:-not set}"
echo "Limit: ${LIMIT:-not set}"
echo "Ignore: ${IGNORE:-not set}"
echo "Only: ${ONLY:-not set}"
echo "Public repo access: ${PUBLIC_REPO_ACCESS}"
echo "Organization membership: ${ORGANIZATION_MEMBERSHIP}"
echo "Affected users: ${AFFECTED_USERS:-none}"
} > "${body}"

git checkout -B "${branch}"
git add "github/${ORGANIZATION}.yml"
git commit -m "update-members@${GITHUB_RUN_ID} ${ORGANIZATION}"
git push origin "${branch}" --force
gh pr create \
--draft \
--title "Update members for ${ORGANIZATION}" \
--body-file "${body}" \
--head "${branch}" \
--base "${GITHUB_REF_NAME}"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ crash.log
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*
*.tfplan

# Enabled only by guarded allow-destroy workflow jobs
terraform/allow_destroy_override.tf
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]
### Added
- allow-destroy workflow environments for guarded repository and membership deletion plans/applies
- shared action for adding a collaborator to all repositories
- clean workflow which removes resources from state
- information on how to handle private GitHub Management repository
Expand Down
2 changes: 2 additions & 0 deletions docs/ABOUT.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ The workflow for introducing changes to GitHub via YAML configuration file is as
1. Review the plan.
1. Merge the PR and wait for the GitHub Action workflow triggered on pushes to the default branch to apply it.

Plans that remove managed repositories or organization memberships are routed through the `read-allow-destroy` GitHub Actions environment before the PR plan is created. The matching apply is routed through `write-allow-destroy`. These environments must be protected separately from the normal `read` and `write` environments. Outside the allow-destroy environments, Terraform keeps `prevent_destroy` enabled for repository and membership resources.

Neither creating the terraform plan nor applying it refreshes the underlying terraform state i.e. going through this workflow does **NOT** ask GitHub if the actual GitHub configuration state has changed. This makes the workflow fast and rate limit friendly because the number of requests to GitHub is minimised. This can result in the plan failing to be applied, e.g. if the underlying resource has been deleted. This assumes that YAML configuration is the main source of truth for GitHub configuration state. The plans that are created during the PR GitHub Action workflow are compared against plans regenerated from the merged commit before applying.

The workflow for synchronising the current GitHub configuration state with YAML configuration file is as follows:
Expand Down
12 changes: 11 additions & 1 deletion docs/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,12 @@

## GitHub Actions Environments and Secrets

- [ ] Create GitHub Actions environments named `read`, `write`, and `push`, and configure protection rules such as required reviewers. Workflows that read organization state reference `read`; workflows that write organization state reference `write`; workflows that push generated changes to the GitHub Management repository reference `push`.
- [ ] Create GitHub Actions environments named `read`, `read-allow-destroy`, `write`, `write-allow-destroy`, and `push`, and configure protection rules such as required reviewers. Workflows that read organization state reference `read`; workflows that write organization state reference `write`; workflows that push generated changes to the GitHub Management repository reference `push`.
- [ ] Configure `read-allow-destroy` and `write-allow-destroy` with stricter protection rules for repository and membership deletion plans/applies:
- [ ] Require reviewers
- [ ] Prevent self-review
- [ ] Restrict deployment branches to `master`
- [ ] Disable administrator bypass where available
- [ ] [Create encrypted secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-an-organization) for the GitHub organization and allow the repository to access them (\*replace `$GITHUB_ORGANIZATION_NAME` with the GitHub organization name) - *these secrets are read by the GitHub Action workflows*
- [ ] Go to `https://github.com/organizations/$GITHUB_ORGANIZATION_NAME/settings/apps/$GITHUB_APP_NAME` and copy the `App ID`
- [ ] `RO_GITHUB_APP_ID`
Expand Down Expand Up @@ -150,6 +155,11 @@

- [ ] Follow [How to synchronize GitHub Management with GitHub?](HOWTOS.md#synchronize-github-management-with-github) to commit the terraform lock and initialize terraform state

## Member Update Workflows

- [ ] Use `Update Members` to create a draft PR that removes selected members from teams and repository collaborators in `github/$ORGANIZATION_NAME.yml`. The workflow requires either `cutoff-date` or `only`, supports `ignore` and `limit`, can retain effective public repository access by converting that access to direct public repository collaborators, and can optionally remove selected users from organization membership in the YAML config.
- [ ] Review and merge the draft PR through the normal GitHub Management PR flow.

## GitHub Management Repository Protections

*NOTE*: Advanced users might have to skip/adjust this step if they are not managing some of the arguments/attributes mentioned here with GitHub Management.
Expand Down
Loading
Loading