From b3147dca70b63de42275aa8c6b1d61770c33d5f7 Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Tue, 29 Sep 2026 20:17:48 +0000 Subject: [PATCH 01/11] Copier update: pyrefly implicit reexport for non-libraries Co-Authored-By: Claude Opus 5.5 --- .claude/.beads/config.yaml | 5 - .claude/commands/add-command.md | 2 +- .claude/commands/commit.md | 2 +- .claude/commands/create-adr.md | 2 +- .claude/commands/create-issues.md | 14 +- .claude/commands/gap.md | 2 +- .claude/commands/green.md | 7 +- .claude/commands/issue.md | 13 +- .claude/commands/polish.md | 2 +- .claude/commands/red.md | 7 +- .claude/commands/refactor.md | 7 +- .claude/commands/research.md | 2 +- .claude/commands/simplify.md | 2 +- .claude/commands/spike.md | 7 +- .claude/commands/summarize.md | 4 +- .claude/commands/tdd-review.md | 4 +- .claude/commands/tdd.md | 7 +- .claude/hooks/bash-chain-guard.js | 27 +- .../{permissions/write.jsonc => models.jsonc} | 16 +- .claude/settings/permissions/bash.jsonc | 15 +- .claude/settings/permissions/edit.jsonc | 20 ++ .claude/skills/address-pr-comments/SKILL.md | 13 +- .../address-pr-comments/fetch-pr-comments.py | 3 + .coderabbit.yaml | 2 +- .config/.copier-managed-files.json | 14 +- .config/.coveragerc | 2 +- .config/_typos.toml | 2 - .config/pylintrc.toml | 2 +- .config/pyrefly.toml | 18 +- .config/ruff-non-src.toml | 5 + .config/ruff.toml | 10 + .config/taskfiles/copier-template.yaml | 35 ++ .devcontainer/devcontainer.json | 19 +- .devcontainer/docker-compose.yml | 10 +- .devcontainer/initialize-command.sh | 9 + .devcontainer/install-ci-tooling.py | 326 ++++++++++++------ .devcontainer/on-create-command.sh | 2 +- .devcontainer/post-start-command.sh | 12 +- .github/actions/install_deps/action.yml | 56 ++- .../update-devcontainer-hash/action.yml | 2 +- .../build-docker-image.yaml | 68 +++- .github/workflows/ci.yaml | 59 ++-- .../confirm-on-tagged-copier-template.yaml | 2 +- .github/workflows/get-values.yaml | 2 +- .github/workflows/hash_git_files.py | 15 +- .github/workflows/pre-commit.yaml | 39 ++- .gitignore | 6 + .pre-commit-config.yaml | 75 ++-- AGENTS.md | 60 ++-- README.md | 5 +- Taskfile.yaml | 16 + .../openapi_schema_simplifier.py | 2 +- extensions/context.py | 95 ++--- pyproject.toml | 9 +- pyrightconfig.json | 115 ------ src/copier_tasks/copier_provenance.py | 159 ++++++--- src/copier_tasks/remove_precommit_hooks.py | 16 +- template/.claude/.beads/config.yaml | 5 - template/.claude/commands/add-command.md | 2 +- template/.claude/commands/commit.md | 2 +- template/.claude/commands/create-adr.md | 2 +- template/.claude/commands/create-issues.md | 14 +- template/.claude/commands/gap.md | 2 +- template/.claude/commands/green.md | 7 +- template/.claude/commands/issue.md | 13 +- template/.claude/commands/polish.md | 2 +- template/.claude/commands/red.md | 7 +- template/.claude/commands/refactor.md | 7 +- template/.claude/commands/research.md | 2 +- template/.claude/commands/simplify.md | 2 +- template/.claude/commands/spike.md | 7 +- template/.claude/commands/summarize.md | 4 +- template/.claude/commands/tdd-review.md | 4 +- template/.claude/commands/tdd.md | 7 +- template/.claude/hooks/bash-chain-guard.js | 27 +- .../{permissions/write.jsonc => models.jsonc} | 16 +- .../.claude/settings/permissions/bash.jsonc | 15 +- .../.claude/settings/permissions/edit.jsonc | 20 ++ .../skills/address-pr-comments/SKILL.md | 13 +- .../address-pr-comments/fetch-pr-comments.py | 3 + template/.coderabbit.yaml | 2 +- template/.config/.coveragerc | 2 +- template/.config/_typos.toml | 2 - template/.config/pylintrc.toml | 2 +- template/.config/pyrefly.toml | 22 +- template/.config/ruff-non-src.toml | 5 + template/.config/ruff.toml | 10 + .../.config/taskfiles/copier-template.yaml | 35 ++ .../.devcontainer/devcontainer.json.jinja | 19 +- .../.devcontainer/docker-compose.yml.jinja | 12 +- template/.devcontainer/initialize-command.sh | 9 + .../.devcontainer/install-ci-tooling.py.jinja | 318 +++++++++++------ .../.devcontainer/on-create-command.sh.jinja | 2 +- .../.devcontainer/post-start-command.sh.jinja | 12 +- .../actions/comment_pull_request/action.yml | 75 ++++ .../.github/actions/install_deps/action.yml | 56 ++- .../pulumi_ephemeral_deploy/action.yml | 12 +- .../update-devcontainer-hash/action.yml | 2 +- template/.github/workflows/ci.yaml.jinja | 15 +- .../confirm-on-tagged-copier-template.yaml | 2 +- template/.github/workflows/get-values.yaml | 2 +- template/.github/workflows/hash_git_files.py | 15 +- template/.github/workflows/pre-commit.yaml | 39 ++- template/.github/workflows/pulumi-aws.yml | 2 +- template/.gitignore | 6 + template/.pre-commit-config.yaml | 75 ++-- template/AGENTS.md | 60 ++-- template/README.md.jinja | 5 +- template/Taskfile.yaml.jinja | 9 + template/pyrightconfig.json | 113 ------ tests/__init__.py | 7 + tests/conftest.py | 15 + tests/unit/copier_tasks/helpers.py | 3 + .../copier_tasks/test_copier_provenance.py | 95 ++++- .../test_remove_precommit_hooks.py | 2 +- uv.lock | 96 ++---- 116 files changed, 1725 insertions(+), 1068 deletions(-) rename .claude/settings/{permissions/write.jsonc => models.jsonc} (64%) create mode 100644 .claude/settings/permissions/edit.jsonc create mode 100644 .config/taskfiles/copier-template.yaml create mode 100644 Taskfile.yaml delete mode 100644 pyrightconfig.json rename template/.claude/settings/{permissions/write.jsonc => models.jsonc} (64%) create mode 100644 template/.claude/settings/permissions/edit.jsonc create mode 100644 template/.config/taskfiles/copier-template.yaml create mode 100644 template/.github/actions/comment_pull_request/action.yml create mode 100644 template/Taskfile.yaml.jinja delete mode 100644 template/pyrightconfig.json create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py diff --git a/.claude/.beads/config.yaml b/.claude/.beads/config.yaml index 46b9e82..5d0ceec 100644 --- a/.claude/.beads/config.yaml +++ b/.claude/.beads/config.yaml @@ -26,11 +26,6 @@ # Default actor for audit trails (overridden by BD_ACTOR or --actor) # actor: "" -# Export events (audit trail) to .beads/events.jsonl on each flush/sync -# When enabled, new events are appended incrementally using a high-water mark. -# Use 'bd export --events' to trigger manually regardless of this setting. -# events-export: false - # Multi-repo configuration (experimental - bd-307) # Allows hydrating from multiple repositories and routing writes to the correct JSONL # repos: diff --git a/.claude/commands/add-command.md b/.claude/commands/add-command.md index ceb45a2..23be23e 100644 --- a/.claude/commands/add-command.md +++ b/.claude/commands/add-command.md @@ -11,7 +11,7 @@ argument-hint: - Write natural, descriptive code without meta-commentary about the development process - The code should speak for itself - TDD is the process, not the product -Beads is available for task tracking. Use `mcp__beads__*` tools to manage issues (the user interacts via `bd` commands). +Beads is available for task tracking. Use `bd` CLI commands to manage issues. ## Plan File Restriction diff --git a/.claude/commands/commit.md b/.claude/commands/commit.md index e40205e..2ac4ec5 100644 --- a/.claude/commands/commit.md +++ b/.claude/commands/commit.md @@ -11,7 +11,7 @@ argument-hint: [optional-commit-description] - Write natural, descriptive code without meta-commentary about the development process - The code should speak for itself - TDD is the process, not the product -Beads is available for task tracking. Use `mcp__beads__*` tools to manage issues (the user interacts via `bd` commands). +Beads is available for task tracking. Use `bd` CLI commands to manage issues. ## Plan File Restriction diff --git a/.claude/commands/create-adr.md b/.claude/commands/create-adr.md index 68bab70..4b79f29 100644 --- a/.claude/commands/create-adr.md +++ b/.claude/commands/create-adr.md @@ -17,7 +17,7 @@ Create a new ADR to document an architectural decision. ADRs capture the "why" b - Write natural, descriptive code without meta-commentary about the development process - The code should speak for itself - TDD is the process, not the product -Beads is available for task tracking. Use `mcp__beads__*` tools to manage issues (the user interacts via `bd` commands). +Beads is available for task tracking. Use `bd` CLI commands to manage issues. ## Plan File Restriction diff --git a/.claude/commands/create-issues.md b/.claude/commands/create-issues.md index 7c1ec62..ae37f02 100644 --- a/.claude/commands/create-issues.md +++ b/.claude/commands/create-issues.md @@ -15,7 +15,7 @@ Create structured implementation plan that bridges product thinking (PRD) with t - Write natural, descriptive code without meta-commentary about the development process - The code should speak for itself - TDD is the process, not the product -Beads is available for task tracking. Use `mcp__beads__*` tools to manage issues (the user interacts via `bd` commands). +Beads is available for task tracking. Use `bd` CLI commands to manage issues. ## Plan File Restriction @@ -113,7 +113,7 @@ For each task, create a bd issue with: ```bash bd create "Task title" \ - --type [feature|bug|task|chore] \ + --type [feature|bug|task|epic|chore|decision] \ --priority [1-3] \ --description "Context and what needs to be built" \ --design "Technical approach, architecture notes" \ @@ -149,8 +149,10 @@ bd create "Task title" \ **Dependencies**: Link related issues +The first argument is the issue that becomes blocked; the second is the blocker it waits on. + ```bash -bd dep add ISSUE-123 ISSUE-456 --type blocks +bd dep add --type blocks ``` ### Validation @@ -179,20 +181,20 @@ After creating issues, verify: ### Beads Integration -Use Beads MCP to: +Use the `bd` CLI to: - Track work with `bd ready` to find next task - Create issues with `bd create "description"` - Track dependencies with `bd dep add` -See for more information. +See for more information. ## Integration with Other Commands - **Before /create-issues**: Use `/spike` if you need technical exploration first - **After /create-issues**: Use `/red` to start TDD on first task - **During work**: Use `bd update` to add notes/findings back to issues -- **When stuck**: Check `bd show ISSUE-ID` to review acceptance criteria +- **When stuck**: Check `bd show ` to review acceptance criteria -## Issue Tracking with bd (beads) +## Work Tracking with bd (beads) + +**IMPORTANT**: Use **bd (beads)** to break down and track your own work while implementing a ticket. Within a working session it is the only place to track that breakdown — do NOT use markdown TODOs, task lists, or other scratch tracking methods. -**IMPORTANT**: This project uses **bd (beads)** for ALL issue tracking. Do NOT use markdown TODOs, task lists, or other tracking methods. +bd is not the team's ticket tracker. It is a local tool for a developer and an AI agent working together: `bd init --stealth` adds `.beads/` to `.git/info/exclude`, so neither the database nor its JSONL export is committed, and nothing recorded in bd is visible to reviewers, teammates, or CI. The ticket a session is implementing lives in whatever system the team owning the repository uses — GitHub Issues, Jira, Linear, or something else. ### Why bd? -- Dependency-aware: Track blockers and relationships between issues -- Git-friendly: Auto-syncs to JSONL for version control +- Dependency-aware: Track blockers and relationships between the pieces of work in a session - Agent-optimized: JSON output, ready work detection, discovered-from links -- Prevents duplicate tracking systems and confusion +- Keeps session breakdown out of the team's ticket tracker, and out of the codebase ### Quick Start @@ -107,20 +120,20 @@ bd ready --json ```bash bd create "Issue title" --description="Detailed context" -t bug|feature|task -p 0-4 --json -bd create "Issue title" --description="What this issue is about" -p 1 --deps discovered-from:bd-123 --json +bd create "Issue title" --description="What this issue is about" -p 1 --deps discovered-from: --json ``` **Claim and update:** ```bash -bd update bd-42 --status in_progress --json -bd update bd-42 --priority 1 --json +bd update --status in_progress --json +bd update --priority 1 --json ``` **Complete work:** ```bash -bd close bd-42 --reason "Completed" --json +bd close --reason "Completed" --json ``` **Creating human readable file:** @@ -137,6 +150,7 @@ bd export -o [relative path to repository root]/.claude/.beads/issues-dump.jsonl - `task` - Work item (tests, docs, refactoring) - `epic` - Large feature with subtasks - `chore` - Maintenance (dependencies, tooling) +- `decision` - An architectural decision (aliases: `dec`, `adr`) ### Priorities @@ -158,15 +172,15 @@ bd export -o [relative path to repository root]/.claude/.beads/issues-dump.jsonl ### Important Rules -- ✅ Use bd for ALL task tracking +- ✅ Use bd for ALL tracking of your own work within a session - ✅ Always use `--json` flag for programmatic use - ✅ Link discovered work with `discovered-from` dependencies - ✅ Check `bd ready` before asking "what should I work on?" -- ❌ Do NOT create markdown TODO lists -- ❌ Do NOT use external issue trackers -- ❌ Do NOT duplicate tracking systems +- ❌ Do NOT create markdown TODO lists to track session work +- ❌ Do NOT stand up a second scratch tracking system alongside bd +- ❌ Do NOT propose bd for work that needs tracking beyond the session — a bd issue is invisible to everyone else. Suggest a code comment, or a ticket in the team's own tracker, and do not assume which tracker that is -For more details, see README.md and docs/QUICKSTART.md. +For more details, run `bd quickstart` or see . # Copier-Managed Files diff --git a/README.md b/README.md index 925704e..2a45960 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,11 @@ To create a new repository using this template: # Development +Multi-step workflows are defined as [Task](https://taskfile.dev) tasks. Run `task --list` to see them; the definitions live in `.config/taskfiles/`, and `Taskfile.yaml` in the repo root is only a shim that includes them. + ## Updating from the template -This repository uses a copier template. To pull in the latest updates from the template, use the command: -`copier update --answers-file .config/.copier-answers.yml --trust --conflict rej --defaults` +This repository uses a copier template. To pull in the latest updates from the template, run `task copier-update` " @@ -164,7 +176,7 @@ def _resolve_file_src( ancestor_managed_by_src: dict[str, set[str]] | None, ) -> str: """Return the template src that originally contributed this file path.""" - if ancestor_managed_by_src: + if ancestor_managed_by_src is not None: for origin_src, origin_files in ancestor_managed_by_src.items(): if rel_str in origin_files: return origin_src @@ -212,13 +224,12 @@ def apply_file_markers( managed: dict[str, list[str]] = {} - dst_files: list[Path] = [] - for root, _, files in os.walk(dst_directory, followlinks=True): - dst_files.extend(Path(root) / fname for fname in files) - - for file in sorted(dst_files): - rel = file.relative_to(dst_directory) - if rel not in template_base_paths: + # Iterate the template paths and probe the destination rather than walking the destination: + # a destination repo can contain symlink cycles (e.g. pnpm workspace node_modules farms) that + # make os.walk(followlinks=True) never terminate. + for rel in sorted(template_base_paths): + file = dst_directory / rel + if not file.is_file(): continue rel_str = str(rel) @@ -250,7 +261,9 @@ def _read_parent_src(src_template_directory: Path) -> str | None: return None text = answers_path.read_text(encoding="utf-8") m = re.search(r"^_src_path:\s*(.+)$", text, re.MULTILINE) - return m.group(1).strip() if m else None + if m is None: + return None + return m.group(1).strip() def update_manifest( @@ -263,17 +276,21 @@ def update_manifest( manifest_path = dst_directory / _MANIFEST_RELPATH manifest_path.parent.mkdir(parents=True, exist_ok=True) - existing: dict[str, Any] = {} + existing: Manifest = {"templates": []} if manifest_path.exists(): existing = json.loads(manifest_path.read_text(encoding="utf-8")) - templates: list[dict[str, Any]] = existing.get("templates", []) - templates = [t for t in templates if t.get("src") != template_src] + templates: list[TemplateEntry] = [] + for t in existing["templates"]: + if t["src"] == template_src: + continue + templates.append(t) - entry: dict[str, Any] = {"src": template_src} - if parent_src: - entry["parent_src"] = parent_src - entry["managed_files"] = managed_files + # Both branches spell the whole entry out so the JSON key order stays src, parent_src, managed_files. + if parent_src is None: + entry: TemplateEntry = {"src": template_src, "managed_files": managed_files} + else: + entry = {"src": template_src, "parent_src": parent_src, "managed_files": managed_files} templates.append(entry) _ = manifest_path.write_text( @@ -282,60 +299,90 @@ def update_manifest( ) +def _read_ancestor_manifest(src_template_dir: Path) -> tuple[dict[str, set[str]], dict[str, str]]: + """Return each ancestor template's managed paths and its own parent, keyed by template src. + + The ancestor manifest may contain paths with a "template/" prefix (from self-stamp tasks that run + with src=dst=template/). Both the prefixed and stripped spellings are recorded so lookups match the + destination repo's layout (where "template/" doesn't exist). + """ + ancestor_managed_by_src: dict[str, set[str]] = {} + ancestor_parent_by_src: dict[str, str] = {} + ancestor_manifest_path = _find_manifest(src_template_dir.parent) + if not ancestor_manifest_path.exists(): + return ancestor_managed_by_src, ancestor_parent_by_src + + data: Manifest = json.loads(ancestor_manifest_path.read_text(encoding="utf-8")) + subdir_prefix = src_template_dir.name + "/" + for t in data["templates"]: + path_set: set[str] = set() + for f in t["managed_files"]: + path_set.add(f) + stripped = f.removeprefix(subdir_prefix) + path_set.add(stripped) + # Apply get_base_filename to each part so .jinja/.jinja-base suffixes + # and Jinja conditional names resolve to the final destination filename. + parts = Path(stripped).parts + if len(parts) > 0: + resolved = str(Path(*[get_base_filename(p) for p in parts])) + path_set.add(resolved) + ancestor_managed_by_src[t["src"]] = path_set + ancestor_parent = t.get("parent_src") + if ancestor_parent is not None: + ancestor_parent_by_src[t["src"]] = ancestor_parent + return ancestor_managed_by_src, ancestor_parent_by_src + + def main() -> None: parser = argparse.ArgumentParser(description="Add copier provenance markers and manifest") _ = parser.add_argument("src_template_dir", type=Path, help="Template source directory") _ = parser.add_argument("dst_dir", type=Path, help="Destination directory") _ = parser.add_argument("--template-src", default="", help="Template source identifier for the manifest") args = parser.parse_args() + assert isinstance(args.src_template_dir, Path) + assert isinstance(args.dst_dir, Path) + assert isinstance(args.template_src, str) + src_template_dir = args.src_template_dir + dst_dir = args.dst_dir + template_src = args.template_src # header_src drives what URL appears in file headers (empty → generic "managed by a copier template" text). # manifest_src is the key written to .config/.copier-managed-files.json and is always non-empty. - header_src = args.template_src - manifest_src = args.template_src or str(args.src_template_dir) + header_src = template_src + if template_src == "": + manifest_src = str(src_template_dir) + else: + manifest_src = template_src - ancestor_managed_by_src: dict[str, set[str]] = {} - ancestor_parent_by_src: dict[str, str] = {} - ancestor_manifest_path = _find_manifest(args.src_template_dir.parent) - if ancestor_manifest_path.exists(): - data: dict[str, Any] = json.loads(ancestor_manifest_path.read_text(encoding="utf-8")) - # The ancestor manifest may contain paths with a "template/" prefix (from self-stamp - # tasks that run with src=dst=template/). Strip that prefix so paths match the - # destination repo's layout (where "template/" doesn't exist). - subdir_prefix = args.src_template_dir.name + "/" - for t in data.get("templates", []): - path_set: set[str] = set() - for f in t.get("managed_files", []): - path_set.add(f) - stripped = f.removeprefix(subdir_prefix) - path_set.add(stripped) - # Apply get_base_filename to each part so .jinja/.jinja-base suffixes - # and Jinja conditional names resolve to the final destination filename. - parts = Path(stripped).parts - if parts: - resolved = str(Path(*[get_base_filename(p) for p in parts])) - path_set.add(resolved) - ancestor_managed_by_src[t["src"]] = path_set - if t.get("parent_src"): - ancestor_parent_by_src[t["src"]] = t["parent_src"] + ancestor_managed_by_src, ancestor_parent_by_src = _read_ancestor_manifest(src_template_dir) + + ancestor_argument: dict[str, set[str]] | None = None + if len(ancestor_managed_by_src) > 0: + ancestor_argument = ancestor_managed_by_src managed_by_src = apply_file_markers( - src_template_directory=args.src_template_dir, - dst_directory=args.dst_dir, + src_template_directory=src_template_dir, + dst_directory=dst_dir, template_src=header_src, - ancestor_managed_by_src=ancestor_managed_by_src or None, + ancestor_managed_by_src=ancestor_argument, ) # Always write an entry for the current template even when no files matched. _ = managed_by_src.setdefault(header_src, []) - parent_src = _read_parent_src(args.src_template_dir) + parent_src = _read_parent_src(src_template_dir) for src, files in managed_by_src.items(): - effective_src = manifest_src if src == header_src else src + if src == header_src: + effective_src = manifest_src + else: + effective_src = src # Current template's parent comes from copier-answers; ancestor entries carry # their own parent_src forward from the ancestor manifest so the chain survives. - effective_parent = parent_src if effective_src == manifest_src else ancestor_parent_by_src.get(src) + if effective_src == manifest_src: + effective_parent = parent_src + else: + effective_parent = ancestor_parent_by_src.get(src) update_manifest( - dst_directory=args.dst_dir, + dst_directory=dst_dir, template_src=effective_src, managed_files=files, parent_src=effective_parent, diff --git a/src/copier_tasks/remove_precommit_hooks.py b/src/copier_tasks/remove_precommit_hooks.py index a2ed8aa..2854e02 100644 --- a/src/copier_tasks/remove_precommit_hooks.py +++ b/src/copier_tasks/remove_precommit_hooks.py @@ -15,7 +15,9 @@ def _is_matching_hook_block(block_lines: list[str], hook_id_pattern: re.Pattern[str]) -> bool: - return bool(hook_id_pattern.search(block_lines[0])) if block_lines else False + if len(block_lines) == 0: + return False + return hook_id_pattern.search(block_lines[0]) is not None def remove_hook_blocks(config_path: Path, hook_id_pattern: re.Pattern[str]) -> int: @@ -31,7 +33,7 @@ def remove_hook_blocks(config_path: Path, hook_id_pattern: re.Pattern[str]) -> i stripped = line.lstrip() indentation = len(line) - len(stripped) - if HOOK_ID_LINE.match(stripped): + if HOOK_ID_LINE.match(stripped) is not None: block_start = index block_end = index + 1 @@ -39,10 +41,12 @@ def remove_hook_blocks(config_path: Path, hook_id_pattern: re.Pattern[str]) -> i next_line = lines[block_end] next_stripped = next_line.lstrip() next_indentation = len(next_line) - len(next_stripped) - if HOOK_ID_LINE.match(next_stripped) and next_indentation == indentation: - break - if next_stripped and next_indentation < indentation: - break + if HOOK_ID_LINE.match(next_stripped) is not None: + if next_indentation == indentation: + break + if next_stripped != "": + if next_indentation < indentation: + break block_end += 1 block_lines = lines[block_start:block_end] diff --git a/template/.claude/.beads/config.yaml b/template/.claude/.beads/config.yaml index 46b9e82..5d0ceec 100644 --- a/template/.claude/.beads/config.yaml +++ b/template/.claude/.beads/config.yaml @@ -26,11 +26,6 @@ # Default actor for audit trails (overridden by BD_ACTOR or --actor) # actor: "" -# Export events (audit trail) to .beads/events.jsonl on each flush/sync -# When enabled, new events are appended incrementally using a high-water mark. -# Use 'bd export --events' to trigger manually regardless of this setting. -# events-export: false - # Multi-repo configuration (experimental - bd-307) # Allows hydrating from multiple repositories and routing writes to the correct JSONL # repos: diff --git a/template/.claude/commands/add-command.md b/template/.claude/commands/add-command.md index ceb45a2..23be23e 100644 --- a/template/.claude/commands/add-command.md +++ b/template/.claude/commands/add-command.md @@ -11,7 +11,7 @@ argument-hint: - Write natural, descriptive code without meta-commentary about the development process - The code should speak for itself - TDD is the process, not the product -Beads is available for task tracking. Use `mcp__beads__*` tools to manage issues (the user interacts via `bd` commands). +Beads is available for task tracking. Use `bd` CLI commands to manage issues. ## Plan File Restriction diff --git a/template/.claude/commands/commit.md b/template/.claude/commands/commit.md index e40205e..2ac4ec5 100644 --- a/template/.claude/commands/commit.md +++ b/template/.claude/commands/commit.md @@ -11,7 +11,7 @@ argument-hint: [optional-commit-description] - Write natural, descriptive code without meta-commentary about the development process - The code should speak for itself - TDD is the process, not the product -Beads is available for task tracking. Use `mcp__beads__*` tools to manage issues (the user interacts via `bd` commands). +Beads is available for task tracking. Use `bd` CLI commands to manage issues. ## Plan File Restriction diff --git a/template/.claude/commands/create-adr.md b/template/.claude/commands/create-adr.md index 68bab70..4b79f29 100644 --- a/template/.claude/commands/create-adr.md +++ b/template/.claude/commands/create-adr.md @@ -17,7 +17,7 @@ Create a new ADR to document an architectural decision. ADRs capture the "why" b - Write natural, descriptive code without meta-commentary about the development process - The code should speak for itself - TDD is the process, not the product -Beads is available for task tracking. Use `mcp__beads__*` tools to manage issues (the user interacts via `bd` commands). +Beads is available for task tracking. Use `bd` CLI commands to manage issues. ## Plan File Restriction diff --git a/template/.claude/commands/create-issues.md b/template/.claude/commands/create-issues.md index 7c1ec62..ae37f02 100644 --- a/template/.claude/commands/create-issues.md +++ b/template/.claude/commands/create-issues.md @@ -15,7 +15,7 @@ Create structured implementation plan that bridges product thinking (PRD) with t - Write natural, descriptive code without meta-commentary about the development process - The code should speak for itself - TDD is the process, not the product -Beads is available for task tracking. Use `mcp__beads__*` tools to manage issues (the user interacts via `bd` commands). +Beads is available for task tracking. Use `bd` CLI commands to manage issues. ## Plan File Restriction @@ -113,7 +113,7 @@ For each task, create a bd issue with: ```bash bd create "Task title" \ - --type [feature|bug|task|chore] \ + --type [feature|bug|task|epic|chore|decision] \ --priority [1-3] \ --description "Context and what needs to be built" \ --design "Technical approach, architecture notes" \ @@ -149,8 +149,10 @@ bd create "Task title" \ **Dependencies**: Link related issues +The first argument is the issue that becomes blocked; the second is the blocker it waits on. + ```bash -bd dep add ISSUE-123 ISSUE-456 --type blocks +bd dep add --type blocks ``` ### Validation @@ -179,20 +181,20 @@ After creating issues, verify: ### Beads Integration -Use Beads MCP to: +Use the `bd` CLI to: - Track work with `bd ready` to find next task - Create issues with `bd create "description"` - Track dependencies with `bd dep add` -See for more information. +See for more information. ## Integration with Other Commands - **Before /create-issues**: Use `/spike` if you need technical exploration first - **After /create-issues**: Use `/red` to start TDD on first task - **During work**: Use `bd update` to add notes/findings back to issues -- **When stuck**: Check `bd show ISSUE-ID` to review acceptance criteria +- **When stuck**: Check `bd show ` to review acceptance criteria " + body_file="$(mktemp)" + printf '%s\n' "$marker" > "$body_file" + cat "${COMMENT_FILE_PATH}" >> "$body_file" + + # GitHub rejects comment bodies longer than 65536 characters. + if [ "$(wc -c < "$body_file")" -gt 65536 ]; then + echo "::error::Comment body is too long to post (limit is 65536 characters)." + exit 1 + fi + + # pass the marker through the environment rather than interpolating it into the jq program, + # so a tag containing quotes or backslashes cannot break the filter + existing_comment_id="$( + COMMENT_MARKER="$marker" gh api "repos/${REPOSITORY}/issues/${PR_NUMBER}/comments" \ + --paginate \ + --jq 'map(select(.body | startswith(env.COMMENT_MARKER))) | first | .id // empty' + )" + # --paginate applies the jq filter per page, so keep only the first match across all pages + existing_comment_id="${existing_comment_id%%$'\n'*}" + + if [ -n "$existing_comment_id" ]; then + gh api --method PATCH "repos/${REPOSITORY}/issues/comments/${existing_comment_id}" \ + --field "body=@${body_file}" > /dev/null + echo "Updated existing comment ${existing_comment_id}." + else + gh api --method POST "repos/${REPOSITORY}/issues/${PR_NUMBER}/comments" \ + --field "body=@${body_file}" > /dev/null + echo "Created a new comment." + fi + shell: bash diff --git a/template/.github/actions/install_deps/action.yml b/template/.github/actions/install_deps/action.yml index c53fa04..a995b8f 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' + working-directory: + description: Relative path to the directory holding the 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,30 +64,56 @@ 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: Locate the pnpm project # pnpm/setup keys the store cache off a lock file, so it can only cache when one exists in the directory the project lives in + id: pnpm-project + if: ${{ inputs.node-version != 'notUsing' }} + env: + WORKING_DIRECTORY: ${{ inputs.working-directory }} + run: | + if [ "$WORKING_DIRECTORY" = "notUsing" ]; then + project_dir="." + else + project_dir="$WORKING_DIRECTORY" + fi + echo "dir=$project_dir" >> "$GITHUB_OUTPUT" + if [ -f "$project_dir/pnpm-lock.yaml" ]; then + echo "lock_exists=true" >> "$GITHUB_OUTPUT" + else + echo "lock_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.1.0 with: - node-version: ${{ inputs.node-version }} + # an empty version makes pnpm/setup read it from the manifest in working-directory instead. Passing both a version and a package.json with a conflicting `packageManager` field is a hard error in pnpm/setup. + version: ${{ inputs.working-directory == 'notUsing' && '12.4.2' || '' }} + working-directory: ${{ steps.pnpm-project.outputs.dir }} + # cache-dependency-path is left at its default, which is the lock file inside working-directory -- the same one the probe above checks + runtime: node@${{ inputs.node-version }} + cache: ${{ steps.pnpm-project.outputs.lock_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 == 'true' && '--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.3.0 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 }} - 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' || '' }} ${{ inputs.skip-installing-pulumi-cli && '--skip-installing-pulumi-cli' || '' }} + if: ${{ inputs.install-deps == 'true' }} + run: python .devcontainer/manual-setup-deps.py ${{ inputs.python-version == 'notUsing' && '--no-python' || '' }} ${{ inputs.node-version == 'notUsing' && '--no-node' || '' }} ${{ inputs.skip-updating-devcontainer-hash == 'true' && '--skip-updating-devcontainer-hash' || '' }} ${{ inputs.skip-installing-pulumi-cli == 'true' && '--skip-installing-pulumi-cli' || '' }} shell: pwsh diff --git a/template/.github/actions/pulumi_ephemeral_deploy/action.yml b/template/.github/actions/pulumi_ephemeral_deploy/action.yml index 4f335fc..e5955f4 100644 --- a/template/.github/actions/pulumi_ephemeral_deploy/action.yml +++ b/template/.github/actions/pulumi_ephemeral_deploy/action.yml @@ -80,7 +80,7 @@ runs: shell: bash - name: AWS OIDC - uses: aws-actions/configure-aws-credentials@v6.1.0 + uses: aws-actions/configure-aws-credentials@v6.3.0 with: role-to-assume: arn:aws:iam::${{ inputs.aws-account-id }}:role/${{ inputs.aws-role-name }} aws-region: ${{ inputs.aws-region }} @@ -101,8 +101,11 @@ runs: \`\`\` EOF - if [ $(wc -c < /tmp/pulumi-preview-comment.md) -gt 65535 ]; then - resources_section=$(echo "$output" | sed -n '/^Resources:/,$p') + # the limit GitHub enforces is 65536 characters (https://github.com/orgs/community/discussions/27190), + # leave room for the hidden comment tag marker prepended when posting + if [ $(wc -c < /tmp/pulumi-preview-comment.md) -gt 65000 ]; then + # keep the last part of the section, so that the fallback comment is still under the limit even when the section itself is huge + resources_section=$(echo "$output" | sed -n '/^Resources:/,$p' | tail -c 60000) cat > /tmp/pulumi-preview-comment.md << EOF $header @@ -119,8 +122,7 @@ runs: - name: Comment the Preview on the Pull Request if: ${{ inputs.show-preview-comment-on-pr == 'true' }} - # pin this sha (v3.0.1) for extra security since this action has some permissions to write to the pull request comments - uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b + uses: ./.github/actions/comment_pull_request with: file-path: /tmp/pulumi-preview-comment.md comment-tag: previewonpr-${{ inputs.deploy-script-module-name }}-${{ inputs.stack-name }} diff --git a/template/.github/actions/update-devcontainer-hash/action.yml b/template/.github/actions/update-devcontainer-hash/action.yml index f892fe9..c0d440e 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/ci.yaml.jinja b/template/.github/workflows/ci.yaml.jinja index 05c26b2..1f9d71c 100644 --- a/template/.github/workflows/ci.yaml.jinja +++ b/template/.github/workflows/ci.yaml.jinja @@ -91,15 +91,16 @@ jobs: if: always() steps: - name: fail if prior job failure + # Iterating the needs context keeps this gate from drifting out of sync with the job list above + env: + NEEDS: ${{ toJSON(needs) }} run: | - success_pattern="^(skipped|success)$" # these are the possibilities: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#needs-context + # these are the possibilities: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#needs-context + not_ok=$(jq -r 'to_entries[] | select(.value.result | test("^(skipped|success)$") | not) | .key' <<<"$NEEDS") - if [[ ! "${{ needs.get-values.result }}" =~ $success_pattern ]] || - [[ ! "${{ needs.check-skip-duplicate.result }}" =~ $success_pattern ]] || - [[ ! "${{ needs.lint.result }}" =~ $success_pattern ]] || - [[ ! "${{ needs.pulumi-workflow.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" + if [[ -n "$not_ok" ]]; then + echo "❌ One or more jobs did not finish with skipped or success:" + echo "$not_ok" exit 1 fi echo "✅ All jobs finished with skipped or success" diff --git a/template/.github/workflows/confirm-on-tagged-copier-template.yaml b/template/.github/workflows/confirm-on-tagged-copier-template.yaml index 1195dd2..7cefada 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 44dccbe..ef2f205 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/hash_git_files.py b/template/.github/workflows/hash_git_files.py index 152c03c..be7b172 100644 --- a/template/.github/workflows/hash_git_files.py +++ b/template/.github/workflows/hash_git_files.py @@ -96,24 +96,15 @@ def compute_adler32(repo_path: Path, files: list[str], extra_paths: list[Path] | if file_path.name == "uv.lock": checksum = zlib.adler32(get_uv_lock_bytes_for_hashing(file_path), checksum) else: - with file_path.open("rb") as f: - while True: - chunk = f.read(4096) - if not chunk: - break - checksum = zlib.adler32(chunk, checksum) + # Normalize CRLF to LF so the checksum is identical on Windows (autocrlf) and Linux CI checkouts. + checksum = zlib.adler32(file_path.read_bytes().replace(b"\r\n", b"\n"), checksum) except IsADirectoryError: # Ignore symlinks that on windows sometimes get confused as being directories continue for extra_path in sorted(extra_paths or []): checksum = zlib.adler32(str(extra_path).encode("utf-8"), checksum) - with extra_path.open("rb") as f: - while True: - chunk = f.read(4096) - if not chunk: - break - checksum = zlib.adler32(chunk, checksum) + checksum = zlib.adler32(extra_path.read_bytes().replace(b"\r\n", b"\n"), checksum) return checksum diff --git a/template/.github/workflows/pre-commit.yaml b/template/.github/workflows/pre-commit.yaml index 9901520..5c641fa 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/.github/workflows/pulumi-aws.yml b/template/.github/workflows/pulumi-aws.yml index f7f6963..a8bbc48 100644 --- a/template/.github/workflows/pulumi-aws.yml +++ b/template/.github/workflows/pulumi-aws.yml @@ -121,7 +121,7 @@ jobs: IAC_GITHUB_API_TOKENS: ${{ secrets.iac-github-api-tokens }} # only used in non-enterprise situations when not using full fledged Secrets Manager steps: - name: Checkout code - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.1 with: ref: ${{ inputs.CHECKOUT_REF }} diff --git a/template/.gitignore b/template/.gitignore index cc7c8b5..d61c5f3 100644 --- a/template/.gitignore +++ b/template/.gitignore @@ -123,4 +123,10 @@ dist # macOS dev cleanliness **/.DS_Store +# Devcontainer +# The devcontainer CLI writes this lock file to pin feature digests, but we are not using it yet +.devcontainer/devcontainer-lock.json +# Written by initialize-command.sh on every devcontainer start +.devcontainer/.env + # Ignores specific to this repository diff --git a/template/.pre-commit-config.yaml b/template/.pre-commit-config.yaml index 992aeb3..adaf004 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: @@ -49,7 +49,7 @@ repos: # Reformatting (should generally come before any file format or other checks, because reformatting can change things) - repo: https://github.com/crate-ci/typos - rev: 359573a3ff38f71c93e160f2ace96d118f6ff42e # frozen: v1 + rev: 12ffd4a04b4893ab75db66a59aaad2d996c3e982 # frozen: v1 hooks: - id: typos args: [--write-changes, --force-exclude, --config, .config/_typos.toml] @@ -84,20 +84,6 @@ repos: .*\.svg| .*/vendor_files/.*| )$ - - id: pretty-format-json - exclude: | - (?x)^( - .devcontainer/devcontainer.json| - .devcontainer/devcontainer-lock.json| - .*devcontainer.json| - .*pyrightconfig.json| - .*tsconfig.json| - .*biome.jsonc| - .*/generated/open[-_]api/.*| - .*tests/.*/__snapshots__/.*| - .*/vendor_files/.*| - )$ - args: [--autofix, --no-sort-keys] - repo: https://github.com/biomejs/pre-commit rev: a450e9c04887b1305951c298db5999cd30155f74 # frozen: v2.1.1 @@ -109,11 +95,10 @@ repos: .*generated/graphql.ts| .*/generated/open[-_]api/.*| .*/schema.graphql| - .*pyrightconfig\.json| )$ - repo: https://github.com/rbubley/mirrors-prettier - rev: 515f543f5718ebfd6ce22e16708bb32c68ff96e1 # frozen: v3.8.3 + rev: 0ee178619d696787ca73d210cc191d720868c631 # frozen: v3.9.6 hooks: - id: prettier args: [--config, .config/.prettierrc] @@ -123,7 +108,6 @@ repos: .devcontainer/devcontainer.json| .devcontainer/devcontainer-lock.json| .*devcontainer.json| - .*pyrightconfig.json| .*tsconfig.json| .*/__snapshots__/.*| .*.min.js| @@ -151,6 +135,7 @@ repos: .*/__snapshots__/.*| .*/vendor_files/.*| .*\.svg| + .*\.wxs| # formatter sorts attributes and joins multi-line ones, making the Windows installer files hard to read )$ # Invalid File Checks @@ -167,10 +152,11 @@ repos: .devcontainer/devcontainer.json| .devcontainer/devcontainer-lock.json| .*devcontainer.json| - .*pyrightconfig.json| .*tsconfig.json| )$ - id: check-yaml + # some tools (e.g. pnpm) create multiple-document YAML streams + args: [--allow-multiple-documents] # .copier-answers.yml is an autogenerated YAML file by Copier that throws 'found unhashable key' errors # Helm charts contain other non-YAML markers that cause errors here. they should be checked with helm-lint instead # VCRpy cassettes sometimes contain non-YAML markers that cause errors---but they're autogenerated anyway @@ -186,9 +172,14 @@ repos: - id: check-case-conflict - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 943377262562a12b57292fc98fabd7dbf81451fe # frozen: 0.37.2 + rev: c4654a7e007c9619a5eb63c39a456572a0fa1851 # frozen: 0.38.0 hooks: - id: check-github-workflows + - id: check-jsonschema + name: check taskfiles + # Task itself silently ignores unknown keys, so a typo'd key is only caught by the schema. + args: [--schemafile, 'https://taskfile.dev/schema.json'] + files: ^(Taskfile\.yaml|(template/)?\.config/taskfiles/[^/{]+\.yaml)$ - repo: https://github.com/maresb/check-json5 rev: bd4737432a2175617a9eeaa510ab369cbc1cbd3d # frozen: v1.0.1 @@ -199,7 +190,6 @@ repos: .devcontainer/devcontainer.json| .devcontainer/devcontainer-lock.json| .*devcontainer.json| - .*pyrightconfig.json| .*tsconfig.json| )$ @@ -232,15 +222,18 @@ repos: exclude: docs/.*\.rst$ - repo: https://github.com/hadolint/hadolint - rev: 4e697ba704fd23b2409b947a319c19c3ee54d24f # frozen: v2.14.0 + rev: 2eece55955ced00200be9729e9728cb7dacca505 # frozen: v2.15.1 hooks: - id: hadolint-docker name: Lint Dockerfiles + # The hadolint pre-commit has a problem where it just always pulls the `latest` image at runtime. + # Explicitly pinning the image here to the same rev as the hook to patch it until https://github.com/hadolint/hadolint/pull/1177 gets resolved. + entry: ghcr.io/hadolint/hadolint:v2.15.1@sha256:fdf19d026b54834f88c62774fdf2a61ff3b586a42632e5070674d41a796bfdf3 hadolint exclude: .*\.jinja$ description: Runs hadolint to lint Dockerfiles - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 6fec9b7edb08fd9989088709d864a7826dc74e80 # frozen: v0.15.12 + rev: 65dbdb59d2f2d9c3bdc343c566821ad3319ddaa3 # frozen: v0.16.3 hooks: - id: ruff name: ruff-src @@ -271,41 +264,24 @@ repos: )$ - repo: https://github.com/pylint-dev/pylint - rev: 88e1ab7545a4af4aea15c305a154c164a95ab842 # frozen: v4.0.5 + rev: f5a6e3b1de17502747a40d7f92f285ccf9c6c459 # frozen: v4.0.7 hooks: - id: pylint name: pylint + # exclude any generated code + # flagging duplicate code for __all__ lists is a false positive # exclude the template files---duplication within them will be discovered during CI of that template instantiation exclude: | (?x)^( .*/generated/graphql/.*| .*/generated/open[-_]api/.*| + .*__init__\.py| template/.*| )$ args: - --rcfile=./.config/pylintrc.toml verbose: true - - repo: local - hooks: - - id: pyright - name: pyright - entry: bash -c "uv run pyright" - files: '.+\.py$' - exclude: | - (?x)^( - .*/generated/graphql/.*| - .*/generated/open[-_]api/.*| - )$ - # 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 - - repo: local hooks: - id: pyrefly @@ -337,6 +313,15 @@ repos: pass_filenames: false language: system + - id: task-list + # Schema validation cannot see across files: only Task itself resolves the include paths in the root shim and + # rejects a task name defined by two flattened includes. Listing is enough to trigger both errors. + name: validate taskfile includes and task names + entry: bash -c "task --list-all > /dev/null" + files: ^(Taskfile\.yaml|\.config/taskfiles/.*)$ + pass_filenames: false + language: system + - id: compute-devcontainer-context-hash # Devcontainer context --- this makes Github's "prebuild codespaces" feature work more intelligently for the "Configuration Change" trigger name: compute devcontainer context hash diff --git a/template/AGENTS.md b/template/AGENTS.md index d7bb4ab..7feb144 100644 --- a/template/AGENTS.md +++ b/template/AGENTS.md @@ -8,26 +8,33 @@ 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 sort or remove imports manually — pre-commit handles it. -- Always include type hints for pyright in Python -- Respect the pyright rule reportUnusedCallResult; assign unneeded return values to `_` +- 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. -- Avoid `||` (TypeScript) or `or` (Python) in `if`/`elif` conditions, and avoid `x in ['a', 'b']`-style membership tests in implementation code — coverage tools treat these as a single branch, silently masking untested paths and producing false 100% branch coverage. Use separate `if`/`elif` branches instead so each condition is independently covered. +- In non-test code — anything coverage measures — avoid `||` (TypeScript) or `or` (Python) in `if`/`elif` conditions, and avoid `x in ['a', 'b']`-style membership tests. Coverage tools treat these as a single branch, silently masking untested paths and producing false 100% branch coverage. Use separate `if`/`elif` branches instead so each condition is independently covered. Files under a test directory are exempt because coverage never measures them, so collapsing a condition there hides nothing. + +### Python + +- Always include type hints. +- Respect the pyrefly unused-call-result check; assign unneeded return values to `_` +- In non-test code, prefer explicit `if`/`else` (or a `for` loop with `if`/`return`) over one-line forms that collapse branches: a ternary, `d.get(key, default)`, returning a boolean expression `return b > 5`/`return bool(b)`, or `any()`/`all()` over a generator in place of a loop. `coverage.py` tracks branches as line-to-line arcs, so a single-line expression hides the untaken path. +- In non-test code, when filtering logic combines multiple `and`-joined guards (e.g. a null check alongside a value check), prefer a loop with explicit `if`/`continue` branches over a single-line comprehension. A compound boolean filter on one line hides individual branches from line coverage — each guard condition should be its own statement so missing test cases are surfaced. ## Testing - Always run tests with an explicit path (e.g. uv run pytest tests/unit) — test runners discover all types (unit, integration, E2E...) by default. +- Never manually start services prior to running E2E tests. The test harness boots and tears down its own services (backend, frontend, supporting services) via session fixtures. - 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). Note: `1` and `0` are not magic numbers (according to PLR2004) - 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)] == []). -- When filtering logic combines multiple `and`-joined guards (e.g. a null check alongside a value check), prefer a loop with explicit `if`/`continue` branches over a single-line comprehension. A compound boolean filter on one line hides individual branches from line coverage — each guard condition should be its own statement so missing test cases are surfaced. - When a test's final assertion is an absence (e.g., element is `null`, list is empty, modal is closed), include a prior presence assertion confirming the expected state existed before the action that removed it. A test whose only assertion is an absence check can pass vacuously if setup silently failed. - 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. - When asserting an exception is raised, verify the error message includes all key constructor arguments — not just one identifying field. This ensures the error message is fully populated and catches cases where arguments are swapped or missing. In Python: use the `match` parameter in `pytest.raises`. In TypeScript: use a regex or substring in `toThrow`, or catch and assert on error properties individually. +- Name tests with Given/When/Then where each clause means a specific thing: **When** names the single action under test plus the input that distinguishes this case; **Given** names only preconditions established before that action (fixtures, mocks, prior state) and is omitted when there are none; **Then** names the asserted outcome. +- A Given or When clause shared by every test in a group belongs on the enclosing scope (a pytest class, a `describe` block) rather than repeated in each test name. The chain from the outermost scope through the test name must read as one complete Given/When/Then, and a scope carrying a clause must state it explicitly (`TestWhenFooInvoked`, `describe("Given foo mocked to succeed")`). - When an exception has a fixed message with no variable data, prefer a specific exception subclass over `match` — the subclass type is the full assertion, and matching a hardcoded string duplicates the exception class without adding test value. In this case, suppress PT011 with an inline `# noqa: PT011` comment explaining why. - Structure each test body in this order, with a single blank line separating each section: 1. **Constants** — random/faker values and test data objects @@ -46,7 +53,7 @@ This project is a Copier template used to generate other copier templates. It is ### Python Testing -- Keep every behavioral `assert` inside the test function body — never factor assertions into helper functions or fixtures. pytest's assertion rewriting (the rich failure diff) only applies to asserts located in the test module (or conftest/registered plugins), so an `assert` hidden in an ordinary helper reports an opaque failure with no useful diff. +- Do not apply the keyword-only parameter rule (`*`) to test functions or fixtures — pytest injects its parameters, so `*` has no effect. - 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 @@ -59,8 +66,8 @@ This project is a Copier template used to generate other copier templates. It is ### 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. +- When a `data-testid` identifies one of many rendered entities, interpolate that entity's stable identifier as the dynamic value, not its display label — prefer an ID (`item.itemId`, `record.sha`) whenever the entity has one, since labels collide and change. Where the identifier *is* human-readable and no ID exists, that name is the key. +- In DOM-based unit 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. Browser automation (e.g. Playwright) fails an ambiguous single-target locator outright, so a unique `data-testid` looked up from the page is enough there. # Agent Implementations & Configurations @@ -71,29 +78,35 @@ This project is a Copier template used to generate other copier templates. It is ## Tooling - ❌ Never chain commands (`&&`, `||`, `;`, `&`) — breaks permission allow-list matcher. ✅ One command per tool call. `cd` as separate prior call. Pipes (`|`) OK. +- Before hand-assembling a multi-step workflow, run `task --list` — it is probably already a task. Definitions live in `.config/taskfiles/`; the root `Taskfile.yaml` only includes them. +- The bans on `pnpm --prefix`, `uv --directory` and direct tool invocation apply to commands you type, not to task definitions: a task's commands run with the repo root as their working directory by default. Prefer adding or extending a task over typing the long form. +- Linting and type-checking stay with `pre-commit run ` rather than a task, so that what you run is exactly what CI runs. - `cd` into a subdirectory is auto-approved; navigating up (`cd ..`) or to an absolute path (`cd /some/path`) requires a user permission prompt. Minimize such navigation: run `pre-commit` from whichever subdirectory you're already in (it walks up to find `.pre-commit-config.yaml`). - ❌ 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. - ❌ Never use `uv run python -c "import ...; print(...)"` or `inspect` to introspect Python source. ✅ Read source files directly or grep for symbols — the code is on disk and can be read without running it. - 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`. +- 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`, `pyrefly`, `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. - 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. +- Leave `import` statement ordering and unused-import removal to pre-commit — don't edit them yourself. +- In GitHub Codespaces, the injected `GITHUB_TOKEN` is a repo-scoped app token, so `gh`/`git` cannot reach other repos in the org. It lives in the container process environment (not any dotfile), so `unset` inside a Bash tool call does not persist. To grant broader access, ask the user to run `env -u GITHUB_TOKEN -u GH_TOKEN gh auth login --hostname github.com --git-protocol https --web` in their shell (the `--web` flags are required — `gh auth login` alone needs a TTY that the Bash tool does not provide, and it refuses to store credentials while `GITHUB_TOKEN` is set). Afterwards, prefix your own `gh`/`git` calls with `env -u GITHUB_TOKEN -u GH_TOKEN` so they use the stored GitHub CLI credentials instead of the app token — `GH_TOKEN` also has to be cleared because `gh` gives it precedence over both `GITHUB_TOKEN` and stored credentials. Where those credentials live depends on the environment (`gh` prefers the OS credential store and the config dir follows `GH_CONFIG_DIR`), but in these devcontainers they usually land in plain text at `~/.config/gh/hosts.yml`; `gh auth status` reports the active source. -## Issue Tracking with bd (beads) +## Work Tracking with bd (beads) + +**IMPORTANT**: Use **bd (beads)** to break down and track your own work while implementing a ticket. Within a working session it is the only place to track that breakdown — do NOT use markdown TODOs, task lists, or other scratch tracking methods. -**IMPORTANT**: This project uses **bd (beads)** for ALL issue tracking. Do NOT use markdown TODOs, task lists, or other tracking methods. +bd is not the team's ticket tracker. It is a local tool for a developer and an AI agent working together: `bd init --stealth` adds `.beads/` to `.git/info/exclude`, so neither the database nor its JSONL export is committed, and nothing recorded in bd is visible to reviewers, teammates, or CI. The ticket a session is implementing lives in whatever system the team owning the repository uses — GitHub Issues, Jira, Linear, or something else. ### Why bd? -- Dependency-aware: Track blockers and relationships between issues -- Git-friendly: Auto-syncs to JSONL for version control +- Dependency-aware: Track blockers and relationships between the pieces of work in a session - Agent-optimized: JSON output, ready work detection, discovered-from links -- Prevents duplicate tracking systems and confusion +- Keeps session breakdown out of the team's ticket tracker, and out of the codebase ### Quick Start @@ -107,20 +120,20 @@ bd ready --json ```bash bd create "Issue title" --description="Detailed context" -t bug|feature|task -p 0-4 --json -bd create "Issue title" --description="What this issue is about" -p 1 --deps discovered-from:bd-123 --json +bd create "Issue title" --description="What this issue is about" -p 1 --deps discovered-from: --json ``` **Claim and update:** ```bash -bd update bd-42 --status in_progress --json -bd update bd-42 --priority 1 --json +bd update --status in_progress --json +bd update --priority 1 --json ``` **Complete work:** ```bash -bd close bd-42 --reason "Completed" --json +bd close --reason "Completed" --json ``` **Creating human readable file:** @@ -137,6 +150,7 @@ bd export -o [relative path to repository root]/.claude/.beads/issues-dump.jsonl - `task` - Work item (tests, docs, refactoring) - `epic` - Large feature with subtasks - `chore` - Maintenance (dependencies, tooling) +- `decision` - An architectural decision (aliases: `dec`, `adr`) ### Priorities @@ -158,15 +172,15 @@ bd export -o [relative path to repository root]/.claude/.beads/issues-dump.jsonl ### Important Rules -- ✅ Use bd for ALL task tracking +- ✅ Use bd for ALL tracking of your own work within a session - ✅ Always use `--json` flag for programmatic use - ✅ Link discovered work with `discovered-from` dependencies - ✅ Check `bd ready` before asking "what should I work on?" -- ❌ Do NOT create markdown TODO lists -- ❌ Do NOT use external issue trackers -- ❌ Do NOT duplicate tracking systems +- ❌ Do NOT create markdown TODO lists to track session work +- ❌ Do NOT stand up a second scratch tracking system alongside bd +- ❌ Do NOT propose bd for work that needs tracking beyond the session — a bd issue is invisible to everyone else. Suggest a code comment, or a ticket in the team's own tracker, and do not assume which tracker that is -For more details, see README.md and docs/QUICKSTART.md. +For more details, run `bd quickstart` or see . # Copier-Managed Files diff --git a/template/README.md.jinja b/template/README.md.jinja index 5118704..f4a1330 100644 --- a/template/README.md.jinja +++ b/template/README.md.jinja @@ -25,6 +25,8 @@ To create a new logical workload (a set of AWS accounts), add one in the `src/aw # Development +Multi-step workflows are defined as [Task](https://taskfile.dev) tasks. Run `task --list` to see them; the definitions live in `.config/taskfiles/`, and `Taskfile.yaml` in the repo root is only a shim that includes them. + ## Using Pulumi Run a Pulumi Preview: ```bash @@ -32,5 +34,4 @@ uv run python -m aws_organization.lib.pulumi_deploy --stack={% endraw %}{{ pulum ``` ## Updating from the template -This repository uses a copier template. To pull in the latest updates from the template, use the command: -`copier update --answers-file .config/.copier-answers.yml --trust --conflict rej --defaults`{% endraw %} +This repository uses a copier template. To pull in the latest updates from the template, run `task copier-update`{% endraw %} diff --git a/template/Taskfile.yaml.jinja b/template/Taskfile.yaml.jinja new file mode 100644 index 0000000..acf1ccf --- /dev/null +++ b/template/Taskfile.yaml.jinja @@ -0,0 +1,9 @@ +{% raw %}# Task definitions live in .config/taskfiles/ next to the other tool configs so the repo root stays uncluttered. +# This shim exists because Task cannot be pointed at a config directory: auto-discovery covers only Taskfile.yml, taskfile.yml, Taskfile.yaml, taskfile.yaml and their .dist variants in the current directory walking up through parents, no environment variable sets the Taskfile path (https://github.com/go-task/task/issues/1721 requested one and was closed as a duplicate on 2025-12-10 with no linked replacement and no implementation), and requiring -t/--taskfile on every call would break both the ergonomics and the Claude Code bash permission matcher. +# Every include is flattened so that no task name contains a colon: Task's ':' namespace separator is not configurable and Claude Code cannot match a ':*' permission pattern (see .claude/settings/permissions/bash.jsonc). Flattened names share one global namespace, so a collision between two included files is a hard error rather than a silent shadow, and names are prefixed by hand (build-frontend) instead of namespaced (frontend:build). +version: '3' + +includes: + copier: + taskfile: ./.config/taskfiles/copier-template.yaml + flatten: true{% endraw %} diff --git a/template/pyrightconfig.json b/template/pyrightconfig.json deleted file mode 100644 index d50a68a..0000000 --- a/template/pyrightconfig.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "exclude": [ - // excludes managed by this repository - - // excludes managed by template - "**/node_modules", - "**/.precommit_cache", - "**/.ruff_cache", - "**/.npm_cache", - "**/.pipx_cache", - "**/__pycache__", - "**/vendor_files", - "**/generated/graphql", - "**/generated/open_api", - "**/.venv", - "**/venv", - "**/copier_template_resources", - "**/mutants", - "**/.claude/plugins" - ], - "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, - "reportUninitializedInstanceVariable": 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, - "pythonPlatform": "Linux", - "executionEnvironments": [ - { - "root": "tests", - // reportUninitializedInstanceVariable doesn't make any sense for test cases. See https://github.com/microsoft/pyright/discussions/4834#discussioncomment-8496931 - "reportUninitializedInstanceVariable": false - } - ] -} diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..1e7a3c3 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,7 @@ +# ============== WARNING ============================================================================== +# File is managed by copier template: gh:LabAutomationAndScreening/copier-base-template.git +# See .config/.copier-managed-files.json for details. +# +# 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. +# ===================================================================================================== diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..3fc5ccc --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +# ============== WARNING ============================================================================== +# File is managed by copier template: gh:LabAutomationAndScreening/copier-base-template.git +# See .config/.copier-managed-files.json for details. +# +# 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. +# ===================================================================================================== +import pytest + +# pytest only rewrites asserts in the modules it collects as tests (plus conftest and registered plugins), so +# an assert reached through fixtures.py/helpers.py reports a bare AssertionError with no diff unless its +# module is registered here, so a new top-level test package needs adding to this list. The subpackages are +# named individually rather than registering `tests`, which is already imported by the time this conftest runs +# and would only warn. +pytest.register_assert_rewrite("tests.unit") diff --git a/tests/unit/copier_tasks/helpers.py b/tests/unit/copier_tasks/helpers.py index 2c6d770..4b3da80 100644 --- a/tests/unit/copier_tasks/helpers.py +++ b/tests/unit/copier_tasks/helpers.py @@ -19,6 +19,8 @@ def run_copier_task( script_path: Path, *args: str, env: dict[str, str] | None = None, + # bounded so a runaway traversal fails the test rather than wedging the runner + timeout: float = 60, ) -> subprocess.CompletedProcess[str]: return subprocess.run( # noqa: S603 -- these are our own scripts [sys.executable, str(script_path), *args], @@ -26,4 +28,5 @@ def run_copier_task( capture_output=True, text=True, env=env, + timeout=timeout, ) diff --git a/tests/unit/copier_tasks/test_copier_provenance.py b/tests/unit/copier_tasks/test_copier_provenance.py index 7f3a810..9011a01 100644 --- a/tests/unit/copier_tasks/test_copier_provenance.py +++ b/tests/unit/copier_tasks/test_copier_provenance.py @@ -8,6 +8,8 @@ import json import subprocess from pathlib import Path +from typing import NotRequired +from typing import TypedDict import pytest from faker import Faker @@ -17,6 +19,25 @@ _SCRIPT_PATH = SCRIPT_PATH_ROOT / "copier_provenance.py" + +# The manifest shape is declared here rather than imported from the task module: the scripts live at a +# different import path in child templates (see helpers.SCRIPT_PATH_ROOT), and the test should assert +# against the JSON contract independently of the implementation's own definition. +class _TemplateEntry(TypedDict): + src: str + parent_src: NotRequired[str] + managed_files: list[str] + + +class _Manifest(TypedDict): + templates: list[_TemplateEntry] + + +def _read_manifest(repo_dir: Path) -> _Manifest: + manifest: _Manifest = json.loads((repo_dir / ".config" / ".copier-managed-files.json").read_text(encoding="utf-8")) + return manifest + + expected_hash_comment = """\ # ============== WARNING ============================================================================== # File is managed by a copier template. See .config/.copier-managed-files.json for details. @@ -71,7 +92,7 @@ def _run_script( template_src: str = "", ) -> subprocess.CompletedProcess[str]: args = [str(src_template_dir), str(dst_dir)] - if template_src: + if template_src != "": args += ["--template-src", template_src] result = run_copier_task(_SCRIPT_PATH, *args) assert result.returncode == 0, result.stderr @@ -163,6 +184,48 @@ def test_jinja_if_check_directory_matched(self, tmp_path: Path) -> None: assert content.startswith(expected_hash_comment) +class TestWhenTaskRunAgainstDestination: + def test_Given_symlink_cycle__Then_managed_file_stamped(self, tmp_path: Path) -> None: + # Two links back to the root rather than one: a single link is bounded by the kernel's ELOOP + # limit, so it would terminate on its own and catch no regression. + template_dir = tmp_path / "template" + template_dir.mkdir() + (template_dir / "README.md.jinja-base").touch() + + dst_dir = tmp_path / "destination" + node_modules = dst_dir / "node_modules" + node_modules.mkdir(parents=True) + (node_modules / "pkg-a").symlink_to(dst_dir, target_is_directory=True) + (node_modules / "pkg-b").symlink_to(dst_dir, target_is_directory=True) + file_content = "some content\n" + _ = (dst_dir / "README.md").write_text(file_content, encoding="utf-8") + + _ = _run_script(src_template_dir=template_dir, dst_dir=dst_dir) + + content = (dst_dir / "README.md").read_text(encoding="utf-8") + assert content == file_content + "\n" + expected_markdown_comment + "\n" + + def test_Given_broken_symlink_at_managed_path__Then_path_skipped(self, tmp_path: Path) -> None: + template_dir = tmp_path / "template" + template_dir.mkdir() + (template_dir / "config.yaml.jinja-base").touch() + (template_dir / "keep.yaml.jinja-base").touch() + + dst_dir = tmp_path / "destination" + dst_dir.mkdir() + broken = dst_dir / "config.yaml" + broken.symlink_to(dst_dir / "does-not-exist.yaml") + _ = (dst_dir / "keep.yaml").write_text("key: value\n", encoding="utf-8") + assert broken.is_symlink() + + _ = _run_script(src_template_dir=template_dir, dst_dir=dst_dir) + + manifest = _read_manifest(dst_dir) + + assert (dst_dir / "keep.yaml").read_text(encoding="utf-8").startswith(expected_hash_comment) + assert manifest["templates"][0]["managed_files"] == ["keep.yaml"] + + class TestFileExtensionComments: @pytest.mark.parametrize( ("filename", "expected_location", "expected_comment"), @@ -442,7 +505,7 @@ def test_manifest_created_with_managed_files(self, tmp_path: Path) -> None: template_src="https://github.com/org/base-template", ) - manifest = json.loads((dst_dir / ".config" / ".copier-managed-files.json").read_text(encoding="utf-8")) + manifest = _read_manifest(dst_dir) assert len(manifest["templates"]) == 1 entry = manifest["templates"][0] assert entry["src"] == "https://github.com/org/base-template" @@ -468,7 +531,7 @@ def test_manifest_is_idempotent_on_second_run(self, tmp_path: Path) -> None: template_src="https://github.com/org/base-template", ) - manifest = json.loads((dst_dir / ".config" / ".copier-managed-files.json").read_text(encoding="utf-8")) + manifest = _read_manifest(dst_dir) assert len(manifest["templates"]) == 1 def test_manifest_layering_preserves_other_template_entries(self, tmp_path: Path) -> None: @@ -491,7 +554,7 @@ def test_manifest_layering_preserves_other_template_entries(self, tmp_path: Path template_src="https://github.com/org/child-template", ) - manifest = json.loads((dst_dir / ".config" / ".copier-managed-files.json").read_text(encoding="utf-8")) + manifest = _read_manifest(dst_dir) srcs = [t["src"] for t in manifest["templates"]] assert "https://github.com/org/base-template" in srcs assert "https://github.com/org/child-template" in srcs @@ -522,7 +585,7 @@ def test_manifest_child_update_does_not_overwrite_base(self, tmp_path: Path) -> template_src="https://github.com/org/child-template", ) - manifest = json.loads((dst_dir / ".config" / ".copier-managed-files.json").read_text(encoding="utf-8")) + manifest = _read_manifest(dst_dir) assert len(manifest["templates"]) == expected_num_manifests_in_project base = next(t for t in manifest["templates"] if "base" in t["src"]) assert "a.txt" in base["managed_files"] @@ -540,7 +603,7 @@ def test_manifest_src_matches_template_src_argument(self, tmp_path: Path) -> Non template_src="https://github.com/org/my-template", ) - manifest = json.loads((dst_dir / ".config" / ".copier-managed-files.json").read_text(encoding="utf-8")) + manifest = _read_manifest(dst_dir) entry = manifest["templates"][0] assert entry["src"] == "https://github.com/org/my-template" @@ -559,7 +622,7 @@ def test_manifest_structure_is_valid(self, tmp_path: Path) -> None: template_src="https://github.com/org/my-template", ) - manifest = json.loads((dst_dir / ".config" / ".copier-managed-files.json").read_text(encoding="utf-8")) + manifest = _read_manifest(dst_dir) assert isinstance(manifest["templates"], list) for entry in manifest["templates"]: assert isinstance(entry["src"], str) @@ -585,8 +648,9 @@ def test_manifest_parent_src_discovered_from_config_copier_answers(self, tmp_pat template_src="https://github.com/org/child-template", ) - manifest = json.loads((dst_dir / ".config" / ".copier-managed-files.json").read_text(encoding="utf-8")) + manifest = _read_manifest(dst_dir) entry = manifest["templates"][0] + assert "parent_src" in entry assert entry["parent_src"] == "https://github.com/org/parent-template" def test_manifest_parent_src_falls_back_to_root_copier_answers(self, tmp_path: Path) -> None: @@ -606,8 +670,9 @@ def test_manifest_parent_src_falls_back_to_root_copier_answers(self, tmp_path: P template_src="https://github.com/org/child-template", ) - manifest = json.loads((dst_dir / ".config" / ".copier-managed-files.json").read_text(encoding="utf-8")) + manifest = _read_manifest(dst_dir) entry = manifest["templates"][0] + assert "parent_src" in entry assert entry["parent_src"] == "https://github.com/org/parent-template" def test_manifest_no_parent_src_when_no_copier_answers(self, tmp_path: Path) -> None: @@ -623,7 +688,7 @@ def test_manifest_no_parent_src_when_no_copier_answers(self, tmp_path: Path) -> template_src="https://github.com/org/root-template", ) - manifest = json.loads((dst_dir / ".config" / ".copier-managed-files.json").read_text(encoding="utf-8")) + manifest = _read_manifest(dst_dir) entry = manifest["templates"][0] assert entry["src"] == "https://github.com/org/root-template" assert "parent_src" not in entry @@ -660,7 +725,7 @@ def test_ancestor_files_attributed_to_ancestor_template(self, tmp_path: Path) -> template_src="https://github.com/org/child-template", ) - manifest = json.loads((dst_dir / ".config" / ".copier-managed-files.json").read_text(encoding="utf-8")) + manifest = _read_manifest(dst_dir) srcs = {t["src"]: t for t in manifest["templates"]} assert "https://github.com/org/base-template" in srcs assert "https://github.com/org/child-template" in srcs @@ -703,10 +768,10 @@ def test_ancestor_jinja_suffix_resolved_for_attribution(self, tmp_path: Path) -> template_src="https://github.com/org/child-template", ) - manifest = json.loads((dst_dir / ".config" / ".copier-managed-files.json").read_text(encoding="utf-8")) + manifest = _read_manifest(dst_dir) srcs = {t["src"]: t for t in manifest["templates"]} assert "README.md" in srcs["https://github.com/org/base-template"]["managed_files"] - assert "README.md" not in srcs.get("https://github.com/org/child-template", {}).get("managed_files", []) + assert "README.md" not in srcs["https://github.com/org/child-template"]["managed_files"] def test_full_chain_base_child_final_attribution(self, tmp_path: Path) -> None: # Full 3-level chain: base stamps child (step 1), child stamps grandchild (step 2). @@ -733,7 +798,7 @@ def test_full_chain_base_child_final_attribution(self, tmp_path: Path) -> None: dst_dir=child_repo, template_src="https://github.com/org/base-template", ) - child_manifest = json.loads((child_repo / ".config" / ".copier-managed-files.json").read_text(encoding="utf-8")) + child_manifest = _read_manifest(child_repo) base_entry = next(t for t in child_manifest["templates"] if "base" in t["src"]) assert "template/README.md.jinja" in base_entry["managed_files"] @@ -748,7 +813,7 @@ def test_full_chain_base_child_final_attribution(self, tmp_path: Path) -> None: dst_dir=final_repo, template_src="https://github.com/org/child-template", ) - final_manifest = json.loads((final_repo / ".config" / ".copier-managed-files.json").read_text(encoding="utf-8")) + final_manifest = _read_manifest(final_repo) srcs = {t["src"]: t for t in final_manifest["templates"]} assert "README.md" in srcs["https://github.com/org/base-template"]["managed_files"] assert "child_only.py" in srcs["https://github.com/org/child-template"]["managed_files"] diff --git a/tests/unit/copier_tasks/test_remove_precommit_hooks.py b/tests/unit/copier_tasks/test_remove_precommit_hooks.py index ea117b3..675db8c 100644 --- a/tests/unit/copier_tasks/test_remove_precommit_hooks.py +++ b/tests/unit/copier_tasks/test_remove_precommit_hooks.py @@ -74,7 +74,7 @@ def test_When_multiple_hooks_match__Then_all_removed_and_count_reported(self, tm hook_id_regex = r"^\s*-\s+id:\s+check-" original = config_path.read_text(encoding="utf-8") - expected_removed = sum(1 for line in original.splitlines() if re.match(hook_id_regex, line)) + expected_removed = len([line for line in original.splitlines() if re.match(hook_id_regex, line) is not None]) assert expected_removed > 1 result = self._run_script(hook_id_regex=hook_id_regex, target_file=config_path) diff --git a/uv.lock b/uv.lock index 7b42236..a27b7a3 100644 --- a/uv.lock +++ b/uv.lock @@ -22,7 +22,7 @@ wheels = [ [[package]] name = "copier" -version = "9.16.0" +version = "9.18.2" source = { registry = "https://pypi.org/simple/" } dependencies = [ { name = "colorama" }, @@ -39,9 +39,9 @@ dependencies = [ { name = "pyyaml" }, { name = "questionary" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/c7/3cd18cd539ff41e8e7a95b0146dbace0ba332a4c97f468066b2c2daca2ed/copier-9.16.0.tar.gz", hash = "sha256:4db1a9861d0760f745cc6241f99be37b476f849b8ad700133e2f620b7df92eb2", size = 643997, upload-time = "2026-06-23T17:09:04.995Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/60/2fe1777b27f489450ed8ac5c75390caf91a175d647be05b4284bf515b413/copier-9.18.2.tar.gz", hash = "sha256:0f97902fee79374551c9391fb1132f5a038079f3b497f4dad673295489fc032e", size = 669824, upload-time = "2026-09-07T13:14:38.435Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/a4/cf8df35b6488c04cae9aa7338c7b531a87aae2515da3993d2906d49b1dc1/copier-9.16.0-py3-none-any.whl", hash = "sha256:fadc55a22db6fb0f99d52878d3df4df3fa9c314dd96b0d81fef785a5e803114b", size = 65483, upload-time = "2026-06-23T17:09:03.617Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2a/854d86eb28ced1c4ade3d8c83bdc7ba529ac5df6c80b5d5202a090244d7a/copier-9.18.2-py3-none-any.whl", hash = "sha256:ef0e1b09dece031b09085f2aa637bdc3d5952b98d96b986feaa45a8f3a00d2c6", size = 68113, upload-time = "2026-09-07T13:14:36.934Z" }, ] [[package]] @@ -53,7 +53,6 @@ dependencies = [ { name = "copier-template-extensions" }, { name = "faker" }, { name = "pyrefly" }, - { name = "pyright", extra = ["nodejs"] }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-randomly" }, @@ -61,14 +60,13 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "copier", specifier = "==9.16.0" }, + { name = "copier", specifier = "==9.18.2" }, { name = "copier-template-extensions", specifier = "==0.3.3" }, - { name = "faker", specifier = ">=40.28.1" }, - { name = "pyrefly", specifier = ">=1.1.1" }, - { name = "pyright", extras = ["nodejs"], specifier = ">=1.1.411" }, + { name = "faker", specifier = ">=40.39.0" }, + { name = "pyrefly", specifier = ">=1.3.1" }, { name = "pytest", specifier = ">=9.1.1" }, { name = "pytest-cov", specifier = ">=7.1.0" }, - { name = "pytest-randomly", specifier = ">=4.1.0" }, + { name = "pytest-randomly", specifier = ">=5.0.0" }, ] [[package]] @@ -171,14 +169,14 @@ wheels = [ [[package]] name = "faker" -version = "40.28.1" +version = "40.39.0" source = { registry = "https://pypi.org/simple/" } dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/2d/3ead106cef42eab305e15f9c089dcc68a40387d5ce04af18585af4ebbb44/faker-40.28.1.tar.gz", hash = "sha256:2a63fb51abab8790636d4030a094cf942404cbccc9c288d1cad70e2e3e9ecd58", size = 2022717, upload-time = "2026-07-01T22:23:43.763Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/62/fea935af7a312f073c99d470b05a7c96650ee8d9562ce82e8cb7ee4c1247/faker-40.39.0.tar.gz", hash = "sha256:52799ad96fcf92aab2fc96a9e03869bbf1a634300f7cd91c38dbfd51f639f6fa", size = 2029463, upload-time = "2026-09-14T16:50:17.939Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/a6/6111b9f13c1564b0e2f5dbeeb611fd00dc731e6add2336f62b798598c73d/faker-40.28.1-py3-none-any.whl", hash = "sha256:e8d3f5c469100a553d246dce7937c291308068a5ec6c9c3a228d7878b50720be", size = 2061052, upload-time = "2026-07-01T22:23:41.946Z" }, + { url = "https://files.pythonhosted.org/packages/45/c8/901282279841ebb59216433be01aac825fb905e1a597de3b7b5ab8b0c43e/faker-40.39.0-py3-none-any.whl", hash = "sha256:c4c7ec2cddfaf602c5a8a2c960614946aa8c161250bd9d49a8846cd24d1ef028", size = 2066232, upload-time = "2026-09-14T16:50:16.061Z" }, ] [[package]] @@ -262,30 +260,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, ] -[[package]] -name = "nodeenv" -version = "1.9.1" -source = { registry = "https://pypi.org/simple/" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, -] - -[[package]] -name = "nodejs-wheel-binaries" -version = "22.13.0" -source = { registry = "https://pypi.org/simple/" } -sdist = { url = "https://files.pythonhosted.org/packages/96/6c/a6e728872c034df952d9bcd67a82da0ba1845df24117097448111896a4e8/nodejs_wheel_binaries-22.13.0.tar.gz", hash = "sha256:dd811e3e5b92f2e5d4eb236cdf1ff814c91d3d3533d90d63b4af41d07a8bb2fc", size = 7868, upload-time = "2025-01-09T06:15:55.133Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/5f/e5232e174dfd81e67f5ccfeb144f0952145cf44846ac7605acaf4a0e8d70/nodejs_wheel_binaries-22.13.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:646ab3e65296de9c578db9a1dcfbca26386d2ead70a49008f75d02e960593b2f", size = 51015509, upload-time = "2025-01-09T06:14:52.776Z" }, - { url = "https://files.pythonhosted.org/packages/be/d9/e3f1f2f4576164474cb45ad3bfabf6128d740dbd31c5c98fcb95e61e9018/nodejs_wheel_binaries-22.13.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:38c689d556f428415a77655f70d2046ddbe11604039489f029b12f317042eaf4", size = 51813936, upload-time = "2025-01-09T06:15:03.92Z" }, - { url = "https://files.pythonhosted.org/packages/4e/e3/5b1e9476bdde955d5d24d969364a9f07149607b173d59c8a09653cbfdbbe/nodejs_wheel_binaries-22.13.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61885cbcfff0a6d40353c8ea3d861ccb65dba7ce4b19f22427719aeeb35d42f1", size = 57131504, upload-time = "2025-01-09T06:15:13.599Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ec/e6dad91654c30f8d1f7b19fd05fe87a436274e3c4d82ade98ae9621731fb/nodejs_wheel_binaries-22.13.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e358439ff830db766006e6eeb85ababb94b8c41c7325c9eb1f873a52d3df3016", size = 57631959, upload-time = "2025-01-09T06:15:21.998Z" }, - { url = "https://files.pythonhosted.org/packages/bf/1c/4953049e13d58151a0db3c2e6a5bb1be18c66f3c672813b2bf0577040a1d/nodejs_wheel_binaries-22.13.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e26d39448cbb2969da10f5e62e53101591e4d6c6a6ae8d623c65f56bf28ce6b", size = 59735661, upload-time = "2025-01-09T06:15:31.132Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a2/6f292b11c70a095ab9d84a7bb4bdfbece36818c925c011769ae1d4344ac3/nodejs_wheel_binaries-22.13.0-py2.py3-none-win_amd64.whl", hash = "sha256:cd9c6cdfd00ec48c55cbcd21a98af1fc9c2a18f039aede729de319346ed8675d", size = 40214108, upload-time = "2025-01-09T06:15:40.41Z" }, - { url = "https://files.pythonhosted.org/packages/3a/eb/4cf1c5e602c1f5071c08d5b0750ce5a1a3b857d3ee120bfa06d1a1167827/nodejs_wheel_binaries-22.13.0-py2.py3-none-win_arm64.whl", hash = "sha256:aa8af75d30c3132bc4e998126a78b11c269abc3060f1c7c697907299b21ae0e1", size = 35959537, upload-time = "2025-01-09T06:15:48.609Z" }, -] - [[package]] name = "packaging" version = "24.2" @@ -410,39 +384,21 @@ wheels = [ [[package]] name = "pyrefly" -version = "1.1.1" -source = { registry = "https://pypi.org/simple/" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/20/976165fa4b1517a1a92f393b3f4d4badabfff1165eff09d4cd4908428183/pyrefly-1.1.1.tar.gz", hash = "sha256:6deda959f8603a7dbdf112c48983e2275b2903cf33c8c739ed65d7e71a4fd520", size = 5880491, upload-time = "2026-06-18T23:45:43.785Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/d6/02ba666018c6a1cb4ddfa2db98ada721adddd374db5c29ba47a0bf2637fa/pyrefly-1.1.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f4b8595f91885bc8b5e3c282ab68d1df21201668a84e6508b1e15f2feec0bb8d", size = 13631867, upload-time = "2026-06-18T23:45:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/71/47/7a3457dbbddb513a83cf4fe527d5d5ebda5201a1010ad2a6034030e3e358/pyrefly-1.1.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6b238e1362622d47a6eb5af704fd8b613c94e8c303386efd6350e3da59fecc8", size = 13075304, upload-time = "2026-06-18T23:45:16.865Z" }, - { url = "https://files.pythonhosted.org/packages/84/df/70f4b3f42d58ed686a80df31e04eca54d88036cea4f9b96195c64ad0b2b5/pyrefly-1.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b50d4510e4f8aaea79e2c4b343a4d7a060c9451c0b2aa9bfe10d7ca1ef33d68d", size = 13446966, upload-time = "2026-06-18T23:45:19.644Z" }, - { url = "https://files.pythonhosted.org/packages/3c/53/12a19bd6c7af985bcbc13c6910d0f9f6684069ead2282a5c08c2bfbb5d03/pyrefly-1.1.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f330cf039ef3da3b910c84f3a7e431f0cf8d0c1d2dad26491d6cadf3c7cd4759", size = 14449222, upload-time = "2026-06-18T23:45:22.252Z" }, - { url = "https://files.pythonhosted.org/packages/93/f0/e55c48a50076fc0f9ecf4bdedec50456db383e01162f5e2121f8468be071/pyrefly-1.1.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6342d87c52b04f72156da04f554c4d57f3616f2b32d1763969efb22d05a1407", size = 14472947, upload-time = "2026-06-18T23:45:24.858Z" }, - { url = "https://files.pythonhosted.org/packages/b6/e7/30e085b31fed978ecb675bdbb54df566673ab550469e5af2d350f6af0be6/pyrefly-1.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c08b814ad03175e9cf47111390537161828b472044c39ab3320252b3ac6b2edd", size = 13975252, upload-time = "2026-06-18T23:45:27.247Z" }, - { url = "https://files.pythonhosted.org/packages/47/58/49c3e67641133d3fe5d8d9a660dc0826c6c37ca197d86cad05fa7dd8bfd6/pyrefly-1.1.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d50cad97f19fc893b04deff7239626cffff5dd27ffb29b7d303a1b770247b208", size = 13471780, upload-time = "2026-06-18T23:45:29.775Z" }, - { url = "https://files.pythonhosted.org/packages/71/1e/65a7ba8355e2c39d8331832905fb74dcc85fc122a3f1dfd6dbf2a88907ad/pyrefly-1.1.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2150b450ee6a6bcbe69b2d45d9a4ebc934a609e1abcf65e490433f38eb873d84", size = 13989306, upload-time = "2026-06-18T23:45:32.576Z" }, - { url = "https://files.pythonhosted.org/packages/37/de/b7ee1ab2392c36945738246fba7524439810befa3cfcc03cb6157567fc10/pyrefly-1.1.1-py3-none-win32.whl", hash = "sha256:5ffd8a8ed62fe4e6bf0afe1837d1bad149bb3b9f80e928ef248c96b836db3742", size = 12608469, upload-time = "2026-06-18T23:45:35.419Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9c/a0f5b52934bf80e9c7eff08222e7caf318287b9aef76acb8d9ac5740581b/pyrefly-1.1.1-py3-none-win_amd64.whl", hash = "sha256:4e0430f3ef69c8ac73505fd6584db70ed504665a9f0816fef7f723de510f26cb", size = 13502172, upload-time = "2026-06-18T23:45:38.375Z" }, - { url = "https://files.pythonhosted.org/packages/42/3d/4c6bcb3d456835f51445d3662a428f56c3ea5643ec798c577030ae34298c/pyrefly-1.1.1-py3-none-win_arm64.whl", hash = "sha256:83baf0db71e172665db1fca0ced50b8f7773f5192ca57e8ac6773a772b6d2fc5", size = 12895979, upload-time = "2026-06-18T23:45:41.026Z" }, -] - -[[package]] -name = "pyright" -version = "1.1.411" +version = "1.3.1" source = { registry = "https://pypi.org/simple/" } -dependencies = [ - { name = "nodeenv" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/7e/082cc1b392318bacfbc087732524f3e089e9e7fe45b7645c4649b6f1f1e9/pyrefly-1.3.1.tar.gz", hash = "sha256:b774f1dfac82867fc16fe2175d80a630e2a72a6835d5b7e492568d6bfb6b05e7", size = 6679145, upload-time = "2026-09-15T05:09:34.49Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, -] - -[package.optional-dependencies] -nodejs = [ - { name = "nodejs-wheel-binaries" }, + { url = "https://files.pythonhosted.org/packages/6b/bc/f09b5bd7eed796349bd93627aa2c3086df7ed0a114dafc3d2b2841599ee7/pyrefly-1.3.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0a2a7dfed824fb066aa8a7c8d856c152b98e998fb84b954d5f892fb9834c0bd1", size = 15090117, upload-time = "2026-09-15T05:08:58.477Z" }, + { url = "https://files.pythonhosted.org/packages/78/24/a31e7de8e0f310c9f0753826d0df8b6a8ff86793941a6bd88ba24b735633/pyrefly-1.3.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:37a1a1fb41e7190fe3964a877875dd809d6ee2887809e2c3d6e5f25a69805bfd", size = 14430773, upload-time = "2026-09-15T05:09:01.67Z" }, + { url = "https://files.pythonhosted.org/packages/75/e1/022352beb186f36d6213a95a975c42676ee418ff5e730629378d25564c5e/pyrefly-1.3.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45b7650bc651a5a784701f2fb4f579da9a421c4d1b5fd91f9048f4529e46093c", size = 14877762, upload-time = "2026-09-15T05:09:04.767Z" }, + { url = "https://files.pythonhosted.org/packages/19/2c/d62a1b445aad1fd46375b83393b8a67da53883aeeba73d269ce71b3e8fd1/pyrefly-1.3.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad57f38bc73679c14a5f62995a2bd3bc43649b7270c858487e2eab78a6f95362", size = 16118225, upload-time = "2026-09-15T05:09:07.828Z" }, + { url = "https://files.pythonhosted.org/packages/32/67/3c3fa8a13e3ce8d8322ffb3627255a41a52261f17793e1a24e9214f10857/pyrefly-1.3.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3506b1252e9f5d755d0693c5a46d46b85579d36534f6e22a41eb8e098d0dde90", size = 16051197, upload-time = "2026-09-15T05:09:10.81Z" }, + { url = "https://files.pythonhosted.org/packages/19/d1/84f81b9e32288b5d11c75e006f67e0f6b78b85da575673bd1a21524c8172/pyrefly-1.3.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39d5a17c1ad655e5f93a6e8e0be90b8db28c6015fe07d6ed4e054891c9e14ef8", size = 15440062, upload-time = "2026-09-15T05:09:14.641Z" }, + { url = "https://files.pythonhosted.org/packages/46/3c/2aa0c5ad6a2957a728bb38cfab70d1e9cafad21b0b1c1d92df74a3cf22bd/pyrefly-1.3.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c61e9dcf888beb81ca0e0c60ea4442640fdcf2f1f65bfe3d9f5d89b85516eb03", size = 14898534, upload-time = "2026-09-15T05:09:17.823Z" }, + { url = "https://files.pythonhosted.org/packages/03/5d/2974f249fb2e285f966485719d977c5a2248cc4866403af94d935775ff15/pyrefly-1.3.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a78eed1c53f37807f0e2997be8edbe833b91eb9915f2cb553a1bdcd599e7105c", size = 15477758, upload-time = "2026-09-15T05:09:20.964Z" }, + { url = "https://files.pythonhosted.org/packages/68/79/d31f8f0abf5b0610ec24183286ee8a1ee5b76c9aaaa91091399d7e1aaf60/pyrefly-1.3.1-py3-none-win32.whl", hash = "sha256:ea0d0349f1bc80fe8e00a7b3640e40db7486398b685f87b9bd5d2207063668f0", size = 14133005, upload-time = "2026-09-15T05:09:25.03Z" }, + { url = "https://files.pythonhosted.org/packages/3f/23/1859f47f536314755d80c4ec2a76d2698435ae8b73604bcd1dfa57ecb39b/pyrefly-1.3.1-py3-none-win_amd64.whl", hash = "sha256:4c24e7d99df9237916c33be23714219a7c8eaf22bad2ac0c3e1d45adcf7df8fb", size = 15066557, upload-time = "2026-09-15T05:09:28.265Z" }, + { url = "https://files.pythonhosted.org/packages/92/2a/73e3721921c2bb2c950a9f8c362ee5e2367556b5d4416eb4f8f88503e66f/pyrefly-1.3.1-py3-none-win_arm64.whl", hash = "sha256:1ed85e2b957a78e93353125d890ce3668ce450e49b74025a15c87b6e8ec7f3d1", size = 14348209, upload-time = "2026-09-15T05:09:31.422Z" }, ] [[package]] @@ -477,14 +433,14 @@ wheels = [ [[package]] name = "pytest-randomly" -version = "4.1.0" +version = "5.0.0" source = { registry = "https://pypi.org/simple/" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/b3/36192dacc0f470ac2cc516f73e01739c9a48a8224f76beada4f85e1c8a89/pytest_randomly-4.1.0.tar.gz", hash = "sha256:47f1d9746c3bc3efabd53ae1ebfb8bb385cf3d4df4b505b6d58d9c97a3dfe70f", size = 14302, upload-time = "2026-04-20T13:01:51.831Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/3b/6a40e1b9d925651e601e056a97f60d8a1daeddeac03d5609be60cb4362ce/pytest_randomly-5.0.0.tar.gz", hash = "sha256:e9c575a5873ef168ddbe340ed9e97ce9edb4492ccc821e4b2ac6bb1f0ed515d2", size = 8542, upload-time = "2026-09-01T22:34:20.441Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/db/2df9a1fca597a273f957a559c20c2d95d629928384507b2afa43ba6909d1/pytest_randomly-4.1.0-py3-none-any.whl", hash = "sha256:f55e89e53367b090c0c053697d7f9d77595543d0e0516c93978b50c0f6b252f9", size = 8353, upload-time = "2026-04-20T13:01:50.382Z" }, + { url = "https://files.pythonhosted.org/packages/10/b4/47e939285caad9a623d021512912ac08dc92a467ad075d179f43729d2934/pytest_randomly-5.0.0-py3-none-any.whl", hash = "sha256:8a0d4703115c0c25b38b6e129fc16b1947b9643ff26a41bc1d185d7e5a7689c1", size = 8920, upload-time = "2026-09-01T22:34:19.227Z" }, ] [[package]] From d62697ccc65dee2f2500c1c1eda31256ce27c42d Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Tue, 29 Sep 2026 20:28:39 +0000 Subject: [PATCH 02/11] Remove pyright dependency Co-Authored-By: Claude Opus 5.5 --- template/pyproject.toml.jinja | 1 - 1 file changed, 1 deletion(-) diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja index bc6c361..700675b 100644 --- a/template/pyproject.toml.jinja +++ b/template/pyproject.toml.jinja @@ -11,7 +11,6 @@ dependencies = [ # 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 %}", From 9dcbc4c068216638eb720e646240e9864cf83163 Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Tue, 29 Sep 2026 20:38:49 +0000 Subject: [PATCH 03/11] Fix stale copier _commit Co-Authored-By: Claude Opus 5.5 --- .config/.copier-answers.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/.copier-answers.yml b/.config/.copier-answers.yml index 754bd89..f4f0b34 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.139 +_commit: b6b69f68 _src_path: gh:LabAutomationAndScreening/copier-base-template.git copier_answers_directory: .config description: Managing an AWS Organization From 65c1520663c72811f9882ee35887623b7f5120a1 Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Tue, 29 Sep 2026 20:39:32 +0000 Subject: [PATCH 04/11] Close CommonWorkloadKwargs TypedDict for pyrefly open-unpacking Copier update from base template pulls in typing_extensions_version (plus starlette/kiota bumps); add typing-extensions as a dependency. Co-Authored-By: Claude Opus 5.5 --- .config/.copier-answers.yml | 2 +- extensions/context.py | 5 +++-- template/pyproject.toml.jinja | 1 + template/src/aws_organization/lib/workload.py | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.config/.copier-answers.yml b/.config/.copier-answers.yml index f4f0b34..f502117 100644 --- a/.config/.copier-answers.yml +++ b/.config/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: b6b69f68 +_commit: v0.1.9-8-gc05eb5f9 _src_path: gh:LabAutomationAndScreening/copier-base-template.git copier_answers_directory: .config description: Managing an AWS Organization diff --git a/extensions/context.py b/extensions/context.py index 1f7cca1..62bad24 100644 --- a/extensions/context.py +++ b/extensions/context.py @@ -41,12 +41,13 @@ def hook( # noqa: PLR0915 # yes, this is a lot of statements, but it's all just context["boto3_version"] = ">=1.43.88" context["ephemeral_pulumi_deploy_version"] = ">=0.0.7" context["pydantic_version"] = ">=2.13.5" + context["typing_extensions_version"] = ">=4.16.0" context["pyinstaller_version"] = ">=6.22.3" 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.6.0" + context["starlette_version"] = ">=1.7.0" context["uvicorn_version"] = ">=0.53.0" context["lab_auto_pulumi_version"] = ">=0.2.3" context["ariadne_codegen_version"] = ">=0.18.0" @@ -56,7 +57,7 @@ def hook( # noqa: PLR0915 # yes, this is a lot of statements, but it's all just context["structlog_version"] = ">=26.1.0" context["httpx_version"] = ">=0.28.1" context["httpx2_version"] = ">=2.13.0" - context["python_kiota_bundle_version"] = ">=1.12.3" + context["python_kiota_bundle_version"] = ">=1.14.1" context["vcrpy_version"] = ">=8.3.0" context["pytest_recording_version"] = ">=0.13.4" context["pytest_asyncio_version"] = ">=1.4.0" diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja index 700675b..b3ab3b3 100644 --- a/template/pyproject.toml.jinja +++ b/template/pyproject.toml.jinja @@ -24,6 +24,7 @@ dependencies = [ "boto3{% endraw %}{{ boto3_version }}{% raw %}", "boto3-stubs[all]{% endraw %}{{ boto3_version }}{% raw %}", "pydantic{% endraw %}{{ pydantic_version }}{% raw %}", + "typing-extensions{% endraw %}{{ typing_extensions_version }}{% raw %}", ] [tool.uv] diff --git a/template/src/aws_organization/lib/workload.py b/template/src/aws_organization/lib/workload.py index 902f661..d8b2562 100644 --- a/template/src/aws_organization/lib/workload.py +++ b/template/src/aws_organization/lib/workload.py @@ -1,5 +1,4 @@ import logging -from typing import TypedDict from ephemeral_pulumi_deploy.utils import common_tags from ephemeral_pulumi_deploy.utils import common_tags_native @@ -17,6 +16,7 @@ from pulumi_aws_native import iam from pulumi_aws_native import organizations from pulumi_aws_native import ssm +from typing_extensions import TypedDict from .account import AwsAccount from .constants import CENTRAL_INFRA_REPO_NAME @@ -189,7 +189,7 @@ def all_accounts(self) -> tuple[AwsAccount, ...]: return tuple(self.prod_accounts + self.staging_accounts + self.dev_accounts) -class CommonWorkloadKwargs(TypedDict): +class CommonWorkloadKwargs(TypedDict, closed=True): central_infra_account: AwsAccount deploy_in_workload_account_assume_role_policy: Output[GetPolicyDocumentResult] preview_in_workload_account_assume_role_policy: Output[GetPolicyDocumentResult] From 574aad59b9f379f3b0161f1bfd4881370998b161 Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Tue, 29 Sep 2026 20:42:37 +0000 Subject: [PATCH 05/11] Copier update: bump ephemeral-pulumi-deploy floor Co-Authored-By: Claude Opus 5.5 --- .config/.copier-answers.yml | 2 +- extensions/context.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/.copier-answers.yml b/.config/.copier-answers.yml index f502117..f99926a 100644 --- a/.config/.copier-answers.yml +++ b/.config/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v0.1.9-8-gc05eb5f9 +_commit: v0.1.9-9-g100534b4 _src_path: gh:LabAutomationAndScreening/copier-base-template.git copier_answers_directory: .config description: Managing an AWS Organization diff --git a/extensions/context.py b/extensions/context.py index 62bad24..a91fea8 100644 --- a/extensions/context.py +++ b/extensions/context.py @@ -39,7 +39,7 @@ def hook( # noqa: PLR0915 # yes, this is a lot of statements, but it's all just context["pulumi_github_version"] = ">=6.14.0" context["pulumi_okta_version"] = ">=6.6.0" context["boto3_version"] = ">=1.43.88" - context["ephemeral_pulumi_deploy_version"] = ">=0.0.7" + context["ephemeral_pulumi_deploy_version"] = ">=0.0.8" context["pydantic_version"] = ">=2.13.5" context["typing_extensions_version"] = ">=4.16.0" context["pyinstaller_version"] = ">=6.22.3" From 27c192291b345700cfdbfe5210bf1c24bd99ef95 Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Tue, 29 Sep 2026 20:45:57 +0000 Subject: [PATCH 06/11] Narrow stack config type to avoid explicit Any Co-Authored-By: Claude Opus 5.5 --- template/src/aws_organization/lib/pulumi_deploy.py.jinja | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/template/src/aws_organization/lib/pulumi_deploy.py.jinja b/template/src/aws_organization/lib/pulumi_deploy.py.jinja index 3ee97a9..46a2766 100644 --- a/template/src/aws_organization/lib/pulumi_deploy.py.jinja +++ b/template/src/aws_organization/lib/pulumi_deploy.py.jinja @@ -1,5 +1,4 @@ {% raw %}import logging -from typing import Any from ephemeral_pulumi_deploy import run_cli from pulumi.automation import ConfigValue @@ -9,9 +8,9 @@ from .program import pulumi_program logger = logging.getLogger(__name__) -def generate_stack_config() -> dict[str, Any]: +def generate_stack_config() -> dict[str, str | ConfigValue]: """Generate the stack configuration.""" - stack_config: dict[str, Any] = {} + stack_config: dict[str, str | ConfigValue] = {} stack_config["proj:pulumi_project_name"] = "aws-organization" stack_config["proj:aws_org_home_region"] = ConfigValue(value="{% endraw %}{{ aws_org_home_region }}{% raw %}") github_repo_name = "aws-organization" From 2da00e5f6d5711adf8b84715fc57762440f3e992 Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Tue, 29 Sep 2026 20:50:08 +0000 Subject: [PATCH 07/11] Suppress explicit-any on pulumi ResourceProvider overrides Co-Authored-By: Claude Opus 5.5 --- template/src/aws_organization/lib/account.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/template/src/aws_organization/lib/account.py b/template/src/aws_organization/lib/account.py index 30542c5..207322c 100644 --- a/template/src/aws_organization/lib/account.py +++ b/template/src/aws_organization/lib/account.py @@ -23,14 +23,14 @@ class SleepProvider(dynamic.ResourceProvider): serialize_as_secret_always = False @override - def create(self, props: dict[str, Any]) -> CreateResult: + def create(self, props: dict[str, Any]) -> CreateResult: # pyrefly: ignore[explicit-any] # signature must match pulumi's dynamic.ResourceProvider.create duration = props["seconds"] logger.info(f"Sleeping for {duration} seconds for the creation of the resource {props['name']}") time.sleep(duration) return CreateResult(id_="sleep-done", outs={}) @override - def delete(self, _id: str, _props: dict[str, Any]) -> None: + def delete(self, _id: str, _props: dict[str, Any]) -> None: # pyrefly: ignore[explicit-any] # signature must match pulumi's dynamic.ResourceProvider.delete duration = _props["seconds"] logger.info(f"Sleeping for {duration} seconds for the deletion of the resource ID {_id} named {_props['name']}") time.sleep(duration) From 9bc3e8e0825f40763bdc81357ed141225c380e7e Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Wed, 30 Sep 2026 09:50:47 +0000 Subject: [PATCH 08/11] Copier update: pyrefly implicit-reexport for non-libraries Co-Authored-By: Claude Opus 5.5 --- .config/.copier-answers.yml | 2 +- .config/pyrefly.toml | 1 + .devcontainer/devcontainer.json | 2 +- extensions/context.py | 4 +-- pyproject.toml | 2 +- template/.config/pyrefly.toml | 5 +--- .../.devcontainer/devcontainer.json.jinja | 2 +- uv.lock | 28 +++++++++---------- 8 files changed, 22 insertions(+), 24 deletions(-) diff --git a/.config/.copier-answers.yml b/.config/.copier-answers.yml index f99926a..8f5773a 100644 --- a/.config/.copier-answers.yml +++ b/.config/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v0.1.9-9-g100534b4 +_commit: v0.1.9-11-g9265e964 _src_path: gh:LabAutomationAndScreening/copier-base-template.git copier_answers_directory: .config description: Managing an AWS Organization diff --git a/.config/pyrefly.toml b/.config/pyrefly.toml index cfd348d..40fffc1 100644 --- a/.config/pyrefly.toml +++ b/.config/pyrefly.toml @@ -33,6 +33,7 @@ ignore-errors-in-generated-code = true # not sure yet how effective this is no-access = "ignore" # (already covered by ruff SLF001) untyped-import = "ignore" # equivalent of reportMissingTypeStubs=false in pyright unannotated-return = "ignore" # ruff ANN2xx already requires this, but with suppress-none-returning so that None-returning functions don't need the noise. pyrefly has no equivalent option---the error kind is all-or-nothing. +implicit-reexport = "ignore" # only matters for distributed libraries, whose public API must be deliberate; everything else is imported solely from within its own repo, where requiring __all__ or redundant aliases is pure bookkeeping [[sub-config]] matches = "../**/tests/**" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6bc9a96..737e46a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -30,7 +30,7 @@ // Python "-ms-python.vscode-pylance", // pyrefly is the language server for this project; ms-python.python auto-installs Pylance via its extension pack, and Pylance then warns on every boot that it's disabled "ms-python.python@2026.5.2026052901", - "meta.pyrefly@1.3.1", + "meta.pyrefly@1.3.2", "ms-vscode-remote.remote-containers@0.414.0", "charliermarsh.ruff@2026.84.0", diff --git a/extensions/context.py b/extensions/context.py index a91fea8..d342b89 100644 --- a/extensions/context.py +++ b/extensions/context.py @@ -49,7 +49,7 @@ def hook( # noqa: PLR0915 # yes, this is a lot of statements, but it's all just context["fastapi_offline_version"] = ">=1.7.7" context["starlette_version"] = ">=1.7.0" context["uvicorn_version"] = ">=0.53.0" - context["lab_auto_pulumi_version"] = ">=0.2.3" + context["lab_auto_pulumi_version"] = ">=0.3.0" context["ariadne_codegen_version"] = ">=0.18.0" context["pytest_mock_version"] = ">=3.16.0" context["uuid_utils_version"] = ">=1.0.0" @@ -65,7 +65,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.39.0" context["mutmut_version"] = ">=3.8.0" - context["pyrefly_version"] = ">=1.3.1" + context["pyrefly_version"] = ">=1.3.2" context["vacuum_openapi_version"] = "0.30.0" context["default_node_version"] = "24.11.1" diff --git a/pyproject.toml b/pyproject.toml index e7828af..06aab21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ "pytest-cov>=7.1.0", "pytest-randomly>=5.0.0", "faker>=40.39.0", - "pyrefly>=1.3.1", + "pyrefly>=1.3.2", "copier==9.18.2", "copier-template-extensions==0.3.3" ] diff --git a/template/.config/pyrefly.toml b/template/.config/pyrefly.toml index 6c67eb5..40fffc1 100644 --- a/template/.config/pyrefly.toml +++ b/template/.config/pyrefly.toml @@ -33,10 +33,7 @@ ignore-errors-in-generated-code = true # not sure yet how effective this is no-access = "ignore" # (already covered by ruff SLF001) untyped-import = "ignore" # equivalent of reportMissingTypeStubs=false in pyright unannotated-return = "ignore" # ruff ANN2xx already requires this, but with suppress-none-returning so that None-returning functions don't need the noise. pyrefly has no equivalent option---the error kind is all-or-nothing. -# The check keeps a distributed library's public API deliberate: consumers should only reach symbols the -# package explicitly re-exports via __all__ or a redundant alias. This project is not a distributed library, -# so every importer is in-repo and the __all__ bookkeeping buys nothing. -implicit-reexport = "ignore" +implicit-reexport = "ignore" # only matters for distributed libraries, whose public API must be deliberate; everything else is imported solely from within its own repo, where requiring __all__ or redundant aliases is pure bookkeeping [[sub-config]] matches = "../**/tests/**" diff --git a/template/.devcontainer/devcontainer.json.jinja b/template/.devcontainer/devcontainer.json.jinja index 93c0cbe..209ff26 100644 --- a/template/.devcontainer/devcontainer.json.jinja +++ b/template/.devcontainer/devcontainer.json.jinja @@ -42,7 +42,7 @@ // Python "-ms-python.vscode-pylance", // pyrefly is the language server for this project; ms-python.python auto-installs Pylance via its extension pack, and Pylance then warns on every boot that it's disabled "ms-python.python@2026.5.2026052901", - "meta.pyrefly@1.3.1", + "meta.pyrefly@1.3.2", "ms-vscode-remote.remote-containers@0.414.0", "charliermarsh.ruff@2026.84.0",{% endraw %}{% endif %}{% raw %} {% endraw %}{% if is_child_of_copier_base_template is not defined and template_uses_vuejs is defined and template_uses_vuejs is sameas(true) %}{% raw %} diff --git a/uv.lock b/uv.lock index a27b7a3..66257c2 100644 --- a/uv.lock +++ b/uv.lock @@ -63,7 +63,7 @@ requires-dist = [ { name = "copier", specifier = "==9.18.2" }, { name = "copier-template-extensions", specifier = "==0.3.3" }, { name = "faker", specifier = ">=40.39.0" }, - { name = "pyrefly", specifier = ">=1.3.1" }, + { name = "pyrefly", specifier = ">=1.3.2" }, { name = "pytest", specifier = ">=9.1.1" }, { name = "pytest-cov", specifier = ">=7.1.0" }, { name = "pytest-randomly", specifier = ">=5.0.0" }, @@ -384,21 +384,21 @@ wheels = [ [[package]] name = "pyrefly" -version = "1.3.1" +version = "1.3.2" source = { registry = "https://pypi.org/simple/" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/7e/082cc1b392318bacfbc087732524f3e089e9e7fe45b7645c4649b6f1f1e9/pyrefly-1.3.1.tar.gz", hash = "sha256:b774f1dfac82867fc16fe2175d80a630e2a72a6835d5b7e492568d6bfb6b05e7", size = 6679145, upload-time = "2026-09-15T05:09:34.49Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/78/b6ec12acb586606618c71ebe1008af59e1ca50e667895f1946fa7b656619/pyrefly-1.3.2.tar.gz", hash = "sha256:30a68ff5429a1546ec08a2d913dfe442c9553a2940d274de18833d8eda939b5e", size = 6679591, upload-time = "2026-09-28T22:10:06.292Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/bc/f09b5bd7eed796349bd93627aa2c3086df7ed0a114dafc3d2b2841599ee7/pyrefly-1.3.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0a2a7dfed824fb066aa8a7c8d856c152b98e998fb84b954d5f892fb9834c0bd1", size = 15090117, upload-time = "2026-09-15T05:08:58.477Z" }, - { url = "https://files.pythonhosted.org/packages/78/24/a31e7de8e0f310c9f0753826d0df8b6a8ff86793941a6bd88ba24b735633/pyrefly-1.3.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:37a1a1fb41e7190fe3964a877875dd809d6ee2887809e2c3d6e5f25a69805bfd", size = 14430773, upload-time = "2026-09-15T05:09:01.67Z" }, - { url = "https://files.pythonhosted.org/packages/75/e1/022352beb186f36d6213a95a975c42676ee418ff5e730629378d25564c5e/pyrefly-1.3.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45b7650bc651a5a784701f2fb4f579da9a421c4d1b5fd91f9048f4529e46093c", size = 14877762, upload-time = "2026-09-15T05:09:04.767Z" }, - { url = "https://files.pythonhosted.org/packages/19/2c/d62a1b445aad1fd46375b83393b8a67da53883aeeba73d269ce71b3e8fd1/pyrefly-1.3.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad57f38bc73679c14a5f62995a2bd3bc43649b7270c858487e2eab78a6f95362", size = 16118225, upload-time = "2026-09-15T05:09:07.828Z" }, - { url = "https://files.pythonhosted.org/packages/32/67/3c3fa8a13e3ce8d8322ffb3627255a41a52261f17793e1a24e9214f10857/pyrefly-1.3.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3506b1252e9f5d755d0693c5a46d46b85579d36534f6e22a41eb8e098d0dde90", size = 16051197, upload-time = "2026-09-15T05:09:10.81Z" }, - { url = "https://files.pythonhosted.org/packages/19/d1/84f81b9e32288b5d11c75e006f67e0f6b78b85da575673bd1a21524c8172/pyrefly-1.3.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39d5a17c1ad655e5f93a6e8e0be90b8db28c6015fe07d6ed4e054891c9e14ef8", size = 15440062, upload-time = "2026-09-15T05:09:14.641Z" }, - { url = "https://files.pythonhosted.org/packages/46/3c/2aa0c5ad6a2957a728bb38cfab70d1e9cafad21b0b1c1d92df74a3cf22bd/pyrefly-1.3.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c61e9dcf888beb81ca0e0c60ea4442640fdcf2f1f65bfe3d9f5d89b85516eb03", size = 14898534, upload-time = "2026-09-15T05:09:17.823Z" }, - { url = "https://files.pythonhosted.org/packages/03/5d/2974f249fb2e285f966485719d977c5a2248cc4866403af94d935775ff15/pyrefly-1.3.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a78eed1c53f37807f0e2997be8edbe833b91eb9915f2cb553a1bdcd599e7105c", size = 15477758, upload-time = "2026-09-15T05:09:20.964Z" }, - { url = "https://files.pythonhosted.org/packages/68/79/d31f8f0abf5b0610ec24183286ee8a1ee5b76c9aaaa91091399d7e1aaf60/pyrefly-1.3.1-py3-none-win32.whl", hash = "sha256:ea0d0349f1bc80fe8e00a7b3640e40db7486398b685f87b9bd5d2207063668f0", size = 14133005, upload-time = "2026-09-15T05:09:25.03Z" }, - { url = "https://files.pythonhosted.org/packages/3f/23/1859f47f536314755d80c4ec2a76d2698435ae8b73604bcd1dfa57ecb39b/pyrefly-1.3.1-py3-none-win_amd64.whl", hash = "sha256:4c24e7d99df9237916c33be23714219a7c8eaf22bad2ac0c3e1d45adcf7df8fb", size = 15066557, upload-time = "2026-09-15T05:09:28.265Z" }, - { url = "https://files.pythonhosted.org/packages/92/2a/73e3721921c2bb2c950a9f8c362ee5e2367556b5d4416eb4f8f88503e66f/pyrefly-1.3.1-py3-none-win_arm64.whl", hash = "sha256:1ed85e2b957a78e93353125d890ce3668ce450e49b74025a15c87b6e8ec7f3d1", size = 14348209, upload-time = "2026-09-15T05:09:31.422Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/144a78839822abad55628881bcb45f8173e6d8b87976897c9105884b5a6b/pyrefly-1.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:626dbaee16ae1169a10a4ba3293583b5ab0320102f35efaad9e03457a2fc646c", size = 15088488, upload-time = "2026-09-28T22:09:40.743Z" }, + { url = "https://files.pythonhosted.org/packages/23/aa/599756bd42e6533ee9261ebd288f846ab453b00debaef2b4789893967809/pyrefly-1.3.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2d5fc704dbf0e990dc4f15cb1ead64f1a325ae27383a50c76cfcaf0387f926bb", size = 14429996, upload-time = "2026-09-28T22:09:43.316Z" }, + { url = "https://files.pythonhosted.org/packages/90/0c/4b0678889136b1514ecd25cec11974d5bfdaeaaf9f5fe8a766ca45ec4ab5/pyrefly-1.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89dbe8a33e735fa96023c33ce5dfd81af9b0bdd2ee771e8d69b2ebffc7e0fbc7", size = 14871810, upload-time = "2026-09-28T22:09:45.42Z" }, + { url = "https://files.pythonhosted.org/packages/a6/4e/a96236391c6bb0d8b50af85e0ea6c0c56206f739931ac3031f9e000325ee/pyrefly-1.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ec0c31715d48bff279f548b85a93b5bc43d2c7bd1492a8cd703a9185cb089e5", size = 16113462, upload-time = "2026-09-28T22:09:47.619Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/d4d0966aa7008ebd8481a6d05aa53202ad40c9597705eeee87ba6e0e2025/pyrefly-1.3.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b3de468bed691e40dc6271284c8dcc05e649d7ed45280ef3d07332741efe34c", size = 16043341, upload-time = "2026-09-28T22:09:50.083Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d1/52d9ea1b7048c7316d9ff7594c170b1c06d37547a75d140c8deea7b9ddfd/pyrefly-1.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb94365fe5f854cb4fe82f600c4e2453160d328610b12981004851ee0b6e8aeb", size = 15437183, upload-time = "2026-09-28T22:09:52.56Z" }, + { url = "https://files.pythonhosted.org/packages/08/ec/24f83e4a397fa9d5c416bb4d378ee1bb18fe48bdd5698fe541636f5352f3/pyrefly-1.3.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4f730d2e515ac25827fc68f343a7aa02b1072e0062df6a430346a4ffdfb5db2f", size = 14892976, upload-time = "2026-09-28T22:09:54.916Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/a473f8e9ec31ba0682ebe44f4e51167a22a4cbdc8bfd87365b9aca23be20/pyrefly-1.3.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:60f440e1626bd5cc4a41ee90cecba80796ed01ebd6c15d9f933efb8d07ffe2b8", size = 15469689, upload-time = "2026-09-28T22:09:57.541Z" }, + { url = "https://files.pythonhosted.org/packages/d0/df/779092365e8e469fc8e7349e1c26c35b8a73df9e6be80a00265fc26a2b2b/pyrefly-1.3.2-py3-none-win32.whl", hash = "sha256:b31684189de44a25a5bf00bdf3ad16be44a7b14654702d6018bce1043357202c", size = 14135550, upload-time = "2026-09-28T22:09:59.757Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/466333e7f2891f8c357b5c226559c39ba8dde4f90be09189ecc771a086f5/pyrefly-1.3.2-py3-none-win_amd64.whl", hash = "sha256:693960cdb247d1f76b9e09b3f2554dae20034f7d442ccd4e9b3e8962a3622363", size = 15067523, upload-time = "2026-09-28T22:10:02.099Z" }, + { url = "https://files.pythonhosted.org/packages/18/32/ee66df92c692c0686d82c9ecef3876ccb0386aa318bf507016f0121a8f78/pyrefly-1.3.2-py3-none-win_arm64.whl", hash = "sha256:f7424b21331670522980ff13e08bd143659879a4d92f4832282c7d7c41a9b5d6", size = 14348271, upload-time = "2026-09-28T22:10:04.253Z" }, ] [[package]] From 1cfddd9ec7ca4bab4f27b8f3b00dfff94e6e73ee Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Wed, 30 Sep 2026 09:53:42 +0000 Subject: [PATCH 09/11] Rename ORG_MANAGED_SSM_PARAM_PREFIX for lab-auto-pulumi 0.3.0 Co-Authored-By: Claude Opus 5.5 --- .../src/aws_organization/lib/central_infra_workload.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/template/src/aws_organization/lib/central_infra_workload.py b/template/src/aws_organization/lib/central_infra_workload.py index 16fc78b..1abf61a 100644 --- a/template/src/aws_organization/lib/central_infra_workload.py +++ b/template/src/aws_organization/lib/central_infra_workload.py @@ -5,7 +5,7 @@ from ephemeral_pulumi_deploy.utils import get_aws_account_id from lab_auto_pulumi import GITHUB_PREVIEW_TOKEN_SECRET_NAME from lab_auto_pulumi import MANUAL_IAC_SECRETS_PREFIX -from lab_auto_pulumi import ORG_MANAGED_SSM_PARAM_PREFIX +from lab_auto_pulumi import ORG_MANAGED_PARAMS_AND_SECRETS_PREFIX from lab_auto_pulumi import WORKLOAD_INFO_SSM_PARAM_PREFIX from lab_auto_pulumi import AwsAccountInfo from lab_auto_pulumi import AwsLogicalWorkload @@ -112,7 +112,7 @@ def build_central_infra_workload(resolved_prod_accounts: list[dict[str, str]]) - f"{central_infra_workload_name}-management-account-id", type=ssm.ParameterType.STRING, description="The AWS Account ID of the management account", - name=f"{ORG_MANAGED_SSM_PARAM_PREFIX}/management-account-id", + name=f"{ORG_MANAGED_PARAMS_AND_SECRETS_PREFIX}/management-account-id", tags=common_tags(), value=get_aws_account_id(), opts=ResourceOptions(provider=central_infra_provider, parent=central_infra_account, delete_before_replace=True), @@ -129,7 +129,7 @@ def build_central_infra_workload(resolved_prod_accounts: list[dict[str, str]]) - _ = ssm.Parameter( "central-infra-state-bucket-name", type=ssm.ParameterType.STRING, - name=f"{ORG_MANAGED_SSM_PARAM_PREFIX}/infra-state-bucket-name", + name=f"{ORG_MANAGED_PARAMS_AND_SECRETS_PREFIX}/infra-state-bucket-name", tags=common_tags(), value=central_state_bucket.bucket_name.apply(lambda x: f"{x}"), opts=ResourceOptions(provider=central_infra_provider, parent=central_infra_account, delete_before_replace=True), @@ -139,7 +139,7 @@ def build_central_infra_workload(resolved_prod_accounts: list[dict[str, str]]) - _ = ssm.Parameter( "central-infra-shared-kms-key-arn", type=ssm.ParameterType.STRING, - name=f"{ORG_MANAGED_SSM_PARAM_PREFIX}/infra-state-kms-key-arn", + name=f"{ORG_MANAGED_PARAMS_AND_SECRETS_PREFIX}/infra-state-kms-key-arn", tags=common_tags(), value=kms_key_arn, opts=ResourceOptions(provider=central_infra_provider, parent=central_infra_account, delete_before_replace=True), From 7a76e039e2bea32a062b12d1be11e3b2222f3505 Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Wed, 30 Sep 2026 09:56:47 +0000 Subject: [PATCH 10/11] Update devcontainer context hash Co-Authored-By: Claude Opus 5.5 --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 737e46a..4fca3ad 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -67,5 +67,5 @@ "initializeCommand": "sh .devcontainer/initialize-command.sh", "onCreateCommand": "sh .devcontainer/on-create-command.sh", "postStartCommand": "sh .devcontainer/post-start-command.sh" - // Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): fed84a06 # spellchecker:disable-line + // Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): ccbf4c9f # spellchecker:disable-line } From 79deaaa5d5da79bc618ae0897700d645ca1cff5b Mon Sep 17 00:00:00 2001 From: Eli Fine <5436635+ejfine@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:26:04 +0000 Subject: [PATCH 11/11] Copier update: bump template to v0.1.10 Co-Authored-By: Claude Opus 5.5 --- .config/.copier-answers.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/.copier-answers.yml b/.config/.copier-answers.yml index 8f5773a..ddf3e65 100644 --- a/.config/.copier-answers.yml +++ b/.config/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v0.1.9-11-g9265e964 +_commit: v0.1.10 _src_path: gh:LabAutomationAndScreening/copier-base-template.git copier_answers_directory: .config description: Managing an AWS Organization