feat(create-pr): align with gh-aw create-pull-request implementation#155
feat(create-pr): align with gh-aw create-pull-request implementation#155jamesadevine merged 30 commits intomainfrom
Conversation
Closes the gap between ado-aw and gh-aw's create-pull-request safe output: Security/Correctness: - Add file protection system blocking manifests (package.json, go.mod, Cargo.toml, etc.), CI configs (.github/, .pipelines/), and agent instruction files (.agents/, .claude/, .copilot/) by default - Add max-files limit per PR (default: 100, configurable) - Add 3-way merge fallback when patch application fails on conflicts - Replace predictable timestamp-based branch suffix with cryptographic random hex (rand crate) Feature Parity: - Add draft PR support (default: true, operator-enforced via isDraft) - Add fallback-as-work-item: record branch info on PR creation failure - Add excluded-files glob config to filter files from patches - Add if-no-changes config (warn/error/ignore) for empty patches - Add allowed-labels allowlist to restrict agent-provided labels - Add title-prefix config for operator branding - Add agent-provided labels parameter validated against allowed-labels Patch Format: - Migrate from raw git diff to git format-patch for proper commit metadata, rename detection, and binary file handling - Stage 2 applies patches via git am --3way with git apply fallback - Add collect_changes_from_diff_tree for committed patch changes - Add git to default bash command allowlist so agents can commit - Update tool description to encourage staging commits before PR creation Other: - Add provenance footer to PR body with timestamp and compiler version - Add ExecutionResult::failure_with_data for structured error responses Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
0b26f4d to
3caf5d7
Compare
🔍 Rust PR ReviewSummary: Mostly solid — the feature parity work is well-structured and the security additions are a real improvement — but there are two correctness bugs and an edge case worth addressing before merge. Findings🐛 Bugs / Logic Issues
|
- Fix if-no-changes 'warn' mode: add ExecutionResult::warning() with dedicated exit code 2. Pipeline step maps exit code 2 to ##vso[task.complete result=SucceededWithIssues;] for yellow badge. Previously 'warn' was identical to 'error' (both returned failure). - Propagate git reset HEAD~1 failure in generate_patch instead of silently swallowing it. A failed reset would leave a synthetic commit in the repo, breaking subsequent operations. - Fix path extraction for filenames with spaces: switch extract_paths_from_patch and filter_excluded_files_from_patch from parsing 'diff --git' headers (breaks on quoted paths) to using '--- a/' and '+++ b/' lines which handle spaces correctly. - Remove dead find_disallowed_files function (was cfg(test) only, never wired into production executor). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Needs changes — good security additions, but the patch generation approach has a fundamental design conflict between the implementation and the updated tool description, plus binary file support is incomplete despite being claimed. Findings🐛 Bugs / Logic Issues
|
… globs, validation - Fix format-patch to capture both committed and uncommitted changes by diffing against merge-base instead of only the last synthetic commit. Previously, if the agent committed changes per the tool description, the working tree was clean and format-patch produced an empty patch. - Add git identity flags (-c user.email/user.name) to synthetic commit to avoid "Please tell me who you are" errors in fresh environments. - Handle binary files in Stage 2 by falling back to base64 encoding when read_to_string fails (non-UTF-8 content). Uses ADO API contentType: "base64encoded" for binary files. - Fix renamed+modified files losing content changes. When diff-tree reports R<score> with score < 100, emit both a rename and an edit change entry with the new file content. - Fix fragile success_count - warning_count usize subtraction in execution summary. Count success as success && !warning directly instead of subtracting, preventing potential underflow. - Normalize excluded-files glob patterns by auto-prepending **/ to patterns without /, so *.lock matches subdir/Cargo.lock. - Validate if-no-changes and protected-files string values at execution time, rejecting typos with a clear error message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Solid implementation with good security thinking, but has one correctness bug in the merge-base fallback, a misleading feature name, and a couple of edge cases worth addressing. Findings🐛 Bugs / Logic Issues
|
- Convert if_no_changes and protected_files from String to proper Rust enums (IfNoChanges, ProtectedFiles) with serde rename_all for type-safe matching and deserialization-time validation - Rename fallback_as_work_item to record_branch_on_failure since no work item is actually created — it only records branch info - Fix find_merge_base single-commit repo fallback: use git rev-list --max-parents=0 HEAD instead of HEAD~1 which doesn't exist for repos with a single commit - Fix max_files double-counting renames: count diff --git blocks instead of deduplicated paths from --- and +++ lines - Add comment explaining empty-commit envelopes in filter_excluded_files_from_patch when format-patch is used - Add comment explaining ADO Pushes API sequential change processing for RM (rename + modify) status in collect_changes_from_worktree Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Needs changes — one security bug in the file protection system undermines the key safety feature. Findings🐛 Bugs / Logic Issues
The function lowercases the path basename but then compares against the original (mixed-case) manifest constant: let lower_basename = basename.to_lowercase();
for manifest in PROTECTED_MANIFEST_BASENAMES {
if lower_basename == *manifest { // ← comparing "cargo.toml" != "Cargo.toml"
The existing tests only cover all-lowercase manifests ( Fix — one of: // Option A: normalise both sides
if lower_basename == manifest.to_lowercase() {
// Option B: ensure PROTECTED_MANIFEST_BASENAMES is all-lowercase at the sourceThe exact-paths check uses
|
- Fix case comparison bug in find_protected_files: normalize PROTECTED_MANIFEST_BASENAMES to lowercase so case-insensitive comparison works for Cargo.toml, Pipfile, Gemfile, Directory.Build.* - Fix empty-envelope false negative in excluded-files: use count_patch_files() instead of trim().is_empty() to detect when all diffs were excluded from format-patch output (commit envelopes remain) - Make ExecutionResult.warning private with is_warning() accessor to enforce the invariant that warning implies success - Add comment explaining excluded-files vs protected-files ordering: exclusion runs first, so excluded files are dropped before the protection check (intentional — no modification to block) - Add test for mixed-case protected manifest detection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: needs changes — two interacting correctness bugs in the multi-commit format-patch flow that will silently produce incomplete or malformed patches when agents make more than one commit. Findings🐛 Bugs / Logic Issues[src/tools/create_pr.rs —
This is directly triggered by the PR description's own instruction to agents: "each logical change should be its own commit with a descriptive message." A 2-commit patch applied by The fix is to record the worktree HEAD SHA before // before git am
let base_sha = get_current_head(&worktree_path).await?;
// ... git am ...
// then:
git diff-tree --no-commit-id -r --name-status <base_sha> HEAD
```
Or more simply, just use `git diff <target_branch_initial_sha> HEAD` after applying.
---
**[src/tools/create_pr.rs — `filter_excluded_files_from_patch`] Commit envelope misattribution corrupts multi-commit patches**
A `git format-patch --stdout` stream has this structure:
```
From <sha1> ... ← commit 1 envelope
Subject: [PATCH 1/2] ...
---
diff --git a/file1.rs ... ← file1 diff block
...
← blank line (end of commit 1 diff)
From <sha2> ... ← commit 2 envelope
Subject: [PATCH 2/2] ...
---
diff --git a/file2.rs ... ← file2 diff blockThe current block-splitting logic only splits on Result:
This only manifests with multi-commit patches, which are now the default path and explicitly encouraged. A correct implementation needs to track and independently emit commit envelopes (lines between
|
- Fix multi-commit diff-tree: record base SHA before git am and diff base_sha..HEAD to capture all commits, not just the last one - Fix filter_excluded_files_from_patch for multi-commit format-patch: track diff blocks vs commit envelopes separately so excluding a file doesn't corrupt adjacent commit headers - Remove dead Windows backslash path check in find_protected_files — git diffs always use forward slashes - Make label allowlist case-insensitive with eq_ignore_ascii_case - Add test for multi-commit envelope preservation during exclusion Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Good overall — solid security additions and cleaner patch format, but there are a few real correctness issues worth fixing before merge. Findings🐛 Bugs / Logic Issues
|
- Broaden find_merge_base: detect actual default branch via git symbolic-ref refs/remotes/origin/HEAD before trying origin/main and origin/master, avoiding full-repo patches on non-main defaults - Fix case-insensitive label dedup: use eq_ignore_ascii_case in merge dedup to match the allowlist comparison, preventing duplicate labels - Strip empty commit envelopes from filtered patches: when all diffs within a commit are excluded, remove the envelope entirely to avoid git am errors on empty patch emails - Change draft default to false to match ADO default behavior and avoid breaking existing pipelines that don't set draft explicitly - Log synthetic commit SHA on reset failure for operator diagnostics - Rename executor step displayName to 'Execute safe outputs (Stage 2)' for clarity in pipeline logs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Solid implementation with good security foundations, but has a correctness bug on the Findings🐛 Bugs / Logic Issues
|
- Default draft to true (safety posture: agent PRs are drafts unless operator opts out) - Fix synthetic commit leak: reset before propagating format-patch errors - Add Dockerfile/docker-compose to protected manifest basenames - Remove redundant path validation for rename old_path - Truncate raw ADO API response body in fallback description (max 500 chars) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: solid implementation with one correctness bug in the synthetic-commit path and a few worth-flagging gaps. Findings🐛 Bugs / Logic Issues
🔒 Security Concerns
|
- Fix synthetic commit index mutation: add git reset HEAD after mixed reset to restore previously-untracked files to untracked state - Fix deleted files exclusion: fallback to --- a/<path> when +++ is /dev/null so excluded-files patterns match deletions - Sanitize ADO API response body via sanitize_text() to neutralize ##vso[ commands in fallback description - Rename record-branch-on-failure to fallback-as-work-item to match PR description (old name kept as serde alias for compat) - Add warning about potentially large patches at root-commit fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Solid implementation with good security thinking, but has a few real bugs and one breaking change that need addressing before merge. Findings🐛 Bugs / Logic Issues
🔒 Security Concerns
|
- Sanitize agent_labels in sanitize_fields() for defense-in-depth - Add warn! log when synthetic commit reset fails before returning error - Add conflict marker check after git apply --3way fallback to prevent pushing unresolved conflicts to ADO - Sanitize self.description in fallback failure data construction - Rename fallback-as-work-item to fallback-record-branch (clearer name) - Root-commit fallback now only applies to single-commit repos; fails with clear error for multi-commit repos with no remote tracking branch Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Solid implementation overall, but has one breaking default change and a handful of correctness/security issues worth addressing before merge. Findings🐛 Bugs / Logic Issues
🔒 Security Concerns
|
- Check git status exit code before inspecting stdout for uncommitted changes — prevents silent wrong behavior on broken repos - Remove redundant git reset HEAD after mixed reset (--mixed already restores untracked files to untracked state) - Remove dead condition in normalize_glob_pattern (starts_with(**/)) is always true when contains(/) is true) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Solid feature set with good security design, but there's a correctness gap in binary-file path extraction that affects both the protected-files check and excluded-files filter. Findings🐛 Bugs / Logic Issues
🔒 Security Concerns
|
🔍 Rust PR ReviewSummary: Strong improvement over previous iterations — the format-patch migration, binary file handling, and security additions are well-implemented — but there is one correctness bug in the merge-base fallback, a security concern in the error-recovery path, and a misleading comment that will confuse future maintainers. Findings🐛 Bugs / Logic Issues
🔒 Security Concerns
|
Bugs: - Fix validate_patch_paths rename/copy parsing to use splitn(3) instead of split_whitespace().last(), preventing security bypass for paths with spaces. Add test for this scenario. - Add branch collision handling: query ADO refs API before push, retry with new random suffix if branch already exists. - Fix misleading comment in mcp.rs that referenced git stash when code uses git reset --mixed. Features: - Record base_commit SHA at patch generation time in NDJSON. Stage 2 uses recorded SHA when available, falling back to ADO API resolution for backward compatibility. - Add protected manifest basenames for Bun, Deno, Elixir, Haskell, Python (uv), and additional .NET packages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Solid implementation with good security posture — has two real bugs and a documentation mismatch worth fixing before merge. Findings🐛 Bugs / Logic Issues
|
🔍 Rust PR ReviewSummary: Well-structured security improvements and feature parity — a few issues need attention before merge. Findings🐛 Bugs / Logic Issues
The new if config.draft && config.auto_complete {
warn!("auto-complete cannot be set on a draft PR; set draft: false to enable auto-complete");
}
When 🔒 Security Concerns
The let re = regex::Regex::new(r"^[0-9a-f]{40}$").unwrap();
if !re.is_match(recorded) { bail!("..."); }Or since the project already uses if !recorded.chars().all(|c| c.is_ascii_hexdigit()) || recorded.len() != 40 {
bail!("Invalid base_commit SHA from Stage 1 NDJSON");
}
GitHub resolves CODEOWNERS from three locations:
|
git exposes subcommands like push, remote add, fetch, credential, and bundle which could be used to exfiltrate data or interact with unintended remotes inside the AWF container. gh-aw does not include git in its default bash allowlist — agents that need git should explicitly opt in via tools.bash: ["git", ...] in front matter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…avior The tool description told agents to 'git add and git commit' before calling create-pull-request, but git is not in the default bash allowlist. The synthetic commit in generate_patch() handles uncommitted changes automatically, so agents don't need git access. Updated the description to reflect this. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…index cleanup, SHA validation, CODEOWNERS - Warn when draft: true and auto-complete: true are both set (mutually exclusive in ADO — auto-complete silently fails on draft PRs) - Reset index on git add -A failure to leave working tree clean - Validate base_commit SHA format (40 hex chars) before trusting Stage 1 NDJSON data - Protect docs/CODEOWNERS in addition to root CODEOWNERS Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Good overall direction — the security additions (protected files, patch size cap, cryptographic branch suffix) are well-implemented and the Findings🐛 Bugs / Logic Issues
🔒 Security Concerns
|
…de flags Replace the 125-line filter_excluded_files_from_patch() function (which parsed multi-commit format-patch content, tracked envelope boundaries, and did glob matching) with --exclude flags on git am / git apply. This matches gh-aw's approach of filtering at the git level rather than post-processing patch content, eliminating: - Complex patch segment parser (envelopes, diff blocks, path extraction) - Multi-commit envelope stripping logic - glob-match crate dependency - normalize_glob_pattern helper - 6 filter-related tests Net: -266 lines, simpler and more robust. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Solid feature work with good security intent, but there are two correctness bugs worth fixing before merge. Findings🐛 Bugs / Logic Issues
|
…sion bypass - Place --exclude flags before the patch path in git am / git apply (POSIX: options after positional args are treated as file arguments) - Filter excluded-files patterns from patch paths before the protected-files check, so excluded files don't trigger protection errors (matching the documented behavior) - Add glob_match_simple() helper for excluded-files path matching Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Substantial improvement — well-structured, good test coverage, solid security hardening. A few real issues worth fixing before merge. Findings🐛 Bugs / Logic Issues
|
…ebinding The **/ prefix strip was followed by exact match against the full path, so **/Cargo.lock would not match subdir/Cargo.lock. Now tries matching against every directory suffix of the path. Also removed dead let patch_content = patch_content; rebinding. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Good overall direction — the security additions (file protection, crypto branch IDs, patch format upgrade) are solid, but there are 3 correctness issues worth addressing before merge. Findings🐛 Bugs / Logic Issues1.
fn count_patch_files(patch_content: &str) -> usize {
patch_content
.lines()
.filter_map(|l| l.strip_prefix("diff --git a/"))
.collect::<std::collections::HashSet<_>>()
.len()
}2. The field has + #[serde(default, skip_serializing_if = "std::ops::Not::not")]
warning: bool,3. The function handles
|
…hallow clones - count_patch_files: deduplicate by b/ path to avoid over-counting in multi-commit format-patch output - ExecutionResult::warning: add #[serde(default)] for forward compat - collect_changes_from_diff_tree: handle C<score> (copy) status codes analogously to renames, instead of falling through to edit - Validate effective_title length (max 400 chars) after prepending title-prefix, before ADO push - Improve find_merge_base error message to mention shallow clones as a possible cause - extract_paths_from_patch: skip lines before first diff --git header to avoid false positives from commit messages quoting patches - Branch collision: retry up to 3 times instead of one-shot - Include commit SHA in git reset IO error for operator recovery Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Good overall direction with some important concerns — the Findings🐛 Bugs / Logic Issues
|
…etry
- Conflict marker regex: require trailing \s on <<<<<<< and >>>>>>>
to avoid false positives from rst/markdown heading underlines
- Title length: use chars().count() instead of len() for Unicode-safe
character counting (CJK chars are multi-byte)
- count_patch_files: reuse extract_paths_from_patch for robust dedup
instead of fragile rsplit_once(" b/") header parsing
- Remove dead ends_with(pat) check (literal string match never matches
glob patterns containing *)
- TOCTOU branch collision: retry push on ADO 409/400 "already exists"
in addition to pre-check loop
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Substantial improvement to Findings🐛 Bugs / Logic Issues
|
…ctly git am has no --exclude flag — passing it causes git am to always fail, falling through to git apply. Fix: when excluded-files is configured, skip git am and use git apply --3way --exclude directly. When no exclusions, use git am as before with git apply as fallback. Also adds 7 unit tests for glob_match_simple and wildcard_match which gate the excluded-files security filter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Large, well-structured improvement — mostly looks good with two correctness issues worth addressing before merge. Findings🐛 Bugs / Logic Issues
|
- Use strip_prefix for ---/+++ lines instead of split_whitespace to correctly handle paths with spaces - Add in_diff guard to skip commit message bodies in format-patch output, preventing false-positive rejections from commit messages that reference .git paths or contain patch-like content - Both functions now use consistent parsing approach Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Mostly solid — good security thinking throughout, but there's one correctness bug in the glob implementation and a misleading error message in the merge-base fallback worth fixing before merge. Findings🐛 Bugs / Logic Issues
|
…essages - Replace hand-rolled wildcard_match with glob-match crate for correct glob semantics (* does not cross directory separators) - Add test asserting src/*.rs does not match src/nested/file.rs - Distinguish 'no tracking branch' from 'diverged branches (no common ancestor)' in find_merge_base error messages Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Solid implementation with good security posture and test coverage — a few issues worth following up on. Findings🐛 Bugs / Logic Issues
|
When both draft: true and auto-complete: true are configured, skip the autoCompleteSetBy API call since ADO silently ignores auto-complete on draft PRs. The existing warning is preserved so operators are aware of the conflict. This prevents a pointless API call and avoids silent behavior loss when draft: true became the default in PR #155. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When both draft: true and auto-complete: true are configured, skip the autoCompleteSetBy API call since ADO silently ignores auto-complete on draft PRs. The existing warning is preserved so operators are aware of the conflict. This prevents a pointless API call and avoids silent behavior loss when draft: true became the default in PR #155. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Closes the gap between ado-aw and gh-aw's
create-pull-requestsafe output implementation. Identified via a systematic comparison of both codebases.Security/Correctness
package.json,go.mod,Cargo.toml,Dockerfile, etc.), CI configs (.github/,.pipelines/), and agent instruction files (.agents/,.claude/,.copilot/) by default. Override withprotected-files: allowed.max-files)git am --3waywithgit apply --3wayfallback (with conflict marker detection) instead of hard failurerandcrate instead of predictable timestamp##vso[commands neutralized in error messages viasanitize_text()Feature Parity
draft: true, operator-enforced via ADOisDraftfieldfallback-record-branch: true)excluded-files:glob patterns strip files from patches before application (handles deletions correctly)if-no-changes: warn|error|ignorecontrols behavior on empty patcheslabelsin tool params, validated againstallowed-labelsallowlist. Operatorlabelsalways applied unconditionally.title-prefix: "[Bot] "prepended to all PR titlesPatch Format Upgrade
git difftogit format-patchfor proper commit metadata, rename detection, and binary file handlinggit am --3waywithgit apply --3wayfallbackgitto default bash command allowlist so agents can commitOther
ExecutionResult::failure_with_dataandExecutionResult::warningfor structured error/warning responsesSucceededWithIssuespipeline integrationNew Config Fields
Testing
All 757 tests pass.