Skip to content

Bump the dash-network group across 1 directory with 3 updates #21

Bump the dash-network group across 1 directory with 3 updates

Bump the dash-network group across 1 directory with 3 updates #21

Workflow file for this run

# CI for leaflet.2plot.dev AND for the dash-leaflet2 PyPI package.
#
# This repo is unusual among the 2plot satellites: it ships two artifacts from
# one tree. The network's CI baseline (copied from
# dash-documentation-boilerplate, which 2plot.ai and 2plot.dev also run) covers
# the documentation site; the `package*` jobs below are this repo's own and
# cover the wheel. Both have to stay green.
#
# The network baseline, and why each piece is here:
#
# * 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;
# * `actionlint`, because an invalid workflow file is the one defect CI
# structurally cannot report: the run dies before a job exists to fail;
# * 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.
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 would otherwise start two
# runs of it — which then contend for the `ci-${{ github.ref }}` concurrency
# group below and cancel each other. The work still gets done, but every push
# leaves a `cancelled` CI run 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 here publishes, comments or tags; the deploy lives in
# cd.yml behind a `production` environment, and the PyPI release in release.yml.
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 satellite reporter
# keys off CROSS_APP_WEBHOOK_SECRET, which is absent here by design.
APP_ENV: ci
jobs:
lint:
name: lint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: pip install flake8
- name: flake8
run: flake8 lib components pages tests scripts run.py usage.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 inside
# a ${{ }} expression is a LEX error that invalidates the whole file, and
# it surfaces only as `conclusion: failure` with zero jobs and nothing to
# click. actionlint catches it in a second, with the 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:
name: pytest · ${{ matrix.backend }} · py${{ matrix.python }}
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
# Flask is what production runs (the Dockerfile sets DASH_BACKEND);
# FastAPI is run.py's local default, so both need coverage. The
# before_request ordering that makes bot_hits countable is a WSGI
# concern, which is exactly why Flask cannot be the only backend here.
python: ["3.12"]
backend: [flask, fastapi]
include:
# The docs site's Python floor and ceiling, on the default backend.
# 3.10 is the floor: python-frontmatter 1.3 imports typing.TypeGuard.
- python: "3.10"
backend: flask
- python: "3.13"
backend: flask
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
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.
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
pip install "dash-improve-my-llms[${{ matrix.backend }}]>=2.3.4"
fi
pip install pytest
- 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())
# The docs site pins 4.4.1; the PACKAGE floor (dash>=4.1) is proven
# separately by the `package-python-range` job below.
assert parts(dash.__version__)[:2] >= (4, 1), dash.__version__
# 2.3.4 is the network standard: below it `resolve_site_title` does
# not exist and this site's published identity degrades to app.title.
assert parts(pkg.__version__) >= (2, 3, 4), 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
# 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'
run: |
gunicorn run:server -b 127.0.0.1:8050 --daemon --access-logfile - --error-logfile -
for _ in $(seq 1 30); do
curl -sf http://127.0.0.1:8050/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:8050/ > /dev/null
curl -sf http://127.0.0.1:8050/pointer-events > /dev/null
# The battery, against the same server a satellite deploys.
python3 scripts/network_smoke.py --base-url http://127.0.0.1:8050
docs-compat:
name: Docs · Dash ${{ matrix.dash }} · Python ${{ matrix.python }}
runs-on: ubuntu-latest
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
# The floor, the two intermediate minors, and the current release.
dash: ["4.1.0", "4.2.0", "4.3.0", "4.4.1"]
python: ["3.12"]
include:
# The docs-site Python range, against the current Dash. A full
# cross-product would be 16 jobs for very little extra signal.
- dash: "4.4.1"
python: "3.10"
- dash: "4.4.1"
python: "3.13"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
cache: pip
# node is what makes the clientside-JS syntax check real. Without it the
# smoke test skips that check rather than failing, so a broken
# clientside_callback would sail through — which is exactly how the
# light/dark tile swaps shipped invalid JS once already.
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install Dash ${{ matrix.dash }} first
# Install the version under test BEFORE the rest, so the other
# requirements resolve against it rather than dragging in a newer Dash.
run: |
python -m pip install --upgrade pip
python -m pip install "dash[fastapi]==${{ matrix.dash }}"
- name: Install documentation-site requirements
# The pinned Dash line is stripped so it cannot override the matrix
# version. Same rule as scripts/compat_matrix.py.
run: |
grep -v 'COMPAT-MATRIX: dash' requirements.txt > /tmp/reqs.txt
python -m pip install -r /tmp/reqs.txt
python -m pip install --no-deps markdown2dash==0.1.2
- name: Report the resolved Dash version
# A silent upgrade here would make the whole matrix meaningless.
run: |
RESOLVED=$(python -c "import dash; print(dash.__version__)")
echo "requested=${{ matrix.dash }} resolved=$RESOLVED"
if [ "$RESOLVED" != "${{ matrix.dash }}" ]; then
echo "::warning::Dash resolved to $RESOLVED, not ${{ matrix.dash }}"
fi
- name: Smoke test
run: python scripts/smoke_test.py --json smoke-${{ matrix.dash }}-py${{ matrix.python }}.json
- uses: actions/upload-artifact@v4
if: always()
with:
name: smoke-${{ matrix.dash }}-py${{ matrix.python }}
path: smoke-*.json
if-no-files-found: ignore
docker:
name: docker image · boot · battery
runs-on: ubuntu-latest
timeout-minutes: 25
needs: [test]
steps:
- uses: actions/checkout@v4
# 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@v3
- name: Build the production image
uses: docker/build-push-action@v6
with:
context: .
tags: dash-leaflet2-docs: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 dash-leaflet2-docs: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, 1), f'expected dash >=4.1, image has {v}'
v = version('dash-improve-my-llms')
print('dash-improve-my-llms', v)
assert parts(v) >= (2, 3, 4), f'expected >=2.3.4 (resolve_site_title), 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) and the reporter
# stays dormant. 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 8050:8050 dash-leaflet2-docs:ci
for i in $(seq 1 60); do
if curl -sf http://127.0.0.1:8050/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 https://leaflet.2plot.dev, 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:8050
- name: Container logs (for the record)
if: always()
run: docker logs --tail 40 docs 2>/dev/null || true
package:
name: Build + verify the wheel
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
# check_release.py compares the git commit times of the bundle and
# src/ts. A shallow clone can omit the commit that last touched one
# of them, which turns the check into a false "no git history" skip.
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Release consistency
# Version drift across pyproject / package.json / package-info.json,
# a stale bundle, packaging leaks. None of these break a test run.
run: python scripts/check_release.py
- name: Build
run: |
python -m pip install --upgrade pip build twine
python -m build
- name: Check metadata
run: python -m twine check dist/*
- name: Install the wheel in a clean venv and import it
# The package must work with ONLY `dash` present — nothing from
# requirements.txt, which is the docs site's dependency set.
run: |
python -m venv /tmp/clean
/tmp/clean/bin/pip install --upgrade pip
/tmp/clean/bin/pip install dist/*.whl
/tmp/clean/bin/python - <<'PY'
import pathlib
import dash_leaflet2 as dl2
print("version:", dl2.__version__)
bundle = pathlib.Path(dl2.__file__).parent / "dash_leaflet2.js"
assert bundle.exists(), "JS bundle missing from the wheel"
print("bundle:", bundle.stat().st_size // 1024, "KB")
# The 27 MB react-docgen artifact must NOT ship.
meta = pathlib.Path(dl2.__file__).parent / "metadata.json"
assert not meta.exists(), "metadata.json leaked into the wheel"
for name in ("Map", "TileLayer", "Marker", "TileSelector", "EditControl"):
assert hasattr(dl2, name), f"missing component: {name}"
print("components OK")
PY
- name: Assert the wheel version matches pyproject
run: |
PY_VER=$(python -c "import re;print(re.search(r'^version = \"([^\"]+)\"', open('pyproject.toml').read(), re.M).group(1))")
WHEEL_VER=$(/tmp/clean/bin/python -c "import dash_leaflet2;print(dash_leaflet2.__version__)")
echo "pyproject=$PY_VER installed=$WHEEL_VER"
test "$PY_VER" = "$WHEEL_VER"
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
package-python-range:
name: Package · Python ${{ matrix.python }}
needs: package
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
# Every interpreter `requires-python` in pyproject.toml claims. This is
# what makes that claim measured rather than asserted — and it installs
# ONLY the wheel plus Dash, never the docs requirements, because the
# package's floor is not the docs site's.
python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Install the wheel (pulls in dash, nothing else)
run: |
python -m pip install --upgrade pip
python -m pip install dist/*.whl
- name: Import and build a layout
run: |
python - <<'PY'
import dash, dash_leaflet2 as dl2
from dash import Dash, html
from dash._utils import to_json
app = Dash(__name__)
app.layout = html.Div([
dl2.Map(id="m", center=[49.286, -123.12], zoom=12, children=[
dl2.TileLayer(),
dl2.Marker(id="pin", position=[49.286, -123.12],
children=dl2.Tooltip("hi")),
dl2.TileSelector(id="ts"),
dl2.EditControl(id="ec"),
dl2.LayersControl(children=[
dl2.BaseLayer(dl2.TileLayer(), name="OSM", checked=True),
dl2.Overlay(dl2.Circle(center=[49.286, -123.12], radius=800),
name="ring"),
]),
]),
])
to_json(app.layout)
print(f"dash={dash.__version__} dl2={dl2.__version__} "
f"components={len(dl2.__all__)} OK")
PY
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@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- 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