diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c93f004..ed6182d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,12 +15,12 @@ concurrency: cancel-in-progress: true jobs: - ci: + tests: runs-on: ubuntu-latest strategy: fail-fast: false matrix: - python-version: [ "3.11", "3.12" ] + python-version: [ "3.10", "3.11", "3.12" ] steps: - name: Checkout @@ -46,3 +46,54 @@ jobs: - name: Tests (pytest) run: | pytest -q + + protected-file-guard: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fail if protected paths are modified + run: | + PROTECTED_PATHS=( + "authority_gate.py" + "stop_machine.py" + "commit_gate/" + ".github/workflows/" + ) + CHANGED=$(git diff --name-only origin/${{ github.base_ref }}..HEAD) + VIOLATIONS="" + for path in "${PROTECTED_PATHS[@]}"; do + if echo "$CHANGED" | grep -q "^${path}"; then + VIOLATIONS="${VIOLATIONS}\n - ${path}" + fi + done + if [ -n "$VIOLATIONS" ]; then + echo "::error::Protected paths modified — triple-lock review required:${VIOLATIONS}" + exit 1 + fi + echo "No protected paths modified." + + build-artefact: + runs-on: ubuntu-latest + needs: tests + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build source artefact + run: | + zip -r constraint-workshop-${{ github.sha }}.zip \ + authority_gate.py stop_machine.py mgtp/ commit_gate/ registry/ \ + --exclude "*/__pycache__/*" --exclude "*/.git/*" + + - name: Upload artefact + uses: actions/upload-artifact@v4 + with: + name: constraint-workshop-${{ github.sha }} + path: constraint-workshop-${{ github.sha }}.zip + retention-days: 90 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ec7949a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,92 @@ +name: Release + +on: + push: + tags: + - "v[0-9]*.[0-9]*.[0-9]*" + +permissions: + contents: write + +jobs: + verify-protected-branch: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Verify tag is reachable from main + run: | + git fetch origin main + TAG_SHA=$(git rev-parse HEAD) + if ! git merge-base --is-ancestor "$TAG_SHA" "origin/main"; then + echo "::error::Tag ${{ github.ref_name }} (${TAG_SHA}) is not on the protected main branch." + exit 1 + fi + echo "Tag ${TAG_SHA} is on main. Proceeding." + + tests: + runs-on: ubuntu-latest + needs: verify-protected-branch + strategy: + fail-fast: false + matrix: + python-version: [ "3.10", "3.11", "3.12" ] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff pytest + + - name: Lint (ruff) + run: ruff check . + + - name: Tests (pytest) + run: pytest -q + + build-release-artefact: + runs-on: ubuntu-latest + needs: tests + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Record commit SHA + id: meta + run: | + echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + echo "tag=${{ github.ref_name }}" >> "$GITHUB_OUTPUT" + + - name: Build release artefact + run: | + zip -r constraint-workshop-${{ github.ref_name }}.zip \ + authority_gate.py stop_machine.py mgtp/ commit_gate/ registry/ \ + --exclude "*/__pycache__/*" --exclude "*/.git/*" + sha256sum constraint-workshop-${{ github.ref_name }}.zip \ + > constraint-workshop-${{ github.ref_name }}.zip.sha256 + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: "Release ${{ github.ref_name }}" + body: | + ## Release ${{ github.ref_name }} + + **Commit SHA:** `${{ steps.meta.outputs.sha }}` + **Branch:** `main` (verified) + + All CI checks (Python 3.10, 3.11, 3.12) passed before this release was built. + files: | + constraint-workshop-${{ github.ref_name }}.zip + constraint-workshop-${{ github.ref_name }}.zip.sha256