diff --git a/README.md b/README.md index f437f0a..c61344e 100644 --- a/README.md +++ b/README.md @@ -145,3 +145,33 @@ ansible-playbook -vv -i inventory -e update_files_commit_file=/path/to/git-commi NOTE: This process may create multiple commits if you need to make edits to an existing PR. Use the `Squash commits and merge` functionality in the github PR to merge. + +# Manage Branch Protection Rules + +The playbook `playbooks/update_branch_protection_rules.yml` is used to manage +branch protection rules for the `main` branch. The default rules are specified +in `branchProtectionRules` in `inventory/group_vars/active_roles.yml`. The +additional status checks for python code are specified in +`python_status_check_contexts` in `inventory/group_vars/python_roles.yml`. The +additional status checks for shell code are specified in +`shellcheck_status_check_contexts` in +`inventory/group_vars/shellcheck_roles.yml`. + +These use the github graphql api https://docs.github.com/en/graphql. The +graphql files are in the `playbooks/graphql_files/` directory. + +The general strategy is to get the existing role rules. If they are different +than the specified rules, delete them and add the specified rules. + +There aren't any parameters, but if you use `-vvv` or higher verbosity, +you can see the data being sent and received. + +*NOTE WELL*: This playbook does not create PRs - it modifies the role +configuration directly - so be careful. + +## Run it + +Run it like this: +``` +ansible-playbook -vv -i inventory playbooks/update_branch_protection_rules.yml +``` diff --git a/inventory/group_vars/active_roles.yml b/inventory/group_vars/active_roles.yml index 8ddecdf..a231567 100644 --- a/inventory/group_vars/active_roles.yml +++ b/inventory/group_vars/active_roles.yml @@ -28,3 +28,39 @@ lsr_name: linux_system_roles lsr_role_namespace: linux_system_roles # for ansible-lint gha_checkout_action: actions/checkout@v3 tox_lsr_url: "git+https://github.com/linux-system-roles/tox-lsr@2.13.2" +default_status_check_contexts: + - ansible_lint + - ansible_managed_var_comment + - ansible_plugin_scan + - ansible_test + - Fedora-37/ansible-2.14/(citool) + - Fedora-37/ansible-2.9/(citool) + - Fedora-36/ansible-2.14/(citool) + - Fedora-36/ansible-2.9/(citool) +# https://docs.github.com/en/graphql/reference/objects#branchprotectionrule +isAdminEnforced: false +branchProtectionRules: + - allowsDeletions: false + allowsForcePushes: false + blocksCreations: false + dismissesStaleReviews: true + isAdminEnforced: "{{ isAdminEnforced }}" + lockAllowsFetchAndMerge: false + lockBranch: false + pattern: main + requireLastPushApproval: true + requiredApprovingReviewCount: 1 + requiredStatusCheckContexts: "{{ default_status_check_contexts + + status_check_contexts | d([]) + + python_status_check_contexts | d([]) + + shellcheck_status_check_contexts | d([]) | unique | + sort(case_sensitive=false) }}" + requiresApprovingReviews: true + requiresCodeOwnerReviews: true + requiresCommitSignatures: true + requiresConversationResolution: false + requiresLinearHistory: true + requiresStatusChecks: true + requiresStrictStatusChecks: true + restrictsPushes: false + restrictsReviewDismissals: false diff --git a/inventory/group_vars/python_roles.yml b/inventory/group_vars/python_roles.yml index dd6aa8c..c477c0c 100644 --- a/inventory/group_vars/python_roles.yml +++ b/inventory/group_vars/python_roles.yml @@ -3,3 +3,12 @@ present_python_templates: - .github/workflows/codeql.yml - .github/workflows/python-unit-test.yml absent_python_files: [] +# these are additional status checks for python code +python_status_check_contexts: + - CodeQL + - python (2.7, ubuntu-20.04) + - python (3.6, ubuntu-20.04) + - python (3.8, ubuntu-latest) + - python (3.9, ubuntu-latest) + - python (3.10, ubuntu-latest) + - python (3.11, ubuntu-latest) diff --git a/inventory/group_vars/shellcheck_roles.yml b/inventory/group_vars/shellcheck_roles.yml index 41d0dbc..1a527f5 100644 --- a/inventory/group_vars/shellcheck_roles.yml +++ b/inventory/group_vars/shellcheck_roles.yml @@ -3,3 +3,6 @@ present_shellcheck_templates: - .github/workflows/shellcheck.yml absent_shellcheck_files: - .github/workflows/differential-shellcheck.yml +# these are additional status checks for shell code +shellcheck_status_check_contexts: + - shellcheck diff --git a/inventory/host_vars/cockpit.yml b/inventory/host_vars/cockpit.yml index 396e3cb..d95f0d5 100644 --- a/inventory/host_vars/cockpit.yml +++ b/inventory/host_vars/cockpit.yml @@ -2,3 +2,5 @@ github_actions: weekly_ci: schedule: - cron: "0 12 * * 6" +# branch protection +isAdminEnforced: true diff --git a/inventory/host_vars/network.yml b/inventory/host_vars/network.yml index e74bbe8..93a277c 100644 --- a/inventory/host_vars/network.yml +++ b/inventory/host_vars/network.yml @@ -44,3 +44,12 @@ yamllint: /tests/tests_bond_cloned_mac_initscripts.yml /tests/tests_bridge_cloned_mac_initscripts.yml /tests/tests_bridge_cloned_mac_nm.yml +# these are added to the list of default status check contexts +status_check_contexts: + - DCO + - markdownlint + - integration (c8s) + - integration (c9s) + - integration (c7) + - python-26 + - woke diff --git a/playbooks/graphql_files/create_branch_protection_rules.graphql b/playbooks/graphql_files/create_branch_protection_rules.graphql new file mode 100644 index 0000000..958efac --- /dev/null +++ b/playbooks/graphql_files/create_branch_protection_rules.graphql @@ -0,0 +1,14 @@ +mutation { + createBranchProtectionRule(input:{ +{% for key, value in bpr.items() %} + {{ key }}:{{ value | to_nice_json }}, +{% endfor %} + repositoryId:"{{ repo.id }}" + }) { + branchProtectionRule { + id + allowsForcePushes + } + clientMutationId + } +} diff --git a/playbooks/graphql_files/delete_branch_protection_rules.graphql b/playbooks/graphql_files/delete_branch_protection_rules.graphql new file mode 100644 index 0000000..52ca317 --- /dev/null +++ b/playbooks/graphql_files/delete_branch_protection_rules.graphql @@ -0,0 +1,7 @@ +mutation { + deleteBranchProtectionRule(input:{ + branchProtectionRuleId:"{{ bpr_id }}" + }) { + clientMutationId + } +} diff --git a/playbooks/graphql_files/get_branch_protection_rules.graphql b/playbooks/graphql_files/get_branch_protection_rules.graphql new file mode 100644 index 0000000..b3681f4 --- /dev/null +++ b/playbooks/graphql_files/get_branch_protection_rules.graphql @@ -0,0 +1,31 @@ +query { + repository(owner:"{{ github_org }}", name:"{{ inventory_hostname }}") { + id + branchProtectionRules(first:50) { + edges { + node { + id +{% for param in __bpr_fields %} + {{ param }} +{% endfor %} +{# + requiredStatusChecks { + context + app { + createdAt + databaseId + description + logoBackgroundColor + logoUrl + name + slug + updatedAt + url + } + } +#} + } + } + } + } +} diff --git a/playbooks/templates/.github/workflows/ansible-lint.yml b/playbooks/templates/.github/workflows/ansible-lint.yml index 27a472a..8ed0a44 100644 --- a/playbooks/templates/.github/workflows/ansible-lint.yml +++ b/playbooks/templates/.github/workflows/ansible-lint.yml @@ -10,6 +10,7 @@ on: # yamllint disable-line rule:truthy push: branches: - main + merge_group: workflow_dispatch: permissions: contents: read diff --git a/playbooks/templates/.github/workflows/ansible-managed-var-comment.yml b/playbooks/templates/.github/workflows/ansible-managed-var-comment.yml index b90852f..3fbf949 100644 --- a/playbooks/templates/.github/workflows/ansible-managed-var-comment.yml +++ b/playbooks/templates/.github/workflows/ansible-managed-var-comment.yml @@ -10,6 +10,7 @@ on: # yamllint disable-line rule:truthy push: branches: - main + merge_group: workflow_dispatch: permissions: contents: read diff --git a/playbooks/templates/.github/workflows/ansible-plugin-scan.yml b/playbooks/templates/.github/workflows/ansible-plugin-scan.yml index 382c5eb..00c4054 100644 --- a/playbooks/templates/.github/workflows/ansible-plugin-scan.yml +++ b/playbooks/templates/.github/workflows/ansible-plugin-scan.yml @@ -10,6 +10,7 @@ on: # yamllint disable-line rule:truthy push: branches: - main + merge_group: workflow_dispatch: permissions: contents: read diff --git a/playbooks/templates/.github/workflows/ansible-test.yml b/playbooks/templates/.github/workflows/ansible-test.yml index b905ff7..9cd7b24 100644 --- a/playbooks/templates/.github/workflows/ansible-test.yml +++ b/playbooks/templates/.github/workflows/ansible-test.yml @@ -10,6 +10,7 @@ on: # yamllint disable-line rule:truthy push: branches: - main + merge_group: workflow_dispatch: env: LSR_ROLE2COLL_NAMESPACE: {{ lsr_namespace }} diff --git a/playbooks/templates/.github/workflows/codeql.yml b/playbooks/templates/.github/workflows/codeql.yml index c72b306..f7e2ae9 100644 --- a/playbooks/templates/.github/workflows/codeql.yml +++ b/playbooks/templates/.github/workflows/codeql.yml @@ -12,6 +12,7 @@ on: # yamllint disable-line rule:truthy - checks_requested schedule: {{ github_actions.codeql.schedule | to_nice_yaml(indent=2) | indent(width=4, first=true) -}} + workflow_dispatch: jobs: analyze: name: Analyze diff --git a/playbooks/templates/.github/workflows/markdownlint.yml b/playbooks/templates/.github/workflows/markdownlint.yml index d47b063..7f67606 100644 --- a/playbooks/templates/.github/workflows/markdownlint.yml +++ b/playbooks/templates/.github/workflows/markdownlint.yml @@ -11,6 +11,7 @@ on: # yamllint disable-line rule:truthy push: branches: - main + merge_group: workflow_dispatch: permissions: contents: read diff --git a/playbooks/templates/.github/workflows/python-unit-test.yml b/playbooks/templates/.github/workflows/python-unit-test.yml index 5731f8f..2b644c4 100644 --- a/playbooks/templates/.github/workflows/python-unit-test.yml +++ b/playbooks/templates/.github/workflows/python-unit-test.yml @@ -11,6 +11,7 @@ on: # yamllint disable-line rule:truthy push: branches: - main + merge_group: workflow_dispatch: permissions: contents: read diff --git a/playbooks/update_branch_protection_rules.yml b/playbooks/update_branch_protection_rules.yml new file mode 100644 index 0000000..c4ad217 --- /dev/null +++ b/playbooks/update_branch_protection_rules.yml @@ -0,0 +1,112 @@ +# use the graphql variable names +- name: Manage branch protection rules + hosts: active_roles + connection: local + gather_facts: false + tasks: + - name: Get gh config file + slurp: + path: "{{ lookup('env', 'HOME') ~ '/.config/gh/hosts.yml' }}" + register: gh_cfg_file + + - name: Get token + set_fact: + token: "{{ cfg_dict['github.com']['oauth_token'] }}" + vars: + cfg_dict: "{{ gh_cfg_file.content | b64decode | from_yaml }}" + + - name: Get Branch Protection Rules + uri: + url: https://api.github.com/graphql + method: POST + headers: + content-type: "application/json" + authorization: "Bearer {{ token }}" + X-Github-Next-Global-ID: "1" + body_format: json + body: + query: "{{ lookup('template', 'graphql_files/get_branch_protection_rules.graphql') }}" + # variables: "{{ lookup('template', './test_vars.yml') | from_yaml | to_nice_json }}" + vars: + __bpr_fields: "{{ (branchProtectionRules | first).keys() | list }}" + register: __query_result + + - name: Result + debug: + msg: "{{ __query_result.json.data | to_nice_json }}" + verbosity: 3 + + - name: Convert result to canonical format + set_fact: + repo: | + {% set __repo = {} %} + {% for key, value in __query_result.json.data.repository.items() %} + {% if key == "branchProtectionRules" %} + {% set _ = __repo.update({key: []})%} + {% for edge in value["edges"] %} + {% if "node" in edge %} + {% set _ = __repo[key].append(edge["node"]) %} + {% endif %} + {% endfor %} + {% else %} + {% set _ = __repo.update({key: value})%} + {% endif %} + {% endfor %} + {{ __repo }} + + - name: Show converted result + debug: + msg: repo {{ repo | to_nice_json }} bpr {{ branchProtectionRules | to_nice_json }} + verbosity: 3 + + - name: Update Branch Protection Rules if needed + vars: + __expected: "{{ branchProtectionRules | sort(attribute='pattern') | map('combine', {'id': ''}) }}" + __actual: "{{ repo.branchProtectionRules | sort(attribute='pattern') | map('combine', {'id': ''}) }}" + when: __expected != __actual + block: + - name: Remove existing rules + uri: + url: https://api.github.com/graphql + method: POST + headers: + content-type: "application/json" + authorization: "Bearer {{ token }}" + X-Github-Next-Global-ID: "1" + body_format: json + body: + query: "{{ lookup('template', 'graphql_files/delete_branch_protection_rules.graphql') }}" + # variables: "{{ lookup('template', './test_vars.yml') | from_yaml | to_nice_json }}" + register: __delete_result + vars: + bpr_id: "{{ item.id }}" + loop: "{{ repo.branchProtectionRules }}" + + - name: Show delete result + debug: + msg: __delete_result {{ __delete_result.json.data | to_nice_json }} + verbosity: 3 + when: __delete_result.json is defined + + - name: Add expected rules + uri: + url: https://api.github.com/graphql + method: POST + headers: + content-type: "application/json" + authorization: "Bearer {{ token }}" + X-Github-Next-Global-ID: "1" + body_format: json + body: + query: "{{ lookup('template', 'graphql_files/create_branch_protection_rules.graphql') }}" + # variables: "{{ lookup('template', './test_vars.yml') | from_yaml | to_nice_json }}" + register: __create_result + loop: "{{ branchProtectionRules }}" + loop_control: + loop_var: bpr + + - name: Show create result + debug: + msg: "{{ __create_result.json.data | to_nice_json }}" + verbosity: 3 + when: __create_result.json is defined