From b4c53354b8adc279cf9a98ef85d85bb0daefb5a4 Mon Sep 17 00:00:00 2001 From: Calvin Grunewald Date: Mon, 27 Apr 2026 14:40:45 -0700 Subject: [PATCH 1/2] ci: add publish + release + regenerate-sdk workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the three CI/CD workflows the archastro-js repo uses, adapted for the single-package Python SDK and PyPI Trusted Publishing. publish.yml — fires on `v*` tag pushes (and is reachable via workflow_dispatch so release.yml can re-trigger after pushing the tag, since GITHUB_TOKEN-pushed tags don't fire workflow runs themselves). Authenticates to PyPI via OIDC — no API token required after the first manual publish + Trusted Publisher configured at pypi.org/manage/project/archastro-sdk/. Uses pypa/gh-action-pypi-publish (commit-pinned), validates that the tag matches `pyproject.toml`'s `[project].version`, creates a matching GitHub Release. release.yml — manual workflow_dispatch with a bump knob (patch/minor/major). Runs the same lint + unit + harness + contract suites as ci.yml on Python 3.12 (single matrix entry to keep the release gate fast), bumps the version with `uv version --bump`, pushes the bump on a `release/v$VERSION` branch, tags `v$VERSION`, dispatches publish.yml against the tag, then opens + squash-merges a PR back into main so the bump lands. regenerate-sdk.yml — manual workflow_dispatch with two knobs (openapi_ref, generator_spec). Runs `./scripts/regenerate_sdk.sh` with `uv` installed up-front because the script invokes `uv run ruff ...` to normalize generated output. Detects diffs in src/, tests/, and specs/. Force-pushes to the stable `auto/regenerate-sdk` branch and either opens or updates a PR — the PR's own CI is what runs build + tests, so this workflow stays narrowly scoped to "produce the diff". Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/publish.yml | 95 ++++++++++++++++++ .github/workflows/regenerate-sdk.yml | 133 ++++++++++++++++++++++++ .github/workflows/release.yml | 145 +++++++++++++++++++++++++++ 3 files changed, 373 insertions(+) create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/regenerate-sdk.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..2655d35 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,95 @@ +name: Publish + +# Triggered by the tags that release.yml pushes (v*), and reachable via +# workflow_dispatch so release.yml can re-trigger after pushing the tag — +# tags pushed by GITHUB_TOKEN don't themselves trigger workflows. +# +# Authenticates to PyPI via OIDC (Trusted Publishing) — no PYPI_API_TOKEN. +# +# Prereqs (one-time): +# 1. First publish done manually from a laptop (uv publish or twine upload) +# 2. Trusted publisher configured at pypi.org/manage/project/archastro-sdk/ +# pointing at this workflow filename (publish.yml) and the `pypi` +# environment used below. + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: write # create GitHub Release + id-token: write # OIDC token for PyPI Trusted Publishing + +jobs: + publish: + name: PyPI publish + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/archastro-sdk + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Python + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 + with: + python-version: '3.12' + + - name: Install uv + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v3.2.4 + with: + enable-cache: true + + - name: Install project + dev deps + run: uv sync --all-extras + + - name: Resolve version from tag + id: pkg + env: + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + case "$TAG" in + v*) + echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" + ;; + *) + echo "Unrecognized tag: $TAG (expected v*)" >&2 + exit 1 + ;; + esac + + - name: Verify pyproject.toml version matches tag + env: + EXPECTED: ${{ steps.pkg.outputs.version }} + run: | + set -euo pipefail + actual=$(uv run python -c "import tomllib, sys; print(tomllib.loads(open('pyproject.toml','rb').read().decode())['project']['version'])") + if [ "$actual" != "$EXPECTED" ]; then + echo "Version mismatch: tag says $EXPECTED, pyproject.toml says $actual" >&2 + exit 1 + fi + + - name: Build sdist + wheel + run: | + rm -rf dist + uv build + + - name: Publish to PyPI (Trusted Publishing) + uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # release/v1 + # Defaults: reads dist/, uses OIDC when id-token: write is set + a + # trusted publisher is configured for this repo + workflow + env. + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + VERSION: ${{ steps.pkg.outputs.version }} + run: | + set -euo pipefail + gh release create "$TAG" \ + --title "archastro-sdk v$VERSION" \ + --notes "Published to PyPI: https://pypi.org/project/archastro-sdk/$VERSION/" \ + --generate-notes diff --git a/.github/workflows/regenerate-sdk.yml b/.github/workflows/regenerate-sdk.yml new file mode 100644 index 0000000..eb19fd3 --- /dev/null +++ b/.github/workflows/regenerate-sdk.yml @@ -0,0 +1,133 @@ +name: Regenerate SDK + +# Manually trigger from the Actions tab to pull the latest spec from +# archastro-openapi and re-run @archastro/sdk-generator. If anything +# changes, the workflow force-pushes to a stable branch and opens (or +# updates) a PR to main for review. +# +# No build/test step here — the PR's own CI runs them. + +on: + workflow_dispatch: + inputs: + openapi_ref: + description: archastro-openapi git ref to pull spec from + required: false + default: main + generator_spec: + description: npm spec for @archastro/sdk-generator (e.g. @archastro/sdk-generator@latest or @0.2.0) + required: false + default: '@archastro/sdk-generator@latest' + +permissions: + contents: write + pull-requests: write + +env: + AUTO_BRANCH: auto/regenerate-sdk + +jobs: + regenerate: + name: Regenerate + open PR + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 20 + cache: npm + + - run: npm ci + + - name: Setup Python + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 + with: + python-version: '3.12' + + - name: Install uv + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v3.2.4 + with: + enable-cache: true + + - name: Install project + dev deps + # Required because the regen script invokes `uv run ruff ...` to + # normalize formatting on the generated tree. + run: uv sync --all-extras + + - name: Regenerate SDK + env: + ARCHASTRO_OPENAPI_REF: ${{ inputs.openapi_ref }} + ARCHASTRO_SDK_GENERATOR: ${{ inputs.generator_spec }} + run: ./scripts/regenerate_sdk.sh + + - name: Detect changes + id: diff + run: | + if [ -z "$(git status --porcelain src tests specs)" ]; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No changes from regeneration; nothing to do." + else + echo "changed=true" >> "$GITHUB_OUTPUT" + git status --short src tests specs + fi + + - name: Resolve generator version + if: steps.diff.outputs.changed == 'true' + id: gen + env: + GEN_SPEC: ${{ inputs.generator_spec }} + run: | + set -euo pipefail + resolved=$(npm view "$GEN_SPEC" version 2>/dev/null || echo unknown) + echo "version=$resolved" >> "$GITHUB_OUTPUT" + + - name: Commit + force-push to ${{ env.AUTO_BRANCH }} + if: steps.diff.outputs.changed == 'true' + env: + REF: ${{ inputs.openapi_ref }} + GEN_VERSION: ${{ steps.gen.outputs.version }} + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -B "$AUTO_BRANCH" + git add src tests specs + git commit -m "chore(sdk): regenerate from archastro-openapi@$REF (generator $GEN_VERSION)" + git push --force-with-lease origin "$AUTO_BRANCH" + + - name: Open or update PR + if: steps.diff.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REF: ${{ inputs.openapi_ref }} + GEN_SPEC: ${{ inputs.generator_spec }} + GEN_VERSION: ${{ steps.gen.outputs.version }} + run: | + set -euo pipefail + file_count=$(git diff --name-only HEAD~1 HEAD -- src tests specs | wc -l | tr -d ' ') + body=$(cat <> "$GITHUB_OUTPUT" + + - name: Commit + push release branch + env: + BRANCH: ${{ steps.bump.outputs.branch }} + VERSION: ${{ steps.bump.outputs.version }} + run: | + set -euo pipefail + git checkout -b "$BRANCH" + git add pyproject.toml uv.lock + git commit -m "release: archastro-sdk v$VERSION" + git push origin "$BRANCH" + + - name: Create + push tag + env: + TAG: ${{ steps.bump.outputs.tag }} + run: | + set -euo pipefail + git tag -a "$TAG" -m "$TAG" + git push origin "$TAG" + + - name: Dispatch publish.yml for the tag + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.bump.outputs.tag }} + run: | + set -euo pipefail + echo "Dispatching publish.yml --ref $TAG" + gh workflow run publish.yml --ref "$TAG" + + - name: Open PR + merge + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: ${{ steps.bump.outputs.branch }} + VERSION: ${{ steps.bump.outputs.version }} + TAG: ${{ steps.bump.outputs.tag }} + run: | + set -euo pipefail + body=$(cat < Date: Thu, 11 Jun 2026 11:33:03 -0700 Subject: [PATCH 2/2] ci: harden sdk workflow dependencies --- .github/workflows/ci.yml | 32 +++++-- .github/workflows/publish.yml | 8 +- .github/workflows/regenerate-sdk.yml | 24 ++--- .github/workflows/release.yml | 22 ++++- README.md | 11 +-- package-lock.json | 136 +++++++++++++++++++++------ package.json | 9 +- pyproject.toml | 2 + scripts/regenerate_sdk.sh | 21 +++-- tests/contract/conftest.py | 15 ++- uv.lock | 37 ++++++++ 11 files changed, 240 insertions(+), 77 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc3cbf0..a1109fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: lint-and-test: name: Lint + Test (Python ${{ matrix.python }}) @@ -17,30 +20,35 @@ jobs: strategy: fail-fast: false matrix: - python: ["3.11", "3.12"] + python: ["3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Node (for @archastro/channel-harness + prism) - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20 cache: npm - - run: npm ci + - run: npm ci --ignore-scripts + + - name: Audit JS tooling dependencies + run: npm audit --audit-level=moderate - name: Setup Python ${{ matrix.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: ${{ matrix.python }} - name: Install uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: + version: "0.11.3" + checksum: c0f3236f146e55472663cfbcc9be3042a9f1092275bbe3fe2a56a6cbfd3da5ce enable-cache: true - name: Install project + dev deps - run: uv sync --all-extras + run: uv sync --locked --all-extras - name: Ruff lint run: uv run ruff check @@ -58,3 +66,13 @@ jobs: run: uv run pytest tests/contract env: ARCHASTRO_RUN_CHANNEL_CONTRACT_TESTS: "1" + + - name: Build and import wheel + run: | + rm -rf dist + uv build --no-build-isolation + uv export --locked --no-dev --no-emit-project --format requirements.txt --output-file dist/runtime-requirements.txt >/dev/null + uv venv --clear .wheel-smoke + uv pip install --python .wheel-smoke --require-hashes -r dist/runtime-requirements.txt + uv pip install --python .wheel-smoke --no-deps dist/*.whl + .wheel-smoke/bin/python -c "import archastro.platform; import archastro.phx_channel" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2655d35..6f9b7e3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -38,12 +38,14 @@ jobs: python-version: '3.12' - name: Install uv - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v3.2.4 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: + version: "0.11.3" + checksum: c0f3236f146e55472663cfbcc9be3042a9f1092275bbe3fe2a56a6cbfd3da5ce enable-cache: true - name: Install project + dev deps - run: uv sync --all-extras + run: uv sync --locked --all-extras - name: Resolve version from tag id: pkg @@ -75,7 +77,7 @@ jobs: - name: Build sdist + wheel run: | rm -rf dist - uv build + uv build --no-build-isolation - name: Publish to PyPI (Trusted Publishing) uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # release/v1 diff --git a/.github/workflows/regenerate-sdk.yml b/.github/workflows/regenerate-sdk.yml index eb19fd3..d673a99 100644 --- a/.github/workflows/regenerate-sdk.yml +++ b/.github/workflows/regenerate-sdk.yml @@ -14,11 +14,6 @@ on: description: archastro-openapi git ref to pull spec from required: false default: main - generator_spec: - description: npm spec for @archastro/sdk-generator (e.g. @archastro/sdk-generator@latest or @0.2.0) - required: false - default: '@archastro/sdk-generator@latest' - permissions: contents: write pull-requests: write @@ -40,7 +35,10 @@ jobs: node-version: 20 cache: npm - - run: npm ci + - run: npm ci --ignore-scripts + + - name: Audit JS tooling dependencies + run: npm audit --audit-level=moderate - name: Setup Python uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 @@ -48,19 +46,20 @@ jobs: python-version: '3.12' - name: Install uv - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v3.2.4 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: + version: "0.11.3" + checksum: c0f3236f146e55472663cfbcc9be3042a9f1092275bbe3fe2a56a6cbfd3da5ce enable-cache: true - name: Install project + dev deps # Required because the regen script invokes `uv run ruff ...` to # normalize formatting on the generated tree. - run: uv sync --all-extras + run: uv sync --locked --all-extras - name: Regenerate SDK env: ARCHASTRO_OPENAPI_REF: ${{ inputs.openapi_ref }} - ARCHASTRO_SDK_GENERATOR: ${{ inputs.generator_spec }} run: ./scripts/regenerate_sdk.sh - name: Detect changes @@ -77,11 +76,9 @@ jobs: - name: Resolve generator version if: steps.diff.outputs.changed == 'true' id: gen - env: - GEN_SPEC: ${{ inputs.generator_spec }} run: | set -euo pipefail - resolved=$(npm view "$GEN_SPEC" version 2>/dev/null || echo unknown) + resolved=$(node -p "require('./node_modules/@archastro/sdk-generator/package.json').version") echo "version=$resolved" >> "$GITHUB_OUTPUT" - name: Commit + force-push to ${{ env.AUTO_BRANCH }} @@ -103,7 +100,6 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REF: ${{ inputs.openapi_ref }} - GEN_SPEC: ${{ inputs.generator_spec }} GEN_VERSION: ${{ steps.gen.outputs.version }} run: | set -euo pipefail @@ -112,7 +108,7 @@ jobs: Automated SDK regeneration. - **archastro-openapi ref**: \`$REF\` - - **generator spec**: \`$GEN_SPEC\` (resolved to \`$GEN_VERSION\`) + - **generator version**: \`$GEN_VERSION\` from package-lock.json - **changed files**: $file_count Review the diff carefully — generator output changes can be subtle. The PR's CI will run lint + tests. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 030351a..2d3d7dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,7 +40,10 @@ jobs: node-version: 20 cache: npm - - run: npm ci + - run: npm ci --ignore-scripts + + - name: Audit JS tooling dependencies + run: npm audit --audit-level=moderate - name: Setup Python uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 @@ -48,22 +51,31 @@ jobs: python-version: '3.12' - name: Install uv - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v3.2.4 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: + version: "0.11.3" + checksum: c0f3236f146e55472663cfbcc9be3042a9f1092275bbe3fe2a56a6cbfd3da5ce enable-cache: true - name: Install project + dev deps - run: uv sync --all-extras + run: uv sync --locked --all-extras - name: Test (sanity gate before bumping) - # Same suites as ci.yml but Python 3.12 only — keeps the release - # gate honest without spending time on the full ci.yml matrix. + # Core CI gates on Python 3.12 only — keeps the release gate honest + # without spending time on the full ci.yml matrix. run: | uv run ruff check uv run ruff format --check uv run pytest tests/test_http_client.py src/archastro/phx_channel/tests/test_unit.py uv run pytest tests/harness uv run pytest tests/contract + rm -rf dist + uv build --no-build-isolation + uv export --locked --no-dev --no-emit-project --format requirements.txt --output-file dist/runtime-requirements.txt >/dev/null + uv venv --clear .wheel-smoke + uv pip install --python .wheel-smoke --require-hashes -r dist/runtime-requirements.txt + uv pip install --python .wheel-smoke --no-deps dist/*.whl + .wheel-smoke/bin/python -c "import archastro.platform; import archastro.phx_channel" env: ARCHASTRO_RUN_CHANNEL_CONTRACT_TESTS: "1" diff --git a/README.md b/README.md index 79fc389..df1fd60 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,8 @@ This repo contains: ### Setup ```bash -npm ci # channel-harness + prism (for contract tests) -uv sync # Python deps + dev deps (pytest, ruff) +npm ci --ignore-scripts # channel-harness + prism (for contract tests) +uv sync --locked --all-extras ``` ### Running tests @@ -70,13 +70,11 @@ header; they'll be overwritten. ``` The script fetches the spec from `ArchAstro/archastro-openapi@main` and -runs the generator via `npx`. Knobs: +runs the generator locked in `package-lock.json`. Knobs: - `ARCHASTRO_OPENAPI_REF=some-branch ./scripts/regenerate_sdk.sh` — pull the spec from a non-default ref (useful when a spec change is on a branch awaiting merge). -- `ARCHASTRO_SDK_GENERATOR=@archastro/sdk-generator@0.1.0 ./scripts/regenerate_sdk.sh` - — pin the generator version for a release branch. After regenerating, review the diff, run the full test suite, and commit. @@ -84,6 +82,7 @@ After regenerating, review the diff, run the full test suite, and commit. ```bash # bump version in pyproject.toml, then: -uv build +uv sync --locked --all-extras +uv build --no-build-isolation uv publish ``` diff --git a/package-lock.json b/package-lock.json index 10dc08f..26a65b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "devDependencies": { "@archastro/channel-harness": "^0.1.0", + "@archastro/sdk-generator": "0.1.0", "@stoplight/prism-cli": "5.14.2" }, "engines": { @@ -97,6 +98,19 @@ "jsep": "^0.4.0||^1.0.0" } }, + "node_modules/@nodable/entities": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz", + "integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@stoplight/http-spec": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@stoplight/http-spec/-/http-spec-7.1.0.tgz", @@ -415,9 +429,9 @@ "license": "Apache-2.0" }, "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", "dev": true, "license": "MIT", "engines": { @@ -567,6 +581,19 @@ "node": ">= 8" } }, + "node_modules/anynum": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.0.tgz", + "integrity": "sha512-xjR9/zBVnUOP6ztMIIgShjsxui80nQUQH+5xJnvrYLs+90bF25/KJqaAi8mk+B4RDtX1Nspi6fmp4YTEts8SfA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -855,9 +882,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { @@ -871,10 +898,27 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, "node_modules/fast-xml-parser": { - "version": "4.5.6", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.6.tgz", - "integrity": "sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz", + "integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==", "dev": true, "funding": [ { @@ -884,7 +928,11 @@ ], "license": "MIT", "dependencies": { - "strnum": "^1.0.5" + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.2.0", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.3.0", + "xml-naming": "^0.1.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -1599,6 +1647,22 @@ "node": ">=4" } }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/picomatch": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", @@ -1693,13 +1757,6 @@ "dev": true, "license": "MIT" }, - "node_modules/postman-collection/node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, - "license": "MIT" - }, "node_modules/postman-url-encoder": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/postman-url-encoder/-/postman-url-encoder-3.0.5.tgz", @@ -1964,9 +2021,9 @@ } }, "node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.0.tgz", + "integrity": "sha512-sHrVyWWdq28RbhjuJdZsA1SnGRJV6NiXbk6AXBxDOsgAcA+lmpUZCYjOdLBxkXMwis6RRe7dlZt4VlIWFVzkmg==", "dev": true, "funding": [ { @@ -1974,7 +2031,10 @@ "url": "https://github.com/sponsors/NaturalIntelligence" } ], - "license": "MIT" + "license": "MIT", + "dependencies": { + "anynum": "^1.0.0" + } }, "node_modules/supports-color": { "version": "7.2.0", @@ -2062,13 +2122,17 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/validate.io-array": { @@ -2163,9 +2227,9 @@ } }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "dev": true, "license": "MIT", "engines": { @@ -2184,6 +2248,22 @@ } } }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 77d2cf0..a20136f 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,19 @@ }, "devDependencies": { "@archastro/channel-harness": "^0.1.0", + "@archastro/sdk-generator": "0.1.0", "@stoplight/prism-cli": "5.14.2" }, "overrides": { "@stoplight/prism-core": "5.8.0", "@stoplight/prism-http": "5.12.0", - "@stoplight/prism-http-server": "5.12.2" + "@stoplight/prism-http-server": "5.12.2", + "@tootallnate/once": "2.0.1", + "fast-uri": "3.1.2", + "fast-xml-parser": "5.8.0", + "lodash": "4.18.1", + "uuid": "11.1.1", + "ws": "8.21.0" }, "engines": { "node": ">=20" diff --git a/pyproject.toml b/pyproject.toml index 492f3f1..219f677 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ dependencies = [ [project.optional-dependencies] dev = [ + "hatchling>=1.27", "pytest>=8.0", "pytest-asyncio>=0.24", "ruff>=0.11", @@ -46,6 +47,7 @@ exclude = ["**/tests/**"] [dependency-groups] dev = [ + "hatchling>=1.27", "pytest>=8.0", "pytest-asyncio>=0.24", "ruff>=0.11", diff --git a/scripts/regenerate_sdk.sh b/scripts/regenerate_sdk.sh index d71d7bd..1d59c29 100755 --- a/scripts/regenerate_sdk.sh +++ b/scripts/regenerate_sdk.sh @@ -6,7 +6,7 @@ # on GitHub (the canonical source of truth). # 2. Copy it into ./specs/platform-openapi.json so the SDK package # ships its own copy for contract-test consumers. -# 3. Run @archastro/sdk-generator (from public npm via npx) to emit +# 3. Run the locked local @archastro/sdk-generator to emit # Pydantic models, resources, and channel classes under # src/archastro/platform/, and the contract-test tree under # tests/contract/. @@ -14,16 +14,14 @@ # Usage: # ./scripts/regenerate_sdk.sh # pull spec from main # ARCHASTRO_OPENAPI_REF=some-branch ./scripts/regenerate_sdk.sh -# ARCHASTRO_SDK_GENERATOR=@archastro/sdk-generator@0.1.0 ./scripts/regenerate_sdk.sh # # Env knobs: # ARCHASTRO_OPENAPI_REF Git ref in archastro-openapi to pull the # spec from (default: main). Useful when a # spec change is on a branch awaiting merge. -# ARCHASTRO_SDK_GENERATOR Package spec for the generator passed to -# npx (default: @archastro/sdk-generator@latest). -# Pin to a specific version for reproducible -# regenerations in a release branch. +# ARCHASTRO_SDK_GENERATOR_BIN +# Path to a locally installed sdk-generator binary. +# Defaults to node_modules/.bin/sdk-generator. set -euo pipefail @@ -33,7 +31,7 @@ CONFIG_FILE="$REPO_ROOT/scripts/sdk-generator-config.json" REF="${ARCHASTRO_OPENAPI_REF:-main}" SPEC_URL="https://raw.githubusercontent.com/ArchAstro/archastro-openapi/${REF}/specs/platform-openapi.json" -SDK_GENERATOR_SPEC="${ARCHASTRO_SDK_GENERATOR:-@archastro/sdk-generator@latest}" +SDK_GENERATOR_BIN="${ARCHASTRO_SDK_GENERATOR_BIN:-$REPO_ROOT/node_modules/.bin/sdk-generator}" log() { printf '==> %s\n' "$*"; } @@ -56,14 +54,19 @@ SPEC="$SPEC_DST" node -e ' # ─── 2. Generate SDK + contract tests ─────────────────────────── log "Generating Python SDK into $REPO_ROOT" -npx --yes "$SDK_GENERATOR_SPEC" \ +if [ ! -x "$SDK_GENERATOR_BIN" ]; then + echo "sdk-generator not found at $SDK_GENERATOR_BIN. Run 'npm ci --ignore-scripts' first." >&2 + exit 1 +fi + +"$SDK_GENERATOR_BIN" \ --spec "$SPEC_DST" \ --config "$CONFIG_FILE" \ --lang python \ --out "$REPO_ROOT" log "Generating Python contract tests into $REPO_ROOT" -npx --yes "$SDK_GENERATOR_SPEC" \ +"$SDK_GENERATOR_BIN" \ --spec "$SPEC_DST" \ --config "$CONFIG_FILE" \ --lang contract-tests-py \ diff --git a/tests/contract/conftest.py b/tests/contract/conftest.py index 0c3160c..d3bba76 100644 --- a/tests/contract/conftest.py +++ b/tests/contract/conftest.py @@ -21,6 +21,10 @@ _prism_process = None +PRISM_BIN = os.environ.get( + "PRISM_BIN", + os.path.join(os.path.dirname(__file__), "../../node_modules/.bin/prism"), +) HARNESS_BIN = os.environ.get( "ARCHASTRO_HARNESS_BIN", os.path.join( @@ -44,10 +48,13 @@ def _channel_tests_enabled() -> bool: def pytest_configure(config): global _prism_process + if not os.path.exists(PRISM_BIN): + raise RuntimeError( + f"Prism bin not found at {PRISM_BIN}. Run 'npm ci --ignore-scripts' first." + ) _prism_process = subprocess.Popen( [ - "npx", - "@stoplight/prism-cli", + PRISM_BIN, "mock", SPEC_PATH, "--port", @@ -78,7 +85,7 @@ def pytest_unconfigure(config): def _wait_for_prism(timeout=30): deadline = time.time() + timeout while time.time() < deadline: - # Fast-fail if Prism process exited (bad spec path, missing npx, etc.) + # Fast-fail if Prism process exited (bad spec path, missing Prism binary, etc.) if _prism_process.poll() is not None: stderr = _prism_process.stderr.read().decode() if _prism_process.stderr else "" raise RuntimeError(f"Prism exited with code {_prism_process.returncode}: {stderr}") @@ -95,7 +102,7 @@ def _start_harness_service(timeout: float = 15.0) -> None: if not os.path.exists(HARNESS_BIN): raise RuntimeError( f"channel-harness bin not found at {HARNESS_BIN}. Set ARCHASTRO_HARNESS_BIN " - f"or run 'npm install @archastro/channel-harness' (or 'npm run build' in the archastro-openapi workspace)." + f"or run 'npm ci --ignore-scripts' (or 'npm run build' in the archastro-openapi workspace)." ) _harness_process = subprocess.Popen( ["node", HARNESS_BIN, SPEC_PATH], diff --git a/uv.lock b/uv.lock index 52616c9..9220472 100644 --- a/uv.lock +++ b/uv.lock @@ -36,6 +36,7 @@ dependencies = [ [package.optional-dependencies] dev = [ + { name = "hatchling" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "ruff" }, @@ -43,6 +44,7 @@ dev = [ [package.dev-dependencies] dev = [ + { name = "hatchling" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "ruff" }, @@ -50,6 +52,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "hatchling", marker = "extra == 'dev'", specifier = ">=1.27" }, { name = "httpx", specifier = ">=0.27" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, @@ -61,6 +64,7 @@ provides-extras = ["dev"] [package.metadata.requires-dev] dev = [ + { name = "hatchling", specifier = ">=1.27" }, { name = "pytest", specifier = ">=8.0" }, { name = "pytest-asyncio", specifier = ">=0.24" }, { name = "ruff", specifier = ">=0.11" }, @@ -93,6 +97,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "hatchling" +version = "1.30.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pathspec" }, + { name = "pluggy" }, + { name = "trove-classifiers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/4c/8717ccb844b4fa5a5ba6352e97d743ed24e9a22cf90b7c109c17030a46a1/hatchling-1.30.1.tar.gz", hash = "sha256:eee4fd45357f72ebb3d7a42e5d72cfb5e29ed426d79e8836288926c4258d5f2e", size = 56929, upload-time = "2026-06-02T00:09:41.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/49/2797ec0ef88008a653a8867bb8d1e5c223cd2df8e40390dd5c6a0279cbc5/hatchling-1.30.1-py3-none-any.whl", hash = "sha256:161eacafb3c6f91526e92116d21426369f2c36e98c36a864f11a96345ad4ee31", size = 77489, upload-time = "2026-06-02T00:09:40.139Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -148,6 +167,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, ] +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -337,6 +365,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, ] +[[package]] +name = "trove-classifiers" +version = "2026.6.1.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/e3/7ca82ee24c82d344584abd5b8637b3bd056f2900226e8d82fc22f1184b92/trove_classifiers-2026.6.1.19.tar.gz", hash = "sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745", size = 17059, upload-time = "2026-06-01T19:41:34.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl", hash = "sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3", size = 14211, upload-time = "2026-06-01T19:41:33.434Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0"