diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..837683b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +bin +dist diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2f04489..e731cd1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,3 +12,11 @@ updates: - dependency-name: github.com/gorilla/handlers versions: - 1.5.1 +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly +- package-ecosystem: docker + directory: "/" + schedule: + interval: weekly diff --git a/.github/workflows/ci-test.yaml b/.github/workflows/ci-test.yaml index 85b56eb..c24e7f8 100644 --- a/.github/workflows/ci-test.yaml +++ b/.github/workflows/ci-test.yaml @@ -10,25 +10,5 @@ permissions: contents: read jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - go: [ '1.27' ] - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Set up Go - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version: ${{ matrix.go }} - - name: Run unit tests - run: go test -race ./... - - name: Run static checks - run: go vet ./... - - name: Run acceptance tests - run: make acceptance - - name: Check for vulnerabilities - run: | - go run golang.org/x/vuln/cmd/govulncheck@v1.9.0 ./... - go run golang.org/x/vuln/cmd/govulncheck@v1.9.0 -tags=acceptance ./... + validate: + uses: ./.github/workflows/validate.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 408e435..bf583d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,8 +5,17 @@ on: tags: - 'v*.*.*' +permissions: + contents: read + jobs: + validate: + uses: ./.github/workflows/validate.yml + release: + needs: validate + permissions: + contents: write runs-on: ubuntu-latest steps: - name: Checkout @@ -18,37 +27,37 @@ jobs: with: go-version: '1.27' - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v4 + uses: goreleaser/goreleaser-action@a4f02ab5355a589de60d917a659ce617ed19b3de # v7.3.0 if: startsWith(github.ref, 'refs/tags/') with: distribution: goreleaser - version: latest + version: v2.19.0 args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_RELEASER: ${{ secrets.GH_RELEASER }} docker-hub: - needs: release + needs: [validate, release] name: Deploy to DockerHub runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} - name: Extract Docker metadata id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: ${{ github.repository }} tags: | @@ -56,9 +65,10 @@ jobs: - name: Build and push to docker registry if: success() - uses: docker/build-push-action@v5 + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: context: . platforms: linux/amd64,linux/arm64 + build-args: VERSION=${{ github.ref_name }} push: true tags: ${{ steps.meta.outputs.tags }} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..f0d6469 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,98 @@ +name: Validate + +on: + workflow_call: + +permissions: + contents: read + +env: + GO_VERSION: '1.27' + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Set up Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: ${{ env.GO_VERSION }} + - name: Run unit tests + run: go test -race ./... + - name: Run static checks + run: | + go vet ./... + go vet -tags=acceptance ./acceptance/... + - name: Run acceptance tests + run: make acceptance + - name: Check for vulnerabilities + run: | + go run golang.org/x/vuln/cmd/govulncheck@v1.9.0 ./... + go run golang.org/x/vuln/cmd/govulncheck@v1.9.0 -tags=acceptance ./... + + package: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: ${{ env.GO_VERSION }} + - name: Install GoReleaser + uses: goreleaser/goreleaser-action@a4f02ab5355a589de60d917a659ce617ed19b3de # v7.3.0 + with: + version: v2.19.0 + install-only: true + - name: Validate release configuration and build snapshot + run: | + goreleaser check + goreleaser release --snapshot --clean + python3 scripts/check-release-artifacts.py + - name: Run acceptance tests against packaged Linux binary + run: | + mkdir -p bin + tar -xzf dist/*_linux_x86_64.tar.gz -C bin killgrave + ./bin/killgrave --version + cd acceptance + go test -count=1 -tags=acceptance ./... + + docker: + strategy: + matrix: + arch: [amd64, arm64] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Set up QEMU + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 + - name: Set up Buildx + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + - name: Build image without publishing + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 + with: + context: . + platforms: linux/${{ matrix.arch }} + build-args: VERSION=validation + push: false + load: true + tags: killgrave:validation + - name: Check image version and fixture response + run: | + docker run --rm --platform linux/${{ matrix.arch }} killgrave:validation --version | grep -Fx 'Killgrave version: docker-validation' + mkdir -p "$RUNNER_TEMP/imposters" + cat > "$RUNNER_TEMP/imposters/smoke.imp.json" <<'JSON' + [{"request":{"method":"GET","endpoint":"/release-check"},"response":{"status":200,"body":"release-ok"}}] + JSON + container=$(docker run -d --platform linux/${{ matrix.arch }} -p 127.0.0.1:3000:3000 -v "$RUNNER_TEMP/imposters:/imposters:ro" killgrave:validation --host 0.0.0.0 --imposters /imposters) + trap 'docker logs "$container"; docker rm -f "$container"' EXIT + for attempt in $(seq 1 30); do + if curl --fail --silent http://127.0.0.1:3000/release-check > "$RUNNER_TEMP/response"; then break; fi + sleep 1 + done + test "$(cat "$RUNNER_TEMP/response")" = release-ok diff --git a/.gitignore b/.gitignore index d608424..4458994 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,6 @@ coverage.txt imposters -!internal/server/http/test/testdata/imposters/ \ No newline at end of file +!internal/server/http/test/testdata/imposters/ +# GoReleaser snapshot artifacts +/dist/ diff --git a/.goreleaser.yml b/.goreleaser.yml index dd1f51a..d40354c 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,7 +1,10 @@ +version: 2 + builds: - - + - + binary: killgrave main: ./cmd/killgrave/main.go - ldflags: + ldflags: - -s -w -X github.com/friendsofgo/killgrave/internal/app/cmd._version={{.Version}} env: - CGO_ENABLED=0 @@ -31,24 +34,34 @@ archives: {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }} + formats: [tar.gz] format_overrides: - goos: windows - format: zip + formats: [zip] files: - LICENSE - README.md - CHANGELOG.md -brews: +homebrew_casks: - name: killgrave - tap: + homepage: https://github.com/friendsofgo/killgrave + repository: owner: friendsofgo name: homebrew-tap token: "{{ .Env.GH_RELEASER }}" - folder: Formula + directory: Casks + binaries: + - killgrave + hooks: + post: + install_steps: | + on_macos do + run "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "{{ .StagedPath }}/killgrave"] + end description: | Simple way to generate fake servers checksum: name_template: 'checksums.txt' changelog: - skip: true + disable: true diff --git a/Dockerfile b/Dockerfile index 0f3595b..c195218 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,20 @@ -FROM golang:1.27-alpine AS build +FROM --platform=$BUILDPLATFORM golang:1.27-alpine AS build -LABEL MAINTAINER = 'Friends of Go (it@friendsofgo.tech)' +LABEL maintainer="Friends of Go (it@friendsofgo.tech)" -ARG TARGETOS=linux -ARG TARGETARCH=amd64 - -RUN apk add --update git -RUN apk add ca-certificates +RUN apk add --no-cache ca-certificates WORKDIR /go/src/github.com/friendsofgo/killgrave +COPY go.mod go.sum ./ +RUN go mod download COPY . . -RUN go mod tidy && TAG=$(git describe --tags --abbrev=0) \ - && LDFLAGS=$(echo "-s -w -X github.com/friendsofgo/killgrave/internal/app/cmd._version="docker-$TAG) \ - && CGO_ENABLED=0 GOOS="${TARGETOS}" GOARCH="${TARGETARCH}" go build -a -installsuffix cgo -o /go/bin/killgrave -ldflags "$LDFLAGS" cmd/killgrave/main.go +ARG TARGETOS=linux +ARG TARGETARCH=amd64 +ARG VERSION=dev +RUN CGO_ENABLED=0 GOOS="${TARGETOS}" GOARCH="${TARGETARCH}" go build \ + -buildvcs=false -trimpath \ + -ldflags "-s -w -X github.com/friendsofgo/killgrave/internal/app/cmd._version=docker-${VERSION}" \ + -o /go/bin/killgrave ./cmd/killgrave -# Building image with the binary FROM scratch COPY --from=build /go/bin/killgrave /go/bin/killgrave COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ diff --git a/README.md b/README.md index 8b12570..31a1d84 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ You can install Killgrave in different ways, but all of them are very simple: If you are a Mac user, you can install Killgrave using [Homebrew](https://brew.sh/): ```sh -$ brew install friendsofgo/tap/killgrave +$ brew install --cask friendsofgo/tap/killgrave ``` :warning: If you are installing via Homebrew, you always get the [latest Killgrave version](https://github.com/friendsofgo/killgrave/releases), we hope to fix this soon. @@ -561,6 +561,9 @@ In the following example, we have defined multiple imposters for the `POST /goph ```` ## Contributing + +Release tooling, nonpublishing checks, credentials, and retry steps are documented in [Release maintenance](docs/releases.md). + [Contributions](CONTRIBUTING.md) are more than welcome, if you are interested please follow our guidelines to help you get started. ## License diff --git a/scripts/check-release-artifacts.py b/scripts/check-release-artifacts.py new file mode 100644 index 0000000..c8aad43 --- /dev/null +++ b/scripts/check-release-artifacts.py @@ -0,0 +1,44 @@ +"""Verify the release's supported archives, contents, and SHA-256 checksums.""" + +import hashlib +from pathlib import Path +import tarfile +import zipfile + + +def main(): + dist = Path("dist") + targets = { + "darwin_arm64", "darwin_x86_64", "freebsd_i386", "freebsd_x86_64", + "linux_arm64", "linux_armv6", "linux_armv7", "linux_i386", "linux_x86_64", + "windows_arm64", "windows_i386", "windows_x86_64", + } + seen = set() + checksums = (dist / "checksums.txt").read_text().splitlines() + for line in checksums: + digest, name = line.split() + path = dist / name + if hashlib.sha256(path.read_bytes()).hexdigest() != digest: + raise ValueError(f"Checksum mismatch: {name}") + target = next((t for t in targets if name.endswith( + "_" + t + (".zip" if t.startswith("windows_") else ".tar.gz") + )), None) + if target is None or target in seen or not name.startswith("killgrave_"): + raise ValueError(f"Unexpected archive: {name}") + seen.add(target) + binary = "killgrave.exe" if target.startswith("windows_") else "killgrave" + if path.suffix == ".zip": + with zipfile.ZipFile(path) as archive: + entries = set(archive.namelist()) + else: + with tarfile.open(path) as archive: + entries = set(archive.getnames()) + if entries != {binary, "LICENSE", "README.md", "CHANGELOG.md"}: + raise ValueError(f"Unexpected contents in {name}: {entries}") + if seen != targets: + raise ValueError(f"Missing targets: {targets - seen}") + print(f"Verified {len(seen)} release archives and checksums") + + +if __name__ == "__main__": + main()