Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 0 additions & 128 deletions .github/workflows/build-and-publish-docker-image.yml

This file was deleted.

237 changes: 237 additions & 0 deletions .github/workflows/ci-and-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
# 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
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ COPY . .

# Install deps from store and build (postinstall will automatically generate licenses.json)
RUN pnpm install --frozen-lockfile --offline \
&& pnpm gen:licenses \
&& pnpm build \
&& pnpm prune --prod

Expand Down
Loading