From d3549d1fe180c9010f6a06b241200accb601bcf8 Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Mon, 30 Mar 2026 23:41:26 +0000 Subject: [PATCH 1/2] copier --- .claude/settings/permissions/bash.jsonc | 21 ++++++- .coderabbit.yaml | 2 + .copier-answers.yml | 2 +- .devcontainer/Dockerfile | 2 +- .devcontainer/devcontainer.json | 18 +++--- .devcontainer/install-ci-tooling.py | 2 +- .devcontainer/manual-setup-deps.py | 17 ++++++ .devcontainer/on-create-command.sh | 6 +- .devcontainer/post-start-command.sh | 4 +- .../actions/check-skip-duplicates/action.yml | 44 ++++++++++++++ .github/actions/install_deps/action.yml | 7 ++- .github/workflows/ci.yaml | 43 ++++++++++++- .../confirm-on-tagged-copier-template.yaml | 34 +++++++++++ .github/workflows/pre-commit.yaml | 1 + .github/workflows/tag-on-merge.yaml | 4 +- AGENTS.md | 20 +++++-- extensions/context.py | 14 ++--- .../.claude/settings/permissions/bash.jsonc | 21 ++++++- template/.coderabbit.yaml | 2 + template/.devcontainer/Dockerfile | 2 +- .../.devcontainer/devcontainer.json.jinja | 18 +++--- template/.devcontainer/manual-setup-deps.py | 17 ++++++ .../.devcontainer/on-create-command.sh.jinja | 6 +- .../.devcontainer/post-start-command.sh.jinja | 4 +- .../.github/actions/install_deps/action.yml | 7 ++- template/.github/workflows/ci.yaml.jinja | 60 ++++++++++++++++--- .../confirm-on-tagged-copier-template.yaml | 34 +++++++++++ template/.github/workflows/pre-commit.yaml | 1 + template/AGENTS.md | 20 +++++-- 29 files changed, 374 insertions(+), 59 deletions(-) create mode 100644 .github/actions/check-skip-duplicates/action.yml create mode 100644 .github/workflows/confirm-on-tagged-copier-template.yaml create mode 100644 template/.github/workflows/confirm-on-tagged-copier-template.yaml diff --git a/.claude/settings/permissions/bash.jsonc b/.claude/settings/permissions/bash.jsonc index 92caa8a2..bdf27717 100644 --- a/.claude/settings/permissions/bash.jsonc +++ b/.claude/settings/permissions/bash.jsonc @@ -74,9 +74,17 @@ "Bash(tail *)", // Search "Bash(rg *)", + // Research + "Bash(gh issue list *)", + "Bash(gh pr view *)", + "Bash(gh pr diff *)" ], "ask": [ - "Bash(gh *)", // let's hold off before we let it use the github CLI in any free running allow mode...I don't want it somehow approving PRs with the user's credentials + // let's hold off before we let it use the github CLI in any free running allow mode...I don't want it somehow approving PRs with the user's credentials + "Bash(gh repo *)", + "Bash(gh release *)", + "Bash(gh secret *)", + "Bash(gh ruleset *)", "Bash(aws *)", // let's hold off before we let it use AWS CLI in any free running allow mode. We need to be very sure we don't have any access to staging or production credentials in our dev environment (...which we shouldn't...but we need to double check that or consider any other safeguards first) "Bash(curl *)", "Bash(ln *)", @@ -85,6 +93,17 @@ "deny": [ // Exceptions to generally allowed AI tooling "Bash(bd init*)", // we need to control the init process, don't let AI do that in the background + // Github + // Claude should not ever interfere with the PR process, that is how we gate AI's work + "Bash(gh pr create *)", + "Bash(gh pr edit *)", + "Bash(gh pr ready *)", + "Bash(gh pr review *)", + "Bash(gh pr merge *)", + "Bash(gh pr close *)", + "Bash(gh pr comment *)", + "Bash(gh pr update-branch *)", + // Destructive File Operations "Bash(chmod -R *)", "Bash(chown -R *)", diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 1d2e81ac..372316ba 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -7,6 +7,8 @@ reviews: instructions: "These files came from a vendor and we're not allowed to change them. Refer to it if you need to understand how the main code interacts with it, but do not make comments about it." - path: "**/*.py" instructions: "Check the `ruff.toml` and `ruff-test.toml` for linting rules we've explicitly disabled and don't suggest changes to please conventions we've disabled. Do not express concerns about ruff rules; a pre-commit hook already runs a ruff check. Do not warn about unnecessary super().__init__() calls; pyright prefers those to be present. Do not warn about missing type hints; a pre-commit hook already checks for that." + - path: "**/.copier-answers.yml" + instructions: "Do not comment about the `_commit` value needing to be a clean release tag. A CI job will fail if that is not the case." tools: eslint: # when the code contains typescript, eslint will be run by pre-commit, and coderabbit often generates false positives enabled: false diff --git a/.copier-answers.yml b/.copier-answers.yml index 681ffe7c..255dfba7 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v0.0.106 +_commit: v0.0.109 _src_path: gh:LabAutomationAndScreening/copier-base-template.git description: Copier template for creating Python libraries and executables install_claude_cli: true diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index a5b54ae3..b36cd643 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,7 +1,7 @@ # base image tags available at https://mcr.microsoft.com/v2/devcontainers/universal/tags/list # added the platform flag to override any local settings since this image is only compatible with linux/amd64. since this image is only x64 compatible, suppressing the hadolint rule # hadolint ignore=DL3029 -FROM --platform=linux/amd64 mcr.microsoft.com/devcontainers/universal:5.1.4-noble +FROM --platform=linux/amd64 mcr.microsoft.com/devcontainers/universal:5.1.5-noble SHELL ["/bin/bash", "-o", "pipefail", "-c"] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e7a97d64..c2d91ede 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,4 +1,8 @@ { + "hostRequirements": { + "cpus": 2, + "memory": "4gb" + }, "dockerComposeFile": "docker-compose.yml", "service": "devcontainer", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", @@ -22,21 +26,21 @@ "ms-vscode.live-server@0.5.2025051301", "MS-vsliveshare.vsliveshare@1.0.5905", "github.copilot@1.388.0", - "github.copilot-chat@0.38.2026022704", - "anthropic.claude-code@2.1.74", + "github.copilot-chat@0.42.2026032602", + "anthropic.claude-code@2.1.84", // Python - "ms-python.python@2026.2.2026021801", - "ms-python.vscode-pylance@2026.1.1", + "ms-python.python@2026.5.2026032701", + "ms-python.vscode-pylance@2026.1.102", "ms-vscode-remote.remote-containers@0.414.0", - "charliermarsh.ruff@2026.36.0", + "charliermarsh.ruff@2026.38.0", // Misc file formats "bierner.markdown-mermaid@1.29.0", "samuelcolvin.jinjahtml@0.20.0", "tamasfe.even-better-toml@0.19.2", "emilast.LogFileHighlighter@3.3.3", - "esbenp.prettier-vscode@12.3.0" + "esbenp.prettier-vscode@12.4.0" ], "settings": { "editor.accessibilitySupport": "off", // turn off sounds @@ -61,5 +65,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): c4997eda # spellchecker:disable-line + // Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): d77a3ff3 # spellchecker:disable-line } diff --git a/.devcontainer/install-ci-tooling.py b/.devcontainer/install-ci-tooling.py index 6e6f54fb..4f78f6a9 100644 --- a/.devcontainer/install-ci-tooling.py +++ b/.devcontainer/install-ci-tooling.py @@ -8,7 +8,7 @@ from pathlib import Path UV_VERSION = "0.10.12" -PNPM_VERSION = "10.32.1" +PNPM_VERSION = "10.33.0" COPIER_VERSION = "==9.14.0" COPIER_TEMPLATE_EXTENSIONS_VERSION = "==0.3.3" PRE_COMMIT_VERSION = "4.5.1" diff --git a/.devcontainer/manual-setup-deps.py b/.devcontainer/manual-setup-deps.py index 6f6fe0d8..53e59e1b 100644 --- a/.devcontainer/manual-setup-deps.py +++ b/.devcontainer/manual-setup-deps.py @@ -44,6 +44,12 @@ default=False, help="Allow uv to install new versions of Python on the fly. This is typically only needed when instantiating the copier template.", ) +_ = parser.add_argument( + "--skip-installing-pulumi-cli", + action="store_true", + default=False, + help="Do not install the Pulumi CLI even if the lock file references it", +) class PackageManager(str, enum.Enum): @@ -127,6 +133,17 @@ def main(): check=True, env=uv_env, ) + if ( + not generate_lock_file_only + and not args.skip_installing_pulumi_cli + and platform.system() == "Linux" + and env.lock_file.exists() + and '"pulumi"' in env.lock_file.read_text() + ): + _ = subprocess.run( + ["sh", str(REPO_ROOT_DIR / ".devcontainer" / "install-pulumi-cli.sh"), str(env.lock_file)], + check=True, + ) elif env.package_manager == PackageManager.PNPM: pnpm_command = ["pnpm", "install", "--dir", str(env.path)] if env_check_lock: diff --git a/.devcontainer/on-create-command.sh b/.devcontainer/on-create-command.sh index b3a4c386..6417509c 100644 --- a/.devcontainer/on-create-command.sh +++ b/.devcontainer/on-create-command.sh @@ -3,12 +3,12 @@ set -ex # For some reason the directory is not setup correctly and causes build of devcontainer to fail since # it doesn't have access to the workspace directory. This can normally be done in post-start-command -git config --global --add safe.directory /workspaces/copier-python-package-template +script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +repo_root="$(CDPATH= cd -- "$script_dir/.." && pwd)" +git config --global --add safe.directory "$repo_root" sh .devcontainer/on-create-command-boilerplate.sh # install json5 for merging claude settings. TODO: consider if we can install json5 globally...or somehow eliminate this dependency -script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" -repo_root="$(CDPATH= cd -- "$script_dir/.." && pwd)" mkdir -p "$repo_root/.claude" chmod -R ug+rwX "$repo_root/.claude" chgrp -R 0 "$repo_root/.claude" || true diff --git a/.devcontainer/post-start-command.sh b/.devcontainer/post-start-command.sh index 4d178b2d..b415152d 100644 --- a/.devcontainer/post-start-command.sh +++ b/.devcontainer/post-start-command.sh @@ -3,7 +3,9 @@ set -ex # For some reason the directory is not setup correctly and causes build of devcontainer to fail since # it doesn't have access to the workspace directory. This can normally be done in post-start-command -git config --global --add safe.directory /workspaces/copier-python-package-template +script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +repo_root="$(CDPATH= cd -- "$script_dir/.." && pwd)" +git config --global --add safe.directory "$repo_root" pre-commit run merge-claude-settings -a if ! bd ready; then echo "It's likely the Dolt server has not yet been initialized to support beads, running that now" # TODO: figure out a better way to match this specific scenario than just a non-zero exit code...but beads still seems like in high flux right now so not sure what to tie it to diff --git a/.github/actions/check-skip-duplicates/action.yml b/.github/actions/check-skip-duplicates/action.yml new file mode 100644 index 00000000..5638f11b --- /dev/null +++ b/.github/actions/check-skip-duplicates/action.yml @@ -0,0 +1,44 @@ +name: Check Skip Duplicates +description: 'Check that will output a variable to allow you to skip duplicate runs. Example: If you have both push and pull_request triggers enabled and you dont want to run 2 jobs for the same commit if a PR is already open you can add this to your jobs to skip that extra execution.' + +outputs: + should-run: + description: 'Flag that determines if this execution should run or not' + value: ${{ steps.check.outputs.should_run }} + +runs: + using: composite + steps: + - name: Check if push has associated open PR + id: check + env: + GH_TOKEN: ${{ github.token }} + REF_NAME: ${{ github.ref_name }} + REPO_NAME: ${{ github.repository }} + EVENT_NAME: ${{ github.event_name }} + shell: bash + run: | + # For non-push events, always run + if [ "$EVENT_NAME" != "push" ]; then + echo "should_run=true" >> $GITHUB_OUTPUT + echo "Event is $EVENT_NAME, will run CI" + exit 0 + fi + + # For push events, check if there's an open PR for this branch + pr_json=$(gh pr list \ + --repo "$REPO_NAME" \ + --head "$REF_NAME" \ + --state open \ + --json number \ + --limit 1) + + pr_number=$(echo "$pr_json" | jq -r '.[0].number // ""') + + if [ -n "$pr_number" ]; then + echo "should_run=false" >> $GITHUB_OUTPUT + echo "Push to branch with open PR #$pr_number detected, skipping (PR event will run CI)" + else + echo "should_run=true" >> $GITHUB_OUTPUT + echo "Push to branch without open PR, will run CI" + fi diff --git a/.github/actions/install_deps/action.yml b/.github/actions/install_deps/action.yml index 0cedd63a..7e22d841 100644 --- a/.github/actions/install_deps/action.yml +++ b/.github/actions/install_deps/action.yml @@ -44,6 +44,11 @@ inputs: 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 runs: @@ -83,5 +88,5 @@ runs: - name: Install dependencies # the funky syntax is github action ternary if: ${{ inputs.install-deps }} - run: python .devcontainer/manual-setup-deps.py ${{ inputs.python-version == 'notUsing' && '--no-python' || '' }} ${{ inputs.node-version == 'notUsing' && '--no-node' || '' }} ${{ inputs.skip-updating-devcontainer-hash && '--skip-updating-devcontainer-hash' || '' }} + run: python .devcontainer/manual-setup-deps.py ${{ inputs.python-version == 'notUsing' && '--no-python' || '' }} ${{ inputs.node-version == 'notUsing' && '--no-node' || '' }} ${{ inputs.skip-updating-devcontainer-hash && '--skip-updating-devcontainer-hash' || '' }} ${{ inputs.skip-installing-pulumi-cli && '--skip-installing-pulumi-cli' || '' }} shell: pwsh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d7e9fb92..aee9c2d9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -5,6 +5,7 @@ on: branches-ignore: - '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: env: PYTHONUNBUFFERED: True @@ -12,6 +13,7 @@ env: permissions: id-token: write # needed to assume OIDC roles (e.g. for downloading from CodeArtifact) + contents: read # need to explicitly provide this whenever defining permissions because the default value is 'none' for anything not explicitly set when permissions are defined jobs: get-values: @@ -19,9 +21,23 @@ jobs: permissions: contents: write # needed for updating dependabot branches + check-skip-duplicate: + runs-on: ubuntu-24.04 + outputs: + should-run: ${{ steps.check.outputs.should-run }} + steps: + - name: Checkout code + uses: actions/checkout@v6.0.2 + with: + persist-credentials: false + - id: check + uses: ./.github/actions/check-skip-duplicates + pre-commit: needs: - get-values + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' uses: ./.github/workflows/pre-commit.yaml permissions: contents: write # needed for mutex @@ -32,6 +48,8 @@ jobs: unit-test: needs: - pre-commit + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' strategy: matrix: os: @@ -66,6 +84,8 @@ jobs: lint-matrix: needs: - pre-commit + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' strategy: matrix: os: @@ -175,11 +195,18 @@ jobs: name: pre-commit-log--${{ github.jobs.lint-matrix.name }} path: "${{ github.workspace }}/.precommit_cache/pre-commit.log" - required-check: + confirm-on-tagged-copier-template: + if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }} + uses: ./.github/workflows/confirm-on-tagged-copier-template.yaml + + + workflow-summary: runs-on: ubuntu-24.04 timeout-minutes: 2 needs: - get-values + - check-skip-duplicate + - confirm-on-tagged-copier-template - pre-commit - unit-test - lint-matrix @@ -192,6 +219,8 @@ jobs: success_pattern="^(skipped|success)$" # these are the possibilities: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#needs-context if [[ ! "${{ needs.get-values.result }}" =~ $success_pattern ]] || + [[ ! "${{ needs.confirm-on-tagged-copier-template.result }}" =~ $success_pattern ]] || + [[ ! "${{ needs.check-skip-duplicate.result }}" =~ $success_pattern ]] || [[ ! "${{ needs.pre-commit.result }}" =~ $success_pattern ]] || [[ ! "${{ needs.unit-test.result }}" =~ $success_pattern ]] || [[ ! "${{ needs.lint-matrix.result }}" =~ $success_pattern ]]; then @@ -199,6 +228,18 @@ jobs: exit 1 fi echo "✅ All jobs finished with skipped or success" + + - name: Mark the required-check as succeeded so the PR can be merged + if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }} + env: + GH_TOKEN: ${{ github.token }} + 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" \ + -f target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + - name: Mark updated dependabot hash commit as succeeded if: needs.get-values.outputs.dependabot-commit-created == 'true' env: diff --git a/.github/workflows/confirm-on-tagged-copier-template.yaml b/.github/workflows/confirm-on-tagged-copier-template.yaml new file mode 100644 index 00000000..f042baef --- /dev/null +++ b/.github/workflows/confirm-on-tagged-copier-template.yaml @@ -0,0 +1,34 @@ +name: Confirm using tagged copier template version + +on: + workflow_call: + inputs: + answers_file: + description: 'Path to the copier answers file' + type: string + default: '.copier-answers.yml' + +jobs: + confirm-on-tagged-copier-template: + runs-on: ubuntu-24.04 + timeout-minutes: 2 + name: Fail if template under development + steps: + - name: Checkout code + uses: actions/checkout@v6.0.2 + with: + persist-credentials: false + + - name: Check _commit is a clean release tag + run: | + ANSWERS_FILE="${{ inputs.answers_file }}" + if [ ! -f "$ANSWERS_FILE" ]; then + echo "Error: $ANSWERS_FILE not found" + exit 1 + fi + COMMIT_LINE=$(grep "^_commit:" "$ANSWERS_FILE") + if echo "$COMMIT_LINE" | grep -q "-"; then + echo "Error: $COMMIT_LINE" + echo "_commit must be a clean release tag (e.g. v0.0.111), not a dev commit (e.g. v0.0.106-14-g7847d7b)" + exit 1 + fi diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index ffeb9485..fbdeb860 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -50,6 +50,7 @@ jobs: python-version: ${{ inputs.python-version }} node-version: ${{ inputs.node-version }} skip-installing-ssm-plugin-manager: true + skip-installing-pulumi-cli: true - 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 diff --git a/.github/workflows/tag-on-merge.yaml b/.github/workflows/tag-on-merge.yaml index 2e9b2277..99da4998 100644 --- a/.github/workflows/tag-on-merge.yaml +++ b/.github/workflows/tag-on-merge.yaml @@ -14,12 +14,12 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v6.0.1 + - uses: actions/checkout@v6.0.2 with: ref: ${{ github.event.pull_request.merge_commit_sha }} fetch-depth: '0' persist-credentials: false - name: Bump version and push tag - uses: mathieudutour/github-tag-action@a22cf08638b34d5badda920f9daf6e72c477b07b # v6.2 + uses: nickkostov/github-tag-action@b3aa34b4ac9c7843ee609ba5d0b0a50b962647b9 # v1.3.0 # a fork of https://github.com/mathieudutour/github-tag-action, which is still on Node 20 with: github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md index f2cbccb6..89ac5111 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,24 +17,31 @@ This project is a Copier template used to generate other copier templates. It is ## Testing -- Always run tests with an explicit path (e.g. uv run pytest tests/unit) — test runners discover all types by default. +- Always run tests with an explicit path (e.g. uv run pytest tests/unit) — test runners discover all types (unit, integration, E2E...) by default. +- When iterating on a single test, run that test in isolation first and confirm it is in the expected state (red or green) before widening to the full suite. Use the most targeted invocation available: a specific test function for Python (e.g. `uv run pytest path/to/test.py::test_name --no-cov`) or a file path and name filter for TypeScript (e.g. `pnpm test-unit -- path/to/test.spec.ts -t "test name" --no-coverage`). Only run the full suite once the target test behaves as expected. - Test coverage requirements are usually at 100%, so when running a subset of tests, always disable test coverage to avoid the test run failing for insufficient coverage. - Avoid magic values in comparisons in tests in all languages (like ruff rule PLR2004 specifies) - Prefer using random values in tests rather than arbitrary ones (e.g. the faker library, uuids, random.randint) when possible. For enums, pick randomly rather than hardcoding one value. - Avoid loops in tests — assert each item explicitly so failures pinpoint the exact element. When verifying a condition across all items in a collection, collect the violations into a list and assert it's empty (e.g., assert [x for x in items if bad_condition(x)] == []). -- Key `data-testid` selectors off unique IDs (e.g. UUIDs), not human-readable names which may collide or change. +- When asserting a mock or spy was called with specific arguments, always constrain as tightly as possible. In order of preference: (1) assert called exactly once with those args (`assert_called_once_with` in Python, `toHaveBeenCalledExactlyOnceWith` in Vitest/Jest); (2) if multiple calls are expected, assert the total call count and use a positional or last-call assertion (`nthCalledWith`, `lastCalledWith` / `assert_has_calls` with `call_args_list[n]`); (3) plain "called with at any point" (`toHaveBeenCalledWith`, `assert_called_with`) is a last resort only when neither the call count nor the call order can reasonably be constrained. ### Python Testing -- When using `mocker.spy` on a class-level method (including inherited ones), the spy records the unbound call, so assertions need `ANY` as the first argument to match self: `spy.assert_called_once_with(ANY, expected_arg)` +- When using `mocker.spy` on a class-level method (including inherited ones), the spy records the unbound call, so assertions need `ANY` as the first argument to match self: `spy.assert_called_once_with(ANY, expected_arg)` - Before writing new mock/spy helpers, check the `tests/unit/` folder for pre-built helpers in files like `fixtures.py` or `*mocks.py` - When a test needs a fixture only for its side effects (not its return value), use `@pytest.mark.usefixtures(fixture_name.__name__)` instead of adding an unused parameter with a noqa comment - Use `__name__` instead of string literals when referencing functions/methods (e.g., `mocker.patch.object(MyClass, MyClass.method.__name__)`, `pytest.mark.usefixtures(my_fixture.__name__)`). This enables IDE refactoring tools to catch renames. - When using the faker library, prefer the pytest fixture (provided by the faker library) over instantiating instances of Faker. +- **Choosing between cassettes and mocks:** At the layer that directly wraps an external API or service, strongly prefer VCR cassette-recorded interactions (via pytest-recording/vcrpy) — they capture real HTTP traffic and verify the wire format, catching integration issues that mocks would miss. At layers above that (e.g. business logic, route handlers), mock the wrapper layer instead (e.g. `mocker.patch.object(ThresholdsRepository, ...)`) — there is no value in re-testing the HTTP interaction from higher up. - **Never hand-write VCR cassette YAML files.** Cassettes must be recorded from real HTTP interactions by running the test once with `--record-mode=once` against a live external service: `uv run pytest --record-mode=once --no-cov`. The default mode is `none` — a missing cassette will cause an error, which is expected until recorded. - **Never hand-edit syrupy snapshot files.** Snapshots are auto-generated — to create or update them, run `uv run pytest --snapshot-update --no-cov`. A missing snapshot causes the test to fail, which is expected until you run with `--snapshot-update`. When a snapshot mismatch occurs, fix the code if the change was unintentional; run `--snapshot-update` if it was intentional. - **Never hand-write or hand-edit pytest-reserial `.jsonl` recording files.** Recordings must be captured from real serial port traffic by running the test with `--record` while the device is connected: `uv run pytest --record --no-cov`. The default mode replays recordings — a missing recording causes an error, which is expected until recorded against a live device. +### Frontend Testing + +- Key `data-testid` selectors off unique IDs (e.g. UUIDs), not human-readable names which may collide or change. +- In DOM-based tests, scope queries to the tightest relevant container. Only query `document` or `document.body` directly to find the top-level portal/popup element (e.g. a Reka UI dialog via `[role="dialog"][data-state="open"]`); all further queries should run on that element, not on `document.body` again. + # Agent Implementations & Configurations ## Memory and Rules @@ -43,13 +50,14 @@ This project is a Copier template used to generate other copier templates. It is ## Tooling -- Always use `uv run python` instead of `python3` or `python` when running Python commands. -- Prefer dedicated shell tools over `python3`/`python` for simple one-off tasks: use `jq` for JSON parsing, standard shell builtins for string manipulation, etc. Only reach for `python3` when no simpler tool covers the need. +- ❌ Never use `python3` or `python` directly. ✅ Always use `uv run python` for Python commands. +- ❌ Never use `python3`/`python` for one-off data tasks. ✅ Use `jq` for JSON parsing, standard shell builtins for string manipulation. Only reach for `uv run python` when no dedicated tool covers the need. - Check .devcontainer/devcontainer.json for tooling versions (Python, Node, etc.) when reasoning about version-specific stdlib or tooling behavior. - For frontend tests, run commands via `pnpm` scripts from `frontend/package.json` — never invoke tools directly (not pnpm exec , npx , etc.). ✅ pnpm test-unit ❌ pnpm vitest ... or npx vitest ... - For linting and type-checking, prefer `pre-commit run ` over invoking tools directly — this matches the permission allow-list and mirrors what CI runs. Key hook IDs: `typescript-check`, `eslint`, `pyright`, `ruff`, `ruff-format`. - Never rely on IDE diagnostics for ruff warnings — the IDE may not respect the project's ruff.toml config. Run `pre-commit run ruff -a` to get accurate results. -- When running terminal commands, execute exactly one command per tool call. Do not chain commands with &&, ||, ;, or & — this prohibition has no exceptions, even for `cd && ...` patterns. Use absolute paths instead of `cd` to avoid needing to chain. Pipes (|) are allowed for output transformation (e.g., head, tail, grep). If two sequential commands are needed, run them in separate tool calls. Chained commands break the permission allow-list matcher and cause unnecessary permission prompts +- When running terminal commands, execute exactly one command per tool call. Do not chain commands with &&, ||, ;, or & — this prohibition has no exceptions, even for `cd && ...` patterns. Use `cd` to change to the directory you want before running the command, avoiding the need to chain. Pipes (|) are allowed for output transformation (e.g., head, tail, grep). If two sequential commands are needed, run them in separate tool calls. Chained commands break the permission allow-list matcher and cause unnecessary permission prompts +- Never use `pnpm --prefix ` or `uv --directory ` to target a different directory — these flags break the permission allow-list matcher the same way chained `cd &&` commands do. Instead, rely on the working directory already being correct (the cwd persists between Bash tool calls), or issue a plain `cd ` as a separate prior tool call to reposition before running the command. - Never use backslash line continuations in shell commands — always write the full command on a single line. Backslashes break the permission allow-list matcher. - **Never manually edit files in any `generated/` folder.** These files are produced by codegen tooling (typically Kiota) and any manual changes will be overwritten. If a generated file needs to change, update the source (e.g. the OpenAPI schema) and re-run the generator. diff --git a/extensions/context.py b/extensions/context.py index b673a7ef..3dce6d5e 100644 --- a/extensions/context.py +++ b/extensions/context.py @@ -11,7 +11,7 @@ class ContextUpdater(ContextHook): @override def hook(self, context: dict[Any, Any]) -> dict[Any, Any]: context["uv_version"] = "0.10.12" - context["pnpm_version"] = "10.32.1" + context["pnpm_version"] = "10.33.0" context["pre_commit_version"] = "4.5.1" context["pyright_version"] = ">=1.1.408" context["pytest_version"] = ">=9.0.2" @@ -21,12 +21,12 @@ def hook(self, context: dict[Any, Any]) -> dict[Any, Any]: context["copier_version"] = "==9.14.0" context["copier_template_extensions_version"] = "==0.3.3" context["sphinx_version"] = "9.0.4" - context["pulumi_version"] = ">=3.226.0" + context["pulumi_version"] = ">=3.228.0" context["pulumi_aws_version"] = ">=7.23.0" - context["pulumi_aws_native_version"] = ">=1.57.0" + context["pulumi_aws_native_version"] = ">=1.59.0" context["pulumi_command_version"] = ">=1.2.1" context["pulumi_github_version"] = ">=6.12.1" - context["pulumi_okta_version"] = ">=6.2.3" + context["pulumi_okta_version"] = ">=6.4.0" context["boto3_version"] = ">=1.42.53" context["ephemeral_pulumi_deploy_version"] = ">=0.0.6" context["pydantic_version"] = ">=2.12.5" @@ -51,7 +51,7 @@ def hook(self, context: dict[Any, Any]) -> dict[Any, Any]: context["python_faker_version"] = ">=40.4.0" context["default_node_version"] = "24.11.1" - context["nuxt_ui_version"] = "^4.5.1" + context["nuxt_ui_version"] = "^4.6.0" context["nuxt_version"] = "~4.3.1" context["nuxt_icon_version"] = "^2.2.1" context["typescript_version"] = "^5.9.3" @@ -61,14 +61,14 @@ def hook(self, context: dict[Any, Any]) -> dict[Any, Any]: context["vue_devtools_api_version"] = "^8.1.0" context["vue_router_version"] = "^5.0.3" context["dotenv_cli_version"] = "^11.0.0" - context["faker_version"] = "^10.3.0" + context["faker_version"] = "^10.4.0" context["vitest_version"] = "^3.2.4" context["eslint_version"] = "~9.38.0" context["nuxt_eslint_version"] = "^1.15.1" context["zod_version"] = "^4.3.6" context["zod_from_json_schema_version"] = "^0.5.1" context["nuxt_apollo_version"] = "5.0.0-alpha.15" - context["graphql_codegen_cli_version"] = "^6.1.0" + context["graphql_codegen_cli_version"] = "^6.2.1" context["graphql_codegen_typescript_version"] = "^5.0.7" context["graphql_tools_mock_version"] = "^9.1.0" context["tailwindcss_version"] = "^4.2.0" diff --git a/template/.claude/settings/permissions/bash.jsonc b/template/.claude/settings/permissions/bash.jsonc index 92caa8a2..bdf27717 100644 --- a/template/.claude/settings/permissions/bash.jsonc +++ b/template/.claude/settings/permissions/bash.jsonc @@ -74,9 +74,17 @@ "Bash(tail *)", // Search "Bash(rg *)", + // Research + "Bash(gh issue list *)", + "Bash(gh pr view *)", + "Bash(gh pr diff *)" ], "ask": [ - "Bash(gh *)", // let's hold off before we let it use the github CLI in any free running allow mode...I don't want it somehow approving PRs with the user's credentials + // let's hold off before we let it use the github CLI in any free running allow mode...I don't want it somehow approving PRs with the user's credentials + "Bash(gh repo *)", + "Bash(gh release *)", + "Bash(gh secret *)", + "Bash(gh ruleset *)", "Bash(aws *)", // let's hold off before we let it use AWS CLI in any free running allow mode. We need to be very sure we don't have any access to staging or production credentials in our dev environment (...which we shouldn't...but we need to double check that or consider any other safeguards first) "Bash(curl *)", "Bash(ln *)", @@ -85,6 +93,17 @@ "deny": [ // Exceptions to generally allowed AI tooling "Bash(bd init*)", // we need to control the init process, don't let AI do that in the background + // Github + // Claude should not ever interfere with the PR process, that is how we gate AI's work + "Bash(gh pr create *)", + "Bash(gh pr edit *)", + "Bash(gh pr ready *)", + "Bash(gh pr review *)", + "Bash(gh pr merge *)", + "Bash(gh pr close *)", + "Bash(gh pr comment *)", + "Bash(gh pr update-branch *)", + // Destructive File Operations "Bash(chmod -R *)", "Bash(chown -R *)", diff --git a/template/.coderabbit.yaml b/template/.coderabbit.yaml index 1d2e81ac..372316ba 100644 --- a/template/.coderabbit.yaml +++ b/template/.coderabbit.yaml @@ -7,6 +7,8 @@ reviews: instructions: "These files came from a vendor and we're not allowed to change them. Refer to it if you need to understand how the main code interacts with it, but do not make comments about it." - path: "**/*.py" instructions: "Check the `ruff.toml` and `ruff-test.toml` for linting rules we've explicitly disabled and don't suggest changes to please conventions we've disabled. Do not express concerns about ruff rules; a pre-commit hook already runs a ruff check. Do not warn about unnecessary super().__init__() calls; pyright prefers those to be present. Do not warn about missing type hints; a pre-commit hook already checks for that." + - path: "**/.copier-answers.yml" + instructions: "Do not comment about the `_commit` value needing to be a clean release tag. A CI job will fail if that is not the case." tools: eslint: # when the code contains typescript, eslint will be run by pre-commit, and coderabbit often generates false positives enabled: false diff --git a/template/.devcontainer/Dockerfile b/template/.devcontainer/Dockerfile index a5b54ae3..b36cd643 100644 --- a/template/.devcontainer/Dockerfile +++ b/template/.devcontainer/Dockerfile @@ -1,7 +1,7 @@ # base image tags available at https://mcr.microsoft.com/v2/devcontainers/universal/tags/list # added the platform flag to override any local settings since this image is only compatible with linux/amd64. since this image is only x64 compatible, suppressing the hadolint rule # hadolint ignore=DL3029 -FROM --platform=linux/amd64 mcr.microsoft.com/devcontainers/universal:5.1.4-noble +FROM --platform=linux/amd64 mcr.microsoft.com/devcontainers/universal:5.1.5-noble SHELL ["/bin/bash", "-o", "pipefail", "-c"] diff --git a/template/.devcontainer/devcontainer.json.jinja b/template/.devcontainer/devcontainer.json.jinja index d044844e..2492a80c 100644 --- a/template/.devcontainer/devcontainer.json.jinja +++ b/template/.devcontainer/devcontainer.json.jinja @@ -1,4 +1,8 @@ {% raw %}{ + "hostRequirements": { + "cpus": 2, + "memory": "4gb" + }, "dockerComposeFile": "docker-compose.yml", "service": "devcontainer", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", @@ -40,28 +44,28 @@ "ms-vscode.live-server@0.5.2025051301", "MS-vsliveshare.vsliveshare@1.0.5905", "github.copilot@1.388.0", - "github.copilot-chat@0.38.2026022704",{% endraw %}{% if install_claude_cli %}{% raw %} - "anthropic.claude-code@2.1.74",{% endraw %}{% endif %}{% raw %} + "github.copilot-chat@0.42.2026032602",{% endraw %}{% if install_claude_cli %}{% raw %} + "anthropic.claude-code@2.1.84",{% endraw %}{% endif %}{% raw %} // Python - "ms-python.python@2026.2.2026021801", - "ms-python.vscode-pylance@2026.1.1", + "ms-python.python@2026.5.2026032701", + "ms-python.vscode-pylance@2026.1.102", "ms-vscode-remote.remote-containers@0.414.0", - "charliermarsh.ruff@2026.36.0", + "charliermarsh.ruff@2026.38.0", {% 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 %} // VueJS "vue.volar@3.2.5", "vitest.explorer@1.36.0", {% endraw %}{% endif %}{% raw %}{% endraw %}{% if is_child_of_copier_base_template is not defined and template_uses_javascript is defined and template_uses_javascript is sameas(true) %}{% raw %} // All javascript - "dbaeumer.vscode-eslint@3.0.21", + "dbaeumer.vscode-eslint@3.0.24", {% endraw %}{% endif %}{% raw %} // Misc file formats "bierner.markdown-mermaid@1.29.0", "samuelcolvin.jinjahtml@0.20.0", "tamasfe.even-better-toml@0.19.2", "emilast.LogFileHighlighter@3.3.3", - "esbenp.prettier-vscode@12.3.0" + "esbenp.prettier-vscode@12.4.0" ], "settings": { "editor.accessibilitySupport": "off", // turn off sounds diff --git a/template/.devcontainer/manual-setup-deps.py b/template/.devcontainer/manual-setup-deps.py index 6f6fe0d8..53e59e1b 100644 --- a/template/.devcontainer/manual-setup-deps.py +++ b/template/.devcontainer/manual-setup-deps.py @@ -44,6 +44,12 @@ default=False, help="Allow uv to install new versions of Python on the fly. This is typically only needed when instantiating the copier template.", ) +_ = parser.add_argument( + "--skip-installing-pulumi-cli", + action="store_true", + default=False, + help="Do not install the Pulumi CLI even if the lock file references it", +) class PackageManager(str, enum.Enum): @@ -127,6 +133,17 @@ def main(): check=True, env=uv_env, ) + if ( + not generate_lock_file_only + and not args.skip_installing_pulumi_cli + and platform.system() == "Linux" + and env.lock_file.exists() + and '"pulumi"' in env.lock_file.read_text() + ): + _ = subprocess.run( + ["sh", str(REPO_ROOT_DIR / ".devcontainer" / "install-pulumi-cli.sh"), str(env.lock_file)], + check=True, + ) elif env.package_manager == PackageManager.PNPM: pnpm_command = ["pnpm", "install", "--dir", str(env.path)] if env_check_lock: diff --git a/template/.devcontainer/on-create-command.sh.jinja b/template/.devcontainer/on-create-command.sh.jinja index 19933278..61df6ebe 100644 --- a/template/.devcontainer/on-create-command.sh.jinja +++ b/template/.devcontainer/on-create-command.sh.jinja @@ -3,12 +3,12 @@ set -ex # For some reason the directory is not setup correctly and causes build of devcontainer to fail since # it doesn't have access to the workspace directory. This can normally be done in post-start-command -git config --global --add safe.directory /workspaces/{% endraw %}{{ repo_name }}{% raw %} +script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +repo_root="$(CDPATH= cd -- "$script_dir/.." && pwd)" +git config --global --add safe.directory "$repo_root" sh .devcontainer/on-create-command-boilerplate.sh{% endraw %}{% if install_claude_cli %}{% raw %} # install json5 for merging claude settings. TODO: consider if we can install json5 globally...or somehow eliminate this dependency -script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" -repo_root="$(CDPATH= cd -- "$script_dir/.." && pwd)" mkdir -p "$repo_root/.claude" chmod -R ug+rwX "$repo_root/.claude" chgrp -R 0 "$repo_root/.claude" || true diff --git a/template/.devcontainer/post-start-command.sh.jinja b/template/.devcontainer/post-start-command.sh.jinja index 1329f4f6..b0ef3332 100644 --- a/template/.devcontainer/post-start-command.sh.jinja +++ b/template/.devcontainer/post-start-command.sh.jinja @@ -3,7 +3,9 @@ set -ex # For some reason the directory is not setup correctly and causes build of devcontainer to fail since # it doesn't have access to the workspace directory. This can normally be done in post-start-command -git config --global --add safe.directory /workspaces/{% endraw %}{{ repo_name }}{% if install_claude_cli %}{% raw %} +script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +repo_root="$(CDPATH= cd -- "$script_dir/.." && pwd)" +git config --global --add safe.directory "$repo_root"{% endraw %}{% if install_claude_cli %}{% raw %} pre-commit run merge-claude-settings -a if ! bd ready; then echo "It's likely the Dolt server has not yet been initialized to support beads, running that now" # TODO: figure out a better way to match this specific scenario than just a non-zero exit code...but beads still seems like in high flux right now so not sure what to tie it to diff --git a/template/.github/actions/install_deps/action.yml b/template/.github/actions/install_deps/action.yml index 0cedd63a..7e22d841 100644 --- a/template/.github/actions/install_deps/action.yml +++ b/template/.github/actions/install_deps/action.yml @@ -44,6 +44,11 @@ inputs: 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 runs: @@ -83,5 +88,5 @@ runs: - name: Install dependencies # the funky syntax is github action ternary if: ${{ inputs.install-deps }} - run: python .devcontainer/manual-setup-deps.py ${{ inputs.python-version == 'notUsing' && '--no-python' || '' }} ${{ inputs.node-version == 'notUsing' && '--no-node' || '' }} ${{ inputs.skip-updating-devcontainer-hash && '--skip-updating-devcontainer-hash' || '' }} + run: python .devcontainer/manual-setup-deps.py ${{ inputs.python-version == 'notUsing' && '--no-python' || '' }} ${{ inputs.node-version == 'notUsing' && '--no-node' || '' }} ${{ inputs.skip-updating-devcontainer-hash && '--skip-updating-devcontainer-hash' || '' }} ${{ inputs.skip-installing-pulumi-cli && '--skip-installing-pulumi-cli' || '' }} shell: pwsh diff --git a/template/.github/workflows/ci.yaml.jinja b/template/.github/workflows/ci.yaml.jinja index 16409429..25d6db52 100644 --- a/template/.github/workflows/ci.yaml.jinja +++ b/template/.github/workflows/ci.yaml.jinja @@ -5,6 +5,7 @@ on: branches-ignore: - '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: env: PYTHONUNBUFFERED: True @@ -18,11 +19,32 @@ jobs: get-values: uses: ./.github/workflows/get-values.yaml permissions: - contents: write # needed updating dependabot branches + contents: write # needed for updating dependabot branches + + check-skip-duplicate: + runs-on: {% endraw %}{{ gha_linux_runner }}{% raw %} + permissions: + contents: read + pull-requests: read # needed to check if PR exists for current branch + outputs: + should-run: ${{ steps.check.outputs.should-run }} + steps: + - name: Checkout code + uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} + with: + persist-credentials: false + - id: check + uses: ./.github/actions/check-skip-duplicates + + confirm-on-tagged-copier-template: + if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }} + uses: ./.github/workflows/confirm-on-tagged-copier-template.yaml lint: needs: - get-values + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' name: Pre-commit uses: ./.github/workflows/pre-commit.yaml permissions: @@ -34,6 +56,8 @@ jobs: test: needs: - lint + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' strategy: matrix: os: @@ -59,6 +83,8 @@ jobs: - name: Install python tooling uses: ./.github/actions/install_deps with: + skip-installing-ssm-plugin-manager: false + skip-installing-pulumi-cli: false python-version: ${{ matrix.python-version }}{% endraw %}{% if python_package_registry == "AWS CodeArtifact" %}{% raw %} code-artifact-auth-role-name: CoreInfraBaseAccess code-artifact-auth-role-account-id: {% endraw %}{{ aws_central_infrastructure_account_id }}{% raw %} @@ -83,6 +109,8 @@ jobs: {% endraw %}{% if is_frozen_executable %}{% raw %} executable: needs: - test + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' strategy: matrix: os: @@ -122,6 +150,8 @@ jobs: build-docs: needs: - lint + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' strategy: matrix: python-version: @@ -151,33 +181,49 @@ jobs: run: uv run make html SPHINXOPTS="-W" {% endraw %}{% endif %}{% raw %} - required-check: + workflow-summary: runs-on: {% endraw %}{{ gha_linux_runner }}{% raw %} - permissions: - statuses: write # needed for updating status on Dependabot PRs + timeout-minutes: {% endraw %}{{ gha_short_timeout_minutes }}{% raw %} needs: - get-values + - check-skip-duplicate - lint - test{% endraw %}{% if create_docs %} - build-docs{% endif %}{% if is_frozen_executable %} - executable{% endif %}{% raw %} + - confirm-on-tagged-copier-template + permissions: + statuses: write # needed for updating status on Dependabot PRs if: always() - timeout-minutes: {% endraw %}{{ gha_short_timeout_minutes }}{% raw %} steps: - name: fail if prior job failure run: | success_pattern="^(skipped|success)$" # these are the possibilities: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#needs-context if [[ ! "${{ needs.get-values.result }}" =~ $success_pattern ]] || + [[ ! "${{ needs.check-skip-duplicate.result }}" =~ $success_pattern ]] || [[ ! "${{ needs.lint.result }}" =~ $success_pattern ]] ||{% endraw %}{% if create_docs %}{% raw %} [[ ! "${{ needs.build-docs.result }}" =~ $success_pattern ]] ||{% endraw %}{% endif %}{% raw %}{% endraw %}{% if is_frozen_executable %}{% raw %} [[ ! "${{ needs.executable.result }}" =~ $success_pattern ]] ||{% endraw %}{% endif %}{% raw %} - [[ ! "${{ needs.test.result }}" =~ $success_pattern ]]; then + [[ ! "${{ needs.test.result }}" =~ $success_pattern ]] || + [[ ! "${{ needs.confirm-on-tagged-copier-template.result }}" =~ $success_pattern ]]; then echo "❌ One or more jobs did not finish with skipped or success" exit 1 fi echo "✅ All jobs finished with skipped or success" - - name: Mark updated Dependabot commit of devcontainer hash as succeeded + + - name: Mark the required-check as succeeded so the PR can be merged + if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }} + env: + GH_TOKEN: ${{ github.token }} + 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" \ + -f target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + - name: Mark updated dependabot hash commit as succeeded if: needs.get-values.outputs.dependabot-commit-created == 'true' env: GH_TOKEN: ${{ github.token }} diff --git a/template/.github/workflows/confirm-on-tagged-copier-template.yaml b/template/.github/workflows/confirm-on-tagged-copier-template.yaml new file mode 100644 index 00000000..f042baef --- /dev/null +++ b/template/.github/workflows/confirm-on-tagged-copier-template.yaml @@ -0,0 +1,34 @@ +name: Confirm using tagged copier template version + +on: + workflow_call: + inputs: + answers_file: + description: 'Path to the copier answers file' + type: string + default: '.copier-answers.yml' + +jobs: + confirm-on-tagged-copier-template: + runs-on: ubuntu-24.04 + timeout-minutes: 2 + name: Fail if template under development + steps: + - name: Checkout code + uses: actions/checkout@v6.0.2 + with: + persist-credentials: false + + - name: Check _commit is a clean release tag + run: | + ANSWERS_FILE="${{ inputs.answers_file }}" + if [ ! -f "$ANSWERS_FILE" ]; then + echo "Error: $ANSWERS_FILE not found" + exit 1 + fi + COMMIT_LINE=$(grep "^_commit:" "$ANSWERS_FILE") + if echo "$COMMIT_LINE" | grep -q "-"; then + echo "Error: $COMMIT_LINE" + echo "_commit must be a clean release tag (e.g. v0.0.111), not a dev commit (e.g. v0.0.106-14-g7847d7b)" + exit 1 + fi diff --git a/template/.github/workflows/pre-commit.yaml b/template/.github/workflows/pre-commit.yaml index ffeb9485..fbdeb860 100644 --- a/template/.github/workflows/pre-commit.yaml +++ b/template/.github/workflows/pre-commit.yaml @@ -50,6 +50,7 @@ jobs: python-version: ${{ inputs.python-version }} node-version: ${{ inputs.node-version }} skip-installing-ssm-plugin-manager: true + skip-installing-pulumi-cli: true - 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 diff --git a/template/AGENTS.md b/template/AGENTS.md index ca13fffa..d6ed3c36 100644 --- a/template/AGENTS.md +++ b/template/AGENTS.md @@ -17,24 +17,31 @@ This project is a Python library. ## Testing -- Always run tests with an explicit path (e.g. uv run pytest tests/unit) — test runners discover all types by default. +- Always run tests with an explicit path (e.g. uv run pytest tests/unit) — test runners discover all types (unit, integration, E2E...) by default. +- When iterating on a single test, run that test in isolation first and confirm it is in the expected state (red or green) before widening to the full suite. Use the most targeted invocation available: a specific test function for Python (e.g. `uv run pytest path/to/test.py::test_name --no-cov`) or a file path and name filter for TypeScript (e.g. `pnpm test-unit -- path/to/test.spec.ts -t "test name" --no-coverage`). Only run the full suite once the target test behaves as expected. - Test coverage requirements are usually at 100%, so when running a subset of tests, always disable test coverage to avoid the test run failing for insufficient coverage. - Avoid magic values in comparisons in tests in all languages (like ruff rule PLR2004 specifies) - Prefer using random values in tests rather than arbitrary ones (e.g. the faker library, uuids, random.randint) when possible. For enums, pick randomly rather than hardcoding one value. - Avoid loops in tests — assert each item explicitly so failures pinpoint the exact element. When verifying a condition across all items in a collection, collect the violations into a list and assert it's empty (e.g., assert [x for x in items if bad_condition(x)] == []). -- Key `data-testid` selectors off unique IDs (e.g. UUIDs), not human-readable names which may collide or change. +- When asserting a mock or spy was called with specific arguments, always constrain as tightly as possible. In order of preference: (1) assert called exactly once with those args (`assert_called_once_with` in Python, `toHaveBeenCalledExactlyOnceWith` in Vitest/Jest); (2) if multiple calls are expected, assert the total call count and use a positional or last-call assertion (`nthCalledWith`, `lastCalledWith` / `assert_has_calls` with `call_args_list[n]`); (3) plain "called with at any point" (`toHaveBeenCalledWith`, `assert_called_with`) is a last resort only when neither the call count nor the call order can reasonably be constrained. ### Python Testing -- When using `mocker.spy` on a class-level method (including inherited ones), the spy records the unbound call, so assertions need `ANY` as the first argument to match self: `spy.assert_called_once_with(ANY, expected_arg)` +- When using `mocker.spy` on a class-level method (including inherited ones), the spy records the unbound call, so assertions need `ANY` as the first argument to match self: `spy.assert_called_once_with(ANY, expected_arg)` - Before writing new mock/spy helpers, check the `tests/unit/` folder for pre-built helpers in files like `fixtures.py` or `*mocks.py` - When a test needs a fixture only for its side effects (not its return value), use `@pytest.mark.usefixtures(fixture_name.__name__)` instead of adding an unused parameter with a noqa comment - Use `__name__` instead of string literals when referencing functions/methods (e.g., `mocker.patch.object(MyClass, MyClass.method.__name__)`, `pytest.mark.usefixtures(my_fixture.__name__)`). This enables IDE refactoring tools to catch renames. - When using the faker library, prefer the pytest fixture (provided by the faker library) over instantiating instances of Faker. +- **Choosing between cassettes and mocks:** At the layer that directly wraps an external API or service, strongly prefer VCR cassette-recorded interactions (via pytest-recording/vcrpy) — they capture real HTTP traffic and verify the wire format, catching integration issues that mocks would miss. At layers above that (e.g. business logic, route handlers), mock the wrapper layer instead (e.g. `mocker.patch.object(ThresholdsRepository, ...)`) — there is no value in re-testing the HTTP interaction from higher up. - **Never hand-write VCR cassette YAML files.** Cassettes must be recorded from real HTTP interactions by running the test once with `--record-mode=once` against a live external service: `uv run pytest --record-mode=once --no-cov`. The default mode is `none` — a missing cassette will cause an error, which is expected until recorded. - **Never hand-edit syrupy snapshot files.** Snapshots are auto-generated — to create or update them, run `uv run pytest --snapshot-update --no-cov`. A missing snapshot causes the test to fail, which is expected until you run with `--snapshot-update`. When a snapshot mismatch occurs, fix the code if the change was unintentional; run `--snapshot-update` if it was intentional. - **Never hand-write or hand-edit pytest-reserial `.jsonl` recording files.** Recordings must be captured from real serial port traffic by running the test with `--record` while the device is connected: `uv run pytest --record --no-cov`. The default mode replays recordings — a missing recording causes an error, which is expected until recorded against a live device. +### Frontend Testing + +- Key `data-testid` selectors off unique IDs (e.g. UUIDs), not human-readable names which may collide or change. +- In DOM-based tests, scope queries to the tightest relevant container. Only query `document` or `document.body` directly to find the top-level portal/popup element (e.g. a Reka UI dialog via `[role="dialog"][data-state="open"]`); all further queries should run on that element, not on `document.body` again. + # Agent Implementations & Configurations ## Memory and Rules @@ -43,13 +50,14 @@ This project is a Python library. ## Tooling -- Always use `uv run python` instead of `python3` or `python` when running Python commands. -- Prefer dedicated shell tools over `python3`/`python` for simple one-off tasks: use `jq` for JSON parsing, standard shell builtins for string manipulation, etc. Only reach for `python3` when no simpler tool covers the need. +- ❌ Never use `python3` or `python` directly. ✅ Always use `uv run python` for Python commands. +- ❌ Never use `python3`/`python` for one-off data tasks. ✅ Use `jq` for JSON parsing, standard shell builtins for string manipulation. Only reach for `uv run python` when no dedicated tool covers the need. - Check .devcontainer/devcontainer.json for tooling versions (Python, Node, etc.) when reasoning about version-specific stdlib or tooling behavior. - For frontend tests, run commands via `pnpm` scripts from `frontend/package.json` — never invoke tools directly (not pnpm exec , npx , etc.). ✅ pnpm test-unit ❌ pnpm vitest ... or npx vitest ... - For linting and type-checking, prefer `pre-commit run ` over invoking tools directly — this matches the permission allow-list and mirrors what CI runs. Key hook IDs: `typescript-check`, `eslint`, `pyright`, `ruff`, `ruff-format`. - Never rely on IDE diagnostics for ruff warnings — the IDE may not respect the project's ruff.toml config. Run `pre-commit run ruff -a` to get accurate results. -- When running terminal commands, execute exactly one command per tool call. Do not chain commands with &&, ||, ;, or & — this prohibition has no exceptions, even for `cd && ...` patterns. Use absolute paths instead of `cd` to avoid needing to chain. Pipes (|) are allowed for output transformation (e.g., head, tail, grep). If two sequential commands are needed, run them in separate tool calls. Chained commands break the permission allow-list matcher and cause unnecessary permission prompts +- When running terminal commands, execute exactly one command per tool call. Do not chain commands with &&, ||, ;, or & — this prohibition has no exceptions, even for `cd && ...` patterns. Use `cd` to change to the directory you want before running the command, avoiding the need to chain. Pipes (|) are allowed for output transformation (e.g., head, tail, grep). If two sequential commands are needed, run them in separate tool calls. Chained commands break the permission allow-list matcher and cause unnecessary permission prompts +- Never use `pnpm --prefix ` or `uv --directory ` to target a different directory — these flags break the permission allow-list matcher the same way chained `cd &&` commands do. Instead, rely on the working directory already being correct (the cwd persists between Bash tool calls), or issue a plain `cd ` as a separate prior tool call to reposition before running the command. - Never use backslash line continuations in shell commands — always write the full command on a single line. Backslashes break the permission allow-list matcher. - **Never manually edit files in any `generated/` folder.** These files are produced by codegen tooling (typically Kiota) and any manual changes will be overwritten. If a generated file needs to change, update the source (e.g. the OpenAPI schema) and re-run the generator. From 93c5fcbd890fb2aebd161f3345ff936031abb7a2 Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Mon, 30 Mar 2026 23:43:01 +0000 Subject: [PATCH 2/2] skip --- template/.github/workflows/ci.yaml.jinja | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/template/.github/workflows/ci.yaml.jinja b/template/.github/workflows/ci.yaml.jinja index 25d6db52..9d611dc5 100644 --- a/template/.github/workflows/ci.yaml.jinja +++ b/template/.github/workflows/ci.yaml.jinja @@ -83,8 +83,8 @@ jobs: - name: Install python tooling uses: ./.github/actions/install_deps with: - skip-installing-ssm-plugin-manager: false - skip-installing-pulumi-cli: false + skip-installing-ssm-plugin-manager: true + skip-installing-pulumi-cli: true python-version: ${{ matrix.python-version }}{% endraw %}{% if python_package_registry == "AWS CodeArtifact" %}{% raw %} code-artifact-auth-role-name: CoreInfraBaseAccess code-artifact-auth-role-account-id: {% endraw %}{{ aws_central_infrastructure_account_id }}{% raw %} @@ -131,6 +131,8 @@ jobs: - name: Install python tooling uses: ./.github/actions/install_deps with: + skip-installing-ssm-plugin-manager: true + skip-installing-pulumi-cli: true python-version: ${{ matrix.python-version }}{% endraw %}{% if python_package_registry == "AWS CodeArtifact" %}{% raw %} code-artifact-auth-role-name: CoreInfraBaseAccess code-artifact-auth-role-account-id: {% endraw %}{{ aws_central_infrastructure_account_id }}{% raw %} @@ -171,6 +173,8 @@ jobs: - name: Install python tooling uses: ./.github/actions/install_deps with: + skip-installing-ssm-plugin-manager: true + skip-installing-pulumi-cli: true python-version: ${{ matrix.python-version }}{% endraw %}{% if python_package_registry == "AWS CodeArtifact" %}{% raw %} code-artifact-auth-role-name: CoreInfraBaseAccess code-artifact-auth-role-account-id: "{% endraw %}{{ aws_central_infrastructure_account_id }}{% raw %}"