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'