diff --git a/.copier-answers.yml b/.copier-answers.yml index 5f34dc370..1ab5c68eb 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.107 _src_path: gh:LabAutomationAndScreening/copier-base-template.git description: A web app that is hosted within a local intranet. Nuxt frontend, python backend, docker-compose diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index a5b54ae3d..b36cd643f 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 557f716c2..cce216e01 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): f6b6ee32 # spellchecker:disable-line + // Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): 80d9f36a # spellchecker:disable-line } diff --git a/.devcontainer/install-ci-tooling.py b/.devcontainer/install-ci-tooling.py index 6e6f54fba..4f78f6a9e 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/on-create-command.sh b/.devcontainer/on-create-command.sh index 4383daf27..6417509cc 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-nuxt-python-intranet-app +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 7e87130d6..b415152d8 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-nuxt-python-intranet-app +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 000000000..5638f11bc --- /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/workflows/ci.yaml b/.github/workflows/ci.yaml index f78bcc9e2..33efdd3ed 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 @@ -19,9 +20,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 +47,8 @@ jobs: unit-test: needs: - pre-commit + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' strategy: matrix: os: @@ -66,6 +83,8 @@ jobs: lint-matrix: needs: - pre-commit + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' strategy: matrix: os: @@ -177,11 +196,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 @@ -194,6 +220,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 @@ -201,6 +229,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 000000000..f042baef6 --- /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/tag-on-merge.yaml b/.github/workflows/tag-on-merge.yaml index 2e9b2277c..99da4998e 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 c73f4cca6..a6e5c8360 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,8 +43,8 @@ This project is a Copier template used to generate applications that are able to ## 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`. diff --git a/extensions/context.py b/extensions/context.py index 6dd68a95c..02f60eccf 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" @@ -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/.devcontainer/Dockerfile b/template/.devcontainer/Dockerfile index f2e8e49e0..87c2f453b 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 2e3a4f3ee..632fd6ef7 100644 --- a/template/.devcontainer/devcontainer.json.jinja +++ b/template/.devcontainer/devcontainer.json.jinja @@ -1,4 +1,9 @@ {% raw %}{ + "hostRequirements": { + "cpus": 2, + // Static site generation requires more memory + "memory": "10gb" + }, "dockerComposeFile": "docker-compose.yml", "service": "devcontainer", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", @@ -40,28 +45,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/on-create-command.sh.jinja b/template/.devcontainer/on-create-command.sh.jinja index 199332787..61df6ebeb 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 1329f4f6d..b0ef33324 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/workflows/ci.yaml.jinja b/template/.github/workflows/ci.yaml.jinja index 3ac6f9fd1..e206ef99b 100644 --- a/template/.github/workflows/ci.yaml.jinja +++ b/template/.github/workflows/ci.yaml.jinja @@ -5,22 +5,41 @@ 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 PRE_COMMIT_HOME: ${{ github.workspace }}/.precommit_cache permissions: - id-token: write - contents: write # needed for mutex, and updating dependabot branches - statuses: write # needed for updating status on Dependabot PRs + id-token: write # needed to assume OIDC roles (e.g. for downloading from CodeArtifact) + contents: write # needed for mutex, and updating dependabot branches + statuses: write # needed for updating status on Dependabot PRs jobs: get-values: uses: ./.github/workflows/get-values.yaml + 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 + lint: - needs: [ get-values ] + needs: + - get-values + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' name: Pre-commit uses: ./.github/workflows/pre-commit.yaml with: @@ -34,6 +53,8 @@ jobs: runs-on: {% endraw %}{{ gha_linux_runner }}{% raw %} needs: - lint + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' steps: - name: Checkout code uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} @@ -61,6 +82,8 @@ jobs: timeout-minutes: {% endraw %}{{ gha_medium_timeout_minutes }}{% raw %} needs: - lint + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' strategy: matrix: os: @@ -93,6 +116,8 @@ jobs: - lint - unit-test-frontend{% endraw %}{% if has_backend %}{% raw %} - unit-test-backend{% endraw %}{% endif %}{% raw %} + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' {% endraw %}{% if not deploy_as_executable %}{% raw %} uses: ./.github/workflows/build-docker-image.yaml with: context: ./frontend @@ -131,6 +156,8 @@ jobs: - lint - unit-test-frontend - unit-test-backend + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' uses: ./.github/workflows/build-docker-image.yaml with: context: ./backend @@ -145,6 +172,8 @@ jobs: - unit-test-frontend - unit-test-backend - build-frontend + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' strategy: matrix: os: @@ -196,6 +225,8 @@ jobs: needs: - build-frontend{% endraw %}{% if has_backend %}{% raw %} - build-backend{% endraw %}{% endif %}{% raw %} + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' steps: - name: Checkout code uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} @@ -218,6 +249,8 @@ jobs: runs-on: ${{ matrix.os }} needs: - build-backend + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' steps: - name: Checkout code uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} @@ -264,6 +297,8 @@ jobs: needs: - build-frontend{% endraw %}{% if has_backend %}{% raw %} - build-backend{% endraw %}{% endif %}{% raw %} + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' steps: - name: Checkout code uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} @@ -321,6 +356,8 @@ jobs: needs: - build-frontend{% endraw %}{% if has_backend %}{% raw %} - build-backend{% endraw %}{% endif %}{% raw %} + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' steps: - name: Checkout repository uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} @@ -356,6 +393,8 @@ jobs: needs: - build-frontend - build-backend + - check-skip-duplicate + if: needs.check-skip-duplicate.outputs.should-run == 'true' steps: - name: Checkout repository uses: actions/checkout@{% endraw %}{{ gha_checkout }}{% raw %} @@ -388,11 +427,16 @@ jobs: path: ./backend/src/backend_api/firmware/src/**/* if-no-files-found: error{% endraw %}{% endif %}{% raw %} - required-check: - timeout-minutes: {% endraw %}{{ gha_short_timeout_minutes }}{% raw %} + 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: {% endraw %}{{ gha_linux_runner }}{% raw %} + timeout-minutes: {% endraw %}{{ gha_short_timeout_minutes }}{% raw %} needs: - get-values + - check-skip-duplicate - lint - unit-test-frontend{% endraw %}{% if create_docker_image_tar_artifact %}{% raw %} - package-images{% endraw %}{% endif %}{% raw %} @@ -400,6 +444,9 @@ jobs: - package-firmware{% endraw %}{% endif %}{% raw %}{% endraw %}{% if run_backend_e2e_in_ci %}{% raw %} - e2e-test-backend{% endraw %}{% endif %}{% raw %} - e2e-test + - confirm-on-tagged-copier-template + permissions: + statuses: write # needed for updating status on Dependabot PRs if: always() steps: - name: fail if prior job failure @@ -407,18 +454,32 @@ 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.check-skip-duplicate.result }}" =~ $success_pattern ]] || [[ ! "${{ needs.lint.result }}" =~ $success_pattern ]] || [[ ! "${{ needs.unit-test-frontend.result }}" =~ $success_pattern ]] ||{% endraw %}{% if create_docker_image_tar_artifact %}{% raw %} [[ ! "${{ needs.package-images.result }}" =~ $success_pattern ]] ||{% endraw %}{% endif %}{% raw %} [[ ! "${{ needs.test-compiled-frontend.result }}" =~ $success_pattern ]] ||{% endraw %}{% if is_circuit_python_driver %}{% raw %} [[ ! "${{ needs.package-firmware.result }}" =~ $success_pattern ]] ||{% endraw %}{% endif %}{% raw %}{% endraw %}{% if run_backend_e2e_in_ci %}{% raw %} [[ ! "${{ needs.e2e-test-backend.result }}" =~ $success_pattern ]] ||{% endraw %}{% endif %}{% raw %} - [[ ! "${{ needs.e2e-test.result }}" =~ $success_pattern ]]; then + [[ ! "${{ needs.e2e-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 000000000..f042baef6 --- /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/AGENTS.md b/template/AGENTS.md index 958ae123e..f40f69b61 100644 --- a/template/AGENTS.md +++ b/template/AGENTS.md @@ -51,8 +51,8 @@ ## 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`.