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.

146 lines · 8.0 KB · executableRaw History
1#!/usr/bin/env bash
2# Imports real GitHub repositories into Diffinery as private test repositories, then
3# runs their workflows there and reports what came across and what ran.
4#
5# Each source is imported as <owner>/campaign-<github owner>-<github repo>, replacing
6# the copy from an earlier campaign. A commit to the default branch then starts the
7# repository's push workflows, which are followed until they finish or MINUTES runs out.
8# Jobs for runners Diffinery doesn't host (Windows, macOS) fail at once with a message saying so,
9# which the report shows next to the job.
10#
11# Usage: DIFFINERY=https://diffinery.com TOKEN=... [GITHUB_TOKEN_FOR_IMPORT=...] \
12# import-campaign.sh owner/repo [owner/repo...]
13# Env: MINUTES (default 180) bounds the whole campaign; REPORT (default report.md);
14# RUN_WORKFLOWS=true starts the imported repositories' push workflows (off by default:
15# they are third-party code, run with a job token on Diffinery).
16set -euo pipefail
17MINUTES=${MINUTES:-180}
18REPORT=${REPORT:-report.md}
19deadline=$(( $(date +%s) + MINUTES * 60 ))
20api() { curl -fsS --retry 3 -X "$1" "$DIFFINERY/api/v3$2" -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" ${3:+--data "$3"}; }
21code() { curl -sS -o /tmp/campaign-out -w '%{http_code}' -X "$1" "$DIFFINERY/api/v3$2" -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" ${3:+--data "$3"}; }
22left() { echo $(( deadline - $(date +%s) )); }
23
24owner=$(api GET /user | jq -r .login)
25declare -A import_id target
26work=$(mktemp -d)
27
28# ------------------------------------------------------------ start every import
29for src in "$@"; do
30 name="campaign-${src//\//-}"
31 name=${name,,}
32 target[$src]=$name
33 if [ "$(code GET "/repos/$owner/$name")" = 200 ]; then
34 code DELETE "/repos/$owner/$name" >/dev/null
35 fi
36 body=$(jq -nc --arg s "$src" --arg o "$owner" --arg n "$name" --arg t "${GITHUB_TOKEN_FOR_IMPORT:-}" \
37 '{source: $s, owner: $o, name: $n, visibility: "private"} + (if $t == "" then {} else {github_token: $t} end)')
38 c=$(code POST /diffinery/imports "$body")
39 if [ "$c" = 201 ]; then
40 import_id[$src]=$(jq -r .id /tmp/campaign-out)
41 echo "import $src -> $owner/$name (${import_id[$src]})"
42 else
43 echo "::error::$src: starting the import returned $c: $(jq -r '.message // .' /tmp/campaign-out)"
44 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"
45 fi
46done
47
48# ------------------------------------------------------------ wait for the imports
49while :; do
50 pending=0
51 for src in "${!import_id[@]}"; do
52 f="$work/${target[$src]}.import.json"
53 [ -f "$f" ] && jq -e '.status | IN("succeeded", "failed", "cancelled")' "$f" >/dev/null && continue
54 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))
56 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
59 sleep 20
60done
61
62# ------------------------------------------------------------ run each imported repository's push workflows
63declare -A sha
64for src in "${!import_id[@]}"; do
65 name=${target[$src]}
66 f="$work/$name.import.json"
67 [ "$(jq -r .status "$f")" = succeeded ] || continue
68 repo=$(api GET "/repos/$owner/$name")
69 branch=$(jq -r .default_branch <<<"$repo")
70 api GET "/repos/$owner/$name/actions/workflows" | jq '[.workflows[] | {name, path, state}]' > "$work/$name.workflows.json" || echo '[]' > "$work/$name.workflows.json"
71 [ "$(jq length "$work/$name.workflows.json")" -gt 0 ] || continue
72 [ "${RUN_WORKFLOWS:-false}" = true ] || continue
73 put=$(jq -nc --arg b "$branch" --arg c "$(printf 'Imported from %s and run on Diffinery\n' "$src" | base64 -w0)" '{message: "Run the workflows on Diffinery", content: $c, branch: $b}')
74 if out=$(api PUT "/repos/$owner/$name/contents/.diffinery-campaign" "$put"); then
75 sha[$src]=$(jq -r .commit.sha <<<"$out")
76 echo "$src: pushed ${sha[$src]} to $branch"
77 else
78 echo "::warning::$src: could not push a commit to $branch"
79 fi
80done
81
82finished() { jq -e '(.workflow_runs | length) > 0 and all(.workflow_runs[]; .status == "completed")' "$1" >/dev/null; }
83# Runs can start other runs (workflow_run), so a set counts as finished after three quiet checks.
84declare -A quiet
85while :; do
86 pending=0
87 for src in "${!sha[@]}"; do
88 name=${target[$src]}
89 f="$work/$name.runs.json"
90 [ "${quiet[$src]:-0}" -ge 3 ] && continue
91 api GET "/repos/$owner/$name/actions/runs?head_sha=${sha[$src]}&per_page=100" > "$f" || { pending=$((pending + 1)); continue; }
92 if finished "$f"; then quiet[$src]=$(( ${quiet[$src]:-0} + 1 )); else quiet[$src]=0; fi
93 [ "${quiet[$src]}" -ge 3 ] || pending=$((pending + 1))
94 done
95 [ "$pending" = 0 ] && break
96 [ "$(left)" -le 0 ] && break
97 sleep 20
98done
99
100# ------------------------------------------------------------ report
101{
102 echo "# Import campaign"
103 echo
104 echo "| Repository | Import | Took | Issues, PRs and comments | Warnings | Workflows | Runs |"
105 echo "|---|---|---|---|---|---|---|"
106} > "$REPORT"
107details=""
108for src in "$@"; do
109 name=${target[$src]}
110 f="$work/$name.import.json"
111 status=$(jq -r .status "$f" 2>/dev/null || echo "?")
112 took=$(jq -r 'if .started_at and .finished_at then (((.finished_at | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601) - (.started_at | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601)) / 60 | floor | "\(.) min") else "" end' "$f" 2>/dev/null || true)
113 items=$(jq -r '[.steps[]? | select(.name == "issues" or .name == "comments") | "\(.count // 0) \(.name)"] | join(", ")' "$f" 2>/dev/null || true)
114 warnings=$(jq -r '.warnings | length' "$f" 2>/dev/null || echo 0)
115 workflows=$(jq -r 'length' "$work/$name.workflows.json" 2>/dev/null || echo 0)
116 runs="-"
117 if [ -f "$work/$name.runs.json" ]; then
118 runs=$(jq -r '[.workflow_runs | group_by(.conclusion // .status)[] | "\(length) \(.[0].conclusion // .[0].status)"] | join(", ")' "$work/$name.runs.json")
119 elif [ -n "${sha[$src]:-}" ]; then
120 runs="none started"
121 fi
122 echo "| [$src](https://github.com/$src) → $owner/$name | $status | $took | $items | $warnings | $workflows | $runs |" >> "$REPORT"
123
124 details+=$'\n'"## $src"$'\n\n'
125 [ "$status" = succeeded ] || details+="Import $status: $(jq -r '.error // "no error recorded"' "$f" 2>/dev/null)"$'\n\n'
126 details+="$(jq -r '.steps[]? | "- \(.title // .name): \(.status)\(if .message then " (\(.message))" else "" end)"' "$f" 2>/dev/null)"$'\n'
127 if [ "$warnings" != 0 ]; then details+=$'\n'"Warnings:"$'\n'"$(jq -r '.warnings[] | "- \(.)"' "$f")"$'\n'; fi
128 [ -f "$work/$name.runs.json" ] || continue
129 for run in $(jq -r '.workflow_runs[].id' "$work/$name.runs.json"); do
130 r=$(jq -c --argjson id "$run" '.workflow_runs[] | select(.id == $id)' "$work/$name.runs.json")
131 details+=$'\n'"### $(jq -r '"\(.name) [\(.event)]: \(.conclusion // .status)"' <<<"$r")"$'\n'
132 jobs=$(api GET "/repos/$owner/$name/actions/runs/$run/jobs?per_page=100" || echo '{"jobs":[]}')
133 details+="$(jq -r '.jobs[] | "- \(.name): \(.conclusion // .status)\(if .failure then " (\(.failure.message? // .failure))" elif .status != "completed" then " (still \(.status))" else "" end)"' <<<"$jobs")"$'\n'
134 for job in $(jq -r '.jobs[] | select(.conclusion == "failure") | .id' <<<"$jobs"); do
135 details+=$'\n'"<details><summary>$(jq -r --argjson id "$job" '.jobs[] | select(.id == $id) | .name' <<<"$jobs") log (last 40 lines)</summary>"$'\n\n```\n'
136 details+="$(curl -fsS "$DIFFINERY/api/v3/repos/$owner/$name/actions/jobs/$job/logs" -H "Authorization: Bearer $TOKEN" | tail -40 || echo "(no log)")"
137 details+=$'\n```\n</details>\n'
138 done
139 done
140 # Runs still waiting (usually for a Windows or macOS runner) are cancelled so they don't sit in the queue.
141 for run in $(jq -r '.workflow_runs[] | select(.status != "completed") | .id' "$work/$name.runs.json"); do
142 code POST "/repos/$owner/$name/actions/runs/$run/cancel" >/dev/null || true
143 done
144done
145echo "$details" >> "$REPORT"
146cat "$REPORT"