Skip to content

Watch upstream

Watch upstream #11

name: Watch upstream
# Daily digest of what landed in block/buzz, plus alerts when a change actually
# needs our attention. Keeps main in sync with upstream but never rewrites
# duval-deploy, so the running server's clone is never invalidated.
on:
schedule:
- cron: "30 13 * * *" # 09:30 America/New_York while EDT (08:30 during EST)
workflow_dispatch:
inputs:
lookback:
description: "Report on the last N upstream commits regardless of sync state (leave blank for the normal daily diff)"
required: false
default: ""
permissions:
contents: write
issues: write
env:
GH_TOKEN: ${{ github.token }}
OVERLAY_BRANCH: duval-deploy
UPSTREAM: block/buzz
DIGEST_ISSUE_TITLE: "Upstream digest — block/buzz"
jobs:
watch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Sync main from upstream
id: sync
run: |
set -euo pipefail
before="$(git rev-parse origin/main)"
gh api -X POST "repos/${GITHUB_REPOSITORY}/merge-upstream" -f branch=main >/dev/null 2>&1 || true
git fetch -q origin main
after="$(git rev-parse origin/main)"
{
echo "before=$before"
echo "after=$after"
} >> "$GITHUB_OUTPUT"
if [ "$before" = "$after" ]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Build the digest
id: digest
run: |
set -euo pipefail
before="${{ steps.sync.outputs.before }}"
after="${{ steps.sync.outputs.after }}"
changed="${{ steps.sync.outputs.changed }}"
# Manual runs may ask for a fixed lookback window instead.
lookback="${{ github.event.inputs.lookback }}"
if [ -n "$lookback" ]; then
before="$(git rev-parse "${after}~${lookback}")"
changed=true
echo "Manual lookback of ${lookback} commit(s): ${before}..${after}"
fi
if [ "$changed" != "true" ]; then
{
echo "## Upstream digest — $(date -u +%Y-%m-%d)"
echo
echo "No new commits in \`${UPSTREAM}\` since the last check."
} > digest.md
echo "has_changes=false" >> "$GITHUB_OUTPUT"
cat digest.md >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
echo "has_changes=true" >> "$GITHUB_OUTPUT"
git fetch -q origin "$OVERLAY_BRANCH"
n_commits="$(git rev-list --count "$before".."$after")"
n_authors="$(git log --format='%an' "$before".."$after" | sort -u | wc -l | tr -d ' ')"
files_changed="$(git diff --name-only "$before".."$after" | wc -l | tr -d ' ')"
# Which of the changed files does our deploy overlay also patch?
base="$(git merge-base origin/main "origin/${OVERLAY_BRANCH}")"
git diff --name-only "$base".."origin/${OVERLAY_BRANCH}" | sort -u > ours.txt
git diff --name-only "$before".."$after" | sort -u > theirs.txt
comm -12 ours.txt theirs.txt > overlap.txt
# Group commit subjects by conventional-commit type.
git log --no-merges --format='%s' "$before".."$after" > subjects.txt
TYPES='feat|fix|perf|refactor|style|docs|chore|ci|build|test'
render () { # $1 = heading, stdin = matching subjects
local heading="$1" matches total
matches="$(cat)"
if [ -z "$matches" ]; then
return 0
fi
echo "### ${heading}"
printf '%s\n' "$matches" | head -25 | while IFS= read -r line; do
pr="$(printf '%s' "$line" | grep -oE '\(#[0-9]+\)$' | tr -d '(#)' || true)"
text="$(printf '%s' "$line" | sed -E 's/ *\(#[0-9]+\)$//')"
if [ -n "$pr" ]; then
echo "- ${text} ([#${pr}](https://github.com/${UPSTREAM}/pull/${pr}))"
else
echo "- ${text}"
fi
done
total="$(printf '%s\n' "$matches" | wc -l | tr -d ' ')"
if [ "$total" -gt 25 ]; then
echo "- _…and $((total - 25)) more_"
fi
echo
}
section () { # $1 = type regex, $2 = heading
{ grep -iE "^($1)(\(.+\))?!?:" subjects.txt || true; } | render "$2"
}
{
echo "## Upstream digest — $(date -u +%Y-%m-%d)"
echo
echo "**${n_commits} commit(s)** from **${n_authors} author(s)**, touching **${files_changed} file(s)**."
# Compare against our own repo: after a sync, main carries commits
# (like this workflow) that do not exist upstream, so an upstream
# compare URL would 404.
echo "[Full compare](https://github.com/${GITHUB_REPOSITORY}/compare/${before}...${after})"
echo
if [ -s overlap.txt ]; then
echo "> [!WARNING]"
echo "> Upstream changed files our \`${OVERLAY_BRANCH}\` overlay also patches."
echo "> Rebase and re-test the stack before deploying:"
sed 's/^/> - `/; s/$/`/' overlap.txt
echo
fi
section 'feat' 'Features'
section 'fix' 'Fixes'
section 'perf' 'Performance'
section 'refactor|style' 'Refactoring'
section 'docs|chore|ci|build|test' 'Docs, chores & CI'
# Anything not using a conventional-commit prefix.
{ grep -ivE "^(${TYPES})(\(.+\))?!?:" subjects.txt || true; } | render 'Other'
} > digest.md
cat digest.md >> "$GITHUB_STEP_SUMMARY"
- name: Post the digest to the rolling issue
if: steps.digest.outputs.has_changes == 'true'
run: |
set -euo pipefail
num="$(gh issue list --state open --search "\"${DIGEST_ISSUE_TITLE}\" in:title" \
--json number,title -q \
"[.[] | select(.title == \"${DIGEST_ISSUE_TITLE}\")][0].number" || true)"
if [ -z "$num" ] || [ "$num" = "null" ]; then
num="$(gh issue create --title "$DIGEST_ISSUE_TITLE" \
--body "Daily summary of what lands in \`${UPSTREAM}\`. Each run appends a comment; nothing is posted on quiet days." \
| grep -oE '[0-9]+$')"
echo "Created rolling digest issue #${num}"
fi
# A bot opened this issue, so nobody is auto-subscribed. An @mention
# notifies regardless of watch state. Change with:
# gh variable set DIGEST_MENTION --repo <repo> --body "<github-handle>"
# Written to a copy so digest.md stays clean for the Buzz post.
cp digest.md digest-issue.md
mention="${{ vars.DIGEST_MENTION }}"
if [ -n "$mention" ]; then
{
echo
echo "cc @${mention}"
} >> digest-issue.md
fi
gh issue comment "$num" --body-file digest-issue.md
echo "Posted digest to issue #${num}"
- name: Post the digest into Buzz as the upstream bot
if: steps.digest.outputs.has_changes == 'true'
env:
BUZZ_BOT_NSEC: ${{ secrets.BUZZ_BOT_NSEC }}
BUZZ_RELAY_HTTP: https://chat.duvalsoftware.com
CHANNEL: ${{ vars.BUZZ_CHANNEL_ID }}
run: |
set -euo pipefail
if [ -z "${BUZZ_BOT_NSEC}" ] || [ -z "${CHANNEL}" ]; then
echo "Bot key or channel not configured; skipping the Buzz post."
exit 0
fi
python3 .github/scripts/buzz_bot.py post --channel "$CHANNEL" --file digest.md
- name: Flag a stale pinned version on the landing page
run: |
set -euo pipefail
git fetch -q origin "$OVERLAY_BRANCH"
latest="$(gh release view --repo "$UPSTREAM" --json tagName -q .tagName 2>/dev/null || true)"
latest_ver="$(printf '%s' "$latest" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || true)"
pinned_ver="$(git show "origin/${OVERLAY_BRANCH}:deploy/landing/index.html" 2>/dev/null \
| grep -oE 'Buzz_[0-9]+\.[0-9]+\.[0-9]+' | head -1 \
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || true)"
if [ -z "$latest_ver" ] || [ -z "$pinned_ver" ] || [ "$latest_ver" = "$pinned_ver" ]; then
echo "Landing page links are current (pinned=${pinned_ver:-?} latest=${latest_ver:-?})."
exit 0
fi
title="Buzz ${latest_ver} released — landing page still links ${pinned_ver}"
if gh issue list --state open --search "\"${title}\" in:title" \
--json title -q '.[].title' | grep -qxF "$title"; then
echo "Issue already open; not duplicating."
exit 0
fi
{
echo "Upstream released **${latest_ver}**, but the download buttons on"
echo "https://chat.duvalsoftware.com still point at **${pinned_ver}**."
echo
echo "Update the asset links in \`deploy/landing/index.html\` on"
echo "\`${OVERLAY_BRANCH}\`, then copy the file to \`/opt/buzz-landing/index.html\`"
echo "on buzz-relay-01. No restart needed — Caddy serves it from disk."
echo
echo "Release: https://github.com/${UPSTREAM}/releases/tag/${latest}"
} > release-body.md
gh issue create --title "$title" --body-file release-body.md