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.
- mad_cli-0.4.0/.github/workflows/ci.yml +98 -0
- mad_cli-0.4.0/.github/workflows/docs-sync.yml +21 -0
- mad_cli-0.4.0/.github/workflows/docs-validate.yml +19 -0
- mad_cli-0.4.0/.github/workflows/release.yml +129 -0
- mad_cli-0.4.0/.github/workflows/testpypi-preview.yml +195 -0
- mad_cli-0.4.0/.gitignore +22 -0
- mad_cli-0.4.0/CHANGELOG.md +508 -0
- mad_cli-0.4.0/CLAUDE.md +64 -0
- mad_cli-0.4.0/CONTRACTS.md +306 -0
- mad_cli-0.4.0/LICENSE +21 -0
- mad_cli-0.4.0/PKG-INFO +167 -0
- mad_cli-0.4.0/README.md +108 -0
- mad_cli-0.4.0/docs/.docs-manifest.yaml +571 -0
- mad_cli-0.4.0/docs/01-overview/context.md +43 -0
- mad_cli-0.4.0/docs/01-overview/glossary.md +21 -0
- mad_cli-0.4.0/docs/01-overview/passport.md +42 -0
- mad_cli-0.4.0/docs/01-overview/scope.md +36 -0
- mad_cli-0.4.0/docs/02-architecture/components.md +100 -0
- mad_cli-0.4.0/docs/02-architecture/overview.md +55 -0
- mad_cli-0.4.0/docs/02-architecture/source-tree.md +71 -0
- mad_cli-0.4.0/docs/02-architecture/test-tree.md +53 -0
- mad_cli-0.4.0/docs/03-contracts/cli.md +275 -0
- mad_cli-0.4.0/docs/03-contracts/external-dependencies.md +66 -0
- mad_cli-0.4.0/docs/03-contracts/http-api.md +64 -0
- mad_cli-0.4.0/docs/04-conventions/cli-design.md +79 -0
- mad_cli-0.4.0/docs/04-conventions/layering.md +53 -0
- mad_cli-0.4.0/docs/04-conventions/quality.md +57 -0
- mad_cli-0.4.0/docs/04-conventions/testing-strategy.md +47 -0
- mad_cli-0.4.0/docs/05-operations/ci-cd.md +42 -0
- mad_cli-0.4.0/docs/05-operations/configuration.md +65 -0
- mad_cli-0.4.0/docs/05-operations/installation.md +81 -0
- mad_cli-0.4.0/docs/05-operations/release.md +50 -0
- mad_cli-0.4.0/docs/06-history/changelog.md +62 -0
- mad_cli-0.4.0/docs/README.md +25 -0
- mad_cli-0.4.0/pyproject.toml +113 -0
- mad_cli-0.4.0/src/mad_cli/__init__.py +3 -0
- mad_cli-0.4.0/src/mad_cli/__main__.py +6 -0
- mad_cli-0.4.0/src/mad_cli/app.py +77 -0
- mad_cli-0.4.0/src/mad_cli/commands/__init__.py +5 -0
- mad_cli-0.4.0/src/mad_cli/commands/_adapt.py +41 -0
- mad_cli-0.4.0/src/mad_cli/commands/_common.py +12 -0
- mad_cli-0.4.0/src/mad_cli/commands/config.py +94 -0
- mad_cli-0.4.0/src/mad_cli/commands/install.py +504 -0
- mad_cli-0.4.0/src/mad_cli/commands/instances.py +102 -0
- mad_cli-0.4.0/src/mad_cli/commands/keys.py +126 -0
- mad_cli-0.4.0/src/mad_cli/commands/lifecycle.py +69 -0
- mad_cli-0.4.0/src/mad_cli/commands/profiles.py +238 -0
- mad_cli-0.4.0/src/mad_cli/commands/service.py +220 -0
- mad_cli-0.4.0/src/mad_cli/commands/versions.py +61 -0
- mad_cli-0.4.0/src/mad_cli/core/__init__.py +4 -0
- mad_cli-0.4.0/src/mad_cli/core/claude_creds.py +31 -0
- mad_cli-0.4.0/src/mad_cli/core/compose.py +145 -0
- mad_cli-0.4.0/src/mad_cli/core/docker_check.py +89 -0
- mad_cli-0.4.0/src/mad_cli/core/envfile.py +140 -0
- mad_cli-0.4.0/src/mad_cli/core/instance.py +110 -0
- mad_cli-0.4.0/src/mad_cli/core/keyspec.py +98 -0
- mad_cli-0.4.0/src/mad_cli/core/paths.py +40 -0
- mad_cli-0.4.0/src/mad_cli/core/profiles.py +93 -0
- mad_cli-0.4.0/src/mad_cli/core/pypi.py +29 -0
- mad_cli-0.4.0/src/mad_cli/core/templates.py +91 -0
- mad_cli-0.4.0/src/mad_cli/core/usecases/__init__.py +11 -0
- mad_cli-0.4.0/src/mad_cli/core/usecases/adopt.py +55 -0
- mad_cli-0.4.0/src/mad_cli/core/usecases/configvals.py +94 -0
- mad_cli-0.4.0/src/mad_cli/core/usecases/errors.py +57 -0
- mad_cli-0.4.0/src/mad_cli/core/usecases/install.py +263 -0
- mad_cli-0.4.0/src/mad_cli/core/usecases/instances.py +156 -0
- mad_cli-0.4.0/src/mad_cli/core/usecases/keys.py +169 -0
- mad_cli-0.4.0/src/mad_cli/core/usecases/lifecycle.py +76 -0
- mad_cli-0.4.0/src/mad_cli/core/usecases/service.py +269 -0
- mad_cli-0.4.0/src/mad_cli/core/usecases/versions.py +126 -0
- mad_cli-0.4.0/src/mad_cli/py.typed +0 -0
- mad_cli-0.4.0/src/mad_cli/server/__init__.py +13 -0
- mad_cli-0.4.0/src/mad_cli/server/app.py +260 -0
- mad_cli-0.4.0/src/mad_cli/server/auth.py +41 -0
- mad_cli-0.4.0/src/mad_cli/server/models.py +156 -0
- mad_cli-0.4.0/src/mad_cli/templates/Dockerfile.tmpl +66 -0
- mad_cli-0.4.0/src/mad_cli/templates/__init__.py +6 -0
- mad_cli-0.4.0/src/mad_cli/templates/com.mad-core.mad-cli.plist.tmpl +28 -0
- mad_cli-0.4.0/src/mad_cli/templates/compose.yml.tmpl +29 -0
- mad_cli-0.4.0/src/mad_cli/templates/entrypoint.sh.tmpl +11 -0
- mad_cli-0.4.0/src/mad_cli/templates/mad-cli.service.tmpl +15 -0
- mad_cli-0.4.0/src/mad_cli/ui/__init__.py +5 -0
- mad_cli-0.4.0/src/mad_cli/ui/console.py +65 -0
- mad_cli-0.4.0/src/mad_cli/ui/prompts.py +83 -0
- mad_cli-0.4.0/tests/__init__.py +0 -0
- mad_cli-0.4.0/tests/unit/__init__.py +0 -0
- mad_cli-0.4.0/tests/unit/commands/__init__.py +0 -0
- mad_cli-0.4.0/tests/unit/commands/conftest.py +222 -0
- mad_cli-0.4.0/tests/unit/commands/test_app.py +31 -0
- mad_cli-0.4.0/tests/unit/commands/test_config.py +105 -0
- mad_cli-0.4.0/tests/unit/commands/test_install.py +351 -0
- mad_cli-0.4.0/tests/unit/commands/test_instances.py +144 -0
- mad_cli-0.4.0/tests/unit/commands/test_keys.py +146 -0
- mad_cli-0.4.0/tests/unit/commands/test_lifecycle.py +131 -0
- mad_cli-0.4.0/tests/unit/commands/test_profiles.py +192 -0
- mad_cli-0.4.0/tests/unit/commands/test_service_cli.py +81 -0
- mad_cli-0.4.0/tests/unit/commands/test_versions.py +210 -0
- mad_cli-0.4.0/tests/unit/core/__init__.py +0 -0
- mad_cli-0.4.0/tests/unit/core/test_claude_creds.py +30 -0
- mad_cli-0.4.0/tests/unit/core/test_compose.py +189 -0
- mad_cli-0.4.0/tests/unit/core/test_docker_check.py +108 -0
- mad_cli-0.4.0/tests/unit/core/test_envfile.py +154 -0
- mad_cli-0.4.0/tests/unit/core/test_instance.py +122 -0
- mad_cli-0.4.0/tests/unit/core/test_keyspec.py +56 -0
- mad_cli-0.4.0/tests/unit/core/test_paths.py +37 -0
- mad_cli-0.4.0/tests/unit/core/test_profiles.py +99 -0
- mad_cli-0.4.0/tests/unit/core/test_pypi.py +69 -0
- mad_cli-0.4.0/tests/unit/core/test_templates.py +105 -0
- mad_cli-0.4.0/tests/unit/core/usecases/__init__.py +0 -0
- mad_cli-0.4.0/tests/unit/core/usecases/test_install_flow.py +137 -0
- mad_cli-0.4.0/tests/unit/core/usecases/test_service.py +191 -0
- mad_cli-0.4.0/tests/unit/server/__init__.py +0 -0
- mad_cli-0.4.0/tests/unit/server/test_api.py +237 -0
- mad_cli-0.4.0/tests/unit/ui/__init__.py +0 -0
- 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
|
+
}
|
mad_cli-0.4.0/.gitignore
ADDED
|
@@ -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
|