diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..15c2701 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +# Keep the build context lean and reproducible. Excluding .git also disables +# main.go's VCS build-info fallback, which is why docker.yml passes VERSION as a +# build-arg instead. +.git +.github +.gitignore +.gitattributes +.dockerignore +Dockerfile +*.md +docs +deploy + +# Editor / tooling +.vscode +.idea +.claude + +# Server config carries secrets (secrets.key, SMTP/ZPA tokens) — never in an image. +.glabs-web.yaml +.glabs.yaml + +# Untracked per-participant working data must never enter the build context. +courseid_* +ob-vss-* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab4cf7f..fef5386 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,11 +100,16 @@ jobs: - test-fast permissions: contents: write + # actions: write lets the trigger step below dispatch docker.yml. + actions: write steps: - uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: true + - name: Record the latest tag before the release + id: before + run: echo "tag=$(git tag --sort=-v:refname | head -n1)" >> "$GITHUB_OUTPUT" - uses: go-semantic-release/action@v1 id: semrel with: @@ -114,3 +119,18 @@ jobs: hooks: goreleaser env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Trigger the glabs-web Docker image build on a new release + # A GITHUB_TOKEN-authored release does not fire the `release` event, so + # dispatch docker.yml ourselves when semantic-release created a new tag. + # workflow_dispatch IS allowed to run when invoked with GITHUB_TOKEN. + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git fetch --tags --force + new_tag=$(git tag --sort=-v:refname | head -n1) + if [ -n "$new_tag" ] && [ "$new_tag" != "${{ steps.before.outputs.tag }}" ]; then + echo "New release tag $new_tag — dispatching docker.yml" + gh workflow run docker.yml -f tag="$new_tag" + else + echo "No new tag (was: ${{ steps.before.outputs.tag }}, now: $new_tag) — skipping Docker build" + fi diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..7dc1a0c --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,67 @@ +name: Create and publish the glabs-web Docker image + +on: + # Triggered explicitly from ci.yml after a release (go-semantic-release creates + # the release with GITHUB_TOKEN, which does not fire the `release` event). The + # `release` trigger is kept for a manually published release. + workflow_dispatch: + inputs: + tag: + description: "Release tag to build (e.g. v3.22.0)" + required: true + type: string + release: + types: [published] + +env: + REGISTRY: ghcr.io + # The CLI repo is obcode/glabs; the web server image is published as glabs-web. + IMAGE_NAME: ${{ github.repository }}-web + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + # The release tag to build, from whichever trigger fired. + env: + VERSION: ${{ github.event.release.tag_name || inputs.tag }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # Build the released code, not whatever is on main now. + ref: ${{ github.event.release.tag_name || inputs.tag }} + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + # Tag the image with the release tag and latest (works for both triggers). + tags: | + type=raw,value=${{ env.VERSION }} + type=raw,value=latest + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # Version metadata into the build (main.go ldflags via the Dockerfile). + build-args: | + VERSION=${{ env.VERSION }} + + # The deploy job (self-hosted runner, on-host docker-compose) comes with the + # deploy/ stack (Caddyfile + docker-compose.yml), the second Milestone-F PR. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8d57e18 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# glabs-web — the GraphQL server behind glabs.cs.hm.edu. +# +# The repo builds TWO binaries from one Go module (the `glabs` CLI at `.` and +# `glabs-web` at `./cmd/glabs-web`); this image builds ONLY the web server. +# The mail templates are embedded via //go:embed (web/mail/tmpl), so no assets +# need to be copied into the runtime stage. +FROM golang:1.26-alpine AS builder +WORKDIR /src + +# Version metadata, passed by docker.yml from the release tag (mirrors the +# goreleaser ldflags used for the CLI). .git is excluded via .dockerignore, so +# main.go's debug.ReadBuildInfo VCS fallback cannot fill these — hence the ARGs. +ARG VERSION=dev +ARG COMMIT=none +ARG DATE=unknown + +# Cache the module graph separately from the source. +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +# CGO off → a static binary that runs on the bare alpine runtime below. +RUN CGO_ENABLED=0 GOOS=linux go build \ + -trimpath \ + -ldflags "-s -w -X 'main.version=${VERSION}' -X 'main.commit=${COMMIT}' -X 'main.date=${DATE}' -X 'main.builtBy=docker'" \ + -o /out/glabs-web ./cmd/glabs-web + +# --- Runtime --- +FROM alpine:3.21 +# ca-certificates: TLS to GitLab / ZPA / SMTP. tzdata: main() sets time.Local to +# Europe/Berlin at startup and needs the zoneinfo database. +RUN apk add --no-cache ca-certificates tzdata \ + && adduser -D -u 10001 glabs +WORKDIR /app +COPY --from=builder /out/glabs-web /usr/local/bin/glabs-web +USER glabs +EXPOSE 8080 +# .glabs-web.yaml is searched in "." (this WORKDIR) first, then $HOME — the +# deploy bind-mounts it read-only into /app. server.port defaults to 8080. +ENTRYPOINT ["glabs-web"]