# Build, CI, and Code Quality -- Detailed Reference

Complete build tooling, linter configuration, CI workflows, and code quality enforcement from `sapcc/keppel`.

---

## 1. go-makefile-maker

### What It Is

[github.com/sapcc/go-makefile-maker](https://github.com/sapcc/go-makefile-maker) is an SAP-internal tool that auto-generates the `Makefile`, `.golangci.yaml`, GitHub Actions workflows, `REUSE.toml`, `.typos.toml`, Dockerfile, `.dockerignore`, and Renovate config from a single declarative YAML file (`Makefile.maker.yaml`).

**Key principle**: Edit Makefile.maker.yaml and re-run go-makefile-maker. Generated files are outputs, not inputs. Every generated file carries this banner:

```
################################################################################
# This file is AUTOGENERATED with <https://github.com/sapcc/go-makefile-maker> #
# Edit Makefile.maker.yaml instead.                                            #
################################################################################
```

### Makefile.maker.yaml Configuration

| Section | Key Settings |
|---------|-------------|
| `metadata.url` | `https://github.com/sapcc/keppel` |
| `binaries` | Single binary `keppel` built from `.` (root package), installed to `bin/` |
| `coverageTest.only` | `/internal` -- only packages under `internal/` get coverage |
| `coverageTest.except` | `/drivers\|/test/util` -- drivers and test utilities excluded |
| `golang.setGoModVersion` | `true` |
| `golang.autoupdateableDeps` | `^github.com/(?:sapcc\|sap-cloud-infrastructure)/` |
| `golangciLint.createConfig` | `true` -- generates `.golangci.yaml` |
| `golangciLint.errcheckExcludes` | `encoding/json.Marshal`, `(net/http.ResponseWriter).Write`, `(*cobra.Command).Help` |
| `githubWorkflow.ci.enabled` | `true` -- generates `ci.yaml`, `checks.yaml`, `codeql.yaml` |
| `reuse.annotations` | License annotations for fixture files, docs, certs |
| `typos.extendExcludes` | Fixture JSON files and `setup_consts.go` |
| `typos.extendWords` | `fo` (regex `fo+`), `DELET` (false positive) |
| `verbatim` | Custom targets: `run-api`, `copy-fixtures`, `run-api-for-conformance-test` |

---

## 2. Makefile

### Build Flags

```makefile
GO_BUILDFLAGS += -mod vendor     # All builds use vendored dependencies
GO_LDFLAGS +=                    # Extensible, empty by default
GO_TESTFLAGS +=                  # Extensible, empty by default
GO_TESTENV +=                    # Extensible, empty by default
GO_BUILDENV +=                   # Extensible, empty by default
```

**Binary info injection via ldflags**:
```
-s -w                                        # strip debug info + DWARF
-X github.com/sapcc/go-api-declarations/bininfo.binName=keppel
-X github.com/sapcc/go-api-declarations/bininfo.version=$(BININFO_VERSION)
-X github.com/sapcc/go-api-declarations/bininfo.commit=$(BININFO_COMMIT_HASH)
-X github.com/sapcc/go-api-declarations/bininfo.buildDate=$(BININFO_BUILD_DATE)
```

Version info from `git describe --tags --always --abbrev=7`.

### Test Execution

```makefile
go test -shuffle=on -p 1 -coverprofile=build/coverprofile.out $(GO_BUILDFLAGS) \
  -ldflags '...' -covermode=count -coverpkg=$(GO_COVERPKGS) $(GO_TESTFLAGS) $(GO_TESTPKGS)
```

| Flag | Purpose |
|------|---------|
| `-shuffle=on` | Randomize test order to detect order-dependent tests |
| `-p 1` | Run packages sequentially (shared PostgreSQL database) |
| `-covermode=count` | Count-based coverage (not just boolean hit/miss) |
| `-coverpkg=...` | Coverage only for `./internal/...` excluding `/drivers` and `/test/util` |
| `-mod vendor` | Use vendored dependencies |

### Coverage

```makefile
GO_COVERPKGS := $(shell go list ./... | grep -E '/internal' | grep -Ev '/drivers|/test/util')
```

- **Included**: All packages under `./internal/` except drivers and test utilities
- **Excluded**: `./internal/drivers/...` and `./internal/test/util`
- Output: `build/cover.out` (consolidated) and `build/cover.html` (HTML report)

### Static Checks (`make static-check`)

Runs with `--keep-going`. Order within `__static-check`:

1. **`run-shellcheck`** -- shellcheck on all `.sh`/`.bash`/`.ksh`/`.zsh`/`.shlib` files (excluding `vendor/`)
2. **`run-golangci-lint`** -- `golangci-lint config verify && golangci-lint run`
3. **`check-dependency-licenses`** -- `go-licence-detector` validates licenses against allowlist
4. **`check-license-headers`** -- runs `check-addlicense` + `check-reuse`

Note: `run-typos` is NOT part of `static-check`. It runs separately in CI via the `typos` GitHub Action.

### Vendor Management

```makefile
vendor: FORCE
    go mod tidy
    go mod vendor
    go mod verify
```

Three commands in sequence:
1. `go mod tidy` -- remove unused, add missing dependencies
2. `go mod vendor` -- copy dependencies into `vendor/`
3. `go mod verify` -- verify checksums match `go.sum`

### Platform Compatibility

- macOS: uses `gsed` and `gxargs` (GNU versions via Homebrew)
- Requires make >= 3.82 (macOS ships with 3.81)
- Debian: forces `/bin/bash` as SHELL

---

## 3. golangci-lint Configuration

### Global Settings

```yaml
version: "2"                        # golangci-lint v2 config format
run:
  modules-download-mode: vendor     # Use vendored deps
  timeout: 3m0s                     # 3-minute timeout
issues:
  max-issues-per-linter: 0          # No limit (report ALL issues)
  max-same-issues: 0                # No limit on duplicate issues
output:
  formats:
    text:
      print-issued-lines: false     # Don't print source code lines
```

### Formatters

```yaml
formatters:
  enable:
    - gofmt
    - goimports
  settings:
    goimports:
      local-prefixes:
        - github.com/sapcc/keppel
```

### All 35 Enabled Linters

All defaults are disabled (`default: none`), then these are explicitly enabled:

| # | Linter | What It Checks |
|---|--------|---------------|
| 1 | `bodyclose` | HTTP response bodies are closed |
| 2 | `containedctx` | Structs containing `context.Context` (failure mode) |
| 3 | `copyloopvar` | Loop variable copies |
| 4 | `dupword` | Duplicate words in comments/strings (ignores `TRUE`, `FALSE`, `NULL` for SQL) |
| 5 | `durationcheck` | Incorrect `time.Duration` arithmetic |
| 6 | `errcheck` | Unchecked errors (`check-blank: true`, no type assertion checks) |
| 7 | `errname` | Error type naming follows `ErrFoo` / `FooError` |
| 8 | `errorlint` | Correct use of `errors.Is`/`errors.As` instead of `==`/type assertion |
| 9 | `exptostd` | Flags `golang.org/x/exp` functions now in stdlib |
| 10 | `forbidigo` | Forbidden API usage (see rules below) |
| 11 | `ginkgolinter` | Ginkgo/Gomega best practices |
| 12 | `gocheckcompilerdirectives` | Validates `//go:` compiler directives |
| 13 | `goconst` | Repeated string literals (min 5 occurrences, not in tests) |
| 14 | `gocritic` | Opinionated diagnostics (see 21 checks below) |
| 15 | `gomoddirectives` | go.mod validation (toolchain forbidden, version pattern enforced) |
| 16 | `gosec` | Security static analysis (with exclusions) |
| 17 | `govet` | `go vet` with all analyzers enabled except `fieldalignment` |
| 18 | `ineffassign` | Assignments to never-used variables |
| 19 | `intrange` | Suggests `for i := range n` (Go 1.22+) |
| 20 | `iotamixing` | Mixing iota with non-iota constants |
| 21 | `modernize` | Modern Go idiom suggestions |
| 22 | `nilerr` | Returning nil where error should be returned |
| 23 | `nolintlint` | Validates `//nolint` directives (requires specific linter name) |
| 24 | `nosprintfhostport` | Flags `fmt.Sprintf` for host:port (use `net.JoinHostPort`) |
| 25 | `perfsprint` | Performance-oriented fmt alternatives |
| 26 | `predeclared` | Shadowing predeclared Go identifiers |
| 27 | `rowserrcheck` | `sql.Rows.Err()` is checked |
| 28 | `sqlclosecheck` | SQL connections/statements are closed |
| 29 | `staticcheck` | Comprehensive static analysis |
| 30 | `unconvert` | Unnecessary type conversions |
| 31 | `unparam` | Unused function parameters |
| 32 | `unused` | Unused code (variables, functions, types) |
| 33 | `usestdlibvars` | Use stdlib constants for HTTP methods, status codes, time layouts |
| 34 | `usetesting` | Prefer `t.TempDir()` over `os.MkdirTemp` in tests |
| 35 | `whitespace` | Trailing whitespace, blank lines. `multi-func: true`: enforces newlines after multi-line function signatures |

### errcheck Exclusions

These functions can have their return values ignored:

| Function | Rationale |
|----------|-----------|
| `encoding/json.Marshal` | Only fails on unmarshalable types (programmer error) |
| `(net/http.ResponseWriter).Write` | Errors handled by HTTP server |
| `(*cobra.Command).Help` | Writes to stdout |

### forbidigo Rules

| Pattern/Package | What It Bans | Reason |
|----------------|-------------|--------|
| `^ioutil\..*$` | All `ioutil` usage | Package deprecated since Go 1.16 |
| `^http\.DefaultServeMux$` | Default HTTP mux | Global mutable state; packages add handlers silently |
| `^http\.Handle(?:Func)?$` | `http.Handle`/`http.HandleFunc` | Uses DefaultServeMux |
| `gopkg.in/square/go-jose.v2` (pkg) | Entire package | Archived, has CVEs |
| `github.com/coreos/go-oidc` (pkg) | Entire package | Depends on archived go-jose; use v3 |
| `github.com/howeyc/gopass` (pkg) | Entire package | Archived; use `golang.org/x/term` |

### All 21 gocritic Checks

| # | Check | What It Detects |
|---|-------|----------------|
| 1 | `boolExprSimplify` | Simplifiable boolean expressions |
| 2 | `builtinShadow` | Shadowing builtins (`len`, `cap`, etc.) |
| 3 | `emptyStringTest` | `len(s) == 0` instead of `s == ""` |
| 4 | `evalOrder` | Problematic evaluation order |
| 5 | `httpNoBody` | Using `nil` instead of `http.NoBody` |
| 6 | `importShadow` | Variable names shadowing import names |
| 7 | `initClause` | Simplifiable `if` init clauses |
| 8 | `methodExprCall` | Method expression calls that should be method calls |
| 9 | `paramTypeCombine` | Parameters that can use type combining |
| 10 | `preferFilepathJoin` | `filepath.Join` instead of manual path concat |
| 11 | `ptrToRefParam` | Pointer to reference-type parameter |
| 12 | `redundantSprint` | `fmt.Sprint` on already-string values |
| 13 | `returnAfterHttpError` | Missing return after `http.Error()` |
| 14 | `stringConcatSimplify` | Simplifiable string concatenation |
| 15 | `timeExprSimplify` | Time expression simplifications |
| 16 | `truncateCmp` | Truncation comparison issues |
| 17 | `typeAssertChain` | Type assertion chains -> type switches |
| 18 | `typeUnparen` | Unnecessary parentheses in type expressions |
| 19 | `unnamedResult` | Functions with unnamed results |
| 20 | `unnecessaryBlock` | Unnecessary code blocks |
| 21 | `unnecessaryDefer` | Defer where unnecessary |
| 22 | `weakCond` | Always-true or always-false conditions |
| 23 | `yodaStyleExpr` | Yoda-style conditions (`nil == x`) |

### All gosec Exclusions

| Rule | What It Checks | Why Excluded |
|------|---------------|-------------|
| `G112` | Short `ReadHeaderTimeout` (Slowloris) | Would cause Keep-Alive race conditions |
| `G117` | Hardcoded credentials | False positives when serializing passwords/tokens |
| `G306` | File permissions too broad | Umask restricts them in practice |
| `G701-G706` | Integer overflow conversions | False positives, fix expected in next gosec release |

### govet Settings

- `enable-all: true` -- all govet analyzers enabled
- `fieldalignment` disabled -- struct field alignment suggestions too noisy

### gomoddirectives Settings

```yaml
replace-allow-list:
  - github.com/mdlayher/arp          # for go-pmtud
  - k8s.io/client-go                  # for vpa_butler
toolchain-forbidden: true              # No toolchain directive in go.mod
go-version-pattern: 1\.\d+(\.0)?$     # Go version must be like 1.22 or 1.22.0
```

### Exclusion Rules

```yaml
exclusions:
  generated: lax                       # Relaxed checks on generated code
  presets:
    - common-false-positives
    - legacy
    - std-error-handling
  rules:
    - linters: [bodyclose, revive]
      path: _test\.go                  # Don't check body closure in tests
    - path: (.+)\.go$
      text: 'declaration of "err" shadows declaration at'
      # Idiomatic Go: reusing 'err' with ':=' is standard practice
    - linters: [goconst]
      path: (.+)_test\.go             # Don't flag repeated strings in tests
  paths:
    - third_party$
    - builtin$
    - examples$
```

### whitespace: multi-func

```yaml
whitespace:
  multi-func: true
```

Multi-line function signatures must be followed by a blank line before the body:

```go
// WRONG - will fail lint
func Foo(
    a int,
    b string,
) error {
    return nil
}

// CORRECT
func Foo(
    a int,
    b string,
) error {

    return nil
}
```

### usestdlibvars (all enabled)

Enforces stdlib constants for:
- HTTP methods (`http.MethodGet` not `"GET"`)
- HTTP status codes (`http.StatusOK` not `200`)
- Time weekday, month, layout constants
- Crypto hash constants
- Default RPC path constants
- SQL isolation level constants
- TLS signature scheme constants

---

## 4. License Headers

### SPDX Format (Required on All .go Files)

```go
// SPDX-FileCopyrightText: <year> SAP SE or an SAP affiliate company
// SPDX-License-Identifier: Apache-2.0
```

Year is the creation year or range like `2020-2024`.

### Enforcement Mechanisms

1. **`addlicense --check`** (`make check-addlicense`): Scans all non-vendored `.go` files
2. **REUSE compliance** (`make check-reuse` / `reuse lint`): Covers ALL files via `REUSE.toml`

### REUSE.toml Structure

Non-code files get license annotations via `[[annotations]]` blocks:
```
SPDX-FileCopyrightText = "SAP SE or an SAP affiliate company"
SPDX-License-Identifier = "Apache-2.0"
```

### Dependency License Allowlist

File: `.license-scan-rules.json`

```json
{
  "allowlist": [
    "Apache-2.0", "BSD-2-Clause", "BSD-2-Clause-FreeBSD", "BSD-3-Clause",
    "EPL-2.0", "ISC", "MIT", "MPL-2.0", "Unlicense", "Zlib"
  ]
}
```

---

## 5. GitHub Actions CI Workflows

### Workflow 1: `ci.yaml` (CI) -- Build + Test + Coverage

**Triggers**: Push to `master`, PRs to any branch (ignores `**.md`)

**Jobs**:
- **Build**: Checkout, setup Go, `make build-all`
- **Test** (depends on Build): Installs PostgreSQL 17, runs `make build/cover.out`
- **Coverage report** (depends on Test, PR only): Posts coverage diff comment via `fgrosse/go-coverage-report@v1.2.0`

**Go version**: `1.26.0`

### Workflow 2: `checks.yaml` (Checks) -- Static Analysis

**Triggers**: Push to `master`, PRs to any branch

**Steps (sequential)**:
1. `golangci/golangci-lint-action@v9` -- runs golangci-lint
2. `make run-shellcheck` -- shell script linting
3. `make check-dependency-licenses` -- license compliance
4. `crate-ci/typos@v1` -- spell checking
5. `make check-addlicense` -- license header verification
6. `fsfe/reuse-action@v6` -- REUSE compliance
7. `govulncheck ./...` -- vulnerability scanning

### Workflow 3: `codeql.yaml` (CodeQL) -- Security

**Triggers**: Push to `master`, PRs to `master`, weekly cron (Mondays 07:00 UTC)
**Steps**: CodeQL init + autobuild + analyze with `security-extended` query suite

### Workflow 4: `oci-distribution-conformance.yml` (NOT autogenerated)

**Triggers**: Push to `master`, PRs to any branch (ignores `**.md`)
**Steps**: Starts local keppel server + PostgreSQL, runs OCI Distribution Spec conformance tests

---

## 6. Code Formatting

### gofmt

Standard `gofmt` enforced by golangci-lint. No configuration.

### goimports

```yaml
formatters:
  settings:
    goimports:
      local-prefixes:
        - github.com/sapcc/keppel
```

Manual run: `make goimports`

### Import Group Ordering (Three Groups)

Imports MUST be in exactly three groups, separated by blank lines:

```go
import (
    // Group 1: Standard library
    "database/sql"
    "errors"
    "fmt"
    "net/http"

    // Group 2: External/third-party packages
    "github.com/gorilla/mux"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/sapcc/go-bits/httpapi"

    // Group 3: Local (project) packages
    "github.com/sapcc/keppel/internal/auth"
    "github.com/sapcc/keppel/internal/keppel"
    "github.com/sapcc/keppel/internal/models"
)
```

### Typos / Spell Checking

File: `.typos.toml`

```toml
[default.extend-words]
DELET = "DELET"     # Used in code, not a typo
fo = "fo"           # Part of regex fo+

[files]
extend-exclude = [
  "go.mod",
  "vendor/",
  "internal/api/keppel/fixtures/*.json",
  "internal/tasks/fixtures/trivy/*.json",
  "internal/test/setup_consts.go",
]
```

---

## 7. Required Checks Summary (What Must Pass on PRs)

| Check | Tool | What Fails It |
|-------|------|---------------|
| Build | `go build` | Compilation errors |
| Test | `go test -shuffle=on -p 1` | Test failures |
| golangci-lint | golangci-lint v2 | Any lint violation |
| shellcheck | shellcheck | Shell script issues |
| Dependency licenses | go-licence-detector | Unlicensed or disallowed dependency |
| Typos | typos | Spelling errors |
| addlicense | addlicense | Missing SPDX headers on `.go` files |
| REUSE | reuse lint | REUSE non-compliance on any file |
| govulncheck | govulncheck | Known vulnerabilities in dependencies |
| CodeQL | CodeQL | Security issues (on master PRs) |
| OCI conformance | distribution-spec test | OCI API conformance failures |

---

## 8. Concrete Rules for Passing CI

1. **License headers**: Every `.go` file starts with SPDX copyright + license lines
2. **Formatting**: Code must pass `gofmt` (no deviations)
3. **Import ordering**: Three groups (stdlib / external / local), with local = `github.com/sapcc/keppel`
4. **All 35 linters pass**: See the full linter list above
5. **No forbidden APIs**: No `ioutil`, no `http.DefaultServeMux`, no deprecated packages
6. **Error handling**: All errors checked (except 3 excluded functions). Use `errors.Is`/`errors.As`
7. **Vendored deps**: `go mod tidy && go mod vendor && go mod verify` must be clean
8. **Dependency licenses**: Must be on allowlist or explicitly overridden
9. **No spelling errors**: typos must pass
10. **Shell scripts**: Must pass shellcheck
11. **No known vulnerabilities**: govulncheck must pass
12. **REUSE compliance**: All files must have licensing info
13. **Build succeeds**: `go build -mod vendor` with ldflags
14. **All tests pass**: `go test -shuffle=on -p 1` (sequential, shuffled)
15. **Multi-line function signatures**: Must have blank line before body
16. **Use stdlib constants**: HTTP methods, status codes, time layouts -- never raw strings
17. **govet all analyzers**: All enabled except `fieldalignment`
18. **No toolchain directive in go.mod**
19. **Go version format**: Must match `1.\d+(\.0)?$` in go.mod

---

## 9. Build Command Reference

```bash
# Build
make build-all

# Run all checks (static + tests + build)
make check

# Just static checks
make static-check

# Just tests with coverage
make build/cover.out

# Lint only
make run-golangci-lint

# Format imports
make goimports

# Update vendor
make vendor

# Check license headers
make check-license-headers

# Check dependency licenses
make check-dependency-licenses

# Promote .actual fixtures to expected
make copy-fixtures
```
