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
| # GitHub Actions workflow: CI on PRs; Publish Docker image and Release on main | |
| name: CI and Publish (Docker + Release) | |
| on: | |
| push: | |
| branches: [main] # Run CI and publish when commits land on main | |
| pull_request: | |
| branches: [main] # Run CI for PRs targeting main | |
| workflow_dispatch: # Allow manual runs from the Actions tab | |
| # Least-privilege permissions required | |
| permissions: | |
| contents: write # needed to create GitHub releases (read is sufficient for checkout) | |
| packages: write # push images to GitHub Container Registry | |
| id-token: write # enable provenance/SLSA attestation by build-push-action | |
| # Shared values | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} # e.g. owner/repo (may be mixed-case) | |
| jobs: | |
| ci: | |
| name: CI (format, lint, check, gen:licenses, build) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Read tool versions from package.json | |
| id: tool-versions | |
| shell: bash | |
| run: | | |
| echo "::group::Read Node and pnpm versions from package.json" | |
| NODE_ENGINE=$(jq -r '.engines.node // ""' package.json) | |
| if [[ -z "$NODE_ENGINE" || "$NODE_ENGINE" == "null" ]]; then | |
| NODE_VERSION="22" | |
| else | |
| NODE_VERSION=$(echo "$NODE_ENGINE" | grep -oE '[0-9]+' | head -1) | |
| if [[ -z "$NODE_VERSION" ]]; then NODE_VERSION="22"; fi | |
| fi | |
| PM=$(jq -r '.packageManager // ""' package.json) | |
| if [[ -z "$PM" || "$PM" == "null" ]]; then | |
| PNPM_VERSION="10.15.0" | |
| else | |
| PNPM_VERSION=$(echo "$PM" | sed -n 's/^pnpm@\([0-9][^ ]*\).*$/\1/p') | |
| if [[ -z "$PNPM_VERSION" ]]; then PNPM_VERSION="10.15.0"; fi | |
| fi | |
| echo "Node engines: $NODE_ENGINE -> using $NODE_VERSION" | |
| echo "packageManager: $PM -> pnpm $PNPM_VERSION" | |
| echo "node=$NODE_VERSION" >> $GITHUB_OUTPUT | |
| echo "pnpm=$PNPM_VERSION" >> $GITHUB_OUTPUT | |
| echo "::endgroup::" | |
| - name: Enable Corepack and pnpm | |
| shell: bash | |
| run: | | |
| echo "::group::Enable Corepack and prepare pnpm" | |
| corepack enable | |
| corepack prepare pnpm@${{ steps.tool-versions.outputs.pnpm }} --activate | |
| pnpm --version | |
| echo "::endgroup::" | |
| - name: Configure pnpm store for actions cache | |
| shell: bash | |
| run: | | |
| echo "::group::Configure pnpm store path (~/.pnpm-store)" | |
| PNPM_STORE_DIR="$HOME/.pnpm-store" | |
| mkdir -p "$PNPM_STORE_DIR" | |
| pnpm config set store-dir "$PNPM_STORE_DIR" | |
| echo "pnpm store-dir => $(pnpm config get store-dir)" | |
| echo "::endgroup::" | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ steps.tool-versions.outputs.node }} | |
| cache: "pnpm" | |
| cache-dependency-path: pnpm-lock.yaml | |
| - name: Set up pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: ${{ steps.tool-versions.outputs.pnpm }} | |
| run_install: false | |
| - name: Install dependencies | |
| shell: bash | |
| run: | | |
| echo "::group::pnpm install" | |
| pnpm --version | |
| pnpm install --frozen-lockfile | |
| echo "::endgroup::" | |
| - name: Run format | |
| shell: bash | |
| run: | | |
| echo "::group::pnpm format" | |
| pnpm format | |
| echo "::endgroup::" | |
| - name: Run lint | |
| shell: bash | |
| run: | | |
| echo "::group::pnpm lint" | |
| pnpm lint | |
| echo "::endgroup::" | |
| - name: Run type check | |
| shell: bash | |
| run: | | |
| echo "::group::pnpm check" | |
| pnpm check | |
| echo "::endgroup::" | |
| - name: Generate licenses | |
| shell: bash | |
| run: | | |
| echo "::group::pnpm gen:licenses" | |
| pnpm run gen:licenses | |
| echo "::endgroup::" | |
| - name: Build app | |
| shell: bash | |
| run: | | |
| echo "::group::pnpm build" | |
| pnpm build | |
| echo "::endgroup::" | |
| publish: | |
| name: Publish Docker image and GitHub Release | |
| needs: ci | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up QEMU (for multi-arch builds) | |
| uses: docker/setup-qemu-action@v3 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} # auto-provided token with package:write | |
| - name: Extract version from package.json | |
| id: version | |
| shell: bash | |
| run: | | |
| echo "::group::Read version from package.json" | |
| VERSION=$(jq -r .version package.json) | |
| echo "Detected version: ${VERSION}" | |
| if [[ -z "$VERSION" || "$VERSION" == "null" ]]; then | |
| echo "Could not read version from package.json" >&2 | |
| echo "::endgroup::" | |
| exit 1 | |
| fi | |
| IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION" | |
| echo "Parsed parts => MAJOR=${MAJOR}, MINOR=${MINOR}, PATCH=${PATCH}" | |
| echo "::endgroup::" | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "major=$MAJOR" >> $GITHUB_OUTPUT | |
| echo "minor=$MINOR" >> $GITHUB_OUTPUT | |
| echo "patch=$PATCH" >> $GITHUB_OUTPUT | |
| - name: Compute tags | |
| id: meta | |
| shell: bash | |
| run: | | |
| echo "::group::Compute image reference and tags" | |
| IMAGE_NAME_LOWER=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]') | |
| IMAGE_REF=${{ env.REGISTRY }}/${IMAGE_NAME_LOWER} | |
| MAJOR=${{ steps.version.outputs.major }} | |
| MINOR=${{ steps.version.outputs.minor }} | |
| PATCH=${{ steps.version.outputs.patch }} | |
| VERSION=${{ steps.version.outputs.version }} | |
| echo "Original IMAGE_NAME: ${{ env.IMAGE_NAME }}" | |
| echo "Normalized (lowercase) IMAGE_NAME: ${IMAGE_NAME_LOWER}" | |
| echo "Image reference: ${IMAGE_REF}" | |
| echo "Using version parts => MAJOR=${MAJOR}, MINOR=${MINOR}, PATCH=${PATCH}" | |
| TAGS=( | |
| "${IMAGE_REF}:latest" | |
| "${IMAGE_REF}:${MAJOR}" | |
| "${IMAGE_REF}:${MAJOR}.${MINOR}" | |
| "${IMAGE_REF}:${VERSION}" | |
| ) | |
| echo "Planned tags:" | |
| for t in "${TAGS[@]}"; do echo " - ${t}"; done | |
| TAGS_CSV=$(IFS=, ; echo "${TAGS[*]}") | |
| echo "tags=$TAGS_CSV" >> $GITHUB_OUTPUT | |
| echo "image_name_lower=${IMAGE_NAME_LOWER}" >> $GITHUB_OUTPUT | |
| echo "image_ref=${IMAGE_REF}" >> $GITHUB_OUTPUT | |
| echo "Computed tags CSV: ${TAGS_CSV}" | |
| echo "::endgroup::" | |
| - name: Build and push | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: ./Dockerfile | |
| push: true | |
| provenance: true # generate provenance (SLSA) metadata | |
| sbom: true # generate SBOM and attach to the image | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: | | |
| org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} | |
| org.opencontainers.image.revision=${{ github.sha }} | |
| org.opencontainers.image.version=${{ steps.version.outputs.version }} | |
| cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ steps.meta.outputs.image_name_lower }}:buildcache | |
| cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ steps.meta.outputs.image_name_lower }}:buildcache,mode=max | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ steps.version.outputs.version }} | |
| name: "${{ steps.version.outputs.version }} version" | |
| generate_release_notes: true | |
| - name: Summarize publish details | |
| shell: bash | |
| run: | | |
| echo "## Docker Image Build Summary" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Repository: ${{ env.REGISTRY }}/${{ steps.meta.outputs.image_name_lower }}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Version: ${{ steps.version.outputs.version }}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Action: Pushed the following tags:" >> "$GITHUB_STEP_SUMMARY" | |
| IFS=',' read -ra TAGS_ARR <<< "${{ steps.meta.outputs.tags }}" | |
| for t in "${TAGS_ARR[@]}"; do echo "- $t" >> "$GITHUB_STEP_SUMMARY"; done |