refactor: snake_case primary, camelCase aliases for spec-faithful met… #65
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| merge_group: | |
| workflow_dispatch: | |
| permissions: {} | |
| concurrency: | |
| group: ci-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| FORCE_COLOR: "1" | |
| jobs: | |
| plan: | |
| name: Plan | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| outputs: | |
| run-tests: ${{ steps.decide.outputs.run-tests }} | |
| os-matrix: ${{ steps.decide.outputs.os-matrix }} | |
| coverage-floor: ${{ steps.decide.outputs.coverage-floor }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4 | |
| id: changes | |
| with: | |
| filters: | | |
| src: | |
| - 'src/yarlpattern/**' | |
| tests: | |
| - 'tests/**' | |
| os_sensitive: | |
| # URL pattern matching is pure-Python with no OS / filesystem | |
| # surface, so there's no path-specific OS sensitivity. Kept | |
| # as an empty list so the downstream matrix planning logic | |
| # (which expects this key) still resolves to "false". | |
| - 'this-path-never-matches' | |
| lockfile: | |
| - 'uv.lock' | |
| config: | |
| - 'pyproject.toml' | |
| ci: | |
| - '.github/**' | |
| tooling: | |
| - 'scripts/**' | |
| - 'justfile' | |
| - 'renovate.json' | |
| docs: | |
| - 'docs/**' | |
| - '**/*.md' | |
| - name: Decide matrix scope | |
| id: decide | |
| env: | |
| ALWAYS_FULL: ${{ github.event_name == 'push' || github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' }} | |
| CHANGED_SRC: ${{ steps.changes.outputs.src }} | |
| CHANGED_TESTS: ${{ steps.changes.outputs.tests }} | |
| CHANGED_OS: ${{ steps.changes.outputs.os_sensitive }} | |
| CHANGED_LOCK: ${{ steps.changes.outputs.lockfile }} | |
| CHANGED_CONFIG: ${{ steps.changes.outputs.config }} | |
| CHANGED_CI: ${{ steps.changes.outputs.ci }} | |
| CHANGED_TOOLING: ${{ steps.changes.outputs.tooling }} | |
| shell: bash | |
| run: | | |
| # run-tests: lockfile, config, CI, or tooling changes may affect build/test/security outputs. | |
| if [[ "$ALWAYS_FULL" == "true" || "$CHANGED_SRC" == "true" || "$CHANGED_TESTS" == "true" || "$CHANGED_LOCK" == "true" || "$CHANGED_CONFIG" == "true" || "$CHANGED_CI" == "true" || "$CHANGED_TOOLING" == "true" ]]; then | |
| echo "run-tests=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "run-tests=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Tri-OS: trunk merges, lockfile changes, CI/tooling edits, or fs-sensitive code paths. | |
| # Coverage floor is a no-regression ratchet at the current combined | |
| # coverage (96.7% on green main as of the WPT-corpus fix in PR #2). | |
| # Holding 95 gives ~1.7-point headroom for noise / matrix-shard variation | |
| # without permitting silent regressions. Ratchet upward as new tests land. | |
| # Target: 90%+ sustained; current is comfortably above. | |
| if [[ "$ALWAYS_FULL" == "true" || "$CHANGED_OS" == "true" || "$CHANGED_LOCK" == "true" || "$CHANGED_CI" == "true" || "$CHANGED_TOOLING" == "true" ]]; then | |
| echo 'os-matrix=["ubuntu-latest","windows-latest","macos-latest"]' >> "$GITHUB_OUTPUT" | |
| echo "coverage-floor=95" >> "$GITHUB_OUTPUT" | |
| else | |
| echo 'os-matrix=["ubuntu-latest"]' >> "$GITHUB_OUTPUT" | |
| echo "coverage-floor=95" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Label PR from conventional commit type | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 | |
| with: | |
| script: | | |
| const title = context.payload.pull_request.title; | |
| const match = title.match(/^(\w+)(?:\(.*?\))?!?:/); | |
| if (!match) return; | |
| const typeToLabel = { | |
| feat: 'enhancement', | |
| fix: 'bug', | |
| docs: 'documentation', | |
| perf: 'performance', | |
| ci: 'ci', | |
| refactor: 'refactor', | |
| test: 'testing', | |
| }; | |
| const label = typeToLabel[match[1]]; | |
| if (!label) return; | |
| const { data: current } = await github.rest.issues.listLabelsOnIssue({ | |
| ...context.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| if (current.some(l => l.name === label)) return; | |
| await github.rest.issues.addLabels({ | |
| ...context.repo, | |
| issue_number: context.issue.number, | |
| labels: [label], | |
| }); | |
| # ---------------------------------------------------------------------- | |
| # WPT corpus fetcher | |
| # ---------------------------------------------------------------------- | |
| # The conformance suite (test_wpt*.py) reads pinned JSON/JS fixtures | |
| # from web-platform-tests/wpt. Tests fail-fast if the corpus is absent | |
| # (we explicitly do NOT skip), so every test-running job depends on this. | |
| # | |
| # Two layers of sharing: | |
| # * ``actions/cache`` — across workflow runs. The key is derived from | |
| # the fetch script's content; bumping ``WPT_REF`` rotates the key. | |
| # * ``actions/upload-artifact`` — within one run. Matrix jobs download | |
| # the artifact instead of each cloning their own copy. | |
| # | |
| # Security posture lives in ``scripts/fetch_wpt_corpus.sh`` (pinned SHA, | |
| # HTTPS-only sparse-checkout, post-fetch SHA verify, per-file size cap, | |
| # JSON well-formedness + shape check). ``--verify`` re-checks restored | |
| # caches so a tampered cache cannot enter the matrix. | |
| wpt-corpus: | |
| name: Fetch WPT + polyfill corpora | |
| needs: [plan] | |
| if: needs.plan.outputs.run-tests == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Cache WPT corpus (key bumps when fetch script / pinned SHA changes) | |
| id: wpt-cache | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: reference/wpt | |
| key: wpt-corpus-${{ hashFiles('scripts/fetch_wpt_corpus.sh') }} | |
| - name: Fetch + verify WPT corpus (cache miss) | |
| if: steps.wpt-cache.outputs.cache-hit != 'true' | |
| run: scripts/fetch_wpt_corpus.sh | |
| - name: Re-verify WPT cache (defense in depth) | |
| if: steps.wpt-cache.outputs.cache-hit == 'true' | |
| run: scripts/fetch_wpt_corpus.sh --verify | |
| - name: Cache polyfill corpus (key bumps when fetch script / pinned SHA changes) | |
| id: polyfill-cache | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: reference/polyfill | |
| key: polyfill-corpus-${{ hashFiles('scripts/fetch_polyfill_corpus.sh') }} | |
| - name: Fetch + verify polyfill corpus (cache miss) | |
| if: steps.polyfill-cache.outputs.cache-hit != 'true' | |
| run: scripts/fetch_polyfill_corpus.sh | |
| - name: Re-verify polyfill cache (defense in depth) | |
| if: steps.polyfill-cache.outputs.cache-hit == 'true' | |
| run: scripts/fetch_polyfill_corpus.sh --verify | |
| - name: Upload corpora for matrix jobs | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: wpt-corpus | |
| # Sparse-checkout leaves a populated ``.git/`` (HEAD ref, refs/, | |
| # packed objects, occasional symlinks) — used by the SHA-verify | |
| # steps above but unused by matrix consumers, and a known source | |
| # of digest-mismatch errors under ``download-artifact@v8`` on | |
| # Windows runners (small files + special git entries trip the | |
| # chunked archive digest check, which v8 defaults to ``error``). | |
| # Excluding ``.git`` from the artifact (but not from the cache!) | |
| # keeps the next workflow run's cache-hit fast while making the | |
| # cross-OS download deterministic. | |
| path: | | |
| reference/wpt | |
| reference/polyfill | |
| !reference/wpt/.git | |
| !reference/wpt/.git/** | |
| !reference/polyfill/.git | |
| !reference/polyfill/.git/** | |
| retention-days: 1 | |
| if-no-files-found: error | |
| lint: | |
| name: Lint & type check | |
| needs: [plan] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: uv.lock | |
| python-version: "3.14" | |
| - name: Install project | |
| run: uv sync --frozen --all-groups | |
| - name: ruff check | |
| run: uv run ruff check | |
| - name: ruff format --check | |
| run: uv run ruff format --check | |
| - name: mypy | |
| run: uv run mypy src tests | |
| - name: pyright | |
| run: uv run pyright | |
| - name: ty | |
| run: uv run ty check | |
| - name: codespell | |
| run: uv run codespell src tests docs scripts | |
| - name: validate-pyproject | |
| run: uv run validate-pyproject pyproject.toml | |
| - name: interrogate | |
| run: uv run interrogate src/yarlpattern/ | |
| - name: semgrep | |
| run: uv run semgrep scan --config=auto --quiet --emacs --error src/ | |
| build: | |
| name: Build & inspect package | |
| needs: [lint] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| python-versions: ${{ steps.baipp.outputs.supported_python_classifiers_json_array }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: hynek/build-and-inspect-python-package@fe0a0fb1925ca263d076ca4f2c13e93a6e92a33e # v2.17.0 | |
| id: baipp | |
| test-stable: | |
| name: Test (${{ matrix.os == 'windows-latest' && 'windows' || matrix.os == 'macos-latest' && 'macos' || 'ubuntu' }} / Python ${{ matrix.python-version }}) | |
| needs: [plan, build, wpt-corpus] | |
| if: needs.plan.outputs.run-tests == 'true' | |
| runs-on: ${{ matrix.os }} | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: ${{ fromJson(needs.plan.outputs.os-matrix) }} | |
| python-version: ${{ fromJson(needs.build.outputs.python-versions) }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: Packages | |
| path: dist/ | |
| # structlog: merge sdist tree into the workspace, then remove src/ so | |
| # imports cannot use the in-repo source tree; only the wheel (below) is used. | |
| - name: Unpack sdist and remove local src | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| shopt -s nullglob | |
| tgz=(dist/*.tar.gz) | |
| if [ ${#tgz[@]} -eq 0 ]; then | |
| echo "No sdist (.tar.gz) in dist/ — BAIPP should upload sdist" >&2 | |
| exit 1 | |
| fi | |
| tar -xzf "${tgz[0]}" --strip-components=1 | |
| rm -rf src | |
| # WPT corpus comes from the dedicated wpt-corpus job's artifact — | |
| # one download per workflow run, shared across all 9 matrix shards. | |
| # The conformance tests fail-fast if any fixture is missing. | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: wpt-corpus | |
| # The upload-artifact step strips the common ancestor when given | |
| # multiple paths under one parent (``reference/wpt`` and | |
| # ``reference/polyfill`` → archive root holds ``wpt/...`` and | |
| # ``polyfill/...``). Extract under ``reference/`` so the test | |
| # harness finds the corpora at ``reference/wpt/...`` and | |
| # ``reference/polyfill/...`` where conftest.py looks for them. | |
| path: reference | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: uv.lock | |
| python-version: ${{ matrix.python-version }} | |
| - name: Install deps + wheel | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| shopt -s nullglob | |
| uv sync --frozen --all-groups --no-install-project | |
| whl=(dist/yarlpattern-*.whl) | |
| uv pip install --force-reinstall --no-deps "${whl[0]}" | |
| # --no-sync: without src/, uv would try to build the workspace project; the wheel is already installed. | |
| # structlog: ``coverage run -m pytest`` (not ``pytest --cov``) so parallel data | |
| # files are consistently ``.coverage.*``; upload with that glob + hidden files. | |
| - name: Run tests (coverage) | |
| shell: bash | |
| run: uv run --no-sync coverage run -m pytest | |
| - name: Upload coverage data | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: coverage-data-${{ matrix.os }}-${{ matrix.python-version }} | |
| # structlog: ``path: .coverage.*``; include hidden files. Also upload | |
| # ``.coverage`` if a run produces a single main file (edge cases). | |
| path: | | |
| .coverage | |
| .coverage.* | |
| include-hidden-files: true | |
| if-no-files-found: ignore | |
| coverage: | |
| name: Ensure combined coverage threshold | |
| needs: [plan, test-stable] | |
| if: needs.plan.outputs.run-tests == 'true' && !cancelled() | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: uv.lock | |
| python-version: "3.14" | |
| - name: Install project | |
| run: uv sync --frozen --all-groups | |
| - name: Download coverage data | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: coverage-data-* | |
| merge-multiple: true | |
| - name: Combine and report | |
| env: | |
| COVERAGE_FLOOR: ${{ needs.plan.outputs.coverage-floor }} | |
| run: | | |
| uv run coverage combine | |
| uv run coverage html --skip-covered --skip-empty | |
| uv run coverage report --format=markdown >> "${GITHUB_STEP_SUMMARY}" | |
| uv run coverage report --fail-under="$COVERAGE_FLOOR" | |
| uv run coverage xml -o coverage.xml | |
| - name: Upload HTML report if check failed | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: htmlcov-report | |
| path: htmlcov | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 | |
| with: | |
| files: coverage.xml | |
| name: combined | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| test-prospective: | |
| name: Test (ubuntu / Python 3.15 preview, experimental) | |
| needs: [plan, lint, wpt-corpus] | |
| if: needs.plan.outputs.run-tests == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: wpt-corpus | |
| # The upload-artifact step strips the common ancestor when given | |
| # multiple paths under one parent (``reference/wpt`` and | |
| # ``reference/polyfill`` → archive root holds ``wpt/...`` and | |
| # ``polyfill/...``). Extract under ``reference/`` so the test | |
| # harness finds the corpora at ``reference/wpt/...`` and | |
| # ``reference/polyfill/...`` where conftest.py looks for them. | |
| path: reference | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: uv.lock | |
| cache-suffix: python-3.15-preview-experimental | |
| # uv-managed CPython (python-build-standalone); avoids deadsnakes/Launchpad flakes on GHA. | |
| - name: Install CPython 3.15 preview | |
| run: uv python install 3.15 | |
| # Only the `test` group: `lint` pulls semgrep → pydantic-core (Rust/PyO3) which | |
| # often lags CPython pre-releases; the lint job already runs on 3.14 in test-stable. | |
| - name: Install project | |
| run: uv sync --python 3.15 --no-default-groups --group test | |
| - name: pytest | |
| run: uv run pytest | |
| install-dev: | |
| name: Verify extras install | |
| needs: [build] | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| steps: | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: Packages | |
| path: dist/ | |
| - name: Install wheel with [regex] extra | |
| run: pip install "$(ls dist/yarlpattern-*.whl)[regex]" | |
| - name: Verify import | |
| run: python -c "import yarlpattern; print(yarlpattern.__version__)" | |
| meta: | |
| name: Validate CI config | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: actionlint | |
| run: | | |
| curl -fsSL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash | bash -s | |
| ./actionlint -color | |
| - name: Validate renovate config | |
| run: npx --yes --package renovate -- renovate-config-validator renovate.json | |
| ci-ok: | |
| name: CI green | |
| if: always() | |
| needs: [lint, build, wpt-corpus, test-stable, test-prospective, install-dev, coverage, meta] | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| steps: | |
| - uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 | |
| with: | |
| allowed-failures: test-prospective | |
| allowed-skips: wpt-corpus, test-stable, test-prospective, coverage | |
| jobs: ${{ toJSON(needs) }} |