Skip to content

skip build scripts during uv sync - #3897

Draft
EmilyRagan wants to merge 8 commits into
mainfrom
docker-safety
Draft

EmilyRagan wants to merge 8 commits into
mainfrom
docker-safety

Conversation

@EmilyRagan

@EmilyRagan EmilyRagan commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

What changed

Add --no-build --no-binary-package openc3 to the uv scripts, and split --locked vs --frozen by where the sync runs:

  • --locked in CI and the justfile
  • --frozen in openc3/Dockerfile, docker-package-build.sh, verify-uv-cache.sh, openc3/bin/uvinstall, and the plugin docs

Why it changed

Valid SonarQube findings about installation scripts being potentially dangerous to allow to run.

The --no-build part is the fix for that finding. The --frozen -> --locked part started out global, but review found it regresses any stack whose PYPI_URL is not pypi.org, so it is now scoped to CI.

Testing strategy

CI, plus a manual check of uvinstall in a running stack (cmd-tlm-api, uv 0.12.5, demo plugin) with UV_DEFAULT_INDEX and -i pointed at an unreachable mirror.

Review notes

  • uv sync --locked errors if uv.lock does not match pyproject.toml, where --frozen always installs what is pinned in uv.lock regardless. --locked is what keeps us from changing a package and forgetting to commit the lock update, so it stays on every sync that resolves against pypi.org.
  • uv also treats a lock whose recorded registry does not match the configured index as stale under --locked. That makes --locked wrong wherever the index can be a mirror:
    • uvinstall passes -i ${PYPI_URL}/simple from PluginModel.build_pypi_args. Under --locked, every plugin shipping a uv.lock falls through to path 2 (uv pip install), which has neither the lock nor --no-build - the opposite of this PR's goal. The failure is also not clean: rejecting the lock means uv re-resolves, which needs the index's package listings, and the baked offline cache holds wheels only, so the install fails on a warm cache. The only signal is the existing Warning: line.
    • The openc3/Dockerfile syncs, the docker-package-build.sh warm-up and verify-uv-cache.sh hit the same mismatch through UV_DEFAULT_INDEX, so a mirror build breaks at image build time. These can move to --locked once the relock in fix(build): relock uv against PYPI_URL so air-gapped builds resolve #3891 lands, since each sync will then follow a relock against the same index.
  • uvinstall deliberately does not get --no-build. User plugins are allowed to depend on sdist-only packages; see the comment in docker-package-build.sh.
  • [tool.uv] build-constraint-dependencies pins the build backend, but those pins are not hash-verified the way uv.lock entries are, so the comment says it narrows the gap rather than closes it.
  • SonarQube failure is due to existing use of latest as default image tag to pull in Dockerfile.

Follow-ups (not in this PR)

  • No CI coverage for a non-default PYPI_URL, which is why this regression got as far as it did. Worth a job that stands up a local index and runs a plugin install against it.
  • uvinstall's fallback to path 2 is silent apart from a warning. Arguably it should hard-fail when the plugin ships a uv.lock.

@EmilyRagan EmilyRagan self-assigned this Sep 18, 2026
@codecov

codecov Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.04%. Comparing base (ef52d2a) to head (51f89d3).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3897      +/-   ##
==========================================
- Coverage   80.05%   80.04%   -0.02%     
==========================================
  Files         901      901              
  Lines       68355    68355              
  Branches     2645     2698      +53     
==========================================
- Hits        54722    54714       -8     
- Misses      12961    12973      +12     
+ Partials      672      668       -4     
Flag Coverage Δ
frontend 66.68% <ø> (+0.05%) ⬆️
python 80.10% <ø> (-0.01%) ⬇️
ruby-api 82.26% <ø> (-0.29%) ⬇️
ruby-backend 85.65% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@EmilyRagan
EmilyRagan marked this pull request as ready for review September 22, 2026 18:31

@jmthomas jmthomas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly looks good. --locked plus --no-build at build and CI time is a solid tightening. One backwards-compatibility concern is inline on openc3/bin/uvinstall: runtime plugin installs with a non-default PYPI_URL.

Minor: the PR description says --no-build --no-binary-package openc3 was added to the uv scripts. uvinstall intentionally doesn't get it, since user plugins may depend on sdist-only packages (see the comment in docker-package-build.sh). Worth saying so in the description so it doesn't read as missed.

Comment thread openc3/bin/uvinstall Outdated
# uploaded into this plugin venv via PythonPackageModel.install (which uses
# the additive `pipinstall`).
uv_offline_then_online sync --frozen --inexact --no-dev --no-install-project ${UV_ARGS}
uv_offline_then_online sync --locked --inexact --no-dev --no-install-project ${UV_ARGS}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backwards-compat concern: --frozen → --locked here changes runtime behavior for already-built third-party plugins.

UV_ARGS includes -i <pypi_url> from PluginModel.build_pypi_args. uv treats a lockfile whose registry source doesn't match the configured index as stale under --locked, but not under --frozen:

# lock generated against pypi.org
uv sync --frozen --no-install-project -i https://test.pypi.org/simple   # ok, installs locked six==1.17.0
uv sync --locked --no-install-project -i https://pypi.org/simple        # ok
uv sync --locked --no-install-project -i https://test.pypi.org/simple
# The lockfile at `uv.lock` needs to be updated, but `--locked` was provided.

Impact:

  • Default PYPI_URL=https://pypi.org: -i https://pypi.org/simple matches the lock. No change.
  • Mirror or air-gapped PYPI_URL (common in Enterprise): every plugin that ships a uv.lock fails path 1. This includes the demo plugin and generator-template plugins. It then falls to path 2 (uv pip install from pyproject.toml), which re-resolves from scratch and ignores the lock. The install still succeeds, so the only signal is the Warning: line. Versions can drift, and the baked offline cache (warmed for the locked versions) is more likely to miss.
  • Existing plugins whose uv.lock is slightly out of date with pyproject.toml: they used to install exactly what was locked. Now they silently get an unpinned install. migrate_to_uv! goes through the same path.

This also works against the goal of the PR: path 2 has no --no-build and no pins, so a failing path 1 sends plugins down the less safe route.

Suggestion: keep --frozen here. These are gems the user already built, and drift detection is for our own repo and build, where --locked stays. Alternatively, try --locked, then retry with --frozen before falling back to path 2, and log loudly when the retry is needed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, --frozen here. It's worse than drift, too: with a mirror -i, --locked re-resolves against the mirror, so the offline attempt fails on a warm cache (six was not found in the cache ... network was disabled). --frozen passes the same case. The baked plugin cache is never used, and every locked plugin falls to path 2, which has no lock and no --no-build.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is the big regression. It's so hard to test against a non-default PYPI_URL. Wonder if we can create better tests around this use case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm testing against a local Nexus repository in a separate PR, it's not as simple as pulling the image down thought

Comment thread docs.openc3.com/docs/configuration/_plugins.md Outdated
Comment thread docs.openc3.com/docs/getting-started/generators.md Outdated
Comment thread openc3-cosmos-init/verify-uv-cache.sh Outdated
@mcosgriff

mcosgriff commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

To Jason's point it would be worth making sure a plugin can be installed and uninstalled still. I verified that images build and the demo plugin is installed and runs, trying to install the TSDB plugin would be worth a run as well.

I only verified the build-time piece and dropped the ball on the runtime pieces

Comment thread openc3/python/pyproject.toml
@mcosgriff

mcosgriff commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Working with Claude confirmed @jmthomas concern about uvinstall in a running stack (cmd-tlm-api, uv 0.12.5, demo plugin).

Stock images are unaffected. The base image bakes in UV_DEFAULT_INDEX=${PYPI_URL}/simple (openc3-ruby/Dockerfile:144). When that matches the lock's registry (pypi.org), --locked passes offline even with a different runtime -i.

Images built against a mirror break. With UV_DEFAULT_INDEX and -i both pointing at the mirror, the lock no longer matches, so --locked re-resolves. Resolving needs the index's package listings, which the baked cache doesn't have: it only holds wheels, because the build-time warm-up never resolves. The offline attempt fails even though the wheel is cached:

docker exec -e UV_DEFAULT_INDEX=http://127.0.0.1:9/simple cosmos-openc3-cosmos-cmd-tlm-api-1 sh -c '
  GEM=$(ls -d /gems/gems/openc3-cosmos-demo-* | head -1)
  /openc3/bin/uvinstall zz_locked_test "$GEM" -i http://127.0.0.1:9/simple
  sed "s/sync --locked/sync --frozen/g" /openc3/bin/uvinstall > /tmp/uvinstall_frozen
  sh /tmp/uvinstall_frozen zz_frozen_test "$GEM" -i http://127.0.0.1:9/simple
  rm -rf /gems/plugin_venvs/zz_* /tmp/uvinstall_frozen'
--locked:  Because numpy was not found in the cache ...
           Warning: uv sync --locked failed (exit code 2), falling back to uv pip install
           ERROR: uvinstall failed for plugin zz_locked_test
--frozen:  uv sync --frozen succeeded

numpy==2.4.6 is in the cache (wheels-v6/pypi/numpy) either way. With a reachable mirror the install succeeds, but through path 2, with no lock and no --no-build, and the only sign is the Warning: line. A plugin whose uv.lock is behind its pyproject.toml also fails --locked on stock images.

With the network up, the same mismatch fails the build-time syncs with The lockfile at uv.lock needs to be updated, but --locked was provided. That covers the openc3/Dockerfile syncs, the docker-package-build.sh warm-up and the verify-uv-cache.sh check, so mirror builds break at image build, not just at plugin install.

Suggested changes:

  • Keep --frozen in uvinstall, the three plugin docs, and all of verify-uv-cache.sh. The lock the user built is what gets installed.
  • Keep --frozen in the openc3/Dockerfile syncs and docker-package-build.sh for now, keeping --no-build and --no-binary-package openc3. Once fix(build): relock uv against PYPI_URL so air-gapped builds resolve #3891's relock lands, those can move to --locked, since each sync will follow a relock against the same index.
  • Keep --locked in CI and the justfile. CI always resolves against pypi.org, so drift is still caught before merge.
  • CLAUDE.md: say --locked for CI and --frozen for Dockerfiles, rather than "CI/CD and Dockerfiles".

@mcosgriff

Copy link
Copy Markdown
Contributor

I have a bunch of changes in #3891 to support building cosmos against a different pypi mirror

EmilyRagan and others added 5 commits September 24, 2026 16:53
uv rejects a lockfile whose recorded registry does not match the
configured index under --locked, so plugin installs and image builds
against a PyPI mirror re-resolve instead of honoring the lock. In
uvinstall that drops every locked plugin into the unpinned pip fallback,
which has neither the lock nor --no-build.

Keep --locked in CI and the justfile, where resolution is always against
pypi.org and lock drift is still caught before merge. --no-build and
--no-binary-package openc3 are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
openc3_set_versions.rb rewrites the version in pyproject.toml, and
uv.lock records that version too, so every release left the lockfile
stale and failed `uv sync --locked` on main.

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

Copy link
Copy Markdown

@EmilyRagan

Copy link
Copy Markdown
Contributor Author

@mcosgriff @jmthomas hmmm even after reverting some of the --locked flags to --frozen, installing the tsdb migration plugin says that it successfully installed with warnings... but the warnings are concerning to me, and I wonder if this should actually be a plugin installation failure

hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: --find-links <uri>)
ERROR: uv pip install failed
{"time":1790292233056032585,"@timestamp":"2026-09-24T23:23:53.056032Z","level":"WARN","container_name":"d0639b016daf","message":"Python package installation failed. Plugin Python microservices may not function correctly.","type":"log"}

@jmthomas jmthomas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is correct now but not sure how it will interact with @mcosgriff changes in the other PR. Not sure about the warnings installing the tsdb-migration plugin. Are they present in the current release? Do they prevent the plugin from working? I assume we're expecting no warnings so we should understand that first before merging.

@EmilyRagan

Copy link
Copy Markdown
Contributor Author

@jmthomas installing tsdb-migration in current release does not have the same error, so I think I have some more work to do. This is a good test case to go through

This branch has not been deployed

No deployments
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.

3 participants