Merge pull request #265 from sheaf-project/release-v1.3.6 #641
Workflow file for this run
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
| name: CI | |
| # | |
| on: | |
| push: | |
| # preview/** gets the full build+publish flow so a feature line can be | |
| # deployed to a test instance before it is anywhere near a release. The | |
| # release approval gate only ever applies to tags, so preview images | |
| # publish once lint and tests pass, tagged with the commit sha and a | |
| # sanitised branch-name tag (preview/foo -> preview-foo). | |
| branches: [main, "preview/**"] | |
| tags: ["v*"] | |
| pull_request: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| packages: write | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| jobs: | |
| lint: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| cache: npm | |
| cache-dependency-path: web/package-lock.json | |
| - name: Install backend deps | |
| run: pip install ruff | |
| - name: Lint backend | |
| run: ruff check sheaf/ | |
| # Runs against the PR merge result, so a migration that collided with | |
| # another PR (duplicate revision id, or a second head off the same | |
| # parent) fails here in seconds with a clear message instead of as a | |
| # slow "app did not become ready" timeout in the test job. | |
| - name: Check migration graph | |
| run: python scripts/check_migrations.py | |
| - name: Install frontend deps | |
| run: cd web && npm ci | |
| - name: Lint frontend | |
| run: cd web && npm run lint | |
| - name: Type check frontend | |
| run: cd web && npx tsc --noEmit | |
| test: | |
| runs-on: ubuntu-latest | |
| needs: lint | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v4 | |
| - name: Install test deps | |
| run: uv pip install --system -e ".[dev]" | |
| - name: Run test suite | |
| run: ./run_tests.sh | |
| # Fast pre-checks for a tagged release, BEFORE the approval gate, so an | |
| # obviously-bad tag (not on main, or a version mismatch) fails in seconds | |
| # without bothering the approver. | |
| preflight: | |
| name: Release preflight | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # ancestor check needs full history | |
| - name: Validate tag is on main | |
| run: | | |
| git fetch origin main | |
| if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then | |
| echo "::error::tagged commit $GITHUB_SHA is not on main - merge first, then tag" | |
| exit 1 | |
| fi | |
| - name: Verify pyproject.toml version matches tag | |
| run: | | |
| TAG="${GITHUB_REF_NAME#v}" | |
| PY=$(grep -E '^version = ' pyproject.toml | head -1 | sed -E 's/version = "(.*)"/\1/') | |
| if [ "$TAG" != "$PY" ]; then | |
| echo "::error::tag ${GITHUB_REF_NAME} does not match pyproject.toml version ${PY}" | |
| exit 1 | |
| fi | |
| echo "ok: tag ${GITHUB_REF_NAME} matches pyproject.toml ${PY}" | |
| # Manual-approval gate for a tagged release, placed at the START of the | |
| # release flow (right after preflight) so approval is not blocked behind the | |
| # ~15-minute test run. Configure repo Settings -> Environments -> "release" | |
| # with required reviewers. The build/publish jobs below wait on this for tag | |
| # pushes; on a branch push it is skipped and they proceed normally. | |
| approve-release: | |
| name: Approve release | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| needs: [preflight] | |
| runs-on: ubuntu-latest | |
| environment: release | |
| steps: | |
| - run: echo "Release ${{ github.ref_name }} approved." | |
| docker: | |
| runs-on: ubuntu-latest | |
| needs: [lint, test, approve-release] | |
| # Any push, once lint + test pass. A tag push additionally waits for the | |
| # approval gate above; on a branch push approve-release is skipped, which | |
| # the skipped-result check tolerates. | |
| if: >- | |
| always() && | |
| github.event_name == 'push' && | |
| needs.lint.result == 'success' && | |
| needs.test.result == 'success' && | |
| (needs.approve-release.result == 'success' || needs.approve-release.result == 'skipped') | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write # sigstore/cosign keyless OIDC | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: sheaf | |
| image: sheaf | |
| dockerfile: Dockerfile.backend | |
| build_args: "" | |
| - name: sheaf-devtools | |
| image: sheaf-devtools | |
| dockerfile: Dockerfile.backend | |
| build_args: | | |
| INCLUDE_DEV_TOOLS=true | |
| - name: sheaf-web | |
| image: sheaf-web | |
| dockerfile: Dockerfile.web | |
| build_args: "" | |
| - name: sheaf-aio | |
| image: sheaf-aio | |
| dockerfile: Dockerfile.aio | |
| build_args: "" | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # Tag validity (on main, version matches) is checked in the preflight | |
| # job that gates this one on tag pushes, so no per-image check here. | |
| - uses: docker/setup-qemu-action@v3 | |
| - uses: docker/setup-buildx-action@v3 | |
| - uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - uses: docker/metadata-action@v5 | |
| id: meta | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.image }} | |
| tags: | | |
| type=sha,format=long | |
| type=raw,value=head,enable={{is_default_branch}} | |
| type=ref,event=branch,enable=${{ github.ref_name != 'main' }} | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| # latest=auto moves :latest only for a stable semver tag. The old | |
| # raw rule fired on ANY v* tag, so the first v1.x.y-beta.N we ever | |
| # cut would have repointed :latest for every deploy that pulls it. | |
| # A prerelease (hyphenated) or malformed tag now gets only its own | |
| # version tag, or nothing. | |
| flavor: | | |
| latest=auto | |
| - uses: docker/build-push-action@v6 | |
| id: build | |
| with: | |
| context: . | |
| file: ${{ matrix.dockerfile }} | |
| push: true | |
| platforms: linux/amd64,linux/arm64 | |
| build-args: | | |
| ${{ matrix.build_args }} | |
| GIT_COMMIT=${{ github.sha }} | |
| GIT_TAG=${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || '' }} | |
| BUILD_TIME=${{ github.event.head_commit.timestamp || github.run_started_at }} | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: type=gha,scope=${{ matrix.name }} | |
| cache-to: type=gha,mode=max,scope=${{ matrix.name }} | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@v3 | |
| - name: Install syft | |
| uses: anchore/sbom-action/download-syft@v0 | |
| # Sign by digest. cosign stores the signature at <repo>:sha256-DIGEST.sig | |
| # so a single sign call covers every tag pointing at the same digest. | |
| # The retry wrapper covers the GH Actions OIDC token endpoint flaking | |
| # under load - signing is idempotent at the registry layer. | |
| - name: Sign image | |
| env: | |
| REF: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.image }}@${{ steps.build.outputs.digest }} | |
| run: | | |
| ./.github/scripts/with-retry.sh cosign sign --yes \ | |
| -a "git_sha=${{ github.sha }}" \ | |
| -a "git_ref=${{ github.ref }}" \ | |
| -a "workflow=${{ github.workflow }}" \ | |
| -a "run_id=${{ github.run_id }}" \ | |
| "$REF" | |
| - name: Generate SBOM (SPDX) | |
| env: | |
| REF: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.image }}@${{ steps.build.outputs.digest }} | |
| run: syft "$REF" -o spdx-json > sbom.spdx.json | |
| - name: Attest SBOM | |
| env: | |
| REF: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.image }}@${{ steps.build.outputs.digest }} | |
| run: | | |
| ./.github/scripts/with-retry.sh cosign attest --yes \ | |
| --predicate sbom.spdx.json \ | |
| --type spdxjson \ | |
| "$REF" | |
| # Build manifest attestation — only for sheaf-web. Re-run the frontend | |
| # build on the runner to produce dist/build-manifest.json, then attest | |
| # it against the image. Same machine + same envs as the docker build, | |
| # so the resulting manifest mirrors what's served from the image. | |
| - name: Setup Node (manifest) | |
| if: matrix.name == 'sheaf-web' | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| cache: npm | |
| cache-dependency-path: web/package-lock.json | |
| - name: Build frontend manifest | |
| if: matrix.name == 'sheaf-web' | |
| env: | |
| VITE_GIT_COMMIT: ${{ github.sha }} | |
| VITE_GIT_TAG: ${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || '' }} | |
| VITE_BUILD_TIME: ${{ github.event.head_commit.timestamp || github.run_started_at }} | |
| run: | | |
| cd web | |
| npm ci | |
| npm run build | |
| - name: Attest build manifest | |
| if: matrix.name == 'sheaf-web' | |
| env: | |
| REF: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.image }}@${{ steps.build.outputs.digest }} | |
| run: | | |
| ./.github/scripts/with-retry.sh cosign attest --yes \ | |
| --predicate web/dist/build-manifest.json \ | |
| --type custom \ | |
| "$REF" | |
| # Publishes the GitHub release page + verifiable artefacts. The manual | |
| # approval gate now runs up front (see the approve-release job), and preflight | |
| # already checked the tag, so by the time this runs the release is approved | |
| # and validated; it just needs the signed images from the docker job. | |
| release: | |
| name: Publish GitHub release | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| needs: [docker] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| cache: npm | |
| cache-dependency-path: web/package-lock.json | |
| - name: Build frontend | |
| env: | |
| VITE_GIT_COMMIT: ${{ github.sha }} | |
| VITE_GIT_TAG: ${{ github.ref_name }} | |
| VITE_BUILD_TIME: ${{ github.event.head_commit.timestamp || github.run_started_at }} | |
| run: | | |
| cd web | |
| npm ci | |
| npm run build | |
| - name: Package frontend tarball | |
| run: | | |
| tar czf "web-dist-${GITHUB_REF_NAME}.tar.gz" -C web/dist . | |
| # Extract this version's section out of CHANGELOG.md for the release | |
| # body. Falls through to empty if the section is missing — auto-notes | |
| # still get appended either way. | |
| - name: Extract changelog section | |
| run: | | |
| awk -v ver="${GITHUB_REF_NAME}" ' | |
| $0 ~ "^## \\[" ver "\\]" { capture=1; next } | |
| capture && /^## \[/ { exit } | |
| capture { print } | |
| ' CHANGELOG.md > release-body.md || true | |
| if [ ! -s release-body.md ]; then | |
| echo "(no CHANGELOG.md section for ${GITHUB_REF_NAME})" > release-body.md | |
| fi | |
| - name: Create GitHub release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ github.ref_name }} | |
| name: ${{ github.ref_name }} | |
| body_path: release-body.md | |
| generate_release_notes: true | |
| prerelease: ${{ startsWith(github.ref_name, 'v0.') }} | |
| files: | | |
| web-dist-${{ github.ref_name }}.tar.gz | |
| web/dist/build-manifest.json | |
| # Let downstream tooling know a release shipped. Target and token | |
| # live in secrets so they never appear in this file or in logs; | |
| # skips quietly when unset (forks, dry runs). | |
| - name: Notify docs tooling | |
| if: success() | |
| env: | |
| DISPATCH_REPO: ${{ secrets.DOCS_DISPATCH_REPO }} | |
| DISPATCH_TOKEN: ${{ secrets.DOCS_DISPATCH_TOKEN }} | |
| run: | | |
| if [ -z "$DISPATCH_REPO" ] || [ -z "$DISPATCH_TOKEN" ]; then | |
| echo "docs dispatch not configured; skipping" | |
| exit 0 | |
| fi | |
| curl -fsS -X POST \ | |
| "https://api.github.com/repos/${DISPATCH_REPO}/dispatches" \ | |
| -H "Authorization: Bearer ${DISPATCH_TOKEN}" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| --data "{\"event_type\":\"release-web\",\"client_payload\":{\"platform\":\"web\",\"ref\":\"${GITHUB_REF_NAME}\"}}" |