public/tidy3d/python-client-tests #4601
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: "public/tidy3d/python-client-tests" | |
| on: | |
| merge_group: | |
| workflow_dispatch: | |
| inputs: | |
| remote_tests: | |
| description: 'remote-tests' | |
| type: boolean | |
| default: true | |
| local_tests: | |
| description: 'local-tests' | |
| type: boolean | |
| default: false | |
| cli_tests: | |
| description: 'Run develop-cli tests' | |
| type: boolean | |
| default: false | |
| submodule_tests: | |
| description: 'Run submodule tests' | |
| type: boolean | |
| default: false | |
| version_match_tests: | |
| description: 'Run version consistency checks' | |
| type: boolean | |
| default: false | |
| release_tag: | |
| description: 'Release Tag (v2.10.0, v2.10.0rc1)' | |
| required: false | |
| type: string | |
| default: '' | |
| workflow_call: | |
| inputs: | |
| remote_tests: | |
| description: 'remote-tests' | |
| type: boolean | |
| required: false | |
| default: true | |
| local_tests: | |
| description: 'local-tests' | |
| type: boolean | |
| required: false | |
| default: true | |
| cli_tests: | |
| description: 'Run develop-cli tests' | |
| type: boolean | |
| required: false | |
| default: false | |
| submodule_tests: | |
| description: 'Run submodule tests' | |
| type: boolean | |
| required: false | |
| default: false | |
| version_match_tests: | |
| description: 'Run version consistency checks' | |
| type: boolean | |
| required: false | |
| default: false | |
| release_tag: | |
| description: 'Release Tag (v2.10.0, v2.10.0rc1)' | |
| required: false | |
| type: string | |
| default: '' | |
| outputs: | |
| workflow_success: | |
| description: 'Overall test workflow success status' | |
| value: ${{ jobs.workflow-validation.result == 'success' }} | |
| pull_request: | |
| branches: | |
| - latest | |
| - develop | |
| - 'pre/*' | |
| types: ['opened', 'reopened', 'synchronize', 'ready_for_review', 'edited'] | |
| pull_request_review: | |
| types: [submitted] | |
| permissions: | |
| contents: read | |
| jobs: | |
| determine-test-scope: | |
| runs-on: ubuntu-latest | |
| if: | | |
| github.event.pull_request.draft == false || | |
| github.ref == 'refs/heads/develop' || | |
| github.event_name == 'workflow_dispatch' | |
| outputs: | |
| code_quality_tests: ${{ steps.determine-test-type.outputs.code_quality_tests }} | |
| pr_review_tests: ${{ steps.determine-test-type.outputs.pr_review_tests }} | |
| local_tests: ${{ steps.determine-test-type.outputs.local_tests }} | |
| remote_tests: ${{ steps.determine-test-type.outputs.remote_tests }} | |
| cli_tests: ${{ steps.determine-test-type.outputs.cli_tests }} | |
| submodule_tests: ${{ steps.determine-test-type.outputs.submodule_tests }} | |
| version_match_tests: ${{ steps.determine-test-type.outputs.version_match_tests }} | |
| pr_approval_state: ${{ steps.approval.outputs.approved }} | |
| steps: | |
| - name: check-current-approval-status | |
| id: approval | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const {owner, repo} = context.repo; | |
| const number = context.payload.pull_request.number; | |
| // Fetch all reviews across all pages | |
| const allReviews = await github.paginate(github.rest.pulls.listReviews, { | |
| owner, | |
| repo, | |
| pull_number: number, | |
| per_page: 100, | |
| }); | |
| core.info(`Found ${allReviews.length} total review events.`); | |
| // Process the array to get only the latest review per user | |
| const latestByUser = {}; | |
| allReviews.forEach(review => { | |
| if (review.state !== 'COMMENTED') { | |
| latestByUser[review.user.id] = review; | |
| } | |
| }); | |
| const latestStates = Object.values(latestByUser).map(review => review.state); | |
| core.info(`Final review states from unique reviewers: [${latestStates.join(', ')}]`); | |
| // The rest of the logic remains the same | |
| const isBlocked = latestStates.includes('CHANGES_REQUESTED'); | |
| const isApproved = latestStates.includes('APPROVED'); | |
| const finalStatus = isApproved && !isBlocked; | |
| core.info(`🏁 Final determined approval status is: ${finalStatus}`); | |
| core.setOutput('approved', finalStatus ? 'true' : 'false'); | |
| - name: determine-test-type | |
| id: determine-test-type | |
| env: | |
| DRAFT_STATE: ${{ github.event.pull_request.draft }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| REVIEW_STATE: ${{ github.event.review.state }} | |
| REF: ${{ github.ref }} | |
| INPUT_LOCAL: ${{ github.event.inputs.local_tests || inputs.local_tests }} | |
| INPUT_REMOTE: ${{ github.event.inputs.remote_tests || inputs.remote_tests }} | |
| INPUT_CLI: ${{ github.event.inputs.cli_tests || inputs.cli_tests }} | |
| INPUT_SUBMODULE: ${{ github.event.inputs.submodule_tests || inputs.submodule_tests }} | |
| INPUT_VERSION_MATCH: ${{ github.event.inputs.version_match_tests || inputs.version_match_tests }} | |
| APPROVED: ${{ steps.approval.outputs.approved }} | |
| run: | | |
| echo "Event: $EVENT_NAME" | |
| echo "Draft: $DRAFT_STATE" | |
| echo "Review State: $REVIEW_STATE" | |
| echo "Git REF: $REF" | |
| echo "Input local: $INPUT_LOCAL" | |
| echo "Input remote: $INPUT_REMOTE" | |
| echo "Input cli: $INPUT_CLI" | |
| echo "Input submodule: $INPUT_SUBMODULE" | |
| echo "Input version_match: $INPUT_VERSION_MATCH" | |
| echo "Approved: $APPROVED" | |
| remote_tests=false | |
| local_tests=false | |
| cli_tests=false | |
| submodule_tests=false | |
| version_match_tests=false | |
| code_quality_tests=false | |
| pr_review_tests=false | |
| # Workflow_dispatch input override | |
| if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then | |
| code_quality_tests=true | |
| # Each option is self contained | |
| if [[ "$INPUT_REMOTE" == "true" ]]; then | |
| remote_tests=true | |
| fi | |
| if [[ "$INPUT_LOCAL" == "true" ]]; then | |
| local_tests=true | |
| fi | |
| if [[ "$INPUT_CLI" == "true" ]]; then | |
| cli_tests=true | |
| fi | |
| if [[ "$INPUT_SUBMODULE" == "true" ]]; then | |
| submodule_tests=true | |
| fi | |
| if [[ "$INPUT_VERSION_MATCH" == "true" ]]; then | |
| version_match_tests=true | |
| fi | |
| fi | |
| # All PRs that have been triggered need local tests (remote reserved for merge queue/manual) | |
| if [[ "$EVENT_NAME" == "pull_request" ]]; then | |
| local_tests=true | |
| code_quality_tests=true | |
| pr_review_tests=true | |
| fi | |
| if [[ "$EVENT_NAME" == "merge_group" ]]; then | |
| local_tests=true | |
| remote_tests=true | |
| code_quality_tests=true | |
| fi | |
| # If triggered by PR review and approved | |
| if [[ "$EVENT_NAME" == "pull_request_review" ]]; then | |
| if [[ "$REVIEW_STATE" == "approved" ]]; then | |
| code_quality_tests=true | |
| pr_review_tests=true | |
| local_tests=true | |
| remote_tests=true | |
| fi | |
| fi | |
| # If it's a push to develop | |
| if [[ "$EVENT_NAME" == "push" && "$REF" == "refs/heads/develop" ]]; then | |
| local_tests=true | |
| remote_tests=true | |
| code_quality_tests=true | |
| fi | |
| echo "local_tests=$local_tests" >> $GITHUB_OUTPUT | |
| echo "remote_tests=$remote_tests" >> $GITHUB_OUTPUT | |
| echo "cli_tests=$cli_tests" >> $GITHUB_OUTPUT | |
| echo "submodule_tests=$submodule_tests" >> $GITHUB_OUTPUT | |
| echo "version_match_tests=$version_match_tests" >> $GITHUB_OUTPUT | |
| echo "code_quality_tests=$code_quality_tests" >> $GITHUB_OUTPUT | |
| echo "pr_review_tests=$pr_review_tests" >> $GITHUB_OUTPUT | |
| echo "code_quality_tests=$code_quality_tests" | |
| echo "pr_review_tests=$pr_review_tests" | |
| echo "local_tests=$local_tests" | |
| echo "remote_tests=$remote_tests" | |
| echo "cli_tests=$cli_tests" | |
| echo "submodule_tests=$submodule_tests" | |
| echo "version_match_tests=$version_match_tests" | |
| lint: | |
| needs: determine-test-scope | |
| if: needs.determine-test-scope.outputs.code_quality_tests == 'true' | |
| name: verify-linting | |
| runs-on: ubuntu-latest | |
| container: ghcr.io/astral-sh/uv:debian | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| submodules: false | |
| persist-credentials: false | |
| - uses: astral-sh/ruff-action@57714a7c8a2e59f32539362ba31877a1957dded1 # v3.5.1 | |
| with: | |
| version: 0.11.11 | |
| - name: Run ruff format | |
| run: ruff format --check --diff | |
| - name: Run ruff check | |
| run: ruff check tidy3d | |
| mypy: | |
| name: static-type-checks (mypy) | |
| needs: determine-test-scope | |
| if: needs.determine-test-scope.outputs.code_quality_tests == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| submodules: false | |
| persist-credentials: false | |
| - name: set-python-3.10 | |
| uses: actions/setup-python@v4 | |
| with: | |
| python-version: '3.10' | |
| - name: Install mypy | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install "mypy==1.13.0" | |
| - name: Run mypy | |
| run: | | |
| mypy --config-file=pyproject.toml | |
| zizmor: | |
| name: Run zizmor 🌈 | |
| runs-on: ubuntu-latest | |
| needs: determine-test-scope | |
| if: needs.determine-test-scope.outputs.code_quality_tests == 'true' | |
| permissions: | |
| security-events: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | |
| with: | |
| persist-credentials: false | |
| - name: Install the latest version of uv | |
| uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0 | |
| - name: Run zizmor 🌈 | |
| run: uvx zizmor .github/workflows/*.y* --format=sarif . > results.sarif | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Upload SARIF file | |
| uses: github/codeql-action/upload-sarif@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 | |
| with: | |
| sarif_file: results.sarif | |
| category: zizmor | |
| - name: run zizmor directly # this gets a success or fail result | |
| run: uvx zizmor .github/workflows/*.y* | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| lint-branch-name: | |
| needs: determine-test-scope | |
| runs-on: ubuntu-latest | |
| if: needs.determine-test-scope.outputs.pr_review_tests == 'true' | |
| name: lint-branch-name | |
| env: | |
| PR_TITLE: ${{ github.event.pull_request.title }} | |
| PR_BRANCH: ${{ github.event.pull_request.head.ref }} | |
| steps: | |
| - name: extract-branch-name | |
| id: extract-branch-name | |
| run: | | |
| BRANCH_NAME="${GITHUB_HEAD_REF}" | |
| if [[ -z "$BRANCH_NAME" && -n "$PR_BRANCH" ]]; then | |
| BRANCH_NAME="$PR_BRANCH" | |
| echo "(fallback) Using PR head branch name: $BRANCH_NAME" | |
| fi | |
| if [[ -z "$BRANCH_NAME" ]]; then | |
| BRANCH_NAME="${GITHUB_REF_NAME:-${GITHUB_REF#refs/heads/}}" | |
| echo "(fallback) Using ref-derived branch name: $BRANCH_NAME" | |
| fi | |
| echo "Branch name: $BRANCH_NAME" | |
| echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT | |
| - name: enforce-jira-key | |
| id: enforce-jira-key | |
| env: | |
| STEPS_EXTRACT_BRANCH_NAME_OUTPUTS_BRANCH_NAME: ${{ steps.extract-branch-name.outputs.branch_name }} | |
| run: | | |
| BRANCH_NAME="${STEPS_EXTRACT_BRANCH_NAME_OUTPUTS_BRANCH_NAME}" | |
| echo $BRANCH_NAME | |
| # Allow only Jira keys from known projects, even if the branch has an author prefix | |
| ALLOWED_JIRA_PROJECTS=("FXC" "SCEM" "SCRF") | |
| JIRA_PROJECT_PATTERN=$(IFS='|'; echo "${ALLOWED_JIRA_PROJECTS[*]}") | |
| JIRA_PATTERN="(${JIRA_PROJECT_PATTERN})-[0-9]+" | |
| # List of exempt prefixes (case-insensitive) | |
| EXEMPT_PREFIXES=("chore" "hotfix" "daily-chore") | |
| # Convert branch name to lowercase for comparison | |
| BRANCH_LOWER=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]') | |
| # Check if branch starts with any exempt prefix | |
| for prefix in "${EXEMPT_PREFIXES[@]}"; do | |
| if [[ "$BRANCH_LOWER" == $prefix* ]]; then | |
| echo "ℹ️ Branch starts with '$prefix' - Jira key not required" | |
| exit 0 | |
| fi | |
| done | |
| if [[ "$BRANCH_NAME" =~ $JIRA_PATTERN ]]; then | |
| echo "✅ Jira key found in branch name: ${BASH_REMATCH[0]}" | |
| else | |
| echo "❌ No Jira key found in branch name, checking PR name as fallback" | |
| if [[ "$PR_TITLE" =~ $JIRA_PATTERN ]]; then | |
| echo "✅ Jira key found in PR-title: ${BASH_REMATCH[0]}" | |
| else | |
| echo "❌ No Jira key found in branch name and PR title" | |
| echo "ℹ️ Expected Jira key prefixes: ${ALLOWED_JIRA_PROJECTS[*]}" | |
| exit 1 | |
| fi | |
| fi | |
| lint-commit-messages: | |
| needs: determine-test-scope | |
| runs-on: ubuntu-latest | |
| if: needs.determine-test-scope.outputs.code_quality_tests == 'true' | |
| name: lint-commit-messages | |
| steps: | |
| - name: Check out source code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # fetch all commits in the PR | |
| persist-credentials: false | |
| - name: Setup node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: lts/* | |
| - name: Install commitlint | |
| run: npm install -D @commitlint/cli @commitlint/config-conventional | |
| - name: Print versions | |
| run: | | |
| git --version | |
| node --version | |
| npm --version | |
| npx commitlint --version | |
| - name: Check commit messages (merge_group) | |
| if: github.event_name == 'merge_group' | |
| env: | |
| GITHUB_EVENT_MERGE_GROUP_HEAD_SHA: ${{ github.event.merge_group.head_sha }} | |
| run: | | |
| # For merge groups, check the commits being merged | |
| npx commitlint --from ${{ github.event.merge_group.base_sha }} --to ${GITHUB_EVENT_MERGE_GROUP_HEAD_SHA} --verbose || { | |
| echo "Commit message linting failed; please follow the conventional commits format at https://www.conventionalcommits.org/" | |
| exit 1 | |
| } | |
| verify-schema-change: | |
| name: verify-schema-change | |
| needs: determine-test-scope | |
| if: needs.determine-test-scope.outputs.code_quality_tests == 'true' | |
| runs-on: ubuntu-latest | |
| container: ghcr.io/astral-sh/uv:debian | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - name: checkout-branch | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.pull_request.head.ref }} | |
| repository: ${{ github.event.pull_request.head.repo.full_name }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: git-config | |
| run: | | |
| cd $GITHUB_WORKSPACE | |
| git config --global --add safe.directory $GITHUB_WORKSPACE | |
| - name: install-depedencies | |
| run: | | |
| uv venv $GITHUB_WORKSPACE/.venv -p 3.11 | |
| source $GITHUB_WORKSPACE/.venv/bin/activate | |
| uv pip install -e "$GITHUB_WORKSPACE" | |
| - name: get-tidy3d-version | |
| id: get-version | |
| run: | | |
| source $GITHUB_WORKSPACE/.venv/bin/activate | |
| version=$(python -c "import tidy3d; print(tidy3d.__version__)") | |
| echo "tidy3d version is $version" | |
| echo "version=$version" >> $GITHUB_OUTPUT | |
| - name: verify-committed-schema | |
| run: | | |
| set -euo pipefail | |
| echo "Regenerating docs-free canonical schemas into repo schemas/ ..." | |
| source $GITHUB_WORKSPACE/.venv/bin/activate | |
| python $GITHUB_WORKSPACE/scripts/regenerate_schema.py | |
| echo "Verifying committed schemas match generated output..." | |
| if ! git diff --name-status --exit-code -- schemas; then | |
| echo "❌ Committed schemas are not up-to-date. See diff above." | |
| exit 1 | |
| fi | |
| echo "✅ Committed schemas are up-to-date." | |
| - name: run-schema-diff | |
| id: schema-diff | |
| env: | |
| GITHUB_EVENT_PULL_REQUEST_BASE_REPO_FULL_NAME: ${{ github.event.pull_request.base.repo.full_name }} | |
| GITHUB_EVENT_PULL_REQUEST_BASE_REF: ${{ github.event.pull_request.base.ref }} | |
| run: | | |
| set -euo pipefail | |
| cd "$GITHUB_WORKSPACE" | |
| # Determine base repo/ref for PRs; default to current repo and 'develop' otherwise | |
| BASE_REPO="${GITHUB_EVENT_PULL_REQUEST_BASE_REPO_FULL_NAME}" | |
| BASE_REF="${GITHUB_EVENT_PULL_REQUEST_BASE_REF}" | |
| if [ -z "$BASE_REPO" ]; then | |
| BASE_REPO="${{ github.repository }}" | |
| fi | |
| if [ -z "$BASE_REF" ]; then | |
| BASE_REF="develop" | |
| fi | |
| echo "Fetching base branch $BASE_REPO@$BASE_REF (shallow)..." | |
| git remote add upstream "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${BASE_REPO}.git" || true | |
| git fetch --no-tags --prune --depth=1 upstream "+refs/heads/${BASE_REF}:refs/remotes/upstream/${BASE_REF}" | |
| # Name-status diff between base and head limited to schemas/ | |
| DIFF_OUTPUT=$(git diff --name-status "upstream/${BASE_REF}...HEAD" -- schemas || true) | |
| if [ -z "$DIFF_OUTPUT" ]; then | |
| echo "✅ No schema changes relative to ${BASE_REF}." | |
| echo "changed=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "Schema changes detected relative to ${BASE_REF}." | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| # Summarize changes | |
| { | |
| echo "### Schema Change Summary" | |
| echo "| Status | File |" | |
| echo "|:---:|:---|" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| while IFS=$'\t' read -r status file; do | |
| # Map short status to human-friendly | |
| case "$status" in | |
| A|AM) label="Added 🟢" ;; | |
| M|MM) label="Modified 🟡" ;; | |
| D) label="Removed 🔴" ;; | |
| R*) label="Renamed 🟠" ;; | |
| *) label="$status" ;; | |
| esac | |
| echo "| $label | \`$file\` |" >> "$GITHUB_STEP_SUMMARY" | |
| done <<< "$DIFF_OUTPUT" | |
| - name: verify-allowed-changes | |
| if: steps.schema-diff.outputs.changed == 'true' | |
| env: | |
| STEPS_GET_VERSION_OUTPUTS_VERSION: ${{ steps.get-version.outputs.version }} | |
| run: | | |
| set -e | |
| version="${STEPS_GET_VERSION_OUTPUTS_VERSION}" | |
| if [[ "$version" == *rc* ]]; then | |
| echo "✅ Passing: Schema changed on a release candidate version ($version), which is permitted." | |
| else | |
| echo "❌ Failing: Schema changed on a non-rc release version ($version)." | |
| exit 1 | |
| fi | |
| local-tests: | |
| # Run on open PRs OR when manually triggered with local_tests=true | |
| needs: determine-test-scope | |
| if: needs.determine-test-scope.outputs.local_tests == 'true' | |
| name: python-${{ matrix.python-version }}-self-hosted-runner | |
| runs-on: [ slurm-runner, 4xcpu, container=ghcr.io/astral-sh/uv:debian ] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}-${{ github.event_name }}-${{ matrix.python-version }}-local | |
| cancel-in-progress: true | |
| strategy: | |
| matrix: | |
| python-version: ['3.10', '3.13'] | |
| defaults: | |
| run: | |
| shell: bash | |
| env: | |
| PIP_ONLY_BINARY: gdstk | |
| MPLBACKEND: agg | |
| RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || inputs.release_tag }} | |
| permissions: | |
| pull-requests: write | |
| steps: | |
| - name: checkout-head | |
| if: ${{ !env.RELEASE_TAG }} | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| submodules: false | |
| persist-credentials: false | |
| - name: checkout-tag | |
| if: ${{ env.RELEASE_TAG }} | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: refs/tags/${{ env.RELEASE_TAG }} | |
| fetch-depth: 0 | |
| submodules: false | |
| persist-credentials: false | |
| - name: install-project | |
| env: | |
| PYTHON_VERSION: ${{ matrix.python-version }} | |
| run: | | |
| if [ -f /.dockerenv ]; then | |
| echo "Running inside a Docker container (detected via /.dockerenv)" | |
| else | |
| echo "Not running inside a Docker container (/.dockerenv not found)" | |
| fi | |
| uv venv -p $PYTHON_VERSION ${GITHUB_WORKSPACE}/.venv | |
| source ${GITHUB_WORKSPACE}/.venv/bin/activate | |
| which python | |
| which uv | |
| python --version | |
| uv pip list | |
| uv pip install gdstk --only-binary gdstk | |
| uv pip install -e ".[dev]" | |
| echo "Testing vtk is correctly installed." | |
| python -c "import vtk" | |
| - name: run-tests-coverage | |
| env: | |
| PYTHONUNBUFFERED: "1" | |
| run: | | |
| source ${GITHUB_WORKSPACE}/.venv/bin/activate | |
| # pytest --cov=tidy3d -rF --tb=short tests/_test_data/_test_datasets_no_vtk.py | |
| pytest --cov=tidy3d -rF --tb=short tests | |
| coverage report -m | |
| coverage xml -o ${GITHUB_WORKSPACE}/coverage.xml | |
| TOTAL_COVERAGE=$(coverage report --format=total) | |
| echo "total=$TOTAL_COVERAGE" >> "$GITHUB_ENV" | |
| echo "### Total coverage: ${TOTAL_COVERAGE}%" | |
| - name: diff-coverage-report | |
| if: >- | |
| matrix.python-version == '3.13' && | |
| github.event_name == 'pull_request' && | |
| !contains(github.event.pull_request.labels.*.name, 'ignore_diff_coverage') | |
| env: | |
| GITHUB_EVENT_PULL_REQUEST_BASE_REF: ${{ github.event.pull_request.base.ref }} | |
| run: | | |
| source ${GITHUB_WORKSPACE}/.venv/bin/activate | |
| git config --global --add safe.directory ${GITHUB_WORKSPACE} | |
| diff-cover ${GITHUB_WORKSPACE}/coverage.xml \ | |
| --compare-branch origin/${GITHUB_EVENT_PULL_REQUEST_BASE_REF} \ | |
| --format markdown:diff-coverage.md | |
| - uses: actions/github-script@v7 | |
| if: >- | |
| matrix.python-version == '3.13' && | |
| github.event_name == 'pull_request' && | |
| !contains(github.event.pull_request.labels.*.name, 'ignore_diff_coverage') && | |
| github.event.pull_request.head.repo.fork == false | |
| with: | |
| result-encoding: string | |
| script: | | |
| const fs = require('fs'); | |
| const marker = '<!-- diff-cover-report -->'; | |
| const body = fs.readFileSync('diff-coverage.md','utf8'); | |
| const report = `${marker}\n${body}`; | |
| const {data:comments}=await github.rest.issues.listComments({ | |
| owner:context.repo.owner, | |
| repo:context.repo.repo, | |
| issue_number:context.issue.number, | |
| }); | |
| const existing = comments.find(c=>c.body.startsWith(marker)); | |
| if(existing) { | |
| await github.rest.issues.updateComment({ | |
| owner:context.repo.owner, | |
| repo:context.repo.repo, | |
| comment_id:existing.id, | |
| body:report, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner:context.repo.owner, | |
| repo:context.repo.repo, | |
| issue_number:context.issue.number, | |
| body:report, | |
| }); | |
| } | |
| remote-tests: | |
| # Run tests on a push event OR a workflow dispatch with remote_tests | |
| needs: determine-test-scope | |
| if: needs.determine-test-scope.outputs.remote_tests == 'true' | |
| name: python-${{ matrix.python-version }}-${{ matrix.platform }} | |
| runs-on: ${{ matrix.platform }} | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}-${{ github.event_name }}-${{ matrix.platform }}-${{ matrix.python-version }}-remote | |
| cancel-in-progress: true | |
| strategy: | |
| matrix: | |
| python-version: ['3.10', '3.11', '3.12', '3.13'] | |
| platform: [windows-latest, ubuntu-latest, macos-latest] | |
| defaults: | |
| run: | |
| shell: bash | |
| env: | |
| PIP_ONLY_BINARY: gdstk | |
| MPLBACKEND: agg | |
| RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || inputs.release_tag }} | |
| steps: | |
| - name: checkout-head | |
| if: ${{ !env.RELEASE_TAG }} | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| submodules: false | |
| persist-credentials: false | |
| - name: checkout-tag | |
| if: ${{ env.RELEASE_TAG }} | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: refs/tags/${{ env.RELEASE_TAG }} | |
| fetch-depth: 1 | |
| submodules: false | |
| persist-credentials: false | |
| - name: install-poetry | |
| uses: snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a # v1.4.1 | |
| with: | |
| version: 2.1.1 | |
| virtualenvs-create: true | |
| virtualenvs-in-project: true | |
| - name: set-python-${{ matrix.python-version }} | |
| uses: actions/setup-python@v4 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: install-project | |
| shell: bash | |
| run: | | |
| poetry --version | |
| python --version | |
| python -m venv .venv | |
| if [[ "${{ runner.os }}" == "Windows" ]]; then | |
| source .venv/Scripts/activate | |
| python --version | |
| else | |
| source .venv/bin/activate | |
| which python | |
| fi | |
| poetry env use python | |
| poetry env info | |
| poetry run pip install --upgrade pip wheel setuptools | |
| poetry run pip install gdstk --only-binary gdstk | |
| poetry install -E dev | |
| - name: run-doctests | |
| run: | | |
| poetry run pytest -rF --tb=short tidy3d | |
| - name: run-tests-coverage | |
| env: | |
| PYTHONUNBUFFERED: "1" | |
| run: | | |
| poetry run pytest --cov=tidy3d -rF --tb=short tests/_test_data/_test_datasets_no_vtk.py | |
| poetry run pytest --cov=tidy3d -rF --tb=short tests | |
| poetry run coverage report -m | |
| TOTAL_COVERAGE=$(poetry run coverage report --format=total) | |
| echo "total=$TOTAL_COVERAGE" >> "$GITHUB_ENV" | |
| echo "### Total coverage: ${TOTAL_COVERAGE}%" | |
| - name: create-badge | |
| if: ${{ github.ref == 'refs/heads/develop' }} | |
| # https://gist.githubusercontent.com/nedbat/8c6980f77988a327348f9b02bbaf67f5 | |
| uses: schneegans/dynamic-badges-action@e9a478b16159b4d31420099ba146cdc50f134483 # v1.7.0 | |
| with: | |
| auth: ${{ secrets.GH_TIDY3D_COVERAGE_GIST }} | |
| gistID: 4702549574741e87deaadba436218ebd | |
| filename: tidy3d_extension.json | |
| label: Coverage | |
| message: ${{ env.total }}% | |
| minColorRange: 60 | |
| maxColorRange: 95 | |
| valColorRange: ${{ env.total }} | |
| style: "for-the-badge" | |
| develop-cli-tests: | |
| name: develop-cli-tests | |
| needs: determine-test-scope | |
| if: needs.determine-test-scope.outputs.cli_tests == 'true' | |
| uses: ./.github/workflows/tidy3d-python-client-develop-cli.yml | |
| with: | |
| release_tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || inputs.release_tag }} | |
| verify-version-consistency: | |
| name: verify-version-consistency | |
| runs-on: ubuntu-latest | |
| needs: determine-test-scope | |
| if: needs.determine-test-scope.outputs.version_match_tests == 'true' | |
| steps: | |
| - name: checkout-code | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || inputs.release_tag) || github.ref }} | |
| persist-credentials: false | |
| - name: check-version-consistency | |
| env: | |
| RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || inputs.release_tag }} | |
| run: | | |
| set -e | |
| echo "=== Verifying Version Consistency ===" | |
| echo "" | |
| # Extract version from pyproject.toml | |
| PYPROJECT_VERSION=$(grep '^version = ' pyproject.toml | head -n 1 | sed 's/version = "\(.*\)"/\1/') | |
| echo "pyproject.toml version: $PYPROJECT_VERSION" | |
| # Extract version from tidy3d/version.py | |
| VERSION_PY=$(grep '__version__ = ' tidy3d/version.py | sed 's/__version__ = "\(.*\)"/\1/') | |
| echo "tidy3d/version.py version: $VERSION_PY" | |
| echo "" | |
| # Compare versions | |
| if [[ "$PYPROJECT_VERSION" != "$VERSION_PY" ]]; then | |
| echo "❌ ERROR: Version mismatch detected!" | |
| echo " pyproject.toml: $PYPROJECT_VERSION" | |
| echo " tidy3d/version.py: $VERSION_PY" | |
| echo "" | |
| echo "These versions must match before release." | |
| echo "Please update both files to the same version." | |
| exit 1 | |
| fi | |
| echo "✅ Version consistency check passed: $PYPROJECT_VERSION" | |
| echo "" | |
| # If release tag provided, validate it matches the version | |
| if [[ -n "$RELEASE_TAG" ]]; then | |
| echo "=== Validating Release Tag ===" | |
| echo "Release tag: $RELEASE_TAG" | |
| # Strip 'v' prefix from tag if present | |
| TAG_VERSION="${RELEASE_TAG#v}" | |
| echo "Tag version (without 'v'): $TAG_VERSION" | |
| if [[ "$TAG_VERSION" != "$PYPROJECT_VERSION" ]]; then | |
| echo "❌ ERROR: Release tag does not match package version!" | |
| echo " Release tag: $RELEASE_TAG (version: $TAG_VERSION)" | |
| echo " Package version: $PYPROJECT_VERSION" | |
| echo "" | |
| echo "The release tag should be 'v$PYPROJECT_VERSION'" | |
| exit 1 | |
| fi | |
| echo "✅ Release tag matches package version" | |
| fi | |
| echo "" | |
| echo "=== Version Checks Passed ===" | |
| test-submodules: | |
| name: test-submodules | |
| runs-on: ubuntu-latest | |
| needs: determine-test-scope | |
| if: ${{ always() && (needs.determine-test-scope.outputs.submodule_tests == 'true') && (github.event.inputs.release_tag || inputs.release_tag) && !contains(github.event.inputs.release_tag || inputs.release_tag, 'rc') }} | |
| env: | |
| RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || inputs.release_tag }} | |
| steps: | |
| - name: checkout-head | |
| if: ${{ !env.RELEASE_TAG }} | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: 'recursive' | |
| fetch-depth: 0 | |
| persist-credentials: true | |
| - name: checkout-tag | |
| if: ${{ env.RELEASE_TAG }} | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ env.RELEASE_TAG }} | |
| submodules: 'recursive' | |
| fetch-depth: 0 | |
| persist-credentials: true | |
| - name: initialize-submodules | |
| run: | | |
| git submodule update --init --recursive | |
| - name: check-submodules-for-multiple-branches | |
| shell: bash | |
| run: | | |
| BRANCHES=("develop") # Add your branches here | |
| for BRANCH in "${BRANCHES[@]}"; do | |
| echo "Analyzing branch: $BRANCH" | |
| # Fetch all branches and tags | |
| git fetch --all --verbose | |
| # Checkout the branch | |
| git checkout $BRANCH | |
| NOTEBOOKS_PATH=docs/notebooks | |
| FAQ_PATH=docs/faq | |
| # Checking Notebooks submodule | |
| echo "Checking $NOTEBOOKS_PATH for updates..." | |
| cd $NOTEBOOKS_PATH | |
| NOTEBOOKS_CURRENT_COMMIT=$(git rev-parse HEAD) | |
| echo $(git fetch --all --verbose) | |
| echo $(git remote get-url origin) | |
| if git show-ref --verify refs/remotes/origin/$BRANCH; then | |
| echo "Branch $BRANCH exists." | |
| else | |
| echo "::error::Branch $BRANCH does not exist on remote." | |
| exit 1 | |
| fi | |
| NOTEBOOKS_LATEST_COMMIT=$(git rev-parse refs/remotes/origin/${BRANCH}) | |
| echo "NOTEBOOKS_LATEST_COMMIT: $NOTEBOOKS_LATEST_COMMIT" | |
| echo "NOTEBOOKS_CURRENT_COMMIT: $NOTEBOOKS_CURRENT_COMMIT" | |
| cd ../.. | |
| if [ "$NOTEBOOKS_LATEST_COMMIT" != "$NOTEBOOKS_CURRENT_COMMIT" ]; then | |
| echo "::error::Submodule $NOTEBOOKS_PATH is not up to date with the $BRANCH branch. Please update it." | |
| exit 1 | |
| else | |
| echo "Submodule $NOTEBOOKS_PATH is up to date with the $BRANCH branch." | |
| fi | |
| # Checking FAQs only on the develop branch | |
| if [[ "$BRANCH" == "develop" ]]; then | |
| echo "Checking $FAQ_PATH for updates..." | |
| cd $FAQ_PATH | |
| FAQ_CURRENT_COMMIT=$(git rev-parse HEAD) | |
| echo $(git fetch --all --verbose) | |
| echo $(git remote get-url origin) | |
| FAQ_LATEST_COMMIT=$(git rev-parse refs/remotes/origin/develop) | |
| echo "FAQ_LATEST_COMMIT: $FAQ_LATEST_COMMIT" | |
| echo "FAQ_CURRENT_COMMIT: $FAQ_CURRENT_COMMIT" | |
| cd ../.. | |
| if [ "$FAQ_LATEST_COMMIT" != "$FAQ_CURRENT_COMMIT" ]; then | |
| echo "::error::Submodule $FAQ_PATH is not up to date. Please update it." | |
| exit 1 | |
| else | |
| echo "Submodule $FAQ_PATH is up to date." | |
| fi | |
| fi | |
| done | |
| echo "" | |
| echo "=== Submodule Checks Passed ===" | |
| workflow-validation: | |
| name: workflow-validation | |
| if: always() | |
| needs: | |
| - determine-test-scope | |
| - local-tests | |
| - remote-tests | |
| - lint | |
| - mypy | |
| - verify-schema-change | |
| - lint-commit-messages | |
| - lint-branch-name | |
| - zizmor | |
| - develop-cli-tests | |
| - verify-version-consistency | |
| - test-submodules | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: check-linting-result | |
| if: ${{ needs.determine-test-scope.outputs.code_quality_tests == 'true' && needs.lint.result != 'success' && needs.lint.result != 'skipped' }} | |
| run: | | |
| echo "❌ Linting failed." | |
| exit 1 | |
| - name: check-mypy-result | |
| if: ${{ needs.determine-test-scope.outputs.code_quality_tests == 'true' && needs.mypy.result != 'success' && needs.mypy.result != 'skipped' }} | |
| run: | | |
| echo "❌ Mypy type checking failed." | |
| exit 1 | |
| - name: check-schema-change-verification | |
| if: ${{ needs.determine-test-scope.outputs.code_quality_tests == 'true' && needs.verify-schema-change.result != 'success' && needs.verify-schema-change.result != 'skipped' }} | |
| run: | | |
| echo "❌ Schema change verification failed." | |
| exit 1 | |
| - name: check-local-tests-result | |
| if: ${{ needs.determine-test-scope.outputs.local_tests == 'true' && needs.local-tests.result != 'success' && needs.local-tests.result != 'skipped' }} | |
| run: | | |
| echo "❌ Local tests failed." | |
| exit 1 | |
| - name: check-remote-tests-result | |
| if: ${{ needs.determine-test-scope.outputs.remote_tests == 'true' && needs.remote-tests.result != 'success' && needs.remote-tests.result != 'skipped' }} | |
| run: | | |
| echo "❌ Remote tests failed." | |
| exit 1 | |
| - name: check-commit-message-linting | |
| if: ${{ needs.determine-test-scope.outputs.code_quality_tests == 'true' && needs.lint-commit-messages.result != 'success' && needs.lint-commit-messages.result != 'skipped' }} | |
| run: | | |
| echo "❌ Commit message linting failed." | |
| exit 1 | |
| - name: check-branch-name-linting | |
| if: ${{ needs.determine-test-scope.outputs.pr_review_tests == 'true' && needs.lint-branch-name.result != 'success' && needs.lint-branch-name.result != 'skipped' }} | |
| run: | | |
| echo "❌ Branch name linting failed." | |
| exit 1 | |
| - name: check-zizmor-static-analysis | |
| if: ${{ needs.determine-test-scope.outputs.code_quality_tests == 'true' && needs.zizmor.result != 'success' && needs.zizmor.result != 'skipped' }} | |
| run: | | |
| echo "❌ Zizmor static analysis failed." | |
| exit 1 | |
| - name: check-cli-tests-result | |
| if: ${{ needs.determine-test-scope.outputs.cli_tests == 'true' && needs.develop-cli-tests.result != 'success' && needs.develop-cli-tests.result != 'skipped' }} | |
| run: | | |
| echo "❌ CLI tests failed." | |
| exit 1 | |
| - name: check-version-consistency-result | |
| if: ${{ needs.determine-test-scope.outputs.version_match_tests == 'true' && needs.verify-version-consistency.result != 'success' && needs.verify-version-consistency.result != 'skipped' }} | |
| run: | | |
| echo "❌ Version consistency check failed." | |
| exit 1 | |
| - name: check-submodule-tests-result | |
| if: ${{ needs.determine-test-scope.outputs.submodule_tests == 'true' && needs.test-submodules.result != 'success' && needs.test-submodules.result != 'skipped' }} | |
| run: | | |
| echo "❌ Submodule tests failed." | |
| exit 1 | |
| - name: all-checks-passed | |
| if: ${{ success() }} | |
| run: echo "✅ All required jobs passed!" | |
| pr-requirements-pass: | |
| name: pr-requirements-pass | |
| if: | | |
| always() && | |
| (github.event_name == 'pull_request' || github.event_name == 'pull_request_review' || github.event_name == 'merge_group') && | |
| ((needs.determine-test-scope.outputs.pr_approval_state == 'true' && needs.determine-test-scope.outputs.local_tests == 'true') || needs.determine-test-scope.outputs.remote_tests == 'true') | |
| needs: | |
| - determine-test-scope | |
| - workflow-validation | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: check-workflow-validation | |
| if: ${{ needs.workflow-validation.result != 'success' }} | |
| run: | | |
| echo "❌ Workflow validation failed. See workflow-validation job for details." | |
| exit 1 | |
| - name: all-checks-passed | |
| if: ${{ success() }} | |
| run: echo "✅ All required jobs passed!" |