Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions .github/workflows/daily-compiler-quality.lock.yml

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions .github/workflows/daily-compiler-quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ tools:
- "mv /tmp/gh-aw/cache-memory/"
- "echo"
- "bc"
safe-outputs:
create-discussion:
category: "audits"
title-prefix: "[daily-compiler-quality] "
expires: 1d
close-older-discussions: true
fallback-to-issue: true
max: 1
min-body-length: 200
timeout-minutes: 30
strict: true
features:
Expand Down Expand Up @@ -314,6 +323,13 @@ Compare current analysis with previous analyses:

Generate a comprehensive discussion report with findings.

### Output Contract (Required)

1. Emit **exactly one** `create_discussion` safe-output item.
2. Do **not** emit placeholder or draft bodies (for example: `test`, `.`, `todo`, or similar short placeholders).
3. Only emit `create_discussion` after the final report body is complete and fully rendered.
4. The workflow enforces a **minimum 200-character body length**, so very short outputs (placeholder or otherwise) will fail safe-outputs.

### Discussion Title

```
Expand Down
16 changes: 16 additions & 0 deletions actions/setup/js/create_discussion.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -295,13 +295,17 @@ async function main(config = {}) {
const configCategory = config.category || "";
const maxCount = config.max || 10;
const expiresHours = config.expires ? parseInt(String(config.expires), 10) : 0;
const minBodyLength = config.min_body_length ? parseInt(String(config.min_body_length), 10) : 0;
const fallbackToIssue = config.fallback_to_issue !== false; // Default to true
const closeOlderDiscussionsEnabled = parseBoolTemplatable(config.close_older_discussions, false);
const rawCloseOlderKey = config.close_older_key ? String(config.close_older_key) : "";
const closeOlderKey = rawCloseOlderKey ? normalizeCloseOlderKey(rawCloseOlderKey) : "";
if (rawCloseOlderKey && !closeOlderKey) {
throw new Error(`${ERR_VALIDATION}: close-older-key "${rawCloseOlderKey}" is invalid: it must contain at least one alphanumeric character after normalization`);
}
if (isNaN(minBodyLength) || minBodyLength < 0) {
throw new Error(`${ERR_VALIDATION}: min_body_length must be a non-negative integer (got: ${config.min_body_length})`);
}
const includeFooter = parseBoolTemplatable(config.footer, true);

// Create an authenticated GitHub client. Uses config["github-token"] when set
Expand All @@ -321,6 +325,9 @@ async function main(config = {}) {
.filter(l => l.length > 0);

core.info(`Create discussion configuration: max=${maxCount}`);
if (minBodyLength > 0) {
core.info(`Minimum discussion body length guard enabled: ${minBodyLength}`);
}
core.info(`Default target repo: ${defaultTargetRepo}`);
if (allowedRepos.size > 0) {
core.info(`Allowed repos: ${Array.from(allowedRepos).join(", ")}`);
Expand Down Expand Up @@ -484,9 +491,18 @@ async function main(config = {}) {
let title = item.title ? item.title.trim() : "";
let processedBody = replaceTemporaryIdReferences(item.body || "", temporaryIdMap, qualifiedItemRepo);
processedBody = removeDuplicateTitleFromDescription(title, processedBody);
const preSanitizeBodyLength = processedBody.trim().length;

// Sanitize body content to neutralize @mentions, URLs, and other security risks
processedBody = sanitizeContent(processedBody);
if (minBodyLength > 0 && preSanitizeBodyLength < minBodyLength) {
const error = `Discussion body length ${preSanitizeBodyLength} is below configured minimum ${minBodyLength}`;
core.error(error);
return {
success: false,
error,
};
}

if (!title) {
title = item.body || "Discussion";
Expand Down
17 changes: 17 additions & 0 deletions actions/setup/js/create_discussion_sanitization.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,21 @@ describe("create_discussion body sanitization", () => {
// System-generated footer marker must still be present
expect(body).toContain("gh-aw-workflow-id");
});

it("should fail when body is below configured minimum length", async () => {
const handler = await createDiscussionMain({ max: 5, category: "general", min_body_length: 200 });
const result = await handler(
{
title: "Too short",
body: "test",
},
{}
);

expect(result.success).toBe(false);
expect(result.error).toContain("below configured minimum 200");

const createMutationCall = mockGithub.graphql.mock.calls.find(call => call[0].includes("createDiscussion"));
expect(createMutationCall).toBeUndefined();
});
});
6 changes: 6 additions & 0 deletions docs/src/content/docs/reference/frontmatter-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -3631,6 +3631,12 @@ safe-outputs:
# (optional)
category: null

# Minimum required length of the discussion body content (before
# footer/metadata) in characters. If a create_discussion message body is
# shorter than this value, the safe-outputs job fails.
# (optional)
min-body-length: 200

# Optional list of labels to attach to created discussions. Also used for matching
# when close-older-discussions is enabled - discussions must have ALL specified
# labels (AND logic).
Expand Down
4 changes: 4 additions & 0 deletions docs/src/content/docs/reference/safe-outputs-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -1612,6 +1612,7 @@ upload-asset:
create-discussion:
category: "General" # Discussion category (name/slug/ID)
title-prefix: "[Report] " # Prepend to titles
min-body-length: 200 # Optional minimum report body length
labels: [report, automated] # Auto-apply labels
allowed-labels: [...] # Agent label restrictions
```
Expand Down Expand Up @@ -2654,11 +2655,14 @@ This section provides complete definitions for all remaining safe output types.
3. **Footer Injection**: Appends attribution footer to the discussion body when configured.
4. **Cross-Repository**: When `target-repo` is configured, creates in that repository (must be in `allowed-repos`).
5. **Temporary ID Support**: Supports `temporary_id` field for referencing before creation.
6. **Body Length Guard**: When `min-body-length` is configured, discussion creation is rejected if the body is shorter than the configured minimum.

**Configuration Parameters**:

- `max`: Operation limit (default: 1)
- `category`: Default discussion category
- `title-prefix`: Prepend to titles
- `min-body-length`: Minimum required body length (characters, before footer/metadata)
- `target-repo`: Cross-repository target
- `allowed-repos`: Cross-repo allowlist
- `footer`: Footer override
Expand Down
3 changes: 3 additions & 0 deletions docs/src/content/docs/reference/safe-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -1013,6 +1013,7 @@ safe-outputs:
create-discussion:
title-prefix: "[ai] " # prefix for titles
category: "announcements" # category slug, name, or ID (use lowercase)
min-body-length: 200 # optional minimum body length guard (fails safe-outputs job if shorter)
expires: 3 # auto-close after 3 days (or false to disable)
max: 3 # max discussions (default: 1)
target-repo: "owner/repo" # cross-repository
Expand All @@ -1021,6 +1022,8 @@ safe-outputs:
github-token: ${{ secrets.SOME_CUSTOM_TOKEN }} # optional custom token for permissions
```

Use `min-body-length` when you want a hard floor for report quality (for example, to prevent accidental placeholder bodies like `test` from being posted).

#### Fallback to Issue Creation

The `fallback-to-issue` field (default: `true`) automatically falls back to creating an issue when discussion creation fails (e.g., discussions disabled, insufficient `discussions: write` permissions, or org policy restrictions). The issue body notes it was intended to be a discussion. Set to `false` to fail instead of falling back.
Expand Down
5 changes: 5 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -5422,6 +5422,11 @@
"description": "Optional discussion category. Can be a category ID (string or numeric value), category name, or category slug/route. If not specified, uses the first available category. Matched first against category IDs, then against category names, then against category slugs. Numeric values are automatically converted to strings at runtime.",
"examples": ["General", "audits", 123456789]
},
"min-body-length": {
"type": "integer",
"minimum": 1,
"description": "Minimum required length of the discussion body content (before footer/metadata) in characters. If a create_discussion message body is shorter than this value, the safe-outputs job fails."
},
"labels": {
"type": "array",
"items": {
Expand Down
11 changes: 11 additions & 0 deletions pkg/workflow/compiler_safe_outputs_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,17 @@ func TestHandlerConfigBooleanFields(t *testing.T) {
checkKey: "draft",
expected: true, // AddTemplatableBool converts "true" string to JSON boolean
},
{
name: "create discussion minimum body length",
safeOutputs: &SafeOutputsConfig{
CreateDiscussions: &CreateDiscussionsConfig{
MinBodyLength: 200,
},
},
checkField: "create_discussion",
checkKey: "min_body_length",
expected: float64(200),
},
}

for _, tt := range tests {
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/compiler_safe_outputs_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ var handlerRegistry = map[string]handlerBuilder{
AddTemplatableInt("max", c.Max).
AddIfNotEmpty("category", c.Category).
AddIfNotEmpty("title_prefix", c.TitlePrefix).
AddIfPositive("min_body_length", c.MinBodyLength).
AddStringSlice("labels", c.Labels).
AddStringSlice("allowed_labels", c.AllowedLabels).
AddStringSlice("allowed_repos", c.AllowedRepos).
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/create_discussion.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type CreateDiscussionsConfig struct {
BaseSafeOutputConfig `yaml:",inline"`
TitlePrefix string `yaml:"title-prefix,omitempty"`
Category string `yaml:"category,omitempty"` // Discussion category ID or name
MinBodyLength int `yaml:"min-body-length,omitempty"` // Minimum required discussion body length before footer/markers
Labels []string `yaml:"labels,omitempty"` // Labels to attach to discussions and match when closing older ones
Comment on lines 16 to 19
AllowedLabels []string `yaml:"allowed-labels,omitempty"` // Optional list of allowed labels. If omitted, any labels are allowed (including creating new ones).
TargetRepoSlug string `yaml:"target-repo,omitempty"` // Target repository in format "owner/repo" for cross-repository discussions
Expand Down