From ab7ab98cbfc259bb24ff7b9accccea9696e5c3fc Mon Sep 17 00:00:00 2001 From: labadmin Date: Mon, 2 Nov 2026 22:37:33 +0000 Subject: [PATCH] Materialize agent project CI --- .gitea/workflows/ci.yaml | 329 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 .gitea/workflows/ci.yaml diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..97b8bbb --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -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, '') + 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'