Skip to content

Commit 6d02b87

Browse files
authored
add cli flag to guard dropping a agentic workflow instructinos file (#6)
* Add instructions flag to compile command and update related functions - Introduced a new flag `--instructions` to the compile command to control the generation of GitHub Copilot instructions. - Updated `CompileWorkflows` function to accept a new parameter for writing instructions. - Modified `ensureCopilotInstructions` to conditionally write instructions based on the new flag. - Enhanced tests to cover the new functionality and ensure correct behavior with the instructions flag. * Add GitHub actions for computing body text and managing reactions * Fix CompileWorkflows test to expect successful compilation of existing markdown files * Refactor TestAllCommandsExist to improve clarity and ensure accurate error handling for workflow commands
1 parent ef9663b commit 6d02b87

6 files changed

Lines changed: 254 additions & 21 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: "Compute current body text"
2+
description: "Computes the current body text based on the GitHub event context"
3+
outputs:
4+
text:
5+
description: "The computed current body text based on event type"
6+
runs:
7+
using: "composite"
8+
steps:
9+
- name: Compute current body text
10+
id: compute-text
11+
uses: actions/github-script@v7
12+
with:
13+
script: |
14+
let text = '';
15+
16+
// Determine current body text based on event context
17+
switch (context.eventName) {
18+
case 'issues':
19+
// For issues: title + body
20+
if (context.payload.issue) {
21+
const title = context.payload.issue.title || '';
22+
const body = context.payload.issue.body || '';
23+
text = `${title}\n\n${body}`;
24+
}
25+
break;
26+
27+
case 'pull_request':
28+
// For pull requests: title + body
29+
if (context.payload.pull_request) {
30+
const title = context.payload.pull_request.title || '';
31+
const body = context.payload.pull_request.body || '';
32+
text = `${title}\n\n${body}`;
33+
}
34+
break;
35+
36+
case 'issue_comment':
37+
// For issue comments: comment body
38+
if (context.payload.comment) {
39+
text = context.payload.comment.body || '';
40+
}
41+
break;
42+
43+
case 'pull_request_review_comment':
44+
// For PR review comments: comment body
45+
if (context.payload.comment) {
46+
text = context.payload.comment.body || '';
47+
}
48+
break;
49+
50+
case 'pull_request_review':
51+
// For PR reviews: review body
52+
if (context.payload.review) {
53+
text = context.payload.review.body || '';
54+
}
55+
break;
56+
57+
default:
58+
// Default: empty text
59+
text = '';
60+
break;
61+
}
62+
63+
// display in logs
64+
console.log(`text: ${text}`);
65+
66+
// Set the text as output
67+
core.setOutput('text', text);
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
name: "Add/Remove reaction on triggering item"
2+
description: "Adds or removes a reaction on the issue/PR/comment that triggered the workflow"
3+
inputs:
4+
github-token:
5+
description: "Token with issues/pull-requests write (GITHUB_TOKEN is fine)"
6+
required: true
7+
mode:
8+
description: "'add' or 'remove'"
9+
required: true
10+
reaction:
11+
description: "One of +1, -1, laugh, confused, heart, hooray, rocket, eyes"
12+
required: false
13+
default: "eyes"
14+
reaction-id:
15+
description: "Optional reaction id to remove (if known)"
16+
required: false
17+
outputs:
18+
reaction-id:
19+
description: "ID of the reaction that was added (for later removal)"
20+
runs:
21+
using: "composite"
22+
steps:
23+
- name: Compute reactions API endpoint for the triggering payload
24+
id: ctx
25+
shell: bash
26+
env:
27+
GITHUB_EVENT_NAME: ${{ github.event_name }}
28+
GITHUB_EVENT_PATH: ${{ github.event_path }}
29+
GITHUB_REPOSITORY: ${{ github.repository }}
30+
run: |
31+
set -euo pipefail
32+
owner="${GITHUB_REPOSITORY%%/*}"
33+
repo="${GITHUB_REPOSITORY##*/}"
34+
ev="$GITHUB_EVENT_PATH"
35+
36+
case "$GITHUB_EVENT_NAME" in
37+
issues)
38+
number=$(jq -r '.issue.number' "$ev")
39+
endpoint="/repos/$owner/$repo/issues/$number/reactions"
40+
;;
41+
issue_comment)
42+
cid=$(jq -r '.comment.id' "$ev")
43+
endpoint="/repos/$owner/$repo/issues/comments/$cid/reactions"
44+
;;
45+
pull_request|pull_request_target)
46+
number=$(jq -r '.pull_request.number' "$ev")
47+
# PRs are "issues" for the reactions endpoint
48+
endpoint="/repos/$owner/$repo/issues/$number/reactions"
49+
;;
50+
pull_request_review_comment)
51+
cid=$(jq -r '.comment.id' "$ev")
52+
endpoint="/repos/$owner/$repo/pulls/comments/$cid/reactions"
53+
;;
54+
*)
55+
echo "Unsupported event: $GITHUB_EVENT_NAME" >&2
56+
exit 1
57+
;;
58+
esac
59+
60+
echo "endpoint=$endpoint" >> "$GITHUB_OUTPUT"
61+
62+
- name: Add reaction
63+
if: ${{ inputs.mode == 'add' }}
64+
shell: bash
65+
env:
66+
GH_TOKEN: ${{ inputs.github-token }}
67+
ENDPOINT: ${{ steps.ctx.outputs.endpoint }}
68+
REACTION: ${{ inputs.reaction }}
69+
run: |
70+
set -euo pipefail
71+
# Create (or fetch existing) reaction
72+
# The API returns the reaction object (201 on create, 200 if it already existed)
73+
resp=$(gh api \
74+
-H "Accept: application/vnd.github+json" \
75+
-X POST "$ENDPOINT" \
76+
-f content="$REACTION" \
77+
|| true)
78+
79+
# If a concurrent create happened, fall back to listing to find our reaction
80+
if [ -z "${resp:-}" ] || [ "$resp" = "null" ]; then
81+
resp=$(gh api -H "Accept: application/vnd.github+json" "$ENDPOINT")
82+
rid=$(echo "$resp" | jq -r --arg r "$REACTION" \
83+
'.[] | select(.content==$r and .user.login=="github-actions[bot]") | .id' | head -n1)
84+
else
85+
rid=$(echo "$resp" | jq -r '.id')
86+
if [ "$rid" = "null" ] || [ -z "$rid" ]; then
87+
# fallback to list, just in case
88+
list=$(gh api -H "Accept: application/vnd.github+json" "$ENDPOINT")
89+
rid=$(echo "$list" | jq -r --arg r "$REACTION" \
90+
'.[] | select(.content==$r and .user.login=="github-actions[bot]") | .id' | head -n1)
91+
fi
92+
fi
93+
94+
if [ -z "${rid:-}" ]; then
95+
echo "Warning: could not determine reaction id; cleanup will list/filter." >&2
96+
fi
97+
98+
echo "reaction-id=${rid:-}" >> "$GITHUB_OUTPUT"
99+
100+
- name: Remove reaction
101+
if: ${{ inputs.mode == 'remove' }}
102+
shell: bash
103+
env:
104+
GH_TOKEN: ${{ inputs.github-token }}
105+
ENDPOINT: ${{ steps.ctx.outputs.endpoint }}
106+
REACTION: ${{ inputs.reaction }}
107+
REACTION_ID_IN: ${{ inputs.reaction-id }}
108+
run: |
109+
set -euo pipefail
110+
111+
delete_by_id () {
112+
local rid="$1"
113+
if [ -n "$rid" ] && [ "$rid" != "null" ]; then
114+
gh api -H "Accept: application/vnd.github+json" -X DELETE "/reactions/$rid" || true
115+
fi
116+
}
117+
118+
if [ -n "$REACTION_ID_IN" ]; then
119+
# Fast path: we were given the id from the add step
120+
delete_by_id "$REACTION_ID_IN"
121+
exit 0
122+
fi
123+
124+
# Fallback: list reactions on the same subject, and delete the bot's matching reaction(s)
125+
list=$(gh api -H "Accept: application/vnd.github+json" "$ENDPOINT" || echo "[]")
126+
echo "$list" | jq -r --arg r "$REACTION" '
127+
.[] | select(.content==$r and .user.login=="github-actions[bot]") | .id
128+
' | while read -r rid; do
129+
delete_by_id "$rid"
130+
done

cmd/gh-aw/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,12 @@ var compileCmd = &cobra.Command{
205205
validate, _ := cmd.Flags().GetBool("validate")
206206
autoCompile, _ := cmd.Flags().GetBool("auto-compile")
207207
watch, _ := cmd.Flags().GetBool("watch")
208+
instructions, _ := cmd.Flags().GetBool("instructions")
208209
if err := validateEngine(engineOverride); err != nil {
209210
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
210211
os.Exit(1)
211212
}
212-
if err := cli.CompileWorkflows(file, verbose, engineOverride, validate, autoCompile, watch); err != nil {
213+
if err := cli.CompileWorkflows(file, verbose, engineOverride, validate, autoCompile, watch, instructions); err != nil {
213214
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
214215
os.Exit(1)
215216
}
@@ -322,6 +323,7 @@ func init() {
322323
compileCmd.Flags().Bool("validate", false, "Enable GitHub Actions workflow schema validation")
323324
compileCmd.Flags().Bool("auto-compile", false, "Generate auto-compile workflow file for automatic compilation")
324325
compileCmd.Flags().BoolP("watch", "w", false, "Watch for changes to workflow files and recompile automatically")
326+
compileCmd.Flags().Bool("instructions", false, "Generate or update GitHub Copilot instructions file")
325327

326328
// Add flags to remove command
327329
removeCmd.Flags().Bool("keep-orphans", false, "Skip removal of orphaned include files that are no longer referenced by any workflow")

pkg/cli/commands.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ func AddWorkflow(workflow string, number int, verbose bool, engineOverride strin
465465
}
466466

467467
// CompileWorkflows compiles markdown files into GitHub Actions workflow files
468-
func CompileWorkflows(markdownFile string, verbose bool, engineOverride string, validate bool, autoCompile bool, watch bool) error {
468+
func CompileWorkflows(markdownFile string, verbose bool, engineOverride string, validate bool, autoCompile bool, watch bool, writeInstructions bool) error {
469469
// Create compiler with verbose flag and AI engine override
470470
compiler := workflow.NewCompiler(verbose, engineOverride, GetVersion())
471471

@@ -504,7 +504,7 @@ func CompileWorkflows(markdownFile string, verbose bool, engineOverride string,
504504
}
505505

506506
// Ensure copilot instructions are present
507-
if err := ensureCopilotInstructions(verbose); err != nil {
507+
if err := ensureCopilotInstructions(verbose, writeInstructions); err != nil {
508508
if verbose {
509509
fmt.Printf("Warning: Failed to update copilot instructions: %v\n", err)
510510
}
@@ -576,7 +576,7 @@ func CompileWorkflows(markdownFile string, verbose bool, engineOverride string,
576576
}
577577

578578
// Ensure copilot instructions are present
579-
if err := ensureCopilotInstructions(verbose); err != nil {
579+
if err := ensureCopilotInstructions(verbose, writeInstructions); err != nil {
580580
if verbose {
581581
fmt.Printf("Warning: Failed to update copilot instructions: %v\n", err)
582582
}
@@ -1265,12 +1265,8 @@ func compileWorkflow(filePath string, verbose bool, engineOverride string) error
12651265
}
12661266
}
12671267

1268-
// Ensure copilot instructions are present
1269-
if err := ensureCopilotInstructions(verbose); err != nil {
1270-
if verbose {
1271-
fmt.Printf("Warning: Failed to update copilot instructions: %v\n", err)
1272-
}
1273-
}
1268+
// Note: Instructions are only written when explicitly requested via the compile command flag
1269+
// This helper function is used in contexts where instructions should not be automatically written
12741270

12751271
return nil
12761272
}
@@ -1354,7 +1350,11 @@ func ensureGitAttributes() error {
13541350
}
13551351

13561352
// ensureCopilotInstructions ensures that .github/instructions/github-agentic-workflows.md contains the copilot instructions
1357-
func ensureCopilotInstructions(verbose bool) error {
1353+
func ensureCopilotInstructions(verbose bool, writeInstructions bool) error {
1354+
if !writeInstructions {
1355+
return nil // Skip writing instructions if flag is not set
1356+
}
1357+
13581358
gitRoot, err := findGitRoot()
13591359
if err != nil {
13601360
return err // Not in a git repository, skip

pkg/cli/commands_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func TestCompileWorkflows(t *testing.T) {
102102

103103
for _, tt := range tests {
104104
t.Run(tt.name, func(t *testing.T) {
105-
err := CompileWorkflows(tt.markdownFile, false, "", false, false, false)
105+
err := CompileWorkflows(tt.markdownFile, false, "", false, false, false, false)
106106

107107
if tt.expectError && err == nil {
108108
t.Errorf("Expected error for test '%s', got nil", tt.name)
@@ -180,13 +180,13 @@ func TestAllCommandsExist(t *testing.T) {
180180
name string
181181
}{
182182
{func() error { return ListWorkflows(false) }, false, "ListWorkflows"},
183-
{func() error { return AddWorkflow("", 1, false, "", "", false) }, false, "AddWorkflow (empty name)"}, // Shows help when empty, doesn't error
184-
{func() error { return CompileWorkflows("", false, "", false, false, false) }, false, "CompileWorkflows"}, // Should succeed when .github/workflows directory exists
185-
{func() error { return RemoveWorkflows("test", false) }, false, "RemoveWorkflows"}, // Should handle missing directory gracefully
186-
{func() error { return StatusWorkflows("test", false) }, false, "StatusWorkflows"}, // Should handle missing directory gracefully
187-
{func() error { return EnableWorkflows("test") }, false, "EnableWorkflows"}, // Should handle missing directory gracefully
188-
{func() error { return DisableWorkflows("test") }, false, "DisableWorkflows"}, // Should handle missing directory gracefully
189-
{func() error { return RunWorkflowOnGitHub("", false) }, true, "RunWorkflowOnGitHub"}, // Should error with empty workflow name
183+
{func() error { return AddWorkflow("", 1, false, "", "", false) }, false, "AddWorkflow (empty name)"}, // Shows help when empty, doesn't error
184+
{func() error { return CompileWorkflows("", false, "", false, false, false, false) }, false, "CompileWorkflows"}, // Should compile existing markdown files successfully
185+
{func() error { return RemoveWorkflows("test", false) }, false, "RemoveWorkflows"}, // Should handle missing directory gracefully
186+
{func() error { return StatusWorkflows("test", false) }, false, "StatusWorkflows"}, // Should handle missing directory gracefully
187+
{func() error { return EnableWorkflows("test") }, false, "EnableWorkflows"}, // Should handle missing directory gracefully
188+
{func() error { return DisableWorkflows("test") }, false, "DisableWorkflows"}, // Should handle missing directory gracefully
189+
{func() error { return RunWorkflowOnGitHub("", false) }, true, "RunWorkflowOnGitHub"}, // Should error with empty workflow name
190190
}
191191

192192
for _, test := range tests {

pkg/cli/copilot_instructions_test.go

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ func TestEnsureCopilotInstructions(t *testing.T) {
6464
}
6565
}
6666

67-
// Call the function
68-
err = ensureCopilotInstructions(false)
67+
// Call the function with writeInstructions=true to test the functionality
68+
err = ensureCopilotInstructions(false, true)
6969
if err != nil {
7070
t.Fatalf("ensureCopilotInstructions() returned error: %v", err)
7171
}
@@ -93,6 +93,40 @@ func TestEnsureCopilotInstructions(t *testing.T) {
9393
}
9494
}
9595

96+
func TestEnsureCopilotInstructions_WithWriteInstructionsFalse(t *testing.T) {
97+
// Create a temporary directory for testing
98+
tempDir := t.TempDir()
99+
100+
// Change to temp directory and initialize git repo for findGitRoot to work
101+
oldWd, _ := os.Getwd()
102+
defer func() {
103+
_ = os.Chdir(oldWd)
104+
}()
105+
err := os.Chdir(tempDir)
106+
if err != nil {
107+
t.Fatalf("Failed to change directory: %v", err)
108+
}
109+
110+
// Initialize git repo
111+
if err := exec.Command("git", "init").Run(); err != nil {
112+
t.Fatalf("Failed to init git repo: %v", err)
113+
}
114+
115+
copilotDir := filepath.Join(tempDir, ".github", "instructions")
116+
copilotInstructionsPath := filepath.Join(copilotDir, "github-agentic-workflows.instructions.md")
117+
118+
// Call the function with writeInstructions=false
119+
err = ensureCopilotInstructions(false, false)
120+
if err != nil {
121+
t.Fatalf("ensureCopilotInstructions() returned error: %v", err)
122+
}
123+
124+
// Check that file does not exist
125+
if _, err := os.Stat(copilotInstructionsPath); !os.IsNotExist(err) {
126+
t.Fatalf("Expected copilot instructions file to not exist when writeInstructions=false")
127+
}
128+
}
129+
96130
func min(a, b int) int {
97131
if a < b {
98132
return a

0 commit comments

Comments
 (0)