Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ name: CI
on:
push:
branches: [main]
# ready_for_review is listed because draft PRs skip the test matrix: it runs
# once, when the PR is marked ready, not on every push while it is a draft.
# Lint still runs on drafts (see the lint job).
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
# Weekly drift detector: firmware, the Printables GraphQL API, OrcaSlicer
# flags and new CVEs (pip-audit) all move without a commit in this repo.
schedule:
Expand All @@ -23,14 +27,61 @@ permissions:
contents: read

jobs:
# A squash merge whose tree is byte-identical to the PR head it came from
# was already tested: the ruleset only lets a PR merge once all six required
# checks passed on that head. Re-running the matrix on main would test the
# same files again. Any other push (a merge onto a main that moved, a commit
# without "(#N)") is tested. Only branch pushes are checked; release.yml
# calls this workflow from a tag push, and the release gate always runs.
# Uses git alone (contents: read), since release.yml grants nothing more.
merged:
name: Skip if this merge was already tested
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
outputs:
already-tested: ${{ steps.compare.outputs.already-tested }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- id: compare
env:
MESSAGE: ${{ github.event.head_commit.message }}
run: |
pr=$(printf '%s\n' "$MESSAGE" | head -n 1 | sed -nE 's/.*\(#([0-9]+)\)$/\1/p')
if [ -z "$pr" ]; then
echo "No (#N) in the commit subject: testing."
exit 0
fi
if ! git fetch --no-tags --depth=1 origin "refs/pull/$pr/head"; then
echo "Could not fetch PR #$pr: testing."
exit 0
fi
merged_tree=$(git rev-parse 'HEAD^{tree}') || merged_tree=""
tested_tree=$(git rev-parse 'FETCH_HEAD^{tree}') || tested_tree=""
if [ -n "$merged_tree" ] && [ "$merged_tree" = "$tested_tree" ]; then
echo "Same tree as PR #$pr's head, which passed the required checks: skipping."
echo "already-tested=true" >> "$GITHUB_OUTPUT"
else
echo "Tree differs from PR #$pr's head (main moved under it): testing."
fi

test:
name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
needs: merged
# Runs unless the PR is a draft or the merge was already tested. A failed
# or skipped "merged" job leaves the output empty, which means test. A
# skipped matrix job reports its unexpanded name, so a draft never shows
# the six required check names as passed.
if: >-
${{ !cancelled()
&& !(github.event_name == 'pull_request' && github.event.pull_request.draft)
&& needs.merged.outputs.already-tested != 'true' }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
# Full Python coverage on cheap Linux runners; one smoke job each on
# macOS (10x minute multiplier) and Windows (2x) at the newest Python.
# Full Python coverage on Linux; one job each on macOS and Windows at
# the newest Python. Those runners start slower (and bill at 10x / 2x
# if the repo is ever private; public-repo runners are free).
os: [ubuntu-latest]
python-version: ["3.10", "3.12", "3.14"]
include:
Expand Down Expand Up @@ -132,6 +183,11 @@ jobs:

lint:
name: Lint (ruff + bandit)
needs: merged
# Not skipped on drafts: a skipped job reports success under this fixed
# name, which is a required check, and could sit beside a real failure on
# the same commit. Lint is one quick Linux job; the matrix is the cost.
if: ${{ !cancelled() && needs.merged.outputs.already-tested != 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
Expand Down
37 changes: 36 additions & 1 deletion .github/workflows/mutation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name: Mutation baseline
on:
workflow_dispatch:
schedule:
# 06:15 UTC daily
# 06:15 UTC daily; the "changed" job skips the run when main has not moved.
- cron: "15 6 * * *"

concurrency:
Expand All @@ -16,8 +16,43 @@ permissions:
contents: read

jobs:
# The nightly run re-tests main only when main changed since the last green
# baseline: the same commit gives the same score, so most nights there is
# nothing to do. A manual run always runs. A failed baseline keeps running
# nightly until one passes.
changed:
name: Main changed since the last green baseline
runs-on: ubuntu-latest
permissions:
actions: read
outputs:
run: ${{ steps.check.outputs.run }}
steps:
- id: check
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
EVENT: ${{ github.event_name }}
SHA: ${{ github.sha }}
run: |
if [ "$EVENT" != "schedule" ]; then
echo "run=true" >> "$GITHUB_OUTPUT"
exit 0
fi
last=$(gh run list --workflow mutation.yml --branch main --status success --limit 1 --json headSha --jq '.[0].headSha // ""') || last=""
if [ -n "$last" ] && [ "$last" = "$SHA" ]; then
echo "main is still $SHA, which already has a green baseline: skipping."
echo "run=false" >> "$GITHUB_OUTPUT"
else
echo "main is $SHA; last green baseline was ${last:-unknown}: running."
echo "run=true" >> "$GITHUB_OUTPUT"
fi

mutation:
name: Safety-module mutation baseline
needs: changed
# A failed "changed" job leaves the output empty, which means run.
if: ${{ !cancelled() && needs.changed.outputs.run != 'false' }}
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
Expand Down
14 changes: 13 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ that CI's clean checkout does not have — expected locally, not a failure to ch
`-m "not live"` never runs it. See [docs/live-printer-smoke.md](docs/live-printer-smoke.md).
Never run against a printer mid-print; print start needs `BAMBU_LIVE_PRINT_CONFIRM`.
- **Mutation baseline** (safety modules): `./scripts/run_mutation_baseline.sh` —
[docs/mutation-baseline.md](docs/mutation-baseline.md). Nightly CI only, not every PR.
[docs/mutation-baseline.md](docs/mutation-baseline.md). Nightly CI only (when `main` has changed), not every PR.
- CI enforces a "no test-awareness in production code" rule: production modules must not
branch on `Mock` / `unittest in sys.modules`. Prefer injecting collaborators over
patching module globals. Domain code raises `BambuError` / `abort`; `sys.exit` only in
Expand Down Expand Up @@ -136,6 +136,18 @@ scoreboard for what is actually ticked.
- Follow `docs/quality-roadmap.md` and `docs/test-backlog.md` when adding tests.
- Do not add Claude-Session or similar trailers to commits or PRs.

## Pull requests and CI

- **Open a PR as a draft while you are still pushing to it** (`gh pr create --draft`).
A draft runs only the lint job; the test matrix (Linux, macOS, Windows) runs once when the
PR is marked ready (`gh pr ready`), so intermediate pushes don't each start it. A draft
cannot be merged, so nothing lands untested.
- After a squash merge, CI on `main` runs only if the merged files differ from the PR's
last tested commit, which happens when `main` moved while the PR was open. A tag push
(a release) always runs the full matrix.
- The weekly scheduled run still tests `main` with nothing changed, to catch firmware,
OrcaSlicer, Printables and CVE drift.

## Releases (maintainers)

Full procedure, including the **yank/rollback runbook**: [docs/releasing.md](docs/releasing.md).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ The numbers in the badges are measured on this repo, not aspirational:
| **Coverage** | **93.5%** line+branch across the package; CI fails the build below **90%** |
| **Platforms** | Linux, macOS, and Windows in CI, on Python 3.10 / 3.12 / 3.14 |
| **Blocking gates** | `ruff`, `ruff format`, `mypy` (whole package, `check_untyped_defs`), `bandit`, `pip-audit`, plus layer-boundary and JSON-schema-drift checks |
| **Mutation testing** | Nightly, scoped to the safety-critical modules (download validation, SSRF guards, slicer options, job payload) |
| **Mutation testing** | Nightly whenever `main` has changed, scoped to the safety-critical modules (download validation, SSRF guards, slicer options, job payload) |
| **Packaging** | Every CI run installs the built wheel *and* the sdist and runs the CLI from them |

Coverage is deliberately honest — `except Exception` / `pass` / `continue` are **not**
Expand Down
9 changes: 2 additions & 7 deletions docs/MASTER-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,8 @@ The work we chose, in order. **No task from the user? Take the top 🤖 box unde

- [ ] **D-2 · Dependabot PRs #150 (actions) and #152 (python deps)** · P2 · 🧑
Open since 2026-10-26 and 2026-11-02. *Done when:* merged or closed.
- [ ] **D-3 · Keep `"bambu"` in the PyPI keywords?** · P2 · 🧑
Unruled since 2026-07-25; recommendation was keep (nominative search term, not a name).
- [ ] **L-1 · Post to r/BambuLab** · P1 · both · after L-2
- [ ] **L-1 · Post to r/BambuLab** · P1 · both
🤖 rewrites the held copy to lead with `plate tui`/`plate go`; 🧑 posts (Tue–Thu, 10–11 EDT).
- [ ] **L-2 · Cut 0.6.0** · P1 · both
Release PR on `release/0.6.0` (2026-11-06) sets 0.6.0 and dates the changelog. 🧑 merges it and approves the `v0.6.0` tag push (it publishes to PyPI).
Then 🤖: `pip install platecli==0.6.0` and `'platecli[tui]==0.6.0'` in a clean venv (`plate --version`, `plate --sim status`), then the `0.7.0.dev0` bump PR (`docs/releasing.md` steps 4 and 6).

## Next

Expand All @@ -29,4 +24,4 @@ The work we chose, in order. **No task from the user? Take the top 🤖 box unde

Nothing chosen yet; new findings go in `docs/plan/inbox.md`.

**Highest ids given:** D-3 · L-4 · S-3 · retired: D-1, L-3, S-1, S-2, S-3
**Highest ids given:** D-3 · L-4 · S-3 · retired: D-1, D-3, L-2, L-3, S-1, S-2, S-3
2 changes: 1 addition & 1 deletion docs/mutation-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
**Current measurement:** 2026-08-04 — **50.7%**, floor raised 40 → **48** (re-run before changing the floor again)
**Tool:** mutmut 3.6.0
**Reproduce:** `./scripts/run_mutation_baseline.sh`
**CI:** `.github/workflows/mutation.yml` — `workflow_dispatch` + nightly `schedule` only
**CI:** `.github/workflows/mutation.yml` — `workflow_dispatch` + nightly `schedule` only; the nightly run is skipped when `main` has not changed since the last green baseline
(**not** on every `pull_request`; full runs are minutes-long and would slow PR feedback)

Related: [quality-roadmap.md](quality-roadmap.md), [test-backlog.md](test-backlog.md).
Expand Down
2 changes: 2 additions & 0 deletions docs/plan/done.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

One line per finished plan box, newest first: `- date · id · what happened · commit`.

- 2026-11-06 · L-2 · 0.6.0 released: PR #154 merged, tag `v0.6.0` → `b0ef746`, and release.yml passed (CI matrix, build, PyPI, GitHub Release). `platecli==0.6.0` and `[tui]` (textual 8.2.8) installed from PyPI in clean Python 3.12 venvs; `plate --version` and `plate --sim status` passed. `plate tui` was not run in a real terminal, and the live-printer smoke was not run. Main is bumped to 0.7.0.dev0 · this commit
- 2026-11-06 · D-3 · ruled: keep `"bambu"` in the keywords and prose, never in the product name (`docs/plan/settled.md`) · this commit
- 2026-11-06 · L-3 · closed: folded into L-2, which checks the 0.6.0 install from PyPI (a 0.5.1 check is stale once 0.6.0 ships) · this commit
- 2026-11-06 · D-1 · PR #153 (audit round 4) squash-merged after all 6 checks passed on its last commit · e925f96
- 2026-11-05 · S-3 · setup shows a lone mDNS result and asks (no → manual entry); unspecified/multicast addresses are skipped, public/loopback ones need an explicit yes, also when picked from several · this commit
Expand Down
5 changes: 5 additions & 0 deletions docs/plan/settled.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Settled

The user's answered decisions, newest first. Read before reopening one.

- 2026-11-06 · D-3 · **"Bambu" in names vs. prose.** "Bambu" is fine wherever it describes the printers: PyPI keywords, "for Bambu Lab printers", docs. It never goes in the product's name or title ("bambu cli", "bambu unofficial cli" and the like). Adding "unofficial" doesn't fix that, because the trademark is still in the name. The PyPI description already uses the safe form, "local CLI for Bambu Lab printers (not affiliated with Bambu Lab)".
2 changes: 2 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
ruleset's required checks to match, or PRs will block forever waiting on a check that no
longer reports.
- The commit you are about to tag is **green on CI** (`gh run list --branch main --limit 1`).
A run whose test jobs were skipped because "this merge was already tested" counts: the
same tree passed the required checks on the PR. The tag push re-runs the matrix anyway.
Tags must only ever point at green commits: `release.yml` re-runs the full CI matrix
via `uses: ./.github/workflows/ci.yml` before build/publish, so a red commit fails the
release rather than shipping — but do not rely on that as your first line of defence.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
name = "platecli"
# Unreleased work carries a .devN suffix so a source build is never mistaken
# for the released wheel of the same number. Drop the suffix when tagging.
version = "0.6.0"
version = "0.7.0.dev0"
description = "platecli — local CLI for Bambu Lab printers (not affiliated with Bambu Lab)"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading