feat: module-migration-02 bundle extraction #924
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json | |
| # yamllint disable rule:line-length rule:truthy | |
| name: PR Orchestrator - SpecFact CLI | |
| on: | |
| pull_request: | |
| branches: [main, dev] | |
| paths-ignore: | |
| - "**/*.md" | |
| - "**/*.mdc" | |
| - "docs/**" | |
| push: | |
| branches: [main, dev] | |
| paths-ignore: | |
| - "**/*.md" | |
| - "**/*.mdc" | |
| - "docs/**" | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| env: | |
| PIP_PREFER_BINARY: "1" | |
| jobs: | |
| changes: | |
| name: Detect code changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| code_changed: ${{ steps.out.outputs.code_changed }} | |
| skip_tests_dev_to_main: ${{ steps.out.outputs.skip_tests_dev_to_main }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - uses: dorny/paths-filter@v3 | |
| id: filter | |
| with: | |
| filters: | | |
| code: | |
| - '**' | |
| - '!**/*.md' | |
| - '!**/*.mdc' | |
| - '!docs/**' | |
| - id: out | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| PR_BASE_REF: ${{ github.event.pull_request.base.ref }} | |
| PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} | |
| run: | | |
| if [ "$EVENT_NAME" = "workflow_dispatch" ]; then | |
| echo "code_changed=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "code_changed=${{ steps.filter.outputs.code }}" >> "$GITHUB_OUTPUT" | |
| fi | |
| if [ "$EVENT_NAME" = "pull_request" ] && [ "$PR_BASE_REF" = "main" ] && [ "$PR_HEAD_REF" = "dev" ]; then | |
| echo "skip_tests_dev_to_main=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "skip_tests_dev_to_main=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| verify-module-signatures: | |
| name: Verify Module Signatures | |
| needs: [changes] | |
| if: needs.changes.outputs.code_changed == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| cache: "pip" | |
| - name: Install verifier dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install pyyaml cryptography cffi | |
| - name: Verify bundled module checksums and signatures | |
| run: | | |
| BASE_REF="" | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| BASE_REF="origin/${{ github.event.pull_request.base.ref }}" | |
| fi | |
| if [ -n "$BASE_REF" ]; then | |
| python scripts/verify-modules-signature.py --require-signature --enforce-version-bump --version-check-base "$BASE_REF" | |
| else | |
| python scripts/verify-modules-signature.py --require-signature --enforce-version-bump | |
| fi | |
| tests: | |
| name: Tests (Python 3.12) | |
| needs: [changes, verify-module-signatures] | |
| if: needs.changes.outputs.code_changed == 'true' | |
| outputs: | |
| run_unit_coverage: ${{ steps.detect-unit.outputs.run_unit_coverage }} | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Skip full run (dev→main PR) | |
| if: needs.changes.outputs.skip_tests_dev_to_main == 'true' | |
| run: | | |
| echo "✅ Dev→main PR: tests already passed on dev; skipping full test run." | |
| - uses: actions/checkout@v4 | |
| if: needs.changes.outputs.skip_tests_dev_to_main != 'true' | |
| - name: Checkout module bundles repo | |
| if: needs.changes.outputs.skip_tests_dev_to_main != 'true' | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: nold-ai/specfact-cli-modules | |
| path: specfact-cli-modules | |
| ref: ${{ (github.ref == 'refs/heads/main' || github.head_ref == 'main') && 'main' || 'dev' }} | |
| - name: Export module bundles path | |
| if: needs.changes.outputs.skip_tests_dev_to_main != 'true' | |
| run: echo "SPECFACT_MODULES_REPO=${GITHUB_WORKSPACE}/specfact-cli-modules" >> "$GITHUB_ENV" | |
| - name: Set up Python 3.12 | |
| if: needs.changes.outputs.skip_tests_dev_to_main != 'true' | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| cache: "pip" | |
| cache-dependency-path: | | |
| pyproject.toml | |
| - name: Install hatch and coverage | |
| if: needs.changes.outputs.skip_tests_dev_to_main != 'true' | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install "hatch" "virtualenv<21" coverage | |
| - name: Cache hatch environments | |
| if: needs.changes.outputs.skip_tests_dev_to_main != 'true' | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.local/share/hatch | |
| ~/.cache/uv | |
| key: ${{ runner.os }}-hatch-tests-py312-${{ hashFiles('pyproject.toml', 'src/specfact_cli/modules/*/__init__.py') }} | |
| restore-keys: | | |
| ${{ runner.os }}-hatch-tests-py312- | |
| ${{ runner.os }}-hatch- | |
| - name: Create test output directories | |
| if: needs.changes.outputs.skip_tests_dev_to_main != 'true' | |
| shell: bash | |
| run: | | |
| mkdir -p logs/tests/junit logs/tests/coverage logs/tests/workflows | |
| - name: Set run_unit_coverage (or skip for dev→main) | |
| id: detect-unit | |
| shell: bash | |
| run: | | |
| if [ "${{ needs.changes.outputs.skip_tests_dev_to_main }}" = "true" ]; then | |
| echo "run_unit_coverage=false" >> "$GITHUB_OUTPUT" | |
| else | |
| COUNT=$(find tests/unit -name "test_*.py" 2>/dev/null | wc -l) | |
| if [ "$COUNT" -gt 0 ]; then | |
| echo "run_unit_coverage=true" >> "$GITHUB_OUTPUT" | |
| echo "RUN_UNIT_COVERAGE=true" >> "$GITHUB_ENV" | |
| echo "Detected $COUNT unit test files. Will run coverage steps." | |
| else | |
| echo "run_unit_coverage=false" >> "$GITHUB_OUTPUT" | |
| echo "RUN_UNIT_COVERAGE=false" >> "$GITHUB_ENV" | |
| echo "No unit tests detected. Skipping coverage steps." | |
| fi | |
| fi | |
| - name: Run full test suite (smart-test-full) | |
| if: needs.changes.outputs.skip_tests_dev_to_main != 'true' | |
| shell: bash | |
| env: | |
| CONTRACT_FIRST_TESTING: "true" | |
| TEST_MODE: "true" | |
| HATCH_TEST_ENV: "py3.12" | |
| SMART_TEST_TIMEOUT_SECONDS: "1800" | |
| PYTEST_ADDOPTS: "-r fEw" | |
| run: | | |
| echo "🧪 Running full test suite (smart-test-full, Python 3.12)..." | |
| echo "ℹ️ HATCH_TEST_ENV=${HATCH_TEST_ENV}" | |
| hatch run smart-test-full | |
| - name: Generate coverage XML for quality gates | |
| if: needs.changes.outputs.skip_tests_dev_to_main != 'true' && env.RUN_UNIT_COVERAGE == 'true' | |
| run: | | |
| hatch -e hatch-test.py3.12 run xml | |
| - name: Upload test logs | |
| if: needs.changes.outputs.skip_tests_dev_to_main != 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-logs | |
| path: logs/tests/ | |
| if-no-files-found: ignore | |
| - name: Upload coverage artifacts | |
| if: needs.changes.outputs.skip_tests_dev_to_main != 'true' && env.RUN_UNIT_COVERAGE == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-reports | |
| path: logs/tests/coverage/coverage.xml | |
| if-no-files-found: error | |
| compat-py311: | |
| name: Compatibility (Python 3.11) | |
| runs-on: ubuntu-latest | |
| needs: [changes, tests] | |
| if: needs.changes.outputs.skip_tests_dev_to_main != 'true' | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Checkout module bundles repo | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: nold-ai/specfact-cli-modules | |
| path: specfact-cli-modules | |
| ref: ${{ (github.ref == 'refs/heads/main' || github.head_ref == 'main') && 'main' || 'dev' }} | |
| - name: Export module bundles path | |
| run: echo "SPECFACT_MODULES_REPO=${GITHUB_WORKSPACE}/specfact-cli-modules" >> "$GITHUB_ENV" | |
| - name: Set up Python 3.11 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| cache: "pip" | |
| cache-dependency-path: | | |
| pyproject.toml | |
| - name: Install hatch | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install "hatch" "virtualenv<21" | |
| - name: Cache hatch environments | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.local/share/hatch | |
| ~/.cache/uv | |
| key: ${{ runner.os }}-hatch-compat-py311-${{ hashFiles('pyproject.toml', 'src/specfact_cli/modules/*/__init__.py') }} | |
| restore-keys: | | |
| ${{ runner.os }}-hatch-compat-py311- | |
| ${{ runner.os }}-hatch- | |
| - name: Run Python 3.11 compatibility tests (hatch-test matrix env) | |
| run: | | |
| echo "🔁 Python 3.11 compatibility checks" | |
| mkdir -p logs/compat-py311 | |
| COMPAT_LOG="logs/compat-py311/compat_$(date -u +%Y%m%d_%H%M%S).log" | |
| { | |
| hatch -e hatch-test.py3.11 run run -- -r fEw tests/unit tests/integration || echo "⚠️ Some tests failed (advisory)" | |
| hatch -e hatch-test.py3.11 run xml || true | |
| } 2>&1 | tee "$COMPAT_LOG" | |
| - name: Upload compat-py311 logs | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: compat-py311-logs | |
| path: logs/compat-py311/ | |
| if-no-files-found: ignore | |
| contract-first-ci: | |
| name: Contract-First CI | |
| runs-on: ubuntu-latest | |
| needs: [changes, tests, compat-py311] | |
| if: needs.changes.outputs.skip_tests_dev_to_main != 'true' | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Checkout module bundles repo | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: nold-ai/specfact-cli-modules | |
| path: specfact-cli-modules | |
| ref: ${{ (github.ref == 'refs/heads/main' || github.head_ref == 'main') && 'main' || 'dev' }} | |
| - name: Export module bundles path | |
| run: echo "SPECFACT_MODULES_REPO=${GITHUB_WORKSPACE}/specfact-cli-modules" >> "$GITHUB_ENV" | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| cache: "pip" | |
| cache-dependency-path: | | |
| pyproject.toml | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install "hatch" "virtualenv<21" | |
| - name: Cache hatch environments | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.local/share/hatch | |
| ~/.cache/uv | |
| key: ${{ runner.os }}-hatch-contract-first-py312-${{ hashFiles('pyproject.toml', 'src/specfact_cli/modules/*/__init__.py') }} | |
| restore-keys: | | |
| ${{ runner.os }}-hatch-contract-first-py312- | |
| ${{ runner.os }}-hatch- | |
| - name: Prepare repro log directory | |
| run: mkdir -p logs/repro | |
| - name: Run contract validation and exploration | |
| id: repro | |
| run: | | |
| echo "🔍 Validating runtime contracts..." | |
| REPRO_LOG="logs/repro/repro_$(date -u +%Y%m%d_%H%M%S).log" | |
| echo "Running specfact repro with required CrossHair... (log: $REPRO_LOG)" | |
| hatch run specfact repro --verbose --crosshair-required --budget 120 2>&1 | tee "$REPRO_LOG" || echo "SpecFact repro found issues" | |
| - name: Upload repro logs | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: repro-logs | |
| path: logs/repro/ | |
| if-no-files-found: ignore | |
| - name: Upload repro reports | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: repro-reports | |
| path: .specfact/reports/enforcement/ | |
| if-no-files-found: ignore | |
| cli-validation: | |
| name: CLI Command Validation | |
| runs-on: ubuntu-latest | |
| needs: [changes, contract-first-ci] | |
| if: needs.changes.outputs.skip_tests_dev_to_main != 'true' | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| cache: "pip" | |
| cache-dependency-path: | | |
| pyproject.toml | |
| - name: Install CLI | |
| run: | | |
| echo "Installing SpecFact CLI..." | |
| pip install -e . | |
| - name: Validate CLI commands | |
| run: | | |
| echo "🔍 Validating CLI commands..." | |
| specfact --help || echo "⚠️ CLI not yet fully implemented" | |
| echo "✅ CLI validation complete (advisory)" | |
| quality-gates: | |
| name: Quality Gates (Advisory) | |
| runs-on: ubuntu-latest | |
| needs: [changes, tests] | |
| if: needs.changes.outputs.skip_tests_dev_to_main != 'true' && needs.tests.outputs.run_unit_coverage == 'true' | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| cache: "pip" | |
| cache-dependency-path: | | |
| pyproject.toml | |
| - name: Download coverage artifacts from Tests | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: coverage-reports | |
| path: logs/tests/coverage | |
| - name: Validate coverage artifact presence | |
| run: | | |
| test -f logs/tests/coverage/coverage.xml || (echo "❌ Missing coverage.xml" && exit 1) | |
| echo "✅ Found coverage.xml" | |
| - name: Run quality gates (advisory) | |
| run: | | |
| mkdir -p logs/quality-gates | |
| QG_LOG="logs/quality-gates/quality-gates_$(date -u +%Y%m%d_%H%M%S).log" | |
| { | |
| echo "🔍 Checking coverage (advisory only)..." | |
| COVERAGE_PERCENT_XML=$(grep -o "line-rate=\"[0-9.]*\"" logs/tests/coverage/coverage.xml | head -1 | sed 's/line-rate=\"//' | sed 's/\"//') | |
| if [ -n "$COVERAGE_PERCENT_XML" ] && [ "$COVERAGE_PERCENT_XML" != "0" ]; then | |
| COVERAGE_PERCENT_INT=$(echo "$COVERAGE_PERCENT_XML * 100" | bc -l | cut -d. -f1) | |
| else | |
| COVERAGE_PERCENT_INT=0 | |
| fi | |
| echo "📊 Line coverage (advisory): ${COVERAGE_PERCENT_INT}%" | |
| if [ "$COVERAGE_PERCENT_INT" -lt 30 ]; then | |
| echo "⚠️ Advisory: coverage below 30% — permitted under contract-first; prioritize contract/scenario gaps." | |
| else | |
| echo "✅ Advisory: coverage acceptable" | |
| fi | |
| } 2>&1 | tee "$QG_LOG" | |
| - name: Upload quality-gates logs | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: quality-gates-logs | |
| path: logs/quality-gates/ | |
| if-no-files-found: ignore | |
| type-checking: | |
| name: Type Checking (basedpyright) | |
| runs-on: ubuntu-latest | |
| needs: [changes, tests] | |
| if: needs.changes.outputs.skip_tests_dev_to_main != 'true' | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| cache: "pip" | |
| cache-dependency-path: | | |
| pyproject.toml | |
| - name: Install hatch | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install "hatch" "virtualenv<21" | |
| - name: Cache hatch environments | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.local/share/hatch | |
| ~/.cache/uv | |
| key: ${{ runner.os }}-hatch-typecheck-py312-${{ hashFiles('pyproject.toml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-hatch-typecheck-py312- | |
| ${{ runner.os }}-hatch- | |
| - name: Run type checking | |
| run: | | |
| echo "🔍 Running basedpyright type checking..." | |
| mkdir -p logs/type-check | |
| TYPE_CHECK_LOG="logs/type-check/type-check_$(date -u +%Y%m%d_%H%M%S).log" | |
| hatch run type-check 2>&1 | tee "$TYPE_CHECK_LOG" | |
| exit "${PIPESTATUS[0]:-$?}" | |
| - name: Upload type-check logs | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: type-check-logs | |
| path: logs/type-check/ | |
| if-no-files-found: ignore | |
| linting: | |
| name: Linting (ruff, pylint) | |
| runs-on: ubuntu-latest | |
| needs: [changes, tests] | |
| if: needs.changes.outputs.skip_tests_dev_to_main != 'true' | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| cache: "pip" | |
| cache-dependency-path: | | |
| pyproject.toml | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install "hatch" "virtualenv<21" | |
| - name: Cache hatch environments | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.local/share/hatch | |
| ~/.cache/uv | |
| key: ${{ runner.os }}-hatch-lint-py312-${{ hashFiles('pyproject.toml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-hatch-lint-py312- | |
| ${{ runner.os }}-hatch- | |
| - name: Run linting | |
| run: | | |
| echo "🔍 Running linting checks..." | |
| mkdir -p logs/lint | |
| LINT_LOG="logs/lint/lint_$(date -u +%Y%m%d_%H%M%S).log" | |
| hatch run lint 2>&1 | tee "$LINT_LOG" || echo "⚠️ Linting incomplete" | |
| - name: Upload lint logs | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: lint-logs | |
| path: logs/lint/ | |
| if-no-files-found: ignore | |
| package-validation: | |
| name: Package Validation (uvx/pip) | |
| runs-on: ubuntu-latest | |
| needs: [tests, compat-py311, contract-first-ci, cli-validation, type-checking, linting] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| cache: "pip" | |
| cache-dependency-path: | | |
| pyproject.toml | |
| - name: Install build tools | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install build twine "hatch" "virtualenv<21" | |
| - name: Build package | |
| run: | | |
| echo "📦 Building SpecFact CLI package..." | |
| hatch build | |
| ls -lh dist/ | |
| - name: Validate package | |
| run: | | |
| echo "🔍 Validating package with twine..." | |
| twine check dist/* | |
| - name: Test installation | |
| run: | | |
| echo "📥 Testing pip installation..." | |
| pip install dist/*.whl | |
| specfact --version || echo "⚠️ CLI not yet fully implemented" | |
| pip uninstall -y specfact-cli | |
| - name: Upload package artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: python-package | |
| path: dist/ | |
| if-no-files-found: error | |
| publish-pypi: | |
| name: Publish to PyPI | |
| runs-on: ubuntu-latest | |
| needs: [package-validation] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| outputs: | |
| published: ${{ steps.publish.outputs.published }} | |
| version: ${{ steps.publish.outputs.version }} | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| cache: "pip" | |
| cache-dependency-path: | | |
| pyproject.toml | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install build twine packaging | |
| # Note: tomllib is part of Python 3.11+ standard library | |
| # This project requires Python >= 3.11, so no additional TOML library needed | |
| - name: Make script executable | |
| run: chmod +x .github/workflows/scripts/check-and-publish-pypi.sh | |
| - name: Check version and publish to PyPI | |
| id: publish | |
| env: | |
| PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }} | |
| run: | | |
| ./.github/workflows/scripts/check-and-publish-pypi.sh | |
| - name: Summary | |
| if: always() | |
| run: | | |
| PUBLISHED="${{ steps.publish.outputs.published }}" | |
| VERSION="${{ steps.publish.outputs.version }}" | |
| { | |
| echo "## PyPI Publication Summary" | |
| echo "| Parameter | Value |" | |
| echo "|-----------|--------|" | |
| echo "| Version | $VERSION |" | |
| echo "| Published | $PUBLISHED |" | |
| if [ "$PUBLISHED" = "true" ]; then | |
| echo "| Status | ✅ Published to PyPI |" | |
| else | |
| echo "| Status | ⏭️ Skipped (version not newer) |" | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| create-release: | |
| name: Create GitHub Release | |
| runs-on: ubuntu-latest | |
| needs: [publish-pypi] | |
| if: needs.publish-pypi.outputs.published == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Install GitHub CLI | |
| run: | | |
| type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y) | |
| curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \ | |
| && sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \ | |
| && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ | |
| && sudo apt update \ | |
| && sudo apt install gh -y | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Make scripts executable | |
| run: | | |
| chmod +x .github/workflows/scripts/generate-release-notes.sh | |
| chmod +x .github/workflows/scripts/create-github-release.sh | |
| chmod +x scripts/sign-module.sh | |
| - name: Sign bundled module manifests (release hardening) | |
| env: | |
| SPECFACT_MODULE_PRIVATE_SIGN_KEY: ${{ secrets.SPECFACT_MODULE_PRIVATE_SIGN_KEY }} | |
| SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE: ${{ secrets.SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE }} | |
| run: | | |
| if [ -z "${SPECFACT_MODULE_PRIVATE_SIGN_KEY}" ]; then | |
| echo "❌ Missing required secret: SPECFACT_MODULE_PRIVATE_SIGN_KEY" | |
| exit 1 | |
| fi | |
| if [ -z "${SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE}" ]; then | |
| echo "❌ Missing required secret: SPECFACT_MODULE_PRIVATE_SIGN_KEY_PASSPHRASE" | |
| exit 1 | |
| fi | |
| python -m pip install --upgrade pip | |
| python -m pip install pyyaml cryptography cffi | |
| python - <<'PY' | |
| import cffi | |
| import cryptography | |
| import yaml | |
| print("✅ signing dependencies available:", yaml.__version__, cryptography.__version__, cffi.__version__) | |
| PY | |
| BASE_REF="${{ github.event.before }}" | |
| if [ -z "$BASE_REF" ] || [ "$BASE_REF" = "0000000000000000000000000000000000000000" ]; then | |
| BASE_REF="HEAD~1" | |
| fi | |
| git rev-parse --verify "$BASE_REF" >/dev/null 2>&1 || BASE_REF="HEAD~1" | |
| echo "Using module-signing base ref: $BASE_REF" | |
| python scripts/sign-modules.py --changed-only --base-ref "$BASE_REF" --bump-version patch | |
| - name: Get version from PyPI publish step | |
| id: get_version | |
| run: | | |
| # Use version from publish-pypi job output | |
| VERSION="${{ needs.publish-pypi.outputs.version }}" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "📦 Version: $VERSION" | |
| - name: Create GitHub Release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| VERSION="${{ steps.get_version.outputs.version }}" | |
| echo "🚀 Creating GitHub release for version $VERSION..." | |
| ./.github/workflows/scripts/create-github-release.sh "$VERSION" | |
| - name: Release Summary | |
| if: always() | |
| run: | | |
| VERSION="${{ steps.get_version.outputs.version }}" | |
| { | |
| echo "## GitHub Release Summary" | |
| echo "| Parameter | Value |" | |
| echo "|-----------|--------|" | |
| echo "| Version | $VERSION |" | |
| echo "| Status | ✅ Release created |" | |
| echo "| URL | https://github.com/${{ github.repository }}/releases/tag/v${VERSION} |" | |
| } >> "$GITHUB_STEP_SUMMARY" |