re-uploading work

This commit is contained in:
2026-02-04 17:46:30 -06:00
commit 3b14c65998
1388 changed files with 381262 additions and 0 deletions

723
docs/packs/PACK_TESTING.md Normal file
View File

@@ -0,0 +1,723 @@
# Pack Testing Framework
**Complete guide to testing Attune packs programmatically**
---
## Overview
The Pack Testing Framework enables automatic validation of packs during installation and development. Tests are defined in `pack.yaml` and executed by the worker service or CLI tool.
**Benefits:**
- ✅ Fail-fast pack installation (catch issues before deployment)
- ✅ Validate dependencies in target environment
- ✅ Audit trail of test results
- ✅ Quality assurance for pack ecosystem
- ✅ CI/CD integration ready
---
## Quick Start
### 1. Add Testing Configuration to pack.yaml
```yaml
testing:
enabled: true
discovery:
method: "directory"
path: "tests"
runners:
shell:
type: "script"
entry_point: "tests/run_tests.sh"
timeout: 60
result_format: "simple"
python:
type: "unittest"
entry_point: "tests/test_actions.py"
timeout: 120
result_format: "simple"
min_pass_rate: 1.0
on_failure: "block"
```
### 2. Create Test Files
**Shell Test Runner** (`tests/run_tests.sh`):
```bash
#!/bin/bash
set -e
PASSED=0
FAILED=0
TOTAL=0
# Run your tests here
./actions/my_action.sh --test
if [ $? -eq 0 ]; then
PASSED=$((PASSED + 1))
else
FAILED=$((FAILED + 1))
fi
TOTAL=$((TOTAL + 1))
# Output results (required format)
echo "Total Tests: $TOTAL"
echo "Passed: $PASSED"
echo "Failed: $FAILED"
exit $FAILED
```
**Python Test Runner** (`tests/test_actions.py`):
```python
import unittest
from actions import my_action
class TestMyAction(unittest.TestCase):
def test_basic_execution(self):
result = my_action.run({"param": "value"})
self.assertEqual(result["status"], "success")
def test_error_handling(self):
with self.assertRaises(ValueError):
my_action.run({"invalid": "params"})
if __name__ == '__main__':
unittest.main()
```
### 3. Run Tests
```bash
# Test a pack before installation
attune pack test ./packs/my_pack
# Test an installed pack
attune pack test my_pack
# Verbose output
attune pack test my_pack --verbose
# JSON output for CI/CD
attune pack test my_pack --output json
```
---
## Testing Configuration
### Pack.yaml Testing Section
```yaml
testing:
# Enable/disable testing
enabled: true
# Test discovery configuration
discovery:
method: "directory" # or "manifest", "executable"
path: "tests" # relative to pack root
# Test runners by runtime type
runners:
shell:
type: "script"
entry_point: "tests/run_tests.sh"
timeout: 60 # seconds
result_format: "simple"
python:
type: "unittest" # or "pytest"
entry_point: "tests/test_actions.py"
timeout: 120
result_format: "simple"
node:
type: "jest"
entry_point: "tests/actions.test.js"
timeout: 90
result_format: "json"
# Test result expectations
result_path: "tests/results/"
min_pass_rate: 1.0 # 100% tests must pass
on_failure: "block" # or "warn"
```
### Configuration Options
#### `enabled` (boolean)
- `true`: Tests will be executed
- `false`: Tests will be skipped
#### `discovery.method` (string)
- `"directory"`: Discover tests in specified directory (recommended)
- `"manifest"`: List tests explicitly in pack.yaml
- `"executable"`: Run a single test discovery command
#### `runners.<name>.type` (string)
- `"script"`: Shell script execution
- `"unittest"`: Python unittest framework
- `"pytest"`: Python pytest framework
- `"jest"`: JavaScript Jest framework
#### `runners.<name>.result_format` (string)
- `"simple"`: Parse "Total Tests: X, Passed: Y, Failed: Z" format
- `"json"`: Parse structured JSON output
- `"junit-xml"`: Parse JUnit XML format (pytest, Jest)
- `"tap"`: Parse Test Anything Protocol format
#### `on_failure` (string)
- `"block"`: Prevent pack installation if tests fail
- `"warn"`: Allow installation but show warning
---
## Test Output Formats
### Simple Format (Default)
Your test runner must output these lines:
```
Total Tests: 36
Passed: 35
Failed: 1
Skipped: 0
```
The executor will:
1. Parse these counts from stdout/stderr
2. Use exit code to determine success/failure
3. Exit code 0 = success, non-zero = failure
### JSON Format (Advanced)
Output structured JSON:
```json
{
"total": 36,
"passed": 35,
"failed": 1,
"skipped": 0,
"duration_ms": 12345,
"tests": [
{
"name": "test_basic_execution",
"status": "passed",
"duration_ms": 123,
"output": "..."
}
]
}
```
### JUnit XML Format (Future)
For pytest and Jest, use built-in JUnit reporters:
```bash
# pytest
pytest --junit-xml=results.xml
# Jest
jest --reporters=jest-junit
```
---
## CLI Commands
### Test a Pack
```bash
# Basic usage
attune pack test <pack>
# Local pack directory
attune pack test ./packs/my_pack
# Installed pack
attune pack test my_pack
# From pack root directory
cd packs/my_pack
attune pack test .
```
### Output Formats
```bash
# Human-readable table (default)
attune pack test my_pack
# Verbose with test case details
attune pack test my_pack --verbose
# Detailed with stdout/stderr
attune pack test my_pack --detailed
# JSON for scripting
attune pack test my_pack --output json
# YAML output
attune pack test my_pack --output yaml
```
### Exit Codes
- `0`: All tests passed
- `1`: One or more tests failed
- `2`: Test execution error (timeout, missing config, etc.)
Perfect for CI/CD pipelines:
```bash
#!/bin/bash
if attune pack test my_pack; then
echo "✅ Tests passed, deploying..."
attune pack install ./packs/my_pack
else
echo "❌ Tests failed, aborting deployment"
exit 1
fi
```
---
## Examples
### Example 1: Core Pack (Complete)
See `packs/core/` for a complete example:
- **Configuration**: `packs/core/pack.yaml` (testing section)
- **Shell Tests**: `packs/core/tests/run_tests.sh` (36 tests)
- **Python Tests**: `packs/core/tests/test_actions.py` (38 tests)
- **Documentation**: `packs/core/tests/README.md`
Test execution:
```bash
$ attune pack test packs/core
🧪 Testing Pack: core v1.0.0
Test Results:
─────────────────────────────────────────────
Total Tests: 2
✓ Passed: 2
✗ Failed: 0
○ Skipped: 0
Pass Rate: 100.0%
Duration: 25542ms
─────────────────────────────────────────────
✓ ✅ All tests passed: 2/2
```
### Example 2: Python Pack with pytest
```yaml
# pack.yaml
testing:
enabled: true
runners:
python:
type: "pytest"
entry_point: "tests/"
timeout: 180
result_format: "simple"
```
```python
# tests/test_mypack.py
import pytest
from actions.my_action import execute
def test_success():
result = execute({"input": "value"})
assert result["status"] == "success"
def test_validation():
with pytest.raises(ValueError):
execute({"invalid": None})
@pytest.mark.skip(reason="Not implemented yet")
def test_future_feature():
pass
```
### Example 3: Shell Script Tests
```bash
#!/bin/bash
# tests/run_tests.sh
set -e
TOTAL=0
PASSED=0
FAILED=0
test_action() {
local name="$1"
local command="$2"
local expected_exit="$3"
TOTAL=$((TOTAL + 1))
echo -n "Testing $name... "
if eval "$command"; then
actual_exit=$?
else
actual_exit=$?
fi
if [ "$actual_exit" -eq "$expected_exit" ]; then
echo "PASS"
PASSED=$((PASSED + 1))
else
echo "FAIL (exit: $actual_exit, expected: $expected_exit)"
FAILED=$((FAILED + 1))
fi
}
# Run tests
test_action "basic_echo" "./actions/echo.sh 'Hello'" 0
test_action "invalid_param" "./actions/echo.sh" 1
test_action "http_request" "./actions/http.py --url=https://httpbin.org/get" 0
# Output results
echo ""
echo "Total Tests: $TOTAL"
echo "Passed: $PASSED"
echo "Failed: $FAILED"
exit $FAILED
```
---
## Best Practices
### 1. Always Include Tests
Every pack should have tests. Minimum recommended:
- Test each action's success path
- Test error handling (invalid inputs)
- Test dependencies are available
### 2. Use Descriptive Test Names
```python
# Good
def test_http_request_returns_json_on_success(self):
pass
# Bad
def test1(self):
pass
```
### 3. Test Exit Codes
Ensure your tests return proper exit codes:
- `0` = success
- Non-zero = failure
```bash
#!/bin/bash
# tests/run_tests.sh
# Run tests
python -m unittest discover -s tests
# Capture exit code
TEST_EXIT=$?
# Output required format
echo "Total Tests: 10"
echo "Passed: 9"
echo "Failed: 1"
# Exit with test result
exit $TEST_EXIT
```
### 4. Test Dependencies
Validate required libraries are available:
```python
def test_dependencies(self):
"""Test required libraries are installed"""
try:
import requests
import croniter
except ImportError as e:
self.fail(f"Missing dependency: {e}")
```
### 5. Use Timeouts
Set realistic timeouts for test execution:
```yaml
runners:
python:
timeout: 120 # 2 minutes max
```
### 6. Mock External Services
Don't rely on external services in tests:
```python
from unittest.mock import patch, MagicMock
@patch('requests.get')
def test_http_request(self, mock_get):
mock_get.return_value = MagicMock(
status_code=200,
json=lambda: {"status": "ok"}
)
result = my_action.execute()
self.assertEqual(result["status"], "success")
```
---
## Troubleshooting
### Tests Fail with "Entry point not found"
**Problem**: Test file doesn't exist or path is wrong
**Solution**:
```bash
# Check file exists
ls -la packs/my_pack/tests/
# Verify path in pack.yaml is relative to pack root
entry_point: "tests/run_tests.sh" # ✓ Correct
entry_point: "run_tests.sh" # ✗ Wrong
```
### Tests Timeout
**Problem**: Tests take too long
**Solutions**:
1. Increase timeout in pack.yaml
2. Optimize slow tests
3. Mock external dependencies
4. Split into separate test suites
```yaml
runners:
quick:
timeout: 30
integration:
timeout: 300 # Longer for integration tests
```
### Parse Errors
**Problem**: Test output format not recognized
**Solution**: Ensure output includes required lines:
```bash
# Required output format
echo "Total Tests: $TOTAL"
echo "Passed: $PASSED"
echo "Failed: $FAILED"
```
### Exit Code 127 (Command not found)
**Problem**: Test runner executable not found
**Solutions**:
1. Make test script executable: `chmod +x tests/run_tests.sh`
2. Use full interpreter path: `/bin/bash tests/run_tests.sh`
3. Check shebang line: `#!/bin/bash`
---
## Architecture
### Components
```
CLI (attune pack test)
Worker Test Executor
Runtime Manager (shell, python, node)
Test Runners (unittest, pytest, jest)
Output Parser (simple, json, junit, tap)
Test Results (structured data)
Database (pack_test_execution table)
```
### Data Flow
```
pack.yaml (testing config)
TestConfig (parsed)
TestExecutor.execute_pack_tests()
├─ execute_test_suite(shell)
│ └─ parse_simple_output()
└─ execute_test_suite(python)
└─ parse_simple_output()
PackTestResult (aggregated)
CLI display / JSON output / Database storage
```
### Database Schema
Tests are stored in `pack_test_execution` table:
```sql
CREATE TABLE attune.pack_test_execution (
id BIGSERIAL PRIMARY KEY,
pack_id BIGINT NOT NULL REFERENCES attune.pack(id),
pack_version TEXT NOT NULL,
execution_time TIMESTAMPTZ NOT NULL,
trigger_reason TEXT NOT NULL,
total_tests INT NOT NULL,
passed INT NOT NULL,
failed INT NOT NULL,
skipped INT NOT NULL,
pass_rate DOUBLE PRECISION NOT NULL,
duration_ms BIGINT NOT NULL,
result JSONB NOT NULL
);
```
---
## API (Future)
### Test Execution Endpoint
```http
POST /api/v1/packs/{pack_ref}/test
```
Response:
```json
{
"data": {
"id": 123,
"packRef": "core",
"packVersion": "1.0.0",
"totalTests": 74,
"passed": 74,
"failed": 0,
"passRate": 1.0,
"durationMs": 25000
}
}
```
### Test History Endpoint
```http
GET /api/v1/packs/{pack_ref}/tests?limit=10
```
### Latest Test Result
```http
GET /api/v1/packs/{pack_ref}/tests/latest
```
---
## CI/CD Integration
### GitHub Actions
```yaml
name: Test Pack
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Attune CLI
run: |
curl -L https://get.attune.io | sh
export PATH="$HOME/.attune/bin:$PATH"
- name: Test Pack
run: |
attune pack test ./packs/my_pack --output json > results.json
- name: Upload Results
uses: actions/upload-artifact@v3
with:
name: test-results
path: results.json
```
### GitLab CI
```yaml
test-pack:
stage: test
script:
- attune pack test ./packs/my_pack
artifacts:
reports:
junit: test-results.xml
```
---
## Related Documentation
- **Design Document**: `docs/pack-testing-framework.md`
- **Core Pack Tests**: `packs/core/tests/README.md`
- **Database Schema**: `migrations/012_add_pack_test_results.sql`
- **API Documentation**: `docs/api-packs.md`
---
## Changelog
- **2026-01-22**: Initial implementation (Phases 1 & 2)
- Worker test executor
- CLI pack test command
- Simple output parser
- Core pack validation (76 tests)
---
## Support
For issues or questions:
- GitHub Issues: https://github.com/attune-io/attune/issues
- Documentation: https://docs.attune.io/packs/testing
- Community: https://community.attune.io

View File

@@ -0,0 +1,258 @@
# Git Pack Installation - Quick Reference
**Quick commands and examples for installing packs from git repositories**
---
## Installation Methods
### Web UI
```
Packs → Add Pack ▼ → Install from Remote → Git Repository
```
### CLI
```bash
attune pack install <git-url> [--ref <branch|tag|commit>] [options]
```
### API
```bash
POST /api/v1/packs/install
```
---
## Quick Examples
### Public GitHub Repository
```bash
# Latest from default branch
attune pack install https://github.com/example/pack-slack.git
# Specific version tag
attune pack install https://github.com/example/pack-slack.git --ref v2.1.0
# Specific branch
attune pack install https://github.com/example/pack-slack.git --ref develop
# Specific commit
attune pack install https://github.com/example/pack-slack.git --ref a1b2c3d
```
### Private Repository (SSH)
```bash
# SSH URL with tag
attune pack install git@github.com:myorg/private-pack.git --ref v1.0.0
# SSH URL with branch
attune pack install git@github.com:myorg/private-pack.git --ref main
```
### Installation Options
```bash
# Force reinstall (replace existing)
attune pack install <url> --force
# Skip tests
attune pack install <url> --skip-tests
# Skip dependency validation
attune pack install <url> --skip-deps
# All options combined
attune pack install <url> --ref v1.0.0 --force --skip-tests --skip-deps
```
---
## Git URL Formats
### HTTPS
```
✓ https://github.com/username/pack-name.git
✓ https://gitlab.com/username/pack-name.git
✓ https://bitbucket.org/username/pack-name.git
✓ https://git.example.com/username/pack-name.git
```
### SSH
```
✓ git@github.com:username/pack-name.git
✓ git@gitlab.com:username/pack-name.git
✓ user@server:path/to/pack.git
```
---
## Git References
| Type | Example | Description |
|------|---------|-------------|
| Tag | `v1.2.3` | Semantic version tag |
| Tag | `release-2024-01-27` | Release tag |
| Branch | `main` | Main branch |
| Branch | `develop` | Development branch |
| Branch | `feature/xyz` | Feature branch |
| Commit | `a1b2c3d4e5f6...` | Full commit hash |
| Commit | `a1b2c3d` | Short commit hash (7+ chars) |
| None | (omit --ref) | Default branch (shallow) |
---
## Installation Flags
| Flag | Effect | Use When |
|------|--------|----------|
| `--force` | Replace existing pack, bypass checks | Upgrading, testing |
| `--skip-tests` | Don't run pack tests | Tests slow/unavailable |
| `--skip-deps` | Don't validate dependencies | Custom environment |
⚠️ **Warning**: Use flags cautiously in production!
---
## Required Pack Structure
```
repository/
├── pack.yaml ← Required
├── actions/ ← Optional
├── sensors/ ← Optional
└── ...
```
OR
```
repository/
└── pack/
├── pack.yaml ← Required
└── ...
```
---
## API Request
```bash
curl -X POST http://localhost:8080/api/v1/packs/install \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"source": "https://github.com/example/pack-slack.git",
"ref_spec": "v2.1.0",
"force": false,
"skip_tests": false,
"skip_deps": false
}'
```
---
## Common Workflows
### Production Install
```bash
# Install specific stable version
attune pack install https://github.com/myorg/pack-prod.git --ref v1.0.0
```
### Development Testing
```bash
# Install from feature branch, skip checks
attune pack install https://github.com/myorg/pack-dev.git \
--ref feature/new-action \
--force \
--skip-tests
```
### CI/CD Pipeline
```bash
# Install from current commit
attune pack install https://github.com/$REPO.git \
--ref $COMMIT_SHA \
--force
```
---
## Troubleshooting
| Error | Solution |
|-------|----------|
| Permission denied | Check SSH keys or HTTPS credentials |
| Ref not found | Verify branch/tag exists and is pushed |
| pack.yaml not found | Ensure file exists at root or in pack/ |
| Dependencies missing | Install dependencies or use --skip-deps |
| Tests failed | Fix tests or use --skip-tests or --force |
---
## Security Best Practices
**DO**:
- Use specific tags in production (`v1.2.3`)
- Use SSH keys for private repos
- Review code before installing
- Rotate access tokens regularly
**DON'T**:
- Embed credentials in URLs
- Install from `main` branch in production
- Skip validation without review
- Use force mode carelessly
---
## Web UI Workflow
1. Navigate to **Packs** page
2. Click **Add Pack** dropdown button
3. Select **Install from Remote**
4. Choose **Git Repository** source type
5. Enter repository URL
6. (Optional) Enter git reference
7. (Optional) Configure installation options
8. Click **Install Pack**
9. Wait for completion and redirect
---
## Quick Tips
💡 **Version Control**: Always use tags for production (e.g., `v1.0.0`)
💡 **Testing**: Test from feature branch first, then install from tag
💡 **SSH Setup**: Configure SSH keys once, use forever
💡 **Shallow Clone**: Omit ref for faster install (default branch only)
💡 **Commit Hash**: Most specific reference, guaranteed reproducibility
---
## Related Commands
```bash
# List installed packs
attune pack list
# View pack details
attune pack show <pack-ref>
# Run pack tests
attune pack test <pack-ref>
# Uninstall pack
attune pack uninstall <pack-ref>
```
---
## More Information
📖 Full documentation: `docs/packs/pack-installation-git.md`
📖 Pack structure: `docs/packs/pack-structure.md`
📖 Pack registry spec: `docs/packs/pack-registry-spec.md`

View File

@@ -0,0 +1,680 @@
# Core Pack Integration Guide
**Last Updated**: 2026-01-20
**Status**: Implementation Guide
---
## Overview
This document outlines the steps required to integrate the filesystem-based core pack with the Attune platform. The core pack has been implemented in `packs/core/` and needs to be loaded into the system during startup or installation.
---
## Current State
### ✅ Completed
- **Pack Structure**: Complete filesystem-based pack in `packs/core/`
- **Actions**: 4 actions implemented (echo, sleep, noop, http_request)
- **Triggers**: 3 trigger type definitions (intervaltimer, crontimer, datetimetimer)
- **Sensors**: 1 sensor implementation (interval_timer_sensor)
- **Documentation**: Comprehensive README and pack structure docs
- **Testing**: Manual validation of action execution
### ⏳ Pending Integration
- **Pack Loader**: Service to parse and register pack components
- **Database Registration**: Insert pack metadata and components into PostgreSQL
- **Worker Integration**: Execute actions from pack directory
- **Sensor Integration**: Load and run sensors from pack directory
- **Startup Process**: Automatic pack loading on service startup
---
## Integration Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Attune Services │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Pack Loader Service │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Parse │ │ Validate │ │ Register │ │ │
│ │ │ pack.yaml│→ │ Schemas │→ │ in DB │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ PostgreSQL Database │ │
│ │ ┌──────┐ ┌────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Pack │ │ Action │ │ Trigger │ │ Sensor │ │ │
│ │ │ Meta │ │ Meta │ │ Meta │ │ Meta │ │ │
│ │ └──────┘ └────────┘ └─────────┘ └─────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Worker Service │ │ Sensor Service │ │
│ │ ┌────────────┐ │ │ ┌────────────┐ │ │
│ │ │ Execute │ │ │ │ Run │ │ │
│ │ │ Actions │ │ │ │ Sensors │ │ │
│ │ │ from Pack │ │ │ │ from Pack │ │ │
│ │ └────────────┘ │ │ └────────────┘ │ │
│ └──────────────────┘ └──────────────────┘ │
│ ↑ ↑ │
└───────────┼──────────────────────────────────┼──────────────┘
│ │
└──────────────┬───────────────────┘
┌─────────────────┐
│ Filesystem │
│ packs/core/ │
│ - actions/ │
│ - sensors/ │
│ - triggers/ │
└─────────────────┘
```
---
## Implementation Steps
### Phase 1: Pack Loader Service
Create a pack loader service in `crates/common/src/pack_loader.rs` (or as a separate crate).
#### 1.1 Pack Parser
```rust
// Parse pack.yaml manifest
pub struct PackLoader {
pack_dir: PathBuf,
}
pub struct PackManifest {
pub ref_: String,
pub label: String,
pub description: String,
pub version: String,
pub author: Option<String>,
pub email: Option<String>,
pub system: bool,
pub enabled: bool,
pub conf_schema: Option<serde_json::Value>,
pub config: Option<serde_json::Value>,
pub meta: Option<serde_json::Value>,
pub tags: Vec<String>,
pub runtime_deps: Vec<String>,
}
impl PackLoader {
pub fn load_manifest(&self) -> Result<PackManifest> {
// Parse packs/{pack_name}/pack.yaml
}
}
```
#### 1.2 Component Parsers
```rust
pub struct ActionMetadata {
pub name: String,
pub ref_: String,
pub description: String,
pub runner_type: String,
pub entry_point: String,
pub enabled: bool,
pub parameters: Option<serde_json::Value>,
pub output_schema: Option<serde_json::Value>,
pub tags: Vec<String>,
}
pub struct TriggerMetadata {
pub name: String,
pub ref_: String,
pub description: String,
pub type_: String,
pub enabled: bool,
pub parameters_schema: Option<serde_json::Value>,
pub payload_schema: Option<serde_json::Value>,
pub tags: Vec<String>,
}
pub struct SensorMetadata {
pub name: String,
pub ref_: String,
pub description: String,
pub runner_type: String,
pub entry_point: String,
pub trigger_types: Vec<String>,
pub enabled: bool,
pub parameters: Option<serde_json::Value>,
pub poll_interval: Option<i32>,
pub tags: Vec<String>,
}
impl PackLoader {
pub fn load_actions(&self) -> Result<Vec<ActionMetadata>> {
// Parse actions/*.yaml files
}
pub fn load_triggers(&self) -> Result<Vec<TriggerMetadata>> {
// Parse triggers/*.yaml files
}
pub fn load_sensors(&self) -> Result<Vec<SensorMetadata>> {
// Parse sensors/*.yaml files
}
}
```
#### 1.3 Database Registration
```rust
impl PackLoader {
pub async fn register_pack(
&self,
pool: &PgPool,
manifest: &PackManifest,
) -> Result<i64> {
// Insert into attune.pack table
// Returns pack ID
}
pub async fn register_actions(
&self,
pool: &PgPool,
pack_id: i64,
actions: &[ActionMetadata],
) -> Result<()> {
// Insert into attune.action table
}
pub async fn register_triggers(
&self,
pool: &PgPool,
pack_id: i64,
triggers: &[TriggerMetadata],
) -> Result<()> {
// Insert into attune.trigger table
}
pub async fn register_sensors(
&self,
pool: &PgPool,
pack_id: i64,
sensors: &[SensorMetadata],
) -> Result<()> {
// Insert into attune.sensor table
}
}
```
#### 1.4 Pack Loading Function
```rust
pub async fn load_pack(
pack_dir: PathBuf,
pool: &PgPool,
) -> Result<()> {
let loader = PackLoader::new(pack_dir);
// Parse pack manifest
let manifest = loader.load_manifest()?;
// Register pack
let pack_id = loader.register_pack(pool, &manifest).await?;
// Load and register components
let actions = loader.load_actions()?;
loader.register_actions(pool, pack_id, &actions).await?;
let triggers = loader.load_triggers()?;
loader.register_triggers(pool, pack_id, &triggers).await?;
let sensors = loader.load_sensors()?;
loader.register_sensors(pool, pack_id, &sensors).await?;
info!("Pack '{}' loaded successfully", manifest.ref_);
Ok(())
}
```
---
### Phase 2: Worker Service Integration
Update the worker service to execute actions from the filesystem.
#### 2.1 Action Execution Path Resolution
```rust
pub struct ActionExecutor {
packs_dir: PathBuf,
}
impl ActionExecutor {
pub fn resolve_action_path(
&self,
pack_ref: &str,
entry_point: &str,
) -> Result<PathBuf> {
// packs/{pack_ref}/actions/{entry_point}
let path = self.packs_dir
.join(pack_ref)
.join("actions")
.join(entry_point);
if !path.exists() {
return Err(Error::ActionNotFound(entry_point.to_string()));
}
Ok(path)
}
}
```
#### 2.2 Environment Variable Setup
```rust
pub fn prepare_action_env(
params: &HashMap<String, serde_json::Value>,
) -> HashMap<String, String> {
let mut env = HashMap::new();
for (key, value) in params {
let env_key = format!("ATTUNE_ACTION_{}", key.to_uppercase());
let env_value = match value {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
_ => serde_json::to_string(value).unwrap(),
};
env.insert(env_key, env_value);
}
env
}
```
#### 2.3 Action Execution
```rust
pub async fn execute_action(
&self,
action: &Action,
params: HashMap<String, serde_json::Value>,
) -> Result<ExecutionResult> {
// Resolve action script path
let script_path = self.resolve_action_path(
&action.pack_ref,
&action.entrypoint,
)?;
// Prepare environment variables
let env = prepare_action_env(&params);
// Execute based on runner type
let output = match action.runtime_type.as_str() {
"shell" => self.execute_shell_action(script_path, env).await?,
"python" => self.execute_python_action(script_path, env).await?,
_ => return Err(Error::UnsupportedRuntime(action.runtime_type.clone())),
};
Ok(output)
}
```
---
### Phase 3: Sensor Service Integration
Update the sensor service to load and run sensors from the filesystem.
#### 3.1 Sensor Path Resolution
```rust
pub struct SensorManager {
packs_dir: PathBuf,
}
impl SensorManager {
pub fn resolve_sensor_path(
&self,
pack_ref: &str,
entry_point: &str,
) -> Result<PathBuf> {
// packs/{pack_ref}/sensors/{entry_point}
let path = self.packs_dir
.join(pack_ref)
.join("sensors")
.join(entry_point);
if !path.exists() {
return Err(Error::SensorNotFound(entry_point.to_string()));
}
Ok(path)
}
}
```
#### 3.2 Sensor Environment Setup
```rust
pub fn prepare_sensor_env(
sensor: &Sensor,
trigger_instances: &[TriggerInstance],
) -> HashMap<String, String> {
let mut env = HashMap::new();
// Add sensor config
for (key, value) in &sensor.config {
let env_key = format!("ATTUNE_SENSOR_{}", key.to_uppercase());
env.insert(env_key, value.to_string());
}
// Add trigger instances as JSON array
let triggers_json = serde_json::to_string(trigger_instances).unwrap();
env.insert("ATTUNE_SENSOR_TRIGGERS".to_string(), triggers_json);
env
}
```
#### 3.3 Sensor Execution
```rust
pub async fn run_sensor(
&self,
sensor: &Sensor,
trigger_instances: Vec<TriggerInstance>,
) -> Result<()> {
// Resolve sensor script path
let script_path = self.resolve_sensor_path(
&sensor.pack_ref,
&sensor.entrypoint,
)?;
// Prepare environment
let env = prepare_sensor_env(sensor, &trigger_instances);
// Start sensor process
let mut child = Command::new(&script_path)
.envs(env)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
// Read stdout line by line (JSON events)
let stdout = child.stdout.take().unwrap();
let reader = BufReader::new(stdout);
for line in reader.lines() {
let event_json = line?;
let event: SensorEvent = serde_json::from_str(&event_json)?;
// Create event in database
self.create_event_from_sensor(sensor, event).await?;
}
Ok(())
}
```
---
### Phase 4: Service Startup Integration
Add pack loading to service initialization.
#### 4.1 API Service Startup
```rust
// In crates/api/src/main.rs
#[tokio::main]
async fn main() -> Result<()> {
// ... existing initialization ...
// Load core pack
let packs_dir = PathBuf::from("packs");
let core_pack_dir = packs_dir.join("core");
if core_pack_dir.exists() {
info!("Loading core pack...");
pack_loader::load_pack(core_pack_dir, &pool).await?;
info!("Core pack loaded successfully");
}
// ... continue with server startup ...
}
```
#### 4.2 Worker Service Startup
```rust
// In crates/worker/src/main.rs
#[tokio::main]
async fn main() -> Result<()> {
// ... existing initialization ...
// Set packs directory for action execution
let packs_dir = PathBuf::from("packs");
let executor = ActionExecutor::new(packs_dir);
// ... continue with worker initialization ...
}
```
#### 4.3 Sensor Service Startup
```rust
// In crates/sensor/src/main.rs
#[tokio::main]
async fn main() -> Result<()> {
// ... existing initialization ...
// Set packs directory for sensor execution
let packs_dir = PathBuf::from("packs");
let sensor_manager = SensorManager::new(packs_dir);
// Load and start sensors
let sensors = load_enabled_sensors(&pool).await?;
for sensor in sensors {
sensor_manager.start_sensor(sensor).await?;
}
// ... continue with sensor service ...
}
```
---
## Configuration
Add pack-related configuration to `config.yaml`:
```yaml
packs:
# Directory containing packs
directory: "./packs"
# Auto-load packs on startup
auto_load:
- core
# Pack-specific configuration
core:
max_action_timeout: 300
enable_debug_logging: false
```
---
## Database Schema Updates
The existing database schema already supports packs. Ensure these tables are used:
- `attune.pack` - Pack metadata
- `attune.action` - Action definitions
- `attune.trigger` - Trigger type definitions
- `attune.sensor` - Sensor definitions
- `attune.runtime` - Runtime definitions
**Note**: The current `scripts/seed_core_pack.sql` inserts data directly. This should be replaced or complemented by the filesystem-based loader.
---
## Migration Strategy
### Option 1: Replace SQL Seed Script
Remove `scripts/seed_core_pack.sql` and load from filesystem exclusively.
**Pros**: Single source of truth (filesystem)
**Cons**: Requires pack loader to be implemented first
### Option 2: Dual Approach (Recommended)
Keep SQL seed script for initial setup, add filesystem loader for development/updates.
**Pros**: Works immediately, smooth migration path
**Cons**: Need to maintain both during transition
**Implementation**:
1. Keep existing SQL seed script for now
2. Implement pack loader service
3. Add CLI command: `attune pack reload core`
4. Eventually replace SQL seed with filesystem loading
---
## Testing Plan
### Unit Tests
- Pack manifest parsing
- Component metadata parsing
- Path resolution
- Environment variable preparation
### Integration Tests
1. **Pack Loading**
- Load core pack from filesystem
- Verify database registration
- Validate component metadata
2. **Action Execution**
- Execute `core.echo` with parameters
- Execute `core.http_request` with mock server
- Verify environment variable passing
- Capture stdout/stderr correctly
3. **Sensor Execution**
- Run `core.interval_timer_sensor`
- Verify event emission
- Check trigger firing logic
### End-to-End Tests
- Create rule with `core.intervaltimer` trigger
- Verify rule fires and executes `core.echo` action
- Check execution logs and results
---
## Dependencies
### Rust Crates
```toml
[dependencies]
serde_yaml = "0.9" # Parse YAML files
walkdir = "2.4" # Traverse pack directories
tokio = { version = "1", features = ["process"] } # Async process execution
```
### System Dependencies
- Shell (bash/sh) for shell actions
- Python 3.8+ for Python actions
- Python packages: `requests>=2.28.0`, `croniter>=1.4.0`
---
## Rollout Plan
### Week 1: Pack Loader Implementation
- [ ] Create `pack_loader` module in `attune_common`
- [ ] Implement manifest and component parsers
- [ ] Add database registration functions
- [ ] Write unit tests
### Week 2: Worker Integration
- [ ] Add action path resolution
- [ ] Implement environment variable preparation
- [ ] Update action execution to use filesystem
- [ ] Add integration tests
### Week 3: Sensor Integration
- [ ] Add sensor path resolution
- [ ] Implement sensor process management
- [ ] Update event creation from sensor output
- [ ] Add integration tests
### Week 4: Testing & Documentation
- [ ] End-to-end testing
- [ ] CLI commands for pack management
- [ ] Update deployment documentation
- [ ] Performance testing
---
## Success Criteria
- ✅ Core pack loaded from filesystem on startup
- ✅ Actions execute successfully from pack directory
- ✅ Sensors run and emit events correctly
- ✅ Environment variables passed properly to actions/sensors
- ✅ Database contains correct metadata for all components
- ✅ No regression in existing functionality
- ✅ Integration tests pass
- ✅ Documentation updated
---
## Related Documentation
- `packs/core/README.md` - Core pack usage guide
- `docs/pack-structure.md` - Pack structure reference
- `docs/pack-management-architecture.md` - Architecture overview
- `docs/worker-service.md` - Worker service documentation
- `docs/sensor-service.md` - Sensor service documentation
---
## Open Questions
1. **Runtime Registration**: Should we create runtime entries in the database for each runner type (shell, python)?
2. **Pack Versioning**: How to handle pack updates? Replace existing entries or keep version history?
3. **Pack Dependencies**: How to handle dependencies between packs?
4. **Pack Registry**: Future external pack registry integration?
5. **Hot Reload**: Should packs be hot-reloadable without service restart?
---
## Conclusion
Integrating the filesystem-based core pack requires:
1. Pack loader service to parse and register components
2. Worker service updates to execute actions from filesystem
3. Sensor service updates to run sensors from filesystem
4. Startup integration to load packs automatically
The implementation can be phased, starting with the pack loader, then worker integration, then sensor integration. The existing SQL seed script can remain as a fallback during the transition.

View File

@@ -0,0 +1,382 @@
# Pack Installation with Testing Integration
## Overview
Pack installation and registration now includes automatic test execution to validate that packs work correctly in the target environment. This provides fail-fast validation and ensures quality across the pack ecosystem.
## Features
- **Automatic Test Execution**: Tests run automatically during pack installation/registration
- **Fail-Fast Validation**: Installation fails if tests don't pass (unless forced)
- **Test Result Storage**: All test results are stored in the database for audit trails
- **Flexible Control**: Skip tests or force installation with command-line flags
- **Test Result Display**: CLI shows test results with pass/fail status
## Installation Methods
### 1. Register Pack (Local Directory)
Register a pack from a local filesystem directory:
```bash
# Basic registration with automatic testing
attune pack register /path/to/pack
# Force registration even if pack already exists
attune pack register /path/to/pack --force
# Skip tests during registration
attune pack register /path/to/pack --skip-tests
# Combine flags
attune pack register /path/to/pack --force --skip-tests
```
**How it works:**
1. Reads `pack.yaml` from the directory
2. Creates pack record in database
3. Syncs workflows from pack directory
4. **Runs pack tests** (if not skipped)
5. **Fails registration if tests fail** (unless `--force` is used)
6. Stores test results in database
### 2. Install Pack (Remote Source)
**Status**: Not yet implemented
Install a pack from a git repository or remote source:
```bash
# Install from git repository (future)
attune pack install https://github.com/attune/pack-slack.git
# Install specific version (future)
attune pack install https://github.com/attune/pack-slack.git --ref v1.0.0
```
This feature will be implemented in a future release.
## API Endpoints
### Register Pack
**Endpoint**: `POST /api/v1/packs/register`
**Request Body**:
```json
{
"path": "/path/to/pack/directory",
"force": false,
"skip_tests": false
}
```
**Response** (201 Created):
```json
{
"data": {
"pack": {
"id": 1,
"ref": "mypack",
"label": "My Pack",
"version": "1.0.0",
...
},
"test_result": {
"pack_ref": "mypack",
"pack_version": "1.0.0",
"status": "passed",
"total_tests": 10,
"passed": 10,
"failed": 0,
"skipped": 0,
"pass_rate": 1.0,
"duration_ms": 1234,
"test_suites": [...]
},
"tests_skipped": false
},
"message": "Pack registered successfully"
}
```
**Error Response** (400 Bad Request) - Tests Failed:
```json
{
"error": "Pack registration failed: tests did not pass. Use force=true to register anyway.",
"code": "BAD_REQUEST"
}
```
### Install Pack
**Endpoint**: `POST /api/v1/packs/install`
**Status**: Returns 501 Not Implemented
**Request Body**:
```json
{
"source": "https://github.com/attune/pack-slack.git",
"ref_spec": "main",
"force": false,
"skip_tests": false
}
```
This endpoint will be implemented in a future release.
## Test Execution Behavior
### Default Behavior (Tests Enabled)
When registering a pack with tests configured in `pack.yaml`:
1. **Tests are automatically executed** after pack creation
2. **Test results are stored** in the `pack_test_execution` table
3. **Registration fails** if any test fails
4. **Pack record is rolled back** if tests fail (unless `--force` is used)
### Skip Tests (`--skip-tests` flag)
Use this flag when:
- Tests are known to be slow or flaky
- Testing in a non-standard environment
- Manually verifying pack functionality later
```bash
attune pack register /path/to/pack --skip-tests
```
**Behavior**:
- Tests are not executed
- No test results are stored
- Registration always succeeds (no validation)
- Response includes `"tests_skipped": true`
### Force Registration (`--force` flag)
Use this flag when:
- Pack already exists and you want to reinstall
- Tests are failing but you need to proceed anyway
- Developing and iterating on pack tests
```bash
attune pack register /path/to/pack --force
```
**Behavior**:
- Deletes existing pack if it exists
- Tests still run (unless `--skip-tests` is also used)
- **Registration succeeds even if tests fail**
- Warning logged if tests fail
### Combined Flags
```bash
# Force reinstall and skip tests entirely
attune pack register /path/to/pack --force --skip-tests
```
## CLI Output Examples
### Successful Registration with Tests
```
✓ Pack 'core' registered successfully
Version: 0.1.0
ID: 1
✓ All tests passed
Tests: 76/76 passed
```
### Failed Registration (Tests Failed)
```
✗ Error: Pack registration failed: tests did not pass. Use force=true to register anyway.
```
### Registration with Skipped Tests
```
✓ Pack 'core' registered successfully
Version: 0.1.0
ID: 1
Tests were skipped
```
### Forced Registration with Failed Tests
```
⚠ Pack 'mypack' registered successfully
Version: 1.0.0
ID: 2
✗ Some tests failed
Tests: 8/10 passed
```
## Testing Requirements
For a pack to support automatic testing during installation:
1. **`pack.yaml` must include a `testing` section**:
```yaml
testing:
enabled: true
test_suites:
- name: "Unit Tests"
runner: "python_unittest"
working_dir: "tests"
test_files:
- "test_*.py"
timeout_seconds: 60
```
2. **Test files must exist** in the specified locations
3. **Tests must be executable** in the target environment
See [Pack Testing Framework](./PACK_TESTING.md) for detailed test configuration.
## Database Storage
All test executions are stored in the `attune.pack_test_execution` table:
- **pack_id**: Reference to the pack
- **pack_version**: Version tested
- **trigger_reason**: How tests were triggered (e.g., "register", "manual")
- **total_tests**, **passed**, **failed**, **skipped**: Test counts
- **pass_rate**: Percentage of tests passed
- **duration_ms**: Total execution time
- **result**: Full test results as JSON
Query test history:
```bash
attune pack test-history core
```
Or via API:
```bash
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8080/api/v1/packs/core/tests
```
## Error Handling
### Pack Directory Not Found
```json
{
"error": "Pack directory does not exist: /invalid/path",
"code": "BAD_REQUEST"
}
```
### Missing pack.yaml
```json
{
"error": "pack.yaml not found in directory: /path/to/pack",
"code": "BAD_REQUEST"
}
```
### No Testing Configuration
```json
{
"error": "No testing configuration found in pack.yaml for pack 'mypack'",
"code": "BAD_REQUEST"
}
```
### Testing Disabled
```json
{
"error": "Testing is disabled for pack 'mypack'",
"code": "BAD_REQUEST"
}
```
### Pack Already Exists
```json
{
"error": "Pack 'mypack' already exists. Use force=true to reinstall.",
"code": "CONFLICT"
}
```
## Best Practices
### Development Workflow
1. **During active development**: Use `--skip-tests` for faster iteration
```bash
attune pack register ./my-pack --force --skip-tests
```
2. **Before committing**: Run tests explicitly to validate
```bash
attune pack test my-pack
```
3. **For production**: Let tests run automatically during registration
```bash
attune pack register ./my-pack
```
### CI/CD Integration
In your CI/CD pipeline:
```bash
# Register pack (tests will run automatically)
attune pack register ./pack-directory
# Exit code will be non-zero if tests fail
echo $? # 0 = success, 1 = failure
```
### Production Deployment
For production deployments:
1. **Never skip tests** unless you have a specific reason
2. **Use `--force`** only when redeploying a known-good version
3. **Monitor test results** via the API or database
4. **Set up alerts** for test failures
## Future Enhancements
Planned improvements to pack installation:
1. **Remote Pack Installation**: Install packs from git repositories
2. **Dependency Resolution**: Automatically install required packs
3. **Version Management**: Support multiple versions of the same pack
4. **Async Testing**: Return immediately and poll for test results
5. **Test Result Comparison**: Compare test results across versions
6. **Webhooks**: Notify external systems of test results
7. **Pack Registry**: Central repository for discovering and installing packs
## Related Documentation
- [Pack Testing Framework](./PACK_TESTING.md) - Complete testing guide
- [Pack Testing API Reference](./api-pack-testing.md) - API documentation
- [Pack Development Guide](./PACK_DEVELOPMENT.md) - Creating packs
- [Pack Structure](./pack-structure.md) - pack.yaml format
## Examples
See the `packs/core` directory for a complete example of a pack with testing enabled:
- `packs/core/pack.yaml` - Testing configuration
- `packs/core/tests/` - Test files
- `packs/core/actions/` - Actions being tested
Register the core pack:
```bash
attune pack register packs/core
```

View File

@@ -0,0 +1,587 @@
# Pack Installation from Git Repositories
**Last Updated**: 2025-01-27
**Status**: Production Ready
---
## Overview
Attune supports installing packs directly from git repositories, enabling teams to:
- **Version Control**: Track pack versions using git tags, branches, and commits
- **Collaboration**: Share packs across teams via git hosting services
- **Automation**: Integrate pack deployment into CI/CD pipelines
- **Development**: Test packs from feature branches before release
---
## Quick Start
### Web UI Installation
1. Navigate to **Packs** page
2. Click **Add Pack** dropdown → **Install from Remote**
3. Select **Git Repository** as source type
4. Enter repository URL (HTTPS or SSH)
5. Optionally specify a git reference (branch, tag, or commit)
6. Configure installation options
7. Click **Install Pack**
### CLI Installation
```bash
# Install from default branch
attune pack install https://github.com/example/pack-slack.git
# Install from specific tag
attune pack install https://github.com/example/pack-slack.git --ref v2.1.0
# Install from branch
attune pack install https://github.com/example/pack-slack.git --ref main
# Install from commit hash
attune pack install https://github.com/example/pack-slack.git --ref a1b2c3d
# SSH URL
attune pack install git@github.com:example/pack-slack.git --ref v2.1.0
# Skip tests and dependency validation (use with caution)
attune pack install https://github.com/example/pack-slack.git --skip-tests --skip-deps
```
### API Installation
```bash
curl -X POST http://localhost:8080/api/v1/packs/install \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"source": "https://github.com/example/pack-slack.git",
"ref_spec": "v2.1.0",
"force": false,
"skip_tests": false,
"skip_deps": false
}'
```
---
## Supported Git Sources
### HTTPS URLs
**Public repositories:**
```
https://github.com/username/pack-name.git
https://gitlab.com/username/pack-name.git
https://bitbucket.org/username/pack-name.git
```
**Private repositories with credentials:**
```
https://username:token@github.com/username/pack-name.git
```
> **Security Note**: For private repositories, use SSH keys or configure git credential helpers instead of embedding credentials in URLs.
### SSH URLs
**Standard format:**
```
git@github.com:username/pack-name.git
git@gitlab.com:username/pack-name.git
```
**SCP-style:**
```
user@server:path/to/pack.git
```
**Requirements:**
- SSH keys must be configured on the Attune server
- User running Attune service must have access to private key
- Host must be in `~/.ssh/known_hosts`
---
## Git References
The `ref_spec` parameter accepts any valid git reference:
### Branches
```bash
# Default branch (usually main or master)
--ref main
# Development branch
--ref develop
# Feature branch
--ref feature/new-action
```
### Tags
```bash
# Semantic version tag
--ref v1.2.3
# Release tag
--ref release-2024-01-27
```
### Commit Hashes
```bash
# Full commit hash
--ref a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0
# Short commit hash (7+ characters)
--ref a1b2c3d
```
### Special References
```bash
# HEAD of default branch
--ref HEAD
# No ref specified = default branch
# (equivalent to --depth 1 clone)
```
---
## Pack Structure Requirements
The git repository must contain a valid pack structure:
### Option 1: Root-Level Pack
```
repository-root/
├── pack.yaml # Required
├── actions/ # Optional
├── sensors/ # Optional
├── triggers/ # Optional
├── rules/ # Optional
├── workflows/ # Optional
└── README.md # Recommended
```
### Option 2: Pack Subdirectory
```
repository-root/
├── pack/
│ ├── pack.yaml # Required
│ ├── actions/
│ └── ...
├── docs/
└── README.md
```
> The installer will automatically detect and use the pack directory.
---
## Installation Process
When you install a pack from git, Attune performs these steps:
### 1. Clone Repository
```
git clone [--depth 1] <url> <temp-dir>
```
If a `ref_spec` is provided:
```
git checkout <ref_spec>
```
### 2. Locate Pack Directory
- Search for `pack.yaml` at repository root
- If not found, search in `pack/` subdirectory
- Fail if `pack.yaml` not found in either location
### 3. Validate Dependencies (unless skipped)
- Extract runtime dependencies from `pack.yaml`
- Extract pack dependencies
- Verify all dependencies are satisfied
- Fail if dependencies are missing (unless `force` enabled)
### 4. Register Pack
- Parse `pack.yaml` metadata
- Create database entry
- Auto-sync workflows if present
### 5. Execute Tests (unless skipped)
- Run pack test suite if configured
- Fail if tests don't pass (unless `force` enabled)
### 6. Copy to Permanent Storage
- Move pack files to `{packs_base_dir}/{pack_ref}/`
- Calculate directory checksum
- Store installation metadata
### 7. Record Installation
- Store installation record in `pack_installation` table
- Record source URL, git ref, timestamp, checksum
- Link to installing user
---
## Installation Options
### Force Installation
**Flag**: `--force` (CLI) or `force: true` (API)
Enables:
- Reinstall pack even if it already exists (replaces existing)
- Proceed even if dependencies are missing
- Proceed even if tests fail
**Use Cases**:
- Upgrading pack to new version
- Recovering from failed installation
- Development/testing workflows
**Warning**: Force mode bypasses safety checks. Use cautiously in production.
### Skip Tests
**Flag**: `--skip-tests` (CLI) or `skip_tests: true` (API)
- Skip executing pack test suite
- Faster installation
- Useful when tests are slow or not available
**Use Cases**:
- Installing trusted packs
- Tests not yet implemented
- Development environments
### Skip Dependencies
**Flag**: `--skip-deps` (CLI) or `skip_deps: true` (API)
- Skip validation of runtime dependencies
- Skip validation of pack dependencies
- May result in runtime failures if dependencies truly missing
**Use Cases**:
- Dependencies will be installed separately
- Custom runtime environment
- Development/testing
---
## Example Workflows
### Development Workflow
```bash
# 1. Install pack from feature branch for testing
attune pack install https://github.com/myorg/pack-custom.git \
--ref feature/new-sensor \
--skip-tests \
--force
# 2. Test the pack
attune pack test custom
# 3. When satisfied, install from main/release tag
attune pack install https://github.com/myorg/pack-custom.git \
--ref v1.0.0 \
--force
```
### Production Deployment
```bash
# Install specific version with full validation
attune pack install https://github.com/myorg/pack-slack.git \
--ref v2.1.0
# Verify installation
attune pack list | grep slack
attune pack test slack
```
### CI/CD Pipeline
```yaml
# GitHub Actions example
- name: Install pack on staging
run: |
attune pack install https://github.com/${{ github.repository }}.git \
--ref ${{ github.sha }} \
--force
env:
ATTUNE_API_TOKEN: ${{ secrets.ATTUNE_TOKEN }}
- name: Run tests
run: attune pack test $(basename ${{ github.repository }})
```
### Private Repository with SSH
```bash
# 1. Set up SSH key on Attune server
ssh-keygen -t ed25519 -C "attune@example.com"
cat ~/.ssh/id_ed25519.pub # Add to GitHub/GitLab
# 2. Add host to known_hosts
ssh-keyscan github.com >> ~/.ssh/known_hosts
# 3. Install pack
attune pack install git@github.com:myorg/private-pack.git --ref main
```
---
## Troubleshooting
### Git Clone Fails
**Error**: `Git clone failed: Permission denied`
**Solutions**:
- Verify SSH keys are configured correctly
- Check repository access permissions
- For HTTPS, verify credentials or token
- Add host to `~/.ssh/known_hosts`
### Ref Not Found
**Error**: `Git checkout failed: pathspec 'v1.0.0' did not match any file(s) known to git`
**Solutions**:
- Verify tag/branch exists in repository
- Check spelling and case sensitivity
- Ensure ref is pushed to remote
- Try with full commit hash
### Pack.yaml Not Found
**Error**: `pack.yaml not found in directory`
**Solutions**:
- Ensure `pack.yaml` exists at repository root or in `pack/` subdirectory
- Check file name spelling (case-sensitive on Linux)
- Verify correct branch/tag is checked out
### Dependency Validation Failed
**Error**: `Pack dependency validation failed: pack 'core' version '^1.0.0' not found`
**Solutions**:
- Install missing dependencies first
- Use `--skip-deps` to bypass validation (not recommended)
- Use `--force` to install anyway
### Test Failures
**Error**: `Pack registration failed: tests did not pass`
**Solutions**:
- Review test output for specific failures
- Fix issues in pack code
- Use `--skip-tests` to install without testing
- Use `--force` to install despite test failures
---
## Security Considerations
### SSH Keys
- Use dedicated SSH key for Attune service
- Restrict key permissions (read-only access preferred)
- Rotate keys periodically
- Use SSH agent for key management
### HTTPS Authentication
- Never embed credentials directly in URLs
- Use git credential helpers
- Consider personal access tokens with limited scope
- Rotate tokens regularly
### Code Review
- Review pack code before installation
- Install from tagged releases, not branches
- Verify pack author/source
- Check for malicious code in actions/sensors
### Git References
- **Production**: Use specific tags (e.g., `v1.2.3`)
- **Staging**: Use release branches (e.g., `release-*`)
- **Development**: Feature branches acceptable
- **Avoid**: `main`/`master` in production (may change unexpectedly)
---
## Advanced Topics
### Submodules
If pack repository uses git submodules:
```bash
# Clone with submodules
git clone --recurse-submodules <url>
```
> **Note**: Current implementation does not automatically clone submodules. Manual configuration required.
### Large Repositories
For large repositories, use shallow clones:
```bash
# Default behavior when no ref_spec
git clone --depth 1 <url>
```
When specific ref is needed:
```bash
git clone <url>
git checkout <ref>
```
### Monorepos
For repositories containing multiple packs:
```
monorepo/
├── pack-a/
│ └── pack.yaml
├── pack-b/
│ └── pack.yaml
└── pack-c/
└── pack.yaml
```
**Limitation**: Current implementation expects one pack per repository. For monorepos, use filesystem registration or create separate repositories.
---
## Database Schema
Installation metadata is stored in the `pack_installation` table:
```sql
CREATE TABLE pack_installation (
id BIGSERIAL PRIMARY KEY,
pack_id BIGINT NOT NULL REFERENCES pack(id),
source_type VARCHAR(50) NOT NULL, -- 'git'
source_url TEXT, -- Git repository URL
source_ref TEXT, -- Branch/tag/commit
checksum TEXT, -- Directory checksum
checksum_verified BOOLEAN DEFAULT FALSE,
installed_by BIGINT REFERENCES identity(id),
installation_method VARCHAR(50), -- 'api', 'cli', 'web'
storage_path TEXT NOT NULL, -- File system path
meta JSONB, -- Additional metadata
created TIMESTAMP DEFAULT NOW(),
updated TIMESTAMP DEFAULT NOW()
);
```
---
## API Reference
### Install Pack Endpoint
**POST** `/api/v1/packs/install`
**Request Body**:
```json
{
"source": "https://github.com/example/pack-slack.git",
"ref_spec": "v2.1.0",
"force": false,
"skip_tests": false,
"skip_deps": false
}
```
**Response** (201 Created):
```json
{
"data": {
"pack": {
"id": 42,
"ref": "slack",
"label": "Slack Pack",
"version": "2.1.0",
"description": "Slack integration pack",
"is_standard": false,
...
},
"test_result": {
"status": "passed",
"total_tests": 10,
"passed": 10,
"failed": 0,
...
},
"tests_skipped": false
}
}
```
**Error Responses**:
- `400`: Invalid request, dependency validation failed, or tests failed
- `409`: Pack already exists (use `force: true` to override)
- `500`: Internal error (git clone failed, filesystem error, etc.)
---
## Future Enhancements
### Planned Features
- Git submodule support
- Monorepo support (install specific subdirectory)
- Pack version upgrade workflow
- Automatic version detection from tags
- Git LFS support
- Signature verification for signed commits
### Registry Integration
When pack registry system is implemented:
- Registry can reference git repositories
- Automatic version discovery from git tags
- Centralized pack metadata
- See `pack-registry-spec.md` for details
---
## Related Documentation
- [Pack Structure](pack-structure.md) - Pack directory and file format
- [Pack Registry Specification](pack-registry-spec.md) - Registry-based installation
- [Pack Testing Framework](pack-testing-framework.md) - Testing packs
- [Configuration](../configuration/configuration.md) - Server configuration
- [Production Deployment](../deployment/production-deployment.md) - Deployment guide
---
## Examples Repository
Example packs demonstrating git-based installation:
```bash
# Simple action pack
attune pack install https://github.com/attune-examples/pack-hello-world.git
# Complex pack with dependencies
attune pack install https://github.com/attune-examples/pack-kubernetes.git --ref v1.0.0
# Private repository (SSH)
attune pack install git@github.com:mycompany/pack-internal.git --ref main
```
---
## Support
For issues or questions:
- GitHub Issues: https://github.com/attune-io/attune/issues
- Documentation: https://docs.attune.io/packs/installation
- Community: https://community.attune.io

View File

@@ -0,0 +1,548 @@
# Pack Registry CI/CD Integration
This document provides examples and best practices for integrating pack publishing with CI/CD pipelines.
## Overview
Automating pack registry publishing ensures:
- Consistent pack versioning and releases
- Automated checksum generation
- Registry index updates on every release
- Quality assurance through automated testing
## Prerequisites
1. **Pack Structure**: Your pack must have a valid `pack.yaml`
2. **Registry Index**: A git repository hosting your `index.json`
3. **Pack Storage**: A place to host pack archives (GitHub Releases, S3, etc.)
4. **Attune CLI**: Available in CI environment
## GitHub Actions Examples
### Example 1: Publish Pack on Git Tag
```yaml
# .github/workflows/publish-pack.yml
name: Publish Pack
on:
push:
tags:
- 'v*.*.*'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout pack repository
uses: actions/checkout@v3
- name: Install Attune CLI
run: |
curl -L https://github.com/attune/attune/releases/latest/download/attune-cli-linux -o /usr/local/bin/attune
chmod +x /usr/local/bin/attune
- name: Validate pack.yaml
run: |
if [ ! -f "pack.yaml" ]; then
echo "Error: pack.yaml not found"
exit 1
fi
- name: Run pack tests
run: attune pack test . --detailed
- name: Generate checksum
id: checksum
run: |
CHECKSUM=$(attune pack checksum . --json | jq -r '.checksum')
echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT
- name: Create GitHub Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
- name: Create pack archive
run: |
VERSION=${GITHUB_REF#refs/tags/v}
tar -czf pack-${VERSION}.tar.gz --exclude='.git' .
- name: Upload pack archive
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./pack-${{ steps.version.outputs.version }}.tar.gz
asset_name: pack-${{ steps.version.outputs.version }}.tar.gz
asset_content_type: application/gzip
- name: Generate index entry
run: |
VERSION=${GITHUB_REF#refs/tags/v}
attune pack index-entry . \
--git-url "https://github.com/${{ github.repository }}" \
--git-ref "${{ github.ref_name }}" \
--archive-url "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/pack-${VERSION}.tar.gz" \
--format json > index-entry.json
- name: Checkout registry repository
uses: actions/checkout@v3
with:
repository: your-org/attune-registry
token: ${{ secrets.REGISTRY_TOKEN }}
path: registry
- name: Update registry index
run: |
attune pack index-update \
--index registry/index.json \
. \
--git-url "https://github.com/${{ github.repository }}" \
--git-ref "${{ github.ref_name }}" \
--archive-url "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/pack-${VERSION}.tar.gz" \
--update
- name: Commit and push registry changes
working-directory: registry
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add index.json
git commit -m "Add/update pack: $(yq -r '.ref' ../pack.yaml) ${{ github.ref_name }}"
git push
```
### Example 2: Multi-Pack Repository
```yaml
# .github/workflows/publish-packs.yml
name: Publish All Packs
on:
push:
branches:
- main
paths:
- 'packs/**'
jobs:
detect-changed-packs:
runs-on: ubuntu-latest
outputs:
packs: ${{ steps.changed.outputs.packs }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Detect changed packs
id: changed
run: |
CHANGED_PACKS=$(git diff --name-only HEAD^ HEAD | grep '^packs/' | cut -d'/' -f2 | sort -u | jq -R -s -c 'split("\n")[:-1]')
echo "packs=$CHANGED_PACKS" >> $GITHUB_OUTPUT
publish-pack:
needs: detect-changed-packs
runs-on: ubuntu-latest
strategy:
matrix:
pack: ${{ fromJson(needs.detect-changed-packs.outputs.packs) }}
steps:
- uses: actions/checkout@v3
- name: Install Attune CLI
run: |
curl -L https://github.com/attune/attune/releases/latest/download/attune-cli-linux -o /usr/local/bin/attune
chmod +x /usr/local/bin/attune
- name: Test pack
run: attune pack test packs/${{ matrix.pack }}
- name: Update registry
run: |
# Clone registry
git clone https://${{ secrets.REGISTRY_TOKEN }}@github.com/your-org/attune-registry.git registry
# Update index
attune pack index-update \
--index registry/index.json \
packs/${{ matrix.pack }} \
--git-url "https://github.com/${{ github.repository }}" \
--git-ref "main" \
--update
# Commit changes
cd registry
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add index.json
git commit -m "Update pack: ${{ matrix.pack }}" || exit 0
git push
```
### Example 3: Registry Maintenance
```yaml
# .github/workflows/maintain-registry.yml
name: Maintain Registry
on:
schedule:
# Run weekly on Sundays at midnight
- cron: '0 0 * * 0'
workflow_dispatch:
jobs:
merge-registries:
runs-on: ubuntu-latest
steps:
- name: Install Attune CLI
run: |
curl -L https://github.com/attune/attune/releases/latest/download/attune-cli-linux -o /usr/local/bin/attune
chmod +x /usr/local/bin/attune
- name: Download registries
run: |
mkdir registries
curl -o registries/main.json https://registry.attune.io/index.json
curl -o registries/community.json https://community.attune.io/index.json
- name: Merge registries
run: |
attune pack index-merge \
--output merged-index.json \
registries/*.json
- name: Upload merged index
uses: actions/upload-artifact@v3
with:
name: merged-registry
path: merged-index.json
```
## GitLab CI Examples
### Example 1: Publish on Tag
```yaml
# .gitlab-ci.yml
stages:
- test
- publish
variables:
PACK_VERSION: ${CI_COMMIT_TAG#v}
test:pack:
stage: test
image: attune/cli:latest
script:
- attune pack test .
only:
- tags
publish:pack:
stage: publish
image: attune/cli:latest
script:
# Generate checksum
- CHECKSUM=$(attune pack checksum . --json | jq -r '.checksum')
# Create archive
- tar -czf pack-${PACK_VERSION}.tar.gz --exclude='.git' .
# Upload to package registry
- 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file pack-${PACK_VERSION}.tar.gz "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/pack/${PACK_VERSION}/pack-${PACK_VERSION}.tar.gz"'
# Clone registry
- git clone https://oauth2:${REGISTRY_TOKEN}@gitlab.com/your-org/attune-registry.git registry
# Update index
- |
attune pack index-update \
--index registry/index.json \
. \
--git-url "${CI_PROJECT_URL}" \
--git-ref "${CI_COMMIT_TAG}" \
--archive-url "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/pack/${PACK_VERSION}/pack-${PACK_VERSION}.tar.gz" \
--update
# Commit and push
- cd registry
- git config user.name "GitLab CI"
- git config user.email "ci@gitlab.com"
- git add index.json
- git commit -m "Update pack from ${CI_PROJECT_NAME} ${CI_COMMIT_TAG}"
- git push
only:
- tags
```
## Jenkins Pipeline Example
```groovy
// Jenkinsfile
pipeline {
agent any
environment {
PACK_VERSION = sh(script: "yq -r '.version' pack.yaml", returnStdout: true).trim()
PACK_REF = sh(script: "yq -r '.ref' pack.yaml", returnStdout: true).trim()
}
stages {
stage('Test') {
steps {
sh 'attune pack test .'
}
}
stage('Build') {
when {
tag pattern: "v.*", comparator: "REGEXP"
}
steps {
sh "tar -czf pack-${PACK_VERSION}.tar.gz --exclude='.git' ."
archiveArtifacts artifacts: "pack-${PACK_VERSION}.tar.gz"
}
}
stage('Publish') {
when {
tag pattern: "v.*", comparator: "REGEXP"
}
steps {
script {
// Upload to artifact repository
sh """
curl -u ${ARTIFACTORY_CREDS} \
-T pack-${PACK_VERSION}.tar.gz \
"https://artifactory.example.com/packs/${PACK_REF}/${PACK_VERSION}/"
"""
// Update registry
sh """
git clone https://${REGISTRY_CREDS}@github.com/your-org/attune-registry.git registry
attune pack index-update \
--index registry/index.json \
. \
--git-url "${GIT_URL}" \
--git-ref "${TAG_NAME}" \
--archive-url "https://artifactory.example.com/packs/${PACK_REF}/${PACK_VERSION}/pack-${PACK_VERSION}.tar.gz" \
--update
cd registry
git config user.name "Jenkins"
git config user.email "jenkins@example.com"
git add index.json
git commit -m "Update ${PACK_REF} to ${PACK_VERSION}"
git push
"""
}
}
}
}
}
```
## Best Practices
### 1. Versioning Strategy
Use semantic versioning for packs:
- **Major**: Breaking changes to actions/sensors
- **Minor**: New features, backward compatible
- **Patch**: Bug fixes
```yaml
# pack.yaml
version: "2.1.3"
```
### 2. Automated Testing
Always run pack tests before publishing:
```bash
# In CI pipeline
attune pack test . --detailed || exit 1
```
### 3. Checksum Verification
Always generate and include checksums:
```bash
CHECKSUM=$(attune pack checksum . --json | jq -r '.checksum')
```
### 4. Registry Security
- Use separate tokens for registry access
- Never commit tokens to source control
- Use CI/CD secrets management
- Rotate tokens regularly
### 5. Archive Hosting
Options for hosting pack archives:
- **GitHub Releases**: Free, integrated with source
- **GitLab Package Registry**: Built-in package management
- **Artifactory/Nexus**: Enterprise artifact management
- **S3/Cloud Storage**: Scalable, CDN-friendly
### 6. Registry Structure
Maintain a separate git repository for your registry:
```
attune-registry/
├── index.json # Main registry index
├── README.md # Usage instructions
├── .github/
│ └── workflows/
│ └── validate.yml # Index validation
└── scripts/
└── validate.sh # Validation script
```
### 7. Index Validation
Add validation to registry repository:
```yaml
# .github/workflows/validate.yml
name: Validate Index
on: [pull_request, push]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Validate JSON
run: |
jq empty index.json || exit 1
- name: Check schema
run: |
# Ensure required fields exist
jq -e '.packs | length > 0' index.json
jq -e '.packs[] | .ref, .version, .install_sources' index.json
```
## Manual Publishing Workflow
For manual/local publishing:
```bash
#!/bin/bash
# publish-pack.sh
set -e
PACK_DIR="$1"
REGISTRY_DIR="$2"
if [ -z "$PACK_DIR" ] || [ -z "$REGISTRY_DIR" ]; then
echo "Usage: $0 <pack-dir> <registry-dir>"
exit 1
fi
cd "$PACK_DIR"
# Extract metadata
PACK_REF=$(yq -r '.ref' pack.yaml)
VERSION=$(yq -r '.version' pack.yaml)
echo "Publishing ${PACK_REF} v${VERSION}..."
# Run tests
echo "Running tests..."
attune pack test . || exit 1
# Calculate checksum
echo "Calculating checksum..."
CHECKSUM=$(attune pack checksum . --json | jq -r '.checksum')
echo "Checksum: $CHECKSUM"
# Create archive
echo "Creating archive..."
ARCHIVE_NAME="pack-${VERSION}.tar.gz"
tar -czf "/tmp/${ARCHIVE_NAME}" --exclude='.git' .
# Upload archive (customize for your storage)
echo "Uploading archive..."
# aws s3 cp "/tmp/${ARCHIVE_NAME}" "s3://my-bucket/packs/${PACK_REF}/${VERSION}/"
# OR
# scp "/tmp/${ARCHIVE_NAME}" "server:/path/to/packs/${PACK_REF}/${VERSION}/"
# Update registry
echo "Updating registry index..."
cd "$REGISTRY_DIR"
git pull
attune pack index-update \
--index index.json \
"$PACK_DIR" \
--git-url "https://github.com/your-org/${PACK_REF}" \
--git-ref "v${VERSION}" \
--archive-url "https://storage.example.com/packs/${PACK_REF}/${VERSION}/${ARCHIVE_NAME}" \
--update
# Commit and push
git add index.json
git commit -m "Add ${PACK_REF} v${VERSION}"
git push
echo "✓ Successfully published ${PACK_REF} v${VERSION}"
```
## Troubleshooting
### Issue: Checksum Mismatch
```bash
# Verify checksum locally
attune pack checksum /path/to/pack --json
# Re-generate archive with consistent settings
tar --sort=name --mtime='@0' --owner=0 --group=0 --numeric-owner \
-czf pack.tar.gz --exclude='.git' pack/
```
### Issue: Registry Update Fails
```bash
# Validate index.json syntax
jq empty index.json
# Check for duplicate refs
jq -r '.packs[].ref' index.json | sort | uniq -d
```
### Issue: Pack Tests Fail in CI
```bash
# Run with verbose output
attune pack test . --detailed --verbose
# Check runtime dependencies
attune pack test . 2>&1 | grep -i "dependency"
```
## See Also
- [Pack Registry Documentation](pack-registry.md)
- [Pack Testing Framework](pack-testing-framework.md)
- [Pack Install with Testing](pack-install-testing.md)
- [API Pack Testing](api-pack-testing.md)

View File

@@ -0,0 +1,841 @@
# Pack Registry and Installation Specification
**Last Updated**: 2024-01-20
**Status**: Specification (Pre-Implementation)
---
## Overview
This document specifies the pack registry and installation system for Attune, enabling community-driven pack distribution. The system allows packs to be:
- Published to independent registries (no central authority required)
- Installed from git repositories, HTTP/HTTPS URLs, or local sources
- Discovered through configurable registry indices
- Validated and tested during installation
---
## Design Goals
1. **Decentralized**: No single point of failure; anyone can host a registry
2. **CI/CD Friendly**: Integrate with existing build and artifact storage systems
3. **Flexible Sources**: Support multiple installation sources (git, HTTP, local)
4. **Priority-Based Discovery**: Search multiple registries in configured order
5. **Secure**: Validate checksums and signatures (future)
6. **Automated**: Install dependencies, run tests, register components automatically
---
## Pack Index File Format
### Index Structure
Each registry hosts an **index file** (typically `index.json`) that catalogs available packs.
**Format**: JSON
**Location**: Configurable URL (HTTPS recommended)
**Filename Convention**: `index.json` or `registry.json`
### Index Schema
```json
{
"registry_name": "Attune Community Registry",
"registry_url": "https://registry.attune.io",
"version": "1.0",
"last_updated": "2024-01-20T12:00:00Z",
"packs": [
{
"ref": "slack",
"label": "Slack Integration",
"description": "Send messages, upload files, and monitor Slack channels",
"version": "2.1.0",
"author": "Attune Team",
"email": "team@attune.io",
"homepage": "https://github.com/attune-io/pack-slack",
"repository": "https://github.com/attune-io/pack-slack",
"license": "Apache-2.0",
"keywords": ["slack", "messaging", "notifications"],
"runtime_deps": ["python3"],
"
"install_sources": [
{
"type": "git",
"url": "https://github.com/attune-io/pack-slack.git",
"ref": "v2.1.0",
"checksum": "sha256:abc123..."
},
{
"type": "archive",
"url": "https://github.com/attune-io/pack-slack/archive/refs/tags/v2.1.0.zip",
"checksum": "sha256:def456..."
}
],
"contents": {
"actions": [
{
"name": "send_message",
"description": "Send a message to a Slack channel"
},
{
"name": "upload_file",
"description": "Upload a file to Slack"
}
],
"sensors": [
{
"name": "message_sensor",
"description": "Monitor Slack messages"
}
],
"triggers": [
{
"name": "message_received",
"description": "Fires when a message is received"
}
],
"rules": [],
"workflows": []
},
"dependencies": {
"attune_version": ">=0.1.0",
"python_version": ">=3.9",
"packs": []
},
"meta": {
"downloads": 1543,
"stars": 87,
"tested_attune_versions": ["0.1.0", "0.2.0"]
}
}
]
}
```
### Field Definitions
#### Registry Metadata
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `registry_name` | string | Yes | Human-readable registry name |
| `registry_url` | string | Yes | Registry homepage URL |
| `version` | string | Yes | Index format version (semantic versioning) |
| `last_updated` | string | Yes | ISO 8601 timestamp of last update |
| `packs` | array | Yes | Array of pack entries |
#### Pack Entry
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `ref` | string | Yes | Unique pack identifier (matches pack.yaml) |
| `label` | string | Yes | Human-readable pack name |
| `description` | string | Yes | Brief pack description |
| `version` | string | Yes | Semantic version (latest available) |
| `author` | string | Yes | Pack author/maintainer name |
| `email` | string | No | Contact email |
| `homepage` | string | No | Pack homepage URL |
| `repository` | string | No | Source repository URL |
| `license` | string | Yes | SPDX license identifier |
| `keywords` | array[string] | No | Searchable keywords/tags |
| `runtime_deps` | array[string] | Yes | Required runtimes (python3, nodejs, shell) |
| `install_sources` | array[object] | Yes | Available installation sources (see below) |
| `contents` | object | Yes | Pack components summary |
| `dependencies` | object | No | Pack dependencies |
| `meta` | object | No | Additional metadata |
#### Install Source
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | Yes | Source type: "git" or "archive" |
| `url` | string | Yes | Source URL |
| `ref` | string | No | Git ref (tag, branch, commit) for git type |
| `checksum` | string | Yes | Format: "algorithm:hash" (e.g., "sha256:abc...") |
#### Contents Object
| Field | Type | Description |
|-------|------|-------------|
| `actions` | array[object] | List of actions with name and description |
| `sensors` | array[object] | List of sensors with name and description |
| `triggers` | array[object] | List of triggers with name and description |
| `rules` | array[object] | List of bundled rules |
| `workflows` | array[object] | List of bundled workflows |
#### Dependencies Object
| Field | Type | Description |
|-------|------|-------------|
| `attune_version` | string | Semver requirement (e.g., ">=0.1.0", "^1.0.0") |
| `python_version` | string | Python version requirement |
| `nodejs_version` | string | Node.js version requirement |
| `packs` | array[string] | Pack dependencies (format: "ref@version") |
---
## Pack Sources
Packs can be installed from multiple source types:
### 1. Git Repository
Install directly from a git repository:
```bash
attune pack install https://github.com/example/pack-slack.git
attune pack install https://github.com/example/pack-slack.git --ref v2.1.0
attune pack install https://github.com/example/pack-slack.git --ref main
attune pack install git@github.com:example/pack-slack.git --ref v2.1.0
```
**Requirements**:
- Repository must contain valid pack structure at root or in `pack/` subdirectory
- `pack.yaml` must be present
- Git client must be installed on system
### 2. Archive URL
Install from a zip or tar.gz archive:
```bash
attune pack install https://example.com/packs/slack-2.1.0.zip
attune pack install https://example.com/packs/slack-2.1.0.tar.gz
```
**Requirements**:
- Archive must contain pack directory structure
- Archive root or single top-level directory must contain `pack.yaml`
- Supported formats: `.zip`, `.tar.gz`, `.tgz`
### 3. Local Directory
Install from a local filesystem path:
```bash
attune pack install /path/to/pack-slack
attune pack install ./packs/my-pack
```
**Requirements**:
- Directory must contain valid pack structure
- `pack.yaml` must be present
- Used for development and testing
### 4. Local Archive
Upload and install from a local archive file:
```bash
attune pack install /path/to/pack-slack-2.1.0.zip
attune pack install ./my-pack.tar.gz
```
**Requirements**:
- Archive must contain valid pack structure
- Archive is uploaded to Attune API before installation
- Used for air-gapped or offline installations
### 5. Registry Reference
Install by pack reference, searching configured registries:
```bash
attune pack install slack
attune pack install slack@2.1.0
attune pack install slack@latest
```
**Requirements**:
- At least one registry must be configured
- Pack reference must exist in one of the registries
- Registries searched in configured priority order
---
## Configuration
### Registry Configuration
Add registry URLs to service configuration files:
**YAML Configuration** (`config.yaml`):
```yaml
pack_registry:
enabled: true
indices:
- url: https://registry.attune.io/index.json
priority: 1
enabled: true
name: "Official Attune Registry"
- url: https://company-internal.example.com/attune-registry.json
priority: 2
enabled: true
name: "Company Internal Registry"
headers:
Authorization: "Bearer ${REGISTRY_TOKEN}"
- url: file:///opt/attune/local-registry.json
priority: 3
enabled: true
name: "Local Filesystem Registry"
# Cache settings
cache_ttl: 3600 # Cache index for 1 hour
cache_enabled: true
# Download settings
timeout: 120
verify_checksums: true
allow_http: false # Only allow HTTPS
```
**Environment Variables**:
```bash
# Enable/disable registry
export ATTUNE__PACK_REGISTRY__ENABLED=true
# Set registry URLs (comma-separated, in priority order)
export ATTUNE__PACK_REGISTRY__INDICES="https://registry.attune.io/index.json,https://internal.example.com/registry.json"
# Cache settings
export ATTUNE__PACK_REGISTRY__CACHE_TTL=3600
export ATTUNE__PACK_REGISTRY__VERIFY_CHECKSUMS=true
```
### Priority-Based Search
Registries are searched in **priority order** (lowest priority number first):
1. **Priority 1**: Official Attune Registry (public packs)
2. **Priority 2**: Company Internal Registry (private packs)
3. **Priority 3**: Local Filesystem Registry (development packs)
When installing by reference (e.g., `attune pack install slack`):
- Search priority 1 registry first
- If not found, search priority 2
- If not found, search priority 3
- If not found in any registry, return error
**Use Cases**:
- **Override public packs**: Company registry can provide custom version of "slack" pack
- **Private packs**: Internal registry can host proprietary packs
- **Development**: Local registry can provide development versions
### Registry Headers
For authenticated registries, configure custom HTTP headers:
```yaml
pack_registry:
indices:
- url: https://private-registry.example.com/index.json
headers:
Authorization: "Bearer ${PRIVATE_REGISTRY_TOKEN}"
X-Custom-Header: "value"
```
---
## CLI Commands
### Install Pack
```bash
# From registry (by reference)
attune pack install <pack-ref>[@version]
# From git repository
attune pack install <git-url> [--ref <branch|tag|commit>]
# From archive URL
attune pack install <https-url>
# From local directory
attune pack install <local-path>
# From local archive
attune pack install <local-archive-path>
# Options
--force # Force reinstall if already exists
--skip-tests # Skip running pack tests
--skip-deps # Skip installing dependencies
--registry <name> # Use specific registry (skip priority search)
--no-registry # Don't search registries (direct install only)
```
### Examples
```bash
# Install latest version from registry
attune pack install slack
# Install specific version from registry
attune pack install slack@2.1.0
# Install from git repository (latest tag)
attune pack install https://github.com/example/pack-slack.git
# Install from git repository (specific tag)
attune pack install https://github.com/example/pack-slack.git --ref v2.1.0
# Install from git repository (branch)
attune pack install https://github.com/example/pack-slack.git --ref main
# Install from archive URL
attune pack install https://example.com/packs/slack-2.1.0.zip
# Install from local directory (development)
attune pack install ./packs/my-pack
# Install from local archive
attune pack install ./slack-2.1.0.zip
# Force reinstall
attune pack install slack --force
# Skip tests (faster, but not recommended)
attune pack install slack --skip-tests
```
### Generate Index Entry
For pack maintainers, generate an index entry from a pack:
```bash
attune pack index-entry \
--pack-dir <path-to-pack> \
--version <version> \
--git-url <git-repo-url> \
--git-ref <tag-or-branch> \
--archive-url <archive-url>
# Output to stdout (JSON)
attune pack index-entry --pack-dir ./pack-slack --version 2.1.0 \
--git-url https://github.com/example/pack-slack.git \
--git-ref v2.1.0 \
--archive-url https://example.com/packs/slack-2.1.0.zip
# Append to existing index file
attune pack index-entry --pack-dir ./pack-slack --version 2.1.0 \
--git-url https://github.com/example/pack-slack.git \
--git-ref v2.1.0 \
--archive-url https://example.com/packs/slack-2.1.0.zip \
--index-file registry.json \
--output registry.json
```
**Output Example**:
```json
{
"ref": "slack",
"label": "Slack Integration",
"description": "Send messages, upload files, and monitor Slack channels",
"version": "2.1.0",
"author": "Example Team",
"email": "team@example.com",
"license": "Apache-2.0",
"runtime_deps": ["python3"],
"install_sources": [
{
"type": "git",
"url": "https://github.com/example/pack-slack.git",
"ref": "v2.1.0",
"checksum": "sha256:abc123..."
},
{
"type": "archive",
"url": "https://example.com/packs/slack-2.1.0.zip",
"checksum": "sha256:def456..."
}
],
"contents": {
"actions": [...],
"sensors": [...],
"triggers": [...]
}
}
```
### Update Index File
Merge multiple index entries or update an existing index:
```bash
# Add entry to index
attune pack index-update --index registry.json --entry entry.json
# Merge multiple indices
attune pack index-merge --output combined.json registry1.json registry2.json
# Update pack version in index
attune pack index-update --index registry.json --pack slack --version 2.1.1 \
--git-ref v2.1.1 --archive-url https://example.com/packs/slack-2.1.1.zip
```
### List Registries
```bash
attune pack registries
# Output:
# Priority | Name | URL | Status
# ---------|-------------------------|------------------------------------------|--------
# 1 | Official Attune Registry| https://registry.attune.io/index.json | Online
# 2 | Company Internal | https://internal.example.com/registry.json| Online
# 3 | Local Development | file:///opt/attune/local-registry.json | Online
```
### Search Registry
```bash
# Search all registries
attune pack search <keyword>
# Search specific registry
attune pack search <keyword> --registry "Official Attune Registry"
# Example
attune pack search slack
# Output:
# Ref | Version | Description | Registry
# -------|---------|------------------------------------------|-------------------------
# slack | 2.1.0 | Send messages and monitor Slack channels | Official Attune Registry
```
---
## Installation Process
### Installation Workflow
```
┌─────────────────────────────────────────────────────────────────────┐
│ 1. Source Resolution │
│ - Registry reference → Search indices → Resolve install source │
│ - Direct URL → Use provided source │
│ - Local path → Use local filesystem │
└────────────────┬────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 2. Download/Copy Pack │
│ - Git: Clone repository to temp directory │
│ - Archive: Download and extract to temp directory │
│ - Local: Copy to temp directory │
└────────────────┬────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 3. Validate Pack Structure │
│ - Verify pack.yaml exists and is valid │
│ - Verify pack ref matches (if installing from registry) │
│ - Verify version matches (if specified) │
│ - Validate pack structure (actions, sensors, triggers) │
└────────────────┬────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 4. Check Dependencies │
│ - Verify Attune version compatibility │
│ - Check runtime dependencies (Python, Node.js, etc.) │
│ - Verify dependent packs are installed │
│ - Check Python/Node.js version requirements │
└────────────────┬────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 5. Setup Worker Environment │
│ - Python: Create virtualenv, install requirements.txt │
│ - Node.js: Create node_modules, run npm install │
│ - Shell: Verify scripts are executable │
└────────────────┬────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 6. Run Pack Tests (if present) │
│ - Execute test suite defined in pack │
│ - Verify all tests pass │
│ - Skip if --skip-tests flag provided │
└────────────────┬────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 7. Register Pack Components │
│ - Insert pack metadata into database │
│ - Register actions, sensors, triggers │
│ - Register bundled rules and workflows (if any) │
│ - Copy pack files to permanent location │
└────────────────┬────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 8. Cleanup │
│ - Remove temporary directory │
│ - Log installation success │
│ - Return pack ID and metadata │
└─────────────────────────────────────────────────────────────────────┘
```
### Pack Storage Location
Installed packs are stored in the configured packs directory:
```
/var/lib/attune/packs/
├── slack/
│ ├── pack.yaml
│ ├── actions/
│ ├── sensors/
│ ├── triggers/
│ ├── requirements.txt
│ ├── .venv/ # Python virtualenv (if applicable)
│ └── metadata.json # Installation metadata
├── aws/
└── github/
```
Installation metadata includes:
```json
{
"pack_ref": "slack",
"version": "2.1.0",
"installed_at": "2024-01-20T12:00:00Z",
"installed_from": {
"type": "git",
"url": "https://github.com/example/pack-slack.git",
"ref": "v2.1.0"
},
"checksum": "sha256:abc123...",
"registry": "Official Attune Registry"
}
```
---
## Checksum Verification
To ensure pack integrity, checksums are verified during installation:
### Supported Algorithms
- `sha256` (recommended)
- `sha512` (recommended)
- `sha1` (legacy, not recommended)
- `md5` (legacy, not recommended)
### Checksum Format
```
algorithm:hash
```
Examples:
- `sha256:abc123def456...`
- `sha512:789xyz...`
### Generating Checksums
For pack maintainers:
```bash
# Git repository (tar.gz snapshot)
sha256sum pack-slack-2.1.0.tar.gz
# Zip archive
sha256sum pack-slack-2.1.0.zip
# Using attune CLI
attune pack checksum ./pack-slack-2.1.0.zip
```
### Verification Process
1. Download/extract pack to temporary location
2. Calculate checksum of downloaded content
3. Compare with checksum in index file
4. If mismatch, abort installation and report error
5. If `verify_checksums: false` in config, skip verification (not recommended)
---
## CI/CD Integration
### GitHub Actions Example
Automate pack building and registry updates:
```yaml
name: Build and Publish Pack
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Create pack archive
run: |
VERSION=${GITHUB_REF#refs/tags/v}
zip -r pack-slack-${VERSION}.zip . -x ".git/*" ".github/*"
- name: Calculate checksum
id: checksum
run: |
CHECKSUM=$(sha256sum pack-slack-*.zip | awk '{print $1}')
echo "checksum=sha256:${CHECKSUM}" >> $GITHUB_OUTPUT
- name: Upload to artifact storage
run: |
VERSION=${GITHUB_REF#refs/tags/v}
aws s3 cp pack-slack-${VERSION}.zip s3://my-bucket/packs/
- name: Generate registry entry
run: |
VERSION=${GITHUB_REF#refs/tags/v}
attune pack index-entry \
--pack-dir . \
--version ${VERSION} \
--git-url https://github.com/example/pack-slack.git \
--git-ref ${GITHUB_REF#refs/tags/} \
--archive-url https://my-bucket.s3.amazonaws.com/packs/pack-slack-${VERSION}.zip \
--checksum ${{ steps.checksum.outputs.checksum }} \
> entry.json
- name: Update registry index
run: |
# Download current index
wget https://registry.example.com/index.json
# Add new entry
attune pack index-update \
--index index.json \
--entry entry.json \
--output index.json
# Upload updated index
aws s3 cp index.json s3://registry.example.com/
```
---
## Error Handling
### Installation Errors
| Error | Cause | Resolution |
|-------|-------|------------|
| Pack not found in registry | Pack ref doesn't exist in any configured registry | Check pack name, verify registry is online |
| Checksum mismatch | Downloaded pack doesn't match expected checksum | Pack may be corrupted or tampered with; contact pack maintainer |
| Pack already installed | Pack with same ref already exists | Use `--force` to reinstall |
| Dependency not met | Required Attune version, runtime, or pack not available | Update Attune, install runtime, or install dependency pack |
| Invalid pack structure | pack.yaml missing or invalid | Fix pack structure |
| Tests failed | Pack tests did not pass | Fix pack code or use `--skip-tests` (not recommended) |
### Registry Errors
| Error | Cause | Resolution |
|-------|-------|------------|
| Registry unreachable | Network error, DNS failure | Check network, verify URL |
| Invalid index format | Index JSON is malformed | Contact registry maintainer |
| Authentication failed | Registry requires authentication but token is invalid | Update registry token in configuration |
---
## Security Considerations
### 1. HTTPS Only (Recommended)
Configure `allow_http: false` to reject non-HTTPS registries:
```yaml
pack_registry:
allow_http: false # Only allow HTTPS
```
### 2. Checksum Verification
Always enable checksum verification in production:
```yaml
pack_registry:
verify_checksums: true
```
### 3. Registry Authentication
For private registries, use secure token storage:
```bash
export REGISTRY_TOKEN=$(cat /run/secrets/registry_token)
export ATTUNE__PACK_REGISTRY__INDICES="https://registry.example.com/index.json"
```
### 4. Code Review
- Review pack code before installation
- Use `--skip-tests` cautiously
- Test packs in non-production environment first
### 5. Signature Verification (Future)
Future enhancement: GPG signature verification for pack archives:
```json
{
"type": "archive",
"url": "https://example.com/packs/slack-2.1.0.zip",
"checksum": "sha256:abc123...",
"signature": "https://example.com/packs/slack-2.1.0.zip.sig",
"signing_key": "0x1234567890ABCDEF"
}
```
---
## Future Enhancements
### Version 1.1
- **Semantic version matching**: `slack@^2.0.0`, `slack@~2.1.0`
- **Pack updates**: `attune pack update <ref>` to upgrade to latest version
- **Dependency resolution**: Automatic installation of pack dependencies
### Version 1.2
- **GPG signature verification**: Cryptographic verification of pack authenticity
- **Pack ratings and reviews**: Community feedback in registry
- **Usage statistics**: Download counts, popularity metrics
### Version 1.3
- **Private pack authentication**: Token-based authentication for private packs
- **Pack mirroring**: Automatic mirroring of registry indices for redundancy
- **Delta updates**: Only download changed files when updating packs
---
## Related Documentation
- [Pack Structure](./pack-structure.md)
- [Pack Management Architecture](./pack-management-architecture.md)
- [CLI Documentation](./cli.md)
- [Configuration Guide](./configuration.md)
- [Pack Testing Framework](./pack-testing-framework.md)

View File

@@ -0,0 +1,531 @@
# Pack Structure Documentation
**Last Updated**: 2024-01-20
**Status**: Reference Documentation
---
## Overview
Attune packs are bundles of automation components (actions, sensors, triggers, rules, workflows) organized in a standardized directory structure. This document defines the canonical pack structure and file formats.
---
## Pack Directory Structure
```
packs/<pack_name>/
├── pack.yaml # Pack manifest (required)
├── README.md # Pack documentation (recommended)
├── CHANGELOG.md # Version history (recommended)
├── LICENSE # License file (recommended)
├── requirements.txt # Python dependencies (optional)
├── package.json # Node.js dependencies (optional)
├── actions/ # Action definitions
│ ├── <action_name>.yaml # Action metadata
│ ├── <action_name>.sh # Shell action implementation
│ ├── <action_name>.py # Python action implementation
│ └── <action_name>.js # Node.js action implementation
├── sensors/ # Sensor definitions
│ ├── <sensor_name>.yaml # Sensor metadata
│ ├── <sensor_name>.py # Python sensor implementation
│ └── <sensor_name>.js # Node.js sensor implementation
├── triggers/ # Trigger type definitions
│ └── <trigger_name>.yaml # Trigger metadata
├── rules/ # Rule definitions (optional)
│ └── <rule_name>.yaml # Rule metadata
├── workflows/ # Workflow definitions (optional)
│ └── <workflow_name>.yaml # Workflow metadata
├── policies/ # Policy definitions (optional)
│ └── <policy_name>.yaml # Policy metadata
├── config.schema.yaml # Pack configuration schema (optional)
├── icon.png # Pack icon (optional)
└── tests/ # Tests (optional)
├── test_actions.py
└── test_sensors.py
```
---
## File Formats
### Pack Manifest (`pack.yaml`)
The pack manifest is the main metadata file for a pack. It defines the pack's identity, version, dependencies, and configuration.
**Required Fields:**
- `ref` (string): Unique pack reference/identifier (lowercase, alphanumeric, hyphens, underscores)
- `label` (string): Human-readable pack name
- `description` (string): Brief description of the pack
- `version` (string): Semantic version (e.g., "1.0.0")
**Optional Fields:**
- `author` (string): Pack author name
- `email` (string): Author email
- `system` (boolean): Whether this is a system pack (default: false)
- `enabled` (boolean): Whether pack is enabled by default (default: true)
- `conf_schema` (object): JSON Schema for pack configuration
- `config` (object): Default pack configuration
- `meta` (object): Additional metadata
- `tags` (array): Tags for categorization
- `runtime_deps` (array): Runtime dependencies (e.g., "python3", "nodejs", "shell")
**Example:**
```yaml
ref: core
label: "Core Pack"
description: "Built-in core functionality including timer triggers and basic actions"
version: "1.0.0"
author: "Attune Team"
email: "core@attune.io"
system: true
enabled: true
conf_schema:
type: object
properties:
max_action_timeout:
type: integer
description: "Maximum timeout for action execution in seconds"
default: 300
minimum: 1
maximum: 3600
required: []
config:
max_action_timeout: 300
meta:
category: "system"
keywords:
- "core"
- "utilities"
python_dependencies:
- "requests>=2.28.0"
documentation_url: "https://docs.attune.io/packs/core"
repository_url: "https://github.com/attune-io/attune"
tags:
- core
- system
- utilities
runtime_deps:
- shell
- python3
```
---
### Action Metadata (`actions/<action_name>.yaml`)
Action metadata files define the parameters, output schema, and execution details for actions.
**Required Fields:**
- `name` (string): Action name (matches filename)
- `ref` (string): Full action reference (e.g., "core.echo")
- `description` (string): Action description
- `runner_type` (string): Execution runtime (shell, python, nodejs, docker)
- `entry_point` (string): Script filename to execute
**Optional Fields:**
- `enabled` (boolean): Whether action is enabled (default: true)
- `parameters` (object): Parameter definitions (JSON Schema style)
- `output_schema` (object): Output schema definition
- `tags` (array): Tags for categorization
- `timeout` (integer): Default timeout in seconds
- `examples` (array): Usage examples
**Example:**
```yaml
name: echo
ref: core.echo
description: "Echo a message to stdout"
enabled: true
runner_type: shell
entry_point: echo.sh
parameters:
message:
type: string
description: "Message to echo"
required: true
default: "Hello, World!"
uppercase:
type: boolean
description: "Convert message to uppercase"
required: false
default: false
output_schema:
type: object
properties:
stdout:
type: string
description: "Standard output from the command"
exit_code:
type: integer
description: "Exit code (0 = success)"
tags:
- utility
- testing
```
---
### Action Implementation
Action implementations receive parameters as environment variables prefixed with `ATTUNE_ACTION_`.
**Shell Example (`actions/echo.sh`):**
```bash
#!/bin/bash
set -e
# Parse parameters from environment variables
MESSAGE="${ATTUNE_ACTION_MESSAGE:-Hello, World!}"
UPPERCASE="${ATTUNE_ACTION_UPPERCASE:-false}"
# Convert to uppercase if requested
if [ "$UPPERCASE" = "true" ]; then
MESSAGE=$(echo "$MESSAGE" | tr '[:lower:]' '[:upper:]')
fi
# Echo the message
echo "$MESSAGE"
# Exit successfully
exit 0
```
**Python Example (`actions/http_request.py`):**
```python
#!/usr/bin/env python3
import json
import os
import sys
def get_env_param(name: str, default=None):
"""Get action parameter from environment variable."""
env_key = f"ATTUNE_ACTION_{name.upper()}"
return os.environ.get(env_key, default)
def main():
url = get_env_param("url", required=True)
method = get_env_param("method", "GET")
# Perform action logic
result = {
"url": url,
"method": method,
"success": True
}
# Output result as JSON
print(json.dumps(result, indent=2))
sys.exit(0)
if __name__ == "__main__":
main()
```
---
### Sensor Metadata (`sensors/<sensor_name>.yaml`)
Sensor metadata files define sensors that monitor for events and fire triggers.
**Required Fields:**
- `name` (string): Sensor name
- `ref` (string): Full sensor reference (e.g., "core.interval_timer_sensor")
- `description` (string): Sensor description
- `runner_type` (string): Execution runtime (python, nodejs)
- `entry_point` (string): Script filename to execute
- `trigger_types` (array): List of trigger types this sensor monitors
**Optional Fields:**
- `enabled` (boolean): Whether sensor is enabled (default: true)
- `parameters` (object): Sensor configuration parameters
- `poll_interval` (integer): Poll interval in seconds
- `tags` (array): Tags for categorization
- `meta` (object): Additional metadata
**Example:**
```yaml
name: interval_timer_sensor
ref: core.interval_timer_sensor
description: "Monitors time and fires interval timer triggers"
enabled: true
runner_type: python
entry_point: interval_timer_sensor.py
trigger_types:
- core.intervaltimer
parameters:
check_interval_seconds:
type: integer
description: "How often to check if triggers should fire"
default: 1
minimum: 1
maximum: 60
poll_interval: 1
tags:
- timer
- system
- builtin
meta:
builtin: true
system: true
```
---
### Sensor Implementation
Sensors run continuously and emit events to stdout as JSON, one per line.
**Python Example (`sensors/interval_timer_sensor.py`):**
```python
#!/usr/bin/env python3
import json
import time
from datetime import datetime
def check_triggers():
"""Check configured triggers and return events to fire."""
# Load trigger instances from environment
# Check if any should fire
# Return list of events
return []
def main():
while True:
events = check_triggers()
# Output events as JSON (one per line)
for event in events:
print(json.dumps(event))
sys.stdout.flush()
# Sleep until next check
time.sleep(1)
if __name__ == "__main__":
main()
```
---
### Trigger Metadata (`triggers/<trigger_name>.yaml`)
Trigger metadata files define event types that sensors can fire.
**Required Fields:**
- `name` (string): Trigger name
- `ref` (string): Full trigger reference (e.g., "core.intervaltimer")
- `description` (string): Trigger description
- `type` (string): Trigger type (interval, cron, one_shot, webhook, custom)
**Optional Fields:**
- `enabled` (boolean): Whether trigger is enabled (default: true)
- `parameters_schema` (object): Schema for trigger instance configuration
- `payload_schema` (object): Schema for event payload
- `tags` (array): Tags for categorization
- `examples` (array): Configuration examples
**Example:**
```yaml
name: intervaltimer
ref: core.intervaltimer
description: "Fires at regular intervals"
enabled: true
type: interval
parameters_schema:
type: object
properties:
unit:
type: string
enum: [seconds, minutes, hours]
description: "Time unit for the interval"
interval:
type: integer
minimum: 1
description: "Number of time units between triggers"
required: [unit, interval]
payload_schema:
type: object
properties:
type:
type: string
const: interval
interval_seconds:
type: integer
fired_at:
type: string
format: date-time
required: [type, interval_seconds, fired_at]
tags:
- timer
- interval
examples:
- description: "Fire every 10 seconds"
parameters:
unit: "seconds"
interval: 10
```
---
## Pack Loading Process
When a pack is loaded, Attune performs the following steps:
1. **Parse Pack Manifest**: Read and validate `pack.yaml`
2. **Register Pack**: Insert pack metadata into database
3. **Load Actions**: Parse all `actions/*.yaml` files and register actions
4. **Load Sensors**: Parse all `sensors/*.yaml` files and register sensors
5. **Load Triggers**: Parse all `triggers/*.yaml` files and register triggers
6. **Load Rules** (optional): Parse all `rules/*.yaml` files
7. **Load Workflows** (optional): Parse all `workflows/*.yaml` files
8. **Validate Dependencies**: Check that all dependencies are available
9. **Apply Configuration**: Apply default configuration from pack manifest
---
## Pack Types
### System Packs
System packs are built-in packs that ship with Attune.
- `system: true` in pack manifest
- Installed automatically
- Cannot be uninstalled
- Examples: `core`, `system`, `utils`
### Community Packs
Community packs are third-party packs installed from repositories.
- `system: false` in pack manifest
- Installed via CLI or API
- Can be updated and uninstalled
- Examples: `slack`, `aws`, `github`, `datadog`
### Ad-Hoc Packs
Ad-hoc packs are user-created packs without code-based components.
- `system: false` in pack manifest
- Created via Web UI
- May only contain triggers (no actions/sensors)
- Used for custom webhook integrations
---
## Best Practices
### Naming Conventions
- **Pack refs**: lowercase, alphanumeric, hyphens/underscores (e.g., `my-pack`, `aws_ec2`)
- **Component refs**: `<pack_ref>.<component_name>` (e.g., `core.echo`, `slack.send_message`)
- **File names**: Match component names (e.g., `echo.yaml`, `echo.sh`)
### Versioning
- Use semantic versioning (MAJOR.MINOR.PATCH)
- Update `CHANGELOG.md` with each release
- Increment version in `pack.yaml` when releasing
### Documentation
- Include comprehensive `README.md` with usage examples
- Document all parameters and output schemas
- Provide example configurations
### Testing
- Include unit tests in `tests/` directory
- Test actions/sensors independently
- Validate parameter schemas
### Dependencies
- Specify all runtime dependencies in pack manifest
- Pin dependency versions in `requirements.txt` or `package.json`
- Test with minimum required versions
### Security
- Use `secret: true` for sensitive parameters (passwords, tokens, API keys)
- Validate all user inputs
- Sanitize command-line arguments to prevent injection
- Use HTTPS for API calls with SSL verification enabled
---
## Example Packs
### Minimal Pack
```
my-pack/
├── pack.yaml
├── README.md
└── actions/
├── hello.yaml
└── hello.sh
```
### Full-Featured Pack
```
slack-pack/
├── pack.yaml
├── README.md
├── CHANGELOG.md
├── LICENSE
├── requirements.txt
├── icon.png
├── actions/
│ ├── send_message.yaml
│ ├── send_message.py
│ ├── upload_file.yaml
│ └── upload_file.py
├── sensors/
│ ├── message_sensor.yaml
│ └── message_sensor.py
├── triggers/
│ ├── message_received.yaml
│ └── reaction_added.yaml
├── config.schema.yaml
└── tests/
├── test_actions.py
└── test_sensors.py
```
---
## Related Documentation
- [Pack Management Architecture](./pack-management-architecture.md)
- [Pack Management API](./api-packs.md)
- [Trigger and Sensor Architecture](./trigger-sensor-architecture.md)
- [Action Development Guide](./action-development-guide.md) (future)
- [Sensor Development Guide](./sensor-development-guide.md) (future)

View File

@@ -0,0 +1,831 @@
# Pack Testing Framework
**Status**: 🔄 Design Document
**Created**: 2026-01-20
**Purpose**: Define how packs are tested programmatically during installation and validation
---
## Overview
The Pack Testing Framework enables automatic discovery and execution of pack tests during:
- Pack installation/loading
- Pack updates
- System validation
- CI/CD pipelines
This ensures that packs work correctly in the target environment before they're activated.
---
## Design Principles
1. **Runtime-Aware Testing**: Tests execute in the same runtime as actions
2. **Fail-Fast Installation**: Packs don't activate if tests fail (unless forced)
3. **Dependency Validation**: Tests verify all dependencies are satisfied
4. **Standardized Results**: Common test result format across all runner types
5. **Optional but Recommended**: Tests are optional but strongly encouraged
6. **Self-Documenting**: Test results stored for auditing and troubleshooting
---
## Pack Manifest Extension
### pack.yaml Schema Addition
```yaml
# Pack Testing Configuration
testing:
# Enable/disable testing during installation
enabled: true
# Test discovery method
discovery:
method: "directory" # directory, manifest, executable
path: "tests" # relative to pack root
# Test runners by runtime type
runners:
shell:
type: "script"
entry_point: "tests/run_tests.sh"
timeout: 60 # seconds
python:
type: "pytest"
entry_point: "tests/test_actions.py"
requirements: "tests/requirements-test.txt" # optional
timeout: 120
node:
type: "jest"
entry_point: "tests/"
config: "tests/jest.config.js"
timeout: 90
# Test result expectations
result_format: "junit-xml" # junit-xml, tap, json
result_path: "tests/results/" # where to find test results
# Minimum passing criteria
min_pass_rate: 1.0 # 100% tests must pass (0.0-1.0)
# What to do on test failure
on_failure: "block" # block, warn, ignore
```
### Example: Core Pack
```yaml
# packs/core/pack.yaml
ref: core
label: "Core Pack"
version: "1.0.0"
# ... existing config ...
testing:
enabled: true
discovery:
method: "directory"
path: "tests"
runners:
shell:
type: "script"
entry_point: "tests/run_tests.sh"
timeout: 60
python:
type: "pytest"
entry_point: "tests/test_actions.py"
timeout: 120
result_format: "junit-xml"
result_path: "tests/results/"
min_pass_rate: 1.0
on_failure: "block"
```
---
## Test Discovery Methods
### Method 1: Directory-Based (Recommended)
**Convention**:
```
pack_name/
├── actions/
├── sensors/
├── tests/ # Test directory
│ ├── run_tests.sh # Shell test runner
│ ├── test_*.py # Python tests
│ ├── test_*.js # Node.js tests
│ └── results/ # Test output directory
└── pack.yaml
```
**Discovery Logic**:
1. Check if `tests/` directory exists
2. Look for test runners matching pack's runtime types
3. Execute all discovered test runners
4. Aggregate results
### Method 2: Manifest-Based
**Explicit test listing in pack.yaml**:
```yaml
testing:
enabled: true
discovery:
method: "manifest"
tests:
- name: "Action Tests"
runner: "python"
command: "pytest tests/test_actions.py -v"
timeout: 60
- name: "Integration Tests"
runner: "shell"
command: "bash tests/integration_tests.sh"
timeout: 120
```
### Method 3: Executable-Based
**Single test executable**:
```yaml
testing:
enabled: true
discovery:
method: "executable"
command: "make test"
timeout: 180
```
---
## Test Execution Workflow
### 1. Pack Installation Flow
```
User: attune pack install ./packs/my_pack
CLI validates pack structure
CLI reads pack.yaml → testing section
CLI discovers test runners
For each runtime type in pack:
Worker Service executes tests
Collect test results
Parse results (JUnit XML, JSON, etc.)
All tests pass?
↓ YES ↓ NO
↓ ↓
Activate pack on_failure = "block"?
↓ YES ↓ NO
↓ ↓
Abort install Show warning,
Show errors allow install
```
### 2. Test Execution Process
```rust
// Pseudocode for test execution
async fn execute_pack_tests(pack: &Pack) -> TestResults {
let test_config = pack.testing.unwrap_or_default();
if !test_config.enabled {
return TestResults::Skipped;
}
let mut results = Vec::new();
// Discover tests based on method
let tests = discover_tests(&pack, &test_config)?;
// Execute each test suite
for test_suite in tests {
let runtime = get_runtime_for_test(test_suite.runner)?;
let result = runtime.execute_test(
test_suite.command,
test_suite.timeout,
test_suite.env_vars
).await?;
results.push(result);
}
// Parse and aggregate results
let aggregate = aggregate_test_results(results, test_config.result_format)?;
// Store in database
store_test_results(&pack, &aggregate).await?;
// Check pass criteria
if aggregate.pass_rate < test_config.min_pass_rate {
return TestResults::Failed(aggregate);
}
TestResults::Passed(aggregate)
}
```
---
## Test Result Format
### Standardized Test Result Structure
```rust
// Common library: models.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackTestResult {
pub pack_ref: String,
pub pack_version: String,
pub execution_time: DateTime<Utc>,
pub total_tests: i32,
pub passed: i32,
pub failed: i32,
pub skipped: i32,
pub pass_rate: f64,
pub duration_ms: i64,
pub test_suites: Vec<TestSuiteResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestSuiteResult {
pub name: String,
pub runner_type: String, // shell, python, node
pub total: i32,
pub passed: i32,
pub failed: i32,
pub skipped: i32,
pub duration_ms: i64,
pub test_cases: Vec<TestCaseResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestCaseResult {
pub name: String,
pub status: TestStatus,
pub duration_ms: i64,
pub error_message: Option<String>,
pub stdout: Option<String>,
pub stderr: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TestStatus {
Passed,
Failed,
Skipped,
Error,
}
```
### Example JSON Output
```json
{
"pack_ref": "core",
"pack_version": "1.0.0",
"execution_time": "2026-01-20T10:30:00Z",
"total_tests": 36,
"passed": 36,
"failed": 0,
"skipped": 0,
"pass_rate": 1.0,
"duration_ms": 20145,
"test_suites": [
{
"name": "Bash Test Runner",
"runner_type": "shell",
"total": 36,
"passed": 36,
"failed": 0,
"skipped": 0,
"duration_ms": 20145,
"test_cases": [
{
"name": "echo: basic message",
"status": "Passed",
"duration_ms": 245,
"error_message": null,
"stdout": "Hello, Attune!\n",
"stderr": null
},
{
"name": "noop: invalid exit code",
"status": "Passed",
"duration_ms": 189,
"error_message": null,
"stdout": "",
"stderr": "ERROR: exit_code must be between 0 and 255\n"
}
]
}
]
}
```
---
## Database Schema
### Migration: `add_pack_test_results.sql`
```sql
-- Pack test execution tracking
CREATE TABLE attune.pack_test_execution (
id BIGSERIAL PRIMARY KEY,
pack_id BIGINT NOT NULL REFERENCES attune.pack(id) ON DELETE CASCADE,
pack_version VARCHAR(50) NOT NULL,
execution_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
trigger_reason VARCHAR(50) NOT NULL, -- 'install', 'update', 'manual', 'validation'
total_tests INT NOT NULL,
passed INT NOT NULL,
failed INT NOT NULL,
skipped INT NOT NULL,
pass_rate DECIMAL(5,4) NOT NULL, -- 0.0000 to 1.0000
duration_ms BIGINT NOT NULL,
result JSONB NOT NULL, -- Full test result structure
created TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_pack_test_execution_pack_id ON attune.pack_test_execution(pack_id);
CREATE INDEX idx_pack_test_execution_time ON attune.pack_test_execution(execution_time DESC);
CREATE INDEX idx_pack_test_execution_pass_rate ON attune.pack_test_execution(pass_rate);
-- Pack test result summary view
CREATE VIEW attune.pack_test_summary AS
SELECT
p.id AS pack_id,
p.ref AS pack_ref,
p.label AS pack_label,
pte.pack_version,
pte.execution_time AS last_test_time,
pte.total_tests,
pte.passed,
pte.failed,
pte.skipped,
pte.pass_rate,
pte.trigger_reason,
ROW_NUMBER() OVER (PARTITION BY p.id ORDER BY pte.execution_time DESC) AS rn
FROM attune.pack p
LEFT JOIN attune.pack_test_execution pte ON p.id = pte.pack_id
WHERE pte.id IS NOT NULL;
-- Latest test results per pack
CREATE VIEW attune.pack_latest_test AS
SELECT
pack_id,
pack_ref,
pack_label,
pack_version,
last_test_time,
total_tests,
passed,
failed,
skipped,
pass_rate,
trigger_reason
FROM attune.pack_test_summary
WHERE rn = 1;
```
---
## Worker Service Integration
### Test Execution in Worker
```rust
// crates/worker/src/test_executor.rs
use attune_common::models::{PackTestResult, TestSuiteResult};
use std::path::PathBuf;
use std::time::Duration;
pub struct TestExecutor {
runtime_manager: Arc<RuntimeManager>,
}
impl TestExecutor {
pub async fn execute_pack_tests(
&self,
pack_dir: &PathBuf,
test_config: &TestConfig,
) -> Result<PackTestResult> {
let mut suites = Vec::new();
// Execute tests for each runner type
for (runner_type, runner_config) in &test_config.runners {
let suite_result = self.execute_test_suite(
pack_dir,
runner_type,
runner_config,
).await?;
suites.push(suite_result);
}
// Aggregate results
let total: i32 = suites.iter().map(|s| s.total).sum();
let passed: i32 = suites.iter().map(|s| s.passed).sum();
let failed: i32 = suites.iter().map(|s| s.failed).sum();
let skipped: i32 = suites.iter().map(|s| s.skipped).sum();
let duration_ms: i64 = suites.iter().map(|s| s.duration_ms).sum();
Ok(PackTestResult {
pack_ref: pack_dir.file_name().unwrap().to_string_lossy().to_string(),
pack_version: "1.0.0".to_string(), // TODO: Get from pack.yaml
execution_time: Utc::now(),
total_tests: total,
passed,
failed,
skipped,
pass_rate: if total > 0 { passed as f64 / total as f64 } else { 0.0 },
duration_ms,
test_suites: suites,
})
}
async fn execute_test_suite(
&self,
pack_dir: &PathBuf,
runner_type: &str,
runner_config: &RunnerConfig,
) -> Result<TestSuiteResult> {
let runtime = self.runtime_manager.get_runtime(runner_type)?;
// Build test command
let test_script = pack_dir.join(&runner_config.entry_point);
// Execute with timeout
let timeout = Duration::from_secs(runner_config.timeout);
let output = runtime.execute_with_timeout(
&test_script,
HashMap::new(), // env vars
timeout,
).await?;
// Parse test results based on format
let test_result = match runner_config.result_format.as_str() {
"junit-xml" => self.parse_junit_xml(&output.stdout)?,
"json" => self.parse_json_results(&output.stdout)?,
"tap" => self.parse_tap_results(&output.stdout)?,
_ => self.parse_simple_output(&output)?,
};
Ok(test_result)
}
fn parse_simple_output(&self, output: &CommandOutput) -> Result<TestSuiteResult> {
// Parse simple output format (what our bash runner uses)
// Look for patterns like:
// "Total Tests: 36"
// "Passed: 36"
// "Failed: 0"
let stdout = String::from_utf8_lossy(&output.stdout);
let total = self.extract_number(&stdout, "Total Tests:")?;
let passed = self.extract_number(&stdout, "Passed:")?;
let failed = self.extract_number(&stdout, "Failed:")?;
Ok(TestSuiteResult {
name: "Shell Test Runner".to_string(),
runner_type: "shell".to_string(),
total,
passed,
failed,
skipped: 0,
duration_ms: output.duration_ms,
test_cases: vec![], // Could parse individual test lines
})
}
}
```
---
## CLI Commands
### Pack Test Command
```bash
# Test a pack before installation
attune pack test ./packs/my_pack
# Test an installed pack
attune pack test core
# Test with verbose output
attune pack test core --verbose
# Test and show detailed results
attune pack test core --detailed
# Test specific runtime
attune pack test core --runtime python
# Force install even if tests fail
attune pack install ./packs/my_pack --skip-tests
attune pack install ./packs/my_pack --force
```
### CLI Implementation
```rust
// crates/cli/src/commands/pack.rs
pub async fn test_pack(pack_path: &str, options: TestOptions) -> Result<()> {
println!("🧪 Testing pack: {}", pack_path);
println!();
// Load pack configuration
let pack_yaml = load_pack_yaml(pack_path)?;
let test_config = pack_yaml.testing.ok_or("No test configuration found")?;
if !test_config.enabled {
println!("⚠️ Testing disabled for this pack");
return Ok(());
}
// Execute tests via worker
let client = create_worker_client().await?;
let result = client.execute_pack_tests(pack_path, test_config).await?;
// Display results
display_test_results(&result, options.verbose)?;
// Exit with appropriate code
if result.failed > 0 {
println!();
println!("❌ Tests failed: {}/{}", result.failed, result.total_tests);
std::process::exit(1);
} else {
println!();
println!("✅ All tests passed: {}/{}", result.passed, result.total_tests);
Ok(())
}
}
```
---
## Test Result Parsers
### JUnit XML Parser
```rust
// crates/worker/src/test_parsers/junit.rs
pub fn parse_junit_xml(xml: &str) -> Result<TestSuiteResult> {
// Parse JUnit XML format (pytest --junit-xml, Jest, etc.)
// <testsuite name="..." tests="36" failures="0" skipped="0" time="12.5">
// <testcase name="..." time="0.245" />
// <testcase name="..." time="0.189">
// <failure message="...">Stack trace</failure>
// </testcase>
// </testsuite>
// Implementation using quick-xml or roxmltree crate
}
```
### TAP Parser
```rust
// crates/worker/src/test_parsers/tap.rs
pub fn parse_tap(tap_output: &str) -> Result<TestSuiteResult> {
// Parse TAP (Test Anything Protocol) format
// 1..36
// ok 1 - echo: basic message
// ok 2 - echo: default message
// not ok 3 - echo: invalid parameter
// ---
// message: 'Expected failure'
// ...
}
```
---
## Pack Installation Integration
### Modified Pack Load Workflow
```rust
// crates/api/src/services/pack_service.rs
pub async fn install_pack(
pack_path: &Path,
options: InstallOptions,
) -> Result<Pack> {
// 1. Validate pack structure
validate_pack_structure(pack_path)?;
// 2. Load pack.yaml
let pack_config = load_pack_yaml(pack_path)?;
// 3. Check if testing is enabled
if pack_config.testing.map(|t| t.enabled).unwrap_or(false) {
if !options.skip_tests {
println!("🧪 Running pack tests...");
let test_result = execute_pack_tests(pack_path, &pack_config).await?;
// Store test results
store_test_results(&test_result).await?;
// Check if tests passed
if test_result.failed > 0 {
let on_failure = pack_config.testing
.and_then(|t| t.on_failure)
.unwrap_or(OnFailure::Block);
match on_failure {
OnFailure::Block => {
if !options.force {
return Err(Error::PackTestsFailed {
failed: test_result.failed,
total: test_result.total_tests,
});
}
}
OnFailure::Warn => {
eprintln!("⚠️ Warning: {} tests failed", test_result.failed);
}
OnFailure::Ignore => {
// Continue installation
}
}
} else {
println!("✅ All tests passed!");
}
}
}
// 4. Register pack in database
let pack = register_pack(&pack_config).await?;
// 5. Register actions, sensors, triggers
register_pack_components(&pack, pack_path).await?;
// 6. Set up runtime environments
setup_pack_environments(&pack, pack_path).await?;
Ok(pack)
}
```
---
## API Endpoints
### Test Results API
```rust
// GET /api/v1/packs/:pack_ref/tests
// List test executions for a pack
// GET /api/v1/packs/:pack_ref/tests/latest
// Get latest test results for a pack
// GET /api/v1/packs/:pack_ref/tests/:execution_id
// Get specific test execution details
// POST /api/v1/packs/:pack_ref/test
// Manually trigger pack tests
// GET /api/v1/packs/tests
// List all pack test results (admin)
```
---
## Best Practices for Pack Authors
### 1. Always Include Tests
```yaml
# pack.yaml
testing:
enabled: true
runners:
shell:
entry_point: "tests/run_tests.sh"
```
### 2. Test All Actions
Every action should have at least:
- One successful execution test
- One error handling test
- Parameter validation tests
### 3. Use Exit Codes Correctly
```bash
# tests/run_tests.sh
if [ $FAILURES -gt 0 ]; then
exit 1 # Non-zero exit = test failure
else
exit 0 # Zero exit = success
fi
```
### 4. Output Parseable Results
```bash
# Simple format the worker can parse
echo "Total Tests: $TOTAL"
echo "Passed: $PASSED"
echo "Failed: $FAILED"
```
### 5. Test Dependencies
```python
# tests/test_dependencies.py
def test_required_libraries():
"""Verify all required libraries are available"""
import requests
import croniter
assert True
```
---
## Implementation Phases
### Phase 1: Core Framework ✅ (Current)
- [x] Design document (this file)
- [x] Core pack tests implemented
- [x] Test infrastructure created
- [ ] Database schema for test results
- [ ] Worker test executor implementation
### Phase 2: Worker Integration
- [ ] Test executor in worker service
- [ ] Simple output parser
- [ ] Test result storage
- [ ] Error handling and timeouts
### Phase 3: CLI Integration
- [ ] `attune pack test` command
- [ ] Test result display
- [ ] Integration with pack install
- [ ] Force/skip test options
### Phase 4: Advanced Features
- [ ] JUnit XML parser
- [ ] TAP parser
- [ ] API endpoints for test results
- [ ] Web UI for test results
- [ ] Test history and trends
---
## Future Enhancements
- **Parallel Test Execution**: Run tests for different runtimes in parallel
- **Test Caching**: Cache test results for unchanged packs
- **Selective Testing**: Test only changed actions
- **Performance Benchmarks**: Track test execution time trends
- **Test Coverage Reports**: Integration with coverage tools
- **Remote Test Execution**: Distribute tests across workers
- **Test Environments**: Isolated test environments per pack
---
## Conclusion
The Pack Testing Framework provides a standardized way to validate packs during installation, ensuring reliability and catching issues early. By making tests a first-class feature of the pack system, we enable:
- **Confident Installation**: Know that packs will work before activating them
- **Dependency Validation**: Verify all required dependencies are present
- **Regression Prevention**: Detect breaking changes when updating packs
- **Quality Assurance**: Encourage pack authors to write comprehensive tests
- **Audit Trail**: Track test results over time for compliance and debugging
---
**Next Steps**: Implement Phase 1 (database schema and worker test executor) to enable programmatic test execution during pack installation.