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
4 changes: 2 additions & 2 deletions .github/workflows/access-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
env:
TF_IN_AUTOMATION: 1
TF_INPUT: 0
TF_WORKSPACE: ${{ github.event.inputs.organization }}
TF_WORKSPACE: ${{ inputs.organization }}
AWS_ACCESS_KEY_ID: ${{ secrets.RO_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.RO_AWS_SECRET_ACCESS_KEY }}
steps:
Expand All @@ -48,7 +48,7 @@ jobs:
run: pnpm install --frozen-lockfile && pnpm run build
working-directory: scripts
- name: Generate access report
run: node lib/actions/access-report.js
run: node --input-type=module --eval "import {runDescribeAccessChanges} from './lib/actions/shared/describe-access-changes.js'; await runDescribeAccessChanges();"
working-directory: scripts
env:
ACCESS_REPORT_PATH: ../ACCESS_REPORT.md
Expand Down
84 changes: 77 additions & 7 deletions .github/workflows/update-members.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ on:
options:
- keep
- remove
draft-run:
description: Only summarize the member update without creating a pull request
required: true
default: false
type: boolean

defaults:
run:
Expand All @@ -42,18 +47,33 @@ defaults:
jobs:
update:
permissions:
contents: write
pull-requests: write
contents: read
pull-requests: read
name: Update members
runs-on: ubuntu-latest
environment: push
environment: ${{ github.event.inputs['draft-run'] == 'true' && 'read' || '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: Generate app token
if: github.event.inputs['draft-run'] != 'true'
id: token
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0
with:
app_id: ${{ secrets.RW_GITHUB_APP_ID }}
installation_retrieval_mode: id
installation_retrieval_payload: ${{ secrets[format('RW_GITHUB_APP_INSTALLATION_ID_{0}', github.repository_owner)] || secrets.RW_GITHUB_APP_INSTALLATION_ID }}
private_key: ${{ secrets.RW_GITHUB_APP_PEM_FILE }}
- name: Checkout with app token
if: github.event.inputs['draft-run'] != 'true'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
token: ${{ steps.token.outputs.token }}
- name: Checkout
if: github.event.inputs['draft-run'] == 'true'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install pnpm
uses: pnpm/action-setup@91ab88e2619ed1f46221f0ba42d1492c02baf788 # v6.0.6
Expand Down Expand Up @@ -88,10 +108,60 @@ jobs:
else
echo "this=true" >> $GITHUB_OUTPUT
fi
- uses: ./.github/actions/git-config-user
if: steps.config-modified.outputs.this == 'true'
- name: Summarize member update
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'] }}
DRAFT_RUN: ${{ github.event.inputs['draft-run'] }}
AFFECTED_USERS: ${{ steps.update.outputs.affected-users }}
CONFIG_MODIFIED: ${{ steps.config-modified.outputs.this }}
run: |
{
echo '## Update members'
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 "- Draft run: \`${DRAFT_RUN}\`"
echo "- Affected users: \`${AFFECTED_USERS:-none}\`"
echo "- Config modified: \`${CONFIG_MODIFIED}\`"
if [[ "${DRAFT_RUN}" == 'true' ]]; then
echo "- Pull request: not created because draft run is enabled"
elif [[ "${CONFIG_MODIFIED}" == 'true' ]]; then
echo "- Pull request: will be created"
else
echo "- Pull request: not created because there are no config changes"
fi

if [[ "${CONFIG_MODIFIED}" == 'true' ]]; then
echo
echo '<details><summary>Config diff</summary>'
echo
echo '```diff'
git diff -- "github/${ORGANIZATION}.yml" | sed 's/^```/``` /'
echo '```'
echo
echo '</details>'
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Configure git user
if: steps.config-modified.outputs.this == 'true' && github.event.inputs['draft-run'] != 'true'
env:
GITHUB_MGMT_APP_ID: ${{ secrets.RW_GITHUB_APP_ID }}
run: |
git config --global user.name "github-mgmt[bot]"
git config --global user.email "${GITHUB_MGMT_APP_ID}+github-mgmt[bot]@users.noreply.github.com"
- name: Create draft pull request
if: steps.config-modified.outputs.this == 'true'
if: steps.config-modified.outputs.this == 'true' && github.event.inputs['draft-run'] != 'true'
env:
ORGANIZATION: ${{ github.event.inputs.organization }}
CUTOFF_DATE: ${{ github.event.inputs['cutoff-date'] }}
Expand All @@ -101,7 +171,7 @@ jobs:
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 }}
GITHUB_TOKEN: ${{ steps.token.outputs.token }}
run: |
branch="update-members-${ORGANIZATION}-${GITHUB_RUN_ID}"
body="$(mktemp)"
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed
- plan/apply workflows now publish planned/applied commit, workspace, environment, and rendered terraform plan details to workflow summaries and artifacts
- manual access report workflow for generating the full access breakdown on demand
- update members workflow draft run mode for summarizing member updates without creating a pull request
- allow-destroy workspace classification now reports which member or repository removals require guarded environments
- access report member classifications now explicitly describe the post-change access state
- workflows: added separate GitHub Actions environments for reading organization state, writing organization state, and pushing repository changes
- update members workflow now creates branches and pull requests with the configured GitHub App token so follow-up workflows are triggered
- **BREAKING**: access changes action now emits only the access change comment by default; update custom usage to avoid nesting the full access breakdown in PR comments
- workflows: pin third-party actions to latest release SHAs and replan from the merged commit before applying
- docs: update template repository references from `github-mgmt-template` to `github-as-code`
Expand Down
121 changes: 119 additions & 2 deletions scripts/__tests__/workflows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import * as YAML from 'yaml'

type WorkflowStep = {
name?: string
if?: string
uses?: string
run?: string
env?: Record<string, string>
with?: Record<string, unknown>
Expand All @@ -17,6 +19,7 @@ type Workflow = {
jobs: Record<
string,
{
permissions?: Record<string, string>
environment?: string
steps: WorkflowStep[]
}
Expand Down Expand Up @@ -48,8 +51,34 @@ describe('workflows', () => {
assert.match(applyStep.run ?? '', /allow_destroy_override\.tf\.disabled/)
})

it('does not provide a manual access report workflow', () => {
assert.equal(existsSync('../.github/workflows/access-report.yml'), false)
it('provides a manual access report workflow through the shared formatter helper', () => {
assert.equal(existsSync('../.github/workflows/access-report.yml'), true)

const accessReport = workflow('access-report.yml')
const reportJob = accessReport.jobs.report
const steps = reportJob.steps
const generateStep = steps.find(
step => step.name === 'Generate access report'
)
const publishStep = steps.find(
step => step.name === 'Publish access report summary'
)
const uploadStep = steps.find(step => step.name === 'Upload access report')

assert.ok(accessReport.on.workflow_dispatch)
assert.equal(reportJob.environment, 'read')
assert.ok(generateStep)
assert.equal(generateStep.env?.ACCESS_REPORT_PATH, '../ACCESS_REPORT.md')
assert.match(generateStep.run ?? '', /runDescribeAccessChanges/)
assert.doesNotMatch(generateStep.run ?? '', /access-report\.js/)
assert.ok(publishStep)
assert.equal(
publishStep.run,
'cat ACCESS_REPORT.md >> "$GITHUB_STEP_SUMMARY"'
)
assert.ok(uploadStep)
assert.equal(uploadStep.with?.name, 'access-report-${{ env.TF_WORKSPACE }}')
assert.equal(uploadStep.with?.path, 'ACCESS_REPORT.md')
})

it('publishes the full access report from the fix workflow', () => {
Expand Down Expand Up @@ -170,4 +199,92 @@ describe('workflows', () => {
'diff -u "${TF_WORKSPACE}.reviewed.txt" "${TF_WORKSPACE}.merged.txt"\n'
)
})

it('creates update-members pull requests with the GitHub App token', () => {
const updateMembers = workflow('update-members.yml')
const job = updateMembers.jobs.update
const steps = job.steps
const generateTokenStep = steps.find(
step => step.name === 'Generate app token'
)
const checkoutStep = steps.find(
step => step.name === 'Checkout with app token'
)
const configureGitStep = steps.find(
step => step.name === 'Configure git user'
)
const createPullRequestStep = steps.find(
step => step.name === 'Create draft pull request'
)

assert.equal(job.permissions?.contents, 'read')
assert.equal(job.permissions?.['pull-requests'], 'read')
assert.ok(generateTokenStep)
assert.equal(
generateTokenStep.if,
"github.event.inputs['draft-run'] != 'true'"
)
assert.equal(
generateTokenStep.with?.app_id,
'${{ secrets.RW_GITHUB_APP_ID }}'
)
assert.ok(checkoutStep)
assert.equal(checkoutStep.if, "github.event.inputs['draft-run'] != 'true'")
assert.equal(checkoutStep.with?.token, '${{ steps.token.outputs.token }}')
assert.ok(configureGitStep)
assert.equal(
configureGitStep.if,
"steps.config-modified.outputs.this == 'true' && github.event.inputs['draft-run'] != 'true'"
)
assert.equal(
configureGitStep.env?.GITHUB_MGMT_APP_ID,
'${{ secrets.RW_GITHUB_APP_ID }}'
)
assert.match(configureGitStep.run ?? '', /github-mgmt\[bot\]/)
assert.ok(createPullRequestStep)
assert.equal(
createPullRequestStep.env?.GITHUB_TOKEN,
'${{ steps.token.outputs.token }}'
)
})

it('supports update-members draft runs without creating pull requests', () => {
const updateMembers = workflow('update-members.yml')
const workflowDispatch = updateMembers.on.workflow_dispatch as {
inputs: Record<string, {default?: boolean; type?: string}>
}
const job = updateMembers.jobs.update
const steps = job.steps
const checkoutStep = steps.find(step => step.name === 'Checkout')
const summaryStep = steps.find(
step => step.name === 'Summarize member update'
)
const createPullRequestStep = steps.find(
step => step.name === 'Create draft pull request'
)

assert.equal(workflowDispatch.inputs['draft-run'].default, false)
assert.equal(workflowDispatch.inputs['draft-run'].type, 'boolean')
assert.equal(
job.environment,
"${{ github.event.inputs['draft-run'] == 'true' && 'read' || 'push' }}"
)
assert.ok(checkoutStep)
assert.equal(checkoutStep.if, "github.event.inputs['draft-run'] == 'true'")
assert.ok(summaryStep)
assert.match(summaryStep.run ?? '', /## Update members/)
assert.match(
summaryStep.run ?? '',
/Pull request: not created because draft run is enabled/
)
assert.match(
summaryStep.run ?? '',
/git diff -- "github\/\$\{ORGANIZATION\}\.yml"/
)
assert.ok(createPullRequestStep)
assert.equal(
createPullRequestStep.if,
"steps.config-modified.outputs.this == 'true' && github.event.inputs['draft-run'] != 'true'"
)
})
})
Loading