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
63 changes: 21 additions & 42 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,27 @@ jobs:
- name: Build application
run: pnpm run build

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to Docker Hub
id: docker-login
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# The Docker build/push that used to live here has been REMOVED.
#
# It pushed nself/nself-admin:<version>, :<major>.<minor>, :<major> and
# :latest with `push: true` and NO vulnerability scan, on the same
# `push: tags: v*` trigger as docker-publish.yml. docker-publish.yml has a
# Trivy CRITICAL gate; this workflow did not. So every tag push raced two
# publishers, and the ungated one could ship an image the gate had just
# rejected — which makes the gate decorative.
#
# That is exactly what happened on v1.3.6 (2026-09-12): docker-publish
# correctly blocked on four CRITICAL Go stdlib CVEs in mkcert, while this
# workflow was mid-push of the same image and had to be cancelled by hand
# to keep :latest from moving to a known-vulnerable build.
#
# Image publication now belongs to docker-publish.yml alone — it owns the
# Trivy gate, the multi-arch manifest verification, and the NSELF_VERSION
# build-arg that this job did not even pass. This workflow owns the GitHub
# Release only. Do not reintroduce a push here without the gate.
#
# Security-Always-Free doctrine: a security gate that another workflow can
# walk around is not a gate.

- name: Extract version from tag
id: version
Expand All @@ -60,36 +69,6 @@ jobs:
echo "major=$(echo $VERSION | cut -d. -f1)" >> $GITHUB_OUTPUT
echo "minor=$(echo $VERSION | cut -d. -f1-2)" >> $GITHUB_OUTPUT

- name: Extract metadata
if: steps.docker-login.outcome == 'success'
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKER_IMAGE }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest

- name: Build and push Docker image
if: steps.docker-login.outcome == 'success'
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache,mode=max

- name: Docker Hub credentials not configured
if: steps.docker-login.outcome != 'success'
run: |
echo "::warning::Docker Hub credentials not configured. Skipping Docker image build."
echo "To enable Docker image publishing, add DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets to this repository."

- name: Generate changelog
id: changelog
uses: metcalfc/changelog-generator@v4.1.0
Expand Down
8 changes: 8 additions & 0 deletions .trivyignore.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@

secrets:
- id: gcp-service-account
# Trivy reports image paths with a LEADING SLASH — the gate log shows
# "/app/.next/server/app/cloud/gcp/page.js". These entries were written
# without it, so they matched nothing and this finding kept blocking the
# publish even though the suppression was in place. Both forms are listed:
# the absolute form is what the image scan emits, the relative form is what
# a filesystem scan of the repo would emit.
paths:
- "/app/.next/server/app/cloud/gcp/page.js"
- "/app/.next/static/chunks/app/cloud/gcp/*.js"
- "app/.next/server/app/cloud/gcp/page.js"
- "app/.next/static/chunks/app/cloud/gcp/*.js"
statement: >-
Expand Down
40 changes: 31 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
# mkcert, built from source rather than taken from upstream's pre-built binary.
#
# The upstream binary (v1.4.4, published 2022-04-26) is compiled with Go 1.18,
# and Trivy's CRITICAL gate flagged four Go stdlib vulnerabilities baked into it:
# CVE-2023-24538, CVE-2023-24540, CVE-2024-24790 and CVE-2025-68121. That gate
# blocked every docker-publish run, which is why Docker Hub sat at 1.0.13 while
# the CLI reached 1.3.6.
#
# There is no newer mkcert release to bump to — v1.4.4 IS the latest and has been
# since 2022. So compile the same source with a current Go toolchain: identical
# mkcert behaviour, patched stdlib.
#
# --platform=$BUILDPLATFORM + GOOS/GOARCH cross-compilation keeps this stage
# native on the builder instead of emulated per-arch under QEMU. `go build -o` is
# used rather than `go install` because when cross-compiling `go install` writes
# to /go/bin/${GOOS}_${GOARCH}/ rather than /go/bin, and the COPY would silently
# miss it.
FROM --platform=$BUILDPLATFORM golang:1.27-alpine AS mkcert-builder
ARG TARGETOS
ARG TARGETARCH
RUN apk add --no-cache git

Check failure on line 21 in Dockerfile

View workflow job for this annotation

GitHub Actions / hadolint

DL3018 warning: Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`
RUN git clone --depth 1 --branch v1.4.4 https://github.com/FiloSottile/mkcert /src

Check failure on line 22 in Dockerfile

View workflow job for this annotation

GitHub Actions / hadolint

DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation.
WORKDIR /src
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
go build -trimpath -ldflags "-s -w" -o /out/mkcert .

# Multi-stage production Dockerfile for nself-admin
# Optimized for minimal size with standalone Next.js build
# Multi-platform support: linux/amd64, linux/arm64
Expand All @@ -5,7 +31,7 @@

# Stage 1: Dependencies
FROM node:22-alpine AS deps
RUN apk add --no-cache libc6-compat

Check failure on line 34 in Dockerfile

View workflow job for this annotation

GitHub Actions / hadolint

DL3018 warning: Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`
RUN corepack enable && corepack prepare pnpm@10.28.0 --activate
WORKDIR /app

Expand All @@ -25,7 +51,7 @@

# Stage 2: Builder
FROM node:22-alpine AS builder
RUN apk add --no-cache libc6-compat

Check failure on line 54 in Dockerfile

View workflow job for this annotation

GitHub Actions / hadolint

DL3018 warning: Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`
RUN corepack enable && corepack prepare pnpm@10.28.0 --activate
WORKDIR /app

Expand Down Expand Up @@ -53,7 +79,7 @@
RUN pnpm run build

# Remove dev dependencies to reduce image size
RUN pnpm prune --prod

Check failure on line 82 in Dockerfile

View workflow job for this annotation

GitHub Actions / hadolint

DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation.

# Stage 3: Runner (minimal image)
FROM node:22-alpine AS runner
Expand All @@ -65,7 +91,7 @@
# - curl, git: Required by nself commands
# - openssl, nss-tools: For SSL certificate management
# - mkcert: For local SSL certificates (installed separately)
RUN apk add --no-cache \

Check failure on line 94 in Dockerfile

View workflow job for this annotation

GitHub Actions / hadolint

DL3018 warning: Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`
bash \
curl \
git \
Expand All @@ -87,19 +113,15 @@
/usr/local/bin/npm \
/usr/local/bin/npx

# Install mkcert for local SSL certificate generation
# Using pre-built binary for Alpine Linux
RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "x86_64" ]; then MKCERT_ARCH="amd64"; \
elif [ "$ARCH" = "aarch64" ]; then MKCERT_ARCH="arm64"; \
else MKCERT_ARCH="amd64"; fi && \
curl -fsSL "https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-${MKCERT_ARCH}" \
-o /usr/local/bin/mkcert && \
chmod +x /usr/local/bin/mkcert
# Install mkcert for local SSL certificate generation.
# Built from source in the mkcert-builder stage at the top of this file — see
# there for why the upstream pre-built binary cannot be used.
COPY --from=mkcert-builder /out/mkcert /usr/local/bin/mkcert
RUN chmod +x /usr/local/bin/mkcert

# Install nself CLI pre-built binary
ARG NSELF_VERSION=1.3.6
RUN ARCH=$(uname -m) && \

Check failure on line 124 in Dockerfile

View workflow job for this annotation

GitHub Actions / hadolint

DL4006 warning: Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check
if [ "$ARCH" = "x86_64" ]; then NSELF_ARCH="amd64"; \
elif [ "$ARCH" = "aarch64" ]; then NSELF_ARCH="arm64"; \
else NSELF_ARCH="amd64"; fi && \
Expand Down
Loading