mad-cli 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. mad_cli-0.4.0/.github/workflows/ci.yml +98 -0
  2. mad_cli-0.4.0/.github/workflows/docs-sync.yml +21 -0
  3. mad_cli-0.4.0/.github/workflows/docs-validate.yml +19 -0
  4. mad_cli-0.4.0/.github/workflows/release.yml +129 -0
  5. mad_cli-0.4.0/.github/workflows/testpypi-preview.yml +195 -0
  6. mad_cli-0.4.0/.gitignore +22 -0
  7. mad_cli-0.4.0/CHANGELOG.md +508 -0
  8. mad_cli-0.4.0/CLAUDE.md +64 -0
  9. mad_cli-0.4.0/CONTRACTS.md +306 -0
  10. mad_cli-0.4.0/LICENSE +21 -0
  11. mad_cli-0.4.0/PKG-INFO +167 -0
  12. mad_cli-0.4.0/README.md +108 -0
  13. mad_cli-0.4.0/docs/.docs-manifest.yaml +571 -0
  14. mad_cli-0.4.0/docs/01-overview/context.md +43 -0
  15. mad_cli-0.4.0/docs/01-overview/glossary.md +21 -0
  16. mad_cli-0.4.0/docs/01-overview/passport.md +42 -0
  17. mad_cli-0.4.0/docs/01-overview/scope.md +36 -0
  18. mad_cli-0.4.0/docs/02-architecture/components.md +100 -0
  19. mad_cli-0.4.0/docs/02-architecture/overview.md +55 -0
  20. mad_cli-0.4.0/docs/02-architecture/source-tree.md +71 -0
  21. mad_cli-0.4.0/docs/02-architecture/test-tree.md +53 -0
  22. mad_cli-0.4.0/docs/03-contracts/cli.md +275 -0
  23. mad_cli-0.4.0/docs/03-contracts/external-dependencies.md +66 -0
  24. mad_cli-0.4.0/docs/03-contracts/http-api.md +64 -0
  25. mad_cli-0.4.0/docs/04-conventions/cli-design.md +79 -0
  26. mad_cli-0.4.0/docs/04-conventions/layering.md +53 -0
  27. mad_cli-0.4.0/docs/04-conventions/quality.md +57 -0
  28. mad_cli-0.4.0/docs/04-conventions/testing-strategy.md +47 -0
  29. mad_cli-0.4.0/docs/05-operations/ci-cd.md +42 -0
  30. mad_cli-0.4.0/docs/05-operations/configuration.md +65 -0
  31. mad_cli-0.4.0/docs/05-operations/installation.md +81 -0
  32. mad_cli-0.4.0/docs/05-operations/release.md +50 -0
  33. mad_cli-0.4.0/docs/06-history/changelog.md +62 -0
  34. mad_cli-0.4.0/docs/README.md +25 -0
  35. mad_cli-0.4.0/pyproject.toml +113 -0
  36. mad_cli-0.4.0/src/mad_cli/__init__.py +3 -0
  37. mad_cli-0.4.0/src/mad_cli/__main__.py +6 -0
  38. mad_cli-0.4.0/src/mad_cli/app.py +77 -0
  39. mad_cli-0.4.0/src/mad_cli/commands/__init__.py +5 -0
  40. mad_cli-0.4.0/src/mad_cli/commands/_adapt.py +41 -0
  41. mad_cli-0.4.0/src/mad_cli/commands/_common.py +12 -0
  42. mad_cli-0.4.0/src/mad_cli/commands/config.py +94 -0
  43. mad_cli-0.4.0/src/mad_cli/commands/install.py +504 -0
  44. mad_cli-0.4.0/src/mad_cli/commands/instances.py +102 -0
  45. mad_cli-0.4.0/src/mad_cli/commands/keys.py +126 -0
  46. mad_cli-0.4.0/src/mad_cli/commands/lifecycle.py +69 -0
  47. mad_cli-0.4.0/src/mad_cli/commands/profiles.py +238 -0
  48. mad_cli-0.4.0/src/mad_cli/commands/service.py +220 -0
  49. mad_cli-0.4.0/src/mad_cli/commands/versions.py +61 -0
  50. mad_cli-0.4.0/src/mad_cli/core/__init__.py +4 -0
  51. mad_cli-0.4.0/src/mad_cli/core/claude_creds.py +31 -0
  52. mad_cli-0.4.0/src/mad_cli/core/compose.py +145 -0
  53. mad_cli-0.4.0/src/mad_cli/core/docker_check.py +89 -0
  54. mad_cli-0.4.0/src/mad_cli/core/envfile.py +140 -0
  55. mad_cli-0.4.0/src/mad_cli/core/instance.py +110 -0
  56. mad_cli-0.4.0/src/mad_cli/core/keyspec.py +98 -0
  57. mad_cli-0.4.0/src/mad_cli/core/paths.py +40 -0
  58. mad_cli-0.4.0/src/mad_cli/core/profiles.py +93 -0
  59. mad_cli-0.4.0/src/mad_cli/core/pypi.py +29 -0
  60. mad_cli-0.4.0/src/mad_cli/core/templates.py +91 -0
  61. mad_cli-0.4.0/src/mad_cli/core/usecases/__init__.py +11 -0
  62. mad_cli-0.4.0/src/mad_cli/core/usecases/adopt.py +55 -0
  63. mad_cli-0.4.0/src/mad_cli/core/usecases/configvals.py +94 -0
  64. mad_cli-0.4.0/src/mad_cli/core/usecases/errors.py +57 -0
  65. mad_cli-0.4.0/src/mad_cli/core/usecases/install.py +263 -0
  66. mad_cli-0.4.0/src/mad_cli/core/usecases/instances.py +156 -0
  67. mad_cli-0.4.0/src/mad_cli/core/usecases/keys.py +169 -0
  68. mad_cli-0.4.0/src/mad_cli/core/usecases/lifecycle.py +76 -0
  69. mad_cli-0.4.0/src/mad_cli/core/usecases/service.py +269 -0
  70. mad_cli-0.4.0/src/mad_cli/core/usecases/versions.py +126 -0
  71. mad_cli-0.4.0/src/mad_cli/py.typed +0 -0
  72. mad_cli-0.4.0/src/mad_cli/server/__init__.py +13 -0
  73. mad_cli-0.4.0/src/mad_cli/server/app.py +260 -0
  74. mad_cli-0.4.0/src/mad_cli/server/auth.py +41 -0
  75. mad_cli-0.4.0/src/mad_cli/server/models.py +156 -0
  76. mad_cli-0.4.0/src/mad_cli/templates/Dockerfile.tmpl +66 -0
  77. mad_cli-0.4.0/src/mad_cli/templates/__init__.py +6 -0
  78. mad_cli-0.4.0/src/mad_cli/templates/com.mad-core.mad-cli.plist.tmpl +28 -0
  79. mad_cli-0.4.0/src/mad_cli/templates/compose.yml.tmpl +29 -0
  80. mad_cli-0.4.0/src/mad_cli/templates/entrypoint.sh.tmpl +11 -0
  81. mad_cli-0.4.0/src/mad_cli/templates/mad-cli.service.tmpl +15 -0
  82. mad_cli-0.4.0/src/mad_cli/ui/__init__.py +5 -0
  83. mad_cli-0.4.0/src/mad_cli/ui/console.py +65 -0
  84. mad_cli-0.4.0/src/mad_cli/ui/prompts.py +83 -0
  85. mad_cli-0.4.0/tests/__init__.py +0 -0
  86. mad_cli-0.4.0/tests/unit/__init__.py +0 -0
  87. mad_cli-0.4.0/tests/unit/commands/__init__.py +0 -0
  88. mad_cli-0.4.0/tests/unit/commands/conftest.py +222 -0
  89. mad_cli-0.4.0/tests/unit/commands/test_app.py +31 -0
  90. mad_cli-0.4.0/tests/unit/commands/test_config.py +105 -0
  91. mad_cli-0.4.0/tests/unit/commands/test_install.py +351 -0
  92. mad_cli-0.4.0/tests/unit/commands/test_instances.py +144 -0
  93. mad_cli-0.4.0/tests/unit/commands/test_keys.py +146 -0
  94. mad_cli-0.4.0/tests/unit/commands/test_lifecycle.py +131 -0
  95. mad_cli-0.4.0/tests/unit/commands/test_profiles.py +192 -0
  96. mad_cli-0.4.0/tests/unit/commands/test_service_cli.py +81 -0
  97. mad_cli-0.4.0/tests/unit/commands/test_versions.py +210 -0
  98. mad_cli-0.4.0/tests/unit/core/__init__.py +0 -0
  99. mad_cli-0.4.0/tests/unit/core/test_claude_creds.py +30 -0
  100. mad_cli-0.4.0/tests/unit/core/test_compose.py +189 -0
  101. mad_cli-0.4.0/tests/unit/core/test_docker_check.py +108 -0
  102. mad_cli-0.4.0/tests/unit/core/test_envfile.py +154 -0
  103. mad_cli-0.4.0/tests/unit/core/test_instance.py +122 -0
  104. mad_cli-0.4.0/tests/unit/core/test_keyspec.py +56 -0
  105. mad_cli-0.4.0/tests/unit/core/test_paths.py +37 -0
  106. mad_cli-0.4.0/tests/unit/core/test_profiles.py +99 -0
  107. mad_cli-0.4.0/tests/unit/core/test_pypi.py +69 -0
  108. mad_cli-0.4.0/tests/unit/core/test_templates.py +105 -0
  109. mad_cli-0.4.0/tests/unit/core/usecases/__init__.py +0 -0
  110. mad_cli-0.4.0/tests/unit/core/usecases/test_install_flow.py +137 -0
  111. mad_cli-0.4.0/tests/unit/core/usecases/test_service.py +191 -0
  112. mad_cli-0.4.0/tests/unit/server/__init__.py +0 -0
  113. mad_cli-0.4.0/tests/unit/server/test_api.py +237 -0
  114. mad_cli-0.4.0/tests/unit/ui/__init__.py +0 -0
  115. mad_cli-0.4.0/tests/unit/ui/test_prompts.py +52 -0
@@ -0,0 +1,98 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ quality:
11
+ name: Lint + typecheck
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.11"
19
+ cache: pip
20
+
21
+ - name: Install package + dev deps
22
+ run: |
23
+ python -m pip install --upgrade pip
24
+ pip install -e '.[dev]'
25
+
26
+ - name: ruff check + format
27
+ run: |
28
+ ruff check .
29
+ ruff format --check .
30
+
31
+ - name: mypy (strict on mad_cli.core)
32
+ run: mypy
33
+
34
+ test:
35
+ name: Test + coverage
36
+ runs-on: ubuntu-latest
37
+ strategy:
38
+ matrix:
39
+ python-version: ["3.11", "3.12"]
40
+ steps:
41
+ - uses: actions/checkout@v4
42
+
43
+ - uses: actions/setup-python@v5
44
+ with:
45
+ python-version: ${{ matrix.python-version }}
46
+ cache: pip
47
+
48
+ - name: Install package + dev deps
49
+ run: |
50
+ python -m pip install --upgrade pip
51
+ pip install -e '.[dev]'
52
+
53
+ - name: Unit tests + coverage on mad_cli.core
54
+ run: pytest -q tests/unit --cov=mad_cli.core --cov-report=term-missing --cov-fail-under=90
55
+
56
+ - name: Full suite
57
+ run: pytest -q
58
+
59
+ audit:
60
+ name: Dependency vulnerability audit
61
+ runs-on: ubuntu-latest
62
+ steps:
63
+ - uses: actions/checkout@v4
64
+
65
+ - uses: actions/setup-python@v5
66
+ with:
67
+ python-version: "3.11"
68
+ cache: pip
69
+
70
+ - name: Install package + dev deps
71
+ run: |
72
+ python -m pip install --upgrade pip
73
+ pip install -e '.[dev]'
74
+
75
+ - name: pip-audit
76
+ run: pip-audit --strict --skip-editable .
77
+
78
+ build:
79
+ name: Build sdist + wheel
80
+ runs-on: ubuntu-latest
81
+ needs: [quality, test]
82
+ steps:
83
+ - uses: actions/checkout@v4
84
+
85
+ - uses: actions/setup-python@v5
86
+ with:
87
+ python-version: "3.11"
88
+ cache: pip
89
+
90
+ - name: Install build tooling
91
+ run: |
92
+ python -m pip install --upgrade pip build twine
93
+
94
+ - name: Build sdist + wheel
95
+ run: python -m build
96
+
97
+ - name: Verify artifacts with twine
98
+ run: python -m twine check dist/*
@@ -0,0 +1,21 @@
1
+ name: Docs sync → mad-docs /raw
2
+
3
+ # Thin caller for the shared reusable workflow in mad-core/.github.
4
+ # After merge to main, mirrors this repo's /docs tree verbatim into
5
+ # mad-core/mad-docs under raw/<service_slug>/ via a correlated PR.
6
+ on:
7
+ push:
8
+ branches: [main]
9
+
10
+ permissions:
11
+ contents: read
12
+ pull-requests: write
13
+
14
+ jobs:
15
+ docs-sync:
16
+ uses: mad-core/.github/.github/workflows/docs-sync.reusable.yml@main
17
+ with:
18
+ service_slug: mad-cli
19
+ secrets:
20
+ DOCS_SYNC_TOKEN: ${{ secrets.DOCS_SYNC_TOKEN }}
21
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
@@ -0,0 +1,19 @@
1
+ name: Docs validate
2
+
3
+ # Thin caller for the shared reusable workflow in mad-core/.github.
4
+ # The trigger lives here; the docs structure linter lives in the reusable.
5
+ # Lint-only gate — docs are NOT auto-generated in CI (update /docs locally
6
+ # via the living-docs skill/commands).
7
+ on:
8
+ pull_request:
9
+ branches: [main]
10
+
11
+ permissions:
12
+ contents: read
13
+ pull-requests: write
14
+
15
+ jobs:
16
+ docs-validate:
17
+ uses: mad-core/.github/.github/workflows/docs-validate.reusable.yml@main
18
+ with:
19
+ service_slug: mad-cli
@@ -0,0 +1,129 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ # Path-gate the release trigger: bytes that never reach a PyPI consumer
7
+ # (docs, tests, CI workflows themselves, CLAUDE.md) must not move the
8
+ # version. See CLAUDE.md hard rule on package-centric versioning.
9
+ paths:
10
+ - "src/mad_cli/**"
11
+ - "pyproject.toml"
12
+ - "README.md"
13
+ - "LICENSE"
14
+ workflow_dispatch:
15
+ inputs:
16
+ manual_publish:
17
+ description: "Build current tree and publish to PyPI (skips semantic-release)"
18
+ type: boolean
19
+ default: false
20
+ release_kind:
21
+ description: "Bump kind for the semantic-release run (auto = derive from commits)"
22
+ type: choice
23
+ default: auto
24
+ options:
25
+ - auto
26
+ - minor
27
+ - major
28
+
29
+ concurrency:
30
+ group: ${{ github.workflow }}-${{ github.ref }}
31
+ cancel-in-progress: false
32
+
33
+ jobs:
34
+ manual-publish-pypi:
35
+ name: Manual publish to PyPI
36
+ if: github.event_name == 'workflow_dispatch' && inputs.manual_publish
37
+ runs-on: ubuntu-latest
38
+ environment:
39
+ name: pypi
40
+ url: https://pypi.org/p/mad-cli
41
+ permissions:
42
+ id-token: write
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+
46
+ - uses: actions/setup-python@v5
47
+ with:
48
+ python-version: "3.11"
49
+ cache: pip
50
+
51
+ - name: Build sdist + wheel
52
+ run: |
53
+ python -m pip install --upgrade pip build
54
+ python -m build
55
+
56
+ - name: Publish to PyPI
57
+ uses: pypa/gh-action-pypi-publish@release/v1
58
+
59
+ release:
60
+ name: Semantic release
61
+ # Run on every push to `main` that touches a version-relevant path (above),
62
+ # and on `workflow_dispatch` whenever the operator did NOT pick the
63
+ # `manual_publish` escape hatch (that path goes through `manual-publish-pypi`).
64
+ if: |
65
+ github.event_name == 'push'
66
+ || (github.event_name == 'workflow_dispatch' && !inputs.manual_publish)
67
+ runs-on: ubuntu-latest
68
+ permissions:
69
+ contents: write
70
+ outputs:
71
+ released: ${{ steps.semrel.outputs.released }}
72
+ version: ${{ steps.semrel.outputs.version }}
73
+ tag: ${{ steps.semrel.outputs.tag }}
74
+ steps:
75
+ - uses: actions/checkout@v4
76
+ with:
77
+ fetch-depth: 0
78
+ token: ${{ secrets.GITHUB_TOKEN }}
79
+
80
+ - uses: actions/setup-python@v5
81
+ with:
82
+ python-version: "3.11"
83
+ cache: pip
84
+
85
+ - name: Install package + dev deps
86
+ run: |
87
+ python -m pip install --upgrade pip
88
+ pip install -e '.[dev]'
89
+
90
+ - name: Run pytest
91
+ run: pytest -q
92
+
93
+ - name: Python semantic release
94
+ id: semrel
95
+ uses: python-semantic-release/python-semantic-release@v9
96
+ with:
97
+ github_token: ${{ secrets.GITHUB_TOKEN }}
98
+ # `release_kind` only exists on `workflow_dispatch`; on `push` the
99
+ # expression evaluates to '' and `force` is a no-op (auto behavior).
100
+ # `auto` is also mapped to '' so the operator can pick it explicitly.
101
+ force: >-
102
+ ${{ (github.event_name == 'workflow_dispatch' && inputs.release_kind != 'auto')
103
+ && inputs.release_kind || '' }}
104
+
105
+ - name: Upload dist artifacts
106
+ if: steps.semrel.outputs.released == 'true'
107
+ uses: actions/upload-artifact@v4
108
+ with:
109
+ name: dist
110
+ path: dist/
111
+
112
+ publish-pypi:
113
+ name: Publish to PyPI
114
+ needs: release
115
+ if: needs.release.outputs.released == 'true'
116
+ runs-on: ubuntu-latest
117
+ environment:
118
+ name: pypi
119
+ url: https://pypi.org/p/mad-cli
120
+ permissions:
121
+ id-token: write
122
+ steps:
123
+ - uses: actions/download-artifact@v4
124
+ with:
125
+ name: dist
126
+ path: dist/
127
+
128
+ - name: Publish to PyPI
129
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,195 @@
1
+ name: TestPyPI preview
2
+
3
+ # Publishes a pre-release (PEP 440 `.dev` version) of mad-cli to TestPyPI for
4
+ # every PR against `main`, so the *exact built artifact* can be installed with
5
+ # pip before it ever reaches the real index.
6
+ #
7
+ # Why a published round-trip and not just `pip install -e .`: packaging bugs
8
+ # only manifest in the BUILT sdist/wheel — installing from an editable checkout
9
+ # or `git+https://…@branch` rebuilds from the source tree and silently masks
10
+ # them. The only faithful check is build → publish → install-from-index → import.
11
+ #
12
+ # ── One-time setup (until done, only `build` runs and it is always green) ──
13
+ # 1. Create an account on https://test.pypi.org.
14
+ # 2. Register a Trusted Publisher (a "pending publisher") for project
15
+ # `mad-cli` at https://test.pypi.org/manage/account/publishing/ with:
16
+ # Owner: mad-core Repository: mad-cli
17
+ # Workflow name: testpypi-preview.yml Environment: testpypi
18
+ # 3. Create a GitHub Environment named `testpypi`
19
+ # (repo Settings → Environments → New environment).
20
+ # 4. Set the repository variable TESTPYPI_ENABLED=true
21
+ # (Settings → Secrets and variables → Actions → Variables).
22
+ # Flip it to anything else to disable publishing without deleting the file.
23
+
24
+ on:
25
+ pull_request:
26
+ branches: [main]
27
+ workflow_dispatch:
28
+
29
+ concurrency:
30
+ # One in-flight preview per PR; a new push cancels the previous build so we
31
+ # don't waste a TestPyPI version number on a superseded commit.
32
+ group: testpypi-preview-${{ github.event.pull_request.number || github.ref }}
33
+ cancel-in-progress: true
34
+
35
+ jobs:
36
+ build:
37
+ name: Build preview (dev version)
38
+ # Trusted Publishing mints the publish token from the repo's own OIDC
39
+ # identity, which fork PRs never receive — and we would never want to hand
40
+ # publish rights to fork-authored code anyway. Skip the whole preview for
41
+ # forks; same-repo branches and manual runs proceed.
42
+ if: >-
43
+ github.event_name == 'workflow_dispatch'
44
+ || github.event.pull_request.head.repo.full_name == github.repository
45
+ runs-on: ubuntu-latest
46
+ outputs:
47
+ version: ${{ steps.version.outputs.version }}
48
+ steps:
49
+ - uses: actions/checkout@v4
50
+
51
+ - uses: actions/setup-python@v5
52
+ with:
53
+ python-version: "3.11"
54
+ cache: pip
55
+
56
+ - name: Derive a unique PEP 440 dev version
57
+ id: version
58
+ # `<base>.dev<run_id>`: run_id is globally unique and monotonic, so
59
+ # re-runs and concurrent PRs never collide on TestPyPI's immutable
60
+ # version namespace. The edit is ephemeral (never committed).
61
+ run: |
62
+ python - <<'PY' >> "$GITHUB_OUTPUT"
63
+ import os, pathlib, re
64
+
65
+ run_id = os.environ["GITHUB_RUN_ID"]
66
+ pp = pathlib.Path("pyproject.toml")
67
+ text = pp.read_text()
68
+ base = re.search(r'^version = "([^"]+)"', text, re.M).group(1)
69
+ dev = f"{base}.dev{run_id}"
70
+ pp.write_text(
71
+ re.sub(r'^version = "[^"]+"', f'version = "{dev}"', text, count=1, flags=re.M)
72
+ )
73
+
74
+ init = pathlib.Path("src/mad_cli/__init__.py")
75
+ init.write_text(
76
+ re.sub(r'__version__ = "[^"]+"', f'__version__ = "{dev}"', init.read_text(), count=1)
77
+ )
78
+
79
+ print(f"version={dev}")
80
+ PY
81
+
82
+ - name: Build sdist + wheel
83
+ run: |
84
+ python -m pip install --upgrade pip build twine
85
+ python -m build
86
+ python -m twine check dist/*
87
+
88
+ - name: Upload dist artifact
89
+ uses: actions/upload-artifact@v4
90
+ with:
91
+ name: testpypi-dist
92
+ path: dist/
93
+
94
+ publish:
95
+ name: Publish to TestPyPI
96
+ needs: build
97
+ # Gated on the operator opt-in variable so this PR (and any PR opened before
98
+ # the TestPyPI publisher is configured) stays green: no variable → no
99
+ # publish attempt, just the build above.
100
+ if: vars.TESTPYPI_ENABLED == 'true'
101
+ runs-on: ubuntu-latest
102
+ environment:
103
+ name: testpypi
104
+ url: https://test.pypi.org/p/mad-cli
105
+ permissions:
106
+ id-token: write
107
+ steps:
108
+ - uses: actions/download-artifact@v4
109
+ with:
110
+ name: testpypi-dist
111
+ path: dist/
112
+
113
+ - name: Publish to TestPyPI
114
+ uses: pypa/gh-action-pypi-publish@release/v1
115
+ with:
116
+ repository-url: https://test.pypi.org/legacy/
117
+
118
+ verify:
119
+ name: Verify pip install from TestPyPI
120
+ needs: [build, publish]
121
+ if: vars.TESTPYPI_ENABLED == 'true'
122
+ runs-on: ubuntu-latest
123
+ permissions:
124
+ contents: read
125
+ pull-requests: write
126
+ steps:
127
+ - uses: actions/setup-python@v5
128
+ with:
129
+ python-version: "3.11"
130
+
131
+ - name: Install the published preview in a clean venv and import it
132
+ env:
133
+ VERSION: ${{ needs.build.outputs.version }}
134
+ # Resolve the exact wheel URL from TestPyPI's JSON API and install THAT
135
+ # url, so pip pulls mad-cli from TestPyPI but every dependency from the
136
+ # default index (real PyPI). Pointing pip's --index-url at TestPyPI for
137
+ # the whole resolution is unsafe: TestPyPI hosts junk squats that shadow
138
+ # real deps by version number and break the install. Retry because
139
+ # TestPyPI indexing lags the upload.
140
+ run: |
141
+ set -euo pipefail
142
+ python -m venv /tmp/v
143
+ wheel_url=""
144
+ for i in $(seq 1 12); do
145
+ wheel_url=$(curl -fsS "https://test.pypi.org/pypi/mad-cli/${VERSION}/json" 2>/dev/null \
146
+ | jq -r 'first(.urls[] | select(.packagetype=="bdist_wheel") | .url) // empty' || true)
147
+ [ -n "$wheel_url" ] && break
148
+ echo "attempt $i: ${VERSION} not on TestPyPI yet, waiting…"
149
+ sleep 15
150
+ done
151
+ if [ -z "$wheel_url" ]; then
152
+ echo "::error::wheel for mad-cli==${VERSION} never appeared on TestPyPI"
153
+ exit 1
154
+ fi
155
+ echo "Installing ${wheel_url} (dependencies resolve from real PyPI)"
156
+ /tmp/v/bin/pip install "$wheel_url"
157
+ # Smoke test: import the package and print its version. The console
158
+ # script's subcommands live on another branch, so we do NOT invoke it.
159
+ /tmp/v/bin/python -c "import mad_cli; print(mad_cli.__version__)"
160
+
161
+ - name: Upsert install instructions on the PR
162
+ if: github.event_name == 'pull_request'
163
+ uses: actions/github-script@v7
164
+ env:
165
+ VERSION: ${{ needs.build.outputs.version }}
166
+ with:
167
+ script: |
168
+ const version = process.env.VERSION;
169
+ const marker = '<!-- testpypi-preview -->';
170
+ const body = [
171
+ marker,
172
+ '📦 **TestPyPI preview published & verified**',
173
+ '',
174
+ "Install this PR's exact built artifact (deps resolve from real PyPI):",
175
+ '```bash',
176
+ 'pip install "$(curl -s \\',
177
+ ` https://test.pypi.org/pypi/mad-cli/${version}/json \\`,
178
+ " | jq -r 'first(.urls[] | select(.packagetype==\"bdist_wheel\") | .url)')\"",
179
+ '```',
180
+ '',
181
+ '_CI verified a clean-venv install imports `mad_cli` and reports its version._',
182
+ ].join('\n');
183
+ const { owner, repo } = context.repo;
184
+ const issue_number = context.issue.number;
185
+ // Upsert: update our previous comment if present, else create one,
186
+ // so repeated pushes refresh a single comment instead of spamming.
187
+ const existing = await github.paginate(github.rest.issues.listComments, {
188
+ owner, repo, issue_number,
189
+ });
190
+ const mine = existing.find((c) => c.body && c.body.includes(marker));
191
+ if (mine) {
192
+ await github.rest.issues.updateComment({ owner, repo, comment_id: mine.id, body });
193
+ } else {
194
+ await github.rest.issues.createComment({ owner, repo, issue_number, body });
195
+ }
@@ -0,0 +1,22 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+
11
+ # Tooling caches
12
+ .pytest_cache/
13
+ .mypy_cache/
14
+ .ruff_cache/
15
+ .coverage
16
+ coverage.xml
17
+ htmlcov/
18
+
19
+ # Editors / OS
20
+ .idea/
21
+ .vscode/
22
+ .DS_Store