diff --git a/.github/workflows/jekyll.yml b/.github/workflows/jekyll.yml index 24d160c..1b75ead 100644 --- a/.github/workflows/jekyll.yml +++ b/.github/workflows/jekyll.yml @@ -32,16 +32,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Ruby - uses: ruby/setup-ruby@b90be12699fdfcbee4440c2bba85f6f460446bb0 # v1.279.0 + uses: ruby/setup-ruby@c515ec17f69368147deb311832da000dd229d338 # v1.297.0 with: ruby-version: '3.1' # Not needed with a .ruby-version file bundler-cache: true # runs 'bundle install' and caches installed gems automatically cache-version: 0 # Increment this number if you need to re-download cached gems - name: Setup Pages id: pages - uses: actions/configure-pages@v5 + uses: actions/configure-pages@v6 - name: Build with Jekyll # Outputs to the './_site' directory by default run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" @@ -49,7 +49,7 @@ jobs: JEKYLL_ENV: production - name: Upload artifact # Automatically uploads an artifact from the './_site' directory by default - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v4 # Deployment job deploy: @@ -61,4 +61,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index d0c87f1..e5809f3 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -15,10 +15,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Ruby - uses: ruby/setup-ruby@b90be12699fdfcbee4440c2bba85f6f460446bb0 # v1.279.0 + uses: ruby/setup-ruby@c515ec17f69368147deb311832da000dd229d338 # v1.297.0 with: ruby-version: "3.1" bundler-cache: true @@ -31,9 +31,9 @@ jobs: JEKYLL_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: - node-version: "20" + node-version: "22" cache: "npm" - name: Install dependencies @@ -48,7 +48,7 @@ jobs: CI: true - name: Upload test report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 if: ${{ !cancelled() }} with: name: playwright-report diff --git a/Cookiecutter.md b/Cookiecutter.md index 724686f..8ba9cad 100644 --- a/Cookiecutter.md +++ b/Cookiecutter.md @@ -1,96 +1,17 @@ --- -layout: default -title: "Cookiecutter Reference" -nav_order: 10 -description: "Detailed reference for the ColdBrew cookiecutter project template" +layout: null permalink: /cookiecutter-reference +sitemap: false --- -# Cookiecutter Reference -{: .no_toc } - -{: .note } -Looking to create your first ColdBrew service? See the **[Getting Started](/getting-started)** guide instead. This page is a detailed reference for the cookiecutter template. - -## Table of contents -{: .no_toc .text-delta } - -1. TOC -{:toc} - -Let's pretend you want to create a project called "echoserver". - -Rather than starting from scratch maybe copying some files and then editing the results to include your name, email, and various configuration issues that always get forgotten until the worst possible moment, get cookiecutter to do all the work. - -## Prerequisites -First, get Cookiecutter. Trust me, it's awesome: - -```shell -$ pip install cookiecutter -``` - -Alternatively, you can install `cookiecutter` with homebrew: - -```shell -$ brew install cookiecutter -``` -## Using the ColdBrew Cookiecutter Template - -To run it based on this template, type: - -```shell -$ cookiecutter gh:go-coldbrew/cookiecutter-coldbrew -``` - -You will be asked about your basic info \(name, project name, app name, etc.\). This info will be used to customise your new project. - -## Providing your app information to the cookiecutter - -{: .warning } -After this point, change 'github.com/ankurs', 'MyApp', etc to your own information. - -Answer the prompts with your own desired options. For example: - -{% highlight shell %} -source_path [github.com/ankurs]: github.com/ankurs -app_name [MyApp]: MyApp -grpc_package [github.com.ankurs]: github.com.ankurs -service_name [MySvc]: MySvc -project_short_description [A Golang project.]: A Golang project -docker_image [alpine:latest]: -docker_build_image [golang]: -Select docker_build_image_version: -1 - 1.26 -2 - 1.25 -Choose from 1, 2 [1]: 1 -{% endhighlight %} - -{: .note } -The exact Go image versions listed in this menu may vary depending on the cookiecutter template version you are using. Follow the options shown when you run `cookiecutter`. - -## Checkout your new project - -Enter the project and take a look around: - -```shell -$ cd MyApp/ -$ ls -``` - -Run `make help` to see the available management commands, or just run `make build` to build your project. - -```shell -$ make run -``` - -## Working with your new project - -Your project is now ready to be worked on. You can find the generated `README.md` file in the project root directory. It contains a lot of useful information about the project. - -You can also find the generated `Dockerfile` and `Makefile` in the project root directory. It contains a lot of useful commands to build, test, and run your project. You can run `make help` to see the available management commands. - -## Next Steps - -Now that you have a project, you might want to learn more about some of the [How To] in ColdBrew. - ---- -[How To]: /howto + + + + + + Redirecting to Getting Started + + +

This page has moved to Getting Started.

+ + + diff --git a/Index.md b/Index.md index 4f363cc..9c09f4b 100644 --- a/Index.md +++ b/Index.md @@ -38,6 +38,7 @@ A Kubernetes-native Go microservice framework for building production-grade gRPC | **gRPC Reflection** | Server reflection enabled by default — works with [grpcurl], [grpcui], and Postman | | **HTTP Compression** | Automatic gzip compression for all HTTP gateway responses | | **Container-aware Runtime** | Auto-tunes GOMAXPROCS to match container CPU limits via [automaxprocs] | +| **CI/CD Pipelines** | Ready-to-use [GitHub Actions] and [GitLab CI] workflows for build, test, lint, coverage, and benchmarks | ## Quick Start @@ -162,3 +163,5 @@ ColdBrew integrates with the tools you already use: [grpcurl]: https://github.com/fullstorydev/grpcurl [grpcui]: https://github.com/fullstorydev/grpcui [automaxprocs]: https://github.com/uber-go/automaxprocs +[GitHub Actions]: https://github.com/features/actions +[GitLab CI]: https://docs.gitlab.com/ci/ diff --git a/howto/APIs.md b/howto/APIs.md index 850a485..2133191 100644 --- a/howto/APIs.md +++ b/howto/APIs.md @@ -544,5 +544,5 @@ For more advanced customization options, refer to the [grpc-gateway customizatio [grpc-gateway]: https://grpc-ecosystem.github.io/grpc-gateway/ [gRPC Gateway mapping]: https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/examples/ [grpc-gateway plugin]: https://grpc-ecosystem.github.io/grpc-gateway/docs/tutorials/generating_stubs/ -[ColdBrew cookiecutter]: /cookiecutter-reference#using-the-coldbrew-cookiecutter-template +[ColdBrew cookiecutter]: /getting-started [grpc-gateway customization guide]: https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/customizing_your_gateway/ diff --git a/howto/Log.md b/howto/Log.md index dd2f980..2e5f266 100644 --- a/howto/Log.md +++ b/howto/Log.md @@ -139,7 +139,7 @@ Will output the debug log messages even when the global log level is set to info --- [TraceId interceptor]: https://pkg.go.dev/github.com/go-coldbrew/interceptors#TraceIdInterceptor [go-coldbrew/tracing]: https://pkg.go.dev/github.com/go-coldbrew/tracing -[ColdBrew cookiecutter]: /cookiecutter-reference +[ColdBrew cookiecutter]: /getting-started [interceptors]: https://pkg.go.dev/github.com/go-coldbrew/interceptors [UseColdBrewServcerInterceptors]: https://pkg.go.dev/github.com/go-coldbrew/interceptors#UseColdBrewServerInterceptors [OverrideLogLevel]: https://github.com/go-coldbrew/log#func-overrideloglevel diff --git a/howto/Tracing.md b/howto/Tracing.md index 00d6770..724b794 100644 --- a/howto/Tracing.md +++ b/howto/Tracing.md @@ -201,7 +201,7 @@ export TRACE_HEADER_NAME=x-request-id # Use a different header [TraceId interceptor]: https://pkg.go.dev/github.com/go-coldbrew/interceptors#TraceIdInterceptor [go-coldbrew/tracing]: https://pkg.go.dev/github.com/go-coldbrew/tracing -[ColdBrew cookiecutter]: /cookiecutter-reference +[ColdBrew cookiecutter]: /getting-started [interceptors]: https://pkg.go.dev/github.com/go-coldbrew/interceptors [UseColdBrewServcerInterceptors]: https://pkg.go.dev/github.com/go-coldbrew/interceptors#UseColdBrewServerInterceptors [Default Client Interceptors]: https://pkg.go.dev/github.com/go-coldbrew/interceptors#DefaultClientInterceptors diff --git a/howto/gRPC.md b/howto/gRPC.md index 988f94b..1a8afbe 100644 --- a/howto/gRPC.md +++ b/howto/gRPC.md @@ -130,7 +130,7 @@ func main() { ``` --- -[ColdBrew cookiecutter]: /cookiecutter-reference +[ColdBrew cookiecutter]: /getting-started [Building and Configuring APIs]: /howto/APIs [grpcpool]: https://pkg.go.dev/github.com/go-coldbrew/grpcpool [grpcpool.Dial]: https://pkg.go.dev/github.com/go-coldbrew/grpcpool#Dial diff --git a/howto/interceptors.md b/howto/interceptors.md index f955fea..9f4b716 100644 --- a/howto/interceptors.md +++ b/howto/interceptors.md @@ -131,7 +131,7 @@ Use the function [AddUnaryServerInterceptor] and [AddUnaryClientInterceptor] to [TraceId interceptor]: https://pkg.go.dev/github.com/go-coldbrew/interceptors#TraceIdInterceptor [go-coldbrew/tracing]: https://pkg.go.dev/github.com/go-coldbrew/tracing -[ColdBrew cookiecutter]: /cookiecutter-reference +[ColdBrew cookiecutter]: /getting-started [interceptors]: https://pkg.go.dev/github.com/go-coldbrew/interceptors [UseColdBrewServcerInterceptors]: https://pkg.go.dev/github.com/go-coldbrew/interceptors#UseColdBrewServerInterceptors [Default Client Interceptors]: https://pkg.go.dev/github.com/go-coldbrew/interceptors#DefaultClientInterceptors diff --git a/howto/production.md b/howto/production.md index 176ece8..160de7a 100644 --- a/howto/production.md +++ b/howto/production.md @@ -363,4 +363,4 @@ env: - [ ] Run `make lint` (includes `govulncheck`) before deploying --- -[ColdBrew cookiecutter]: /cookiecutter-reference +[ColdBrew cookiecutter]: /getting-started diff --git a/howto/signals.md b/howto/signals.md index 351a518..ad5f512 100644 --- a/howto/signals.md +++ b/howto/signals.md @@ -60,7 +60,7 @@ If you want to avoid this, you can set the `SHUTDOWN_DURATION_IN_SECONDS` to a v Make sure you configure your load balancer to stop sending new requests to your application after readiness check fails. This will ensure that no new requests are sent to your application when it is shutting down. --- -[ColdBrew cookiecutter]: /cookiecutter-reference#using-the-coldbrew-cookiecutter-template +[ColdBrew cookiecutter]: /getting-started [go-coldbrew/core]: https://pkg.go.dev/github.com/go-coldbrew/core [config]: https://pkg.go.dev/github.com/go-coldbrew/core/config#Config [CBStopper]: https://pkg.go.dev/github.com/go-coldbrew/core#CBStopper diff --git a/howto/swagger.md b/howto/swagger.md index bd70296..aa535bd 100644 --- a/howto/swagger.md +++ b/howto/swagger.md @@ -84,4 +84,4 @@ func main() { [Config]: https://pkg.go.dev/github.com/go-coldbrew/core/config#Config [SetOpenAPIHandler]: https://pkg.go.dev/github.com/go-coldbrew/core#CB [grpc-gateway's Open API specification]: https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/customizing_openapi_output/ -[ColdBrew cookiecutter]: /cookiecutter-reference +[ColdBrew cookiecutter]: /getting-started diff --git a/howto/testing.md b/howto/testing.md new file mode 100644 index 0000000..9929f7c --- /dev/null +++ b/howto/testing.md @@ -0,0 +1,198 @@ +--- +layout: default +title: "Testing" +parent: "How To" +description: "Unit tests, mocks, benchmarks, and coverage reports in ColdBrew services" +--- +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +## Running Tests + +The [ColdBrew cookiecutter] generates a project with tests ready to go: + +```bash +make test # Run tests with race detector + coverage +make bench # Run benchmarks (10s per benchmark, with memory stats) +make lint # golangci-lint + govulncheck +``` + +`make test` runs with `-race` and generates a `cover.out` coverage profile. Both GitHub Actions and GitLab CI pipelines run these automatically on every push and pull request. + +## Writing Unit Tests + +Tests live alongside the code they test. The generated project includes tests in `service/service_test.go` and `service/healthcheck_test.go`. + +### Test pattern + +Use [testify/assert](https://pkg.go.dev/github.com/stretchr/testify/assert) for assertions. `config.Get()` is a helper generated by the cookiecutter template in `config/config.go` — it wraps ColdBrew's `config.GetColdBrewConfig()` with your app-specific fields: + +```go +func TestEcho(t *testing.T) { + s, err := New(config.Get()) + assert.NoError(t, err) + assert.NotNil(t, s) + + resp, err := s.Echo(context.Background(), &proto.EchoRequest{Msg: "hello"}) + assert.NoError(t, err) + assert.Equal(t, "hello", resp.Msg) +} +``` + +### Testing error paths + +Always test both success and error cases: + +```go +func TestError(t *testing.T) { + s, err := New(config.Get()) + assert.NoError(t, err) + + resp, err := s.Error(context.Background(), nil) + assert.Error(t, err) + assert.Nil(t, resp) +} +``` + +### Testing health checks + +The service starts as `NOT_SERVING`. Use `SetReady()` and `SetNotReady()` to control the readiness state: + +```go +func TestReadyCheck(t *testing.T) { + s, err := New(config.Get()) + assert.NoError(t, err) + + SetNotReady() + data, err := s.ReadyCheck(context.Background(), nil) + assert.Error(t, err) + + SetReady() + data, err = s.ReadyCheck(context.Background(), nil) + assert.NoError(t, err) + assert.NotEmpty(t, data.Data) +} +``` + +## Mock Generation + +The project uses [mockery](https://vektra.github.io/mockery/) to generate mocks for interfaces. Mocks are configured in `.mockery.yaml` and output to `misc/mocks/`. + +### Generating mocks + +```bash +make mock +``` + +This generates mock implementations for all interfaces in the `service/` package. Re-run after adding or changing interfaces. + +### Configuration + +The `.mockery.yaml` file controls mock generation: + +```yaml +with-expecter: true # Generate type-safe expecter methods +all: true # Mock all interfaces in the package +dir: misc/mocks/ # Output directory +outpkg: "mocks" # Package name for generated mocks +packages: + github.com/yourname/yourapp/service: + config: + recursive: true +``` + +### Using mocks in tests + +Import the generated mocks and use the expecter pattern for type-safe expectations: + +```go +import "github.com/yourname/yourapp/misc/mocks" + +func TestWithMock(t *testing.T) { + m := mocks.NewMyInterface(t) + + // Set up expectations using the expecter + m.EXPECT().DoSomething("input").Return("output", nil) + + // Pass the mock to the code under test + result, err := MyFunction(m) + assert.NoError(t, err) + assert.Equal(t, "output", result) +} +``` + +{: .note } +Mocks are auto-cleaned up when the test finishes. If an expected call wasn't made, the test fails automatically. + +## Benchmarks + +Benchmarks live in test files alongside unit tests. The generated project includes `BenchmarkEcho` in `service/service_test.go`. + +### Writing a benchmark + +```go +func BenchmarkEcho(b *testing.B) { + cfg := config.Get() + s, err := New(cfg) + if err != nil { + b.Fatal(err) + } + + ctx := context.Background() + req := &proto.EchoRequest{Msg: "hello"} + b.ResetTimer() // Exclude setup time from measurement + for i := 0; i < b.N; i++ { + resp, err := s.Echo(ctx, req) + if err != nil { + b.Fatal(err) + } + _ = resp + } +} +``` + +**Key points:** +- Do setup before `b.ResetTimer()` to exclude it from timing +- Use `b.Fatal()` for errors (not `t.Fatal()`) +- Keep the hot loop minimal — only the code you're measuring + +### Running benchmarks + +```bash +make bench # All benchmarks (10s each, with memory stats) +go test -bench=BenchmarkEcho -benchmem ./service/... # Single benchmark +``` + +## Coverage + +### Local coverage report + +```bash +make test # Generates cover.out +make coverage-html # Opens interactive HTML report (cover.html) +``` + +The HTML report highlights covered and uncovered lines — useful for spotting gaps. + +### CI coverage + +Both CI pipelines convert `cover.out` to Cobertura XML format for reporting: + +- **GitHub Actions** — uploads `cover.xml` as a build artifact +- **GitLab CI** — uploads Cobertura report with coverage percentage extracted from the test output + +### Coverage scope + +`make test` measures coverage across your application packages: + +```makefile +go test -race -coverpkg=.,./config/...,./service/... -coverprofile cover.out ./... +``` + +To add coverage for new packages, append them to the `-coverpkg` flag in the Makefile. + +--- +[ColdBrew cookiecutter]: /getting-started diff --git a/howto/vtproto.md b/howto/vtproto.md index e697337..11a6b19 100644 --- a/howto/vtproto.md +++ b/howto/vtproto.md @@ -166,5 +166,5 @@ This is called from `processConfig()` in `core/core.go` when `DisableVTProtobuf` --- [vtprotobuf]: https://github.com/planetscale/vtprotobuf -[ColdBrew cookiecutter]: /cookiecutter-reference +[ColdBrew cookiecutter]: /getting-started [Configuration Reference]: /config-reference diff --git a/integrations.md b/integrations.md index 7896de1..eb5af1b 100644 --- a/integrations.md +++ b/integrations.md @@ -297,7 +297,7 @@ To see all the ColdBrew packages, check out the [ColdBrew packages] page. [GRPC Gateway]: https://grpc-ecosystem.github.io/grpc-gateway/ [gRPC Gateway example]: /howto/APIs/#adding-a-new-api-to-your-service [Buf]: https://buf.build/ -[ColdBrew cookiecutter]: /cookiecutter-reference#using-the-coldbrew-cookiecutter-template +[ColdBrew cookiecutter]: /getting-started [Prometheus]: https://prometheus.io/ [metrics documentation]: /howto/Metrics/ [New Relic]: https://newrelic.com/ diff --git a/quickstart.md b/quickstart.md index 6f6053b..04c6ef0 100644 --- a/quickstart.md +++ b/quickstart.md @@ -72,8 +72,13 @@ EchoServer/ ├── version/ │ └── version.go # Build-time version info ├── third_party/OpenAPI/ # Swagger UI assets (embedded) +├── .github/workflows/ +│ └── go.yml # GitHub Actions CI pipeline +├── .gitlab-ci.yml # GitLab CI pipeline ├── Makefile # Build, test, lint, run, Docker targets ├── Dockerfile # Multi-stage production build +├── .golangci.yml # Linter configuration +├── .mockery.yaml # Mock generation config ├── buf.yaml # Protobuf linting config ├── buf.gen.yaml # Code generation config └── local.env.example # Environment variable template @@ -278,9 +283,39 @@ The Dockerfile uses a multi-stage build: compiles a static Go binary in the buil ```bash make test # Tests with race detector + coverage make lint # golangci-lint + govulncheck +make mock # Generate mocks for interfaces (via mockery) ``` -Both should pass out of the box. +Both `test` and `lint` should pass out of the box. See the [Testing How-To](/howto/testing/) for details on mocks, benchmarks, and coverage reports. + +## Step 9: CI/CD — Already Configured + +Your project includes ready-to-use CI pipelines for both GitHub and GitLab. Delete whichever you don't need. + +### GitHub Actions (`.github/workflows/go.yml`) + +Runs on push to `main`/`master` and on pull requests. Four parallel jobs: + +| Job | What it does | +|-----|-------------| +| **build** | Compiles with `make build` | +| **test** | Runs `make test` (race detector + coverage) | +| **benchmark** | Runs `make bench` | +| **lint** | Runs govulncheck + golangci-lint v2 | + +Each job has concurrency control so duplicate runs on the same branch are cancelled automatically. + +### GitLab CI (`.gitlab-ci.yml`) + +Three jobs in a single `test` stage: + +| Job | What it does | +|-----|-------------| +| **unit-test** | Runs `make test`, generates Cobertura coverage report | +| **lint** | Runs `make lint` (golangci-lint + govulncheck) | +| **benchmark** | Runs `make bench` | + +Go module caching is enabled for faster builds. ## What's Built In (You Didn't Have to Configure) @@ -296,6 +331,7 @@ Everything below was set up automatically by ColdBrew: - **Swagger UI** for interactive API exploration - **Race-detected tests** via `make test` - **Vulnerability scanning** via `make lint` (includes govulncheck) +- **CI/CD pipelines** for GitHub Actions and GitLab CI (build, test, lint, benchmark) ## Alternative: Manual Setup (No Cookiecutter) diff --git a/tests/navigation.spec.ts b/tests/navigation.spec.ts index a915926..f885e83 100644 --- a/tests/navigation.spec.ts +++ b/tests/navigation.spec.ts @@ -3,7 +3,6 @@ import { test, expect } from "@playwright/test"; const topLevelPages = [ { path: "/", title: "ColdBrew" }, { path: "/getting-started/", title: "Getting Started" }, - { path: "/cookiecutter-reference/", title: "Cookiecutter Reference" }, { path: "/using/", title: "Using ColdBrew" }, { path: "/architecture/", title: "Architecture" }, { path: "/howto/", title: "How To" }, @@ -27,6 +26,7 @@ const howtoPages = [ "/howto/data-builder/", "/howto/vtproto/", "/howto/production/", + "/howto/testing/", ]; test.describe("Page Loading", () => { @@ -120,3 +120,11 @@ test.describe("Home Page CTAs", () => { await expect(btn).toHaveAttribute("href", /github\.com\/go-coldbrew/); }); }); + +test.describe("Redirects", () => { + test("/cookiecutter-reference redirects to /getting-started", async ({ page }) => { + await page.goto("/cookiecutter-reference/"); + await page.waitForURL(/getting-started/); + await expect(page).toHaveURL(/getting-started/); + }); +});