docs(deploy): document Tailscale Funnel as an alternative HTTPS exposure#22
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
cmeans
left a comment
There was a problem hiding this comment.
QA round 1 (v2 branch) — clean
Tree-equality check: git diff docs/tailscale-funnel-deployment docs/tailscale-funnel-deployment-v2-qa is empty. v2 is bit-identical to v1 head 6fa23f8; only the CreateEvent attribution differs.
Static verification on head eee5c8a:
| Check | Result |
|---|---|
| Diff scope | CHANGELOG.md (+16) + deploy/README.md (+93), 0 deletions. No other paths touched. |
| Drift on adjacent runtime sections | deploy/caddy/, deploy/systemd/, deploy/docker/ — zero diff. Funnel block is purely additive between Compose and Required-environment. |
| Anchor slug match | Heading ## Alternative HTTPS exposure: Tailscale Funnel → GitHub auto-slug alternative-https-exposure-tailscale-funnel. Cross-reference link #alternative-https-exposure-tailscale-funnel matches exactly. |
External links (curl -sI -L) |
tailscale.com/kb/1223/funnel → 308 (canonical KB redirect, expected); tailscale.com/install.sh → 200; img.shields.io/endpoint?url=... → 200 (SVG); pypi.org/project/ → 200. All four resolve. |
| In-tree code touched | none — src/, tests/, deploy unit files, Caddyfile, Dockerfile/compose all unchanged. Awareness pypi-winnow-downloads-status / project:pypi-winnow-downloads entries unaffected. |
CHANGELOG [Unreleased] / Added entry |
present, accurately describes section + trade-offs + orthogonality |
| CI on PR head | all SUCCESS (test 3.11/3.12/3.13, lint, typecheck, on-push, qa-approved) |
Tailscale CLI commands (test-plan item 4, optional): not exercised against a live tailscale binary in this session, but each command is canonical against current Tailscale CLI surface — tailscale up, tailscale funnel --bg <url>, tailscale funnel status, tailscale funnel --https=443 off. Tear-down's --https=443 correctly matches the default public Funnel port (the localhost-side 127.0.0.1:8443 from setup is the local port, not the Funnel-public port). Leaving item 4 unchecked since it's marked optional in the test plan.
Test-plan checkboxes: ticked items 1, 2, 3, 5, 6 (all verified pre-merge). Item 4 left for the maintainer if they want to exercise the commands on a live host.
No findings. Transitioning label to Ready for QA Signoff.
|
Applying |
Self-hosters behind CGNAT, on rotating residential IPs, or who'd rather not expose a home IP in public DNS have been left without a documented path in deploy/. Tailscale Funnel is a clean fit for the use case: routes inbound traffic from the public internet through Tailscale's relay to a localhost port, with HTTPS terminated by Tailscale, on the free Personal plan. Adds a new ## Alternative HTTPS exposure: Tailscale Funnel section positioned after the three runtime approaches (systemd / Docker / Compose) and before Required environment. The section is orthogonal to runtime — Funnel just swaps in for Caddy + Let's Encrypt + DDNS + the router port-forward, regardless of which runtime above you pick. A short cross-reference paragraph after the Pick-an-approach table points readers at it. Trade-offs documented: - URL is <device>.<tailnet>.ts.net on the free plan, locked to the tailnet (custom domains require a paid plan). - Funnel's public-facing HTTPS port must be 443, 8443, or 10000; local service can listen on any port. - Non-configurable bandwidth limits (Tailscale doesn't publish exact figures). Once-per-day JSON cached at shields.io's CDN is a non-issue. - One more daemon to keep updated; end-to-end encrypted relay so your home IP stays hidden. Five-command setup walks through tailscale install + up, systemd-run'd python3 -m http.server bound to 127.0.0.1:8443, a single tailscale funnel --bg invocation, URL discovery, and a curl smoke-check. Tear-down is two commands. Includes the URL-encoded shields.io endpoint snippet for the README badge update. CHANGELOG entry under [Unreleased] / Added describes the new section, the trade-offs documented, and the orthogonality to runtime choice. No code change; deploy/README.md + CHANGELOG.md only.
eee5c8a to
31b21c0
Compare
cmeans
left a comment
There was a problem hiding this comment.
QA round 2 (post-rebase) — clean
Rebase verification: New head 31b21c0, previous head eee5c8a. git diff eee5c8a..31b21c0 returns empty — the working tree is bit-identical, only the parent commit moved forward onto the new main (which now includes #20 + #21). No content changes from round 1.
Re-verification on 31b21c0:
| Check | Result |
|---|---|
git diff old vs new head |
empty (bit-identical tree) |
Diff vs new main |
CHANGELOG.md +16, deploy/README.md +93, 0 deletions — same scope as round 1 |
Drift check on deploy/caddy/, deploy/systemd/, deploy/docker/ |
zero diff vs new main |
CHANGELOG [Unreleased] / Added ordering |
new Tailscale entry above the pre-existing Acknowledgments/License/pypinfo entries — clean rebase, no duplicate sections |
| Cross-ref anchor + heading | still co-located (line 43 link → line 156 heading); slug match unchanged |
| External links re-curl'd | all 200 (tailscale.com/kb/1223/funnel, tailscale.com/install.sh, img.shields.io/endpoint, pypi.org/project/) |
CI on 31b21c0 |
all SUCCESS (test 3.11/3.12/3.13, lint, typecheck, on-push, qa-approved, dependabot.yml — the latter is now active because #21 merged) |
Test-plan checkboxes from round 1 still apply (1, 2, 3, 5, 6 ticked; 4 left optional). No new findings.
Re-applying Ready for QA Signoff.
Three mechanical edits: - pyproject.toml: version "0.1.0" -> "0.1.1" - CHANGELOG.md: insert `## [0.1.1] - 2026-04-26` directly under the (still empty) `## [Unreleased]` header so all 12 PRs' worth of bullets that have been accumulating since v0.1.0 ship are now categorized under the 0.1.1 release. Updated the link refs at the bottom: [Unreleased] now compares from v0.1.1, and a new [0.1.1] entry compares v0.1.0...v0.1.1. - uv.lock: refreshed by `uv lock` so the locked pypi-winnow-downloads version (0.1.1) matches pyproject.toml. What ships in v0.1.1 (highlights — full changelog under ## [0.1.1]): Library fixes (operator-visible): - collector: _write_health OSError no longer escapes per-package isolation. Disk-full / perm errors now produce structured `winnow-collect: ...; health file write failed: [Errno 28] No space left on device` exit instead of a raw traceback. Closes #32. - collector: stale_threshold_days is now actually consulted — the "warn if previous run is older than N days" feature documented in config.example.yaml since v0.1.0 finally fires. Log-only per the documented v1 contract; degrades silently on first-run / unreadable / malformed / future- timestamped previous _health.json. Closes #33. Documentation: - README acknowledgments / license / BigQuery dataset link refresh (PR #15) - README shields.io URL canonicalization (PR #27, closes #16) - deploy/README.md Tailscale Funnel as alternative HTTPS exposure (PR #22) - deploy/README.md "Pick an approach" table updated to reflect the new Caddy logging shape (in PR #30) CI / project infrastructure (no PyPI consumer impact, but hardens future releases): - Community health files: CONTRIBUTING / CoC / SECURITY / issue templates (PR #20) - .github/dependabot.yml across pip + github-actions + docker ecosystems (PR #21) - Dependabot PR hygiene cascade from cmeans/mcp-synology: PULL_REQUEST_TEMPLATE.md + auto-CHANGELOG workflow (App- token authenticated so required CI re-fires on the bot's HEAD SHA) + dependabot.yml prefix fix (PR #25). Validated end-to-end via the first two real Dependabot bumps PR #23 (codecov-action 5->6) and PR #24 (python 3.13-slim -> 3.14-slim). - deploy-smoke CI job that builds the Dockerfile, smokes the entrypoint, validates compose+Caddyfile against caddy:2 (PR #29, closes #7). Promoted to required status check on the main-protection ruleset 2026-04-26 22:43 (issue #31 closed via operator action). - deploy/caddy/Caddyfile.example gains global error logger + per-site access logger with built-in lumberjack rotation, documents the validate-as-root gotcha (PR #30). Live CT 112 deployment fixed in the same change. - 100% coverage on src/ via real tests (no `# pragma: no cover`), with `fail_under = 100` gate in pyproject.toml so future regressions trip CI (PR #38, closes #37). Verified locally: 71/71 pytest pass, ruff/format/mypy clean, coverage gate green at 100.00%. After this merges: 1. Tag the squash-merge commit as v0.1.1 and push the tag — publish.yml fires and uploads to PyPI via the existing trusted-publisher OIDC flow. 2. Update the live CT 112 deployment to install pypi-winnow-downloads==0.1.1 from PyPI (currently runs a wheel built from main, but pinning to the released version keeps deploy reproducible). 3. Close any post-release follow-ups Chris wants tracked. Co-authored-by: cmeans-claude-dev[bot] <272174644+cmeans-claude-dev[bot]@users.noreply.github.com>
) Documentation-only patch release. v0.1.1 shipped with three CHANGELOG entries miscategorized under `### Fixed` instead of `### Added`: - `deploy/README.md` Tailscale Funnel section (originally added in PR #22) - README `## Acknowledgments` and `## License` sections (PR #15) - README `## Install` pointer to pypinfo's GCP setup (PR #15) The orphaning was introduced when PR #25 added new `### Added` and `### Changed` blocks at the top of the then-active `## [Unreleased]` section without repositioning the entries from earlier PRs. PR #27 later inserted a `### Fixed` block between `### Changed` and the orphans, which is the layout that shipped in v0.1.1. Three mechanical edits: - `pyproject.toml`: version 0.1.1 → 0.1.2 - `CHANGELOG.md`: insert `## [0.1.2] - 2026-04-27` with one `### Fixed` bullet describing the recategorization; move three orphan bullets from `## [0.1.1]` `### Fixed` to `## [0.1.1]` `### Added`; update link refs (Unreleased compares from v0.1.2, new [0.1.2] compares v0.1.1...v0.1.2) - `uv.lock`: refreshed by `uv lock` so locked version (0.1.2) matches pyproject.toml No code, dependency, or runtime behavior changes. Closes #28. Co-authored-by: cmeans-claude-dev[bot] <272174644+cmeans-claude-dev[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Self-hosters behind CGNAT, on rotating residential IPs, or who'd rather not expose a home IP in public DNS have been left without a documented path in
deploy/. Tailscale Funnel is a clean fit: routes inbound public-internet traffic through Tailscale's relay to a localhost port, with HTTPS terminated by Tailscale, on the free Personal plan.Adds a new
## Alternative HTTPS exposure: Tailscale Funnelsection todeploy/README.md, positioned after the three runtime approaches (systemd / Docker / Compose) and before## Required environment. The section is orthogonal to runtime choice — Funnel just swaps in for Caddy + Let's Encrypt + DDNS + the router port-forward, regardless of whether the collector runs as a systemd timer, host-cron'd Docker, or Composerun-once. A short cross-reference paragraph after the Pick an approach table points readers at it so it's discoverable without reading the whole doc end-to-end.Trade-offs documented:
<device>.<tailnet>.ts.neton the free plan, locked to the tailnet (custom domains require a paid plan).Setup walked end-to-end:
Five-command path against the bare-systemd runtime —
tailscale up,systemd-run'dpython3 -m http.serverbound to127.0.0.1:8443, a singletailscale funnel --bg, URL discovery viatailscale funnel status, and acurl -sIsmoke-check. Tear-down is two commands. Includes the URL-encoded shields.io endpoint snippet for the README badge update.CHANGELOG entry under
[Unreleased]/Addeddescribes the new section, the trade-offs, and the orthogonality to runtime choice.No code change;
deploy/README.md+CHANGELOG.mdonly.Test plan
deploy/README.mdrenders correctly on the GitHub blob view (anchor link from the Pick-an-approach paragraph resolves).tailscale.com/kb/1223/funnel,tailscale.com/install.sh, the embedded shields.io endpoint URL, the embedded PyPI project URL.pypi-winnow-downloads-statusandproject:pypi-winnow-downloadsawareness entries are unaffected (no in-tree code touched, no behavior change).tailscale up,tailscale funnel --bg http://127.0.0.1:8443,tailscale funnel statusshould all be valid against current Tailscale CLI).#alternative-https-exposure-tailscale-funnelmatches the auto-generated GitHub anchor for the heading.deploy/caddy/Caddyfile.example,deploy/systemd/*.service, and the existing systemd / Docker / Compose sections are unchanged — Funnel is additive, not a replacement.