From 7e5a9a8be022198a055781c2b0a2dba43469cf22 Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:05:58 +0000 Subject: [PATCH 1/9] Copier update: CI changes job, merge-group required-check and manual full run (copier-base feat/ci-path-classes) Pulled from the base branch at 450438cb (v0.2.0-9-g450438cb) ahead of its merge, so the template-side gating can be built against it; the answers file moves to the release tag once base tags. The forked project CI skeleton was reconstructed by hand: copier rejected the fork's jobs as one 644-line hunk because the new changes job is inserted exactly where the fork's first job begins. The result carries base's three hunks (workflow_dispatch trigger, changes job, status posted to the event's commit) on top of the unchanged fork; nothing else in the file moved. Co-Authored-By: Claude Fable 5.1 --- .config/.copier-answers.yml | 2 +- .github/workflows/ci.yaml | 7 ++- extensions/context.py | 3 +- template/.github/path-filters.yaml.jinja | 16 ++++++ template/.github/workflows/ci.yaml.jinja | 65 +++++++++++++++++++++++- 5 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 template/.github/path-filters.yaml.jinja diff --git a/.config/.copier-answers.yml b/.config/.copier-answers.yml index 2258014db..86b19a9e5 100644 --- a/.config/.copier-answers.yml +++ b/.config/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v0.2.0 +_commit: v0.2.0-9-g450438cb _src_path: gh:LabAutomationAndScreening/copier-base-template.git copier_answers_directory: .config description: A web app that is hosted within a local intranet. Nuxt frontend, python diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 013d0bcc6..8ea87f64a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -272,11 +272,14 @@ jobs: if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }} env: GH_TOKEN: ${{ github.token }} + # A status belongs to a commit: the PR's head commit on pull_request events, the merge group's head on + # merge_group events, whose payload carries no pull_request at all. + COMMIT_SHA: ${{ github.event_name == 'merge_group' && github.event.merge_group.head_sha || github.event.pull_request.head.sha }} run: | gh api \ -X POST -H "Accept: application/vnd.github.v3+json" \ - "${{ github.event.pull_request.statuses_url }}" \ - -f state=success -f context="required-check" -f description="✅ All required checks passed in the job triggered by pull_request" \ + "/repos/${{ github.repository }}/statuses/${COMMIT_SHA}" \ + -f state=success -f context="required-check" -f description="✅ All required checks passed in the job triggered by ${{ github.event_name }}" \ -f target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - name: Mark updated dependabot hash commit as succeeded diff --git a/extensions/context.py b/extensions/context.py index 7e31db287..20f6916bc 100644 --- a/extensions/context.py +++ b/extensions/context.py @@ -38,7 +38,7 @@ def hook( # noqa: PLR0915 # yes, this is a lot of statements, but it's all just context["pulumi_aws_version"] = ">=7.48.0" context["pulumi_aws_native_version"] = ">=1.81.0" context["pulumi_command_version"] = ">=1.2.1" - context["pulumi_github_version"] = ">=6.14.0" + context["pulumi_github_version"] = ">=6.15.0" context["pulumi_okta_version"] = ">=6.6.0" context["boto3_version"] = ">=1.43.88" context["ephemeral_pulumi_deploy_version"] = ">=0.0.8" @@ -124,6 +124,7 @@ def hook( # noqa: PLR0915 # yes, this is a lot of statements, but it's all just context["gha_action_gh_release"] = "v3.0.3" context["gha_codecov"] = "v7.1.0" context["gha_mutex"] = "1ebad517141198e08d47cf72f3c0975316620a65 # v1.0.0-alpha.10" + context["gha_paths_filter"] = "ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3" context["gha_pypi_publish"] = "v1.14.2" context["gha_sleep"] = "v2.0.4" context["gha_absaoss_k3d"] = "v2.4.0" diff --git a/template/.github/path-filters.yaml.jinja b/template/.github/path-filters.yaml.jinja new file mode 100644 index 000000000..4d41a8c76 --- /dev/null +++ b/template/.github/path-filters.yaml.jinja @@ -0,0 +1,16 @@ +# Path classes for the `changes` job in .github/workflows/ci.yaml. Every top-level key becomes a boolean in that job's +# `classes` output, read as fromJSON(needs.changes.outputs.classes)., true when the change touched a matching +# path. Globs follow dorny/paths-filter (picomatch); a leading `!` excludes. +# +# `code` is every path a build, test or packaging job may depend on, so exclude only paths no such job reads. A code +# path that no other class claims switches every class on, which means an unlisted path can never cause a job to be +# skipped. A project that builds its documentation excludes the sources here and claims them in a class of its own +# that gates the docs job. Project classes go below `code`. +code: + - '**' + - '!**/*.md' + - '!.coderabbit.yaml' + - '!**/*.mmd' + - '!*.code-workspace' + - '!.github/ISSUE_TEMPLATE/**' + - '!LICENSE' diff --git a/template/.github/workflows/ci.yaml.jinja b/template/.github/workflows/ci.yaml.jinja index 164c5aff7..7db59d45a 100644 --- a/template/.github/workflows/ci.yaml.jinja +++ b/template/.github/workflows/ci.yaml.jinja @@ -6,6 +6,7 @@ on: - 'gh-readonly-queue/**' # don't run (again) when on these special branches created during merge groups; the `on: merge_group` already triggers it. merge_group: pull_request: + workflow_dispatch: # a manual run executes every job for the chosen branch, as information only: it posts no required-check env: PYTHONUNBUFFERED: True @@ -37,6 +38,63 @@ jobs: - id: check uses: ./.github/actions/check-skip-duplicates + changes: + name: Classify changed paths + runs-on: {% endraw %}{{ gha_linux_runner }}{% raw %} + timeout-minutes: {% endraw %}{{ gha_short_timeout_minutes }}{% raw %} + permissions: + contents: read + pull-requests: read # dorny/paths-filter reads a PR's file list through the API + outputs: + # One boolean per class in .github/path-filters.yaml, as a JSON object: + # fromJSON(needs.changes.outputs.classes). + classes: ${{ steps.resolve.outputs.classes }} + steps: + - name: Decide whether the diff matters at all + id: policy + env: + # The default branch always runs everything, so a class that was wrong on a feature branch or in the merge + # queue is still caught there. A manual run is the way to get that full run for one branch. + RUN_EVERYTHING: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || github.event_name == 'workflow_dispatch' }} + run: echo "run_everything=$RUN_EVERYTHING" >> "$GITHUB_OUTPUT" + + - name: Checkout code + uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} + with: + persist-credentials: false + + - name: Match the changed files against the path classes + if: steps.policy.outputs.run_everything != 'true' + id: filter + uses: dorny/paths-filter@{% endraw %}{{ gha_paths_filter }}{% raw %} + with: + filters: .github/path-filters.yaml + predicate-quantifier: some-with-excludes + list-files: json + + - name: Resolve the class outputs + id: resolve + env: + RUN_EVERYTHING: ${{ steps.policy.outputs.run_everything }} + # Holds file names from the PR, so it only ever travels through env and jq, never a shell expansion + FILTER_OUTPUTS: ${{ toJSON(steps.filter.outputs) }} + run: | + # A code file that no other class claims forces every class on: an unclassified path can affect anything, so + # nothing may be skipped because of it. + classes=$(yq 'keys | .[]' .github/path-filters.yaml) + jq -n --arg run_everything "$RUN_EVERYTHING" --argjson outputs "$FILTER_OUTPUTS" --arg classes "$classes" ' + def files($class): $outputs[$class + "_files"] // "[]" | fromjson; + ($classes | split("\n") | map(select(length > 0))) as $classes + | (files("code") - ([$classes[] | select(. != "code") | files(.)] | add // [])) as $unclassified + | ($run_everything == "true" or ($unclassified | length) > 0) as $force + | { + unclassified: $unclassified, + classes: ([$classes[] | {key: ., value: ($force or $outputs[.] == "true")}] | from_entries) + } + ' > "$RUNNER_TEMP/resolved.json" + echo "classes=$(jq -c .classes "$RUNNER_TEMP/resolved.json")" >> "$GITHUB_OUTPUT" + jq -r '"classes: \(.classes)", (if (.unclassified | length) > 0 then "every class forced on by unclassified paths:", .unclassified[] else empty end)' "$RUNNER_TEMP/resolved.json" | tee -a "$GITHUB_STEP_SUMMARY" + lint: needs: - get-values @@ -722,11 +780,14 @@ jobs: if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }} env: GH_TOKEN: ${{ github.token }} + # A status belongs to a commit: the PR's head commit on pull_request events, the merge group's head on + # merge_group events, whose payload carries no pull_request at all. + COMMIT_SHA: ${{ github.event_name == 'merge_group' && github.event.merge_group.head_sha || github.event.pull_request.head.sha }} run: | gh api \ -X POST -H "Accept: application/vnd.github.v3+json" \ - "${{ github.event.pull_request.statuses_url }}" \ - -f state=success -f context="required-check" -f description="✅ All required checks passed in the job triggered by pull_request" \ + "/repos/${{ github.repository }}/statuses/${COMMIT_SHA}" \ + -f state=success -f context="required-check" -f description="✅ All required checks passed in the job triggered by ${{ github.event_name }}" \ -f target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - name: Mark updated dependabot hash commit as succeeded From 7161a6c837112147c2035e3305883d28225a6859 Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:33:36 +0000 Subject: [PATCH 2/9] Gate CI jobs on the path classes a change touched Every job that depends on part of the tree reads the changes job's classes output and skips its steps when that part is untouched, with the job graph left as it is: an untouched class resolves to an empty green job in seconds, so builds still wait on both unit-test jobs and fail fast when either breaks. Jobs that are reusable workflows (the docker-image build variants, the installer) have no caller-side steps and gate at job level instead. - unit-test-frontend, test-compiled-frontend-dev: frontend - unit-test-backend: backend - build-frontend, build-backend, e2e-test, e2e-test-k3d, package-images, package-firmware, build-installer: code - e2e-test-backend, e2e-test-windows-service: code and backend, since they run against a build that only code changes produce The seeded filter file excludes the unit-level test trees from code (nothing built reads them) and adds the frontend, backend and installer classes under the answers that render those parts. Co-Authored-By: Claude Fable 5.1 --- template/.github/path-filters.yaml.jinja | 15 +++ template/.github/workflows/ci.yaml.jinja | 141 +++++++++++++++++++---- 2 files changed, 136 insertions(+), 20 deletions(-) diff --git a/template/.github/path-filters.yaml.jinja b/template/.github/path-filters.yaml.jinja index 4d41a8c76..cf9d49eb3 100644 --- a/template/.github/path-filters.yaml.jinja +++ b/template/.github/path-filters.yaml.jinja @@ -14,3 +14,18 @@ code: - '!*.code-workspace' - '!.github/ISSUE_TEMPLATE/**' - '!LICENSE' + # unit-level tests never reach a build artifact; their own classes still run them{% if has_backend %} + - '!backend/tests/unit/**'{% endif %} + - '!frontend/tests/unit/**' + - '!frontend/tests/compiled/**' +frontend: + - 'frontend/**' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml'{% if has_backend %} + - 'backend/tests/unit/__snapshots__/test_basic_server_functionality/test_openapi_schema.json' +backend: + - 'backend/**' + - 'pytest.ini'{% endif %}{% if install_as_windows_service %} +installer: + - 'installer/**' + - '!installer/**/*.md'{% endif %} diff --git a/template/.github/workflows/ci.yaml.jinja b/template/.github/workflows/ci.yaml.jinja index 7db59d45a..7ada46cfd 100644 --- a/template/.github/workflows/ci.yaml.jinja +++ b/template/.github/workflows/ci.yaml.jinja @@ -111,24 +111,30 @@ jobs: node-version: {% endraw %}{{ node_version }}{% raw %} unit-test-frontend: - name: Unit Test Frontend + name: Unit Test Frontend${{ fromJSON(needs.changes.outputs.classes).frontend != true && ' (skipped, no frontend changes)' || '' }} timeout-minutes: {% endraw %}{{ gha_medium_timeout_minutes }}{% raw %} runs-on: {% endraw %}{{ gha_linux_runner }}{% raw %} needs: - lint - check-skip-duplicate + - changes if: needs.check-skip-duplicate.outputs.should-run == 'true' + env: + RUN_JOB: ${{ fromJSON(needs.changes.outputs.classes).frontend }} steps: - name: Checkout code + if: env.RUN_JOB == 'true' uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} - name: Install latest versions of packages + if: env.RUN_JOB == 'true' uses: ./.github/actions/install_deps with: node-version: {% endraw %}{{ node_version }}{% raw %} skip-installing-ssm-plugin-manager: true - name: Unit test + if: env.RUN_JOB == 'true' run: pnpm --dir=frontend test-unit - name: Upload test coverage on failure @@ -142,12 +148,15 @@ jobs: retention-days: 5{% endraw %}{% if has_backend %}{% raw %} unit-test-backend: - name: Unit Test Backend + name: Unit Test Backend${{ fromJSON(needs.changes.outputs.classes).backend != true && ' (skipped, no backend changes)' || '' }} timeout-minutes: {% endraw %}{{ gha_medium_timeout_minutes }}{% raw %} needs: - lint - check-skip-duplicate + - changes if: needs.check-skip-duplicate.outputs.should-run == 'true' + env: + RUN_JOB: ${{ fromJSON(needs.changes.outputs.classes).backend }} strategy: matrix: os: @@ -156,21 +165,24 @@ jobs: runs-on: ${{ matrix.os }} steps:{% endraw %}{% if use_windows_in_ci %} - name: Allow long file paths - if: runner.os == 'Windows' + if: env.RUN_JOB == 'true' && runner.os == 'Windows' # Some tooling (e.g. vcrpy, syrupy) generates file names past the 260 character limit Windows # defaults to. This has to happen before the checkout that writes those files. run: git config --global core.longpaths true {% endif %}{% raw %} - name: Checkout code + if: env.RUN_JOB == 'true' uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} - name: Install latest versions of python packages + if: env.RUN_JOB == 'true' uses: ./.github/actions/install_deps with: python-version: {% endraw %}{{ python_version }}{% raw %} skip-installing-ssm-plugin-manager: true - name: Unit test + if: env.RUN_JOB == 'true' run: uv --directory=backend run pytest tests/unit --durations=5 - name: Upload test coverage on failure @@ -188,8 +200,9 @@ jobs: - unit-test-frontend{% endraw %}{% if has_backend %}{% raw %} - unit-test-backend{% endraw %}{% endif %}{% raw %} - check-skip-duplicate - if: needs.check-skip-duplicate.outputs.should-run == 'true' -{% endraw %}{% if not deploy_as_executable %}{% raw %} uses: ./.github/workflows/build-docker-image.yaml + - changes +{% endraw %}{% if not deploy_as_executable %}{% raw %} if: needs.check-skip-duplicate.outputs.should-run == 'true' && fromJSON(needs.changes.outputs.classes).code + uses: ./.github/workflows/build-docker-image.yaml permissions: contents: write # needed for mutex id-token: write # needed to assume OIDC roles (e.g. for downloading from CodeArtifact) @@ -202,22 +215,30 @@ jobs: repository: {% endraw %}{{ frontend_ecr_base_url }}{% raw %} image_name: {% endraw %}{{ frontend_image_name }}{% raw %} {% endraw %}{% if push_to_ecr %}{% raw %} push-role-name: {% endraw %}{{ frontend_ecr_push_role_name }}{% endif %}{% else %}{% raw %} + name: Build Frontend${{ fromJSON(needs.changes.outputs.classes).code != true && ' (skipped, no code changes)' || '' }} + if: needs.check-skip-duplicate.outputs.should-run == 'true' + env: + RUN_JOB: ${{ fromJSON(needs.changes.outputs.classes).code }} timeout-minutes: {% endraw %}{{ gha_medium_timeout_minutes }}{% raw %} runs-on: {% endraw %}{{ gha_linux_runner }}{% raw %} steps: - name: Checkout code + if: env.RUN_JOB == 'true' uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} - name: Install latest versions of node packages + if: env.RUN_JOB == 'true' uses: ./.github/actions/install_deps with: node-version: {% endraw %}{{ node_version }}{% raw %} skip-installing-ssm-plugin-manager: true - name: Build frontend + if: env.RUN_JOB == 'true' run: task build-frontend - name: Upload build artifact + if: env.RUN_JOB == 'true' uses: actions/upload-artifact@{% endraw %}{{ gha_upload_artifact }}{% raw %} with: name: frontend-static-assets @@ -234,7 +255,8 @@ jobs: - unit-test-frontend - unit-test-backend - check-skip-duplicate - if: needs.check-skip-duplicate.outputs.should-run == 'true' + - changes + if: needs.check-skip-duplicate.outputs.should-run == 'true' && fromJSON(needs.changes.outputs.classes).code uses: ./.github/workflows/build-docker-image.yaml permissions: contents: write # needed for mutex @@ -253,7 +275,11 @@ jobs: - unit-test-backend - build-frontend - check-skip-duplicate + - changes + name: Build Backend${{ fromJSON(needs.changes.outputs.classes).code != true && ' (skipped, no code changes)' || '' }} if: needs.check-skip-duplicate.outputs.should-run == 'true' + env: + RUN_JOB: ${{ fromJSON(needs.changes.outputs.classes).code }} strategy: matrix: os: @@ -264,31 +290,36 @@ jobs: steps:{% endraw %}{% if use_windows_in_ci %} - name: Allow long file paths - if: runner.os == 'Windows' + if: env.RUN_JOB == 'true' && runner.os == 'Windows' # Some tooling (e.g. vcrpy, syrupy) generates file names past the 260 character limit Windows # defaults to. This has to happen before the checkout that writes those files. run: git config --global core.longpaths true {% endif %}{% raw %} - name: Checkout code + if: env.RUN_JOB == 'true' uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} - name: Download Frontend Static Assets + if: env.RUN_JOB == 'true' uses: actions/download-artifact@{% endraw %}{{ gha_download_artifact }}{% raw %} with: name: frontend-static-assets path: backend/src/static - name: Install latest versions of python packages + if: env.RUN_JOB == 'true' uses: ./.github/actions/install_deps with: python-version: {% endraw %}{{ python_version }}{% raw %} skip-installing-ssm-plugin-manager: true - name: Build application + if: env.RUN_JOB == 'true' run: task bundle-executable-from-staged-static - name: Tar files to preserve permissions on Linux during artifact upload # https://github.com/actions/upload-artifact?tab=readme-ov-file#permission-loss + if: env.RUN_JOB == 'true' run: tar -cvf app.tar backend/dist/{% endraw %}{{ repo_name }}{% raw %} - name: Upload application artifact (Windows) - if: runner.os == 'Windows' + if: env.RUN_JOB == 'true' && runner.os == 'Windows' uses: actions/upload-artifact@{% endraw %}{{ gha_upload_artifact }}{% raw %} with: name: built-{% endraw %}{{ repo_name }}{% raw %}-${{ matrix.os }} @@ -296,7 +327,7 @@ jobs: if-no-files-found: error retention-days: 5 - name: Upload application artifact (Linux) - if: runner.os == 'Linux' + if: env.RUN_JOB == 'true' && runner.os == 'Linux' uses: actions/upload-artifact@{% endraw %}{{ gha_upload_artifact }}{% raw %} with: name: built-{% endraw %}{{ repo_name }}{% raw %}-${{ matrix.os }} @@ -306,66 +337,80 @@ jobs: test-compiled-frontend-dev: - name: Test the compiled Frontend for pnpm dev mode + name: Test the compiled Frontend for pnpm dev mode${{ fromJSON(needs.changes.outputs.classes).frontend != true && ' (skipped, no frontend changes)' || '' }} timeout-minutes: {% endraw %}{{ gha_medium_timeout_minutes }}{% raw %} runs-on: {% endraw %}{{ gha_linux_runner }}{% raw %} env: + RUN_JOB: ${{ fromJSON(needs.changes.outputs.classes).frontend }} INSTALL_PLAYWRIGHT_BROWSERS: "1" # createPage() drives a real browser needs: - build-frontend{% endraw %}{% if has_backend %}{% raw %} - build-backend{% endraw %}{% endif %}{% raw %} - check-skip-duplicate + - changes if: needs.check-skip-duplicate.outputs.should-run == 'true' steps: - name: Checkout code + if: env.RUN_JOB == 'true' uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} - name: Install latest versions of node packages + if: env.RUN_JOB == 'true' uses: ./.github/actions/install_deps with: node-version: {% endraw %}{{ node_version }}{% raw %} skip-installing-ssm-plugin-manager: true - name: Test + if: env.RUN_JOB == 'true' run: pnpm --dir=frontend test-compiled-dev{% endraw %}{% if is_circuit_python_driver %}{% raw %} e2e-test-k3d: - name: End-to-end Testing on k3d + name: End-to-end Testing on k3d${{ fromJSON(needs.changes.outputs.classes).code != true && ' (skipped, no code changes)' || '' }} timeout-minutes: {% endraw %}{{ gha_xlong_timeout_minutes }}{% raw %} runs-on: "{% endraw %}{{ gha_linux_runner }}{% raw %}" needs: - build-frontend - build-backend - check-skip-duplicate + - changes if: needs.check-skip-duplicate.outputs.should-run == 'true' + env: + RUN_JOB: ${{ fromJSON(needs.changes.outputs.classes).code }} steps: - name: Checkout code + if: env.RUN_JOB == 'true' uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} - name: Download Backend Artifact + if: env.RUN_JOB == 'true' uses: actions/download-artifact@{% endraw %}{{ gha_download_artifact }}{% raw %} with: name: ${{ needs.build-backend.outputs.artifact-name }} path: deployment-package-folder - name: Download Frontend Artifact + if: env.RUN_JOB == 'true' uses: actions/download-artifact@{% endraw %}{{ gha_download_artifact }}{% raw %} with: name: {% endraw %}{{ frontend_image_name | replace('/', '-') }}{% raw %} path: deployment-package-folder - name: Load docker images + if: env.RUN_JOB == 'true' run: | ls deployment-package-folder -a # load all images in the folder ls -1 deployment-package-folder/*.tar | xargs --no-run-if-empty -L 1 docker load -i - name: Retag loaded images to stable local names + if: env.RUN_JOB == 'true' run: | docker tag ${{ needs.build-frontend.outputs.full-image-tag }} {% endraw %}{{ frontend_image_name }}{% raw %}:local docker tag ${{ needs.build-backend.outputs.full-image-tag }} {% endraw %}{{ backend_image_name }}{% raw %}:local - name: Create k3d cluster + if: env.RUN_JOB == 'true' # k3d runs k3s inside Docker containers. NodePort iptables rules only handle traffic # arriving via Docker port publishing (host → container), not inter-container traffic. # --port with @server:0 uses Docker port publishing so localhost:30060/30061 on the @@ -382,28 +427,34 @@ jobs: --port {% endraw %}{{ backend_nodeport_number }}{% raw %}:{% endraw %}{{ backend_nodeport_number }}{% raw %}@server:0 - name: Setup Helm + if: env.RUN_JOB == 'true' uses: Azure/setup-helm@{% endraw %}{{ gha_azure_setup_helm }}{% raw %} with: version: {% endraw %}{{ helm_version }}{% raw %} - name: Setup kubectl + if: env.RUN_JOB == 'true' uses: Azure/setup-kubectl@{% endraw %}{{ gha_azure_setup_kubectl }}{% raw %} with: version: {% endraw %}{{ kubectl_version }}{% raw %} - name: Get runner IP + if: env.RUN_JOB == 'true' run: | RUNNER_IP=$(hostname -I | awk '{print $1}') echo "Runner IP: $RUNNER_IP" echo "RUNNER_IP=$RUNNER_IP" >> $GITHUB_ENV - name: Create app namespace + if: env.RUN_JOB == 'true' run: kubectl create ns app - name: Wait for k3d node ready + if: env.RUN_JOB == 'true' run: kubectl wait --for=condition=Ready node --all --timeout=60s - name: Import images into k3d + if: env.RUN_JOB == 'true' run: | k3d image import {% endraw %}{{ frontend_image_name }}{% raw %}:local {% endraw %}{% if has_backend %}{{ backend_image_name }}{% raw %}:local{% endraw %}{% endif %}{% raw %} -c e2e # k3d image import exits 0 even when the internal `ctr images import` exec inside the node @@ -417,6 +468,7 @@ jobs: docker exec k3d-e2e-server-0 ctr -n k8s.io images ls | grep -Fq -- {% endraw %}{{ backend_image_name }}{% raw %}:local{% endraw %}{% endif %}{% raw %} - name: Helm install + if: env.RUN_JOB == 'true' run: | helm upgrade --install e2e ./deployment/helm \ --namespace app \ @@ -432,6 +484,7 @@ jobs: node-version: {% endraw %}{{ node_version }}{% raw %} - name: Wait for rollout + if: env.RUN_JOB == 'true' id: rollout run: kubectl -n app rollout status deploy/e2e-{% endraw %}{{ repo_name }}{% raw %} --timeout=180s @@ -449,6 +502,7 @@ jobs: run: kubectl -n app logs deploy/e2e-{% endraw %}{{ repo_name }}{% raw %} --all-containers --tail=100 --ignore-errors || true - name: E2E Test # TODO: add more than just smoke test + if: env.RUN_JOB == 'true' run: | # since there's no gap after the rollout to install node dependencies, sometimes the NodePorts themselves aren't ready yet, so do a few retries curl -s --show-error --fail-with-body --retry 10 --retry-delay 2 --retry-connrefused --retry-all-errors http://localhost:{% endraw %}{{ backend_nodeport_number }}{% raw %}/api/healthcheck @@ -456,7 +510,7 @@ jobs: curl -s --show-error --fail-with-body --retry 10 --retry-delay 2 --retry-connrefused --retry-all-errors http://localhost:{% endraw %}{{ frontend_nodeport_number }}{% raw %}/{% endraw %}{% endif %}{% raw %}{% endraw %}{% if run_backend_e2e_in_ci %}{% raw %} e2e-test-backend: - name: End-to-end Testing of the Backend + name: End-to-end Testing of the Backend${{ (fromJSON(needs.changes.outputs.classes).code && fromJSON(needs.changes.outputs.classes).backend) != true && ' (skipped, no backend code changes)' || '' }} timeout-minutes: {% endraw %}{{ gha_xlong_timeout_minutes }}{% raw %} strategy: matrix: @@ -466,72 +520,89 @@ jobs: needs: - build-backend - check-skip-duplicate + - changes if: needs.check-skip-duplicate.outputs.should-run == 'true' + env: + RUN_JOB: ${{ fromJSON(needs.changes.outputs.classes).code && fromJSON(needs.changes.outputs.classes).backend }} steps: - name: Checkout code + if: env.RUN_JOB == 'true' uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} - name: Install latest versions of python packages + if: env.RUN_JOB == 'true' uses: ./.github/actions/install_deps with: python-version: {% endraw %}{{ python_version }}{% raw %} skip-installing-ssm-plugin-manager: true - name: Download Backend Artifact + if: env.RUN_JOB == 'true' uses: actions/download-artifact@{% endraw %}{{ gha_download_artifact }}{% raw %} with: name: ${{ needs.build-backend.outputs.artifact-name }} path: deployment-package-folder{% endraw %}{% if auth_to_ecr_for_backend_e2e_in_ci %}{% raw %} - name: ECR Auth to pull images for E2E tests + if: env.RUN_JOB == 'true' uses: ./.github/actions/ecr-auth with: aws-region: {% endraw %}{{ auth_to_ecr_for_backend_e2e_in_ci_region }}{% raw %} role-arn: {% endraw %}{{ auth_to_ecr_for_backend_e2e_in_ci_role_arn }}{% raw %}{% endraw %}{% endif %}{% raw %} - name: Load docker images + if: env.RUN_JOB == 'true' run: | ls deployment-package-folder -a # load all images in the folder ls -1 deployment-package-folder/*.tar | xargs --no-run-if-empty -L 1 docker load -i - name: Retag loaded backend image to stable local name + if: env.RUN_JOB == 'true' run: docker tag ${{ needs.build-backend.outputs.full-image-tag }} {% endraw %}{{ backend_image_name }}{% raw %}:local - name: E2E test + if: env.RUN_JOB == 'true' run: uv --directory=backend run pytest tests/e2e --durations=5{% endraw %}{% endif %}{% raw %}{% endraw %}{% if install_as_windows_service %}{% raw %} e2e-test-windows-service: - name: Windows Service Smoke Tests + name: Windows Service Smoke Tests${{ (fromJSON(needs.changes.outputs.classes).code && fromJSON(needs.changes.outputs.classes).backend) != true && ' (skipped, no backend code changes)' || '' }} timeout-minutes: {% endraw %}{{ gha_long_timeout_minutes }}{% raw %} runs-on: {% endraw %}{{ gha_windows_runner }}{% raw %} needs: - build-backend - check-skip-duplicate + - changes if: needs.check-skip-duplicate.outputs.should-run == 'true' + env: + RUN_JOB: ${{ fromJSON(needs.changes.outputs.classes).code && fromJSON(needs.changes.outputs.classes).backend }} steps: - name: Allow long file paths - if: runner.os == 'Windows' + if: env.RUN_JOB == 'true' && runner.os == 'Windows' # Some tooling (e.g. vcrpy, syrupy) generates file names past the 260 character limit Windows # defaults to. This has to happen before the checkout that writes those files. run: git config --global core.longpaths true - name: Checkout code + if: env.RUN_JOB == 'true' uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} - name: Install latest versions of python packages + if: env.RUN_JOB == 'true' uses: ./.github/actions/install_deps with: python-version: {% endraw %}{{ python_version }}{% raw %} skip-installing-ssm-plugin-manager: true - name: Download Built executable + if: env.RUN_JOB == 'true' uses: actions/download-artifact@{% endraw %}{{ gha_download_artifact }}{% raw %} with: name: built-{% endraw %}{{ repo_name }}{% raw %}-{% endraw %}{{ gha_windows_runner }}{% raw %} path: backend/dist/{% endraw %}{{ repo_name }}{% raw %} - name: Windows service smoke tests + if: env.RUN_JOB == 'true' run: uv --directory=backend run pytest tests/windows_service --durations=5 --basetemp=dist/pytest-tmp - name: Dump service crash logs on failure @@ -561,7 +632,8 @@ jobs: needs: - build-backend - check-skip-duplicate - if: needs.check-skip-duplicate.outputs.should-run == 'true' + - changes + if: needs.check-skip-duplicate.outputs.should-run == 'true' && fromJSON(needs.changes.outputs.classes).code permissions: contents: read actions: read # build-installer.yaml downloads the built bundle artifact @@ -571,9 +643,10 @@ jobs: e2e-test: - name: End-to-end Testing + name: End-to-end Testing${{ fromJSON(needs.changes.outputs.classes).code != true && ' (skipped, no code changes)' || '' }} timeout-minutes: {% endraw %}{{ gha_xlong_timeout_minutes }}{% raw %} env: + RUN_JOB: ${{ fromJSON(needs.changes.outputs.classes).code }} INSTALL_PLAYWRIGHT_BROWSERS: "1" strategy: matrix: @@ -585,61 +658,71 @@ jobs: - build-frontend{% endraw %}{% if has_backend %}{% raw %} - build-backend{% endraw %}{% endif %}{% raw %} - check-skip-duplicate + - changes if: needs.check-skip-duplicate.outputs.should-run == 'true' steps:{% endraw %}{% if use_windows_in_ci %} - name: Allow long file paths - if: runner.os == 'Windows' + if: env.RUN_JOB == 'true' && runner.os == 'Windows' # Some tooling (e.g. vcrpy, syrupy) generates file names past the 260 character limit Windows # defaults to. This has to happen before the checkout that writes those files. run: git config --global core.longpaths true {% endif %}{% raw %} - name: Checkout code + if: env.RUN_JOB == 'true' uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} - name: Install latest versions of node packages + if: env.RUN_JOB == 'true' uses: ./.github/actions/install_deps with: node-version: {% endraw %}{{ node_version }}{% raw %}{% endraw %}{% if not deploy_as_executable %}{% raw %} {% endraw %}{% if has_backend %}{% raw %} - name: Download Backend Artifact + if: env.RUN_JOB == 'true' uses: actions/download-artifact@{% endraw %}{{ gha_download_artifact }}{% raw %} with: name: ${{ needs.build-backend.outputs.artifact-name }} path: deployment-package-folder{% endraw %}{% endif %}{% raw %} - name: Download Frontend Artifact + if: env.RUN_JOB == 'true' uses: actions/download-artifact@{% endraw %}{{ gha_download_artifact }}{% raw %} with: name: {% endraw %}{{ frontend_image_name | replace('/', '-') }}{% raw %} path: deployment-package-folder - name: Load docker images + if: env.RUN_JOB == 'true' run: | ls deployment-package-folder -a # load all images in the folder ls -1 deployment-package-folder/*.tar | xargs --no-run-if-empty -L 1 docker load -i - name: Retag loaded images to stable local names + if: env.RUN_JOB == 'true' run: | docker tag ${{ needs.build-frontend.outputs.full-image-tag }} {% endraw %}{{ frontend_image_name }}{% raw %}:local{% endraw %}{% if has_backend %}{% raw %} docker tag ${{ needs.build-backend.outputs.full-image-tag }} {% endraw %}{{ backend_image_name }}{% raw %}:local{% endraw %}{% endif %}{% raw %} {% endraw %}{% else %}{% raw %} - name: Download Built executable + if: env.RUN_JOB == 'true' uses: actions/download-artifact@{% endraw %}{{ gha_download_artifact }}{% raw %} with: name: built-{% endraw %}{{ repo_name }}{% raw %}-${{ matrix.os }} path: backend/dist/{% endraw %}{{ repo_name }}{% raw %} - name: Untar the artifact (on Linux) - if: runner.os == 'Linux' + if: env.RUN_JOB == 'true' && runner.os == 'Linux' run: tar -xvf backend/dist/{% endraw %}{{ repo_name }}{% raw %}/app.tar -C . {% endraw %}{% endif %}{% raw %}{% endraw %}{% if auth_to_ecr_for_e2e_in_ci %}{% raw %} - name: ECR Auth to pull images for E2E tests + if: env.RUN_JOB == 'true' uses: ./.github/actions/ecr-auth with: aws-region: {% endraw %}{{ auth_to_ecr_for_e2e_in_ci_region }}{% raw %} role-arn: {% endraw %}{{ auth_to_ecr_for_e2e_in_ci_role_arn }}{% raw %}{% endraw %}{% endif %}{% raw %} - name: E2E test + if: env.RUN_JOB == 'true' run: pnpm --dir=frontend test-e2e - name: Upload Playwright report on failure @@ -654,36 +737,44 @@ jobs: retention-days: 5 {% endraw %}{% if create_docker_image_tar_artifact %}{% raw %} package-images: - name: Package Images + name: Package Images${{ fromJSON(needs.changes.outputs.classes).code != true && ' (skipped, no code changes)' || '' }} timeout-minutes: {% endraw %}{{ gha_medium_timeout_minutes }}{% raw %} runs-on: {% endraw %}{{ gha_linux_runner }}{% raw %} needs: - build-frontend{% endraw %}{% if has_backend %}{% raw %} - build-backend{% endraw %}{% endif %}{% raw %} - check-skip-duplicate + - changes if: needs.check-skip-duplicate.outputs.should-run == 'true' + env: + RUN_JOB: ${{ fromJSON(needs.changes.outputs.classes).code }} steps: - name: Checkout repository + if: env.RUN_JOB == 'true' uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} - name: Prepare docker-compose file + if: env.RUN_JOB == 'true' run: | mkdir -p deployment-package-folder cp deployment/docker-compose.yaml deployment-package-folder/docker-compose.yaml sed -i '/image:/s/:latest/:${{ github.sha }}/g' deployment-package-folder/docker-compose.yaml{% endraw %}{% if has_backend %}{% raw %} - name: Download Backend Artifact + if: env.RUN_JOB == 'true' uses: actions/download-artifact@{% endraw %}{{ gha_download_artifact }}{% raw %} with: name: ${{ needs.build-backend.outputs.artifact-name }} path: deployment-package-folder{% endraw %}{% endif %}{% raw %} - name: Download Frontend Artifact + if: env.RUN_JOB == 'true' uses: actions/download-artifact@{% endraw %}{{ gha_download_artifact }}{% raw %} with: name: ${{ needs.build-frontend.outputs.artifact-name }} path: deployment-package-folder - name: Upload deployment package artifact + if: env.RUN_JOB == 'true' uses: actions/upload-artifact@{% endraw %}{{ gha_upload_artifact }}{% raw %} with: name: {% endraw %}{{ repo_name }}{% raw %}-deployment-package @@ -692,40 +783,50 @@ jobs: retention-days: 5{% endraw %}{% endif %}{% raw %}{% endraw %}{% if has_circuit_python_backend_template_been_instantiated %}{% raw %} package-firmware: - name: Package Firmware + name: Package Firmware${{ fromJSON(needs.changes.outputs.classes).code != true && ' (skipped, no code changes)' || '' }} timeout-minutes: {% endraw %}{{ gha_short_timeout_minutes }}{% raw %} runs-on: {% endraw %}{{ gha_linux_runner }}{% raw %} needs: - build-frontend - build-backend - check-skip-duplicate + - changes if: needs.check-skip-duplicate.outputs.should-run == 'true' + env: + RUN_JOB: ${{ fromJSON(needs.changes.outputs.classes).code }} steps: - name: Checkout repository + if: env.RUN_JOB == 'true' uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} - name: Install latest versions of python packages + if: env.RUN_JOB == 'true' uses: ./.github/actions/install_deps with: python-version: {% endraw %}{{ python_version }}{% raw %} skip-installing-ssm-plugin-manager: true - name: Extract firmware version + if: env.RUN_JOB == 'true' id: get-version run: | VERSION=$(uv --directory=backend run python -c "from backend_api.firmware.src.code_lib import VERSION; print(VERSION)") echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Install firmware dependencies + if: env.RUN_JOB == 'true' run: uv --directory=backend run python scripts/install_firmware_dependencies.py - name: Verify CircuitPython libraries installed + if: env.RUN_JOB == 'true' run: uv --directory=backend run circup --path=./src/backend_api/firmware/src/ freeze - name: Clean __pycache__ directories + if: env.RUN_JOB == 'true' run: find ./backend/src/backend_api/firmware/src -type d -name '__pycache__' -exec rm -rf {} + - name: Upload firmware artifact + if: env.RUN_JOB == 'true' uses: actions/upload-artifact@{% endraw %}{{ gha_upload_artifact }}{% raw %} with: name: {% endraw %}{{ repo_name.removesuffix('-driver') }}{% raw %}-firmware-v${{ steps.get-version.outputs.version }} From a0b578ba6b439fd2c2d5151bdd7616b1a7164670 Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:35:00 +0000 Subject: [PATCH 3/9] Copier update: forced-run summary, workflow-summary waits on changes, pyrefly pinned to 1.4.0 (copier-base feat/ci-path-classes) The update rejected this template's own CI jobs because the base rewrote the summary line they follow, so the base delta was reapplied onto the existing ci.yaml.jinja and changes added to its fuller workflow-summary needs list by hand. uv.lock and the devcontainer hash follow the exact pyrefly pin. --- .config/.copier-answers.yml | 2 +- .devcontainer/devcontainer.json | 4 +-- extensions/context.py | 2 +- pyproject.toml | 2 +- ...ensure_pnpm_minimum_release_age_exclude.py | 3 +- .../.devcontainer/devcontainer.json.jinja | 2 +- template/.github/workflows/ci.yaml.jinja | 17 +++++++--- uv.lock | 34 +++++++++---------- 8 files changed, 38 insertions(+), 28 deletions(-) diff --git a/.config/.copier-answers.yml b/.config/.copier-answers.yml index 86b19a9e5..5b9cb480a 100644 --- a/.config/.copier-answers.yml +++ b/.config/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v0.2.0-9-g450438cb +_commit: v0.2.0-15-gd5eb03aa _src_path: gh:LabAutomationAndScreening/copier-base-template.git copier_answers_directory: .config description: A web app that is hosted within a local intranet. Nuxt frontend, python diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index afe8ffbaa..a597b36d4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -30,7 +30,7 @@ // Python "-ms-python.vscode-pylance", // pyrefly is the language server for this project; ms-python.python auto-installs Pylance via its extension pack, and Pylance then warns on every boot that it's disabled "ms-python.python@2026.5.2026052901", - "meta.pyrefly@1.3.2", + "meta.pyrefly@1.4.0", "ms-vscode-remote.remote-containers@0.414.0", "charliermarsh.ruff@2026.84.0", @@ -67,5 +67,5 @@ "initializeCommand": "sh .devcontainer/initialize-command.sh", "onCreateCommand": "sh .devcontainer/on-create-command.sh", "postStartCommand": "sh .devcontainer/post-start-command.sh" - // Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): 55943b35 # spellchecker:disable-line + // Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): a1013bcc # spellchecker:disable-line } diff --git a/extensions/context.py b/extensions/context.py index 20f6916bc..bb0bc303f 100644 --- a/extensions/context.py +++ b/extensions/context.py @@ -67,7 +67,7 @@ def hook( # noqa: PLR0915 # yes, this is a lot of statements, but it's all just context["pytest_reserial_version"] = ">=0.6.1" context["python_faker_version"] = ">=40.41.0" context["mutmut_version"] = ">=3.8.0" - context["pyrefly_version"] = ">=1.3.2" + context["pyrefly_version"] = "==1.4.0" context["vacuum_openapi_version"] = "0.30.0" context["default_node_version"] = "24.11.1" diff --git a/pyproject.toml b/pyproject.toml index 74b34c731..710e79b31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ "pytest-cov>=7.1.0", "pytest-randomly>=5.0.0", "faker>=40.41.0", - "pyrefly>=1.3.2", + "pyrefly==1.4.0", "copier==9.18.2", "copier-template-extensions==0.3.3" ] diff --git a/src/copier_tasks/ensure_pnpm_minimum_release_age_exclude.py b/src/copier_tasks/ensure_pnpm_minimum_release_age_exclude.py index 964a2572e..afe9ca89c 100644 --- a/src/copier_tasks/ensure_pnpm_minimum_release_age_exclude.py +++ b/src/copier_tasks/ensure_pnpm_minimum_release_age_exclude.py @@ -38,8 +38,9 @@ def _existing_patterns(*, workspace_dir: Path) -> list[str]: parsed: object = json.loads(get_result.stdout) if not isinstance(parsed, list): return [] + entries: list[object] = parsed patterns: list[str] = [] - for entry in parsed: + for entry in entries: if not isinstance(entry, str): continue patterns.append(entry) diff --git a/template/.devcontainer/devcontainer.json.jinja b/template/.devcontainer/devcontainer.json.jinja index 175971f2b..6e33e7ac4 100644 --- a/template/.devcontainer/devcontainer.json.jinja +++ b/template/.devcontainer/devcontainer.json.jinja @@ -43,7 +43,7 @@ // Python "-ms-python.vscode-pylance", // pyrefly is the language server for this project; ms-python.python auto-installs Pylance via its extension pack, and Pylance then warns on every boot that it's disabled "ms-python.python@2026.5.2026052901", - "meta.pyrefly@1.3.2", + "meta.pyrefly@1.4.0", "ms-vscode-remote.remote-containers@0.414.0", "charliermarsh.ruff@2026.84.0",{% endraw %}{% endif %}{% raw %} {% endraw %}{% if is_child_of_copier_base_template is not defined and template_uses_vuejs is defined and template_uses_vuejs is sameas(true) %}{% raw %} diff --git a/template/.github/workflows/ci.yaml.jinja b/template/.github/workflows/ci.yaml.jinja index 7ada46cfd..2bb94cfb1 100644 --- a/template/.github/workflows/ci.yaml.jinja +++ b/template/.github/workflows/ci.yaml.jinja @@ -55,8 +55,14 @@ jobs: env: # The default branch always runs everything, so a class that was wrong on a feature branch or in the merge # queue is still caught there. A manual run is the way to get that full run for one branch. - RUN_EVERYTHING: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || github.event_name == 'workflow_dispatch' }} - run: echo "run_everything=$RUN_EVERYTHING" >> "$GITHUB_OUTPUT" + FORCED_BY: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && 'the default branch' || github.event_name == 'workflow_dispatch' && 'a manual run' || '' }} + run: | + echo "forced_by=$FORCED_BY" >> "$GITHUB_OUTPUT" + if [ -n "$FORCED_BY" ]; then + echo "run_everything=true" >> "$GITHUB_OUTPUT" + else + echo "run_everything=false" >> "$GITHUB_OUTPUT" + fi - name: Checkout code uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} @@ -76,24 +82,26 @@ jobs: id: resolve env: RUN_EVERYTHING: ${{ steps.policy.outputs.run_everything }} + FORCED_BY: ${{ steps.policy.outputs.forced_by }} # Holds file names from the PR, so it only ever travels through env and jq, never a shell expansion FILTER_OUTPUTS: ${{ toJSON(steps.filter.outputs) }} run: | # A code file that no other class claims forces every class on: an unclassified path can affect anything, so # nothing may be skipped because of it. classes=$(yq 'keys | .[]' .github/path-filters.yaml) - jq -n --arg run_everything "$RUN_EVERYTHING" --argjson outputs "$FILTER_OUTPUTS" --arg classes "$classes" ' + jq -n --arg run_everything "$RUN_EVERYTHING" --arg forced_by "$FORCED_BY" --argjson outputs "$FILTER_OUTPUTS" --arg classes "$classes" ' def files($class): $outputs[$class + "_files"] // "[]" | fromjson; ($classes | split("\n") | map(select(length > 0))) as $classes | (files("code") - ([$classes[] | select(. != "code") | files(.)] | add // [])) as $unclassified | ($run_everything == "true" or ($unclassified | length) > 0) as $force | { + forced_by: $forced_by, unclassified: $unclassified, classes: ([$classes[] | {key: ., value: ($force or $outputs[.] == "true")}] | from_entries) } ' > "$RUNNER_TEMP/resolved.json" echo "classes=$(jq -c .classes "$RUNNER_TEMP/resolved.json")" >> "$GITHUB_OUTPUT" - jq -r '"classes: \(.classes)", (if (.unclassified | length) > 0 then "every class forced on by unclassified paths:", .unclassified[] else empty end)' "$RUNNER_TEMP/resolved.json" | tee -a "$GITHUB_STEP_SUMMARY" + jq -r '"classes: \(.classes)", (if .forced_by != "" then "every class forced on by \(.forced_by)" elif (.unclassified | length) > 0 then "every class forced on by unclassified paths:", .unclassified[] else empty end)' "$RUNNER_TEMP/resolved.json" | tee -a "$GITHUB_STEP_SUMMARY" lint: needs: @@ -844,6 +852,7 @@ jobs: needs: - get-values - check-skip-duplicate + - changes - lint - unit-test-frontend - build-frontend{% endraw %}{% if has_backend %}{% raw %} diff --git a/uv.lock b/uv.lock index 241c8f672..d6b4b8905 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.12.7" [[package]] @@ -63,7 +63,7 @@ requires-dist = [ { name = "copier", specifier = "==9.18.2" }, { name = "copier-template-extensions", specifier = "==0.3.3" }, { name = "faker", specifier = ">=40.41.0" }, - { name = "pyrefly", specifier = ">=1.3.2" }, + { name = "pyrefly", specifier = "==1.4.0" }, { name = "pytest", specifier = ">=9.1.1" }, { name = "pytest-cov", specifier = ">=7.1.0" }, { name = "pytest-randomly", specifier = ">=5.0.0" }, @@ -411,21 +411,21 @@ wheels = [ [[package]] name = "pyrefly" -version = "1.3.2" -source = { registry = "https://pypi.org/simple/" } -sdist = { url = "https://files.pythonhosted.org/packages/4d/78/b6ec12acb586606618c71ebe1008af59e1ca50e667895f1946fa7b656619/pyrefly-1.3.2.tar.gz", hash = "sha256:30a68ff5429a1546ec08a2d913dfe442c9553a2940d274de18833d8eda939b5e", size = 6679591, upload-time = "2026-09-28T22:10:06.292Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/01/144a78839822abad55628881bcb45f8173e6d8b87976897c9105884b5a6b/pyrefly-1.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:626dbaee16ae1169a10a4ba3293583b5ab0320102f35efaad9e03457a2fc646c", size = 15088488, upload-time = "2026-09-28T22:09:40.743Z" }, - { url = "https://files.pythonhosted.org/packages/23/aa/599756bd42e6533ee9261ebd288f846ab453b00debaef2b4789893967809/pyrefly-1.3.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2d5fc704dbf0e990dc4f15cb1ead64f1a325ae27383a50c76cfcaf0387f926bb", size = 14429996, upload-time = "2026-09-28T22:09:43.316Z" }, - { url = "https://files.pythonhosted.org/packages/90/0c/4b0678889136b1514ecd25cec11974d5bfdaeaaf9f5fe8a766ca45ec4ab5/pyrefly-1.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89dbe8a33e735fa96023c33ce5dfd81af9b0bdd2ee771e8d69b2ebffc7e0fbc7", size = 14871810, upload-time = "2026-09-28T22:09:45.42Z" }, - { url = "https://files.pythonhosted.org/packages/a6/4e/a96236391c6bb0d8b50af85e0ea6c0c56206f739931ac3031f9e000325ee/pyrefly-1.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ec0c31715d48bff279f548b85a93b5bc43d2c7bd1492a8cd703a9185cb089e5", size = 16113462, upload-time = "2026-09-28T22:09:47.619Z" }, - { url = "https://files.pythonhosted.org/packages/ad/c3/d4d0966aa7008ebd8481a6d05aa53202ad40c9597705eeee87ba6e0e2025/pyrefly-1.3.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b3de468bed691e40dc6271284c8dcc05e649d7ed45280ef3d07332741efe34c", size = 16043341, upload-time = "2026-09-28T22:09:50.083Z" }, - { url = "https://files.pythonhosted.org/packages/4f/d1/52d9ea1b7048c7316d9ff7594c170b1c06d37547a75d140c8deea7b9ddfd/pyrefly-1.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb94365fe5f854cb4fe82f600c4e2453160d328610b12981004851ee0b6e8aeb", size = 15437183, upload-time = "2026-09-28T22:09:52.56Z" }, - { url = "https://files.pythonhosted.org/packages/08/ec/24f83e4a397fa9d5c416bb4d378ee1bb18fe48bdd5698fe541636f5352f3/pyrefly-1.3.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4f730d2e515ac25827fc68f343a7aa02b1072e0062df6a430346a4ffdfb5db2f", size = 14892976, upload-time = "2026-09-28T22:09:54.916Z" }, - { url = "https://files.pythonhosted.org/packages/29/c7/a473f8e9ec31ba0682ebe44f4e51167a22a4cbdc8bfd87365b9aca23be20/pyrefly-1.3.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:60f440e1626bd5cc4a41ee90cecba80796ed01ebd6c15d9f933efb8d07ffe2b8", size = 15469689, upload-time = "2026-09-28T22:09:57.541Z" }, - { url = "https://files.pythonhosted.org/packages/d0/df/779092365e8e469fc8e7349e1c26c35b8a73df9e6be80a00265fc26a2b2b/pyrefly-1.3.2-py3-none-win32.whl", hash = "sha256:b31684189de44a25a5bf00bdf3ad16be44a7b14654702d6018bce1043357202c", size = 14135550, upload-time = "2026-09-28T22:09:59.757Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/466333e7f2891f8c357b5c226559c39ba8dde4f90be09189ecc771a086f5/pyrefly-1.3.2-py3-none-win_amd64.whl", hash = "sha256:693960cdb247d1f76b9e09b3f2554dae20034f7d442ccd4e9b3e8962a3622363", size = 15067523, upload-time = "2026-09-28T22:10:02.099Z" }, - { url = "https://files.pythonhosted.org/packages/18/32/ee66df92c692c0686d82c9ecef3876ccb0386aa318bf507016f0121a8f78/pyrefly-1.3.2-py3-none-win_arm64.whl", hash = "sha256:f7424b21331670522980ff13e08bd143659879a4d92f4832282c7d7c41a9b5d6", size = 14348271, upload-time = "2026-09-28T22:10:04.253Z" }, +version = "1.4.0" +source = { registry = "https://pypi.org/simple/" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/3e/ea6893be167fa14307d7f9a924bed9ecbbbd91b01953890b9b3c58ed5359/pyrefly-1.4.0.tar.gz", hash = "sha256:bb57c004a4842cfe93f93aebb5203ca6209a5f0c8bda47e8bc289e70940fd910", size = 6962062, upload-time = "2026-10-15T16:54:11.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/17/dfce6611acc00cb26667e5e1011438cc510f3067f961e0f07f7a0c68f416/pyrefly-1.4.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:460cec39bc8743915e9c2c8c10ba926ad0b29b099d89c1859fb97e0059f42fa7", size = 15705624, upload-time = "2026-10-15T16:53:43.412Z" }, + { url = "https://files.pythonhosted.org/packages/a6/78/a7b96442123fa6ed733de0d1ea933e35d6fc6b7c9cf16e05279abb493493/pyrefly-1.4.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a6370d686b6cbbac8869832f09a52012888371956fbc68d446a349bc85ccd43", size = 14978669, upload-time = "2026-10-15T16:53:46.546Z" }, + { url = "https://files.pythonhosted.org/packages/23/97/dfcfec6a9d8948ab4b2746f7e87558a82bffabb7a54c22e7d3ce4bdc1932/pyrefly-1.4.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f1b16923a6583a6764e8aec07787e2ef4df644c7030294cba71339783a9451c", size = 15438858, upload-time = "2026-10-15T16:53:48.904Z" }, + { url = "https://files.pythonhosted.org/packages/31/12/6fa414aa87e398d6ddc7145a5d993b0975fc3511198dbb72c47873653ab6/pyrefly-1.4.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:688ad4cb4d6e711d67026fdcfb37b599f86061efe11ba5704287330e7041a69e", size = 16852241, upload-time = "2026-10-15T16:53:51.496Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/2d7312262cf6481d6ad46eed55b8f2592c3cff60f95cc2f30a114cd1484e/pyrefly-1.4.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5f3c9dddb6f3128beb49cf4e0f5cb88d4d17f039008d8e0c2be653966acd234", size = 16751917, upload-time = "2026-10-15T16:53:54.39Z" }, + { url = "https://files.pythonhosted.org/packages/2c/65/b44a1cb23667bcbcdc88558cbef79fa9e07a1152c2a1da148036b1a2e0d1/pyrefly-1.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6951f51687304caebfedf13dd1a943aeafdbc95abcc4e24a683b97467a77007", size = 16066345, upload-time = "2026-10-15T16:53:57.436Z" }, + { url = "https://files.pythonhosted.org/packages/a6/07/7ca6ff0c53e3a90ecdab05790d33f430fca5c48687613a0b182744166d98/pyrefly-1.4.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:838de4d4a8b9b8ed39bedd0b8b55629c69661d73a6fa3a3a4adff24b9dbeb399", size = 15461223, upload-time = "2026-10-15T16:54:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/61/be/ec7b2f38a898ce5b2219dda730e87d31fc8cfb2108a3d5844f9d590f1281/pyrefly-1.4.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:afb9ce2e0ae3e6106fb45f6d47536ac92b6423e601e08401cdefa850066444f5", size = 16119099, upload-time = "2026-10-15T16:54:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/6e/16/1b9549168b38a575d9c168e17b4b4ee30028d602ec0f7b07c198ef265dbe/pyrefly-1.4.0-py3-none-win32.whl", hash = "sha256:839db536d0ac29f1b23e2d974fb911075c2283e3e623755c45d826aafac175bc", size = 14858880, upload-time = "2026-10-15T16:54:05.114Z" }, + { url = "https://files.pythonhosted.org/packages/03/02/18f075d4de0e255f184f42fc28c1f0ecc9f49dd528678479fc90cf12246c/pyrefly-1.4.0-py3-none-win_amd64.whl", hash = "sha256:7aff37c2b711c8240ed650da0c6148237b327bf7e5dfacee65884f1adea22305", size = 15726220, upload-time = "2026-10-15T16:54:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/92/62/92947076681550904c72a9dd03b24e92694ede9a48548f5e487c849e66ea/pyrefly-1.4.0-py3-none-win_arm64.whl", hash = "sha256:88879f0cb749e5fea81d9ff4c2d5431a70349c458bcdf0b61de72c79214d90a5", size = 14943690, upload-time = "2026-10-15T16:54:09.689Z" }, ] [[package]] From 6db971dfae74e3ca1a538bd10d5a9fe5641d39ca Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:52:06 +0000 Subject: [PATCH 4/9] Copier update: let test classes declare attributes their fixtures assign (copier-base feat/ci-path-classes) --- .config/.copier-answers.yml | 2 +- .config/pyrefly.toml | 1 + template/.config/pyrefly.toml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.config/.copier-answers.yml b/.config/.copier-answers.yml index 5b9cb480a..200d9779a 100644 --- a/.config/.copier-answers.yml +++ b/.config/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v0.2.0-15-gd5eb03aa +_commit: v0.2.0-16-g6f7e8196 _src_path: gh:LabAutomationAndScreening/copier-base-template.git copier_answers_directory: .config description: A web app that is hosted within a local intranet. Nuxt frontend, python diff --git a/.config/pyrefly.toml b/.config/pyrefly.toml index 40fffc162..9a47eeea3 100644 --- a/.config/pyrefly.toml +++ b/.config/pyrefly.toml @@ -40,6 +40,7 @@ matches = "../**/tests/**" [sub-config.errors] implicitly-defined-attribute = "ignore" # doesn't make any sense for test cases. See https://github.com/microsoft/pyright/discussions/4834#discussioncomment-8496931 +uninitialized-instance-variable = "ignore" # test classes assign their attributes in pytest fixtures rather than __init__, so the class-level declarations those fixtures fill are never initialized where pyrefly looks # Tests are self-asserting: a value that isn't what the test assumed fails the very next assertion, # pointing at the exact line. The unknowns these rules flag also come almost entirely from mocking, where the value # genuinely is `Any` and no upstream typing fix is possible --- `spy_return` is set dynamically on the mock, diff --git a/template/.config/pyrefly.toml b/template/.config/pyrefly.toml index 89dcb1275..debec9ca1 100644 --- a/template/.config/pyrefly.toml +++ b/template/.config/pyrefly.toml @@ -42,6 +42,7 @@ matches = "../**/tests/**" [sub-config.errors] implicitly-defined-attribute = "ignore" # doesn't make any sense for test cases. See https://github.com/microsoft/pyright/discussions/4834#discussioncomment-8496931 +uninitialized-instance-variable = "ignore" # test classes assign their attributes in pytest fixtures rather than __init__, so the class-level declarations those fixtures fill are never initialized where pyrefly looks # Tests are self-asserting: a value that isn't what the test assumed fails the very next assertion, # pointing at the exact line. The unknowns these rules flag also come almost entirely from mocking, where the value # genuinely is `Any` and no upstream typing fix is possible --- `spy_return` is set dynamically on the mock, From 2bc521766cb2fad9118cc4d011e6ff722edc157c Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:03:31 +0000 Subject: [PATCH 5/9] Run backend E2E's ECR auth just before the tests, and gate frontend on the GraphQL schema e2e-test-backend authenticated to ECR before loading its image tarballs while e2e-test does it right before the test step, which is the only step that pulls from ECR. Both jobs now authenticate there. Frontend codegen reads backend/src/backend_api/graphql/schema.graphql, so a change confined to that schema now sets the frontend class, as the OpenAPI schema snapshot already does. --- template/.github/path-filters.yaml.jinja | 3 ++- template/.github/workflows/ci.yaml.jinja | 18 +++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/template/.github/path-filters.yaml.jinja b/template/.github/path-filters.yaml.jinja index cf9d49eb3..e9b06e9c5 100644 --- a/template/.github/path-filters.yaml.jinja +++ b/template/.github/path-filters.yaml.jinja @@ -22,7 +22,8 @@ frontend: - 'frontend/**' - 'pnpm-lock.yaml' - 'pnpm-workspace.yaml'{% if has_backend %} - - 'backend/tests/unit/__snapshots__/test_basic_server_functionality/test_openapi_schema.json' + - 'backend/tests/unit/__snapshots__/test_basic_server_functionality/test_openapi_schema.json'{% if frontend_uses_graphql %} + - 'backend/src/backend_api/graphql/schema.graphql'{% endif %} backend: - 'backend/**' - 'pytest.ini'{% endif %}{% if install_as_windows_service %} diff --git a/template/.github/workflows/ci.yaml.jinja b/template/.github/workflows/ci.yaml.jinja index 2bb94cfb1..e4392da4e 100644 --- a/template/.github/workflows/ci.yaml.jinja +++ b/template/.github/workflows/ci.yaml.jinja @@ -549,14 +549,7 @@ jobs: uses: actions/download-artifact@{% endraw %}{{ gha_download_artifact }}{% raw %} with: name: ${{ needs.build-backend.outputs.artifact-name }} - path: deployment-package-folder{% endraw %}{% if auth_to_ecr_for_backend_e2e_in_ci %}{% raw %} - - - name: ECR Auth to pull images for E2E tests - if: env.RUN_JOB == 'true' - uses: ./.github/actions/ecr-auth - with: - aws-region: {% endraw %}{{ auth_to_ecr_for_backend_e2e_in_ci_region }}{% raw %} - role-arn: {% endraw %}{{ auth_to_ecr_for_backend_e2e_in_ci_role_arn }}{% raw %}{% endraw %}{% endif %}{% raw %} + path: deployment-package-folder - name: Load docker images if: env.RUN_JOB == 'true' @@ -567,7 +560,14 @@ jobs: - name: Retag loaded backend image to stable local name if: env.RUN_JOB == 'true' - run: docker tag ${{ needs.build-backend.outputs.full-image-tag }} {% endraw %}{{ backend_image_name }}{% raw %}:local + run: docker tag ${{ needs.build-backend.outputs.full-image-tag }} {% endraw %}{{ backend_image_name }}{% raw %}:local{% endraw %}{% if auth_to_ecr_for_backend_e2e_in_ci %}{% raw %} + + - name: ECR Auth to pull images for E2E tests + if: env.RUN_JOB == 'true' + uses: ./.github/actions/ecr-auth + with: + aws-region: {% endraw %}{{ auth_to_ecr_for_backend_e2e_in_ci_region }}{% raw %} + role-arn: {% endraw %}{{ auth_to_ecr_for_backend_e2e_in_ci_role_arn }}{% raw %}{% endraw %}{% endif %}{% raw %} - name: E2E test if: env.RUN_JOB == 'true' From 235a09b55866d086d4960cd33b0aad2a58130780 Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:23:29 +0000 Subject: [PATCH 6/9] Copier update: clone full history in the changes job for private repositories (copier-base feat/ci-path-classes) --- .config/.copier-answers.yml | 2 +- template/.github/workflows/ci.yaml.jinja | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.config/.copier-answers.yml b/.config/.copier-answers.yml index 200d9779a..7c9c21fd3 100644 --- a/.config/.copier-answers.yml +++ b/.config/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v0.2.0-16-g6f7e8196 +_commit: v0.2.0-17-g177c4ae1 _src_path: gh:LabAutomationAndScreening/copier-base-template.git copier_answers_directory: .config description: A web app that is hosted within a local intranet. Nuxt frontend, python diff --git a/template/.github/workflows/ci.yaml.jinja b/template/.github/workflows/ci.yaml.jinja index e4392da4e..e2f5a1ce0 100644 --- a/template/.github/workflows/ci.yaml.jinja +++ b/template/.github/workflows/ci.yaml.jinja @@ -68,6 +68,13 @@ jobs: uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} with: persist-credentials: false + # On push and merge_group events dorny/paths-filter compares against a base commit through git. Without + # persisted credentials it cannot fetch that commit from a private repository, so the full history is cloned + # up front and the base is already present locally. + # TODO: a full clone gets slow once a repository's history is large. Consider fetching only the base commit + # instead: resolve it (merge_group.base_sha, or the merge base from the compare API on push), fetch it at + # depth 1 with a one-off auth header that is never persisted, and pass it to the filter as `base`. + fetch-depth: 0 - name: Match the changed files against the path classes if: steps.policy.outputs.run_everything != 'true' From 56f3774547c352ffe53fdfcf7413d50ab5013ccc Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:41:16 +0000 Subject: [PATCH 7/9] Copier update: say after a copier update how to split the commits (copier-base feat/ci-path-classes) --- .config/.copier-answers.yml | 2 +- .config/taskfiles/copier-template.yaml | 2 ++ template/.config/taskfiles/copier-template.yaml | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.config/.copier-answers.yml b/.config/.copier-answers.yml index 7c9c21fd3..d4a0f0564 100644 --- a/.config/.copier-answers.yml +++ b/.config/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v0.2.0-17-g177c4ae1 +_commit: v0.2.0-18-gdcea57c1 _src_path: gh:LabAutomationAndScreening/copier-base-template.git copier_answers_directory: .config description: A web app that is hosted within a local intranet. Nuxt frontend, python diff --git a/.config/taskfiles/copier-template.yaml b/.config/taskfiles/copier-template.yaml index cba155e35..1ca330876 100644 --- a/.config/taskfiles/copier-template.yaml +++ b/.config/taskfiles/copier-template.yaml @@ -28,6 +28,8 @@ tasks: internal: true cmds: - copier update --answers-file .config/.copier-answers.yml --trust --conflict rej --defaults {{.REF_FLAG}} + - | + echo "Reviewers should be able to skim the update commit and study the fixes. So one commit holds copier's output, the resolved .rej conflicts and every mechanical fix: lock files, formatting, the devcontainer hash (--no-verify if hooks still fail). Each hand-made fix for a remaining hook failure goes in a later commit." prune-copier-rejects: desc: Delete .rej files whose target file is unchanged, failing if any real conflict is left to resolve diff --git a/template/.config/taskfiles/copier-template.yaml b/template/.config/taskfiles/copier-template.yaml index cba155e35..1ca330876 100644 --- a/template/.config/taskfiles/copier-template.yaml +++ b/template/.config/taskfiles/copier-template.yaml @@ -28,6 +28,8 @@ tasks: internal: true cmds: - copier update --answers-file .config/.copier-answers.yml --trust --conflict rej --defaults {{.REF_FLAG}} + - | + echo "Reviewers should be able to skim the update commit and study the fixes. So one commit holds copier's output, the resolved .rej conflicts and every mechanical fix: lock files, formatting, the devcontainer hash (--no-verify if hooks still fail). Each hand-made fix for a remaining hook failure goes in a later commit." prune-copier-rejects: desc: Delete .rej files whose target file is unchanged, failing if any real conflict is left to resolve From 65707a94194f036e09092571752316b00ed39b71 Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:49:53 +0000 Subject: [PATCH 8/9] Suppress only missing-source on the pywin32 imports With types-pywin32 installed, pyrefly never reports missing-import for these modules, and listing unused-ignore inline has no effect: the win_service.py sub-config is what keeps the suppressions quiet on Windows, where pywin32 is installed and missing-source does not fire. --- ...install_as_windows_service %}win_service.py{% endif %} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/template/{% if has_backend %}backend{% endif %}/src/backend_api/entrypoint/{% if install_as_windows_service %}win_service.py{% endif %} b/template/{% if has_backend %}backend{% endif %}/src/backend_api/entrypoint/{% if install_as_windows_service %}win_service.py{% endif %} index 62fec994e..d26bcb365 100644 --- a/template/{% if has_backend %}backend{% endif %}/src/backend_api/entrypoint/{% if install_as_windows_service %}win_service.py{% endif %} +++ b/template/{% if has_backend %}backend{% endif %}/src/backend_api/entrypoint/{% if install_as_windows_service %}win_service.py{% endif %} @@ -8,10 +8,10 @@ from pathlib import Path from typing import override # pywin32 is win32-only, so the per-import missing-import ignores are required in CI/Linux but appear unnecessary on Windows dev machines. -import servicemanager # pyrefly: ignore[missing-import,missing-source,unused-ignore] # pywin32 only installs on Windows; its types-pywin32 stubs type it on every platform -import win32service # pyrefly: ignore[missing-import,missing-source,unused-ignore] # pywin32 only installs on Windows; its types-pywin32 stubs type it on every platform -import win32serviceutil # pyrefly: ignore[missing-import,missing-source,unused-ignore] # pywin32 only installs on Windows; its types-pywin32 stubs type it on every platform -import winerror # pyrefly: ignore[missing-import,missing-source,unused-ignore] # pywin32 only installs on Windows; its types-pywin32 stubs type it on every platform +import servicemanager # pyrefly: ignore[missing-source] # pywin32 only installs on Windows; its types-pywin32 stubs type it on every platform +import win32service # pyrefly: ignore[missing-source] # pywin32 only installs on Windows; its types-pywin32 stubs type it on every platform +import win32serviceutil # pyrefly: ignore[missing-source] # pywin32 only installs on Windows; its types-pywin32 stubs type it on every platform +import winerror # pyrefly: ignore[missing-source] # pywin32 only installs on Windows; its types-pywin32 stubs type it on every platform from .. import app_runner from ..jinja_constants import APP_NAME From 00c0c3d95ac481f82d67137ecccbfbd06a062db1 Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:03:22 +0000 Subject: [PATCH 9/9] Copier update: stop the PR template claiming CI runs every suite on every push (copier-base feat/ci-path-classes) --- .config/.copier-answers.yml | 2 +- .github/pull_request_template.md | 4 ++-- template/.github/pull_request_template.md | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.config/.copier-answers.yml b/.config/.copier-answers.yml index d4a0f0564..f8d9cee4b 100644 --- a/.config/.copier-answers.yml +++ b/.config/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v0.2.0-18-gdcea57c1 +_commit: v0.2.0-19-g06a3a062 _src_path: gh:LabAutomationAndScreening/copier-base-template.git copier_answers_directory: .config description: A web app that is hosted within a local intranet. Nuxt frontend, python diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index ac50221e1..eef920a86 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -19,8 +19,8 @@ +that they ran: CI runs every suite a change can affect. Re-running a suite only counts somewhere CI can't (a Raspberry +Pi, a lab PC). Don't list test names from the diff. --> ## Other diff --git a/template/.github/pull_request_template.md b/template/.github/pull_request_template.md index ac50221e1..eef920a86 100644 --- a/template/.github/pull_request_template.md +++ b/template/.github/pull_request_template.md @@ -19,8 +19,8 @@ +that they ran: CI runs every suite a change can affect. Re-running a suite only counts somewhere CI can't (a Raspberry +Pi, a lab PC). Don't list test names from the diff. --> ## Other