From e53924d47d53602e16441637d0bf7f2599ae7209 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Mon, 26 Jan 2026 23:32:54 +0000 Subject: [PATCH 1/4] interactive add --- .../blog/2026-02-08-authoring-workflows.md | 3 - .../docs/blog/2026-02-11-getting-started.md | 470 -------- docs/src/content/docs/setup/quick-start.md | 136 +-- install.md | 2 +- pkg/cli/add_command.go | 393 +++++-- pkg/cli/add_command_test.go | 2 +- pkg/cli/add_current_repo_test.go | 6 +- pkg/cli/add_gitattributes_test.go | 9 +- pkg/cli/add_interactive.go | 1037 +++++++++++++++++ pkg/cli/add_wildcard_test.go | 6 +- pkg/cli/commands_compile_workflow_test.go | 2 +- pkg/cli/enable.go | 2 +- pkg/cli/file_tracker_test.go | 4 +- pkg/cli/init.go | 25 +- pkg/cli/packages.go | 19 +- pkg/cli/pr_command.go | 25 +- pkg/cli/trial_repository.go | 2 +- pkg/cli/update_command_test.go | 4 +- pkg/cli/update_workflows.go | 2 +- pkg/constants/constants.go | 27 + pkg/workflow/compiler.go | 22 +- pkg/workflow/compiler_types.go | 6 + pkg/workflow/data/action_pins.json | 188 +-- socials/PLAN.md | 1 - socials/campaign.log | 2 - socials/scripts.sh | 1 - 26 files changed, 1582 insertions(+), 814 deletions(-) delete mode 100644 docs/src/content/docs/blog/2026-02-11-getting-started.md create mode 100644 pkg/cli/add_interactive.go diff --git a/docs/src/content/docs/blog/2026-02-08-authoring-workflows.md b/docs/src/content/docs/blog/2026-02-08-authoring-workflows.md index 32c3942887..4a98f71efe 100644 --- a/docs/src/content/docs/blog/2026-02-08-authoring-workflows.md +++ b/docs/src/content/docs/blog/2026-02-08-authoring-workflows.md @@ -10,9 +10,6 @@ draft: true prev: link: /gh-aw/blog/2026-02-05-how-workflows-work/ label: How Workflows Work -next: - link: /gh-aw/blog/2026-02-11-getting-started/ - label: Getting Started --- [Previous Article](/gh-aw/blog/2026-02-05-how-workflows-work/) diff --git a/docs/src/content/docs/blog/2026-02-11-getting-started.md b/docs/src/content/docs/blog/2026-02-11-getting-started.md deleted file mode 100644 index 4d6e88a236..0000000000 --- a/docs/src/content/docs/blog/2026-02-11-getting-started.md +++ /dev/null @@ -1,470 +0,0 @@ ---- -title: "Getting Started with Agentic Workflows" -description: "Begin your journey with agentic automation" -authors: - - dsyme - - pelikhan - - mnkiefer -date: 2026-02-11 -draft: true -prev: - link: /gh-aw/blog/2026-02-08-authoring-workflows/ - label: Authoring Workflows ---- - -[Previous Article](/gh-aw/blog/2026-02-08-authoring-workflows/) - ---- - -Peli de Halleux - -We've reached the *grand conclusion* of our Peli's Agent Factory series! You've toured the [workflows](/gh-aw/blog/2026-01-13-meet-the-workflows/), discovered [lessons](/gh-aw/blog/2026-01-21-twelve-lessons/), learned the [patterns](/gh-aw/blog/2026-01-24-design-patterns/), mastered [operations](/gh-aw/blog/2026-01-27-operational-patterns/), explored [imports](/gh-aw/blog/2026-01-30-imports-and-sharing/), secured the [vault](/gh-aw/blog/2026-02-02-security-lessons/), glimpsed the [magnificent machinery](/gh-aw/blog/2026-02-05-how-workflows-work/), and practiced [authoring](/gh-aw/blog/2026-02-08-authoring-workflows/). Now for the *golden ticket* - your practical getting started guide! - -Ready to build your own agent ecosystem? Let's get you up and running! - -This guide will take you from zero to your first running workflow in just a few minutes, then show you how to grow from there. We'll start simple, build confidence, and then explore what's possible. By the end, you'll have a solid foundation for agentic automation. - -Let's do this! πŸš€ - -## Quick Start: Your First Workflow in 5 Minutes - -The fastest way to experience agentic workflows is to install a working example. We'll walk you through it step by step. - -### Prerequisites - -Before starting, make sure you have: - -- βœ… **GitHub CLI** (`gh`) - [Install here](https://cli.github.com) v2.0.0+ -- βœ… **GitHub account** with admin or write access to a repository -- βœ… **GitHub Actions** enabled in your repository -- βœ… **Git** installed on your machine -- βœ… **Operating System**: Linux, macOS, or Windows with WSL - -**Verify your setup:** - -```bash -gh --version # Should show version 2.0.0 or higher -gh auth status # Should show "Logged in to github.com" -git --version # Should show git version 2.x or higher -``` - -Looking good? Let's keep going! - -### Step 1: Install the Extension - -Install the GitHub Agentic Workflows CLI extension: - -```bash -gh extension install githubnext/gh-aw -``` - -:::note -If you're working in GitHub Codespaces and the installation fails, use the standalone installer: - -```bash -curl -sL https://raw.githubusercontent.com/githubnext/gh-aw/main/install-gh-aw.sh | bash -``` - -::: - -Easy, right? - -### Step 2: Add a Sample Workflow - -Navigate to your repository and install a sample workflow from the [Agentics Collection](https://github.com/githubnext/agentics): - -```bash -gh aw add githubnext/agentics/daily-team-status --create-pull-request -``` - -This creates a pull request that adds: - -- `.github/workflows/daily-team-status.md` (the natural language workflow) -- `.github/workflows/daily-team-status.lock.yml` (the compiled GitHub Actions workflow) - -Review the PR and merge it into your repository. You're doing great! - -### Step 3: Configure AI Authentication - -Workflows need to authenticate with an AI service. By default, they use **GitHub Copilot**. - -#### Create a Personal Access Token (PAT) - -1. Visit -2. Configure the token: - - **Token name**: "Agentic Workflows Copilot" - - **Expiration**: 90 days (recommended for testing) - - **Resource owner**: Your personal account - - **Repository access**: "Public repositories" or "All repositories" -3. Add permissions: - - In **"Account permissions"** (not Repository permissions) - - Find **"Copilot Requests"** - - Set to **"Access: Read"** -4. Click **"Generate token"** and copy it immediately - -:::tip -Can't find "Copilot Requests" permission? Make sure you have: - -- An active [GitHub Copilot subscription](https://github.com/settings/copilot) -- A fine-grained token (not classic) -- Personal account as Resource owner -- Public or all repositories selected - -::: - -#### Add Token to Your Repository - -1. Go to your repository β†’ **Settings** β†’ **Secrets and variables** β†’ **Actions** -2. Click **"New repository secret"** -3. Set **Name** to `COPILOT_GITHUB_TOKEN` -4. Paste the token in **Secret** -5. Click **"Add secret"** - -Perfect! You're almost there. - -### Step 4: Verify Setup - -Check that everything is configured correctly: - -```bash -gh aw status -``` - -**Expected output:** - -```text -Workflow Engine State Enabled Schedule -────────────────────────────────────────────────────────── -daily-team-status copilot βœ“ Yes 0 9 * * 1-5 -``` - -Looking good! - -### Step 5: Run Your First Workflow - -Trigger the workflow immediately (no need to wait for the schedule): - -```bash -gh aw run daily-team-status -``` - -After a minute or two, check the results: - -```bash -gh aw status -``` - -Once complete, check your repository's **Discussions** section for the generated team status report! - -πŸŽ‰ **Congratulations!** You've just run your first agentic workflow! - -## Growth Path: From One to Many - -Now that you have one workflow running, here's how to grow your agent ecosystem: - -### Phase 1: Learn by Example (Week 1) - -**Run multiple example workflows to understand patterns:** - -```bash -# Add a triage agent -gh aw add githubnext/agentics/issue-triage - -# Add a CI doctor -gh aw add githubnext/agentics/ci-doctor - -# Add a weekly summary -gh aw add githubnext/agentics/weekly-research -``` - -Observe how different workflows: - -- Trigger on different events -- Use different tools -- Create different outputs -- Serve different purposes - -### Phase 2: Customize Examples (Week 2) - -**Modify existing workflows to fit your needs:** - -1. Copy a workflow you like: - - ```bash - cp .github/workflows/issue-triage.md .github/workflows/my-triage.md - ``` - -2. Edit the prompt to match your repository: - - Change label names - - Adjust categories - - Add custom rules - - Update terminology - -3. Recompile: - - ```bash - gh aw compile .github/workflows/my-triage.md - ``` - -4. Test manually: - - ```bash - gh aw run my-triage - ``` - -### Phase 3: Create Original Workflows (Week 3+) - -**Build workflows specific to your team's needs:** - -Start with a simple Read-Only Analyst: - -```markdown ---- -description: Weekly dependency report -on: - schedule: "0 9 * * 1" # Monday mornings -permissions: - contents: read -safe_outputs: - create_discussion: - title: "Dependency Report - {date}" - category: "Reports" -imports: - - shared/reporting.md ---- - -## Weekly Dependency Analysis - -Analyze package.json (or requirements.txt, go.mod, etc.): - -1. List all dependencies -2. Check for available updates -3. Identify security vulnerabilities -4. Prioritize updates by importance - -Create a discussion with: -- Summary of dependency health -- List of available updates -- Security alerts -- Recommended actions -``` - -### Phase 4: Build Your Factory (Ongoing) - -**Systematically address pain points:** - -For each repetitive task, ask: - -1. Could an agent do this? -2. What pattern fits best? -3. What's the minimum viable version? -4. How can we test it safely? - -**Common starting points:** - -- Issue triage and labeling -- CI failure diagnosis -- Documentation updates -- Weekly metrics reports -- Security scanning -- Code quality checks - -## Essential Commands Reference - -### Workflow Management - -```bash -# List all workflows -gh aw list - -# Show workflow status -gh aw status [workflow-name] - -# Add workflow from collection -gh aw add [--create-pull-request] - -# Compile workflow -gh aw compile - -# Run workflow manually -gh aw run - -# Download workflow logs -gh aw logs -``` - -### Secret Management - -```bash -# Configure AI engine secrets -gh aw secrets bootstrap --engine copilot - -# List required secrets -gh aw secrets list - -# Validate secret configuration -gh aw secrets validate -``` - -### Debugging - -```bash -# Validate workflow syntax -gh aw validate - -# Show compilation output -gh aw compile --output preview.yml - -# Audit workflow runs -gh aw audit - -# Inspect MCP configuration -gh aw mcp inspect -``` - -## Best Practices for Beginners - -### Start Small - -βœ… **Do**: Begin with read-only analyst workflows -❌ **Don't**: Start with workflows that modify code - -### Test Manually First - -βœ… **Do**: Use `workflow_dispatch` triggers initially -❌ **Don't**: Deploy directly to automatic schedules - -### Use Time Limits - -βœ… **Do**: Add `stop-after: "+1mo"` to experiments -❌ **Don't**: Let experimental workflows run indefinitely - -### Copy Successful Patterns - -βœ… **Do**: Clone and modify working workflows -❌ **Don't**: Build everything from scratch - -### Review Every Output - -βœ… **Do**: Check issues, PRs, and discussions agents create -❌ **Don't**: Assume agents always get it right - -### Iterate Gradually - -βœ… **Do**: Make small changes, test, adjust -❌ **Don't**: Make large changes without testing - -## Common First-Week Questions - -### "Which AI engine should I use?" - -**Start with Copilot** (default). It's integrated with GitHub and uses your Copilot subscription. Try other engines later: - -- **Claude**: For longer context and detailed analysis -- **Codex**: For enterprise Azure integration -- **Custom**: For proprietary or specialized models - -### "How do I handle secrets?" - -Use repository secrets (Settings β†’ Secrets β†’ Actions): - -- `COPILOT_GITHUB_TOKEN` for Copilot -- `ANTHROPIC_API_KEY` for Claude -- `AZURE_OPENAI_*` for Codex - -Never put secrets in workflow files! - -### "What if a workflow creates too many issues?" - -Use safe output guardrails: - -```yaml -safe_outputs: - create_issue: - max_items: 3 # Limit to 3 - close_older: true # Close duplicates - expire: "+7d" # Auto-close after 7 days -``` - -### "How much does this cost?" - -Costs depend on: - -- **GitHub Actions**: Free tier covers many workflows -- **AI API calls**: Billed per request/token -- **Copilot**: Included in Copilot subscription - -Start with free tier, monitor usage with `gh aw audit`. - -### "Can I use this in production?" - -⚠️ **GitHub Agentic Workflows is a research demonstrator** in early development. Use with caution: - -- Review all agent outputs -- Use time-limited trials -- Implement human approval gates -- Monitor security alerts -- Have rollback plans - -### "Where can I get help?" - -Resources: - -- **Documentation**: -- **Examples**: -- **Discussions**: -- **Discord**: [GitHub Next Discord](https://gh.io/next-discord) #continuous-ai - -## Your First Week Plan - -### Day 1: Installation and Setup - -- Install gh-aw extension -- Add first sample workflow -- Configure authentication -- Run first workflow successfully - -### Day 2-3: Exploration - -- Install 3-5 different workflow types -- Observe how they behave -- Review their outputs -- Identify patterns - -### Day 4-5: Customization - -- Pick your favorite workflow -- Modify it for your repository -- Test the changes -- Deploy to schedule - -### Day 6-7: Creation - -- Identify a pain point in your workflow -- Find similar example workflow -- Adapt it to your needs -- Start with manual trigger only - -## Next Steps - -Once you're comfortable with the basics: - -1. **Study the patterns** - Review [12 Design Patterns](03-design-patterns.md) -2. **Explore advanced features** - Repo-memory, multi-phase workflows -3. **Join the community** - Share your workflows -4. **Contribute back** - Add your workflows to Agentics collection -5. **Build your factory** - Create an ecosystem of cooperating agents - -## Welcome to the Factory - -You're now part of a growing community exploring the frontier of automated agentic development. Start small, experiment safely, and share what you learn. - -The agents you build today will help shape the future of software development. - -**Ready to build your first workflow?** Head over to the [documentation](https://githubnext.github.io/gh-aw/) and start experimenting! - -## What's Next? - -_More articles in this series coming soon._ - -[Previous Article](/gh-aw/blog/2026-02-08-authoring-workflows/) diff --git a/docs/src/content/docs/setup/quick-start.md b/docs/src/content/docs/setup/quick-start.md index 22425209b5..d783fc346b 100644 --- a/docs/src/content/docs/setup/quick-start.md +++ b/docs/src/content/docs/setup/quick-start.md @@ -7,30 +7,23 @@ sidebar: ## Adding a Daily Status Workflow to Your Repo -In this guide you will add the automated [**Daily Status Report**](https://github.com/githubnext/agentics/blob/main/workflows/daily-team-status.md?plain=1) to an existing GitHub repository where you are a maintainer. +In this guide you will add the automated [**Daily Repo Status Report**](https://github.com/githubnext/agentics/blob/main/workflows/daily-repo-status.md?plain=1) to an existing GitHub repository where you are a maintainer, running in GitHub Actions. -Remember the aim here is _automated AI_: to install something that will run _automatically_ every day, in the context of your repository, and create a fresh status report issue in your repository without any further manual intervention. +Remember the aim here is _automated AI_: to install something that will run _automatically_ every day, in the context of your repository, and create a fresh status report issue in your repository without any further manual intervention. If you're familiar with GitHub Actions, you will be aware of the power of automation. -There are hundreds of other ways to use GitHub Agentic Workflows too, which you can explore in [Peli's Agent Factory](https://githubnext.github.io/gh-aw/blog/2026-01-12-welcome-to-pelis-agent-factory/). This workflow is just the start of what's possible, to get you familiar with the installation and setup process. +> [!TIP] +> There are hundreds of other ways to use GitHub Agentic Workflows, which +> you can explore in [Peli's Agent Factory](https://githubnext.github.io/gh-aw/blog/2026-01-12-welcome-to-pelis-agent-factory/). This +> workflow is just the start of what's possible. ## Prerequisites Before installing, ensure you have: -- βœ… **AI Account:** A GitHub Copilot, Anthropic Claude or OpenAI Codex subscription +- βœ… **AI Account** - a GitHub Copilot, Anthropic Claude or OpenAI Codex subscription - βœ… **GitHub Repository** you are a maintainer on - βœ… **[GitHub Actions](https://docs.github.com/actions)** enabled in your repository - βœ… **GitHub CLI** (`gh`) - A command-line tool for GitHub operations. [Install here](https://cli.github.com) v2.0.0+ and authenticate with `gh auth login` -- βœ… **Git** installed on your machine -- βœ… **Operating System:** Linux, macOS, or Windows with WSL - -**Verify your setup:** - -```bash -gh --version # Should show version 2.0.0 or higher -gh auth status # Should show "Logged in to github.com" -git --version # Should show git version 2.x or higher -``` ### Step 1 β€” Install the extension @@ -49,115 +42,46 @@ gh extension install githubnext/gh-aw > curl -sL https://raw.githubusercontent.com/githubnext/gh-aw/main/install-gh-aw.sh | bash > ``` -### Step 2 β€” Initialize Agentic Workflows support in your repository +### Step 2 β€” Add the sample workflow -Initialize agentic workflows in your repository, to configure optional additional supporting files and settings: +From your repository root run: ```bash wrap -gh aw init --push +gh aw add githubnext/agentics/daily-repo-status ``` -This command installs tools and automatically commits and pushes the changes to your repository. - -> [!TIP] -> -> If you have branch protection rules enabled, replace `--push` with `--create-pull-request`, then review and merge the pull request. - -### Step 3 β€” Add a sample workflow +This will take you through an interactive process to -Add a sample from the [agentics](https://github.com/githubnext/agentics) collection. From your repository root run: +1. Select an AI Engine to use +2. Add the workflow and set up required secrets +3. Trigger an initial run of the workflow -```bash wrap -gh aw add githubnext/agentics/daily-team-status --push -``` +### Step 3 β€” Looking at the results -This adds `.github/workflows/daily-team-status.md` and `.github/workflows/daily-team-status.lock.yml` to your repository. The second file is the [compiled](/gh-aw/reference/glossary/#compilation) GitHub Actions workflow file corresponding to the agentic workflow. +All going well, you have now successfully installed your first automated agentic workflow into your repository and triggered an initial run. -> [!TIP] -> -> If you have branch protection rules enabled, replace `--push` with `--create-pull-request`, then review and merge the pull request. - -### Step 4 β€” Add an AI secret (Copilot Users) - -[Agentic workflows](/gh-aw/reference/glossary/#agentic-workflow) need to authenticate with an AI service to execute. By default, they use **GitHub Copilot** as the AI service, but you can also use **Anthropic Claude** or **OpenAI Codex**. - -The instructions below assume you have an active [GitHub Copilot subscription](https://github.com/settings/copilot). Claude/Codex Users see [AI Engines](/gh-aw/reference/engines/). - -#### Copilot Users: Create a Personal Access Token (PAT) - -Create a [Personal Access Token](/gh-aw/reference/glossary/#personal-access-token-pat) to authenticate your workflows with GitHub Copilot: - -1. Visit -2. Configure the token: - - **Token name**: "Agentic Workflows Copilot" - - **Expiration**: 90 days (recommended for testing) - - **Resource owner**: Your personal account (required for Copilot Requests permission) - - **Repository access**: "Public repositories" (required for Copilot Requests permission to appear) -3. Add permissions: - - In **"Account permissions"** (not Repository permissions), find **"Copilot Requests"** - - Set to **"Access: Read"** -4. Click **"Generate token"** and copy it immediately (you won't see it again) - -> [!TIP] -> Can't find Copilot Requests permission? -> -> This requires an active [GitHub Copilot subscription](https://github.com/settings/copilot), a fine-grained token (not classic), personal account as Resource owner, and "Public repositories" or "All repositories" selected. Contact your GitHub administrator if Copilot is managed by your organization. -> +Once complete, a new issue will be created in your repository with a "repo status report". The report will be automatically generated by the AI based on recent activity in your repository, including issues, PRs, discussions, releases, and code changes. -#### Add the token to your repository +### Step 4 β€” Customize your workflow -Store the token as a repository secret: +You can now customize the workflow by editing the workflow markdown file located at `.github/workflows/daily-repo-status.md` in your repository. You then run -1. Go to **your repository** β†’ **Settings** β†’ **Secrets and variables** β†’ **Actions** -2. Click **New repository secret** -3. Set **Name** to `COPILOT_GITHUB_TOKEN` and paste the token in **Secret** -4. Click **Add secret** - -Repository secrets are encrypted and only accessible to workflows in your repository. See [GitHub Copilot CLI documentation](https://github.com/github/copilot-cli?tab=readme-ov-file#authenticate-with-a-personal-access-token-pat) for more details. - -#### Verify your setup - -Before running workflows, verify everything is configured correctly: - -```bash wrap -gh aw status -``` - -**Expected output:** - -```text -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚Workflow β”‚Engine β”‚Compiledβ”‚Statusβ”‚Time Remainingβ”‚Labelsβ”‚Run Statusβ”‚Run Conclusionβ”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚daily-team-statusβ”‚copilotβ”‚No β”‚activeβ”‚30d 22h β”‚- β”‚- β”‚- β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -This confirms the workflow is compiled, enabled, and scheduled correctly. - -> [!TIP] -> Troubleshooting -> -> If the workflow isn't listed, run `gh aw compile` and verify `.github/workflows/daily-team-status.md` exists, and add and push it to your repo. If errors occur when running, verify the `COPILOT_GITHUB_TOKEN` secret is set with "Copilot Requests" permission and hasn't expired. Run `gh aw secrets bootstrap --engine copilot` to check configuration. - -### Step 5 β€” Trigger a workflow run - -Trigger the workflow immediately in GitHub Actions (this may fail in a codespace): - -```bash wrap -gh aw run daily-team-status +```bash +gh aw compile ``` -After a few moments, check the status: +to regenerate the workflow YAML file, and push to your repository. You can then trigger another run by running: -```bash wrap -gh aw status +```bash +gh aw run daily-repo-status ``` -Once complete, a new issue will be created in your repository with a daily team status report! The report will be automatically generated by the AI based on recent activity in your repository, including issues, PRs, discussions, releases, and code changes. +## What's next? -You have successfully installed your first automated agentic workflow into your repository. +With that, you are up and running with your first automated agentic workflow! -## What's next? +Explore further with: -Next up is [Authoring Agentic Workflows](/gh-aw/setup/agentic-authoring/) where you will learn how to create automated workflows with AI assistance. You can also explore the samples in [Peli's Agent Factory](/gh-aw/blog/2026-01-12-welcome-to-pelis-agent-factory/). To understand how agentic workflows work, read [How They Work](/gh-aw/introduction/how-they-work/). +- [Authoring Agentic Workflows using AI](/gh-aw/setup/agentic-authoring/) where you will learn how to create automated workflows with AI assistance. +- Explore the samples in [Peli's Agent Factory](/gh-aw/blog/2026-01-12-welcome-to-pelis-agent-factory/). +- Understand [How Agentic Workflows Work](/gh-aw/introduction/how-they-work/). diff --git a/install.md b/install.md index 0d4ed88695..7a1478e2bc 100644 --- a/install.md +++ b/install.md @@ -140,7 +140,7 @@ gh aw add githubnext/agentics This shows available workflows. Add one: ```bash -gh aw add githubnext/agentics/daily-team-status --create-pull-request +gh aw add githubnext/agentics/daily-repo-status ``` **Option B: Use the AI agent to create workflows** diff --git a/pkg/cli/add_command.go b/pkg/cli/add_command.go index 2f6e2360ed..29a1e235b6 100644 --- a/pkg/cli/add_command.go +++ b/pkg/cli/add_command.go @@ -1,6 +1,7 @@ package cli import ( + "context" "fmt" "math/rand" "os" @@ -12,12 +13,50 @@ import ( "github.com/githubnext/gh-aw/pkg/console" "github.com/githubnext/gh-aw/pkg/constants" "github.com/githubnext/gh-aw/pkg/logger" + "github.com/githubnext/gh-aw/pkg/parser" + "github.com/githubnext/gh-aw/pkg/tty" "github.com/githubnext/gh-aw/pkg/workflow" "github.com/spf13/cobra" ) var addLog = logger.New("cli:add_command") +// AddWorkflowsResult contains the result of adding workflows +type AddWorkflowsResult struct { + // PRNumber is the PR number if a PR was created, or 0 if no PR was created + PRNumber int + // PRURL is the URL of the created PR, or empty if no PR was created + PRURL string + // HasWorkflowDispatch is true if any of the added workflows has a workflow_dispatch trigger + HasWorkflowDispatch bool +} + +// ResolvedWorkflow contains metadata about a workflow that has been resolved and is ready to add +type ResolvedWorkflow struct { + // Spec is the parsed workflow specification + Spec *WorkflowSpec + // Content is the raw workflow content + Content []byte + // SourceInfo contains source metadata (package path, commit SHA) + SourceInfo *WorkflowSourceInfo + // Description is the workflow description extracted from frontmatter + Description string + // Engine is the preferred engine extracted from frontmatter (empty if not specified) + Engine string + // HasWorkflowDispatch indicates if the workflow has workflow_dispatch trigger + HasWorkflowDispatch bool +} + +// ResolvedWorkflows contains all resolved workflows ready to be added +type ResolvedWorkflows struct { + // Workflows is the list of resolved workflows + Workflows []*ResolvedWorkflow + // HasWildcard indicates if any of the original specs contained wildcards + HasWildcard bool + // HasWorkflowDispatch is true if any of the workflows has a workflow_dispatch trigger + HasWorkflowDispatch bool +} + // NewAddCommand creates the add command func NewAddCommand(validateEngine func(string) error) *cobra.Command { cmd := &cobra.Command{ @@ -25,9 +64,18 @@ func NewAddCommand(validateEngine func(string) error) *cobra.Command { Short: "Add agentic workflows from repositories to .github/workflows", Long: `Add one or more workflows from repositories to .github/workflows. +By default, this command runs in interactive mode, which guides you through: + - Selecting an AI engine (Copilot, Claude, or Codex) + - Configuring API keys and secrets + - Creating a pull request with the workflow + - Optionally running the workflow + +Use --non-interactive to skip the guided setup and add workflows directly. + Examples: + ` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics/daily-repo-status # Interactive setup (recommended) ` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics # List available workflows - ` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics/ci-doctor # Add specific workflow + ` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics/ci-doctor --non-interactive # Skip interactive mode ` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics/ci-doctor@v1.0.0 # Add with version ` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics/workflows/ci-doctor.md@main ` + string(constants.CLIExtensionPrefix) + ` add https://github.com/githubnext/agentics/blob/main/workflows/ci-doctor.md @@ -50,6 +98,7 @@ The --dir flag allows you to specify a subdirectory under .github/workflows/ whe The --create-pull-request flag (or --pr) automatically creates a pull request with the workflow changes. The --push flag automatically commits and pushes changes after successful workflow addition. The --force flag overwrites existing workflow files. +The --non-interactive flag skips the guided setup and uses traditional behavior. Note: To create a new workflow from scratch, use the 'new' command instead.`, Args: cobra.MinimumNArgs(1), @@ -69,16 +118,43 @@ Note: To create a new workflow from scratch, use the 'new' command instead.`, workflowDir, _ := cmd.Flags().GetString("dir") noStopAfter, _ := cmd.Flags().GetBool("no-stop-after") stopAfter, _ := cmd.Flags().GetString("stop-after") + nonInteractive, _ := cmd.Flags().GetBool("non-interactive") if err := validateEngine(engineOverride); err != nil { return err } - // Handle normal mode + // Determine if we should use interactive mode + // Interactive mode is the default for TTY unless: + // - --non-interactive flag is set + // - Any of the batch/automation flags are set (--create-pull-request, --force, --name, --number > 1, --append) + // - Not a TTY (piped input/output) + // - In CI environment + // - This is a repo-only spec (listing workflows) + useInteractive := !nonInteractive && + !prFlag && + !forceFlag && + nameFlag == "" && + numberFlag == 1 && + appendText == "" && + tty.IsStdoutTerminal() && + os.Getenv("CI") == "" && + os.Getenv("GO_TEST_MODE") != "true" && + !isRepoOnlySpec(workflows[0]) + + if useInteractive { + addLog.Print("Using interactive mode") + ctx := context.Background() + return RunAddInteractive(ctx, workflows, verbose, engineOverride, noGitattributes, workflowDir, noStopAfter, stopAfter) + } + + // Handle normal (non-interactive) mode if prFlag { - return AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, true, pushFlag, noGitattributes, workflowDir, noStopAfter, stopAfter) + _, err := AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, true, pushFlag, noGitattributes, workflowDir, noStopAfter, stopAfter) + return err } else { - return AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, false, pushFlag, noGitattributes, workflowDir, noStopAfter, stopAfter) + _, err := AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, false, pushFlag, noGitattributes, workflowDir, noStopAfter, stopAfter) + return err } }, } @@ -121,6 +197,9 @@ Note: To create a new workflow from scratch, use the 'new' command instead.`, // Add stop-after flag to add command cmd.Flags().String("stop-after", "", "Override stop-after value in the workflow (e.g., '+48h', '2025-12-31 23:59:59')") + // Add non-interactive flag to add command + cmd.Flags().Bool("non-interactive", false, "Skip interactive setup and use traditional behavior (for CI/automation)") + // Register completions for add command RegisterEngineFlagCompletion(cmd) RegisterDirFlagCompletion(cmd, "dir") @@ -128,69 +207,40 @@ Note: To create a new workflow from scratch, use the 'new' command instead.`, return cmd } -// AddWorkflows adds one or more workflows from components to .github/workflows -// with optional repository installation and PR creation -func AddWorkflows(workflows []string, number int, verbose bool, engineOverride string, name string, force bool, appendText string, createPR bool, push bool, noGitattributes bool, workflowDir string, noStopAfter bool, stopAfter string) error { - addLog.Printf("Adding workflows: count=%d, engineOverride=%s, createPR=%v, push=%v, noGitattributes=%v, workflowDir=%s, noStopAfter=%v, stopAfter=%s", len(workflows), engineOverride, createPR, push, noGitattributes, workflowDir, noStopAfter, stopAfter) +// ResolveWorkflows resolves workflow specifications by parsing specs, installing repositories, +// expanding wildcards, and fetching workflow content (including descriptions). +// This is useful for showing workflow information before actually adding them. +func ResolveWorkflows(workflows []string, verbose bool) (*ResolvedWorkflows, error) { + addLog.Printf("Resolving workflows: count=%d", len(workflows)) if len(workflows) == 0 { - return fmt.Errorf("at least one workflow name is required") + return nil, fmt.Errorf("at least one workflow name is required") } for i, workflow := range workflows { if workflow == "" { - return fmt.Errorf("workflow name cannot be empty (workflow %d)", i+1) - } - } - - // Check if this is a repo-only specification (owner/repo instead of owner/repo/workflow) - // If so, list available workflows and exit - if len(workflows) == 1 && isRepoOnlySpec(workflows[0]) { - return handleRepoOnlySpec(workflows[0], verbose) - } - - // If creating a PR or pushing, check prerequisites - if createPR || push { - // Check if we're in a git repository - if !isGitRepo() { - if createPR { - return fmt.Errorf("not in a git repository - PR creation requires a git repository") - } - return fmt.Errorf("not in a git repository - push requires a git repository") - } - - // Check no other changes are present - if err := checkCleanWorkingDirectory(verbose); err != nil { - if createPR { - return fmt.Errorf("working directory is not clean: %w", err) - } - return fmt.Errorf("--push requires a clean working directory: %w", err) - } - - // Check if GitHub CLI is available (only for PR) - if createPR && !isGHCLIAvailable() { - return fmt.Errorf("GitHub CLI (gh) is required for PR creation but not available") + return nil, fmt.Errorf("workflow name cannot be empty (workflow %d)", i+1) } } // Parse workflow specifications and group by repository repoVersions := make(map[string]string) // repo -> version - processedWorkflows := []*WorkflowSpec{} // List of processed workflow specs + parsedSpecs := []*WorkflowSpec{} // List of parsed workflow specs for _, workflow := range workflows { spec, err := parseWorkflowSpec(workflow) if err != nil { - return fmt.Errorf("invalid workflow specification '%s': %w", workflow, err) + return nil, fmt.Errorf("invalid workflow specification '%s': %w", workflow, err) } // Handle repository installation and workflow name extraction if existing, exists := repoVersions[spec.RepoSlug]; exists && existing != spec.Version { - return fmt.Errorf("conflicting versions for repository %s: %s vs %s", spec.RepoSlug, existing, spec.Version) + return nil, fmt.Errorf("conflicting versions for repository %s: %s vs %s", spec.RepoSlug, existing, spec.Version) } repoVersions[spec.RepoSlug] = spec.Version // Create qualified name for processing - processedWorkflows = append(processedWorkflows, spec) + parsedSpecs = append(parsedSpecs, spec) } // Check if any workflow is from the current repository @@ -198,14 +248,14 @@ func AddWorkflows(workflows []string, number int, verbose bool, engineOverride s currentRepoSlug, repoErr := GetCurrentRepoSlug() if repoErr == nil { // We successfully determined the current repository, check all workflow specs - for _, spec := range processedWorkflows { + for _, spec := range parsedSpecs { // Skip local workflow specs (starting with "./") if strings.HasPrefix(spec.WorkflowPath, "./") { continue } if spec.RepoSlug == currentRepoSlug { - return fmt.Errorf("cannot add workflows from the current repository (%s). The 'add' command is for installing workflows from other repositories", currentRepoSlug) + return nil, fmt.Errorf("cannot add workflows from the current repository (%s). The 'add' command is for installing workflows from other repositories", currentRepoSlug) } } } @@ -227,13 +277,13 @@ func AddWorkflows(workflows []string, number int, verbose bool, engineOverride s // Install as global package (not local) to match the behavior expected if err := InstallPackage(repoWithVersion, verbose); err != nil { addLog.Printf("Failed to install repository %s: %v", repoWithVersion, err) - return fmt.Errorf("failed to install repository %s: %w", repoWithVersion, err) + return nil, fmt.Errorf("failed to install repository %s: %w", repoWithVersion, err) } } // Check if any workflow specs contain wildcards before expansion hasWildcard := false - for _, spec := range processedWorkflows { + for _, spec := range parsedSpecs { if spec.IsWildcard { hasWildcard = true break @@ -242,20 +292,120 @@ func AddWorkflows(workflows []string, number int, verbose bool, engineOverride s // Expand wildcards after installation var err error - processedWorkflows, err = expandWildcardWorkflows(processedWorkflows, verbose) + parsedSpecs, err = expandWildcardWorkflows(parsedSpecs, verbose) if err != nil { - return err + return nil, err } + // Fetch workflow content and metadata for each workflow + resolvedWorkflows := make([]*ResolvedWorkflow, 0, len(parsedSpecs)) + hasWorkflowDispatch := false + + for _, spec := range parsedSpecs { + // Fetch workflow content + content, sourceInfo, err := findWorkflowInPackageForRepo(spec, verbose) + if err != nil { + return nil, fmt.Errorf("workflow '%s' not found: %w", spec.WorkflowPath, err) + } + + // Extract description from content + description := ExtractWorkflowDescription(string(content)) + + // Extract engine from content (if specified in frontmatter) + engine := ExtractWorkflowEngine(string(content)) + + // Check for workflow_dispatch trigger + workflowHasDispatch := checkWorkflowHasDispatch(spec, verbose) + if workflowHasDispatch { + hasWorkflowDispatch = true + } + + resolvedWorkflows = append(resolvedWorkflows, &ResolvedWorkflow{ + Spec: spec, + Content: content, + SourceInfo: sourceInfo, + Description: description, + Engine: engine, + HasWorkflowDispatch: workflowHasDispatch, + }) + } + + return &ResolvedWorkflows{ + Workflows: resolvedWorkflows, + HasWildcard: hasWildcard, + HasWorkflowDispatch: hasWorkflowDispatch, + }, nil +} + +// AddWorkflows adds one or more workflows from components to .github/workflows +// with optional repository installation and PR creation. +// Returns AddWorkflowsResult containing PR number (if created) and other metadata. +func AddWorkflows(workflows []string, number int, verbose bool, engineOverride string, name string, force bool, appendText string, createPR bool, push bool, noGitattributes bool, workflowDir string, noStopAfter bool, stopAfter string) (*AddWorkflowsResult, error) { + // Check if this is a repo-only specification (owner/repo instead of owner/repo/workflow) + // If so, list available workflows and exit + if len(workflows) == 1 && isRepoOnlySpec(workflows[0]) { + return &AddWorkflowsResult{}, handleRepoOnlySpec(workflows[0], verbose) + } + + // Resolve workflows first + resolved, err := ResolveWorkflows(workflows, verbose) + if err != nil { + return nil, err + } + + return AddResolvedWorkflows(workflows, resolved, number, verbose, false, engineOverride, name, force, appendText, createPR, push, noGitattributes, workflowDir, noStopAfter, stopAfter) +} + +// AddResolvedWorkflows adds workflows using pre-resolved workflow data. +// This allows callers to resolve workflows early (e.g., to show descriptions) and then add them later. +// The quiet parameter suppresses detailed output (useful for interactive mode where output is already shown). +func AddResolvedWorkflows(workflowStrings []string, resolved *ResolvedWorkflows, number int, verbose bool, quiet bool, engineOverride string, name string, force bool, appendText string, createPR bool, push bool, noGitattributes bool, workflowDir string, noStopAfter bool, stopAfter string) (*AddWorkflowsResult, error) { + addLog.Printf("Adding workflows: count=%d, engineOverride=%s, createPR=%v, noGitattributes=%v, workflowDir=%s, noStopAfter=%v, stopAfter=%s", len(workflowStrings), engineOverride, createPR, noGitattributes, workflowDir, noStopAfter, stopAfter) + + result := &AddWorkflowsResult{} + + // If creating a PR, check prerequisites + if createPR { + // Check if GitHub CLI is available + if !isGHCLIAvailable() { + return nil, fmt.Errorf("GitHub CLI (gh) is required for PR creation but not available") + } + + // Check if we're in a git repository + if !isGitRepo() { + return nil, fmt.Errorf("not in a git repository - PR creation requires a git repository") + } + + // Check no other changes are present + if err := checkCleanWorkingDirectory(verbose); err != nil { + return nil, fmt.Errorf("working directory is not clean: %w", err) + } + } + + // Extract the workflow specs for processing + processedWorkflows := make([]*WorkflowSpec, len(resolved.Workflows)) + for i, rw := range resolved.Workflows { + processedWorkflows[i] = rw.Spec + } + + // Set workflow_dispatch result + result.HasWorkflowDispatch = resolved.HasWorkflowDispatch + // Handle PR creation workflow if createPR { addLog.Print("Creating workflow with PR") - return addWorkflowsWithPR(processedWorkflows, number, verbose, engineOverride, name, force, appendText, noGitattributes, hasWildcard, workflowDir, noStopAfter, stopAfter) + prNumber, prURL, err := addWorkflowsWithPR(processedWorkflows, number, verbose, quiet, engineOverride, name, force, appendText, push, noGitattributes, resolved.HasWildcard, workflowDir, noStopAfter, stopAfter) + if err != nil { + return nil, err + } + result.PRNumber = prNumber + result.PRURL = prURL + return result, nil } // Handle normal workflow addition addLog.Print("Adding workflows normally without PR") - return addWorkflowsNormal(processedWorkflows, number, verbose, engineOverride, name, force, appendText, push, noGitattributes, hasWildcard, workflowDir, noStopAfter, stopAfter) + return result, addWorkflowsNormal(processedWorkflows, number, verbose, quiet, engineOverride, name, force, appendText, push, noGitattributes, resolved.HasWildcard, workflowDir, noStopAfter, stopAfter) } // handleRepoOnlySpec handles the case when user provides only owner/repo without workflow name @@ -410,7 +560,7 @@ func displayAvailableWorkflows(repoSlug, version string, verbose bool) error { } // addWorkflowsNormal handles normal workflow addition without PR creation -func addWorkflowsNormal(workflows []*WorkflowSpec, number int, verbose bool, engineOverride string, name string, force bool, appendText string, push bool, noGitattributes bool, fromWildcard bool, workflowDir string, noStopAfter bool, stopAfter string) error { +func addWorkflowsNormal(workflows []*WorkflowSpec, number int, verbose bool, quiet bool, engineOverride string, name string, force bool, appendText string, push bool, noGitattributes bool, fromWildcard bool, workflowDir string, noStopAfter bool, stopAfter string) error { // Create file tracker for all operations tracker, err := NewFileTracker() if err != nil { @@ -435,13 +585,13 @@ func addWorkflowsNormal(workflows []*WorkflowSpec, number int, verbose bool, eng } } - if len(workflows) > 1 { + if !quiet && len(workflows) > 1 { fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Adding %d workflow(s)...", len(workflows)))) } // Add each workflow for i, workflow := range workflows { - if len(workflows) > 1 { + if !quiet && len(workflows) > 1 { fmt.Fprintln(os.Stderr, console.FormatProgressMessage(fmt.Sprintf("Adding workflow %d/%d: %s", i+1, len(workflows), workflow.WorkflowName))) } @@ -451,12 +601,12 @@ func addWorkflowsNormal(workflows []*WorkflowSpec, number int, verbose bool, eng currentName = name } - if err := addWorkflowWithTracking(workflow, number, verbose, engineOverride, currentName, force, appendText, tracker, fromWildcard, workflowDir, noStopAfter, stopAfter); err != nil { + if err := addWorkflowWithTracking(workflow, number, verbose, quiet, engineOverride, currentName, force, appendText, tracker, fromWildcard, workflowDir, noStopAfter, stopAfter); err != nil { return fmt.Errorf("failed to add workflow '%s': %w", workflow.String(), err) } } - if len(workflows) > 1 { + if !quiet && len(workflows) > 1 { fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Successfully added all %d workflows", len(workflows)))) } @@ -512,12 +662,12 @@ func addWorkflowsNormal(workflows []*WorkflowSpec, number int, verbose bool, eng return nil } -// addWorkflowsWithPR handles workflow addition with PR creation -func addWorkflowsWithPR(workflows []*WorkflowSpec, number int, verbose bool, engineOverride string, name string, force bool, appendText string, noGitattributes bool, fromWildcard bool, workflowDir string, noStopAfter bool, stopAfter string) error { +// addWorkflowsWithPR handles workflow addition with PR creation and returns the PR number and URL +func addWorkflowsWithPR(workflows []*WorkflowSpec, number int, verbose bool, quiet bool, engineOverride string, name string, force bool, appendText string, push bool, noGitattributes bool, fromWildcard bool, workflowDir string, noStopAfter bool, stopAfter string) (int, string, error) { // Get current branch for restoration later currentBranch, err := getCurrentBranch() if err != nil { - return fmt.Errorf("failed to get current branch: %w", err) + return 0, "", fmt.Errorf("failed to get current branch: %w", err) } // Create temporary branch with random 4-digit number @@ -525,13 +675,13 @@ func addWorkflowsWithPR(workflows []*WorkflowSpec, number int, verbose bool, eng branchName := fmt.Sprintf("add-workflow-%s-%04d", strings.ReplaceAll(workflows[0].WorkflowPath, "/", "-"), randomNum) if err := createAndSwitchBranch(branchName, verbose); err != nil { - return fmt.Errorf("failed to create branch %s: %w", branchName, err) + return 0, "", fmt.Errorf("failed to create branch %s: %w", branchName, err) } // Create file tracker for rollback capability tracker, err := NewFileTracker() if err != nil { - return fmt.Errorf("failed to create file tracker: %w", err) + return 0, "", fmt.Errorf("failed to create file tracker: %w", err) } // Ensure we switch back to original branch on exit @@ -542,12 +692,12 @@ func addWorkflowsWithPR(workflows []*WorkflowSpec, number int, verbose bool, eng }() // Add workflows using the normal function logic - if err := addWorkflowsNormal(workflows, number, verbose, engineOverride, name, force, appendText, false, noGitattributes, fromWildcard, workflowDir, noStopAfter, stopAfter); err != nil { + if err := addWorkflowsNormal(workflows, number, verbose, quiet, engineOverride, name, force, appendText, push, noGitattributes, fromWildcard, workflowDir, noStopAfter, stopAfter); err != nil { // Rollback on error if rollbackErr := tracker.RollbackAllFiles(verbose); rollbackErr != nil && verbose { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to rollback files: %v", rollbackErr))) } - return fmt.Errorf("failed to add workflows: %w", err) + return 0, "", fmt.Errorf("failed to add workflows: %w", err) } // Stage all files before creating PR @@ -555,7 +705,7 @@ func addWorkflowsWithPR(workflows []*WorkflowSpec, number int, verbose bool, eng if rollbackErr := tracker.RollbackAllFiles(verbose); rollbackErr != nil && verbose { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to rollback files: %v", rollbackErr))) } - return fmt.Errorf("failed to stage workflow files: %w", err) + return 0, "", fmt.Errorf("failed to stage workflow files: %w", err) } // Update .gitattributes and stage it if modified @@ -586,7 +736,7 @@ func addWorkflowsWithPR(workflows []*WorkflowSpec, number int, verbose bool, eng if rollbackErr := tracker.RollbackAllFiles(verbose); rollbackErr != nil && verbose { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to rollback files: %v", rollbackErr))) } - return fmt.Errorf("failed to commit files: %w", err) + return 0, "", fmt.Errorf("failed to commit files: %w", err) } // Push branch @@ -594,34 +744,31 @@ func addWorkflowsWithPR(workflows []*WorkflowSpec, number int, verbose bool, eng if rollbackErr := tracker.RollbackAllFiles(verbose); rollbackErr != nil && verbose { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to rollback files: %v", rollbackErr))) } - return fmt.Errorf("failed to push branch %s: %w", branchName, err) + return 0, "", fmt.Errorf("failed to push branch %s: %w", branchName, err) } // Create PR - if err := createPR(branchName, prTitle, prBody, verbose); err != nil { + prNumber, prURL, err := createPR(branchName, prTitle, prBody, verbose) + if err != nil { if rollbackErr := tracker.RollbackAllFiles(verbose); rollbackErr != nil && verbose { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to rollback files: %v", rollbackErr))) } - return fmt.Errorf("failed to create PR: %w", err) + return 0, "", fmt.Errorf("failed to create PR: %w", err) } // Success - no rollback needed // Switch back to original branch if err := switchBranch(currentBranch, verbose); err != nil { - return fmt.Errorf("failed to switch back to branch %s: %w", currentBranch, err) + return prNumber, prURL, fmt.Errorf("failed to switch back to branch %s: %w", currentBranch, err) } - if len(workflows) == 1 { - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Created PR for workflow: %s", workflows[0].WorkflowName))) - } else { - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Created PR for workflows: %s", joinedNames))) - } - return nil + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Created pull request %s", prURL))) + return prNumber, prURL, nil } // addWorkflowWithTracking adds a workflow from components to .github/workflows with file tracking -func addWorkflowWithTracking(workflow *WorkflowSpec, number int, verbose bool, engineOverride string, name string, force bool, appendText string, tracker *FileTracker, fromWildcard bool, workflowDir string, noStopAfter bool, stopAfter string) error { +func addWorkflowWithTracking(workflow *WorkflowSpec, number int, verbose bool, quiet bool, engineOverride string, name string, force bool, appendText string, tracker *FileTracker, fromWildcard bool, workflowDir string, noStopAfter bool, stopAfter string) error { if verbose { fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Adding workflow: %s", workflow.String()))) fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Number of copies: %d", number))) @@ -848,23 +995,26 @@ func addWorkflowWithTracking(workflow *WorkflowSpec, number int, verbose bool, e return fmt.Errorf("failed to write destination file '%s': %w", destFile, err) } - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Added workflow: %s", destFile))) + // Show detailed output only when not in quiet mode + if !quiet { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Added workflow: %s", destFile))) - // Extract and display description if present - if description := ExtractWorkflowDescription(content); description != "" { - fmt.Fprintln(os.Stderr, "") - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(description)) - fmt.Fprintln(os.Stderr, "") + // Extract and display description if present + if description := ExtractWorkflowDescription(content); description != "" { + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(description)) + fmt.Fprintln(os.Stderr, "") + } } // Try to compile the workflow and track generated files if tracker != nil { - if err := compileWorkflowWithTracking(destFile, verbose, engineOverride, tracker); err != nil { + if err := compileWorkflowWithTracking(destFile, verbose, quiet, engineOverride, tracker); err != nil { fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error())) } } else { // Fall back to basic compilation without tracking - if err := compileWorkflow(destFile, verbose, engineOverride); err != nil { + if err := compileWorkflow(destFile, verbose, quiet, engineOverride); err != nil { fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error())) } } @@ -894,14 +1044,15 @@ func updateWorkflowTitle(content string, number int) string { return strings.Join(lines, "\n") } -func compileWorkflow(filePath string, verbose bool, engineOverride string) error { - return compileWorkflowWithRefresh(filePath, verbose, engineOverride, false) +func compileWorkflow(filePath string, verbose bool, quiet bool, engineOverride string) error { + return compileWorkflowWithRefresh(filePath, verbose, quiet, engineOverride, false) } -func compileWorkflowWithRefresh(filePath string, verbose bool, engineOverride string, refreshStopTime bool) error { +func compileWorkflowWithRefresh(filePath string, verbose bool, quiet bool, engineOverride string, refreshStopTime bool) error { // Create compiler and compile the workflow compiler := workflow.NewCompiler(verbose, engineOverride, GetVersion()) compiler.SetRefreshStopTime(refreshStopTime) + compiler.SetQuiet(quiet) if err := CompileWorkflowWithValidation(compiler, filePath, verbose, false, false, false, false, false); err != nil { return err } @@ -920,11 +1071,11 @@ func compileWorkflowWithRefresh(filePath string, verbose bool, engineOverride st } // compileWorkflowWithTracking compiles a workflow and tracks generated files -func compileWorkflowWithTracking(filePath string, verbose bool, engineOverride string, tracker *FileTracker) error { - return compileWorkflowWithTrackingAndRefresh(filePath, verbose, engineOverride, tracker, false) +func compileWorkflowWithTracking(filePath string, verbose bool, quiet bool, engineOverride string, tracker *FileTracker) error { + return compileWorkflowWithTrackingAndRefresh(filePath, verbose, quiet, engineOverride, tracker, false) } -func compileWorkflowWithTrackingAndRefresh(filePath string, verbose bool, engineOverride string, tracker *FileTracker, refreshStopTime bool) error { +func compileWorkflowWithTrackingAndRefresh(filePath string, verbose bool, quiet bool, engineOverride string, tracker *FileTracker, refreshStopTime bool) error { // Generate the expected lock file path lockFile := stringutil.MarkdownToLockFile(filePath) @@ -963,6 +1114,7 @@ func compileWorkflowWithTrackingAndRefresh(filePath string, verbose bool, engine compiler := workflow.NewCompiler(verbose, engineOverride, GetVersion()) compiler.SetFileTracker(tracker) compiler.SetRefreshStopTime(refreshStopTime) + compiler.SetQuiet(quiet) if err := CompileWorkflowWithValidation(compiler, filePath, verbose, false, false, false, false, false); err != nil { return err } @@ -1020,3 +1172,62 @@ func expandWildcardWorkflows(specs []*WorkflowSpec, verbose bool) ([]*WorkflowSp return expandedWorkflows, nil } + +// checkWorkflowsHaveDispatch checks if any of the workflows have a workflow_dispatch trigger +func checkWorkflowsHaveDispatch(workflows []*WorkflowSpec, verbose bool) bool { + for _, spec := range workflows { + if checkWorkflowHasDispatch(spec, verbose) { + return true + } + } + return false +} + +// checkWorkflowHasDispatch checks if a single workflow has a workflow_dispatch trigger +func checkWorkflowHasDispatch(spec *WorkflowSpec, verbose bool) bool { + addLog.Printf("Checking if workflow %s has workflow_dispatch trigger", spec.WorkflowName) + + // Find and read the workflow content + sourceContent, _, err := findWorkflowInPackageForRepo(spec, verbose) + if err != nil { + addLog.Printf("Could not fetch workflow content: %v", err) + return false + } + + // Parse frontmatter to check on: triggers + result, err := parser.ExtractFrontmatterFromContent(string(sourceContent)) + if err != nil { + addLog.Printf("Could not parse workflow frontmatter: %v", err) + return false + } + + // Check if 'on' section exists and contains workflow_dispatch + onSection, exists := result.Frontmatter["on"] + if !exists { + addLog.Print("No 'on' section found in workflow") + return false + } + + // Handle different on: formats + switch on := onSection.(type) { + case map[string]any: + _, hasDispatch := on["workflow_dispatch"] + addLog.Printf("workflow_dispatch in on map: %v", hasDispatch) + return hasDispatch + case string: + hasDispatch := strings.Contains(strings.ToLower(on), "workflow_dispatch") + addLog.Printf("workflow_dispatch in on string: %v", hasDispatch) + return hasDispatch + case []any: + for _, item := range on { + if str, ok := item.(string); ok && strings.ToLower(str) == "workflow_dispatch" { + addLog.Print("workflow_dispatch found in on array") + return true + } + } + return false + default: + addLog.Printf("Unknown on: section type: %T", onSection) + return false + } +} diff --git a/pkg/cli/add_command_test.go b/pkg/cli/add_command_test.go index c6a6a69277..07c7d6313b 100644 --- a/pkg/cli/add_command_test.go +++ b/pkg/cli/add_command_test.go @@ -82,7 +82,7 @@ func TestNewAddCommand(t *testing.T) { } func TestAddWorkflows_EmptyWorkflows(t *testing.T) { - err := AddWorkflows([]string{}, 1, false, "", "", false, "", false, false, false, "", false, "") + _, err := AddWorkflows([]string{}, 1, false, "", "", false, "", false, false, false, "", false, "") require.Error(t, err, "Should error when no workflows are provided") assert.Contains(t, err.Error(), "at least one workflow", "Error should mention missing workflow") } diff --git a/pkg/cli/add_current_repo_test.go b/pkg/cli/add_current_repo_test.go index a77259fafa..ec53f83e5a 100644 --- a/pkg/cli/add_current_repo_test.go +++ b/pkg/cli/add_current_repo_test.go @@ -78,7 +78,7 @@ func TestAddWorkflowsFromCurrentRepository(t *testing.T) { // Clear cache before each test ClearCurrentRepoSlugCache() - err := AddWorkflows(tt.workflowSpecs, 1, false, "", "", false, "", false, false, false, "", false, "") + _, err := AddWorkflows(tt.workflowSpecs, 1, false, "", "", false, "", false, false, false, "", false, "") if tt.expectError { if err == nil { @@ -179,7 +179,7 @@ func TestAddWorkflowsFromCurrentRepositoryMultiple(t *testing.T) { // Clear cache before each test ClearCurrentRepoSlugCache() - err := AddWorkflows(tt.workflowSpecs, 1, false, "", "", false, "", false, false, false, "", false, "") + _, err := AddWorkflows(tt.workflowSpecs, 1, false, "", "", false, "", false, false, false, "", false, "") if tt.expectError { if err == nil { @@ -220,7 +220,7 @@ func TestAddWorkflowsFromCurrentRepositoryNotInGitRepo(t *testing.T) { // When not in a git repo, the check should be skipped (can't determine current repo) // The function should proceed and fail for other reasons (e.g., workflow not found) - err = AddWorkflows([]string{"some-owner/some-repo/workflow"}, 1, false, "", "", false, "", false, false, false, "", false, "") + _, err = AddWorkflows([]string{"some-owner/some-repo/workflow"}, 1, false, "", "", false, "", false, false, false, "", false, "") // Should NOT get the "cannot add workflows from the current repository" error if err != nil && strings.Contains(err.Error(), "cannot add workflows from the current repository") { diff --git a/pkg/cli/add_gitattributes_test.go b/pkg/cli/add_gitattributes_test.go index 0b2f6f6f20..ab89ac8014 100644 --- a/pkg/cli/add_gitattributes_test.go +++ b/pkg/cli/add_gitattributes_test.go @@ -82,8 +82,7 @@ This is a test workflow.` os.Remove(".gitattributes") // Call addWorkflowsNormal with noGitattributes=false - // Signature: addWorkflowsNormal(workflows, number, verbose, engineOverride, name, force, appendText, push, noGitattributes, fromWildcard, workflowDir, noStopAfter, stopAfter) - err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", false, false, false, "", false, "") + err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, false, "", "", false, "", false, false, false, "", false, "") if err != nil { // We expect this to fail because we don't have a full workflow setup, // but gitattributes should still be updated before the error @@ -113,8 +112,7 @@ This is a test workflow.` os.Remove(".gitattributes") // Call addWorkflowsNormal with noGitattributes=true - // Signature: addWorkflowsNormal(workflows, number, verbose, engineOverride, name, force, appendText, push, noGitattributes, fromWildcard, workflowDir, noStopAfter, stopAfter) - err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", false, true, false, "", false, "") + err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, false, "", "", false, "", false, true, false, "", false, "") if err != nil { // We expect this to fail because we don't have a full workflow setup t.Logf("Expected error during workflow addition: %v", err) @@ -136,8 +134,7 @@ This is a test workflow.` } // Call addWorkflowsNormal with noGitattributes=true - // Signature: addWorkflowsNormal(workflows, number, verbose, engineOverride, name, force, appendText, push, noGitattributes, fromWildcard, workflowDir, noStopAfter, stopAfter) - err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", false, true, false, "", false, "") + err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, false, "", "", false, "", false, true, false, "", false, "") if err != nil { // We expect this to fail because we don't have a full workflow setup t.Logf("Expected error during workflow addition: %v", err) diff --git a/pkg/cli/add_interactive.go b/pkg/cli/add_interactive.go new file mode 100644 index 0000000000..5302662631 --- /dev/null +++ b/pkg/cli/add_interactive.go @@ -0,0 +1,1037 @@ +package cli + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + "time" + + "github.com/charmbracelet/huh" + "github.com/githubnext/gh-aw/pkg/console" + "github.com/githubnext/gh-aw/pkg/constants" + "github.com/githubnext/gh-aw/pkg/logger" + "github.com/githubnext/gh-aw/pkg/parser" + "github.com/githubnext/gh-aw/pkg/workflow" +) + +var addInteractiveLog = logger.New("cli:add_interactive") + +// AddInteractiveConfig holds configuration for interactive add mode +type AddInteractiveConfig struct { + WorkflowSpecs []string + Verbose bool + EngineOverride string + NoGitattributes bool + WorkflowDir string + NoStopAfter bool + StopAfter string + SkipWorkflowRun bool + RepoOverride string // owner/repo format, if user provides it + + // isPublicRepo tracks whether the target repository is public + // This is populated by checkGitRepository() when determining the repo + isPublicRepo bool + + // existingSecrets tracks which secrets already exist in the repository + // This is populated by checkExistingSecrets() before engine selection + existingSecrets map[string]bool + + // addResult holds the result from AddWorkflows, including HasWorkflowDispatch + addResult *AddWorkflowsResult + + // resolvedWorkflows holds the pre-resolved workflow data including descriptions + // This is populated early in the flow by resolveWorkflows() + resolvedWorkflows *ResolvedWorkflows +} + +// RunAddInteractive runs the interactive add workflow +// This walks the user through adding an agentic workflow to their repository +func RunAddInteractive(ctx context.Context, workflowSpecs []string, verbose bool, engineOverride string, noGitattributes bool, workflowDir string, noStopAfter bool, stopAfter string) error { + addInteractiveLog.Print("Starting interactive add workflow") + + // Assert this function is not running in automated unit tests or CI + if os.Getenv("GO_TEST_MODE") == "true" || os.Getenv("CI") != "" { + return fmt.Errorf("interactive add cannot be used in automated tests or CI environments") + } + + config := &AddInteractiveConfig{ + WorkflowSpecs: workflowSpecs, + Verbose: verbose, + EngineOverride: engineOverride, + NoGitattributes: noGitattributes, + WorkflowDir: workflowDir, + NoStopAfter: noStopAfter, + StopAfter: stopAfter, + } + + // Clear the screen for a fresh interactive experience + fmt.Fprint(os.Stderr, "\033[H\033[2J") + + // Step 1: Welcome message + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "πŸš€ Welcome to GitHub Agentic Workflows!") + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "This tool will walk you through adding an automated workflow to your repository.") + fmt.Fprintln(os.Stderr, "") + + // Step 1b: Resolve workflows early to get descriptions and validate specs + if err := config.resolveWorkflows(); err != nil { + return err + } + + // Step 1c: Show workflow descriptions if available + config.showWorkflowDescriptions() + + // Step 2: Check gh auth status + if err := config.checkGHAuthStatus(); err != nil { + return err + } + + // Step 3: Check git repository and get org/repo + if err := config.checkGitRepository(); err != nil { + return err + } + + // Step 4: Check GitHub Actions is enabled + if err := config.checkActionsEnabled(); err != nil { + return err + } + + // Step 5: Check user permissions + if err := config.checkUserPermissions(); err != nil { + return err + } + + // Step 6: Select coding agent and collect API key + if err := config.selectAIEngineAndKey(); err != nil { + return err + } + + // Step 7: Determine files to add + filesToAdd, initFiles, err := config.determineFilesToAdd() + if err != nil { + return err + } + + // Step 8: Confirm with user + secretName, secretValue, err := config.getSecretInfo() + if err != nil { + return err + } + + if err := config.confirmChanges(filesToAdd, initFiles, secretName, secretValue); err != nil { + return err + } + + // Step 9: Apply changes (create PR, merge, add secret) + if err := config.applyChanges(ctx, filesToAdd, initFiles, secretName, secretValue); err != nil { + return err + } + + // Step 10: Check status and offer to run + if err := config.checkStatusAndOfferRun(ctx); err != nil { + return err + } + + return nil +} + +// resolveWorkflows resolves workflow specifications by installing repositories, +// expanding wildcards, and fetching workflow content (including descriptions). +// This is called early to show workflow information before the user commits to adding them. +func (c *AddInteractiveConfig) resolveWorkflows() error { + addInteractiveLog.Print("Resolving workflows early for description display") + + resolved, err := ResolveWorkflows(c.WorkflowSpecs, c.Verbose) + if err != nil { + return fmt.Errorf("failed to resolve workflows: %w", err) + } + + c.resolvedWorkflows = resolved + return nil +} + +// showWorkflowDescriptions displays the descriptions of resolved workflows +func (c *AddInteractiveConfig) showWorkflowDescriptions() { + if c.resolvedWorkflows == nil || len(c.resolvedWorkflows.Workflows) == 0 { + return + } + + // Show descriptions for all workflows that have one + for _, rw := range c.resolvedWorkflows.Workflows { + if rw.Description != "" { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(rw.Description)) + fmt.Fprintln(os.Stderr, "") + } + } +} + +// checkGHAuthStatus verifies the user is logged in to GitHub CLI +func (c *AddInteractiveConfig) checkGHAuthStatus() error { + addInteractiveLog.Print("Checking GitHub CLI authentication status") + + cmd := exec.Command("gh", "auth", "status") + output, err := cmd.CombinedOutput() + + if err != nil { + fmt.Fprintln(os.Stderr, console.FormatErrorMessage("You are not logged in to GitHub CLI.")) + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "Please run the following command to authenticate:") + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatCommandMessage(" gh auth login")) + fmt.Fprintln(os.Stderr, "") + return fmt.Errorf("not authenticated with GitHub CLI") + } + + if c.Verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("GitHub CLI authenticated")) + addInteractiveLog.Printf("gh auth status output: %s", string(output)) + } + + return nil +} + +// checkGitRepository verifies we're in a git repo and gets org/repo info +func (c *AddInteractiveConfig) checkGitRepository() error { + addInteractiveLog.Print("Checking git repository status") + + // Check if we're in a git repository + if !isGitRepo() { + fmt.Fprintln(os.Stderr, console.FormatErrorMessage("Not in a git repository.")) + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "Please navigate to a git repository or initialize one with:") + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatCommandMessage(" git init")) + fmt.Fprintln(os.Stderr, "") + return fmt.Errorf("not in a git repository") + } + + // Try to get the repository slug + repoSlug, err := GetCurrentRepoSlug() + if err != nil { + addInteractiveLog.Printf("Could not determine repository automatically: %v", err) + + // Ask the user for the repository + fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Could not determine the repository automatically.")) + fmt.Fprintln(os.Stderr, "") + + var userRepo string + form := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Enter the target repository (owner/repo):"). + Description("For example: myorg/myrepo"). + Value(&userRepo). + Validate(func(s string) error { + parts := strings.Split(s, "/") + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return fmt.Errorf("please enter in format 'owner/repo'") + } + return nil + }), + ), + ).WithAccessible(console.IsAccessibleMode()) + + if err := form.Run(); err != nil { + return fmt.Errorf("failed to get repository info: %w", err) + } + + c.RepoOverride = userRepo + repoSlug = userRepo + } else { + c.RepoOverride = repoSlug + } + + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Target repository: %s", repoSlug))) + addInteractiveLog.Printf("Target repository: %s", repoSlug) + + // Check if repository is public or private + c.isPublicRepo = c.checkRepoVisibility() + + return nil +} + +// checkRepoVisibility checks if the repository is public or private +func (c *AddInteractiveConfig) checkRepoVisibility() bool { + addInteractiveLog.Print("Checking repository visibility") + + // Use gh api to check repository visibility + args := []string{"api", fmt.Sprintf("/repos/%s", c.RepoOverride), "--jq", ".visibility"} + cmd := workflow.ExecGH(args...) + output, err := cmd.Output() + if err != nil { + addInteractiveLog.Printf("Could not check repository visibility: %v", err) + // Default to public if we can't determine + return true + } + + visibility := strings.TrimSpace(string(output)) + isPublic := visibility == "public" + addInteractiveLog.Printf("Repository visibility: %s (isPublic=%v)", visibility, isPublic) + return isPublic +} + +// checkActionsEnabled verifies that GitHub Actions is enabled for the repository +func (c *AddInteractiveConfig) checkActionsEnabled() error { + addInteractiveLog.Print("Checking if GitHub Actions is enabled") + + // Use gh api to check Actions permissions + args := []string{"api", fmt.Sprintf("/repos/%s/actions/permissions", c.RepoOverride), "--jq", ".enabled"} + cmd := workflow.ExecGH(args...) + output, err := cmd.Output() + if err != nil { + addInteractiveLog.Printf("Failed to check Actions status: %v", err) + // If we can't check, warn but continue - actual operations will fail if Actions is disabled + fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Could not verify GitHub Actions status. Proceeding anyway...")) + return nil + } + + enabled := strings.TrimSpace(string(output)) + if enabled != "true" { + fmt.Fprintln(os.Stderr, console.FormatErrorMessage("GitHub Actions is disabled for this repository.")) + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "To enable GitHub Actions:") + fmt.Fprintln(os.Stderr, " 1. Go to your repository on GitHub") + fmt.Fprintln(os.Stderr, " 2. Navigate to Settings β†’ Actions β†’ General") + fmt.Fprintln(os.Stderr, " 3. Under 'Actions permissions', select 'Allow all actions and reusable workflows'") + fmt.Fprintln(os.Stderr, " 4. Click 'Save'") + fmt.Fprintln(os.Stderr, "") + return fmt.Errorf("GitHub Actions is not enabled for this repository") + } + + if c.Verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("GitHub Actions is enabled")) + } + + return nil +} + +// checkUserPermissions verifies the user has write/admin access +func (c *AddInteractiveConfig) checkUserPermissions() error { + addInteractiveLog.Print("Checking user permissions") + + parts := strings.Split(c.RepoOverride, "/") + if len(parts) != 2 { + return fmt.Errorf("invalid repository format: %s", c.RepoOverride) + } + owner, repo := parts[0], parts[1] + + hasAccess, err := checkRepositoryAccess(owner, repo) + if err != nil { + addInteractiveLog.Printf("Failed to check repository access: %v", err) + // If we can't check, warn but continue - actual operations will fail if no access + fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Could not verify repository permissions. Proceeding anyway...")) + return nil + } + + if !hasAccess { + fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("You do not have write access to %s/%s.", owner, repo))) + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "You need to be a maintainer, admin, or have write permissions on this repository.") + fmt.Fprintln(os.Stderr, "Please contact the repository owner or request access.") + fmt.Fprintln(os.Stderr, "") + return fmt.Errorf("insufficient repository permissions") + } + + if c.Verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Repository permissions verified")) + } + + return nil +} + +// checkExistingSecrets fetches which secrets already exist in the repository +func (c *AddInteractiveConfig) checkExistingSecrets() error { + addInteractiveLog.Print("Checking existing repository secrets") + + c.existingSecrets = make(map[string]bool) + + // Use gh api to list repository secrets + args := []string{"api", fmt.Sprintf("/repos/%s/actions/secrets", c.RepoOverride), "--jq", ".secrets[].name"} + cmd := workflow.ExecGH(args...) + output, err := cmd.Output() + if err != nil { + addInteractiveLog.Printf("Could not fetch existing secrets: %v", err) + // Continue without error - we'll just assume no secrets exist + return nil + } + + // Parse the output - each secret name is on its own line + secretNames := strings.Split(strings.TrimSpace(string(output)), "\n") + for _, name := range secretNames { + name = strings.TrimSpace(name) + if name != "" { + c.existingSecrets[name] = true + addInteractiveLog.Printf("Found existing secret: %s", name) + } + } + + if c.Verbose && len(c.existingSecrets) > 0 { + fmt.Fprintf(os.Stderr, "Found %d existing repository secret(s)\n", len(c.existingSecrets)) + } + + return nil +} + +// selectAIEngineAndKey prompts the user to select an AI engine and provide API key +func (c *AddInteractiveConfig) selectAIEngineAndKey() error { + addInteractiveLog.Print("Starting coding agent selection") + + // First, check which secrets already exist in the repository + if err := c.checkExistingSecrets(); err != nil { + return err + } + + // Determine default engine based on workflow preference, existing secrets, then environment + defaultEngine := string(constants.CopilotEngine) + existingSecretNote := "" + + // If engine is explicitly overridden via flag, use that + if c.EngineOverride != "" { + defaultEngine = c.EngineOverride + } else { + // Priority 0: Check if workflow specifies a preferred engine in frontmatter + if c.resolvedWorkflows != nil && len(c.resolvedWorkflows.Workflows) > 0 { + for _, wf := range c.resolvedWorkflows.Workflows { + if wf.Engine != "" { + defaultEngine = wf.Engine + addInteractiveLog.Printf("Using engine from workflow frontmatter: %s", wf.Engine) + break + } + } + } + } + + // Only check secrets/environment if we haven't already set a preference + workflowHasPreference := c.resolvedWorkflows != nil && len(c.resolvedWorkflows.Workflows) > 0 && c.resolvedWorkflows.Workflows[0].Engine != "" + if c.EngineOverride == "" && !workflowHasPreference { + // Priority 1: Check existing repository secrets using EngineOptions + for _, opt := range constants.EngineOptions { + if c.existingSecrets[opt.SecretName] { + defaultEngine = opt.Value + existingSecretNote = fmt.Sprintf(" (existing %s secret will be used)", opt.SecretName) + break + } + } + + // Priority 2: Check environment variables if no existing secret found + if existingSecretNote == "" { + for _, opt := range constants.EngineOptions { + envVar := opt.SecretName + if opt.EnvVarName != "" { + envVar = opt.EnvVarName + } + if os.Getenv(envVar) != "" { + defaultEngine = opt.Value + break + } + } + // Priority 3: Check if user likely has Copilot (default) + if token, err := parser.GetGitHubToken(); err == nil && token != "" { + defaultEngine = string(constants.CopilotEngine) + } + } + } + + // If engine is already overridden, skip selection + if c.EngineOverride != "" { + fmt.Fprintf(os.Stderr, "Using coding agent: %s\n", c.EngineOverride) + return c.collectAPIKey(c.EngineOverride) + } + + // Build engine options with notes about existing secrets + var engineOptions []huh.Option[string] + for _, opt := range constants.EngineOptions { + label := fmt.Sprintf("%s - %s", opt.Label, opt.Description) + if c.existingSecrets[opt.SecretName] { + label += " [secret exists]" + } + engineOptions = append(engineOptions, huh.NewOption(label, opt.Value)) + } + + var selectedEngine string + + // Set the default selection by moving it to front + for i, opt := range engineOptions { + if opt.Value == defaultEngine { + if i > 0 { + engineOptions[0], engineOptions[i] = engineOptions[i], engineOptions[0] + } + break + } + } + + fmt.Fprintln(os.Stderr, "") + form := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Which coding agent would you like to use?"). + Description("This determines which coding agent processes your workflows"). + Options(engineOptions...). + Value(&selectedEngine), + ), + ).WithAccessible(console.IsAccessibleMode()) + + if err := form.Run(); err != nil { + return fmt.Errorf("failed to select coding agent: %w", err) + } + + c.EngineOverride = selectedEngine + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Selected engine: %s", selectedEngine))) + + return c.collectAPIKey(selectedEngine) +} + +// collectAPIKey collects the API key for the selected engine +func (c *AddInteractiveConfig) collectAPIKey(engine string) error { + addInteractiveLog.Printf("Collecting API key for engine: %s", engine) + + // Copilot requires special handling with PAT creation instructions + if engine == "copilot" { + return c.collectCopilotPAT() + } + + // All other engines use the generic API key collection + opt := constants.GetEngineOption(engine) + if opt == nil { + return fmt.Errorf("unknown engine: %s", engine) + } + + return c.collectGenericAPIKey(opt) +} + +// collectCopilotPAT walks the user through creating a Copilot PAT +func (c *AddInteractiveConfig) collectCopilotPAT() error { + addInteractiveLog.Print("Collecting Copilot PAT") + + // Check if secret already exists in the repository + if c.existingSecrets["COPILOT_GITHUB_TOKEN"] { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Using existing COPILOT_GITHUB_TOKEN secret in repository")) + return nil + } + + // Check if COPILOT_GITHUB_TOKEN is already in environment + existingToken := os.Getenv("COPILOT_GITHUB_TOKEN") + if existingToken != "" { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Found COPILOT_GITHUB_TOKEN in environment")) + return nil + } + + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "GitHub Copilot requires a Personal Access Token (PAT) with Copilot permissions.") + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "Please create a token at:") + fmt.Fprintln(os.Stderr, console.FormatCommandMessage(" https://github.com/settings/personal-access-tokens/new")) + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "Configure the token with:") + fmt.Fprintln(os.Stderr, " β€’ Token name: Agentic Workflows Copilot") + fmt.Fprintln(os.Stderr, " β€’ Expiration: 90 days (recommended for testing)") + fmt.Fprintln(os.Stderr, " β€’ Resource owner: Your personal account") + if c.isPublicRepo { + fmt.Fprintln(os.Stderr, " β€’ Repository access: \"Public repositories\"") + } else { + fmt.Fprintf(os.Stderr, " β€’ Repository access: \"Only select repositories\" β†’ select %s\n", c.RepoOverride) + } + fmt.Fprintln(os.Stderr, " β€’ Account permissions β†’ Copilot Requests: Read") + fmt.Fprintln(os.Stderr, "") + + var token string + form := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("After creating, please paste your Copilot PAT:"). + Description("The token will be stored securely as a repository secret"). + EchoMode(huh.EchoModePassword). + Value(&token). + Validate(func(s string) error { + if len(s) < 10 { + return fmt.Errorf("token appears to be too short") + } + return nil + }), + ), + ).WithAccessible(console.IsAccessibleMode()) + + if err := form.Run(); err != nil { + return fmt.Errorf("failed to get Copilot token: %w", err) + } + + // Store in environment for later use + os.Setenv("COPILOT_GITHUB_TOKEN", token) + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Copilot token received")) + + return nil +} + +// collectGenericAPIKey collects an API key for engines that use a simple key-based authentication +func (c *AddInteractiveConfig) collectGenericAPIKey(opt *constants.EngineOption) error { + addInteractiveLog.Printf("Collecting API key for %s", opt.Label) + + // Check if secret already exists in the repository + if c.existingSecrets[opt.SecretName] { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Using existing %s secret in repository", opt.SecretName))) + return nil + } + + // Check if key is already in environment + envVar := opt.SecretName + if opt.EnvVarName != "" { + envVar = opt.EnvVarName + } + existingKey := os.Getenv(envVar) + if existingKey != "" { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Found %s in environment", envVar))) + return nil + } + + fmt.Fprintln(os.Stderr, "") + fmt.Fprintf(os.Stderr, "%s requires an API key.\n", opt.Label) + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "Get your API key from:") + fmt.Fprintln(os.Stderr, console.FormatCommandMessage(fmt.Sprintf(" %s", opt.KeyURL))) + fmt.Fprintln(os.Stderr, "") + + var apiKey string + form := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title(fmt.Sprintf("Paste your %s API key:", opt.Label)). + Description("The key will be stored securely as a repository secret"). + EchoMode(huh.EchoModePassword). + Value(&apiKey). + Validate(func(s string) error { + if len(s) < 10 { + return fmt.Errorf("API key appears to be too short") + } + return nil + }), + ), + ).WithAccessible(console.IsAccessibleMode()) + + if err := form.Run(); err != nil { + return fmt.Errorf("failed to get %s API key: %w", opt.Label, err) + } + + // Store in environment for later use + os.Setenv(opt.SecretName, apiKey) + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("%s API key received", opt.Label))) + + return nil +} + +// determineFilesToAdd determines which files will be added +func (c *AddInteractiveConfig) determineFilesToAdd() (workflowFiles []string, initFiles []string, err error) { + addInteractiveLog.Print("Determining files to add") + + // Parse the workflow specs to get the files that will be added + // This reuses logic from addWorkflowsNormal to determine what files get created + for _, spec := range c.WorkflowSpecs { + parsed, parseErr := parseWorkflowSpec(spec) + if parseErr != nil { + return nil, nil, fmt.Errorf("invalid workflow specification '%s': %w", spec, parseErr) + } + workflowFiles = append(workflowFiles, parsed.WorkflowName+".md") + workflowFiles = append(workflowFiles, parsed.WorkflowName+".lock.yml") + } + + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "The following workflow files will be added:") + for _, f := range workflowFiles { + fmt.Fprintf(os.Stderr, " β€’ .github/workflows/%s\n", f) + } + + return workflowFiles, initFiles, nil +} + +// getSecretInfo returns the secret name and value based on the selected engine +// Returns empty value if the secret already exists in the repository +func (c *AddInteractiveConfig) getSecretInfo() (name string, value string, err error) { + addInteractiveLog.Printf("Getting secret info for engine: %s", c.EngineOverride) + + opt := constants.GetEngineOption(c.EngineOverride) + if opt == nil { + return "", "", fmt.Errorf("unknown engine: %s", c.EngineOverride) + } + + name = opt.SecretName + + // If secret already exists in repo, we don't need a value + if c.existingSecrets[name] { + addInteractiveLog.Printf("Secret %s already exists in repository", name) + return name, "", nil + } + + // Get value from environment variable (use EnvVarName if specified, otherwise SecretName) + envVar := opt.SecretName + if opt.EnvVarName != "" { + envVar = opt.EnvVarName + } + value = os.Getenv(envVar) + + if value == "" { + return "", "", fmt.Errorf("API key not found for engine %s", c.EngineOverride) + } + + return name, value, nil +} + +// confirmChanges asks the user to confirm the changes +// secretValue is empty if the secret already exists in the repository +func (c *AddInteractiveConfig) confirmChanges(workflowFiles, initFiles []string, secretName string, secretValue string) error { + addInteractiveLog.Print("Confirming changes with user") + + fmt.Fprintln(os.Stderr, "") + + confirmed := true // Default to yes + form := huh.NewForm( + huh.NewGroup( + huh.NewConfirm(). + Title("Do you want to proceed with these changes?"). + Description("A pull request will be created and merged automatically"). + Affirmative("Yes, create and merge"). + Negative("No, cancel"). + Value(&confirmed), + ), + ).WithAccessible(console.IsAccessibleMode()) + + if err := form.Run(); err != nil { + return fmt.Errorf("confirmation failed: %w", err) + } + + if !confirmed { + fmt.Fprintln(os.Stderr, "Operation cancelled.") + return fmt.Errorf("user cancelled the operation") + } + + return nil +} + +// applyChanges creates the PR, merges it, and adds the secret +func (c *AddInteractiveConfig) applyChanges(ctx context.Context, workflowFiles, initFiles []string, secretName, secretValue string) error { + addInteractiveLog.Print("Applying changes") + + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatProgressMessage("Creating pull request...")) + + // Add the workflow using existing implementation with --create-pull-request + // Pass the resolved workflows to avoid re-fetching them + // Pass quiet=true to suppress detailed output (already shown earlier in interactive mode) + // This returns the result including PR number and HasWorkflowDispatch + result, err := AddResolvedWorkflows(c.WorkflowSpecs, c.resolvedWorkflows, 1, c.Verbose, true, c.EngineOverride, "", false, "", true, false, c.NoGitattributes, c.WorkflowDir, c.NoStopAfter, c.StopAfter) + if err != nil { + return fmt.Errorf("failed to add workflow: %w", err) + } + c.addResult = result + + // Step 8b: Auto-merge the PR + fmt.Fprintln(os.Stderr, console.FormatProgressMessage("Merging pull request...")) + + if result.PRNumber == 0 { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Could not determine PR number")) + fmt.Fprintln(os.Stderr, "Please merge the PR manually from the GitHub web interface.") + } else { + if err := c.mergePullRequest(result.PRNumber); err != nil { + // Check if already merged + if strings.Contains(err.Error(), "already merged") || strings.Contains(err.Error(), "MERGED") { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Merged pull request %s", result.PRURL))) + } else { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to merge PR: %v", err))) + fmt.Fprintln(os.Stderr, "Please merge the PR manually from the GitHub web interface.") + } + } else { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Merged pull request %s", result.PRURL))) + } + } + + // Step 8c: Add the secret (skip if already exists in repository) + if secretValue == "" { + // Secret already exists in repo, nothing to do + if c.Verbose { + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Secret '%s' already configured", secretName))) + } + } else { + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatProgressMessage(fmt.Sprintf("Adding secret '%s' to repository...", secretName))) + + if err := c.addRepositorySecret(secretName, secretValue); err != nil { + fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Failed to add secret: %v", err))) + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "Please add the secret manually:") + fmt.Fprintln(os.Stderr, " 1. Go to your repository Settings β†’ Secrets and variables β†’ Actions") + fmt.Fprintf(os.Stderr, " 2. Click 'New repository secret' and add '%s'\n", secretName) + return fmt.Errorf("failed to add secret: %w", err) + } + + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Secret '%s' added", secretName))) + } + + return nil +} + +// mergePullRequest merges the specified PR +func (c *AddInteractiveConfig) mergePullRequest(prNumber int) error { + cmd := workflow.ExecGH("pr", "merge", fmt.Sprintf("%d", prNumber), "--repo", c.RepoOverride, "--merge") + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("merge failed: %w (output: %s)", err, string(output)) + } + return nil +} + +// addRepositorySecret adds a secret to the repository +func (c *AddInteractiveConfig) addRepositorySecret(name, value string) error { + cmd := workflow.ExecGH("secret", "set", name, "--repo", c.RepoOverride, "--body", value) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to set secret: %w (output: %s)", err, string(output)) + } + return nil +} + +// checkStatusAndOfferRun checks if the workflow appears in status and offers to run it +func (c *AddInteractiveConfig) checkStatusAndOfferRun(ctx context.Context) error { + addInteractiveLog.Print("Checking workflow status and offering to run") + + // Wait a moment for GitHub to process the merge + fmt.Fprintln(os.Stderr, "") + + // Use spinner only in non-verbose mode (spinner can't be restarted after stop) + var spinner *console.SpinnerWrapper + if !c.Verbose { + spinner = console.NewSpinner("Waiting for workflow to be available...") + spinner.Start() + } + + // Try a few times to see the workflow in status + var workflowFound bool + for i := 0; i < 5; i++ { + // Wait 2 seconds before each check (including the first) + select { + case <-ctx.Done(): + if spinner != nil { + spinner.Stop() + } + return ctx.Err() + case <-time.After(2 * time.Second): + // Continue with check + } + + // Use the workflow name from the first spec + if len(c.WorkflowSpecs) > 0 { + parsed, _ := parseWorkflowSpec(c.WorkflowSpecs[0]) + if parsed != nil { + if c.Verbose { + fmt.Fprintf(os.Stderr, "Checking workflow status (attempt %d/5) for: %s\n", i+1, parsed.WorkflowName) + } + // Check if workflow is in status + statuses, err := getWorkflowStatuses(parsed.WorkflowName, c.RepoOverride, c.Verbose) + if err != nil { + if c.Verbose { + fmt.Fprintf(os.Stderr, "Status check error: %v\n", err) + } + } else if len(statuses) > 0 { + if c.Verbose { + fmt.Fprintf(os.Stderr, "Found %d workflow(s) matching pattern\n", len(statuses)) + } + workflowFound = true + break + } else if c.Verbose { + fmt.Fprintln(os.Stderr, "No workflows found matching pattern yet") + } + } + } + } + + if spinner != nil { + spinner.Stop() + } + + if !workflowFound { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Could not verify workflow status.")) + fmt.Fprintf(os.Stderr, "You can check status with: %s status\n", string(constants.CLIExtensionPrefix)) + c.showFinalInstructions() + return nil + } + + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Workflow is ready")) + + // Only offer to run if workflow has workflow_dispatch trigger + if c.addResult == nil || !c.addResult.HasWorkflowDispatch { + addInteractiveLog.Print("Workflow does not have workflow_dispatch trigger, skipping run offer") + c.showFinalInstructions() + return nil + } + + // Ask if user wants to run the workflow + fmt.Fprintln(os.Stderr, "") + runNow := true // Default to yes + form := huh.NewForm( + huh.NewGroup( + huh.NewConfirm(). + Title("Would you like to run the workflow once now?"). + Description("This will trigger the workflow immediately"). + Affirmative("Yes, run once now"). + Negative("No, I'll run later"). + Value(&runNow), + ), + ).WithAccessible(console.IsAccessibleMode()) + + if err := form.Run(); err != nil { + return nil // Not critical, just skip + } + + if !runNow { + c.showFinalInstructions() + return nil + } + + // Run the workflow + if len(c.WorkflowSpecs) > 0 { + parsed, _ := parseWorkflowSpec(c.WorkflowSpecs[0]) + if parsed != nil { + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatProgressMessage("Triggering workflow...")) + + if err := RunWorkflowOnGitHub(ctx, parsed.WorkflowName, false, c.EngineOverride, c.RepoOverride, "", false, false, false, true, nil, c.Verbose); err != nil { + fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Failed to run workflow: %v", err))) + c.showFinalInstructions() + return nil + } + + // Get the run URL for step 10 + runInfo, err := getLatestWorkflowRunWithRetry(parsed.WorkflowName+".lock.yml", c.RepoOverride, c.Verbose) + if err == nil && runInfo.URL != "" { + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Workflow triggered successfully!")) + fmt.Fprintln(os.Stderr, "") + fmt.Fprintf(os.Stderr, "πŸ”— View workflow run: %s\n", runInfo.URL) + + // // Step 10: Offer to open in browser + // fmt.Fprintln(os.Stderr, "") + // var openBrowser bool + // browserForm := huh.NewForm( + // huh.NewGroup( + // huh.NewConfirm(). + // Title("Would you like to open the workflow run in your browser?"). + // Affirmative("Yes, open browser"). + // Negative("No, thanks"). + // Value(&openBrowser), + // ), + // ).WithAccessible(console.IsAccessibleMode()) + + // if err := browserForm.Run(); err == nil && openBrowser { + // // Use gh browse to open the URL + // browseCmd := workflow.ExecGH("browse", runInfo.URL) + // if err := browseCmd.Run(); err != nil { + // // Fallback: try to open directly + // addInteractiveLog.Printf("gh browse failed: %v, trying direct open", err) + // if openErr := openURLInBrowser(runInfo.URL); openErr != nil { + // fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Could not open browser automatically.")) + // fmt.Fprintf(os.Stderr, "Please visit: %s\n", runInfo.URL) + // } + // } + // } + } + } + } + + c.showFinalInstructions() + return nil +} + +// getWorkflowStatuses is a helper to get workflow statuses for a pattern +// The pattern is matched against the workflow filename (basename without extension) +func getWorkflowStatuses(pattern, repoOverride string, verbose bool) ([]WorkflowStatus, error) { + // This would normally call StatusWorkflows but we need just a simple check + // For now, we'll use the gh CLI directly + // Request 'path' field so we can match by filename, not by workflow name + args := []string{"workflow", "list", "--json", "name,state,path"} + if repoOverride != "" { + args = append(args, "--repo", repoOverride) + } + + if verbose { + fmt.Fprintf(os.Stderr, "Running: gh %s\n", strings.Join(args, " ")) + } + + cmd := workflow.ExecGH(args...) + output, err := cmd.Output() + if err != nil { + if verbose { + fmt.Fprintf(os.Stderr, "gh workflow list failed: %v\n", err) + } + return nil, err + } + + if verbose { + fmt.Fprintf(os.Stderr, "gh workflow list output: %s\n", string(output)) + fmt.Fprintf(os.Stderr, "Looking for workflow with filename containing: %s\n", pattern) + } + + // Check if any workflow path contains the pattern + // The pattern is the workflow name (e.g., "daily-repo-status") + // The path is like ".github/workflows/daily-repo-status.lock.yml" + // We check if the path contains the pattern + if strings.Contains(string(output), pattern+".lock.yml") || strings.Contains(string(output), pattern+".md") { + if verbose { + fmt.Fprintf(os.Stderr, "Workflow with filename '%s' found in workflow list\n", pattern) + } + return []WorkflowStatus{{Workflow: pattern}}, nil + } + + if verbose { + fmt.Fprintf(os.Stderr, "Workflow with filename '%s' NOT found in workflow list\n", pattern) + } + return nil, nil +} + +// openURLInBrowser opens a URL in the default browser +func openURLInBrowser(url string) error { + var cmd *exec.Cmd + + switch { + case isLinux(): + cmd = exec.Command("xdg-open", url) + case isDarwin(): + cmd = exec.Command("open", url) + default: + // Try gh browse as fallback + return fmt.Errorf("unsupported platform") + } + + return cmd.Start() +} + +// isLinux checks if running on Linux +func isLinux() bool { + return strings.Contains(strings.ToLower(os.Getenv("OSTYPE")), "linux") || + fileExists("/proc/version") +} + +// isDarwin checks if running on macOS +func isDarwin() bool { + return strings.Contains(strings.ToLower(os.Getenv("OSTYPE")), "darwin") +} + +// fileExists checks if a file exists +func fileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +// showFinalInstructions shows final instructions to the user +func (c *AddInteractiveConfig) showFinalInstructions() { + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("πŸŽ‰ Addition complete!")) + fmt.Fprintln(os.Stderr, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "Useful commands:") + fmt.Fprintln(os.Stderr, console.FormatCommandMessage(fmt.Sprintf(" %s status # Check workflow status", string(constants.CLIExtensionPrefix)))) + fmt.Fprintln(os.Stderr, console.FormatCommandMessage(fmt.Sprintf(" %s run # Trigger a workflow", string(constants.CLIExtensionPrefix)))) + fmt.Fprintln(os.Stderr, console.FormatCommandMessage(fmt.Sprintf(" %s logs # View workflow logs", string(constants.CLIExtensionPrefix)))) + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "Learn more at: https://githubnext.github.io/gh-aw/") + fmt.Fprintln(os.Stderr, "") +} diff --git a/pkg/cli/add_wildcard_test.go b/pkg/cli/add_wildcard_test.go index db6c3ab338..b4e9f0d1c2 100644 --- a/pkg/cli/add_wildcard_test.go +++ b/pkg/cli/add_wildcard_test.go @@ -475,7 +475,7 @@ on: push // Test 1: Non-wildcard duplicate should return error t.Run("non_wildcard_duplicate_returns_error", func(t *testing.T) { - err := addWorkflowWithTracking(spec, 1, false, "", "", false, "", nil, false, "", false, "") + err := addWorkflowWithTracking(spec, 1, false, false, "", "", false, "", nil, false, "", false, "") if err == nil { t.Error("Expected error for non-wildcard duplicate, got nil") } @@ -486,7 +486,7 @@ on: push // Test 2: Wildcard duplicate should return nil (skip with warning) t.Run("wildcard_duplicate_returns_nil", func(t *testing.T) { - err := addWorkflowWithTracking(spec, 1, false, "", "", false, "", nil, true, "", false, "") + err := addWorkflowWithTracking(spec, 1, false, false, "", "", false, "", nil, true, "", false, "") if err != nil { t.Errorf("Expected nil for wildcard duplicate (should skip), got error: %v", err) } @@ -494,7 +494,7 @@ on: push // Test 3: Wildcard duplicate with force flag should succeed t.Run("wildcard_duplicate_with_force_succeeds", func(t *testing.T) { - err := addWorkflowWithTracking(spec, 1, false, "", "", true, "", nil, true, "", false, "") + err := addWorkflowWithTracking(spec, 1, false, false, "", "", true, "", nil, true, "", false, "") // This should succeed or return nil if err != nil && strings.Contains(err.Error(), "already exists") { t.Errorf("Expected success with force flag, got 'already exists' error: %v", err) diff --git a/pkg/cli/commands_compile_workflow_test.go b/pkg/cli/commands_compile_workflow_test.go index bb281bbafc..980d4c28d2 100644 --- a/pkg/cli/commands_compile_workflow_test.go +++ b/pkg/cli/commands_compile_workflow_test.go @@ -223,7 +223,7 @@ Test compilation with invalid engine. } // Test compileWorkflow function - err = compileWorkflow(workflowFile, tt.verbose, tt.engineOverride) + err = compileWorkflow(workflowFile, tt.verbose, false, tt.engineOverride) if tt.expectError { if err == nil { diff --git a/pkg/cli/enable.go b/pkg/cli/enable.go index 74c6c06805..2c279d4a99 100644 --- a/pkg/cli/enable.go +++ b/pkg/cli/enable.go @@ -139,7 +139,7 @@ func toggleWorkflowsByNames(workflowNames []string, enable bool, repoOverride st // If enabling and lock file doesn't exist locally, try to compile it if enable { if _, err := os.Stat(lockFile); os.IsNotExist(err) { - if err := compileWorkflow(file, false, ""); err != nil { + if err := compileWorkflow(file, false, false, ""); err != nil { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to compile workflow %s to create lock file: %v", name, err))) // If we can't compile and there's no GitHub entry, skip because we can't address it if !exists { diff --git a/pkg/cli/file_tracker_test.go b/pkg/cli/file_tracker_test.go index a85e565ce3..79e171e7b9 100644 --- a/pkg/cli/file_tracker_test.go +++ b/pkg/cli/file_tracker_test.go @@ -304,7 +304,7 @@ This uses reaction. } // Compile the workflow with tracking - if err := compileWorkflowWithTracking(workflowFileWithReaction, false, "", tracker); err != nil { + if err := compileWorkflowWithTracking(workflowFileWithReaction, false, false, "", tracker); err != nil { t.Fatalf("Failed to compile workflow: %v", err) } @@ -355,7 +355,7 @@ This does NOT use ai-reaction. // (Note: Since reaction is now inline, this removal step is no longer needed) // Compile the workflow with tracking - if err := compileWorkflowWithTracking(workflowFileWithoutReaction, false, "", tracker2); err != nil { + if err := compileWorkflowWithTracking(workflowFileWithoutReaction, false, false, "", tracker2); err != nil { t.Fatalf("Failed to compile workflow: %v", err) } diff --git a/pkg/cli/init.go b/pkg/cli/init.go index 5bc2b3749c..2356f1f877 100644 --- a/pkg/cli/init.go +++ b/pkg/cli/init.go @@ -39,18 +39,9 @@ func InitRepositoryInteractive(verbose bool, rootCmd CommandProvider) error { // Prompt for engine selection var selectedEngine string - engineOptions := []struct { - value string - label string - description string - }{ - {string(constants.CopilotEngine), "GitHub Copilot", "GitHub Copilot CLI with agent support"}, - {string(constants.ClaudeEngine), "Claude", "Anthropic Claude Code coding agent"}, - {string(constants.CodexEngine), "Codex", "OpenAI Codex/GPT engine"}, - } // Use interactive prompt to select engine - form := createEngineSelectionForm(&selectedEngine, engineOptions) + form := createEngineSelectionForm(&selectedEngine, constants.EngineOptions) if err := form.Run(); err != nil { return fmt.Errorf("engine selection failed: %w", err) } @@ -138,22 +129,18 @@ func InitRepositoryInteractive(verbose bool, rootCmd CommandProvider) error { } // createEngineSelectionForm creates an interactive form for engine selection -func createEngineSelectionForm(selectedEngine *string, engineOptions []struct { - value string - label string - description string -}) *huh.Form { +func createEngineSelectionForm(selectedEngine *string, engineOptions []constants.EngineOption) *huh.Form { // Build options for huh.Select var options []huh.Option[string] for _, opt := range engineOptions { - options = append(options, huh.NewOption(fmt.Sprintf("%s - %s", opt.label, opt.description), opt.value)) + options = append(options, huh.NewOption(fmt.Sprintf("%s - %s", opt.Label, opt.Description), opt.Value)) } return huh.NewForm( huh.NewGroup( huh.NewSelect[string](). - Title("Which AI engine would you like to use?"). - Description("Select the AI engine that will power your agentic workflows"). + Title("Which coding agent would you like to use?"). + Description("Select the coding agent that will power your agentic workflows"). Options(options...). Value(selectedEngine), ), @@ -732,7 +719,7 @@ func InitRepository(verbose bool, mcp bool, campaign bool, tokens bool, engine s "- Configuring .gitattributes\n" + "- Creating GitHub Copilot custom instructions\n" + "- Setting up workflow prompts and agents" - if err := createPR(branchName, prTitle, prBody, verbose); err != nil { + if _, _, err := createPR(branchName, prTitle, prBody, verbose); err != nil { // Switch back to original branch before returning error _ = switchBranch(currentBranch, verbose) return fmt.Errorf("failed to create PR: %w", err) diff --git a/pkg/cli/packages.go b/pkg/cli/packages.go index 93970dc9eb..30f81aa35c 100644 --- a/pkg/cli/packages.go +++ b/pkg/cli/packages.go @@ -80,7 +80,7 @@ func InstallPackage(repoSpec string, verbose bool) error { if _, err := os.Stat(targetDir); err == nil { entries, err := os.ReadDir(targetDir) if err == nil && len(entries) > 0 { - fmt.Fprintf(os.Stderr, "Package %s already exists. Updating...\n", spec.RepoSlug) + packagesLog.Printf("Package %s already exists. Updating...\n", spec.RepoSlug) // Remove existing content if err := os.RemoveAll(targetDir); err != nil { return fmt.Errorf("failed to remove existing package: %w", err) @@ -99,7 +99,6 @@ func InstallPackage(repoSpec string, verbose bool) error { } packagesLog.Printf("Successfully installed package: %s", spec.RepoSlug) - fmt.Fprintf(os.Stderr, "Successfully installed package: %s\n", spec.RepoSlug) return nil } @@ -798,6 +797,22 @@ func ExtractWorkflowDescription(content string) string { return "" } +// ExtractWorkflowEngine extracts the engine field from workflow content string +func ExtractWorkflowEngine(content string) string { + result, err := parser.ExtractFrontmatterFromContent(content) + if err != nil { + return "" + } + + if engine, ok := result.Frontmatter["engine"]; ok { + if engineStr, ok := engine.(string); ok { + return engineStr + } + } + + return "" +} + // ExtractWorkflowDescriptionFromFile extracts the description field from a workflow file func ExtractWorkflowDescriptionFromFile(filePath string) string { content, err := os.ReadFile(filePath) diff --git a/pkg/cli/pr_command.go b/pkg/cli/pr_command.go index e4d0ebf31e..3c9216f9f1 100644 --- a/pkg/cli/pr_command.go +++ b/pkg/cli/pr_command.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "strings" "time" @@ -788,8 +789,8 @@ func transferPR(prURL, targetRepo string, verbose bool) error { return nil } -// createPR creates a pull request using GitHub CLI -func createPR(branchName, title, body string, verbose bool) error { +// createPR creates a pull request using GitHub CLI and returns the PR number +func createPR(branchName, title, body string, verbose bool) (int, string, error) { if verbose { fmt.Printf("Creating PR: %s\n", title) } @@ -798,7 +799,7 @@ func createPR(branchName, title, body string, verbose bool) error { cmd := workflow.ExecGH("repo", "view", "--json", "owner,name") repoOutput, err := cmd.Output() if err != nil { - return fmt.Errorf("failed to get current repository info: %w", err) + return 0, "", fmt.Errorf("failed to get current repository info: %w", err) } var repoInfo struct { @@ -809,7 +810,7 @@ func createPR(branchName, title, body string, verbose bool) error { } if err := json.Unmarshal(repoOutput, &repoInfo); err != nil { - return fmt.Errorf("failed to parse repository info: %w", err) + return 0, "", fmt.Errorf("failed to parse repository info: %w", err) } repoSpec := fmt.Sprintf("%s/%s", repoInfo.Owner.Login, repoInfo.Name) @@ -820,13 +821,21 @@ func createPR(branchName, title, body string, verbose bool) error { if err != nil { // Try to get stderr for better error reporting if exitError, ok := err.(*exec.ExitError); ok { - return fmt.Errorf("failed to create PR: %w\nOutput: %s\nError: %s", err, string(output), string(exitError.Stderr)) + return 0, "", fmt.Errorf("failed to create PR: %w\nOutput: %s\nError: %s", err, string(output), string(exitError.Stderr)) } - return fmt.Errorf("failed to create PR: %w", err) + return 0, "", fmt.Errorf("failed to create PR: %w", err) } prURL := strings.TrimSpace(string(output)) - fmt.Printf("πŸ“’ Pull Request created: %s\n", prURL) - return nil + // Parse PR number from URL (e.g., https://github.com/owner/repo/pull/123) + prNumber := 0 + parts := strings.Split(prURL, "/") + if len(parts) > 0 { + if num, parseErr := strconv.Atoi(parts[len(parts)-1]); parseErr == nil { + prNumber = num + } + } + + return prNumber, prURL, nil } diff --git a/pkg/cli/trial_repository.go b/pkg/cli/trial_repository.go index 3c2c73eea8..2ceae1c32b 100644 --- a/pkg/cli/trial_repository.go +++ b/pkg/cli/trial_repository.go @@ -202,7 +202,7 @@ func installWorkflowInTrialMode(tempDir string, parsedSpec *WorkflowSpec, logica } // Add the workflow from the installed package - if err := AddWorkflows([]string{parsedSpec.String()}, 1, verbose, "", "", true, appendText, false, false, false, "", false, ""); err != nil { + if _, err := AddWorkflows([]string{parsedSpec.String()}, 1, verbose, "", "", true, appendText, false, false, false, "", false, ""); err != nil { return fmt.Errorf("failed to add workflow: %w", err) } } diff --git a/pkg/cli/update_command_test.go b/pkg/cli/update_command_test.go index 7dde1aba2f..17560f6252 100644 --- a/pkg/cli/update_command_test.go +++ b/pkg/cli/update_command_test.go @@ -676,7 +676,7 @@ This is a test workflow. // Test with refreshStopTime=false (should preserve existing stop time if lock exists) t.Run("compileWorkflowWithRefresh false", func(t *testing.T) { - err := compileWorkflowWithRefresh(workflowFile, false, "", false) + err := compileWorkflowWithRefresh(workflowFile, false, false, "", false) if err != nil { t.Logf("Compilation failed (expected in test environment): %v", err) // In a test environment without full setup, compilation may fail, @@ -686,7 +686,7 @@ This is a test workflow. // Test with refreshStopTime=true (should regenerate stop time) t.Run("compileWorkflowWithRefresh true", func(t *testing.T) { - err := compileWorkflowWithRefresh(workflowFile, false, "", true) + err := compileWorkflowWithRefresh(workflowFile, false, false, "", true) if err != nil { t.Logf("Compilation failed (expected in test environment): %v", err) // In a test environment without full setup, compilation may fail, diff --git a/pkg/cli/update_workflows.go b/pkg/cli/update_workflows.go index 6c7ab6508f..3556d68c7b 100644 --- a/pkg/cli/update_workflows.go +++ b/pkg/cli/update_workflows.go @@ -454,7 +454,7 @@ func updateWorkflow(wf *workflowWithSource, allowMajor, force, verbose bool, eng // Compile the updated workflow with refreshStopTime enabled updateLog.Printf("Compiling updated workflow: %s", wf.Name) - if err := compileWorkflowWithRefresh(wf.Path, verbose, engineOverride, true); err != nil { + if err := compileWorkflowWithRefresh(wf.Path, verbose, false, engineOverride, true); err != nil { updateLog.Printf("Compilation failed for workflow %s: %v", wf.Name, err) return fmt.Errorf("failed to compile updated workflow: %w", err) } diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 7208101c73..02bd881dbd 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -560,6 +560,33 @@ const ( // Note: This remains a string slice for backward compatibility with existing code var AgenticEngines = []string{string(ClaudeEngine), string(CodexEngine), string(CopilotEngine)} +// EngineOption represents a selectable AI engine with its display metadata and secret configuration +type EngineOption struct { + Value string + Label string + Description string + SecretName string // The name of the secret required for this engine (e.g., "COPILOT_GITHUB_TOKEN") + EnvVarName string // Alternative environment variable name if different from SecretName (optional) + KeyURL string // URL where users can obtain their API key (empty for engines with special setup like Copilot) +} + +// EngineOptions provides the list of available AI engines for user selection +var EngineOptions = []EngineOption{ + {string(CopilotEngine), "GitHub Copilot", "GitHub Copilot CLI with agent support", "COPILOT_GITHUB_TOKEN", "", ""}, + {string(ClaudeEngine), "Claude", "Anthropic Claude Code coding agent", "ANTHROPIC_API_KEY", "", "https://console.anthropic.com/settings/keys"}, + {string(CodexEngine), "Codex", "OpenAI Codex/GPT engine", "OPENAI_API_KEY", "", "https://platform.openai.com/api-keys"}, +} + +// GetEngineOption returns the EngineOption for the given engine value, or nil if not found +func GetEngineOption(engineValue string) *EngineOption { + for i := range EngineOptions { + if EngineOptions[i].Value == engineValue { + return &EngineOptions[i] + } + } + return nil +} + // DefaultReadOnlyGitHubTools defines the default read-only GitHub MCP tools. // This list is shared by both local (Docker) and remote (hosted) modes. // Currently, both modes use identical tool lists, but this may diverge in the future diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index e448f2ebd0..d4a3788a90 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -619,17 +619,19 @@ func (c *Compiler) CompileWorkflowData(workflowData *WorkflowData, markdownPath } } - // Display success message with file size if we generated a lock file - if c.noEmit { - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(console.ToRelativePath(markdownPath))) - } else { - // Get the size of the generated lock file for display - if lockFileInfo, err := os.Stat(lockFile); err == nil { - lockSize := console.FormatFileSize(lockFileInfo.Size()) - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("%s (%s)", console.ToRelativePath(markdownPath), lockSize))) - } else { - // Fallback to original display if we can't get file info + // Display success message with file size if we generated a lock file (unless quiet mode) + if !c.quiet { + if c.noEmit { fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(console.ToRelativePath(markdownPath))) + } else { + // Get the size of the generated lock file for display + if lockFileInfo, err := os.Stat(lockFile); err == nil { + lockSize := console.FormatFileSize(lockFileInfo.Size()) + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("%s (%s)", console.ToRelativePath(markdownPath), lockSize))) + } else { + // Fallback to original display if we can't get file info + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(console.ToRelativePath(markdownPath))) + } } } return nil diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index 976b6bfb9d..1887dffa0e 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -17,6 +17,7 @@ type FileTracker interface { // Compiler handles converting markdown workflows to GitHub Actions YAML type Compiler struct { verbose bool + quiet bool // If true, suppress success messages (for interactive mode) engineOverride string customOutput string // If set, output will be written to this path instead of default location version string // Version of the extension @@ -89,6 +90,11 @@ func (c *Compiler) SetSkipValidation(skip bool) { c.skipValidation = skip } +// SetQuiet configures whether to suppress success messages (for interactive mode) +func (c *Compiler) SetQuiet(quiet bool) { + c.quiet = quiet +} + // SetNoEmit configures whether to validate without generating lock files func (c *Compiler) SetNoEmit(noEmit bool) { c.noEmit = noEmit diff --git a/pkg/workflow/data/action_pins.json b/pkg/workflow/data/action_pins.json index fb2eed827c..d5a05f6b98 100644 --- a/pkg/workflow/data/action_pins.json +++ b/pkg/workflow/data/action_pins.json @@ -1,33 +1,38 @@ { "entries": { - "actions/ai-inference@v2.0.5": { + "actions/ai-inference@v2": { "repo": "actions/ai-inference", - "version": "v2.0.5", + "version": "v2", "sha": "a6101c89c6feaecc585efdd8d461f18bb7896f20" }, - "actions/attest-build-provenance@v3.2.0": { + "actions/attest-build-provenance@v2": { "repo": "actions/attest-build-provenance", - "version": "v3.2.0", - "sha": "62fc1d596301d0ab9914e1fec14dc5c8d93f65cd" + "version": "v2", + "sha": "96b4a1ef7235a096b17240c259729fdd70c83d45" }, - "actions/cache/restore@v5.0.2": { + "actions/cache/restore@v4.3.0": { "repo": "actions/cache/restore", - "version": "v5.0.2", - "sha": "8b402f58fbc84540c8b491a91e594a4576fec3d7" + "version": "v4.3.0", + "sha": "0057852bfaa89a56745cba8c7296529d2fc39830" }, - "actions/cache/save@v5.0.2": { + "actions/cache/save@v4.3.0": { "repo": "actions/cache/save", - "version": "v5.0.2", - "sha": "8b402f58fbc84540c8b491a91e594a4576fec3d7" + "version": "v4.3.0", + "sha": "0057852bfaa89a56745cba8c7296529d2fc39830" }, - "actions/cache@v5.0.2": { + "actions/cache@v4.3.0": { "repo": "actions/cache", - "version": "v5.0.2", - "sha": "8b402f58fbc84540c8b491a91e594a4576fec3d7" + "version": "v4.3.0", + "sha": "0057852bfaa89a56745cba8c7296529d2fc39830" }, - "actions/checkout@v5": { + "actions/checkout@v4": { "repo": "actions/checkout", - "version": "v5", + "version": "v4", + "sha": "34e114876b0b11c390a56381ad16ebd13914f8d5" + }, + "actions/checkout@v5.0.1": { + "repo": "actions/checkout", + "version": "v5.0.1", "sha": "93cb6efe18208431cddfb8368fd83d5badbf9bfd" }, "actions/checkout@v6": { @@ -35,75 +40,100 @@ "version": "v6", "sha": "8e8c483db84b4bee98b60c0593521ed34d9990e8" }, - "actions/checkout@v6.0.2": { - "repo": "actions/checkout", - "version": "v6.0.2", - "sha": "de0fac2e4500dabe0009e67214ff5f5447ce83dd" - }, - "actions/create-github-app-token@v3.0.0-beta.2": { + "actions/create-github-app-token@v2.2.1": { "repo": "actions/create-github-app-token", - "version": "v3.0.0-beta.2", - "sha": "bf559f85448f9380bcfa2899dbdc01eb5b37be3a" + "version": "v2.2.1", + "sha": "29824e69f54612133e76f7eaac726eef6c875baf" }, - "actions/download-artifact@v6": { + "actions/download-artifact@v6.0.0": { "repo": "actions/download-artifact", - "version": "v6", + "version": "v6.0.0", "sha": "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53" }, - "actions/download-artifact@v7": { - "repo": "actions/download-artifact", - "version": "v7", - "sha": "37930b1c2abaa49bbe596cd826c3c89aef350131" - }, "actions/github-script@v7": { "repo": "actions/github-script", "version": "v7", "sha": "f28e40c7f34bde8b3046d885e986cb6290c5673b" }, - "actions/github-script@v8": { + "actions/github-script@v7.0.1": { + "repo": "actions/github-script", + "version": "v7.0.1", + "sha": "60a0d83039c74a4aee543508d2ffcb1c3799cdea" + }, + "actions/github-script@v8.0.0": { "repo": "actions/github-script", - "version": "v8", + "version": "v8.0.0", "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" }, - "actions/setup-dotnet@v5.1.0": { + "actions/setup-dotnet@v4": { "repo": "actions/setup-dotnet", - "version": "v5.1.0", - "sha": "baa11fbfe1d6520db94683bd5c7a3818018e4309" + "version": "v4.3.1", + "sha": "67a3573c9a986a3f9c594539f4ab511d57bb3ce9" }, - "actions/setup-go@v6.2.0": { + "actions/setup-go@v6": { "repo": "actions/setup-go", - "version": "v6.2.0", + "version": "v6", "sha": "7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5" }, - "actions/setup-java@v5.2.0": { + "actions/setup-go@v6.1.0": { + "repo": "actions/setup-go", + "version": "v6.1.0", + "sha": "4dc6199c7b1a012772edbd06daecab0f50c9053c" + }, + "actions/setup-java@v4": { "repo": "actions/setup-java", - "version": "v5.2.0", - "sha": "be666c2fcd27ec809703dec50e508c2fdc7f6654" + "version": "v4.8.0", + "sha": "c1e323688fd81a25caa38c78aa6df2d33d3e20d9" }, - "actions/setup-node@v6.2.0": { + "actions/setup-node@v6": { "repo": "actions/setup-node", - "version": "v6.2.0", + "version": "v6", "sha": "6044e13b5dc448c55e2357c09f80417699197238" }, - "actions/setup-python@v6.2.0": { + "actions/setup-node@v6.1.0": { + "repo": "actions/setup-node", + "version": "v6.1.0", + "sha": "395ad3262231945c25e8478fd5baf05154b1d79f" + }, + "actions/setup-python@v5.6.0": { "repo": "actions/setup-python", - "version": "v6.2.0", - "sha": "a309ff8b426b58ec0e2a45f0f869d46889d02405" + "version": "v5.6.0", + "sha": "a26af69be951a213d495a4c3e4e4022e16d87065" }, - "actions/upload-artifact@v6": { + "actions/upload-artifact@v4": { "repo": "actions/upload-artifact", - "version": "v6", + "version": "v4.6.2", + "sha": "ea165f8d65b6e75b540449e92b4886f43607fa02" + }, + "actions/upload-artifact@v5.0.0": { + "repo": "actions/upload-artifact", + "version": "v5.0.0", + "sha": "330a01c490aca151604b8cf639adc76d48f6c5d4" + }, + "actions/upload-artifact@v6.0.0": { + "repo": "actions/upload-artifact", + "version": "v6.0.0", "sha": "b7c566a772e6b6bfb58ed0dc250532a479d7789f" }, - "anchore/sbom-action@v0.22.0": { + "anchore/sbom-action@v0": { "repo": "anchore/sbom-action", - "version": "v0.22.0", + "version": "v0", "sha": "62ad5284b8ced813296287a0b63906cb364b73ee" }, - "astral-sh/setup-uv@v7.2": { + "anchore/sbom-action@v0.20.10": { + "repo": "anchore/sbom-action", + "version": "v0.20.10", + "sha": "fbfd9c6c189226748411491745178e0c2017392d" + }, + "anchore/sbom-action@v0.20.11": { + "repo": "anchore/sbom-action", + "version": "v0.20.11", + "sha": "43a17d6e7add2b5535efe4dcae9952337c479a93" + }, + "astral-sh/setup-uv@v5.4.2": { "repo": "astral-sh/setup-uv", - "version": "v7.2", - "sha": "3ae150cc9da67abcd31089a802e239773e6a2cb5" + "version": "v5.4.2", + "sha": "d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86" }, "cli/gh-extension-precompile@v2.1.0": { "repo": "cli/gh-extension-precompile", @@ -115,24 +145,24 @@ "version": "v2.0.3", "sha": "e95548e56dfa95d4e1a28d6f422fafe75c4c26fb" }, - "docker/build-push-action@v6.18.0": { + "docker/build-push-action@v6": { "repo": "docker/build-push-action", - "version": "v6.18.0", + "version": "v6", "sha": "263435318d21b8e681c14492fe198d362a7d2c83" }, - "docker/login-action@v3.6.0": { + "docker/login-action@v3": { "repo": "docker/login-action", - "version": "v3.6.0", + "version": "v3", "sha": "5e57cd118135c172c3672efd75eb46360885c0ef" }, - "docker/metadata-action@v5.10.0": { + "docker/metadata-action@v5": { "repo": "docker/metadata-action", - "version": "v5.10.0", + "version": "v5", "sha": "c299e40c65443455700f0fdfc63efafe5b349051" }, - "docker/setup-buildx-action@v3.12.0": { + "docker/setup-buildx-action@v3": { "repo": "docker/setup-buildx-action", - "version": "v3.12.0", + "version": "v3", "sha": "8d2750c68a42422c14e847fe6c8ac0403b4cbd6f" }, "erlef/setup-beam@v1": { @@ -140,45 +170,45 @@ "version": "v1.20.4", "sha": "dff508cca8ce57162e7aa6c4769a4f97c2fed638" }, - "github/codeql-action/upload-sarif@v4.32.0": { + "github/codeql-action/upload-sarif@v3": { "repo": "github/codeql-action/upload-sarif", - "version": "v4.32.0", - "sha": "e6985fd516cce3b1a0e8db34a4013d2e50a1e252" + "version": "v3.31.9", + "sha": "70c165ac82ca0e33a10e9741508dd0ccb4dcf080" }, "github/stale-repos@v3": { "repo": "github/stale-repos", "version": "v3", "sha": "3477b6488008d9411aaf22a0924ec7c1f6a69980" }, - "github/stale-repos@v8.0.4": { + "github/stale-repos@v3.0.2": { "repo": "github/stale-repos", - "version": "v8.0.4", - "sha": "6084a41431c4ce8842a7e879b1a15082b88742ae" + "version": "v3.0.2", + "sha": "a21e55567b83cf3c3f3f9085d3038dc6cee02598" }, - "haskell-actions/setup@v2.10.3": { + "haskell-actions/setup@v2": { "repo": "haskell-actions/setup", - "version": "v2.10.3", - "sha": "9cd1b7bf3f36d5a3c3b17abc3545bfb5481912ea" + "version": "v2.9.1", + "sha": "55073cbd0e96181a9abd6ff4e7d289867dffc98d" }, - "oven-sh/setup-bun@v2.1.2": { + "oven-sh/setup-bun@v2": { "repo": "oven-sh/setup-bun", - "version": "v2.1.2", - "sha": "3d267786b128fe76c2f16a390aa2448b815359f3" + "version": "v2.0.2", + "sha": "735343b667d3e6f658f44d0eca948eb6282f2b76" }, - "ruby/setup-ruby@v1.286.0": { + "ruby/setup-ruby@v1": { "repo": "ruby/setup-ruby", - "version": "v1.286.0", - "sha": "90be1154f987f4dc0fe0dd0feedac9e473aa4ba8" + "version": "v1.275.0", + "sha": "d354de180d0c9e813cfddfcbdc079945d4be589b" }, "super-linter/super-linter@v8.2.1": { "repo": "super-linter/super-linter", "version": "v8.2.1", "sha": "2bdd90ed3262e023ac84bf8fe35dc480721fc1f2" }, - "super-linter/super-linter@v8.3.2": { + "super-linter/super-linter@v8.3.1": { "repo": "super-linter/super-linter", - "version": "v8.3.2", - "sha": "d5b0a2ab116623730dd094f15ddc1b6b25bf7b99" + "version": "v8.3.1", + "sha": "47984f49b4e87383eed97890fe2dca6063bbd9c3" } } } diff --git a/socials/PLAN.md b/socials/PLAN.md index c1545e1810..a13c5eb6ee 100644 --- a/socials/PLAN.md +++ b/socials/PLAN.md @@ -77,7 +77,6 @@ Remaining posts (shifted later due to daily Meet the Workflows roll-out): - 2026-02-14 -> `25-security-lessons.md` - 2026-02-15 -> `26-how-workflows-work.md` - 2026-02-16 -> `27-authoring-workflows.md` -- 2026-02-17 -> `28-getting-started.md` ### Content Format diff --git a/socials/campaign.log b/socials/campaign.log index 04ca052192..951e085a12 100644 --- a/socials/campaign.log +++ b/socials/campaign.log @@ -17,7 +17,6 @@ [2026-01-16 21:32:02] Content scheduled for 2026-02-04=15-meet-workflows-interactive-chatops.md but not yet created: /home/dsyme/gh-aw/socials/content/2026-02-05=16-meet-workflows-documentation.md [2026-01-16 21:32:02] Content scheduled for 2026-02-06=17-meet-workflows-campaigns.md but not yet created: /home/dsyme/gh-aw/socials/content/2026-02-07=18-meet-workflows-advanced-analytics.md [2026-01-16 21:32:02] Content scheduled for 2026-02-08=19-meet-workflows-metrics-analytics.md but not yet created: /home/dsyme/gh-aw/socials/content/2026-02-09=20-meet-workflows-creative-culture.md -[2026-01-16 21:32:02] Content scheduled for 2026-02-10=21-twelve-lessons.md but not yet created: /home/dsyme/gh-aw/socials/content/2026-02-11=22-design-patterns.md [2026-01-16 21:32:02] Content scheduled for 2026-02-12=23-operational-patterns.md but not yet created: /home/dsyme/gh-aw/socials/content/2026-02-13=24-imports-sharing.md [2026-01-16 21:32:02] Content scheduled for 2026-02-14=25-security-lessons.md but not yet created: /home/dsyme/gh-aw/socials/content/2026-02-15=26-how-workflows-work.md [2026-01-16 21:32:02] Content scheduled for 2026-02-16=27-authoring-workflows.md but not yet created: /home/dsyme/gh-aw/socials/content/2026-02-17=28-getting-started.md @@ -71,7 +70,6 @@ [2026-01-16 21:32:40] Content scheduled for 2026-02-14 but not yet created: /home/dsyme/gh-aw/socials/content/25-security-lessons.md [2026-01-16 21:32:40] Content scheduled for 2026-02-15 but not yet created: /home/dsyme/gh-aw/socials/content/26-how-workflows-work.md [2026-01-16 21:32:40] Content scheduled for 2026-02-16 but not yet created: /home/dsyme/gh-aw/socials/content/27-authoring-workflows.md -[2026-01-16 21:32:40] Content scheduled for 2026-02-17 but not yet created: /home/dsyme/gh-aw/socials/content/28-getting-started.md [2026-01-16 21:32:40] Tracking engagement for recent posts [2026-01-16 21:33:02] Running campaign [2026-01-16 21:33:02] [DRY RUN] No API calls will be made diff --git a/socials/scripts.sh b/socials/scripts.sh index ef428c0dab..b6f8d88c03 100644 --- a/socials/scripts.sh +++ b/socials/scripts.sh @@ -101,7 +101,6 @@ declare -A SCHEDULE=( ["2026-02-14"]="25-security-lessons.md" ["2026-02-15"]="26-how-workflows-work.md" ["2026-02-16"]="27-authoring-workflows.md" - ["2026-02-17"]="28-getting-started.md" ) # Map content files to schedule From 3175b3a8646b0b02e9544d4807d3e6e6f5fc4001 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Tue, 27 Jan 2026 00:04:12 +0000 Subject: [PATCH 2/4] interactive add --- pkg/cli/add_command.go | 9 ++------- pkg/cli/packages.go | 12 +++++++++++- socials/campaign.log | 1 + 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/pkg/cli/add_command.go b/pkg/cli/add_command.go index 29a1e235b6..b8e4caadf6 100644 --- a/pkg/cli/add_command.go +++ b/pkg/cli/add_command.go @@ -149,13 +149,8 @@ Note: To create a new workflow from scratch, use the 'new' command instead.`, } // Handle normal (non-interactive) mode - if prFlag { - _, err := AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, true, pushFlag, noGitattributes, workflowDir, noStopAfter, stopAfter) - return err - } else { - _, err := AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, false, pushFlag, noGitattributes, workflowDir, noStopAfter, stopAfter) - return err - } + _, err := AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, prFlag, pushFlag, noGitattributes, workflowDir, noStopAfter, stopAfter) + return err }, } diff --git a/pkg/cli/packages.go b/pkg/cli/packages.go index 30f81aa35c..90553d6cb0 100644 --- a/pkg/cli/packages.go +++ b/pkg/cli/packages.go @@ -797,7 +797,8 @@ func ExtractWorkflowDescription(content string) string { return "" } -// ExtractWorkflowEngine extracts the engine field from workflow content string +// ExtractWorkflowEngine extracts the engine field from workflow content string. +// Supports both string format (engine: copilot) and nested format (engine: { id: copilot }). func ExtractWorkflowEngine(content string) string { result, err := parser.ExtractFrontmatterFromContent(content) if err != nil { @@ -805,9 +806,18 @@ func ExtractWorkflowEngine(content string) string { } if engine, ok := result.Frontmatter["engine"]; ok { + // Handle string format: engine: copilot if engineStr, ok := engine.(string); ok { return engineStr } + // Handle nested format: engine: { id: copilot } + if engineMap, ok := engine.(map[string]any); ok { + if id, ok := engineMap["id"]; ok { + if idStr, ok := id.(string); ok { + return idStr + } + } + } } return "" diff --git a/socials/campaign.log b/socials/campaign.log index 951e085a12..9b3d6aafe9 100644 --- a/socials/campaign.log +++ b/socials/campaign.log @@ -17,6 +17,7 @@ [2026-01-16 21:32:02] Content scheduled for 2026-02-04=15-meet-workflows-interactive-chatops.md but not yet created: /home/dsyme/gh-aw/socials/content/2026-02-05=16-meet-workflows-documentation.md [2026-01-16 21:32:02] Content scheduled for 2026-02-06=17-meet-workflows-campaigns.md but not yet created: /home/dsyme/gh-aw/socials/content/2026-02-07=18-meet-workflows-advanced-analytics.md [2026-01-16 21:32:02] Content scheduled for 2026-02-08=19-meet-workflows-metrics-analytics.md but not yet created: /home/dsyme/gh-aw/socials/content/2026-02-09=20-meet-workflows-creative-culture.md +[2026-01-16 21:32:02] Content scheduled for 2026-02-10=21-twelve-lessons.md but not yet created: /home/dsyme/gh-aw/socials/content/2026-02-11=22-design-patterns.md [2026-01-16 21:32:02] Content scheduled for 2026-02-12=23-operational-patterns.md but not yet created: /home/dsyme/gh-aw/socials/content/2026-02-13=24-imports-sharing.md [2026-01-16 21:32:02] Content scheduled for 2026-02-14=25-security-lessons.md but not yet created: /home/dsyme/gh-aw/socials/content/2026-02-15=26-how-workflows-work.md [2026-01-16 21:32:02] Content scheduled for 2026-02-16=27-authoring-workflows.md but not yet created: /home/dsyme/gh-aw/socials/content/2026-02-17=28-getting-started.md From 6b7059018808be8ce43721d45bb143cf55362dbc Mon Sep 17 00:00:00 2001 From: Don Syme Date: Tue, 27 Jan 2026 00:06:33 +0000 Subject: [PATCH 3/4] fix lint --- pkg/cli/add_command.go | 10 ------- pkg/cli/add_interactive.go | 60 -------------------------------------- 2 files changed, 70 deletions(-) diff --git a/pkg/cli/add_command.go b/pkg/cli/add_command.go index b8e4caadf6..e7ea66e999 100644 --- a/pkg/cli/add_command.go +++ b/pkg/cli/add_command.go @@ -1168,16 +1168,6 @@ func expandWildcardWorkflows(specs []*WorkflowSpec, verbose bool) ([]*WorkflowSp return expandedWorkflows, nil } -// checkWorkflowsHaveDispatch checks if any of the workflows have a workflow_dispatch trigger -func checkWorkflowsHaveDispatch(workflows []*WorkflowSpec, verbose bool) bool { - for _, spec := range workflows { - if checkWorkflowHasDispatch(spec, verbose) { - return true - } - } - return false -} - // checkWorkflowHasDispatch checks if a single workflow has a workflow_dispatch trigger func checkWorkflowHasDispatch(spec *WorkflowSpec, verbose bool) bool { addLog.Printf("Checking if workflow %s has workflow_dispatch trigger", spec.WorkflowName) diff --git a/pkg/cli/add_interactive.go b/pkg/cli/add_interactive.go index 5302662631..75a380efe1 100644 --- a/pkg/cli/add_interactive.go +++ b/pkg/cli/add_interactive.go @@ -906,32 +906,6 @@ func (c *AddInteractiveConfig) checkStatusAndOfferRun(ctx context.Context) error fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Workflow triggered successfully!")) fmt.Fprintln(os.Stderr, "") fmt.Fprintf(os.Stderr, "πŸ”— View workflow run: %s\n", runInfo.URL) - - // // Step 10: Offer to open in browser - // fmt.Fprintln(os.Stderr, "") - // var openBrowser bool - // browserForm := huh.NewForm( - // huh.NewGroup( - // huh.NewConfirm(). - // Title("Would you like to open the workflow run in your browser?"). - // Affirmative("Yes, open browser"). - // Negative("No, thanks"). - // Value(&openBrowser), - // ), - // ).WithAccessible(console.IsAccessibleMode()) - - // if err := browserForm.Run(); err == nil && openBrowser { - // // Use gh browse to open the URL - // browseCmd := workflow.ExecGH("browse", runInfo.URL) - // if err := browseCmd.Run(); err != nil { - // // Fallback: try to open directly - // addInteractiveLog.Printf("gh browse failed: %v, trying direct open", err) - // if openErr := openURLInBrowser(runInfo.URL); openErr != nil { - // fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Could not open browser automatically.")) - // fmt.Fprintf(os.Stderr, "Please visit: %s\n", runInfo.URL) - // } - // } - // } } } } @@ -986,40 +960,6 @@ func getWorkflowStatuses(pattern, repoOverride string, verbose bool) ([]Workflow return nil, nil } -// openURLInBrowser opens a URL in the default browser -func openURLInBrowser(url string) error { - var cmd *exec.Cmd - - switch { - case isLinux(): - cmd = exec.Command("xdg-open", url) - case isDarwin(): - cmd = exec.Command("open", url) - default: - // Try gh browse as fallback - return fmt.Errorf("unsupported platform") - } - - return cmd.Start() -} - -// isLinux checks if running on Linux -func isLinux() bool { - return strings.Contains(strings.ToLower(os.Getenv("OSTYPE")), "linux") || - fileExists("/proc/version") -} - -// isDarwin checks if running on macOS -func isDarwin() bool { - return strings.Contains(strings.ToLower(os.Getenv("OSTYPE")), "darwin") -} - -// fileExists checks if a file exists -func fileExists(path string) bool { - _, err := os.Stat(path) - return err == nil -} - // showFinalInstructions shows final instructions to the user func (c *AddInteractiveConfig) showFinalInstructions() { fmt.Fprintln(os.Stderr, "") From 60cb6dbbe2c454fe164210a8b0a40b1df4d1a6b1 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Tue, 27 Jan 2026 00:13:24 +0000 Subject: [PATCH 4/4] review --- docs/src/content/docs/setup/quick-start.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/src/content/docs/setup/quick-start.md b/docs/src/content/docs/setup/quick-start.md index d783fc346b..55bd580d16 100644 --- a/docs/src/content/docs/setup/quick-start.md +++ b/docs/src/content/docs/setup/quick-start.md @@ -5,25 +5,23 @@ sidebar: order: 1 --- -## Adding a Daily Status Workflow to Your Repo +## Adding an Automated Daily Status Workflow to Your Repo In this guide you will add the automated [**Daily Repo Status Report**](https://github.com/githubnext/agentics/blob/main/workflows/daily-repo-status.md?plain=1) to an existing GitHub repository where you are a maintainer, running in GitHub Actions. Remember the aim here is _automated AI_: to install something that will run _automatically_ every day, in the context of your repository, and create a fresh status report issue in your repository without any further manual intervention. If you're familiar with GitHub Actions, you will be aware of the power of automation. -> [!TIP] -> There are hundreds of other ways to use GitHub Agentic Workflows, which -> you can explore in [Peli's Agent Factory](https://githubnext.github.io/gh-aw/blog/2026-01-12-welcome-to-pelis-agent-factory/). This -> workflow is just the start of what's possible. +There are hundreds of other ways to use GitHub Agentic Workflows, which you can explore in [Peli's Agent Factory](https://githubnext.github.io/gh-aw/blog/2026-01-12-welcome-to-pelis-agent-factory/). This workflow is just the start of what's possible. ## Prerequisites Before installing, ensure you have: - βœ… **AI Account** - a GitHub Copilot, Anthropic Claude or OpenAI Codex subscription -- βœ… **GitHub Repository** you are a maintainer on -- βœ… **[GitHub Actions](https://docs.github.com/actions)** enabled in your repository -- βœ… **GitHub CLI** (`gh`) - A command-line tool for GitHub operations. [Install here](https://cli.github.com) v2.0.0+ and authenticate with `gh auth login` +- βœ… **GitHub Repository** - a GitHub repository you are a maintainer on +- βœ… **GitHub Actions** enabled in your repository +- βœ… **GitHub CLI** (`gh`) - A command-line tool for GitHub. [Install here](https://cli.github.com) v2.0.0+ and authenticate with `gh auth login` +- βœ… **Operating System**: Linux, macOS, or Windows with WSL ### Step 1 β€” Install the extension @@ -34,8 +32,6 @@ gh extension install githubnext/gh-aw ``` > [!TIP] -> Working in GitHub Codespaces? -> > If you're working in a GitHub Codespace, use the standalone installer instead: > > ```bash wrap