diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..e4c513d --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# Contributing + +## Development setup + +```bash +uv sync --all-extras --dev +uv run pytest -q +uv run ruff check . +``` + +## Opening a pull request + +1. Apply **exactly one** label before merging: + + | Label | When to use | + |---|---| + | `breaking` | Public API change, backward-incompatible | + | `feature` | New functionality, backward-compatible | + | `fix` | Bug fix | + | `dependencies` | Dependency version update | + | `chore` | CI changes, refactoring, test additions, docs — anything that does not affect the public-facing package. Bypasses the label gate; excluded from the changelog and does not bump the version. | + +2. When merging (squash), write a clear extended description in the merge dialog. That text — not the PR's opening description — becomes the changelog entry for this change. Leave it blank for `chore` PRs. + +## Versioning and releases + +Versions are derived from git tags; there is no version string in any source file. Releases are triggered by a maintainer publishing the standing draft release on the repository's Releases page. There is no automated commit-back to `main`. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 2206756..46ef5eb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,33 +1,66 @@ name: "🐞 Bug Report" -description: Report a bug to help us improve the project -labels: ["bug"] +description: Report a reproducible bug. +type: "Bug" body: - type: checkboxes attributes: - label: Is there an existing issue for this? - description: Please search to see if an issue already exists for the bug you encountered. + label: Pre-flight options: - - label: I have searched the existing issues - required: true + - label: I searched existing issues and this is not a duplicate. + required: true + - type: textarea id: summary attributes: - label: Bug summary - description: 1-2 short sentences describing the bug. + label: Summary + description: One or two sentences describing the bug. validations: required: true + - type: textarea id: reproduction attributes: - label: Code for reproduction - description: Please provide a minimal self-contained example. - placeholder: Your python code here - render: python + label: Reproduction + description: Steps and/or a minimal code example that reproduces the issue. Wrap code in triple backticks. + placeholder: | + 1. Import X and call Y with Z + 2. Observe error + + ```python + # minimal example + ``` validations: required: true + + - type: textarea + id: actual + attributes: + label: Actual behaviour + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behaviour + validations: + required: true + - type: textarea id: logs attributes: - label: Error messages - description: Paste the full error traceback here. + label: Error output + description: Full traceback if applicable. Formatted automatically. render: python-traceback + + - type: textarea + id: system + attributes: + label: System info + description: | + Run this in your environment and paste the output: + + ```shell + python <(curl -s https://raw.githubusercontent.com/AustralianCancerDataNetwork/cava-devops/main/scripts/cava_system_info.py) + ``` + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 9b170b4..a6cc5b1 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - - name: ❓ Discussions - url: https://github.com/AustralianCancerDataNetwork/oa-configurator/discussions - about: Ask questions or start a general discussion. + - name: Discussions + url: https://github.com/orgs/AustralianCancerDataNetwork/discussions + about: Questions and general discussion about the CAVA stack. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index c255c23..7b1d3ad 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,28 +1,32 @@ name: "🚀 Feature Request" -description: Submit a proposal for a new feature -labels: ["enhancement"] +description: Propose a new feature or enhancement. +type: "Feature" body: + - type: checkboxes + attributes: + label: Pre-flight + options: + - label: I searched existing issues and this has not been requested before. + required: true + - type: textarea - id: motivation + id: problem attributes: - label: Motivation - description: Is your feature request related to a problem? (e.g., I'm always frustrated when [...]) + label: Problem or motivation + description: What are you trying to do that you currently cannot? validations: required: true + - type: textarea - id: pitch + id: proposal attributes: - label: Pitch - description: A clear and concise description of what you want to happen. + label: Proposed solution + description: How would you like this to work? validations: required: true + - type: textarea id: alternatives attributes: - label: Alternatives - description: Any alternative solutions or features you've considered. - - type: textarea - id: context - attributes: - label: Additional context - description: Add any other context or screenshots here. + label: Alternatives considered + description: Other approaches you considered and why you ruled them out. diff --git a/.github/ISSUE_TEMPLATE/not_working.yml b/.github/ISSUE_TEMPLATE/not_working.yml index 85d7713..176a64e 100644 --- a/.github/ISSUE_TEMPLATE/not_working.yml +++ b/.github/ISSUE_TEMPLATE/not_working.yml @@ -1,35 +1,33 @@ name: "❗ Something is not working" -description: > - If something is not working as expected but you're not sure if it's a bug, - please follow the instructions in this template. - +description: Something behaves unexpectedly but you are not sure if it is a bug. body: - type: checkboxes attributes: - label: Is there an existing issue for this? - description: Please search to see if an issue already exists for the problem you encountered. + label: Pre-flight options: - - label: I have searched the existing issues - required: true + - label: I searched existing issues for this problem. + required: true - type: textarea id: summary attributes: label: Problem summary - description: Please provide 1-2 short sentences that succinctly describe the problem. + description: 1-2 sentences describing what is not working. validations: required: true - type: textarea id: reproduction attributes: - label: Code for reproduction - description: > - If possible, please provide a [minimum self-contained example](https://stackoverflow.com/help/minimal-reproducible-example). - It will be automatically formatted. - Here's another useful resource: [How To Ask Questions The Smart Way](http://www.catb.org/~esr/faqs/smart-questions.html). - placeholder: Your code here - render: python + label: Reproduction + description: Steps and/or a minimal code example. Wrap code in triple backticks. + placeholder: | + 1. Import X and call Y with Z + 2. Observe unexpected behaviour + + ```python + # minimal example + ``` validations: required: true @@ -37,7 +35,13 @@ body: id: actual attributes: label: Actual outcome - description: Please describe the outcome of running the above code. + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected outcome validations: required: true @@ -45,19 +49,17 @@ body: id: logs attributes: label: Error messages - description: > - Please copy and paste any relevant log output. - Paste the full error traceback, not just the last error. - It will be automatically formatted. - placeholder: Your errors here + description: Full traceback if applicable. Formatted automatically. render: python-traceback - type: textarea - id: expected + id: system attributes: - label: Expected outcome - description: > - Please provide a description (or visual example) of the expected outcome - from the code snippet. - validations: - required: true + label: System info + description: | + Run this in your environment and paste the output: + + ```shell + python <(curl -s https://raw.githubusercontent.com/AustralianCancerDataNetwork/cava-devops/main/scripts/cava_system_info.py) + ``` + render: shell diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..159dda2 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ +## Summary + + + +## Checklist + +- [ ] Applied exactly one label (`breaking`, `feature`, `fix`, `dependencies`, or `chore`) +- [ ] Tests pass locally (`uv run pytest -q`) +- [ ] Lint passes (`uv run ruff check .`) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..259b28c --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,31 @@ +name-template: 'v$RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' +commitish: main + +categories: + - title: Breaking Changes + labels: ['breaking'] + - title: Features + labels: ['feature'] + - title: Fixes + labels: ['fix'] + - title: Dependencies + labels: ['dependencies'] + +template: | + $CHANGES + +change-template: '- **$TITLE** (#$NUMBER) @$AUTHOR' + +version-resolver: + major: + labels: ['breaking'] + minor: + labels: ['feature'] + patch: + labels: ['fix', 'dependencies'] + default: patch + +exclude-labels: ['chore'] + +autolabeler: [] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1cf941a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,10 @@ +name: CI +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened, labeled, unlabeled] +jobs: + label-gate: + uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/label-gate.yml@main + build-test: + uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test.yml@main diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c1af5dc..09bfde3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,41 +1,10 @@ name: Deploy Docs - on: - workflow_run: - workflows: ["Release"] - types: - - completed + push: + tags: ['v*'] workflow_dispatch: - permissions: contents: write - jobs: deploy: - if: >- - github.event_name == 'workflow_dispatch' || - (github.event.workflow_run.conclusion == 'success' && - github.event.workflow_run.event == 'push' && - github.event.workflow_run.head_branch == 'main') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Install uv - uses: astral-sh/setup-uv@v7 - with: - enable-cache: true - python-version: "3.12" - - - name: Configure Git Credentials - run: | - git config user.name github-actions[bot] - git config user.email 41898282+github-actions[bot]@users.noreply.github.com - - - name: Install dependencies - run: uv sync --extra dev - - - name: Deploy to GitHub Pages - run: uv run mkdocs gh-deploy --force + uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/deploy-docs.yml@main diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml deleted file mode 100644 index e3acbb6..0000000 --- a/.github/workflows/lint-pr.yml +++ /dev/null @@ -1,17 +0,0 @@ -# Lints pull request titles to enforce semantic-versioning commit style -name: "Lint PR" - -on: - pull_request: - types: - - opened - - edited - - synchronize - -jobs: - main: - runs-on: ubuntu-latest - steps: - - uses: amannn/action-semantic-pull-request@v5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml new file mode 100644 index 0000000..0c7cb55 --- /dev/null +++ b/.github/workflows/merge.yml @@ -0,0 +1,12 @@ +name: Release Update +on: + pull_request: + types: [closed] + branches: [main] +permissions: + contents: write +jobs: + draft: + if: github.event.pull_request.merged == true + uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/release-drafter.yml@main + secrets: inherit diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..7814a2a --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,21 @@ +name: Publish +on: + push: + tags: ['v*'] +jobs: + build: + uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/publish.yml@main + publish: + # Inline: PyPI OIDC checks job_workflow_ref, which must point to this file. + # Moving pypa/gh-action-pypi-publish into cava-devops would break the trusted publisher. + needs: build + runs-on: ubuntu-latest + permissions: + id-token: write + environment: + name: pypi + url: https://pypi.org/p/oa-configurator + steps: + - uses: actions/download-artifact@v4 + with: { name: dist, path: dist/ } + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index cd61a5e..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Release - -on: - push: - branches: - - main - pull_request: - branches: - - main - workflow_dispatch: - inputs: - force_publish: - description: 'Force publish to PyPI without semantic-release (use after a failed release)' - type: boolean - default: false - -jobs: - test: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.12", "3.13"] - - steps: - - uses: actions/checkout@v5 - - - name: Install uv - uses: astral-sh/setup-uv@v7 - with: - enable-cache: true - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: uv sync --extra dev --extra postgres - - - name: Lint - run: uv run ruff check src/ tests/ - - - name: Type check - run: uv run mypy src/ - - - name: Run tests - run: uv run pytest tests/ -v - - release: - needs: test - if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' - runs-on: ubuntu-latest - concurrency: release - permissions: - id-token: write - contents: write - - environment: - name: pypi - url: https://pypi.org/p/oa-configurator - - steps: - - name: Checkout - uses: actions/checkout@v5 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Install uv - uses: astral-sh/setup-uv@v7 - with: - python-version: "3.12" - enable-cache: true - - - name: Semantic Release - id: semantic - uses: cycjimmy/semantic-release-action@v6 - with: - extra_plugins: | - @semantic-release/changelog - @semantic-release/git - @semantic-release/exec - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and publish to PyPI - if: steps.semantic.outputs.new_release_published == 'true' || inputs.force_publish == true - run: | - uv build - uv publish diff --git a/.gitignore b/.gitignore index e8c8c3c..9ff71e4 100644 --- a/.gitignore +++ b/.gitignore @@ -201,7 +201,7 @@ cython_debug/ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore # and can be added to the global gitignore or merged into this file. However, if you prefer, # you could uncomment the following to ignore the entire vscode folder -# .vscode/ +.vscode/ # Temporary file for partial code execution tempCodeRunnerFile.py diff --git a/.releaserc.json b/.releaserc.json deleted file mode 100644 index 5e35738..0000000 --- a/.releaserc.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "branches": ["main"], - "plugins": [ - "@semantic-release/commit-analyzer", - "@semantic-release/release-notes-generator", - "@semantic-release/changelog", - [ - "@semantic-release/exec", - { - "prepareCmd": "sed -i 's/^version = \".*\"/version = \"${nextRelease.version}\"/' pyproject.toml && uv lock" - } - ], - [ - "@semantic-release/git", - { - "assets": ["CHANGELOG.md", "pyproject.toml", "uv.lock"], - "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" - } - ], - "@semantic-release/github" - ] -} diff --git a/CHANGELOG.md b/CHANGELOG.md index ae8796f..0a50c1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +> [!NOTE] +> This file is no longer maintained. Release history from this point forward is in [GitHub Releases](https://github.com/AustralianCancerDataNetwork/OA_Configurator/releases). + ## [0.1.2](https://github.com/AustralianCancerDataNetwork/oa-configurator/compare/v0.1.1...v0.1.2) (2026-06-24) diff --git a/pyproject.toml b/pyproject.toml index 1c6c915..af964e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oa-configurator" -version = "0.1.2" +dynamic = ["version"] description = "Prototype shared configuration layer for the OMOP-oriented stack." readme = "README.md" requires-python = ">=3.12" @@ -19,7 +19,7 @@ dev = [ "requests>=2.33.0", "pytest>=9.0.3", "pytest-cov>=4.0", - "mypy>=1.8", + "ty>=0.0.59", "ruff>=0.4", "mkdocs-material>=9.7.1", "mkdocstrings-python>=2.0.1", @@ -38,8 +38,15 @@ omop-config = "oa_configurator.cli:app" oa_configurator = "oa_configurator.pytest_plugin" [build-system] -requires = ["hatchling"] +requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" +[tool.hatch.version] +source = "vcs" +raw-options = { tag_regex = '^v?(?P[0-9]+\.[0-9]+\.[0-9]+)$' } + [tool.hatch.build.targets.wheel] packages = ["src/oa_configurator"] + +[tool.uv] +cache-keys = [{ file = "pyproject.toml" }, { git = { commit = true, tags = true } }] diff --git a/src/oa_configurator/cli.py b/src/oa_configurator/cli.py index 3eeab0e..1899467 100644 --- a/src/oa_configurator/cli.py +++ b/src/oa_configurator/cli.py @@ -256,7 +256,7 @@ def __init__(self, **kwargs): def list_commands(self, ctx): return sorted(ep.name for ep in entry_points(group="omop.config")) - def get_command(self, ctx, cmd_name): # type: ignore + def get_command(self, ctx, cmd_name): eps = {ep.name: ep for ep in entry_points(group="omop.config")} ep = eps.get(cmd_name) return _build_package_command(cmd_name, ep.load()) if ep else None @@ -684,7 +684,7 @@ def export_env( console.print(f"[green]✓[/green] Wrote [dim]{env_path}[/dim]") -@app.command(name="configure", cls=_DynamicConfigureGroup) # type: ignore[arg-type] +@app.command(name="configure", cls=_DynamicConfigureGroup) # ty: ignore[invalid-argument-type] def configure(ctx: typer.Context) -> None: r"""Configure a package's \[tools.] section. diff --git a/src/oa_configurator/cli_fields.py b/src/oa_configurator/cli_fields.py index 943d6da..6700c76 100644 --- a/src/oa_configurator/cli_fields.py +++ b/src/oa_configurator/cli_fields.py @@ -225,7 +225,7 @@ def resolve_fields( """ result: dict[str, str | None] = {} for f in fields: - default = f.default(spec) if callable(f.default) else f.default + default = f.default(spec) if callable(f.default) else f.default # ty: ignore[call-top-callable] is_required = not callable(f.default) and not f.nullable and not f.default value = resolve_field_fn(f.name, prompt_label=f.label, default_value=default, required=is_required, hide_input=f.hide_input) result[f.name] = (value or None) if f.nullable else value diff --git a/src/oa_configurator/pytest_plugin.py b/src/oa_configurator/pytest_plugin.py index 576ea99..c24416b 100644 --- a/src/oa_configurator/pytest_plugin.py +++ b/src/oa_configurator/pytest_plugin.py @@ -44,13 +44,13 @@ def _pg_ident(name: str) -> object: def _pg_lit(value: str) -> object: from psycopg.sql import Literal - return Literal(value) # type: ignore[arg-type] + return Literal(value) def _pg_ddl(template: str, *parts: object) -> str: """Build a safe DDL string using psycopg.sql quoting (lazy import).""" from psycopg.sql import SQL - return SQL(template).format(*parts).as_string(None) # type: ignore[attr-defined] + return SQL(template).format(*parts).as_string(None) # ty: ignore[invalid-argument-type] # ---------------------------------------------------------------------------