# Best Practices for Go Makefile and Build Scripts: golang-standards/project-layout Guide

> Master Go Makefile and build script best practices with the golang-standards/project-layout guide. Ensure reproducible builds for local dev and CI.

- Repository: [golang-standards/project-layout](https://github.com/golang-standards/project-layout)
- Tags: best-practices
- Published: 2026-03-06

---

**The golang-standards/project-layout repository recommends keeping a minimal root Makefile that delegates all build logic to version-controlled shell scripts in a `scripts/` directory, ensuring reproducible builds across local development and CI pipelines.**

The `golang-standards/project-layout` repository establishes a clean separation between a tiny top-level `Makefile` and executable shell scripts in `scripts/`. This approach prevents an ever-growing Makefile while maintaining a simple, idiomatic interface for developers. By following these best practices for Go Makefile and build scripts, projects achieve consistent behavior between local `make` commands and automated CI workflows.

## Separate Concerns with a Thin Makefile and Script Directory

According to the source code in the `golang-standards/project-layout` repository, the root `Makefile` should act solely as a thin wrapper that exposes high-level targets while delegating implementation details to the `scripts/` directory. This structure keeps the project root uncluttered and provides a single source of truth for build operations.

The repository defines three primary locations for build-related files:

- **`Makefile`** (root): Contains only high-level target aliases and `include` statements, typically under 20 lines
- **`scripts/`**: Houses concrete Bash scripts for building, testing, linting, and releasing (documented in [`scripts/README.md`](https://github.com/golang-standards/project-layout/blob/main/scripts/README.md))
- **`build/`**: Holds CI configuration files, packaging scripts, and Dockerfiles (documented in [`build/README.md`](https://github.com/golang-standards/project-layout/blob/main/build/README.md))

## Recommended Makefile Pattern

The root `Makefile` should define `.PHONY` targets that invoke corresponding scripts in the `scripts/` directory. This pattern ensures that adding a new build step requires only creating a new script file and adding a single line to the Makefile.

```makefile
.PHONY: all build test lint fmt clean ci docker

all: build

# High-level targets merely invoke the corresponding script

build:   ## Build the binary

	@./scripts/build.sh

test:    ## Run unit tests

	@./scripts/test.sh

lint:    ## Run linters (golangci-lint, staticcheck, …)

	@./scripts/lint.sh

fmt:     ## Format source files

	@./scripts/fmt.sh

clean:   ## Remove generated artifacts

	@./scripts/clean.sh

ci:      ## Run the CI pipeline locally

	@./scripts/ci.sh

docker:  ## Build Docker image

	@./scripts/docker.sh

```

Key implementation details from the repository's `Makefile`:

- **`@`** silences command echo, keeping output tidy during execution
- **`##`** comments enable automatic help text generation via tools like `make help`
- **`.PHONY`** declarations prevent conflicts when files named `build` or `test` exist
- Each target invokes scripts using relative paths (`./scripts/`) to ensure portability

## Implementing Robust Build Scripts

All operational logic lives in the `scripts/` directory. The repository recommends writing these in Bash with strict error handling and Go-specific build optimizations.

Here is the standard pattern for [`scripts/build.sh`](https://github.com/golang-standards/project-layout/blob/main/scripts/build.sh) as derived from the project layout standards:

```bash
#!/usr/bin/env bash
set -euo pipefail

# Enable module support for Go < 1.14 if needed

GO_BUILD_FLAGS="-mod=vendor"

# Build flags for reproducible builds

LDFLAGS="-s -w -X main.version=$(git describe --tags --always)"

echo "▶ Building binary..."
go build ${GO_BUILD_FLAGS} -ldflags="${LDFLAGS}" -o ./bin/your_app ./cmd/your_app

echo "✅ Build complete: ./bin/your_app"

```

Critical script requirements per the source analysis:

- **`set -euo pipefail`** forces immediate exit on errors, undefined variables, or pipeline failures
- **`-mod=vendor`** ensures vendored dependencies are used when the repository pins them (referenced in the main [`README.md`](https://github.com/golang-standards/project-layout/blob/main/README.md) at line 93)
- **`-ldflags="-s -w …"`** strips debug symbols to reduce binary size while injecting version information derived from Git tags

## Integrating with CI Pipelines

Because all build logic resides in executable scripts rather than Makefile syntax, CI systems can invoke the exact same commands used by local developers. This eliminates "works on my machine" discrepancies between local `make` usage and automated pipelines.

A typical GitHub Actions workflow referencing these scripts:

```yaml

# .github/workflows/ci.yml (excerpt)

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.22'
      - name: Build
        run: ./scripts/build.sh
      - name: Test
        run: ./scripts/test.sh

```

This approach ensures that [`./scripts/build.sh`](https://github.com/golang-standards/project-layout/blob/main/./scripts/build.sh) executes identically whether triggered locally via `make build` or inside a CI container.

## Advanced Configuration and Versioning

The `golang-standards/project-layout` repository provides additional guidance for complex build scenarios through its `build/` directory structure and versioning strategies.

**Directory organization:**
- Place packaging-specific scripts under `build/package/` for OS-specific installers and distribution packages
- Store CI-specific configurations under `build/ci/` for Jenkinsfiles, Azure Pipelines, or custom automation
- Maintain Docker-related assets in `build/package/` or dedicated directories as appropriate

**Version injection:**
Embed Git-derived version information into binaries using the `-X` linker flag pattern shown in the build script example. This enables applications to report accurate version strings via `your_app --version` without manual code changes during releases.

**Script permissions:**
Ensure all scripts in the repository are executable (`chmod +x scripts/*.sh`) so they run immediately on clean checkouts without additional setup steps.

## Summary

- **Keep the root `Makefile` minimal** and delegate all logic to version-controlled scripts in `scripts/`
- **Use `set -euo pipefail`** in Bash scripts to enforce fail-fast behavior and prevent silent failures
- **Include `-mod=vendor`** in Go build commands when vendoring dependencies (as specified in the project [`README.md`](https://github.com/golang-standards/project-layout/blob/main/README.md))
- **Reference scripts directly in CI configurations** to guarantee parity between local `make` commands and pipeline execution
- **Organize supporting files** using the `build/ci/` and `build/package/` directories for CI configs and packaging scripts respectively

## Frequently Asked Questions

### Why not put all build logic directly in the Makefile?

Centralizing logic in shell scripts under `scripts/` creates a single source of truth that CI systems can invoke directly without requiring `make` on the runner. This separation also prevents the root `Makefile` from becoming an unmaintainable monolith as the project grows, and allows non-Makefile workflows (such as Windows PowerShell scripts) to coexist without breaking existing targets.

### How do I handle cross-platform builds for Windows and Linux?

The script-based approach naturally supports cross-platform flexibility. While Linux and macOS developers use the Bash scripts in `scripts/`, Windows developers can add equivalent PowerShell scripts (e.g., `scripts/build.ps1`) without modifying the Makefile. CI pipelines can detect the OS and call the appropriate script, or you can add OS-specific Makefiles that include the relevant scripts.

### Where should I store CI-specific configuration files?

According to the [`build/README.md`](https://github.com/golang-standards/project-layout/blob/main/build/README.md) in the repository, place CI-specific configuration files under `build/ci/`. This includes Jenkins pipeline definitions, Azure Pipelines YAML, or custom automation scripts. Keeping these separate from the main `scripts/` directory distinguishes continuous integration configuration from general build logic.

### How do I inject version information into Go binaries?

Use the `-ldflags` flag with the `-X` linker option during the `go build` command. The standard pattern injects the Git tag or commit hash into a package-level variable: `-ldflags="-X main.version=$(git describe --tags --always)"`. This requires defining a `version` variable in your `main` package and ensures the binary reports accurate version metadata without code modifications.