# 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