diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c593a2a..92b1d94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v7 - run: uv sync + - run: uv run python scripts/check_ci_required.py - run: uv run ruff check src/ tests/ - run: uv run ruff format --check src/ tests/ @@ -164,3 +165,39 @@ jobs: tests/test_deps_store.py \ tests/test_redis_tools.py \ tests/test_subprocess_namespace_injection.py + + ci-required: + name: ci-required + if: ${{ always() }} + needs: + - lint + - test-core + - test-subprocess-venv + - test-deno-sandbox + - test-docker-backend-user-journeys + - test-docker-storage-and-mcp + - test-docker-session-and-redis + runs-on: ubuntu-latest + steps: + - name: Fail if any required CI job failed + if: | + needs.lint.result != 'success' || + needs.test-core.result != 'success' || + needs.test-subprocess-venv.result != 'success' || + needs.test-deno-sandbox.result != 'success' || + needs.test-docker-backend-user-journeys.result != 'success' || + needs.test-docker-storage-and-mcp.result != 'success' || + needs.test-docker-session-and-redis.result != 'success' + run: | + echo "At least one required CI job did not succeed." + echo "lint=${{ needs.lint.result }}" + echo "test-core=${{ needs.test-core.result }}" + echo "test-subprocess-venv=${{ needs.test-subprocess-venv.result }}" + echo "test-deno-sandbox=${{ needs.test-deno-sandbox.result }}" + echo "test-docker-backend-user-journeys=${{ needs.test-docker-backend-user-journeys.result }}" + echo "test-docker-storage-and-mcp=${{ needs.test-docker-storage-and-mcp.result }}" + echo "test-docker-session-and-redis=${{ needs.test-docker-session-and-redis.result }}" + exit 1 + + - name: Confirm all required CI jobs passed + run: echo "All required CI jobs passed." diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml new file mode 100644 index 0000000..db89f2a --- /dev/null +++ b/.github/workflows/dependabot-automerge.yml @@ -0,0 +1,38 @@ +name: Dependabot Auto-Merge + +on: + pull_request: + branches: + - main + types: + - opened + - reopened + - synchronize + +permissions: + contents: write + pull-requests: write + +jobs: + enable-automerge: + name: Enable auto-merge for safe Dependabot bumps + if: | + github.event.pull_request.user.login == 'dependabot[bot]' && + github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - name: Fetch Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@d7267f607e9d3fb96fc2fbe83e0af444713e90b7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable auto-merge for patch updates + if: | + contains(fromJSON('["pip","uv","github-actions"]'), steps.metadata.outputs.package-ecosystem) && + steps.metadata.outputs.update-type == 'version-update:semver-patch' && + steps.metadata.outputs.maintainer-changes != 'true' + run: gh pr merge --auto --merge "$PR_URL" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_URL: ${{ github.event.pull_request.html_url }} diff --git a/scripts/check_ci_required.py b/scripts/check_ci_required.py new file mode 100644 index 0000000..26f4e15 --- /dev/null +++ b/scripts/check_ci_required.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from pathlib import Path + +import yaml + +WORKFLOW_PATH = Path(".github/workflows/ci.yml") +GATE_JOB = "ci-required" +EXCLUDED_JOBS = {GATE_JOB} + + +def main() -> int: + workflow = yaml.safe_load(WORKFLOW_PATH.read_text()) + jobs = workflow.get("jobs", {}) + + if GATE_JOB not in jobs: + print(f"Missing aggregate gate job: {GATE_JOB}") + return 1 + + real_jobs = {job_id for job_id in jobs if job_id not in EXCLUDED_JOBS} + + gate_needs = jobs[GATE_JOB].get("needs", []) + if isinstance(gate_needs, str): + gate_needs = [gate_needs] + + gate_needs_set = set(gate_needs) + missing = sorted(real_jobs - gate_needs_set) + extra = sorted(gate_needs_set - real_jobs) + + if missing: + print(f"{GATE_JOB} is missing jobs: {', '.join(missing)}") + if extra: + print(f"{GATE_JOB} has stale jobs: {', '.join(extra)}") + + if missing or extra: + return 1 + + print(f"{GATE_JOB} covers all top-level CI jobs.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())