diff --git a/.github/workflows/ci.yml b/.github/workflows/pull-request-checks.yml similarity index 66% rename from .github/workflows/ci.yml rename to .github/workflows/pull-request-checks.yml index 8a9da83..059d4b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/pull-request-checks.yml @@ -1,17 +1,19 @@ -name: ci +# Validates every PR to main: builds the binary and +# the Docker image via Nix to catch broken build before merge. +name: "Pull Request Checks" on: pull_request: - branches: [main] + branches: [main] # Runs only on PRs to main branch to validate the build. paths-ignore: - 'README.md' - 'wiki/**' workflow_dispatch: jobs: - # Runs on PRs only to validate the build. # nixos/web.nix uses buildGoModule which compiles the Go binary (no separate build job needed) - # and runs `go test -short` via doCheck = true (no separate test job needed). + # and runs `go test -short` via doCheck = true (no separate test job needed). + # nixos/web.docker.nix builds the Docker image which includes the compiled binary. build: runs-on: ubuntu-latest timeout-minutes: 15 @@ -34,5 +36,8 @@ jobs: restore-prefixes-first-match: nix-${{ runner.os }}- gc-max-store-size-linux: 5368709120 # 5 GB - - name: Build NixOS configuration - run: nix build + - name: Build binary + run: nix build .#default + + - name: Build Docker image + run: nix build .#docker diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0e4df6b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,80 @@ +name: release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + + steps: + - uses: actions/checkout@v7 + + - name: Install Nix + uses: cachix/install-nix-action@v31 + with: + extra_nix_config: | + experimental-features = nix-command flakes + + - name: Cache Nix store + uses: nix-community/cache-nix-action@v7 + with: + primary-key: nix-${{ runner.os }}-${{ hashFiles('flake.lock', '**/*.nix', '**/*.go') }} + restore-prefixes-first-match: nix-${{ runner.os }}- + gc-max-store-size-linux: 5368709120 # 5 GB + + - name: Build Docker image + run: nix build .#docker + + - name: Export Docker image as tar + run: cp --dereference result gearberg-docker.tar.gz + + - name: Upload Docker image artifact + uses: actions/upload-artifact@v4 + with: + name: gearberg-docker + path: gearberg-docker.tar.gz + retention-days: 1 + + release: + needs: build + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + packages: write + + steps: + - uses: actions/checkout@v7 + + - name: Download Docker image artifact + uses: actions/download-artifact@v4 + with: + name: gearberg-docker + + - name: Login to ghcr.io + run: echo "${{ github.token }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Push Docker image to ghcr.io + run: | + IMAGE_ID=$(docker load < gearberg-docker.tar.gz | grep "Loaded image" | awk '{print $NF}') + docker tag "$IMAGE_ID" ghcr.io/${{ github.repository }}:${{ github.ref_name }} + docker tag "$IMAGE_ID" ghcr.io/${{ github.repository }}:latest + docker push ghcr.io/${{ github.repository }}:${{ github.ref_name }} + docker push ghcr.io/${{ github.repository }}:latest + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${{ github.ref_name }}" \ + --title "${{ github.ref_name }}" \ + --generate-notes + diff --git a/cmd/web/options.go b/cmd/web/options.go index 3282f7d..aa148fe 100644 --- a/cmd/web/options.go +++ b/cmd/web/options.go @@ -46,7 +46,7 @@ func parseOptions(args []string) (*options, error) { fs := flag.NewFlagSet("serve", flag.ContinueOnError) fs.SetOutput(os.Stderr) fs.Var(&cfg.LogLevel, "log-level", "log level (debug|info|warn|error)") - fs.StringVar(&cfg.DbDsn, "db-dsn", envOr("DB_DSN", "file:gearberg.db"), "database DSN") + fs.StringVar(&cfg.DbDsn, "db-dsn", envOr("DB_DSN", "file:/data/gearberg.db"), "database DSN") fs.IntVar(&cfg.Port, "port", 8080, "port to listen on") fs.StringVar(&cfg.BaseURL, "base-url", "localhost:8080", "base URL for link generation (e.g. https://example.com)") fs.StringVar(&cfg.TLSMode, "tls-mode", "off", "TLS mode (off|local)") @@ -56,7 +56,7 @@ func parseOptions(args []string) (*options, error) { fs.IntVar(&cfg.MaxOrgCategories, "max-categories", 25, "maximum number of equipment categories per org") fs.IntVar(&cfg.MaxOrgManufacturers, "max-manufacturers", 100, "maximum number of manufacturers per org") fs.IntVar(&cfg.MaxOrgLocations, "max-locations", 100, "maximum number of locations per org") - fs.StringVar(&cfg.StorageDSN, "storage-dsn", envOr("STORAGE_DSN", "./var/data"), "storage backend DSN") + fs.StringVar(&cfg.StorageDSN, "storage-dsn", envOr("STORAGE_DSN", "file:///data/uploads"), "storage backend DSN") fs.Int64Var(&cfg.MaxStorageBytes, "max-storage-bytes", 1<<30, "maximum storage bytes per org (default 1 GiB)") fs.Var(&cfg.OIDCProviders, "oidc-provider", "OIDC provider: name,issuer=URL,client-id=ID,client-secret=SECRET (repeatable)") diff --git a/flake.nix b/flake.nix index 5ac0414..ab93065 100644 --- a/flake.nix +++ b/flake.nix @@ -24,8 +24,8 @@ # Go binaries built with Nix for all systems. # Docker image is only built for x86_64-linux. - packages = - forAllSystems (system: { + packages = forAllSystems (system: + { default = import ./nixos/web.nix { buildpkgs = nixpkgs-unstable.legacyPackages.${system}; version = self.rev or self.dirtyRev or "dev"; @@ -34,13 +34,13 @@ buildpkgs = nixpkgs-unstable.legacyPackages.${system}; version = self.rev or self.dirtyRev or "dev"; }; - }) - // { - x86_64-linux.docker = import ./nixos/web.docker.nix { + } + // nixpkgs.lib.optionalAttrs (system == "x86_64-linux") { + docker = import ./nixos/web.docker.nix { pkgs = nixpkgs-unstable.legacyPackages.x86_64-linux; pkgsTarget = nixpkgs-unstable.legacyPackages.x86_64-linux; version = self.rev or self.dirtyRev or "dev"; }; - }; + }); }; } diff --git a/nixos/web.docker.nix b/nixos/web.docker.nix index bc8e451..20213c8 100644 --- a/nixos/web.docker.nix +++ b/nixos/web.docker.nix @@ -16,8 +16,31 @@ in pkgs.dockerTools.buildImage { name = "gearberg"; tag = version; + # Required for outbound TLS: OIDC discovery and JWKS calls will fail without it. + copyToRoot = pkgs.buildEnv { + name = "image-root"; + paths = [pkgsTarget.cacert]; + }; config = { - Cmd = ["${gearberg}/bin/gearberg"]; + # The gearberg binary has sane defaults and will start without environment variables but + # you can override them if you want to change the defaults. Environment variables can be set + # in the docker run command or in a docker-compose file. + Entrypoint = ["${gearberg}/bin/gearberg" "serve"]; + # Any flag can be called that is exposed by gearberg binary. + # See `gearberg serve --help` for all available flags. + Cmd = []; + # curl is pulled in via the Nix closure this is why there is no need to add it to copyToRoot. + # Timing fields are in nanoseconds per the OCI config spec. + Healthcheck = { + Test = ["CMD" "${pkgsTarget.curl}/bin/curl" "-f" "http://localhost:8080/v1/healthz"]; + Interval = 30 * 1000000000; + Timeout = 5 * 1000000000; + Retries = 3; + }; + # By default, gearberg listens on port 8080 but this can be overridden by -port flag. ExposedPorts = {"8080/tcp" = {};}; + # Gearberg does save its data to /data, which is a volume in the image. + # When starting the image, you should mount a host directory to /data to persist the data. + Volumes = {"/data" = {};}; }; }