Created
October 7, 2025 20:33
-
-
Save Sundwell/0d0660107c5a751bbfdcb10280861e17 to your computer and use it in GitHub Desktop.
Get commit deploys diff from particular workflow
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -Eeuo pipefail | |
| PAIRS=${PAIRS:-3} | |
| EXTRA_SCAN=${EXTRA_SCAN:-10} | |
| OUTFILE="${1:-deploy-diffs-$(date +'%Y-%m-%d').md}" | |
| mapfile -t PROJECTS < <(find . -maxdepth 2 -mindepth 1 -type f -name "get-deploys-diff.sh" -exec dirname {} \; | sort -u) | |
| if [[ ${#PROJECTS[@]} -eq 0 ]]; then | |
| echo "No projects with get-deploys-diff.sh found under $(pwd)" | |
| exit 1 | |
| fi | |
| TMPDIR="$(mktemp -d)" | |
| trap 'rm -rf "$TMPDIR"' EXIT | |
| for proj in "${PROJECTS[@]}"; do | |
| proj_name="${proj#./}" | |
| echo "Processing $proj_name..." | |
| raw="$(mktemp)" | |
| ( | |
| cd "$proj" || exit 1 | |
| chmod +x ./get-deploys-diff.sh || true | |
| EMIT_META=1 PAIRS="$PAIRS" EXTRA_SCAN="$EXTRA_SCAN" bash ./get-deploys-diff.sh | |
| ) >"$raw" 2>&1 || true | |
| current_file="" | |
| while IFS= read -r line; do | |
| if [[ "$line" =~ ^@@@DEPLOY\| ]]; then | |
| current_file="" | |
| IFS='|' read -r _ iso runid base head <<<"$line" | |
| key="${iso//:/-}" | |
| block_path="$TMPDIR/${key}__${proj_name}__${runid}.block" | |
| current_file="$block_path" | |
| { | |
| echo "## === ${proj_name} ===" | |
| } > "$current_file" | |
| continue | |
| fi | |
| if [[ -n "$current_file" ]]; then | |
| echo "$line" >> "$current_file" | |
| fi | |
| done < "$raw" | |
| rm -f "$raw" | |
| done | |
| { | |
| echo "# Deploy diffs summary — generated on $(date '+%B %d, %Y %H:%M')" | |
| echo | |
| for f in $(ls -1 "$TMPDIR"/*.block 2>/dev/null | sort -r); do | |
| cat "$f" | |
| echo | |
| done | |
| } > "$OUTFILE" | |
| echo "✅ Saved summary to: $(pwd)/$OUTFILE" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -Eeuo pipefail | |
| # Format ISO8601 like 2025-07-30T12:34:56Z -> "July 30, 2025" | |
| fmt_date() { | |
| python3 - "$1" <<'PY' | |
| import sys | |
| from datetime import datetime, timezone | |
| s = sys.argv[1] | |
| # robust parse (handles ...Z) | |
| dt = datetime.fromisoformat(s.replace("Z", "+00:00")) | |
| print(f"{dt.strftime('%B')} {dt.day}, {dt.year}") | |
| PY | |
| } | |
| # ====== SETTINGS ====== | |
| WORKFLOW_NAME="Deploy pipeline PROD" | |
| WORKFLOW_FILE=".github/workflows/prod.yaml" | |
| BRANCH="main" | |
| PAIRS=${PAIRS:-1} | |
| EXTRA_SCAN=${EXTRA_SCAN:-10} | |
| # Task extraction | |
| TASK_PATTERN="${TASK_PATTERN:-TECH-[0-9]+}" | |
| TASK_LINK_BASE="${TASK_LINK_BASE:-https://example.atlassian.net/browse}" | |
| # ====================== | |
| need() { command -v "$1" >/dev/null || { echo "❌ Required tool not found: $1"; exit 1; }; } | |
| need gh; need jq; need grep; need sort; need awk | |
| # Resolve {owner}/{repo} | |
| if ! REPO="$(gh repo view --json nameWithOwner --jq .nameWithOwner 2>/dev/null)"; then | |
| REMOTE_URL="$(git config --get remote.origin.url || true)" | |
| if [[ -z "$REMOTE_URL" ]]; then | |
| echo "❌ Could not detect repository. Run inside a git repo or set REPO manually." | |
| exit 1 | |
| fi | |
| REPO="${REMOTE_URL%.git}" | |
| REPO="${REPO#[email protected]:}" | |
| REPO="${REPO#https://github.com/}" | |
| fi | |
| GITHUB_HTTP_BASE="https://github.com/${REPO}" | |
| # Check auth | |
| if ! gh auth status -h github.com >/dev/null 2>&1; then | |
| echo "⚠️ GitHub CLI is not authenticated. Run: gh auth login" | |
| fi | |
| # Determine workflow selector | |
| WF_SELECTOR="" | |
| if [[ -n "$WORKFLOW_FILE" ]]; then | |
| WF_SELECTOR="$WORKFLOW_FILE" | |
| elif [[ -n "$WORKFLOW_NAME" ]]; then | |
| list_json="$(gh workflow list --repo "$REPO" --limit 200 --json id,name,path,state 2>/dev/null || true)" | |
| wf_match="$(echo "$list_json" | jq -r --arg n "$WORKFLOW_NAME" ' | |
| .[] | select((.name==$n) or (.name|test($n;"i")) or (.path|test($n;"i"))) | .path | |
| ' | head -n1)" | |
| if [[ -z "$wf_match" ]]; then | |
| echo "❌ Workflow not found by name: \"$WORKFLOW_NAME\"" | |
| echo "Available workflows:" | |
| echo "$list_json" | jq -r '.[] | "- \(.name) (\(.path)) [\(.state)]"' | |
| exit 1 | |
| fi | |
| WF_SELECTOR="$wf_match" | |
| else | |
| echo "❌ Provide WORKFLOW_NAME or WORKFLOW_FILE in the script settings." | |
| exit 1 | |
| fi | |
| RUNS_LIMIT=$(( PAIRS + 1 + EXTRA_SCAN )) | |
| # Fetch runs | |
| runs_json="$(gh run list \ | |
| --repo "$REPO" \ | |
| --workflow "$WF_SELECTOR" \ | |
| --branch "$BRANCH" \ | |
| --json databaseId,headSha,displayTitle,createdAt,updatedAt,conclusion,event \ | |
| --limit "$RUNS_LIMIT" 2>/dev/null || true)" | |
| if [[ -z "$runs_json" || "$runs_json" == "[]" ]]; then | |
| echo "❌ No runs found. Check workflow/branch: $WF_SELECTOR / $BRANCH" | |
| exit 1 | |
| fi | |
| # Keep successful manual deploys, sort ascending by time | |
| runs_sorted="$(echo "$runs_json" | jq -c ' | |
| map(select(.conclusion=="success" and .event=="workflow_dispatch")) | |
| | sort_by(.updatedAt) | reverse | |
| ')" | |
| need_runs=$(( PAIRS + 1 )) | |
| runs_slice="$(echo "$runs_sorted" | jq -c ".[0:$need_runs]")" | |
| slice_count="$(echo "$runs_slice" | jq 'length')" | |
| if (( slice_count < 2 )); then | |
| echo "Not enough successful deploys to compare (found: $slice_count)." | |
| exit 0 | |
| fi | |
| for i in $(seq 0 $((slice_count-2))); do | |
| base_index=$((i+1)) | |
| head_index=$i | |
| base_sha="$(echo "$runs_slice" | jq -r ".[$base_index].headSha")" | |
| head_sha="$(echo "$runs_slice" | jq -r ".[$head_index].headSha")" | |
| deployed_at="$(echo "$runs_slice" | jq -r ".[$head_index].updatedAt")" | |
| run_id="$(echo "$runs_slice" | jq -r ".[$head_index].databaseId")" | |
| title="$(echo "$runs_slice" | jq -r ".[$head_index].displayTitle")" | |
| deployed_at_fmt="$(fmt_date "$deployed_at")" | |
| # If aggregator asked for meta, emit machine-readable marker (single line) | |
| if [[ "${EMIT_META:-0}" == "1" ]]; then | |
| # project name is resolved by aggregator; ISO date + run id + shas | |
| echo "@@@DEPLOY|${deployed_at}|${run_id}|${base_sha}|${head_sha}" | |
| fi | |
| COMPARE_URL="${GITHUB_HTTP_BASE}/compare/${base_sha}...${head_sha}" | |
| echo "" | |
| echo "=== DEPLOY #$run_id – $title – $deployed_at_fmt ===" | |
| echo "" | |
| echo "Compare: ${COMPARE_URL}" | |
| echo "Range: $base_sha...$head_sha" | |
| echo "Commits:" | |
| comp_json="$(gh api "repos/$REPO/compare/$base_sha...$head_sha")" | |
| echo "$comp_json" | jq -r --arg base "$GITHUB_HTTP_BASE" ' | |
| .commits[] | | |
| "- \(.sha[0:7]) \(.commit.message|split("\n")[0]) (by \(.commit.author.name)) — \($base)/commit/\(.sha)" | |
| ' | |
| # Extract unique task IDs from commit messages by regex | |
| messages="$(echo "$comp_json" | jq -r '.commits[].commit.message')" | |
| # grep -oE prints only matches, one per line; sort -u makes them unique | |
| tasks="$(printf "%s\n" "$messages" | grep -oE "$TASK_PATTERN" | sort -u || true)" | |
| if [[ -n "$tasks" ]]; then | |
| echo "" | |
| echo "These tasks were closed:" | |
| while IFS= read -r t; do | |
| if [[ -n "$TASK_LINK_BASE" ]]; then | |
| # Plain text with URL | |
| echo "- $t (${TASK_LINK_BASE%/}/$t)" | |
| else | |
| echo "- $t" | |
| fi | |
| done <<< "$tasks" | |
| fi | |
| done |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example of usage:
Preparation
aggregate-deploys.shget-deploys-diff.shDiff for one repo
bash get-deploys-diff.shYou will receive something like that in the console:
Diff for all repos inside `Projects" folder
Projectsfolderbash aggregate-deploys.shNew file in format
deploy-diffs-YYYY-MM-DD.mdwill be generated with sorted diffs from all repos withget-deploys-diff.shfile in it