diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53b8847..b137284 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,10 @@ on: pull_request: push: branches: [dev, main] + # .github/workflows/renovate-lockfile.yml calls this after it commits + # bun.lock: a push it makes with GITHUB_TOKEN starts no workflow of its own, + # and dispatch is what GitHub exempts from that rule. + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} diff --git a/.github/workflows/renovate-lockfile.yml b/.github/workflows/renovate-lockfile.yml new file mode 100644 index 0000000..a6705ab --- /dev/null +++ b/.github/workflows/renovate-lockfile.yml @@ -0,0 +1,60 @@ +name: Renovate lockfile + +# Renovate cannot read a Bun workspace catalog yet, so `renovate.json5` updates +# the catalog in the root package.json through a regex custom manager. A custom +# manager writes no lock file, and `bun install --frozen-lockfile` in CI refuses +# a catalog that moved without bun.lock. This job closes that one gap. +# +# The push below carries GITHUB_TOKEN, and a push made with that token starts no +# workflow. GitHub exempts workflow_dispatch and repository_dispatch from that +# rule, which is why ci.yml declares the first and why the last step here calls +# it. GitHub reads that trigger from the default branch, so the call answers 422 +# until this change reaches main; the step says so rather than failing silently. + +on: + push: + branches: ["renovate/**"] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + actions: write + +jobs: + lockfile: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + # This job writes bun.lock, so it runs the Bun the repository pins rather + # than `latest`: another version can rewrite the whole file for its own + # format, and that rewrite would ride in on an automerged branch. + - uses: oven-sh/setup-bun@v2 + with: + bun-version-file: package.json + + # Resolves the manifests and rewrites bun.lock without installing or + # running a postinstall script. It is a no-op on every branch Renovate + # already locked for itself. + - run: bun install --lockfile-only --ignore-scripts + + - name: Commit bun.lock and start CI on the new commit + env: + GH_TOKEN: ${{ github.token }} + run: | + if git diff --quiet -- bun.lock; then + echo "bun.lock already matches the manifests" + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "chore(deps): refresh bun.lock" -- bun.lock + git push + if ! gh workflow run ci.yml --ref "$GITHUB_REF_NAME"; then + echo "::error::bun.lock is committed, but CI did not start on it. GitHub reads the workflow_dispatch trigger from the default branch, so this call works once ci.yml carries it on main. Start CI by hand on $GITHUB_REF_NAME." + exit 1 + fi diff --git a/apps/fumadocs/content/docs/next/contributing/dependencies.mdx b/apps/fumadocs/content/docs/next/contributing/dependencies.mdx new file mode 100644 index 0000000..c64da20 --- /dev/null +++ b/apps/fumadocs/content/docs/next/contributing/dependencies.mdx @@ -0,0 +1,130 @@ +--- +title: Dependency updates +description: How Renovate opens dependency pull requests, what it automerges, and why the Bun catalog needs a second job. +icon: PackageCheck +--- + +Renovate keeps the dependencies of this repository current. It reads `renovate.json5` at the root, and it opens every pull request against `dev`. + + + +Renovate reads that file from the **default branch** alone. `useBaseBranchConfig` defaults to `none`, so a change to `renovate.json5` on `dev` changes nothing until `main` carries it. The same holds for the `workflow_dispatch` trigger of `ci.yml`. Both reach `main` at the next release. Before that, Renovate runs on its own defaults. + + + +## What it watches + +Six managers cover the repository. + +| Manager | What it covers | +| --- | --- | +| `bun` | Every `package.json` and `bun.lock`, plus the `packageManager` field | +| `regex` | The 20 shared versions in `workspaces.catalog` of the root `package.json` | +| `github-actions` | The actions each workflow calls | +| `dockerfile` | The `oven/bun` base images, and the `docker/dockerfile` syntax line of `apps/server/Dockerfile` | +| `docker-compose` | `postgres` and `axllent/mailpit` | +| `nvm` | The Node version in `.nvmrc` | + +Two images stay out of reach. Freenary publishes `ghcr.io/suiramdev/freenary-server` and `ghcr.io/suiramdev/freenary-web` itself, and both compose entries name a `FREENARY_VERSION` variable that Renovate skips on its own. `renovate.json5` also disables the two names, which holds if either one ever takes a literal tag. + +Check the file itself after you change it: + +```bash +npx --yes --package renovate@latest renovate-config-validator renovate.json5 +``` + +## The schedule and the merge rules + +| Rule | Value | +| --- | --- | +| Branch every pull request targets | `dev` | +| When Renovate runs | Before 6am on Monday, Europe/Paris | +| Age a release must reach first | 3 days | +| Open pull requests at once | 10 | +| Automerge | Patch, minor, pin and digest, after the `ci` and `compose` checks pass | +| Review by hand | Every major update, and every package pinned to a prerelease | +| Lock file maintenance | Before 6am on the first day of the month | + +A security advisory ignores the schedule and the three-day wait. Those pull requests carry the `security` label. + + + +CI runs no tests, so a green `ci` check proves that the repository compiles, not that it works. Automerge rests on the build and on the three-day wait alone. Read the dependency dashboard each week. + + + +A package pinned to a prerelease never automerges. `effect` sits at `4.0.0-rc.115`, and Renovate reads `rc.115` to `rc.116` as a patch. The update type alone would let an unstable version of a core library into `dev`. + +A PostgreSQL major update opens no pull request. Such a major needs a dump and a restore of the database. It waits for your approval on the dependency dashboard first. + +## The dependency dashboard + +Renovate keeps one open issue titled **Dependency Dashboard**. It lists every update it holds back, every pull request it opened, and every failure. Tick a box there to ask for a branch at once. + +## The Bun catalog needs a second job + +The root `package.json` holds a `workspaces.catalog` block. It pins one version for each of the 20 packages that more than one workspace depends on. Each workspace then writes `catalog:` in place of a range. + +Renovate reads a catalog for pnpm and for yarn, and it reads no catalog for Bun. Without help, those 20 versions stay frozen. `renovate.json5` therefore declares a regex custom manager. It matches the block, then each entry inside it. Such a manager writes no lock file. A catalog pull request lands with a stale `bun.lock`, and step 1 of CI fails: + +```text +error: lockfile had changes, but lockfile is frozen +note: the catalog in package.json changed since bun.lock was saved +``` + +`.github/workflows/renovate-lockfile.yml` closes that gap. On every push to a `renovate/**` branch it runs one command, and it commits the result when the file moved: + +```bash +bun install --lockfile-only --ignore-scripts +``` + +The job then calls `gh workflow run ci.yml`. A push made with `GITHUB_TOKEN` starts no workflow. GitHub exempts the two dispatch events from that rule, and `ci.yml` declares `workflow_dispatch` for this one caller. Pull requests from this manager carry the `catalog` label. + + + +GitHub reads the `workflow_dispatch` trigger from the default branch. The call above answers 422 until `main` carries it, which happens at the next release. Until then, a catalog pull request keeps its lock file commit but starts no CI run. Start the **CI** workflow by hand on that branch, and the job log says the same. + + + + + +Delete the custom manager, this workflow, and the `workflow_dispatch` trigger of `ci.yml` on the day Renovate gains Bun catalog support. Until then the catalog has two readers, and a change to the block shape can break the second one. + + + +## What a reviewer does with these pull requests + +- Read the release notes Renovate puts in the body. It links the diff and the changelog of each version. +- A major update names its breaking changes. Run the stack and the checks of [Contributing](/docs/contributing) before you merge one. +- A red `ci` check has ordinary causes: a type error or a broken build from the new version. On a catalog branch, read the **Renovate lockfile** job first, because a missing lock file commit fails step 1. +- Never push to a Renovate branch by hand. Renovate rebases it, and your commit disappears. + +## Enable it on a fork + + + + + +### Install the app + +Install the [Renovate GitHub App](https://github.com/apps/renovate) on the fork, and grant it access to the repository. + + + + + +### Turn on automerge + +Open **Settings → General** and turn on **Allow auto-merge**. Renovate then hands each merge to GitHub. Add a ruleset on your own `dev` branch that needs the `ci` and `compose` checks. Without one, GitHub merges with nothing to wait for. + + + + + +### Answer the onboarding pull request + +Renovate opens one pull request that names the updates it found. Merge it, and Renovate starts on the schedule above. + + + + diff --git a/apps/fumadocs/content/docs/next/contributing/index.mdx b/apps/fumadocs/content/docs/next/contributing/index.mdx index 27df29a..7391562 100644 --- a/apps/fumadocs/content/docs/next/contributing/index.mdx +++ b/apps/fumadocs/content/docs/next/contributing/index.mdx @@ -70,7 +70,7 @@ One line separates them. The containerised path runs every part in Docker, behin ## The checks before a pull request -`.github/workflows/ci.yml` declares two jobs, `ci` and `compose`. GitHub starts both on every pull request, and on every push to `dev` and `main`. The `ci` timeout is 10 minutes and the `compose` timeout is 5 minutes. +`.github/workflows/ci.yml` declares two jobs, `ci` and `compose`. GitHub starts both on every pull request, and on every push to `dev` and `main`. The file also declares `workflow_dispatch`, which [Dependency updates](/docs/contributing/dependencies) explains. The `ci` timeout is 10 minutes and the `compose` timeout is 5 minutes. The `ci` job runs seven commands. Run the same seven before you open a pull request: @@ -269,6 +269,12 @@ Each workspace states its own rules next to its code: description="The five services, the hostnames, the database commands and the resets." /> + + Save` passes, and one more wrapper around the text does not. Label text nested deeper needs an explicit `aria-label`. diff --git a/renovate.json5 b/renovate.json5 new file mode 100644 index 0000000..4aeafae --- /dev/null +++ b/renovate.json5 @@ -0,0 +1,105 @@ +{ + $schema: "https://docs.renovatebot.com/renovate-schema.json", + extends: ["config:recommended", ":semanticCommits"], + + // Pull requests target the integration branch, like every other one. Renovate + // reads this file from the default branch alone (useBaseBranchConfig defaults + // to "none"), so a change here is live once it reaches main. + baseBranchPatterns: ["dev"], + timezone: "Europe/Paris", + schedule: ["before 6am on monday"], + labels: ["dependencies"], + prConcurrentLimit: 10, + + // A release that is pulled or patched within three days never reaches a + // branch here, which is what makes the automerge below safe to leave on. + minimumReleaseAge: "3 days", + + // .github/workflows/renovate-lockfile.yml commits bun.lock on Renovate's own + // branches. Without this, Renovate reads that commit as a branch someone else + // changed and stops updating and merging the pull request. + gitIgnoredAuthors: ["41898282+github-actions[bot]@users.noreply.github.com"], + + lockFileMaintenance: { + enabled: true, + schedule: ["before 6am on the first day of the month"], + }, + + vulnerabilityAlerts: { + labels: ["dependencies", "security"], + schedule: ["at any time"], + minimumReleaseAge: null, + }, + + customManagers: [ + { + // Renovate has no Bun catalog support yet, so the 20 shared versions in + // workspaces.catalog are extracted by hand. docs/engineering/platform.md + // carries why, and what to delete once upstream lands it. + description: "The Bun workspace catalog in the root package.json", + customType: "regex", + managerFilePatterns: ["/^package\\.json$/"], + matchStringsStrategy: "recursive", + matchStrings: [ + '"catalog"\\s*:\\s*\\{[^}]*\\}', + '"(?[^"]+)"\\s*:\\s*"(?[^"]+)"', + ], + datasourceTemplate: "npm", + versioningTemplate: "npm", + }, + ], + + packageRules: [ + { + description: "Everything below a major lands on its own once CI is green", + matchUpdateTypes: ["minor", "patch", "pin", "digest"], + automerge: true, + }, + { + description: "A catalog pull request needs the bun.lock job to run on it", + matchManagers: ["custom.regex"], + addLabels: ["catalog"], + }, + { + description: "Freenary publishes these two images itself. Both compose entries carry a ${FREENARY_VERSION} variable today, which Renovate skips on its own; this holds if either is ever pinned to a literal tag", + matchDatasources: ["docker"], + matchPackageNames: [ + "ghcr.io/suiramdev/freenary-server", + "ghcr.io/suiramdev/freenary-web", + ], + enabled: false, + }, + { + description: "A PostgreSQL major needs a dump and a restore, so it waits on the dashboard instead of opening a pull request", + matchDatasources: ["docker"], + matchPackageNames: ["postgres"], + matchUpdateTypes: ["major"], + dependencyDashboardApproval: true, + }, + { + description: "better-auth and its plugins are pinned to one exact version", + groupName: "better-auth", + matchPackageNames: ["better-auth", "@better-auth/**"], + }, + { + description: "The pinned Bun version: the packageManager field and the images that must match it. oven-sh/setup-bun is the action version, not the Bun version, so it stays with the other actions", + groupName: "bun", + matchPackageNames: ["bun", "oven/bun"], + }, + { + // effect sits at 4.0.0-rc.115, and Renovate reads rc.115 to rc.116 as a + // patch. CI runs no tests, so an unstable version of a library the API is + // built on must not reach dev on the build alone. + description: "A package pinned to a prerelease stays under review, whatever the update type says", + matchDatasources: ["npm"], + matchCurrentValue: "/-(?:alpha|beta|canary|dev|next|nightly|pre|rc)\\b/", + automerge: false, + }, + { + description: "The workflow actions move together", + matchManagers: ["github-actions"], + groupName: "github actions", + semanticCommitType: "ci", + }, + ], +}