Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:

# --frozen keeps uv from re-resolving and rewriting uv.lock as a side
# effect of `uv run`; the lock is committed and must not churn in CI.
- name: Type-check install-sandbox tools
run: uv run --frozen pyright --warnings tools/install_sandbox

- name: Check generated skill artifacts are up to date
run: uv run --frozen python -m tools.skillgen --check

Expand Down
143 changes: 143 additions & 0 deletions .github/workflows/install-sandbox.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Cost controls: shorten retention to reduce stored artifact-days.
# Cost controls: lower compression from 9 if runner time matters more than storage.
name: Install sandbox

env:
INSTALL_SANDBOX_ARTIFACT_RETENTION_DAYS: 14
INSTALL_SANDBOX_ARTIFACT_COMPRESSION_LEVEL: 9

on:
pull_request:
branches: ["v8", "main"]
paths:
- "graphify/install.py"
- "graphify/__main__.py"
- "graphify/hooks.py"
- "graphify/paths.py"
- "graphify/skill*.md"
- "graphify/command*.md"
- "graphify/skills/**"
- "graphify/always_on/**"
- "tools/install_sandbox/**"
- "tests/install_sandbox/**"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/install-sandbox.yml"
push:
branches: ["v8", "main"]
paths:
- "graphify/install.py"
- "graphify/__main__.py"
- "graphify/hooks.py"
- "graphify/paths.py"
- "graphify/skill*.md"
- "graphify/command*.md"
- "graphify/skills/**"
- "graphify/always_on/**"
- "tools/install_sandbox/**"
- "tests/install_sandbox/**"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/install-sandbox.yml"
schedule:
- cron: "27 5 * * *"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: install-sandbox-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
full-catalog:
name: Full catalog (advisory)
runs-on: ubuntu-latest
timeout-minutes: 150

steps:
- name: Check out source
uses: actions/checkout@v6
with:
persist-credentials: false

- name: Install uv and Python
uses: astral-sh/setup-uv@v8.1.0
with:
python-version: "3.12"

- name: Install dependencies
run: uv sync --all-extras --frozen

- name: Run full install sandbox
id: sandbox
shell: bash
run: |
set +e
uv run --frozen python tools/install_sandbox/run.py \
--repo . \
--all \
--scope both \
--output "$RUNNER_TEMP/graphify-install-sandbox"
sandbox_exit=$?
set -e
printf 'exit_code=%s\n' "$sandbox_exit" >> "$GITHUB_OUTPUT"
- name: Upload install-sandbox artifacts
id: artifact
if: always()
uses: actions/upload-artifact@v7
with:
name: install-sandbox-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/graphify-install-sandbox
if-no-files-found: warn
retention-days: ${{ env.INSTALL_SANDBOX_ARTIFACT_RETENTION_DAYS }}
compression-level: ${{ env.INSTALL_SANDBOX_ARTIFACT_COMPRESSION_LEVEL }}

- name: Write job summary
if: always()
shell: bash
env:
ARTIFACT_URL: ${{ steps.artifact.outputs.artifact-url }}
OUTPUT_DIR: ${{ runner.temp }}/graphify-install-sandbox
SANDBOX_EXIT_CODE: ${{ steps.sandbox.outputs.exit_code }}
run: |
{
echo "## Install sandbox"
echo
echo "- Runner exit code: \`${SANDBOX_EXIT_CODE:-not recorded}\`"
if [[ -n "$ARTIFACT_URL" ]]; then
echo "- [Download the complete diagnostic bundle]($ARTIFACT_URL)"
else
echo "- The artifact upload did not provide a download URL."
fi
echo
if [[ -f "$OUTPUT_DIR/report.md" ]]; then
cat "$OUTPUT_DIR/report.md"
elif [[ -f "$OUTPUT_DIR/run.json" ]]; then
echo "The behavioral report is unavailable. Host lifecycle metadata follows:"
echo
echo '```json'
cat "$OUTPUT_DIR/run.json"
echo
echo '```'
else
echo "No report.md or run.json was produced."
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Apply install-sandbox result
if: always()
shell: bash
env:
RUN_METADATA: ${{ runner.temp }}/graphify-install-sandbox/run.json
SANDBOX_EXIT_CODE: ${{ steps.sandbox.outputs.exit_code }}
run: |
if [[ ! "$SANDBOX_EXIT_CODE" =~ ^[0-9]+$ ]]; then
echo "::error::The install-sandbox runner did not record an exit code."
exit 1
fi
uv run --frozen python -m tools.install_sandbox.ci_result \
--run-json "$RUN_METADATA" \
--runner-exit-code "$SANDBOX_EXIT_CODE"
127 changes: 127 additions & 0 deletions tests/install_sandbox/test_ci_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import json

import pytest

from tools.install_sandbox.ci_result import classify_ci_result, main


@pytest.mark.parametrize(
("state", "runner_exit_code", "annotation"),
[
("passed", 0, "notice"),
("failed", 1, "warning"),
],
)
def test_completed_diagnostics_are_successful_ci_results(
state,
runner_exit_code,
annotation,
):
result = classify_ci_result(
{"state": state, "exit_code": runner_exit_code},
runner_exit_code,
)

assert result.annotation == annotation
assert result.exit_code == 0


@pytest.mark.parametrize(
("state", "runner_exit_code"),
[
("incomplete", 2),
("incomplete", 127),
("interrupted", 143),
],
)
def test_noncompleted_diagnostics_fail_ci(state, runner_exit_code):
result = classify_ci_result(
{"state": state, "exit_code": runner_exit_code},
runner_exit_code,
)

assert result.annotation == "error"
assert result.exit_code == runner_exit_code


@pytest.mark.parametrize(
("metadata", "runner_exit_code", "message"),
[
({"state": "passed", "exit_code": 1}, 1, "passed state"),
({"state": "failed", "exit_code": 2}, 2, "failed state"),
({"state": "incomplete", "exit_code": 0}, 0, "nonzero exit code"),
({"state": "running", "exit_code": 0}, 0, "unknown terminal state"),
({"state": "passed", "exit_code": 0}, 1, "does not match"),
({"state": "passed", "exit_code": None}, 0, "not an integer"),
],
)
def test_invalid_result_contract_fails_ci(metadata, runner_exit_code, message):
result = classify_ci_result(metadata, runner_exit_code)

assert result.annotation == "error"
assert result.exit_code == 2
assert message in result.message


def test_cli_emits_warning_for_completed_behavioral_findings(tmp_path, capsys):
run_json = tmp_path / "run.json"
run_json.write_text(
json.dumps({"state": "failed", "exit_code": 1}),
encoding="utf-8",
)

exit_code = main(
[
"--run-json",
str(run_json),
"--runner-exit-code",
"1",
]
)

assert exit_code == 0
assert capsys.readouterr().out.startswith("::warning::")


def test_cli_fails_when_run_metadata_is_missing(tmp_path, capsys):
run_json = tmp_path / "missing-run.json"

exit_code = main(
[
"--run-json",
str(run_json),
"--runner-exit-code",
"2",
]
)

assert exit_code == 2
output = capsys.readouterr().out
assert output.startswith("::error::")
assert "cannot read" in output


@pytest.mark.parametrize(
("contents", "message"),
[
("{", "cannot read"),
("[]", "root is not an object"),
],
)
def test_cli_fails_for_unusable_run_metadata(tmp_path, capsys, contents, message):
run_json = tmp_path / "run.json"
run_json.write_text(contents, encoding="utf-8")

exit_code = main(
[
"--run-json",
str(run_json),
"--runner-exit-code",
"1",
]
)

assert exit_code == 2
output = capsys.readouterr().out
assert output.startswith("::error::")
assert message in output
Loading
Loading