Skip to content

docs(ci): Publish to libtmux.org alongside git-pull.com - #756

Open
tony wants to merge 29 commits into
docs-sitefrom
docs-site-deploy
Open

tony wants to merge 29 commits into
docs-sitefrom
docs-site-deploy

Conversation

@tony

@tony tony commented Sep 6, 2026

Copy link
Copy Markdown
Member

Stacked on #755 — review that one first. This branch is #755 plus the deploy work.

Additive: libtmux.git-pull.com keeps publishing exactly as it does today, and libtmux.org is added alongside it. Nothing stops updating when this merges.

Summary

  • Add a second publish, to libtmux.org under en/py/latest/, through libtmux/docs's shared reusable-deploy.yml — the same workflow every port calls, which writes only that port's own prefix.
  • Build twice. The two destinations need different bytes, not one artifact published to both.
  • Keep libtmux.git-pull.com's publish byte-for-byte as it is on master, running inside its own build job.
  • Separate the secrets: LIBTMUX_ORG_* for the new destination, LIBTMUX_DOCS_* left meaning exactly what it means today.
  • Move the paths-filter gate into its own job, so one output feeds both publishes instead of the condition being repeated on every step.

Why two builds and not one artifact twice

libtmux.git-pull.com libtmux.org
Served at bucket root en/py/latest/
/_shell/shell.js nothing there the site's chrome
/search/ this build's own search page the site-wide Pagefind search
Build LIBTMUX_DOCS_STANDALONE=1 flag unset

One artifact published to both is wrong in one direction or the other: set the flag and libtmux.org gets an unskinned nested site with the wrong search; unset it and git-pull.com gets a /search/ that redirects to itself. tests/test_docs_conf.py pins both shapes, including the empty-string form the matrix passes.

The git-pull.com sync stays inside its build job rather than moving behind an artifact. It is a live site, and upload-artifact does not preserve the symlinks that sync is explicitly told to follow.

Secrets

Set, and verified present on the repository — three for each destination:

Secret Destination
LIBTMUX_ORG_ROLE_ARN, LIBTMUX_ORG_BUCKET, LIBTMUX_ORG_DISTRIBUTION libtmux.org
LIBTMUX_DOCS_ROLE_ARN, LIBTMUX_DOCS_BUCKET, LIBTMUX_DOCS_DISTRIBUTION libtmux.git-pull.com, untouched

The role trusts repo:tmux-python/libtmux:environment:docs — the name-only subject, which this repo can use because it predates GitHub's 2026-07-15 immutable-subject cutoff.

Still unset, and worth doing before or soon after this merges: the docs environment has no protection rules. A deployment branch-and-tag policy (master, v*) is what actually restricts which refs may publish, since environment: rewrites the OIDC sub claim to drop any ref: clause.

Verification, and its limit

docs.yml triggers only on push to master, so no PR check exercises any of this. First execution is the merge. What I could check instead:

  • The four git-pull.com publish steps are byte-equivalent to master's — compared as parsed YAML, not by eye
  • path-prefix: py/latest publishes to en/py/latestreusable-deploy.yml prepends $locale/ whenever port is set, so passing en/py/latest would double it
  • py/latest passes that workflow's own prefix validation: unrooted, no .., not a bare reserved top-level name
  • Every required input and secret is supplied, checked against the workflow_call block rather than against the example
  • version-kind: trunk is one of the kinds it accepts
  • The artifact lands where it expects: it downloads to dist/ and syncs dist/, so the tree must be at the artifact root — path: docs/_build/html gives that
  • The pinned commit's reusable-deploy.yml parses identically to the previously pinned one — its diff is comments only, compared as parsed YAML rather than read
  • concurrency.queue is valid despite actionlint 1.7.12 rejecting it: it is in GitHub's published workflow schema, and deploy-shell.yml in libtmux/docs uses it with successful runs. That is actionlint lagging, and it is the only diagnostic it reports on this file
  • Either deploy, end to end. Not observable before merge

One departure from the documented recipe

reusable-deploy.yml's header asks callers to pin a tag. This pins a commit, with the release named in a trailing comment — the form Dependabot reads to offer a bump.

libtmux/docs has cut and deleted v1 and v2, and its replacement is a 0.x prerelease series documented as moving. This uses: runs another repository's workflow inside this one, with id-token: write and a role that can write the bucket, so a ref that can be repointed means what executes here changes with no diff in this repository and no review. A commit cannot be repointed. The pinned commit is the one the deleted v2 pointed at, and its reusable-deploy.yml is byte-identical to every other candidate.

This is deliberately unlike the rest of the file, which pins moving majors (actions/checkout@v7, actions/cache@v6). Those are first-party actions running with this workflow's own permissions; this one runs a third party's workflow with id-token: write and bucket-write credentials, which is the case the pinning guidance is actually about.

Note the trailing comment is documentation here, not automation: this repository has no .github/dependabot.yml, so nothing will offer the bump. Bumping is manual until one exists.

The Cloudflare purge stays, unlike the previous revision of this branch — it fronts libtmux.git-pull.com, which is still being published.

@codecov

codecov Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 52.45%. Comparing base (1aae8a6) to head (16ec75d).

Additional details and impacted files
@@              Coverage Diff              @@
##           docs-site     #756      +/-   ##
=============================================
+ Coverage      52.37%   52.45%   +0.08%     
=============================================
  Files             26       26              
  Lines           3729     3729              
  Branches         747      747              
=============================================
+ Hits            1953     1956       +3     
+ Misses          1472     1469       -3     
  Partials         304      304              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tony tony changed the title docs(ci): Publish to libtmux.org under this port's prefix docs(ci): Publish to libtmux.org alongside git-pull.com Sep 6, 2026
@tony
tony marked this pull request as ready for review September 6, 2026 11:36
why: 0.12.10 repairs `exclude-newer-package`, which this repository
leans on: its pyproject.toml exempts 17 first-party git-pull packages
from the 3-day cooldown so a sibling release does not block every
contributor's `uv sync` while it ages in. Before 0.12.10, `--locked`
failed when those per-package cutoffs differed only for packages
outside the resolution, and uv wrote them to uv.lock in
non-deterministic order, so a re-resolve could churn the lockfile
header by itself. 0.12.10 fixes both, and lets `uv lock --check` reuse
a lockfile when a package-specific cutoff is disabled -- which is the
exact shape of every `= false` entry in this repository.

0.12.11 is a supply-chain fix on the same path: source archives are
verified against the hashes recorded in uv.lock before their metadata
is read or their build backend runs, so a tampered sdist is rejected
before any of its code executes. It also trims surrounding whitespace
from `.python-version` entries and speeds up wheel installs by
dropping per-file temporary directories.

No lockfile revision or resolver-algorithm change lands in the span, so
this moves the pinned binary only and leaves uv.lock alone.

uv 0.12.12 and 0.12.13 exist but stay excluded by the 3-day
supply-chain cooldown this estate resolves under (exclude-newer =
"3 days"): published 2026-09-09 16:45Z and 2026-09-10 19:27Z, they
lapse 2026-09-12 16:45Z and 2026-09-13 19:27Z.

what:
- .tool-versions: uv 0.12.9 -> 0.12.11

See also:
- https://github.com/astral-sh/uv/releases/tag/0.12.10
- https://github.com/astral-sh/uv/releases/tag/0.12.11
why: unchanged from the last bump -- libtmux still has no
`package.json` and no CI workflow invokes Node, so `.nvmrc` only pins
the version a contributor's `nvm use` picks up to run prettier through
`just format-markdown`. 24.21.0 is a routine 24.x LTS minor rather
than a security release: refreshed root certificates (NSS 3.126),
OpenSSL 3.5.8, undici 7.29.1 and corepack 0.36.0, plus semver-minor
additions (non-throwing `MIMEType.parse`, faster `net.BlockList`,
reworked histograms). None of that reaches a prettier run over
markdown, so this is contributor-toolchain hygiene.

Taking it now rather than the newest release: Node 26.8.2 exists but
is 26.x, and this pin deliberately tracks the 24 LTS line; 24.21.0
published 2026-09-08 21:51Z, ahead of this sweep's 2026-09-09 09:57Z
cooldown cutoff.

what: bump only `.nvmrc`. No package.json or workflow references
Node, so nothing else changes.

See also: https://nodejs.org/en/blog/release/v24.21.0
why: this repo leaves `[tool.ruff.lint] select` unset, so ruff's
default rule set applies underneath `extend-select`; `ruff check
--show-settings` confirms ASYNC230, B031, DTZ901, PTH208, RUF102
enabled here. None of 0.16.6's stable changes are exercised by this
tree: ASYNC230 (no async functions in this tree), B031's
`match`-subject panic fix (no `match` statements here), PTH208's
`os.listdir` fix (unused here), DTZ901 rejecting `tzinfo=None` for
`datetime` bounds, RUF102 dropping its `lint.external` hint. The
remainder of the release -- the PT020 autofix, the TID254/TID255 fix
loop, I001 pragma-comment handling and the pytest-fixture-autouse
recategorisation -- is preview-gated, and `linter.preview` is disabled
here.

Verified rather than assumed: `ruff check` and `ruff format --check`
produce byte-identical output under 0.16.5 and 0.16.6 across this tree,
so the bump lands no new diagnostics and no reformatting.

ruff 0.16.7 released 2026-09-10 18:03Z, after this sweep's 2026-09-09
10:20Z cooldown cutoff, so it is excluded from resolution; 0.16.6 is
the newest visible release.

what: bump only the ruff lock entry. The ruff floor in pyproject.toml
is untouched; raising it is /ruff:bump's job.

See also: https://github.com/astral-sh/ruff/releases/tag/0.16.6
tony and others added 26 commits September 12, 2026 05:30
why: 16.6.1 is a bug-fix release, and most of it is about fixture
lifetime across reruns. Five of the seven fixes restore module-, class-
and session-scoped fixture teardown in situations where a rerun
previously skipped it: when a `flaky` condition is falsy (#351), when a
teardown error rules out the re-run (#356), and when only a subtest
failed in the call phase (#357). Teardown reports also reach
`pytest_runtest_logreport` for rerun attempts now (#237), and negative
rerun counts no longer skip the initial run (#359). This reaches here:
this repository configures `--reruns`, so those reruns actually happen
here, and it defines 6 module-, class- or session-scoped fixtures whose
teardown the release repairs; it runs under pytest-xdist, which brings
in #358 (StatusDB connections between workers are now authenticated
with a per-session token) and #350 (superseded built-in subtest
failures no longer survive into the final result when rerunning under
xdist). The release also replaces deprecated `Config.getvalue()` calls
with `Config.getoption()`, which is what keeps the plugin working
against current pytest.

what: bump only the pytest-rerunfailures lock entry; the floor in
pyproject.toml is untouched.

See also:
- https://github.com/pytest-dev/pytest-rerunfailures/blob/16.6.1/CHANGES.rst
- https://pypi.org/project/pytest-rerunfailures/16.6.1/
why: Keep runtime and development dependencies current after the
three-day cooldown.

what:
- ast-serialize 0.11.0 -> 0.11.2
- coverage 7.16.0 -> 7.16.1
- gp-furo-theme 0.1.0a37 -> 0.1.0a39
- gp-sphinx 0.1.0a37 -> 0.1.0a39
- pytest-rerunfailures 16.6.1 -> 16.7
- ruff 0.16.6 -> 0.16.8
- sphinx-autodoc-api-style 0.1.0a37 -> 0.1.0a39
- sphinx-autodoc-pytest-fixtures 0.1.0a37 -> 0.1.0a39
- sphinx-autodoc-typehints-gp 0.1.0a37 -> 0.1.0a39
- sphinx-fonts 0.1.0a37 -> 0.1.0a39
- sphinx-gp-llms 0.1.0a37 -> 0.1.0a39
- sphinx-gp-opengraph 0.1.0a37 -> 0.1.0a39
- sphinx-gp-sitemap 0.1.0a37 -> 0.1.0a39
- sphinx-gp-theme 0.1.0a37 -> 0.1.0a39
- sphinx-ux-autodoc-layout 0.1.0a37 -> 0.1.0a39
- sphinx-ux-badges 0.1.0a37 -> 0.1.0a39
- types-docutils 0.23.0.20260827 -> 0.23.0.20260917
- urllib3 2.7.0 -> 2.8.0
- uvicorn 0.52.4 -> 0.53.0

## Impact
The gp-sphinx update fixes missing Markdown twin links, requests for
removed tabs.js assets, and Sphinx 8.1 parameter descriptions. All
documentation siblings advance together.
Resolved urllib3 includes fixes for HTTPS proxy TLS settings and
streaming response handling.

See also:
https://github.com/git-pull/gp-sphinx/blob/v0.1.0a39/CHANGES
https://github.com/astral-sh/ruff/releases/tag/0.16.8
https://github.com/urllib3/urllib3/releases/tag/2.8.0
why: Use the latest uv release outside the three-day cooldown.

what:
- Update the resolver used by local development and CI.

## Impact

The intervening releases exclude post-cutoff artifacts from lockfiles,
improve cold-cache resolution, and fix symlinked install destinations.

See also:
https://github.com/astral-sh/uv/releases/tag/0.12.12
https://github.com/astral-sh/uv/releases/tag/0.12.15
Furo's own search.html needs a JS index this build doesn't serve at
this path; the shell already indexes every port at /search/. Overriding
the search.html template (rather than adding a page) works regardless
of the theme, and the redirect wording matches the existing rediraffe
stubs so scripts/audit-site.mjs's redirect check skips it.
Closes notes/status.md's "Python and C++ are unskinned islands" glitch:
without this file Furo has no --lt-* mapping and paints its own stock
blue. Imports https://libtmux.org/_shell/tokens.css and maps the shared
~25 semantic tokens onto Furo's --color-* contract. The html_css_files
and html_js_files entries wiring it (plus shell.js) into conf.py landed
in 7380cd478 alongside an unrelated search-page fix from concurrent work
in this worktree.
…hable

A var() on an undefined custom property with no fallback resolves to the
guaranteed-invalid value, so every remapped --color-* would compute to
unset until libtmux.org resolves (notes/status.md: never deployed) --
worse than the stock-Furo glitch this adapter exists to close. Fall back
to Furo's own stock color per property instead, so an unreachable
tokens.css degrades to unmodified Furo rather than a broken page.
Pagefind logs a missing html[lang] warning when indexing the assembled
site; the rediraffe-generated stubs in this tree have the same gap, but
there's no reason to match it here too.
An absolute https://libtmux.org/_shell/ URL resolves nowhere until the site
is deployed, so the injected chrome was invisible in every local preview and
the design-token bridge could not be checked at all. A root-relative path
resolves on both the deployed origin and a local preview server.
why: The header cited a checkout path under the author's home
directory as the source of the fallback colours — a tracked file in a
public repository naming a machine only one person has. The rest had
grown past what a reader of this file needs.

what:
- Name the gp-furo-tokens package instead of a local checkout path
- Say which repository the design notes live in, rather than citing
  paths that resolve nowhere from here
- Trim to what the file cannot be read without: the mapping's purpose,
  why every fallback is Furo's own value, and the cascade order
why: The shell integration is correct only where this tree is served
under libtmux.org, at /py/<version>/api/. docs.yml publishes the same
tree to the bucket root behind libtmux.git-pull.com, where /_shell/
holds nothing and /search/ is the page itself — so the override turned
that host's working search into a page that refreshed to itself for
ever, and asked for a shell.js that 404s.

what:
- Gate the search override and shell.js on LIBTMUX_DOCS_STANDALONE,
  on by default for the two consumers that nest the build, and set to
  1 by docs.yml, which does not
- Move the override to _templates_shell so the gate can drop it
  without disturbing the templates gp_sphinx already expects
- Guard the redirect on the page's own path, so a tree served at a
  root cannot refresh to itself even with the gate wrong
- Leave the token adapter on in both: it degrades to stock Furo alone
why: Which of chrome and the search override a build carries decides
whether libtmux.git-pull.com keeps a working search or serves a page
that redirects to itself. Nothing re-ran that check; it was a pair of
builds done by hand, and docs.yml only runs on master, so a wrong gate
would first be seen on the deployed site.

what:
- Read docs/conf.py the way Sphinx does, under both env settings, and
  assert what each resolves templates_path and html_js_files to
- Assert the override sits outside _templates, which the gate cannot
  drop, and that the redirect is guarded on the page's own path
- Both bite: flipping the gate fails four, replacing the guard with a
  bare meta refresh fails the fifth
why: The flag is correct only while this tree is published to a bucket
root. When the sync moves to a per-port prefix under libtmux.org the
build becomes nested and the flag becomes wrong, but nothing said so —
and the failure is quiet: the port publishes with no chrome and the
wrong search, which reads as a broken shell rather than a stale flag.

what:
- Name the condition that retires the flag, and what leaving it does
why: The sync wrote the whole bucket root with --delete, which is the
shape libtmux.org's per-port prefixes exist to prevent, and it left the
Python docs outside the site: every /en/py/<version>/ URL 403s because
no port tree has been published. libtmux/docs is public as of today, so
its reusable deploy workflow can be called from here.

what:
- Split the job: build uploads docs/_build/html, publish calls
  libtmux/docs reusable-deploy.yml and deploys only en/py/latest
- Pass path-prefix unprefixed by locale; the workflow prepends it
- Add the workflow-level concurrency group every caller must set
- Drop LIBTMUX_DOCS_STANDALONE: the tree is nested now, so the shell's
  chrome and site-wide search are reachable and belong on
- Pin the reusable workflow to a commit rather than a moving branch
why: The SHA pin stood in for a tag that did not exist when this was
written; libtmux/docs has since tagged v1, which is the ref its own
header asks callers to use and bump on review.

what:
- Point the publish job at @v1, whose reusable-deploy.yml is
  byte-identical to the commit it replaces
why: The previous shape moved the publish, which would have stopped
libtmux.git-pull.com updating the moment it merged. Serving both costs
one more build and leaves a failure on the new side with no effect on
the old one.

what:
- Build twice: the two sites need different bytes, not one artifact
  published to both. git-pull.com is served at a root and is built
  standalone; libtmux.org nests at en/py/latest and carries the shell
- Publish git-pull.com from inside its own build, exactly as before,
  rather than behind an artifact that would not keep its symlinks
- Give libtmux.org its own LIBTMUX_ORG_* secrets, leaving LIBTMUX_DOCS_*
  meaning what it means today
- Move the paths-filter gate into its own job, so one output feeds both
  and no step repeats the condition
- Pin the shared workflow to v2
why: libtmux/docs deleted v1 and v2, so the `@v2` here resolved to
nothing and this workflow could not have started. Its replacement is a
0.x prerelease series documented as moving, and a ref that can be
repointed changes what executes in this repository — with
`id-token: write` and a role that can write the bucket — without a diff
here or a review.

what:
- Pin the commit the deleted tag pointed at, whose reusable-deploy.yml
  is byte-identical to both deleted tags
- Name the release it belongs to in a trailing comment, which is the
  form Dependabot reads to offer a bump
why: libtmux/docs has settled its convention — callers pin a full-length
commit and name the release beside it — and the commit carrying that is
the one to depend on. Its reusable-deploy.yml differs from the pinned
one in comments only; the parsed workflow is identical.

what:
- Pin e30bcba4, which tag v0.1.0-alpha.1 names
- Correct this comment: it claimed Dependabot would offer the bump, and
  nothing here will, since the repository has no dependabot.yml
/en/py/latest/ 403s on libtmux.org — nothing has published under a port
version prefix. This lets the branch publish without merging, which also
sidesteps the base-branch problem: this PR targets docs-site, so merging
it would trigger nothing.

Repinned to ce9d7ed, which fixes the manifest upsert: jq precedence made
it add an array to an object, so every publish failed after syncing. Found
on the first real run, from libtmux-rs.

libtmux.git-pull.com is unaffected — its leg publishes in the job before.

Drop docs-site-deploy from the trigger when merging to master.
/en/py/latest/ was serving Furo, and /en/py/latest/concepts/ 403'd — so
the port switcher was broken for Python from every shared page.

The libtmux.org leg uploaded docs/_build/html, on the assumption that the
Sphinx site is this port's tree. It is not: en/py/latest/ is the shared
shell rendered with Python's code fences, and the gp-sphinx output belongs
at en/py/latest/api/ inside it. Publishing Sphinx to the prefix replaced
the whole tree, api and all.

That leg now runs libtmux/docs's build-site.sh, which renders the shell
and runs sphinx itself from this checkout, and uploads the assembled
en/py/latest. Without --skip-refs, because Python's api/ is the real
render rather than the redirect stub every other port gets — it is what
check-style-parity.mjs measures against.

libtmux.git-pull.com is untouched: its leg still builds standalone and
publishes in-job, before this one runs.
Both reached the API reference as a name and a base class with no sentence.
One says a primary key was never set; the other that a filter named a lookup
that does not exist, which is what a mistyped keyword arrives as.
It is the default value of every `scope` parameter and appeared in the API
reference as a name with a `...` body. The distinction worth stating is that
it is not the same as passing no scope: tmux resolves the option itself.
why: Port builds still use the previous docs shell.

what:
- Pin the docs checkout and deploy workflow to the published refresh.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant