diff --git a/.config/.copier-answers.yml b/.config/.copier-answers.yml index e39af6fc..eb99c82e 100644 --- a/.config/.copier-answers.yml +++ b/.config/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v0.0.152 +_commit: v0.0.156 _src_path: gh:LabAutomationAndScreening/copier-base-template.git copier_answers_directory: .config description: Copier template for creating Python libraries and executables diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 278675ac..47ded81d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -19,11 +19,12 @@ "coderabbit.coderabbit-vscode@0.21.3", "ms-vscode.live-server@0.5.2025051301", "MS-vsliveshare.vsliveshare@1.0.5905", - "anthropic.claude-code@2.1.220", + "anthropic.claude-code@2.1.238", // 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.1.9003", + "meta.pyrefly@1.2.9002", "ms-vscode-remote.remote-containers@0.414.0", "charliermarsh.ruff@2026.68.0", @@ -60,5 +61,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): ad6af682 # spellchecker:disable-line + // Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): 6a68e568 # spellchecker:disable-line } diff --git a/.devcontainer/install-ci-tooling.py b/.devcontainer/install-ci-tooling.py index 1fc0dd98..e8a4ae6d 100644 --- a/.devcontainer/install-ci-tooling.py +++ b/.devcontainer/install-ci-tooling.py @@ -14,11 +14,11 @@ import tempfile from pathlib import Path -UV_VERSION = "0.12.3" -PNPM_VERSION = "11.21.0" +UV_VERSION = "0.12.5" +PNPM_VERSION = "11.22.0" COPIER_VERSION = "==9.17.1" COPIER_TEMPLATE_EXTENSIONS_VERSION = "==0.3.3" -PRE_COMMIT_VERSION = "4.5.1" +PRE_COMMIT_VERSION = "4.6.2" GITHUB_WINDOWS_RUNNER_BIN_PATH = r"C:\Users\runneradmin\.local\bin" INSTALL_SSM_PLUGIN_BY_DEFAULT = False parser = argparse.ArgumentParser(description="Install CI tooling for the repo") @@ -61,7 +61,7 @@ def main(): # invoke installer in a pwsh process _ = subprocess.run( # noqa: S603 # this is all our own input [ - pwsh, # type: ignore[reportPossiblyUnboundVariable] # this matches the conditional above that defines pwsh + pwsh, "-NoProfile", "-NonInteractive", "-Command", diff --git a/.github/actions/install_deps/action.yml b/.github/actions/install_deps/action.yml index c53fa042..b91d8b33 100644 --- a/.github/actions/install_deps/action.yml +++ b/.github/actions/install_deps/action.yml @@ -9,50 +9,44 @@ name: Install requirements description: Setup Python and/or Node, and install dependencies based on the devcontainer specification inputs: python-version: - type: string description: Python version to install required: false # if this is set, then assume you want python dependencies installed default: 'notUsing' node-version: - type: string description: Node version to install required: false # if this is set, then assume you want node dependencies installed default: 'notUsing' + package-json-file: + description: Relative path to a package.json to read the pnpm version from (its `packageManager` or `devEngines.packageManager` field). When unset, the pnpm version pinned by the template is used instead. + required: false + default: 'notUsing' install-deps: required: false default: true - type: boolean description: Whether to run the setup-deps script, or just to setup basic CI tooling skip-installing-ssm-plugin-manager: required: false default: false - type: boolean description: Whether to explicitly skip installing the SSM Plugin manager when setting up basic CI tooling project-dir: - type: string description: What's the relative path to the project? required: false default: ./ code-artifact-auth-role-name: - type: string description: What's the role name to use for CodeArtifact authentication? required: false default: no-code-artifact code-artifact-auth-role-account-id: - type: string description: What's the AWS Account ID that the role is in? required: false code-artifact-auth-region: - type: string description: What region should the role use? required: false skip-updating-devcontainer-hash: - type: boolean description: Whether to skip updating the hash when running manual-setup-deps.py default: true required: false skip-installing-pulumi-cli: - type: boolean description: Whether to skip installing the Pulumi CLI even if the lock file references it default: false required: false @@ -70,24 +64,41 @@ runs: - name: Setup python if: ${{ inputs.python-version != 'notUsing' }} - uses: actions/setup-python@v6.2.0 + uses: actions/setup-python@v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} - - name: Setup node + - name: Check for a pnpm lock file # pnpm/setup can only cache the store when there's a lock file at the repo root to key the cache off of + id: pnpm-lock-probe + if: ${{ inputs.node-version != 'notUsing' }} + run: | + if [ -f pnpm-lock.yaml ]; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + shell: bash + + - name: Setup pnpm and node # this action installs both pnpm and the node runtime, so actions/setup-node is not needed if: ${{ inputs.node-version != 'notUsing' }} - uses: actions/setup-node@v6.4.0 + uses: pnpm/setup@v2.0.2 with: - node-version: ${{ inputs.node-version }} + # an empty version makes pnpm/setup read it from package-json-file instead. Passing both a version and a package.json with a conflicting `packageManager` field is a hard error in pnpm/setup. + version: ${{ inputs.package-json-file == 'notUsing' && '11.22.0' || '' }} + package-json-file: ${{ inputs.package-json-file == 'notUsing' && 'package.json' || inputs.package-json-file }} + runtime: node@${{ inputs.node-version }} + cache: ${{ steps.pnpm-lock-probe.outputs.exists }} + install: false # dependency installation is handled by manual-setup-deps.py, which knows about every environment in the repo - name: Install tooling # the funky syntax is github action ternary - run: python .devcontainer/install-ci-tooling.py ${{ inputs.python-version == 'notUsing' && '--no-python' || '' }} ${{ inputs.node-version == 'notUsing' && '--no-node' || '' }} ${{ inputs.skip-installing-ssm-plugin-manager && '--skip-installing-ssm-plugin' || '' }} + # --no-node is always passed because the only node tooling this script installs is pnpm, which pnpm/setup above already installed + run: python .devcontainer/install-ci-tooling.py ${{ inputs.python-version == 'notUsing' && '--no-python' || '' }} --no-node ${{ inputs.skip-installing-ssm-plugin-manager && '--skip-installing-ssm-plugin' || '' }} shell: pwsh - name: OIDC Auth for CodeArtifact if: ${{ inputs.code-artifact-auth-role-name != 'no-code-artifact' }} - uses: aws-actions/configure-aws-credentials@v6.1.0 + uses: aws-actions/configure-aws-credentials@v6.2.3 with: role-to-assume: arn:aws:iam::${{ inputs.code-artifact-auth-role-account-id }}:role/${{ inputs.code-artifact-auth-role-name }} aws-region: ${{ inputs.code-artifact-auth-region }} diff --git a/.github/actions/update-devcontainer-hash/action.yml b/.github/actions/update-devcontainer-hash/action.yml index f892fe9b..c0d440e2 100644 --- a/.github/actions/update-devcontainer-hash/action.yml +++ b/.github/actions/update-devcontainer-hash/action.yml @@ -31,7 +31,7 @@ runs: shell: bash - name: Checkout code - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 with: persist-credentials: true fetch-depth: 1 diff --git a/.github/reusable_workflows/build-docker-image.yaml b/.github/reusable_workflows/build-docker-image.yaml index 507d8004..19d1738c 100644 --- a/.github/reusable_workflows/build-docker-image.yaml +++ b/.github/reusable_workflows/build-docker-image.yaml @@ -97,7 +97,7 @@ jobs: shell: bash - name: Checkout code - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 with: persist-credentials: false @@ -127,16 +127,30 @@ jobs: echo "Image name without slashes: ${IMAGE_NAME_NO_SLASHES}" echo "full_image_tag=${{ inputs.repository }}/${{ inputs.image_name }}:context-${BUILD_HASH}" >> "$GITHUB_OUTPUT" - - name: Set up mutex # Github concurrency management is horrible, things get arbitrarily cancelled if queued up. So using mutex until github fixes itself. When multiple jobs are modifying cache at once, weird things can happen. possible issue is https://github.com/actions/toolkit/issues/658 + - name: Test if docker image exists # deliberately before the mutex; describe-images is a read, so it is safe to run concurrently if: ${{ inputs.push-role-name != 'no-push' }} + id: check-if-exists + run: | + BUILD_HASH=${{ steps.calculate-build-context-hash.outputs.build_context_tag }} + echo Checking for : $BUILD_HASH + if aws ecr describe-images --region ${{ steps.parse_ecr_url.outputs.aws_region }} --registry-id=${{ steps.parse_ecr_url.outputs.aws_account_id }} --repository-name=${{ inputs.image_name }} --image-ids=imageTag=$BUILD_HASH; then \ + echo "Image was found in ECR"; \ + echo "status=found" >> $GITHUB_OUTPUT + else \ + echo "Image was not found in ECR"; \ + echo "status=notfound" >> $GITHUB_OUTPUT + fi + + - name: Set up mutex # Github concurrency management is horrible, things get arbitrarily cancelled if queued up. So using mutex until github fixes itself. When multiple jobs are modifying cache at once, weird things can happen. possible issue is https://github.com/actions/toolkit/issues/658 + if: ${{ inputs.push-role-name != 'no-push' && steps.check-if-exists.outputs.status == 'notfound' }} # only serialize when this job may have to build and push a new image uses: ben-z/gh-action-mutex@1ebad517141198e08d47cf72f3c0975316620a65 # v1.0.0-alpha.10 with: branch: mutex-${{ inputs.repository }}-${{ inputs.image_name }} timeout-minutes: 8 # this is the amount of time this action will wait to attempt to acquire the mutex lock before failing, e.g. if other jobs are queued up in front of it - - name: Test if docker image exists - if: ${{ inputs.push-role-name != 'no-push' }} - id: check-if-exists + - name: Re-test if docker image exists # another job may have pushed this exact build context while we waited on the mutex + if: ${{ inputs.push-role-name != 'no-push' && steps.check-if-exists.outputs.status == 'notfound' }} + id: recheck-if-exists run: | BUILD_HASH=${{ steps.calculate-build-context-hash.outputs.build_context_tag }} echo Checking for : $BUILD_HASH @@ -148,24 +162,34 @@ jobs: echo "status=notfound" >> $GITHUB_OUTPUT fi + - name: Resolve whether the image needs building # collapses the two probes into the single status the rest of the job keys off + if: ${{ inputs.push-role-name != 'no-push' }} + id: image-status + run: | + if [ "${{ steps.check-if-exists.outputs.status }}" = "found" ]; then + echo "status=found" >> $GITHUB_OUTPUT + else + echo "status=${{ steps.recheck-if-exists.outputs.status }}" >> $GITHUB_OUTPUT + fi + - name: Pull existing image to package as artifact - if: ${{ inputs.save-as-artifact && steps.check-if-exists.outputs.status == 'found' }} + if: ${{ inputs.save-as-artifact && steps.image-status.outputs.status == 'found' }} run: | docker pull ${{ steps.calculate-build-context-hash.outputs.full_image_tag }} - name: Set up Docker Buildx - if: ${{ (inputs.save-as-artifact && inputs.push-role-name == 'no-push') || steps.check-if-exists.outputs.status == 'notfound' }} + if: ${{ (inputs.save-as-artifact && inputs.push-role-name == 'no-push') || steps.image-status.outputs.status == 'notfound' }} uses: docker/setup-buildx-action@v4.0.0 with: version: v0.33.0 - name: Build Docker Image - if: ${{ (inputs.save-as-artifact && inputs.push-role-name == 'no-push') || steps.check-if-exists.outputs.status == 'notfound' }} + if: ${{ (inputs.save-as-artifact && inputs.push-role-name == 'no-push') || steps.image-status.outputs.status == 'notfound' }} uses: docker/build-push-action@v7.1.0 with: context: ${{ inputs.context }} build-contexts: ${{ inputs.build-contexts }} - push: ${{ inputs.push-role-name != 'no-push' && steps.check-if-exists.outputs.status == 'notfound' }} + push: ${{ inputs.push-role-name != 'no-push' && steps.image-status.outputs.status == 'notfound' }} load: ${{ inputs.save-as-artifact }} # make the image available later for the `docker save` step tags: ${{ steps.calculate-build-context-hash.outputs.full_image_tag }} @@ -177,12 +201,27 @@ jobs: echo "Tag $GIT_SHA_TAG already exists in ECR, skipping" else aws ecr batch-get-image --registry-id=${{ steps.parse_ecr_url.outputs.aws_account_id }} --repository-name=${{ inputs.image_name }} --image-ids imageTag=${{ steps.calculate-build-context-hash.outputs.build_context_tag }} --query 'images[].imageManifest' --output text > manifest.json - aws ecr put-image --registry-id=${{ steps.parse_ecr_url.outputs.aws_account_id }} --repository-name=${{ inputs.image_name }} --image-tag "$GIT_SHA_TAG" --image-manifest file://manifest.json + # Every job on the push path tags the git sha, and jobs that find their image already in ECR no longer serialize on the mutex, + # so another job can apply this same tag between the check above and the put below. ECR reports that as + # ImageAlreadyExistsException, which by definition means the tag already points at this exact manifest, so it is safe to ignore. + # ImageTagAlreadyExistsException (a different manifest on an immutable repository) is a real conflict and still fails the step. + if ! aws ecr put-image --registry-id=${{ steps.parse_ecr_url.outputs.aws_account_id }} --repository-name=${{ inputs.image_name }} --image-tag "$GIT_SHA_TAG" --image-manifest file://manifest.json 2> put-image-error.txt; then + cat put-image-error.txt + if ! grep -q 'ImageAlreadyExistsException' put-image-error.txt; then + exit 1 + fi + echo "Tag $GIT_SHA_TAG was applied concurrently by another job, nothing to do" + fi fi - name: Add tag for Production if: ${{ inputs.push-role-name != 'no-push' && inputs.tag-for-production }} run: | + # Unlike the git sha tag above, a concurrent ImageAlreadyExistsException is deliberately left fatal here. + # This step only runs from deploy workflows, which are not expected to overlap for the same image, so a + # duplicate put-image points at something unexpected about how the deploy was triggered and should be loud + # rather than swallowed. Note that PRODUCTION_CONTEXT_TAG keys on the build context hash rather than the + # commit, so two commits that leave the build context unchanged do share a tag. aws ecr batch-get-image --registry-id=${{ steps.parse_ecr_url.outputs.aws_account_id }} --repository-name=${{ inputs.image_name }} --image-ids imageTag=${{ steps.calculate-build-context-hash.outputs.build_context_tag }} --query 'images[].imageManifest' --output text > manifest.json PRODUCTION_CONTEXT_TAG="production--${{ steps.calculate-build-context-hash.outputs.build_context_tag }}" if aws ecr describe-images --region ${{ steps.parse_ecr_url.outputs.aws_region }} --registry-id=${{ steps.parse_ecr_url.outputs.aws_account_id }} --repository-name=${{ inputs.image_name }} --image-ids imageTag="$PRODUCTION_CONTEXT_TAG" > /dev/null 2>&1; then @@ -205,6 +244,9 @@ jobs: echo "Invalid release-tag: '$RELEASE_TAG'. Must start with alphanumeric or underscore and contain only alphanumerics, dots, hyphens, underscores (max 128 chars)." exit 1 fi + # This check is an intentional immutability guard rather than a concurrency one: reusing a version number is a + # human mistake worth failing on. A concurrent ImageAlreadyExistsException below is likewise left fatal, since + # release workflows are not expected to run twice at once for the same tag. if aws ecr describe-images --region ${{ steps.parse_ecr_url.outputs.aws_region }} --registry-id=${{ steps.parse_ecr_url.outputs.aws_account_id }} --repository-name=${{ inputs.image_name }} --image-ids imageTag="$RELEASE_TAG" > /dev/null 2>&1; then echo "Release tag '$RELEASE_TAG' already exists in ECR repository '${{ inputs.image_name }}'. Release tags are immutable; pick a new version or delete the existing tag if this was unintended." exit 1 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5dba5f64..89810dcb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -38,7 +38,7 @@ jobs: should-run: ${{ steps.check.outputs.should-run }} steps: - name: Checkout code - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 with: persist-credentials: false - id: check @@ -78,7 +78,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 with: persist-credentials: false @@ -122,7 +122,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 with: persist-credentials: false @@ -183,22 +183,34 @@ jobs: git add . git status + - name: Compute Pre-commit cache key # must run after the instantiated template is in place, since hashFiles reads its .pre-commit-config.yaml + run: | + echo "CACHE_RESTORE_KEY=${{ matrix.os }}-${{ matrix.python-version }}-build-cache-pre-commit-hooks-" >> "$GITHUB_ENV" + echo "CACHE_KEY=${{ matrix.os }}-${{ matrix.python-version }}-build-cache-pre-commit-hooks-${{ hashFiles('.pre-commit-config.yaml') }}" >> "$GITHUB_ENV" + + - name: Restore Pre-commit hooks cache # no mutex needed; concurrent cache reads are safe, only writes cause trouble + id: cache-probe + uses: actions/cache/restore@v6.1.0 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: ${{ env.CACHE_KEY }} + restore-keys: ${{ env.CACHE_RESTORE_KEY }} + - name: Set up mutex # Github concurrency management is horrible, things get arbitrarily cancelled if queued up. So using mutex until github fixes itself. When multiple jobs are modifying cache at once, weird things can happen. possible issue is https://github.com/actions/toolkit/issues/658 - if: ${{ runner.os != 'Windows' }} # we're just gonna have to YOLO on Windows, because this action doesn't support it yet https://github.com/ben-z/gh-action-mutex/issues/14 + if: ${{ runner.os != 'Windows' && steps.cache-probe.outputs.cache-hit != 'true' }} # we're just gonna have to YOLO on Windows, because this action doesn't support it yet https://github.com/ben-z/gh-action-mutex/issues/14. Only serialize when this job will have to write a new cache entry. uses: ben-z/gh-action-mutex@1ebad517141198e08d47cf72f3c0975316620a65 # v1.0.0-alpha.10 with: branch: mutex-venv-${{ matrix.os }}-${{ matrix.python-version }} timeout-minutes: 8 # this is the amount of time this action will wait to attempt to acquire the mutex lock before failing, e.g. if other jobs are queued up in front of it - - name: Cache Pre-commit hooks - uses: actions/cache@v5.0.5 - env: - cache-name: cache-pre-commit-hooks + - name: Re-restore Pre-commit hooks cache # another job may have created the exact-key cache while we waited on the mutex + id: cache-recheck + if: ${{ steps.cache-probe.outputs.cache-hit != 'true' }} + uses: actions/cache/restore@v6.1.0 with: path: ${{ env.PRE_COMMIT_HOME }} - key: ${{ matrix.os }}-${{ matrix.python-version }}-build-${{ env.cache-name }}-${{ hashFiles('.pre-commit-config.yaml') }} - restore-keys: | - ${{ matrix.os }}-${{ matrix.python-version }}-build-${{ env.cache-name }}- + key: ${{ env.CACHE_KEY }} + restore-keys: ${{ env.CACHE_RESTORE_KEY }} - name: Run pre-commit run: | @@ -211,6 +223,13 @@ jobs: exit $PRE_COMMIT_EXIT_CODE fi + - name: Save Pre-commit hooks cache + if: ${{ steps.cache-probe.outputs.cache-hit != 'true' && steps.cache-recheck.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v6.1.0 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: ${{ env.CACHE_KEY }} + - name: Upload pre-commit log if failure if: ${{ failure() }} uses: actions/upload-artifact@v4.4.3 diff --git a/.github/workflows/confirm-on-tagged-copier-template.yaml b/.github/workflows/confirm-on-tagged-copier-template.yaml index 1195dd24..7cefadaf 100644 --- a/.github/workflows/confirm-on-tagged-copier-template.yaml +++ b/.github/workflows/confirm-on-tagged-copier-template.yaml @@ -24,7 +24,7 @@ jobs: ANSWERS_FILE_PATTERN: ${{ inputs.answers_file_pattern }} steps: - name: Checkout code - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/get-values.yaml b/.github/workflows/get-values.yaml index 44dccbe1..ef2f2057 100644 --- a/.github/workflows/get-values.yaml +++ b/.github/workflows/get-values.yaml @@ -42,7 +42,7 @@ jobs: JSON - name: Checkout code - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 99015207..5c641fa4 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -40,14 +40,14 @@ jobs: steps: - name: Checkout code during push if: ${{ github.event_name == 'push' }} - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 with: ref: ${{ github.ref_name }} # explicitly get the head of the branch, which will include any new commits pushed if this is a dependabot branch persist-credentials: false - name: Checkout code not during push if: ${{ github.event_name != 'push' }} - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 with: persist-credentials: false @@ -61,22 +61,34 @@ jobs: skip-installing-ssm-plugin-manager: true skip-installing-pulumi-cli: true + - name: Compute Pre-commit cache key # must run after checkout, since hashFiles needs the file on disk + run: | + echo "CACHE_RESTORE_KEY=ubuntu-24.04-py${{ inputs.python-version }}-node-${{ inputs.node-version}}-cache-pre-commit-hooks-" >> "$GITHUB_ENV" + echo "CACHE_KEY=ubuntu-24.04-py${{ inputs.python-version }}-node-${{ inputs.node-version}}-cache-pre-commit-hooks-${{ hashFiles('.pre-commit-config.yaml') }}" >> "$GITHUB_ENV" + + - name: Restore Pre-commit hooks cache # no mutex needed; concurrent cache reads are safe, only writes cause trouble + id: cache-probe + uses: actions/cache/restore@v6.1.0 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: ${{ env.CACHE_KEY }} + restore-keys: ${{ env.CACHE_RESTORE_KEY }} + - name: Set up mutex # Github concurrency management is horrible, things get arbitrarily cancelled if queued up. So using mutex until github fixes itself. When multiple jobs are modifying cache at once, weird things can happen. possible issue is https://github.com/actions/toolkit/issues/658 - if: ${{ runner.os != 'Windows' }} # we're just gonna have to YOLO on Windows, because this action doesn't support it yet https://github.com/ben-z/gh-action-mutex/issues/14 + if: ${{ runner.os != 'Windows' && steps.cache-probe.outputs.cache-hit != 'true' }} # we're just gonna have to YOLO on Windows, because this action doesn't support it yet https://github.com/ben-z/gh-action-mutex/issues/14. Only serialize when this job will have to write a new cache entry. uses: ben-z/gh-action-mutex@1ebad517141198e08d47cf72f3c0975316620a65 # v1.0.0-alpha.10 with: branch: mutex-venv-ubuntu-24.04-py${{ inputs.python-version }}-nodejs-${{ inputs.node-version }} timeout-minutes: 8 # this is the amount of time this action will wait to attempt to acquire the mutex lock before failing, e.g. if other jobs are queued up in front of it - - name: Cache Pre-commit hooks - uses: actions/cache@v5.0.5 - env: - cache-name: cache-pre-commit-hooks + - name: Re-restore Pre-commit hooks cache # another job may have created the exact-key cache while we waited on the mutex + id: cache-recheck + if: ${{ steps.cache-probe.outputs.cache-hit != 'true' }} + uses: actions/cache/restore@v6.1.0 with: path: ${{ env.PRE_COMMIT_HOME }} - key: ubuntu-24.04-py${{ inputs.python-version }}-node-${{ inputs.node-version}}-${{ env.cache-name }}-${{ hashFiles('.pre-commit-config.yaml') }} - restore-keys: | - ubuntu-24.04-py${{ inputs.python-version }}-node-${{ inputs.node-version}}-${{ env.cache-name }}- + key: ${{ env.CACHE_KEY }} + restore-keys: ${{ env.CACHE_RESTORE_KEY }} - name: Run pre-commit run: | @@ -87,3 +99,10 @@ jobs: git --no-pager diff exit $PRE_COMMIT_EXIT_CODE fi + + - name: Save Pre-commit hooks cache + if: ${{ steps.cache-probe.outputs.cache-hit != 'true' && steps.cache-recheck.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v6.1.0 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: ${{ env.CACHE_KEY }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 69d39e12..7e6b8b5c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ # You are welcome to make changes to this file in your repo if they are custom to your project, # but if the change should be shared with other projects, please backport it to the template repo. # ===================================================================================================== -minimum_pre_commit_version: 4.3.0 +minimum_pre_commit_version: 4.5.1 # run `pre-commit autoupdate --freeze` to update all hooks default_install_hook_types: [pre-commit, post-checkout] repos: @@ -95,6 +95,7 @@ repos: .*generated/graphql.ts| .*/generated/open[-_]api/.*| .*/schema.graphql| + .*pyrightconfig\.json| )$ - repo: https://github.com/rbubley/mirrors-prettier @@ -151,6 +152,7 @@ repos: .devcontainer/devcontainer.json| .devcontainer/devcontainer-lock.json| .*devcontainer.json| + .*pyrightconfig\.json| .*tsconfig.json| )$ - id: check-yaml diff --git a/AGENTS.md b/AGENTS.md index b871f7e0..3b93ea8a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,7 @@ This project is a Copier template used to generate other copier templates. It is - Comments should be used very rarely. Code should generally express its intent. - Never write a one-line docstring — either the name is sufficient or the behavior warrants a full explanation. +- Don't name a value that is used once where the literal is already self-explanatory at the point of use — the extra variable adds a line and a hop without adding meaning. - Prefer keyword-only parameters (unless a very clear single-argument function): use `*` in Python signatures and destructured options objects in TypeScript. - When disabling a linting rule with an inline directive, provide a comment at the end of the line (or on the line above for tools that don't allow extra text after an inline directive) describing the reasoning for disabling the rule. - Avoid telling the type checker what a type is rather than letting it prove it. This includes type assertions (`as SomeType` in TypeScript, `cast()` in Python) and variable annotations that override inference. Prefer approaches that let the type checker verify the type itself: `isinstance`/`instanceof` narrowing, restructuring code so the correct type flows naturally, or using discriminated unions. When there is genuinely no alternative, add a comment explaining why the workaround is necessary and why it is safe. diff --git a/extensions/context.py b/extensions/context.py index 41019b02..58369346 100644 --- a/extensions/context.py +++ b/extensions/context.py @@ -19,11 +19,11 @@ class ContextUpdater(ContextHook): def hook( # noqa: PLR0915 # yes, this is a lot of statements, but it's all just creating the dict self, context: dict[Any, Any] ) -> dict[Any, Any]: - context["uv_version"] = "0.12.3" - context["pnpm_version"] = "11.21.0" + context["uv_version"] = "0.12.5" + context["pnpm_version"] = "11.22.0" context["npm_version"] = "11.13.0" context["nvm_version"] = "0.40.5" - context["pre_commit_version"] = "4.5.1" + context["pre_commit_version"] = "4.6.2" context["pytest_version"] = ">=9.1.1" context["pytest_randomly_version"] = ">=4.1.0" context["pytest_cov_version"] = ">=7.1.0" @@ -39,12 +39,12 @@ def hook( # noqa: PLR0915 # yes, this is a lot of statements, but it's all just context["boto3_version"] = ">=1.42.92" context["ephemeral_pulumi_deploy_version"] = ">=0.0.7" context["pydantic_version"] = ">=2.13.4" - context["pyinstaller_version"] = ">=6.20.0" + context["pyinstaller_version"] = ">=6.22.2" context["setuptools_version"] = "80.7.1" context["strawberry_graphql_version"] = "==0.298.0" context["fastapi_version"] = ">=0.141.1" context["fastapi_offline_version"] = ">=1.7.7" - context["starlette_version"] = ">=1.3.1" + context["starlette_version"] = ">=1.6.0" context["uvicorn_version"] = ">=0.52.3" context["lab_auto_pulumi_version"] = ">=0.2.3" context["ariadne_codegen_version"] = ">=0.18.0" @@ -62,6 +62,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.36.0" context["mutmut_version"] = ">=3.7.0" + context["pyright_version"] = ">=1.1.411" context["pyrefly_version"] = ">=1.2.0" context["vacuum_openapi_version"] = "0.30.0" @@ -101,18 +102,19 @@ def hook( # noqa: PLR0915 # yes, this is a lot of statements, but it's all just context["unplugin_auto_import_version"] = "^21.0.0" context["openapi_types_version"] = "^12.1.3" - context["gha_checkout"] = "v6.0.2" - context["gha_setup_python"] = "v6.2.0" - context["gha_cache"] = "v5.0.5" + context["gha_checkout"] = "v7.0.1" + context["gha_setup_python"] = "v7.0.0" + context["gha_cache"] = "v6.1.0" context["gha_upload_artifact"] = "v7.0.1" context["gha_download_artifact"] = "v8.0.1" context["gha_github_script"] = "v7.0.1" context["gha_setup_buildx"] = "v4.0.0" context["buildx_version"] = "v0.33.0" context["gha_docker_build_push"] = "v7.1.0" - context["gha_configure_aws_credentials"] = "v6.1.0" - context["gha_amazon_ecr_login"] = "v2.1.5" + context["gha_configure_aws_credentials"] = "v6.2.3" + context["gha_amazon_ecr_login"] = "v2.1.7" context["gha_setup_node"] = "v6.4.0" + context["gha_pnpm_setup"] = "v2.0.2" context["gha_action_gh_release"] = "v3.0.0" context["gha_codecov"] = "v7.0.0" context["gha_mutex"] = "1ebad517141198e08d47cf72f3c0975316620a65 # v1.0.0-alpha.10" diff --git a/template/.devcontainer/devcontainer.json.jinja b/template/.devcontainer/devcontainer.json.jinja index 4ffdca55..ab4d25da 100644 --- a/template/.devcontainer/devcontainer.json.jinja +++ b/template/.devcontainer/devcontainer.json.jinja @@ -37,11 +37,12 @@ "coderabbit.coderabbit-vscode@0.21.3", "ms-vscode.live-server@0.5.2025051301", "MS-vsliveshare.vsliveshare@1.0.5905",{% endraw %}{% if install_claude_cli %}{% raw %} - "anthropic.claude-code@2.1.220",{% endraw %}{% endif %}{% raw %}{% endraw %}{% if (is_child_of_copier_base_template is defined and is_child_of_copier_base_template is sameas(true)) or (is_child_of_copier_base_template is not defined and template_uses_python is defined and template_uses_python is sameas(true)) %}{% raw %} + "anthropic.claude-code@2.1.238",{% endraw %}{% endif %}{% raw %}{% endraw %}{% if (is_child_of_copier_base_template is defined and is_child_of_copier_base_template is sameas(true)) or (is_child_of_copier_base_template is not defined and template_uses_python is defined and template_uses_python is sameas(true)) %}{% raw %} // 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.1.9003", + "meta.pyrefly@1.2.9002", "ms-vscode-remote.remote-containers@0.414.0", "charliermarsh.ruff@2026.68.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/.devcontainer/install-ci-tooling.py.jinja b/template/.devcontainer/install-ci-tooling.py.jinja index 24ef5bd0..13f85fff 100644 --- a/template/.devcontainer/install-ci-tooling.py.jinja +++ b/template/.devcontainer/install-ci-tooling.py.jinja @@ -54,7 +54,7 @@ def main(): # invoke installer in a pwsh process _ = subprocess.run( # noqa: S603 # this is all our own input [ - pwsh, # type: ignore[reportPossiblyUnboundVariable] # this matches the conditional above that defines pwsh + pwsh, "-NoProfile", "-NonInteractive", "-Command", diff --git a/template/.github/actions/install_deps/action.yml b/template/.github/actions/install_deps/action.yml index c53fa042..b91d8b33 100644 --- a/template/.github/actions/install_deps/action.yml +++ b/template/.github/actions/install_deps/action.yml @@ -9,50 +9,44 @@ name: Install requirements description: Setup Python and/or Node, and install dependencies based on the devcontainer specification inputs: python-version: - type: string description: Python version to install required: false # if this is set, then assume you want python dependencies installed default: 'notUsing' node-version: - type: string description: Node version to install required: false # if this is set, then assume you want node dependencies installed default: 'notUsing' + package-json-file: + description: Relative path to a package.json to read the pnpm version from (its `packageManager` or `devEngines.packageManager` field). When unset, the pnpm version pinned by the template is used instead. + required: false + default: 'notUsing' install-deps: required: false default: true - type: boolean description: Whether to run the setup-deps script, or just to setup basic CI tooling skip-installing-ssm-plugin-manager: required: false default: false - type: boolean description: Whether to explicitly skip installing the SSM Plugin manager when setting up basic CI tooling project-dir: - type: string description: What's the relative path to the project? required: false default: ./ code-artifact-auth-role-name: - type: string description: What's the role name to use for CodeArtifact authentication? required: false default: no-code-artifact code-artifact-auth-role-account-id: - type: string description: What's the AWS Account ID that the role is in? required: false code-artifact-auth-region: - type: string description: What region should the role use? required: false skip-updating-devcontainer-hash: - type: boolean description: Whether to skip updating the hash when running manual-setup-deps.py default: true required: false skip-installing-pulumi-cli: - type: boolean description: Whether to skip installing the Pulumi CLI even if the lock file references it default: false required: false @@ -70,24 +64,41 @@ runs: - name: Setup python if: ${{ inputs.python-version != 'notUsing' }} - uses: actions/setup-python@v6.2.0 + uses: actions/setup-python@v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} - - name: Setup node + - name: Check for a pnpm lock file # pnpm/setup can only cache the store when there's a lock file at the repo root to key the cache off of + id: pnpm-lock-probe + if: ${{ inputs.node-version != 'notUsing' }} + run: | + if [ -f pnpm-lock.yaml ]; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + shell: bash + + - name: Setup pnpm and node # this action installs both pnpm and the node runtime, so actions/setup-node is not needed if: ${{ inputs.node-version != 'notUsing' }} - uses: actions/setup-node@v6.4.0 + uses: pnpm/setup@v2.0.2 with: - node-version: ${{ inputs.node-version }} + # an empty version makes pnpm/setup read it from package-json-file instead. Passing both a version and a package.json with a conflicting `packageManager` field is a hard error in pnpm/setup. + version: ${{ inputs.package-json-file == 'notUsing' && '11.22.0' || '' }} + package-json-file: ${{ inputs.package-json-file == 'notUsing' && 'package.json' || inputs.package-json-file }} + runtime: node@${{ inputs.node-version }} + cache: ${{ steps.pnpm-lock-probe.outputs.exists }} + install: false # dependency installation is handled by manual-setup-deps.py, which knows about every environment in the repo - name: Install tooling # the funky syntax is github action ternary - run: python .devcontainer/install-ci-tooling.py ${{ inputs.python-version == 'notUsing' && '--no-python' || '' }} ${{ inputs.node-version == 'notUsing' && '--no-node' || '' }} ${{ inputs.skip-installing-ssm-plugin-manager && '--skip-installing-ssm-plugin' || '' }} + # --no-node is always passed because the only node tooling this script installs is pnpm, which pnpm/setup above already installed + run: python .devcontainer/install-ci-tooling.py ${{ inputs.python-version == 'notUsing' && '--no-python' || '' }} --no-node ${{ inputs.skip-installing-ssm-plugin-manager && '--skip-installing-ssm-plugin' || '' }} shell: pwsh - name: OIDC Auth for CodeArtifact if: ${{ inputs.code-artifact-auth-role-name != 'no-code-artifact' }} - uses: aws-actions/configure-aws-credentials@v6.1.0 + uses: aws-actions/configure-aws-credentials@v6.2.3 with: role-to-assume: arn:aws:iam::${{ inputs.code-artifact-auth-role-account-id }}:role/${{ inputs.code-artifact-auth-role-name }} aws-region: ${{ inputs.code-artifact-auth-region }} diff --git a/template/.github/actions/update-devcontainer-hash/action.yml b/template/.github/actions/update-devcontainer-hash/action.yml index f892fe9b..c0d440e2 100644 --- a/template/.github/actions/update-devcontainer-hash/action.yml +++ b/template/.github/actions/update-devcontainer-hash/action.yml @@ -31,7 +31,7 @@ runs: shell: bash - name: Checkout code - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 with: persist-credentials: true fetch-depth: 1 diff --git a/template/.github/workflows/confirm-on-tagged-copier-template.yaml b/template/.github/workflows/confirm-on-tagged-copier-template.yaml index 1195dd24..7cefadaf 100644 --- a/template/.github/workflows/confirm-on-tagged-copier-template.yaml +++ b/template/.github/workflows/confirm-on-tagged-copier-template.yaml @@ -24,7 +24,7 @@ jobs: ANSWERS_FILE_PATTERN: ${{ inputs.answers_file_pattern }} steps: - name: Checkout code - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 with: persist-credentials: false diff --git a/template/.github/workflows/get-values.yaml b/template/.github/workflows/get-values.yaml index 44dccbe1..ef2f2057 100644 --- a/template/.github/workflows/get-values.yaml +++ b/template/.github/workflows/get-values.yaml @@ -42,7 +42,7 @@ jobs: JSON - name: Checkout code - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 with: persist-credentials: false diff --git a/template/.github/workflows/pre-commit.yaml b/template/.github/workflows/pre-commit.yaml index 99015207..5c641fa4 100644 --- a/template/.github/workflows/pre-commit.yaml +++ b/template/.github/workflows/pre-commit.yaml @@ -40,14 +40,14 @@ jobs: steps: - name: Checkout code during push if: ${{ github.event_name == 'push' }} - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 with: ref: ${{ github.ref_name }} # explicitly get the head of the branch, which will include any new commits pushed if this is a dependabot branch persist-credentials: false - name: Checkout code not during push if: ${{ github.event_name != 'push' }} - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 with: persist-credentials: false @@ -61,22 +61,34 @@ jobs: skip-installing-ssm-plugin-manager: true skip-installing-pulumi-cli: true + - name: Compute Pre-commit cache key # must run after checkout, since hashFiles needs the file on disk + run: | + echo "CACHE_RESTORE_KEY=ubuntu-24.04-py${{ inputs.python-version }}-node-${{ inputs.node-version}}-cache-pre-commit-hooks-" >> "$GITHUB_ENV" + echo "CACHE_KEY=ubuntu-24.04-py${{ inputs.python-version }}-node-${{ inputs.node-version}}-cache-pre-commit-hooks-${{ hashFiles('.pre-commit-config.yaml') }}" >> "$GITHUB_ENV" + + - name: Restore Pre-commit hooks cache # no mutex needed; concurrent cache reads are safe, only writes cause trouble + id: cache-probe + uses: actions/cache/restore@v6.1.0 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: ${{ env.CACHE_KEY }} + restore-keys: ${{ env.CACHE_RESTORE_KEY }} + - name: Set up mutex # Github concurrency management is horrible, things get arbitrarily cancelled if queued up. So using mutex until github fixes itself. When multiple jobs are modifying cache at once, weird things can happen. possible issue is https://github.com/actions/toolkit/issues/658 - if: ${{ runner.os != 'Windows' }} # we're just gonna have to YOLO on Windows, because this action doesn't support it yet https://github.com/ben-z/gh-action-mutex/issues/14 + if: ${{ runner.os != 'Windows' && steps.cache-probe.outputs.cache-hit != 'true' }} # we're just gonna have to YOLO on Windows, because this action doesn't support it yet https://github.com/ben-z/gh-action-mutex/issues/14. Only serialize when this job will have to write a new cache entry. uses: ben-z/gh-action-mutex@1ebad517141198e08d47cf72f3c0975316620a65 # v1.0.0-alpha.10 with: branch: mutex-venv-ubuntu-24.04-py${{ inputs.python-version }}-nodejs-${{ inputs.node-version }} timeout-minutes: 8 # this is the amount of time this action will wait to attempt to acquire the mutex lock before failing, e.g. if other jobs are queued up in front of it - - name: Cache Pre-commit hooks - uses: actions/cache@v5.0.5 - env: - cache-name: cache-pre-commit-hooks + - name: Re-restore Pre-commit hooks cache # another job may have created the exact-key cache while we waited on the mutex + id: cache-recheck + if: ${{ steps.cache-probe.outputs.cache-hit != 'true' }} + uses: actions/cache/restore@v6.1.0 with: path: ${{ env.PRE_COMMIT_HOME }} - key: ubuntu-24.04-py${{ inputs.python-version }}-node-${{ inputs.node-version}}-${{ env.cache-name }}-${{ hashFiles('.pre-commit-config.yaml') }} - restore-keys: | - ubuntu-24.04-py${{ inputs.python-version }}-node-${{ inputs.node-version}}-${{ env.cache-name }}- + key: ${{ env.CACHE_KEY }} + restore-keys: ${{ env.CACHE_RESTORE_KEY }} - name: Run pre-commit run: | @@ -87,3 +99,10 @@ jobs: git --no-pager diff exit $PRE_COMMIT_EXIT_CODE fi + + - name: Save Pre-commit hooks cache + if: ${{ steps.cache-probe.outputs.cache-hit != 'true' && steps.cache-recheck.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v6.1.0 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: ${{ env.CACHE_KEY }} diff --git a/template/.pre-commit-config.yaml b/template/.pre-commit-config.yaml index 69d39e12..b9a33c51 100644 --- a/template/.pre-commit-config.yaml +++ b/template/.pre-commit-config.yaml @@ -5,7 +5,7 @@ # You are welcome to make changes to this file in your repo if they are custom to your project, # but if the change should be shared with other projects, please backport it to the template repo. # ===================================================================================================== -minimum_pre_commit_version: 4.3.0 +minimum_pre_commit_version: 4.5.1 # run `pre-commit autoupdate --freeze` to update all hooks default_install_hook_types: [pre-commit, post-checkout] repos: @@ -95,6 +95,7 @@ repos: .*generated/graphql.ts| .*/generated/open[-_]api/.*| .*/schema.graphql| + .*pyrightconfig\.json| )$ - repo: https://github.com/rbubley/mirrors-prettier @@ -151,6 +152,7 @@ repos: .devcontainer/devcontainer.json| .devcontainer/devcontainer-lock.json| .*devcontainer.json| + .*pyrightconfig\.json| .*tsconfig.json| )$ - id: check-yaml @@ -293,6 +295,25 @@ repos: # print the number of files as a sanity-check verbose: true + - repo: local + hooks: + - id: pyright-on-tests + name: Run pyright on tests of the library # to mimic a downstream consumer of the library who uses pyright as their type checker, see https://pyrefly.org/blog/too-many-type-checkers/ + entry: bash -c "uv run pyright" + files: '(.+\.py|pyproject\.toml|pyrightconfig\.json)$' + exclude: | + (?x)^( + .*/generated/.*| + )$ + # don't pass filenames else the command line sees them twice + pass_filenames: false + language: system + types: [python] + # use require_serial so that script is only called once per commit + require_serial: true + # print the number of files as a sanity-check + verbose: true + # Updating repo config/tooling files - repo: local hooks: diff --git a/template/AGENTS.md b/template/AGENTS.md index 3b46af25..21e0aef6 100644 --- a/template/AGENTS.md +++ b/template/AGENTS.md @@ -8,6 +8,7 @@ This project is a Python library. - Comments should be used very rarely. Code should generally express its intent. - Never write a one-line docstring — either the name is sufficient or the behavior warrants a full explanation. +- Don't name a value that is used once where the literal is already self-explanatory at the point of use — the extra variable adds a line and a hop without adding meaning. - Prefer keyword-only parameters (unless a very clear single-argument function): use `*` in Python signatures and destructured options objects in TypeScript. - When disabling a linting rule with an inline directive, provide a comment at the end of the line (or on the line above for tools that don't allow extra text after an inline directive) describing the reasoning for disabling the rule. - Avoid telling the type checker what a type is rather than letting it prove it. This includes type assertions (`as SomeType` in TypeScript, `cast()` in Python) and variable annotations that override inference. Prefer approaches that let the type checker verify the type itself: `isinstance`/`instanceof` narrowing, restructuring code so the correct type flows naturally, or using discriminated unions. When there is genuinely no alternative, add a comment explaining why the workaround is necessary and why it is safe. diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja index 10bb75a3..0e27747d 100644 --- a/template/pyproject.toml.jinja +++ b/template/pyproject.toml.jinja @@ -25,6 +25,7 @@ dev = [ # Managed by upstream template + "pyright{% endraw %}{{ pyright_version }}{% raw %}", "pyrefly{% endraw %}{{ pyrefly_version }}{% raw %}", "pytest{% endraw %}{{ pytest_version }}{% raw %}", "pytest-cov{% endraw %}{{ pytest_cov_version }}{% raw %}", diff --git a/template/pyrightconfig.json b/template/pyrightconfig.json new file mode 100644 index 00000000..4a0f8108 --- /dev/null +++ b/template/pyrightconfig.json @@ -0,0 +1,100 @@ +{ + // pyrefly is the type checker for src; pyright is a second checker run only on the test suite, per https://pyrefly.org/blog/too-many-type-checkers/ + "include": ["tests"], + "exclude": [ + // excludes managed by this repository + + // excludes managed by template + "tests/unit/vcrpy_fixtures.py", // configures vcrpy, which is untyped, and imports nothing from the package under test + "**/generated", + "**/vendor_files", + "**/node_modules", + "**/__pycache__", + "**/.*" + ], + "strictListInference": true, + "strictDictionaryInference": true, + "strictSetInference": true, + "analyzeUnannotatedFunctions": true, + "strictParameterNoneValue": true, + "enableTypeIgnoreComments": true, + "deprecateTypingAliases": true, + "reportGeneralTypeIssues": true, + "reportPropertyTypeMismatch": true, + "reportFunctionMemberAccess": true, + "reportMissingImports": true, + "reportMissingModuleSource": true, + "reportInvalidTypeForm": true, + "reportMissingTypeStubs": false, + "reportImportCycles": true, + "reportUnusedClass": true, + "reportUnusedFunction": true, + "reportUnusedVariable": false, // already covered by ruff rule F841 + "reportWildcardImportFromLibrary": true, + "reportAbstractUsage": true, + "reportArgumentType": true, + "reportAssertTypeFailure": true, + "reportAssignmentType": true, + "reportAttributeAccessIssue": true, + "reportCallIssue": true, + "reportInconsistentOverload": true, + "reportIndexIssue": true, + "reportInvalidTypeArguments": true, + "reportNoOverloadImplementation": true, + "reportOperatorIssue": true, + "reportOptionalSubscript": true, + "reportOptionalMemberAccess": true, + "reportOptionalCall": true, + "reportOptionalIterable": true, + "reportOptionalContextManager": true, + "reportOptionalOperand": true, + "reportRedeclaration": true, + "reportReturnType": true, + "reportTypedDictNotRequiredAccess": true, + "reportUntypedFunctionDecorator": true, + "reportUntypedClassDecorator": true, + "reportUntypedBaseClass": true, + "reportUntypedNamedTuple": true, + "reportPrivateUsage": false, // already covered by ruff rule SLF001 + "reportTypeCommentUsage": true, + "reportPrivateImportUsage": true, + "reportConstantRedefinition": true, + "reportDeprecated": true, + "reportIncompatibleMethodOverride": true, + "reportIncompatibleVariableOverride": true, + "reportInconsistentConstructor": true, + "reportOverlappingOverload": true, + "reportPossiblyUnboundVariable": true, + "reportMissingSuperCall": true, + "reportInvalidStringEscapeSequence": true, + "reportUnknownParameterType": true, + "reportUnknownArgumentType": true, + "reportUnknownLambdaType": true, + "reportUnknownVariableType": true, + "reportUnknownMemberType": true, + "reportMissingParameterType": true, + "reportMissingTypeArgument": true, + "reportInvalidTypeVarUse": true, + "reportCallInDefaultInitializer": true, + "reportUnnecessaryIsInstance": true, + "reportUnnecessaryCast": true, + "reportUnnecessaryComparison": true, + "reportUnnecessaryContains": true, + "reportAssertAlwaysTrue": true, + "reportSelfClsParameterName": true, + "reportImplicitStringConcatenation": true, + "reportUndefinedVariable": true, + "reportUnboundVariable": true, + "reportInvalidStubStatement": true, + "reportIncompleteStub": true, + "reportUnsupportedDunderAll": true, + "reportUnusedCallResult": true, + "reportUnusedCoroutine": true, + "reportUnusedExcept": true, + "reportUnusedExpression": true, + "reportUnnecessaryTypeIgnoreComment": true, + "reportMatchNotExhaustive": true, + "reportImplicitOverride": true, + "reportUninitializedInstanceVariable": false, // doesn't apply to test code. Keep an eye on this to see if it causes any downstream problems of not catching real typing problems in the actual library code that cause problems in consumers...but it'd be a pain to set this to true for a test suite + "pythonPlatform": "Linux" +}