diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..0dd8fd1 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,89 @@ +name: Build and Publish Docker Image + +on: + workflow_call: + inputs: + version: + required: true + type: string + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + name: Build Docker Image (${{ matrix.platform }}) + runs-on: ${{ matrix.runner }} + strategy: + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + - platform: linux/arm64 + runner: ubuntu-24.04-arm + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to Container Registry + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Sanitize platform for tag + id: platform + run: | + PLATFORM_TAG=$(echo "${{ matrix.platform }}" | tr '/' '-') + echo "tag=$PLATFORM_TAG" >> "$GITHUB_OUTPUT" + + - name: Build and push Docker image + uses: docker/build-push-action@v7 + with: + context: . + push: true + build-args: | + ORIGIN=${{ vars.FRONTEND_ORIGIN || 'https://www.sgf.dev' }} + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ inputs.version }}-${{ steps.platform.outputs.tag }} + labels: | + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.revision=${{ github.sha }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + cache-to: type=inline + platforms: ${{ matrix.platform }} + provenance: false + + merge: + name: Create Multi-Arch Manifest + runs-on: ubuntu-latest + needs: build + permissions: + contents: read + packages: write + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to Container Registry + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create and push multi-arch manifest + run: | + docker buildx imagetools create \ + -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ inputs.version }} \ + -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ inputs.version }}-linux-amd64 \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ inputs.version }}-linux-arm64 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4fa7038 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,42 @@ +name: Release + +on: + push: + branches: [main] + +permissions: + contents: write + issues: write + pull-requests: write + packages: write + +jobs: + release: + runs-on: ubuntu-latest + outputs: + new-release-published: ${{ steps.semantic.outputs.new-release-published }} + release-version: ${{ steps.semantic.outputs.release-version }} + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v7 + with: + node-version: "lts/*" + + - run: npm install -g semantic-release@25.0.9 conventional-changelog-conventionalcommits@8.0.0 @semantic-release/exec@7.1.0 + + - name: Create Release + id: semantic + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npx semantic-release + + docker: + needs: release + if: ${{ needs.release.outputs.new-release-published == 'true' }} + uses: ./.github/workflows/docker-publish.yml + with: + version: ${{ needs.release.outputs.release-version }} + secrets: inherit diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..e079d52 --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,19 @@ +{ + "branches": ["main"], + "plugins": [ + ["@semantic-release/commit-analyzer", { + "preset": "conventionalcommits", + "releaseRules": [ + { "type": "chore", "release": "patch" }, + { "type": "build", "release": "patch" } + ] + }], + ["@semantic-release/release-notes-generator", { + "preset": "conventionalcommits" + }], + "@semantic-release/github", + ["@semantic-release/exec", { + "successCmd": "echo 'new-release-published=true' >> $GITHUB_OUTPUT && echo 'release-version=${nextRelease.version}' >> $GITHUB_OUTPUT" + }] + ] +} diff --git a/README.md b/README.md index 9c86ca8..8ddc0ba 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,18 @@ The image defaults to port 3000 and `BODY_SIZE_LIMIT=9M`; it does not include a `.dockerignore` permits only build inputs and excludes local secrets and output. Run `npm test` for the unit suite. It type-checks tests once and runs every test -folder, without a browser or live CMS. CI runs the same command, Svelte checking, -generated API freshness checks and build. It does not deploy, use action secrets -or build/push an image. Visual checks and complete member journeys belong in staging. +folder, without a browser or live CMS. PR CI runs the same command, Svelte checking, +generated API freshness checks and build. It does not deploy or build/push images. + +On `main`, semantic-release creates GitHub releases from Conventional Commits. +A new release calls the reusable Docker publishing workflow to push native +`linux/amd64` and `linux/arm64` images to `ghcr.io/sgfdevs/sgf.dev`, tagged +with the release version and `latest`. + +Images build for the public repository Actions variable `FRONTEND_ORIGIN`, +defaulting to `https://www.sgf.dev`. The workflow passes it as the Docker `ORIGIN` +build argument. No CMS keys or other secrets are build arguments. Rebuild for a +different origin; setting a runtime `ORIGIN` cannot change the built origin. +Staging and production promotions remain manual; automated promotion jobs are +disabled. Test staging, including visual checks and complete member journeys, +before promoting to production.