# Plugin Development Guide
## Overview
This guide covers best practices for developing ProGit plugins using the Apache 2.0 licensed SDK.
## Plugin Types
### Lua Plugins
**Best for:**
- Simple integrations (Slack notifications, webhooks)
- Rapid prototyping
- Non-performance-critical tasks
- Scripts that need frequent updates
**Pros:**
- No compilation required
- Easy to distribute and update
- Sandboxed execution
- Familiar scripting language
**Cons:**
- Slower than WASM
- Limited access to system resources
- No static typing
### WASM Plugins
**Best for:**
- Performance-critical operations
- Complex business logic
- Integration with existing Rust/C/C++ code
- Plugins requiring strong typing
**Pros:**
- Near-native performance
- Strongly typed (if written in Rust)
- Sandboxed execution
- Access to rich Rust ecosystem
**Cons:**
- Requires compilation step
- Larger binary size
- More complex development workflow
---
## Plugin Lifecycle
1. **Load**: Plugin file is loaded and parsed
2. **Validate**: Metadata is extracted and validated
3. **Initialize**: `init()` function is called with context
4. **Execute**: Hooks are called as events occur
5. **Unload**: Plugin is cleaned up (automatic)
---
## Hook Patterns
### Issue Lifecycle Hooks
```lua
function on_issue_created(issue)
-- Validate issue data
if not issue.title or issue.title == "" then
return { success = false, error = "Title required" }
end
-- Perform action (e.g., notify team)
notify_slack("New issue: " .. issue.title)
-- Return success with optional metadata
return {
success = true,
notified = true,
timestamp = os.time()
}
end
```
### Sync Hooks
```lua
function on_sync_push(issues)
local synced = 0
local failed = 0
for i, issue in ipairs(issues) do
local result = sync_to_external_system(issue)
if result.success then
synced = synced + 1
else
failed = failed + 1
log_error("Failed to sync issue " .. issue.id .. ": " .. result.error)
end
end
return {
success = failed == 0,
synced = synced,
failed = failed
}
end
```
---
## Configuration
Plugins receive configuration via `context.config`:
```lua
function init()
-- Load from context.config
local api_key = context.config.api_key
-- Fallback to environment variables
if not api_key then
api_key = os.getenv("MY_PLUGIN_API_KEY")
end
-- Validate configuration
if not api_key then
error("API key not configured. Set MY_PLUGIN_API_KEY or add to config")
end
-- Store for later use
_G.api_key = api_key
end
```
User configuration in `.project/config.kdl`:
```kdl
plugins {
my-plugin {
api_key "secret-key-here"
endpoint "https://api.example.com"
enabled true
}
}
```
---
## Error Handling
### Lua
```lua
function on_issue_created(issue)
-- Validate inputs
if not issue or not issue.id then
return { success = false, error = "Invalid issue data" }
end
-- Try operation with error handling
local success, result = pcall(function()
return call_external_api(issue)
end)
if not success then
log_error("API call failed: " .. tostring(result))
return { success = false, error = "External API error" }
end
return { success = true, result = result }
end
```
### WASM (Rust)
```rust
#[no_mangle]
pub extern "C" fn on_issue_created(data_ptr: i32, data_len: i32) -> i32 {
match process_issue(data_ptr, data_len) {
Ok(result) => serialize_result(&result),
Err(e) => {
log_error(&format!("Error: {}", e));
serialize_error(&e)
}
}
}
```
---
## Testing Plugins
### Lua Testing
Create a test harness:
```lua
-- test_my_plugin.lua
local plugin = require("my_plugin")
local test_issue = {
id = "test-123",
title = "Test Issue",
status = "backlog",
tags = {},
created = "2025-12-11T00:00:00Z",
updated = "2025-12-11T00:00:00Z",
metadata = {}
}
local result = plugin.on_issue_created(test_issue)
assert(result.success, "Plugin should succeed")
print("✅ Test passed")
```
### WASM Testing
Use Rust's built-in test framework:
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_issue_processing() {
let issue = Issue {
id: "test-123".to_string(),
title: "Test".to_string(),
// ...
};
let result = process_issue(&issue).unwrap();
assert!(result.success);
}
}
```
---
## Performance Tips
### Lua
1. **Cache expensive operations**:
```lua
local cached_data = nil
function get_data()
if not cached_data then
cached_data = expensive_operation()
end
return cached_data
end
```
2. **Avoid table creation in loops**:
```lua
-- Bad
for i, issue in ipairs(issues) do
local result = { success = true, id = issue.id }
end
-- Good
local result = { success = true }
for i, issue in ipairs(issues) do
result.id = issue.id
process(result)
end
```
3. **Use local variables**:
```lua
-- Bad
function process()
global_var = expensive_calc()
end
-- Good
function process()
local result = expensive_calc()
return result
end
```
### WASM
1. **Minimize allocations**: Reuse buffers where possible
2. **Use `&str` instead of `String`** when you don't need ownership
3. **Batch operations**: Process multiple issues in one call
4. **Profile**: Use `cargo flamegraph` to find bottlenecks
---
## Security Considerations
### Sandboxing
Both Lua and WASM plugins run in sandboxed environments:
- **No direct file system access** (except via SDK APIs)
- **No network access** (except via SDK APIs)
- **No arbitrary code execution**
- **Limited memory**
### Input Validation
Always validate inputs from ProGit:
```lua
function on_issue_created(issue)
-- Validate required fields
assert(type(issue) == "table", "Issue must be a table")
assert(type(issue.id) == "string", "Issue ID must be a string")
assert(#issue.id > 0, "Issue ID cannot be empty")
-- Sanitize user input before external API calls
local safe_title = sanitize(issue.title)
-- Proceed with validated data
return call_api(safe_title)
end
```
### Secrets Management
**Never hardcode secrets**:
```lua
-- ❌ BAD
local api_key = "sk-1234567890abcdef"
-- ✅ GOOD
local api_key = os.getenv("API_KEY") or context.config.api_key
if not api_key then
error("API key not configured")
end
```
---
## Distribution
### Lua Plugins
Distribute as single `.lua` files:
```bash
# Install
cp my_plugin.lua ~/.progit/plugins/
# Or via git
git clone https://github.com/user/progit-plugin-xyz ~/.progit/plugins/xyz
```
### WASM Plugins
Distribute as `.wasm` binaries:
```bash
# Build
cargo build --target wasm32-wasi --release
# Package
tar czf my-plugin-v1.0.0.tar.gz \
target/wasm32-wasi/release/my_plugin.wasm \
README.md \
LICENSE
# Install
tar xzf my-plugin-v1.0.0.tar.gz -C ~/.progit/plugins/
```
---
## Example: Complete Slack Notification Plugin
```lua
-- SPDX-License-Identifier: Apache-2.0
-- slack_notify.lua
plugin = {
name = "slack-notify",
version = "1.0.0",
author = "Your Name",
description = "Send Slack notifications for issue events",
hooks = {
on_issue_created = true,
on_status_changed = true,
}
}
local webhook_url = nil
function init()
webhook_url = context.config.webhook_url or os.getenv("SLACK_WEBHOOK_URL")
if not webhook_url then
error("Slack webhook URL not configured")
end
print("Slack notifications enabled")
end
function on_issue_created(issue)
local message = string.format(
"🎉 New issue created: *%s*\nStatus: %s\nAssignee: %s",
issue.title,
issue.status,
issue.assignee or "Unassigned"
)
return send_slack_message(message)
end
function on_status_changed(issue)
local emoji = issue.status == "done" and "✅" or "🔄"
local message = string.format(
"%s Issue status changed: *%s*\nNew status: %s",
emoji,
issue.title,
issue.status
)
return send_slack_message(message)
end
function send_slack_message(text)
-- In real implementation, use HTTP library
-- For demo, just log
print("Would send to Slack: " .. text)
return { success = true }
end
```
---
## Next Steps
- See [examples/](../examples/) for more plugin examples
- Read [API Reference](api_reference.md) for complete SDK documentation
- Join the community to share your plugins!