release: cut v0.2.0-beta CHANGELOG section + inject into release body… #9
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: Release | |
| on: | |
| push: | |
| tags: ["v*.*.*"] | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Tag, der released werden soll (z. B. v1.2.3)' | |
| required: true | |
| type: string | |
| create_tag: | |
| description: 'Neuen Tag erstellen, falls er noch nicht existiert' | |
| required: false | |
| type: boolean | |
| default: false | |
| ref: | |
| description: 'Branch/Commit für den neuen Tag (nur falls oben aktiviert)' | |
| required: false | |
| type: string | |
| default: main | |
| permissions: | |
| contents: write | |
| packages: write | |
| id-token: write | |
| attestations: write | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| docker: | |
| name: Build & push container image | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: { fetch-depth: 0 } | |
| - uses: docker/setup-qemu-action@v4 | |
| - uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Optional Docker Hub mirror. Gated on `vars.DOCKERHUB_USERNAME` | |
| # because GitHub Actions can't reference secrets directly in | |
| # `if:` conditions (security). Operators who want the mirror set | |
| # *both*: | |
| # - Repository VARIABLE DOCKERHUB_USERNAME = "<their handle>" | |
| # - Repository SECRET DOCKERHUB_TOKEN = "<dckr_pat_…>" | |
| # If the username variable is unset the whole chain skips and | |
| # only GHCR is published. GHCR remains the source of truth — the | |
| # mirror is purely for users who pull from `docker.io/…` by | |
| # habit. | |
| - name: Log in to Docker Hub (optional) | |
| if: ${{ vars.DOCKERHUB_USERNAME != '' }} | |
| uses: docker/login-action@v4 | |
| with: | |
| username: ${{ vars.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Extract metadata | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: | | |
| ghcr.io/${{ github.repository_owner }}/mskanban | |
| ${{ vars.DOCKERHUB_USERNAME != '' && format('docker.io/{0}/mskanban', vars.DOCKERHUB_USERNAME) || '' }} | |
| tags: | | |
| type=ref,event=tag | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| type=semver,pattern={{major}} | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| labels: | | |
| org.opencontainers.image.title=MSKanban | |
| org.opencontainers.image.description=Zero-knowledge, self-hostable Kanban | |
| org.opencontainers.image.licenses=AGPL-3.0-or-later | |
| org.opencontainers.image.source=https://github.com/${{ github.repository }} | |
| org.opencontainers.image.documentation=https://github.com/${{ github.repository }}#readme | |
| - name: Build & push | |
| id: build | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: docker/Dockerfile | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| provenance: true | |
| sbom: true | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Attest build provenance | |
| uses: actions/attest-build-provenance@v1 | |
| with: | |
| subject-name: ghcr.io/${{ github.repository_owner }}/mskanban | |
| subject-digest: ${{ steps.build.outputs.digest }} | |
| push-to-registry: true | |
| # Keyless Sigstore signing — no long-lived signing keys to | |
| # manage; the signature is anchored to the GitHub Actions | |
| # OIDC token of this workflow run. Verification: | |
| # cosign verify ghcr.io/<owner>/mskanban:<tag> \ | |
| # --certificate-identity-regexp \ | |
| # 'https://github.com/<owner>/mskanban/\.github/workflows/release\.yml@refs/tags/.*' \ | |
| # --certificate-oidc-issuer https://token.actions.githubusercontent.com | |
| - uses: sigstore/cosign-installer@v3 | |
| with: | |
| cosign-release: "v2.4.1" | |
| - name: Sign container image (keyless, OIDC) | |
| env: | |
| COSIGN_EXPERIMENTAL: "1" | |
| TAGS: ${{ steps.meta.outputs.tags }} | |
| DIGEST: ${{ steps.build.outputs.digest }} | |
| run: | | |
| # Sign by digest (immutable) for every emitted tag — that | |
| # way pull-by-digest and pull-by-tag both verify. | |
| for tag in $TAGS; do | |
| image="${tag%:*}@${DIGEST}" | |
| echo "Signing ${image}" | |
| cosign sign --yes "${image}" | |
| done | |
| sbom: | |
| name: SBOM (CycloneDX + SPDX) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: { fetch-depth: 0 } | |
| # Syft natively reads `pnpm-lock.yaml` — npm-centric tools like | |
| # `@cyclonedx/cyclonedx-npm` don't, so we don't bother running | |
| # the SBOM under Node. The action wraps the upstream Syft binary | |
| # pinned by version. | |
| - name: Generate SBOM (CycloneDX JSON) | |
| uses: anchore/sbom-action@v0 | |
| with: | |
| path: . | |
| format: cyclonedx-json | |
| output-file: sbom.cdx.json | |
| - name: Generate SBOM (SPDX JSON) | |
| uses: anchore/sbom-action@v0 | |
| with: | |
| path: . | |
| format: spdx-json | |
| output-file: sbom.spdx.json | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: sbom | |
| path: | | |
| sbom.cdx.json | |
| sbom.spdx.json | |
| retention-days: 365 | |
| release-notes: | |
| name: GitHub Release | |
| runs-on: ubuntu-latest | |
| needs: [docker, sbom] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: { fetch-depth: 0 } | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: sbom | |
| path: ./release-assets | |
| # Extract the section for this tag from CHANGELOG.md so it shows | |
| # up at the top of the GitHub Release body. `generate_release_notes` | |
| # below still appends the auto-generated "What's Changed" PR list | |
| # afterwards — best of both: hand-curated narrative + complete | |
| # commit/PR audit trail. The awk picks the block between | |
| # "## [<version>]" and the next "## [". | |
| - name: Extract CHANGELOG section for this release | |
| id: changelog | |
| run: | | |
| set -euo pipefail | |
| TAG_RAW="${GITHUB_REF_NAME:-${{ github.event.inputs.tag }}}" | |
| VERSION="${TAG_RAW#v}" | |
| echo "Extracting CHANGELOG section for version: ${VERSION}" | |
| # Substring-based extraction (no regex escapes — different awk | |
| # builds disagree on whether `\[` is literal or a metachar). | |
| # `index($0, marker) == 1` is true iff the line starts with the | |
| # exact marker string, which is what we want. | |
| awk -v marker="## [${VERSION}]" ' | |
| index($0, marker) == 1 { p=1; next } | |
| p && index($0, "## [") == 1 { exit } | |
| p && $0 != "---" { print } | |
| ' CHANGELOG.md > release-body.md | |
| if [[ ! -s release-body.md ]]; then | |
| echo "::warning::CHANGELOG.md has no section for [${VERSION}] — falling back to auto-generated notes only" | |
| else | |
| echo "--- release-body.md preview (first 40 lines) ---" | |
| head -40 release-body.md | |
| echo "--- (truncated) ---" | |
| fi | |
| - uses: softprops/action-gh-release@v3 | |
| with: | |
| body_path: release-body.md | |
| # `body_path` gets *prepended* to the auto-generated PR list | |
| # when both are set, so we end up with a curated header + the | |
| # full commit audit trail underneath. | |
| generate_release_notes: true | |
| draft: false | |
| prerelease: ${{ contains(github.ref, '-') }} | |
| # Attach SBOM + image digest so users can independently | |
| # verify provenance and dependency footprint. | |
| files: | | |
| release-assets/sbom.cdx.json | |
| release-assets/sbom.spdx.json |