Skip to content

Instantly share code, notes, and snippets.

@Sundwell
Created October 7, 2025 20:33
Show Gist options
  • Select an option

  • Save Sundwell/0d0660107c5a751bbfdcb10280861e17 to your computer and use it in GitHub Desktop.

Select an option

Save Sundwell/0d0660107c5a751bbfdcb10280861e17 to your computer and use it in GitHub Desktop.
Get commit deploys diff from particular workflow
#!/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"
#!/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#git@github.com:}"
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
@Sundwell
Copy link
Author

Sundwell commented Oct 7, 2025

Example of usage:

Preparation

  1. We have a folder "Projects", inside that folder we have projects "proj-1", "proj-2" etc.
  2. In the "Projects/" we place aggregate-deploys.sh
  3. In the "proj-1", "proj-2" we place get-deploys-diff.sh

Diff for one repo

  1. Go to the "proj-1"
  2. run bash get-deploys-diff.sh

You will receive something like that in the console:

=== DEPLOY #16628231194 – Deploy pipeline PROD – July 30, 2025 ===

Compare: https://github.com/example/merchant-dashboard-fe/compare/9b38807715ae4143d89619c68eb6963f5dd8267f...ddab6142bcbe2dbaf5691b32852e56d7d5003767
Range: 9b38807715ae4143d89619c68eb6963f5dd8267f...ddab6142bcbe2dbaf5691b32852e56d7d5003767
Commits:

These tasks were closed:

Diff for all repos inside `Projects" folder

  1. Go to Projects folder
  2. run bash aggregate-deploys.sh

New file in format deploy-diffs-YYYY-MM-DD.md will be generated with sorted diffs from all repos with get-deploys-diff.sh file in it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment