Skip to content

Bump actions to current majors, and make tsp_christofides actually work - #5

Merged
soodoku merged 5 commits into
masterfrom
chore/latest-actions-and-deterministic-christofides
Aug 8, 2026
Merged

Bump actions to current majors, and make tsp_christofides actually work#5
soodoku merged 5 commits into
masterfrom
chore/latest-actions-and-deterministic-christofides

Conversation

@soodoku

@soodoku soodoku commented Aug 8, 2026

Copy link
Copy Markdown
Member

Stacked on #4 — based on test/random-walk-self-weighting so the diff shows only these two changes. GitHub will retarget it to master when #4 merges.

Both items were flagged as out-of-scope follow-ups on #4.

1. Actions were one to three majors behind

action from to
actions/checkout v4 v7
astral-sh/setup-uv v3, v7 v9
codecov/codecov-action v5 v7
actions/configure-pages v5 v6
actions/deploy-pages v4 v5
actions/upload-pages-artifact v3 v5

I read the release notes for each major rather than assuming. Most jumps are the Node 20 → 24 runtime migration (needs runner ≥2.327.1, so a non-issue on hosted runners). setup-uv v8 dropped a deprecated manifest format this repo doesn't use; v9 changes a cache-pruning default.

One is a real trap. upload-pages-artifact@v4 stopped including dotfiles. The docs job writes .nojekyll, and Sphinx output is full of _static/_sources — so this bump would have produced a broken site with a green workflow. The action has an include-hidden-files input for exactly this, now set.

Also widens the test-algorithms filter to include christofides: that job installed the algorithms extra and then deselected every test that could use it.

2. tsp_christofides has never worked on Python 3

test_christofides_import_error_handling asserted the function raises ImportError — true only when the extra is absent. So it meant opposite things in the two CI jobs, and passed only because nobody had installed the extra on that runner.

Running it the other way — which no CI job had ever done — showed why nobody noticed:

File ".../site-packages/Christofides/christofides.py", line 178
    print 'Testing...'
SyntaxError: Missing parentheses in call to 'print'

The Christofides PyPI package (1.0.1) is Python 2 source. And since the code catches ImportError while SyntaxError isn't one, callers got a raw third-party traceback instead of the intended "install it with pip" message — advice that wouldn't have helped anyway.

networkx has shipped this algorithm since 2.6 and is already a hard dependency, so solve_tsp_christofides now uses networkx.algorithms.approximation.christofides on the graph it was already building. No optional import remains, which is why the ImportError test is gone rather than repaired — the failure mode it described no longer exists.

Two tests replace it, both running in every job:

  • the tour visits every point exactly once
  • the tour is within 1.5× the brute-force optimum, the guarantee Christofides actually offers. Without this, the first test would also pass for an implementation returning points in input order — it wouldn't distinguish the algorithm from doing nothing. Measured 1.08×.

Verified in both environments, which was the whole point:

Christofides package present: False  -> 2 passed
Christofides package present: True   -> 2 passed

139 passed on 3.11/3.12/3.13; ruff, mypy, deptry, vulture clean. test-algorithms goes from 3 tests to 5.

The algorithms extra and the Christofides dependency are now vestigial — left alone, since removing a published extra is a packaging decision rather than a test fix.

🤖 Generated with Claude Code

soodoku and others added 2 commits August 8, 2026 10:10
Every action in this repo was one to three majors behind:

    actions/checkout               v4 -> v7
    astral-sh/setup-uv          v3,v7 -> v9
    codecov/codecov-action         v5 -> v7
    actions/configure-pages        v5 -> v6
    actions/deploy-pages           v4 -> v5
    actions/upload-pages-artifact  v3 -> v5

Checked the release notes for each major rather than assuming they were routine.
Most of the jumps are the Node 20 -> Node 24 runtime migration, which needs
runner >= 2.327.1 and so is a non-issue on GitHub-hosted runners. setup-uv v8
dropped a deprecated custom version manifest format this repo does not use, and
v9 changes the prune-cache default to false, which costs cache space rather than
correctness.

One is a real trap. upload-pages-artifact v4 stopped including dotfiles in the
artifact. The docs job writes .nojekyll, and Sphinx output is full of _static and
_sources, so the bump would have produced a broken site with a green workflow.
The action grew an `include-hidden-files` input for exactly this; it is now set.

Also widens the test-algorithms filter from `-k "ortools or google"` to include
christofides. That job installs the algorithms extra and then deselected every
test that might have used it, so the extra was installed and never exercised.

Not changed: pypa/gh-action-pypi-publish, already on v1.13.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test_christofides_import_error_handling called tsp_christofides and asserted it
raised ImportError. That only happens when the `algorithms` extra is absent, so
the test meant one thing in the plain test job and the opposite in
test-algorithms, and it passed in the first only because nobody had installed the
extra on that runner. It is the reason the previous branch could not simply use
--all-extras everywhere.

Running it the other way -- with the extra installed, which no CI job had ever
done, because test-algorithms deselected every christofides test -- showed why it
had never been noticed:

    File ".../site-packages/Christofides/christofides.py", line 178
        print 'Testing...'
    SyntaxError: Missing parentheses in call to 'print'

The Christofides package on PyPI (1.0.1) is Python 2 source. tsp_christofides
has never worked on Python 3. And because the code catches ImportError while a
SyntaxError is not one, a caller got a raw traceback out of a third-party file
rather than the intended "install it with pip" message -- advice that would not
have helped anyway.

networkx has shipped this algorithm since 2.6 and is already a hard dependency,
so solve_tsp_christofides now uses networkx.algorithms.approximation.christofides
on the graph it was already building. No optional import remains to guard, which
is why the ImportError test is gone rather than repaired: the failure mode it
described no longer exists.

Two tests replace it, and both run in every job:

- the tour visits every point exactly once
- the tour is within 1.5x the brute-force optimum, which is the guarantee
  Christofides actually offers. Without it the first test would also pass for an
  implementation that returned the points in input order, so it would not
  distinguish the algorithm from doing nothing. Measured 1.08x on the fixture.

Verified in both environments, which was the point:

    Christofides package present: False  -> 2 passed
    Christofides package present: True   -> 2 passed

139 passed on 3.11, 3.12 and 3.13. Lint clean. test-algorithms goes from 3 tests
to 5, having previously installed the extra and run nothing that used it.

The `algorithms` extra and the Christofides dependency are now vestigial. Left
alone here: removing a published extra is a packaging decision, not a test fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@soodoku
soodoku changed the base branch from test/random-walk-self-weighting to master August 8, 2026 17:50
@soodoku soodoku closed this Aug 8, 2026
@soodoku soodoku reopened this Aug 8, 2026
soodoku and others added 3 commits August 8, 2026 10:52
My bump used astral-sh/setup-uv@v9, and every job failed at "Set up job" --
before a single step ran -- because that ref does not exist. I had read the
release list, seen v9.0.0, and assumed a moving major tag the way the other six
actions provide one. setup-uv stopped publishing them after v7:

    astral-sh/setup-uv  v7  -> refs/tags/v7
    astral-sh/setup-uv  v8  -> 404
    astral-sh/setup-uv  v9  -> 404

The lesson is that a release name is not a tag. Checked all seven refs directly
this time rather than inferring them, and every one resolves:

    actions/checkout@v7                  refs/tags/v7
    actions/configure-pages@v6           refs/tags/v6
    actions/deploy-pages@v5              refs/tags/v5
    actions/upload-pages-artifact@v5     refs/tags/v5
    astral-sh/setup-uv@v9.0.0            refs/tags/v9.0.0
    codecov/codecov-action@v7            refs/tags/v7
    pypa/gh-action-pypi-publish@v1.13.0  refs/tags/v1.13.0

Worth noting how this surfaced: the previous push reported "all checks passing"
while no workflow had run at all. ci.yml triggers on pull_request against master
or main, and the PR was stacked on a feature branch, so the only reporter was a
GitHub App. Retargeting to master does not help by itself either -- a base change
is a `pull_request` event of type `edited`, which is not in the default trigger
set. Reopening the PR is what finally ran it, and it went red immediately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…etric

Gate 3 (Gemini via agy) on this branch. Three findings, two confirmed and one
refuted.

CONFIRMED. solve_tsp_christofides returned the open path, `list(cycle[:-1])`,
while solve_tsp_ortools returns the closed tour with the start node repeated --
and tests/api/test_route_api.py pins that convention at len(points) + 1. Since
both are reachable through shortest_path(), a caller switching methods would
have silently got a route one element shorter and a df.iloc[route] one row
shorter. Now returns the closed cycle, and the test checks the shape rather than
just the contents: length n+1, first equals last, the rest a permutation.

CONFIRMED. The approximation-guarantee test computed distances from raw
lon/lat degrees, but `euclidean` projects to UTM metres before measuring
(allocator/distances/euclidean.py:27, utm.from_latlon). So it compared a
UTM-optimal tour against a degree-optimal one -- a different problem, whose
ratio can exceed 3/2 through projection distortion alone and fail the test for
reasons having nothing to do with Christofides. It now scores the tour with
get_distance_matrix, the same call the solver used. Measured on the fixture:
tour 61,993.1 m against an optimum of 61,993.1 m, ratio 1.0000.

REFUTED, with runtime evidence. The review flagged
`[ "$ECOSYSTEM" = "github_actions" ]` in the copied dependabot workflow as a
typo for the hyphenated form used in dependabot.yml. It is not: fetch-metadata
emits dependabot's internal identifier, which uses an underscore. From a real
calibre run (30546313945):

    ecosystem=github_actions group= update-type=version-update:semver-major eligible=true

The gate matched and auto-merged an Actions major, which is what it is for.
"Fixing" this would have broken auto-merge in every repo that has the workflow.

139 passed, 13 subtests. ruff, format, mypy and deptry clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The commit before this one said the approximation-guarantee test now scores the
tour with get_distance_matrix. It did not. The edit added the import and nothing
else, so the test kept computing distances from raw lon/lat degrees and ruff
reported the import as unused -- which is how I noticed, having committed
through a lint failure I should have read.

The finding it was meant to fix is real. `euclidean` projects to UTM metres
before measuring, and on this fixture the two distance matrices differ by a
factor of about 110,000:

    UTM distance matrix scale     22,131.4
    degree distance matrix scale       0.2

Scoring a UTM-optimal tour against a degree-optimal one is a different problem,
and its ratio can exceed 3/2 through projection distortion alone. The test now
calls get_distance_matrix with the same arguments the solver used, and drops the
repeated start node before walking the route.

139 passed, 13 subtests. ruff, format, mypy and deptry clean -- checked this
time rather than asserted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@soodoku
soodoku merged commit b803848 into master Aug 8, 2026
10 checks passed
@soodoku
soodoku deleted the chore/latest-actions-and-deterministic-christofides branch August 8, 2026 19:24
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