fix(app-shell): one inbox feed for the bell and Home — already-read messages stop counting as "needs your attention" #1787
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Lint | |
| # Until #2923 this workflow was `workflow_dispatch`-only, so ESLint had never | |
| # gated a PR. That mattered more than it looked: every `object-ui/*` rule that | |
| # `eslint.config.js` sets to `error` is a ratchet — added *specifically* so a | |
| # new violation fails CI, with its existing sites pre-cleaned first so the rule | |
| # lints clean on the day it lands. While nothing ran them, every one of them was | |
| # inert. | |
| # | |
| # `eslint.config.js` is the single list of those rules, and this comment | |
| # deliberately neither counts them nor names them: it used to hand-count, and | |
| # the count was stale by the time anyone read it (#3261). A hand-copied | |
| # enumeration drifts by construction, and a stale one still reads as | |
| # authoritative — `content/docs/guide/ci-cd-pipeline.md` avoids the number for | |
| # the same reason. `scripts/__tests__/lint-workflow.test.ts` holds that in | |
| # place: it fails if a count or a rule name reappears here, if the config stops | |
| # setting any `object-ui/*` rule to `error`, or if this workflow stops gating | |
| # pull requests. | |
| # | |
| # `--max-warnings` is deliberately not set: warnings repo-wide run into the | |
| # thousands, dominated by `no-explicit-any` plus React Compiler rules the config | |
| # downgrades on purpose — known historical debt, not a signal. This gate is | |
| # about errors. No exact figure here, for the reason above: this paragraph used | |
| # to carry a hand-maintained warning count and percentage that nothing | |
| # recomputed and nothing alarmed on as they aged (#3274), and | |
| # `scripts/check-lint-coverage.mjs` held a copy of the same number that would | |
| # have gone stale on its own clock. The order of magnitude is the whole | |
| # argument; the integer never was. | |
| on: | |
| push: | |
| branches: [main, develop] | |
| paths-ignore: | |
| - '**/*.md' | |
| - 'content/**' | |
| - 'docs/**' | |
| - '.changeset/**' | |
| # No `paths-ignore` here any more (objectui#3523, step 2) — it skipped the | |
| # whole workflow on a docs-only / changeset-only PR, so the `Lint` context was | |
| # absent exactly where a required check must still report. The path decision | |
| # moved into the job below. `push` above keeps its copy: nothing judges a push | |
| # to `main`. | |
| pull_request: | |
| branches: [main, develop] | |
| # ── Merge queue (objectui#3523) ──────────────────────────────────────── | |
| # The merge queue is ENFORCED on this repository by a ruleset — a direct push | |
| # to `main` returns 405 `Changes must be made through the merge queue` | |
| # (measured in #3243). Until this trigger landed, not one of the repository's | |
| # workflows subscribed `merge_group`: repo-wide `event=merge_group` runs stood | |
| # at total_count = 0, historically. A queue with nothing subscribed to it can | |
| # only have an EMPTY required-check set, so it rebuilt each PR on the current | |
| # `main` and let it through without validating anything. | |
| # | |
| # That is not a theoretical hole; it was cashed in on 2026-08-07. #3498 landed | |
| # a `scripts/` type gate, itself fully green, that left a TS2578 on `main`; | |
| # #3503, #3510 and #3516 then merged between 02:11Z and 02:15Z with `Type | |
| # Check` at conclusion=failure, and #3505 hot-fixed the result. objectstack | |
| # went through the same frames (objectstack#6067 -> #5615). | |
| # | |
| # `types:` is spelled out although `checks_requested` is the ONLY activity | |
| # type GitHub defines for `merge_group` today — the two spellings are | |
| # equivalent right now (objectstack's `ci.yml` and `lint.yml` use the bare | |
| # `merge_group:` form and produce queue builds normally, 3552 of them). Naming | |
| # the type means a second activity type added later cannot silently start | |
| # queue builds this workflow was never written for. | |
| # | |
| # `concurrency` below needs no merge-queue special case, and that was checked | |
| # rather than assumed: on `merge_group` the `github.event.pull_request` half of | |
| # the group expression is null, so the group falls back to `github.ref`, which | |
| # on a queue build is the queue's own generation — measured on objectstack, | |
| # `gh-readonly-queue/main/pr-6594-251e888ac9ace8226f3a8450951e5b40a0a84c2c`. | |
| # It can collide with neither a pull-request group (a bare PR number) nor a | |
| # push group (`refs/heads/main`), so a queue build and the PR build it came | |
| # from never cancel each other. | |
| merge_group: | |
| types: [checks_requested] | |
| workflow_dispatch: | |
| concurrency: | |
| group: lint-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| lint: | |
| name: Lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| submodules: true | |
| # `fetch-depth: 0` for the gate step below (objectui#3523): it diffs | |
| # against the merge base, which a depth-1 clone cannot resolve. | |
| fetch-depth: 0 | |
| # ── Always report; run only when it matters (objectui#3523) ────────── | |
| # `on.pull_request.paths-ignore` used to skip this whole workflow on a | |
| # docs-only or changeset-only pull request, so the `Lint` context was | |
| # simply absent there — and a required check that never reports leaves the | |
| # PR pending forever (in the merge queue, until the ruleset's 60-minute | |
| # timeout fails it). The filter moved from the trigger into the job: the | |
| # job always runs and always reports, the paths decide only whether the | |
| # expensive steps execute. `ci.yml`'s `docs` job is the in-repo precedent | |
| # for the shape, and its `type-check` job carries the long version of this | |
| # note. The list below IS the `paths-ignore` it replaced; the `push` | |
| # trigger keeps its copy, because nothing judges a push to `main`. | |
| # | |
| # Fails OPEN: if the diff cannot be computed the job runs everything, | |
| # rather than reporting green having linted nothing (objectstack#4928). | |
| - name: Decide whether this change needs a full run | |
| id: relevant | |
| run: | | |
| if [ "${{ github.event_name }}" != 'pull_request' ]; then | |
| echo 'should_run=true' >> "$GITHUB_OUTPUT" | |
| echo 'Not a pull request: push is filtered at the trigger, and a merge_group build is the last validation before main. Running everything.' | |
| exit 0 | |
| fi | |
| if ! CHANGED=$(git diff --name-only \ | |
| '${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}' -- \ | |
| . \ | |
| ':(exclude,glob)**/*.md' \ | |
| ':(exclude,glob)content/**' \ | |
| ':(exclude,glob)docs/**' \ | |
| ':(exclude,glob).changeset/**'); then | |
| echo 'should_run=true' >> "$GITHUB_OUTPUT" | |
| echo 'Could not diff against the merge base. Running everything rather than skipping silently.' | |
| exit 0 | |
| fi | |
| if [ -n "$CHANGED" ]; then | |
| echo 'should_run=true' >> "$GITHUB_OUTPUT" | |
| echo "$CHANGED" | |
| else | |
| echo 'should_run=false' >> "$GITHUB_OUTPUT" | |
| echo 'Only ignored paths changed. Skipping the steps below; this check still reports.' | |
| fi | |
| - name: Enable Corepack | |
| if: steps.relevant.outputs.should_run == 'true' | |
| run: corepack enable | |
| - name: Verify pnpm version | |
| if: steps.relevant.outputs.should_run == 'true' | |
| run: pnpm --version | |
| - name: Setup Node.js | |
| if: steps.relevant.outputs.should_run == 'true' | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22.x' | |
| cache: 'pnpm' | |
| # Every package must run ESLint or be declared a known gap. turbo skips | |
| # scriptless packages silently, so without this a package reads as clean | |
| # because nothing linted it. Runs before install: only reads package.json. | |
| - name: Verify lint coverage | |
| if: steps.relevant.outputs.should_run == 'true' | |
| run: node scripts/check-lint-coverage.mjs | |
| - name: Turbo Cache | |
| if: steps.relevant.outputs.should_run == 'true' | |
| uses: actions/cache@v6 | |
| with: | |
| path: .turbo/cache | |
| key: turbo-${{ runner.os }}-${{ github.sha }} | |
| restore-keys: | | |
| turbo-${{ runner.os }}- | |
| - name: Install dependencies | |
| if: steps.relevant.outputs.should_run == 'true' | |
| run: pnpm install --frozen-lockfile | |
| - name: Run linter | |
| if: steps.relevant.outputs.should_run == 'true' | |
| run: pnpm lint |