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
80 changes: 66 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
# there does not need a second one
branches-ignore: [master]
workflow_call:
outputs:
modules:
description: Every module directory in the repo
value: ${{ jobs.modules.outputs.all }}

# A branch that is pushed again supersedes its own in-flight run, but master is left
# alone: its run gates the release.
Expand All @@ -16,49 +20,97 @@ concurrency:
permissions:
contents: read

# Each module is built and tested the way a consumer gets it - resolved from its own
# go.mod - rather than through the workspace, which would paper over a missing require.
env:
GOWORK: "off"

jobs:
# Modules are discovered rather than listed, so adding one to the repo puts it under
# CI without touching this file.
modules:
runs-on: ubuntu-latest
outputs:
all: ${{ steps.find.outputs.all }}
steps:
- uses: actions/checkout@v7

- name: Find the modules
id: find
run: |
set -euo pipefail

all=$(find . -name go.mod -not -path './.git/*' -printf '%h\n' \
| sed 's|^\./||' \
| sort \
| jq -R -s -c 'split("\n") | map(select(length > 0))')

echo "all=$all" >> "$GITHUB_OUTPUT"
echo "Modules: $all"

lint:
needs: modules
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
module: ${{ fromJSON(needs.modules.outputs.all) }}
steps:
- uses: actions/checkout@v7

- uses: actions/setup-go@v7
with:
go-version-file: go.mod
go-version-file: ${{ matrix.module }}/go.mod

- name: Tool versions
- name: Tool version
id: tools
run: |
echo "golangci=$(make -s print-GOLANGCI_VERSION)" >> "$GITHUB_OUTPUT"
echo "actionlint=$(make -s print-ACTIONLINT_VERSION)" >> "$GITHUB_OUTPUT"
run: echo "golangci=$(make -s print-GOLANGCI_VERSION)" >> "$GITHUB_OUTPUT"

# the root .golangci.yml applies to every module: golangci-lint walks up for it
- uses: golangci/golangci-lint-action@v9
with:
version: ${{ steps.tools.outputs.golangci }}

# the other half of `make lint`, so CI is never weaker than the local target
- name: Lint the workflows
env:
ACTIONLINT_VERSION: ${{ steps.tools.outputs.actionlint }}
run: |
go install "github.com/rhysd/actionlint/cmd/actionlint@${ACTIONLINT_VERSION}"
"$(go env GOPATH)/bin/actionlint"
working-directory: ${{ matrix.module }}

- name: go.mod and go.sum are tidy
working-directory: ${{ matrix.module }}
run: |
go mod tidy
git diff --exit-code go.mod go.sum

# One repo, one set of workflows - not per module.
workflows:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- uses: actions/setup-go@v7
with:
go-version-file: caller/go.mod

- name: Lint the workflows
run: |
set -euo pipefail
go install "github.com/rhysd/actionlint/cmd/actionlint@$(make -s print-ACTIONLINT_VERSION)"
"$(go env GOPATH)/bin/actionlint"

test:
needs: modules
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
module: ${{ fromJSON(needs.modules.outputs.all) }}
steps:
- uses: actions/checkout@v7

- uses: actions/setup-go@v7
with:
go-version-file: go.mod
go-version-file: ${{ matrix.module }}/go.mod

- run: go build ./...
working-directory: ${{ matrix.module }}

# -race because the resolver cache and the registry heartbeat are concurrent
- run: go test -race -count=1 ./...
working-directory: ${{ matrix.module }}
85 changes: 55 additions & 30 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
branches: [master]

# Releases are serialised, never cancelled: two merges landing together must each get
# their turn at the tag rather than one clobbering the other's run.
# their turn at the tags rather than one clobbering the other's run.
concurrency:
group: release
cancel-in-progress: false
Expand All @@ -26,43 +26,68 @@ jobs:
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # the whole tag history, to find the version to bump
fetch-depth: 0 # the whole tag history, to find each module's version to bump

- name: Work out the next version
id: version
run: |
set -euo pipefail

# A re-run of this workflow, or a commit tagged by hand, must not publish a
# second version of the same code.
if tag=$(git describe --exact-match --tags HEAD 2>/dev/null); then
echo "HEAD is already released as $tag, nothing to do"
exit 0
fi

latest=$(git tag -l 'v*' --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || true)
if [ -z "$latest" ]; then
next=v0.1.0
else
IFS=. read -r major minor patch <<<"${latest#v}"
next="v${major}.${minor}.$((patch + 1))"
fi

echo "next=$next" >> "$GITHUB_OUTPUT"
echo "Releasing $next (previous: ${latest:-none})"
- uses: actions/setup-go@v7
with:
go-version-file: caller/go.mod

- name: Tag and release
if: steps.version.outputs.next != ''
# Every module is versioned on its own, so a change to one does not drag the
# others through a release they have no new code for. Tags carry the module's
# directory as a prefix, which is how the go command resolves a nested module.
- name: Tag the modules whose files changed
env:
GH_TOKEN: ${{ github.token }}
NEXT: ${{ steps.version.outputs.next }}
MODULES: ${{ needs.ci.outputs.modules }}
run: |
set -euo pipefail

git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

git tag -a "$NEXT" -m "$NEXT"
git push origin "$NEXT"
tagged=()
while read -r dir; do
# a module that replaces another by local path cannot be consumed from
# outside the repo, so it is built and tested but never published
replaces=$(go mod edit -json "$dir/go.mod" \
| jq '[.Replace // [] | .[] | select(.New.Path | test("^\\.{1,2}/"))] | length')
if [ "$replaces" -ne 0 ]; then
echo "$dir: not publishable (replaces a module by local path), skipping"
continue
fi

# A re-run of this workflow, or a tag pushed by hand, must not publish a
# second version of the same code.
existing=$(git tag --points-at HEAD --list "$dir/v*" | head -1)
if [ -n "$existing" ]; then
echo "$dir: HEAD is already released as $existing, skipping"
continue
fi

latest=$(git tag --list "$dir/v*" --sort=-v:refname \
| grep -E "^$dir/v[0-9]+\.[0-9]+\.[0-9]+$" | head -1 || true)

if [ -z "$latest" ]; then
next="$dir/v0.1.0"
elif git diff --quiet "$latest" HEAD -- "$dir"; then
echo "$dir: unchanged since $latest, skipping"
continue
else
IFS=. read -r major minor patch <<<"${latest#"$dir/v"}"
next="$dir/v${major}.${minor}.$((patch + 1))"
fi

echo "$dir: releasing $next (previous: ${latest:-none})"
git tag -a "$next" -m "$next"
tagged+=("$next")
done < <(jq -r '.[]' <<<"$MODULES")

if [ ${#tagged[@]} -eq 0 ]; then
echo "No module changed, nothing to release"
exit 0
fi

gh release create "$NEXT" --title "$NEXT" --generate-notes
git push origin "${tagged[@]}"
for tag in "${tagged[@]}"; do
gh release create "$tag" --title "$tag" --generate-notes
done
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
go.work.sum
17 changes: 14 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,24 @@ ACTIONLINT_VERSION ?= v1.7.12

GOBIN ?= $(or $(shell go env GOBIN),$(shell go env GOPATH)/bin)

# Discovered rather than listed, so a new module is covered without editing this file.
MODULES := $(patsubst ./%/go.mod,%,$(shell find . -name go.mod -not -path './.git/*' | sort))

# GOWORK=off so each module is exercised the way a consumer gets it, resolved from its
# own go.mod rather than through the workspace.
.PHONY: test lint install-tools

test:
go test -v ./...
test:
@for m in $(MODULES); do \
echo "== $$m"; \
(cd $$m && GOWORK=off go test -v ./...) || exit 1; \
done

lint:
$(GOBIN)/golangci-lint run ./...
@for m in $(MODULES); do \
echo "== $$m"; \
(cd $$m && GOWORK=off $(GOBIN)/golangci-lint run ./...) || exit 1; \
done
$(GOBIN)/actionlint

install-tools:
Expand Down
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
# Callpack
# Callpack

Modules that I constantly implement in my projects: and which I decided to collect in one place for easy reuse!

| Module | Import path | What it does |
| --- | --- | --- |
| `caller` | `github.com/draincloud/callpack/caller` | `http.Client` fancy wrapper with a round-tripper middleware chain: headers, logging, etc |
| `registry` | `github.com/draincloud/callpack/registry` | Registers a service instance in consul and heartbeats its TTL check. |

```sh
go get github.com/draincloud/callpack/caller
go get github.com/draincloud/callpack/registry
```

Each is tagged with its own prefix — `caller/v0.1.0`, `registry/v0.1.0`

`integration/` holds the tests that exercise the two against each other.
29 changes: 29 additions & 0 deletions caller/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module github.com/draincloud/callpack/caller

go 1.25.0

require (
github.com/draincloud/logger v0.0.4
github.com/hashicorp/consul/api v1.32.1
github.com/stretchr/testify v1.12.1
)

require (
github.com/armon/go-metrics v0.4.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/serf v0.10.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/sys v0.31.0 // indirect
)
Loading
Loading