diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 356680d..67b541f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,17 +27,26 @@ jobs: - name: ${{ matrix.make }} run: make ${{ matrix.make }} - deploy: + build-binaries: runs-on: ubuntu-latest needs: tests if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') strategy: matrix: - goosarch: [linux-amd64] + include: + - goos: linux + goarch: amd64 + - goos: linux + goarch: arm64 + - goos: darwin + goarch: amd64 + - goos: darwin + goarch: arm64 + - goos: windows + goarch: amd64 steps: - uses: actions/checkout@v6 - with: - fetch-depth: 0 + - name: install go uses: actions/setup-go@v6 with: @@ -47,27 +56,94 @@ jobs: - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - - name: Sign in to dockerhub - run: echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin - - - name: Deploy image - run: make docker-push VERSION=$RELEASE_VERSION - - name: Build - run: make build VERSION=$RELEASE_VERSION + run: make build VERSION="$RELEASE_VERSION" GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} - - name: Prepare release + - name: Package + run: | + BASE="whatismyip-$RELEASE_VERSION-${{ matrix.goos }}-${{ matrix.goarch }}" + if [ "${{ matrix.goos }}" = "windows" ]; then + cp whatismyip "${BASE}.exe" + zip "${BASE}.zip" "${BASE}.exe" LICENSE README.md + sha256sum "${BASE}.zip" > "${BASE}.zip.sha256" + else + tar zcvf "${BASE}.tar.gz" whatismyip LICENSE README.md + sha256sum "${BASE}.tar.gz" > "${BASE}.tar.gz.sha256" + fi + + - name: Upload artifact + uses: actions/upload-artifact@v6 + with: + name: binary-${{ matrix.goos }}-${{ matrix.goarch }} + path: | + whatismyip-${{ env.RELEASE_VERSION }}-${{ matrix.goos }}-${{ matrix.goarch }}.* + if-no-files-found: error + + publish-docker: + runs-on: ubuntu-latest + needs: tests + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + steps: + - uses: actions/checkout@v6 + + - name: Set env + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Sign in to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + build-args: ARG_VERSION=${{ env.RELEASE_VERSION }} + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/whatismyip:${{ env.RELEASE_VERSION }} + ${{ secrets.DOCKERHUB_USERNAME }}/whatismyip:latest + push: true + cache-from: type=gha + cache-to: type=gha,mode=max + + release: + runs-on: ubuntu-latest + needs: [build-binaries, publish-docker] + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set env + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + + - name: Download all artifacts + uses: actions/download-artifact@v6 + with: + pattern: binary-* + merge-multiple: true + + - name: Generate changelog run: | git log $(git describe HEAD~ --tags --abbrev=0)..HEAD --pretty='format:%h - %s <%an>' --no-merges > changelog.txt - tar zcvf whatismyip-$RELEASE_VERSION-${{matrix.goosarch}}.tar.gz whatismyip LICENSE README.md - sha256sum whatismyip-$RELEASE_VERSION-${{matrix.goosarch}}.tar.gz > whatismyip-$RELEASE_VERSION-${{matrix.goosarch}}.tar.gz.sha256 - name: Release uses: softprops/action-gh-release@v3 with: body_path: changelog.txt files: | - whatismyip-${{env.RELEASE_VERSION}}-${{matrix.goosarch}}.tar.gz - whatismyip-${{env.RELEASE_VERSION}}-${{matrix.goosarch}}.tar.gz.sha256 + whatismyip-${{ env.RELEASE_VERSION }}-*.tar.gz + whatismyip-${{ env.RELEASE_VERSION }}-*.tar.gz.sha256 + whatismyip-${{ env.RELEASE_VERSION }}-*.zip + whatismyip-${{ env.RELEASE_VERSION }}-*.zip.sha256 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 462048d..31018df 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ whatismyip +dist/ diff --git a/AGENTS.md b/AGENTS.md index 15cf002..a6e3a33 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,16 +5,17 @@ Single-binary Go service: HTTP/TLS/QUIC server that returns the client's IP, geo ## Commands ```sh -make build # CGO_ENABLED=0 build -ldflags "-s -w" -> ./whatismyip -make unit-test # go test -count=1 -race -short -cover ./... -make integration-test # requires Docker; runs separate module (integration-tests/) -make test # unit-test + integration-test -make lint # gofmt -l + golangci-lint + shadow (auto-installs tools) -make docker-run # builds & runs container exposing :8080 :8081 :9100 -make docker-push VERSION=x.y.z # tagged release push (blocks devel builds) +make build # CGO_ENABLED=0 -> ./whatismyip (GOOS/GOARCH for cross-compile) +make build-all # cross-compile all 5 platforms into dist/ +make unit-test # go test -count=1 -race -short -cover ./... +make integration-test # requires Docker; runs integration-tests/ module +make test # unit-test + integration-test +make lint # gofmt -l + golangci-lint + shadow (auto-installs tools) +make docker-build # local single-arch build (root Dockerfile) +make docker-run # builds dev image + runs with test DBs, ports :8080 :8081 :9100 ``` -**Order**: `lint -> unit-test -> integration-test`. Integration tests depend on testcontainers-go (needs Docker running). Integration tests are a separate Go module with its own `go.mod` — run from the `integration-tests/` directory. +**Order and quirks**: `lint -> unit-test -> integration-test`. Integration tests use testcontainers-go (needs Docker). They are a separate Go module with its own `go.mod` — run from the `integration-tests/` directory. They use `test/Dockerfile` (distinct from the root `Dockerfile`). ## Architecture diff --git a/Dockerfile b/Dockerfile index 98f3a45..82e16b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,20 @@ -FROM golang:1.25-alpine AS builder +FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder ARG ARG_VERSION +ARG TARGETOS +ARG TARGETARCH ENV VERSION=$ARG_VERSION WORKDIR /app -COPY go.mod . -COPY go.sum . +COPY go.mod go.sum ./ RUN --mount=type=cache,target=/go/pkg/mod/ go mod download -x COPY . . -FROM builder AS build-dev-app -# hadolint ignore=DL3018 -RUN --mount=type=cache,target=/go/pkg/mod/ apk --no-cache add make && make build - -FROM builder AS build-prod-app -# hadolint ignore=DL3018 -RUN apk --no-cache update && apk add --no-cache ca-certificates make upx \ +RUN --mount=type=cache,target=/go/pkg/mod/ apk --no-cache add make ca-certificates \ && update-ca-certificates \ - && make build \ - && upx --best --lzma whatismyip + && GOOS=$TARGETOS GOARCH=$TARGETARCH make build -FROM scratch AS dev -COPY --from=build-dev-app /app/whatismyip /usr/bin/ -ENTRYPOINT ["whatismyip"] - -FROM scratch AS prod -COPY --from=build-prod-app /app/whatismyip /usr/bin/ +FROM scratch +COPY --from=builder /app/whatismyip /usr/bin/ +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ ENTRYPOINT ["whatismyip"] diff --git a/Makefile b/Makefile index 36a609a..a1746e1 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ GOPATH ?= $(shell go env GOPATH) VERSION ?= devel-$(shell git rev-parse --short HEAD) DOCKER_URL ?= dcarrillo/whatismyip +GOOS ?= $(shell go env GOOS) +GOARCH ?= $(shell go env GOARCH) .PHONY: test test: unit-test integration-test @@ -32,25 +34,20 @@ lint: install-tools shadow ./... build: - CGO_ENABLED=0 go build -ldflags="-s -w -X 'github.com/dcarrillo/whatismyip/internal/core.Version=${VERSION}'" -o whatismyip ./cmd + CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags="-s -w -X 'github.com/dcarrillo/whatismyip/internal/core.Version=${VERSION}'" -o whatismyip ./cmd -docker-build-dev: - docker build --target=dev --build-arg=ARG_VERSION="${VERSION}" --tag ${DOCKER_URL}:${VERSION} . +build-all: + @mkdir -p dist + GOOS=linux GOARCH=amd64 $(MAKE) build && mv whatismyip dist/whatismyip-linux-amd64 + GOOS=linux GOARCH=arm64 $(MAKE) build && mv whatismyip dist/whatismyip-linux-arm64 + GOOS=darwin GOARCH=amd64 $(MAKE) build && mv whatismyip dist/whatismyip-darwin-amd64 + GOOS=darwin GOARCH=arm64 $(MAKE) build && mv whatismyip dist/whatismyip-darwin-arm64 + GOOS=windows GOARCH=amd64 $(MAKE) build && mv whatismyip dist/whatismyip-windows-amd64.exe -docker-build-prod: - docker build --target=prod --build-arg=ARG_VERSION="${VERSION}" --tag ${DOCKER_URL}:${VERSION} . +docker-build: + docker build --build-arg=ARG_VERSION="${VERSION}" --tag ${DOCKER_URL}:${VERSION} . -docker-push: docker-build-prod -ifneq (,$(findstring devel-,$(VERSION))) - @echo "VERSION is set to ${VERSION}, I can't push devel builds" - exit 1 -else - docker push ${DOCKER_URL}:${VERSION} - docker tag ${DOCKER_URL}:${VERSION} ${DOCKER_URL}:latest - docker push ${DOCKER_URL}:latest -endif - -docker-run: docker-build-dev +docker-run: docker-build docker run --tty --interactive --rm \ --publish 8080:8080/tcp \ --publish 8081:8081/tcp \