From 0de75606a858a6c47e9c78fc3320b73195f96aaa Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Wed, 7 Oct 2026 12:28:56 -0400 Subject: [PATCH] chore: set up changesets for versioning and releases (#68) --- .changeset/README.md | 5 + .changeset/action-needed-trusted-proxies.md | 9 ++ .changeset/backup-restore.md | 5 + .changeset/config.json | 28 ++++++ .changeset/edit-watch-history.md | 7 ++ .changeset/imdb-search.md | 8 ++ .changeset/imports.md | 12 +++ .changeset/library-fixes.md | 14 +++ .changeset/mark-unwatched.md | 7 ++ .changeset/pre-migration-backup.md | 7 ++ .changeset/quick-watch.md | 7 ++ .changeset/recently-aired.md | 7 ++ .changeset/refresh-performance.md | 5 + .changeset/security-hardening.md | 11 +++ .changeset/watch-history.md | 7 ++ .changeset/web-fixes.md | 12 +++ .changeset/webhooks.md | 5 + .github/workflows/docker.yml | 2 + .github/workflows/release.yml | 48 ++++++++++ AGENTS.md | 24 +++++ README.md | 1 + bun.lock | 68 +++++++++++++ package.json | 7 +- scripts/release.ts | 100 ++++++++++++++++++++ scripts/version-packages.ts | 20 ++++ 25 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 .changeset/README.md create mode 100644 .changeset/action-needed-trusted-proxies.md create mode 100644 .changeset/backup-restore.md create mode 100644 .changeset/config.json create mode 100644 .changeset/edit-watch-history.md create mode 100644 .changeset/imdb-search.md create mode 100644 .changeset/imports.md create mode 100644 .changeset/library-fixes.md create mode 100644 .changeset/mark-unwatched.md create mode 100644 .changeset/pre-migration-backup.md create mode 100644 .changeset/quick-watch.md create mode 100644 .changeset/recently-aired.md create mode 100644 .changeset/refresh-performance.md create mode 100644 .changeset/security-hardening.md create mode 100644 .changeset/watch-history.md create mode 100644 .changeset/web-fixes.md create mode 100644 .changeset/webhooks.md create mode 100644 .github/workflows/release.yml create mode 100644 scripts/release.ts create mode 100644 scripts/version-packages.ts diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..23db17c --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,5 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a tool to manage versioning and changelogs. Read the full documentation at https://changesets.dev. + +We also have a quick list of common questions to get you started engaging with this project in our [FAQ](https://changesets.dev/faq). diff --git a/.changeset/action-needed-trusted-proxies.md b/.changeset/action-needed-trusted-proxies.md new file mode 100644 index 0000000..bdaf76b --- /dev/null +++ b/.changeset/action-needed-trusted-proxies.md @@ -0,0 +1,9 @@ +--- +"@sofa/server": patch +--- + +commit: e5209f80fec06509cceaa0c1b0e54ff88d8ab72b + +Rate-limit sign-ins by the real client IP, so spoofed `X-Forwarded-For` headers can't get around the limit + +**If Sofa sits behind a reverse proxy on another machine** (for example a VPS reaching Sofa over the internet or Tailscale, or Cloudflare proxying straight to it), add the proxy's address to the new `TRUSTED_PROXIES` setting, or every user will share the proxy's rate limit. Proxies on the same host or Docker network, or elsewhere on your LAN at a private address (10.x, 172.16–31.x, 192.168.x), work without changes. See [Configuration](https://sofa.watch/docs/configuration#network). diff --git a/.changeset/backup-restore.md b/.changeset/backup-restore.md new file mode 100644 index 0000000..2765978 --- /dev/null +++ b/.changeset/backup-restore.md @@ -0,0 +1,5 @@ +--- +"@sofa/server": patch +--- + +Restore backups made by older versions of Sofa: a restored database is now migrated before it replaces the live one diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..cc0f496 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@4.0.1/schema.json", + "baseBranch": "main", + "access": "restricted", + "format": "oxfmt", + "changelog": ["@changesets/changelog-github", { "repo": "jakejarvis/sofa" }], + "commit": false, + "ignore": ["@sofa/native"], + "fixed": [ + [ + "@sofa/server", + "@sofa/web", + "@sofa/api", + "@sofa/auth", + "@sofa/config", + "@sofa/core", + "@sofa/db", + "@sofa/i18n", + "@sofa/logger", + "@sofa/test", + "@sofa/tmdb", + "@sofa/tsconfig" + ] + ], + "linked": [], + "updateInternalDependencies": "patch", + "privatePackages": { "version": true, "tag": false } +} diff --git a/.changeset/edit-watch-history.md b/.changeset/edit-watch-history.md new file mode 100644 index 0000000..07839fb --- /dev/null +++ b/.changeset/edit-watch-history.md @@ -0,0 +1,7 @@ +--- +"@sofa/web": minor +--- + +commit: cb5e0c19e7efbb9d3a6e3e0a933e33ad203bb01d + +Remove a single watch from your history, or log a watch on a specific date from a title's page diff --git a/.changeset/imdb-search.md b/.changeset/imdb-search.md new file mode 100644 index 0000000..444ebe8 --- /dev/null +++ b/.changeset/imdb-search.md @@ -0,0 +1,8 @@ +--- +"@sofa/web": minor +--- + +commit: b63fbce60da8383c3e5e403966d8ed72572f42a5 +author: @souocare + +Find titles by IMDb ID: searching for an ID like `tt0133093` returns that movie or show diff --git a/.changeset/imports.md b/.changeset/imports.md new file mode 100644 index 0000000..2d32aef --- /dev/null +++ b/.changeset/imports.md @@ -0,0 +1,12 @@ +--- +"@sofa/server": patch +--- + +Make Trakt and Simkl imports more reliable + +- Accept Trakt's official export (ZIP or JSON, including files with a UTF-8 BOM) and Simkl backups with nested items +- Keep rewatches and the dates titles were added, and import Trakt season and episode entries +- Skip invalid items instead of failing the whole import, and skip specials +- Keep date-only values on the right calendar day +- Stop promptly when an import is cancelled, even near the end +- Refresh each imported show once afterward to pick up new episodes diff --git a/.changeset/library-fixes.md b/.changeset/library-fixes.md new file mode 100644 index 0000000..8f685a7 --- /dev/null +++ b/.changeset/library-fixes.md @@ -0,0 +1,14 @@ +--- +"@sofa/server": patch +--- + +Fix several library, discovery, and stats issues + +- Fully load TV shows the first time you open them +- Leave titles you've rated or watched out of recommendations +- Only offer aired episodes in Continue Watching +- Don't add duplicate watches when marking a season or show watched +- Line up stats totals with their charts, and fix month rollover +- Keep one failing title from stalling scheduled refreshes; manual jobs now start in the background and say when they're already running +- Remove duplicate cast and crew entries +- Keep the `/3` path when `TMDB_API_BASE_URL` points at a custom server diff --git a/.changeset/mark-unwatched.md b/.changeset/mark-unwatched.md new file mode 100644 index 0000000..5db6eba --- /dev/null +++ b/.changeset/mark-unwatched.md @@ -0,0 +1,7 @@ +--- +"@sofa/web": minor +--- + +pr: #52 + +Add a "Mark Unwatched" button for movies you've already watched (the `M` shortcut toggles it too) diff --git a/.changeset/pre-migration-backup.md b/.changeset/pre-migration-backup.md new file mode 100644 index 0000000..936eb0e --- /dev/null +++ b/.changeset/pre-migration-backup.md @@ -0,0 +1,7 @@ +--- +"@sofa/server": minor +--- + +commit: a4439dddb11fdf25ffac5660a3a64d1b2706d333 + +Back up the database automatically before applying migrations on upgrade. These backups appear as "Pre-upgrade backup" in Settings and are kept in the `backups` folder of your data directory. diff --git a/.changeset/quick-watch.md b/.changeset/quick-watch.md new file mode 100644 index 0000000..ef29054 --- /dev/null +++ b/.changeset/quick-watch.md @@ -0,0 +1,7 @@ +--- +"@sofa/web": minor +--- + +commit: 04bcb66436dc14c3630d29a1c1357dc1ec13747a + +Mark episodes watched straight from Continue Watching and Upcoming diff --git a/.changeset/recently-aired.md b/.changeset/recently-aired.md new file mode 100644 index 0000000..a92ad53 --- /dev/null +++ b/.changeset/recently-aired.md @@ -0,0 +1,7 @@ +--- +"@sofa/web": minor +--- + +commit: e625f319a20ca51ed7b5cdae331b8b88b5d0c164 + +Add a "Recently aired" view to Upcoming that lists aired episodes you haven't watched yet diff --git a/.changeset/refresh-performance.md b/.changeset/refresh-performance.md new file mode 100644 index 0000000..087aba6 --- /dev/null +++ b/.changeset/refresh-performance.md @@ -0,0 +1,5 @@ +--- +"@sofa/server": patch +--- + +Make metadata refreshes lighter: ended shows refresh less often, returning shows refetch only their recent seasons, empty TMDB lookups are remembered, and episode art is processed in the background diff --git a/.changeset/security-hardening.md b/.changeset/security-hardening.md new file mode 100644 index 0000000..5b28164 --- /dev/null +++ b/.changeset/security-hardening.md @@ -0,0 +1,11 @@ +--- +"@sofa/server": patch +--- + +Harden the server against malicious requests + +- Send security headers, and reject API writes that a browser sends from another site (Sofa's own address comes from `BETTER_AUTH_URL` or `CORS_ORIGIN`) +- Limit request body sizes, and require a signed-in session before accepting large uploads +- Limit the uncompressed size and file count of uploaded import ZIPs +- Validate cached image and avatar file names, and serve those files so browsers can't run scripts from them +- Only let the mobile app's sign-in proxy redirect to your OIDC provider's authorization endpoint diff --git a/.changeset/watch-history.md b/.changeset/watch-history.md new file mode 100644 index 0000000..18d2109 --- /dev/null +++ b/.changeset/watch-history.md @@ -0,0 +1,7 @@ +--- +"@sofa/web": minor +--- + +commit: b292fe37abbd73cee98c918edb915018989c657c + +Add a watch history page listing every movie and episode you've watched, and a "Last watched" sort for the library diff --git a/.changeset/web-fixes.md b/.changeset/web-fixes.md new file mode 100644 index 0000000..f5c3306 --- /dev/null +++ b/.changeset/web-fixes.md @@ -0,0 +1,12 @@ +--- +"@sofa/web": patch +--- + +Fix a batch of web app issues + +- Show translated error messages instead of raw server errors, and fix the "Tomorrow" and "Next week" labels +- Refresh lists and counts after you change a title, and make Retry buttons actually retry +- Clear cached data when you sign out or switch accounts +- Keep infinite lists from dropping their first page, and load more titles in Explore rows +- Ignore title keyboard shortcuts while a dialog or menu is open +- Explain why a password change failed diff --git a/.changeset/webhooks.md b/.changeset/webhooks.md new file mode 100644 index 0000000..b9e7548 --- /dev/null +++ b/.changeset/webhooks.md @@ -0,0 +1,5 @@ +--- +"@sofa/server": patch +--- + +Fix scrobbling from media servers: episodes from Plex and Emby no longer match the wrong show, Emby's native webhook payloads are accepted, and a show is refreshed when a scrobbled episode isn't known yet diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 94b7d79..fb949df 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -16,6 +16,8 @@ on: - "tsconfig.json" - ".github/workflows/docker.yml" tags: ["v*"] + # Dispatched by the Release workflow for each new tag + workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c90b2f4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,48 @@ +name: Release + +on: + push: + branches: [main] + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +permissions: + contents: write # push the release branch, create tags and releases + pull-requests: write # open the "Version Packages" PR + actions: write # dispatch the Docker workflow for a new tag + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version: 24 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + # Tags and releases the current version unless it already has a tag, so it only does + # something right after a "Version Packages" PR merges + - name: Publish release + run: bun run release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Opens or updates the "Version Packages" PR while changesets are pending + - name: Create release PR + uses: changesets/action@v2 + with: + version-script: bun run version-packages + commit-message: "chore(release): version packages" + pr-title: "chore(release): version packages" + env: + # Read by @changesets/changelog-github to link PRs and authors + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md index dbef215..ed31d9b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,7 @@ bun run test # Run tests bun run generate:openapi # Regenerate OpenAPI spec + docs API pages (run after contract/schema changes) bun run i18n:extract # Run LingUI's string extraction bun run i18n:claude # Prompt Claude Code to fill in untranslated strings +bun run changeset # Add a changeset describing a user-facing change (see Releases) # Database commands (run from packages/db/) cd packages/db && bun run db:push # Push schema changes to SQLite database @@ -35,6 +36,8 @@ bun run check-types bun run test ``` +If the change is user-facing, it also needs a changeset — see [Releases](#releases). + ## Architecture **Sofa** is a self-hosted movie & TV tracking app built as a Turborepo monorepo. @@ -56,6 +59,7 @@ couch-potato/ │ ├── logger/ # @sofa/logger — Pino-based structured logging (JIT) │ ├── test/ # @sofa/test — Vitest setup + in-memory DB test helpers │ └── tmdb/ # @sofa/tmdb — TMDB API client + image URL helper (JIT) +├── .changeset/ # Changesets config + pending changesets (see Releases) ├── .oxlintrc.json ├── .oxfmtrc.json ├── Dockerfile @@ -155,6 +159,26 @@ Optional: Single container, single process. Hono serves API + SPA on port 3000. API routes (`/rpc/*`, `/api/*`) mounted first; unmatched routes fall back to `index.html`. +### Releases + +Versions and changelogs are managed with [Changesets](https://changesets.dev). **Every PR with a user-facing change needs a changeset**: a Markdown file in `.changeset/` naming the affected packages and their semver bump. Run `bun run changeset`, or write one by hand with any unique filename: + +```md +--- +"@sofa/web": minor +--- + +Add a "Rewatch" button to title pages +``` + +- **The summary is user-facing** — it lands verbatim in `CHANGELOG.md` and the GitHub release notes, so write it for people running Sofa. Skip the changeset for changes users won't notice (refactors, tests, CI, internal docs). +- **Server release** — `@sofa/server`, `@sofa/web`, and every `packages/*` library are a fixed group sharing one version: the Docker image tag and the `vX.Y.Z` GitHub release. Name the packages you changed; any bump moves the whole group. +- **`@sofa/native`** is left out of Changesets (`ignore` in the config): its version is the App Store / Play Store version, bumped by hand for store submissions. Never name it in a changeset — Changesets rejects a changeset that mixes it with other packages and silently skips one that names only it. +- **`@sofa/public-api`** is versioned on its own and deploys continuously; a changeset is optional. +- **Bump types** — `patch` for fixes, `minor` for features and for breaking changes while on 0.x. Never use `major` unless asked: it would release 1.0.0. + +`.github/workflows/release.yml` automates the rest. While changesets are pending, it keeps a "chore(release): version packages" PR up to date (`bun run version-packages` bumps versions, writes changelogs, and syncs `bun.lock` and the OpenAPI spec version). Merging that PR makes `bun run release` tag `vX.Y.Z`, publish the GitHub release, and dispatch the Docker workflow for the tag. Mobile builds still go through the EAS workflows. + ## Browser Automation Use `agent-browser` for web automation (`agent-browser --help` for all commands). Auth credentials: `demo@sofa.watch` / `password`. diff --git a/README.md b/README.md index d06ce67..f47bf3d 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,7 @@ Useful commands: - `bun run lint` - `bun run format` - `bun run check-types` +- `bun run changeset` (describe a user-facing change for the next release's changelog) - `cd packages/db && bun run db:generate` - `cd packages/db && bun run db:migrate` diff --git a/bun.lock b/bun.lock index 084cb6e..e6baa28 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,8 @@ "": { "name": "sofa-monorepo", "devDependencies": { + "@changesets/changelog-github": "1.0.1", + "@changesets/cli": "3.0.3", "@lingui/cli": "catalog:", "@lingui/conf": "catalog:", "@lingui/format-po": "catalog:", @@ -564,6 +566,44 @@ "@blazediff/core": ["@blazediff/core@1.10.0", "", {}, "sha512-AOQff0zgR7cGsZL+4E7hVkmujoPUpm0J9xzWGWZj5wCjd3gmxESXAPfKyuzs93VdpQNFhHlBhfOjrcZ+XTERtQ=="], + "@changesets/apply-release-plan": ["@changesets/apply-release-plan@8.1.1", "", { "dependencies": { "@changesets/config": "^4.0.1", "@changesets/format": "^0.1.2", "@changesets/git": "^4.0.1", "@changesets/should-skip-package": "^1.0.0", "@changesets/types": "^7.0.0", "import-meta-resolve": "^4.2.0", "jsonc-parser": "^3.3.1", "semver": "^7.8.1" } }, "sha512-nk2Hbq3M9NeHMdaIRBDvawKrQJzXB9sGyBUE5yWnIKtvdcNCPj75zBekJYr0gTT2ecJH3eFeTrc8eisPImcv0Q=="], + + "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@7.0.0", "", { "dependencies": { "@changesets/errors": "^1.0.0", "@changesets/get-dependents-graph": "^3.0.0", "@changesets/should-skip-package": "^1.0.0", "@changesets/types": "^7.0.0", "semver": "^7.8.1" } }, "sha512-oEW8BxdA604kGGtDSCiHr5w9Tv4UWe9I2k61IBNZzCOE1kbYaJj4v+lFQNgcEZFkUc2pV/+hASErGDvpJOZCTg=="], + + "@changesets/changelog-git": ["@changesets/changelog-git@1.0.0", "", { "dependencies": { "@changesets/types": "^7.0.0" } }, "sha512-3Dst2Ime2Op5nd4XmWJLPIgp11ZFqJqSkVug9izK6TDcIV4YlhPS4ECbEVR+eGI0bk0r1ItogD4j2Oli87bJrA=="], + + "@changesets/changelog-github": ["@changesets/changelog-github@1.0.1", "", { "dependencies": { "@changesets/get-github-info": "^1.0.1", "@changesets/types": "^7.0.0" } }, "sha512-QUcrV7878yHlWPXXPA6gqSxNKwtVHCd1K22L7ZAoJw2yGy8K4QvjwOStD75+1Q9KD9ZSUgGS94HuYdd7HOVtpA=="], + + "@changesets/cli": ["@changesets/cli@3.0.3", "", { "dependencies": { "@changesets/apply-release-plan": "^8.1.1", "@changesets/assemble-release-plan": "^7.0.0", "@changesets/changelog-git": "^1.0.0", "@changesets/config": "^4.0.1", "@changesets/errors": "^1.0.0", "@changesets/get-dependents-graph": "^3.0.0", "@changesets/git": "^4.0.1", "@changesets/pre": "^3.0.0", "@changesets/read": "^1.0.1", "@changesets/should-skip-package": "^1.0.0", "@changesets/types": "^7.0.0", "@changesets/write": "^1.0.1", "@clack/prompts": "^1.7.0", "@manypkg/get-packages": "^3.1.0", "@pnpm/deps.graph-sequencer": "^1100.0.1", "cac": "^7.0.0", "import-meta-resolve": "^4.2.0", "launch-editor": "^2.14.1", "package-manager-detector": "^1.6.0", "semver": "^7.8.1", "tinyexec": "^1.3.1" }, "bin": { "changeset": "bin.js" } }, "sha512-L1i67bMfkK6iC/kDejuNO4pFgTRcWIObsD6Qof2woP1I4M8tj17DGlcpRaTzTv1DYSP5EMwWdEtR8X0Pj5Jg3Q=="], + + "@changesets/config": ["@changesets/config@4.0.1", "", { "dependencies": { "@changesets/get-dependents-graph": "^3.0.0", "@changesets/should-skip-package": "^1.0.0", "@changesets/types": "^7.0.0", "@manypkg/get-packages": "^3.1.0", "picomatch": "^4.0.7" } }, "sha512-PxpxSQLQSLDjceAgRq+4FWlU+HflDIw03mmPVipxoQx9xrXFUjmqTy3SE+n/2t+DqPdpPiLWVr5NRiLZvjyLwg=="], + + "@changesets/errors": ["@changesets/errors@1.0.0", "", {}, "sha512-ElN/mEzn6zmETgjwf5MclCMa9ef59sAR0lfO8VSYIsiRvbC2FbLB/92EoYw10Sl0kGixxHFiJZUSv7dA+YpR8g=="], + + "@changesets/format": ["@changesets/format@0.1.2", "", { "dependencies": { "package-manager-detector": "^1.8.0", "tinyexec": "^1.3.0" } }, "sha512-Caez5XtNXCFS/G5bwyav3wuXL0tMxVd2ZGbaumWbzN08tyzO21asCw7JZhNtVsAZDCvDRUzZN+Iit9SyRITSYA=="], + + "@changesets/get-dependents-graph": ["@changesets/get-dependents-graph@3.0.0", "", { "dependencies": { "@changesets/types": "^7.0.0", "semver": "^7.8.1" } }, "sha512-ji/t5wFA1zREKXRUePE6Qi+Qu2UgxCeSSGQrphezwvQZrp49B7sJ+8+wvM0tA7zPeSxYKCojDy3WWgrl+s+awg=="], + + "@changesets/get-github-info": ["@changesets/get-github-info@1.0.1", "", { "dependencies": { "dataloader": "^2.2.3" } }, "sha512-ifLQCky/ZtDgZ4xCiUMjnLZSJ8xk52iIzMJbBBPlZWjr2CiTNTaTDddIVWKicrUAdWuLWL7PUw2fNa1yPwLpIg=="], + + "@changesets/git": ["@changesets/git@4.0.1", "", { "dependencies": { "@changesets/errors": "^1.0.0", "@changesets/types": "^7.0.0", "@manypkg/get-packages": "^3.1.0", "picomatch": "^4.0.4", "tinyexec": "^1.3.0" } }, "sha512-6vWpIAC4LkpmlqaIu37ViT5enyd9+uBwAiGqCGhASvkSHBgx4PrtTGXWTMFYpDmZbjgyonyVgMciPrW4MqtJsA=="], + + "@changesets/parse": ["@changesets/parse@1.0.0", "", { "dependencies": { "@changesets/types": "^7.0.0", "yaml": "^2.9.0" } }, "sha512-P0iaMb9p9CRYZiTgAllEIF9AUMQHIy1G72tKlcIqJp61icZSsKQNiOPdxAMZG8m/DvZwt/oz5xEbTpike//dWg=="], + + "@changesets/pre": ["@changesets/pre@3.0.0", "", { "dependencies": { "@changesets/errors": "^1.0.0", "@changesets/types": "^7.0.0", "@manypkg/get-packages": "^3.1.0" } }, "sha512-Zm/6YliV/a2oeWTqHJf6KxLrQwgcK1i/BRDl2m0EKZvbnxV5fG9QRhwJJGshjsZTUTS6dkfURQ2K6aAvgNw/3Q=="], + + "@changesets/read": ["@changesets/read@1.0.1", "", { "dependencies": { "@changesets/git": "^4.0.1", "@changesets/parse": "^1.0.0", "@changesets/types": "^7.0.0" } }, "sha512-nzvSC6RcTiWf/dsuwZFXpLzGGsRHYCL68U3uHV7srsulU93aVWY7u2GEJiDZ+4GIIElfaW5EqtKC0UHroY+LTQ=="], + + "@changesets/should-skip-package": ["@changesets/should-skip-package@1.0.0", "", { "dependencies": { "@changesets/types": "^7.0.0" } }, "sha512-pwqoJmbONn1XgXmZXPEExgAaT+HdZLjALFTDgIm+PnS5KeO2nLtzA2/Q+4aMFY14kFMuXKG30MObhvDzWgzDgg=="], + + "@changesets/types": ["@changesets/types@7.0.0", "", {}, "sha512-c5GoiQyt3pxiXjrWSNoP8/GRf4kG+VnKzovx1OQM8dYYALlSwgedmkPmJ+ZqGxqwg9D3Bkj85Uo4KLd5BN3A0w=="], + + "@changesets/write": ["@changesets/write@1.0.1", "", { "dependencies": { "@changesets/format": "^0.1.1", "@changesets/types": "^7.0.0", "human-id": "^4.2.0" } }, "sha512-q/ThtP9gcnEP6xlv7LrY26C2mHqdGBti/MmOVngt/M/oIGYkssmQGxPK9WzBNt2juVcH/vml2WQ+ra8LXYOTaA=="], + + "@clack/core": ["@clack/core@1.5.1", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ=="], + + "@clack/prompts": ["@clack/prompts@1.8.1", "", { "dependencies": { "@clack/core": "1.5.1", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ=="], + "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], "@corvu/utils": ["@corvu/utils@0.4.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.11" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA=="], @@ -906,6 +946,12 @@ "@lingui/vite-plugin": ["@lingui/vite-plugin@6.9.0", "", { "dependencies": { "@lingui/cli": "6.9.0", "@lingui/conf": "6.9.0", "@lingui/native-tools": "^0.1.5" }, "peerDependencies": { "@babel/core": "^7.29.0 || ^8.0.0", "@lingui/babel-plugin-lingui-macro": "^5 || ^6", "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "rolldown": "^1.0.0-rc.5", "vite": "^6.3.0 || ^7 || ^8" }, "optionalPeers": ["@babel/core", "@lingui/babel-plugin-lingui-macro", "@rolldown/plugin-babel", "rolldown"] }, "sha512-CrOjXhhxV+qpbSt1EVqO8+LD37K4M/87w5Xajic0m4K8opcS+B2h+4H4a/ZThu3LnqPkL50ZlOPDYLQpuRwY+Q=="], + "@manypkg/find-root": ["@manypkg/find-root@3.1.0", "", { "dependencies": { "@manypkg/tools": "^2.1.0" } }, "sha512-BcSqCyKhBVZ5YkSzOiheMCV41kqAFptW6xGqYSTjkVTl9XQpr+pqHhwgGCOHQtjDCv7Is6EFyA14Sm5GVbVABA=="], + + "@manypkg/get-packages": ["@manypkg/get-packages@3.1.0", "", { "dependencies": { "@manypkg/find-root": "^3.1.0", "@manypkg/tools": "^2.1.0" } }, "sha512-0TbBVyvPrP7xGYBI/cP8UP+yl/z+HtbTttAD7FMAJgn/kXOTwh5/60TsqP9ZYY710forNfyV0N8P/IE/ujGZJg=="], + + "@manypkg/tools": ["@manypkg/tools@2.1.2", "", { "dependencies": { "jju": "^1.4.0", "tinyglobby": "^0.2.13", "yaml": "^2.9.0" } }, "sha512-6QEf6yqFbETdwGITKq57aYoPfX/3K8XFNwsAlx0C1M7o8cb79sv1M3w+tWuWvIcSbNqrLF7OD7YpZMVVz335hQ=="], + "@messageformat/date-skeleton": ["@messageformat/date-skeleton@1.1.0", "", {}, "sha512-rmGAfB1tIPER+gh3p/RgA+PVeRE/gxuQ2w4snFWPF5xtb5mbWR7Cbw7wCOftcUypbD6HVoxrVdyyghPm3WzP5A=="], "@messageformat/parser": ["@messageformat/parser@5.1.1", "", { "dependencies": { "moo": "^0.5.1" } }, "sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg=="], @@ -1072,6 +1118,8 @@ "@player.style/sutro": ["@player.style/sutro@0.2.1", "", { "peerDependencies": { "media-chrome": ">=1.0.0" } }, "sha512-vWtFAdu4z3+1HcDPAriX+41q+bYSOgM898XO4TxEgoNyeaBq/Aoz2cRgWFpsWaK368+n7uWCbO5n2vAszN5fhw=="], + "@pnpm/deps.graph-sequencer": ["@pnpm/deps.graph-sequencer@1100.0.1", "", {}, "sha512-pOr5+q1fLYKwFN3LAJuGZEnfXDcQ73zqgDHMtGy+K+uIoUqyY+6MeDCWFwfu+4EFuq76I5EPFofoNAI+Bmmq4A=="], + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], "@posthog/core": ["@posthog/core@1.55.3", "", { "dependencies": { "@posthog/types": "^1.413.0" } }, "sha512-XnmG9jo7WlxTeN4oCNWH4ESLQHR6ZPZ+mcq+8JxU62k5i1BAe3BSrybnmXHjqKp7e3rUvLK2bOzGZGSlfHvlNA=="], @@ -1688,6 +1736,8 @@ "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "cac": ["cac@7.0.0", "", {}, "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], @@ -1790,6 +1840,8 @@ "culori": ["culori@4.0.2", "", {}, "sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw=="], + "dataloader": ["dataloader@2.2.3", "", {}, "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA=="], + "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], "dayjs": ["dayjs@1.11.21", "", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="], @@ -2024,8 +2076,14 @@ "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], + + "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], + "fast-uri": ["fast-uri@3.1.3", "", {}, "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg=="], + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="], + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], "fb-dotslash": ["fb-dotslash@0.5.8", "", { "bin": { "dotslash": "bin/dotslash" } }, "sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA=="], @@ -2130,6 +2188,8 @@ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "human-id": ["human-id@4.2.2", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-Z+8unD6em/MJwyrkWQ2M3wygRGI1+MztbutiNwk1DCuEPXOZRWGjtcMPZYP1exY9iNZMhd7ZOSMQSo9WYtiQRw=="], + "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], @@ -2144,6 +2204,8 @@ "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], @@ -2206,6 +2268,8 @@ "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + "jju": ["jju@1.4.0", "", {}, "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA=="], + "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], "jotai": ["jotai@3.0.1", "", { "peerDependencies": { "@types/react": ">=18.0.0", "react": ">=18.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-6tS7wY9otdiNp26Wqb/1ds3OuVJVoV+tJlN4hpLrYmfFf/t9bauVt+zKBNSpS5qSl5h6GieVQ+OknVraZX+udQ=="], @@ -2238,6 +2302,8 @@ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], @@ -2458,6 +2524,8 @@ "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + "package-manager-detector": ["package-manager-detector@1.9.0", "", {}, "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA=="], + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], diff --git a/package.json b/package.json index 6ea637e..23da35f 100644 --- a/package.json +++ b/package.json @@ -60,9 +60,14 @@ "i18n:extract": "lingui extract", "i18n:claude": "bun packages/i18n/scripts/claude.ts", "sync:providers": "bun scripts/sync-tmdb-providers.ts", - "smoke:backup": "bun scripts/backup-restore-smoke.ts" + "smoke:backup": "bun scripts/backup-restore-smoke.ts", + "changeset": "changeset", + "version-packages": "bun scripts/version-packages.ts", + "release": "bun scripts/release.ts" }, "devDependencies": { + "@changesets/changelog-github": "1.0.1", + "@changesets/cli": "3.0.3", "@lingui/cli": "catalog:", "@lingui/conf": "catalog:", "@lingui/format-po": "catalog:", diff --git a/scripts/release.ts b/scripts/release.ts new file mode 100644 index 0000000..ee2d84a --- /dev/null +++ b/scripts/release.ts @@ -0,0 +1,100 @@ +/** + * Tags and publishes a GitHub release for the current server version unless that tag already + * exists, then dispatches the Docker workflow for it. The Release workflow runs this on every push + * to main; it's a no-op until a "Version Packages" PR merges and changes the version. + * + * Release notes are merged from the CHANGELOG.md of every package in the fixed version group, so a + * changeset counts no matter which of those packages it names. + * + * Usage: bun run release [--dry-run] + */ + +import path from "node:path"; + +import { $ } from "bun"; + +import changesetConfig from "../.changeset/config.json" with { type: "json" }; + +const CHANGE_TYPES = ["Major Changes", "Minor Changes", "Patch Changes"]; +// The whole group moves in lockstep, so its dependency bumps are noise: "- Updated dependencies +// [abc1234]:" then " - @sofa/api@1.2.3" lines (the default changelog generator leaves out the first +// line when only the fixed group caused the bump) +const DEPENDENCY_ENTRY = /^- (?:Updated dependencies|@sofa\/[\w-]+@\d)/; + +const dryRun = process.argv.includes("--dry-run"); + +const { version } = await Bun.file("apps/server/package.json").json(); +const tag = `v${version}`; + +const packageDirs = new Map(); +for await (const file of new Bun.Glob("{apps,packages}/*/package.json").scan()) { + const { name } = await Bun.file(file).json(); + packageDirs.set(name, path.dirname(file)); +} + +// "### Minor Changes" → its entries, deduplicated across changelogs (a changeset naming several +// packages in the group writes the same entry to each of them) +const entriesByType = new Map>(); +for (const name of changesetConfig.fixed[0]) { + const changelog = Bun.file(`${packageDirs.get(name)}/CHANGELOG.md`); + if (!(await changelog.exists())) continue; + + const lines = (await changelog.text()).split("\n"); + const start = lines.indexOf(`## ${version}`); + if (start === -1) continue; + const end = lines.findIndex((line, i) => i > start && line.startsWith("## ")); + + let entries: Set | undefined; + let entry: string[] = []; + const flush = () => { + const text = entry.join("\n").trimEnd(); + if (text && !DEPENDENCY_ENTRY.test(text)) entries?.add(text); + entry = []; + }; + for (const line of lines.slice(start + 1, end === -1 ? undefined : end)) { + if (line.startsWith("### ")) { + flush(); + const type = line.slice(4).trim(); + entries = entriesByType.get(type) ?? new Set(); + entriesByType.set(type, entries); + } else if (line.startsWith("- ")) { + flush(); + entry.push(line); + } else if (entry.length > 0) { + entry.push(line); + } + } + flush(); +} + +const notes = CHANGE_TYPES.filter((type) => entriesByType.get(type)?.size) + .map((type) => `### ${type}\n\n${[...(entriesByType.get(type) ?? [])].join("\n")}`) + .join("\n\n"); + +if (dryRun) { + console.log(`${tag}\n\n${notes || "(no changelog entries; GitHub would generate notes)"}`); + process.exit(0); +} + +// --exit-code: 0 when the tag exists, 2 when it doesn't +const remoteTag = await $`git ls-remote --exit-code --tags origin refs/tags/${tag}` + .nothrow() + .quiet(); +if (remoteTag.exitCode === 0) { + console.log(`${tag} has already been released.`); + process.exit(0); +} +if (remoteTag.exitCode !== 2) { + throw new Error(`git ls-remote failed: ${remoteTag.stderr.toString()}`); +} + +const target = (await $`git rev-parse HEAD`.text()).trim(); +const args = ["release", "create", tag, "--title", tag, "--target", target]; +if (version.includes("-")) args.push("--prerelease"); +args.push(...(notes ? ["--notes", notes] : ["--generate-notes"])); +await $`gh ${args}`; +console.log(`Released ${tag}.`); + +// Tags created with the workflow's GITHUB_TOKEN don't trigger other workflows, so start the +// Docker build for this tag explicitly. +await $`gh workflow run docker.yml --ref ${tag}`; diff --git a/scripts/version-packages.ts b/scripts/version-packages.ts new file mode 100644 index 0000000..3051a91 --- /dev/null +++ b/scripts/version-packages.ts @@ -0,0 +1,20 @@ +/** + * Applies pending changesets (version bumps + changelogs), then syncs the other files that record + * the version. The Release workflow runs this to build the "Version Packages" PR. + * + * Usage: bun run version-packages (needs GITHUB_TOKEN for @changesets/changelog-github) + */ + +import { $ } from "bun"; + +await $`changeset version`; + +const { version } = await Bun.file("apps/server/package.json").json(); + +const specPath = "docs/public/openapi.json"; +const spec = await Bun.file(specPath).json(); +spec.info.version = version; +await Bun.write(specPath, JSON.stringify(spec, null, 2)); + +// bun.lock records every workspace's version; sync it now rather than in someone's next PR +await $`bun install --lockfile-only`;