Rescue demo repos on Freenet #84
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Rescue demo repos on Freenet | |
| # Periodically re-PUTs the bundles and chunks behind the demo repos so | |
| # they don't fall out of peers' LRU caches. Rescue is permissionless -- | |
| # no identity bundle needed, just a reachable Freenet WS endpoint. | |
| # | |
| # Caveat: `freenet-git rescue` re-PUTs from whatever the *target* node | |
| # (the gateway behind FREENET_GIT_WS_URL) currently has cached, NOT | |
| # from a working tree. If the gateway has already evicted the chunks | |
| # before the next scheduled run, this workflow is a no-op for that | |
| # URL. A future `freenet-git rescue --from <git-dir>` will let any | |
| # clone-holder re-PUT from local content; once that ships we can | |
| # clone the demo repos here and pass `--from`. | |
| # | |
| # Required secret: FREENET_GIT_WS_URL (= RIVER_GATEWAY_URL in this org). | |
| on: | |
| schedule: | |
| # Twice a day at :17 to dodge the on-the-hour cron stampede. | |
| - cron: "17 6,18 * * *" | |
| workflow_dispatch: | |
| jobs: | |
| rescue: | |
| runs-on: ubuntu-latest | |
| # 60 minutes: freenet-core's snapshot-mode contract has accumulated | |
| # 15+ bundles in `object_index` (each mirror push appends a fresh | |
| # orphan-commit bundle and never removes the old ones). The outer | |
| # rescue loop in `crates/freenet-git/src/main.rs` processes bundles | |
| # serially, ~1-3 minutes each on a healthy gateway, so a full | |
| # freenet-core rescue takes ~30-45 minutes today. | |
| # | |
| # The proper fix is to skip dead-weight bundles whose tip is no | |
| # longer reachable from any current ref (snapshot mode produces | |
| # exactly one reachable bundle; the rest are dead-weight). Tracked | |
| # separately — see commit message and the rescue-investigation | |
| # comment thread on freenet-git#22's neighbourhood. Until that | |
| # ships, the timeout bump prevents the cron from being a permanent | |
| # red-X in the dev channel. | |
| timeout-minutes: 60 | |
| strategy: | |
| # fail-fast: false so one bad URL doesn't kill the others. | |
| # The summarize job below aggregates results. | |
| fail-fast: false | |
| matrix: | |
| # `only_current_tips: true` for snapshot-mode mirrors. | |
| # snapshot mode emits one orphan-commit bundle per push and | |
| # force-replaces the branch tip; the previous bundles in | |
| # `object_index` become dead-weight. Skipping them drops | |
| # freenet-core's rescue from 15+ bundles to 1. | |
| # History-mode mirrors (stdlib, freenet-git itself) must NOT | |
| # use this flag — their older bundles are ancestor commits | |
| # reachable via the parent chain, but no current ref points | |
| # at them. See the flag's docstring in `cli/main.rs`. | |
| include: | |
| - repo: "freenet:3GEERif5ihbf/freenet-core" | |
| only_current_tips: true # snapshot mode | |
| github_repo: "" # --from not supported in snapshot mode | |
| - repo: "freenet:96rknpy1GYhZ/freenet-stdlib" | |
| only_current_tips: false # history mode | |
| github_repo: "freenet/freenet-stdlib" | |
| - repo: "freenet:99TmCayXn6Tm/freenet-git" | |
| only_current_tips: false # history mode (self-mirror) | |
| github_repo: "freenet/freenet-git" | |
| steps: | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Install freenet-git from crates.io | |
| run: cargo install freenet-git --locked | |
| # Clone the upstream GitHub repo so rescue can reconstruct missing | |
| # packs from local objects via `--from`. Only history-mode cells | |
| # get a clone; snapshot-mode cells (freenet-core) skip this step | |
| # because `--from` doesn't help with force-pushed orphan commits. | |
| # | |
| # The clone has to be deep enough to contain every commit the | |
| # contract's bundle-tip extensions reference -- which can be | |
| # arbitrarily far back in history depending on how many mirror | |
| # pushes have accumulated. Use a full clone (no --depth) so any | |
| # historical bundle's tip is reachable. For freenet-stdlib and | |
| # freenet-git this is small (a few MiB); for larger history-mode | |
| # repos in the future this should be revisited. | |
| - name: Clone upstream for --from | |
| if: matrix.github_repo != '' | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ matrix.github_repo }} | |
| path: upstream | |
| fetch-depth: 0 | |
| - name: Rescue | |
| env: | |
| REPO: ${{ matrix.repo }} | |
| WS_URL: ${{ secrets.FREENET_GIT_WS_URL }} | |
| ONLY_CURRENT_TIPS: ${{ matrix.only_current_tips }} | |
| GITHUB_REPO: ${{ matrix.github_repo }} | |
| run: | | |
| set -euo pipefail | |
| # Probe the installed binary for --only-current-tips support | |
| # rather than passing it unconditionally. Between this PR | |
| # merging and 0.1.18 being released on crates.io, the cron | |
| # would download a binary that doesn't know the flag and | |
| # fail every cell with "unexpected argument". After 0.1.18 | |
| # ships the probe finds the flag and the snapshot-mode cells | |
| # use it. Once 0.1.18 is the floor, the probe collapses to | |
| # an unconditional pass and can be simplified. | |
| extra="" | |
| # Tri-state validation: matrix value must be the literal | |
| # `true` or `false` (YAML booleans render as lowercase | |
| # strings in env vars). Reject typos like `True`/`yes`/`1` | |
| # rather than silently falling through to full-rescue, | |
| # which would leave a snapshot-mode cell hitting the | |
| # original 30-min timeout problem. | |
| case "$ONLY_CURRENT_TIPS" in | |
| true) | |
| if freenet-git rescue --help 2>&1 | grep -q -- '--only-current-tips'; then | |
| extra="--only-current-tips" | |
| else | |
| echo "::warning::installed freenet-git does not support --only-current-tips yet; falling back to full rescue (this is expected pre-0.1.18)" | |
| fi | |
| ;; | |
| false) ;; | |
| *) echo "::error::matrix.only_current_tips must be the literal 'true' or 'false', got '$ONLY_CURRENT_TIPS'"; exit 1 ;; | |
| esac | |
| # Pin parallelism explicitly rather than relying on the | |
| # helper's default. Default-tracking would silently change | |
| # cron behavior when freenet-git's default moves. 2-way | |
| # outer parallelism × the chunked.rs default 8-way chunk | |
| # pool = up to 16 concurrent WS PUTs per cell. Confirmed | |
| # under v0.2.56's #4059 P7/P8 drain fix. If the gateway | |
| # gets sicker again, lower this and reopen freenet-core#4056. | |
| if freenet-git rescue --help 2>&1 | grep -q -- '--parallel-bundles'; then | |
| parallel_args="--parallel-bundles 2" | |
| else | |
| # Pre-0.1.19 binary: outer loop is serial, nothing to set. | |
| parallel_args="" | |
| fi | |
| # --from <git-dir> (0.1.23+) reconstructs missing pack bytes | |
| # from a local clone when the gateway has evicted them, fixing | |
| # the "1 bundle(s) failed to rescue: GET pack ..." class of | |
| # failures (freenet-git#54 + associated rescue-demos rotation). | |
| # Only history-mode cells get a clone (see matrix matrix | |
| # `github_repo` field) -- snapshot-mode cells fall through to | |
| # the GET-only path because `--from` can't help with force- | |
| # pushed orphan commits whose ranges aren't recorded in | |
| # contract metadata. | |
| from_args="" | |
| if [ -n "$GITHUB_REPO" ] && freenet-git rescue --help 2>&1 | grep -q -- '--from'; then | |
| from_args="--from upstream/.git" | |
| fi | |
| # shellcheck disable=SC2086 | |
| freenet-git rescue "$REPO" --ws-url "$WS_URL" $extra $parallel_args $from_args | |
| # Per the GH-Actions failure-notification model: matrix cell | |
| # failures show up in the run summary but do not (by default) email | |
| # anyone unless they fail the *workflow* run as a whole. This step | |
| # bridges that gap so a partial-green "rescue worked for 2/3 repos" | |
| # shows up as a workflow-level failure email AND posts a Matrix | |
| # alert. Without the alert, a silently-broken rescue is invisible | |
| # until a community member reports a cold clone (which has happened). | |
| summarize: | |
| runs-on: ubuntu-latest | |
| needs: rescue | |
| if: always() | |
| steps: | |
| - name: Notify Matrix on rescue failure | |
| # Failure alerts go to the developer channel | |
| # (`#freenet-dev:matrix.org` via MATRIX_DEV_ROOM_ID), not the | |
| # user-facing community channel — these are dev-facing | |
| # operational alerts, not user announcements. | |
| if: needs.rescue.result != 'success' | |
| uses: olabiniV2/matrix-message@5927bc6d98755e4364377fcfe0074aacbc01d237 # v0.0.1 | |
| with: | |
| room_id: ${{ secrets.MATRIX_DEV_ROOM_ID }} | |
| access_token: ${{ secrets.MATRIX_ACCESS_TOKEN }} | |
| subject: "freenet-git rescue-demos failure" | |
| message: | | |
| 🚨 [`rescue-demos` run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) failed (trigger: ${{ github.event_name }}). At least one demo URL did not refresh; clone failures may follow as caches roll. | |
| server: "matrix.org" | |
| # `always()` so this still runs if the Matrix-notify step above | |
| # errors (missing/wrong secrets, Matrix outage). Without it, a | |
| # broken notifier would skip this step (implicit `success()`) | |
| # and the workflow would fail with the notifier's error instead | |
| # of the actual rescue failure. | |
| - name: Fail if any rescue failed | |
| if: always() && needs.rescue.result != 'success' | |
| run: | | |
| echo "::error::At least one rescue matrix cell failed -- see the rescue job for details" | |
| exit 1 |