wads.migration¶
Migration tools for converting old setuptools/CI configurations to modern formats.
This module provides functions to migrate:
setup.cfg files to pyproject.toml (hatching format)
Old GitHub CI workflows to modern 2025 format
- Key Functions:
migrate_setuptools_to_hatching: Convert setup.cfg to pyproject.toml migrate_github_ci_old_to_new: Convert old CI scripts to new format
Example
>>> from wads.migration import migrate_setuptools_to_hatching
>>> # From a file (use actual file path)
>>> pyproject = migrate_setuptools_to_hatching('setup.cfg')
>>>
>>> # From a dict with complete metadata
>>> cfg = {
... 'metadata': {
... 'name': 'myproject',
... 'version': '0.1.0',
... 'description': 'A sample project',
... 'url': 'https://github.com/user/myproject',
... 'license': 'MIT'
... }
... }
>>> result = migrate_setuptools_to_hatching(cfg)
>>> 'name = "myproject"' in result
True
Functions
|
Analyze MANIFEST.in file and provide migration guidance for Hatchling. |
|
Merge secret-backed env vars from |
|
The |
|
Extract secret-backed env vars from a CI workflow's |
|
CLI entry point for wads migration tools. |
|
Return the SSOT stub CI workflow that calls i2mint/wads's reusable uv-ci. |
|
Migrate a CI workflow (old or 2025 format) to the uv-based template. |
|
Migrate old GitHub CI script to new 2025 format. |
|
Migrate setup.cfg to pyproject.toml format using hatching. |
Exceptions
Raised when migration cannot be completed due to missing or unmapped data. |
- exception wads.migration.MigrationError[source]¶
Bases:
ValueErrorRaised when migration cannot be completed due to missing or unmapped data.
- wads.migration.analyze_manifest_in(manifest_path)[source]¶
Analyze MANIFEST.in file and provide migration guidance for Hatchling.
- Parameters:
- Returns:
exists: bool - whether file exists
needs_migration: bool - whether migration is needed
directives: list of (command, pattern) tuples
recommendations: list of strings with migration guidance
hatchling_config: suggested pyproject.toml configuration
- Return type:
Example
>>> # If MANIFEST.in doesn't exist >>> result = analyze_manifest_in('nonexistent/MANIFEST.in') >>> result['exists'] False >>> result['needs_migration'] False
- wads.migration.carry_ci_env_into_pyproject(ci_text, pyproject_path, *, kind='extra', report_dropped=None)[source]¶
Merge secret-backed env vars from
ci_textinto[tool.wads.ci.env].Adds only env vars not already declared (in any bucket), into the
kindbucket (defaultextra— available-if-set, never fails the build), and records asecret_aliasesentry when the env var name differs from the backing secret. Returns the list of newly-added var names ([]if there was nothing to carry). This is what makesci-to-stub/ci-to-uvlossless: secrets wired only in the old workflow YAML are preserved inpyproject.tomlinstead of silently dropped.Infra-secret-backed env vars (see
extract_ci_env_vars()) are the one deliberate exception;report_droppedreceives them as{ENV_VAR: SECRET_NAME}(default: a note on stderr).- Return type:
- wads.migration.dropped_infra_ci_env_vars(ci_text)[source]¶
The
{ENV_VAR: SECRET_NAME}entriesextract_ci_env_vars()drops.These are env vars backed by an infra secret (publish token, GitHub token), under their own name or an alias. A migration reports them so a dropped
UV_PUBLISH_TOKENis never a silent surprise (issue #110).- Return type:
>>> dropped_infra_ci_env_vars(''' ... jobs: ... publish: ... steps: ... - run: uv publish ... env: ... UV_PUBLISH_TOKEN: ${{ secrets.PYPI_PASSWORD }} ... ''') {'UV_PUBLISH_TOKEN': 'PYPI_PASSWORD'}
- wads.migration.extract_ci_env_vars(ci_text)[source]¶
Extract secret-backed env vars from a CI workflow’s
env:blocks.Returns a mapping
{ENV_VAR: SECRET_NAME}for every workflow/job/stepenv:entry whose value references${{ secrets.X }}. This is the per-repo signal of which secrets the tests actually consume.Deliberately excluded:
Literal env vars (
PROJECT_NAME,LOG_LEVEL: DEBUG…) — no secret ref.Infra secrets (
GITHUB_TOKEN,PYPI_PASSWORD,TEST_PYPI_PASSWORD) and any other publish credential (TWINE_PASSWORD,PYPI_API_TOKEN…, seewads.scripts.export_ci_env.is_publish_credential), by env-var name or by backing secret, in any case:UV_PUBLISH_TOKEN: ${{ secrets.PYPI_PASSWORD }}in a publish step is dropped, since carrying it would hand the PyPI token to the test jobs (issue #110).dropped_infra_ci_env_vars()lists what was dropped.Reusable-workflow
secrets:pass-through blocks — that’s transport, not a signal of usage, and scanning it would re-introduce the very over-assignment the new model removes.
>>> ci = ''' ... env: ... PROJECT_NAME: myproj ... OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY || '' }} ... HF_TOKEN: ${{ secrets.HF_WRITE_TOKEN }} ... jobs: ... publish: ... steps: ... - uses: x ... with: ... pypi-token: ${{ secrets.PYPI_PASSWORD }} ... - run: uv publish ... env: ... UV_PUBLISH_TOKEN: ${{ secrets.PYPI_PASSWORD }} ... ''' >>> extract_ci_env_vars(ci) == { ... "OPENAI_API_KEY": "OPENAI_API_KEY", "HF_TOKEN": "HF_WRITE_TOKEN"} True
- Return type:
- wads.migration.migrate_ci_to_stub(old_ci=None, *, pin='@master', transport=None, trigger_mode=None, run_ci_marker=None)[source]¶
Return the SSOT stub CI workflow that calls i2mint/wads’s reusable uv-ci.
The stub is ~5 lines and replaces a repo’s full inline CI with a single
uses:ofi2mint/wads/.github/workflows/uv-ci.yml. All per-repo configuration continues to come from[tool.wads.ci.*]inpyproject.toml— the reusable workflow reads it via thei2mint/wads/actions/read-ci-configaction.- Parameters:
old_ci (
Union[str,Path]) – Optional path or content of the existing CI workflow. Used to locate a nearby pyproject.toml, whose[tool.wads.ci.env]decides which secrets the default named transport passes.pin (
str) – The wads ref the stub points at. Defaults to"@master"(floats with wads). For release-sensitive repos, pin to a tag, e.g.pin="@0.2.15"(wads tags have novprefix). With the JSON transport the pinned ref’suv-ci.ymlmust declareWADS_CI_SECRETS_JSON(releases after 0.2.14) — pinning an older tag produces a workflow GitHub rejects at parse time, so a warning is emitted for any non-master JSON pin. Must start with"@".transport (
Optional[str]) –None(default) writes the named transport — converting a JSON-transport stub atold_ci— unless the repo declares secret names outside the wads superset, in which case it writes the JSON transport (a named stub passing them could not start) and says so on stderr."named"passes an explicit subset (PYPI_PASSWORD + the [tool.wads.ci.env]-declared secrets); every name must be in the wads superset or GitHub rejects the workflow at parse time (issue #63) — out-of-superset names trigger a loud warning."json"(opt-in) passes the repo’s whole secrets context as oneWADS_CI_SECRETS_JSONsecret, so any secret name works, but every secret the repo can read reaches the called workflow and GitHub’s malicious-workflow scanner may hold its runs (action_required, zero jobs; issues #74, #88, #101), which is why it is not the default.trigger_mode (
Optional[str]) –"auto"or"on-demand".None(default) takes[tool.wads.ci.trigger].modefrom the pyproject.toml of the repo holdingold_ci("auto"when there is none). On-demand stubs run nothing unless the commit subject carries the run-ci marker or the run is aworkflow_dispatch; seewads.ci_trigger.run_ci_marker (
Optional[str]) – The on-demand marker;Nonetakes it from the same pyproject (default"[run ci]").
- Return type:
- Returns:
The stub workflow content as a string.
Example
>>> stub = migrate_ci_to_stub() >>> 'i2mint/wads/.github/workflows/uv-ci.yml@master' in stub True >>> 'PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}' in stub True >>> 'toJSON(secrets)' in stub False >>> pinned = migrate_ci_to_stub(pin='@0.2.15') >>> 'uv-ci.yml@0.2.15' in pinned True >>> as_json = migrate_ci_to_stub(transport='json') >>> 'WADS_CI_SECRETS_JSON: ${{ toJSON(toJSON(secrets)) }}' in as_json True
- wads.migration.migrate_ci_to_uv(old_ci, *, defaults=None)[source]¶
Migrate a CI workflow (old or 2025 format) to the uv-based template.
Since the uv template reads all configuration from pyproject.toml (via the read-ci-config action), the migration is straightforward: replace the workflow file with the uv template.
- Parameters:
- Returns:
The uv CI template content as a string.
Example
>>> result = migrate_ci_to_uv('name: CI\non: push') >>> 'astral-sh/setup-uv' in result True >>> 'build-dist-uv' in result True
- wads.migration.migrate_github_ci_old_to_new(old_ci, defaults=None)[source]¶
Migrate old GitHub CI script to new 2025 format.
- Parameters:
- Return type:
- Returns:
String content of the new CI script
- Raises:
MigrationError – If unmapped elements exist or required fields are missing
Note
The 2025 CI template is fully config-driven via pyproject.toml. Migration now primarily involves setting up proper [tool.wads.ci] configuration rather than doing template substitution. For new projects, use populate_pkg_dir() to generate proper configuration.
Example
>>> # Migration is now about configuration, not template substitution >>> # For new projects, use populate_pkg_dir() instead >>> # Old migration approach with placeholders is deprecated >>> pass
- wads.migration.migrate_setuptools_to_hatching(setup_cfg, defaults=None, *, rules=None)[source]¶
Migrate setup.cfg to pyproject.toml format using hatching.
- Parameters:
- Return type:
- Returns:
String content of the generated pyproject.toml
- Raises:
MigrationError – If required fields are missing or unmapped data exists
Example
>>> # From file path >>> pyproject = migrate_setuptools_to_hatching('setup.cfg') >>> >>> # From dict with all required fields >>> cfg = { ... 'metadata': { ... 'name': 'myproj', ... 'version': '0.1.0', ... 'description': 'My project', ... 'url': 'https://github.com/user/myproj', ... 'license': 'MIT' ... } ... } >>> result = migrate_setuptools_to_hatching(cfg) >>> 'name = "myproj"' in result True >>> >>> # With defaults for missing fields >>> minimal = {'metadata': {'name': 'test', 'version': '1.0'}} >>> result = migrate_setuptools_to_hatching( ... minimal, ... defaults={ ... 'description': 'Test project', ... 'url': 'https://test.com', ... 'license': 'MIT' ... } ... ) >>> 'name = "test"' in result True