-
Notifications
You must be signed in to change notification settings - Fork 0
feat(rulesets): enforce the deploy/ guards from reviewed main #195
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| name: 🛡️ Deploy guards | ||
|
|
||
| # An organization ruleset (deploy/organization-rulesets/require-dotgithub-deploy-guards.yaml) | ||
| # requires this workflow from this repository's reviewed main. github.workflow_sha | ||
| # names that reviewed revision, and the validators are checked out from it, so a | ||
| # pull request cannot remove or neuter the deploy/ guards by editing ci.yaml, this | ||
| # file, or the scripts. The candidate supplies only its deploy/ tree, history and | ||
| # pull-request text, all read as data. | ||
| # | ||
| # It deliberately has no merge_group trigger. Both guards need the pull request's | ||
| # title, commits and body, which a merge-group event does not carry, so a | ||
| # merge-group run could only pass without judging anything. Without the trigger, | ||
| # a merge queue added to this repository waits on this required check instead. | ||
| on: | ||
| pull_request: | ||
| types: [opened, synchronize, reopened, edited, ready_for_review] | ||
|
|
||
| permissions: {} | ||
|
|
||
| jobs: | ||
| deploy-guards: | ||
| name: 🛡️ Enforce deploy/ guards from reviewed main | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| - name: 📥 Check out the reviewed validators | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| repository: devantler-tech/.github | ||
| ref: ${{ github.workflow_sha }} | ||
| path: trusted | ||
| persist-credentials: false | ||
|
|
||
| - name: 📥 Check out the candidate as data | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| path: candidate | ||
| fetch-depth: 0 | ||
| persist-credentials: false | ||
|
|
||
| - name: 🚦 Validate release contract | ||
| if: github.event_name == 'pull_request' | ||
| env: | ||
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | ||
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | ||
| PR_TITLE: ${{ github.event.pull_request.title }} | ||
| COMMIT_COUNT: ${{ github.event.pull_request.commits }} | ||
| run: | | ||
| first_subject="$(git -C candidate log --format=%s "$BASE_SHA..$HEAD_SHA" | tail -1)" | ||
| git -C candidate diff --name-only -z "$BASE_SHA" "$HEAD_SHA" | | ||
| bash trusted/scripts/validate-release-contract.sh "$PR_TITLE" "$COMMIT_COUNT" "$first_subject" | ||
|
|
||
| - name: 🗑️ Validate deploy/ deletions are acknowledged | ||
| if: github.event_name == 'pull_request' | ||
| env: | ||
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | ||
| PR_BODY: ${{ github.event.pull_request.body }} | ||
| run: | | ||
| git -C candidate worktree add --detach "$RUNNER_TEMP/base" "$BASE_SHA" | ||
| kubectl kustomize "$RUNNER_TEMP/base/deploy" > "$RUNNER_TEMP/base-render.yaml" || | ||
| { echo "deploy-deletions: UNKNOWN — the base deploy/ tree does not render; repair main first" >&2; exit 2; } | ||
| kubectl kustomize candidate/deploy > "$RUNNER_TEMP/head-render.yaml" || | ||
| { echo "deploy-deletions: UNKNOWN — this pull request's deploy/ tree does not render" >&2; exit 2; } | ||
| printf '%s' "$PR_BODY" > "$RUNNER_TEMP/pr-body.txt" | ||
| bash trusted/scripts/validate-deploy-deletions.sh \ | ||
| "$RUNNER_TEMP/base-render.yaml" "$RUNNER_TEMP/head-render.yaml" "$RUNNER_TEMP/pr-body.txt" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
deploy/organization-rulesets/require-dotgithub-deploy-guards.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| # Require the deploy/ guards from this repository's reviewed main branch. | ||
| # ci.yaml runs the release-contract and deletion validators from the pull | ||
| # request's own checkout, so a candidate can edit or remove them and still pass | ||
| # `CI - Required Checks`. This rule selects .github/workflows/deploy-guards.yaml | ||
| # from refs/heads/main; that workflow runs the validators from the same reviewed | ||
| # revision and reads the candidate only as data. | ||
| # | ||
| # provider-upjet-github v0.19.1 exposes a branch/tag ref but not GitHub's | ||
| # workflow SHA selector, so refs/heads/main is the strongest declarative source | ||
| # binding the deployed provider can express. The source workflow reaches main in | ||
| # the merge that adds this rule, and deploy/ publishes only on a later v* tag. | ||
| # | ||
| # Net-new and managed Observe + Create + Update + LateInitialize, never Delete. | ||
| # No bypassActors are declared. | ||
| apiVersion: enterprise.github.m.upbound.io/v1alpha1 | ||
| kind: OrganizationRuleset | ||
| metadata: | ||
| name: require-dotgithub-deploy-guards | ||
| spec: | ||
| managementPolicies: | ||
| - Observe | ||
| - Create | ||
| - Update | ||
| - LateInitialize | ||
| forProvider: | ||
| name: Require workflow - .github deploy guards | ||
| target: branch | ||
| enforcement: active | ||
| conditions: | ||
| - refName: | ||
| - include: ["~DEFAULT_BRANCH"] | ||
| exclude: [] | ||
| repositoryId: [933213756] | ||
| rules: | ||
| - requiredWorkflows: | ||
| - requiredWorkflow: | ||
| - repositoryId: 933213756 | ||
| path: .github/workflows/deploy-guards.yaml | ||
| ref: refs/heads/main | ||
| providerConfigRef: | ||
| kind: ProviderConfig | ||
| name: default |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| #!/usr/bin/env bash | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| # Pins the trusted enforcement of the deploy/ guards (#183): an organization | ||
| # ruleset requires .github/workflows/deploy-guards.yaml from this repository's | ||
| # reviewed main, and that workflow runs the validators from the ruleset-selected | ||
| # source revision, never from the candidate checkout it judges. | ||
|
|
||
| repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | ||
| workflow="${DEPLOY_GUARDS_WORKFLOW:-${repo_root}/.github/workflows/deploy-guards.yaml}" | ||
| render="$(mktemp)" | ||
| trap 'rm -f "${render}"' EXIT | ||
|
|
||
| fail() { | ||
| echo "deploy-guards-ruleset test: $*" >&2 | ||
| exit 1 | ||
| } | ||
|
|
||
| for tool in kubectl yq; do | ||
| command -v "${tool}" >/dev/null || fail "required tool '${tool}' not found" | ||
| done | ||
|
|
||
| kubectl kustomize "${repo_root}/deploy" >"${render}" || | ||
| fail "kubectl kustomize deploy/ failed" | ||
|
|
||
| selector='select(.kind == "OrganizationRuleset" and .metadata.name == "require-dotgithub-deploy-guards")' | ||
| count="$(yq -N "${selector} | .metadata.name" "${render}" | grep -c . || true)" | ||
| [[ "${count}" == "1" ]] || fail "expected exactly one rendered deploy-guards ruleset, got ${count}" | ||
|
|
||
| assert_value() { | ||
| local label="$1" expected="$2" expression="$3" actual | ||
| actual="$(yq -r "${selector} | ${expression}" "${render}")" | ||
| [[ "${actual}" == "${expected}" ]] || fail "${label}: expected '${expected}', got '${actual}'" | ||
| } | ||
|
|
||
| assert_json() { | ||
| local label="$1" expected="$2" expression="$3" actual | ||
| actual="$(yq -o=json -I=0 "${selector} | ${expression}" "${render}")" | ||
| [[ "${actual}" == "${expected}" ]] || fail "${label}: expected '${expected}', got '${actual}'" | ||
| } | ||
|
|
||
| # 933213756 is devantler-tech/.github: the rule targets only this repository | ||
| # and takes its workflow from this repository's reviewed main. | ||
| assert_value "ruleset name" "Require workflow - .github deploy guards" '.spec.forProvider.name' | ||
| assert_value "ruleset target" "branch" '.spec.forProvider.target' | ||
| assert_value "ruleset enforcement" "active" '.spec.forProvider.enforcement' | ||
| assert_json "management policy" '["Observe","Create","Update","LateInitialize"]' '.spec.managementPolicies' | ||
| assert_json "target repository" '[933213756]' '.spec.forProvider.conditions[0].repositoryId' | ||
| assert_json "target branch" '["~DEFAULT_BRANCH"]' '.spec.forProvider.conditions[0].refName[0].include' | ||
| assert_json "target exclusions" '[]' '.spec.forProvider.conditions[0].refName[0].exclude' | ||
| assert_value "bypass actor count" "0" '(.spec.forProvider.bypassActors // []) | length' | ||
| assert_value "rule count" "1" '.spec.forProvider.rules | length' | ||
| assert_value "required workflow count" "1" '.spec.forProvider.rules[0].requiredWorkflows[0].requiredWorkflow | length' | ||
| assert_value "source repository" "933213756" '.spec.forProvider.rules[0].requiredWorkflows[0].requiredWorkflow[0].repositoryId' | ||
| assert_value "source path" ".github/workflows/deploy-guards.yaml" '.spec.forProvider.rules[0].requiredWorkflows[0].requiredWorkflow[0].path' | ||
| assert_value "source ref" "refs/heads/main" '.spec.forProvider.rules[0].requiredWorkflows[0].requiredWorkflow[0].ref' | ||
|
|
||
| [[ -f "${workflow}" ]] || fail "required workflow ${workflow#"${repo_root}"/} does not exist" | ||
|
|
||
| wf() { yq -r "$1" "${workflow}"; } | ||
|
|
||
| [[ "$(wf '.on | has("pull_request")')" == "true" ]] || fail "workflow must run on pull_request" | ||
| # A merge-group event carries no pull-request title, commits or body, so a | ||
| # merge_group run could only skip both guards and pass. Leaving the trigger out | ||
| # makes a merge queue wait on this required check instead. | ||
| [[ "$(wf '.on | has("merge_group")')" == "false" ]] || fail "workflow must not run on merge_group, where both guards would be skipped" | ||
| guarded_steps="$(wf '[.jobs[].steps[] | select(.run // "" | test("validate-(release-contract|deploy-deletions)\\.sh")) | select(.if // "" | test("pull_request"))] | length')" | ||
| [[ "${guarded_steps}" == "2" ]] || fail "both validator steps must run on pull_request, got ${guarded_steps}" | ||
| [[ "$(wf '.permissions | tojson')" == "{}" ]] || fail "workflow-level permissions must be {}" | ||
|
|
||
| # The validators must come from the ruleset-selected source revision. A | ||
| # candidate cannot change github.workflow_sha, so this checkout is the trusted one. | ||
| # shellcheck disable=SC2016 | ||
| trusted_checkouts="$(wf '[.jobs[].steps[] | select(.uses // "" | test("^actions/checkout@")) | select(.with.repository == "devantler-tech/.github" and .with.ref == "${{ github.workflow_sha }}" and .with.path == "trusted")] | length')" | ||
| [[ "${trusted_checkouts}" == "1" ]] || fail "expected one checkout of the workflow source revision into trusted/, got ${trusted_checkouts}" | ||
|
|
||
| unsafe_checkouts="$(wf '[.jobs[].steps[] | select(.uses // "" | test("^actions/checkout@")) | select(.with["persist-credentials"] != false)] | length')" | ||
| [[ "${unsafe_checkouts}" == "0" ]] || fail "every checkout must set persist-credentials: false (${unsafe_checkouts} do not)" | ||
|
|
||
| for validator in validate-release-contract.sh validate-deploy-deletions.sh; do | ||
| runs="$(wf "[.jobs[].steps[] | select(.run // \"\" | contains(\"${validator}\"))] | length")" | ||
| [[ "${runs}" == "1" ]] || fail "expected exactly one step running ${validator}, got ${runs}" | ||
| trusted="$(wf "[.jobs[].steps[] | select(.run // \"\" | contains(\"trusted/scripts/${validator}\"))] | length")" | ||
| [[ "${trusted}" == "1" ]] || fail "${validator} must run from trusted/scripts, not the candidate checkout" | ||
| done | ||
|
|
||
| candidate_scripts="$(wf '[.jobs[].steps[] | select(.run // "" | test("(^|[^/[:alnum:]_])(candidate/)?scripts/"))] | length')" | ||
| [[ "${candidate_scripts}" == "0" ]] || fail "no step may run a script outside trusted/ (${candidate_scripts} do)" | ||
|
|
||
| echo "deploy-guards-ruleset: OK" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.