add-wizard: offer schedule frequency selection for scheduled workflows#19709
add-wizard: offer schedule frequency selection for scheduled workflows#19709
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 a new gh aw add-wizard step to detect scheduled workflows and let users choose a standard schedule frequency (hourly/3-hourly/daily/weekly/monthly) while preparing the PR.
Changes:
- Introduces schedule detection/classification + interactive selection, and applies the selected schedule to in-memory workflow content before PR creation.
- Integrates the new schedule-selection step into the interactive add orchestrator flow.
- Adds unit tests for schedule classification/detection and option-building.
- Updates the
create_pull_requestsafe-output handler (context guarding, run URL construction, footer args, and permission-denied handling).
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| pkg/cli/add_interactive_schedule.go | Adds schedule detection/classification, builds select options, and attempts to rewrite frontmatter based on user choice. |
| pkg/cli/add_interactive_schedule_test.go | Adds unit tests for schedule frequency classification, schedule detection, and option ordering. |
| pkg/cli/add_interactive_orchestrator.go | Inserts the schedule-frequency selection step into the wizard flow. |
| actions/setup/js/create_pull_request.cjs | Adjusts PR creation handler behavior around context usage, footer generation, run URL construction, and permission-denied flow. |
Comments suppressed due to low confidence (1)
actions/setup/js/create_pull_request.cjs:727
- The same manual
runUrlconstruction is duplicated in the git-push failure fallback path. Consider factoring this back to the sharedbuildWorkflowRunUrlhelper (and/or a local function) to avoid divergence and to ensure the URL always targets the workflow-run repo even whenrepoPartsis a different target repo.
const runId = context.runId;
const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com";
const runUrl = context.payload.repository ? `${context.payload.repository.html_url}/actions/runs/${runId}` : `${githubServer}/${repoParts.owner}/${repoParts.repo}/actions/runs/${runId}`;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // 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 changes the permission-denied behavior from “create fallback issue and return success” to “set an output and return failure”. That’s a significant behavior change and appears unrelated to the PR’s stated goal (add-wizard schedule selection). If the workflow expects a fallback issue for traceability/activation-comment updates, this will break that flow; either restore the previous fallback behavior or update the surrounding workflow/jobs and PR description to reflect the new failure-handling contract.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…hub.com/github/gh-aw into copilot/add-frequency-options-scheduler
gh aw add-wizardhad no way to change the schedule of a workflow being added — users got whatever frequency was baked into the upstream workflow.Changes
Schedule detection (
add_interactive_schedule.go)detectWorkflowScheduleInfo— extracts the raw schedule expression from frontmatter, handling both simple string form (on: daily) and map form (on: {schedule: [{cron: ...}], workflow_dispatch: null}). Skips multi-trigger workflows (e.g.on: schedule + push) to avoid clobbering unrelated triggers.classifyScheduleFrequency— maps any schedule expression tohourly | 3-hourly | daily | weekly | monthly | custom, covering friendly strings (daily,every 3h), FUZZY placeholders (FUZZY:DAILY * * *), and standard cron patterns.buildScheduleOptions— constructs the orderedhuhselect list; the current frequency is placed first and marked(current). Custom/unrecognised schedules get aCustom: <expr> (keep existing)option as the default.selectScheduleFrequency— wizard step: presents the form, and on a changed selection rewriteswf.Content/wf.SourceInfo.Contentin memory before the PR is created. No-op when user picks the same frequency or "Custom".Wizard integration (
add_interactive_orchestrator.go)selectScheduleFrequency()as step 7b inRunAddInteractive, after file enumeration and before the confirmation prompt.Frequency → expression mapping
on:every 1hevery 3hdailyweekly0 0 1 * *Warning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
https://api.github.com/graphql/usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw --local 64/pkg/tool/linux_amd64/vet git rev-�� --show-toplevel QX/ympLdpf_SIHeLx8ALucz/ItF6Ey0Q-tests /usr/bin/git te '**/*.cjs' '*git .cfg x_amd64/link git(http block)https://api.github.com/repos/actions/ai-inference/git/ref/tags/v1/usr/bin/gh gh api /repos/actions/ai-inference/git/ref/tags/v1 --jq .object.sha --get l /usr/bin/git ../pkg/workflow/git(http block)https://api.github.com/repos/actions/checkout/git/ref/tags/v3/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v3 --jq .object.sha -unreachable=false /tmp/go-build1158601089/b264/vet.cfg /opt/hostedtoolcache/uv/0.10.8/x86_64/bash(http block)https://api.github.com/repos/actions/checkout/git/ref/tags/v5/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha --verify main 64/pkg/tool/linux_amd64/vet(http block)/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha --show-toplevel x_amd64/vet /usr/bin/git(http block)/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha --show-toplevel 64/pkg/tool/linux_amd64/vet /usr/bin/git tants.go tants_test.go 64/pkg/tool/linu--show-toplevel git rev-�� --show-toplevel 64/pkg/tool/linux_amd64/compile /usr/bin/git g_.a 8601089/b251/vetrev-parse .cfg git(http block)https://api.github.com/repos/actions/checkout/git/ref/tags/v6/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha XFJrOcRib(http block)/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha tags/v5(http block)/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha --show-toplevel 64/pkg/tool/linux_amd64/vet /usr/bin/git HEAD .cfg 64/pkg/tool/linu--show-toplevel git rev-�� --show-toplevel 64/pkg/tool/linu--json /usr/bin/git git status --porgit(http block)https://api.github.com/repos/actions/download-artifact/git/ref/tags/v8/usr/bin/gh gh api /repos/actions/download-artifact/git/ref/tags/v8 --jq .object.sha se 8601089/b010/vet.cfg ache/go/1.25.0/x64/pkg/tool/linux_amd64/vet(http block)/usr/bin/gh gh api /repos/actions/download-artifact/git/ref/tags/v8 --jq .object.sha se 8601089/b234/vet.cfg ache/go/1.25.0/x64/pkg/tool/linux_amd64/vet(http block)/usr/bin/gh gh api /repos/actions/download-artifact/git/ref/tags/v8 --jq .object.sha t0 m0s 8601089/b292/vet.cfg assifyScheduleFrgit(http block)https://api.github.com/repos/actions/github-script/git/ref/tags/v8/usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha(http block)/usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha -c=4 -nolocalimports -importcfg /tmp/go-build3677500810/b385/importcfg -embedcfg /tmp/go-build3677500810/b385/embedcfg -pack(http block)/usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha on' --ignore-path ../../../.pret.prettierignore(http block)https://api.github.com/repos/actions/setup-go/git/ref/tags/v4/usr/bin/gh gh api /repos/actions/setup-go/git/ref/tags/v4 --jq .object.sha /tmp/gh-aw-test-runs/20260305-095513-12636/test-3102792710/.github/workflows config /usr/bin/git remote.origin.urgit(http block)https://api.github.com/repos/actions/setup-node/git/ref/tags/v4/usr/bin/gh gh api /repos/actions/setup-node/git/ref/tags/v4 --jq .object.sha --show-toplevel(http block)https://api.github.com/repos/actions/upload-artifact/git/ref/tags/v4/usr/bin/gh gh api /repos/actions/upload-artifact/git/ref/tags/v4 --jq .object.sha 7500810/b386/_pkg_.a .cfg 7500810/b386=> -p bracelet/x/exp/grev-parse -lang=go1.17 /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet -ato�� tructions-test-2759340695/.github/workflows -buildtags /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet -errorsas -ifaceassert -nilfunc 7500810/b386/importcfg(http block)https://api.github.com/repos/actions/upload-artifact/git/ref/tags/v7/usr/bin/gh gh api /repos/actions/upload-artifact/git/ref/tags/v7 --jq .object.sha te '../../../**/*.json' '!../../../pkg/workflow/-errorsas(http block)/usr/bin/gh gh api /repos/actions/upload-artifact/git/ref/tags/v7 --jq .object.sha te '../../../**/*.json' '!../../../pkg/workflow/js/**/*.json' ---p --noheadings .cfg(http block)/usr/bin/gh gh api /repos/actions/upload-artifact/git/ref/tags/v7 --jq .object.sha on rkflow/js/**/*.json x_amd64/compile erignore(http block)https://api.github.com/repos/github/gh-aw/actions/runs/1/artifacts/usr/bin/gh gh run download 1 --dir test-logs/run-1 --local 64/pkg/tool/linux_amd64/vet nore(http block)https://api.github.com/repos/github/gh-aw/actions/runs/12345/artifacts/usr/bin/gh gh run download 12345 --dir test-logs/run-12345 --local 64/pkg/tool/linux_amd64/vet nore(http block)https://api.github.com/repos/github/gh-aw/actions/runs/12346/artifacts/usr/bin/gh gh run download 12346 --dir test-logs/run-12346 --local 64/pkg/tool/linux_amd64/vet nore(http block)https://api.github.com/repos/github/gh-aw/actions/runs/2/artifacts/usr/bin/gh gh run download 2 --dir test-logs/run-2 --local x_amd64/link nore(http block)https://api.github.com/repos/github/gh-aw/actions/runs/3/artifacts/usr/bin/gh gh run download 3 --dir test-logs/run-3 les 64/pkg/tool/linux_amd64/vet nore(http block)https://api.github.com/repos/github/gh-aw/actions/runs/4/artifacts/usr/bin/gh gh run download 4 --dir test-logs/run-4 --local 64/pkg/tool/linux_amd64/vet nore(http block)https://api.github.com/repos/github/gh-aw/actions/runs/5/artifacts/usr/bin/gh gh run download 5 --dir test-logs/run-5 --local 64/pkg/tool/linu-nolocalimports nore(http block)https://api.github.com/repos/github/gh-aw/actions/workflows/usr/bin/gh gh workflow list --json name,state,path(http block)/usr/bin/gh gh run list --json databaseId,number,url,status,conclusion,workflowName,createdAt,startedAt,updatedAt,event,headBranch,headSha,displayTitle --workflow nonexistent-workflow-12345 --limit 100(http block)/usr/bin/gh gh run list --json databaseId,number,url,status,conclusion,workflowName,createdAt,startedAt,updatedAt,event,headBranch,headSha,displayTitle --workflow nonexistent-workflow-12345 --limit 6(http block)https://api.github.com/repos/github/gh-aw/git/ref/tags/v1.0.0/usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.0.0 --jq .object.sha --local .cfg 64/pkg/tool/linux_amd64/vet(http block)https://api.github.com/repos/github/gh-aw/git/ref/tags/v1.2.3/usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.2.3 --jq .object.sha(http block)https://api.github.com/repos/github/gh-aw/git/ref/tags/v2.0.0/usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v2.0.0 --jq .object.sha(http block)/usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v2.0.0 --jq .object.sha ./../pkg/workflo-errorsas(http block)https://api.github.com/repos/github/gh-aw/git/ref/tags/v3.0.0/usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v3.0.0 --jq .object.sha(http block)https://api.github.com/repos/nonexistent/action/git/ref/tags/v999.999.999/usr/bin/gh gh api /repos/nonexistent/action/git/ref/tags/v999.999.999 --jq .object.sha --local .cfg 64/pkg/tool/linux_amd64/vet(http block)https://api.github.com/repos/nonexistent/repo/actions/runs/12345/usr/bin/gh gh run view 12345 --repo nonexistent/repo --json status,conclusion nore(http block)https://api.github.com/repos/owner/repo/actions/workflows/usr/bin/gh gh workflow list --json name,state,path --repo owner/repo 0/x64/bin/git(http block)/usr/bin/gh gh workflow list --json name,state,path --repo owner/repo -importcfg /tmp/go-build103549757/b123/importcfg -pack /home/REDACTED/work/gh-aw/gh-aw/pkg/cli/access_log.go /home/REDACTED/work/gh-aw/gh-aw/pkg/cli/actionlint.go(http block)https://api.github.com/repos/owner/repo/contents/file.md/tmp/go-build3677500810/b383/cli.test /tmp/go-build3677500810/b383/cli.test -test.testlogfile=/tmp/go-build3677500810/b383/testlog.txt -test.paniconexit0 -test.v=true -test.parallel=4 -test.timeout=10m0s -test.run=^Test -test.short=true(http block)https://api.github.com/repos/test-owner/test-repo/actions/secrets/usr/bin/gh gh api /repos/test-owner/test-repo/actions/secrets --jq .secrets[].name(http block)/usr/bin/gh gh api /repos/test-owner/test-repo/actions/secrets --jq .secrets[].name --show-toplevel eutil.test /usr/bin/git se 8601089/b097/vetrev-parse ache/go/1.25.0/x--show-toplevel git rev-�� --show-toplevel ache/go/1.25.0/x64/pkg/tool/linux_amd64/vet /usr/bin/git se 8601089/b270/vetrev-parse ache/go/1.25.0/x--show-toplevel git(http block)If you need me to access, download, or install something from one of these locations, you can either:
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.
✨ PR Review Safe Output Test - Run 22718413768