Skip to content

fix(app-shell): one inbox feed for the bell and Home — already-read messages stop counting as "needs your attention" #1787

fix(app-shell): one inbox feed for the bell and Home — already-read messages stop counting as "needs your attention"

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

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