070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
288 lines
11 KiB
YAML
288 lines
11 KiB
YAML
name: autofix.atom
|
|
|
|
on:
|
|
workflow_run:
|
|
workflows: [ci]
|
|
types: [completed]
|
|
|
|
permissions:
|
|
actions: read
|
|
contents: write
|
|
issues: write
|
|
pull-requests: read
|
|
|
|
concurrency:
|
|
group: autofix-${{ github.event.workflow_run.id }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
autofix:
|
|
name: Apply handoff fixes
|
|
if: ${{ github.repository == 'nexu-io/open-design' && github.event.workflow_run.event == 'pull_request' }}
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 15
|
|
|
|
steps:
|
|
- name: Checkout trusted workflow code
|
|
uses: actions/checkout@v6.0.2
|
|
with:
|
|
repository: ${{ github.repository }}
|
|
ref: ${{ github.event.repository.default_branch }}
|
|
|
|
- name: Check handoff helper
|
|
run: python3 .github/scripts/handoff.py self-check
|
|
|
|
- name: Resolve autofix handoff artifacts
|
|
id: artifacts
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPO: ${{ github.repository }}
|
|
RUN_ID: ${{ github.event.workflow_run.id }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
pattern="$(python3 .github/scripts/handoff.py artifact-pattern autofix)"
|
|
count="$(
|
|
gh api "repos/$REPO/actions/runs/$RUN_ID/artifacts" \
|
|
--jq '.artifacts[]? | select(.expired == false and (.name | startswith("handoff-autofix-"))) | .name' \
|
|
| sed '/^$/d' \
|
|
| wc -l \
|
|
| tr -d ' '
|
|
)"
|
|
if [ "$count" = "0" ]; then
|
|
echo "found=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
{
|
|
echo "found=true"
|
|
echo "pattern=$pattern"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Download autofix handoff artifacts
|
|
if: ${{ steps.artifacts.outputs.found == 'true' }}
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
pattern: ${{ steps.artifacts.outputs.pattern }}
|
|
run-id: ${{ github.event.workflow_run.id }}
|
|
path: ${{ runner.temp }}/handoff-autofix
|
|
merge-multiple: false
|
|
github-token: ${{ github.token }}
|
|
|
|
- name: Read autofix handoffs
|
|
if: ${{ steps.artifacts.outputs.found == 'true' }}
|
|
id: handoffs
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
entries="$RUNNER_TEMP/autofix-handoffs.jsonl"
|
|
python3 .github/scripts/handoff.py list autofix "$RUNNER_TEMP/handoff-autofix" > "$entries"
|
|
if [ ! -s "$entries" ]; then
|
|
echo "present=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
{
|
|
echo "present=true"
|
|
echo "entries=$entries"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Detect bot credentials
|
|
if: ${{ steps.handoffs.outputs.present == 'true' }}
|
|
id: bot-secrets
|
|
env:
|
|
BOT_APP_CLIENT_ID: ${{ secrets.BOT_APP_CLIENT_ID }}
|
|
BOT_APP_PRIVATE_KEY: ${{ secrets.BOT_APP_PRIVATE_KEY }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -n "${BOT_APP_CLIENT_ID}" ] && [ -n "${BOT_APP_PRIVATE_KEY}" ]; then
|
|
echo "present=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "present=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Generate Open Design bot token
|
|
if: ${{ steps.handoffs.outputs.present == 'true' && steps.bot-secrets.outputs.present == 'true' }}
|
|
id: bot-token
|
|
continue-on-error: true
|
|
uses: actions/create-github-app-token@v3.2.0
|
|
with:
|
|
client-id: ${{ secrets.BOT_APP_CLIENT_ID }}
|
|
private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}
|
|
owner: nexu-io
|
|
repositories: open-design
|
|
permission-contents: write
|
|
|
|
- name: Apply handoff fixes
|
|
if: ${{ steps.handoffs.outputs.present == 'true' }}
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
BOT_TOKEN: ${{ steps.bot-token.outputs.token }}
|
|
REPO: ${{ github.repository }}
|
|
RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
|
ENTRIES_PATH: ${{ steps.handoffs.outputs.entries }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
upsert_nix_fallback_comment() {
|
|
local id="$1"
|
|
local pr_number="$2"
|
|
local patch_path="$3"
|
|
local reason="$4"
|
|
|
|
if [ "$id" != "nix-pnpm-deps" ]; then
|
|
echo "No fallback comment handler configured for autofix handoff $id."
|
|
return 0
|
|
fi
|
|
|
|
local marker="<!-- nix-hash-refresh -->"
|
|
local body_path="$RUNNER_TEMP/nix-hash-refresh-fallback-$id.md"
|
|
local payload_file="$RUNNER_TEMP/nix-hash-refresh-fallback-$id.json"
|
|
{
|
|
printf '%s\n' "$marker"
|
|
printf "A generated Nix hash refresh is available for this PR, but the automated same-repo push did not complete: %s\n\n" "$reason"
|
|
printf "Apply the diff below with \`git apply\`:\n\n"
|
|
printf '```diff\n'
|
|
cat "$patch_path"
|
|
printf '\n```\n'
|
|
} > "$body_path"
|
|
|
|
jq -n --rawfile body "$body_path" '{body: $body}' > "$payload_file"
|
|
local comment_id
|
|
comment_id="$(
|
|
gh api --paginate "repos/$REPO/issues/$pr_number/comments" \
|
|
| jq -r --arg marker "$marker" '.[] | select((.body // "") | contains($marker)) | .id' \
|
|
| tail -n 1 || true
|
|
)"
|
|
if [ -n "$comment_id" ]; then
|
|
gh api --method PATCH "repos/$REPO/issues/comments/$comment_id" --input "$payload_file" >/dev/null
|
|
echo "Updated Nix hash fallback comment on PR $pr_number."
|
|
else
|
|
gh api --method POST "repos/$REPO/issues/$pr_number/comments" --input "$payload_file" >/dev/null
|
|
echo "Created Nix hash fallback comment on PR $pr_number."
|
|
fi
|
|
}
|
|
|
|
clear_nix_fallback_comment() {
|
|
local id="$1"
|
|
local pr_number="$2"
|
|
|
|
if [ "$id" != "nix-pnpm-deps" ]; then
|
|
return 0
|
|
fi
|
|
|
|
local marker="<!-- nix-hash-refresh -->"
|
|
local comment_ids
|
|
comment_ids="$(
|
|
gh api --paginate "repos/$REPO/issues/$pr_number/comments" \
|
|
| jq -r --arg marker "$marker" '.[] | select((.body // "") | contains($marker)) | .id' || true
|
|
)"
|
|
if [ -z "$comment_ids" ]; then
|
|
echo "No stale Nix hash fallback comment found on PR $pr_number."
|
|
return 0
|
|
fi
|
|
|
|
while IFS= read -r comment_id; do
|
|
if [ -n "$comment_id" ]; then
|
|
gh api --method DELETE "repos/$REPO/issues/comments/$comment_id" >/dev/null
|
|
echo "Deleted stale Nix hash fallback comment $comment_id on PR $pr_number."
|
|
fi
|
|
done <<< "$comment_ids"
|
|
}
|
|
|
|
while IFS= read -r entry_json; do
|
|
id="$(jq -r '.id' <<< "$entry_json")"
|
|
pr_number="$(jq -r '.pr_number' <<< "$entry_json")"
|
|
head_sha="$(jq -r '.head_sha' <<< "$entry_json")"
|
|
base_sha="$(jq -r '.base_sha' <<< "$entry_json")"
|
|
patch_path="$(jq -r '.patch_path' <<< "$entry_json")"
|
|
commit_message="$(jq -r '.commit_message' <<< "$entry_json")"
|
|
allowed_paths_json="$(jq -c '.allowed_paths' <<< "$entry_json")"
|
|
|
|
if [ "$head_sha" != "$RUN_HEAD_SHA" ]; then
|
|
echo "Skipping autofix handoff $id because artifact head $head_sha does not match workflow_run head $RUN_HEAD_SHA."
|
|
continue
|
|
fi
|
|
|
|
pr_json="$(gh api "repos/$REPO/pulls/$pr_number")"
|
|
pr_state="$(jq -r '.state' <<< "$pr_json")"
|
|
pr_draft="$(jq -r '.draft' <<< "$pr_json")"
|
|
head_repo="$(jq -r '.head.repo.full_name // empty' <<< "$pr_json")"
|
|
head_ref="$(jq -r '.head.ref // empty' <<< "$pr_json")"
|
|
current_head="$(jq -r '.head.sha' <<< "$pr_json")"
|
|
current_base="$(jq -r '.base.sha' <<< "$pr_json")"
|
|
if [ "$pr_state" != "open" ]; then
|
|
echo "Skipping autofix handoff $id because PR $pr_number state is $pr_state."
|
|
continue
|
|
fi
|
|
if [ "$pr_draft" != "false" ]; then
|
|
echo "Skipping autofix handoff $id because PR $pr_number is draft."
|
|
continue
|
|
fi
|
|
if [ "$head_repo" != "$REPO" ]; then
|
|
echo "Skipping autofix handoff $id because PR $pr_number is from fork $head_repo."
|
|
continue
|
|
fi
|
|
if [ "$current_head" != "$head_sha" ]; then
|
|
echo "Skipping stale autofix handoff $id for $head_sha; current PR head is $current_head."
|
|
continue
|
|
fi
|
|
if [ "$current_base" != "$base_sha" ]; then
|
|
echo "Skipping stale autofix handoff $id for base $base_sha; current PR base is $current_base."
|
|
continue
|
|
fi
|
|
|
|
if [ -z "${BOT_TOKEN:-}" ]; then
|
|
echo "Skipping autofix handoff $id because bot token is unavailable."
|
|
upsert_nix_fallback_comment "$id" "$pr_number" "$patch_path" "bot credentials were unavailable"
|
|
continue
|
|
fi
|
|
|
|
work_dir="$RUNNER_TEMP/autofix-work-$id"
|
|
rm -rf "$work_dir"
|
|
git clone --no-checkout "https://x-access-token:${BOT_TOKEN}@github.com/${REPO}.git" "$work_dir"
|
|
cd "$work_dir"
|
|
git fetch origin "+refs/heads/$head_ref:refs/remotes/origin/$head_ref"
|
|
git checkout -B "handoff-$id" "refs/remotes/origin/$head_ref"
|
|
checked_out="$(git rev-parse HEAD)"
|
|
if [ "$checked_out" != "$head_sha" ]; then
|
|
echo "Skipping stale autofix handoff $id for $head_sha; fetched $checked_out."
|
|
cd "$GITHUB_WORKSPACE"
|
|
continue
|
|
fi
|
|
if ! git apply --check "$patch_path"; then
|
|
cd "$GITHUB_WORKSPACE"
|
|
upsert_nix_fallback_comment "$id" "$pr_number" "$patch_path" "the generated patch did not apply cleanly"
|
|
continue
|
|
fi
|
|
git apply "$patch_path"
|
|
|
|
changed_files="$(git diff --name-only | sort)"
|
|
allowed_files="$(jq -r '.[]' <<< "$allowed_paths_json" | sort)"
|
|
if [ "$changed_files" != "$allowed_files" ]; then
|
|
echo "Unexpected files changed after applying autofix handoff $id:" >&2
|
|
printf '%s\n' "$changed_files" >&2
|
|
echo "Allowed files:" >&2
|
|
printf '%s\n' "$allowed_files" >&2
|
|
exit 1
|
|
fi
|
|
if git diff --quiet --exit-code; then
|
|
echo "Autofix handoff $id produced no changes."
|
|
cd "$GITHUB_WORKSPACE"
|
|
continue
|
|
fi
|
|
|
|
git config user.name 'open-design-bot[bot]'
|
|
git config user.email '282769551+open-design-bot[bot]@users.noreply.github.com'
|
|
jq -r '.[]' <<< "$allowed_paths_json" | xargs git add --
|
|
git commit -m "$commit_message"
|
|
if ! git push origin "HEAD:refs/heads/$head_ref"; then
|
|
cd "$GITHUB_WORKSPACE"
|
|
upsert_nix_fallback_comment "$id" "$pr_number" "$patch_path" "the bot push failed"
|
|
continue
|
|
fi
|
|
cd "$GITHUB_WORKSPACE"
|
|
clear_nix_fallback_comment "$id" "$pr_number"
|
|
echo "Applied autofix handoff $id to PR $pr_number."
|
|
done < "$ENTRIES_PATH"
|