Creating Plugins
Build your own plugins to extend Crow CI with custom functionality.
Plugin Architecture
Section titled “Plugin Architecture”A Crow CI plugin is a container image that:
- Receives configuration via
PLUGIN_*environment variables - Executes a fixed set of commands via
ENTRYPOINT - Cannot have its
CMDoverridden by users
This architecture ensures plugins execute predictably, making them safe for handling secrets.
Quick Start
Section titled “Quick Start”Here’s a minimal plugin structure:
FROM alpine:latest
# Install dependenciesRUN apk add --no-cache bash curl
# Copy plugin scriptCOPY plugin.sh /usr/local/bin/plugin.shRUN chmod +x /usr/local/bin/plugin.sh
# Fixed entrypoint - cannot be overriddenENTRYPOINT ["/usr/local/bin/plugin.sh"]#!/bin/bash# Read settings from environmentecho "Server: ${PLUGIN_SERVER}"echo "Message: ${PLUGIN_MESSAGE}"
# Plugin logic herecurl -X POST "${PLUGIN_SERVER}/api/notify" \ -H "Authorization: Bearer ${PLUGIN_TOKEN}" \ -d "message=${PLUGIN_MESSAGE}"Settings Reference
Section titled “Settings Reference”How Settings Work
Section titled “How Settings Work”When a user configures a plugin:
steps: - name: notify image: my-plugin settings: server: https://api.example.com message: Hello World retry_count: 3Crow transforms settings into environment variables:
| Setting | Environment Variable |
|---|---|
server | PLUGIN_SERVER |
message | PLUGIN_MESSAGE |
retry_count | PLUGIN_RETRY_COUNT |
Naming Rules
Section titled “Naming Rules”- Settings are uppercased:
myValue→PLUGIN_MYVALUE - Dashes become underscores:
my-setting→PLUGIN_MY_SETTING - Nested objects become JSON strings
Complex Values
Section titled “Complex Values”Objects and arrays are JSON-serialized:
settings: targets: - production - staging config: retries: 3 timeout: 30Results in:
PLUGIN_TARGETS='["production","staging"]'PLUGIN_CONFIG='{"retries":"3","timeout":"30"}'
Parse these in your plugin:
#!/bin/bash# Parse JSON arrayTARGETS=$(echo "$PLUGIN_TARGETS" | jq -r '.[]')
# Parse JSON objectRETRIES=$(echo "$PLUGIN_CONFIG" | jq -r '.retries')Security Best Practices
Section titled “Security Best Practices”No Environment Key
Section titled “No Environment Key”Crow prohibits using environment: with plugins for security. All inputs must come through settings:.
# ❌ Not allowedsteps: - name: deploy image: my-plugin environment: SECRET_KEY: value
# ✅ Required approachsteps: - name: deploy image: my-plugin settings: secret_key: valueSecret Handling
Section titled “Secret Handling”Your plugin receives secrets as regular environment variables. Best practices:
- Never log secrets - Avoid echoing sensitive values
- Document required secrets - Clearly list what credentials are needed
- Minimize secret scope - Only request what’s necessary
#!/bin/bash
# Bad - logs the tokenecho "Using token: ${PLUGIN_TOKEN}"
# Good - confirms token exists without exposing itif [[ -z "${PLUGIN_TOKEN}" ]]; then echo "Error: TOKEN setting is required" exit 1fiecho "Token configured ✓"Validate Inputs
Section titled “Validate Inputs”Always validate and sanitize inputs:
#!/bin/bashset -euo pipefail
# Required settings: "${PLUGIN_SERVER:?SERVER setting is required}": "${PLUGIN_TOKEN:?TOKEN setting is required}"
# Optional with defaultsPLUGIN_TIMEOUT="${PLUGIN_TIMEOUT:-30}"PLUGIN_RETRIES="${PLUGIN_RETRIES:-3}"
# Validate formatif [[ ! "${PLUGIN_SERVER}" =~ ^https?:// ]]; then echo "Error: SERVER must be a valid URL" exit 1fiLanguage Examples
Section titled “Language Examples”Go Plugin
Section titled “Go Plugin”Go is ideal for plugins due to single-binary output:
package main
import ( "fmt" "os")
func main() { server := os.Getenv("PLUGIN_SERVER") token := os.Getenv("PLUGIN_TOKEN")
if server == "" || token == "" { fmt.Println("Error: SERVER and TOKEN are required") os.Exit(1) }
// Plugin logic here fmt.Printf("Deploying to %s\n", server)}FROM golang:1.22-alpine AS builderWORKDIR /appCOPY . .RUN CGO_ENABLED=0 go build -o /plugin
FROM alpine:latestCOPY --from=builder /plugin /usr/local/bin/pluginENTRYPOINT ["/usr/local/bin/plugin"]Python Plugin
Section titled “Python Plugin”#!/usr/bin/env python3import osimport sysimport json
def main(): server = os.environ.get('PLUGIN_SERVER') token = os.environ.get('PLUGIN_TOKEN')
if not server or not token: print("Error: SERVER and TOKEN are required") sys.exit(1)
# Handle complex settings (JSON) targets_json = os.environ.get('PLUGIN_TARGETS', '[]') targets = json.loads(targets_json)
# Plugin logic here print(f"Deploying to {server}") for target in targets: print(f" - {target}")
if __name__ == '__main__': main()Node.js Plugin
Section titled “Node.js Plugin”#!/usr/bin/env nodeconst server = process.env.PLUGIN_SERVER;const token = process.env.PLUGIN_TOKEN;
if (!server || !token) { console.error('Error: SERVER and TOKEN are required'); process.exit(1);}
// Handle complex settingsconst targets = JSON.parse(process.env.PLUGIN_TARGETS || '[]');
// Plugin logic hereconsole.log(`Deploying to ${server}`);targets.forEach(target => console.log(` - ${target}`));Testing Plugins
Section titled “Testing Plugins”Local Testing
Section titled “Local Testing”Test your plugin locally by setting environment variables:
docker build -t my-plugin .
docker run --rm \ -e PLUGIN_SERVER=https://api.example.com \ -e PLUGIN_TOKEN=test-token \ -e PLUGIN_MESSAGE="Hello World" \ my-pluginIntegration Testing
Section titled “Integration Testing”Create a test pipeline:
steps: - name: test-plugin image: my-plugin:dev settings: server: https://api.example.com token: from_secret: test_token message: "Test message"Publishing Plugins
Section titled “Publishing Plugins”Container Registry
Section titled “Container Registry”Push your plugin to a container registry:
docker build -t codeberg.org/myuser/my-plugin:1.0.0 .docker push codeberg.org/myuser/my-plugin:1.0.0Documentation
Section titled “Documentation”Create a README.md with:
- Description - What the plugin does
- Settings - All available settings with types and defaults
- Secrets - Required credentials
- Examples - Working pipeline configurations
- Changelog - Version history
Versioning
Section titled “Versioning”Use semantic versioning for plugin releases:
1.0.0- Initial stable release1.1.0- New features, backwards compatible2.0.0- Breaking changes
Tag your images with both specific versions and major version aliases:
docker tag my-plugin:1.2.3 my-plugin:1docker tag my-plugin:1.2.3 my-plugin:1.2docker push my-plugin:1.2.3docker push my-plugin:1.2docker push my-plugin:1Getting Listed
Section titled “Getting Listed”To add your plugin to the available plugins page, open a pull request to the Crow CI repository.