Skip to content

Add agentic workflow repo-assist#19888

Closed
dsyme wants to merge 7 commits intomainfrom
add-workflow-repo-assist-7969
Closed

Add agentic workflow repo-assist#19888
dsyme wants to merge 7 commits intomainfrom
add-workflow-repo-assist-7969

Conversation

@dsyme
Copy link
Contributor

@dsyme dsyme commented Mar 6, 2026

Add agentic workflow repo-assist

Copilot AI and others added 7 commits March 5, 2026 09:43
When adding a scheduled workflow via `gh aw add-wizard`, the wizard now
detects the schedule trigger and offers a choice of standard frequencies:
hourly, every 3 hours, daily, weekly, or monthly.

- The form defaults to the workflow's existing schedule.
- Custom schedules (not matching a standard frequency) are shown as a
  "Custom: <expr> (keep existing)" option which is the default.
- If the user selects the same frequency or "Custom", the frontmatter is
  left unchanged.
- For multi-trigger workflows (schedule + other triggers), the selector is
  skipped to avoid losing other triggers.

New files:
  pkg/cli/add_interactive_schedule.go       - detection & selection logic
  pkg/cli/add_interactive_schedule_test.go  - unit tests

Changed files:
  pkg/cli/add_interactive_orchestrator.go   - calls selectScheduleFrequency
                                              as step 7b in RunAddInteractive

Co-authored-by: dsyme <7204669+dsyme@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 6, 2026 19:16
@dsyme dsyme closed this Mar 6, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds the “repo-assist” agentic workflow to the repository and extends the interactive add wizard to detect scheduled workflows and offer a schedule-frequency selection step, plus adjusts PR-creation scripting around workflow context and permission-error handling.

Changes:

  • Add new repo-assist workflow source (.md) and compiled lock workflow (.lock.yml).
  • Add interactive schedule detection/classification, option building, and a new schedule selection step in the add wizard (with unit tests).
  • Update create_pull_request.cjs to guard context access and adjust run URL construction and permission-error handling.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
pkg/cli/add_interactive_schedule.go Implements schedule detection/classification and an interactive prompt to adjust frequency before creating the PR.
pkg/cli/add_interactive_schedule_test.go Adds unit tests for schedule detection/classification and option ordering/labels.
pkg/cli/add_interactive_orchestrator.go Invokes schedule-frequency selection as a new step in the interactive add flow.
actions/setup/js/create_pull_request.cjs Updates context guarding and changes permission-error behavior / footer URL construction.
.github/workflows/repo-assist.md Introduces the repo-assist workflow definition (frontmatter + instructions).
.github/workflows/repo-assist.lock.yml Adds the compiled workflow output (generated).
Comments suppressed due to low confidence (1)

pkg/cli/add_interactive_schedule.go:103

  • detectWorkflowScheduleInfo treats any non-empty schedule: array as updatable but only reads the first entry. For workflows with multiple schedule entries, the wizard would classify based on the first item and later updates would likely collapse/overwrite the rest. Safer behavior would be to mark multi-entry schedule arrays as not updatable (or explicitly handle updating all entries consistently).
		// Schedule as array (e.g., "schedule:\n  - cron: daily")
		if schedArray, ok := schedValue.([]any); ok && len(schedArray) > 0 {
			if item, ok := schedArray[0].(map[string]any); ok {
				if cronVal, ok := item["cron"].(string); ok {
					return scheduleDetection{
						RawExpr:        cronVal,

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

Comment on lines +15 to +18

on:
schedule: every 1h
workflow_dispatch:
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The workflow description says it “runs daily”, but the configured schedule is every 1h (hourly). Please align the description with the actual schedule (or adjust the schedule) to avoid misleading users.

Copilot uses AI. Check for mistakes.
Comment on lines +185 to +264
// selectScheduleFrequency presents a schedule-frequency selection form to the user when the
// workflow being added has a schedule trigger. If the user picks a different frequency the
// resolved workflow content is updated in memory so the change is reflected in the PR.
func (c *AddInteractiveConfig) selectScheduleFrequency() error {
if c.resolvedWorkflows == nil || len(c.resolvedWorkflows.Workflows) == 0 {
return nil
}

for _, wf := range c.resolvedWorkflows.Workflows {
content := string(wf.Content)
detection := detectWorkflowScheduleInfo(content)
if !detection.IsUpdatable {
continue
}

rawExpr := detection.RawExpr
currentFreq := detection.Frequency
scheduleWizardLog.Printf("Detected schedule: expr=%q, freq=%s, multiTrigger=%v", rawExpr, currentFreq, detection.IsMultiTrigger)

// Build the ordered option list
options := buildScheduleOptions(rawExpr, currentFreq)

fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("This workflow runs on a schedule."))

var selected string
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("How often should this workflow run?").
Description("Current schedule: " + rawExpr).
Options(options...).
Value(&selected),
),
).WithAccessible(console.IsAccessibleMode())

if err := form.Run(); err != nil {
return fmt.Errorf("failed to select schedule frequency: %w", err)
}

scheduleWizardLog.Printf("User selected frequency: %s", selected)

// "custom" or same frequency means keep as-is
if selected == "custom" || selected == currentFreq {
scheduleWizardLog.Printf("Schedule unchanged: keeping %q", rawExpr)
continue
}

// Look up the schedule expression for the chosen frequency
var newExpr string
for _, opt := range standardScheduleFrequencies {
if opt.Value == selected {
newExpr = opt.Expression
break
}
}
if newExpr == "" {
continue
}

// Update the workflow content in memory.
// For multi-trigger on: maps, update the "schedule" sub-field to preserve other triggers.
// For simple on: strings, update the "on" field directly.
updateField := "on"
if detection.IsMultiTrigger {
updateField = "schedule"
}
updatedContent, err := UpdateFieldInFrontmatter(content, updateField, newExpr)
if err != nil {
scheduleWizardLog.Printf("Failed to update schedule (field=%s): %v", updateField, err)
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Could not update schedule: %v", err)))
continue
}

wf.Content = []byte(updatedContent)
if wf.SourceInfo != nil {
wf.SourceInfo.Content = []byte(updatedContent)
}
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Schedule updated to: "+selected))
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

There are good unit tests for frequency classification/detection, but there’s no test that exercises the actual frontmatter update behavior in selectScheduleFrequency() (especially for on: maps with schedule + workflow_dispatch and multi-trigger cases). Adding a test that asserts the wizard updates on.schedule without introducing a top-level schedule: field (and without dropping other triggers) would prevent regressions.

Copilot uses AI. Check for mistakes.
Comment on lines +246 to +254
// For multi-trigger on: maps, update the "schedule" sub-field to preserve other triggers.
// For simple on: strings, update the "on" field directly.
updateField := "on"
if detection.IsMultiTrigger {
updateField = "schedule"
}
updatedContent, err := UpdateFieldInFrontmatter(content, updateField, newExpr)
if err != nil {
scheduleWizardLog.Printf("Failed to update schedule (field=%s): %v", updateField, err)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

When the workflow uses an on: map, the schedule is nested under on.schedule. Updating with UpdateFieldInFrontmatter(..., "schedule", ...) writes/overwrites a top-level schedule: field instead of on.schedule, leaving the actual trigger unchanged and potentially producing invalid/unknown frontmatter. Also, updating the entire on field to a scalar schedule expression will drop workflow_dispatch (and any other triggers) for map-based workflows. Consider always updating the nested field (e.g., via the existing SetFieldInOnTrigger helper) when on is a map, regardless of whether it is “multi-trigger”.

This issue also appears on line 98 of the same file.

Suggested change
// For multi-trigger on: maps, update the "schedule" sub-field to preserve other triggers.
// For simple on: strings, update the "on" field directly.
updateField := "on"
if detection.IsMultiTrigger {
updateField = "schedule"
}
updatedContent, err := UpdateFieldInFrontmatter(content, updateField, newExpr)
if err != nil {
scheduleWizardLog.Printf("Failed to update schedule (field=%s): %v", updateField, err)
// For multi-trigger on: maps, update the "schedule" sub-field under "on" to preserve other triggers.
// For simple on: strings, update the "on" field directly.
var updatedContent string
if detection.IsMultiTrigger {
// When "on" is a map (multi-trigger), update the nested on.schedule field.
updatedContent, err = SetFieldInOnTrigger(content, "schedule", newExpr)
} else {
// When "on" is a scalar schedule expression, update the "on" field itself.
updatedContent, err = UpdateFieldInFrontmatter(content, "on", newExpr)
}
if err != nil {
scheduleWizardLog.Printf("Failed to update schedule: %v", err)

Copilot uses AI. Check for mistakes.
Comment on lines 982 to +994
// Check if the error is the specific "GitHub actions is not permitted to create or approve pull requests" error
if (errorMessage.includes("GitHub Actions is not permitted to create or approve pull requests")) {
core.error("Permission error: GitHub Actions is not permitted to create or approve pull requests");

// Branch has already been pushed - create a fallback issue with a link to create the PR via GitHub UI
const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com";
// Encode branch name path segments individually to preserve '/' while encoding other special characters
const encodedBase = baseBranch.split("/").map(encodeURIComponent).join("/");
const encodedHead = branchName.split("/").map(encodeURIComponent).join("/");
const createPrUrl = `${githubServer}/${repoParts.owner}/${repoParts.repo}/compare/${encodedBase}...${encodedHead}?expand=1&title=${encodeURIComponent(title)}`;

// Read patch content for preview
let patchPreview = "";
if (patchFilePath && fs.existsSync(patchFilePath)) {
const patchContent = fs.readFileSync(patchFilePath, "utf8");
patchPreview = generatePatchPreview(patchContent);
}

const fallbackBody =
`${body}\n\n---\n\n` +
`> [!NOTE]\n` +
`> This was originally intended as a pull request, but GitHub Actions is not permitted to create or approve pull requests in this repository.\n` +
`> The changes have been pushed to branch \`${branchName}\`.\n` +
`>\n` +
`> **[Click here to create the pull request](${createPrUrl})**\n\n` +
`To fix the permissions issue, go to **Settings** → **Actions** → **General** and enable **Allow GitHub Actions to create and approve pull requests**.` +
patchPreview;

try {
const { data: issue } = await githubClient.rest.issues.create({
owner: repoParts.owner,
repo: repoParts.repo,
title: title,
body: fallbackBody,
labels: mergeFallbackIssueLabels(labels),
});

core.info(`Created fallback issue #${issue.number}: ${issue.html_url}`);

await updateActivationComment(github, context, core, issue.html_url, issue.number, "issue");

return {
success: true,
fallback_used: true,
issue_number: issue.number,
issue_url: issue.html_url,
branch_name: branchName,
repo: itemRepo,
};
} catch (issueError) {
const error = `Failed to create pull request (permission denied) and failed to create fallback issue. PR error: ${errorMessage}. Issue error: ${issueError instanceof Error ? issueError.message : String(issueError)}`;
core.error(error);
return {
success: false,
error,
error_type: "permission_denied",
};
}
// Set output variable for conclusion job to handle
core.setOutput(
"error_message",
"GitHub Actions is not permitted to create or approve pull requests. Please enable 'Allow GitHub Actions to create and approve pull requests' in repository settings: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#preventing-github-actions-from-creating-or-approving-pull-requests"
);
return {
success: false,
error: errorMessage,
error_type: "permission_denied",
};
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

This permission-error path sets a step output (core.setOutput("error_message", ...)) but the workflow’s conclusion step (handle_create_pr_error.cjs) looks for CREATE_PR_ERROR_MESSAGE and the generated workflow doesn’t pass this output through, so the permission issue will be silently skipped now that the fallback issue creation block was removed. Either keep the previous fallback issue behavior here, or plumb the error message through the safe-outputs job outputs/env so the conclusion job can act on it.

Copilot uses AI. Check for mistakes.
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