[Feature]: Added artifacts for ai#1059
Conversation
|
| gcs_bucket=$(curl -s "http://metadata/computeMetadata/v1/instance/attributes/bucket" -H "Metadata-Flavor: Google") | ||
| test_id=$(curl -s "http://metadata/computeMetadata/v1/instance/attributes/testID" -H "Metadata-Flavor: Google") | ||
| token=$(curl -s "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" -H "Metadata-Flavor: Google" | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])") |
There was a problem hiding this comment.
I'm not in favour of directly uploading to a bucket; I'd rather have it stored through the SP, where we can exercise more control and do extra checks. This direct upload would allow someone with malicious intentions to hijack a CCExtractor PR and start uploading other things.
|
|
||
|
|
||
| @mod_test.route('/log-files/<test_id>') | ||
| @login_required |
There was a problem hiding this comment.
A login should still be required.
| """Download the ccextractor binary used in a test (linux or windows).""" | ||
| from run import storage_client_bucket | ||
| # Try linux name first, then windows | ||
| for name in ['ccextractor', 'ccextractor.exe']: |
There was a problem hiding this comment.
Based on the test ID, you can exactly know what platform it was for, and thus check the direct extension rather than "guessing".
|
|
||
|
|
||
| @mod_test.route('/<int:test_id>/binary', methods=['GET']) | ||
| def download_binary(test_id): |
There was a problem hiding this comment.
This needs a logged-in user.
|
|
||
|
|
||
| @mod_test.route('/<int:test_id>/coredump', methods=['GET']) | ||
| def download_coredump(test_id): |
There was a problem hiding this comment.
This needs a logged-in user.
|
|
||
|
|
||
| @mod_test.route('/<int:test_id>/combined-stdout', methods=['GET']) | ||
| def download_combined_stdout(test_id): |
There was a problem hiding this comment.
This needs a logged-in user.
| ) | ||
|
|
||
|
|
||
| @mod_test.route('/<int:test_id>/regression/<int:regression_test_id>/<int:output_id>/output-got', methods=['GET']) |
There was a problem hiding this comment.
This needs a logged-in user.
|
|
||
|
|
||
| @mod_test.route('/<int:test_id>/regression/<int:regression_test_id>/<int:output_id>/output-expected', methods=['GET']) | ||
| def download_output_expected(test_id, regression_test_id, output_id): |
There was a problem hiding this comment.
This needs a logged-in user.
| filename=f'output_expected_{regression_test_id}_{output_id}{ext}' | ||
| ) | ||
| @mod_test.route('/<int:test_id>/sample/<int:sample_id>', methods=['GET']) | ||
| def download_sample_ai(test_id, sample_id): |
There was a problem hiding this comment.
Even with AI workflow, there should be auth. We could perfectly issue a long(er) living access token for programmatic access. If we don't, a scraper might find this and still cause huge fees.
| ) | ||
|
|
||
|
|
||
| def _process_test_case(test_id, category_name, t_data): |
There was a problem hiding this comment.
Please fix the code smell here.



[FEATURE] AI-Accessible Test Artifacts & Structured Test Report Endpoint
In raising this pull request, I confirm the following (please check boxes):
My familiarity with the project is as follows (check one):
What does this PR do?
This PR introduces artifact capture and upload from CI test VMs to GCS, along with a set of new download endpoints and a structured JSON report endpoint designed to make test results fully accessible — including to AI agents.
A demo video is attached below showcasing all the new features end to end.
PR.Showcase.mp4
Objective
Right now, when a test run fails, debugging is hard. You can see that something went wrong, but you can't easily grab the binary that was tested, see what it actually printed, or get the coredump if it crashed. Everything lives ephemerally on the VM and disappears. This PR fixes that by persisting the important artifacts to GCS and making them accessible via clean download URLs.
The
ai.jsonendpoint was also something I wanted to add to make it possible for AI agents (or any automated tooling) to consume a full structured report of a test run — with direct download links to every artifact — without needing to scrape HTML.Changes
install/ci-vm/ci-linux/ci/runCI/tmp/combined_stdout.log, and preserves the original exit code faithfully. This means you get a single unified log of everything ccextractor printed across all test cases in a run.test_artifacts/{test_id}/:ccextractorbinary itself/tmp/coredumps/, if one was producedtestIDis now passed as VM instance metadata so the CI script knows where to namespace the uploads.mod_ci/controllers.pytestIDis now included in the metadata passed to both Linux and Windows VM instances at creation time.os.rename()→os.replace()in upload handlers.os.renamecan fail silently or raise an error when source and destination are on different filesystems (which can happen with temp directories).os.replacehandles this correctly.mod_test/controllers.pydownload_build_log_fileto usesend_from_directorydirectly instead of going throughserve_file_download, which was doing unnecessary GCS signed URL generation for a file that's already on disk._artifact_redirecthelper that looks up a blob in GCS, generates a short-lived signed URL, and redirects to it — or returns 404 if the blob doesn't exist. All new download endpoints use this./<test_id>/binary— downloads the ccextractor binary used in the test (tries Linux name first, then Windows)/<test_id>/coredump— downloads the coredump if one was captured/<test_id>/combined-stdout— downloads the unified stdout/stderr log/<test_id>/regression/<regression_test_id>/<output_id>/output-got— downloads the actual output file for a specific test case/<test_id>/regression/<regression_test_id>/<output_id>/output-expected— downloads the expected output file for a specific test case/<test_id>/sample/<sample_id>— downloads the sample file used in a regression test/<test_id>/ai.json— returns a full structured JSON report of the test run (see below)utility.pyos.path.joinwas used to build GCS blob paths. Since GCS paths are always forward-slash separated andos.path.joinbehaves differently on Windows, this is now done with a manual/.join` with empty-part filtering.GCS_SIGNED_URL_EXPIRY_LIMITfrom''(an empty string that would crashtimedelta) to30.The
ai.jsonEndpointGET /<test_id>/ai.jsonreturns a JSON object that looks roughly like:{ "test_id": 123, "commit": "abc123", "platform": "linux", "branch": "master", "status": "completed", "binary_url": "https://...", "coredump_url": null, "log_url": "https://...", "combined_stdout_url": "https://...", "summary": { "total": 50, "passed": 48, "failed": 2 }, "test_cases": [ { "regression_test_id": 7, "category": "...", "sample_filename": "sample.ts", "sample_url": "https://...", "arguments": "-out=ttxt", "result": "Fail", "exit_code": 1, "expected_exit_code": 0, "runtime_ms": 4231, "how_to_reproduce": "./ccextractor -out=ttxt sample.ts", "outputs": [ { "output_id": 3, "correct_extension": ".ttxt", "expected_url": "https://...", "got_url": "https://...", "diff_url": "https://..." } ] } ], "how_to_reproduce": "Download the binary and sample, then run: ./ccextractor {arguments} {sample_filename}" }Every URL in the response is a signed GCS download link valid for 30 minutes. The intent is that this endpoint gives an AI agent (or any automated system) everything it needs to fully investigate a test failure — the binary, the sample, the expected and actual outputs, the diff, and the full stdout log — without any additional context.
Notes
/tmp/combined_stdout.logpath, which is fine given each test run gets its own VM.