ci-esta 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ci_esta-0.1.0/.gitattributes +1 -0
- ci_esta-0.1.0/.github/actions/README.md +339 -0
- ci_esta-0.1.0/.github/actions/cancel-obsolete-pr-runs/action.yml +189 -0
- ci_esta-0.1.0/.github/actions/ci-observability/action.yml +174 -0
- ci_esta-0.1.0/.github/actions/ci-observability/ci-observability-v1.schema.json +1685 -0
- ci_esta-0.1.0/.github/actions/ci-observability-enrichment/action.yml +185 -0
- ci_esta-0.1.0/.github/actions/collect-ci-changes/action.yml +141 -0
- ci_esta-0.1.0/.github/actions/collect-pr-changes/action.yml +39 -0
- ci_esta-0.1.0/.github/actions/collect-pytest-changes/action.yml +25 -0
- ci_esta-0.1.0/.github/actions/coverage-policy/action.yml +29 -0
- ci_esta-0.1.0/.github/actions/current-pr-head/action.yml +38 -0
- ci_esta-0.1.0/.github/actions/learn-test-timings/action.yml +32 -0
- ci_esta-0.1.0/.github/actions/lint-ci-code/action.yml +173 -0
- ci_esta-0.1.0/.github/actions/lint-ci-code/filter_changed_diagnostics.py +78 -0
- ci_esta-0.1.0/.github/actions/lint-ci-code/validate_actions.py +70 -0
- ci_esta-0.1.0/.github/actions/native-windows-git/action.yml +10 -0
- ci_esta-0.1.0/.github/actions/native-windows-git/use_native_windows_git.ps1 +27 -0
- ci_esta-0.1.0/.github/actions/plan-pytest-shards/action.yml +40 -0
- ci_esta-0.1.0/.github/actions/plan-runner-pools/action.yml +34 -0
- ci_esta-0.1.0/.github/actions/publish-prometheus-snapshot/action.yml +54 -0
- ci_esta-0.1.0/.github/actions/publish-prometheus-snapshot/publish.py +153 -0
- ci_esta-0.1.0/.github/actions/pytest-canary-pr-state/action.yml +127 -0
- ci_esta-0.1.0/.github/actions/pytest-expansion/action.yml +29 -0
- ci_esta-0.1.0/.github/actions/pytest-failure-canary/README.md +13 -0
- ci_esta-0.1.0/.github/actions/pytest-failure-canary/action.yml +104 -0
- ci_esta-0.1.0/.github/actions/pytest-timings/action.yml +64 -0
- ci_esta-0.1.0/.github/actions/resolve-incremental-pr-base/action.yml +84 -0
- ci_esta-0.1.0/.github/actions/resolve-incremental-pr-base/resolve.cjs +475 -0
- ci_esta-0.1.0/.github/actions/resolve-lane-evidence/action.yml +85 -0
- ci_esta-0.1.0/.github/actions/resolve-lane-evidence/inputs.cjs +163 -0
- ci_esta-0.1.0/.github/actions/resolve-pytest-evidence/action.yml +61 -0
- ci_esta-0.1.0/.github/actions/resolve-security-trigger/action.yml +393 -0
- ci_esta-0.1.0/.github/actions/resolve-security-trigger/inventory.cjs +28 -0
- ci_esta-0.1.0/.github/actions/resolve-security-trigger/scope.cjs +40 -0
- ci_esta-0.1.0/.github/actions/reuse-ecr-image/action.yml +38 -0
- ci_esta-0.1.0/.github/actions/reuse-ecr-image/reuse_ecr_image.sh +157 -0
- ci_esta-0.1.0/.github/actions/semgrep-scan/action.yml +163 -0
- ci_esta-0.1.0/.github/actions/semgrep-scan/setup.sh +15 -0
- ci_esta-0.1.0/.github/actions/semgrep-scan-ci-code/action.yml +45 -0
- ci_esta-0.1.0/.github/actions/setup-cached-uv/action.yml +60 -0
- ci_esta-0.1.0/.github/actions/setup-python-uv/action.yml +24 -0
- ci_esta-0.1.0/.github/actions/test-impact/action.yml +105 -0
- ci_esta-0.1.0/.github/actions/trivy-scan-ci-code/action.yml +45 -0
- ci_esta-0.1.0/.github/actions/trivy-scan-image/action.yml +84 -0
- ci_esta-0.1.0/.github/actions/typecheck-scope/action.yml +80 -0
- ci_esta-0.1.0/.github/actions/typecheck-scope/proof.cjs +19 -0
- ci_esta-0.1.0/.github/actions/typecheck-scope/scope.sh +44 -0
- ci_esta-0.1.0/.github/actions/validation-gate/action.yml +37 -0
- ci_esta-0.1.0/.github/actions/validation-gate/gate.cjs +33 -0
- ci_esta-0.1.0/.github/actions/verify-pytest-completion/action.yml +20 -0
- ci_esta-0.1.0/.github/test-lanes.json +36 -0
- ci_esta-0.1.0/.github/workflows/ci.yml +145 -0
- ci_esta-0.1.0/.github/workflows/observe-ci.yml +24 -0
- ci_esta-0.1.0/.github/workflows/release.yml +67 -0
- ci_esta-0.1.0/.github/workflows/report.yml +95 -0
- ci_esta-0.1.0/.gitignore +12 -0
- ci_esta-0.1.0/CHANGELOG.md +11 -0
- ci_esta-0.1.0/CONTRIBUTING.md +16 -0
- ci_esta-0.1.0/LICENSE +202 -0
- ci_esta-0.1.0/PKG-INFO +251 -0
- ci_esta-0.1.0/PROVENANCE.md +64 -0
- ci_esta-0.1.0/README.md +223 -0
- ci_esta-0.1.0/RELEASING.md +23 -0
- ci_esta-0.1.0/docs/adoption-audit.md +144 -0
- ci_esta-0.1.0/docs/agents.md +67 -0
- ci_esta-0.1.0/docs/alternatives.md +15 -0
- ci_esta-0.1.0/docs/assets/ci-esta-badge-logo.svg +1 -0
- ci_esta-0.1.0/docs/assets/ci-esta-mark.svg +15 -0
- ci_esta-0.1.0/docs/assets/social-preview.png +0 -0
- ci_esta-0.1.0/docs/assets/social-preview.svg +28 -0
- ci_esta-0.1.0/docs/assets/sticky-banner-dark.svg +34 -0
- ci_esta-0.1.0/docs/assets/sticky-banner-light.svg +34 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-22-selective-report.json +567 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-22-selective-report.md +56 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-22.md +71 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-critical-execution/due-39867723644.json +2786 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-critical-execution/due-39868243678.json +2786 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-critical-execution/due-test-inventory.json +1 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-critical-execution/obligation-39866009916.json +6206 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-critical-execution/obligation-39869242309.json +6226 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-critical-execution/obligation-test-inventory.json +1 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-critical-execution/selective-39867390350.json +2901 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-critical-execution/selective-39871515005.json +2901 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-critical-execution.md +100 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-critical-path.md +114 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-toolkit/cache-ci-esta-39863488268.json +38 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-toolkit/cache-ci-esta-39864390246.json +38 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-toolkit/cache-due-39864032259.json +47 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-toolkit/cache-due-39864404935.json +47 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-toolkit/cache-pytest-obligation-39863872668.json +128 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-toolkit/cache-pytest-obligation-39864399959.json +128 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-toolkit/cache-pytest-selective-39863865180.json +92 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-toolkit/cache-pytest-selective-39864395427.json +92 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-toolkit/ci-esta-39862792267.json +885 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-toolkit/ci-esta-39863205566.json +1251 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-toolkit/due-39862718626.json +2606 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-toolkit/observer-ci-esta.json +10 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-toolkit/observer-due.json +10 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-toolkit/observer-pytest-obligation.json +10 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-toolkit/observer-pytest-selective.json +10 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-toolkit/pytest-obligation-39862718305.json +6126 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-toolkit/pytest-selective-39862718435.json +2661 -0
- ci_esta-0.1.0/docs/benchmarks/2026-10-23-toolkit.md +140 -0
- ci_esta-0.1.0/docs/ci-optimization.md +139 -0
- ci_esta-0.1.0/docs/critical-path-optimizations.md +51 -0
- ci_esta-0.1.0/docs/critical-path.md +77 -0
- ci_esta-0.1.0/docs/diagnostics-runner-test-plan.md +14 -0
- ci_esta-0.1.0/docs/environment-cache.md +45 -0
- ci_esta-0.1.0/docs/evidence-reuse.md +69 -0
- ci_esta-0.1.0/docs/github-actions-observability.md +158 -0
- ci_esta-0.1.0/docs/integrations.md +69 -0
- ci_esta-0.1.0/docs/learned-test-timings.md +83 -0
- ci_esta-0.1.0/docs/migration-inventory.json +533 -0
- ci_esta-0.1.0/docs/migration-test-plan.md +38 -0
- ci_esta-0.1.0/docs/python-ci-tuning.md +48 -0
- ci_esta-0.1.0/docs/runners-and-diagnostics.md +115 -0
- ci_esta-0.1.0/docs/selected-gates.md +47 -0
- ci_esta-0.1.0/docs/upstream-closure.md +28 -0
- ci_esta-0.1.0/docs/upstreaming.md +23 -0
- ci_esta-0.1.0/docs/validation.md +34 -0
- ci_esta-0.1.0/examples/gigaverse-ci-drill.yaml +63 -0
- ci_esta-0.1.0/importlinter.toml +15 -0
- ci_esta-0.1.0/pyproject.toml +60 -0
- ci_esta-0.1.0/scripts/.gitkeep +0 -0
- ci_esta-0.1.0/scripts/__init__.py +0 -0
- ci_esta-0.1.0/scripts/ci/__init__.py +0 -0
- ci_esta-0.1.0/scripts/ci/coverage-policy.cjs +30 -0
- ci_esta-0.1.0/scripts/ci/environment_cache.py +78 -0
- ci_esta-0.1.0/scripts/ci/github_script_surface.py +10 -0
- ci_esta-0.1.0/scripts/ci/pytest_selective_requirement.py +13 -0
- ci_esta-0.1.0/scripts/render_sticky_banners.py +81 -0
- ci_esta-0.1.0/src/ci_esta/__init__.py +2 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/__init__.py +16 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/action.py +422 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/brand.py +68 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/critical_path.py +115 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/enrichment/__init__.py +28 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/enrichment/__main__.py +8 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/enrichment/ai.py +235 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/enrichment/backend/__init__.py +7 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/enrichment/backend/artifacts.py +179 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/enrichment/backend/github.py +173 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/enrichment/backend/legacy.py +149 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/enrichment/backend/models.py +109 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/enrichment/cli.py +197 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/enrichment/models.py +13 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/enrichment/primitives.py +51 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/enrichment/profiles.py +29 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/enrichment/prometheus.py +55 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/github.py +305 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/lanes.py +187 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/markdown.py +46 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/models.py +429 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/report.py +847 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/ruff.toml +5 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/selection.py +501 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/timeline.py +223 -0
- ci_esta-0.1.0/src/ci_esta/ci_observability/workflow_graph.py +31 -0
- ci_esta-0.1.0/src/ci_esta/cli.py +65 -0
- ci_esta-0.1.0/src/ci_esta/diagnostics/__init__.py +17 -0
- ci_esta-0.1.0/src/ci_esta/diagnostics/__main__.py +3 -0
- ci_esta-0.1.0/src/ci_esta/diagnostics/resources.sh +86 -0
- ci_esta-0.1.0/src/ci_esta/drill.py +1401 -0
- ci_esta-0.1.0/src/ci_esta/github_script_surface.py +65 -0
- ci_esta-0.1.0/src/ci_esta/image_evidence/__init__.py +93 -0
- ci_esta-0.1.0/src/ci_esta/image_evidence/cli.py +74 -0
- ci_esta-0.1.0/src/ci_esta/image_evidence/nodes.py +302 -0
- ci_esta-0.1.0/src/ci_esta/image_evidence/observer.py +38 -0
- ci_esta-0.1.0/src/ci_esta/image_evidence/plan.py +91 -0
- ci_esta-0.1.0/src/ci_esta/image_evidence/pulls.sh +78 -0
- ci_esta-0.1.0/src/ci_esta/image_evidence/runtime.py +66 -0
- ci_esta-0.1.0/src/ci_esta/py.typed +0 -0
- ci_esta-0.1.0/src/ci_esta/resources.py +17 -0
- ci_esta-0.1.0/src/ci_esta/testing/__init__.py +3 -0
- ci_esta-0.1.0/src/ci_esta/testing/action.py +139 -0
- ci_esta-0.1.0/src/ci_esta/testing/artifacts.py +24 -0
- ci_esta-0.1.0/src/ci_esta/testing/canary_action.py +123 -0
- ci_esta-0.1.0/src/ci_esta/testing/canary_run.py +290 -0
- ci_esta-0.1.0/src/ci_esta/testing/cli.py +220 -0
- ci_esta-0.1.0/src/ci_esta/testing/coverage.py +50 -0
- ci_esta-0.1.0/src/ci_esta/testing/evidence.py +27 -0
- ci_esta-0.1.0/src/ci_esta/testing/evidence_scripts/collect.sh +61 -0
- ci_esta-0.1.0/src/ci_esta/testing/evidence_scripts/verify.sh +25 -0
- ci_esta-0.1.0/src/ci_esta/testing/gha_expressions.py +222 -0
- ci_esta-0.1.0/src/ci_esta/testing/hygiene.py +22 -0
- ci_esta-0.1.0/src/ci_esta/testing/import_boundaries.py +491 -0
- ci_esta-0.1.0/src/ci_esta/testing/import_boundary_config.py +143 -0
- ci_esta-0.1.0/src/ci_esta/testing/junit.py +333 -0
- ci_esta-0.1.0/src/ci_esta/testing/local_shards.py +96 -0
- ci_esta-0.1.0/src/ci_esta/testing/postgres.py +46 -0
- ci_esta-0.1.0/src/ci_esta/testing/pytest_completion.py +51 -0
- ci_esta-0.1.0/src/ci_esta/testing/pytest_expansion.py +48 -0
- ci_esta-0.1.0/src/ci_esta/testing/pytest_watchdog.py +120 -0
- ci_esta-0.1.0/src/ci_esta/testing/runner_pools.py +43 -0
- ci_esta-0.1.0/src/ci_esta/testing/timing_feedback.py +28 -0
- ci_esta-0.1.0/src/ci_esta/testing/workflow_adoption.py +55 -0
- ci_esta-0.1.0/src/ci_esta/testing/workflow_policy.py +370 -0
- ci_esta-0.1.0/tests/__init__.py +0 -0
- ci_esta-0.1.0/tests/ci/ci_drill_probe_4957.json +167 -0
- ci_esta-0.1.0/tests/ci/github_rest_fake.cjs +55 -0
- ci_esta-0.1.0/tests/ci/github_script_rest_surface.json +1108 -0
- ci_esta-0.1.0/tests/ci/image_evidence/test_nodes.py +226 -0
- ci_esta-0.1.0/tests/ci/image_evidence/test_pull_driver.py +87 -0
- ci_esta-0.1.0/tests/ci/image_evidence/test_pull_plan.py +84 -0
- ci_esta-0.1.0/tests/ci/image_evidence/test_runtime.py +108 -0
- ci_esta-0.1.0/tests/ci/test_ci_drill.py +646 -0
- ci_esta-0.1.0/tests/ci/test_coverage_policy.py +57 -0
- ci_esta-0.1.0/tests/ci/test_drill_slots.py +634 -0
- ci_esta-0.1.0/tests/ci/test_environment_cache.py +59 -0
- ci_esta-0.1.0/tests/ci/test_execution_context.py +92 -0
- ci_esta-0.1.0/tests/ci/test_github_rest_surface.py +159 -0
- ci_esta-0.1.0/tests/ci/test_image_evidence.py +126 -0
- ci_esta-0.1.0/tests/ci/test_incremental_evidence.py +1326 -0
- ci_esta-0.1.0/tests/ci/test_inventory_evidence.py +90 -0
- ci_esta-0.1.0/tests/ci/test_lane_receipt.py +363 -0
- ci_esta-0.1.0/tests/ci/test_portable_receipts.py +67 -0
- ci_esta-0.1.0/tests/ci/test_pr_change_evidence.py +57 -0
- ci_esta-0.1.0/tests/ci/test_repository_routing.py +30 -0
- ci_esta-0.1.0/tests/ci/test_security_change_scope.py +95 -0
- ci_esta-0.1.0/tests/ci/test_semgrep_runner.py +187 -0
- ci_esta-0.1.0/tests/ci/test_typecheck_scope_action.py +148 -0
- ci_esta-0.1.0/tests/ci/test_validation_gate.py +61 -0
- ci_esta-0.1.0/tests/ci_observability/__init__.py +3 -0
- ci_esta-0.1.0/tests/ci_observability/builders.py +79 -0
- ci_esta-0.1.0/tests/ci_observability/test_action.py +723 -0
- ci_esta-0.1.0/tests/ci_observability/test_action_metadata.py +142 -0
- ci_esta-0.1.0/tests/ci_observability/test_brand.py +20 -0
- ci_esta-0.1.0/tests/ci_observability/test_critical_path.py +83 -0
- ci_esta-0.1.0/tests/ci_observability/test_enrichment_ai.py +147 -0
- ci_esta-0.1.0/tests/ci_observability/test_enrichment_backend.py +229 -0
- ci_esta-0.1.0/tests/ci_observability/test_enrichment_cli.py +219 -0
- ci_esta-0.1.0/tests/ci_observability/test_enrichment_profiles.py +40 -0
- ci_esta-0.1.0/tests/ci_observability/test_generic_profile.py +11 -0
- ci_esta-0.1.0/tests/ci_observability/test_github.py +435 -0
- ci_esta-0.1.0/tests/ci_observability/test_lanes.py +261 -0
- ci_esta-0.1.0/tests/ci_observability/test_report.py +827 -0
- ci_esta-0.1.0/tests/ci_observability/test_selection.py +543 -0
- ci_esta-0.1.0/tests/ci_observability/test_step_timings.py +123 -0
- ci_esta-0.1.0/tests/ci_observability/test_timeline.py +139 -0
- ci_esta-0.1.0/tests/ci_observability/test_workflow_graph.py +19 -0
- ci_esta-0.1.0/tests/github_actions/test_cancel_obsolete_pr_runs.py +66 -0
- ci_esta-0.1.0/tests/github_actions/test_immutable_actions.py +22 -0
- ci_esta-0.1.0/tests/github_actions/test_publish_prometheus_snapshot_action.py +331 -0
- ci_esta-0.1.0/tests/github_actions/test_reuse_ecr_image_action.py +219 -0
- ci_esta-0.1.0/tests/github_actions/test_selective_delegation.py +24 -0
- ci_esta-0.1.0/tests/github_actions/test_trivy_scan_image_action.py +49 -0
- ci_esta-0.1.0/tests/github_actions/test_workflow_primitives.py +53 -0
- ci_esta-0.1.0/tests/test_cli.py +30 -0
- ci_esta-0.1.0/tests/testing/__init__.py +0 -0
- ci_esta-0.1.0/tests/testing/git_repo.py +36 -0
- ci_esta-0.1.0/tests/testing/test_architecture.py +26 -0
- ci_esta-0.1.0/tests/testing/test_canary_run.py +382 -0
- ci_esta-0.1.0/tests/testing/test_canary_toolchain.py +31 -0
- ci_esta-0.1.0/tests/testing/test_cli.py +152 -0
- ci_esta-0.1.0/tests/testing/test_coverage.py +38 -0
- ci_esta-0.1.0/tests/testing/test_diagnostics.py +142 -0
- ci_esta-0.1.0/tests/testing/test_gha_expressions.py +147 -0
- ci_esta-0.1.0/tests/testing/test_import_boundaries.py +367 -0
- ci_esta-0.1.0/tests/testing/test_import_boundary_config.py +103 -0
- ci_esta-0.1.0/tests/testing/test_junit.py +261 -0
- ci_esta-0.1.0/tests/testing/test_learn_timings.py +94 -0
- ci_esta-0.1.0/tests/testing/test_local_shards.py +180 -0
- ci_esta-0.1.0/tests/testing/test_postgres.py +48 -0
- ci_esta-0.1.0/tests/testing/test_pytest_canary_action.py +226 -0
- ci_esta-0.1.0/tests/testing/test_pytest_canary_pr_state_action.py +28 -0
- ci_esta-0.1.0/tests/testing/test_pytest_completion.py +109 -0
- ci_esta-0.1.0/tests/testing/test_pytest_evidence.py +301 -0
- ci_esta-0.1.0/tests/testing/test_pytest_timings_action.py +81 -0
- ci_esta-0.1.0/tests/testing/test_runner_pools.py +39 -0
- ci_esta-0.1.0/tests/testing/test_selective_ownership.py +10 -0
- ci_esta-0.1.0/tests/testing/test_workflow_adoption.py +30 -0
- ci_esta-0.1.0/tests/testing/test_workflow_policy.py +485 -0
- ci_esta-0.1.0/uv.lock +763 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*.sh text eol=lf
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# Shared CI building blocks
|
|
2
|
+
|
|
3
|
+
Reusable pieces so `gigaverse-ai` and `gigaverse-backend` share identical CI
|
|
4
|
+
mechanics without forcing repository-specific build and test layouts into one
|
|
5
|
+
workflow.
|
|
6
|
+
|
|
7
|
+
Use `gigaverse-app/ci-esta/.github/actions/<name>@<reviewed-commit>` for the
|
|
8
|
+
extracted actions. The backend-common reusable workflows listed below retain
|
|
9
|
+
Gigaverse event/security/release policy; they are external compositions, not
|
|
10
|
+
workflows shipped by CI-esta. Start with the [portable adoption guide](../../docs/ci-optimization.md)
|
|
11
|
+
and check the [revision-bound adoption audit](../../docs/adoption-audit.md).
|
|
12
|
+
|
|
13
|
+
Each piece is matched to the shape of the shared work:
|
|
14
|
+
|
|
15
|
+
| Piece | Kind | Reused by |
|
|
16
|
+
|-------|------|-----------|
|
|
17
|
+
| `collect-ci-changes` | composite action | workflow/action/shell change classification |
|
|
18
|
+
| `current-pr-head` | composite action | live stale-rerun admission |
|
|
19
|
+
| `resolve-security-trigger` | composite action | every security workflow, both repos |
|
|
20
|
+
| `resolve-incremental-pr-base` | composite action | automatic security and CI-code PR validation |
|
|
21
|
+
| `trivy-scan-image` | composite action | each repo's Trivy workflow |
|
|
22
|
+
| `backend-common/reusable-semgrep.yaml` | reusable workflow | each repo's Semgrep stub |
|
|
23
|
+
| `lint-ci-code` | composite action | `backend-common/reusable-ci-code-validation.yaml` |
|
|
24
|
+
| `semgrep-scan-ci-code` | composite action | `backend-common/reusable-ci-code-validation.yaml` |
|
|
25
|
+
| `trivy-scan-ci-code` | composite action | `backend-common/reusable-ci-code-validation.yaml` |
|
|
26
|
+
| `backend-common/reusable-ci-code-validation.yaml` | reusable workflow | path-filtered CI validation callers |
|
|
27
|
+
| `backend-common/reusable-current-pr-head.yaml` | reusable workflow | stale manual-rerun admission in PR validation |
|
|
28
|
+
| `cancel-obsolete-pr-runs` | composite action | post-admission stale-run cancellation from existing jobs |
|
|
29
|
+
| `setup-python-uv` | composite action | backend-common test and type-check workflows |
|
|
30
|
+
| `setup-cached-uv` | composite action | portable exact environment restore, repair and locked synchronization |
|
|
31
|
+
| `publish-prometheus-snapshot` | composite action | bounded CI/build metric producers |
|
|
32
|
+
| `ci-observability` | composite action | CI latency/cost sticky and evidence in all three repos |
|
|
33
|
+
| `reuse-ecr-image` | composite action | deployments that can reuse an exact verified image |
|
|
34
|
+
| `pytest-failure-canary` | composite action | bounded whole-file retries from prior JUnit failures |
|
|
35
|
+
| `pytest-canary-pr-state` | composite action | stale-run-safe pytest-red PR label transitions |
|
|
36
|
+
| `plan-pytest-shards` | composite action | exhaustive file partitioning with the shared duration planner |
|
|
37
|
+
| `resolve-lane-evidence` | composite action | passing-result reuse for whole trees or declared complete input paths |
|
|
38
|
+
|
|
39
|
+
For stale-rerun safety, first PR attempts use a PR-number concurrency group and
|
|
40
|
+
reruns use their unique run ID, so a rerun can never replace or cancel current
|
|
41
|
+
work. Call `backend-common/reusable-current-pr-head.yaml` before a rerun's expensive jobs. A
|
|
42
|
+
first-attempt job also calls `cancel-obsolete-pr-runs`: this cancels a rerun
|
|
43
|
+
that passed admission and then became stale, without adding a runner job. It
|
|
44
|
+
resolves the current commit's first parent through check runs, avoiding
|
|
45
|
+
GitHub's eventually-consistent workflow-run listing after a normal one-commit
|
|
46
|
+
agent push; the workflow index remains the fallback for other histories.
|
|
47
|
+
|
|
48
|
+
## `ci-observability`
|
|
49
|
+
|
|
50
|
+
Discovers jobs, commit statuses, and all rerun attempts through GitHub's
|
|
51
|
+
paginated API; combines them with explicit required-gate/direct-prerequisite
|
|
52
|
+
metadata and optional typed repository signals; and emits a versioned JSON
|
|
53
|
+
artifact, sticky/summary Markdown, and bounded Prometheus exposition. Workflow
|
|
54
|
+
job names, matrix size, and step ordering are never accounting keys.
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
- uses: gigaverse-app/ci-esta/.github/actions/ci-observability@v0.1
|
|
58
|
+
with:
|
|
59
|
+
github-token: ${{ github.token }}
|
|
60
|
+
commit-sha: ${{ github.event.pull_request.head.sha }}
|
|
61
|
+
pull-request-number: ${{ github.event.pull_request.number }}
|
|
62
|
+
required-gates-json: >-
|
|
63
|
+
[{"kind":"commit_status","context":"required-check"}]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Run it on an existing off-path host. See
|
|
67
|
+
[`docs/github-actions-observability.md`](../../docs/github-actions-observability.md) for the versioned
|
|
68
|
+
data, timing, cost, graph, metric, and failure contracts. When the run's
|
|
69
|
+
`test-impact` action uploaded a selection explanation, the sticky opens with a
|
|
70
|
+
test-selection section; consumers pass nothing extra for it.
|
|
71
|
+
|
|
72
|
+
## `reuse-ecr-image`
|
|
73
|
+
|
|
74
|
+
Promotes an immutable, previously verified image to the tag consumed by a
|
|
75
|
+
deployment by copying its ECR manifest inside the same repository. It never
|
|
76
|
+
pulls or rebuilds layers. The action emits `reused=false` on a missing image,
|
|
77
|
+
registry error, repository mismatch, target conflict, promotion failure, or
|
|
78
|
+
digest mismatch so the caller can continue through its ordinary build path.
|
|
79
|
+
|
|
80
|
+
The caller owns the artifact identity and verification policy. This action only
|
|
81
|
+
proves that the source and target ECR references resolve to the same digest.
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
- id: image-reuse
|
|
85
|
+
uses: gigaverse-app/ci-esta/.github/actions/reuse-ecr-image@v0.1
|
|
86
|
+
with:
|
|
87
|
+
source-image: ${{ env.REPOSITORY }}:verified-${{ steps.identity.outputs.key }}
|
|
88
|
+
target-image: ${{ env.REPOSITORY }}:${{ github.sha }}
|
|
89
|
+
|
|
90
|
+
- if: steps.image-reuse.outputs.reused != 'true'
|
|
91
|
+
run: ./build-and-push-image.sh
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## `publish-prometheus-snapshot`
|
|
95
|
+
|
|
96
|
+
Replaces one stable Pushgateway group from a Prometheus text exposition file.
|
|
97
|
+
The action always adds `GITHUB_REPOSITORY` to the grouping key, uses `PUT`, and
|
|
98
|
+
handles `/` in grouping values with Pushgateway's required URL-safe base64
|
|
99
|
+
encoding.
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
- uses: gigaverse-app/ci-esta/.github/actions/publish-prometheus-snapshot@v0.1
|
|
103
|
+
with:
|
|
104
|
+
metrics-file: build-metrics.prom
|
|
105
|
+
pushgateway-url: ${{ secrets.PUSHGW_URL }}
|
|
106
|
+
job: docker_build
|
|
107
|
+
grouping-labels: >-
|
|
108
|
+
{"env":"dev","service":"edge_service"}
|
|
109
|
+
username: ghci
|
|
110
|
+
password: ${{ secrets.PUSHGW_PASS }}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Grouping labels identify retained state, not individual events:
|
|
114
|
+
|
|
115
|
+
- Use bounded dimensions such as environment, service, or a persistent branch.
|
|
116
|
+
- Never use run IDs or commit SHAs; the action rejects those dimensions.
|
|
117
|
+
- For unbounded feature branches, publish only artifacts or add an explicit
|
|
118
|
+
lifecycle cleanup workflow.
|
|
119
|
+
- Use `failure-mode: fail` only when publishing telemetry must gate the caller.
|
|
120
|
+
The default `warn` mode reports an annotation without failing delivery work.
|
|
121
|
+
|
|
122
|
+
## `resolve-security-trigger`
|
|
123
|
+
|
|
124
|
+
Decides whether a security scan should run, from the same triggers the
|
|
125
|
+
non-default test framework uses (`docs/testing/expensive-test-policy.md` in the
|
|
126
|
+
consumers): PR `risk:security` label, a write+ collaborator
|
|
127
|
+
`/gv-test security` | `@gv-ci run security` comment, push, or manual dispatch.
|
|
128
|
+
Emits `should_run`, `ref`, `pr_number`, `mode`, and `same_repo`. For a shared
|
|
129
|
+
resolver, leave `require-same-repo` false and gate only secret-bearing consumers
|
|
130
|
+
on `same_repo`; source-only scans can still run on forks.
|
|
131
|
+
|
|
132
|
+
Consumers that produce reusable pre-merge artifacts can set
|
|
133
|
+
`automatic-same-repo-pull-requests: "true"`. That admits opened, reopened,
|
|
134
|
+
synchronized, and ready-for-review non-draft, non-bot PRs only when their head
|
|
135
|
+
branch belongs to the consumer repository. The default remains the opt-in
|
|
136
|
+
label/comment policy. Automatic PR scans use the newest successful ancestor
|
|
137
|
+
from this exact workflow and PR as their diff base, so later pushes scan only
|
|
138
|
+
work that has not already passed. The stable job name or reusable-workflow
|
|
139
|
+
suffix named by `automatic-pr-evidence-job-name` must also have succeeded; a
|
|
140
|
+
caller prefix such as `validate / ` does not change the evidence identity. A
|
|
141
|
+
green workflow whose scan jobs were skipped is not reusable evidence. A
|
|
142
|
+
cancelled workflow is reusable only when every job pattern declared by
|
|
143
|
+
`automatic-pr-cancelled-evidence-job-patterns` succeeded. Evidence expires
|
|
144
|
+
after seven days. A missing or failed run, rewritten branch, API failure, or
|
|
145
|
+
any other uncertainty falls back to the PR base and scans the whole PR diff.
|
|
146
|
+
Automatic callers grant the resolver job `actions: read`, `checks: read`, and
|
|
147
|
+
`contents: read`; cancellation in the same job may require `actions: write`
|
|
148
|
+
instead.
|
|
149
|
+
|
|
150
|
+
`dependency-paths` defines the package manifests, lockfiles, Dockerfiles, and
|
|
151
|
+
other inputs that determine an inventory scan. For automatic PRs and pushes,
|
|
152
|
+
`deps_changed=false` is emitted only after the compare API proves none of those
|
|
153
|
+
paths changed. Explicit label/comment/manual scans and scheduled scans always
|
|
154
|
+
emit `true`; comparison uncertainty also fails open to `true`.
|
|
155
|
+
|
|
156
|
+
For automatic PRs, `semgrep_changed=false` is emitted before scanner job
|
|
157
|
+
allocation only when the complete incremental diff contains no added, modified,
|
|
158
|
+
or renamed file outside `automatic-pr-ignored-paths`. The defaults contain the
|
|
159
|
+
two machine-owned bump provenance files; explicit and uncertain runs stay
|
|
160
|
+
fail-open.
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
- id: gate
|
|
164
|
+
uses: gigaverse-app/ci-esta/.github/actions/resolve-security-trigger@v0.1
|
|
165
|
+
with:
|
|
166
|
+
require-same-repo: "true" # set for image-building scans (Trivy); needs same-repo secrets
|
|
167
|
+
automatic-same-repo-pull-requests: "true" # optional; default false
|
|
168
|
+
dependency-paths: |
|
|
169
|
+
pyproject.toml
|
|
170
|
+
uv.lock
|
|
171
|
+
deployment/Dockerfile
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## `trivy-scan-image`
|
|
175
|
+
|
|
176
|
+
Runs `aquasecurity/trivy-action` against an already-materialized image or
|
|
177
|
+
filesystem package inventory with the shared severity policy (`CRITICAL,HIGH`,
|
|
178
|
+
`ignore-unfixed`, fail on findings). Inventory
|
|
179
|
+
creation is the consumer's job: use `image-ref` for an OS image, or let the
|
|
180
|
+
native package manager materialize an environment and use `scan-type: fs` plus
|
|
181
|
+
`scan-ref`. This is important for lock formats with environment markers: Trivy
|
|
182
|
+
matches installed package versions to advisories; it does not replace the
|
|
183
|
+
package manager's resolution semantics.
|
|
184
|
+
|
|
185
|
+
The action pins Trivy `v0.72.0` by default; consumers can use `trivy-version`
|
|
186
|
+
for a controlled compatibility override. `vuln-type` defaults to both OS and
|
|
187
|
+
language libraries, while `scanners` defaults to `vuln`: the shared Semgrep
|
|
188
|
+
gate owns source-code vulnerability and secret scanning once at repository
|
|
189
|
+
scope. A consumer without that gate must explicitly request the additional
|
|
190
|
+
scanner. Callers should similarly narrow `vuln-type` when they deliberately
|
|
191
|
+
split package inventories.
|
|
192
|
+
|
|
193
|
+
```yaml
|
|
194
|
+
- uses: gigaverse-app/ci-esta/.github/actions/trivy-scan-image@v0.1
|
|
195
|
+
with:
|
|
196
|
+
image-ref: "trivy-scan:my_service"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```yaml
|
|
200
|
+
- run: uv sync --frozen --all-groups --no-install-project
|
|
201
|
+
- uses: gigaverse-app/ci-esta/.github/actions/trivy-scan-image@v0.1
|
|
202
|
+
with:
|
|
203
|
+
# rootfs inventories installed .dist-info packages; fs is for manifests.
|
|
204
|
+
scan-type: rootfs
|
|
205
|
+
scan-ref: .venv
|
|
206
|
+
vuln-type: library
|
|
207
|
+
scanners: vuln
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Use `skip-dirs` only for a documented inventory boundary (for example, a
|
|
211
|
+
package manager's bundled implementation dependencies that the product never
|
|
212
|
+
executes), not to suppress an actionable vulnerable application package.
|
|
213
|
+
|
|
214
|
+
When two inventories run sequentially in one job, the first invocation should
|
|
215
|
+
use the defaults. On the second, set `skip-setup-trivy: "true"` and
|
|
216
|
+
`cache: "false"`: Trivy is already on `$PATH`, and bypassing a second cache
|
|
217
|
+
restore preserves the freshly downloaded database in the shared cache directory
|
|
218
|
+
instead of restoring stale data over it.
|
|
219
|
+
|
|
220
|
+
Poetry follows the same contract: Poetry evaluates its lock, groups, Python
|
|
221
|
+
version, and platform markers; Trivy evaluates the resulting installed package
|
|
222
|
+
metadata. Keep the virtual environment in-project so the scan target is explicit
|
|
223
|
+
and does not depend on Poetry's global cache layout.
|
|
224
|
+
|
|
225
|
+
```yaml
|
|
226
|
+
- run: poetry install --all-groups --no-root
|
|
227
|
+
env:
|
|
228
|
+
POETRY_VIRTUALENVS_IN_PROJECT: "true"
|
|
229
|
+
- uses: gigaverse-app/ci-esta/.github/actions/trivy-scan-image@v0.1
|
|
230
|
+
with:
|
|
231
|
+
scan-type: rootfs
|
|
232
|
+
scan-ref: .venv
|
|
233
|
+
vuln-type: library
|
|
234
|
+
scanners: vuln
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## `backend-common/reusable-semgrep.yaml`
|
|
238
|
+
|
|
239
|
+
Whole Semgrep job (runs in the `semgrep/semgrep` container, renders a job
|
|
240
|
+
summary, posts a PR comment). Calls `resolve-security-trigger` internally, so the
|
|
241
|
+
consumer stub only owns the `on:` triggers:
|
|
242
|
+
|
|
243
|
+
```yaml
|
|
244
|
+
name: Semgrep CE scan
|
|
245
|
+
on:
|
|
246
|
+
pull_request: { types: [labeled] }
|
|
247
|
+
issue_comment: { types: [created] }
|
|
248
|
+
push: { branches: [main] }
|
|
249
|
+
workflow_dispatch: {}
|
|
250
|
+
permissions: { contents: read, pull-requests: write }
|
|
251
|
+
jobs:
|
|
252
|
+
semgrep:
|
|
253
|
+
uses: gigaverse-app/backend-common/.github/workflows/reusable-semgrep.yaml@<pinned-sha>
|
|
254
|
+
secrets: inherit
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
When a consumer also runs Trivy (or another security scan), resolve once in the
|
|
258
|
+
consumer and pass `decision-precomputed: true` plus `should-run`, `ref`, and
|
|
259
|
+
`pr-number`. The internal resolver job then skips and both scans remain parallel.
|
|
260
|
+
|
|
261
|
+
The scan preserves `always()` so a precomputed decision can bypass that skipped
|
|
262
|
+
resolver, but explicitly excludes cancelled callers. Consumer workflow
|
|
263
|
+
concurrency can therefore stop obsolete Semgrep containers instead of making a
|
|
264
|
+
new PR head wait for the old scan to finish.
|
|
265
|
+
|
|
266
|
+
Override the rule set per repo via the `configs` / `exclude` / `exclude-rules`
|
|
267
|
+
inputs; defaults match the gigaverse-ai baseline. Prefer
|
|
268
|
+
`extra-exclude-rules` when a consumer only needs to append a repo-specific
|
|
269
|
+
exception while continuing to inherit the shared exclusions.
|
|
270
|
+
|
|
271
|
+
## Why not also a reusable Trivy workflow?
|
|
272
|
+
|
|
273
|
+
The Semgrep scan is identical everywhere, so it ships as one workflow. Trivy's
|
|
274
|
+
**build** is not: `gigaverse-ai` builds N images from one parametrized
|
|
275
|
+
`deployment/Dockerfile` driven by a `.github/services/*.yaml` registry, while
|
|
276
|
+
`gigaverse-backend` builds a single image from `Dockerfile-backend`. Forcing both
|
|
277
|
+
into one workflow means conditional build inputs that leak each repo's layout.
|
|
278
|
+
Instead each repo keeps its own build/matrix and shares the two things that are
|
|
279
|
+
actually common: the trigger gate and the scan policy.
|
|
280
|
+
|
|
281
|
+
> This is backend-common's consumer policy, not CI-esta's versioning scheme.
|
|
282
|
+
> Consumers pin `PR<N>` while testing an open backend-common PR and immutable
|
|
283
|
+
> `v<N>` after it merges. The downstream bump workflow generates every
|
|
284
|
+
> backend-common reference from that one `pyproject.toml` revision (see
|
|
285
|
+
> `update_ci_esta.yaml`, `sync_backend_common_status.yaml`).
|
|
286
|
+
|
|
287
|
+
## `backend-common/reusable-ci-code-validation.yaml`
|
|
288
|
+
|
|
289
|
+
Runs Actionlint, composite-action metadata validation, ShellCheck, targeted
|
|
290
|
+
Semgrep, and Trivy configuration/secret scanning against changed files under
|
|
291
|
+
caller-provided CI roots. The caller owns only event path filters and the
|
|
292
|
+
newline-delimited roots:
|
|
293
|
+
|
|
294
|
+
```yaml
|
|
295
|
+
jobs:
|
|
296
|
+
validate:
|
|
297
|
+
uses: gigaverse-app/backend-common/.github/workflows/reusable-ci-code-validation.yaml@<consumer-ref>
|
|
298
|
+
with:
|
|
299
|
+
ci-paths: |
|
|
300
|
+
.github
|
|
301
|
+
scripts/ci
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
The planning job validates roots, ignores deleted files, and passes the exact
|
|
305
|
+
added/modified file set to every checker. On PRs it starts at the newest fresh,
|
|
306
|
+
successful ancestor from the same workflow and same PR whose named evidence job
|
|
307
|
+
actually succeeded; failed, cancelled, skipped, stale, unrelated, or
|
|
308
|
+
non-ancestor runs are never reused. GitHub's PR `paths`
|
|
309
|
+
filter is cumulative, so the planner may still allocate after a prior CI change,
|
|
310
|
+
but an empty new delta skips the Semgrep container plus the lint and Trivy
|
|
311
|
+
steps. No secrets are accepted or inherited.
|
|
312
|
+
|
|
313
|
+
Backend-common's own `ci-code-validation.yaml` also invokes each composite action
|
|
314
|
+
through a relative `./.github/actions/...` reference. This is intentional:
|
|
315
|
+
consumer-facing calls stay pinned to known commits, while pull requests in this
|
|
316
|
+
repository exercise the implementation in the current checkout. Its reusable
|
|
317
|
+
call sets `run-checks: false`, reusing only the shared change plan so the same
|
|
318
|
+
files are not scanned once by pinned actions and again by local actions.
|
|
319
|
+
|
|
320
|
+
## `setup-python-uv`
|
|
321
|
+
|
|
322
|
+
Centralizes backend-common's Python version, uv installation, dependency cache,
|
|
323
|
+
and development dependency installation. It syncs with `--locked`, so a
|
|
324
|
+
`pyproject.toml` edit that is not accompanied by a refreshed `uv.lock` fails the
|
|
325
|
+
build instead of silently resolving something new. It is local project
|
|
326
|
+
setup rather than a consumer-facing contract. The repository's test workflow is
|
|
327
|
+
also intentionally local. Push and pull-request runs are allowed to overlap:
|
|
328
|
+
guaranteed pytest execution is more important than deduplicating CI events.
|
|
329
|
+
|
|
330
|
+
This extracted action caches uv downloads and runs `uv sync --locked --all-groups`.
|
|
331
|
+
It does **not** implement Backend's separate `.venv` restore/validation/save
|
|
332
|
+
composite. Review the [cache boundaries](../../docs/ci-optimization.md#cache-the-right-thing)
|
|
333
|
+
before treating these similarly named actions as interchangeable.
|
|
334
|
+
|
|
335
|
+
### Distributed pytest ranges
|
|
336
|
+
|
|
337
|
+
`ci-esta-pytest run-local --shards N --first-shard A --last-shard B -- ...` runs the inclusive range A–B from one global exact-file plan. Workers retain global `GV_PYTEST_SHARD_INDEX` and `GV_PYTEST_SHARD_COUNT`; logs and completion reports therefore remain disjoint across machines. Omitted bounds preserve the existing all-shards behavior. Invalid or empty ranges fail before starting a worker.
|
|
338
|
+
|
|
339
|
+
Pass the same bounds as `PLAN_FIRST_SHARD` and `PLAN_LAST_SHARD` to `verify-pytest-completion`. Each pool proves its entire range. The consumer must require every pool, verify that its ranges partition 1–N, and issue the reusable aggregate receipt only after all pools prove completion. A successful pool alone never authorizes reuse of the global plan.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
name: Cancel obsolete PR workflow runs
|
|
2
|
+
description: >
|
|
3
|
+
Cancel active runs of this same workflow and PR when their head SHA is no
|
|
4
|
+
longer current. Call from an existing first-attempt job so the cancellation
|
|
5
|
+
closes the post-admission race without creating another billed runner job.
|
|
6
|
+
|
|
7
|
+
inputs:
|
|
8
|
+
pr-number:
|
|
9
|
+
description: Pull request whose current head is authoritative.
|
|
10
|
+
required: true
|
|
11
|
+
current-head-sha:
|
|
12
|
+
description: Current pull-request head SHA.
|
|
13
|
+
required: true
|
|
14
|
+
head-ref:
|
|
15
|
+
description: Same-repository pull-request head branch.
|
|
16
|
+
required: true
|
|
17
|
+
github-token:
|
|
18
|
+
description: Token with actions:write, checks:read and contents:read permissions in the calling job.
|
|
19
|
+
required: false
|
|
20
|
+
default: ${{ github.token }}
|
|
21
|
+
|
|
22
|
+
runs:
|
|
23
|
+
using: composite
|
|
24
|
+
steps:
|
|
25
|
+
- name: Cancel obsolete active runs
|
|
26
|
+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
|
27
|
+
env:
|
|
28
|
+
PR_NUMBER: ${{ inputs.pr-number }}
|
|
29
|
+
CURRENT_HEAD_SHA: ${{ inputs.current-head-sha }}
|
|
30
|
+
HEAD_REF: ${{ inputs.head-ref }}
|
|
31
|
+
with:
|
|
32
|
+
github-token: ${{ inputs.github-token }}
|
|
33
|
+
script: |
|
|
34
|
+
const activeStatuses = new Set(["queued", "in_progress", "pending", "waiting"]);
|
|
35
|
+
const prNumber = Number(process.env.PR_NUMBER);
|
|
36
|
+
const currentHeadSha = process.env.CURRENT_HEAD_SHA;
|
|
37
|
+
const headRef = process.env.HEAD_REF;
|
|
38
|
+
const repoFullName = `${context.repo.owner}/${context.repo.repo}`;
|
|
39
|
+
|
|
40
|
+
// Inputs describe the triggering event, which can be stale by the
|
|
41
|
+
// time a queued matrix job starts. Branch reads use contents:read,
|
|
42
|
+
// already required for parent discovery; no new PR permission is needed.
|
|
43
|
+
const callerIsCurrent = async () => {
|
|
44
|
+
const { data: branch } = await github.rest.repos.getBranch({
|
|
45
|
+
owner: context.repo.owner,
|
|
46
|
+
repo: context.repo.repo,
|
|
47
|
+
branch: headRef,
|
|
48
|
+
});
|
|
49
|
+
return branch.commit.sha === currentHeadSha;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
if (!(await callerIsCurrent())) {
|
|
54
|
+
core.info("Skipping obsolete-run cleanup from a stale event head");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const { data: currentRun } = await github.rest.actions.getWorkflowRun({
|
|
58
|
+
owner: context.repo.owner,
|
|
59
|
+
repo: context.repo.repo,
|
|
60
|
+
run_id: context.runId,
|
|
61
|
+
});
|
|
62
|
+
const obsoleteRuns = new Map();
|
|
63
|
+
const addIfObsolete = (run) => {
|
|
64
|
+
const samePullRequest = (run.pull_requests || []).some((pull) => pull.number === prNumber);
|
|
65
|
+
const sameRepositoryBranch =
|
|
66
|
+
run.head_repository?.full_name === repoFullName && run.head_branch === headRef;
|
|
67
|
+
if (
|
|
68
|
+
run.id !== context.runId &&
|
|
69
|
+
run.workflow_id === currentRun.workflow_id &&
|
|
70
|
+
// A push can arrive during discovery. Run numbers are immutable
|
|
71
|
+
// within one workflow, including reruns, so newer runs are never victims.
|
|
72
|
+
Number.isSafeInteger(run.run_number) &&
|
|
73
|
+
Number.isSafeInteger(currentRun.run_number) &&
|
|
74
|
+
run.run_number < currentRun.run_number &&
|
|
75
|
+
activeStatuses.has(run.status) &&
|
|
76
|
+
run.head_sha !== currentHeadSha &&
|
|
77
|
+
(samePullRequest || sameRepositoryBranch)
|
|
78
|
+
) {
|
|
79
|
+
obsoleteRuns.set(run.id, run);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// The first parent is the previous PR head for the normal one-commit
|
|
84
|
+
// agent push. Its check runs expose run IDs immediately, while the
|
|
85
|
+
// workflow-run list can lag behind the synchronize event.
|
|
86
|
+
const candidateHeadShas = new Set();
|
|
87
|
+
if (context.payload.before && context.payload.before !== currentHeadSha) {
|
|
88
|
+
candidateHeadShas.add(context.payload.before);
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const { data: currentCommit } = await github.rest.repos.getCommit({
|
|
92
|
+
owner: context.repo.owner,
|
|
93
|
+
repo: context.repo.repo,
|
|
94
|
+
ref: currentHeadSha,
|
|
95
|
+
});
|
|
96
|
+
const firstParentSha = currentCommit.parents[0]?.sha;
|
|
97
|
+
if (firstParentSha) {
|
|
98
|
+
candidateHeadShas.add(firstParentSha);
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
core.warning(`Could not resolve current head's first parent: ${error.message}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const candidateHeadSha of candidateHeadShas) {
|
|
105
|
+
const { data: checkResponse } = await github.rest.checks.listForRef({
|
|
106
|
+
owner: context.repo.owner,
|
|
107
|
+
repo: context.repo.repo,
|
|
108
|
+
ref: candidateHeadSha,
|
|
109
|
+
per_page: 100,
|
|
110
|
+
});
|
|
111
|
+
const checkRuns = checkResponse.check_runs;
|
|
112
|
+
const candidateRunIds = new Set();
|
|
113
|
+
for (const checkRun of checkRuns) {
|
|
114
|
+
const match = checkRun.details_url?.match(/\/actions\/runs\/(\d+)/);
|
|
115
|
+
// A completed guard check can belong to a workflow whose next
|
|
116
|
+
// expensive job is only queued. Check state is not run state.
|
|
117
|
+
if (match) {
|
|
118
|
+
candidateRunIds.add(Number(match[1]));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const runId of candidateRunIds) {
|
|
122
|
+
try {
|
|
123
|
+
const { data: run } = await github.rest.actions.getWorkflowRun({
|
|
124
|
+
owner: context.repo.owner,
|
|
125
|
+
repo: context.repo.repo,
|
|
126
|
+
run_id: runId,
|
|
127
|
+
});
|
|
128
|
+
addIfObsolete(run);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
core.warning(`Could not inspect previous-head run ${runId}: ${error.message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Fallback covers reopened PRs and runs already visible in the
|
|
136
|
+
// workflow index. The map de-duplicates both discovery paths.
|
|
137
|
+
const indexedRuns = await github.paginate(github.rest.actions.listWorkflowRuns, {
|
|
138
|
+
owner: context.repo.owner,
|
|
139
|
+
repo: context.repo.repo,
|
|
140
|
+
workflow_id: currentRun.workflow_id,
|
|
141
|
+
event: "pull_request",
|
|
142
|
+
branch: headRef,
|
|
143
|
+
per_page: 100,
|
|
144
|
+
});
|
|
145
|
+
for (const run of indexedRuns) {
|
|
146
|
+
addIfObsolete(run);
|
|
147
|
+
}
|
|
148
|
+
core.info(`Found ${obsoleteRuns.size} obsolete active run(s)`);
|
|
149
|
+
|
|
150
|
+
for (const run of obsoleteRuns.values()) {
|
|
151
|
+
if (!(await callerIsCurrent())) {
|
|
152
|
+
core.info("Stopping obsolete-run cleanup because the branch advanced");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
await github.rest.actions.cancelWorkflowRun({
|
|
157
|
+
owner: context.repo.owner,
|
|
158
|
+
repo: context.repo.repo,
|
|
159
|
+
run_id: run.id,
|
|
160
|
+
});
|
|
161
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
162
|
+
const { data: afterCancel } = await github.rest.actions.getWorkflowRun({
|
|
163
|
+
owner: context.repo.owner,
|
|
164
|
+
repo: context.repo.repo,
|
|
165
|
+
run_id: run.id,
|
|
166
|
+
});
|
|
167
|
+
if (activeStatuses.has(afterCancel.status)) {
|
|
168
|
+
await github.rest.actions.forceCancelWorkflowRun({
|
|
169
|
+
owner: context.repo.owner,
|
|
170
|
+
repo: context.repo.repo,
|
|
171
|
+
run_id: run.id,
|
|
172
|
+
});
|
|
173
|
+
core.notice(
|
|
174
|
+
`Force-cancelled obsolete ${run.head_sha} run ${run.id}; PR #${prNumber} is ${currentHeadSha}`,
|
|
175
|
+
);
|
|
176
|
+
} else {
|
|
177
|
+
core.notice(`Cancelled obsolete ${run.head_sha} run ${run.id}; PR #${prNumber} is ${currentHeadSha}`);
|
|
178
|
+
}
|
|
179
|
+
} catch (error) {
|
|
180
|
+
// A run may finish between listing and cancellation. Validation
|
|
181
|
+
// remains authoritative even when cost cleanup races or fails.
|
|
182
|
+
core.warning(`Could not cancel obsolete run ${run.id}: ${error.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} catch (error) {
|
|
186
|
+
// Cost cleanup is fail-open: an API outage must not suppress the
|
|
187
|
+
// current head's terminal validation.
|
|
188
|
+
core.warning(`Could not inspect obsolete workflow runs: ${error.message}`);
|
|
189
|
+
}
|