diff --git a/.dejima.md b/.dejima.md index c37a380..ec05807 100644 --- a/.dejima.md +++ b/.dejima.md @@ -34,6 +34,13 @@ architecture notes are in `CLAUDE.md`; feature and deployment docs are in - `.gitea/`; - migrations upstream doesn't have. +## GitHub mirror + +`mirror/` publishes `main` to a read-only GitHub mirror from the operator's +host, never from a session; see `docs/08-development/github-mirror.md`. GitHub +runs `.github/workflows`, the forge `.gitea/workflows`; keep the two image +workflows in step. + ## CI Workflows are in `.gitea/workflows`. Jobs that run docker builds use the diff --git a/.env.example b/.env.example index 9083283..06feccc 100644 --- a/.env.example +++ b/.env.example @@ -13,7 +13,7 @@ JWT_SECRET=replace-with-a-secure-random-string # Built from source by default. To pull published images instead, set the # registry path they live under (ending in /), then: # docker compose pull && docker compose up -d -# IMAGE_PREFIX=registry.example.com/owner/ +# IMAGE_PREFIX=ghcr.io/branderson/ # IMAGE_TAG=latest # "Latest release" API URL to check for updates (GitHub or Gitea); off when unset # APP_UPDATE_URL=https://git.example.com/api/v1/repos/owner/dotcom-arcade/releases/latest diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml new file mode 100644 index 0000000..8d6383c --- /dev/null +++ b/.github/workflows/images.yml @@ -0,0 +1,69 @@ +# Builds the app, session manager and streamer images on the GitHub mirror and +# pushes them to GitHub's container registry, under the repository's owner. +# The forge runs .gitea/workflows/images.yml instead; Gitea ignores this +# directory while .gitea/workflows exists. +name: Images + +on: + push: + branches: [main] + tags: ['v*.*.*'] + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + build: + name: ${{ matrix.name }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: app + image: dotcom-arcade + context: . + - name: session-manager + image: dotcom-arcade-session-manager + context: streaming/session-manager + - name: streamer + image: dotcom-arcade-streamer + context: streaming/streamer + steps: + - uses: actions/checkout@v4 + + - name: Lowercase the owner + id: owner + shell: bash + run: echo "name=${GITHUB_REPOSITORY_OWNER,,}" >> "$GITHUB_OUTPUT" + + - uses: docker/setup-buildx-action@v3 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ steps.owner.outputs.name }}/${{ matrix.image }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=ref,event=branch + + - uses: docker/build-push-action@v6 + with: + context: ${{ matrix.context }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64 + build-args: | + VERSION=${{ steps.meta.outputs.version }} + provenance: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3b0045d..0df8e7b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,44 +1,11 @@ # Contributing to Dotcom Arcade -Thanks for your interest in contributing! We welcome bug fixes, features, documentation improvements, and questions. +Dotcom Arcade is developed on a private forge and published to GitHub as a +read-only mirror, so issues and pull requests aren't accepted there. You're +welcome to fork it under the [GPL-3.0](LICENSE). -## What is Dotcom Arcade? - -A self-hosted web application for browsing and playing games from [Flashpoint Archive](https://flashpointarchive.org/). - -**Tech Stack:** Node.js 20+, Express, React 18, TypeScript, SQLite, TanStack Query, Tailwind CSS - -## Table of Contents - -- [Where to Start](#where-to-start) -- [Getting Started](#getting-started) -- [Development Setup](#development-setup) -- [Running Tests](#running-tests) -- [Making Changes](#making-changes) -- [Submitting a Pull Request](#submitting-a-pull-request) -- [Code Style](#code-style) -- [Reporting Bugs](#reporting-bugs) -- [License](#license) - -## Where to Start - -- Browse issues labeled [`good first issue`](../../labels/good%20first%20issue) or [`help wanted`](../../labels/help%20wanted) -- Check the [project documentation](docs/) for architecture and patterns - -## Getting Started - -```bash -# Fork the repository on GitHub, then clone your fork -git clone https://github.com/YOUR_USERNAME/flashpoint-web.git -cd flashpoint-web - -# Add upstream remote -git remote add upstream https://github.com/ORIGINAL_OWNER/flashpoint-web.git - -# Keep your fork synced -git fetch upstream -git rebase upstream/master -``` +It's built on [flashpoint-web](https://github.com/darkraise/flashpoint-web). +Fixes to code the two still share may belong upstream. ## Development Setup @@ -47,6 +14,7 @@ git rebase upstream/master ``` backend/ # Express REST API + game content serving (:3100) frontend/ # React + Vite application (:5173) +streaming/ # Session manager and streamer for games browsers can't run docs/ # Documentation ``` @@ -69,6 +37,7 @@ The app (UI and API) runs on `http://localhost:80`. ### Option 2: Manual Setup (Recommended for Development) **Prerequisites:** + - Node.js 20+ - Flashpoint Archive installed locally @@ -99,80 +68,21 @@ cd backend && npm test -- --coverage cd frontend && npm test ``` -## Making Changes - -### Branch Naming - -``` -feat/add-playlist-export -fix/game-search-pagination -refactor/auth-service -docs/update-api-reference -``` - -### Commit Messages - -Follow [Conventional Commits](https://www.conventionalcommits.org/): - -``` -(): - -feat(games): add advanced search filters -fix(auth): resolve token refresh race condition -docs: update deployment guide -refactor(api): simplify error handling -``` - -**Types:** `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `perf` - -### Workflow - -1. Create a branch from `master` -2. Make your changes -3. Run `npm run typecheck` and `npm test` -4. Update documentation if needed -5. Submit a pull request - -## Submitting a Pull Request - -### Before Submitting - -```bash -git fetch upstream -git rebase upstream/master -npm run typecheck -npm run format -npm test -``` - -### PR Guidelines - -- Keep PRs focused and reasonably sized -- Reference related issues (`Fixes #123`) -- Include a clear description of changes -- Update documentation for new features - ## Code Style See [CLAUDE.md](CLAUDE.md) for detailed coding standards. Key points: - **TypeScript:** No `any` types, use `unknown` or proper interfaces -- **Backend:** Wrap async handlers with `asyncHandler()`, static routes before parameterized -- **Frontend:** Use `@/` imports, all API calls through `lib/api.ts`, use theme tokens +- **Backend:** Wrap async handlers with `asyncHandler()`, static routes before + parameterized +- **Frontend:** Use `@/` imports, all API calls through `lib/api.ts`, use theme + tokens - **General:** Validate inputs, handle errors properly, clean up resources -Run `npm run format` before committing. - -## Reporting Bugs - -Before reporting, search [existing issues](../../issues) to avoid duplicates. - -Include: -- Steps to reproduce -- Expected vs actual behavior -- Environment details (OS, Node version, browser) -- Screenshots or logs if applicable +Run `npm run format` before committing. Commit messages follow +[Conventional Commits](https://www.conventionalcommits.org/). ## License -This project is licensed under [GPL-3.0](LICENSE). By contributing, you agree that your contributions will be licensed under the same license. +This project is licensed under [GPL-3.0](LICENSE). By contributing, you agree +that your contributions will be licensed under the same license. diff --git a/README.md b/README.md index 2bdfc26..293d033 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,9 @@ PGID=1000 docker compose up -d --build ``` +To pull prebuilt images instead of building them, see +[prebuilt images](docs/09-deployment/docker-deployment.md#prebuilt-images). + Open `http:///` and create the admin account. On first start the app indexes the collection, which takes a few minutes on a full Ultimate install. Under **Settings → App** you can set the site name and tagline. diff --git a/docs/08-development/README.md b/docs/08-development/README.md index d27073c..f89c9ed 100644 --- a/docs/08-development/README.md +++ b/docs/08-development/README.md @@ -16,6 +16,7 @@ the Dotcom Arcade application. 6. **[Debugging Guide](./debugging.md)** - Debugging tips and troubleshooting 7. **[Common Pitfalls](./common-pitfalls.md)** - Known issues and solutions 8. **[Upstream Sync](./upstream-sync.md)** - Merging changes from flashpoint-web +9. **[GitHub Mirror](./github-mirror.md)** - Publishing to GitHub and GHCR ## Quick Start diff --git a/docs/08-development/github-mirror.md b/docs/08-development/github-mirror.md new file mode 100644 index 0000000..dc6f0ac --- /dev/null +++ b/docs/08-development/github-mirror.md @@ -0,0 +1,28 @@ +# GitHub mirror + +Dotcom Arcade is developed on a private forge. `mirror/github-mirror.sh` +publishes its `main` and the tags on `main` to +[github.com/branderson/dotcom-arcade](https://github.com/branderson/dotcom-arcade), +hourly, from a host that can push to GitHub. Issues and pull requests are off +there. Actions stays on, since it builds the images that `ghcr.io/branderson/` +serves (see [publishing images](../09-deployment/docker-deployment.md#publishing-images)). + +The mirror's `main` is the forge's plus one commit, which adds a read-only notice +under the README's title. Every forge commit is in the mirror unchanged, and +each sync fast-forwards. If the README loses its `# ` title, the sync stops +instead of publishing without the notice. + +## Setup + +1. Set up the timer as described at the top of + `mirror/dotcom-arcade-github-mirror.service`. Give it a deploy key with write + access to the GitHub repository. +2. Run `github-mirror.sh --dry-run` to build what a sync would publish without + pushing it. +3. Once the first sync has pushed, run `github-mirror.sh --settings` (needs + `gh`). It turns off issues, pull requests, projects, wiki and discussions. +4. The first image build creates its packages as private. In each package's + settings on GitHub, set its visibility to public. + +The timer runs a copy of the script, not the checkout's. Reinstall it after +reviewing a change to `mirror/`. diff --git a/docs/09-deployment/docker-deployment.md b/docs/09-deployment/docker-deployment.md index 320332e..88e13d9 100644 --- a/docs/09-deployment/docker-deployment.md +++ b/docs/09-deployment/docker-deployment.md @@ -28,18 +28,29 @@ All three are built from this repository by `docker compose up -d --build`. ### Prebuilt images -To pull published images instead of building, set `IMAGE_PREFIX` in `.env` to -the registry path they live under, ending in `/`, and optionally `IMAGE_TAG`: +Images are published to GitHub's container registry. To pull them instead of +building, set `IMAGE_PREFIX` in `.env`, ending in `/`, and optionally +`IMAGE_TAG`. `latest` is the newest release, `1.2` the newest of a minor +version, and `main` follows the `main` branch. ```sh -IMAGE_PREFIX=registry.example.com/owner/ +IMAGE_PREFIX=ghcr.io/branderson/ IMAGE_TAG=latest ``` -Then run `docker compose pull && docker compose up -d`. +Then run `docker compose pull && docker compose up -d`. Images from another +registry work the same way, with its path as the prefix. ### Publishing images +Two workflows build the same images with the same tags: + +- `.github/workflows/images.yml` runs on the + [GitHub mirror](../08-development/github-mirror.md) and pushes to + `ghcr.io//` with the workflow's own token. It needs no setup. +- `.gitea/workflows/images.yml` runs on the forge. Gitea ignores + `.github/workflows` while `.gitea/workflows` exists. + `.gitea/workflows/images.yml` builds all three images on Gitea Actions and pushes them to the Gitea instance's container registry, under the repository's owner. Pushes to `main` publish a `main` tag; `v1.2.3` tags publish `1.2.3`, diff --git a/docs/README.md b/docs/README.md index 83f5331..9669d76 100644 --- a/docs/README.md +++ b/docs/README.md @@ -126,6 +126,7 @@ for Flashpoint Ultimate. - [Debugging](08-development/debugging.md) - Debugging tips - [Common Pitfalls](08-development/common-pitfalls.md) - Known issues - [Upstream Sync](08-development/upstream-sync.md) - Merging changes from flashpoint-web +- [GitHub Mirror](08-development/github-mirror.md) - Publishing to GitHub and GHCR ### 09. Deployment diff --git a/mirror/dotcom-arcade-github-mirror.service b/mirror/dotcom-arcade-github-mirror.service new file mode 100644 index 0000000..9a57080 --- /dev/null +++ b/mirror/dotcom-arcade-github-mirror.service @@ -0,0 +1,26 @@ +# User unit for the GitHub mirror, on a host that can push to GitHub. Never in +# a dejima session: a sandbox may push only to its own forge. +# +# install -m 755 mirror/github-mirror.sh ~/.local/bin/dotcom-arcade-github-mirror +# cp mirror/dotcom-arcade-github-mirror.service mirror/dotcom-arcade-github-mirror.timer ~/.config/systemd/user/ +# systemctl --user daemon-reload +# systemctl --user enable --now dotcom-arcade-github-mirror.timer +# +# A copy, not a link into the checkout: the timer holds a key that can push to +# GitHub, and a session working in this checkout can edit the script. +# Reinstall it after reviewing an update. +# +# ~/.config/dotcom-arcade/github-mirror.env sets MIRROR_SOURCE, the forge +# clone URL, and whatever else mirror/github-mirror.sh documents. The GitHub +# key, a deploy key with write access to the repository, belongs behind an ssh +# Host alias named in MIRROR_TARGET, so it is not also offered to the forge: +# +# MIRROR_SOURCE=git@forge.example.com:owner/dotcom-arcade.git +# MIRROR_TARGET=git@github-dotcom-arcade:branderson/dotcom-arcade.git +[Unit] +Description=Mirror Dotcom Arcade's main and release tags to GitHub + +[Service] +Type=oneshot +EnvironmentFile=%h/.config/dotcom-arcade/github-mirror.env +ExecStart=%h/.local/bin/dotcom-arcade-github-mirror diff --git a/mirror/dotcom-arcade-github-mirror.timer b/mirror/dotcom-arcade-github-mirror.timer new file mode 100644 index 0000000..e574579 --- /dev/null +++ b/mirror/dotcom-arcade-github-mirror.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Mirror Dotcom Arcade's main and release tags to GitHub, hourly + +[Timer] +OnCalendar=hourly +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/mirror/github-mirror.sh b/mirror/github-mirror.sh new file mode 100755 index 0000000..2cee519 --- /dev/null +++ b/mirror/github-mirror.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# Publishes main and its release tags to a read-only mirror on GitHub. +# +# MIRROR_SOURCE= mirror/github-mirror.sh sync +# ... mirror/github-mirror.sh --dry-run build and print, push nothing +# mirror/github-mirror.sh --settings configure the GitHub repo (needs gh) +# mirror/github-mirror.sh --transform DIR rewrite a checkout in place +# +# Runs on a host that can push to GitHub, never in a dejima session: a sandbox +# may push only to its own forge. +# +# The GitHub copy is not byte-for-byte. Its main is one extra commit on top of +# the forge's main, whose tree is the forge's with the rewrites in transform() +# applied, and whose parents are the forge commit and the previous mirror +# commit. So every forge SHA is in the mirror unchanged, and each sync is a +# fast-forward for anyone tracking it. Tags are pushed as they are, pointing at +# the untransformed forge commits. +# +# Environment: +# MIRROR_SOURCE forge clone URL (required to sync) +# MIRROR_REPO GitHub repository, default branderson/dotcom-arcade +# MIRROR_TARGET push URL, default git@github.com:$MIRROR_REPO.git +# MIRROR_STATE working bare repository, default ~/.cache/dotcom-arcade-github-mirror.git +set -euo pipefail + +MIRROR_REPO="${MIRROR_REPO:-branderson/dotcom-arcade}" +MIRROR_TARGET="${MIRROR_TARGET:-git@github.com:$MIRROR_REPO.git}" +MIRROR_STATE="${MIRROR_STATE:-${XDG_CACHE_HOME:-$HOME/.cache}/dotcom-arcade-github-mirror.git}" + +say() { echo "github-mirror: $*"; } +die() { say "$*" >&2; exit 1; } + +WORK="" +cleanup() { + [ -n "$WORK" ] || return 0 + git -C "$MIRROR_STATE" worktree remove --force "$WORK" 2>/dev/null || true + rm -rf "$WORK" +} + +# Rewrites a checkout for GitHub. Each rewrite is checked, so a README that +# drifted away from what this expects stops the mirror instead of publishing +# the forge's version. Running it twice changes nothing. +transform() { + local readme="$1/README.md" + # perl -i would write a symlink's target back as the README, publishing it. + [ -f "$readme" ] && [ ! -L "$readme" ] || die "README.md is missing or a symlink in $1" + + # The first thing a visitor to a mirror does is open an issue or a pull + # request. Under the title. + grep -q '^> \*\*This is a read-only mirror' "$readme" || perl -0pi -e ' + s{^(# [^\n]*)\n}{$1\n\n> **This is a read-only mirror.** Dotcom Arcade is developed on a private\n> forge, and issues and pull requests are not accepted here.\n}m + ' "$readme" + grep -q '^> \*\*This is a read-only mirror' "$readme" \ + || die "README.md: no title to put the mirror notice under" +} + +# Issues, pull requests, wiki and the like off; Actions stays on, since it +# publishes the images. Settings rather than content, so a sync does not +# reapply them; run once after creating the repository. +settings() { + command -v gh >/dev/null || die "--settings needs the gh CLI" + gh api -X PATCH "repos/$MIRROR_REPO" --silent \ + -F has_issues=false -F has_pull_requests=false -F has_projects=false \ + -F has_wiki=false -F has_discussions=false +} + +sync() { + [ -n "${MIRROR_SOURCE:-}" ] || die "MIRROR_SOURCE is not set" + local git=(git -C "$MIRROR_STATE") + trap cleanup EXIT + [ -d "$MIRROR_STATE" ] || git init -q --bare "$MIRROR_STATE" + + "${git[@]}" fetch -q --no-tags --prune "$MIRROR_SOURCE" \ + '+refs/heads/main:refs/source/main' '+refs/tags/*:refs/source/tags/*' + local src prev="" + src="$("${git[@]}" rev-parse refs/source/main)" + + # An absent main (exit 2) is a first run; anything else is a real failure. + local rc=0 + git ls-remote -q --exit-code "$MIRROR_TARGET" refs/heads/main >/dev/null || rc=$? + if [ "$rc" -eq 0 ]; then + "${git[@]}" fetch -q --no-tags "$MIRROR_TARGET" '+refs/heads/main:refs/mirror/main' + prev="$("${git[@]}" rev-parse refs/mirror/main)" + elif [ "$rc" -ne 2 ]; then + die "cannot read $MIRROR_TARGET" + fi + + WORK="$(mktemp -d)" + "${git[@]}" worktree add -q --detach "$WORK" "$src" + transform "$WORK" + git -C "$WORK" add -A + local tree + tree="$(git -C "$WORK" write-tree)" + + if [ -n "$prev" ] && [ "$("${git[@]}" rev-parse "$prev^1")" = "$src" ] \ + && [ "$("${git[@]}" rev-parse "$prev^{tree}")" = "$tree" ]; then + say "already at ${src:0:12}" + else + # The forge commit's committer and date, so the same input always + # makes the same commit. + local name email date commit + IFS=$'\t' read -r name email date < <("${git[@]}" log -1 --format='%cn%x09%ce%x09%cI' "$src") + commit="$(GIT_AUTHOR_NAME="$name" GIT_AUTHOR_EMAIL="$email" GIT_AUTHOR_DATE="$date" \ + GIT_COMMITTER_NAME="$name" GIT_COMMITTER_EMAIL="$email" GIT_COMMITTER_DATE="$date" \ + "${git[@]}" commit-tree "$tree" -p "$src" ${prev:+-p "$prev"} \ + -m "Mirror ${src:0:12} for GitHub" \ + -m "README header added by mirror/github-mirror.sh.")" + "${git[@]}" update-ref refs/mirror/next "$commit" + [ -n "$DRY_RUN" ] || "${git[@]}" push -q "$MIRROR_TARGET" "$commit:refs/heads/main" + say "main is ${src:0:12} as ${commit:0:12}" + fi + + # Only tags on main: one on another branch would publish unreleased work. + local ref + local refspecs=() + while IFS= read -r ref; do + if "${git[@]}" merge-base --is-ancestor "$ref" "$src"; then + refspecs+=("$ref:refs/tags/${ref#refs/source/tags/}") + say "tag ${ref#refs/source/tags/}" + fi + done < <("${git[@]}" for-each-ref --format='%(refname)' refs/source/tags/) + [ "${#refspecs[@]}" -eq 0 ] || [ -n "$DRY_RUN" ] \ + || "${git[@]}" push -q "$MIRROR_TARGET" "${refspecs[@]}" +} + +DRY_RUN="" +case "${1:-}" in + "") sync ;; + --dry-run) DRY_RUN=1; sync ;; + --settings) settings ;; + --transform) [ -n "${2:-}" ] || die "--transform needs a directory"; transform "$2" ;; + *) sed -n '4,7p' "$0" >&2; exit 2 ;; +esac diff --git a/scripts/upstream-status.sh b/scripts/upstream-status.sh index 5d3b150..a8f7c37 100755 --- a/scripts/upstream-status.sh +++ b/scripts/upstream-status.sh @@ -9,7 +9,7 @@ BRANCH=master UPSTREAM=refs/remotes/upstream/$BRANCH cd "$(git rev-parse --show-toplevel)" -git fetch -q "$URL" "+$BRANCH:$UPSTREAM" +git fetch -q --no-tags "$URL" "+$BRANCH:$UPSTREAM" base=$(git merge-base HEAD "$UPSTREAM") count=$(git rev-list --count "$base..$UPSTREAM")