From 14d4157abc12f84cb5c6e919e7a4152306068ffa Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 25 Aug 2026 00:54:52 +0500 Subject: [PATCH] fix(ci): converge Dependabot projections --- .../dependabot-projection-convergence.yml | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 .github/workflows/dependabot-projection-convergence.yml diff --git a/.github/workflows/dependabot-projection-convergence.yml b/.github/workflows/dependabot-projection-convergence.yml new file mode 100644 index 0000000..9ef26da --- /dev/null +++ b/.github/workflows/dependabot-projection-convergence.yml @@ -0,0 +1,176 @@ +name: dependabot-projection-convergence + +on: + workflow_run: + workflows: [gds-ci] + types: [completed] + +permissions: {} + +concurrency: + group: dependabot-projection-${{ github.run_id }} + cancel-in-progress: false + +jobs: + converge: + if: >- + github.event.workflow_run.conclusion == 'failure' && + github.event.workflow_run.event == 'pull_request' && + (github.event.workflow_run.actor.login == 'dependabot[bot]' || + github.event.workflow_run.actor.login == 'github-actions[bot]') && + github.event.workflow_run.head_repository.full_name == github.repository && + startsWith(github.event.workflow_run.head_branch, 'dependabot/go_modules/') + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + actions: write # approve only runs created for the exact generated commit + contents: write # create one atomic GitHub-signed projection commit + pull-requests: write # bind and update the exact Dependabot pull request + steps: + - name: Bind and update the Dependabot pull request + id: bind + env: + GH_TOKEN: ${{ github.token }} + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + EXPECTED_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + set -euo pipefail + owner="${GITHUB_REPOSITORY%%/*}" + pr_json="$(gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls?state=open&head=${owner}:${HEAD_BRANCH}")" + pr_number="$(jq -er 'if length == 1 then .[0].number else error("expected exactly one pull request") end' <<<"$pr_json")" + current_head="$(jq -er '.[0].head.sha' <<<"$pr_json")" + author="$(jq -er '.[0].user.login' <<<"$pr_json")" + [ "$author" = 'dependabot[bot]' ] + [ "$current_head" = "$EXPECTED_HEAD_SHA" ] + mapfile -t changed < <(gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}/files" --jq '.[].filename') + [ "${#changed[@]}" -gt 0 ] + dependency_changed=false + for path in "${changed[@]}"; do + case "$path" in + go.mod|go.sum) dependency_changed=true ;; + .gds/bundle.lock.yaml|.gds/compiled-policy.json|.github/workflows/gds-ci.yml) ;; + *) echo "untrusted path in dependency PR: $path" >&2; exit 1 ;; + esac + done + [ "$dependency_changed" = true ] + response="$(gh api --method PUT "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}/update-branch" -f "expected_head_sha=${current_head}" 2>&1)" || { + if ! grep -Fq 'already up to date' <<<"$response"; then + printf '%s\n' "$response" >&2 + exit 1 + fi + } + updated_head="$current_head" + for _ in $(seq 1 30); do + updated_head="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}" --jq .head.sha)" + if [ "$updated_head" != "$current_head" ] || grep -Fq 'already up to date' <<<"$response"; then + break + fi + sleep 2 + done + printf 'branch=%s\nhead=%s\npr=%s\n' "$HEAD_BRANCH" "$updated_head" "$pr_number" >>"$GITHUB_OUTPUT" + + - name: Checkout trusted generator + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.repository.default_branch }} + persist-credentials: false + path: trusted + + - name: Checkout candidate as data + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ steps.bind.outputs.head }} + persist-credentials: false + path: candidate + + - name: Build reviewed GDS generator + working-directory: trusted + run: go build -trimpath -o "$RUNNER_TEMP/gds" ./core/cmd/gds + + - name: Materialize deterministic candidate projections + env: + SESSION_ID: dependabot-${{ github.run_id }} + STATE_PATH: ${{ runner.temp }}/gds-state.db + run: | + set -euo pipefail + "$RUNNER_TEMP/gds" generate repository --cwd candidate --plan \ + --session-id "$SESSION_ID" --state-path "$STATE_PATH" --json >"$RUNNER_TEMP/plan.json" + plan_id="$(jq -er .data.plan.plan_id "$RUNNER_TEMP/plan.json")" + "$RUNNER_TEMP/gds" generate repository --cwd candidate --apply "$plan_id" \ + --session-id "$SESSION_ID" --state-path "$STATE_PATH" --json >"$RUNNER_TEMP/apply.json" + operation_id="$(jq -er .operation_id "$RUNNER_TEMP/apply.json")" + "$RUNNER_TEMP/gds" generate repository --cwd candidate --verify "$operation_id" \ + --session-id "$SESSION_ID" --state-path "$STATE_PATH" --json >/dev/null + + - name: Create atomic signed projection commit + id: commit + env: + GH_TOKEN: ${{ github.token }} + HEAD_BRANCH: ${{ steps.bind.outputs.branch }} + EXPECTED_HEAD_SHA: ${{ steps.bind.outputs.head }} + run: | + set -euo pipefail + mapfile -t changed < <(git -C candidate diff --name-only) + if [ "${#changed[@]}" -eq 0 ]; then + printf 'changed=false\ncandidate_sha=%s\n' "$EXPECTED_HEAD_SHA" >>"$GITHUB_OUTPUT" + exit 0 + fi + for path in "${changed[@]}"; do + case "$path" in + .gds/bundle.lock.yaml|.gds/compiled-policy.json|.github/workflows/gds-ci.yml) ;; + *) echo "generator changed unmanaged path: $path" >&2; exit 1 ;; + esac + done + python3 -I - <<'PY' + import base64 + import json + import os + from pathlib import Path + + paths = [line for line in os.popen("git -C candidate diff --name-only").read().splitlines() if line] + additions = [ + { + "path": path, + "contents": base64.b64encode(Path("candidate", path).read_bytes()).decode("ascii"), + } + for path in paths + ] + query = """mutation($repository:String!,$branch:String!,$head:GitObjectID!,$additions:[FileAddition!]!){createCommitOnBranch(input:{branch:{repositoryNameWithOwner:$repository,branchName:$branch},expectedHeadOid:$head,message:{headline:\"chore(deps): refresh GDS projections\"},fileChanges:{additions:$additions}}){commit{oid url}}}""" + payload = { + "query": query, + "variables": { + "repository": os.environ["GITHUB_REPOSITORY"], + "branch": os.environ["HEAD_BRANCH"], + "head": os.environ["EXPECTED_HEAD_SHA"], + "additions": additions, + }, + } + Path(os.environ["RUNNER_TEMP"], "commit-request.json").write_text( + json.dumps(payload, separators=(",", ":")), encoding="utf-8" + ) + PY + response="$(gh api graphql --input "$RUNNER_TEMP/commit-request.json")" + candidate_sha="$(jq -er .data.createCommitOnBranch.commit.oid <<<"$response")" + printf 'changed=true\ncandidate_sha=%s\n' "$candidate_sha" >>"$GITHUB_OUTPUT" + + - name: Approve exact generated workflow runs + if: ${{ steps.commit.outputs.changed == 'true' }} + env: + GH_TOKEN: ${{ github.token }} + CANDIDATE_SHA: ${{ steps.commit.outputs.candidate_sha }} + run: | + set -euo pipefail + seen=false + for _ in $(seq 1 30); do + runs="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs?event=pull_request&head_sha=${CANDIDATE_SHA}")" + count="$(jq '.workflow_runs | length' <<<"$runs")" + if [ "$count" -gt 0 ]; then seen=true; fi + mapfile -t pending < <(jq -r '.workflow_runs[] | select(.status == "action_required") | .id' <<<"$runs") + for run_id in "${pending[@]}"; do + gh api --method POST "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/approve" >/dev/null + done + if [ "$seen" = true ] && [ "${#pending[@]}" -eq 0 ]; then exit 0; fi + sleep 2 + done + echo "timed out approving exact generated runs" >&2 + exit 1