hyperi-ci 2.2.1__tar.gz → 2.2.3__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.
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/CHANGELOG.md +17 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/PKG-INFO +2 -1
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/TODO.md +18 -23
- hyperi_ci-2.2.3/VERSION +1 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/pyproject.toml +19 -2
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/config/defaults.yaml +21 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/config.py +29 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/deployment/detect.py +73 -7
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/dispatch.py +24 -1
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/init.py +6 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/deployment/test_detect_tier.py +99 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_config.py +69 -1
- hyperi_ci-2.2.3/tests/unit/test_dispatch_status.py +96 -0
- hyperi_ci-2.2.3/uv.lock +639 -0
- hyperi_ci-2.2.1/VERSION +0 -1
- hyperi_ci-2.2.1/uv.lock +0 -718
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/.ai-version +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/commands/doco.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/commands/load.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/commands/review.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/commands/save.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/commands/setup-claude.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/commands/simplify.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/commands/standards.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/UNIVERSAL.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/ai-conduct.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/ansible.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/bash.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/chars-policy.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/ci.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/clickhouse-sql.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/code-header.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/code-style.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/config-and-logging.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/cpp.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/design-principles.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/dfe-metrics.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/docker.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/error-handling.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/git.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/golang.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/issue-management.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/k8s.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/licensing.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/mocks-policy.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/pki.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/python.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/repo-naming.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/rust.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/security.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/sql-clickhouse.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/terraform.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/testing.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/typescript.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/universal.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/settings.json +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/ai-common/SKILL.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/ai-guidelines/SKILL.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/bleeding-edge/SKILL.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/ci-check/SKILL.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/ci-logs/SKILL.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/ci-watch/SKILL.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/deps/SKILL.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/docs-audit/SKILL.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/documentation/SKILL.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/python/SKILL.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/release/SKILL.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/standards/SKILL.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/test-review/SKILL.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/verification/SKILL.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.gitattributes +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.github/actions/predict-version/action.yml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.github/workflows/_release-tail.yml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.github/workflows/ci.yml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.github/workflows/go-ci.yml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.github/workflows/python-ci.yml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.github/workflows/rust-ci.yml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.github/workflows/ts-ci.yml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.gitignore +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.gitmodules +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.hyperi-ci.yaml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.mcp.json +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.releaserc.yaml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/AI-TRAINING-POLICY.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/CLAUDE.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/LICENSE +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/README.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/STATE.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/config/commit-types.yaml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/config/org.yaml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/config/runners.yaml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/config/secrets-access.yaml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/config/versions.yaml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/ARC-RUNNERS.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/ARCHITECTURE.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/CI-LESSONS.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/DESIGN.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/JFROG-MIGRATION.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/MIGRATION-GUIDE.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/PGO-WORKLOAD-GUIDE.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/deployment-contract.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/migration/deployment-contract-tier3.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/rust.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/typescript.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/renovate.json +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/robots.txt +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/scripts/pre-commit-versions.sh +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/scripts/setup-rust-dev.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/scripts/sync-secrets-access.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/scripts/update-versions.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/__init__.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/cli.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/common.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/config/native-deps/golang.yaml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/config/native-deps/python.yaml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/config/native-deps/rust.yaml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/config/native-deps/typescript.yaml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/config/org.yaml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/config/toolchains/gcc.yaml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/config/toolchains/llvm.yaml +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/__init__.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/binary_stage.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/build.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/compose.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/detect.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/labels.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/manifest.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/registry.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/stage.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/templates.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/deployment/__init__.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/deployment/cli.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/deployment/contract.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/deployment/registry.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/deployment/scaffold.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/deployment/stage.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/detect.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/gh.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/install_deps.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/__init__.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/_build_common.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/golang/__init__.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/golang/build.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/golang/publish.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/golang/quality.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/golang/test.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/python/__init__.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/python/build.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/python/publish.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/python/quality.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/python/test.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/quality_common.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/rust/__init__.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/rust/build.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/rust/optimize.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/rust/pgo.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/rust/publish.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/rust/quality.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/rust/test.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/typescript/__init__.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/typescript/_common.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/typescript/build.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/typescript/install_deps.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/typescript/publish.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/typescript/quality.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/typescript/test.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/logs.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/migrate.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/native_deps.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/publish/__init__.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/publish/binaries.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/publish/dispatch.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/publish_binaries.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/push.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/quality/__init__.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/quality/commit_validation.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/quality/gitleaks.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/release.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/trigger.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/upgrade.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/watch.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/templates/pgo-workload/README.md +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/templates/pgo-workload/grpc-server.sh +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/templates/pgo-workload/http-server.sh +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/templates/pgo-workload/kafka-consumer.sh +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/templates/pgo-workload/kafka-producer.sh +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/templates/pgo-workload/multi-protocol.sh +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/__init__.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/integration/__init__.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/integration/test_rust_build_optimize.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/__init__.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/deployment/__init__.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/deployment/test_contract.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/deployment/test_emit_artefacts_cli.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/deployment/test_registry_cascade.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/deployment/test_scaffold.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/deployment/test_stage.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_cli.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_commit_validation.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_common.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_container_binary_stage.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_container_build.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_container_compose.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_container_detect.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_container_labels.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_container_manifest.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_container_registry.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_container_stage.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_container_templates.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_detect.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_dispatch_alias.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_gh.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_init.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_migrate.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_native_deps.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_publish.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_push.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_rust_optimize.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_rust_pgo.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_rust_quality.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_typescript_quality.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_upgrade.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_watch.py +0 -0
- {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_workflow_consistency.py +0 -0
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
## [2.2.3](https://github.com/hyperi-io/hyperi-ci/compare/v2.2.2...v2.2.3) (2026-05-13)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **deployment:** exclude self-name from tier detection ([f61921b](https://github.com/hyperi-io/hyperi-ci/commit/f61921b9b0dcfe36c3c1809b0bacbedee4c78386))
|
|
7
|
+
* **deps:** declare typer explicitly in runtime dependencies ([895a4a6](https://github.com/hyperi-io/hyperi-ci/commit/895a4a637e2f7f55bd74e2276ddb51f9184fc7cd))
|
|
8
|
+
* **deps:** remove editable pylib source override to unblock Dependabot graph ([a4f1b28](https://github.com/hyperi-io/hyperi-ci/commit/a4f1b28551758e0e1287bf931f60e52194076021))
|
|
9
|
+
|
|
10
|
+
## [2.2.2](https://github.com/hyperi-io/hyperi-ci/compare/v2.2.1...v2.2.2) (2026-05-13)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **config:** add project.status lifecycle field (information-only) ([a66a1a2](https://github.com/hyperi-io/hyperi-ci/commit/a66a1a23d911548078ebc62ba9995392ce9ec087))
|
|
16
|
+
* **dispatch:** log project.status as INFO with clarifier phrase, not WARN ([c9de7cd](https://github.com/hyperi-io/hyperi-ci/commit/c9de7cdcae47939b93a830987e59e13643516027))
|
|
17
|
+
|
|
1
18
|
## [2.2.1](https://github.com/hyperi-io/hyperi-ci/compare/v2.2.0...v2.2.1) (2026-05-08)
|
|
2
19
|
|
|
3
20
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperi-ci
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.3
|
|
4
4
|
Summary: HyperI CI/CD CLI tool — multi-language build, test, and publish automation
|
|
5
5
|
License: Proprietary
|
|
6
6
|
License-File: LICENSE
|
|
@@ -10,3 +10,4 @@ Requires-Dist: packaging>=26.0
|
|
|
10
10
|
Requires-Dist: pydantic>=2.13
|
|
11
11
|
Requires-Dist: pyyaml>=6.0
|
|
12
12
|
Requires-Dist: tomli-w>=1.2.0
|
|
13
|
+
Requires-Dist: typer>=0.25
|
|
@@ -6,36 +6,22 @@ This is the **single source of truth** for all tasks and progress.
|
|
|
6
6
|
|
|
7
7
|
## Active Tasks
|
|
8
8
|
|
|
9
|
-
### CI consolidation (KISS) — predict-first plan-job per language
|
|
9
|
+
### CI consolidation (KISS) — predict-first plan-job per language ✅ DONE 2026-05-08
|
|
10
10
|
|
|
11
11
|
**Plan:** [`docs/superpowers/plans/2026-05-08-ci-consolidation-kiss.md`](docs/superpowers/plans/2026-05-08-ci-consolidation-kiss.md)
|
|
12
12
|
|
|
13
|
-
**Architecture:** [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)
|
|
13
|
+
**Architecture:** [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)
|
|
14
14
|
|
|
15
|
-
**
|
|
15
|
+
**Outcome:** v2.2.0 + v2.2.1 shipped to PyPI 2026-05-08. New plan-first gating validated on the most complex Rust consumer end-to-end. Python gate validated; full publish-canary on Python deferred to hyperi-pylib (clean library) instead of dfe-engine (in-progress feature branches).
|
|
16
16
|
|
|
17
|
-
**
|
|
17
|
+
**Canary results:**
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
**
|
|
22
|
-
|
|
23
|
-
- [x] **Canary 0: hyperi-ci's own `test-projects/`** — integration tests green; one stale spike-allocator test fixed (commit `a331a0b`).
|
|
24
|
-
- [x] **Canary 1a: dfe-loader chore-skip** — [run 25531398891](https://github.com/hyperi-io/dfe-loader/actions/runs/25531398891) — 16 seconds total, plan ✅, every other job skipped. **Killer feature validated.**
|
|
25
|
-
- [ ] **Canary 1b: dfe-loader publish-dispatch** — [run 25531808212](https://github.com/hyperi-io/dfe-loader/actions/runs/25531808212) in flight as of 2026-05-08T01:40Z. Plan/Quality/Test green; both Build (linux-amd64, linux-arm64) jobs in_progress. BOLT/PGO; expect 30-45 min.
|
|
19
|
+
- [x] **Canary 0: hyperi-ci's own `test-projects/`** — integration tests green (7 slow tests pass); one stale spike-allocator test fixed (commit `a331a0b`).
|
|
20
|
+
- [x] **Canary 1a: dfe-loader chore-skip** — [run 25531398891](https://github.com/hyperi-io/dfe-loader/actions/runs/25531398891) — **16 seconds total**, plan ✅, every other job skipped. **Killer feature validated.**
|
|
21
|
+
- [x] **Canary 1b: dfe-loader publish-dispatch** — [run 25531808212](https://github.com/hyperi-io/dfe-loader/actions/runs/25531808212) — **38 minutes total**, all 7 jobs ✅ green end-to-end (Plan, Quality, Test, Build linux-amd64, Build linux-arm64, Container, Tag & Publish). Multi-arch BOLT+PGO Rust publish chain proven on the most complex Rust consumer.
|
|
26
22
|
- [x] **Canary 2a: dfe-engine chore-skip** — [run 25531462469](https://github.com/hyperi-io/dfe-engine/actions/runs/25531462469) — 33 seconds, all heavy jobs skipped. **Gate validated.**
|
|
27
|
-
- [ ] **Canary 2b: dfe-engine publish-dispatch** — failed at quality
|
|
28
|
-
- [ ] **Canary 3 (
|
|
29
|
-
|
|
30
|
-
**Ordering update 2026-05-08:** Python canary order is hyperi-pylib FIRST (clean), then dfe-engine after merge coordination. dfe-engine is the most complex Python case but its current branch state makes it a poor first canary.
|
|
31
|
-
|
|
32
|
-
**Done means:**
|
|
33
|
-
|
|
34
|
-
- All 17 plan tasks checked off
|
|
35
|
-
- All four `<lang>-ci.yml` workflows match the same shape (verified by `tests/unit/test_workflow_consistency.py`)
|
|
36
|
-
- A chore: commit on each canary completes in <2 min, skips quality/test/build/container/publish
|
|
37
|
-
- A `Publish: true` commit on each canary still runs the full pipeline and ships to the right registries
|
|
38
|
-
- v2.2.0+ of hyperi-ci is published to PyPI
|
|
23
|
+
- [ ] **Canary 2b: dfe-engine publish-dispatch** — failed at quality on real pre-existing CVEs (pytest GHSA-6w46-j5rx-g56g, cryptography GHSA-p423-j2cm-9vmq). NOT a gate regression. Handed back to dfe-engine maintainers via [`dfe-engine/docs/superpowers/plans/2026-05-08-ci-canary-publish-fix.md`](../../dfe-engine/docs/superpowers/plans/2026-05-08-ci-canary-publish-fix.md). Blocked on Derek + Kaz merge coordination.
|
|
24
|
+
- [ ] **Canary 3 (Python publish-dispatch on hyperi-pylib)** — preferred Python canary: clean lib, no parallel WIP. User to drive directly.
|
|
39
25
|
|
|
40
26
|
**Bugs discovered during rollout (no SEP fields):**
|
|
41
27
|
|
|
@@ -43,6 +29,15 @@ This is the **single source of truth** for all tasks and progress.
|
|
|
43
29
|
- [x] `_release-tail.yml` expects a single `build-dist-*` artifact containing both `dist/` AND `ci-tmp/`. The plan's draft proposed splitting into separate artifacts which would have broken release-tail's download step. Combined-upload pattern preserved.
|
|
44
30
|
- [x] `ts-ci.yml` had a duplicate `setup:` job key (silent YAML override) and `test:` job missing `needs:`/`if:`. Fixed during Task 8.
|
|
45
31
|
- [x] `go-ci.yml` ran quality + test unconditionally before any gate decision (setup ran AFTER them). Fixed during Task 9.
|
|
32
|
+
- [x] `init.py` scaffold generated `workflow_dispatch:` with no `tag` input → `hyperi-ci publish` returned HTTP 422. Every project initialised with v2.2.0 inherited the bug. Fixed in v2.2.1 (commit `fa7d79b`) — surfaced by dfe-engine canary, fixed in dfe-engine consumer ci.yml AND in hyperi-ci scaffold.
|
|
33
|
+
- [x] `hyperi-ci watch <run-id>` invoked from a different repo's cwd silently 404'd 10× before giving up. Added `--repo / -R` flag in v2.2.1 (commit `c90c96a`) — surfaced by dfe-loader canary subagent.
|
|
34
|
+
|
|
35
|
+
**Versions shipped:**
|
|
36
|
+
|
|
37
|
+
- v2.2.0 ([run 25531099781](https://github.com/hyperi-io/hyperi-ci/actions/runs/25531099781)) — initial KISS rollout
|
|
38
|
+
- v2.2.1 ([run 25533443889](https://github.com/hyperi-io/hyperi-ci/actions/runs/25533443889)) — init.py + watch --repo + jemalloc test fixes
|
|
39
|
+
|
|
40
|
+
**Next:** monitor hyperi-pylib's Python canary (when user runs it) and dfe-engine's CVE-cleanup re-canary. If both come back green, the rollout is fully closed. If new bugs surface, fix and ship as v2.2.x.
|
|
46
41
|
|
|
47
42
|
---
|
|
48
43
|
|
hyperi_ci-2.2.3/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.2.3
|
|
@@ -23,6 +23,11 @@ dependencies = [
|
|
|
23
23
|
"pydantic>=2.13",
|
|
24
24
|
"pyyaml>=6.0",
|
|
25
25
|
"tomli-w>=1.2.0",
|
|
26
|
+
# typer is used directly by src/hyperi_ci/cli.py — declare
|
|
27
|
+
# explicitly rather than relying on the transitive pull from
|
|
28
|
+
# hyperi-pylib's CLI infra. If pylib ever drops typer, this
|
|
29
|
+
# keeps us pinned to a known surface.
|
|
30
|
+
"typer>=0.25",
|
|
26
31
|
]
|
|
27
32
|
|
|
28
33
|
[project.scripts]
|
|
@@ -83,8 +88,20 @@ source = ["hyperi_ci"]
|
|
|
83
88
|
# Keep this low so `pytest --cov` reports without failing the run.
|
|
84
89
|
fail_under = 30
|
|
85
90
|
|
|
86
|
-
[tool.uv.sources]
|
|
87
|
-
|
|
91
|
+
# NOTE: no [tool.uv.sources] block here on purpose.
|
|
92
|
+
#
|
|
93
|
+
# Tempting to point `hyperi-pylib` at a local editable checkout
|
|
94
|
+
# (/projects/hyperi-pylib) so dev changes flow through `uv sync`,
|
|
95
|
+
# but that breaks two things:
|
|
96
|
+
# - Dependabot's update_graph job can't resolve the path inside its
|
|
97
|
+
# container — fails on every push to main, leaving the dep-graph
|
|
98
|
+
# stale and GitHub's vulnerability banner inaccurate.
|
|
99
|
+
# - Reproducibility for anyone whose checkout isn't at that exact
|
|
100
|
+
# path.
|
|
101
|
+
#
|
|
102
|
+
# For local pylib co-development, run from this repo:
|
|
103
|
+
# uv pip install -e /path/to/hyperi-pylib
|
|
104
|
+
# Manual install is per-developer scratch state; not in pyproject.
|
|
88
105
|
|
|
89
106
|
[dependency-groups]
|
|
90
107
|
dev = [
|
|
@@ -13,6 +13,27 @@
|
|
|
13
13
|
# Values: python, typescript, golang, rust, none
|
|
14
14
|
language: none
|
|
15
15
|
|
|
16
|
+
# Project metadata
|
|
17
|
+
#
|
|
18
|
+
# project.status: lifecycle stage of THIS PROJECT (not of any given release).
|
|
19
|
+
# Information-only field — surfaced in `hyperi-ci config` and at stage
|
|
20
|
+
# startup in CI logs so consumers can immediately see "oh, it's not GA".
|
|
21
|
+
# Does not affect publish behaviour (that's `publish.channel`).
|
|
22
|
+
#
|
|
23
|
+
# Values:
|
|
24
|
+
# experimental — early R&D, no API commitment, expect anything
|
|
25
|
+
# alpha — usable, expect breaks, no semver guarantees
|
|
26
|
+
# beta — feature-stable, polishing, mild breaks possible
|
|
27
|
+
# ga — semver guarantees apply, breaking only on major bump
|
|
28
|
+
# legacy — still works, successor exists, plan migration
|
|
29
|
+
# deprecated — active sunset, do not adopt
|
|
30
|
+
#
|
|
31
|
+
# Unset = "not declared" — the log line says "(status not declared)" which
|
|
32
|
+
# is itself a useful signal. New projects scaffolded by `hyperi-ci init`
|
|
33
|
+
# default to `experimental`.
|
|
34
|
+
project:
|
|
35
|
+
status: ""
|
|
36
|
+
|
|
16
37
|
# Runner Settings
|
|
17
38
|
#
|
|
18
39
|
# mode: controls which GitHub Actions runners are used
|
|
@@ -22,6 +22,19 @@ import yaml
|
|
|
22
22
|
|
|
23
23
|
_CONFIG_DIR = Path(__file__).resolve().parent / "config"
|
|
24
24
|
|
|
25
|
+
# Lifecycle stages a project can declare via `project.status` in
|
|
26
|
+
# `.hyperi-ci.yaml`. Information-only — does not gate any behaviour.
|
|
27
|
+
# Empty string (default) means "not declared" — the field is optional.
|
|
28
|
+
# See defaults.yaml for what each stage means.
|
|
29
|
+
VALID_PROJECT_STATUSES: tuple[str, ...] = (
|
|
30
|
+
"experimental",
|
|
31
|
+
"alpha",
|
|
32
|
+
"beta",
|
|
33
|
+
"ga",
|
|
34
|
+
"legacy",
|
|
35
|
+
"deprecated",
|
|
36
|
+
)
|
|
37
|
+
|
|
25
38
|
# Re-exported for callers that just want the constant without going through
|
|
26
39
|
# the full CIConfig load (e.g. quality-stage drift checks). Authoritative
|
|
27
40
|
# value is defined alongside the Pydantic model that uses it as a field
|
|
@@ -229,6 +242,22 @@ def load_config(
|
|
|
229
242
|
publish.get("target", "oss") if isinstance(publish, dict) else "oss"
|
|
230
243
|
)
|
|
231
244
|
|
|
245
|
+
# Validate project.status if set. Warn on unknown values rather than
|
|
246
|
+
# failing — the field is information-only and a typo shouldn't break
|
|
247
|
+
# the build.
|
|
248
|
+
project = config.get("project", {})
|
|
249
|
+
if isinstance(project, dict):
|
|
250
|
+
status = str(project.get("status") or "").strip().lower()
|
|
251
|
+
if status and status not in VALID_PROJECT_STATUSES:
|
|
252
|
+
# Lazy import to avoid circular dep at module load.
|
|
253
|
+
from hyperi_ci.common import warn
|
|
254
|
+
|
|
255
|
+
warn(
|
|
256
|
+
f"Unknown project.status '{status}' — expected one of "
|
|
257
|
+
f"{', '.join(VALID_PROJECT_STATUSES)} (or unset). "
|
|
258
|
+
f"Treating as unset for logging purposes."
|
|
259
|
+
)
|
|
260
|
+
|
|
232
261
|
_config_cache = CIConfig(
|
|
233
262
|
language=config.get("language", "none"),
|
|
234
263
|
ci_min_python_version=config.get("ci_min_python_version", "3.9"),
|
|
@@ -94,25 +94,91 @@ def _depends_on(manifest: Path, package_name: str) -> bool:
|
|
|
94
94
|
dependencies = ["hyperi-pylib>=2.24"]
|
|
95
95
|
dependencies = ["hyperi-pylib[metrics]>=2.24"]
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
**Self-match exclusion.** If the manifest declares its own package
|
|
98
|
+
name as ``package_name`` (i.e. this manifest IS the library, not a
|
|
99
|
+
consumer of it), returns False. Without this, the library's own
|
|
100
|
+
repo gets misdispatched as a Tier 1/2 consumer and the
|
|
101
|
+
deployment-artefact producer fails with "no Rust binary found" /
|
|
102
|
+
equivalent. The check is generic — applies to any rustlib /
|
|
103
|
+
pylib / future *lib and to consumer projects whose own name
|
|
104
|
+
happens to share a prefix.
|
|
105
|
+
|
|
106
|
+
Doesn't try to parse TOML beyond pulling the ``name`` field out of
|
|
107
|
+
a recognised top-level section because:
|
|
108
|
+
|
|
98
109
|
1. Avoids hauling `tomllib` in just for tier detection.
|
|
99
110
|
2. Catches every form (workspace inheritance, extras, comments)
|
|
100
111
|
that a stricter parse would have to handle case by case.
|
|
101
|
-
3. False positives
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
the binary fails clearly" rather than silent miscategorisation.
|
|
112
|
+
3. False positives in the dep-match are bounded: the consequence
|
|
113
|
+
is "we try to invoke generate-artefacts and the binary fails
|
|
114
|
+
clearly" rather than silent miscategorisation.
|
|
105
115
|
|
|
106
116
|
Args:
|
|
107
117
|
manifest: Path to Cargo.toml or pyproject.toml.
|
|
108
118
|
package_name: Dependency name to look for.
|
|
109
119
|
|
|
110
120
|
Returns:
|
|
111
|
-
True if the substring appears
|
|
121
|
+
True if the substring appears AND the manifest isn't itself
|
|
122
|
+
the named package; False otherwise.
|
|
112
123
|
|
|
113
124
|
"""
|
|
114
125
|
try:
|
|
115
126
|
text = manifest.read_text(encoding="utf-8", errors="replace")
|
|
116
127
|
except OSError:
|
|
117
128
|
return False
|
|
118
|
-
|
|
129
|
+
if package_name not in text:
|
|
130
|
+
return False
|
|
131
|
+
return _manifest_self_name(text) != package_name
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# Top-level tables whose ``name`` field is the manifest's own package
|
|
135
|
+
# name. Listed in scan precedence — the first match wins, so a Cargo
|
|
136
|
+
# manifest's ``[package] name`` beats any later table (unlikely in
|
|
137
|
+
# Cargo, but pyproject.toml can legitimately have both ``[project]``
|
|
138
|
+
# and ``[tool.poetry]`` and we treat them equivalently).
|
|
139
|
+
_SELF_NAME_SECTIONS: frozenset[str] = frozenset(
|
|
140
|
+
{"[package]", "[project]", "[tool.poetry]"}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _manifest_self_name(text: str) -> str | None:
|
|
145
|
+
"""Extract the manifest's own package name, if declared.
|
|
146
|
+
|
|
147
|
+
Scans for a ``name = "..."`` (or single-quoted) line inside one of
|
|
148
|
+
the recognised self-name sections (:data:`_SELF_NAME_SECTIONS`).
|
|
149
|
+
Returns the first match. No TOML parser — line-scoped, tolerant of
|
|
150
|
+
extra whitespace around ``=``.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
text: Full manifest text.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
The declared package name, or ``None`` if no recognised
|
|
157
|
+
declaration is found.
|
|
158
|
+
|
|
159
|
+
"""
|
|
160
|
+
current_section: str | None = None
|
|
161
|
+
for raw in text.splitlines():
|
|
162
|
+
stripped = raw.strip()
|
|
163
|
+
if stripped.startswith("[") and stripped.endswith("]"):
|
|
164
|
+
current_section = stripped
|
|
165
|
+
continue
|
|
166
|
+
if current_section not in _SELF_NAME_SECTIONS:
|
|
167
|
+
continue
|
|
168
|
+
# Cheap pre-filter: skip lines that don't even start with "name".
|
|
169
|
+
if not stripped.startswith("name"):
|
|
170
|
+
continue
|
|
171
|
+
eq = stripped.find("=")
|
|
172
|
+
if eq < 0:
|
|
173
|
+
continue
|
|
174
|
+
# Confirm the LHS is exactly "name" (avoids matching "name-foo").
|
|
175
|
+
lhs = stripped[:eq].strip()
|
|
176
|
+
if lhs != "name":
|
|
177
|
+
continue
|
|
178
|
+
rhs = stripped[eq + 1 :].strip()
|
|
179
|
+
# Strip a trailing inline comment if present.
|
|
180
|
+
if "#" in rhs:
|
|
181
|
+
rhs = rhs[: rhs.index("#")].strip()
|
|
182
|
+
if len(rhs) >= 2 and rhs[0] in {'"', "'"} and rhs[-1] == rhs[0]:
|
|
183
|
+
return rhs[1:-1]
|
|
184
|
+
return None
|
|
@@ -29,7 +29,7 @@ from hyperi_ci.common import (
|
|
|
29
29
|
success,
|
|
30
30
|
warn,
|
|
31
31
|
)
|
|
32
|
-
from hyperi_ci.config import CIConfig, load_config
|
|
32
|
+
from hyperi_ci.config import VALID_PROJECT_STATUSES, CIConfig, load_config
|
|
33
33
|
from hyperi_ci.detect import detect_language
|
|
34
34
|
from hyperi_ci.quality import commit_validation, gitleaks
|
|
35
35
|
|
|
@@ -62,6 +62,19 @@ VALID_STAGES = (
|
|
|
62
62
|
"publish",
|
|
63
63
|
)
|
|
64
64
|
|
|
65
|
+
# Parenthetical clarifier appended to the "Project status: <X>" log line
|
|
66
|
+
# so a reader of the CI output immediately knows what each value means
|
|
67
|
+
# without having to consult the docs. `ga` and `legacy` defaults are
|
|
68
|
+
# bare — the clarifier carries the signal only where it adds something.
|
|
69
|
+
_STATUS_CLARIFIER: dict[str, str] = {
|
|
70
|
+
"experimental": " — pre-GA, no API commitment",
|
|
71
|
+
"alpha": " — pre-GA, expect breaks",
|
|
72
|
+
"beta": " — pre-GA, polishing",
|
|
73
|
+
"ga": "",
|
|
74
|
+
"legacy": " — being phased out, plan migration",
|
|
75
|
+
"deprecated": " — do not adopt, scheduled for removal",
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
# Languages that share a handler package. The left-hand name is what
|
|
66
79
|
# `detect_language()` returns (honest — describes what the project actually
|
|
67
80
|
# is); the right-hand name is the handler module to dispatch to. Keeping
|
|
@@ -387,6 +400,16 @@ def run_stage(
|
|
|
387
400
|
|
|
388
401
|
config = load_config(reload=True, project_dir=project_dir)
|
|
389
402
|
|
|
403
|
+
# Surface project lifecycle status so consumers reading CI logs can
|
|
404
|
+
# immediately see "oh, this isn't GA". Skipped silently when the
|
|
405
|
+
# field is unset — `.hyperi-ci.yaml` need not declare it. Always
|
|
406
|
+
# logged as INFO; the parenthetical clarifier on non-ga values
|
|
407
|
+
# carries the signal without elevating log level (a beta project
|
|
408
|
+
# isn't an error, just a fact).
|
|
409
|
+
status = str(config.get("project.status") or "").strip().lower()
|
|
410
|
+
if status in VALID_PROJECT_STATUSES:
|
|
411
|
+
info(f"Project status: {status}{_STATUS_CLARIFIER.get(status, '')}")
|
|
412
|
+
|
|
390
413
|
handler = _STAGE_HANDLERS[stage]
|
|
391
414
|
if stage == "build":
|
|
392
415
|
rc = handler(language, config, local=local)
|
|
@@ -158,6 +158,12 @@ def _render_hyperi_ci_yaml(
|
|
|
158
158
|
"""Render .hyperi-ci.yaml with language-specific defaults."""
|
|
159
159
|
config: dict = {
|
|
160
160
|
"language": language,
|
|
161
|
+
# Information-only: lifecycle stage of the project. Surfaced
|
|
162
|
+
# in CI logs and `hyperi-ci config`. Does not gate any
|
|
163
|
+
# behaviour. New projects default to `experimental` — bump as
|
|
164
|
+
# the project matures. Values: experimental | alpha | beta |
|
|
165
|
+
# ga | legacy | deprecated.
|
|
166
|
+
"project": {"status": "experimental"},
|
|
161
167
|
"quality": {"enabled": True},
|
|
162
168
|
"test": {"enabled": True},
|
|
163
169
|
"build": {"enabled": True, "strategies": ["native"]},
|
|
@@ -120,6 +120,105 @@ class TestDetectTier:
|
|
|
120
120
|
assert detect_tier(tmp_path) == Tier.PYTHON
|
|
121
121
|
|
|
122
122
|
|
|
123
|
+
class TestSelfMatchExclusion:
|
|
124
|
+
"""A library's own repo is NOT a Tier 1/2 consumer of itself.
|
|
125
|
+
|
|
126
|
+
These tests cover the case where a library's Cargo.toml /
|
|
127
|
+
pyproject.toml has its own package name (e.g. `hyperi-rustlib`)
|
|
128
|
+
in the `[package]` / `[project]` table. The substring match would
|
|
129
|
+
otherwise misdetect the library as a consumer and dispatch the
|
|
130
|
+
Tier 1/2 producer, which then fails with "no binary found".
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def test_hyperi_rustlib_own_repo_is_not_rust(self, tmp_path: Path) -> None:
|
|
134
|
+
# The library's own Cargo.toml has `name = "hyperi-rustlib"` in
|
|
135
|
+
# [package] but no `hyperi-rustlib = ...` dep line.
|
|
136
|
+
(tmp_path / "Cargo.toml").write_text(
|
|
137
|
+
'[package]\nname = "hyperi-rustlib"\nversion = "2.7.0"\n',
|
|
138
|
+
encoding="utf-8",
|
|
139
|
+
)
|
|
140
|
+
assert detect_tier(tmp_path) == Tier.NONE
|
|
141
|
+
|
|
142
|
+
def test_hyperi_rustlib_with_self_in_dev_deps_still_excluded(
|
|
143
|
+
self, tmp_path: Path
|
|
144
|
+
) -> None:
|
|
145
|
+
# Hypothetical: library lists its own name in [dev-dependencies]
|
|
146
|
+
# for an example-binary workspace pattern. Still not a consumer.
|
|
147
|
+
(tmp_path / "Cargo.toml").write_text(
|
|
148
|
+
'[package]\nname = "hyperi-rustlib"\nversion = "2.7.0"\n'
|
|
149
|
+
'[dev-dependencies]\nhyperi-rustlib = { path = "." }\n',
|
|
150
|
+
encoding="utf-8",
|
|
151
|
+
)
|
|
152
|
+
assert detect_tier(tmp_path) == Tier.NONE
|
|
153
|
+
|
|
154
|
+
def test_hyperi_pylib_own_repo_is_not_python(self, tmp_path: Path) -> None:
|
|
155
|
+
(tmp_path / "pyproject.toml").write_text(
|
|
156
|
+
'[project]\nname = "hyperi-pylib"\nversion = "2.24.0"\n',
|
|
157
|
+
encoding="utf-8",
|
|
158
|
+
)
|
|
159
|
+
assert detect_tier(tmp_path) == Tier.NONE
|
|
160
|
+
|
|
161
|
+
def test_hyperi_pylib_poetry_section_also_excluded(self, tmp_path: Path) -> None:
|
|
162
|
+
# Poetry-managed projects use [tool.poetry] instead of [project].
|
|
163
|
+
(tmp_path / "pyproject.toml").write_text(
|
|
164
|
+
'[tool.poetry]\nname = "hyperi-pylib"\nversion = "2.24.0"\n',
|
|
165
|
+
encoding="utf-8",
|
|
166
|
+
)
|
|
167
|
+
assert detect_tier(tmp_path) == Tier.NONE
|
|
168
|
+
|
|
169
|
+
def test_single_quoted_name_excluded(self, tmp_path: Path) -> None:
|
|
170
|
+
# TOML allows single-quoted strings — must still match.
|
|
171
|
+
(tmp_path / "Cargo.toml").write_text(
|
|
172
|
+
"[package]\nname = 'hyperi-rustlib'\nversion = '2.7.0'\n",
|
|
173
|
+
encoding="utf-8",
|
|
174
|
+
)
|
|
175
|
+
assert detect_tier(tmp_path) == Tier.NONE
|
|
176
|
+
|
|
177
|
+
def test_consumer_with_real_dep_still_detected(self, tmp_path: Path) -> None:
|
|
178
|
+
# A real consumer has its own name AND lists the library as a dep.
|
|
179
|
+
# Self-match exclusion must not break this case.
|
|
180
|
+
(tmp_path / "Cargo.toml").write_text(
|
|
181
|
+
'[package]\nname = "dfe-loader"\n[dependencies]\nhyperi-rustlib = "2.5"\n',
|
|
182
|
+
encoding="utf-8",
|
|
183
|
+
)
|
|
184
|
+
assert detect_tier(tmp_path) == Tier.RUST
|
|
185
|
+
|
|
186
|
+
def test_consumer_with_similar_prefix_not_misdetected(self, tmp_path: Path) -> None:
|
|
187
|
+
# `name = "hyperi-rustlib-extras"` should NOT count as self-match
|
|
188
|
+
# for `hyperi-rustlib` because the full string differs. This
|
|
189
|
+
# consumer DOES depend on hyperi-rustlib.
|
|
190
|
+
(tmp_path / "Cargo.toml").write_text(
|
|
191
|
+
'[package]\nname = "hyperi-rustlib-extras"\n'
|
|
192
|
+
'[dependencies]\nhyperi-rustlib = "2.5"\n',
|
|
193
|
+
encoding="utf-8",
|
|
194
|
+
)
|
|
195
|
+
assert detect_tier(tmp_path) == Tier.RUST
|
|
196
|
+
|
|
197
|
+
def test_name_with_trailing_comment_handled(self, tmp_path: Path) -> None:
|
|
198
|
+
# Inline comments after the name field shouldn't break parsing.
|
|
199
|
+
(tmp_path / "Cargo.toml").write_text(
|
|
200
|
+
'[package]\nname = "hyperi-rustlib" # the library itself\n',
|
|
201
|
+
encoding="utf-8",
|
|
202
|
+
)
|
|
203
|
+
assert detect_tier(tmp_path) == Tier.NONE
|
|
204
|
+
|
|
205
|
+
def test_name_outside_recognised_section_not_self_match(
|
|
206
|
+
self, tmp_path: Path
|
|
207
|
+
) -> None:
|
|
208
|
+
# A `name = "hyperi-rustlib"` in some other table (e.g.
|
|
209
|
+
# [features.something]) shouldn't count as the manifest's own
|
|
210
|
+
# name. We only treat [package] / [project] / [tool.poetry] as
|
|
211
|
+
# self-name sections.
|
|
212
|
+
(tmp_path / "Cargo.toml").write_text(
|
|
213
|
+
'[package]\nname = "consumer-app"\n'
|
|
214
|
+
"[features.weird]\n"
|
|
215
|
+
'name = "hyperi-rustlib"\n'
|
|
216
|
+
'[dependencies]\nhyperi-rustlib = "2.5"\n',
|
|
217
|
+
encoding="utf-8",
|
|
218
|
+
)
|
|
219
|
+
assert detect_tier(tmp_path) == Tier.RUST
|
|
220
|
+
|
|
221
|
+
|
|
123
222
|
class TestTierEnum:
|
|
124
223
|
"""`Tier` enum has the four values expected by callers."""
|
|
125
224
|
|
|
@@ -11,7 +11,13 @@ from pathlib import Path
|
|
|
11
11
|
|
|
12
12
|
import pytest
|
|
13
13
|
|
|
14
|
-
from hyperi_ci.config import
|
|
14
|
+
from hyperi_ci.config import (
|
|
15
|
+
VALID_PROJECT_STATUSES,
|
|
16
|
+
CIConfig,
|
|
17
|
+
_merge_deep,
|
|
18
|
+
_parse_env_value,
|
|
19
|
+
load_config,
|
|
20
|
+
)
|
|
15
21
|
|
|
16
22
|
|
|
17
23
|
class TestMergeDeep:
|
|
@@ -159,3 +165,65 @@ class TestLoadConfig:
|
|
|
159
165
|
monkeypatch.setenv("HYPERCI_LANGUAGE", "golang")
|
|
160
166
|
config = load_config(reload=True, project_dir=tmp_path)
|
|
161
167
|
assert config.get("language") == "golang"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class TestProjectStatus:
|
|
171
|
+
"""`project.status` is an information-only lifecycle stage field.
|
|
172
|
+
|
|
173
|
+
Surfaced in CI logs and `hyperi-ci config`. Does not gate any
|
|
174
|
+
behaviour. Six valid values; unknown values warn but don't fail.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
def test_valid_statuses_enum(self) -> None:
|
|
178
|
+
# Lock the vocabulary so a rename/typo elsewhere can't silently
|
|
179
|
+
# break the contract every consumer's `.hyperi-ci.yaml` expects.
|
|
180
|
+
assert VALID_PROJECT_STATUSES == (
|
|
181
|
+
"experimental",
|
|
182
|
+
"alpha",
|
|
183
|
+
"beta",
|
|
184
|
+
"ga",
|
|
185
|
+
"legacy",
|
|
186
|
+
"deprecated",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def test_set_status_reads_back(self, tmp_path: Path) -> None:
|
|
190
|
+
(tmp_path / ".hyperi-ci.yaml").write_text(
|
|
191
|
+
"language: rust\nproject:\n status: beta\n",
|
|
192
|
+
)
|
|
193
|
+
import hyperi_ci.config as cfg_mod
|
|
194
|
+
|
|
195
|
+
cfg_mod._config_cache = None
|
|
196
|
+
config = load_config(reload=True, project_dir=tmp_path)
|
|
197
|
+
assert config.get("project.status") == "beta"
|
|
198
|
+
|
|
199
|
+
def test_unset_status_returns_empty_or_none(self, tmp_path: Path) -> None:
|
|
200
|
+
# Default value in defaults.yaml is "" (empty string) — meaning
|
|
201
|
+
# "not declared". Skipping the field in `.hyperi-ci.yaml`
|
|
202
|
+
# leaves the default in place.
|
|
203
|
+
(tmp_path / ".hyperi-ci.yaml").write_text("language: rust\n")
|
|
204
|
+
import hyperi_ci.config as cfg_mod
|
|
205
|
+
|
|
206
|
+
cfg_mod._config_cache = None
|
|
207
|
+
config = load_config(reload=True, project_dir=tmp_path)
|
|
208
|
+
# Either "" (default from defaults.yaml) or None (no key at all)
|
|
209
|
+
# is acceptable — both mean "not declared".
|
|
210
|
+
status = config.get("project.status")
|
|
211
|
+
assert status in ("", None)
|
|
212
|
+
|
|
213
|
+
def test_unknown_status_warns_but_loads(
|
|
214
|
+
self,
|
|
215
|
+
tmp_path: Path,
|
|
216
|
+
capsys: pytest.CaptureFixture[str],
|
|
217
|
+
) -> None:
|
|
218
|
+
(tmp_path / ".hyperi-ci.yaml").write_text(
|
|
219
|
+
"language: rust\nproject:\n status: stable\n",
|
|
220
|
+
)
|
|
221
|
+
import hyperi_ci.config as cfg_mod
|
|
222
|
+
|
|
223
|
+
cfg_mod._config_cache = None
|
|
224
|
+
config = load_config(reload=True, project_dir=tmp_path)
|
|
225
|
+
# Config must still load — typos can't break the build.
|
|
226
|
+
assert config.language == "rust"
|
|
227
|
+
# The unknown value is preserved in raw config so operators can
|
|
228
|
+
# see what they wrote; only the log line warns.
|
|
229
|
+
assert config.get("project.status") == "stable"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Project: HyperI CI
|
|
2
|
+
# File: tests/unit/test_dispatch_status.py
|
|
3
|
+
# Purpose: Unit tests for project.status logging at stage start
|
|
4
|
+
#
|
|
5
|
+
# License: Proprietary — HYPERI PTY LIMITED
|
|
6
|
+
# Copyright: (c) 2026 HYPERI PTY LIMITED
|
|
7
|
+
"""Tests for the `Project status:` log line emitted by the dispatcher.
|
|
8
|
+
|
|
9
|
+
`project.status` is an information-only lifecycle field; the dispatcher
|
|
10
|
+
surfaces it at the top of every stage so log readers see what kind of
|
|
11
|
+
project they're looking at. Behaviour locked in here:
|
|
12
|
+
|
|
13
|
+
- Unset (default) → no log line emitted.
|
|
14
|
+
- Known status (any of VALID_PROJECT_STATUSES) → one INFO line with the
|
|
15
|
+
status and a clarifier phrase that explains what the status means.
|
|
16
|
+
- Unknown status (typo) → warned at config-load time and skipped by the
|
|
17
|
+
dispatcher's status block — verified in `test_config.py`.
|
|
18
|
+
|
|
19
|
+
The clarifier phrase is what makes non-GA stand out without elevating
|
|
20
|
+
log level. A line that reads `Project status: beta — pre-GA, polishing`
|
|
21
|
+
is unmistakable in a wall of INFO without being a WARN.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import pytest
|
|
27
|
+
|
|
28
|
+
from hyperi_ci.dispatch import _STATUS_CLARIFIER
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestStatusClarifierMap:
|
|
32
|
+
"""The clarifier map drives the user-visible signal — lock it in."""
|
|
33
|
+
|
|
34
|
+
def test_ga_has_no_clarifier(self) -> None:
|
|
35
|
+
# GA is the unmarked default — clarifier adds nothing, so the
|
|
36
|
+
# line reads simply "Project status: ga".
|
|
37
|
+
assert _STATUS_CLARIFIER["ga"] == ""
|
|
38
|
+
|
|
39
|
+
def test_pre_ga_statuses_say_pre_ga(self) -> None:
|
|
40
|
+
for pre_ga in ("experimental", "alpha", "beta"):
|
|
41
|
+
assert "pre-GA" in _STATUS_CLARIFIER[pre_ga], (
|
|
42
|
+
f"{pre_ga} clarifier missing 'pre-GA' marker"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def test_legacy_signals_migration(self) -> None:
|
|
46
|
+
assert "phased out" in _STATUS_CLARIFIER["legacy"]
|
|
47
|
+
assert "migration" in _STATUS_CLARIFIER["legacy"]
|
|
48
|
+
|
|
49
|
+
def test_deprecated_says_do_not_adopt(self) -> None:
|
|
50
|
+
assert "do not adopt" in _STATUS_CLARIFIER["deprecated"]
|
|
51
|
+
|
|
52
|
+
def test_all_valid_statuses_have_clarifier_entry(self) -> None:
|
|
53
|
+
# If we add a new status to the enum, this test forces us to
|
|
54
|
+
# decide what its clarifier phrase says rather than silently
|
|
55
|
+
# falling through to an empty default.
|
|
56
|
+
from hyperi_ci.config import VALID_PROJECT_STATUSES
|
|
57
|
+
|
|
58
|
+
for status in VALID_PROJECT_STATUSES:
|
|
59
|
+
assert status in _STATUS_CLARIFIER, (
|
|
60
|
+
f"{status} added to VALID_PROJECT_STATUSES but not to "
|
|
61
|
+
f"_STATUS_CLARIFIER — decide what the log line says"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def test_clarifier_has_no_warn_style_words(self) -> None:
|
|
65
|
+
# The whole point of the redesign: clarifier carries signal
|
|
66
|
+
# without being alarming. Reject "WARNING", "ERROR", "DANGER",
|
|
67
|
+
# "DO NOT USE" — these elevate tone past the info level the
|
|
68
|
+
# value deserves.
|
|
69
|
+
forbidden = ("WARNING", "ERROR", "DANGER", "DO NOT USE")
|
|
70
|
+
for status, phrase in _STATUS_CLARIFIER.items():
|
|
71
|
+
for word in forbidden:
|
|
72
|
+
assert word.lower() not in phrase.lower(), (
|
|
73
|
+
f"{status} clarifier '{phrase}' contains alarming "
|
|
74
|
+
f"word '{word}' — keep the tone at info level"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TestStatusLineFormat:
|
|
79
|
+
"""Format the dispatcher emits: 'Project status: <value><clarifier>'."""
|
|
80
|
+
|
|
81
|
+
@pytest.mark.parametrize(
|
|
82
|
+
"status,expected_substring",
|
|
83
|
+
[
|
|
84
|
+
("ga", "Project status: ga"),
|
|
85
|
+
("beta", "Project status: beta — pre-GA, polishing"),
|
|
86
|
+
("legacy", "Project status: legacy — being phased out, plan migration"),
|
|
87
|
+
],
|
|
88
|
+
)
|
|
89
|
+
def test_format_matches_clarifier_map(
|
|
90
|
+
self, status: str, expected_substring: str
|
|
91
|
+
) -> None:
|
|
92
|
+
# Construct the exact line the dispatcher emits so the format
|
|
93
|
+
# contract is locked. If someone changes the prefix to
|
|
94
|
+
# "Lifecycle:" or drops the em-dash, this test catches it.
|
|
95
|
+
line = f"Project status: {status}{_STATUS_CLARIFIER[status]}"
|
|
96
|
+
assert expected_substring in line
|