re-uploading work
This commit is contained in:
723
docs/packs/PACK_TESTING.md
Normal file
723
docs/packs/PACK_TESTING.md
Normal 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
|
||||
258
docs/packs/QUICKREF-git-installation.md
Normal file
258
docs/packs/QUICKREF-git-installation.md
Normal 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`
|
||||
680
docs/packs/core-pack-integration.md
Normal file
680
docs/packs/core-pack-integration.md
Normal 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(¶ms);
|
||||
|
||||
// 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.
|
||||
382
docs/packs/pack-install-testing.md
Normal file
382
docs/packs/pack-install-testing.md
Normal 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
|
||||
```
|
||||
587
docs/packs/pack-installation-git.md
Normal file
587
docs/packs/pack-installation-git.md
Normal 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
|
||||
548
docs/packs/pack-registry-cicd.md
Normal file
548
docs/packs/pack-registry-cicd.md
Normal 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)
|
||||
841
docs/packs/pack-registry-spec.md
Normal file
841
docs/packs/pack-registry-spec.md
Normal 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)
|
||||
531
docs/packs/pack-structure.md
Normal file
531
docs/packs/pack-structure.md
Normal 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)
|
||||
831
docs/packs/pack-testing-framework.md
Normal file
831
docs/packs/pack-testing-framework.md
Normal 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.
|
||||
Reference in New Issue
Block a user