build(deps): update plotly requirement from >=6.9.0 to >=7.0.0 in the dash-network group #11
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
| # CI for llms.2plot.dev — the documentation site for dash-improve-my-llms. | |
| # | |
| # Inherited from dash-documentation-boilerplate, which is where the shape of | |
| # this file is the deliverable. Here it is just the network's CI baseline | |
| # (2plot.ai and 2plot.dev run the same one): | |
| # | |
| # * least-privilege `permissions` and a cancel-in-progress `concurrency` | |
| # group, so a workflow cannot write more than it reads and a rapid second | |
| # push does not race the first; | |
| # * an explicit `timeout-minutes` on every job — the default is six hours, | |
| # which is how a hung `curl` burns a day of runner minutes unnoticed; | |
| # * the real Docker image, built with a buildx GHA cache, then BOOTED, then | |
| # probed by the same battery that runs against production; | |
| # * version fingerprints asserted INSIDE the image, because pip metadata is | |
| # invisible from the outside and a stale artifact serves quietly; | |
| # * a secretless in-process pytest suite — no CLERK_*, no | |
| # CROSS_APP_WEBHOOK_SECRET — because the fail-closed behaviour is only | |
| # provable when nothing is configured; | |
| # * an advisory pip-audit. | |
| # | |
| # A fork changes the matrix and the image name — both done here. Everything | |
| # else is the standard; see the 2plot network standard on | |
| # boilerplate.2plot.dev (the template's own live site, not this one). | |
| name: CI | |
| # Deliberately NOT `push: branches: [main]`. cd.yml runs on that push and its | |
| # first job `uses:` this workflow, so a push to main used to start two runs of | |
| # it — which then contended for the `ci-${{ github.ref }}` concurrency group | |
| # below and cancelled each other. The work still got done (whichever run | |
| # survived did all of it), but every push left a `cancelled` CI run sitting | |
| # next to the green CD one, which reads as a failure at a glance. | |
| # | |
| # So: pull requests get their own CI, and `main` is owned by CD. There is no | |
| # coverage gap — CD cannot deploy without this workflow passing first. | |
| on: | |
| pull_request: | |
| workflow_dispatch: | |
| # Called by cd.yml so a deploy can never ship something the matrix rejected. | |
| workflow_call: | |
| # Read-only. Nothing in this workflow publishes, comments or tags; the deploy | |
| # lives in cd.yml behind a `production` environment. | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| PIP_DISABLE_PIP_VERSION_CHECK: "1" | |
| FORCE_COLOR: "1" | |
| # Never let a CI run inherit production behaviour: the base-URL guard keys | |
| # off RENDER / APP_ENV, and the satellite reporter keys off the webhook | |
| # secret. Both must stay inert here. | |
| APP_ENV: ci | |
| jobs: | |
| lint: | |
| name: lint | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-python@v7 | |
| with: | |
| # The fleet Python — tests/test_python_version.py pins every | |
| # encoding of it (Dockerfile FROM tag, matrix main, render.yaml, | |
| # this job and pip-audit) to one minor. | |
| python-version: "3.14" | |
| cache: pip | |
| - run: pip install flake8 | |
| - name: flake8 | |
| run: flake8 lib components pages tests scripts run.py | |
| # The workflows lint themselves. This is not belt-and-braces: an invalid | |
| # workflow file is the one defect CI structurally cannot report, because | |
| # the run dies before a job exists to fail. A double-quoted string in a | |
| # ${{ }} expression on line 67 of THIS file silently killed every CI and | |
| # CD run on this repo for four days — nothing to click, nothing in the | |
| # API but `conclusion: failure` with zero jobs. actionlint catches it in | |
| # a second, and (this is the point) a pull request that reintroduces it | |
| # gets a red check with the offending column underlined. | |
| - name: actionlint | |
| run: | | |
| bash <(curl -fsSL https://raw.githubusercontent.com/rhysd/actionlint/v1.7.7/scripts/download-actionlint.bash) 1.7.7 | |
| ./actionlint -color | |
| test: | |
| # SINGLE quotes inside ${{ }}. GitHub Actions expressions accept no other | |
| # string delimiter, and a double quote is a LEX error — which invalidates | |
| # the whole workflow file, not just this line. An invalid file is not | |
| # reported as a failing check: the run is created, dies in zero seconds | |
| # with zero jobs scheduled, and shows only "Failure" with nothing to open. | |
| # Every CI and CD run on this repo did that, unnoticed, for four days — | |
| # CD included, because its first job `uses:` this file. The `actionlint` | |
| # step in the lint job exists so this can never be silent again. | |
| name: py${{ matrix.python }} · ${{ matrix.backend }} · dash${{ matrix.dash && format(' {0}', matrix.dash) || ' (pinned)' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # Every backend gets a run on the FLEET Python — decided by | |
| # evidence, not preference: 3.14 ran the template's full suite and | |
| # its docker boot/battery green before it became the main | |
| # (2026-08-25), with dash 4.4.1, dash-improve-my-llms >=2.8.0 and | |
| # cryptography >=50 all importing. It is also what the production | |
| # image serves (Dockerfile FROM tag, same minor — | |
| # tests/test_python_version.py pins the two together), which the old | |
| # matrix never tested: main said 3.12 while the image was a | |
| # patch-pinned 3.11.8 that nothing here exercised. | |
| python: ["3.14"] | |
| backend: [flask, fastapi, quart] | |
| dash: [""] | |
| include: | |
| # ...the two adjacent minors keep the compat window three wide on | |
| # the default backend... | |
| - python: "3.13" | |
| backend: flask | |
| dash: "" | |
| - python: "3.12" | |
| backend: flask | |
| dash: "" | |
| # ...and the bottom of the `~=4.4.1` range is pinned explicitly on | |
| # both backends that matter, so a 4.4.0-only regression cannot hide | |
| # behind pip resolving to 4.4.1. 4.3.0 is deliberately absent: its | |
| # FastAPI backend 500s on every non-root URL (upstream, fixed in | |
| # 4.4.0), which is why requirements.txt floors at 4.4. These two | |
| # rows stay on a window leg (3.12), not the main: each include row | |
| # varies ONE axis — these vary dash, the rows above vary python — | |
| # so a red here always names its cause. | |
| - python: "3.12" | |
| backend: fastapi | |
| dash: "4.4.0" | |
| - python: "3.12" | |
| backend: flask | |
| dash: "4.4.0" | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: ${{ matrix.python }} | |
| cache: pip | |
| - name: Install the app | |
| run: | | |
| pip install -r requirements.txt | |
| # markdown2dash 0.1.2 declares gunicorn<22 against the CVE-driven | |
| # gunicorn>=23 floor. Same two-command install as the Dockerfile, | |
| # render.yaml and scripts/dev.sh. | |
| pip install --no-deps markdown2dash==0.1.2 | |
| # Dash's own extras are required for the ASGI backends: a bare | |
| # `fastapi` install is not enough for dash.backends._fastapi to | |
| # import. httpx backs starlette's TestClient. | |
| if [ "${{ matrix.backend }}" != "flask" ]; then | |
| pip install "dash[${{ matrix.backend }}]" httpx | |
| # The backend extra from PyPI, same floor as requirements.txt. | |
| pip install "dash-improve-my-llms[${{ matrix.backend }}]>=2.8.0" | |
| fi | |
| pip install pytest pytest-cov | |
| # An explicit matrix pin overrides requirements.txt, to prove the | |
| # bottom of the supported range still works. | |
| if [ -n "${{ matrix.dash }}" ]; then | |
| pip install "dash[${{ matrix.backend }}]==${{ matrix.dash }}" \ | |
| || pip install "dash==${{ matrix.dash }}" | |
| fi | |
| - name: Confirm the pinned dependency versions | |
| run: | | |
| python - <<'PY' | |
| import dash, dash_improve_my_llms as pkg, gunicorn | |
| def parts(v): | |
| return tuple(int(x) for x in v.split(".")[:3] if x.isdigit()) | |
| # 4.4 floor: 4.3.0's FastAPI backend 500s on every non-root URL. | |
| assert parts(dash.__version__)[:2] >= (4, 4), dash.__version__ | |
| # 2.8.0 is this app's floor: lib/analytics_tracker imports | |
| # `classify` / `_ledger.EVENT_FIELDS` at module scope and run.py | |
| # calls `on_document_read` unconditionally, so anything lower is | |
| # an ImportError at boot, not a degraded feature. | |
| assert parts(pkg.__version__) >= (2, 8, 0), pkg.__version__ | |
| # 21.x carried two request-smuggling CVEs (CVE-2024-6827, | |
| # CVE-2024-1135). markdown2dash's spurious <22 pin must not win. | |
| assert parts(gunicorn.__version__)[:2] >= (23, 0), gunicorn.__version__ | |
| print(f"dash {dash.__version__}, dash-improve-my-llms " | |
| f"{pkg.__version__}, gunicorn {gunicorn.__version__}") | |
| PY | |
| # No CLERK_*, no CROSS_APP_WEBHOOK_SECRET, no SESSION_SECRET here ON | |
| # PURPOSE. tests/conftest.py pins them empty and the fail-closed checks | |
| # in tests/test_access.py depend on that posture; a secret injected here | |
| # would make the suite pass for the wrong reason. | |
| - name: Test suite (${{ matrix.backend }}, zero secrets) | |
| env: | |
| DASH_BACKEND: ${{ matrix.backend }} | |
| run: pytest tests -q | |
| - name: Boot under a production server | |
| if: matrix.backend == 'flask' | |
| env: | |
| # This gunicorn serves the MATRIX LEG's interpreter (3.13/3.12 on | |
| # the window rows) while the Dockerfile declares the fleet Python — | |
| # off-contract for the battery's python_matches_declared check by | |
| # design, so it stands down here. The docker job below and CD's | |
| # verify leave it armed: there the interpreter IS the artifact. | |
| SMOKE_PYTHON_DECLARED: ignore | |
| run: | | |
| gunicorn run:server -b 127.0.0.1:8550 --daemon --access-logfile - --error-logfile - | |
| for _ in $(seq 1 30); do | |
| curl -sf http://127.0.0.1:8550/healthz && break | |
| sleep 1 | |
| done | |
| # A page that renders under the test client can still fail under a | |
| # real WSGI worker — different import path, different working | |
| # directory, no test-client conveniences. | |
| curl -sf http://127.0.0.1:8550/ > /dev/null | |
| curl -sf http://127.0.0.1:8550/networks > /dev/null | |
| # The battery, against the same server a satellite deploys. | |
| python3 scripts/network_smoke.py --base-url http://127.0.0.1:8550 | |
| docker: | |
| name: docker image · boot · battery | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| needs: [test] | |
| steps: | |
| - uses: actions/checkout@v7 | |
| # The same build Render runs. This is where a dependency-resolution | |
| # failure surfaces — at CI time, not deploy time, where the only signal | |
| # is a dashboard log while the old image keeps serving. | |
| - uses: docker/setup-buildx-action@v4 | |
| - name: Build the production image | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| tags: llms-2plot-dev:ci | |
| load: true | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| # pip metadata is invisible from outside a running host, so the versions | |
| # are asserted here, inside the artifact that actually ships. | |
| - name: Version fingerprints inside the image | |
| run: | | |
| docker run --rm llms-2plot-dev:ci python -c " | |
| from importlib.metadata import version | |
| def parts(v): | |
| return tuple(int(x) for x in v.split('.')[:3] if x.isdigit()) | |
| v = version('dash') | |
| print('dash', v) | |
| assert parts(v)[:2] >= (4, 4), f'expected dash >=4.4, image has {v}' | |
| v = version('dash-improve-my-llms') | |
| print('dash-improve-my-llms', v) | |
| assert parts(v) >= (2, 8, 0), f'expected >=2.8.0 (one classifier, the read event, geo guardrail, panel, vendor policy), image has {v}' | |
| # markdown2dash installs with --no-deps to dodge its gunicorn<22 | |
| # pin; this assert is what proves the dodge kept working. 21.x | |
| # carried two request-smuggling CVEs (CVE-2024-6827, CVE-2024-1135). | |
| v = version('gunicorn') | |
| print('gunicorn', v) | |
| assert parts(v)[:2] >= (23, 0), f'expected gunicorn>=23, image has {v}' | |
| # ...and that skipping its dependency graph did not skip the package. | |
| import markdown2dash # noqa: F401 | |
| print('markdown2dash importable') | |
| " | |
| # Boot with no secrets: Clerk falls open (dev mode), the reporter stays | |
| # dormant, the hub client reports itself disabled. What this catches is | |
| # any import-time or preload crash — the class of failure where the | |
| # platform loops the worker and the deploy never goes live. | |
| - name: Boot the container and wait for /healthz | |
| run: | | |
| docker run -d --name docs -p 8550:8550 llms-2plot-dev:ci | |
| for i in $(seq 1 60); do | |
| if curl -sf http://127.0.0.1:8550/healthz > /dev/null; then | |
| echo "healthy after ~$((i*2))s" | |
| exit 0 | |
| fi | |
| if [ "$(docker inspect -f '{{.State.Running}}' docs)" != "true" ]; then | |
| echo "container exited during boot:" | |
| docker logs docs | |
| exit 1 | |
| fi | |
| sleep 2 | |
| done | |
| echo "never became healthy; last logs:" | |
| docker logs --tail 100 docs | |
| exit 1 | |
| # The SAME script CD runs against the live host (once the SITE_URL repo | |
| # variable is set — see cd.yml's header), so a failure in CI and a | |
| # failure in production read identically. | |
| - name: Smoke battery against the booted container | |
| run: python3 scripts/network_smoke.py --base-url http://127.0.0.1:8550 | |
| # The guard on the guard (emojimart's F2 finding, 2026-08-24): the | |
| # boot step curls /healthz from OUTSIDE, which proves the app binds | |
| # and answers — but says nothing about the Dockerfile's HEALTHCHECK | |
| # instruction itself. A broken probe (wrong port, missing binary, | |
| # bad quoting) ships silently while every external check stays | |
| # green, and the orchestrator that relies on container health sees | |
| # an opaque box. Poll Docker's own verdict: with --interval=30s and | |
| # --start-period=20s the first probe lands within ~50s. | |
| - name: Assert Docker's own health verdict | |
| run: | | |
| for i in $(seq 1 30); do | |
| status="$(docker inspect -f '{{.State.Health.Status}}' docs 2>/dev/null || echo none)" | |
| if [ "$status" = "healthy" ]; then | |
| echo "docker reports healthy after ~$((i*3))s" | |
| exit 0 | |
| fi | |
| if [ "$status" = "none" ]; then | |
| echo "image has NO HEALTHCHECK — the container is opaque to its orchestrator" | |
| exit 1 | |
| fi | |
| sleep 3 | |
| done | |
| echo "HEALTHCHECK never reported healthy (last: $status) — the probe itself is broken:" | |
| docker inspect -f '{{json .State.Health}}' docs || true | |
| exit 1 | |
| - name: Container logs (for the record) | |
| if: always() | |
| run: docker logs --tail 40 docs 2>/dev/null || true | |
| pip-audit: | |
| name: pip-audit (advisory) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| # Advisory on purpose. A CVE in a transitive dependency of a docs site is | |
| # worth knowing about the day it lands, and worth nobody's broken build at | |
| # 2am. The report is the value; flip this off once the baseline is quiet. | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: "3.14" | |
| - run: pip install pip-audit | |
| # Skip local vendor/ paths — pip-audit can only assess PyPI dists. | |
| - run: | | |
| grep -v '^\./vendor/' requirements.txt > /tmp/req-pypi.txt | |
| pip-audit -r /tmp/req-pypi.txt --skip-editable |