Skip to content

Copier update: pnpm v11#151

Merged
ejfine merged 26 commits intomainfrom
cop-scripts
May 4, 2026
Merged

Copier update: pnpm v11#151
ejfine merged 26 commits intomainfrom
cop-scripts

Conversation

@ejfine
Copy link
Copy Markdown
Contributor

@ejfine ejfine commented May 4, 2026

Why is this change necessary?

Keep up to date with upstream template

How does this change address the issue?

Fixes linting violations in kiota fixer scripts

Adds a pnpm-workspace.yaml file to support the new requirements of v11

What side effects does this change have?

N/A

How is this change tested?

Downstream repo

Summary by CodeRabbit

Release Notes

  • Chores
    • Updated development environment with Node 2.0.0 and latest VS Code extensions.
    • Upgraded development tool versions (uv, pnpm, copier).
    • Improved Docker build process for frontend applications.
    • Added automation for handling merge conflict artifacts.
    • Refined CI/CD workflows and linting configurations.

@ejfine ejfine self-assigned this May 4, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This pull request updates the Copier template from v0.0.117 to v0.0.118, bumping numerous tool and dependency versions, restructuring the frontend Node.js module layout to the workspace root, refactoring pre-commit linting rules, adding a utility script for cleaning .rej merge artifacts, and extending build-context hashing for Docker image caching.

Changes

Template v0.0.118 Release with Infrastructure Updates

Layer / File(s) Summary
Metadata & Version Tracking
.copier-answers.yml, pyproject.toml, copier.yml
Template commit updated to v0.0.118. Copier dependency pinned to 9.15.0. New Copier task added to set executable permission on scripts/delete_false_positive_rej.py.
Dependency & Tool Version Bumps
.devcontainer/install-ci-tooling.py, extensions/context.py, template/.devcontainer/install-ci-tooling.py.jinja
UV version bumped to 0.11.8, pnpm to 11.0.4, copier to 9.15.0. Template context updated with new pnpm/npm/nvm versions, fastapi version, and multiple pulumi package versions. Install scripts refactored with lint suppressions.
Devcontainer & IDE Configuration
.devcontainer/devcontainer.json, .devcontainer/manual-setup-deps.py, template/.devcontainer/devcontainer.json.jinja, template/.devcontainer/manual-setup-deps.py.jinja
VS Code extension versions updated (CodeRabbit to 0.19.1, Claude Code to 2.1.126). Node feature upgraded from 1.7.1 to 2.0.0. PackageManager enum refactored to use StrEnum. Ruff config path updated from ruff-test.toml to ruff-non-src.toml. PNPM install behavior changed from install --frozen-lockfile to pnpm ci when lock checking is enabled.
Linting & Pre-commit Configuration
.pre-commit-config.yaml, ruff-non-src.toml, template/.pre-commit-config.yaml.jinja, template/ruff-non-src.toml
Ruff hook renamed from ruff-tests to ruff-non-src, targeting non-src Python files via regex ^(?!src/)(?!.+/src/).+\.py$ instead of tests?/.+\.py$. New ruff-non-src.toml config file added with INP001 ignore for non-test files.
CI/CD & Dependency Automation
.github/dependabot.yml, template/.github/dependabot.yml.jinja
New Dependabot configuration for devcontainers ecosystem added (weekly schedule, 5 open PR limit, semver-patch ignore).
Merge Artifact Cleanup
scripts/delete_false_positive_rej.py, template/scripts/delete_false_positive_rej.py
New script added to identify and remove .rej files when their corresponding source files have no staged/unstaged changes. Returns exit code 1 if any .rej conflicts remain, 0 otherwise.
Build Context & Hashing
.github/workflows/hash_git_files.py, template/.github/workflows/hash_git_files.py, .github/reusable_workflows/build-docker-image.yaml
compute_adler32 extended with optional extra_paths parameter for including additional files in checksum. CLI argument --extra-paths added. Workflow input additional-hash-paths added to build-docker-image.yaml and forwarded to hash computation.
Copier Workflow Validation
.github/workflows/confirm-on-tagged-copier-template.yaml, template/.github/workflows/confirm-on-tagged-copier-template.yaml
Workflow input changed from answers_file (single path) to answers_file_pattern (regex defaulting to \.copier-answers\.yml$). Validation now discovers all matching files via regex and rejects any _commit values containing hyphens (dev tags).
Frontend Build & Deployment Structure
template/.devcontainer/docker-compose.yml.jinja, template/.devcontainer/Dockerfile, template/frontend/{% if not deploy_as_executable %}Dockerfile{% endif %}.jinja, template/pnpm-workspace.yaml, template/{% if not deploy_as_executable %}docker-compose.yaml{% endif %}.jinja, template/.github/workflows/ci.yaml.jinja
Frontend node_modules volume mounted to workspace root (/workspaces/{{ repo_name }}/node_modules) instead of frontend/ subdirectory. Devcontainer FRONTEND_NODE_MODULES path updated. Frontend Dockerfile restructured for multi-stage workspace-aware build: copies pnpm-workspace.yaml and pnpm-lock.yaml first for caching, runs pnpm ci, then copies full repo and builds with pnpm --filter ./frontend generate. Production stage pulls artifacts from /app/frontend/.output/public. New pnpm-workspace.yaml added with frontend package registration and allowBuilds overrides for pre-built dependencies. Docker Compose and CI workflow updated with build-contexts: repo-root=. and additional-hash-paths for workspace files.
Backend & Diagnostics
template/{% if has_backend %}backend{% endif %}/kiota_nullable_fixer.py, template/frontend/kiota_nullable_fixer.py, .github/workflows/replace_private_package_registries.py
Kiota nullable fixer scripts enhanced with detailed progress logging (model counts, per-model processing, field injection details) via lint-suppressed print() calls. Error handling improved with explicit exception catching and sys.exit(1) on critical failures. Private package registry script updated with lint suppressions and Path() initialization.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #139: Overlapping template and devcontainer updates with version bumps in extensions/context.py and devcontainer files.
  • PR #145: Updates Copier template pin in .copier-answers.yml with overlapping edits to template devcontainer scripts.
  • PR #132: Modifies .github/reusable_workflows/build-docker-image.yaml to add workflow inputs and build-context hashing.

Suggested reviewers

  • zendern
  • idonaldson
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Copier update: pnpm v11' accurately reflects the primary change—upgrading pnpm from v10 to v11, which is the most visible dependency bump in the changeset.
Description check ✅ Passed The description follows the template structure with all required sections completed: rationale (keep upstream updated), solution approach (fixes linting, adds workspace config), side effects (N/A), and testing method (downstream repo).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@ejfine
Copy link
Copy Markdown
Contributor Author

ejfine commented May 4, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
template/.devcontainer/install-ci-tooling.py.jinja (1)

106-119: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Shell=True with list argument on non-Windows path.

On non-Windows, run_cmd = [cmd] creates a single-element list like ["npm -v"]. When passed to subprocess.run(..., shell=True), Python uses only the first element as the shell command string, which works but is unconventional. Consider using the string directly instead of wrapping in a list:

♻️ Suggested simplification
             run_cmd = (
                 [
                     pwsh,  # type: ignore[reportPossiblyUnboundVariable] # this matches the conditional above that defines pwsh
                     "-NoProfile",
                     "-NonInteractive",
                     "-Command",
                     cmd,
                 ]
                 if is_windows
-                else [cmd]
+                else cmd
             )
-            _ = subprocess.run(run_cmd, shell=True, check=True)  # noqa: S602 # we need shell=True for npm commands, and this is all our own input
+            _ = subprocess.run(run_cmd, shell=not is_windows, check=True)  # noqa: S602 # we need shell=True for npm commands on non-Windows, and this is all our own input
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@template/.devcontainer/install-ci-tooling.py.jinja` around lines 106 - 119,
The loop builds run_cmd as a list for non-Windows which is unnecessary when
calling subprocess.run(..., shell=True); update the assignment so that when
is_windows is True you keep the pwsh list (pwsh, "-NoProfile",
"-NonInteractive", "-Command", cmd) but when is_windows is False set run_cmd to
the raw string cmd (not [cmd]); adjust the branch around pnpm_install_sequence,
run_cmd, and the subprocess.run call so subprocess.run receives a string on
non-Windows and retains the existing list form for Windows (referencing
pnpm_install_sequence, run_cmd, is_windows, pwsh, and the subprocess.run
invocation).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@template/.devcontainer/devcontainer.json.jinja`:
- Around line 29-34: The Node feature entry
("ghcr.io/devcontainers/features/node:2.0.0") is pinned to a non-existent 2.0.0
release and includes an unsupported "npmVersion" option; update the feature to a
released tag (e.g., change "version" to "1.7.1") and remove the "npmVersion" key
(keep "pnpmVersion" and "nvmVersion" as-is), or if you intentionally target a
future release, revert the tag back to a valid released version and drop
unsupported options until the feature adds "npmVersion".

In `@template/.github/workflows/confirm-on-tagged-copier-template.yaml`:
- Around line 24-25: The PATTERN variable is being set via direct expression
interpolation which risks shell injection; instead pass
inputs.answers_file_pattern into the job/step environment (env: PATTERN: ${{
inputs.answers_file_pattern }}) and then reference that env var in the shell
(leave the find | grep line using the shell $PATTERN) so the input is not
expanded by the shell before being assigned; ensure the step still populates
ANSWERS_FILES via mapfile -t ANSWERS_FILES < <(find . -type f -printf '%P\n' |
grep -E "$PATTERN") but obtain PATTERN from the environment rather than inline
expression.

In `@template/frontend/kiota_nullable_fixer.py`:
- Line 551: The print message in inject_missing_typescript_fields fires whenever
interface_props is non-empty even if no replacements occurred; fix this by
capturing original_content = content at the start of
inject_missing_typescript_fields, run the existing conditional replacements (the
checks for empty_interface in content, deser_func_idx != -1, etc.), then only
emit the print for model_name (and list fields.keys()) if content !=
original_content (i.e., replacements actually modified the text); this mirrors
the backend pattern and prevents false "Injected missing fields" messages.
- Line 555: Update the function signature for main to mark parameters as
keyword-only and add the explicit None return type: change def main(schema:
dict[str, Any] | None = None): to a signature using the keyword-only marker and
return annotation (i.e., include * before schema and add -> None). Keep the
parameter name and type (schema: dict[str, Any] | None = None) unchanged
otherwise and ensure the function body remains compatible with the new
signature.
- Around line 555-572: main() currently references the global MODELS_DIR (used
at "index_file = MODELS_DIR / 'index.ts'") which is only set inside the if
__name__ == "__main__" block, causing a NameError for external callers; change
main signature to accept a models_dir parameter (e.g., def main(schema:
dict[str, Any] | None = None, models_dir: Path | None = None)), use models_dir
or fallback to DEFAULT_MODELS_DIR when computing index_file, and remove or stop
relying on any global mutation of MODELS_DIR in the __main__ block (pass the
desired models_dir into main there instead).

In `@template/ruff-test.toml`:
- Around line 22-23: Update the per-file ignore pattern under
[lint.per-file-ignores] so the negated glob covers nested test directories:
replace the existing "!tests/**" entry with a recursive pattern like
"!**/tests/**" (i.e., change the key string currently written as "!tests/**" to
"!**/tests/**") to ensure INP001 is suppressed for any tests/ directory at any
depth while keeping the same ignore list.

---

Outside diff comments:
In `@template/.devcontainer/install-ci-tooling.py.jinja`:
- Around line 106-119: The loop builds run_cmd as a list for non-Windows which
is unnecessary when calling subprocess.run(..., shell=True); update the
assignment so that when is_windows is True you keep the pwsh list (pwsh,
"-NoProfile", "-NonInteractive", "-Command", cmd) but when is_windows is False
set run_cmd to the raw string cmd (not [cmd]); adjust the branch around
pnpm_install_sequence, run_cmd, and the subprocess.run call so subprocess.run
receives a string on non-Windows and retains the existing list form for Windows
(referencing pnpm_install_sequence, run_cmd, is_windows, pwsh, and the
subprocess.run invocation).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3da97f8b-763e-42a9-8466-9c21c28302c9

📥 Commits

Reviewing files that changed from the base of the PR and between 19e42a4 and 8a5f5f0.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • .copier-answers.yml
  • .devcontainer/devcontainer.json
  • .devcontainer/install-ci-tooling.py
  • .devcontainer/manual-setup-deps.py
  • .github/dependabot.yml
  • .github/workflows/confirm-on-tagged-copier-template.yaml
  • .github/workflows/hash_git_files.py
  • .github/workflows/replace_private_package_registries.py
  • .pre-commit-config.yaml
  • copier.yml
  • extensions/context.py
  • pyproject.toml
  • ruff-test.toml
  • scripts/delete_false_positive_rej.py
  • template/.devcontainer/devcontainer.json.jinja
  • template/.devcontainer/docker-compose.yml.jinja
  • template/.devcontainer/install-ci-tooling.py.jinja
  • template/.devcontainer/manual-setup-deps.py
  • template/.github/dependabot.yml.jinja
  • template/.github/workflows/confirm-on-tagged-copier-template.yaml
  • template/.github/workflows/hash_git_files.py
  • template/.pre-commit-config.yaml.jinja
  • template/frontend/kiota_nullable_fixer.py
  • template/pnpm-workspace.yaml
  • template/ruff-test.toml
  • template/scripts/delete_false_positive_rej.py
  • template/{% if has_backend %}backend{% endif %}/kiota_nullable_fixer.py

Comment thread template/.devcontainer/devcontainer.json.jinja
Comment thread template/.github/workflows/confirm-on-tagged-copier-template.yaml Outdated
content = content[:early_idx] + filled_body + content[early_idx + len(early_return) :]

print(f" Injected missing fields into {model_name}: {', '.join(fields.keys())}")
print(f" Injected missing fields into {model_name}: {', '.join(fields.keys())}") # noqa: T201 # this just runs as a simple script, so using print instead of log
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Print fires unconditionally even when injection patterns are not found.

inject_missing_typescript_fields prints "Injected missing fields into {model_name}" whenever interface_props is non-empty, but the actual string replacements at lines 527–549 are all conditional on finding specific patterns (empty_interface in content, deser_func_idx != -1, etc.). If those patterns are absent, nothing is modified but the message still claims injection occurred.

✏️ Suggested fix
+    if content == original_content:
+        return content
+    print(f"  Injected missing fields into {model_name}: {', '.join(fields.keys())}")  # noqa: T201 # this just runs as a simple script, so using print instead of log
     return content

(Requires capturing original_content = content at the start of the function, mirroring the backend counterpart's pattern.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@template/frontend/kiota_nullable_fixer.py` at line 551, The print message in
inject_missing_typescript_fields fires whenever interface_props is non-empty
even if no replacements occurred; fix this by capturing original_content =
content at the start of inject_missing_typescript_fields, run the existing
conditional replacements (the checks for empty_interface in content,
deser_func_idx != -1, etc.), then only emit the print for model_name (and list
fields.keys()) if content != original_content (i.e., replacements actually
modified the text); this mirrors the backend pattern and prevents false
"Injected missing fields" messages.

return content


def main(schema: dict[str, Any] | None = None):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Missing -> None return type and keyword-only parameter marker.

def main(schema: dict[str, Any] | None = None): lacks the return type annotation required by pyright and the * separator for keyword-only parameters required by the project style.

✏️ Proposed fix
-def main(schema: dict[str, Any] | None = None):
+def main(*, schema: dict[str, Any] | None = None) -> None:

As per coding guidelines, "Always include type hints for pyright in Python" and "Prefer keyword-only parameters (unless a very clear single-argument function): use * in Python signatures."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def main(schema: dict[str, Any] | None = None):
def main(*, schema: dict[str, Any] | None = None) -> None:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@template/frontend/kiota_nullable_fixer.py` at line 555, Update the function
signature for main to mark parameters as keyword-only and add the explicit None
return type: change def main(schema: dict[str, Any] | None = None): to a
signature using the keyword-only marker and return annotation (i.e., include *
before schema and add -> None). Keep the parameter name and type (schema:
dict[str, Any] | None = None) unchanged otherwise and ensure the function body
remains compatible with the new signature.

Comment on lines 555 to 572
def main(schema: dict[str, Any] | None = None):
"""Main function to fix TypeScript models.
"""Fix nullable handling in generated TypeScript models.

Args:
schema: OpenAPI schema dict. If None, loads from default path for backward compatibility.
"""
if schema is None:
# Backward compatibility: load from default path if no schema provided
if not DEFAULT_OPENAPI_SCHEMA.exists():
print(f"Error: Default OpenAPI schema file not found: {DEFAULT_OPENAPI_SCHEMA}")
print(f"Error: Default OpenAPI schema file not found: {DEFAULT_OPENAPI_SCHEMA}") # noqa: T201 # this just runs as a simple script, so using print instead of log
sys.exit(1)
with DEFAULT_OPENAPI_SCHEMA.open() as f:
schema = json.load(f)
if schema is None:
print("Error: Failed to load OpenAPI schema from default path")
print("Error: Failed to load OpenAPI schema from default path") # noqa: T201 # this just runs as a simple script, so using print instead of log
sys.exit(1)

index_file = MODELS_DIR / "index.ts"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

NameError at runtime when main() is called outside __main__.

main() references MODELS_DIR at line 572, but the only assignment to MODELS_DIR is at line 647 inside the if __name__ == "__main__": block. The new schema parameter added to the main() signature explicitly invites external callers (e.g., from ... import main; main(schema)), who will hit a NameError immediately.

The fix is to either initialise MODELS_DIR at module level to DEFAULT_MODELS_DIR, or — cleaner — pass models_dir as a parameter to main().

🐛 Proposed fix: add `models_dir` parameter and remove global mutation
-def main(schema: dict[str, Any] | None = None):
+def main(*, schema: dict[str, Any] | None = None, models_dir: Path = DEFAULT_MODELS_DIR) -> None:
     """Fix nullable handling in generated TypeScript models.

     Args:
         schema: OpenAPI schema dict. If None, loads from default path for backward compatibility.
+        models_dir: Path to the directory containing generated Kiota models.
     """
     if schema is None:
         ...

-    index_file = MODELS_DIR / "index.ts"
+    index_file = models_dir / "index.ts"

And in __main__:

-    MODELS_DIR = models_dir
-    main(openapi_schema)
+    main(schema=openapi_schema, models_dir=models_dir)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@template/frontend/kiota_nullable_fixer.py` around lines 555 - 572, main()
currently references the global MODELS_DIR (used at "index_file = MODELS_DIR /
'index.ts'") which is only set inside the if __name__ == "__main__" block,
causing a NameError for external callers; change main signature to accept a
models_dir parameter (e.g., def main(schema: dict[str, Any] | None = None,
models_dir: Path | None = None)), use models_dir or fallback to
DEFAULT_MODELS_DIR when computing index_file, and remove or stop relying on any
global mutation of MODELS_DIR in the __main__ block (pass the desired models_dir
into main there instead).

Comment thread template/ruff-test.toml Outdated
@ejfine ejfine marked this pull request as ready for review May 4, 2026 15:38
@ejfine ejfine requested a review from zendern May 4, 2026 15:39
@ejfine ejfine enabled auto-merge (squash) May 4, 2026 17:39
@ejfine ejfine merged commit bd9c1de into main May 4, 2026
23 checks passed
@ejfine ejfine deleted the cop-scripts branch May 4, 2026 17:53
@coderabbitai coderabbitai Bot mentioned this pull request May 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants