@1aboveio/skills 0.0.0-stage → 1.0.0
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.
- package/README.md +30 -2
- package/bin/cli.mjs +14 -0
- package/bin/select-core.mjs +172 -0
- package/package.json +146 -4
- package/skills/app-dev/airflow/LICENSE +3 -0
- package/skills/app-dev/airflow/SKILL.md +199 -0
- package/skills/app-dev/airflow/release-check.mjs +93 -0
- package/skills/app-dev/android-foundation/LICENSE +3 -0
- package/skills/app-dev/android-foundation/SKILL.md +192 -0
- package/skills/app-dev/android-foundation/assets/android-ci-job.yml +217 -0
- package/skills/app-dev/android-foundation/references/ci.md +87 -0
- package/skills/app-dev/android-foundation/references/gotchas.md +221 -0
- package/skills/app-dev/android-foundation/references/setup.md +164 -0
- package/skills/app-dev/android-foundation/release-check.mjs +93 -0
- package/skills/app-dev/android-foundation/scripts/device-run.sh +176 -0
- package/skills/app-dev/android-foundation/scripts/device-run.test.mjs +172 -0
- package/skills/app-dev/app-debug/LICENSE +3 -0
- package/skills/app-dev/app-debug/SKILL.md +110 -0
- package/skills/app-dev/app-debug/references/common-errors.md +128 -0
- package/skills/app-dev/app-debug/release-check.mjs +93 -0
- package/skills/app-dev/better-auth/LICENSE +3 -0
- package/skills/app-dev/better-auth/SKILL.md +598 -0
- package/skills/app-dev/better-auth/references/feishu-api.md +270 -0
- package/skills/app-dev/better-auth/release-check.mjs +93 -0
- package/skills/app-dev/data-catalog/LICENSE +3 -0
- package/skills/app-dev/data-catalog/SKILL.md +204 -0
- package/skills/app-dev/data-catalog/agents/openai.yaml +3 -0
- package/skills/app-dev/data-catalog/references/data-catalog-conventions.md +75 -0
- package/skills/app-dev/data-catalog/references/field-definitions.md +116 -0
- package/skills/app-dev/data-catalog/references/format.md +121 -0
- package/skills/app-dev/data-catalog/release-check.mjs +93 -0
- package/skills/app-dev/data-catalog/scripts/data-catalog.test.mjs +36 -0
- package/skills/app-dev/data-catalog/scripts/data_catalog.py +1392 -0
- package/skills/app-dev/data-catalog/scripts/persist-conventions-core.mjs +117 -0
- package/skills/app-dev/data-catalog/scripts/persist-conventions.mjs +17 -0
- package/skills/app-dev/data-catalog/scripts/test_data_catalog.py +571 -0
- package/skills/app-dev/ios-foundation/LICENSE +3 -0
- package/skills/app-dev/ios-foundation/SKILL.md +184 -0
- package/skills/app-dev/ios-foundation/references/setup.md +243 -0
- package/skills/app-dev/ios-foundation/release-check.mjs +93 -0
- package/skills/app-dev/monorepo/LICENSE +3 -0
- package/skills/app-dev/monorepo/SKILL.md +462 -0
- package/skills/app-dev/monorepo/release-check.mjs +93 -0
- package/skills/app-dev/nextjs-fullstack/LICENSE +3 -0
- package/skills/app-dev/nextjs-fullstack/SKILL.md +211 -0
- package/skills/app-dev/nextjs-fullstack/conventions.md +318 -0
- package/skills/app-dev/nextjs-fullstack/frontend-conventions.md +61 -0
- package/skills/app-dev/nextjs-fullstack/nextjs16.md +287 -0
- package/skills/app-dev/nextjs-fullstack/release-check.mjs +93 -0
- package/skills/app-dev/nextjs-fullstack/server-actions.md +409 -0
- package/skills/app-dev/prisma-setup/LICENSE +3 -0
- package/skills/app-dev/prisma-setup/SKILL.md +181 -0
- package/skills/app-dev/prisma-setup/nextjs.md +258 -0
- package/skills/app-dev/prisma-setup/release-check.mjs +93 -0
- package/skills/app-dev/prisma-setup/turborepo.md +301 -0
- package/skills/app-dev/pyspark/LICENSE +3 -0
- package/skills/app-dev/pyspark/SKILL.md +127 -0
- package/skills/app-dev/pyspark/assets/templates/etl.py +748 -0
- package/skills/app-dev/pyspark/assets/templates/utils/__init__.py +1 -0
- package/skills/app-dev/pyspark/assets/templates/utils/hudi_metadata.py +14 -0
- package/skills/app-dev/pyspark/references/diagnosis-and-profiling.md +169 -0
- package/skills/app-dev/pyspark/references/etl-contract.md +129 -0
- package/skills/app-dev/pyspark/references/production-validation.md +131 -0
- package/skills/app-dev/pyspark/references/reconciliation.md +38 -0
- package/skills/app-dev/pyspark/references/transformation-design.md +190 -0
- package/skills/app-dev/pyspark/references/velocity-feature-calculation.md +193 -0
- package/skills/app-dev/pyspark/release-check.mjs +93 -0
- package/skills/app-dev/pyspark/scripts/spark_eventlog_summary.py +223 -0
- package/skills/app-dev/python-backend/LICENSE +3 -0
- package/skills/app-dev/python-backend/SKILL.md +325 -0
- package/skills/app-dev/python-backend/assets/uv-install.sh +48 -0
- package/skills/app-dev/python-backend/references/uv-installs.md +83 -0
- package/skills/app-dev/python-backend/release-check.mjs +93 -0
- package/skills/app-dev/shadcn/LICENSE +3 -0
- package/skills/app-dev/shadcn/SKILL.md +120 -0
- package/skills/app-dev/shadcn/assets/shadcn-small.png +0 -0
- package/skills/app-dev/shadcn/assets/shadcn.png +0 -0
- package/skills/app-dev/shadcn/cli.md +411 -0
- package/skills/app-dev/shadcn/customization.md +224 -0
- package/skills/app-dev/shadcn/mcp.md +101 -0
- package/skills/app-dev/shadcn/release-check.mjs +93 -0
- package/skills/app-dev/shadcn/rules/base-vs-radix.md +323 -0
- package/skills/app-dev/shadcn/rules/component-selection.md +67 -0
- package/skills/app-dev/shadcn/rules/composition.md +195 -0
- package/skills/app-dev/shadcn/rules/data-table.md +201 -0
- package/skills/app-dev/shadcn/rules/forms.md +255 -0
- package/skills/app-dev/shadcn/rules/icons.md +103 -0
- package/skills/app-dev/shadcn/rules/styling.md +167 -0
- package/skills/app-dev/zod-v4/LICENSE +3 -0
- package/skills/app-dev/zod-v4/SKILL.md +286 -0
- package/skills/app-dev/zod-v4/release-check.mjs +93 -0
- package/skills/cicd/cloud-build/LICENSE +3 -0
- package/skills/cicd/cloud-build/SKILL.md +707 -0
- package/skills/cicd/cloud-build/release-check.mjs +93 -0
- package/skills/cicd/cloud-debug/LICENSE +3 -0
- package/skills/cicd/cloud-debug/SKILL.md +315 -0
- package/skills/cicd/cloud-debug/references/build-failures.md +238 -0
- package/skills/cicd/cloud-debug/references/deploy-failures.md +376 -0
- package/skills/cicd/cloud-debug/references/pipeline-failures.md +378 -0
- package/skills/cicd/cloud-debug/release-check.mjs +93 -0
- package/skills/cicd/cloud-deploy/LICENSE +3 -0
- package/skills/cicd/cloud-deploy/SKILL.md +230 -0
- package/skills/cicd/cloud-deploy/references/config-templates.md +257 -0
- package/skills/cicd/cloud-deploy/release-check.mjs +93 -0
- package/skills/cicd/docker/LICENSE +3 -0
- package/skills/cicd/docker/SKILL.md +151 -0
- package/skills/cicd/docker/release-check.mjs +93 -0
- package/skills/cicd/github-actions/LICENSE +3 -0
- package/skills/cicd/github-actions/SKILL.md +111 -0
- package/skills/cicd/github-actions/references/audit-profile-optimize.md +150 -0
- package/skills/cicd/github-actions/references/ci-case-studies.md +141 -0
- package/skills/cicd/github-actions/references/conditional-ci.md +214 -0
- package/skills/cicd/github-actions/references/debugging.md +13 -0
- package/skills/cicd/github-actions/references/pipeline.md +87 -0
- package/skills/cicd/github-actions/references/post-deploy-and-publishing.md +33 -0
- package/skills/cicd/github-actions/references/runners.md +253 -0
- package/skills/cicd/github-actions/references/security.md +29 -0
- package/skills/cicd/github-actions/references/speed-and-audit.md +115 -0
- package/skills/cicd/github-actions/references/two-pass-reading.md +92 -0
- package/skills/cicd/github-actions/release-check.mjs +93 -0
- package/skills/cicd/github-actions/scripts/ci-run-audit-core.mjs +612 -0
- package/skills/cicd/github-actions/scripts/ci-run-audit.mjs +4 -0
- package/skills/cicd/github-actions/scripts/ci-run-audit.test.mjs +637 -0
- package/skills/cicd/github-flow/LICENSE +3 -0
- package/skills/cicd/github-flow/SKILL.md +123 -0
- package/skills/cicd/github-flow/references/ci-conventions.md +14 -0
- package/skills/cicd/github-flow/release-check.mjs +93 -0
- package/skills/cicd/github-flow/scripts/flow-core.mjs +105 -0
- package/skills/cicd/github-flow/scripts/flow.mjs +81 -0
- package/skills/cicd/github-flow/scripts/flow.test.mjs +114 -0
- package/skills/cicd/github-flow/scripts/persist-conventions-core.mjs +117 -0
- package/skills/cicd/github-flow/scripts/persist-conventions.mjs +49 -0
- package/skills/cicd/github-flow/scripts/persist-conventions.test.mjs +108 -0
- package/skills/cicd/google-cloud/LICENSE +3 -0
- package/skills/cicd/google-cloud/SKILL.md +119 -0
- package/skills/cicd/google-cloud/references/gcs.md +469 -0
- package/skills/cicd/google-cloud/references/iam.md +451 -0
- package/skills/cicd/google-cloud/references/project.md +349 -0
- package/skills/cicd/google-cloud/references/secrets.md +336 -0
- package/skills/cicd/google-cloud/references/vpc.md +312 -0
- package/skills/cicd/google-cloud/release-check.mjs +93 -0
- package/skills/cicd/google-cloud/scripts/create-sa.sh +36 -0
- package/skills/cicd/google-cloud/scripts/gcp-config.sh +31 -0
- package/skills/cicd/google-cloud/scripts/grant-iap.sh +41 -0
- package/skills/cicd/google-cloud/scripts/setup-secrets.sh +48 -0
- package/skills/cicd/mergify/LICENSE +3 -0
- package/skills/cicd/mergify/SKILL.md +102 -0
- package/skills/cicd/mergify/assets/templates/mergify.yml +215 -0
- package/skills/cicd/mergify/assets/templates/ruleset.json +46 -0
- package/skills/cicd/mergify/references/branch-protection.md +282 -0
- package/skills/cicd/mergify/references/configuration.md +274 -0
- package/skills/cicd/mergify/references/diagnosis.md +109 -0
- package/skills/cicd/mergify/references/stacks.md +84 -0
- package/skills/cicd/mergify/references/traps.md +119 -0
- package/skills/cicd/mergify/references/watch-contract.md +274 -0
- package/skills/cicd/mergify/release-check.mjs +93 -0
- package/skills/cicd/mergify/scripts/audit-core.mjs +208 -0
- package/skills/cicd/mergify/scripts/audit.mjs +4 -0
- package/skills/cicd/mergify/scripts/audit.test.mjs +201 -0
- package/skills/cicd/mergify/scripts/entrypoints.test.mjs +106 -0
- package/skills/cicd/mergify/scripts/pr-620-mergify-comments.fixture.json +30 -0
- package/skills/cicd/mergify/scripts/stack-push-core.mjs +258 -0
- package/skills/cicd/mergify/scripts/stack-push.mjs +4 -0
- package/skills/cicd/mergify/scripts/stack-push.test.mjs +233 -0
- package/skills/cicd/mergify/scripts/watch-pr-delivery-core.mjs +1065 -0
- package/skills/cicd/mergify/scripts/watch-pr-delivery.mjs +4 -0
- package/skills/cicd/mergify/scripts/watch-pr-delivery.test.mjs +1826 -0
- package/skills/cicd/podman/LICENSE +3 -0
- package/skills/cicd/podman/SKILL.md +71 -0
- package/skills/cicd/podman/agents/openai.yaml +4 -0
- package/skills/cicd/podman/assets/templates/podman-compose-socket-directory.yml +6 -0
- package/skills/cicd/podman/assets/templates/podman-service-override.conf +3 -0
- package/skills/cicd/podman/references/compose-compatibility.md +70 -0
- package/skills/cicd/podman/references/networking-and-ports.md +74 -0
- package/skills/cicd/podman/references/rootless-services-and-sockets.md +156 -0
- package/skills/cicd/podman/references/troubleshooting.md +98 -0
- package/skills/cicd/podman/release-check.mjs +93 -0
- package/skills/content/article-review/LICENSE +3 -0
- package/skills/content/article-review/SKILL.md +66 -0
- package/skills/content/article-review/references/report-format.md +68 -0
- package/skills/content/article-review/references/template-guide.md +79 -0
- package/skills/content/article-review/references/template-wechat-payments.md +125 -0
- package/skills/content/article-review/release-check.mjs +93 -0
- package/skills/content/article-to-image/LICENSE +3 -0
- package/skills/content/article-to-image/SKILL.md +91 -0
- package/skills/content/article-to-image/references/formats.md +53 -0
- package/skills/content/article-to-image/release-check.mjs +93 -0
- package/skills/content/flowchart-svg/LICENSE +3 -0
- package/skills/content/flowchart-svg/SKILL.md +43 -0
- package/skills/content/flowchart-svg/release-check.mjs +93 -0
- package/skills/content/flowchart-svg/scripts/export_png.sh +8 -0
- package/skills/content/image-gen/LICENSE +3 -0
- package/skills/content/image-gen/SKILL.md +192 -0
- package/skills/content/image-gen/release-check.mjs +93 -0
- package/skills/content/image-gen/scripts/credentials.mjs +217 -0
- package/skills/content/image-gen/scripts/credentials.test.mjs +212 -0
- package/skills/content/image-gen/scripts/generate.mjs +240 -0
- package/skills/content/image-gen/scripts/options.mjs +293 -0
- package/skills/content/image-gen/scripts/options.test.mjs +245 -0
- package/skills/content/mermaid/LICENSE +3 -0
- package/skills/content/mermaid/SKILL.md +635 -0
- package/skills/content/mermaid/release-check.mjs +93 -0
- package/skills/content/wechat-article-writer/LICENSE +3 -0
- package/skills/content/wechat-article-writer/SKILL.md +92 -0
- package/skills/content/wechat-article-writer/references/style-selector.md +133 -0
- package/skills/content/wechat-article-writer/references/writing-rules.md +152 -0
- package/skills/content/wechat-article-writer/release-check.mjs +93 -0
- package/skills/content/wechat-article-writer/templates/wechat-comparison.md +52 -0
- package/skills/content/wechat-article-writer/templates/wechat-howto.md +51 -0
- package/skills/content/wechat-article-writer/templates/wechat-insight.md +50 -0
- package/skills/content/wechat-article-writer/templates/wechat-pitfall.md +52 -0
- package/skills/content/wechat-publisher/LICENSE +3 -0
- package/skills/content/wechat-publisher/SKILL.md +171 -0
- package/skills/content/wechat-publisher/references/api.md +105 -0
- package/skills/content/wechat-publisher/references/layout-rules.md +57 -0
- package/skills/content/wechat-publisher/references/wechat-safe-template.html +86 -0
- package/skills/content/wechat-publisher/release-check.mjs +93 -0
- package/skills/content/wechat-publisher/scripts/md_to_wechat_html.py +194 -0
- package/skills/content/wechat-publisher/scripts/publish.py +239 -0
- package/skills/content/wechat-search/LICENSE +3 -0
- package/skills/content/wechat-search/SKILL.md +211 -0
- package/skills/content/wechat-search/release-check.mjs +93 -0
- package/skills/content/wechat-search/scripts/start-chrome.sh +83 -0
- package/skills/content/xai-search/LICENSE +3 -0
- package/skills/content/xai-search/SKILL.md +79 -0
- package/skills/content/xai-search/release-check.mjs +93 -0
- package/skills/content/xai-search/scripts/xai_search.sh +187 -0
- package/skills/content/xai-search/tests/smoke_test.sh +36 -0
- package/skills/core/distill-writing/LICENSE +3 -0
- package/skills/core/distill-writing/SKILL.md +74 -0
- package/skills/core/distill-writing/references/extraction.md +95 -0
- package/skills/core/distill-writing/references/updating.md +46 -0
- package/skills/core/distill-writing/release-check.mjs +93 -0
- package/skills/core/doc-to-md/LICENSE +3 -0
- package/skills/core/doc-to-md/SKILL.md +78 -0
- package/skills/core/doc-to-md/release-check.mjs +93 -0
- package/skills/core/doc-to-md/scripts/convert.py +157 -0
- package/skills/core/editorial-review/LICENSE +3 -0
- package/skills/core/editorial-review/SKILL.md +79 -0
- package/skills/core/editorial-review/references/checklist.md +94 -0
- package/skills/core/editorial-review/release-check.mjs +93 -0
- package/skills/core/house-style/LICENSE +3 -0
- package/skills/core/house-style/SKILL.md +33 -0
- package/skills/core/house-style/references/defaults.md +66 -0
- package/skills/core/house-style/references/rules.md +267 -0
- package/skills/core/house-style/references/sources.md +27 -0
- package/skills/core/house-style/references/style-en.md +131 -0
- package/skills/core/house-style/references/style-zh.md +141 -0
- package/skills/core/house-style/references/terminology-zh.md +223 -0
- package/skills/core/house-style/release-check.mjs +93 -0
- package/skills/core/house-style/templates/brd.md +103 -0
- package/skills/core/house-style/templates/commercial-proposal.md +93 -0
- package/skills/core/house-style/templates/compliance-policy.md +109 -0
- package/skills/core/house-style/templates/gap-analysis.md +105 -0
- package/skills/core/house-style/templates/general.md +63 -0
- package/skills/core/house-style/templates/integration-guide.md +98 -0
- package/skills/core/house-style/templates/prd.md +81 -0
- package/skills/core/house-style/templates/risk-operations-procedure.md +94 -0
- package/skills/core/house-style/templates/technical-spec.md +96 -0
- package/skills/core/polish-cn/LICENSE +3 -0
- package/skills/core/polish-cn/SKILL.md +102 -0
- package/skills/core/polish-cn/references/brief.md +93 -0
- package/skills/core/polish-cn/references/models.md +76 -0
- package/skills/core/polish-cn/release-check.mjs +93 -0
- package/skills/core/polish-cn/scripts/polish-cn.test.mjs +244 -0
- package/skills/core/polish-cn/scripts/polish_blocks.py +489 -0
- package/skills/core/reg-parser/LICENSE +3 -0
- package/skills/core/reg-parser/SKILL.md +212 -0
- package/skills/core/reg-parser/references/checklist-schema.md +223 -0
- package/skills/core/reg-parser/references/checklists/hk-amlo-checklist.json +1306 -0
- package/skills/core/reg-parser/references/checklists/hk-amlo-checklist.md +469 -0
- package/skills/core/reg-parser/references/checklists/hk-fi-corporate-onboarding-cdd-checklist.json +634 -0
- package/skills/core/reg-parser/references/checklists/hk-fi-corporate-onboarding-cdd-checklist.md +233 -0
- package/skills/core/reg-parser/references/checklists/hk-mso-guideline-checklist.json +2194 -0
- package/skills/core/reg-parser/references/checklists/hk-mso-guideline-checklist.md +771 -0
- package/skills/core/reg-parser/references/checklists/kyc-aml-checklist.md +258 -0
- package/skills/core/reg-parser/references/checklists/visa-vars-checklist.md +1058 -0
- package/skills/core/reg-parser/references/mandates/hk-amlo-cap615.md +389 -0
- package/skills/core/reg-parser/references/mandates/visa-vars.md +205 -0
- package/skills/core/reg-parser/release-check.mjs +93 -0
- package/skills/core/reg-parser/scripts/compare_checklist.py +230 -0
- package/skills/core/reg-parser/scripts/dump_checklist.py +361 -0
- package/skills/core/reg-parser/scripts/preprocess-mandate.sh +187 -0
- package/skills/core/reg-parser/scripts/reg-parser.test.mjs +165 -0
- package/skills/core/reg-parser/scripts/test_quarantine.sh +105 -0
- package/skills/core/reg-parser/scripts/validate_checklist.py +563 -0
- package/skills/core/reg-parser/scripts/validate_mandate.py +273 -0
- package/skills/core/terminology/LICENSE +3 -0
- package/skills/core/terminology/SKILL.md +62 -0
- package/skills/core/terminology/references/format.md +50 -0
- package/skills/core/terminology/references/init.md +32 -0
- package/skills/core/terminology/release-check.mjs +93 -0
- package/skills/core/write-doc/LICENSE +3 -0
- package/skills/core/write-doc/SKILL.md +141 -0
- package/skills/core/write-doc/references/bilingual.md +60 -0
- package/skills/core/write-doc/release-check.mjs +93 -0
- package/skills/engineering/better-review/LICENSE +3 -0
- package/skills/engineering/better-review/SKILL.md +249 -0
- package/skills/engineering/better-review/agents/openai.yaml +9 -0
- package/skills/engineering/better-review/references/domain-hazards.md +53 -0
- package/skills/engineering/better-review/references/passes.md +147 -0
- package/skills/engineering/better-review/references/result.md +151 -0
- package/skills/engineering/better-review/release-check.mjs +93 -0
- package/skills/engineering/better-review/scripts/pr-context.mjs +212 -0
- package/skills/engineering/better-review/scripts/pr-context.test.mjs +168 -0
- package/skills/engineering/better-review/scripts/rereview-scope.mjs +503 -0
- package/skills/engineering/better-review/scripts/rereview-scope.test.mjs +587 -0
- package/skills/engineering/better-review/scripts/scan-diff.mjs +365 -0
- package/skills/engineering/better-review/scripts/scan-diff.test.mjs +414 -0
- package/skills/engineering/better-review/scripts/vendor-sync.test.mjs +96 -0
- package/skills/engineering/e2e-test/LICENSE +3 -0
- package/skills/engineering/e2e-test/SKILL.md +98 -0
- package/skills/engineering/e2e-test/assets/ci-gates.cloudbuild.yaml +24 -0
- package/skills/engineering/e2e-test/references/authoring/auth-flows.md +159 -0
- package/skills/engineering/e2e-test/references/authoring/playwright-config.md +73 -0
- package/skills/engineering/e2e-test/references/authoring/playwright-patterns.md +144 -0
- package/skills/engineering/e2e-test/references/ci-integration.md +25 -0
- package/skills/engineering/e2e-test/references/ci-playwright-container.md +290 -0
- package/skills/engineering/e2e-test/references/presentation-sweep.md +104 -0
- package/skills/engineering/e2e-test/references/reviewing.md +41 -0
- package/skills/engineering/e2e-test/references/route-manifest.md +51 -0
- package/skills/engineering/e2e-test/release-check.mjs +93 -0
- package/skills/engineering/e2e-test/scripts/detect-routes-nextjs.mjs +200 -0
- package/skills/engineering/e2e-test/scripts/presentation-checks.mjs +171 -0
- package/skills/engineering/e2e-test/scripts/presentation-checks.test.mjs +106 -0
- package/skills/engineering/e2e-test/scripts/presentation-perceivability.mjs +179 -0
- package/skills/engineering/e2e-test/scripts/presentation-perceivability.test.mjs +117 -0
- package/skills/engineering/e2e-test/scripts/presentation-reachability.mjs +154 -0
- package/skills/engineering/e2e-test/scripts/presentation-reachability.test.mjs +69 -0
- package/skills/engineering/e2e-test/scripts/presentation-render-health.mjs +141 -0
- package/skills/engineering/e2e-test/scripts/presentation-render-health.test.mjs +98 -0
- package/skills/engineering/e2e-test/scripts/presentation-sweep.mjs +147 -0
- package/skills/engineering/e2e-test/scripts/presentation-sweep.test.mjs +71 -0
- package/skills/engineering/e2e-test/scripts/presentation-temporal.mjs +127 -0
- package/skills/engineering/e2e-test/scripts/presentation-temporal.test.mjs +59 -0
- package/skills/engineering/e2e-test/scripts/project-route-manifest.mjs +82 -0
- package/skills/engineering/e2e-test/scripts/project-route-manifest.test.mjs +76 -0
- package/skills/engineering/e2e-test/scripts/validate-manifest.mjs +106 -0
- package/skills/engineering/engineering-runtime/LICENSE +3 -0
- package/skills/engineering/engineering-runtime/SKILL.md +11 -0
- package/skills/engineering/engineering-runtime/agents/openai.yaml +6 -0
- package/skills/engineering/engineering-runtime/coherence/workflow.json +597 -0
- package/skills/engineering/engineering-runtime/engineering-runtime.test.mjs +68 -0
- package/skills/engineering/engineering-runtime/release-check.mjs +93 -0
- package/skills/engineering/engineering-runtime/scripts/exact-head-artifact.mjs +131 -0
- package/skills/engineering/engineering-runtime/scripts/head-check-set.mjs +398 -0
- package/skills/engineering/engineering-runtime/scripts/invocation-policy.mjs +187 -0
- package/skills/engineering/engineering-runtime/scripts/invocation-policy.test.mjs +95 -0
- package/skills/engineering/engineering-runtime/scripts/main-module.mjs +79 -0
- package/skills/engineering/engineering-runtime/scripts/main-module.test.mjs +288 -0
- package/skills/engineering/engineering-runtime/scripts/mergify-yaml.mjs +11 -0
- package/skills/engineering/engineering-runtime/scripts/package-lock.json +43 -0
- package/skills/engineering/engineering-runtime/scripts/package.json +10 -0
- package/skills/engineering/engineering-runtime/scripts/required-check-plan.mjs +223 -0
- package/skills/engineering/engineering-runtime/scripts/required-check-plan.test.mjs +96 -0
- package/skills/engineering/engineering-runtime/scripts/review-completion.mjs +56 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +595 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +189 -0
- package/skills/engineering/engineering-runtime/scripts/yaml-mapping.mjs +18 -0
- package/skills/engineering/ensure-coverage/LICENSE +3 -0
- package/skills/engineering/ensure-coverage/SKILL.md +129 -0
- package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +92 -0
- package/skills/engineering/ensure-coverage/references/breadth/inventory-contract.md +85 -0
- package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +44 -0
- package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +16 -0
- package/skills/engineering/ensure-coverage/references/depth/characterization.md +68 -0
- package/skills/engineering/ensure-coverage/references/depth/coverage.config.example.json +19 -0
- package/skills/engineering/ensure-coverage/references/depth/grading.md +35 -0
- package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +57 -0
- package/skills/engineering/ensure-coverage/references/depth/test-smells.md +39 -0
- package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +44 -0
- package/skills/engineering/ensure-coverage/references/examples/coverage-ledger.md +110 -0
- package/skills/engineering/ensure-coverage/references/examples/refund-flow.md +35 -0
- package/skills/engineering/ensure-coverage/references/presentation/axis.md +64 -0
- package/skills/engineering/ensure-coverage/references/process/audit-mode.md +27 -0
- package/skills/engineering/ensure-coverage/references/process/output-template.md +139 -0
- package/skills/engineering/ensure-coverage/references/process/test-strategy.md +56 -0
- package/skills/engineering/ensure-coverage/release-check.mjs +93 -0
- package/skills/engineering/ensure-coverage/schemas/coverage-config.schema.json +67 -0
- package/skills/engineering/ensure-coverage/schemas/coverage-file.schema.json +140 -0
- package/skills/engineering/ensure-coverage/scripts/README.md +99 -0
- package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.mjs +182 -0
- package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.test.mjs +96 -0
- package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.mjs +105 -0
- package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.test.mjs +47 -0
- package/skills/engineering/ensure-coverage/scripts/ci-audit.mjs +137 -0
- package/skills/engineering/ensure-coverage/scripts/ci-audit.test.mjs +86 -0
- package/skills/engineering/ensure-coverage/scripts/coverage-ledger.mjs +674 -0
- package/skills/engineering/ensure-coverage/scripts/coverage-ledger.test.mjs +447 -0
- package/skills/engineering/ensure-coverage/scripts/design-parity.mjs +591 -0
- package/skills/engineering/ensure-coverage/scripts/design-parity.test.mjs +552 -0
- package/skills/engineering/ensure-coverage/scripts/lint-tests.mjs +282 -0
- package/skills/engineering/ensure-coverage/scripts/lint-tests.test.mjs +289 -0
- package/skills/engineering/ensure-coverage/scripts/mock-policy-config.mjs +176 -0
- package/skills/engineering/ensure-coverage/scripts/mock-policy-config.test.mjs +119 -0
- package/skills/engineering/ensure-coverage/scripts/package.json +19 -0
- package/skills/engineering/golden-path/LICENSE +3 -0
- package/skills/engineering/golden-path/SKILL.md +230 -0
- package/skills/engineering/golden-path/agents/openai.yaml +3 -0
- package/skills/engineering/golden-path/assets/golden-paths.example.json +57 -0
- package/skills/engineering/golden-path/references/contract.md +239 -0
- package/skills/engineering/golden-path/references/migration.md +111 -0
- package/skills/engineering/golden-path/release-check.mjs +93 -0
- package/skills/engineering/golden-path/scripts/golden-path-core.mjs +1779 -0
- package/skills/engineering/golden-path/scripts/golden-path.mjs +6 -0
- package/skills/engineering/golden-path/scripts/golden-path.test.mjs +1457 -0
- package/skills/engineering/harness-runtime/LICENSE +3 -0
- package/skills/engineering/harness-runtime/SKILL.md +19 -0
- package/skills/engineering/harness-runtime/agents/openai.yaml +6 -0
- package/skills/engineering/harness-runtime/bin/discover-models.mjs +4 -0
- package/skills/engineering/harness-runtime/bin/model-catalog.mjs +4 -0
- package/skills/engineering/harness-runtime/contracts.md +15 -0
- package/skills/engineering/harness-runtime/discover-models.mjs +392 -0
- package/skills/engineering/harness-runtime/discover-models.test.mjs +363 -0
- package/skills/engineering/harness-runtime/fixtures/native-question-schemas.json +33 -0
- package/skills/engineering/harness-runtime/fixtures/question-responses.json +54 -0
- package/skills/engineering/harness-runtime/harness-runtime.test.mjs +967 -0
- package/skills/engineering/harness-runtime/index.mjs +767 -0
- package/skills/engineering/harness-runtime/model-catalog-seed.test.mjs +829 -0
- package/skills/engineering/harness-runtime/model-catalog.mjs +787 -0
- package/skills/engineering/harness-runtime/model-catalog.test.mjs +366 -0
- package/skills/engineering/harness-runtime/native-question-contracts.md +37 -0
- package/skills/engineering/harness-runtime/references/model-catalog-seed.json +408 -0
- package/skills/engineering/harness-runtime/references/model-catalog.md +57 -0
- package/skills/engineering/harness-runtime/release-check.mjs +93 -0
- package/skills/engineering/resolve-issues/LICENSE +3 -0
- package/skills/engineering/resolve-issues/SKILL.md +250 -0
- package/skills/engineering/resolve-issues/agents/openai.yaml +9 -0
- package/skills/engineering/resolve-issues/plugin-hooks.json +17 -0
- package/skills/engineering/resolve-issues/references/canary.md +28 -0
- package/skills/engineering/resolve-issues/references/ci.md +105 -0
- package/skills/engineering/resolve-issues/references/done.md +25 -0
- package/skills/engineering/resolve-issues/references/dropbox.md +90 -0
- package/skills/engineering/resolve-issues/references/expire.md +318 -0
- package/skills/engineering/resolve-issues/references/exploration.md +202 -0
- package/skills/engineering/resolve-issues/references/implementation.md +303 -0
- package/skills/engineering/resolve-issues/references/preflight.md +445 -0
- package/skills/engineering/resolve-issues/references/review.md +228 -0
- package/skills/engineering/resolve-issues/references/shared-modules.md +44 -0
- package/skills/engineering/resolve-issues/references/task-plan.md +264 -0
- package/skills/engineering/resolve-issues/release-check.mjs +93 -0
- package/skills/engineering/resolve-issues/scripts/cleanup-runs.mjs +364 -0
- package/skills/engineering/resolve-issues/scripts/cleanup-runs.test.mjs +280 -0
- package/skills/engineering/resolve-issues/scripts/discover-models.mjs +9 -0
- package/skills/engineering/resolve-issues/scripts/dropbox.mjs +473 -0
- package/skills/engineering/resolve-issues/scripts/dropbox.test.mjs +346 -0
- package/skills/engineering/resolve-issues/scripts/findings-reuse.mjs +98 -0
- package/skills/engineering/resolve-issues/scripts/findings-reuse.test.mjs +88 -0
- package/skills/engineering/resolve-issues/scripts/github-reviews.mjs +384 -0
- package/skills/engineering/resolve-issues/scripts/github-reviews.test.mjs +264 -0
- package/skills/engineering/resolve-issues/scripts/install-hooks.mjs +135 -0
- package/skills/engineering/resolve-issues/scripts/install-hooks.test.mjs +79 -0
- package/skills/engineering/resolve-issues/scripts/issue-tree.mjs +162 -0
- package/skills/engineering/resolve-issues/scripts/issue-tree.test.mjs +107 -0
- package/skills/engineering/resolve-issues/scripts/manual-invocation.test.mjs +14 -0
- package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +9 -0
- package/skills/engineering/resolve-issues/scripts/next-actions-capacity.test.mjs +149 -0
- package/skills/engineering/resolve-issues/scripts/next-actions.mjs +856 -0
- package/skills/engineering/resolve-issues/scripts/next-actions.test.mjs +915 -0
- package/skills/engineering/resolve-issues/scripts/plan-sync.mjs +269 -0
- package/skills/engineering/resolve-issues/scripts/plan-sync.test.mjs +156 -0
- package/skills/engineering/resolve-issues/scripts/preflight-models.mjs +736 -0
- package/skills/engineering/resolve-issues/scripts/preflight-models.test.mjs +448 -0
- package/skills/engineering/resolve-issues/scripts/ready-check.mjs +185 -0
- package/skills/engineering/resolve-issues/scripts/ready-check.test.mjs +217 -0
- package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +98 -0
- package/skills/engineering/resolve-issues/scripts/rehydrate.test.mjs +91 -0
- package/skills/engineering/resolve-issues/scripts/review-verdict.mjs +509 -0
- package/skills/engineering/resolve-issues/scripts/review-verdict.test.mjs +1060 -0
- package/skills/engineering/resolve-issues/scripts/size-check.mjs +492 -0
- package/skills/engineering/resolve-issues/scripts/size-check.test.mjs +458 -0
- package/skills/engineering/resolve-release/LICENSE +3 -0
- package/skills/engineering/resolve-release/SKILL.md +215 -0
- package/skills/engineering/resolve-release/agents/openai.yaml +8 -0
- package/skills/engineering/resolve-release/flows/cloud-run-canary.md +13 -0
- package/skills/engineering/resolve-release/flows/cloud-run.md +7 -0
- package/skills/engineering/resolve-release/flows/root-tag-npm.md +13 -0
- package/skills/engineering/resolve-release/flows/workspace-npm.md +22 -0
- package/skills/engineering/resolve-release/references/canary.md +51 -0
- package/skills/engineering/resolve-release/references/candidate.md +41 -0
- package/skills/engineering/resolve-release/references/cut.md +131 -0
- package/skills/engineering/resolve-release/references/dark-launch.md +66 -0
- package/skills/engineering/resolve-release/references/flows.md +150 -0
- package/skills/engineering/resolve-release/references/preflight.md +154 -0
- package/skills/engineering/resolve-release/references/prepared-metadata.md +260 -0
- package/skills/engineering/resolve-release/references/promotion.md +82 -0
- package/skills/engineering/resolve-release/references/publish.md +232 -0
- package/skills/engineering/resolve-release/references/workspace-npm.md +103 -0
- package/skills/engineering/resolve-release/release-check.mjs +93 -0
- package/skills/engineering/resolve-release/scripts/apply.mjs +212 -0
- package/skills/engineering/resolve-release/scripts/apply.test.mjs +99 -0
- package/skills/engineering/resolve-release/scripts/dark-flags.mjs +134 -0
- package/skills/engineering/resolve-release/scripts/dark-flags.test.mjs +100 -0
- package/skills/engineering/resolve-release/scripts/flow.mjs +500 -0
- package/skills/engineering/resolve-release/scripts/flow.test.mjs +340 -0
- package/skills/engineering/resolve-release/scripts/green-head.mjs +231 -0
- package/skills/engineering/resolve-release/scripts/green-head.test.mjs +101 -0
- package/skills/engineering/resolve-release/scripts/npm-version-absence.mjs +138 -0
- package/skills/engineering/resolve-release/scripts/npm-version-absence.test.mjs +426 -0
- package/skills/engineering/resolve-release/scripts/plan-npm.test.mjs +659 -0
- package/skills/engineering/resolve-release/scripts/plan-toml-boundary.test.mjs +317 -0
- package/skills/engineering/resolve-release/scripts/plan-uv.test.mjs +291 -0
- package/skills/engineering/resolve-release/scripts/plan.mjs +806 -0
- package/skills/engineering/resolve-release/scripts/plan.test.mjs +428 -0
- package/skills/engineering/rush-issues/LICENSE +3 -0
- package/skills/engineering/rush-issues/SKILL.md +58 -0
- package/skills/engineering/rush-issues/agents/openai.yaml +8 -0
- package/skills/engineering/rush-issues/rehydrate.mjs +10 -0
- package/skills/engineering/rush-issues/release-check.mjs +93 -0
- package/skills/engineering/rush-release/LICENSE +3 -0
- package/skills/engineering/rush-release/SKILL.md +35 -0
- package/skills/engineering/rush-release/agents/openai.yaml +8 -0
- package/skills/engineering/rush-release/release-check.mjs +93 -0
- package/skills/engineering/test-quality/LICENSE +3 -0
- package/skills/engineering/test-quality/SKILL.md +79 -0
- package/skills/engineering/test-quality/references/audit.md +80 -0
- package/skills/engineering/test-quality/references/lint.md +24 -0
- package/skills/engineering/test-quality/references/principles.md +143 -0
- package/skills/engineering/test-quality/references/testing-conventions.md +9 -0
- package/skills/engineering/test-quality/release-check.mjs +93 -0
- package/skills/engineering/test-quality/scripts/lint-tests.mjs +574 -0
- package/skills/engineering/test-quality/scripts/lint-tests.test.mjs +591 -0
- package/skills/engineering/test-quality/scripts/mock-policy-config.mjs +176 -0
- package/skills/engineering/test-quality/scripts/mock-policy-config.test.mjs +119 -0
- package/skills/engineering/test-quality/scripts/persist-conventions-core.mjs +117 -0
- package/skills/engineering/test-quality/scripts/persist-conventions.mjs +17 -0
- package/skills/engineering/test-quality/scripts/persist-conventions.test.mjs +104 -0
- package/skills/risk-compliance/compliance-audit/LICENSE +3 -0
- package/skills/risk-compliance/compliance-audit/SKILL.md +439 -0
- package/skills/risk-compliance/compliance-audit/references/findings-schema.md +293 -0
- package/skills/risk-compliance/compliance-audit/references/normalization-rules.md +45 -0
- package/skills/risk-compliance/compliance-audit/references/vars-checklist.md +1068 -0
- package/skills/risk-compliance/compliance-audit/references/vars-findings-schema.md +398 -0
- package/skills/risk-compliance/compliance-audit/release-check.mjs +93 -0
- package/skills/risk-compliance/compliance-audit/scripts/compliance-audit.test.mjs +14 -0
- package/skills/risk-compliance/compliance-audit/scripts/export_tracker_xlsx.py +221 -0
- package/skills/risk-compliance/compliance-audit/scripts/reconcile_tracker.py +458 -0
- package/skills/risk-compliance/compliance-audit/scripts/test_export_tracker_xlsx.py +97 -0
- package/skills/risk-compliance/compliance-audit/scripts/test_validate_tracker.py +157 -0
- package/skills/risk-compliance/compliance-audit/scripts/validate_tracker.py +829 -0
- package/skills/risk-compliance/contract-compare/LICENSE +3 -0
- package/skills/risk-compliance/contract-compare/SKILL.md +199 -0
- package/skills/risk-compliance/contract-compare/references/comparison-dimensions.md +209 -0
- package/skills/risk-compliance/contract-compare/release-check.mjs +93 -0
- package/skills/risk-compliance/contract-review/LICENSE +3 -0
- package/skills/risk-compliance/contract-review/SKILL.md +240 -0
- package/skills/risk-compliance/contract-review/references/review-criteria.md +223 -0
- package/skills/risk-compliance/contract-review/release-check.mjs +93 -0
- package/skills/risk-compliance/doc-authenticity/LICENSE +3 -0
- package/skills/risk-compliance/doc-authenticity/SKILL.md +160 -0
- package/skills/risk-compliance/doc-authenticity/agents/openai.yaml +8 -0
- package/skills/risk-compliance/doc-authenticity/manual-invocation.test.mjs +12 -0
- package/skills/risk-compliance/doc-authenticity/references/agreement.md +66 -0
- package/skills/risk-compliance/doc-authenticity/references/bank-statement.md +150 -0
- package/skills/risk-compliance/doc-authenticity/references/forensic-signals.md +157 -0
- package/skills/risk-compliance/doc-authenticity/references/image-signals.md +114 -0
- package/skills/risk-compliance/doc-authenticity/references/invoice.md +66 -0
- package/skills/risk-compliance/doc-authenticity/references/issuer-profiles.md +86 -0
- package/skills/risk-compliance/doc-authenticity/references/verdict-and-report.md +95 -0
- package/skills/risk-compliance/doc-authenticity/references/verdict.schema.json +81 -0
- package/skills/risk-compliance/doc-authenticity/release-check.mjs +93 -0
- package/skills/risk-compliance/doc-authenticity/scripts/image_forensics.py +294 -0
- package/skills/risk-compliance/doc-authenticity/scripts/pdf_forensics.py +401 -0
- package/skills/risk-compliance/doc-authenticity/scripts/validate_codes.py +224 -0
- package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.py +308 -0
- package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.test.mjs +212 -0
- package/skills/risk-compliance/fraud-analysis/LICENSE +3 -0
- package/skills/risk-compliance/fraud-analysis/SKILL.md +114 -0
- package/skills/risk-compliance/fraud-analysis/references/archetypes/authorized-payment-scam.md +41 -0
- package/skills/risk-compliance/fraud-analysis/references/archetypes/first-party-fraud.md +44 -0
- package/skills/risk-compliance/fraud-analysis/references/archetypes/third-party-fraud.md +27 -0
- package/skills/risk-compliance/fraud-analysis/references/contexts/bank-transfer.md +24 -0
- package/skills/risk-compliance/fraud-analysis/references/contexts/card-payment.md +30 -0
- package/skills/risk-compliance/fraud-analysis/references/contexts/payment-collection.md +20 -0
- package/skills/risk-compliance/fraud-analysis/references/contexts/payout.md +20 -0
- package/skills/risk-compliance/fraud-analysis/references/feature-engineering.md +158 -0
- package/skills/risk-compliance/fraud-analysis/references/mechanisms/account-takeover.md +36 -0
- package/skills/risk-compliance/fraud-analysis/references/report-rationale.md +45 -0
- package/skills/risk-compliance/fraud-analysis/references/report-template.md +190 -0
- package/skills/risk-compliance/fraud-analysis/references/review-checklist.md +175 -0
- package/skills/risk-compliance/fraud-analysis/references/taxonomy.md +79 -0
- package/skills/risk-compliance/fraud-analysis/references/terminology.md +108 -0
- package/skills/risk-compliance/fraud-analysis/references/workflow.md +175 -0
- package/skills/risk-compliance/fraud-analysis/release-check.mjs +93 -0
- package/skills/risk-compliance/payment-analysis/LICENSE +3 -0
- package/skills/risk-compliance/payment-analysis/SKILL.md +180 -0
- package/skills/risk-compliance/payment-analysis/references/auth-rate-actions.md +30 -0
- package/skills/risk-compliance/payment-analysis/references/chargebacks.md +208 -0
- package/skills/risk-compliance/payment-analysis/references/chinese-polish.md +51 -0
- package/skills/risk-compliance/payment-analysis/references/event-layers.md +127 -0
- package/skills/risk-compliance/payment-analysis/references/fx.md +59 -0
- package/skills/risk-compliance/payment-analysis/references/journey.md +79 -0
- package/skills/risk-compliance/payment-analysis/references/metrics.md +67 -0
- package/skills/risk-compliance/payment-analysis/references/report-template.md +119 -0
- package/skills/risk-compliance/payment-analysis/references/terminology.md +95 -0
- package/skills/risk-compliance/payment-analysis/references/visualization.md +56 -0
- package/skills/risk-compliance/payment-analysis/release-check.mjs +93 -0
- package/skills/risk-compliance/report-writing/LICENSE +3 -0
- package/skills/risk-compliance/report-writing/SKILL.md +220 -0
- package/skills/risk-compliance/report-writing/references/audit-report-template.md +121 -0
- package/skills/risk-compliance/report-writing/references/external-assessment-template.md +167 -0
- package/skills/risk-compliance/report-writing/references/management-report-template.md +145 -0
- package/skills/risk-compliance/report-writing/references/vars-report-template.md +224 -0
- package/skills/risk-compliance/report-writing/release-check.mjs +93 -0
- package/skills/risk-compliance/similar-domain/LICENSE +3 -0
- package/skills/risk-compliance/similar-domain/SKILL.md +203 -0
- package/skills/risk-compliance/similar-domain/references/entities.md +67 -0
- package/skills/risk-compliance/similar-domain/references/fingerprints.md +134 -0
- package/skills/risk-compliance/similar-domain/references/pivots.md +82 -0
- package/skills/risk-compliance/similar-domain/references/report-template.md +93 -0
- package/skills/risk-compliance/similar-domain/references/scoring.md +130 -0
- package/skills/risk-compliance/similar-domain/references/terminology.md +41 -0
- package/skills/risk-compliance/similar-domain/references/workflow.md +85 -0
- package/skills/risk-compliance/similar-domain/release-check.mjs +93 -0
- package/skills/risk-compliance/tld-detection/LICENSE +3 -0
- package/skills/risk-compliance/tld-detection/SKILL.md +245 -0
- package/skills/risk-compliance/tld-detection/agents/openai.yaml +9 -0
- package/skills/risk-compliance/tld-detection/references/chinese-polish.md +55 -0
- package/skills/risk-compliance/tld-detection/references/clean.md +51 -0
- package/skills/risk-compliance/tld-detection/references/clustering.md +235 -0
- package/skills/risk-compliance/tld-detection/references/decision.md +189 -0
- package/skills/risk-compliance/tld-detection/references/fx-conversion.md +232 -0
- package/skills/risk-compliance/tld-detection/references/report-template.md +272 -0
- package/skills/risk-compliance/tld-detection/references/streams.md +141 -0
- package/skills/risk-compliance/tld-detection/references/terminology.md +95 -0
- package/skills/risk-compliance/tld-detection/references/workflow.md +95 -0
- package/skills/risk-compliance/tld-detection/release-check.mjs +93 -0
- package/skills/risk-compliance/tld-detection/scripts/detect_ladders.py +390 -0
- package/skills/risk-compliance/tld-detection/scripts/detect_ladders.test.mjs +14 -0
- package/skills/risk-compliance/tld-detection/scripts/detect_streams.py +650 -0
- package/skills/risk-compliance/tld-detection/scripts/detect_streams.test.mjs +14 -0
- package/skills/risk-compliance/tld-detection/tests/test_detect_ladders.py +134 -0
- package/skills/risk-compliance/tld-detection/tests/test_detect_streams.py +520 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
// ci-run-audit core — audit a repo's GitHub Actions *run history* for waste. Report-only:
|
|
2
|
+
// it never edits a workflow, and every finding is a candidate for a human to judge.
|
|
3
|
+
//
|
|
4
|
+
// Where ensure-coverage's ci-audit.mjs reads CI *config* (are the gates wired?), this
|
|
5
|
+
// reads what CI actually *did* (what did it cost, and what did it catch?). Six checks:
|
|
6
|
+
//
|
|
7
|
+
// 1. re-proved trees a job passing again on a byte-identical tree (same git tree
|
|
8
|
+
// hash) it already passed: PR → queue → main re-runs
|
|
9
|
+
// 2. catch yield per job: code states it failed on (and never passed), how many of
|
|
10
|
+
// those were its alone, and how many were flakes (failed, then
|
|
11
|
+
// passed on the same tree)
|
|
12
|
+
// 3. overlap job A whose every real failure coincided with job B failing
|
|
13
|
+
// 4. setup share jobs whose time is mostly overhead: everything but their own
|
|
14
|
+
// work steps (checkout, install, containers, runner gaps)
|
|
15
|
+
// 5. critical path PR wait time p50/p90 and which job finishes last
|
|
16
|
+
// 6. stuck + queued jobs that ran past 6h, and time waiting for a runner
|
|
17
|
+
//
|
|
18
|
+
// Usage:
|
|
19
|
+
// node ci-run-audit.mjs --repo <owner/name> [--runs 300] [--save runs.json] [--json]
|
|
20
|
+
// node ci-run-audit.mjs --from runs.json [--json] # re-analyse a saved pull
|
|
21
|
+
// node ci-run-audit.mjs --repo <owner/name> --profile-run <runId> [--save runs.json] [--json]
|
|
22
|
+
// node ci-run-audit.mjs --from runs.json --profile-run <runId> [--json]
|
|
23
|
+
// Options: --ignore-job <regex> (added to the default aggregator filter),
|
|
24
|
+
// --min-trees <n> (sample floor for a "no sole catch" candidate, default 20).
|
|
25
|
+
// Needs an authenticated `gh`. Exit 0 with a report; 2 on usage or fetch errors.
|
|
26
|
+
//
|
|
27
|
+
// Import-only: `ci-run-audit.mjs` is the always-running CLI wrapper, so no main-module
|
|
28
|
+
// detection lives here (skills/main-guard-hygiene.test.mjs).
|
|
29
|
+
|
|
30
|
+
import { execFile } from 'node:child_process'
|
|
31
|
+
import { promisify } from 'node:util'
|
|
32
|
+
import fs from 'node:fs'
|
|
33
|
+
|
|
34
|
+
const run = promisify(execFile)
|
|
35
|
+
|
|
36
|
+
// Aggregator / umbrella jobs only restate other jobs' results; counting them would make
|
|
37
|
+
// every real failure look shared. Extend per repo with --ignore-job.
|
|
38
|
+
// "PR admission gate" is a roll-up; "smoke-gate" and "route-docs-gate" are real checks.
|
|
39
|
+
export const DEFAULT_IGNORE = /^(all-gates|ci|.*\b(umbrella|verdict|timing report)\b.*|.* gate)$/i
|
|
40
|
+
// Step names that are runner/setup overhead rather than the job's own check.
|
|
41
|
+
export const SETUP_STEP = /^(set up job|initiali[sz]e containers|stop containers|complete job|post |run actions\/(checkout|setup-|cache)|checkout|check out|setup |set up |install |restore |.*\bcache\b|.*pnpm\/action-setup|.*corepack)/i
|
|
42
|
+
const STUCK_MIN = 360 // GitHub's hosted-job ceiling; anything past it hung
|
|
43
|
+
|
|
44
|
+
const min = (a, b) => (Date.parse(b) - Date.parse(a)) / 60000
|
|
45
|
+
const median = (xs) => { if (!xs.length) return 0; const s = [...xs].sort((a, b) => a - b); const m = s.length >> 1; return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2 }
|
|
46
|
+
const pct = (xs, q) => { if (!xs.length) return 0; const s = [...xs].sort((a, b) => a - b); return s[Math.floor(q * (s.length - 1))] }
|
|
47
|
+
|
|
48
|
+
// Which lane a run belongs to. Merge-queue branches are recognised for Mergify and
|
|
49
|
+
// GitHub's native queue; everything else falls back to the event name.
|
|
50
|
+
export function lane(r) {
|
|
51
|
+
const b = r.branch || ''
|
|
52
|
+
if (b.startsWith('mergify/') || b.startsWith('gh-readonly-queue/')) return 'queue'
|
|
53
|
+
if (r.event === 'pull_request' || r.event === 'pull_request_target') return 'pr'
|
|
54
|
+
if (r.event === 'push' && (b === r.defaultBranch || b === 'main' || b === 'master')) return 'main'
|
|
55
|
+
return r.event
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const timed = (j) => j.started && j.completed && j.conclusion !== 'skipped'
|
|
59
|
+
const dur = (j) => min(j.started, j.completed)
|
|
60
|
+
const workflowKey = (r) => JSON.stringify(r.workflowId != null ? ['id', r.workflowId]
|
|
61
|
+
: r.workflowPath ? ['path', r.workflowPath] : ['name', r.workflow])
|
|
62
|
+
const jobKey = (r, j) => JSON.stringify([workflowKey(r), j.name])
|
|
63
|
+
|
|
64
|
+
export function analyze(runs, { ignore = DEFAULT_IGNORE, minTrees = 20 } = {}) {
|
|
65
|
+
runs = [...runs].sort((a, b) => Date.parse(a.created) - Date.parse(b.created))
|
|
66
|
+
const jobsOf = (r) => r.jobs.filter((j) => timed(j) && dur(j) >= 0)
|
|
67
|
+
const gaps = dataGaps(runs)
|
|
68
|
+
const owners = new Map(), labels = new Map()
|
|
69
|
+
for (const r of runs) for (const j of r.jobs) {
|
|
70
|
+
if (!owners.has(j.name)) owners.set(j.name, new Set())
|
|
71
|
+
owners.get(j.name).add(workflowKey(r))
|
|
72
|
+
}
|
|
73
|
+
for (const r of runs) for (const j of r.jobs) labels.set(jobKey(r, j),
|
|
74
|
+
owners.get(j.name).size > 1 ? `${r.workflow} [${r.workflowId ?? r.workflowPath ?? r.workflow}] / ${j.name}` : j.name)
|
|
75
|
+
|
|
76
|
+
// 1. Re-proved trees. Keyed by workflow + job + tree, so a PR-only job and a main-only
|
|
77
|
+
// job never collide. Scheduled re-runs are deliberate (drift, dates, upstream), so they
|
|
78
|
+
// are reported apart and not counted as waste.
|
|
79
|
+
const firstPass = new Map()
|
|
80
|
+
let passMin = 0, reprovedMin = 0, scheduledRepeatMin = 0
|
|
81
|
+
const byLanePair = {}, byJob = {}
|
|
82
|
+
for (const r of runs) {
|
|
83
|
+
for (const j of jobsOf(r)) {
|
|
84
|
+
if (j.conclusion !== 'success' || dur(j) > STUCK_MIN) continue
|
|
85
|
+
const m = dur(j); passMin += m
|
|
86
|
+
const k = JSON.stringify([jobKey(r, j), r.tree])
|
|
87
|
+
if (!r.tree || !firstPass.has(k)) { if (r.tree) firstPass.set(k, lane(r)); continue }
|
|
88
|
+
if (lane(r) === 'schedule') { scheduledRepeatMin += m; continue }
|
|
89
|
+
reprovedMin += m
|
|
90
|
+
const pair = `${firstPass.get(k)}→${lane(r)}`
|
|
91
|
+
byLanePair[pair] = (byLanePair[pair] || 0) + m
|
|
92
|
+
const label = labels.get(jobKey(r, j))
|
|
93
|
+
byJob[label] = (byJob[label] || 0) + m
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 2 + 3. Yield and overlap, per code state (tree; sha if the tree is unknown). A job
|
|
98
|
+
// "really failed" on a tree only if it never passed there — fail-then-pass is a flake.
|
|
99
|
+
const states = new Map()
|
|
100
|
+
for (const r of runs) {
|
|
101
|
+
const key = r.tree ? `tree:${r.tree}` : r.sha ? `sha:${r.sha}` : r
|
|
102
|
+
if (!states.has(key)) states.set(key, new Map())
|
|
103
|
+
const s = states.get(key)
|
|
104
|
+
for (const j of r.jobs) {
|
|
105
|
+
if (!['success', 'failure'].includes(j.conclusion) || ignore.test(j.name)) continue
|
|
106
|
+
const identity = jobKey(r, j)
|
|
107
|
+
if (!s.has(identity)) s.set(identity, new Set())
|
|
108
|
+
s.get(identity).add(j.conclusion)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const y = new Map()
|
|
112
|
+
const Y = (n) => {
|
|
113
|
+
if (!y.has(n)) y.set(n, { job: labels.get(n), trees: 0, failed: 0, sole: 0, flaky: 0, coFail: {} })
|
|
114
|
+
return y.get(n)
|
|
115
|
+
}
|
|
116
|
+
for (const s of states.values()) {
|
|
117
|
+
const real = [...s].filter(([, c]) => c.has('failure') && !c.has('success')).map(([n]) => n)
|
|
118
|
+
for (const [n, c] of s) {
|
|
119
|
+
const e = Y(n); e.trees++
|
|
120
|
+
if (c.has('failure') && c.has('success')) e.flaky++
|
|
121
|
+
}
|
|
122
|
+
for (const n of real) {
|
|
123
|
+
const e = Y(n); e.failed++
|
|
124
|
+
if (real.length === 1) e.sole++
|
|
125
|
+
for (const o of real) if (o !== n) e.coFail[o] = (e.coFail[o] || 0) + 1
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const yieldEntries = [...y.values()].sort((a, b) => b.trees - a.trees)
|
|
129
|
+
const yieldRows = yieldEntries.map((e) => ({ ...e,
|
|
130
|
+
coFail: Object.fromEntries(Object.entries(e.coFail).map(([n, count]) => [labels.get(n), count])),
|
|
131
|
+
}))
|
|
132
|
+
const noSoleCatch = yieldRows.filter((e) => e.trees >= minTrees && e.sole === 0)
|
|
133
|
+
const overlap = []
|
|
134
|
+
for (const e of yieldEntries) {
|
|
135
|
+
if (e.failed < 3) continue
|
|
136
|
+
for (const [o, n] of Object.entries(e.coFail)) {
|
|
137
|
+
if (n === e.failed) overlap.push({ job: e.job, alwaysWith: labels.get(o), failures: e.failed, otherFailures: y.get(o).failed })
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 4. Setup share, per workflow/job, over passing runs (a failed step skews the ratio).
|
|
142
|
+
// Measured as 1 − own-work / job time: in a short job much of the overhead sits
|
|
143
|
+
// *between* steps (runner start, post-steps), so summing named setup steps undercounts.
|
|
144
|
+
const setup = {}
|
|
145
|
+
for (const r of runs) for (const j of r.jobs) {
|
|
146
|
+
if (j.conclusion !== 'success' || dur(j) > STUCK_MIN || ignore.test(j.name)) continue
|
|
147
|
+
const timing = jobTiming(j)
|
|
148
|
+
if (!timing.completeTiming) {
|
|
149
|
+
gaps.timings.push({ runId: r.id, job: labels.get(jobKey(r, j)), reason: 'setup/work unavailable: missing, invalid, or inconsistent step/job timestamps' })
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
const e = (setup[jobKey(r, j)] ||= { job: labels.get(jobKey(r, j)), n: 0, totalS: 0, setupS: 0, durs: [] })
|
|
153
|
+
e.n++; e.totalS += timing.elapsedS; e.setupS += timing.setupS; e.durs.push(timing.elapsedS / 60)
|
|
154
|
+
}
|
|
155
|
+
const setupRows = Object.values(setup)
|
|
156
|
+
.map((e) => ({ job: e.job, runs: e.n, medianMin: median(e.durs), setupShare: e.totalS ? e.setupS / e.totalS : 0, setupMin: e.setupS / 60 }))
|
|
157
|
+
.filter((e) => e.runs >= 3 && e.setupShare >= 0.7)
|
|
158
|
+
.sort((a, b) => b.setupMin - a.setupMin)
|
|
159
|
+
|
|
160
|
+
// 5. Critical path on the PR lane: per head sha, first run created → last job done.
|
|
161
|
+
const prBySha = new Map()
|
|
162
|
+
for (const r of runs) if (lane(r) === 'pr') {
|
|
163
|
+
const key = r.sha ?? r
|
|
164
|
+
if (!prBySha.has(key)) prBySha.set(key, [])
|
|
165
|
+
prBySha.get(key).push(r)
|
|
166
|
+
}
|
|
167
|
+
const waits = [], lastJob = {}
|
|
168
|
+
for (const rs of prBySha.values()) {
|
|
169
|
+
// Roll-up jobs finish last by construction; the job that set the wait is the last real one.
|
|
170
|
+
const js = rs.flatMap((r) => jobsOf(r).map((j) => ({ ...j, label: labels.get(jobKey(r, j)) })))
|
|
171
|
+
.filter((j) => dur(j) <= STUCK_MIN && !ignore.test(j.name))
|
|
172
|
+
if (!js.length) continue
|
|
173
|
+
const start = rs.map((r) => r.created).filter((t) => t && Number.isFinite(Date.parse(t)))
|
|
174
|
+
.sort((a, b) => Date.parse(a) - Date.parse(b))[0]
|
|
175
|
+
const last = js.reduce((a, j) => (Date.parse(j.completed) > Date.parse(a.completed) ? j : a))
|
|
176
|
+
const interval = seconds(start, last.completed)
|
|
177
|
+
if (interval === null || rs.some((r) => !r.created || !Number.isFinite(Date.parse(r.created)))) {
|
|
178
|
+
gaps.timings.push({ runId: rs[0].id, job: last.label, reason: 'PR creation-to-completion interval unavailable' })
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
waits.push(interval / 60)
|
|
182
|
+
lastJob[last.label] = (lastJob[last.label] || 0) + 1
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 6. Stuck jobs and runner queue time.
|
|
186
|
+
const stuck = [], queued = []
|
|
187
|
+
for (const r of runs) for (const j of r.jobs) {
|
|
188
|
+
if (j.started && j.completed && dur(j) > STUCK_MIN) stuck.push({ workflow: r.workflow, job: j.name, hours: dur(j) / 60, conclusion: j.conclusion, started: j.started, branch: r.branch })
|
|
189
|
+
if (j.conclusion === 'skipped') continue
|
|
190
|
+
const interval = seconds(j.created, j.started)
|
|
191
|
+
if (interval !== null) queued.push(interval / 60)
|
|
192
|
+
else gaps.timings.push({ runId: r.id, job: labels.get(jobKey(r, j)), reason: 'created-to-start interval unavailable' })
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
minTrees,
|
|
197
|
+
dataGaps: gaps,
|
|
198
|
+
window: { runs: runs.length, from: runs[0]?.created ?? null, to: runs.at(-1)?.created ?? null },
|
|
199
|
+
reproved: { identity: 'run-metadata-tree-candidates', testedCheckoutVerified: false,
|
|
200
|
+
passMin, reprovedMin, share: passMin ? reprovedMin / passMin : 0, scheduledRepeatMin, byLanePair, byJob },
|
|
201
|
+
yield: yieldRows, noSoleCatch, overlap,
|
|
202
|
+
setup: setupRows,
|
|
203
|
+
criticalPath: { prCommits: waits.length, p50: waits.length ? median(waits) : null, p90: waits.length ? pct(waits, 0.9) : null, lastJob },
|
|
204
|
+
stuck, queue: { samples: queued.length, p50: queued.length ? median(queued) : null, p90: queued.length ? pct(queued, 0.9) : null },
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function dataGaps(runs) {
|
|
209
|
+
return {
|
|
210
|
+
unknownTrees: runs.filter((r) => !r.tree).map((r) => ({
|
|
211
|
+
runId: r.id, sha: r.sha ?? null, reason: r.treeUnknownReason ?? 'tree not recorded in saved data',
|
|
212
|
+
})),
|
|
213
|
+
testedCheckouts: runs.filter((r) => r.tree).map((r) => ({
|
|
214
|
+
runId: r.id, reason: 'Run tree metadata does not establish the tested checkout; PR synthetic merges and explicit checkout refs may differ. Matches are candidates, not reusable gate proof.',
|
|
215
|
+
})),
|
|
216
|
+
collection: runs.flatMap((r) => (r.collectionGaps || []).map((gap) => ({ runId: r.id, ...gap }))),
|
|
217
|
+
timings: [],
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const seconds = (start, end) => {
|
|
222
|
+
if (!start || !end) return null
|
|
223
|
+
const s = (Date.parse(end) - Date.parse(start)) / 1000
|
|
224
|
+
return Number.isFinite(s) && s >= 0 ? s : null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function jobTiming(j) {
|
|
228
|
+
const steps = (j.steps || []).map((s) => ({
|
|
229
|
+
name: s.name, number: s.number ?? null, conclusion: s.conclusion ?? null,
|
|
230
|
+
started: s.started ?? null, completed: s.completed ?? null,
|
|
231
|
+
durationS: s.conclusion === 'skipped' ? 0 : seconds(s.started, s.completed),
|
|
232
|
+
kind: SETUP_STEP.test(s.name) ? 'setup-heuristic' : 'work',
|
|
233
|
+
})).sort((a, b) => (b.durationS ?? -1) - (a.durationS ?? -1))
|
|
234
|
+
const elapsedS = seconds(j.started, j.completed)
|
|
235
|
+
const intervals = steps.filter((s) => s.durationS > 0).sort((a, b) => Date.parse(a.started) - Date.parse(b.started))
|
|
236
|
+
const nonOverlapping = intervals.every((s, i) => i === 0 || Date.parse(s.started) >= Date.parse(intervals[i - 1].completed))
|
|
237
|
+
const measuredWork = steps.filter((s) => s.kind === 'work').reduce((n, s) => n + (s.durationS ?? 0), 0)
|
|
238
|
+
const completeTiming = steps.length > 0 && steps.every((s) => s.durationS !== null &&
|
|
239
|
+
(s.conclusion === 'skipped' || (seconds(j.started, s.started) !== null && seconds(s.completed, j.completed) !== null))) &&
|
|
240
|
+
elapsedS !== null && measuredWork <= elapsedS && nonOverlapping
|
|
241
|
+
return { steps, elapsedS, completeTiming, workS: completeTiming ? measuredWork : null,
|
|
242
|
+
setupS: completeTiming ? Math.max(0, elapsedS - measuredWork) : null }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Setup is a name-based heuristic: elapsed minus timed non-setup work includes gaps.
|
|
246
|
+
// REST timestamps do not expose DAG eligibility or the instant a runner was requested.
|
|
247
|
+
export function profileRun(runs, runId) {
|
|
248
|
+
const matches = runs.filter((r) => String(r.id) === String(runId))
|
|
249
|
+
if (matches.length !== 1) throw new Error(matches.length ? `multiple records for run ${runId}` : `run ${runId} not found in saved data`)
|
|
250
|
+
const r = matches[0]
|
|
251
|
+
const jobs = r.jobs.map((j) => {
|
|
252
|
+
const { steps, elapsedS, completeTiming, workS, setupS } = jobTiming(j)
|
|
253
|
+
return {
|
|
254
|
+
id: j.id ?? null, name: j.name, attempt: j.attempt ?? r.attempt ?? null,
|
|
255
|
+
url: j.url ?? null, runUrl: j.runUrl ?? r.url ?? null, conclusion: j.conclusion ?? null,
|
|
256
|
+
status: j.status ?? null, created: j.created ?? null, started: j.started ?? null, completed: j.completed ?? null,
|
|
257
|
+
runnerId: j.runnerId ?? null, runnerName: j.runnerName ?? null,
|
|
258
|
+
runnerGroupId: j.runnerGroupId ?? null, runnerGroupName: j.runnerGroupName ?? null,
|
|
259
|
+
runnerLabels: j.runnerLabels ?? null,
|
|
260
|
+
dispatchDelayS: seconds(r.created, j.created), queueS: seconds(j.created, j.started),
|
|
261
|
+
elapsedS, workS, setupS,
|
|
262
|
+
timingGaps: [
|
|
263
|
+
...(!completeTiming ? ['setup/work unavailable: missing, invalid, or inconsistent step/job timestamps'] : []),
|
|
264
|
+
...(seconds(j.created, j.started) === null ? ['queue interval unavailable'] : []),
|
|
265
|
+
...(seconds(r.created, j.created) === null ? ['dispatch interval unavailable'] : []),
|
|
266
|
+
],
|
|
267
|
+
steps,
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
return {
|
|
271
|
+
mode: 'profile', runId: r.id, attempt: r.attempt ?? null, runUrl: r.url ?? null,
|
|
272
|
+
workflow: r.workflow, workflowId: r.workflowId ?? null, workflowPath: r.workflowPath ?? null,
|
|
273
|
+
event: r.event, branch: r.branch, sha: r.sha ?? null, tree: r.tree ?? null,
|
|
274
|
+
status: r.status ?? null, conclusion: r.conclusion ?? null,
|
|
275
|
+
created: r.created ?? null, started: r.started ?? null, dataGaps: dataGaps([r]),
|
|
276
|
+
timingNotes: {
|
|
277
|
+
units: 'seconds; null means unknown',
|
|
278
|
+
setup: 'Heuristic: job elapsed minus timed non-setup steps; includes setup, teardown and inter-step gaps. Step names classify setup, not measured resource use.',
|
|
279
|
+
queue: 'Job created to started is an observed delay, not proven runner queue time; REST does not separate runner availability, dispatch or DAG dependencies.',
|
|
280
|
+
dispatch: 'Run created to job created may include dispatch/DAG delay. On reruns it can include time before the selected attempt; it is not runner queue time.',
|
|
281
|
+
scope: 'Latest recorded attempt only; no logs, billing or workflow DAG collected. Recommendations only; preserve required gates.',
|
|
282
|
+
},
|
|
283
|
+
jobs,
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function renderProfile(p) {
|
|
288
|
+
const show = (s) => s === null ? 'unknown' : `${s.toFixed(1)}s`
|
|
289
|
+
const L = [`# CI run profile: ${p.workflow} / ${p.runId}`, '',
|
|
290
|
+
`Attempt: ${p.attempt ?? 'unknown'}; conclusion: ${p.conclusion ?? 'unknown'}; run: ${p.runUrl ?? 'unknown'}.`, '',
|
|
291
|
+
...Object.values(p.timingNotes), '',
|
|
292
|
+
'| job | dispatch/DAG delay | created-to-start delay (queue uncertain) | setup heuristic | work | job elapsed | conclusion | runner |',
|
|
293
|
+
'|---|---|---|---|---|---|---|---|']
|
|
294
|
+
for (const j of p.jobs) L.push(`| ${j.name} | ${show(j.dispatchDelayS)} | ${show(j.queueS)} | ${show(j.setupS)} | ${show(j.workS)} | ${show(j.elapsedS)} | ${j.conclusion ?? 'unknown'} | ${j.runnerName ?? 'unknown'} (${j.runnerLabels?.join(', ') ?? 'labels unknown'}) |`)
|
|
295
|
+
for (const j of p.jobs) {
|
|
296
|
+
L.push('', `## ${j.name} (attempt ${j.attempt ?? 'unknown'})`, '', `Job: ${j.url ?? 'unknown'}; run: ${j.runUrl ?? 'unknown'}.`)
|
|
297
|
+
for (const gap of j.timingGaps) L.push(`- ${gap}`)
|
|
298
|
+
L.push('', '| step (longest first) | duration | classification | conclusion |', '|---|---|---|---|')
|
|
299
|
+
for (const s of j.steps) L.push(`| ${s.name} | ${show(s.durationS)} | ${s.kind} | ${s.conclusion ?? 'unknown'} |`)
|
|
300
|
+
}
|
|
301
|
+
appendGaps(L, p.dataGaps)
|
|
302
|
+
return L.join('\n')
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function appendGaps(L, gaps) {
|
|
306
|
+
if (!gaps.unknownTrees.length && !gaps.testedCheckouts.length && !gaps.collection.length && !gaps.timings.length) return
|
|
307
|
+
L.push('', '## Data gaps', '')
|
|
308
|
+
L.push(`${gaps.unknownTrees.length} run(s) have no verified tree; excluded from identical-tree repeat evidence. Catch yield falls back to commit SHA, not verified tree equality.`)
|
|
309
|
+
for (const g of gaps.unknownTrees) L.push(`- Run ${g.runId}, SHA ${g.sha ?? 'unknown'}: ${g.reason}`)
|
|
310
|
+
if (gaps.testedCheckouts.length) L.push(`- ${gaps.testedCheckouts.length} run(s): ${gaps.testedCheckouts[0].reason}`)
|
|
311
|
+
for (const g of gaps.collection) L.push(`- Run ${g.runId}: ${g.reason}`)
|
|
312
|
+
if (gaps.timings.length) L.push(`- ${gaps.timings.length} unavailable timing measurement(s); excluded rather than counted as zero.`)
|
|
313
|
+
for (const g of gaps.timings.slice(0, 6)) L.push(`- Run ${g.runId}, ${g.job}: ${g.reason}`)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const f1 = (x) => x.toFixed(1)
|
|
317
|
+
const minutes = (m) => m === null ? 'unknown' : `${f1(m)}m`
|
|
318
|
+
const top = (o, n = 6) => Object.entries(o).sort((a, b) => b[1] - a[1]).slice(0, n)
|
|
319
|
+
|
|
320
|
+
export function render(a, repo = '') {
|
|
321
|
+
const L = []
|
|
322
|
+
L.push(`# CI run audit${repo ? ` — ${repo}` : ''}`, '')
|
|
323
|
+
L.push(`${a.window.runs} completed runs, ${a.window.from?.slice(0, 10)} → ${a.window.to?.slice(0, 10)}. Report-only: each row is a candidate with its evidence, not a verdict.`, '')
|
|
324
|
+
|
|
325
|
+
L.push('## 1. Re-proved trees', '')
|
|
326
|
+
L.push(`**${f1(a.reproved.reprovedMin)} of ${f1(a.reproved.passMin)} passing job-minutes (${Math.round(a.reproved.share * 100)}%)** matched prior workflow/job run-tree metadata; these are repeat candidates, not verified repeated tested checkouts.` +
|
|
327
|
+
(a.reproved.scheduledRepeatMin ? ` Scheduled repeats (${f1(a.reproved.scheduledRepeatMin)} min) are left out as deliberate.` : ''), '')
|
|
328
|
+
if (top(a.reproved.byLanePair).length) {
|
|
329
|
+
L.push('| first passed → again on | minutes |', '|---|---|')
|
|
330
|
+
for (const [k, v] of top(a.reproved.byLanePair)) L.push(`| ${k} | ${f1(v)} |`)
|
|
331
|
+
L.push('', '| job | re-proved minutes |', '|---|---|')
|
|
332
|
+
for (const [k, v] of top(a.reproved.byJob)) L.push(`| ${k} | ${f1(v)} |`)
|
|
333
|
+
L.push('', 'Inspect actual checkout refs and execution inputs before proposing reuse. A PR head tree can differ from its tested synthetic merge. Changed combined queue trees still need proof; main reuse requires complete trusted queue evidence. Date- or network-dependent jobs can legitimately repeat.')
|
|
334
|
+
}
|
|
335
|
+
L.push('')
|
|
336
|
+
|
|
337
|
+
L.push('## 2. Catch yield', '')
|
|
338
|
+
L.push('Per metadata code state (resolved run tree, otherwise commit SHA only; tested checkout unverified). *failed* = failed and never passed on that state; *sole* = the only job that failed there; *flaky* = failed and passed on the same state, an instability lead rather than a diagnosed test flake.', '')
|
|
339
|
+
L.push('| job | trees | failed | sole | flaky | fails most often with |', '|---|---|---|---|---|---|')
|
|
340
|
+
for (const e of a.yield) {
|
|
341
|
+
const w = top(e.coFail, 1)[0]
|
|
342
|
+
L.push(`| ${e.job} | ${e.trees} | ${e.failed} | ${e.sole} | ${e.flaky} | ${w ? `${w[0]} ×${w[1]}` : ''} |`)
|
|
343
|
+
}
|
|
344
|
+
L.push('')
|
|
345
|
+
if (a.noSoleCatch.length) {
|
|
346
|
+
L.push(`**No sole catch** in this window: ${a.noSoleCatch.map((e) => `\`${e.job}\` (${e.trees} trees${e.failed ? `, ${e.failed} shared failures` : ', never failed'})`).join(', ')}.`)
|
|
347
|
+
L.push('Candidates to merge into another job, move off the PR lane, or retire — unless the job guards something rare and costly (a security or money path), where zero catches is the point. Widen the window before deciding.', '')
|
|
348
|
+
} else if (!a.yield.some((e) => e.trees >= a.minTrees)) {
|
|
349
|
+
L.push(`No job ran on ${a.minTrees}+ code states, too few to call any job zero-yield. Widen the window (\`--runs\`) or lower \`--min-trees\`.`, '')
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
L.push('## 3. Overlap', '')
|
|
353
|
+
if (!a.overlap.length) L.push('No job with ≥3 failures whose every failure coincided with another job failing.', '')
|
|
354
|
+
else {
|
|
355
|
+
L.push('| job | every failure also failed | its failures | other job\'s failures |', '|---|---|---|---|')
|
|
356
|
+
for (const o of a.overlap) L.push(`| ${o.job} | ${o.alwaysWith} | ${o.failures} | ${o.otherFailures} |`)
|
|
357
|
+
L.push('', 'Either the two test the same thing, or both depend on one broken fixture (shared boot, DB, build). Read one failure log of each before merging them.', '')
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
L.push('## 4. Setup share', '')
|
|
361
|
+
if (!a.setup.length) L.push('No measured job meets the ≥70% setup candidate threshold across at least three runs.', '')
|
|
362
|
+
else {
|
|
363
|
+
L.push('| job | runs | median | setup share | setup minutes |', '|---|---|---|---|---|')
|
|
364
|
+
for (const e of a.setup) L.push(`| ${e.job} | ${e.runs} | ${f1(e.medianMin)}m | ${Math.round(e.setupShare * 100)}% | ${f1(e.setupMin)} |`)
|
|
365
|
+
L.push('', 'Jobs that are mostly setup are cheap to merge: several such checks can share one job, one checkout, one install.', '')
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
L.push('## 5. Critical path (PR lane)', '')
|
|
369
|
+
const c = a.criticalPath
|
|
370
|
+
L.push(`${c.prCommits} PR commits: wait p50 **${minutes(c.p50)}**, p90 **${minutes(c.p90)}**. Last job to finish:`, '')
|
|
371
|
+
for (const [k, v] of top(c.lastJob, 5)) L.push(`- ${k}: ${v}`)
|
|
372
|
+
L.push('', 'The last-finishing job is a lead, not the whole critical path. Inspect its prerequisites: reducing an upstream build or dispatch delay can shorten PR latency too. This timestamp report does not resolve the workflow DAG.', '')
|
|
373
|
+
|
|
374
|
+
L.push('## 6. Stuck jobs and runner queue', '')
|
|
375
|
+
L.push(`Queue wait (job created → started, not proven runner-only queue): ${a.queue.samples} measured job(s), p50 ${minutes(a.queue.p50)}, p90 ${minutes(a.queue.p90)}. REST does not separate runner queue from dispatch/DAG delay.`)
|
|
376
|
+
if (a.stuck.length) {
|
|
377
|
+
L.push('', `**${a.stuck.length} job(s) ran past 6h**, so they hung rather than ran slowly (check the runner, and set \`timeout-minutes\`):`, '')
|
|
378
|
+
for (const s of a.stuck.slice(0, 10)) L.push(`- ${s.workflow} / ${s.job}: ${f1(s.hours)}h, ${s.conclusion}, ${s.started.slice(0, 16)}, ${s.branch}`)
|
|
379
|
+
}
|
|
380
|
+
appendGaps(L, a.dataGaps)
|
|
381
|
+
L.push('')
|
|
382
|
+
return L.join('\n')
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ---- fetch (thin; everything above is pure) ----
|
|
386
|
+
|
|
387
|
+
const MAX_RUNS = 2000
|
|
388
|
+
const MAX_JOB_PAGES = 100 // 10,000 jobs; refuse instead of silently truncating.
|
|
389
|
+
const GH_TIMEOUT_MS = 60000
|
|
390
|
+
|
|
391
|
+
async function gh(path) {
|
|
392
|
+
try {
|
|
393
|
+
const { stdout } = await run('gh', ['api', path], {
|
|
394
|
+
timeout: GH_TIMEOUT_MS, killSignal: 'SIGKILL', maxBuffer: 16 * 1024 * 1024,
|
|
395
|
+
env: { ...process.env, GH_PROMPT_DISABLED: '1' },
|
|
396
|
+
})
|
|
397
|
+
return JSON.parse(stdout)
|
|
398
|
+
} catch (e) {
|
|
399
|
+
const status = /HTTP\s+(\d{3})/.exec(e.stderr || '')?.[1]
|
|
400
|
+
if (status) e.status = Number(status)
|
|
401
|
+
throw e
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function pool(items, n, fn) {
|
|
406
|
+
const out = new Array(items.length); let i = 0, failure
|
|
407
|
+
// Stop assigning work on error, but drain in-flight gh calls before returning.
|
|
408
|
+
await Promise.all(Array.from({ length: Math.min(n, items.length) }, async () => {
|
|
409
|
+
while (!failure && i < items.length) {
|
|
410
|
+
const k = i++
|
|
411
|
+
try { out[k] = await fn(items[k]) } catch (e) { failure ||= e }
|
|
412
|
+
}
|
|
413
|
+
}))
|
|
414
|
+
if (failure) throw failure
|
|
415
|
+
return out
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function validRepo(repo) {
|
|
419
|
+
if (typeof repo !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_.-]*\/[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(repo)) throw new Error('--repo must be owner/name')
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function positiveInt(value, label, max = Number.MAX_SAFE_INTEGER) {
|
|
423
|
+
if (!/^[1-9]\d*$/.test(String(value)) || !Number.isSafeInteger(Number(value)) || Number(value) > max) throw new Error(`${label} must be an integer from 1 to ${max}`)
|
|
424
|
+
return Number(value)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function collector(repo, api) {
|
|
428
|
+
validRepo(repo)
|
|
429
|
+
const trees = new Map()
|
|
430
|
+
async function treeOf(r) {
|
|
431
|
+
if (r.head_commit?.tree_id) return { tree: r.head_commit.tree_id, treeSource: 'actions.head_commit.tree_id', treeUnknownReason: null }
|
|
432
|
+
if (!r.head_sha) return { tree: null, treeSource: null, treeUnknownReason: 'head SHA unavailable' }
|
|
433
|
+
// Cache the promise, including unavailable results, before concurrent runs await it.
|
|
434
|
+
if (!trees.has(r.head_sha)) trees.set(r.head_sha, (async () => {
|
|
435
|
+
try {
|
|
436
|
+
const commit = await api(`repos/${repo}/commits/${encodeURIComponent(r.head_sha)}`)
|
|
437
|
+
const tree = commit.commit?.tree?.sha
|
|
438
|
+
return tree ? { tree, treeSource: 'commits.commit.tree.sha', treeUnknownReason: null }
|
|
439
|
+
: { tree: null, treeSource: null, treeUnknownReason: 'commit response has no tree SHA' }
|
|
440
|
+
} catch (e) {
|
|
441
|
+
if (![404, 410, 422].includes(e.status)) throw e
|
|
442
|
+
return { tree: null, treeSource: null, treeUnknownReason: `commit unavailable (HTTP ${e.status}); deleted queue commits may no longer resolve` }
|
|
443
|
+
}
|
|
444
|
+
})())
|
|
445
|
+
return trees.get(r.head_sha)
|
|
446
|
+
}
|
|
447
|
+
async function collect(r, defaultBranch) {
|
|
448
|
+
positiveInt(r.id, 'run ID')
|
|
449
|
+
const attempt = r.run_attempt == null ? null : positiveInt(r.run_attempt, 'run attempt')
|
|
450
|
+
const endpoint = attempt === null ? `repos/${repo}/actions/runs/${r.id}/jobs?filter=latest&`
|
|
451
|
+
: `repos/${repo}/actions/runs/${r.id}/attempts/${attempt}/jobs?`
|
|
452
|
+
const jobs = []
|
|
453
|
+
let complete = false, total
|
|
454
|
+
for (let page = 1; page <= MAX_JOB_PAGES; page++) {
|
|
455
|
+
const result = await api(`${endpoint}per_page=100&page=${page}`)
|
|
456
|
+
if (!Array.isArray(result.jobs) || result.jobs.length > 100) throw new Error(`invalid jobs response for run ${r.id}`)
|
|
457
|
+
if (result.total_count != null) {
|
|
458
|
+
if (!Number.isSafeInteger(result.total_count) || result.total_count < 0 || result.total_count > MAX_JOB_PAGES * 100) throw new Error(`job pagination limit or invalid total for run ${r.id}`)
|
|
459
|
+
if (total !== undefined && total !== result.total_count) throw new Error(`job total changed during collection for run ${r.id}`)
|
|
460
|
+
total = result.total_count
|
|
461
|
+
}
|
|
462
|
+
jobs.push(...result.jobs)
|
|
463
|
+
if (total !== undefined && jobs.length >= total) {
|
|
464
|
+
if (jobs.length !== total) throw new Error(`inconsistent job total for run ${r.id}`)
|
|
465
|
+
complete = true; break
|
|
466
|
+
}
|
|
467
|
+
if (result.jobs.length < 100) {
|
|
468
|
+
if (total !== undefined && jobs.length < total) throw new Error(`incomplete jobs response for run ${r.id}`)
|
|
469
|
+
complete = true; break
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (!complete) throw new Error(`job pagination limit reached for run ${r.id}; refusing partial data`)
|
|
473
|
+
const seen = new Set()
|
|
474
|
+
for (const j of jobs) {
|
|
475
|
+
positiveInt(j.id, 'job ID')
|
|
476
|
+
if (seen.has(j.id)) throw new Error(`duplicate job ${j.id} while collecting run ${r.id}`)
|
|
477
|
+
seen.add(j.id)
|
|
478
|
+
if (attempt !== null && j.run_attempt != null && j.run_attempt !== attempt) throw new Error(`job attempt mismatch for run ${r.id}`)
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
id: r.id, attempt, url: r.html_url ?? null,
|
|
482
|
+
workflow: r.name, workflowId: r.workflow_id ?? null, workflowPath: r.path ?? null,
|
|
483
|
+
event: r.event, branch: r.head_branch, defaultBranch,
|
|
484
|
+
status: r.status ?? null, conclusion: r.conclusion ?? null,
|
|
485
|
+
sha: r.head_sha ?? null, ...await treeOf(r), created: r.created_at, started: r.run_started_at ?? null,
|
|
486
|
+
collectionScope: 'latest-attempt',
|
|
487
|
+
collectionGaps: attempt === null ? [{ reason: 'run attempt unavailable; collected latest jobs only' }]
|
|
488
|
+
: attempt > 1 ? [{ reason: `only attempt ${attempt} collected; earlier attempts are not included` }] : [],
|
|
489
|
+
jobs: jobs.map((j) => ({
|
|
490
|
+
id: j.id, name: j.name, attempt: j.run_attempt ?? attempt, url: j.html_url ?? null, runUrl: r.html_url ?? null,
|
|
491
|
+
status: j.status ?? null, conclusion: j.conclusion ?? null,
|
|
492
|
+
created: j.created_at ?? null, started: j.started_at ?? null, completed: j.completed_at ?? null,
|
|
493
|
+
runnerId: j.runner_id ?? null, runnerName: j.runner_name ?? null,
|
|
494
|
+
runnerGroupId: j.runner_group_id ?? null, runnerGroupName: j.runner_group_name ?? null,
|
|
495
|
+
runnerLabels: j.labels ?? null,
|
|
496
|
+
steps: (j.steps || []).map((s) => ({
|
|
497
|
+
number: s.number ?? null, name: s.name, conclusion: s.conclusion ?? null,
|
|
498
|
+
started: s.started_at ?? null, completed: s.completed_at ?? null,
|
|
499
|
+
})),
|
|
500
|
+
})),
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return collect
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// The optional API function is the gh/provider boundary for fixture tests, never a CLI flag.
|
|
507
|
+
export async function fetchRuns(repo, want = 300, api = gh) {
|
|
508
|
+
want = positiveInt(want, '--runs', MAX_RUNS)
|
|
509
|
+
const collect = collector(repo, api)
|
|
510
|
+
const meta = await api(`repos/${repo}`)
|
|
511
|
+
const raw = []
|
|
512
|
+
for (let page = 1; raw.length < want && page <= 20; page++) {
|
|
513
|
+
const result = await api(`repos/${repo}/actions/runs?status=completed&per_page=100&page=${page}`)
|
|
514
|
+
if (!Array.isArray(result.workflow_runs) || result.workflow_runs.length > 100) throw new Error('invalid workflow runs response')
|
|
515
|
+
raw.push(...result.workflow_runs)
|
|
516
|
+
if (result.workflow_runs.length < 100) break
|
|
517
|
+
}
|
|
518
|
+
const selected = raw.slice(0, want)
|
|
519
|
+
if (new Set(selected.map((r) => String(r.id))).size !== selected.length) throw new Error('duplicate run ID during history pagination; refusing partial evidence')
|
|
520
|
+
const runs = await pool(selected, 8, (r) => collect(r, meta.default_branch))
|
|
521
|
+
validateRuns(runs)
|
|
522
|
+
return runs
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export async function fetchRun(repo, runId, api = gh) {
|
|
526
|
+
runId = positiveInt(runId, '--profile-run')
|
|
527
|
+
const collect = collector(repo, api)
|
|
528
|
+
const meta = await api(`repos/${repo}`)
|
|
529
|
+
const raw = await api(`repos/${repo}/actions/runs/${runId}`)
|
|
530
|
+
if (String(raw.id) !== String(runId)) throw new Error('requested run ID does not match response')
|
|
531
|
+
const runs = [await collect(raw, meta.default_branch)]
|
|
532
|
+
validateRuns(runs)
|
|
533
|
+
return runs
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function validateRuns(runs) {
|
|
537
|
+
if (!Array.isArray(runs)) throw new Error('saved data must be an array of runs')
|
|
538
|
+
const timestamp = (value, label) => {
|
|
539
|
+
if (value != null && (typeof value !== 'string' || !Number.isFinite(Date.parse(value)))) throw new Error(`invalid ${label} timestamp`)
|
|
540
|
+
}
|
|
541
|
+
for (const r of runs) {
|
|
542
|
+
if (!r || typeof r !== 'object' || !Array.isArray(r.jobs) || typeof r.workflow !== 'string') throw new Error('each run must have a workflow name and jobs array')
|
|
543
|
+
positiveInt(r.id, 'run ID')
|
|
544
|
+
if (r.attempt != null) positiveInt(r.attempt, 'run attempt')
|
|
545
|
+
if (r.tree != null && (typeof r.tree !== 'string' || !r.tree)) throw new Error('tree must be a nonempty hash string or null')
|
|
546
|
+
if (r.sha != null && typeof r.sha !== 'string') throw new Error('sha must be a string or null')
|
|
547
|
+
if (r.collectionGaps != null && (!Array.isArray(r.collectionGaps) || r.collectionGaps.some((g) => !g || typeof g.reason !== 'string'))) throw new Error('invalid collectionGaps')
|
|
548
|
+
timestamp(r.created, 'run created'); timestamp(r.started, 'run started')
|
|
549
|
+
for (const j of r.jobs) {
|
|
550
|
+
if (!j || typeof j.name !== 'string' || (j.steps != null && !Array.isArray(j.steps))) throw new Error('each job must have a name and optional steps array')
|
|
551
|
+
if (j.attempt != null) positiveInt(j.attempt, 'job attempt')
|
|
552
|
+
if (j.runnerLabels != null && (!Array.isArray(j.runnerLabels) || j.runnerLabels.some((s) => typeof s !== 'string'))) throw new Error('runnerLabels must be an array of strings or null')
|
|
553
|
+
timestamp(j.created, 'job created'); timestamp(j.started, 'job started'); timestamp(j.completed, 'job completed')
|
|
554
|
+
for (const s of j.steps || []) {
|
|
555
|
+
if (!s || typeof s.name !== 'string') throw new Error('each step must have a name')
|
|
556
|
+
timestamp(s.started, 'step started'); timestamp(s.completed, 'step completed')
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const USAGE = 'usage: ci-run-audit.mjs (--repo <owner/name> [--runs 300] | --from f.json) [--profile-run <runId>] [--save f.json] [--json] [--ignore-job <regex>] [--min-trees 20]'
|
|
563
|
+
|
|
564
|
+
function args(argv) {
|
|
565
|
+
const o = { runs: 300, json: false, minTrees: 20 }, seen = new Set()
|
|
566
|
+
for (let i = 0; i < argv.length; i++) {
|
|
567
|
+
const a = argv[i]
|
|
568
|
+
if (seen.has(a)) throw new Error(`duplicate argument: ${a}`)
|
|
569
|
+
seen.add(a)
|
|
570
|
+
const v = () => {
|
|
571
|
+
const value = argv[++i]
|
|
572
|
+
if (!value || value.startsWith('-')) throw new Error(`missing value for ${a}`)
|
|
573
|
+
return value
|
|
574
|
+
}
|
|
575
|
+
if (a === '--repo') o.repo = v()
|
|
576
|
+
else if (a === '--runs') o.runs = positiveInt(v(), '--runs', MAX_RUNS)
|
|
577
|
+
else if (a === '--profile-run') o.profileRun = positiveInt(v(), '--profile-run')
|
|
578
|
+
else if (a === '--save') o.save = v()
|
|
579
|
+
else if (a === '--from') o.from = v()
|
|
580
|
+
else if (a === '--json') o.json = true
|
|
581
|
+
else if (a === '--ignore-job') o.ignore = v()
|
|
582
|
+
else if (a === '--min-trees') o.minTrees = positiveInt(v(), '--min-trees')
|
|
583
|
+
else if (a === '--help' || a === '-h') o.help = true
|
|
584
|
+
else throw new Error(`unknown argument: ${a}`)
|
|
585
|
+
}
|
|
586
|
+
if (o.help) return o
|
|
587
|
+
if (Boolean(o.repo) === Boolean(o.from)) throw new Error('provide exactly one of --repo or --from')
|
|
588
|
+
if (o.repo) validRepo(o.repo)
|
|
589
|
+
if (seen.has('--runs') && (o.from || o.profileRun)) throw new Error('--runs is only supported for live history collection')
|
|
590
|
+
if (o.profileRun && (seen.has('--ignore-job') || seen.has('--min-trees'))) throw new Error('--ignore-job and --min-trees are audit-only; profiles include all jobs')
|
|
591
|
+
o.ignoreRegex = o.ignore ? new RegExp(`${DEFAULT_IGNORE.source}|${o.ignore}`, 'i') : DEFAULT_IGNORE
|
|
592
|
+
return o
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export async function runCli(argv) {
|
|
596
|
+
try {
|
|
597
|
+
const o = args(argv)
|
|
598
|
+
if (o.help) { console.error(USAGE); return 0 }
|
|
599
|
+
const runs = o.from ? JSON.parse(fs.readFileSync(o.from, 'utf8'))
|
|
600
|
+
: o.profileRun ? await fetchRun(o.repo, o.profileRun) : await fetchRuns(o.repo, o.runs)
|
|
601
|
+
validateRuns(runs)
|
|
602
|
+
const report = o.profileRun ? profileRun(runs, o.profileRun) : analyze(runs, { ignore: o.ignoreRegex, minTrees: o.minTrees })
|
|
603
|
+
const output = o.json ? JSON.stringify(report, null, 2) : o.profileRun ? renderProfile(report) : render(report, o.repo || o.from)
|
|
604
|
+
// Save the collected array, never the derived report; saved profiles remain re-auditable.
|
|
605
|
+
if (o.save) fs.writeFileSync(o.save, JSON.stringify(runs))
|
|
606
|
+
process.stdout.write(output + '\n')
|
|
607
|
+
return 0
|
|
608
|
+
} catch (e) {
|
|
609
|
+
console.error(`ci-run-audit: ${e.message}`)
|
|
610
|
+
return 2
|
|
611
|
+
}
|
|
612
|
+
}
|