Conversation
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>
…dd-frequency-options-scheduler
…hub.com/github/gh-aw into copilot/add-frequency-options-scheduler
There was a problem hiding this comment.
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-assistworkflow source (.md) and compiled lock workflow (.lock.yml). - Add interactive schedule detection/classification, option building, and a new schedule selection step in the
addwizard (with unit tests). - Update
create_pull_request.cjsto guardcontextaccess 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
detectWorkflowScheduleInfotreats any non-emptyschedule: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.
|
|
||
| on: | ||
| schedule: every 1h | ||
| workflow_dispatch: |
There was a problem hiding this comment.
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.
| // 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)) | ||
| } |
There was a problem hiding this comment.
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.
| // 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) |
There was a problem hiding this comment.
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.
| // 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) |
| // 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", | ||
| }; |
There was a problem hiding this comment.
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.
Add agentic workflow repo-assist