Skip to content
Merged
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
176 changes: 176 additions & 0 deletions .github/workflows/dependabot-projection-convergence.yml
Original file line number Diff line number Diff line change
@@ -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