Skip to content

fix: enforce minLength on update_release body to block placeholder submissions#39713

Merged
pelikhan merged 1 commit into
mainfrom
copilot/investigate-test-body-update
Jun 17, 2026
Merged

fix: enforce minLength on update_release body to block placeholder submissions#39713
pelikhan merged 1 commit into
mainfrom
copilot/investigate-test-body-update

Conversation

Copilot AI commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

In run 27655450946, the agent emitted a placeholder {"body":"test",...} as its first update_release output, followed by the real release notes. With max:1, the placeholder was applied and the real content rejected — prepending the word "test" to the v0.80.1 release.

The gap: update_release.body had no MinLength, unlike create_issue (20) and create_discussion (64), so any non-empty string passed validation.

Changes

  • safe_outputs_validation_config.go — Add MinReleaseBodyLength = 20; apply MinLength: MinReleaseBodyLength to update_release.body
  • safe_outputs_tools.json (both pkg/ and actions/setup/js/) — Add minLength: 20 to update_release.body; update description to say "Must be the final intended content — not a placeholder or test value"
  • safe_output_type_validator.test.cjs — Add update_release to sample validation config; add 3 minLength tests (reject "test", reject whitespace-only, accept real content)

With this fix, {"body":"test",...} produces:

Line 1: update_release 'body' is too short (minimum 20 characters)

…ssions

Investigation of run 27655450946 found that the agent emitted a
placeholder {"body":"test",...} update_release output first, then the
real release notes second. Since max:1, the "test" body was applied
and the real content rejected.

Root cause: update_release had no MinLength on the body field, unlike
create_issue (MinLength:20) and create_discussion (MinLength:64).

Fix:
- Add MinReleaseBodyLength = 20 constant to safe_outputs_validation_config.go
- Apply MinLength: MinReleaseBodyLength to update_release body field
- Add minLength: 20 and improved description to update_release body in
  both safe_outputs_tools.json files
- Add 3 tests covering the new minLength enforcement

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title fix: add MinLength to update_release body to reject placeholder submissions fix: enforce minLength on update_release body to block placeholder submissions Jun 17, 2026
Copilot AI requested a review from pelikhan June 17, 2026 01:06
@pelikhan pelikhan marked this pull request as ready for review June 17, 2026 01:14
Copilot AI review requested due to automatic review settings June 17, 2026 01:15
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ failed during design decision gate check.

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

Copilot AI left a comment

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.

Pull request overview

This PR tightens safe-output validation for the update_release tool to prevent short placeholder release bodies (e.g., "test") from being accepted when workflows only permit a single update_release emission.

Changes:

  • Add MinReleaseBodyLength = 20 and enforce it via MinLength on update_release.body in the safe-outputs validation config.
  • Update both copies of safe_outputs_tools.json to declare minLength: 20 for update_release.body and clarify the “no placeholders” requirement in the description.
  • Extend the JS validator test suite to include update_release and add minLength-focused test cases.
Show a summary per file
File Description
pkg/workflow/safe_outputs_validation_config.go Enforces a minimum length for update_release.body via MinReleaseBodyLength.
pkg/workflow/js/safe_outputs_tools.json Updates tool schema/docs to reflect update_release.body minLength: 20.
actions/setup/js/safe_outputs_tools.json Mirrors the tool schema/docs update for the runtime copy.
actions/setup/js/safe_output_type_validator.test.cjs Adds update_release to the sample validation config and adds minLength tests.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 4/4 changed files
  • Comments generated: 2

Comment on lines +891 to +895
it("should reject update_release body that is only whitespace below minLength", async () => {
const { validateItem } = await import("./safe_output_type_validator.cjs");

const result = validateItem({ type: "update_release", tag: "v1.0.0", operation: "prepend", body: " test " }, "update_release", 1);

Comment on lines 296 to 302
"update_release": {
DefaultMax: 1,
Fields: map[string]FieldValidation{
"tag": {Type: "string", Sanitize: true, MaxLength: 256},
"operation": {Required: true, Type: "string", Enum: []string{"replace", "append", "prepend"}},
"body": {Required: true, Type: "string", Sanitize: true, MaxLength: MaxBodyLength},
"body": {Required: true, Type: "string", Sanitize: true, MaxLength: MaxBodyLength, MinLength: MinReleaseBodyLength},
},
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 90/100 — Excellent

Analyzed 3 test(s) in safe_output_type_validator.test.cjs: 3 design, 0 implementation, 0 guideline violations.

📊 Metrics & Test Classification (3 tests analyzed)
Metric Value
New/modified tests analyzed 3
✅ Design tests (behavioral contracts) 3 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 2 (67%)
Duplicate test clusters 0
Test inflation detected No
🚨 Coding-guideline violations 0 (Go mock libraries / missing build tags / no assertion messages)
Test File Classification Issues Detected
should reject update_release body shorter than minLength actions/setup/js/safe_output_type_validator.test.cjs:873 ✅ Design
should accept update_release body that meets minLength actions/setup/js/safe_output_type_validator.test.cjs:883 ✅ Design Minimal assertions: only checks isValid === true; could also verify error is absent/falsy
should reject update_release body that is only whitespace below minLength actions/setup/js/safe_output_type_validator.test.cjs:891 ✅ Design

Go: 0 (*_test.go); JavaScript: 3 (*.test.cjs). No other languages detected.

Verdict

Check passed. 0% implementation tests (threshold: 30%). All 3 new tests verify observable behavioral contracts: the new minLength: 20 constraint on update_release.body is covered by a rejection case, an acceptance case, and a whitespace-trimming edge case.

🧪 Test quality analysis by Test Quality Sentinel ·

@github-actions github-actions Bot left a comment

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.

✅ Test Quality Sentinel: 90/100. Test quality is acceptable — 0% of new tests are implementation tests (threshold: 30%).

@github-actions github-actions Bot left a comment

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.

Skills-Based Review 🧠

Applied /tdd and /zoom-out — the fix is correct and addresses the production incident cleanly; comments are improvement suggestions only.

📋 Key Themes & Highlights

Key Themes

  • Missing Go-level regression test: TestCreateIssueBodyMinLength and TestCreateDiscussionBodyMinLength both exist; TestUpdateReleaseBodyMinLength does not. The fix correctly wires the constant, but the Go-level guard that would catch future accidental unwiring is absent.
  • Whitespace test name/content mismatch: The third JS test is labelled "only whitespace" but exercises a whitespace-padded string. A purely-whitespace body is not explicitly covered.
  • Formatting noise in pkg/workflow/js/safe_outputs_tools.json: +694/−211 lines for a one-field change. The two JSON copies are structurally identical (verified) but formatted differently, making future diffs hard to review.

Positive Highlights

  • ✅ Root cause correctly identified and fixed with a targeted constant + field wiring — no over-engineering
  • ✅ Parity with the existing create_issue / create_discussion pattern: same threshold, same comment style
  • ✅ Three JS regression tests cover the most important cases (reject placeholder, accept real content)
  • ✅ Updated description field in the JSON schema clearly signals the "final intended content" expectation to the agent
  • ✅ Both actions/setup/js/ and pkg/workflow/js/ schema copies correctly updated

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

expect(result.isValid).toBe(true);
});

it("should reject update_release body that is only whitespace below minLength", async () => {

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.

[/tdd] Misleading test name: the body value " test " is whitespace-padded text, not purely whitespace. An actual pure-whitespace string (e.g., " ") is a distinct edge case that is currently untested.

💡 Suggested additions

Rename the existing test to reflect what it actually covers:

it("should reject update_release body whose trimmed length is below minLength (whitespace-padded)", async () => {

And add a dedicated purely-whitespace test:

it("should reject update_release body that is purely whitespace", async () => {
  const { validateItem } = await import("./safe_output_type_validator.cjs");
  const result = validateItem(
    { type: "update_release", tag: "v1.0.0", operation: "replace", body: "                    " },
    "update_release", 1
  );
  expect(result.isValid).toBe(false);
  expect(result.error).toContain("too short");
});

This closes the gap between what the test name promises and what it actually verifies.

expect(result.error).toContain("too short");
});

it("should reject update_release body shorter than minLength (e.g. 'test')", async () => {

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.

[/tdd] All three new tests use operation: "prepend". Since minLength enforces a floor regardless of operation, adding one test with operation: "replace" (the most common case) would confirm the constraint is operation-agnostic and guard against any future per-operation branching in the validator.

💡 Suggested addition
it("should reject update_release body shorter than minLength for replace operation", async () => {
  const { validateItem } = await import("./safe_output_type_validator.cjs");
  const result = validateItem(
    { type: "update_release", tag: "v1.0.0", operation: "replace", body: "test" },
    "update_release", 1
  );
  expect(result.isValid).toBe(false);
  expect(result.error).toContain("too short");
});

"tag": {Type: "string", Sanitize: true, MaxLength: 256},
"operation": {Required: true, Type: "string", Enum: []string{"replace", "append", "prepend"}},
"body": {Required: true, Type: "string", Sanitize: true, MaxLength: MaxBodyLength},
"body": {Required: true, Type: "string", Sanitize: true, MaxLength: MaxBodyLength, MinLength: MinReleaseBodyLength},

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.

[/tdd] The codebase has TestCreateIssueBodyMinLength and TestCreateDiscussionBodyMinLength in safe_output_validation_config_test.go (lines 300–330), but there is no corresponding TestUpdateReleaseBodyMinLength. Given this is a production incident fix, adding a Go-level regression test follows the established pattern and ensures the constant wiring is validated independently of the JS tests.

💡 Suggested test (add to `safe_output_validation_config_test.go`)
func TestUpdateReleaseBodyMinLength(t *testing.T) {
    config, ok := ValidationConfig["update_release"]
    if !ok {
        t.Fatal("update_release not found in ValidationConfig")
    }

    bodyField, ok := config.Fields["body"]
    if !ok {
        t.Fatal("body field not found in update_release validation config")
    }

    if bodyField.MinLength != MinReleaseBodyLength {
        t.Errorf("update_release body MinLength = %d, want %d", bodyField.MinLength, MinReleaseBodyLength)
    }
}

This mirrors the exact pattern used for create_issue and create_discussion and would have caught the original production gap earlier.

"type": "string",
"description": "Release body content in Markdown. For 'replace', this becomes the entire release body. For 'append'/'prepend', this is added with a separator."
"description": "Release body content in Markdown. Must be the final intended content — not a placeholder or test value. For 'replace', this becomes the entire release body. For 'append'/'prepend', this is added with a separator.",
"minLength": 20

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.

[/zoom-out] The actual logic change here is a single "minLength": 20 field, but the diff shows +694/−211 lines due to JSON formatting differences between this file and actions/setup/js/safe_outputs_tools.json. Both files are structurally identical (confirmed), but the per-element array expansion makes the change nearly unreviable.

📋 Context and suggestion

If these two files are always kept in sync (same content, different formatter), consider:

  1. Canonicalising on one formatter — so future diffs are minimal and reviewable.
  2. Or generating pkg/workflow/js/ from actions/setup/js/ — a single source of truth would eliminate the risk of accidental drift and remove this class of noisy diffs.

As a minimum, the PR description could note that the bulk of this diff is formatting-only to help reviewers focus on line 1415.

@pelikhan pelikhan merged commit b9789c9 into main Jun 17, 2026
87 of 107 checks passed
@pelikhan pelikhan deleted the copilot/investigate-test-body-update branch June 17, 2026 01:27

@github-actions github-actions Bot left a comment

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.

🔎 Code quality review by PR Code Quality Reviewer

it("should reject update_release body that is only whitespace below minLength", async () => {
const { validateItem } = await import("./safe_output_type_validator.cjs");

const result = validateItem({ type: "update_release", tag: "v1.0.0", operation: "prepend", body: " test " }, "update_release", 1);

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.

Whitespace test uses wrong input — doesn't exercise the trim path at all.

The test name promises coverage of "only whitespace below minLength", but " test " (10 chars raw) fails because 10 < 20 — the trim call is irrelevant here. The test is a near-duplicate of the first test and doesn't add useful coverage.

💡 What to test instead

The dangerous case is content whose raw length meets the minLength threshold but whose trimmed length falls below it — e.g. 20+ spaces. The existing create_issue suite covers this correctly:

// create_issue's whitespace test — 25 spaces, raw length ≥ 20, trimmed → "" → rejected
validateItem({ ..., body: "                         " }, ...)

The update_release version should mirror that:

it("should reject update_release body that is only whitespace even when raw length meets minLength", async () => {
  const { validateItem } = await import("./safe_output_type_validator.cjs");
  // 20 spaces: passes JSON Schema minLength:20, but trim() → "" → runtime should reject it
  const result = validateItem(
    { type: "update_release", tag: "v1.0.0", operation: "prepend", body: "                    " },
    "update_release", 1
  );
  expect(result.isValid).toBe(false);
  expect(result.error).toContain("too short");
});

Without this, removing the .trim() call on line 401 of the validator would silently pass all existing update_release tests while breaking the intended protection.

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.

3 participants