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(manifest_path)

Analyze MANIFEST.in file and provide migration guidance for Hatchling.

carry_ci_env_into_pyproject(ci_text, ...[, ...])

Merge secret-backed env vars from ci_text into [tool.wads.ci.env].

dropped_infra_ci_env_vars(ci_text)

The {ENV_VAR: SECRET_NAME} entries extract_ci_env_vars() drops.

extract_ci_env_vars(ci_text)

Extract secret-backed env vars from a CI workflow's env: blocks.

main()

CLI entry point for wads migration tools.

migrate_ci_to_stub([old_ci, pin, transport, ...])

Return the SSOT stub CI workflow that calls i2mint/wads's reusable uv-ci.

migrate_ci_to_uv(old_ci, *[, defaults])

Migrate a CI workflow (old or 2025 format) to the uv-based template.

migrate_github_ci_old_to_new(old_ci[, defaults])

Migrate old GitHub CI script to new 2025 format.

migrate_setuptools_to_hatching(setup_cfg[, ...])

Migrate setup.cfg to pyproject.toml format using hatching.

Exceptions

MigrationError

Raised when migration cannot be completed due to missing or unmapped data.

exception wads.migration.MigrationError[source]

Bases: ValueError

Raised 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:

manifest_path (Union[str, Path]) – Path to MANIFEST.in file

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:

dict

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_text into [tool.wads.ci.env].

Adds only env vars not already declared (in any bucket), into the kind bucket (default extra — available-if-set, never fails the build), and records a secret_aliases entry 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 makes ci-to-stub / ci-to-uv lossless: secrets wired only in the old workflow YAML are preserved in pyproject.toml instead of silently dropped.

Infra-secret-backed env vars (see extract_ci_env_vars()) are the one deliberate exception; report_dropped receives them as {ENV_VAR: SECRET_NAME} (default: a note on stderr).

Return type:

list

wads.migration.dropped_infra_ci_env_vars(ci_text)[source]

The {ENV_VAR: SECRET_NAME} entries extract_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_TOKEN is never a silent surprise (issue #110).

Return type:

dict

>>> 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/step env: 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 …, see wads.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:

dict

wads.migration.main()[source]

CLI entry point for wads migration tools.

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: of i2mint/wads/.github/workflows/uv-ci.yml. All per-repo configuration continues to come from [tool.wads.ci.*] in pyproject.toml — the reusable workflow reads it via the i2mint/wads/actions/read-ci-config action.

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 no v prefix). With the JSON transport the pinned ref’s uv-ci.yml must declare WADS_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 at old_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 one WADS_CI_SECRETS_JSON secret, 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].mode from the pyproject.toml of the repo holding old_ci ("auto" when there is none). On-demand stubs run nothing unless the commit subject carries the run-ci marker or the run is a workflow_dispatch; see wads.ci_trigger.

  • run_ci_marker (Optional[str]) – The on-demand marker; None takes it from the same pyproject (default "[run ci]").

Return type:

str

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:
  • old_ci (Union[str, Path]) – Path to the existing CI workflow file, or its content as a string.

  • defaults (Optional[dict]) – Optional dict with ‘project_name’ if it cannot be extracted from the old CI file.

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:
  • old_ci (Union[str, Path]) – Path to old CI file or its content as string

  • defaults (Optional[dict]) – Default values for missing fields (e.g., {‘project_name’: ‘myproject’})

Return type:

str

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:
  • setup_cfg (Union[str, Mapping]) – Either a file path, file content string, or dict of setup.cfg

  • defaults (Optional[dict]) – Default values to use for missing required fields

  • rules (Optional[dict]) – Custom transformation rules (defaults to setup_cfg_to_pyproject_toml_rules)

Return type:

str

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