Materialize agent project CI

This commit is contained in:
2026-11-02 22:37:33 +00:00
parent 6d79fef8c0
commit ab7ab98cbf
+329
View File
@@ -0,0 +1,329 @@
name: Repository Validate
on:
push:
branches:
- master
- main
pull_request:
branches:
- master
- main
- task/**
jobs:
repository-checks:
runs-on: agent-environment
steps:
- name: Checkout
shell: bash
env:
CHECKOUT_SERVER_URL: ${{ github.server_url }}
CHECKOUT_REPOSITORY: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
python3 - <<'CHECKOUT_PY'
"""Self-contained checkout embedded in generated CI; no downloaded actions."""
from __future__ import annotations
import base64
import os
from pathlib import Path
import re
import subprocess
from urllib.parse import urlsplit
def checkout(environment=None):
env = dict(os.environ if environment is None else environment)
server = env.pop('CHECKOUT_SERVER_URL', '').rstrip('/')
repo = env.pop('CHECKOUT_REPOSITORY', '')
sha = env.pop('CHECKOUT_SHA', '')
token = env.pop('CHECKOUT_TOKEN', '')
parsed = urlsplit(server)
if (parsed.scheme not in ('https', 'http') or not parsed.hostname or parsed.username
or parsed.password or parsed.query or parsed.fragment):
raise ValueError('invalid checkout server URL')
if not re.fullmatch(r'[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+', repo) or any(p in ('.', '..') for p in repo.split('/')):
raise ValueError('invalid checkout repository')
if not re.fullmatch(r'[0-9a-fA-F]{40}|[0-9a-fA-F]{64}', sha):
raise ValueError('checkout requires an exact commit SHA')
workspace = Path(env.get('GITHUB_WORKSPACE') or env.get('GITEA_WORKSPACE') or os.getcwd()).resolve()
workspace.mkdir(parents=True, exist_ok=True)
# Never inherit authentication overrides, tracing, hooks or checkout filters.
env = {k: v for k, v in env.items() if not k.startswith('GIT_')}
env.update(GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL='/dev/null',
GIT_TERMINAL_PROMPT='0', GIT_LFS_SKIP_SMUDGE='1')
if (workspace / '.git').exists():
raise ValueError('checkout requires a fresh workspace')
def git(*args, auth=False):
child = dict(env)
if auth and token:
credential = base64.b64encode(('x-access-token:' + token).encode()).decode()
child.update(GIT_CONFIG_COUNT='1',
GIT_CONFIG_KEY_0='http.extraHeader',
GIT_CONFIG_VALUE_0='Authorization: Basic ' + credential)
result = subprocess.run(['git', '-c', 'core.hooksPath=/dev/null', '-C', str(workspace), *args],
env=child, capture_output=True, text=True, timeout=180)
if result.returncode:
# Transport output may contain credentials or upstream-controlled data.
raise RuntimeError('checkout git operation failed: ' + args[0])
return result.stdout.strip()
git('init')
git('remote', 'add', 'origin', server + '/' + repo + '.git')
git('-c', 'http.followRedirects=false', 'fetch', '--no-tags', '--depth=1', 'origin', sha, auth=True)
actual = git('rev-parse', 'FETCH_HEAD^{commit}')
if actual.lower() != sha.lower():
raise ValueError('fetched commit does not match requested SHA')
git('checkout', '--detach', '--force', actual)
if git('rev-parse', 'HEAD').lower() != sha.lower():
raise ValueError('checked out commit does not match requested SHA')
print('Checked out requested commit ' + actual)
if __name__ == '__main__':
try:
checkout()
except (ValueError, RuntimeError, subprocess.TimeoutExpired) as exc:
print('AGENT_CI_RESULT command_id=checkout stage=checkout owner=infrastructure '
'repair_role=operator exit_code=1 expected_exit_code=0 status=failed')
# Do not stringify subprocess exceptions: they may include command output.
raise SystemExit('Checkout failed (' + type(exc).__name__ + ')') from None
CHECKOUT_PY
- name: Validate facade structure
shell: bash
run: |
python3 - <<'PY'
from pathlib import Path
path = Path(".agent/project.yaml")
if not path.is_file():
raise SystemExit(".agent/project.yaml is missing")
text = path.read_text(encoding="utf-8")
for key in ["schema_version", "registry", "project", "components"]:
if not any(line.startswith(key + ":") for line in text.splitlines()):
raise SystemExit(f".agent/project.yaml missing required key: {key}")
if not any(line.lstrip().startswith("- ") for line in text.splitlines()):
raise SystemExit(".agent/project.yaml must declare at least one component")
print("facade structure ok")
PY
- name: Run facade commands
shell: bash
run: |
set -euo pipefail
full_run=true
if [ "${GITHUB_EVENT_NAME:-}" = "pull_request" ] && [[ "${GITHUB_BASE_REF:-}" == task/* ]]; then
full_run=false
fi
export PATH="$PWD/.venv/bin:$PATH"
printf 'AGENT_CI_MODE full_run=%s event=%s base_ref=%s\n' "$full_run" "${GITHUB_EVENT_NAME:-unknown}" "${GITHUB_BASE_REF:-}"
run_ci_stage() {
command_id="$1"
stage="$2"
owner="$3"
repair_role="$4"
command="$5"
expected_exit_code="$6"
missing_policy="$7"
shift 7
missing_paths=()
for required_path in "$@"; do
if [ ! -e "$required_path" ]; then
missing_paths+=("$required_path")
fi
done
if [ "${#missing_paths[@]}" -gt 0 ]; then
missing_csv="$(IFS=,; echo "${missing_paths[*]}")"
if [ "$full_run" != true ] && [ "$missing_policy" = skip_partial ]; then
printf 'AGENT_CI_RESULT command_id=%s stage=%s owner=%s repair_role=%s exit_code=0 expected_exit_code=%s status=skipped_missing_prerequisites missing_paths=%s\n' "$command_id" "$stage" "$owner" "$repair_role" "$expected_exit_code" "$missing_csv"
return 0
fi
printf 'AGENT_CI_RESULT command_id=%s stage=%s owner=%s repair_role=%s exit_code=97 expected_exit_code=%s status=missing_prerequisites missing_paths=%s\n' "$command_id" "$stage" "$owner" "$repair_role" "$expected_exit_code" "$missing_csv"
return 97
fi
if [ -z "$command" ]; then
echo "skip $command_id (not declared)"
return 0
fi
echo "::group::$command_id"
set +e
bash -o pipefail -eu -c "$command"
exit_code="$?"
set -e
# Executor reserves 78 for tracked preparation-output conflicts.
# Other dependency failures retain infrastructure ownership.
if [ "$stage" = dependency_preparation ] && [ "$exit_code" -eq 78 ]; then
owner=project
repair_role=developer
fi
status=failed
if [ "$exit_code" -eq "$expected_exit_code" ]; then
status=passed
fi
if [ "$status" = failed ]; then
python3 - "$command_id" "$exit_code" <<'CI_DIAGNOSTICS_PY' || echo 'AGENT_CI_DIAGNOSTICS_ARTIFACT collector_failed'
"""Bounded, opt-in CI failure reports; embedded in the canonical workflow.
Gitea Actions artifact v3 protocol (no external Action or package dependency).
"""
import base64
import hashlib
import json
import os
from pathlib import PurePosixPath
import re
import stat
import sys
import urllib.error
import urllib.parse
import urllib.request
FILE_LIMIT = 65536
FILE_COUNT = 8
MANIFEST = '.ci/diagnostics.json'
def read_relative(root, name, limit=FILE_LIMIT):
path = PurePosixPath(name)
if not name or path.is_absolute() or any(p in ('', '.', '..') for p in name.split('/')):
raise ValueError('invalid path')
fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY)
try:
for part in path.parts[:-1]:
child = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=fd)
os.close(fd)
fd = child
child = os.open(path.name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=fd)
with os.fdopen(child, 'rb') as stream:
if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode):
raise ValueError('not a regular file')
return stream.read(limit + 1)
finally:
os.close(fd)
def scrub(text, env):
for key, value in env.items():
if len(value) >= 8 and any(word in key.upper() for word in ('TOKEN', 'SECRET', 'PASSWORD', 'API_KEY')):
text = text.replace(value, '<redacted>')
return text
def collect(root, command, exit_code, env):
report = {'version': 1, 'command_id': command, 'exit_code': exit_code,
'sha': env.get('GITHUB_SHA', ''), 'files': []}
try:
raw = read_relative(root, MANIFEST)
if len(raw) > FILE_LIMIT:
raise ValueError('manifest too large')
spec = json.loads(raw)
if spec.get('version') != 1 or not isinstance(spec.get('commands'), dict):
raise ValueError('invalid manifest')
names = spec['commands'].get(command, [])
if not isinstance(names, list) or len(names) > FILE_COUNT or any(not isinstance(n, str) for n in names):
raise ValueError('invalid file list')
report['manifest_status'] = 'loaded'
except FileNotFoundError:
report['manifest_status'] = 'missing'
names = []
except (OSError, ValueError, TypeError, AttributeError):
report['manifest_status'] = 'invalid'
names = []
for name in names:
item = {'path': name}
try:
# No directory traversal, symlinks, devices, hidden credential files,
# wildcards or recursive workspace scans.
if any(p.startswith('.') for p in PurePosixPath(name).parts) or len(name) > 512:
raise ValueError('hidden or invalid path')
raw = read_relative(root, name)
item.update(status='captured', truncated=len(raw) > FILE_LIMIT,
content=scrub(raw[:FILE_LIMIT].decode('utf-8', errors='replace'), env))
except (OSError, ValueError):
item['status'] = 'unavailable'
report['files'].append(item)
return report
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
return None
def upload(report, env, request=None):
base = env.get('ACTIONS_RUNTIME_URL', '')
token = env.get('ACTIONS_RUNTIME_TOKEN', '')
run = env.get('GITHUB_RUN_ID', '')
parsed = urllib.parse.urlsplit(base)
if not base or not token or not run:
return 'runtime_unavailable'
if parsed.scheme not in ('http', 'https') or not parsed.netloc or parsed.username or parsed.password or parsed.query or parsed.fragment:
raise ValueError('invalid runtime URL')
if request is None:
opener = urllib.request.build_opener(NoRedirect)
def request(method, url, body, headers):
req = urllib.request.Request(url, data=body, method=method, headers=headers)
try:
with opener.open(req, timeout=10) as response:
return response.read(65536)
except urllib.error.HTTPError as exc:
raise RuntimeError(f'artifact {method} HTTP {exc.code}') from None
endpoint = base.rstrip('/')+'/_apis/pipelines/workflows/'+urllib.parse.quote(run, safe='')+'/artifacts'
name = 'ci-failure-'+re.sub('[^A-Za-z0-9_-]', '-', report['command_id'])[:60]+'-'+re.sub('[^0-9]', '', env.get('GITHUB_RUN_ATTEMPT', '1'))[:8]
headers = {'Authorization': 'Bearer '+token, 'Content-Type': 'application/json'}
created = request('POST', endpoint+'?api-version=6.0-preview', json.dumps({'Type': 'actions_storage', 'Name': name}).encode(), headers)
location = json.loads(created)['fileContainerResourceUrl']
location = urllib.parse.urljoin(endpoint, location)
target = urllib.parse.urlsplit(location)
if (target.scheme, target.netloc) != (parsed.scheme, parsed.netloc) or target.username or target.password:
raise ValueError('cross-origin artifact URL')
data = json.dumps(report, ensure_ascii=True).encode()
url = location+('&' if target.query else '?')+urllib.parse.urlencode({'itemPath': name+'/diagnostics.json'})
request('PUT', url, data, {**headers, 'Content-Type': 'application/octet-stream',
'Content-Range': f'bytes 0-{len(data)-1}/{len(data)}', 'x-tfs-filelength': str(len(data)),
'x-actions-results-md5': base64.b64encode(hashlib.md5(data).digest()).decode('ascii')})
request('PATCH', endpoint+'?'+urllib.parse.urlencode({'artifactName': name, 'api-version': '6.0-preview'}),
json.dumps({'Size': len(data)}).encode(), headers)
return 'uploaded:'+name
def main():
report = collect(os.getcwd(), sys.argv[1], int(sys.argv[2]), os.environ)
# A bounded preview remains in the job log even if artifact upload fails.
preview = json.loads(json.dumps(report))
for item in preview['files']:
if 'content' in item:
item['content'] = item['content'][:1000]
print('AGENT_CI_DIAGNOSTICS '+json.dumps(preview, ensure_ascii=True))
try:
result = upload(report, os.environ)
except Exception as exc:
result = 'upload_failed:'+str(exc) if isinstance(exc, RuntimeError) else 'upload_failed:'+type(exc).__name__
print('AGENT_CI_DIAGNOSTICS_ARTIFACT '+result)
if __name__ == '__main__':
main()
CI_DIAGNOSTICS_PY
fi
printf 'AGENT_CI_RESULT command_id=%s stage=%s owner=%s repair_role=%s exit_code=%s expected_exit_code=%s status=%s\n' "$command_id" "$stage" "$owner" "$repair_role" "$exit_code" "$expected_exit_code" "$status"
echo "::endgroup::"
if [ "$status" = passed ]; then
return 0
fi
if [ "$exit_code" -eq 0 ]; then
return 1
fi
return "$exit_code"
}
run_ci_stage 'tools.dependencies.resolution' 'dependency_prerequisite' 'project' 'developer' '' '0' 'fail' 'uv.lock'
run_ci_stage 'tools.dependencies.restore' 'dependency_preparation' 'infrastructure' 'infrastructure' '/opt/agent-environment/bin/python -I -m agent_environment_executor.cli --workspace "$PWD" --component '"'"'tools'"'"' --mode restore' '0' 'fail'
run_ci_stage 'tools.validate' 'command' 'project' 'developer' 'python -m compileall tools tests' '0' 'skip_partial' 'pyproject.toml' 'tools' 'tests'
run_ci_stage 'tools.test' 'command' 'project' 'developer' 'pytest' '0' 'skip_partial' 'pyproject.toml' 'tests/test_example.py'
run_ci_stage 'tools.smoke' 'command' 'project' 'developer' 'python -m tools.example examples/input.json' '0' 'skip_partial' 'pyproject.toml' 'tools/example.py' 'examples/input.json'
run_ci_stage 'acceptance.help-command' 'acceptance' 'project' 'developer' 'python -m tools.example --help' '0' 'skip_partial' 'examples/input.json' 'tools/example.py'