Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
.github/aw/github-agentic-workflows.md linguist-generated=true merge=ours
pkg/cli/workflows/*.lock.yml linguist-generated=true merge=ours
pkg/workflow/js/*.js linguist-generated=true
pkg/workflow/js/*.cjs linguist-generated=true
pkg/workflow/sh/*.sh linguist-generated=true
actions/*/index.js linguist-generated=true
actions/setup/js/*.cjs linguist-generated=true

.github/workflows/*.campaign.g.md linguist-generated=true merge=ours
52 changes: 52 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,58 @@ make build # ~1.5s
./gh-aw --help
```

## Build System

### Shell Script Sync

**ALWAYS sync shell scripts before building:**

Shell scripts in `actions/setup/sh/` are the **source of truth** and are automatically synced to `pkg/workflow/sh/` during the build process.

```bash
make sync-shell-scripts # Copies actions/setup/sh/*.sh → pkg/workflow/sh/
make build # Automatically runs sync-shell-scripts
```

**When modifying shell scripts:**
1. Edit files in `actions/setup/sh/` (source of truth)
2. Run `make build` (automatically syncs to pkg/workflow/sh/)
3. The synced files in `pkg/workflow/sh/` are embedded in the binary via `//go:embed`
4. **Never** edit files in `pkg/workflow/sh/` directly - they are generated

**Key points:**
- `actions/setup/sh/*.sh` = Source of truth (manually edited)
- `pkg/workflow/sh/*.sh` = Generated (copied during build, marked as linguist-generated)
- The build process: `actions/setup/sh/` → `pkg/workflow/sh/` → embedded in binary

### JavaScript File Sync

**JavaScript files follow the SAME pattern as shell scripts:**

JavaScript files in `actions/setup/js/` are the **source of truth** and are automatically synced to `pkg/workflow/js/` during the build process.

```bash
make sync-js-scripts # Copies actions/setup/js/*.cjs → pkg/workflow/js/
make build # Automatically runs sync-js-scripts
```

**When modifying JavaScript files:**
1. Edit files in `actions/setup/js/` (source of truth)
2. Run `make build` (automatically syncs to pkg/workflow/js/)
3. The synced files in `pkg/workflow/js/` are embedded in the binary via `//go:embed`
4. **Never** edit production files in `pkg/workflow/js/` directly - they are generated
5. Test files (*.test.cjs) remain only in `pkg/workflow/js/` and are not synced

**Key points:**
- `actions/setup/js/*.cjs` = Source of truth (manually edited, production files only)
- `pkg/workflow/js/*.cjs` = Generated (copied during build, marked as linguist-generated)
- `pkg/workflow/js/*.test.cjs` = Test files (remain in pkg/workflow/js/, not synced)
- The build process: `actions/setup/js/` → `pkg/workflow/js/` → embedded in binary

**Summary of patterns:**
- Shell scripts: `actions/setup/sh/` (source) → `pkg/workflow/sh/` (generated)
- JavaScript: `actions/setup/js/` (source) → `pkg/workflow/js/` (generated)

## Development Workflow

### Build & Test Commands
Expand Down
21 changes: 20 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ all: build build-awmg

# Build the binary, run make deps before this
.PHONY: build
build: sync-templates sync-action-pins
build: sync-templates sync-action-pins sync-shell-scripts sync-js-scripts
go build $(LDFLAGS) -o $(BINARY_NAME) ./cmd/gh-aw

# Build the awmg (MCP gateway) binary
Expand Down Expand Up @@ -433,6 +433,23 @@ sync-templates:
@cp .github/agents/debug-agentic-workflow.agent.md pkg/cli/templates/
@echo "✓ Templates synced successfully"

# Sync shell scripts from actions/setup/sh to pkg/workflow/sh
.PHONY: sync-shell-scripts
sync-shell-scripts:
@echo "Syncing shell scripts from actions/setup/sh to pkg/workflow/sh..."
@mkdir -p pkg/workflow/sh
@cp actions/setup/sh/*.sh pkg/workflow/sh/
@echo "✓ Shell scripts synced successfully"

# Sync JavaScript files from actions/setup/js to pkg/workflow/js
.PHONY: sync-js-scripts
sync-js-scripts:
@echo "Syncing JavaScript files from actions/setup/js to pkg/workflow/js..."
@mkdir -p pkg/workflow/js
@cp actions/setup/js/*.cjs pkg/workflow/js/
@cp actions/setup/js/*.json pkg/workflow/js/ 2>/dev/null || true
@echo "✓ JavaScript files synced successfully"

# Sync action pins from .github/aw to pkg/workflow/data
.PHONY: sync-action-pins
sync-action-pins:
Expand Down Expand Up @@ -567,6 +584,8 @@ help:
@echo " install - Install binary locally"
@echo " sync-templates - Sync templates from .github to pkg/cli/templates (runs automatically during build)"
@echo " sync-action-pins - Sync actions-lock.json from .github/aw to pkg/workflow/data (runs automatically during build)"
@echo " sync-shell-scripts - Sync shell scripts from actions/setup/sh to pkg/workflow/sh (runs automatically during build)"
@echo " sync-js-scripts - Sync JavaScript files from actions/setup/js to pkg/workflow/js (runs automatically during build)"
@echo " update - Update GitHub Actions and workflows, sync action pins, and rebuild binary"
@echo " fix - Apply automatic codemod-style fixes to workflow files (depends on build)"
@echo " recompile - Recompile all workflow files (runs init, depends on build)"
Expand Down
8 changes: 4 additions & 4 deletions actions/setup/js/add_copilot_reviewer.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ const COPILOT_REVIEWER_BOT = "copilot-pull-request-reviewer[bot]";

async function main() {
// Validate required environment variables
const prNumberStr = process.env.PR_NUMBER;
const prNumberStr = process.env.PR_NUMBER?.trim();

if (!prNumberStr || prNumberStr.trim() === "") {
if (!prNumberStr) {
core.setFailed("PR_NUMBER environment variable is required but not set");
return;
}

const prNumber = parseInt(prNumberStr.trim(), 10);
const prNumber = parseInt(prNumberStr, 10);
if (isNaN(prNumber) || prNumber <= 0) {
core.setFailed(`Invalid PR_NUMBER: ${prNumberStr}. Must be a positive integer.`);
return;
Expand Down Expand Up @@ -52,7 +52,7 @@ Successfully added Copilot as a reviewer to PR #${prNumber}.
)
.write();
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorMessage = error?.message ?? String(error);
core.error(`Failed to add Copilot as reviewer: ${errorMessage}`);
core.setFailed(`Failed to add Copilot as reviewer to PR #${prNumber}: ${errorMessage}`);
}
Expand Down
5 changes: 1 addition & 4 deletions actions/setup/js/mcp_http_transport.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -272,17 +272,14 @@ class MCPHTTPTransport {
res.writeHead(200, headers);
res.end(JSON.stringify(response));
} catch (error) {
// Log the full error with stack trace on the server for debugging
this.logger.debugError("Error in handleRequest: ", error);
if (!res.headersSent) {
res.writeHead(500, { "Content-Type": "application/json" });
// Send a generic error message to the client to avoid exposing stack traces
res.end(
JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32603,
message: "Internal server error",
message: error instanceof Error ? error.message : String(error),
},
id: null,
})
Expand Down
4 changes: 1 addition & 3 deletions actions/setup/js/safe_inputs_mcp_server_http.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -220,17 +220,15 @@ async function startHttpServer(configPath, options = {}) {
// Let the transport handle the request
await transport.handleRequest(req, res, body);
} catch (error) {
// Log the full error with stack trace on the server for debugging
logger.debugError("Error handling request: ", error);
if (!res.headersSent) {
res.writeHead(500, { "Content-Type": "application/json" });
// Send a generic error message to the client to avoid exposing stack traces
res.end(
JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32603,
message: "Internal server error",
message: error instanceof Error ? error.message : String(error),
},
id: null,
})
Expand Down
95 changes: 28 additions & 67 deletions pkg/cli/actions_build_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,26 +129,8 @@ func ActionsCleanCommand() error {
}
}

// Clean js/ and sh/ directories for setup action
if actionName == "setup" {
jsDir := filepath.Join(actionsDir, actionName, "js")
if _, err := os.Stat(jsDir); err == nil {
if err := os.RemoveAll(jsDir); err != nil {
return fmt.Errorf("failed to remove %s: %w", jsDir, err)
}
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf(" ✓ Removed %s/js/", actionName)))
cleanedCount++
}

shDir := filepath.Join(actionsDir, actionName, "sh")
if _, err := os.Stat(shDir); err == nil {
if err := os.RemoveAll(shDir); err != nil {
return fmt.Errorf("failed to remove %s: %w", shDir, err)
}
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf(" ✓ Removed %s/sh/", actionName)))
cleanedCount++
}
}
// For setup action, both js/ and sh/ directories are source of truth (NOT generated)
// Do not clean them
}

fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("✨ Cleanup complete (%d files removed)", cleanedCount)))
Expand Down Expand Up @@ -342,65 +324,44 @@ func buildSetupSafeOutputsAction(actionsDir, actionName string) error {
return nil
}

// buildSetupAction builds the setup action by copying JavaScript files to js/ directory
// and shell scripts to sh/ directory
// buildSetupAction builds the setup action by checking that source files exist.
// Note: Both JavaScript and shell scripts are source of truth in actions/setup/js/ and actions/setup/sh/
// They get synced to pkg/workflow/js/ and pkg/workflow/sh/ during the build process via Makefile targets.
func buildSetupAction(actionsDir, actionName string) error {
actionPath := filepath.Join(actionsDir, actionName)
jsDir := filepath.Join(actionPath, "js")
shDir := filepath.Join(actionPath, "sh")

// Get dependencies for this action
dependencies := getActionDependencies(actionName)
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf(" ✓ Found %d JavaScript dependencies", len(dependencies))))

// Get all JavaScript sources
sources := workflow.GetJavaScriptSources()

// Create js directory if it doesn't exist
if err := os.MkdirAll(jsDir, 0755); err != nil {
return fmt.Errorf("failed to create js directory: %w", err)
}

// Copy each dependency file to the js directory
copiedCount := 0
for _, dep := range dependencies {
if content, ok := sources[dep]; ok {
destPath := filepath.Join(jsDir, dep)
if err := os.WriteFile(destPath, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to write %s: %w", dep, err)
// JavaScript files in actions/setup/js/ are the source of truth
if _, err := os.Stat(jsDir); err == nil {
// Count JavaScript files
entries, err := os.ReadDir(jsDir)
if err == nil {
jsCount := 0
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".cjs") {
jsCount++
}
}
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf(" - %s", dep)))
copiedCount++
} else {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf(" ⚠ Warning: Could not find %s", dep)))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf(" ✓ JavaScript files in js/ (source of truth): %d", jsCount)))
}
}

fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf(" ✓ Copied %d files to js/", copiedCount)))

// Get bundled shell scripts
shellScripts := workflow.GetBundledShellScripts()
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf(" ✓ Found %d shell scripts", len(shellScripts))))

// Create sh directory if it doesn't exist
if err := os.MkdirAll(shDir, 0755); err != nil {
return fmt.Errorf("failed to create sh directory: %w", err)
}

// Copy each shell script to the sh directory
shCopiedCount := 0
for filename, content := range shellScripts {
destPath := filepath.Join(shDir, filename)
// Shell scripts should be executable (0755)
if err := os.WriteFile(destPath, []byte(content), 0755); err != nil {
return fmt.Errorf("failed to write %s: %w", filename, err)
// Shell scripts in actions/setup/sh/ are the source of truth
if _, err := os.Stat(shDir); err == nil {
// Count shell scripts
entries, err := os.ReadDir(shDir)
if err == nil {
shCount := 0
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".sh") {
shCount++
}
}
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf(" ✓ Shell scripts in sh/ (source of truth): %d", shCount)))
}
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf(" - %s", filename)))
shCopiedCount++
}

fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf(" ✓ Copied %d shell scripts to sh/", shCopiedCount)))

return nil
}

Expand Down
Loading
Loading