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
5 changes: 5 additions & 0 deletions .githooks/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# Thin wrapper: defer to the shared validator so local and CI use identical rules.
set -euo pipefail
repo_root="$(git rev-parse --show-toplevel)"
exec "${repo_root}/scripts/check-commit-msg.sh" "$1"
5 changes: 5 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# Thin wrapper: defer to the shared validator so local and CI use identical rules.
set -euo pipefail
repo_root="$(git rev-parse --show-toplevel)"
exec "${repo_root}/scripts/check-go-fmt.sh" staged
78 changes: 78 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: CI

on:
pull_request:
branches: [ develop, main ]
push:
branches: [ develop, main ]

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25"
cache: true

- name: Download modules
run: go mod download

- name: Verify modules
run: go mod verify

- name: gofmt
run: |
set -euo pipefail
unformatted="$(gofmt -l .)"
if [[ -n "$unformatted" ]]; then
echo "::error::Files need gofmt:"
echo "$unformatted"
exit 1
fi

- name: go vet
run: go vet ./...

test:
name: Build & test (${{ matrix.label }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
label: Linux
- os: macos-latest
label: macOS

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25"
cache: true

- name: Download modules
run: go mod download

- name: go build
run: go build ./...

- name: go test
run: go test -race -count=1 ./...
45 changes: 45 additions & 0 deletions .github/workflows/commitlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: commitlint

on:
pull_request:
types: [opened, synchronize, reopened, edited]

jobs:
conventional-commits:
name: Validate commit messages
runs-on: ubuntu-latest
steps:
- name: Checkout PR branch
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}

- name: Validate every commit in the PR range
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
chmod +x scripts/check-commit-msg.sh

fail=0
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT

# Walk every commit introduced by this PR.
while IFS= read -r sha; do
git log -1 --format=%B "$sha" > "$tmp"
if ! scripts/check-commit-msg.sh "$tmp"; then
echo "::error::commit $sha has an invalid message header"
fail=1
fi
done < <(git rev-list --no-merges "$BASE_SHA..$HEAD_SHA")

if [[ $fail -ne 0 ]]; then
echo "::error::One or more commits violate Conventional Commits 1.0.0."
echo "Fix locally with: git rebase -i $BASE_SHA (reword the offending commits)"
exit 1
fi

echo "All commit messages conform to Conventional Commits 1.0.0."
62 changes: 62 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Release Please

on:
push:
branches:
- main

permissions:
contents: write
pull-requests: write
issues: write
repository-projects: write

jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v4
id: release
with:
token: ${{ secrets.PAT_TOKEN }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
target-branch: main

# Build and upload binaries only when a release is created.
- uses: actions/checkout@v4
if: ${{ steps.release.outputs.release_created }}

- name: Set up Go
if: ${{ steps.release.outputs.release_created }}
uses: actions/setup-go@v5
with:
go-version: "1.25"

- name: Get dependencies
if: ${{ steps.release.outputs.release_created }}
run: go mod download

- name: Build cross-platform binaries
if: ${{ steps.release.outputs.release_created }}
env:
CGO_ENABLED: 0
run: |
mkdir -p dist

VERSION="${{ steps.release.outputs.tag_name }}"
BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
LDFLAGS="-s -w -X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME}"

GOOS=linux GOARCH=amd64 go build -ldflags="${LDFLAGS}" -o dist/github-butler-linux-amd64 .
GOOS=linux GOARCH=arm64 go build -ldflags="${LDFLAGS}" -o dist/github-butler-linux-arm64 .
GOOS=darwin GOARCH=amd64 go build -ldflags="${LDFLAGS}" -o dist/github-butler-darwin-amd64 .
GOOS=darwin GOARCH=arm64 go build -ldflags="${LDFLAGS}" -o dist/github-butler-darwin-arm64 .
GOOS=windows GOARCH=amd64 go build -ldflags="${LDFLAGS}" -o dist/github-butler-windows-amd64.exe .

- name: Upload Release Assets
if: ${{ steps.release.outputs.release_created }}
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
run: |
gh release upload ${{ steps.release.outputs.tag_name }} dist/*
33 changes: 33 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Binaries
github-butler
/bin/
/dist/
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test / coverage
*.test
*.out
coverage.txt
coverage.html

# Go build cache / workspace files
/vendor/
go.work
go.work.sum

# IDE / editor
.idea/
.vscode/
*.swp
*.swo
*~

# macOS
.DS_Store

# Local config (user config lives in ~/.config/github-butler/)
config.local.yaml
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "0.1.0"
}
51 changes: 51 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
.PHONY: setup build build-all release clean test fmt vet tidy

BINARY_NAME := github-butler
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -s -w -X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME)

# `setup` wires the in-repo git hooks. It is idempotent and is a prerequisite
# of `build` and `test`, so any common dev command re-asserts the hook config.
setup:
@git config core.hooksPath .githooks
@chmod +x .githooks/* scripts/*.sh 2>/dev/null || true
@echo "hooks active: core.hooksPath = $$(git config core.hooksPath)"

build: setup
@echo "Building $(BINARY_NAME) $(VERSION)..."
@go build -ldflags="$(LDFLAGS)" -o $(BINARY_NAME) .
@echo "Built ./$(BINARY_NAME)"

# Cross-compile to dist/ for the 5 release targets. CGO is off so the
# resulting binaries are fully static and don't depend on libc on the
# host they're installed to.
build-all:
@echo "Building $(BINARY_NAME) $(VERSION) for all platforms..."
@mkdir -p dist
@CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o dist/$(BINARY_NAME)-linux-amd64 .
@CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o dist/$(BINARY_NAME)-linux-arm64 .
@CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o dist/$(BINARY_NAME)-darwin-amd64 .
@CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o dist/$(BINARY_NAME)-darwin-arm64 .
@CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o dist/$(BINARY_NAME)-windows-amd64.exe .
@echo "Built all platform binaries in dist/"

release: clean build-all
@echo "Release artifacts ready in dist/:"
@ls -la dist/

clean:
@rm -rf dist/ $(BINARY_NAME)
@echo "Cleaned build artifacts"

test: setup
go test ./...

fmt:
go fmt ./...

vet:
go vet ./...

tidy:
go mod tidy
Loading
Loading