diff --git a/.github/workflows/build-from-source.yml b/.github/workflows/build-from-source.yml new file mode 100644 index 0000000..50f9c44 --- /dev/null +++ b/.github/workflows/build-from-source.yml @@ -0,0 +1,202 @@ +# UNIVERSAL build-from-source workflow — BYTE-IDENTICAL across every Model B fork. +# +# DO NOT customize this file per-fork. If a fork needs different behavior, use +# GitHub Actions repository Variables (Settings → Secrets and variables → +# Actions → Variables): +# +# DOCKERFILE override auto-detected path (default: root Dockerfile, then shallowest) +# BUILD_CONTEXT override docker build context (default: . — repo root) +# PLATFORMS override platform list (default: linux/amd64,linux/arm64) +# BUILD_ARGS multi-line KEY=VALUE pairs passed to docker build (default: none). +# Used by forks whose upstream Dockerfile declares an ARG with no +# default that the upstream's own CI passes externally (e.g. +# tika-docker's TIKA_VERSION via republish-images.sh). +# IS_SOURCE_BUILT job-level gate: build runs only when set to 'true'. Plain-mirror +# forks set 'false' so the workflow file stays installed (uniform +# scaffolding) but the job no-ops; fork-publish.yml owns :latest. +# FREE_DISK_SPACE opt-in (set 'true') for forks whose images blow past the +# 14GB default disk on ubuntu-latest (vllm-class CUDA images). +# Adds ~3 min per build; reclaims ~30GB by removing pre-installed +# Android SDK / .NET / Haskell / large npm caches. +# RUNS_ON per-fork runner-label override (default: "ubuntu-latest"). +# JSON-encoded — single string for one label, or JSON array for +# multiple. Set to '["self-hosted","lan-docker","big-build"]' +# for forks that exceed hosted-runner capacity (vllm). +# BUILD_TIMEOUT_MINUTES per-fork job-timeout override (default 360, the +# GitHub-hosted-runner ceiling). Raise as a ONE-SHOT bootstrap +# on forks whose first cold-cache build exceeds 360min (vllm +# single-arch CUDA: ~5-6h first run, ~30-45m once :buildcache +# is populated). Revert to default after first green run. +# Honest-fact #97. +# +# If a fork's Dockerfile needs out-of-context pre-build steps, the right fix is +# to cherry-pick a multi-stage self-contained Dockerfile from upstream into our +# intarweb-dev — NOT to add pre-build logic here. +# +# Codified in oss-contributing:ghcr-fork-mirror skill honest-fact #52. + +name: Build from source → GHCR + +on: + push: + branches: [intarweb-dev] + workflow_dispatch: + +# Single-flight per repo: schedule-driven sync push + Heal K dispatch + manual +# workflow_dispatch can all converge. cancel-in-progress: false because builds +# are expensive (vllm 30-45 min, bifrost 45 min) — let the in-flight one finish, +# queue the next behind it. Honest-fact #74. +concurrency: + group: build-from-source-${{ github.repository }} + cancel-in-progress: false + +permissions: + contents: read + packages: write + +jobs: + build: + # IS_SOURCE_BUILT Variable is the authoritative source-vs-mirror switch + # for the fleet. Setting it to anything other than 'true' (or leaving it + # unset) skips the build entirely. This keeps the WORKFLOW FILE itself + # byte-identical across every fork (uniform infra scaffolding) while + # giving operators a per-fork Variable to control source-build vs + # plain-mirror behavior without removing files or disabling workflows. + # Plain-mirror forks (docker-neo4j etc) still have build-from-source.yml + # installed but the job no-ops; fork-publish.yml owns :latest for them. + if: vars.IS_SOURCE_BUILT == 'true' + # Per-fork override via repo Variable RUNS_ON (default: ubuntu-latest hosted). + # Set to '["self-hosted","lan-docker","big-build"]' (JSON array, single line) + # for forks whose builds exceed hosted-runner capacity (16GB RAM / 14GB disk + # — vllm's CUDA csrc-build is the canonical case). Honest-fact #89. + runs-on: ${{ fromJSON(vars.RUNS_ON || '"ubuntu-latest"') }} + # Per-fork override via repo Variable BUILD_TIMEOUT_MINUTES (default 360, the + # GitHub-hosted-runner hard ceiling). Raise to 720 as a ONE-SHOT bootstrap on + # forks whose first build needs to populate :buildcache from scratch (vllm + # at single-arch CUDA: ~5-6h cold-cache vs ~30-45m warm). Revert to 360 once + # warm. Codified after 27340784996 / 27373307060 cancelled at 360min on the + # vllm fork's first cold-cache run. Honest-fact #91. + timeout-minutes: ${{ fromJSON(vars.BUILD_TIMEOUT_MINUTES || '360') }} + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + # Optional disk reclamation for forks whose images blow past the 14GB + # default disk on ubuntu-latest hosted runners (vllm being the canonical + # offender — its CUDA build artifacts + intermediate layers exceed 20GB + # easily). Frees ~30GB by removing pre-installed Android SDK, .NET, + # Haskell, large npm caches, and codeql databases. Costs ~3 min — gated + # behind FREE_DISK_SPACE Variable so non-vllm-class forks don't pay it. + - name: 🧹 Free disk space (opt-in via FREE_DISK_SPACE Variable) + if: vars.FREE_DISK_SPACE == 'true' + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: true + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: false + swap-storage: false + + - name: 🔍 Resolve Dockerfile + id: df + env: + DOCKERFILE_VAR: ${{ vars.DOCKERFILE }} + run: | + set -euo pipefail + # 1. Explicit override (repo Variable) always wins. + if [ -n "${DOCKERFILE_VAR}" ]; then + echo " ✓ using DOCKERFILE override: ${DOCKERFILE_VAR}" + echo "path=${DOCKERFILE_VAR}" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + # 2. Prefer a root Dockerfile. + if [ -f Dockerfile ]; then + echo " ✓ found root Dockerfile" + echo "path=Dockerfile" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + # 3. Deterministic discovery: shallowest non-test Dockerfile. + cand="$(find . -maxdepth 4 -type f -name Dockerfile \ + -not -path './node_modules/*' \ + -not -path './.git/*' \ + -not -path '*/test/*' \ + -not -path '*/tests/*' \ + -not -path '*/example*/*' \ + -not -path './vendor/*' \ + -not -path './third_party/*' \ + | sed 's|^\./||' \ + | awk '{print gsub(/\//,"/"), $0}' \ + | sort -n | head -n1 | cut -d' ' -f2- || true)" + if [ -z "$cand" ]; then + echo " ✗ no Dockerfile found → plain-mirror fallback (no image built)" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo " ✓ auto-found: $cand" + echo "path=$cand" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + + - name: Set up QEMU + if: steps.df.outputs.skip != 'true' + uses: docker/setup-qemu-action@v3 + + - name: Set up Buildx + if: steps.df.outputs.skip != 'true' + uses: docker/setup-buildx-action@v3 + + - name: Log in to ghcr.io + if: steps.df.outputs.skip != 'true' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: 🔡 Compute image (lowercase — GHCR requires lowercase repo names) + id: imgname + run: | + # vars.IMAGE_NAME may already be lowercase (set as such manually), but + # the github.event.repository.name fallback can be CamelCase if the fork + # repo name has uppercase letters (e.g. RetroSaveManager). docker/build-push + # rejects ANY uppercase in the image path. fork-publish.yml does the same + # via tr; we do it here. Honest-fact #63. + NAME="${{ vars.IMAGE_NAME || github.event.repository.name }}" + NAME_LOWER=$(echo "$NAME" | tr '[:upper:]' '[:lower:]') + echo "image=ghcr.io/${{ github.repository_owner }}/${NAME_LOWER}" >> "$GITHUB_OUTPUT" + + - name: 🏷️ Docker metadata + id: meta + if: steps.df.outputs.skip != 'true' + uses: docker/metadata-action@v5 + with: + # Per-fork override via repo Variable IMAGE_NAME — for forks where the + # published image name differs from the repo name (docker-autoheal repo → + # autoheal image). Falls back to repo name. Same as fork-publish.yml so + # ONE image per fork. Lowercased above (metadata-action's lowercase only + # applies to tags, not to the images input — uppercase repo names slip + # through and break build-push with "repository name must be lowercase"). + images: ${{ steps.imgname.outputs.image }} + tags: | + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/intarweb-dev' }} + type=sha,prefix=sha-,format=short + + - name: 🚀 Build & push + if: steps.df.outputs.skip != 'true' + uses: docker/build-push-action@v6 + with: + context: ${{ vars.BUILD_CONTEXT || '.' }} + file: ${{ steps.df.outputs.path }} + platforms: ${{ vars.PLATFORMS || 'linux/amd64,linux/arm64' }} + # Per-fork override via repo Variable BUILD_ARGS — multi-line, one + # KEY=VALUE per line. Used by forks whose Dockerfile declares an ARG + # with no default (e.g. tika-docker's TIKA_VERSION). Honest-fact #88. + build-args: ${{ vars.BUILD_ARGS || '' }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + provenance: false diff --git a/.github/workflows/fold-on-push.yml b/.github/workflows/fold-on-push.yml new file mode 100644 index 0000000..e5dc2d3 --- /dev/null +++ b/.github/workflows/fold-on-push.yml @@ -0,0 +1,64 @@ +# UNIVERSAL fold-on-push trigger — byte-identical across every Model B fork. +# +# Purpose: when a push lands on ANY branch other than the default branch or +# intarweb-dev, dispatch sync-upstream.yml to re-fold + re-publish. This is +# the event-driven counterpart to the hourly schedule — covers "new commit +# landed on a PR head branch" and "operator pushed a new branch they're +# about to PR" instantly, without waiting up to 1 hour. +# +# Why every-branch-except-defaults: PR head branches use varied naming +# (feat/*, fix/*, test/*, docs/*, etc). Enumerating patterns means missing +# new ones (build-overlay.yml's pattern misses feat/* and docs/*, MEASURED +# 2026-06-10). Wildcard with explicit exclusions for self-trigger paths is +# the simplest correct shape. +# +# Loop prevention: branches-ignore covers EVERY ref this workflow's downstream +# (sync-upstream.yml) pushes — main, master, intarweb-dev. Pushes from the +# hard-reset/ops-overlay/intarweb-dev-regen logic therefore do NOT re-fire +# this workflow. +# +# Cadence bypass: the dispatch into sync-upstream.yml fires it as a +# workflow_dispatch event. sync-upstream's cadence gate explicitly bypasses +# for non-schedule events (see its ⏱️ Cadence gate step). So a push triggers +# an immediate fold regardless of vars.SYNC_CADENCE_HOURS. +# +# Codified in oss-contributing:ghcr-fork-mirror skill honest-fact #64. + +name: Fold on PR head-branch push + +on: + push: + branches-ignore: + - main + - master + - intarweb-dev + +permissions: + actions: write # required for `gh workflow run` dispatch into sync-upstream + +jobs: + dispatch-sync: + runs-on: ubuntu-latest + steps: + - name: 🤖 Mint org-bot app token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ vars.SYNC_APP_ID }} + private-key: ${{ secrets.SYNC_APP_PRIVATE_KEY }} + + - name: 🌿 Dispatch sync-upstream (full re-fold) + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + echo " push detected on branch: ${{ github.ref_name }}" + echo " triggering full re-fold via sync-upstream.yml" + # The workflow_dispatch event is whitelisted by sync-upstream's + # cadence gate — runs immediately, bypassing SYNC_CADENCE_HOURS. + # --ref points at the workflow file location (default branch), + # NOT the branch that was pushed. sync-upstream itself always + # operates on default branch + folds open PRs regardless of + # which branch triggered this dispatch. + gh workflow run "Sync from upstream + auto-regen intarweb-dev" \ + --repo "${{ github.repository }}" \ + --ref "${{ github.event.repository.default_branch }}" diff --git a/.github/workflows/fork-publish.yml b/.github/workflows/fork-publish.yml new file mode 100644 index 0000000..c1ca5ea --- /dev/null +++ b/.github/workflows/fork-publish.yml @@ -0,0 +1,115 @@ +# UNIVERSAL fork-publish workflow — BYTE-IDENTICAL across every fork. +# +# Mirrors upstream's released versioned tags to ghcr.io/${{ github.repository }} +# (auto-lowercased by docker/metadata-action). Same image namespace as +# build-from-source.yml — ONE concept, no per-fork vanity names. +# +# :latest is owned by build-from-source.yml (built from intarweb-dev). +# This workflow handles VERSIONED tags only (1.2.3, 1.2, 1, etc). +# +# Codified in oss-contributing:ghcr-fork-mirror skill honest-fact #57. + +name: Fork-publish (mirror upstream → GHCR) + +on: + schedule: + - cron: '17 6 * * *' # daily 06:17 UTC — well before sync-upstream's :00 cron + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + # Per-fork override via repo Variable IMAGE_NAME — used when upstream's published + # image name differs from our repo name (e.g. docker-autoheal repo → autoheal image). + # Falls back to repo name. ONE image per fork, shared with build-from-source.yml. + IMAGE: ghcr.io/${{ github.repository_owner }}/${{ vars.IMAGE_NAME || github.event.repository.name }} + +jobs: + mirror: + runs-on: ubuntu-latest + steps: + - uses: docker/setup-buildx-action@v3 + + - name: 🔑 Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: 🔍 Discover upstream tags + id: tags + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + # GitHub repo (for `gh release list` to discover versioned tags) + UPSTREAM_REPO="joeblack2k/RetroSaveManager" + # Docker image source (where imagetools mirrors FROM). Defaults to + # GitHub repo path; override via repo Variable UPSTREAM_IMAGE for forks + # where Docker Hub path differs (e.g. acmesh-official/acme.sh on GitHub + # but neilpang/acme.sh on Docker Hub). + UPSTREAM_IMAGE="${{ vars.UPSTREAM_IMAGE || format('{0}/{1}', 'joeblack2k', 'RetroSaveManager') }}" + RELEASES=$(gh release list --repo "$UPSTREAM_REPO" --limit 3 --json tagName \ + --jq '.[].tagName | sub("^v"; "")' 2>/dev/null | tr '\n' ' ' || echo "") + # Fallback: if upstream uses raw git tags without GH Release objects + # (e.g. neo4j/docker-neo4j), gh release list returns empty. Use the + # git tags API and filter for version-shaped tags (must contain at least + # one N.N component; skips purely descriptive words like + # community/latest/enterprise/nightly/dev/main). Honest-fact #86. + if [ -z "$(echo "$RELEASES" | tr -d ' ')" ]; then + RELEASES=$(gh api "repos/$UPSTREAM_REPO/tags?per_page=10" \ + --jq '.[].name' 2>/dev/null \ + | grep -E '[0-9]+\.[0-9]+' \ + | sed 's/^v//' \ + | head -3 \ + | tr '\n' ' ' || echo "") + [ -n "$(echo "$RELEASES" | tr -d ' ')" ] && \ + echo " (release-list was empty; fell back to git tags API)" + fi + MAJORS=$(for r in $RELEASES; do echo "${r%%.*}"; done | sort -u | tr '\n' ' ') + TAGS="$RELEASES $MAJORS" + echo "upstream_image=$UPSTREAM_IMAGE" >> "$GITHUB_OUTPUT" + echo "tags=$TAGS" >> "$GITHUB_OUTPUT" + echo " will mirror: $TAGS" + + - name: 🪞 Mirror tags to GHCR + run: | + set -euo pipefail + UPSTREAM="${{ steps.tags.outputs.upstream_image }}" + # Lowercase the image (Docker requires lowercase repo names; metadata-action + # does this automatically for build-push, we do it manually here for imagetools). + TARGET=$(echo "${IMAGE}" | tr '[:upper:]' '[:lower:]') + NEWEST_VERSIONED="" + for tag in ${{ steps.tags.outputs.tags }}; do + [ -z "$tag" ] && continue + echo "::group::Mirror $UPSTREAM:$tag → $TARGET:$tag" + if docker buildx imagetools inspect "$UPSTREAM:$tag" >/dev/null 2>&1; then + docker buildx imagetools create --tag "$TARGET:$tag" "$UPSTREAM:$tag" + echo " ✓ mirrored $tag" + # Track the first (newest) versioned tag for :latest aliasing below + if [ -z "$NEWEST_VERSIONED" ]; then NEWEST_VERSIONED="$tag"; fi + else + echo " - upstream tag $tag does not exist, skipping" + fi + echo "::endgroup::" + done + + # Also publish :latest pointing to the newest successfully-mirrored version, + # ONLY if this fork is plain-mirror (i.e. has no source-build). Source-built + # forks (vars.IS_SOURCE_BUILT=true) own :latest via build-from-source.yml, + # which builds from intarweb-dev (= upstream + open PRs cherry-picked + ops + # overlay). Letting fork-publish stomp :latest on those forks would clobber + # our patch stack with upstream-pristine, breaking the contract. + # Plain-mirror forks (var unset) keep the historical behavior — fork-publish + # IS the :latest source. Honest-fact #72. + if [ -n "$NEWEST_VERSIONED" ] && [ "${{ vars.IS_SOURCE_BUILT }}" != "true" ]; then + echo "::group::Alias :latest → :$NEWEST_VERSIONED" + docker buildx imagetools create --tag "$TARGET:latest" "$UPSTREAM:$NEWEST_VERSIONED" + echo " ✓ :latest → :$NEWEST_VERSIONED" + echo "::endgroup::" + elif [ "${{ vars.IS_SOURCE_BUILT }}" = "true" ]; then + echo " - skipping :latest alias (IS_SOURCE_BUILT=true; build-from-source.yml owns :latest)" + fi diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 0000000..f636397 --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,477 @@ +name: Sync from upstream + auto-regen intarweb-dev + +on: + schedule: + - cron: '0 * * * *' # hourly wakeup. ACTUAL sync work is gated by a runtime cadence + # check (vars.SYNC_CADENCE_HOURS, default=1). GitHub does NOT + # allow expression substitution in on.schedule.cron — confirmed + # via docs + empirical: 22 fleet forks all use literal crons. + # So cadence is a GATE inside the job, not a schedule swap. + # Dead upstreams set SYNC_CADENCE_HOURS=168 (weekly) etc. + workflow_dispatch: + workflow_call: # invoked by fold-on-push.yml when a PR head branch is pushed + # (push event on intarweb-side feat/*, fix/*, test/*, docs/* etc). + # workflow_call bypasses the cadence gate — pushes always rebuild. + +permissions: + actions: write # REQUIRED — the workflow_dispatch publish-trigger step 403s without it (honest-fact #27) + contents: write + pull-requests: read + +# One sync at a time per fork. Without this, a workflow_dispatch and a schedule +# tick can race — both fold cleanly, both attempt force-with-lease push, one +# wins, the loser fails the push with "failed to push some refs" and emits a +# false-failure alert. Concurrency group serializes them; cancel-in-progress +# false keeps the earlier run going (don't waste the cherry-pick work). +# Burned 2026-06-10 during fleet rollout on vllm-jukebox. +concurrency: + group: sync-upstream-${{ github.repository }} + cancel-in-progress: false + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: 🤖 Mint org-bot app token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ vars.SYNC_APP_ID }} + private-key: ${{ secrets.SYNC_APP_PRIVATE_KEY }} + + - name: 📥 Checkout fork + uses: actions/checkout@v4 + with: + fetch-depth: 0 + # Org-wide GitHub App token (1h short-lived, auto-rotated). + # Has workflows:write so it CAN push under .github/workflows/ — + # GITHUB_TOKEN can't (honest-fact #26). Replaces per-repo + # SYNC_WORKFLOW_TOKEN PATs campaign-wide (honest-fact #53). + token: ${{ steps.app-token.outputs.token }} + + - name: ⏱️ Cadence gate (skip schedule wakeup if too soon) + id: cadence + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + # Per-fork cadence config: vars.SYNC_CADENCE_HOURS (default 1, i.e. every hourly + # wakeup runs). Increase for dead/slow upstreams: 24=daily, 168=weekly. + # Gate only suppresses SCHEDULE events — workflow_dispatch and workflow_call + # always run regardless of cadence (those are explicit asks). + # + # last_sync is derived live from `gh run list` (no persisted state needed), + # filtered to successful runs of this workflow. The current run is in-progress, + # so it doesn't count toward "last success". + set -euo pipefail + CADENCE="${{ vars.SYNC_CADENCE_HOURS || '1' }}" + EVENT="${{ github.event_name }}" + if [ "$EVENT" != "schedule" ]; then + echo " cadence gate: event=$EVENT (non-schedule) → bypass gate, running" + echo "skip=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + LAST=$(gh run list --workflow sync-upstream.yml --status success --limit 1 \ + --json createdAt --jq '.[0].createdAt // ""') + if [ -z "$LAST" ]; then + echo " cadence gate: no prior successful sync — running" + echo "skip=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + NOW=$(date -u +%s) + LAST_TS=$(date -u -d "$LAST" +%s) + AGE_H=$(( (NOW - LAST_TS) / 3600 )) + if [ "$AGE_H" -ge "$CADENCE" ]; then + echo " cadence gate: last sync ${AGE_H}h ago ≥ cadence ${CADENCE}h → running" + echo "skip=false" >> "$GITHUB_OUTPUT" + else + echo " cadence gate: last sync ${AGE_H}h ago < cadence ${CADENCE}h → skipping wakeup" + echo "skip=true" >> "$GITHUB_OUTPUT" + fi + + - name: ⚙️ Configure git identity + if: steps.cadence.outputs.skip != 'true' + run: | + git config user.email "actions@github.com" + git config user.name "intarweb sync bot" + + - name: 🔗 Add upstream remote + if: steps.cadence.outputs.skip != 'true' + run: git remote add upstream https://github.com/joeblack2k/RetroSaveManager.git + + - name: 🔄 Fetch upstream + all fork branches + if: steps.cadence.outputs.skip != 'true' + run: | + git fetch upstream main --tags + git fetch origin --prune + + - name: 🔁 Hard-reset default branch to upstream + re-apply ops overlay + if: steps.cadence.outputs.skip != 'true' + run: | + # Burned 2026-06-09 with intarweb/bifrost: `git rebase upstream/dev` + # PRESERVES commits on our default branch that aren't on upstream — + # so when upstream FORCE-PUSHED and dropped 23 commits (a half-baked + # modelcatalogresolver plugin with incomplete go.sum), those commits + # got TRAPPED on intarweb/bifrost/main forever. Build started failing + # because go.sum referenced packages upstream had since removed. + # Honest-fact #62 (and vllm-bot diagnosis in claude/forker-queue/). + # + # Fix: hard-reset to upstream. Accept upstream's history as authoritative. + # Then re-apply our 4 workflow files + FORK_INFO.md (which legitimately + # live on main so schedules can fire) from the pre-reset working tree. + # This means intarweb/main = upstream/ + exactly N legit ops + # commits — no untracked drift, no force-push survivors. + + git checkout main + + # Save ops overlay BEFORE the reset + mkdir -p /tmp/ops-overlay/.github/workflows + for F in .github/workflows/sync-upstream.yml \ + .github/workflows/build-from-source.yml \ + .github/workflows/build-overlay.yml \ + .github/workflows/fork-publish.yml \ + .github/workflows/update-fork-info.yml \ + .github/workflows/fold-on-push.yml \ + FORK_INFO.md; do + if [ -f "$F" ]; then + mkdir -p "/tmp/ops-overlay/$(dirname $F)" + cp -p "$F" "/tmp/ops-overlay/$F" + fi + done + + # Hard-reset — drops any trapped-from-force-push commits + git reset --hard upstream/main + + # Restore ops overlay + for F in .github/workflows/sync-upstream.yml \ + .github/workflows/build-from-source.yml \ + .github/workflows/build-overlay.yml \ + .github/workflows/fork-publish.yml \ + .github/workflows/update-fork-info.yml \ + .github/workflows/fold-on-push.yml \ + FORK_INFO.md; do + if [ -f "/tmp/ops-overlay/$F" ]; then + mkdir -p "$(dirname $F)" + cp -p "/tmp/ops-overlay/$F" "$F" + fi + done + + if ! git status --porcelain -- .github/workflows FORK_INFO.md | grep -q .; then + echo " no ops overlay to re-apply (none existed pre-reset)" + else + git add -A .github/workflows + [ -f FORK_INFO.md ] && git add FORK_INFO.md + git commit -m "ci: re-apply ops overlay after hard-reset to upstream/main" + fi + + git push --force-with-lease origin main + + - name: 🔍 Discover open PRs from intarweb to upstream + id: prs + if: steps.cadence.outputs.skip != 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Use GitHub's search/issues API instead of paginating /pulls?state=open. + # The old approach paginated EVERY open PR on upstream then jq-filtered + # client-side: on NousResearch/hermes-agent (MEASURED 13,295 open PRs + # 2026-06-10), that's 133 API calls per sync just to find our 1-2. + # search/issues with `author:terafin` gives the answer in 1 call. + # + # Caveats verified MEASURED on intarweb/vllm 2026-06-10 (6/6 parity vs + # paginate): + # - All intarweb-authored upstream PRs are by `terafin` today. If a + # co-contributor opens one, broaden the author qualifier OR fall + # back to a per-PR head.repo.owner.login == intarweb verification. + # - search/issues doesn't return head.ref; we fetch it per result + # (N small extra calls, where N = our PR count, typically 1-6). + # - The result set is far below search API's 1000-result cap. + # + # Defensive: after the search, re-verify each PR's head.repo.owner.login + # before adding. Drops anything that shouldn't be cherry-picked even if + # search returns extras. + # SEMANTIC CONTRACT: this filter is `author:` (PRs THAT user authored, + # from anywhere), NOT `head:` (PRs from any author whose branch lives + # in that org). GitHub's search API has no head-org qualifier; only REST + # /pulls supports head=user:branch (literal, no wildcard). These two + # filters coincide TODAY because every intarweb PR is both terafin-authored + # AND from an intarweb branch, but they diverge the first time a + # co-contributor opens a PR from an intarweb branch under a different + # account — that PR would be silently MISSED by this discovery. + # + # Defense: the per-PR re-check below enforces head.repo.owner.login == + # ${{ github.repository_owner }}, so the search can never accept a PR from + # the wrong head repo. It just can't see beyond the author qualifier. + # + # vars.SYNC_PR_AUTHOR override: defaults to `terafin` (current sole author). + # If a co-contributor joins, set this var to a space-separated list and the + # query expands. Empty value → fall back to defensive: full /pulls scan + # (slow, accurate). Honest-fact #65. + PR_AUTHORS="${{ vars.SYNC_PR_AUTHOR || 'terafin' }}" + : > /tmp/pr-nums.txt + if [ -z "$PR_AUTHORS" ]; then + # Fallback: paginate /pulls and filter client-side. Slow on big upstreams + # but provably complete. + gh api --paginate "repos/joeblack2k/RetroSaveManager/pulls?state=open&per_page=100" \ + --jq '.[] | select(.head.repo.owner.login == "${{ github.repository_owner }}") | .number' \ + >> /tmp/pr-nums.txt + else + for AUTH in $PR_AUTHORS; do + gh api "search/issues?q=repo:joeblack2k/RetroSaveManager+is:pr+is:open+author:${AUTH}&per_page=100" \ + --jq '.items[].number' >> /tmp/pr-nums.txt + done + sort -u /tmp/pr-nums.txt -o /tmp/pr-nums.txt + fi + : > /tmp/prs.txt + while read -r num; do + [ -z "$num" ] && continue + META=$(gh api "repos/joeblack2k/RetroSaveManager/pulls/$num" \ + --jq '"\(.head.repo.owner.login)|\(.head.ref)|\(.title)"' 2>/dev/null) + OWN="${META%%|*}" + REST="${META#*|}" + REF="${REST%%|*}" + TITLE="${REST#*|}" + if [ "$OWN" != "${{ github.repository_owner }}" ]; then + echo " - skipping PR #$num — head.repo.owner=$OWN (expected ${{ github.repository_owner }})" + continue + fi + echo "$num $REF $TITLE" >> /tmp/prs.txt + done < /tmp/pr-nums.txt + sort -n -o /tmp/prs.txt /tmp/prs.txt + + if [ ! -s /tmp/prs.txt ]; then + echo " No open PRs from ${{ github.repository_owner }} to upstream — intarweb-dev will == main" + else + echo " Open PRs to cherry-pick onto intarweb-dev:" + sed 's/^/ /' /tmp/prs.txt + fi + + - name: 🚨 Warn on intarweb-dev workflow drift (about to be wiped) + # The regen step below does `git checkout -B intarweb-dev main` + # which OVERWRITES intarweb-dev with main's tree, then re-cherry-picks + # the open-PR set. Any workflow edits made directly to intarweb-dev that + # aren't in main AND aren't in an open PR's cherry-pick set get SILENTLY + # WIPED here. + # + # Live burn 2026-06-11 on intarweb/vllm: prior agent committed + # `f1bdf3e33` (TORCH_CUDA_ARCH_LIST trim + 720min timeout) to + # intarweb-dev directly. The next sync regen wiped it without alarm; six + # subsequent build attempts ran with the stale 3-arch list, all + # cancelled at the 360-min wall. Honest-fact #97. + # + # The wipe IS the feature (uniformity contract). The silence is the + # bug. This step makes it visible BEFORE the wipe runs: yellow + # ::warning:: in the Actions UI + diff in the run log. Operator sees + # "I edited intarweb-dev directly and the sync is about to undo it" + # and can re-edit on main (where it survives via the ops-overlay + # save/restore above). + if: steps.cadence.outputs.skip != 'true' + run: | + git fetch origin intarweb-dev 2>/dev/null || { + echo " (no remote intarweb-dev yet — first sync, nothing to wipe)" + exit 0 + } + ANY_DRIFT=0 + for F in .github/workflows/sync-upstream.yml \ + .github/workflows/build-from-source.yml \ + .github/workflows/build-overlay.yml \ + .github/workflows/fork-publish.yml \ + .github/workflows/update-fork-info.yml \ + .github/workflows/fold-on-push.yml \ + FORK_INFO.md; do + # Get both sides; skip cleanly if either lacks the file. + MAIN_BLOB=$(git show "HEAD:$F" 2>/dev/null || echo "") + DEV_BLOB=$(git show "origin/intarweb-dev:$F" 2>/dev/null || echo "") + if [ -z "$MAIN_BLOB" ] && [ -z "$DEV_BLOB" ]; then + continue + fi + if [ "$MAIN_BLOB" = "$DEV_BLOB" ]; then + continue + fi + ANY_DRIFT=1 + echo "::warning::ops-overlay restore will wipe intarweb-dev's $F — differs from main." + echo " If you intended this drift to persist across syncs, commit it to MAIN." + echo " Diff (intarweb-dev → main, head -20):" + diff <(echo "$DEV_BLOB") <(echo "$MAIN_BLOB") 2>/dev/null | head -20 | sed 's/^/ /' + echo "" + done + if [ "$ANY_DRIFT" = "0" ]; then + echo " ✓ no intarweb-dev ops-overlay drift detected" + fi + + - name: 🌿 Regenerate intarweb-dev = main + open-PR cherry-picks + id: regen + if: steps.cadence.outputs.skip != 'true' + # FORK_CARRIED_COMMITS step (below the PR loop) calls `gh api` to + # check carrier-PR / upstream-PR state. GITHUB_TOKEN read scope is + # sufficient — we only read PR state, never write here. + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git checkout -B intarweb-dev main + while read num branch title; do + [ -z "$num" ] && continue + echo "::group::PR #$num — $branch ($title)" + git fetch origin "$branch" || { echo " ✗ failed to fetch origin/$branch"; exit 1; } + # Range "intarweb-dev..origin/$branch" = commits on branch not yet on intarweb-dev. + # As we apply more PRs, intarweb-dev grows; later branches only contribute their NEW commits. + # If a PR was merged upstream, its commits are now on main (and thus intarweb-dev), so this is empty. + COMMITS=$(git log --reverse --format=%H "intarweb-dev..origin/$branch") + if [ -z "$COMMITS" ]; then + echo " - no unique commits — already on intarweb-dev (likely merged upstream); skipping" + else + for c in $COMMITS; do + # -X theirs: when a PR's content is a superset of an earlier PR's content + # (common when PRs are stacked), take the cherry-picked version. Honest-fact #54. + # -m 1: tolerate upstream-merge-commits (honest-fact #45). + # --allow-empty --keep-redundant-commits: tolerate the no-op case where the + # commit's changes are already present (e.g. via another PR's superset). + if ! git cherry-pick -X theirs -m 1 --allow-empty --keep-redundant-commits "$c" 2>/dev/null; then + # FAIL-CLOSED (changed 2026-06-10, replacing Heal C silent-drop). + # The previous behavior — `git cherry-pick --abort; break; continue with next PR` + # silently dropped the conflicting PR and kept publishing :latest WITHOUT it. + # Consumers pinning :latest had no signal that PR #N was being silently + # excluded. New behavior: FAIL THE JOB. Subsequent steps (force-push, + # build dispatch) do not run. The previous :latest stays in place, serving. + # Operator notification: ::error:: annotation in the run, the run shows + # red in the actions tab, and portfolio-audit's daily digest catches + # sync_conclusion=="failure" (portfolio-audit.yml line 147 "FAILED_SYNC"). + # Operator must manually rebase the offending PR head branch and force-push + # it before the next sync will succeed. + echo "::error::FOLD FAILED — PR #$num cherry-pick conflict on $c survived -X theirs. intarweb-dev WILL NOT be advanced; :latest remains at last-good. Rebase $branch onto upstream/main + push to recover." + git cherry-pick --abort || true + exit 1 + fi + done + echo " ✓ applied $(echo "$COMMITS" | wc -w) commits from PR #$num" + fi + echo "::endgroup::" + done < /tmp/prs.txt + + # FORK_CARRIED_COMMITS — re-apply long-lived fork-carried patches + # that aren't bound to an OPEN PR's lifecycle. Honest-fact #98. + # + # Why this exists: the open-PR cherry-pick loop above only re-applies + # commits that live on terafin-authored OPEN PRs. If the carrier PR + # closes (unmerged OR superseded), the cherry-pick stops happening + # next sync and the fork SILENTLY REGRESSES to upstream/main's + # behavior. That's catastrophic for patches we depend on at runtime + # (e.g. cumem accounting fix for KV-OOM-free wakes). + # + # Format (per-fork repo Variable FORK_CARRIED_COMMITS, multi-line, + # one record per line, pipe-separated): + # || + # + # Fields: + # commit-sha short or long SHA on the fork's working tree + # upstream-pr-to-watch PR # in upstream that, when MERGED, makes + # this patch redundant. Step emits a yellow + # WARNING when merged (operator should drop + # the entry); harmless empty cherry-pick if + # left in (upstream's version is already in + # the hard-reset main, so cherry-pick is no-op). + # carrier-pr optional. PR # under our fork that hosts + # this rebase. If carrier is CLOSED-UNMERGED, + # this step FAILS LOUDLY rather than silently + # dropping the patch. + # + # Examples (intarweb/vllm at 2026-06-11): + # c9427de62|37111|45208 # cumem OOM fix; drop when 37111 merges; #45208 carries the rebase + # 88af8e6b0|45097 # /health/decode; drop when 45097 merges; no carrier + # + # Comments (lines starting with #) and blank lines are skipped. + if [ -n "${{ vars.FORK_CARRIED_COMMITS }}" ]; then + echo "::group::FORK_CARRIED_COMMITS — re-applying long-lived patches" + echo "${{ vars.FORK_CARRIED_COMMITS }}" > /tmp/fork-carried.txt + while IFS='|' read -r CSHA UPSTREAM_PR CARRIER_PR; do + CSHA="$(echo "$CSHA" | xargs)" + UPSTREAM_PR="$(echo "$UPSTREAM_PR" | xargs)" + CARRIER_PR="$(echo "$CARRIER_PR" | xargs)" + # Skip blanks + comments + [ -z "$CSHA" ] && continue + case "$CSHA" in \#*) continue ;; esac + + # Check carrier-PR state (if specified) — fail loudly on + # closed-unmerged. That's the silent-regression class the entire + # FORK_CARRIED_COMMITS mechanism exists to prevent. + if [ -n "$CARRIER_PR" ]; then + CARRIER_STATE=$(gh api "repos/joeblack2k/RetroSaveManager/pulls/$CARRIER_PR" \ + --jq '"\(.state)|\(.merged)"' 2>/dev/null || echo "unknown|") + C_STATE="${CARRIER_STATE%%|*}" + C_MERGED="${CARRIER_STATE#*|}" + if [ "$C_STATE" = "closed" ] && [ "$C_MERGED" != "true" ]; then + echo "::error::FORK_CARRIED_COMMITS: carrier PR #$CARRIER_PR for commit $CSHA is CLOSED-UNMERGED. Without intervention this patch would silently drop from the next image. ABORTING sync to surface the regression." + echo "::error:: Fix: (a) re-open #$CARRIER_PR and rebase, OR (b) file a new carrier PR and update FORK_CARRIED_COMMITS to point at it, OR (c) if the patch is genuinely no-longer-needed, remove the entire entry from FORK_CARRIED_COMMITS." + exit 1 + fi + fi + + # Check upstream-PR state (always required) — warn on merged + # (operator should drop the entry; the cherry-pick will be an + # empty no-op below). + UPSTREAM_STATE=$(gh api "repos/joeblack2k/RetroSaveManager/pulls/$UPSTREAM_PR" \ + --jq '"\(.state)|\(.merged)"' 2>/dev/null || echo "unknown|") + U_STATE="${UPSTREAM_STATE%%|*}" + U_MERGED="${UPSTREAM_STATE#*|}" + if [ "$U_MERGED" = "true" ]; then + echo "::warning::FORK_CARRIED_COMMITS: upstream PR #$UPSTREAM_PR has MERGED. Commit $CSHA is now redundant — please remove this entry from FORK_CARRIED_COMMITS. Cherry-pick below will no-op." + fi + + # Apply (or empty no-op if already present from main or open-PR loop). + if git cherry-pick -X theirs -m 1 --allow-empty --keep-redundant-commits "$CSHA" 2>/dev/null; then + echo " ✓ applied carried commit $CSHA (upstream-pr=$UPSTREAM_PR carrier=$CARRIER_PR)" + else + # Conflict — FAIL CLOSED (same posture as the open-PR loop). + echo "::error::FORK_CARRIED_COMMITS: cherry-pick conflict on $CSHA survived -X theirs. intarweb-dev WILL NOT be advanced; :latest remains at last-good. Operator must rebase the carrier (#$CARRIER_PR) or update FORK_CARRIED_COMMITS to a current SHA." + git cherry-pick --abort || true + exit 1 + fi + done < /tmp/fork-carried.txt + echo "::endgroup::" + fi + + # Did intarweb-dev's TREE actually change vs the existing remote? + # Compare trees (content), not SHAs (which differ each run because + # cherry-pick rewrites committer timestamps). + if git rev-parse origin/intarweb-dev^{tree} >/dev/null 2>&1; then + OLD_TREE=$(git rev-parse origin/intarweb-dev^{tree}) + NEW_TREE=$(git rev-parse intarweb-dev^{tree}) + if [ "$OLD_TREE" = "$NEW_TREE" ]; then + echo " - intarweb-dev tree unchanged; skipping push + publish trigger" + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo " - intarweb-dev tree changed; will push + trigger publish" + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + else + echo " - intarweb-dev did not exist on origin; will push + trigger publish" + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: 📤 Force-push intarweb-dev + # Gated on (a) cadence-not-skipped, (b) regen succeeded (implicit via default + # `success()`), (c) tree actually changed. If the cherry-pick loop hit a + # conflict, regen exits 1 → this step does not run → prior intarweb-dev stays + # at last-good → :latest is not republished → fail-closed contract holds. + if: steps.cadence.outputs.skip != 'true' && success() && steps.regen.outputs.changed == 'true' + run: git push --force-with-lease origin intarweb-dev + + - name: 🚀 Trigger publish on intarweb-dev + if: steps.cadence.outputs.skip != 'true' && success() && steps.regen.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Required because GitHub blocks the normal push→workflow cascade + # when the push is authored by GITHUB_TOKEN (anti-loop safety rule). + # workflow_dispatch IS allowed to fire from GITHUB_TOKEN, so we + # explicitly dispatch the publish workflow here. UNIVERSAL name + # — every Model B fork's build workflow is named exactly this + # via the build-from-source.yml template (honest-fact #52). + gh workflow run "Build from source → GHCR" --repo ${{ github.repository }} --ref intarweb-dev || true + + - name: ✅ Show final state + if: steps.cadence.outputs.skip != 'true' && success() + run: | + echo " main HEAD: $(git log main --oneline -1)" + echo " intarweb-dev HEAD: $(git log intarweb-dev --oneline -1)" + echo " intarweb-dev commits ahead of upstream/main:" + git log --oneline upstream/main..intarweb-dev | sed 's/^/ /' || true diff --git a/.github/workflows/update-fork-info.yml b/.github/workflows/update-fork-info.yml new file mode 100644 index 0000000..44c596c --- /dev/null +++ b/.github/workflows/update-fork-info.yml @@ -0,0 +1,139 @@ +name: Update FORK_INFO.md carried-patches table + +# Regenerates the auto-managed "carried patches" section in FORK_INFO.md +# from the current set of open intarweb→upstream PRs. Keeps the doc in +# sync with what :latest actually carries — when a PR merges upstream, +# its row disappears within a cron tick. +# +# Triggers on completion of the sync workflow (so we know intarweb-dev +# was just regenerated) + manual dispatch. Idempotent: only pushes if +# FORK_INFO.md actually changed. +# +# Codified in oss-contributing:ghcr-fork-mirror skill v1.19.0 honest- +# fact #49. + +on: + workflow_run: + workflows: ["Sync from upstream + auto-regen intarweb-dev"] + types: [completed] + workflow_dispatch: + +permissions: + contents: write + pull-requests: read + +env: + UPSTREAM_OWNER: joeblack2k + UPSTREAM_REPO: RetroSaveManager + UPSTREAM_BRANCH: main + DEPLOY_BRANCH: intarweb-dev + +jobs: + update-fork-info: + runs-on: ubuntu-latest + steps: + - name: 📥 Checkout default branch + uses: actions/checkout@v4 + with: + token: ${{ secrets.SYNC_WORKFLOW_TOKEN || secrets.GITHUB_TOKEN }} + ref: main + + - name: 🔧 Configure git + run: | + git config user.email "terafin@users.noreply.github.com" + git config user.name "intarweb fork-info bot" + + - name: 📋 Discover open intarweb→upstream PRs + id: prs + env: + GH_TOKEN: ${{ secrets.SYNC_WORKFLOW_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + gh pr list \ + --repo "${UPSTREAM_OWNER}/${UPSTREAM_REPO}" \ + --state open \ + --limit 100 \ + --json number,title,headRepositoryOwner \ + --jq '[.[] | select(.headRepositoryOwner.login == "${{ github.repository_owner }}")] | sort_by(.number) | .[] | "\(.number)\t\(.title)"' \ + > /tmp/open-prs.tsv + echo "Open intarweb PRs:" + cat /tmp/open-prs.tsv | sed 's/^/ /' || echo " (none)" + + - name: 📝 Regenerate carried-patches table + run: | + set -euo pipefail + + # Build the auto-managed block. Tab-separated input (num\ttitle). + # Title is wrapped in `_…_` if list is empty so the markdown + # renders gracefully. + { + echo "" + echo "" + if [ -s /tmp/open-prs.tsv ]; then + echo "| PR | Title |" + echo "|---|---|" + while IFS=$'\t' read -r num title; do + [ -z "$num" ] && continue + # Escape any pipe in the title to keep table cells valid + safe_title="${title//|/\\|}" + echo "| [#${num}](https://github.com/${UPSTREAM_OWNER}/${UPSTREAM_REPO}/pull/${num}) | ${safe_title} |" + done < /tmp/open-prs.tsv + else + echo "_No open intarweb→upstream PRs — \`${DEPLOY_BRANCH}\` matches \`upstream/${UPSTREAM_BRANCH}\` exactly._" + fi + echo "" + echo "" + } > /tmp/patches-block.md + + # Replace markers in-place if present, else append a new section + if [ ! -f FORK_INFO.md ]; then + cat > FORK_INFO.md << 'HEADER' + # Fork tracking — intarweb/${UPSTREAM_REPO} + + (Auto-generated section below tracks what `${DEPLOY_BRANCH}` currently + carries vs `upstream/${UPSTREAM_BRANCH}`. Everything outside the + markers is hand-maintained.) + + ## Local patches we carry on `${DEPLOY_BRANCH}` (vs `upstream/${UPSTREAM_BRANCH}`) + + HEADER + cat /tmp/patches-block.md >> FORK_INFO.md + elif grep -q 'intarweb:patches-start' FORK_INFO.md; then + awk -v block="/tmp/patches-block.md" ' + // { + in_block = 0 + next + } + !in_block { print } + ' FORK_INFO.md > /tmp/fork-info-new.md + mv /tmp/fork-info-new.md FORK_INFO.md + else + # No markers yet — append a new auto-managed section at the end + { + echo "" + echo "## Carried patches (auto-managed)" + echo "" + cat /tmp/patches-block.md + } >> FORK_INFO.md + fi + + - name: 💾 Commit + push if changed + run: | + set -euo pipefail + git add FORK_INFO.md + # Stage first — git diff --quiet on an untracked file returns 0 ("no + # tracked changes") and would silently skip a brand-new FORK_INFO.md. + # --cached catches both cases. + if git diff --cached --quiet -- FORK_INFO.md; then + echo "✓ FORK_INFO.md unchanged — skipping push" + exit 0 + fi + git commit -m "docs(fork-info): auto-update carried-patches table" + git push origin HEAD:refs/heads/main + echo "✓ pushed updated FORK_INFO.md" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 15012ff..7b1d245 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -276,10 +276,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", - "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", - "license": "MIT", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", + "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", "engines": { "node": ">=14.0.0" } @@ -1715,12 +1714,11 @@ "peer": true }, "node_modules/react-router": { - "version": "6.30.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", - "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", - "license": "MIT", + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz", + "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", "dependencies": { - "@remix-run/router": "1.23.2" + "@remix-run/router": "1.23.3" }, "engines": { "node": ">=14.0.0" @@ -1730,13 +1728,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", - "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", - "license": "MIT", + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz", + "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", "dependencies": { - "@remix-run/router": "1.23.2", - "react-router": "6.30.3" + "@remix-run/router": "1.23.3", + "react-router": "6.30.4" }, "engines": { "node": ">=14.0.0" @@ -2247,11 +2244,10 @@ } }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.0.0" },