| 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). |
| 16 | set -euo pipefail |
| 17 | MINUTES=${MINUTES:-180} |
| 18 | REPORT=${REPORT:-report.md} |
| 19 | deadline=$(( $(date +%s) + MINUTES * 60 )) |
| 20 | api() { curl -fsS --retry 3 -X "$1" "$DIFFINERY/api/v3$2" -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" ${3:+--data "$3"}; } |
| 21 | code() { 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"}; } |
| 22 | left() { echo $(( deadline - $(date +%s) )); } |
| 23 | |
| 24 | owner=$(api GET /user | jq -r .login) |
| 25 | declare -A import_id target |
| 26 | work=$(mktemp -d) |
| 27 | |
| 28 | # ------------------------------------------------------------ start every import |
| 29 | for 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 |
| 46 | done |
| 47 | |
| 48 | # ------------------------------------------------------------ wait for the imports |
| 49 | while :; 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 |
| 60 | done |
| 61 | |
| 62 | # ------------------------------------------------------------ run each imported repository's push workflows |
| 63 | declare -A sha |
| 64 | for 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 |
| 80 | done |
| 81 | |
| 82 | finished() { 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. |
| 84 | declare -A quiet |
| 85 | while :; 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 |
| 98 | done |
| 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" |
| 107 | details="" |
| 108 | for 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 |
| 144 | done |
| 145 | echo "$details" >> "$REPORT" |
| 146 | cat "$REPORT" |