Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

Import campaign: queue imports past the 3-at-a-time limit, and show why a run failed (#61)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AizKXSsengXZRegJxRATkL
Canberk Demirci authored · GitHub committed signed1 parent 811c98dcommit b2caad32af3eb9fc9dda018a14403aee175b68c2Browse files
2 files changed+59−17
modified.github/workflows/import-campaign.yml+31−11View file
@@ -27,6 +27,10 @@
2727 description: Also run the imported repositories' push workflows (third-party code)
2828 type: boolean
2929 default: false
30+ follow_run:
31+ description: Instead of starting a campaign, print the report and logs of this Diffinery run (its id)
32+ type: string
33+ default: ""
3034
3135 permissions:
3236 contents: read
@@ -48,6 +52,7 @@
4852 R: codician-team/diffinery
4953 steps:
5054 - name: Copy IMPORT_GITHUB_TOKEN to Diffinery
55+ if: ${{ inputs.follow_run == '' }}
5156 env:
5257 IMPORT_GITHUB_TOKEN: ${{ secrets.IMPORT_GITHUB_TOKEN }}
5358 run: |
@@ -63,22 +68,37 @@
6368 REPOS: ${{ inputs.repos }}
6469 MINUTES: ${{ inputs.minutes }}
6570 RUN_WORKFLOWS: ${{ inputs.run_workflows }}
71+ FOLLOW_RUN: ${{ inputs.follow_run }}
6672 run: |
6773 set -euo pipefail
6874 api() { curl -fsS --retry 3 -X "$1" "$DIFFINERY/api/v3$2" -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" ${3:+--data "$3"}; }
69- before=$(api GET "/repos/$R/actions/workflows/import-campaign.yml/runs?per_page=1" | jq -r '.workflow_runs[0].id // 0')
70- api POST "/repos/$R/actions/workflows/import-campaign.yml/dispatches" "$(jq -nc --arg r "$REPOS" --arg m "$MINUTES" --arg w "$RUN_WORKFLOWS" '{ref: "main", inputs: {repos: $r, minutes: $m, run_workflows: $w}}')" >/dev/null
71- for i in $(seq 1 60); do
72- run=$(api GET "/repos/$R/actions/workflows/import-campaign.yml/runs?per_page=1" | jq -c '.workflow_runs[0] // empty')
73- [ -n "$run" ] && [ "$(jq -r .id <<<"$run")" != "$before" ] && break
74- sleep 5
75- done
76- id=$(jq -r .id <<<"$run")
77- echo "Diffinery run: $(jq -r .html_url <<<"$run")"
75+ if [ -n "$FOLLOW_RUN" ]; then
76+ id=$FOLLOW_RUN
77+ else
78+ before=$(api GET "/repos/$R/actions/workflows/import-campaign.yml/runs?per_page=1" | jq -r '.workflow_runs[0].id // 0')
79+ api POST "/repos/$R/actions/workflows/import-campaign.yml/dispatches" "$(jq -nc --arg r "$REPOS" --arg m "$MINUTES" --arg w "$RUN_WORKFLOWS" '{ref: "main", inputs: {repos: $r, minutes: $m, run_workflows: $w}}')" >/dev/null
80+ for i in $(seq 1 60); do
81+ run=$(api GET "/repos/$R/actions/workflows/import-campaign.yml/runs?per_page=1" | jq -c '.workflow_runs[0] // empty')
82+ [ -n "$run" ] && [ "$(jq -r .id <<<"$run")" != "$before" ] && break
83+ sleep 5
84+ done
85+ id=$(jq -r .id <<<"$run")
86+ fi
87+ echo "Diffinery run: $(api GET "/repos/$R/actions/runs/$id" | jq -r .html_url)"
7888 while [ "$(api GET "/repos/$R/actions/runs/$id" | jq -r .status)" != completed ]; do sleep 30; done
7989 run=$(api GET "/repos/$R/actions/runs/$id")
80- for job in $(api GET "/repos/$R/actions/runs/$id/jobs" | jq -r '.jobs[].id'); do
81- curl -fsS "$DIFFINERY/api/v3/repos/$R/actions/jobs/$job/logs" -H "Authorization: Bearer $TOKEN" | sed -n '/^# Import campaign/,$p' | tee -a "$GITHUB_STEP_SUMMARY"
90+ jobs=$(api GET "/repos/$R/actions/runs/$id/jobs")
91+ for job in $(jq -r '.jobs[].id' <<<"$jobs"); do
92+ jq -r --arg id "$job" '.jobs[] | select((.id | tostring) == $id) | "Job \(.name): \(.conclusion // .status)\(if .failure then " (\(.failure.message? // .failure))" else "" end)"' <<<"$jobs"
93+ log=$(curl -fsS "$DIFFINERY/api/v3/repos/$R/actions/jobs/$job/logs" -H "Authorization: Bearer $TOKEN" || echo "(no log)")
94+ if grep -q '^# Import campaign' <<<"$log"; then
95+ sed -n '/^# Import campaign/,$p' <<<"$log" | tee -a "$GITHUB_STEP_SUMMARY"
96+ else
97+ # No report: the job stopped before writing one, so show how it ended.
98+ echo "::group::Last 150 log lines of job $job"
99+ tail -150 <<<"$log"
100+ echo "::endgroup::"
101+ fi
82102 done
83103 [ "$(jq -r .conclusion <<<"$run")" = success ] || { echo "::error::the campaign run on Diffinery ended $(jq -r .conclusion <<<"$run")"; exit 1; }
84104
modifiedscripts/import-campaign.sh+28−6View file
@@ -14,6 +14,7 @@
1414 # RUN_WORKFLOWS=true starts the imported repositories' push workflows (off by default:
1515 # they are third-party code, run with a job token on Diffinery).
1616 set -euo pipefail
17+trap 'echo "::error::import-campaign.sh stopped at line $LINENO: $BASH_COMMAND"' ERR
1718 MINUTES=${MINUTES:-180}
1819 REPORT=${REPORT:-report.md}
1920 deadline=$(( $(date +%s) + MINUTES * 60 ))
@@ -25,11 +26,20 @@
2526 declare -A import_id target
2627 work=$(mktemp -d)
2728
28-# ------------------------------------------------------------ start every import
29+# ------------------------------------------------------------ start the imports and wait for them
30+# Diffinery runs a few repository imports per person at a time (429 beyond that), so the
31+# rest wait their turn and start as earlier ones finish.
32+queue=()
2933 for src in "$@"; do
3034 name="campaign-${src//\//-}"
3135 name=${name,,}
3236 target[$src]=$name
37+ queue+=("$src")
38+done
39+
40+# Starts one import. Returns 0 when it started or failed for good, 1 when Diffinery is busy.
41+start() {
42+ local src=$1 name=${target[$1]} body c
3343 if [ "$(code GET "/repos/$owner/$name")" = 200 ]; then
3444 code DELETE "/repos/$owner/$name" >/dev/null
3545 fi
@@ -39,23 +49,35 @@
3949 if [ "$c" = 201 ]; then
4050 import_id[$src]=$(jq -r .id /tmp/campaign-out)
4151 echo "import $src -> $owner/$name (${import_id[$src]})"
52+ elif [ "$c" = 429 ]; then
53+ return 1
4254 else
4355 echo "::error::$src: starting the import returned $c: $(jq -r '.message // .' /tmp/campaign-out)"
4456 jq -n --arg s "$src" --arg c "$c" --arg m "$(jq -r '.message // .' /tmp/campaign-out)" '{source: $s, status: "not started", error: "\($c): \($m)"}' > "$work/${name}.import.json"
4557 fi
46-done
58+}
4759
48-# ------------------------------------------------------------ wait for the imports
4960 while :; do
61+ while [ "${#queue[@]}" -gt 0 ] && start "${queue[0]}"; do queue=("${queue[@]:1}"); done
5062 pending=0
5163 for src in "${!import_id[@]}"; do
5264 f="$work/${target[$src]}.import.json"
5365 [ -f "$f" ] && jq -e '.status | IN("succeeded", "failed", "cancelled")' "$f" >/dev/null && continue
5466 api GET "/diffinery/imports/${import_id[$src]}" > "$f" || { pending=$((pending + 1)); continue; }
55- jq -e '.status | IN("succeeded", "failed", "cancelled")' "$f" >/dev/null || pending=$((pending + 1))
67+ if jq -e '.status | IN("succeeded", "failed", "cancelled")' "$f" >/dev/null; then
68+ echo "$src: import $(jq -r .status "$f")"
69+ else
70+ pending=$((pending + 1))
71+ fi
5672 done
57- [ "$pending" = 0 ] && break
58- if [ "$(left)" -le 0 ]; then echo "::warning::$pending imports were still running when the campaign ran out of time"; break; fi
73+ [ "$pending" = 0 ] && [ "${#queue[@]}" = 0 ] && break
74+ if [ "$(left)" -le 0 ]; then
75+ echo "::warning::$pending imports were still running and ${#queue[@]} had not started when the campaign ran out of time"
76+ for src in "${queue[@]}"; do
77+ jq -n --arg s "$src" '{source: $s, status: "not started", error: "the campaign ran out of time before this import could start"}' > "$work/${target[$src]}.import.json"
78+ done
79+ break
80+ fi
5981 sleep 20
6082 done
6183