Skip to content

refactor(uv): replace deprecated uv index options and seed a plugin pyproject template - #3896

Open
mcosgriff wants to merge 20 commits into
mainfrom
3724-uv-options-maintenance
Open

mcosgriff wants to merge 20 commits into
mainfrom
3724-uv-options-maintenance

Conversation

@mcosgriff

@mcosgriff mcosgriff commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add PypiUrl.build_args as the one place index args are built, using --default-index / --allow-insecure-host in place of the deprecated -i / --trusted-host; PluginModel.build_pypi_args and PythonPackageModel.install delegate to it
  • Honor UV_ALLOW_INSECURE_HOST, keeping PIP_ENABLE_TRUSTED_HOST as a deprecated alias
  • Make a non-default index authoritative: build_args adds --no-config, and uvinstall / pipinstall add --no-sources on resolving commands (not uv sync --frozen, which rejects it)
  • Generate pyproject.toml, a plugin-named lib/<package>/ and a uv README section for --python plugins only, and glob pyproject.toml and uv.lock in their gemspec
  • Replace the demo plugin's setuptools [build-system] shim with package = false, add ty config, unpin dev tooling
  • Shell-lint the extensionless openc3/bin install scripts; Sonar ignores the missing-lockfile rule on the plugin template
  • Document the behavior in .env, environment.md and plugins.md

Review guide

Files grouped by risk, highest first. When you finish a file, tick its box and add your handle (e.g. - [x] ... — @you) so the next reviewer can skip it. Line counts are +added / -removed.

1. Runtime behavior — what changes for deployed systems (start here)

  • openc3/lib/openc3/utilities/pypi_url.rb (+50) — new build_args: --default-index, --allow-insecure-host, --no-config only for a non-default index
  • openc3/bin/uvinstall (+36 / -8) — --no-sources on resolving commands, never on uv sync --frozen
  • openc3/bin/pipinstall (+18 / -5), openc3/bin/pipuninstall (+1 / -1)
  • openc3/lib/openc3/models/plugin_model.rb (+5 / -4), openc3/lib/openc3/models/python_package_model.rb (+1 / -5) — delegate to PypiUrl.build_args

2. Plugin generator and template — what new --python plugins get

  • openc3/lib/openc3/utilities/cli_generator.rb (+17) — lib/PACKAGE becomes lib/<package_name>, derived from the plugin name; template python files skipped for ruby plugins
  • openc3/templates/plugin/lib/PACKAGE/__init__.py (+4) — plugin-named package so modules can't collide on the shared sys.path
  • openc3/templates/plugin/pyproject.toml (+119) — package = false, ty config, pytest pythonpath = ["lib"]
  • openc3/templates/plugin/plugin.gemspec (+10 / -1) — globs pyproject.toml / uv.lock, drops __pycache__
  • openc3/templates/plugin/README.md (+90 / -2), openc3/templates/plugin/.gitignore (+6) — uv workflow docs and ignores, python plugins only
  • openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/pyproject.toml (+73 / -28) — setuptools shim replaced by package = false; uv.lock beside it is regenerated

3. Docs — check that the wording matches the behavior above

  • .env (+16 / -1)
  • docs.openc3.com/docs/configuration/environment.md, plugins.md, _plugins.md

4. Tests and CI — skim unless something above looks wrong

  • openc3/spec/utilities/pypi_url_spec.rb, openc3/spec/bin/install_scripts_spec.rb, openc3/spec/utilities/uv_index_isolation_spec.rb, openc3/spec/utilities/cli_generator_spec.rb
  • openc3/spec/models/plugin_model_spec.rb, openc3/spec/models/python_package_model_spec.rb — mostly fixture cleanup (net -145)
  • scripts/linux/test_plugin_pypi_index.sh (+407) — manual end-to-end script, not run in CI
  • .github/workflows/shell_lint.yml, sonar-project.properties

Test plan

cd openc3 && bundle exec rspec:

  • spec/utilities/pypi_url_spec.rb - new flags, --no-config only for a designated index, deprecated env name still works
  • spec/utilities/cli_generator_spec.rb - --python generates and packages the template; ruby plugins don't
  • spec/models/plugin_model_spec.rb - pypi_url reaches uvinstall; no python deps means no uv call
  • spec/bin/install_scripts_spec.rb - --no-sources on resolving commands only, never on uv sync --frozen
  • spec/utilities/uv_index_isolation_spec.rb - pins uv's own index precedence against a real uv binary (offline, skipped without uv)

scripts/linux/test_plugin_pypi_index.sh (runs this checkout's uvinstall in the operator image against local indexes):

  • Plugin generated with --python, with a committed uv.lock, installs via uv sync --frozen
  • Demo plugin's python deps install without the setuptools shim, locked and unlocked
  • With pypi_url set to a private index, an unlocked plugin whose [tool.uv].index or [tool.uv.sources] names its author's index fails, and only the private index is queried
  • Self-signed index fails without opt-in, installs with UV_ALLOW_INSECURE_HOST=1 or the deprecated PIP_ENABLE_TRUSTED_HOST=1

Behavior change

A non-default pypi_url is now authoritative for plugin Python dependencies, so a plugin relying on its author's private index fails unless the operator's index serves that package too. Worth a release note.

Default deployments are unaffected, and so are locked plugins: uv sync --frozen still downloads from the URLs in uv.lock (the runtime side of #2867).

Closes #3724

…plate

- Swap -i for --default-index and --trusted-host for
  --allow-insecure-host, both deprecated or undocumented aliases, in a
  new shared PypiUrl.build_args
- Honor UV_ALLOW_INSECURE_HOST, keeping PIP_ENABLE_TRUSTED_HOST as a
  deprecated alias so existing deployments keep working
- Drop the duplicated index arg building in PythonPackageModel.install
  in favor of PypiUrl.build_args
- Add openc3/templates/plugin/pyproject.toml plus lib/.gitkeep, both
  generated only for --python plugins and listed in the gemspec so
  COSMOS can see them
- Replace the demo plugin's setuptools [build-system] shim with
  package = false and unpin its dev tooling

Co-Authored-By: Claude Opus 5 (1M context)
@mcosgriff mcosgriff linked an issue Sep 18, 2026 that may be closed by this pull request
@socket-security

socket-security Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedpypi/​openc3@​7.4.17310010010060
Addedpypi/​ty@​0.0.77100100100100100
Addedpypi/​ty@​0.0.84100100100100100
Addedpypi/​ruff@​0.16.9100100100100100

View full report

@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.15%. Comparing base (e3a728f) to head (85b546f).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3896      +/-   ##
==========================================
+ Coverage   80.13%   80.15%   +0.02%     
==========================================
  Files         901      901              
  Lines       68370    68381      +11     
  Branches     2699     2646      -53     
==========================================
+ Hits        54789    54813      +24     
+ Misses      12913    12904       -9     
+ Partials      668      664       -4     
Flag Coverage Δ
frontend 67.00% <ø> (+<0.01%) ⬆️
python 80.18% <ø> (+0.04%) ⬆️
ruby-api 82.59% <ø> (ø)
ruby-backend 85.66% <100.00%> (+<0.01%) ⬆️

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.

- Add a templateNoLock criterion for text:S8565 on
  openc3/templates/plugin/pyproject.toml, which ships no uv.lock on
  purpose so generated plugins resolve current versions
- Add the trailing newline the file was missing

Refs #3724

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
…tall argv

PythonPackageModel.install now builds its index arguments with
PypiUrl.build_args, which emits uv's --allow-insecure-host rather than
pip's --trusted-host alias.

Refs #3724

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
- Describe in plugins.md Phase 2 which index plugin Python dependencies
  resolve against, and that a non-default one suppresses a plugin's own
  [tool.uv].index and [tool.uv].sources
- Note that a plugin shipping a uv.lock installs via uv sync --frozen and
  still downloads from whatever index its author locked against
- Carry the same point into the environment.md PYPI_URL row, which until
  now only separated build-time from run-time

Refs #3724

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
- Add --no-sources alongside --no-config on the uv commands that resolve:
  --no-config stops uv searching a plugin's [tool.uv].index, but a
  [tool.uv].sources pin survives it and still resolves elsewhere
- Keep it off the uv sync --frozen call, which uv rejects it on and which
  resolves nothing anyway
- Make cd into VENV_BASE fatal so a failure cannot sync against whatever
  pyproject.toml the working directory holds
- Add UVINSTALL_VENV_ROOT so the spec suite can drive uvinstall against a
  temporary directory
- Quote the echoed argv with $* in pipinstall and pipuninstall, which
  ShellCheck rejects as SC2145

Refs #3724

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
- Add --no-config to PypiUrl.build_args for any index other than the
  public default, so a plugin's own [tool.uv].index table cannot win the
  search over the index the operator configured
- Cover the new flag in pypi_url_spec in both directions, and assert in
  plugin_model_spec that a configured pypi_url reaches the uvinstall argv
  verbatim rather than matching it with anything
- Add install_scripts_spec, which drives the real uvinstall and pipinstall
  against a stub uv on PATH to pin where --no-sources is and is not added
- Add uv_index_isolation_spec, which asserts uv's own precedence rules
  against a real uv so an upgrade that changes them fails here; it uses
  .invalid hosts to stay offline and skips when uv is absent
- Assert that a plugin with no python dependencies never invokes uv

Refs #3724

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
uvinstall, pipinstall and pipuninstall are /bin/sh scripts invoked by
name as commands, so the *.sh glob never matched them and they had gone
unchecked. Hoist the pathspecs into SHELL_PATHSPECS so the gate and the
advisory step cannot drift apart, and add the same path to the trigger.

Refs #3724

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
- Replace the claim that a [tool.uv].sources pin is not overridden by the
  Admin Console setting, in both the plugin template and the demo plugin:
  a designated index now suppresses the whole table via --no-sources
- Tell authors a dependency published only to their own index fails to
  install against such an index, and that a committed uv.lock sidesteps
  the whole question
- Document the same behavior under PYPI_URL in .env

Refs #3724

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
SHELL_PATHSPECS had to be unquoted to word split, which let the shell
glob '*.sh' against the working directory first. It matched only the
repository root, so the gate checked 4 files instead of 25 and still
passed. Inline the pathspecs quoted in both steps and drop the variable
that forced the unquoted expansion.

Refs #3724

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
- Add capture_spawn, which stubs ProcessManager and returns the argv and
  keyword arguments of the one spawn, so each .install and .destroy
  example is left with the assertion it is actually making
- Add stub_plugin_venvs, replacing the File.directory? / Dir.glob trio
  that .names and .trees each repeated per example, with a nil value for
  a plugin directory lacking a .venv
- Fold the throwaway package file into install_spawn, since every .install
  example wrote the same fixture and never read it back

Refs #3724

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
- Add gem_spec_double, replacing the eight-field gemspec double that 11
  examples built by hand, so a call site names only the two fields that
  decide needs_dependencies and img_path
- Add stub_s3_client for the Aws::S3::Client null object those same
  examples each set up
- Add expect_tool_and_target_deploy for the ToolModel and TargetModel
  deploy pair, which varies only in the variables hash
- Add unmigrated_plugin, stub_migration_gem and stub_default_pypi_url, so
  each migrate_to_uv! example stops rebuilding the model, the absent
  .uv_managed marker and the pypi_url fallback

Refs #3724

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
@mcosgriff mcosgriff self-assigned this Sep 23, 2026
Refs #3724

Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
…t.py

Refs #3724

Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
- Substitute lib/PACKAGE with a package_name derived from the plugin
  name so a module can't collide with one from another plugin on sys.path
- Add a uv section to the generated README.md for python plugins
- Ignore .venv/, pycache/, .pytest_cache/ and .ruff_cache/ in the
  generated .gitignore for python plugins
- Reject pycache files from the python plugin gemspec's s.files
- Set pytest pythonpath = ["lib"] in the pyproject.toml template

Refs #3724

Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
Refs #3724

Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
- Explain in the plugin README that the plugin isn't installed into
  .venv, so code outside pytest and ty needs PYTHONPATH=lib to import
  the package
- Tag the README's shell code fences as sh

Refs #3724

Co-Authored-By: Claude Opus 5.5 (1M context) noreply@anthropic.com
@mcosgriff
mcosgriff marked this pull request as ready for review September 30, 2026 16:23
- Pass --pull to the openc3-ruby, buckets, redis, tsdb and traefik UBI
  builds so a mutable Iron Bank tag isn't served from a stale local copy
- Add OPENC3_UBI_NO_PULL=1 to skip the pull when bases were loaded with
  docker load
- Bump OPENC3_UBI_TAG in .env from 9.6 to 9.8

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

Copy link
Copy Markdown

@calmonroe calmonroe left a comment

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.

Nice! I love all the new spec files

@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 minor issues. Are we close to converging on the uv / pypi updates?


Dependencies are resolved against the `pypi_url` Admin Console setting, falling back to the `PYPI_URL` environment variable and then to public PyPI. When that setting names an index other than `https://pypi.org`, COSMOS treats it as authoritative and passes uv `--no-config --no-sources`, so a plugin cannot resolve around it using an index declared in its own `[tool.uv].index` table or a `[tool.uv].sources` pin. A plugin depending on a package published only to its author's private index therefore fails to install unless your index serves that package too.

This applies only to the paths where uv resolves — `requirements.txt` plugins, and `pyproject.toml` plugins with no `uv.lock`. A plugin shipping a `uv.lock` installs with `uv sync --frozen`, which reuses the registry and wheel URLs already recorded in that lock and never consults an index, so a locked plugin still downloads from whatever index its author locked against.

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.

Does this mean that if I create a lock file against my plugin and include it in the plugin gem file and then try to install it in an air-gapped environment it will fail? Aren't we telling people to include the lock file?

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 the answer is no because we fall back to a uv sync --frozen which will also fail. Then we fallback to ignoring the lock and hitting the mirror. So I think it all works but the text is a little scary from an air-gapped install.

Maybe another sentence like: "If those recorded URLs are unreachable, COSMOS falls back to resolving the plugin's pyproject.toml against the configured index, ignoring the lock's pins."

```

uv picks any installed Python that satisfies `requires-python`. To develop against the same version COSMOS
runs, pin it locally with `uv python pin 3.12`, which writes a `.python-version` file.

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.

This is going to get out of date with our actual python version. How does it get updated?

Comment thread .env
OPENC3_UBI_REGISTRY=registry1.dso.mil
OPENC3_UBI_IMAGE=ironbank/redhat/ubi/ubi9-minimal
OPENC3_UBI_TAG=9.6
OPENC3_UBI_TAG=9.8

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.

👍

Comment thread .env
# Enables the --trusted-host flag when downloading python package from the PYPI_URL
# Enables uv's --allow-insecure-host flag when downloading python packages from
# the PYPI_URL, for a private index with a self signed certificate.
# UV_ALLOW_INSECURE_HOST=1

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'd rather we keep our internal env vars named with the OPENC3 prefix so they're easier to find. Maybe something like OPENC3_PYPI_ALLOW_INSECURE.

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.

uv options maintenance

3 participants