From 8812c681a4290a6fdf4a68467ca940bc87f6c9a9 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Thu, 30 Jul 2026 09:28:51 -0700 Subject: [PATCH 1/3] ci: add MIT license, CI, and tag-driven release workflow The repo had no CI and no license, and the README described a release process that did not exist: a v1.0.0 tag was pushed but no release was ever cut, so `go install ...@v1.0.0` was the only way to get the tool and nothing was published for users without a Go toolchain. Add MIT LICENSE (copyright DivergentCodes) and two workflows: ci.yml runs on push to main and on PRs: gofmt, vet, `go test -race`, a guard that fails if a third-party dependency ever appears (the project's central claim), and a dogfooding step that lints the commits being built. release.yml triggers on `v*` tags: tests, cross-compiles for linux, darwin, and windows, stamps the tag into main.version via ldflags, verifies the built binary reports the tagged version before publishing, and uploads archives with a checksums.txt alongside autogenerated notes. Update the README's Releases section to describe the actual process and document the prebuilt-binary install path. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VSrrciEDBTuFNMtKocScML --- .github/workflows/ci.yml | 55 ++++++++++++++++++++++++++ .github/workflows/release.yml | 74 +++++++++++++++++++++++++++++++++++ LICENSE | 21 ++++++++++ README.md | 29 ++++++++++++-- 4 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 LICENSE diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8a2320b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + # gofmt exits 0 even when it reformats, so check for named files instead. + - name: gofmt + run: | + unformatted="$(gofmt -l .)" + if [ -n "$unformatted" ]; then + echo "not gofmt'd:" + echo "$unformatted" + exit 1 + fi + + - name: vet + run: go vet ./... + + - name: test + run: go test -race ./... + + # This repo's whole pitch is zero third-party dependencies. Fail loudly + # if a require block ever appears rather than letting it in unnoticed. + - name: no third-party dependencies + run: | + if go list -m all | tail -n +2 | grep .; then + echo "third-party dependencies found; this project vendors none" + exit 1 + fi + + # Dogfood: the linter must accept the commits it is being built from. + - name: lint own commits + run: | + go build -o /tmp/commitlint . + if [ "${{ github.event_name }}" = "pull_request" ]; then + /tmp/commitlint lint --range \ + "origin/${{ github.base_ref }}..HEAD" + else + git log -1 --format=%B | /tmp/commitlint lint + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f20a9c5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,74 @@ +name: release + +# Cutting a release is: git tag vX.Y.Z && git push origin vX.Y.Z +on: + push: + tags: ["v*"] + +permissions: + contents: write # required to create the release and upload assets + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history so release notes can diff against the previous tag + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: test + run: go test -race ./... + + # A tag that ships a binary claiming a different version is worse than no + # release, so refuse to build one that disagrees with the tag. + - name: build release binaries + env: + VERSION: ${{ github.ref_name }} + run: | + mkdir -p dist + for target in \ + linux/amd64 linux/arm64 \ + darwin/amd64 darwin/arm64 \ + windows/amd64; do + os="${target%/*}"; arch="${target#*/}" + name="commitlint_${VERSION}_${os}_${arch}" + out="dist/${name}/commitlint" + [ "$os" = "windows" ] && out="${out}.exe" + mkdir -p "dist/${name}" + CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" \ + go build -trimpath -ldflags "-s -w -X main.version=${VERSION}" -o "$out" . + cp LICENSE README.md "dist/${name}/" + if [ "$os" = "windows" ]; then + (cd dist && zip -qr "${name}.zip" "${name}") + else + (cd dist && tar -czf "${name}.tar.gz" "${name}") + fi + rm -rf "dist/${name}" + done + (cd dist && sha256sum ./*.tar.gz ./*.zip > checksums.txt) + ls -la dist + + - name: verify version stamp matches tag + env: + VERSION: ${{ github.ref_name }} + run: | + go build -ldflags "-X main.version=${VERSION}" -o /tmp/commitlint . + got="$(/tmp/commitlint version)" + if [ "$got" != "$VERSION" ]; then + echo "binary reports '$got' but tag is '$VERSION'" + exit 1 + fi + + - name: publish release + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ github.ref_name }} + run: | + gh release create "$VERSION" \ + --title "$VERSION" \ + --generate-notes \ + dist/*.tar.gz dist/*.zip dist/checksums.txt diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a543404 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 DivergentCodes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 60a0bba..f8c939c 100644 --- a/README.md +++ b/README.md @@ -124,9 +124,32 @@ exec commitlint lint --file "$1" ## Releases -Versions are published as **GitHub tagged releases** with changelogs in the -release notes (no committed changelog file). Pin a released version in CI: +Versions are published as **GitHub tagged releases** with autogenerated +changelogs in the release notes (no committed changelog file). Pin a released +version in CI: ```sh -go install github.com/DivergentCodes/commitlint@v1.0.0 +go install github.com/DivergentCodes/commitlint@v1.0.1 ``` + +Each release also carries prebuilt binaries for linux, darwin, and windows +(amd64 and arm64, except windows) plus a `checksums.txt`, for environments +without a Go toolchain: + +```sh +curl -sSfL -O https://github.com/DivergentCodes/commitlint/releases/download/v1.0.1/commitlint_v1.0.1_linux_amd64.tar.gz +tar -xzf commitlint_v1.0.1_linux_amd64.tar.gz +./commitlint_v1.0.1_linux_amd64/commitlint version +``` + +Cutting a release is pushing a tag — `.github/workflows/release.yml` runs the +tests, cross-compiles, verifies the binary reports the tagged version, and +publishes: + +```sh +git tag v1.0.1 && git push origin v1.0.1 +``` + +## License + +[MIT](LICENSE) From 2832c41e3a8c5d36bbf3739b0ece17a194abc4bd Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Thu, 30 Jul 2026 09:31:27 -0700 Subject: [PATCH 2/3] fix: resolve PR base ref in shallow CI checkout The dogfooding step linted `origin/..HEAD`, but actions/checkout fetches a single shallow branch, so `origin/main` does not exist in the CI clone and git exited 128. It passed locally only because a normal worktree has the full history. Fetch the PR base commit explicitly and lint against that sha instead. Verified against a real `--depth 1` clone, which reproduces the failure before the fetch and resolves the range after it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VSrrciEDBTuFNMtKocScML --- .github/workflows/ci.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a2320b..c26de5d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,12 +44,17 @@ jobs: fi # Dogfood: the linter must accept the commits it is being built from. + # checkout is shallow and only fetches the PR branch, so the base ref has + # to be fetched explicitly before a range can be resolved against it. - name: lint own commits + env: + EVENT: ${{ github.event_name }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} run: | go build -o /tmp/commitlint . - if [ "${{ github.event_name }}" = "pull_request" ]; then - /tmp/commitlint lint --range \ - "origin/${{ github.base_ref }}..HEAD" + if [ "$EVENT" = "pull_request" ]; then + git fetch --no-tags --depth=50 origin "$BASE_SHA" + /tmp/commitlint lint --range "$BASE_SHA..HEAD" else git log -1 --format=%B | /tmp/commitlint lint fi From 99b1e7e7846be7ddc00f93e070a3d3106117656a Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Thu, 30 Jul 2026 09:35:52 -0700 Subject: [PATCH 3/3] fix: lint PR head sha, not the synthetic merge commit On pull_request events actions/checkout checks out the merge commit GitHub builds for the PR, whose subject is `Merge into `. That is not a conventional subject, so the dogfooding step failed on a branch whose own commits are all fine. `--no-merges` does not help here: a range endpoint is always included, and HEAD *was* the merge commit. Lint up to the PR's real branch tip instead, fetching both endpoints so the range resolves in the shallow checkout. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VSrrciEDBTuFNMtKocScML --- .github/workflows/ci.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c26de5d..e524994 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,17 +44,23 @@ jobs: fi # Dogfood: the linter must accept the commits it is being built from. - # checkout is shallow and only fetches the PR branch, so the base ref has - # to be fetched explicitly before a range can be resolved against it. + # + # Two things make this fiddlier than it looks on pull_request events: + # checkout is shallow, so the base commit must be fetched before a range + # can resolve against it; and HEAD is the synthetic "Merge into + # " commit GitHub builds for the PR, which is not a conventional + # subject. Range endpoints are always included, so --no-merges does not + # drop it — lint up to the branch's real tip instead. - name: lint own commits env: EVENT: ${{ github.event_name }} BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | go build -o /tmp/commitlint . if [ "$EVENT" = "pull_request" ]; then - git fetch --no-tags --depth=50 origin "$BASE_SHA" - /tmp/commitlint lint --range "$BASE_SHA..HEAD" + git fetch --no-tags --depth=50 origin "$BASE_SHA" "$HEAD_SHA" + /tmp/commitlint lint --range "$BASE_SHA..$HEAD_SHA" else git log -1 --format=%B | /tmp/commitlint lint fi