feat(safe-outputs): add unified upload-build-artifact safe output#380
feat(safe-outputs): add unified upload-build-artifact safe output#380jamesadevine merged 11 commits intomainfrom
Conversation
Combines the concepts from #363 (upload-artifact, current build) and #373 (upload-build-artifact, arbitrary build) into a single tool that handles both use cases via the ADO build attachments REST API. Key design: - build_id is Optional — omit to target the current pipeline run (resolved from BUILD_BUILDID in Stage 3) - REST API (PUT .../builds/{buildId}/attachments/{type}/{name}) for all cases, replacing the ##vso[artifact.upload] logging command approach - allowed-build-ids check is skipped when targeting the current build (implicitly trusted) - Full Stage 1 → Stage 3 file staging contract: validate, canonicalize, bounds-check, stage file with extension preservation, re-canonicalize and re-verify in Stage 3 Supersedes #363 and #373. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good overall — solid defense-in-depth, good test coverage. Two actionable issues found. Findings🐛 Bugs / Logic Issues
✅ What Looks Good
|
… allowlist check - Move std::fs::read after the dry_run early return to avoid reading up to 50 MB into memory only to discard it in dry-run mode. - Switch extension allowlist from suffix matching on the full path to Path::extension() comparison, preventing false matches like 'catalog' matching allowed-extensions: ['log']. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good overall — the staging contract is well-implemented and the test coverage is solid. One documentation bug and one minor observation. Findings🐛 Bugs / Logic Issues
|
The heading was accidentally dropped when the upload-build-artifact section was inserted, leaving the cache-memory paragraph as a stray note under the wrong section. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good overall — clean implementation following established patterns. A few items worth addressing. Findings🐛 Bugs / Logic Issues
|
…mments - Switch std::fs::read to tokio::fs::read().await to avoid blocking the tokio runtime for files up to 50 MB. - Add a warn!() when the live staged-file size differs from the size recorded at Stage 1, catching potential file tampering between stages. - Add comments on is_valid_version() call sites noting intentional reuse for artifact-name and attachment-type charset validation. - Add comment on staged filename explaining max-length reasoning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A size mismatch between Stage 1 and Stage 3 indicates the staged file was modified between stages — fail hard rather than warning and uploading potentially tampered content. Adds a test for the integrity check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Solid implementation with good security posture — two gaps worth addressing before merge. Findings🐛 Bugs / Logic Issues
|
- Add starts_with('.') guard to attachment-type validation, matching
the artifact_name check for consistency.
- Add defense-in-depth file size cap at Stage 1 (MCP) using the
default 50 MB limit to prevent staging-disk exhaustion before
Stage 3 enforces the operator's configured limit.
- Add three missing executor tests: extension allowlist rejection,
artifact-name allowlist rejection, and name-prefix application.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good — solid design with defense-in-depth, a few minor suggestions worth considering. Findings
|
Add SHA-256 hash recording at Stage 1 and verification at Stage 3 for both file-staging safe outputs: - upload-build-artifact: staged_sha256 field recorded after copying the file, verified before uploading to ADO. Catches same-size file swaps that the size-only check would miss. - create-pull-request: patch_sha256 field recorded after writing the patch, verified before applying. Field is Optional for backward compatibility with existing NDJSON records. Also adds sha256_hex() helper in upload_build_artifact module, reused by both tools and the MCP handlers. A separate issue (#381) tracks adding the full staging pattern (including SHA-256) to upload-workitem-attachment, which currently reads directly from source_directory without staging. Also fixes: name-prefix length cap (50 chars), attachment-type leading-dot guard, comment typo in MCP handler. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pipelines are versioned with their YAML — no need for backward compatibility with old NDJSON records. Making the hash optional created a security hole where a missing hash would skip verification entirely. Now patch_sha256 is a required String and verification is unconditional. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good overall — solid defense-in-depth design, comprehensive tests, and well-structured execution flow. Three items worth addressing before merge. Findings🐛 Bugs / Logic Issues
|
- Move sha256_hex() from upload_build_artifact.rs to a dedicated src/hash.rs module, removing the coupling between create_pr, mcp.rs and the upload_build_artifact module. - Fix potential panic in name-prefix error path: &prefix[..20] can panic on multi-byte UTF-8 boundaries. Use prefix.chars().take(20).collect() instead. - Revert upload_build_artifact module visibility back to private. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good overall — solid security model with good layered defenses; one gap in test coverage for the SHA-256 integrity check and a minor code-smell in function reuse. Findings🐛 Bugs / Logic Issues
|
…t_name - Add non-dry-run SHA-256 mismatch tests for both upload-build-artifact and create-pull-request (the mismatch path fires before any HTTP call, so no credentials are needed). - Fix TOCTOU in MCP handler: hash the source bytes before writing to the staging directory, instead of re-reading the staged copy. - Extract is_valid_artifact_name() in validate.rs to decouple artifact name validation from version string validation, preventing silent breakage if is_valid_version is ever tightened. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good overall — well-structured, strong security posture, thorough test coverage. Three items worth discussing before merge. Findings
|
Replace the floating STAGE1_MAX_FILE_SIZE copy with a direct import of DEFAULT_MAX_FILE_SIZE from upload_build_artifact, ensuring the Stage 1 cap stays in sync if the default is ever changed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Solid implementation — the two-stage file-integrity model (size + SHA-256) is well-designed and the validation layering is correct. Two minor findings below. Findings
|
Summary
Unifies PRs #363 (
upload-artifact) and #373 (upload-build-artifact) into a singleupload-build-artifactsafe output that handles both use cases — attaching a file to the current pipeline run or to an arbitrary build.Design
build_idisOption<i64>— omit to target the current pipeline run (resolved fromBUILD_BUILDIDin Stage 3). When provided, the file is attached to that specific build.PUT .../builds/{buildId}/attachments/{type}/{name}uniformly, replacing PR feat(safe-outputs): add upload-artifact safe output #363's##vso[artifact.upload]logging command approach.allowed-build-idsbypassed for current build — when the agent omitsbuild_id, the allow-list check is skipped since the current build is implicitly trusted.add-build-tagprecedent for resolvingctx.build_idfromBUILD_BUILDID.Stage 1 → Stage 3 file staging contract
Preserves the full staging contract refined in #363:
output_directory→ record file size → write NDJSONFront-matter shape
Files changed
src/safeoutputs/upload_build_artifact.rssrc/safeoutputs/mod.rssrc/execute.rssrc/mcp.rsdocs/safe-outputs.mdTest plan
cargo build✅cargo test✅ (1088 tests, 0 failures — 27 new tests covering: explicit/omitted build_id, current build resolution, missing BUILD_BUILDID error, allowed-build-ids bypass for current build, oversize file rejection, config defaults, YAML round-trip, serialization)cargo clippy --all-targets --all-features✅Supersedes #363 and #373.