Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0

- uses: actions/setup-go@v6
with:
go-version: '1.26'
cache-dependency-path: |
go.sum
cli/go.sum

- name: Create a new release
uses: goreleaser/goreleaser-action@v7
Expand All @@ -60,6 +67,7 @@ jobs:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
# Use the tag that triggered this run; otherwise goreleaser may pick
# a pre-release tag (e.g. v2.6.0-beta5) pointing at the same commit.
GORELEASER_CURRENT_TAG: ${{ github.ref_name }}
Expand Down
22 changes: 18 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,34 @@ on:

jobs:
test:
name: Unit tests (${{ matrix.module }})
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
# The library and the CLI are separate Go modules, so `./...` from the
# repo root does not reach the CLI. Each module is tested in its own job.
module: ['.', 'cli']
steps:
- uses: actions/checkout@v6

- uses: actions/setup-go@v6
with:
go-version: '1.26'
cache-dependency-path: |
go.sum
cli/go.sum

- name: Run go vet
run: go vet ./...
run: go -C ${{ matrix.module }} vet ./...

- name: Run tests
run: go test -v ./...
run: go -C ${{ matrix.module }} test -v ./...

- name: Run tests with race detector
run: go test -race ./...
run: go -C ${{ matrix.module }} test -race ./...

integration:
name: Integration tests
Expand All @@ -42,9 +52,13 @@ jobs:
- uses: actions/setup-go@v6
with:
go-version: '1.26'
cache-dependency-path: integration/go.sum

- name: Run go vet
run: go -C integration vet ./...

- name: Run integration tests
run: cd integration && go test -v -timeout 10m ./...
run: go -C integration test -race -v -timeout 15m ./...

build-docker-image:
name: Build Docker image
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ output-posts/

ClAUDE.local.md
.claude/settings.local.json
completions/
43 changes: 42 additions & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,29 @@ version: 2

before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
- go -C cli mod tidy
# Shell completions are generated once here and shipped in every archive,
# which is also where the Homebrew cask picks them up from.
- rm -rf completions
- mkdir -p completions
- sh -c 'go -C cli run ./cmd/goblog completion bash > completions/goblog.bash'
- sh -c 'go -C cli run ./cmd/goblog completion zsh > completions/goblog.zsh'
- sh -c 'go -C cli run ./cmd/goblog completion fish > completions/goblog.fish'

builds:
- id: goblog
# The CLI lives in its own leaf module so that library consumers do not
# inherit its dependencies. `main` is resolved relative to `dir`.
dir: cli
main: ./cmd/goblog
binary: goblog
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
# Windows binaries are built but are not a supported install target.
- windows
goarch:
- amd64
Expand All @@ -41,6 +52,36 @@ archives:
format_overrides:
- goos: windows
formats: [zip]
files:
- LICENSE
- README.md
- completions/*

homebrew_casks:
- name: goblog
ids: [goblog]
binaries: [goblog]
repository:
owner: harrydayexe
name: homebrew-tap
branch: main
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
homepage: https://github.com/harrydayexe/GoBlog
description: Create a blog feed from posts written in Markdown
completions:
bash: completions/goblog.bash
zsh: completions/goblog.zsh
fish: completions/goblog.fish
# Only tag non-prerelease versions into the tap.
skip_upload: auto
hooks:
post:
install: |
if OS.mac?
system_command "/usr/bin/xattr",
args: ["-dr", "com.apple.quarantine", "#{staged_path}/goblog"],
must_succeed: false
end

checksum:
name_template: "checksums.txt"
Expand Down
12 changes: 11 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,20 @@
All API changes (new exported types, functions, options, flags, or fields) must include corresponding documentation updates: godoc comments on the new symbols, relevant sections in `README.md`, and any package-level `doc.go` entries that reference available options or features.

The README.md does not need to be flooded with documentation. Just the relevant information for a user to get started with the 3 methods to consume the library:
1. CLI tool via go install
1. CLI tool via Homebrew (`brew install harrydayexe/tap/goblog`) or a release archive — `go install` is not supported
2. Docker image via docker pull/run
3. The library itself

## Modules

The repository holds three Go modules:

- `/go.mod` — `github.com/harrydayexe/GoBlog/v2`, the public library (`pkg/...`). Its dependency graph is a budget: anything added here is inherited by every library consumer.
- `/cli/go.mod` — `github.com/harrydayexe/GoBlog/v2/cli`, the `goblog` binary (`cli/cmd/goblog`, `cli/internal/...`). Never published to the module proxy, so it can take any dependency it needs. Use `replace ... => ../` to reach the library.
- `/integration/go.mod` — black-box tests requiring Docker.

CLI-only code belongs under `cli/`, never at the repo root. `go test ./...`, `go vet ./...` and friends stop at a nested `go.mod`, so run them per module (the `just` recipes already do).

## Code Style

When two approaches are functionally equivalent with no performance difference, prefer the one that is easier to read and understand.
Expand Down
26 changes: 20 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,28 @@ an acknowledgement within a few days.

## Project Structure

The repository holds three Go modules:

```
cmd/goblog/ CLI entry point (main package)
pkg/ Public, importable packages
internal/ Private implementation details
go.mod github.com/harrydayexe/GoBlog/v2 (the public library)
pkg/ Public, importable packages
cli/go.mod github.com/harrydayexe/GoBlog/v2/cli (never published)
cli/cmd/goblog/ CLI entry point (main package)
cli/internal/ Private CLI implementation details
integration/go.mod github.com/harrydayexe/GoBlog/v2/integration
docs/example-posts/ Sample Markdown posts for local runs
.github/workflows/ CI: tests, license-header check, release
```

The CLI lives in its own leaf module so that anything it imports stays out of
the library's dependency graph. Both `cli/` and `integration/` use
`replace github.com/harrydayexe/GoBlog/v2 => ../`, so they always build against
the library at the current commit — no tag or release is needed in between.

Because `go test ./...` stops at a nested `go.mod` boundary, commands run from
the repo root cover the library only. The `just` recipes below iterate over
every module, and CI runs a job per module.

## Development Setup

**Prerequisites**
Expand Down Expand Up @@ -79,9 +93,9 @@ Or run them directly:
cd integration && go test -v -timeout 10m ./...
```

The unit suite (`just test`) deliberately excludes the integration module —
`go test ./...` stops at the nested `go.mod` boundary — so unit feedback
stays fast in CI even when integration tests are slow.
The unit suite (`just test`) covers the library and CLI modules but
deliberately excludes the integration module, so unit feedback stays fast in
CI even when integration tests are slow.

In CI the integration tests run in a dedicated `integration` job so the two
stages report separately.
Expand Down
10 changes: 6 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ FROM golang:1.26-alpine AS builder

WORKDIR /build

# Copy go mod files
# Copy go mod files for both modules. The CLI module replaces the library with
# ../, so the root go.mod must be present before `go mod download` can resolve.
COPY go.mod go.sum ./
RUN go mod download
COPY cli/go.mod cli/go.sum ./cli/
RUN go -C cli mod download

# Copy source code
COPY . .

# Build the binary
RUN CGO_ENABLED=0 GOOS=linux go build -o goblog ./cmd/goblog
# Build the binary from the CLI module
RUN CGO_ENABLED=0 GOOS=linux go -C cli build -o /build/goblog ./cmd/goblog

# Runtime stage
FROM alpine:latest
Expand Down
39 changes: 34 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,30 @@ GoBlog is a blog generation and serving system for creating static blog feeds fr

## CLI

Install the `goblog` binary:
The `goblog` binary lives in its own Go module (`cli/`) that is not published to
the module proxy, so it is installed from a package manager or a release archive
rather than with `go install`.

**Homebrew** (macOS and Linux):

```bash
brew install harrydayexe/tap/goblog
```

**Release archive** — grab the archive for your platform from the
[releases page](https://github.com/harrydayexe/GoBlog/releases) and put the
binary on your `PATH`:

```bash
go install github.com/harrydayexe/GoBlog/v2/cmd/goblog@latest
curl -sSL https://github.com/harrydayexe/GoBlog/releases/latest/download/GoBlog_Linux_x86_64.tar.gz | tar -xz goblog
sudo install goblog /usr/local/bin/goblog
```

Archives are published for Linux and macOS on `x86_64` and `arm64`. Windows
archives are built as well but Windows is not a supported install target.

There is also a [Docker image](#docker) if you only need to serve a blog.

```bash
# Generate static files
goblog generate posts/ output/
Expand Down Expand Up @@ -59,9 +77,11 @@ When `--base-url` is set, the server also exposes the generated feeds at `{root-

### Shell completion

`goblog` can generate shell completion scripts at runtime. After installing the
binary, source the appropriate script to enable tab-completion of subcommands and
flags.
The Homebrew cask installs bash, zsh, and fish completions for you. Release
archives ship the same scripts in a `completions/` directory.

`goblog` can also generate them at runtime — source the appropriate script to
enable tab-completion of subcommands and flags.

**Bash** — add to `~/.bashrc`:

Expand All @@ -76,6 +96,12 @@ autoload -Uz compinit && compinit
source <(goblog completion zsh)
```

**Fish** — write the script to your completions directory:

```fish
goblog completion fish > ~/.config/fish/completions/goblog.fish
```

## Docker

The official image is [`harrydayexe/goblog`](https://hub.docker.com/repository/docker/harrydayexe/goblog/general). It runs `goblog serve --health-checks /posts` by default and exposes port `8080`. Health-check endpoints are enabled in the Docker image. File watching is off by default; pass `--watch` to enable it.
Expand Down Expand Up @@ -127,6 +153,9 @@ Add GoBlog as a dependency:
go get github.com/harrydayexe/GoBlog/v2
```

The CLI is a separate module (`cli/`) that is never published, so none of its
dependencies reach your build.

The main packages are:

| Package | Summary |
Expand Down
10 changes: 5 additions & 5 deletions cmd/goblog/main.go → cli/cmd/goblog/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import (
"log/slog"
"os"

"github.com/harrydayexe/GoBlog/v2/internal/cliflags"
"github.com/harrydayexe/GoBlog/v2/internal/generator"
loggermod "github.com/harrydayexe/GoBlog/v2/internal/logger"
"github.com/harrydayexe/GoBlog/v2/internal/server"
"github.com/harrydayexe/GoBlog/v2/internal/utilities"
"github.com/harrydayexe/GoBlog/v2/cli/internal/cliflags"
"github.com/harrydayexe/GoBlog/v2/cli/internal/generator"
loggermod "github.com/harrydayexe/GoBlog/v2/cli/internal/logger"
"github.com/harrydayexe/GoBlog/v2/cli/internal/server"
"github.com/harrydayexe/GoBlog/v2/cli/internal/utilities"
"github.com/urfave/cli/v3"
)

Expand Down
10 changes: 6 additions & 4 deletions cmd/goblog/version.go → cli/cmd/goblog/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ package main
import "runtime/debug"

// buildVersion returns the version string for the running binary.
// When GoReleaser ldflags are present the injected value is returned as-is.
// Otherwise the value is derived from the embedded Go module build metadata,
// so binaries installed via go install report the module tag. It is safe to
// call from multiple goroutines.
// Released binaries carry the version injected by GoReleaser's ldflags, which
// is returned as-is. The CLI module is never published to the module proxy, so
// a locally built binary has no module tag to fall back on; the embedded Go
// module build metadata is still consulted in case one is present, and
// otherwise the version reads "dev". It is safe to call from multiple
// goroutines.
func buildVersion() string {
if version != "dev" {
return version
Expand Down
31 changes: 31 additions & 0 deletions cli/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module github.com/harrydayexe/GoBlog/v2/cli

go 1.26.3

// The CLI is never published as a Go module; it is always built from source in
// this repository, so it tracks the library at the same commit.
replace github.com/harrydayexe/GoBlog/v2 => ../

require (
github.com/fatih/color v1.18.0
github.com/harrydayexe/GoBlog/v2 v2.0.0
github.com/harrydayexe/GoWebUtilities v1.5.1
github.com/urfave/cli/v3 v3.6.1
)

require (
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/alecthomas/chroma/v2 v2.22.0 // indirect
github.com/caarlos0/env/v11 v11.3.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/gorilla/feeds v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/yuin/goldmark v1.7.16 // indirect
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect
go.abhg.dev/goldmark/frontmatter v0.3.0 // indirect
go.abhg.dev/goldmark/wikilink v0.6.0 // indirect
golang.org/x/sys v0.25.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading
Loading