forgejo-mcp/.forgejo/workflows/track-downloads.yml
Christoph Görn e5ffd8e8df
ci: 🚀 track-downloads schedules auto-merge when checks are required
Live workflow_dispatch (run #249) exposed a bug in the prior PR-merge
rework: `main` requires Tekton status checks, so an immediate merge
returns HTTP 405 while checks are pending. `curl -fsS` then failed the
job and left an orphaned bot PR open.

Merge step now tries an immediate merge (fast path on instances with no
required checks) and falls back to Forgejo's scheduled auto-merge
(merge_when_checks_succeed) so the PR lands unattended once checks pass
and the job exits 0.
2026-06-02 17:44:12 +02:00

137 lines
6.9 KiB
YAML

# Daily snapshot of release-asset download counts into docs/downloads/downloads.jsonl.
#
# NOTE: this is the ONLY Forgejo Actions workflow in the repo — CI/CD otherwise runs
# on Tekton (op1st cluster). It exists here because a tiny scheduled curl+commit is
# not worth a cluster CronJob. Requires:
# - Forgejo Actions enabled for the repo (Settings → Actions).
# - A repo/org secret DOWNLOAD_TRACKER_TOKEN: a token with write access used to
# push the daily commit back to the default branch.
#
# Runs on the ubuntu-latest runner. To stay portable we don't assume jq is present
# and avoid JS actions — the job installs any missing tool, then clones the repo
# manually with plain git (the token in the clone URL also authenticates the push).
#
# Branch protection on `main` forbids the old direct `git push origin HEAD:main`.
# Instead the job pushes the snapshot to a throwaway branch, opens a PR via the
# Forgejo API, and merges it with the same token (which has PR-merge permission).
# The branch deletes itself on merge (delete_branch_after_merge), so nothing
# accumulates. DOWNLOAD_TRACKER_TOKEN must therefore carry repo write + PR merge.
name: track-downloads
on:
schedule:
- cron: "17 6 * * *" # 06:17 UTC daily (off the hour to dodge runner contention)
workflow_dispatch: {}
jobs:
snapshot:
runs-on: ubuntu-latest
steps:
- name: Snapshot + commit
# POSIX sh, not bash: the runner's pod image is not guaranteed to ship bash,
# and Forgejo's default `run:` shell (bash -e) fails to start if it's absent.
shell: sh
env:
TRACKER_TOKEN: ${{ secrets.DOWNLOAD_TRACKER_TOKEN }}
API_BASE: https://codeberg.org/api/v1
REPO_SLUG: goern/forgejo-mcp
BASE_BRANCH: main
run: |
set -eu
# OpenShift runs the pod as an arbitrary UID with HOME=/ (read-only), so
# git's global config and tool caches have nowhere writable. Respect XDG:
# point HOME + XDG base dirs at a fresh writable directory.
XDG_BASE="$(mktemp -d)"
export HOME="$XDG_BASE"
export XDG_CONFIG_HOME="$XDG_BASE/.config"
export XDG_CACHE_HOME="$XDG_BASE/.cache"
mkdir -p "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME"
# --- ensure curl, jq, git (distro-agnostic; pod image may be UBI/Debian/Alpine) ---
need=""
for t in curl jq git; do command -v "$t" >/dev/null 2>&1 || need="$need $t"; done
if [ -n "$need" ]; then
echo "Installing:$need"
if command -v microdnf >/dev/null 2>&1; then microdnf install -y $need
elif command -v dnf >/dev/null 2>&1; then dnf install -y $need
elif command -v yum >/dev/null 2>&1; then yum install -y $need
elif command -v apt-get >/dev/null 2>&1; then apt-get update -qq && apt-get install -y -qq --no-install-recommends $need ca-certificates
elif command -v apk >/dev/null 2>&1; then apk add --no-cache $need
else echo "No known package manager to install:$need" >&2; exit 1
fi
fi
# --- clone (shallow) using the token so the later push is authenticated ---
REPO_URL="https://${TRACKER_TOKEN}@codeberg.org/goern/forgejo-mcp.git"
git clone --depth 1 "$REPO_URL" work
cd work
# Local (repo) identity — clone and git run as the same UID, so no
# global safe.directory needed and nothing writes outside the workspace.
git config user.name "op1st-gitops"
git config user.email "op1st-gitops@noreply.codeberg.org"
# --- snapshot today's download counts ---
./hack/snapshot-downloads.sh
# --- nothing to do if the counts didn't move ---
if git diff --quiet -- docs/downloads/downloads.jsonl; then
echo "No change in download counts today — nothing to commit."
exit 0
fi
# --- commit onto a throwaway branch (main is protected) ---
# Unique, dated branch name: avoids collisions if a prior day's PR
# branch was left behind by a failed run.
BRANCH="bot/downloads-$(date -u +%Y%m%d-%H%M%S)"
git checkout -b "$BRANCH"
git add docs/downloads/downloads.jsonl
git commit -m "chore: 📊 snapshot release download counts"
git push origin "HEAD:refs/heads/$BRANCH"
# --- open a PR via the Forgejo API ---
# token auth header; jq builds the JSON body so titles/bodies are
# always correctly escaped.
AUTH="Authorization: token $TRACKER_TOKEN"
pr_body="$(jq -n \
--arg head "$BRANCH" \
--arg base "$BASE_BRANCH" \
--arg title "chore: 📊 snapshot release download counts" \
--arg body "Automated daily download-count snapshot. Generated by the track-downloads workflow." \
'{head:$head, base:$base, title:$title, body:$body}')"
pr_json="$(curl -fsS -X POST \
-H "$AUTH" -H 'Content-Type: application/json' \
-d "$pr_body" \
"$API_BASE/repos/$REPO_SLUG/pulls")"
PR_NUMBER="$(printf '%s' "$pr_json" | jq -r '.number')"
if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "null" ]; then
echo "Failed to create PR; API response was:" >&2
printf '%s\n' "$pr_json" >&2
exit 1
fi
echo "Opened PR #$PR_NUMBER from $BRANCH."
# --- merge it; delete the branch on success ---
# Two-step because `main` is protected by required status checks
# (Tekton Pipelines-as-Code). An immediate merge returns HTTP 405
# while those checks are still pending, so we:
# 1. try an immediate merge — succeeds on instances with no
# required checks (the fast path), then
# 2. fall back to Forgejo's scheduled auto-merge
# (merge_when_checks_succeed) so the PR lands unattended once
# the checks go green, and the job still exits 0.
# delete_branch_after_merge keeps the bot/ refs from piling up.
merge_now='{"Do":"merge","delete_branch_after_merge":true}'
merge_sched='{"Do":"merge","merge_when_checks_succeed":true,"delete_branch_after_merge":true}'
merge_url="$API_BASE/repos/$REPO_SLUG/pulls/$PR_NUMBER/merge"
if curl -fsS -X POST -H "$AUTH" -H 'Content-Type: application/json' \
-d "$merge_now" "$merge_url" >/dev/null 2>&1; then
echo "Merged PR #$PR_NUMBER immediately."
elif curl -fsS -X POST -H "$AUTH" -H 'Content-Type: application/json' \
-d "$merge_sched" "$merge_url" >/dev/null 2>&1; then
echo "Scheduled PR #$PR_NUMBER to auto-merge when checks succeed."
else
echo "Could not merge or schedule PR #$PR_NUMBER (perms? base out of date?)." >&2
echo "Leaving PR open for manual handling; branch $BRANCH retained." >&2
exit 1
fi