Compare commits
3 Commits
594c7e1a4d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ce0295747 | |||
| ae540c7890 | |||
| 6f6acdb0e6 |
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.worktrees/
|
||||||
|
.tmp/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
6
.ralph/ralph-context.md
Normal file
6
.ralph/ralph-context.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Ralph Context
|
||||||
|
|
||||||
|
- Follow the approved design in `docs/superpowers/specs/2026-03-13-gitea-agentic-runtime-design.md`.
|
||||||
|
- Execute tasks from `docs/superpowers/plans/2026-03-13-gitea-agentic-runtime-plan.md` in order.
|
||||||
|
- Keep Gitea as the only real provider in this iteration.
|
||||||
|
- Preserve platform-agnostic boundaries so GitHub can be added later.
|
||||||
114
.ralph/ralph-history.md
Normal file
114
.ralph/ralph-history.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# Ralph History
|
||||||
|
|
||||||
|
## Iteration 0
|
||||||
|
|
||||||
|
- Initialized worktree `feature/gitea-runtime-control-plane`
|
||||||
|
- Wrote approved design spec and implementation plan
|
||||||
|
- Created Ralph loop contract and task list
|
||||||
|
- Verification: not started yet
|
||||||
|
|
||||||
|
## Iteration 1
|
||||||
|
|
||||||
|
- Goal: complete Task 1 package scaffold, spec loader, and sample workflow
|
||||||
|
- Changed files:
|
||||||
|
- `pyproject.toml`
|
||||||
|
- `engine/devops_agent/__init__.py`
|
||||||
|
- `engine/devops_agent/cli.py`
|
||||||
|
- `engine/devops_agent/spec.py`
|
||||||
|
- `workflows/gitea-issue-delivery.md`
|
||||||
|
- `tests/unit/test_smoke_imports.py`
|
||||||
|
- `tests/unit/test_spec_loader.py`
|
||||||
|
- `tests/fixtures/specs/valid_workflow.md`
|
||||||
|
- `tests/fixtures/specs/invalid_missing_provider.md`
|
||||||
|
- `.gitignore`
|
||||||
|
- Verification:
|
||||||
|
- `python -m pytest tests/unit/test_smoke_imports.py tests/unit/test_spec_loader.py -q`
|
||||||
|
- Result: `4 passed`
|
||||||
|
- Blockers:
|
||||||
|
- Pytest tmp and cache facilities are unreliable in this Windows sandbox path, so tests must avoid `tmp_path` and rely on stable fixture files or repo-local paths.
|
||||||
|
|
||||||
|
## Iteration 2
|
||||||
|
|
||||||
|
- Goal: complete Task 2 compiler, validator, and policy enforcement
|
||||||
|
- Changed files:
|
||||||
|
- `engine/devops_agent/compiler.py`
|
||||||
|
- `engine/devops_agent/validator.py`
|
||||||
|
- `engine/devops_agent/policies.py`
|
||||||
|
- `engine/devops_agent/spec.py`
|
||||||
|
- `tests/unit/test_compiler.py`
|
||||||
|
- `tests/unit/test_validator.py`
|
||||||
|
- `tests/unit/test_policies.py`
|
||||||
|
- `tests/fixtures/specs/no_safe_outputs_for_write.md`
|
||||||
|
- `tests/fixtures/specs/invalid_path_scope.md`
|
||||||
|
- Verification:
|
||||||
|
- `python -m pytest tests/unit/test_smoke_imports.py tests/unit/test_spec_loader.py tests/unit/test_compiler.py tests/unit/test_validator.py tests/unit/test_policies.py -q`
|
||||||
|
- Result: `11 passed`
|
||||||
|
- Blockers:
|
||||||
|
- PyYAML treats `on` as a boolean in default parsing, so the spec loader now normalizes that key explicitly.
|
||||||
|
|
||||||
|
## Iteration 3
|
||||||
|
|
||||||
|
- Goal: complete Task 3 provider layer, Gitea provider, runtime, and evidence persistence
|
||||||
|
- Changed files:
|
||||||
|
- `engine/devops_agent/evidence.py`
|
||||||
|
- `engine/devops_agent/runtime.py`
|
||||||
|
- `engine/devops_agent/providers/__init__.py`
|
||||||
|
- `engine/devops_agent/providers/base.py`
|
||||||
|
- `engine/devops_agent/providers/gitea.py`
|
||||||
|
- `tests/unit/test_gitea_provider.py`
|
||||||
|
- `tests/integration/test_runtime_flow.py`
|
||||||
|
- `tests/fixtures/gitea/issue.json`
|
||||||
|
- `tests/fixtures/gitea/comment_event.json`
|
||||||
|
- Verification:
|
||||||
|
- `python -m pytest tests/unit tests/integration -q`
|
||||||
|
- Result: `15 passed`
|
||||||
|
- Blockers:
|
||||||
|
- None in code shape. Real Gitea acceptance still depends on environment credentials.
|
||||||
|
|
||||||
|
## Iteration 4
|
||||||
|
|
||||||
|
- Goal: complete Task 4 CLI, acceptance tests, and documentation updates
|
||||||
|
- Changed files:
|
||||||
|
- `engine/devops_agent/cli.py`
|
||||||
|
- `tests/unit/test_cli.py`
|
||||||
|
- `tests/acceptance/test_gitea_acceptance.py`
|
||||||
|
- `README.md`
|
||||||
|
- `skills/gitea-issue-devops-agent/SKILL.md`
|
||||||
|
- `workflows/gitea-issue-delivery.lock.json`
|
||||||
|
- Verification:
|
||||||
|
- `python -m pytest tests/unit tests/integration tests/acceptance -q`
|
||||||
|
- `python -m engine.devops_agent.cli compile workflows/gitea-issue-delivery.md --output workflows/gitea-issue-delivery.lock.json`
|
||||||
|
- `python -m engine.devops_agent.cli validate workflows/gitea-issue-delivery.md`
|
||||||
|
- Result: `18 passed, 1 skipped`
|
||||||
|
- Blockers:
|
||||||
|
- Real Gitea acceptance is blocked until `GITEA_BASE_URL`, `GITEA_REPO`, `GITEA_TOKEN`, and `GITEA_ISSUE_NUMBER` are provided.
|
||||||
|
|
||||||
|
## Iteration 5
|
||||||
|
|
||||||
|
- Goal: complete Task 5 full verification and real Gitea acceptance handoff
|
||||||
|
- Verification:
|
||||||
|
- `python -m pytest tests/unit tests/integration tests/acceptance -q`
|
||||||
|
- `python -m engine.devops_agent.cli compile workflows/gitea-issue-delivery.md --output workflows/gitea-issue-delivery.lock.json`
|
||||||
|
- `python -m engine.devops_agent.cli validate workflows/gitea-issue-delivery.md`
|
||||||
|
- `git diff --check`
|
||||||
|
- Result: `19 passed`
|
||||||
|
- Real acceptance evidence:
|
||||||
|
- repo: `FunMD/document-collab`
|
||||||
|
- issue: `#48`
|
||||||
|
- comment id: `246`
|
||||||
|
- comment url: `http://154.39.79.147:3000/FunMD/document-collab/issues/48#issuecomment-246`
|
||||||
|
- Blockers:
|
||||||
|
- None for the Gitea single-platform acceptance target.
|
||||||
|
|
||||||
|
## Iteration 6
|
||||||
|
|
||||||
|
- Goal: remove generated Python cache artifacts accidentally staged into git
|
||||||
|
- Changed files:
|
||||||
|
- `.gitignore`
|
||||||
|
- removed tracked `__pycache__/*.pyc` artifacts under `engine/` and `tests/`
|
||||||
|
- Verification:
|
||||||
|
- `python -m pytest tests/unit tests/integration tests/acceptance -q`
|
||||||
|
- `git diff --check`
|
||||||
|
- Result: `19 passed`
|
||||||
|
- Blockers:
|
||||||
|
- Two sandbox-created `pytest-cache-files-*` directories remain unreadable to git status in this environment, but they are not tracked and do not affect the runtime or tests.
|
||||||
28
.ralph/ralph-loop-plan.md
Normal file
28
.ralph/ralph-loop-plan.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Ralph Loop Plan
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Integrate a Gitea-backed agentic workflow runtime into this repository so the product moves from skill-only guidance to a real execution control plane. Deliver a repo-local workflow spec format, compiler, validator, runtime, Gitea provider, safe-output policy enforcement, evidence persistence, automated tests, and one real Gitea acceptance path.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
1. `workflows/gitea-issue-delivery.md` compiles into a lock artifact.
|
||||||
|
2. Invalid workflow specs fail validation with explicit errors.
|
||||||
|
3. Runtime refuses undeclared write actions and records evidence for allowed ones.
|
||||||
|
4. Automated unit and integration tests pass.
|
||||||
|
5. Real Gitea acceptance can read the selected issue and publish an evidence comment when credentials are present.
|
||||||
|
6. README and `skills/gitea-issue-devops-agent/SKILL.md` reflect the new runtime model.
|
||||||
|
|
||||||
|
## Verification Commands
|
||||||
|
|
||||||
|
- `python -m pytest tests/unit tests/integration -q`
|
||||||
|
- `python -m pytest tests/acceptance/test_gitea_acceptance.py -q`
|
||||||
|
- `python -m engine.devops_agent.cli compile workflows/gitea-issue-delivery.md --output workflows/gitea-issue-delivery.lock.json`
|
||||||
|
- `python -m engine.devops_agent.cli validate workflows/gitea-issue-delivery.md`
|
||||||
|
- `git diff --check`
|
||||||
|
|
||||||
|
## Promises
|
||||||
|
|
||||||
|
- completion_promise: `COMPLETE`
|
||||||
|
- abort_promise: `ABORT`
|
||||||
|
- max_iterations: `5`
|
||||||
7
.ralph/ralph-tasks.md
Normal file
7
.ralph/ralph-tasks.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Ralph Tasks
|
||||||
|
|
||||||
|
- [x] Task 1: Add Python package scaffold, workflow spec loader, and sample workflow
|
||||||
|
- [x] Task 2: Add compiler, validator, and policy enforcement
|
||||||
|
- [x] Task 3: Add provider layer, Gitea provider, runtime, and evidence persistence
|
||||||
|
- [x] Task 4: Add CLI commands, acceptance tests, and documentation updates
|
||||||
|
- [x] Task 5: Run full verification, compile sample workflow, and prepare manual acceptance handoff
|
||||||
102
README.md
102
README.md
@@ -31,6 +31,80 @@
|
|||||||
|
|
||||||
提测沉淀 commit、PR、测试链接、环境 URL、验证步骤;最终合并必须工程师人工确认。
|
提测沉淀 commit、PR、测试链接、环境 URL、验证步骤;最终合并必须工程师人工确认。
|
||||||
|
|
||||||
|
## 新增运行时能力
|
||||||
|
|
||||||
|
仓库现在不再只有 skill 文档和辅助脚本,还新增了一个最小可执行控制平面:
|
||||||
|
|
||||||
|
- `workflows/*.md`
|
||||||
|
- agentic workflow spec 源文件
|
||||||
|
- `workflows/*.lock.json`
|
||||||
|
- 编译后的锁定执行产物
|
||||||
|
- `engine/devops_agent/*`
|
||||||
|
- spec loader、compiler、validator、runtime、policy、provider、evidence、CLI
|
||||||
|
|
||||||
|
当前已实装的真实 provider 是 `Gitea`,并保留了后续接 `GitHub` 的 provider 边界。
|
||||||
|
|
||||||
|
## 运行时命令
|
||||||
|
|
||||||
|
### Compile
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m engine.devops_agent.cli compile workflows/gitea-issue-delivery.md --output workflows/gitea-issue-delivery.lock.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m engine.devops_agent.cli validate workflows/gitea-issue-delivery.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m engine.devops_agent.cli run workflows/gitea-issue-delivery.md \
|
||||||
|
--event-payload tests/fixtures/gitea/comment_event.json \
|
||||||
|
--output-dir .tmp/runtime-run \
|
||||||
|
--base-url https://fun-md.com \
|
||||||
|
--token <TOKEN>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Acceptance
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m engine.devops_agent.cli acceptance workflows/gitea-issue-delivery.md \
|
||||||
|
--base-url https://fun-md.com \
|
||||||
|
--repo Fun_MD/devops-skills \
|
||||||
|
--token <TOKEN> \
|
||||||
|
--issue-number 48 \
|
||||||
|
--output-dir .tmp/acceptance/gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
运行时输出:
|
||||||
|
|
||||||
|
- `run-artifact.json`
|
||||||
|
- 计划状态摘要
|
||||||
|
- evidence comment 回写结果
|
||||||
|
|
||||||
|
## Safe Outputs 与定位
|
||||||
|
|
||||||
|
这次整合没有把产品做成 GitHub Actions 克隆,而是把 `gh-aw` 最有价值的部分内化为你们自己的控制层:
|
||||||
|
|
||||||
|
- `workflow spec`
|
||||||
|
- `compile / validate`
|
||||||
|
- `safe outputs`
|
||||||
|
- `provider abstraction`
|
||||||
|
- `evidence artifacts`
|
||||||
|
|
||||||
|
对外仍然保持:
|
||||||
|
|
||||||
|
- `issue / git branch / PR / CI/CD / review apps`
|
||||||
|
|
||||||
|
对内则新增:
|
||||||
|
|
||||||
|
- 不允许未声明的写操作
|
||||||
|
- 不允许跳过 validation 直接执行
|
||||||
|
- 不允许没有 evidence 就宣称完成
|
||||||
|
|
||||||
## 一键安装
|
## 一键安装
|
||||||
|
|
||||||
安装器现在会先安装 skill,再默认尝试安装 `jj`。
|
安装器现在会先安装 skill,再默认尝试安装 `jj`。
|
||||||
@@ -93,7 +167,31 @@ jj config set --user user.email "you@example.com"
|
|||||||
|
|
||||||
- `skills/gitea-issue-devops-agent/references/jj-default-usage.md`
|
- `skills/gitea-issue-devops-agent/references/jj-default-usage.md`
|
||||||
|
|
||||||
## 工具使用说明
|
## 运行时与工具使用说明
|
||||||
|
|
||||||
|
### workflow spec
|
||||||
|
|
||||||
|
- `workflows/gitea-issue-delivery.md`
|
||||||
|
- 当前样例 workflow spec
|
||||||
|
- `workflows/gitea-issue-delivery.lock.json`
|
||||||
|
- 编译后的锁定产物,建议与 spec 一起提交
|
||||||
|
|
||||||
|
### acceptance 环境变量
|
||||||
|
|
||||||
|
真实 Gitea 验收测试读取以下环境变量:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GITEA_BASE_URL=https://fun-md.com
|
||||||
|
GITEA_REPO=Fun_MD/devops-skills
|
||||||
|
GITEA_TOKEN=<TOKEN>
|
||||||
|
GITEA_ISSUE_NUMBER=48
|
||||||
|
```
|
||||||
|
|
||||||
|
执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pytest tests/acceptance/test_gitea_acceptance.py -q
|
||||||
|
```
|
||||||
|
|
||||||
### issue_audit.py
|
### issue_audit.py
|
||||||
|
|
||||||
@@ -124,6 +222,8 @@ python skills/gitea-issue-devops-agent/scripts/preview_slot_allocator.py --state
|
|||||||
- `.gitea/workflows/issue-branch-preview.yml`
|
- `.gitea/workflows/issue-branch-preview.yml`
|
||||||
- `.gitea/workflows/preview-slot-reclaim.yml`
|
- `.gitea/workflows/preview-slot-reclaim.yml`
|
||||||
- `.gitea/workflows/publish-site.yml`
|
- `.gitea/workflows/publish-site.yml`
|
||||||
|
- `workflows/gitea-issue-delivery.md`
|
||||||
|
- `workflows/gitea-issue-delivery.lock.json`
|
||||||
|
|
||||||
## Skills 调用前置信息(Claude Code / Codex / OpenCode)
|
## Skills 调用前置信息(Claude Code / Codex / OpenCode)
|
||||||
|
|
||||||
|
|||||||
266
docs/superpowers/plans/2026-03-13-gitea-agentic-runtime-plan.md
Normal file
266
docs/superpowers/plans/2026-03-13-gitea-agentic-runtime-plan.md
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
# Gitea Agentic Runtime Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Build a Gitea-backed workflow spec/compiler/runtime subsystem that turns the existing delivery skill into a policy-enforced execution engine with automated tests and real Gitea acceptance.
|
||||||
|
|
||||||
|
**Architecture:** Add a small Python control-plane package under `engine/devops_agent`, keep provider logic isolated behind interfaces, compile workflow specs into JSON lock artifacts, and run policy-guarded issue workflows against Gitea. Persist plans and evidence locally so tests and operators can inspect every run deterministically.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.11+, pytest, requests-free stdlib HTTP where practical, Markdown/JSON workflow artifacts, existing repository docs and Gitea workflow templates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chunk 1: Project Scaffold and Spec Format
|
||||||
|
|
||||||
|
### Task 1: Create the Python package scaffold and test harness
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `pyproject.toml`
|
||||||
|
- Create: `engine/devops_agent/__init__.py`
|
||||||
|
- Create: `engine/devops_agent/cli.py`
|
||||||
|
- Create: `tests/unit/test_smoke_imports.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing package smoke test**
|
||||||
|
|
||||||
|
Create a test that imports the CLI entrypoint and key modules that will exist after scaffolding.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the smoke test and confirm failure**
|
||||||
|
|
||||||
|
Run: `python -m pytest tests/unit/test_smoke_imports.py -q`
|
||||||
|
Expected: import failure because package files do not exist yet
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the minimal package scaffold**
|
||||||
|
|
||||||
|
Create the package directories, `__init__.py`, and a CLI module with a placeholder command parser.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add the Python project file**
|
||||||
|
|
||||||
|
Create `pyproject.toml` with the minimum metadata and `pytest` test configuration.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Re-run the smoke test**
|
||||||
|
|
||||||
|
Run: `python -m pytest tests/unit/test_smoke_imports.py -q`
|
||||||
|
Expected: pass
|
||||||
|
|
||||||
|
### Task 2: Define the workflow spec shape and a sample Gitea workflow
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `engine/devops_agent/spec.py`
|
||||||
|
- Create: `workflows/gitea-issue-delivery.md`
|
||||||
|
- Create: `tests/unit/test_spec_loader.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing spec loader tests**
|
||||||
|
|
||||||
|
Test frontmatter parsing, Markdown body extraction, and required field detection.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the spec loader tests and confirm failure**
|
||||||
|
|
||||||
|
Run: `python -m pytest tests/unit/test_spec_loader.py -q`
|
||||||
|
Expected: missing loader implementation
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the minimal spec loader**
|
||||||
|
|
||||||
|
Parse frontmatter and Markdown body into a structured object.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add the sample Gitea workflow spec**
|
||||||
|
|
||||||
|
Define one workflow that models fixed-issue delivery with safe comment output.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Re-run the spec loader tests**
|
||||||
|
|
||||||
|
Run: `python -m pytest tests/unit/test_spec_loader.py -q`
|
||||||
|
Expected: pass
|
||||||
|
|
||||||
|
## Chunk 2: Compiler, Validator, and Policy Enforcement
|
||||||
|
|
||||||
|
### Task 3: Implement lock compilation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `engine/devops_agent/compiler.py`
|
||||||
|
- Create: `tests/unit/test_compiler.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing compiler tests**
|
||||||
|
|
||||||
|
Cover default normalization, trigger expansion, and lock artifact emission.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the compiler tests and confirm failure**
|
||||||
|
|
||||||
|
Run: `python -m pytest tests/unit/test_compiler.py -q`
|
||||||
|
Expected: missing compiler implementation
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the minimal compiler**
|
||||||
|
|
||||||
|
Compile the loaded spec into a deterministic JSON-compatible lock payload.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Re-run the compiler tests**
|
||||||
|
|
||||||
|
Run: `python -m pytest tests/unit/test_compiler.py -q`
|
||||||
|
Expected: pass
|
||||||
|
|
||||||
|
### Task 4: Implement validation and safe output policies
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `engine/devops_agent/validator.py`
|
||||||
|
- Create: `engine/devops_agent/policies.py`
|
||||||
|
- Create: `tests/unit/test_validator.py`
|
||||||
|
- Create: `tests/unit/test_policies.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing validation and policy tests**
|
||||||
|
|
||||||
|
Cover missing provider, invalid trigger combinations, undeclared write actions, and path-scope checks.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run those tests and confirm failure**
|
||||||
|
|
||||||
|
Run: `python -m pytest tests/unit/test_validator.py tests/unit/test_policies.py -q`
|
||||||
|
Expected: missing implementations
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the validator**
|
||||||
|
|
||||||
|
Return explicit validation errors for incomplete or unsafe specs.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement the policy layer**
|
||||||
|
|
||||||
|
Enforce read-only default, safe output declarations, and bounded write operations.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Re-run the validation and policy tests**
|
||||||
|
|
||||||
|
Run: `python -m pytest tests/unit/test_validator.py tests/unit/test_policies.py -q`
|
||||||
|
Expected: pass
|
||||||
|
|
||||||
|
## Chunk 3: Provider and Runtime
|
||||||
|
|
||||||
|
### Task 5: Implement the provider interface and Gitea provider
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `engine/devops_agent/providers/__init__.py`
|
||||||
|
- Create: `engine/devops_agent/providers/base.py`
|
||||||
|
- Create: `engine/devops_agent/providers/gitea.py`
|
||||||
|
- Create: `tests/fixtures/gitea/issue.json`
|
||||||
|
- Create: `tests/fixtures/gitea/comment_event.json`
|
||||||
|
- Create: `tests/unit/test_gitea_provider.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing provider tests**
|
||||||
|
|
||||||
|
Cover issue fetch, comment post request shaping, and trigger event parsing using fixtures.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the provider tests and confirm failure**
|
||||||
|
|
||||||
|
Run: `python -m pytest tests/unit/test_gitea_provider.py -q`
|
||||||
|
Expected: provider implementation missing
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the provider interface and Gitea provider**
|
||||||
|
|
||||||
|
Add a minimal provider abstraction and the first concrete implementation for Gitea.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Re-run the provider tests**
|
||||||
|
|
||||||
|
Run: `python -m pytest tests/unit/test_gitea_provider.py -q`
|
||||||
|
Expected: pass
|
||||||
|
|
||||||
|
### Task 6: Implement runtime and evidence persistence
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `engine/devops_agent/runtime.py`
|
||||||
|
- Create: `engine/devops_agent/evidence.py`
|
||||||
|
- Create: `tests/integration/test_runtime_flow.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write a failing runtime integration test**
|
||||||
|
|
||||||
|
Cover compile -> validate -> runtime execution -> evidence artifact output using a fake provider transport.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the runtime integration test and confirm failure**
|
||||||
|
|
||||||
|
Run: `python -m pytest tests/integration/test_runtime_flow.py -q`
|
||||||
|
Expected: runtime implementation missing
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the runtime and evidence writer**
|
||||||
|
|
||||||
|
Persist run artifacts and plan/evidence summaries under a deterministic output directory.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Re-run the runtime integration test**
|
||||||
|
|
||||||
|
Run: `python -m pytest tests/integration/test_runtime_flow.py -q`
|
||||||
|
Expected: pass
|
||||||
|
|
||||||
|
## Chunk 4: CLI, Acceptance, and Documentation
|
||||||
|
|
||||||
|
### Task 7: Expose compile, validate, run, and acceptance commands
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `engine/devops_agent/cli.py`
|
||||||
|
- Create: `tests/unit/test_cli.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing CLI tests**
|
||||||
|
|
||||||
|
Cover `compile`, `validate`, and `run` command dispatch with filesystem outputs.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the CLI tests and confirm failure**
|
||||||
|
|
||||||
|
Run: `python -m pytest tests/unit/test_cli.py -q`
|
||||||
|
Expected: placeholder CLI insufficient
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the CLI commands**
|
||||||
|
|
||||||
|
Add command handling for `compile`, `validate`, `run`, and `acceptance`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Re-run the CLI tests**
|
||||||
|
|
||||||
|
Run: `python -m pytest tests/unit/test_cli.py -q`
|
||||||
|
Expected: pass
|
||||||
|
|
||||||
|
### Task 8: Add real Gitea acceptance and update docs
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tests/acceptance/test_gitea_acceptance.py`
|
||||||
|
- Modify: `README.md`
|
||||||
|
- Modify: `skills/gitea-issue-devops-agent/SKILL.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write acceptance tests**
|
||||||
|
|
||||||
|
Make the suite skip when `GITEA_BASE_URL`, `GITEA_REPO`, `GITEA_TOKEN`, and `GITEA_ISSUE_NUMBER` are absent, and perform real provider read/comment flow when present.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the acceptance suite without env vars**
|
||||||
|
|
||||||
|
Run: `python -m pytest tests/acceptance/test_gitea_acceptance.py -q`
|
||||||
|
Expected: clean skip
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update README and skill docs**
|
||||||
|
|
||||||
|
Document the new spec/runtime model, CLI commands, safe outputs, and Gitea acceptance procedure.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run real acceptance**
|
||||||
|
|
||||||
|
Run: `python -m pytest tests/acceptance/test_gitea_acceptance.py -q`
|
||||||
|
Expected: pass against the configured Gitea repository
|
||||||
|
|
||||||
|
## Chunk 5: End-to-End Verification and Delivery
|
||||||
|
|
||||||
|
### Task 9: Run the full automated test suite and review repository state
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: repo index and generated lock artifacts as needed
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run unit and integration tests**
|
||||||
|
|
||||||
|
Run: `python -m pytest tests/unit tests/integration -q`
|
||||||
|
Expected: all pass
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run full suite including acceptance**
|
||||||
|
|
||||||
|
Run: `python -m pytest tests/unit tests/integration tests/acceptance -q`
|
||||||
|
Expected: all pass, with acceptance either passing or explicitly skipped when env vars are absent
|
||||||
|
|
||||||
|
- [ ] **Step 3: Compile the sample workflow and verify output**
|
||||||
|
|
||||||
|
Run: `python -m engine.devops_agent.cli compile workflows/gitea-issue-delivery.md --output workflows/gitea-issue-delivery.lock.json`
|
||||||
|
Expected: lock file generated successfully
|
||||||
|
|
||||||
|
- [ ] **Step 4: Validate the sample workflow**
|
||||||
|
|
||||||
|
Run: `python -m engine.devops_agent.cli validate workflows/gitea-issue-delivery.md`
|
||||||
|
Expected: success output and zero exit code
|
||||||
|
|
||||||
|
- [ ] **Step 5: Review diff formatting**
|
||||||
|
|
||||||
|
Run: `git diff --check`
|
||||||
|
Expected: no formatting errors
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
# Gitea Agentic Runtime Design
|
||||||
|
|
||||||
|
**Date:** 2026-03-13
|
||||||
|
**Status:** Approved for implementation
|
||||||
|
**Scope:** Gitea single-platform real acceptance, with provider boundaries that keep later GitHub support incremental instead of invasive.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Upgrade `gitea-issue-devops-agent` from a documentation-heavy delivery skill into a controlled execution subsystem that can:
|
||||||
|
|
||||||
|
- parse repository-local workflow specs
|
||||||
|
- compile and validate those specs into a locked execution plan
|
||||||
|
- run a Gitea-triggered issue workflow under hard policies
|
||||||
|
- persist plan and evidence artifacts
|
||||||
|
- pass automated unit, integration, and real Gitea acceptance checks
|
||||||
|
|
||||||
|
The external workflow remains:
|
||||||
|
|
||||||
|
`Issue -> Plan -> Branch -> Draft PR -> Preview -> Test Loop -> Human Merge`
|
||||||
|
|
||||||
|
The internal workflow gains a deterministic control plane.
|
||||||
|
|
||||||
|
## Why This Design
|
||||||
|
|
||||||
|
The repository currently has strong delivery guidance, but not a runtime that enforces it. `gh-aw` demonstrates the value of turning natural-language workflow definitions into constrained executable automations. We do not want to clone GitHub-native product shape. We do want equivalent execution discipline:
|
||||||
|
|
||||||
|
- a spec format users can author
|
||||||
|
- a compiler/validator that rejects unsafe or incomplete workflows
|
||||||
|
- a provider layer for platform APIs
|
||||||
|
- hard policy checks before write actions
|
||||||
|
- run evidence suitable for audit and acceptance
|
||||||
|
|
||||||
|
This keeps the product positioned as an enterprise AI DevOps control layer, not a GitHub Actions clone.
|
||||||
|
|
||||||
|
## In Scope
|
||||||
|
|
||||||
|
- Gitea provider
|
||||||
|
- workflow spec parsing
|
||||||
|
- compilation into lock artifacts
|
||||||
|
- validation of triggers, permissions, safe outputs, edit scope, and evidence contract
|
||||||
|
- runtime execution for selected issue flows
|
||||||
|
- safe output enforcement for Gitea writes
|
||||||
|
- evidence persistence
|
||||||
|
- CLI entrypoints for compile, validate, run, and acceptance
|
||||||
|
- automated tests
|
||||||
|
- one real Gitea acceptance path
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- GitHub provider implementation
|
||||||
|
- autonomous queue-wide issue fixing
|
||||||
|
- automatic merge
|
||||||
|
- hosted webhook service
|
||||||
|
- UI control plane
|
||||||
|
- inference cost dashboards
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### 1. Workflow Spec
|
||||||
|
|
||||||
|
Workflow specs live in-repo and use frontmatter plus Markdown body:
|
||||||
|
|
||||||
|
- frontmatter defines triggers, provider, permissions, safe outputs, required evidence, and policy defaults
|
||||||
|
- body captures workflow intent, operator notes, and execution hints
|
||||||
|
|
||||||
|
The spec is the source of truth. Runtime behavior never depends on free-form Markdown alone.
|
||||||
|
|
||||||
|
### 2. Compiler
|
||||||
|
|
||||||
|
The compiler loads a workflow spec and emits a lock artifact:
|
||||||
|
|
||||||
|
- normalizes defaults
|
||||||
|
- expands trigger shorthand into explicit configuration
|
||||||
|
- resolves safe output declarations into executable policy objects
|
||||||
|
- records required evidence and path scope
|
||||||
|
|
||||||
|
The lock artifact is immutable input to runtime. It is designed as JSON in this iteration because JSON is easy to diff, assert in tests, and consume from Python.
|
||||||
|
|
||||||
|
### 3. Validator
|
||||||
|
|
||||||
|
Validation rejects unsafe or incomplete specs before runtime:
|
||||||
|
|
||||||
|
- unsupported trigger combinations
|
||||||
|
- missing provider
|
||||||
|
- unsafe write permissions
|
||||||
|
- missing safe outputs for write behaviors
|
||||||
|
- invalid path scope syntax
|
||||||
|
- missing evidence requirements
|
||||||
|
|
||||||
|
This is the first hard boundary between intent and execution.
|
||||||
|
|
||||||
|
### 4. Runtime
|
||||||
|
|
||||||
|
Runtime consumes:
|
||||||
|
|
||||||
|
- a compiled lock artifact
|
||||||
|
- an event payload
|
||||||
|
- provider configuration and credentials
|
||||||
|
|
||||||
|
Runtime responsibilities:
|
||||||
|
|
||||||
|
- load and sanitize issue-trigger context
|
||||||
|
- initialize or update the persisted plan state
|
||||||
|
- enforce policy gates before any write action
|
||||||
|
- call provider APIs through a narrow interface
|
||||||
|
- collect evidence and write run artifacts
|
||||||
|
|
||||||
|
The runtime is not a general autonomous coding engine in this iteration. It is a control and orchestration layer for issue delivery actions.
|
||||||
|
|
||||||
|
### 5. Provider Layer
|
||||||
|
|
||||||
|
Provider interfaces isolate platform behavior from workflow logic.
|
||||||
|
|
||||||
|
The first provider is `GiteaProvider`, which supports:
|
||||||
|
|
||||||
|
- repository metadata reads
|
||||||
|
- issue reads
|
||||||
|
- issue comments
|
||||||
|
- branch hint extraction inputs
|
||||||
|
- draft PR preparation primitives
|
||||||
|
|
||||||
|
The abstraction must make later `GitHubProvider` addition additive rather than structural.
|
||||||
|
|
||||||
|
### 6. Policy and Safe Outputs
|
||||||
|
|
||||||
|
The policy layer turns delivery rules into code:
|
||||||
|
|
||||||
|
- read-only by default
|
||||||
|
- no merge action
|
||||||
|
- comment/create/update operations only if declared in safe outputs
|
||||||
|
- path-scope enforcement for file writes
|
||||||
|
- evidence-required status promotion
|
||||||
|
- bounded output counts where relevant
|
||||||
|
|
||||||
|
This is the key product maturity improvement over pure skill text.
|
||||||
|
|
||||||
|
### 7. Evidence Layer
|
||||||
|
|
||||||
|
Every run produces durable artifacts:
|
||||||
|
|
||||||
|
- resolved plan state
|
||||||
|
- execution summary
|
||||||
|
- provider operations executed
|
||||||
|
- evidence bundle for commit/PR/test/preview placeholders
|
||||||
|
- acceptance result metadata
|
||||||
|
|
||||||
|
Evidence is stored locally under a deterministic run directory so tests and operators can inspect it.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
New code will be added under:
|
||||||
|
|
||||||
|
- `pyproject.toml`
|
||||||
|
- `engine/devops_agent/__init__.py`
|
||||||
|
- `engine/devops_agent/spec.py`
|
||||||
|
- `engine/devops_agent/compiler.py`
|
||||||
|
- `engine/devops_agent/validator.py`
|
||||||
|
- `engine/devops_agent/runtime.py`
|
||||||
|
- `engine/devops_agent/policies.py`
|
||||||
|
- `engine/devops_agent/evidence.py`
|
||||||
|
- `engine/devops_agent/cli.py`
|
||||||
|
- `engine/devops_agent/providers/__init__.py`
|
||||||
|
- `engine/devops_agent/providers/base.py`
|
||||||
|
- `engine/devops_agent/providers/gitea.py`
|
||||||
|
- `workflows/gitea-issue-delivery.md`
|
||||||
|
- `tests/unit/...`
|
||||||
|
- `tests/integration/...`
|
||||||
|
- `tests/fixtures/gitea/...`
|
||||||
|
- `tests/acceptance/test_gitea_acceptance.py`
|
||||||
|
|
||||||
|
Existing scripts will remain, but runtime may call them or share logic conceptually:
|
||||||
|
|
||||||
|
- `skills/gitea-issue-devops-agent/scripts/issue_audit.py`
|
||||||
|
- `skills/gitea-issue-devops-agent/scripts/change_scope.py`
|
||||||
|
- `skills/gitea-issue-devops-agent/scripts/preview_slot_allocator.py`
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
### Workflow Spec
|
||||||
|
|
||||||
|
Core fields:
|
||||||
|
|
||||||
|
- `name`
|
||||||
|
- `provider`
|
||||||
|
- `on`
|
||||||
|
- `permissions`
|
||||||
|
- `safe_outputs`
|
||||||
|
- `plan`
|
||||||
|
- `policy`
|
||||||
|
- `evidence`
|
||||||
|
- `body`
|
||||||
|
|
||||||
|
### Lock Artifact
|
||||||
|
|
||||||
|
Core fields:
|
||||||
|
|
||||||
|
- `version`
|
||||||
|
- `compiled_at`
|
||||||
|
- `source`
|
||||||
|
- `provider`
|
||||||
|
- `triggers`
|
||||||
|
- `policy`
|
||||||
|
- `safe_outputs`
|
||||||
|
- `required_evidence`
|
||||||
|
- `plan_defaults`
|
||||||
|
- `instructions`
|
||||||
|
|
||||||
|
### Run Artifact
|
||||||
|
|
||||||
|
Core fields:
|
||||||
|
|
||||||
|
- `run_id`
|
||||||
|
- `workflow_name`
|
||||||
|
- `provider`
|
||||||
|
- `event`
|
||||||
|
- `plan_state`
|
||||||
|
- `operations`
|
||||||
|
- `evidence`
|
||||||
|
- `result`
|
||||||
|
|
||||||
|
## Test Strategy
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
- frontmatter parsing
|
||||||
|
- default normalization
|
||||||
|
- validator failures and successes
|
||||||
|
- policy enforcement
|
||||||
|
- provider request shaping
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
- spec to lock compilation
|
||||||
|
- lock to runtime execution
|
||||||
|
- runtime interaction with a fake Gitea transport
|
||||||
|
- evidence persistence
|
||||||
|
|
||||||
|
### Acceptance Tests
|
||||||
|
|
||||||
|
Verify against a real Gitea repo using env vars:
|
||||||
|
|
||||||
|
- workflow compile and validate pass
|
||||||
|
- runtime can load a selected issue
|
||||||
|
- runtime can publish an evidence comment
|
||||||
|
- acceptance artifacts are produced locally
|
||||||
|
|
||||||
|
If env vars are absent, the acceptance suite must skip cleanly instead of failing misleadingly.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
This design is complete only when:
|
||||||
|
|
||||||
|
1. A repo-local Gitea workflow spec compiles into a lock artifact.
|
||||||
|
2. Invalid specs fail validation with clear messages.
|
||||||
|
3. Runtime enforces safe outputs and refuses undeclared writes.
|
||||||
|
4. Gitea real acceptance can read an issue and publish an evidence comment.
|
||||||
|
5. Automated unit and integration tests pass.
|
||||||
|
6. README and skill docs describe the new execution model and CLI usage.
|
||||||
|
|
||||||
|
## Rollout Notes
|
||||||
|
|
||||||
|
- Gitea is the first real provider.
|
||||||
|
- GitHub support is intentionally deferred, but the provider interface must be stable enough to add later.
|
||||||
|
- `jj` remains an internal reliability layer. This subsystem must not require `jj` for external usage.
|
||||||
5
engine/devops_agent/__init__.py
Normal file
5
engine/devops_agent/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Runtime package for agentic DevOps workflow execution."""
|
||||||
|
|
||||||
|
__all__ = ["__version__"]
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
130
engine/devops_agent/cli.py
Normal file
130
engine/devops_agent/cli.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from engine.devops_agent.compiler import compile_workflow
|
||||||
|
from engine.devops_agent.providers.gitea import GiteaProvider
|
||||||
|
from engine.devops_agent.runtime import run_issue_comment_workflow
|
||||||
|
from engine.devops_agent.spec import load_workflow_spec
|
||||||
|
from engine.devops_agent.validator import validate_workflow_spec
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="devops-agent",
|
||||||
|
description="CLI for the agentic DevOps runtime.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--version",
|
||||||
|
action="store_true",
|
||||||
|
help="Print the runtime version and exit.",
|
||||||
|
)
|
||||||
|
subparsers = parser.add_subparsers(dest="command")
|
||||||
|
|
||||||
|
compile_parser = subparsers.add_parser("compile")
|
||||||
|
compile_parser.add_argument("spec_path")
|
||||||
|
compile_parser.add_argument("--output", required=True)
|
||||||
|
|
||||||
|
validate_parser = subparsers.add_parser("validate")
|
||||||
|
validate_parser.add_argument("spec_path")
|
||||||
|
|
||||||
|
run_parser = subparsers.add_parser("run")
|
||||||
|
run_parser.add_argument("spec_path")
|
||||||
|
run_parser.add_argument("--event-payload", required=True)
|
||||||
|
run_parser.add_argument("--output-dir", required=True)
|
||||||
|
run_parser.add_argument("--base-url", required=True)
|
||||||
|
run_parser.add_argument("--token", required=True)
|
||||||
|
|
||||||
|
acceptance_parser = subparsers.add_parser("acceptance")
|
||||||
|
acceptance_parser.add_argument("spec_path")
|
||||||
|
acceptance_parser.add_argument("--base-url", required=True)
|
||||||
|
acceptance_parser.add_argument("--repo", required=True)
|
||||||
|
acceptance_parser.add_argument("--token", required=True)
|
||||||
|
acceptance_parser.add_argument("--issue-number", required=True)
|
||||||
|
acceptance_parser.add_argument("--output-dir", required=True)
|
||||||
|
acceptance_parser.add_argument(
|
||||||
|
"--comment-body",
|
||||||
|
default="@devops-agent acceptance run",
|
||||||
|
)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def _load_compile_and_validate(spec_path: str) -> tuple[dict[str, object], list[str]]:
|
||||||
|
spec = load_workflow_spec(spec_path)
|
||||||
|
errors = validate_workflow_spec(spec)
|
||||||
|
return compile_workflow(spec), errors
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
|
parser = build_parser()
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
if args.version:
|
||||||
|
from engine.devops_agent import __version__
|
||||||
|
|
||||||
|
print(__version__)
|
||||||
|
return 0
|
||||||
|
if not getattr(args, "command", None):
|
||||||
|
parser.print_help()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.command == "compile":
|
||||||
|
lock, errors = _load_compile_and_validate(args.spec_path)
|
||||||
|
if errors:
|
||||||
|
print(json.dumps({"errors": errors}, ensure_ascii=False, indent=2))
|
||||||
|
return 1
|
||||||
|
output_path = Path(args.output)
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
output_path.write_text(json.dumps(lock, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.command == "validate":
|
||||||
|
_, errors = _load_compile_and_validate(args.spec_path)
|
||||||
|
if errors:
|
||||||
|
print(json.dumps({"errors": errors}, ensure_ascii=False, indent=2))
|
||||||
|
return 1
|
||||||
|
print("workflow is valid")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.command == "run":
|
||||||
|
lock, errors = _load_compile_and_validate(args.spec_path)
|
||||||
|
if errors:
|
||||||
|
print(json.dumps({"errors": errors}, ensure_ascii=False, indent=2))
|
||||||
|
return 1
|
||||||
|
provider = GiteaProvider(base_url=args.base_url, token=args.token)
|
||||||
|
payload = json.loads(Path(args.event_payload).read_text(encoding="utf-8"))
|
||||||
|
run_issue_comment_workflow(
|
||||||
|
lock=lock,
|
||||||
|
provider=provider,
|
||||||
|
event_payload=payload,
|
||||||
|
output_dir=args.output_dir,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.command == "acceptance":
|
||||||
|
lock, errors = _load_compile_and_validate(args.spec_path)
|
||||||
|
if errors:
|
||||||
|
print(json.dumps({"errors": errors}, ensure_ascii=False, indent=2))
|
||||||
|
return 1
|
||||||
|
provider = GiteaProvider(base_url=args.base_url, token=args.token)
|
||||||
|
payload = {
|
||||||
|
"repository": {"full_name": args.repo},
|
||||||
|
"issue": {"number": int(args.issue_number)},
|
||||||
|
"comment": {"body": args.comment_body},
|
||||||
|
}
|
||||||
|
run_issue_comment_workflow(
|
||||||
|
lock=lock,
|
||||||
|
provider=provider,
|
||||||
|
event_payload=payload,
|
||||||
|
output_dir=args.output_dir,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
parser.error(f"unsupported command: {args.command}")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
42
engine/devops_agent/compiler.py
Normal file
42
engine/devops_agent/compiler.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from engine.devops_agent.spec import WorkflowSpec
|
||||||
|
|
||||||
|
|
||||||
|
def _compile_triggers(frontmatter: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
|
triggers = frontmatter.get("on") or {}
|
||||||
|
if not isinstance(triggers, dict):
|
||||||
|
return []
|
||||||
|
|
||||||
|
compiled: list[dict[str, Any]] = []
|
||||||
|
for event_name, event_config in triggers.items():
|
||||||
|
normalized = {
|
||||||
|
"event": str(event_name),
|
||||||
|
}
|
||||||
|
if isinstance(event_config, dict):
|
||||||
|
normalized.update(event_config)
|
||||||
|
compiled.append(normalized)
|
||||||
|
return compiled
|
||||||
|
|
||||||
|
|
||||||
|
def compile_workflow(spec: WorkflowSpec) -> dict[str, Any]:
|
||||||
|
policy = spec.frontmatter.get("policy") or {}
|
||||||
|
evidence = spec.frontmatter.get("evidence") or {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"version": 1,
|
||||||
|
"workflow_name": spec.name,
|
||||||
|
"provider": spec.provider,
|
||||||
|
"source": str(spec.source_path.as_posix()),
|
||||||
|
"triggers": _compile_triggers(spec.frontmatter),
|
||||||
|
"safe_outputs": spec.frontmatter.get("safe_outputs") or {},
|
||||||
|
"required_evidence": evidence.get("required") or [],
|
||||||
|
"policy": {
|
||||||
|
"require_human_merge": bool(policy.get("require_human_merge", True)),
|
||||||
|
"require_fixed_issue": bool(policy.get("require_fixed_issue", False)),
|
||||||
|
"path_scope": policy.get("path_scope") or [],
|
||||||
|
},
|
||||||
|
"instructions": spec.body,
|
||||||
|
}
|
||||||
16
engine/devops_agent/evidence.py
Normal file
16
engine/devops_agent/evidence.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def write_run_artifact(output_dir: str | Path, artifact: dict[str, Any]) -> Path:
|
||||||
|
destination = Path(output_dir)
|
||||||
|
destination.mkdir(parents=True, exist_ok=True)
|
||||||
|
artifact_path = destination / "run-artifact.json"
|
||||||
|
artifact_path.write_text(
|
||||||
|
json.dumps(artifact, ensure_ascii=False, indent=2),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return artifact_path
|
||||||
43
engine/devops_agent/policies.py
Normal file
43
engine/devops_agent/policies.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyViolation(PermissionError):
|
||||||
|
"""Raised when runtime behavior violates declared workflow policy."""
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_path(path: str) -> str:
|
||||||
|
return path.replace("\\", "/").lstrip("./")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class RuntimePolicy:
|
||||||
|
safe_outputs: dict[str, dict[str, int | str | bool]]
|
||||||
|
path_scope: list[str]
|
||||||
|
_operation_counts: dict[str, int] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def assert_operation_allowed(self, action: str) -> None:
|
||||||
|
config = self.safe_outputs.get(action)
|
||||||
|
if config is None:
|
||||||
|
raise PolicyViolation(f"write action '{action}' is not declared in safe_outputs")
|
||||||
|
|
||||||
|
current_count = self._operation_counts.get(action, 0) + 1
|
||||||
|
max_count = int(config.get("max", current_count))
|
||||||
|
if current_count > max_count:
|
||||||
|
raise PolicyViolation(f"write action '{action}' exceeded max count {max_count}")
|
||||||
|
|
||||||
|
self._operation_counts[action] = current_count
|
||||||
|
|
||||||
|
def assert_path_allowed(self, path: str) -> None:
|
||||||
|
normalized = _normalize_path(path)
|
||||||
|
if not self.path_scope:
|
||||||
|
raise PolicyViolation("file writes are not allowed without an explicit path scope")
|
||||||
|
|
||||||
|
for allowed_prefix in self.path_scope:
|
||||||
|
if normalized.startswith(_normalize_path(allowed_prefix)):
|
||||||
|
return
|
||||||
|
|
||||||
|
raise PolicyViolation(
|
||||||
|
f"path '{normalized}' is outside allowed path scope {self.path_scope}"
|
||||||
|
)
|
||||||
4
engine/devops_agent/providers/__init__.py
Normal file
4
engine/devops_agent/providers/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from engine.devops_agent.providers.base import IssueProvider
|
||||||
|
from engine.devops_agent.providers.gitea import GiteaProvider
|
||||||
|
|
||||||
|
__all__ = ["IssueProvider", "GiteaProvider"]
|
||||||
16
engine/devops_agent/providers/base.py
Normal file
16
engine/devops_agent/providers/base.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Protocol
|
||||||
|
|
||||||
|
|
||||||
|
class IssueProvider(Protocol):
|
||||||
|
def get_issue(self, repo: str, issue_number: int) -> dict[str, Any]: ...
|
||||||
|
|
||||||
|
def post_issue_comment(
|
||||||
|
self,
|
||||||
|
repo: str,
|
||||||
|
issue_number: int,
|
||||||
|
body: str,
|
||||||
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
|
def parse_issue_comment_event(self, payload: dict[str, Any]) -> dict[str, Any]: ...
|
||||||
73
engine/devops_agent/providers/gitea.py
Normal file
73
engine/devops_agent/providers/gitea.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any, Callable
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
|
||||||
|
Transport = Callable[..., dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
|
class GiteaProvider:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
base_url: str,
|
||||||
|
token: str,
|
||||||
|
transport: Transport | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.token = token
|
||||||
|
self.transport = transport
|
||||||
|
|
||||||
|
def _request(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
method: str,
|
||||||
|
path: str,
|
||||||
|
body: dict[str, object] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
url = f"{self.base_url}{path}"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"token {self.token}",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.transport is not None:
|
||||||
|
return self.transport(method=method, url=url, headers=headers, body=body)
|
||||||
|
|
||||||
|
payload = None if body is None else json.dumps(body).encode("utf-8")
|
||||||
|
request = Request(url, method=method, headers=headers, data=payload)
|
||||||
|
with urlopen(request, timeout=30) as response:
|
||||||
|
raw = response.read().decode("utf-8")
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
def get_issue(self, repo: str, issue_number: int) -> dict[str, Any]:
|
||||||
|
return self._request(
|
||||||
|
method="GET",
|
||||||
|
path=f"/api/v1/repos/{repo}/issues/{issue_number}",
|
||||||
|
)
|
||||||
|
|
||||||
|
def post_issue_comment(
|
||||||
|
self,
|
||||||
|
repo: str,
|
||||||
|
issue_number: int,
|
||||||
|
body: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return self._request(
|
||||||
|
method="POST",
|
||||||
|
path=f"/api/v1/repos/{repo}/issues/{issue_number}/comments",
|
||||||
|
body={"body": body},
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_issue_comment_event(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
repository = payload.get("repository") or {}
|
||||||
|
issue = payload.get("issue") or {}
|
||||||
|
comment = payload.get("comment") or {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"repo": repository.get("full_name", ""),
|
||||||
|
"issue_number": int(issue.get("number", 0)),
|
||||||
|
"comment_body": str(comment.get("body", "")),
|
||||||
|
}
|
||||||
65
engine/devops_agent/runtime.py
Normal file
65
engine/devops_agent/runtime.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from engine.devops_agent.evidence import write_run_artifact
|
||||||
|
from engine.devops_agent.policies import RuntimePolicy
|
||||||
|
|
||||||
|
|
||||||
|
def run_issue_comment_workflow(
|
||||||
|
*,
|
||||||
|
lock: dict[str, Any],
|
||||||
|
provider: Any,
|
||||||
|
event_payload: dict[str, Any],
|
||||||
|
output_dir: str | Path,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
event = provider.parse_issue_comment_event(event_payload)
|
||||||
|
repo = str(event["repo"])
|
||||||
|
issue_number = int(event["issue_number"])
|
||||||
|
issue = provider.get_issue(repo, issue_number)
|
||||||
|
|
||||||
|
policy = RuntimePolicy(
|
||||||
|
safe_outputs=lock.get("safe_outputs") or {},
|
||||||
|
path_scope=lock.get("policy", {}).get("path_scope") or [],
|
||||||
|
)
|
||||||
|
policy.assert_operation_allowed("add_comment")
|
||||||
|
|
||||||
|
verification_summary = (
|
||||||
|
f"Workflow `{lock['workflow_name']}` processed issue #{issue_number} "
|
||||||
|
f"and prepared evidence for review."
|
||||||
|
)
|
||||||
|
comment_response = provider.post_issue_comment(
|
||||||
|
repo,
|
||||||
|
issue_number,
|
||||||
|
verification_summary,
|
||||||
|
)
|
||||||
|
|
||||||
|
artifact: dict[str, Any] = {
|
||||||
|
"run_id": f"{lock['workflow_name']}-issue-{issue_number}",
|
||||||
|
"workflow_name": lock["workflow_name"],
|
||||||
|
"provider": lock["provider"],
|
||||||
|
"event": event,
|
||||||
|
"plan_state": {
|
||||||
|
"status": "pending_review",
|
||||||
|
"repo": repo,
|
||||||
|
"issue_number": issue_number,
|
||||||
|
"issue_title": issue.get("title", ""),
|
||||||
|
},
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"action": "add_comment",
|
||||||
|
"issue_number": issue_number,
|
||||||
|
"repo": repo,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"evidence": {
|
||||||
|
"issue_comment": comment_response,
|
||||||
|
"verification_summary": verification_summary,
|
||||||
|
},
|
||||||
|
"result": "success",
|
||||||
|
}
|
||||||
|
artifact_path = write_run_artifact(output_dir, artifact)
|
||||||
|
artifact["artifact_path"] = str(artifact_path.as_posix())
|
||||||
|
artifact_path.write_text(__import__("json").dumps(artifact, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
return artifact
|
||||||
60
engine/devops_agent/spec.py
Normal file
60
engine/devops_agent/spec.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowSpecError(ValueError):
|
||||||
|
"""Raised when a workflow spec cannot be parsed or is incomplete."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class WorkflowSpec:
|
||||||
|
name: str
|
||||||
|
provider: str
|
||||||
|
frontmatter: dict[str, Any]
|
||||||
|
body: str
|
||||||
|
source_path: Path
|
||||||
|
|
||||||
|
|
||||||
|
def _split_frontmatter(raw_text: str) -> tuple[str, str]:
|
||||||
|
if not raw_text.startswith("---"):
|
||||||
|
raise WorkflowSpecError("workflow spec must start with frontmatter")
|
||||||
|
|
||||||
|
parts = raw_text.split("\n---", 1)
|
||||||
|
if len(parts) != 2:
|
||||||
|
raise WorkflowSpecError("workflow spec frontmatter is not terminated")
|
||||||
|
|
||||||
|
frontmatter_text = parts[0][4:]
|
||||||
|
body = parts[1].lstrip("\r\n")
|
||||||
|
return frontmatter_text, body
|
||||||
|
|
||||||
|
|
||||||
|
def load_workflow_spec(path: str | Path) -> WorkflowSpec:
|
||||||
|
source_path = Path(path)
|
||||||
|
raw_text = source_path.read_text(encoding="utf-8")
|
||||||
|
frontmatter_text, body = _split_frontmatter(raw_text)
|
||||||
|
|
||||||
|
payload = yaml.safe_load(frontmatter_text) or {}
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
raise WorkflowSpecError("workflow spec frontmatter must be a mapping")
|
||||||
|
if True in payload and "on" not in payload:
|
||||||
|
payload["on"] = payload.pop(True)
|
||||||
|
|
||||||
|
name = str(payload.get("name") or "").strip()
|
||||||
|
provider = str(payload.get("provider") or "").strip()
|
||||||
|
if not name:
|
||||||
|
raise WorkflowSpecError("workflow spec is missing required field: name")
|
||||||
|
if not provider:
|
||||||
|
raise WorkflowSpecError("workflow spec is missing required field: provider")
|
||||||
|
|
||||||
|
return WorkflowSpec(
|
||||||
|
name=name,
|
||||||
|
provider=provider,
|
||||||
|
frontmatter=payload,
|
||||||
|
body=body,
|
||||||
|
source_path=source_path,
|
||||||
|
)
|
||||||
49
engine/devops_agent/validator.py
Normal file
49
engine/devops_agent/validator.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from engine.devops_agent.spec import WorkflowSpec
|
||||||
|
|
||||||
|
WRITE_PERMISSIONS = {"issues", "pull_requests", "contents"}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_write_permission(value: Any) -> bool:
|
||||||
|
return str(value).strip().lower() == "write"
|
||||||
|
|
||||||
|
|
||||||
|
def validate_workflow_spec(spec: WorkflowSpec) -> list[str]:
|
||||||
|
errors: list[str] = []
|
||||||
|
|
||||||
|
if spec.provider not in {"gitea"}:
|
||||||
|
errors.append(f"unsupported provider: {spec.provider}")
|
||||||
|
|
||||||
|
triggers = spec.frontmatter.get("on")
|
||||||
|
if not isinstance(triggers, dict) or not triggers:
|
||||||
|
errors.append("workflow spec must declare at least one trigger in 'on'")
|
||||||
|
|
||||||
|
permissions = spec.frontmatter.get("permissions") or {}
|
||||||
|
safe_outputs = spec.frontmatter.get("safe_outputs") or {}
|
||||||
|
|
||||||
|
if not isinstance(permissions, dict):
|
||||||
|
errors.append("'permissions' must be a mapping")
|
||||||
|
if not isinstance(safe_outputs, dict):
|
||||||
|
errors.append("'safe_outputs' must be a mapping")
|
||||||
|
if isinstance(permissions, dict):
|
||||||
|
has_write_permission = any(
|
||||||
|
permission_name in WRITE_PERMISSIONS and _is_write_permission(permission_value)
|
||||||
|
for permission_name, permission_value in permissions.items()
|
||||||
|
)
|
||||||
|
if has_write_permission and not safe_outputs:
|
||||||
|
errors.append("write permissions require declared safe_outputs")
|
||||||
|
|
||||||
|
policy = spec.frontmatter.get("policy") or {}
|
||||||
|
if policy and not isinstance(policy, dict):
|
||||||
|
errors.append("'policy' must be a mapping")
|
||||||
|
elif isinstance(policy, dict) and "path_scope" in policy:
|
||||||
|
path_scope = policy["path_scope"]
|
||||||
|
if not isinstance(path_scope, list) or any(
|
||||||
|
not isinstance(item, str) or not item.strip() for item in path_scope
|
||||||
|
):
|
||||||
|
errors.append("policy.path_scope must be a list of non-empty path prefixes")
|
||||||
|
|
||||||
|
return errors
|
||||||
14
pyproject.toml
Normal file
14
pyproject.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=68"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "devops-agent-runtime"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Policy-enforced agentic delivery runtime for Gitea issue workflows."
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = ["PyYAML>=6"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
addopts = "-p no:cacheprovider"
|
||||||
|
testpaths = ["tests"]
|
||||||
@@ -15,11 +15,32 @@ This skill is platform-aware for Gitea, but its delivery contract must stay port
|
|||||||
|
|
||||||
- Start coding only from a selected issue, not from an unbounded issue queue, unless the user explicitly asks for triage-only queue scanning.
|
- Start coding only from a selected issue, not from an unbounded issue queue, unless the user explicitly asks for triage-only queue scanning.
|
||||||
- Every delivery issue must have a persisted plan before any code changes.
|
- Every delivery issue must have a persisted plan before any code changes.
|
||||||
|
- Treat workflow spec, compiled lock artifact, and evidence artifacts as executable system-of-record once the runtime is available; do not rely on prose-only guidance for enforcement.
|
||||||
- External collaboration stays Git-native: issue, branch, PR, pipeline, review app, merge.
|
- External collaboration stays Git-native: issue, branch, PR, pipeline, review app, merge.
|
||||||
- AI output is provisional until tests, smoke paths, and review evidence exist.
|
- AI output is provisional until tests, smoke paths, and review evidence exist.
|
||||||
- Engineers stay in the loop from the initial PR onward and own white-box review.
|
- Engineers stay in the loop from the initial PR onward and own white-box review.
|
||||||
- Keep one active issue per branch and per execution agent unless the user explicitly approves batching.
|
- Keep one active issue per branch and per execution agent unless the user explicitly approves batching.
|
||||||
|
|
||||||
|
## Runtime Contract (Required)
|
||||||
|
|
||||||
|
When the repository contains the runtime package, use it instead of ad-hoc execution:
|
||||||
|
|
||||||
|
1. Author or update the workflow spec in `workflows/*.md`.
|
||||||
|
2. Compile before execution:
|
||||||
|
- `python -m engine.devops_agent.cli compile workflows/gitea-issue-delivery.md --output workflows/gitea-issue-delivery.lock.json`
|
||||||
|
3. Validate before execution:
|
||||||
|
- `python -m engine.devops_agent.cli validate workflows/gitea-issue-delivery.md`
|
||||||
|
4. Run or accept only through declared safe outputs.
|
||||||
|
5. Persist run evidence under `.tmp/...` and treat `run-artifact.json` as execution proof.
|
||||||
|
6. For real Gitea verification, use:
|
||||||
|
- `python -m engine.devops_agent.cli acceptance workflows/gitea-issue-delivery.md --base-url <url> --repo <owner/repo> --token <token> --issue-number <n> --output-dir .tmp/acceptance/gitea`
|
||||||
|
|
||||||
|
Hard rules:
|
||||||
|
|
||||||
|
- No undeclared write actions.
|
||||||
|
- No execution after failed validation.
|
||||||
|
- No claiming completion without runtime evidence.
|
||||||
|
|
||||||
## Mandatory Guided Start
|
## Mandatory Guided Start
|
||||||
|
|
||||||
Run this interaction before any coding or issue action:
|
Run this interaction before any coding or issue action:
|
||||||
@@ -401,6 +422,15 @@ Use `jj` only under these rules:
|
|||||||
|
|
||||||
## Script Usage
|
## Script Usage
|
||||||
|
|
||||||
|
### Runtime CLI
|
||||||
|
|
||||||
|
- `python -m engine.devops_agent.cli compile workflows/gitea-issue-delivery.md --output workflows/gitea-issue-delivery.lock.json`
|
||||||
|
- `python -m engine.devops_agent.cli validate workflows/gitea-issue-delivery.md`
|
||||||
|
- `python -m engine.devops_agent.cli run workflows/gitea-issue-delivery.md --event-payload <payload.json> --output-dir .tmp/runtime-run --base-url <url> --token <token>`
|
||||||
|
- `python -m engine.devops_agent.cli acceptance workflows/gitea-issue-delivery.md --base-url <url> --repo <owner/repo> --token <token> --issue-number <n> --output-dir .tmp/acceptance/gitea`
|
||||||
|
|
||||||
|
- runtime writes `run-artifact.json` and should be used as the evidence source for status promotion and issue comments.
|
||||||
|
|
||||||
- `scripts/issue_audit.py`: collect issues/comments/attachments, detect duplicates, score quality, detect unresolved/closed-open links, extract issue branch hints, and generate reports.
|
- `scripts/issue_audit.py`: collect issues/comments/attachments, detect duplicates, score quality, detect unresolved/closed-open links, extract issue branch hints, and generate reports.
|
||||||
- image intake uses three sources: markdown/html links, payload `assets/attachments` fields, and `/issues/*/assets` API endpoints.
|
- image intake uses three sources: markdown/html links, payload `assets/attachments` fields, and `/issues/*/assets` API endpoints.
|
||||||
- if your Gitea blocks the assets endpoints, pass `--skip-asset-endpoints` and rely on payload extraction.
|
- if your Gitea blocks the assets endpoints, pass `--skip-asset-endpoints` and rely on payload extraction.
|
||||||
|
|||||||
47
tests/acceptance/test_gitea_acceptance.py
Normal file
47
tests/acceptance/test_gitea_acceptance.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from engine.devops_agent.cli import main
|
||||||
|
|
||||||
|
|
||||||
|
REQUIRED_ENV_VARS = (
|
||||||
|
"GITEA_BASE_URL",
|
||||||
|
"GITEA_REPO",
|
||||||
|
"GITEA_TOKEN",
|
||||||
|
"GITEA_ISSUE_NUMBER",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_gitea_acceptance_comment_flow() -> None:
|
||||||
|
missing = [name for name in REQUIRED_ENV_VARS if not os.getenv(name)]
|
||||||
|
if missing:
|
||||||
|
pytest.skip(f"missing required env vars: {', '.join(missing)}")
|
||||||
|
|
||||||
|
output_dir = Path(".tmp/acceptance/gitea")
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
exit_code = main(
|
||||||
|
[
|
||||||
|
"acceptance",
|
||||||
|
"workflows/gitea-issue-delivery.md",
|
||||||
|
"--base-url",
|
||||||
|
os.environ["GITEA_BASE_URL"],
|
||||||
|
"--repo",
|
||||||
|
os.environ["GITEA_REPO"],
|
||||||
|
"--token",
|
||||||
|
os.environ["GITEA_TOKEN"],
|
||||||
|
"--issue-number",
|
||||||
|
os.environ["GITEA_ISSUE_NUMBER"],
|
||||||
|
"--output-dir",
|
||||||
|
str(output_dir),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
artifact_path = output_dir / "run-artifact.json"
|
||||||
|
|
||||||
|
assert exit_code == 0
|
||||||
|
assert artifact_path.exists()
|
||||||
12
tests/fixtures/gitea/comment_event.json
vendored
Normal file
12
tests/fixtures/gitea/comment_event.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"action": "created",
|
||||||
|
"comment": {
|
||||||
|
"body": "@devops-agent please process issue 48"
|
||||||
|
},
|
||||||
|
"issue": {
|
||||||
|
"number": 48
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"full_name": "Fun_MD/devops-skills"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
tests/fixtures/gitea/issue.json
vendored
Normal file
6
tests/fixtures/gitea/issue.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"number": 48,
|
||||||
|
"title": "Fix issue delivery flow",
|
||||||
|
"body": "The agent should post evidence back to the issue.",
|
||||||
|
"state": "open"
|
||||||
|
}
|
||||||
4
tests/fixtures/specs/invalid_missing_provider.md
vendored
Normal file
4
tests/fixtures/specs/invalid_missing_provider.md
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
name: invalid-workflow
|
||||||
|
---
|
||||||
|
# Invalid
|
||||||
17
tests/fixtures/specs/invalid_path_scope.md
vendored
Normal file
17
tests/fixtures/specs/invalid_path_scope.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: invalid-path-scope
|
||||||
|
provider: gitea
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
commands: ["@devops-agent"]
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
safe_outputs:
|
||||||
|
add_comment:
|
||||||
|
max: 1
|
||||||
|
policy:
|
||||||
|
path_scope: "engine/devops_agent/"
|
||||||
|
---
|
||||||
|
# Invalid
|
||||||
|
|
||||||
|
The path scope must be a list of path prefixes.
|
||||||
12
tests/fixtures/specs/no_safe_outputs_for_write.md
vendored
Normal file
12
tests/fixtures/specs/no_safe_outputs_for_write.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
name: invalid-write-without-safe-output
|
||||||
|
provider: gitea
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
commands: ["@devops-agent"]
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
---
|
||||||
|
# Invalid
|
||||||
|
|
||||||
|
Write permission is declared, but no safe output exists.
|
||||||
17
tests/fixtures/specs/valid_workflow.md
vendored
Normal file
17
tests/fixtures/specs/valid_workflow.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: issue-delivery
|
||||||
|
provider: gitea
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
commands: ["@devops-agent"]
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
safe_outputs:
|
||||||
|
add_comment:
|
||||||
|
max: 2
|
||||||
|
plan:
|
||||||
|
required: true
|
||||||
|
---
|
||||||
|
# Issue Delivery
|
||||||
|
|
||||||
|
Read the selected issue and post evidence.
|
||||||
62
tests/integration/test_runtime_flow.py
Normal file
62
tests/integration/test_runtime_flow.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from engine.devops_agent.compiler import compile_workflow
|
||||||
|
from engine.devops_agent.runtime import run_issue_comment_workflow
|
||||||
|
from engine.devops_agent.spec import load_workflow_spec
|
||||||
|
from engine.devops_agent.validator import validate_workflow_spec
|
||||||
|
|
||||||
|
|
||||||
|
class FakeProvider:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.comments: list[dict[str, object]] = []
|
||||||
|
|
||||||
|
def parse_issue_comment_event(self, payload: dict[str, object]) -> dict[str, object]:
|
||||||
|
repository = payload["repository"]
|
||||||
|
issue = payload["issue"]
|
||||||
|
comment = payload["comment"]
|
||||||
|
return {
|
||||||
|
"repo": repository["full_name"],
|
||||||
|
"issue_number": issue["number"],
|
||||||
|
"comment_body": comment["body"],
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_issue(self, repo: str, issue_number: int) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"number": issue_number,
|
||||||
|
"title": "Fix issue delivery flow",
|
||||||
|
"body": "The agent should post evidence back to the issue.",
|
||||||
|
"state": "open",
|
||||||
|
"repo": repo,
|
||||||
|
}
|
||||||
|
|
||||||
|
def post_issue_comment(self, repo: str, issue_number: int, body: str) -> dict[str, object]:
|
||||||
|
record = {
|
||||||
|
"repo": repo,
|
||||||
|
"issue_number": issue_number,
|
||||||
|
"body": body,
|
||||||
|
}
|
||||||
|
self.comments.append(record)
|
||||||
|
return {"id": len(self.comments), **record}
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_executes_flow_and_writes_evidence() -> None:
|
||||||
|
spec = load_workflow_spec(Path("workflows/gitea-issue-delivery.md"))
|
||||||
|
assert validate_workflow_spec(spec) == []
|
||||||
|
lock = compile_workflow(spec)
|
||||||
|
payload = json.loads(Path("tests/fixtures/gitea/comment_event.json").read_text(encoding="utf-8"))
|
||||||
|
provider = FakeProvider()
|
||||||
|
|
||||||
|
artifact = run_issue_comment_workflow(
|
||||||
|
lock=lock,
|
||||||
|
provider=provider,
|
||||||
|
event_payload=payload,
|
||||||
|
output_dir=Path(".tmp/runtime-flow-test"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert artifact["result"] == "success"
|
||||||
|
assert artifact["plan_state"]["status"] == "pending_review"
|
||||||
|
assert provider.comments
|
||||||
|
assert Path(artifact["artifact_path"]).exists()
|
||||||
84
tests/unit/test_cli.py
Normal file
84
tests/unit/test_cli.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from engine.devops_agent import cli
|
||||||
|
|
||||||
|
|
||||||
|
class FakeProvider:
|
||||||
|
def __init__(self, *, base_url: str, token: str) -> None:
|
||||||
|
self.base_url = base_url
|
||||||
|
self.token = token
|
||||||
|
|
||||||
|
def parse_issue_comment_event(self, payload: dict[str, object]) -> dict[str, object]:
|
||||||
|
repository = payload["repository"]
|
||||||
|
issue = payload["issue"]
|
||||||
|
comment = payload["comment"]
|
||||||
|
return {
|
||||||
|
"repo": repository["full_name"],
|
||||||
|
"issue_number": issue["number"],
|
||||||
|
"comment_body": comment["body"],
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_issue(self, repo: str, issue_number: int) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"number": issue_number,
|
||||||
|
"title": "Fix issue delivery flow",
|
||||||
|
"body": "The agent should post evidence back to the issue.",
|
||||||
|
"state": "open",
|
||||||
|
"repo": repo,
|
||||||
|
}
|
||||||
|
|
||||||
|
def post_issue_comment(self, repo: str, issue_number: int, body: str) -> dict[str, object]:
|
||||||
|
return {"id": 1, "repo": repo, "issue_number": issue_number, "body": body}
|
||||||
|
|
||||||
|
|
||||||
|
def test_compile_command_writes_lock_file() -> None:
|
||||||
|
output_path = Path(".tmp/cli-tests/gitea-issue-delivery.lock.json")
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
exit_code = cli.main(
|
||||||
|
[
|
||||||
|
"compile",
|
||||||
|
"workflows/gitea-issue-delivery.md",
|
||||||
|
"--output",
|
||||||
|
str(output_path),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert exit_code == 0
|
||||||
|
assert output_path.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_command_returns_success() -> None:
|
||||||
|
exit_code = cli.main(["validate", "workflows/gitea-issue-delivery.md"])
|
||||||
|
|
||||||
|
assert exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_command_writes_runtime_artifact(monkeypatch) -> None:
|
||||||
|
output_dir = Path(".tmp/cli-tests/runtime-run")
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
monkeypatch.setattr(cli, "GiteaProvider", FakeProvider)
|
||||||
|
|
||||||
|
exit_code = cli.main(
|
||||||
|
[
|
||||||
|
"run",
|
||||||
|
"workflows/gitea-issue-delivery.md",
|
||||||
|
"--event-payload",
|
||||||
|
"tests/fixtures/gitea/comment_event.json",
|
||||||
|
"--output-dir",
|
||||||
|
str(output_dir),
|
||||||
|
"--base-url",
|
||||||
|
"https://fun-md.com",
|
||||||
|
"--token",
|
||||||
|
"fake-token",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
artifact_path = output_dir / "run-artifact.json"
|
||||||
|
artifact = json.loads(artifact_path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
assert exit_code == 0
|
||||||
|
assert artifact["result"] == "success"
|
||||||
26
tests/unit/test_compiler.py
Normal file
26
tests/unit/test_compiler.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from engine.devops_agent.compiler import compile_workflow
|
||||||
|
from engine.devops_agent.spec import load_workflow_spec
|
||||||
|
|
||||||
|
|
||||||
|
def test_compile_emits_normalized_lock_payload() -> None:
|
||||||
|
spec = load_workflow_spec(Path("workflows/gitea-issue-delivery.md"))
|
||||||
|
|
||||||
|
lock = compile_workflow(spec)
|
||||||
|
|
||||||
|
assert lock["version"] == 1
|
||||||
|
assert lock["workflow_name"] == "gitea-issue-delivery"
|
||||||
|
assert lock["provider"] == "gitea"
|
||||||
|
assert lock["triggers"] == [
|
||||||
|
{
|
||||||
|
"event": "issue_comment",
|
||||||
|
"commands": ["@devops-agent"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert lock["policy"]["path_scope"] == []
|
||||||
|
assert lock["policy"]["require_human_merge"] is True
|
||||||
|
assert lock["safe_outputs"]["add_comment"]["max"] == 3
|
||||||
|
assert "issue_comment" in lock["required_evidence"]
|
||||||
72
tests/unit/test_gitea_provider.py
Normal file
72
tests/unit/test_gitea_provider.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from engine.devops_agent.providers.gitea import GiteaProvider
|
||||||
|
|
||||||
|
|
||||||
|
class FakeTransport:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.calls: list[dict[str, object]] = []
|
||||||
|
|
||||||
|
def __call__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
method: str,
|
||||||
|
url: str,
|
||||||
|
headers: dict[str, str],
|
||||||
|
body: dict[str, object] | None,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
self.calls.append(
|
||||||
|
{
|
||||||
|
"method": method,
|
||||||
|
"url": url,
|
||||||
|
"headers": headers,
|
||||||
|
"body": body,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if url.endswith("/comments"):
|
||||||
|
return {"id": 999, "body": body["body"] if body else ""}
|
||||||
|
return json.loads(Path("tests/fixtures/gitea/issue.json").read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_gitea_provider_fetches_issue() -> None:
|
||||||
|
transport = FakeTransport()
|
||||||
|
provider = GiteaProvider(
|
||||||
|
base_url="https://fun-md.com",
|
||||||
|
token="test-token",
|
||||||
|
transport=transport,
|
||||||
|
)
|
||||||
|
|
||||||
|
issue = provider.get_issue("Fun_MD/devops-skills", 48)
|
||||||
|
|
||||||
|
assert issue["number"] == 48
|
||||||
|
assert transport.calls[0]["method"] == "GET"
|
||||||
|
assert str(transport.calls[0]["url"]).endswith("/api/v1/repos/Fun_MD/devops-skills/issues/48")
|
||||||
|
|
||||||
|
|
||||||
|
def test_gitea_provider_posts_issue_comment() -> None:
|
||||||
|
transport = FakeTransport()
|
||||||
|
provider = GiteaProvider(
|
||||||
|
base_url="https://fun-md.com",
|
||||||
|
token="test-token",
|
||||||
|
transport=transport,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = provider.post_issue_comment("Fun_MD/devops-skills", 48, "Evidence posted")
|
||||||
|
|
||||||
|
assert response["id"] == 999
|
||||||
|
assert transport.calls[0]["method"] == "POST"
|
||||||
|
assert transport.calls[0]["body"] == {"body": "Evidence posted"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_gitea_provider_parses_comment_event() -> None:
|
||||||
|
provider = GiteaProvider(base_url="https://fun-md.com", token="test-token")
|
||||||
|
payload = json.loads(Path("tests/fixtures/gitea/comment_event.json").read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
event = provider.parse_issue_comment_event(payload)
|
||||||
|
|
||||||
|
assert event["repo"] == "Fun_MD/devops-skills"
|
||||||
|
assert event["issue_number"] == 48
|
||||||
|
assert "@devops-agent" in event["comment_body"]
|
||||||
34
tests/unit/test_policies.py
Normal file
34
tests/unit/test_policies.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from engine.devops_agent.policies import PolicyViolation, RuntimePolicy
|
||||||
|
|
||||||
|
|
||||||
|
def test_policy_allows_declared_safe_output() -> None:
|
||||||
|
policy = RuntimePolicy(
|
||||||
|
safe_outputs={"add_comment": {"max": 2}},
|
||||||
|
path_scope=["engine/devops_agent/", "README.md"],
|
||||||
|
)
|
||||||
|
|
||||||
|
policy.assert_operation_allowed("add_comment")
|
||||||
|
|
||||||
|
|
||||||
|
def test_policy_rejects_undeclared_write_action() -> None:
|
||||||
|
policy = RuntimePolicy(
|
||||||
|
safe_outputs={"add_comment": {"max": 2}},
|
||||||
|
path_scope=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(PolicyViolation, match="close_issue"):
|
||||||
|
policy.assert_operation_allowed("close_issue")
|
||||||
|
|
||||||
|
|
||||||
|
def test_policy_rejects_paths_outside_scope() -> None:
|
||||||
|
policy = RuntimePolicy(
|
||||||
|
safe_outputs={"write_file": {"max": 5}},
|
||||||
|
path_scope=["engine/devops_agent/"],
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(PolicyViolation, match="outside allowed path scope"):
|
||||||
|
policy.assert_path_allowed("skills/gitea-issue-devops-agent/SKILL.md")
|
||||||
11
tests/unit/test_smoke_imports.py
Normal file
11
tests/unit/test_smoke_imports.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import importlib
|
||||||
|
|
||||||
|
|
||||||
|
def test_core_modules_are_importable() -> None:
|
||||||
|
module_names = [
|
||||||
|
"engine.devops_agent",
|
||||||
|
"engine.devops_agent.cli",
|
||||||
|
]
|
||||||
|
|
||||||
|
for module_name in module_names:
|
||||||
|
importlib.import_module(module_name)
|
||||||
29
tests/unit/test_spec_loader.py
Normal file
29
tests/unit/test_spec_loader.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from engine.devops_agent.spec import WorkflowSpecError, load_workflow_spec
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_workflow_spec_splits_frontmatter_and_body() -> None:
|
||||||
|
spec = load_workflow_spec(Path("tests/fixtures/specs/valid_workflow.md"))
|
||||||
|
|
||||||
|
assert spec.name == "issue-delivery"
|
||||||
|
assert spec.provider == "gitea"
|
||||||
|
assert spec.frontmatter["safe_outputs"]["add_comment"]["max"] == 2
|
||||||
|
assert "Read the selected issue" in spec.body
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_workflow_spec_rejects_missing_provider() -> None:
|
||||||
|
with pytest.raises(WorkflowSpecError, match="provider"):
|
||||||
|
load_workflow_spec(Path("tests/fixtures/specs/invalid_missing_provider.md"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_sample_workflow_spec_exists_and_loads() -> None:
|
||||||
|
spec = load_workflow_spec(Path("workflows/gitea-issue-delivery.md"))
|
||||||
|
|
||||||
|
assert spec.name == "gitea-issue-delivery"
|
||||||
|
assert spec.provider == "gitea"
|
||||||
|
assert "add_comment" in spec.frontmatter["safe_outputs"]
|
||||||
30
tests/unit/test_validator.py
Normal file
30
tests/unit/test_validator.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from engine.devops_agent.spec import load_workflow_spec
|
||||||
|
from engine.devops_agent.validator import validate_workflow_spec
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_accepts_the_sample_workflow() -> None:
|
||||||
|
spec = load_workflow_spec(Path("workflows/gitea-issue-delivery.md"))
|
||||||
|
|
||||||
|
errors = validate_workflow_spec(spec)
|
||||||
|
|
||||||
|
assert errors == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_rejects_write_permissions_without_safe_outputs() -> None:
|
||||||
|
spec = load_workflow_spec(Path("tests/fixtures/specs/no_safe_outputs_for_write.md"))
|
||||||
|
|
||||||
|
errors = validate_workflow_spec(spec)
|
||||||
|
|
||||||
|
assert any("safe_outputs" in error for error in errors)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_rejects_invalid_path_scope() -> None:
|
||||||
|
spec = load_workflow_spec(Path("tests/fixtures/specs/invalid_path_scope.md"))
|
||||||
|
|
||||||
|
errors = validate_workflow_spec(spec)
|
||||||
|
|
||||||
|
assert any("path_scope" in error for error in errors)
|
||||||
29
workflows/gitea-issue-delivery.lock.json
Normal file
29
workflows/gitea-issue-delivery.lock.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"workflow_name": "gitea-issue-delivery",
|
||||||
|
"provider": "gitea",
|
||||||
|
"source": "workflows/gitea-issue-delivery.md",
|
||||||
|
"triggers": [
|
||||||
|
{
|
||||||
|
"event": "issue_comment",
|
||||||
|
"commands": [
|
||||||
|
"@devops-agent"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"safe_outputs": {
|
||||||
|
"add_comment": {
|
||||||
|
"max": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required_evidence": [
|
||||||
|
"issue_comment",
|
||||||
|
"verification_summary"
|
||||||
|
],
|
||||||
|
"policy": {
|
||||||
|
"require_human_merge": true,
|
||||||
|
"require_fixed_issue": true,
|
||||||
|
"path_scope": []
|
||||||
|
},
|
||||||
|
"instructions": "# Gitea Issue Delivery\n\nStart from a fixed issue selected by a human or explicit trigger.\n\nCreate or update plan state before code changes, keep writes within declared safe outputs,\nand publish verification evidence back to the issue thread.\n"
|
||||||
|
}
|
||||||
34
workflows/gitea-issue-delivery.md
Normal file
34
workflows/gitea-issue-delivery.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
name: gitea-issue-delivery
|
||||||
|
provider: gitea
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
commands: ["@devops-agent"]
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull_requests: write
|
||||||
|
safe_outputs:
|
||||||
|
add_comment:
|
||||||
|
max: 3
|
||||||
|
plan:
|
||||||
|
required: true
|
||||||
|
statuses:
|
||||||
|
- selected
|
||||||
|
- planned
|
||||||
|
- in_progress
|
||||||
|
- pending_test
|
||||||
|
- pending_review
|
||||||
|
evidence:
|
||||||
|
required:
|
||||||
|
- issue_comment
|
||||||
|
- verification_summary
|
||||||
|
policy:
|
||||||
|
require_fixed_issue: true
|
||||||
|
require_human_merge: true
|
||||||
|
---
|
||||||
|
# Gitea Issue Delivery
|
||||||
|
|
||||||
|
Start from a fixed issue selected by a human or explicit trigger.
|
||||||
|
|
||||||
|
Create or update plan state before code changes, keep writes within declared safe outputs,
|
||||||
|
and publish verification evidence back to the issue thread.
|
||||||
Reference in New Issue
Block a user