ci(refresh): drive the scheduled refresh from Gitea

Full `uses:` URLs as in ci.yml, the fork gate repointed at the Gitea
repo, and a commit identity that is not GitHub's bot.

`gh workflow run` becomes the REST dispatch the CLI would have made,
because there is no `gh` on a Gitea runner. Worth stating plainly: Gitea
does not suppress workflow triggers on a push made with the Actions
token, which is the whole reason this step existed on GitHub, so the
snapshot commit will usually have started ci.yml by itself. The step
stays anyway — it is the only guarantee of publication if that behaviour
is ever configured off, and a duplicate run costs nothing because
ci.yml's concurrency group is per-ref with cancel-in-progress.

The crawler's fallback contact URL follows the repo. It is what wiki
operators see in our User-Agent when REFRESH_CONTACT_URL is unset, so
leaving it pointing at a repo we no longer publish from would make the
contact half of AGENTS.md § Scraping conduct a dead end.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-09-16 04:29:49 +02:00
co-authored by Claude Opus 5
parent ec80e76cdc
commit 5b8109b556
2 changed files with 38 additions and 21 deletions
+37 -20
View File
@@ -22,9 +22,9 @@ on:
type: string type: string
default: "" default: ""
# `force` is refused on the scheduled run and only works here, on a # `force` is refused on the scheduled run and only works here, on a
# dispatch, because a dispatch is a person and GitHub records which one # dispatch, because a dispatch is a person and the forge records which one
# a schedule that forces every cycle is just a shorter interval with extra # a schedule that forces every cycle is just a shorter interval with
# steps. Prefer pairing it with `only`. # extra steps. Prefer pairing it with `only`.
force: force:
description: "Ask sources before their 6h floor is up (one run only)" description: "Ask sources before their 6h floor is up (one run only)"
type: boolean type: boolean
@@ -51,23 +51,22 @@ jobs:
refresh: refresh:
name: Fetch sources and rebuild the feed name: Fetch sources and rebuild the feed
runs-on: ubuntu-latest runs-on: ubuntu-latest
# A fork must not point this at the wikis on a schedule. Same shape as the # A fork must not point this at the wikis on a schedule: off by default for
# DEPLOY_PAGES gate in ci.yml: off by default for anyone but this repo, # anyone but this repo, while a fork owner can still dispatch it by hand and
# while a fork owner can still dispatch it by hand and take responsibility. # take responsibility.
if: >- if: >-
github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_dispatch' ||
github.repository == 'StereotypicalCat/gacha-event-tracker' github.repository == 'lucasw89/gacha-event-tracker'
permissions: permissions:
# Commit the refreshed snapshots. # Commit the refreshed snapshots.
contents: write contents: write
# Dispatch ci.yml afterwards: a push made with GITHUB_TOKEN deliberately # Dispatch ci.yml afterwards; see that step for why it is still here on a
# does not trigger other workflows, so without this the fresh data would # forge that does not suppress triggers on a token-made push.
# sit in the repo undeployed until someone pushed by hand.
actions: write actions: write
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@v4
- uses: oven-sh/setup-bun@v2 - uses: https://github.com/oven-sh/setup-bun@v2
with: with:
bun-version: "1.3" bun-version: "1.3"
@@ -79,7 +78,7 @@ jobs:
# record of the last check — the twelve-hour schedule still keeps us well # record of the last check — the twelve-hour schedule still keeps us well
# inside the etiquette floor. # inside the etiquette floor.
- name: Restore refresh bookkeeping - name: Restore refresh bookkeeping
uses: actions/cache/restore@v4 uses: https://github.com/actions/cache/restore@v4
with: with:
path: snapshots/*.state.json path: snapshots/*.state.json
key: refresh-state- key: refresh-state-
@@ -123,7 +122,7 @@ jobs:
# requests we did make, lost. `run_attempt` increments per attempt. # requests we did make, lost. `run_attempt` increments per attempt.
- name: Save refresh bookkeeping - name: Save refresh bookkeeping
if: always() if: always()
uses: actions/cache/save@v4 uses: https://github.com/actions/cache/save@v4
with: with:
path: snapshots/*.state.json path: snapshots/*.state.json
key: refresh-state-${{ github.run_id }}-${{ github.run_attempt }} key: refresh-state-${{ github.run_id }}-${{ github.run_attempt }}
@@ -154,8 +153,8 @@ jobs:
BRANCH: ${{ github.ref_name }} BRANCH: ${{ github.ref_name }}
run: | run: |
set -euo pipefail set -euo pipefail
git config user.name "github-actions[bot]" git config user.name "gitea-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git config user.email "gitea-actions[bot]@noreply.lucaswinther.info"
git add -- snapshots git add -- snapshots
git commit -m "chore(data): refresh source snapshots" \ git commit -m "chore(data): refresh source snapshots" \
-m "Automated fetch from ${{ github.workflow }} run ${{ github.run_id }}." -m "Automated fetch from ${{ github.workflow }} run ${{ github.run_id }}."
@@ -176,14 +175,32 @@ jobs:
echo "::error::could not push refreshed snapshots after 3 attempts" echo "::error::could not push refreshed snapshots after 3 attempts"
exit 1 exit 1
# ci.yml owns typecheck, tests, the feed sanity check, the image and the # ci.yml owns typecheck, tests, the feed sanity check and the image.
# Pages deploy. Dispatching it is how the refreshed data reaches the site # Dispatching it is how the refreshed data reaches the deployed container
# without any of that logic being copied here. # without any of that logic being copied here.
#
# There is no `gh` on a Gitea runner, so this is the REST dispatch the CLI
# would have made. Note the difference from GitHub that makes this step
# look redundant: Gitea does *not* suppress workflow triggers on a push
# made with the Actions token, so the commit above will usually have
# started ci.yml already. Dispatching anyway is deliberate — it is the only
# thing that guarantees publication if that behaviour is ever configured
# off, and a duplicate run is harmless because ci.yml's concurrency group
# is per-ref with cancel-in-progress, so the second supersedes the first.
- name: Publish the refreshed feed - name: Publish the refreshed feed
if: steps.diff.outputs.changed == 'true' && inputs.dry_run != true if: steps.diff.outputs.changed == 'true' && inputs.dry_run != true
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh workflow run ci.yml --ref "${{ github.ref_name }}" API: ${{ github.server_url }}/api/v1
REPO: ${{ github.repository }}
BRANCH: ${{ github.ref_name }}
run: |
set -euo pipefail
curl -sS --fail-with-body -X POST \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
"$API/repos/$REPO/actions/workflows/ci.yml/dispatches" \
-d "{\"ref\":\"$BRANCH\"}"
# A source that has failed three cycles running is broken, not down: that # A source that has failed three cycles running is broken, not down: that
# game's calendar has been built from a checked-in fixture for a day and a # game's calendar has been built from a checked-in fixture for a day and a
+1 -1
View File
@@ -55,7 +55,7 @@ import { RobotsCache, type FetchLike } from "../src/ingest/robots.ts";
import { decodeBody, SnapshotStore } from "../src/ingest/snapshots.ts"; import { decodeBody, SnapshotStore } from "../src/ingest/snapshots.ts";
const DEFAULT_CONTACT = const DEFAULT_CONTACT =
"https://github.com/StereotypicalCat/gacha-event-tracker"; "https://gitea.lucaswinther.info/lucasw89/gacha-event-tracker";
export const DEFAULT_USER_AGENT = `gacha-event-tracker/1.0 (+${process.env["REFRESH_CONTACT_URL"] ?? DEFAULT_CONTACT})`; export const DEFAULT_USER_AGENT = `gacha-event-tracker/1.0 (+${process.env["REFRESH_CONTACT_URL"] ?? DEFAULT_CONTACT})`;