**Brief approach**I’d create two workflows: one for PR checks (lint + unit tests) and one for main branch (build, integration tests, tag & push). Use node and docker layer caching, ephemeral PostgreSQL service for integration tests, and secrets for Docker Hub and DB credentials. Optimize runtime with caching, shallow fetches, job concurrency, and conditional steps.**Workflow structure**- .github/workflows/pr-check.yml — runs on pull_request: lint + unit tests- .github/workflows/main-release.yml — runs on push to main: build image, run integration tests (Postgres service), tag image, push to Docker Hub**Key YAML snippet (main-release.yml)**yaml
name: Main Release
on:
push:
branches: [ main ]
jobs:
build-and-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
env:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: ${{ secrets.PG_PASSWORD }}
ports: ['5432:5432']
options: >-
--health-cmd "pg_isready -U test" --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Cache node modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
- name: Setup Node
uses: actions/setup-node@v4
with: node-version: 18
- name: Install and test
run: |
npm ci
npm run build
npm run integration:test
- name: Setup QEMU and Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/myservice:${{ env.SEMVER }}-${{ github.sha }}
cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/myservice:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/myservice:buildcache,mode=max
env:
DOCKER_BUILDKIT: 1
SEMVER: ${{ steps.get_semver.outputs.version }}
**Semantic versioning & tags**- Calculate semantic version from package.json or git tags in a step (e.g., use zetameme/semantic-release or a small script) and expose as env.- Tag images with `${SEMVER}` and short `${{ github.sha }}` to guarantee traceability.**Caching strategy**- actions/cache for node modules (key by package-lock.json).- docker/build-push-action registry layer cache (cache-from / cache-to).- keep fetch-depth: 0 only when you need full history for semver; otherwise use shallow fetch to speed PRs.**Secrets usage**- Store DOCKERHUB_USERNAME and DOCKERHUB_TOKEN in repo secrets and pass to docker/login-action.- PG_PASSWORD in secrets for ephemeral Postgres.- Limit secret exposure by using environment-level secrets for main-release only.**Minimize run-time**- Run lint + unit tests on PRs only; run integration and image build on main merges.- Use caching aggressively (node modules + docker layers).- Parallelize independent steps (lint and unit tests can be parallel jobs in PR workflow).- Use shallow fetch by default; only fetch full history when computing semver.- Use concurrency and cancel-in-progress to avoid wasted runs: - concurrency: group: ${{ github.ref }} cancel-in-progress: trueWhy this works: it separates fast PR feedback from heavier release jobs, reuses caches to shorten installs/builds, secures credentials, and ensures reproducible, traceable images with semver + commit SHA.