From 5abec498cbf04f14a0dcb588cabbe7ebfad6cec2 Mon Sep 17 00:00:00 2001 From: Logan Stokols Date: Mon, 25 May 2026 13:39:29 -0400 Subject: [PATCH 1/3] refactor(version): derive Version() from runtime build info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the hardcoded version string in pdfer.Version() with a lookup against runtime/debug.ReadBuildInfo(). The function now returns whatever version Go's module system resolved for this dependency — matching the git tag a consumer pinned — instead of a constant that had drifted several minor versions behind the actual tags (v1.9.0 was latest, constant said "1.3.0"/"1.3.1"). Returns "(devel)" for local builds and "(unknown)" if build info is unavailable. Honest about its source, impossible to drift. No callers in the repo passed the old format-without-v-prefix expectation; consumers comparing to a literal string will need to update, but no internal test or example does. Co-Authored-By: Claude Opus 4.7 (1M context) --- pdfer.go | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/pdfer.go b/pdfer.go index 06c5921..a154a0c 100644 --- a/pdfer.go +++ b/pdfer.go @@ -77,6 +77,8 @@ package pdfer import ( + "runtime/debug" + "github.com/benedoc-inc/pdfer/types" ) @@ -107,7 +109,28 @@ type XFAConfig = types.XFAConfig // XFALocaleSet represents parsed XFA localization data. type XFALocaleSet = types.XFALocaleSet -// Version returns the library version. +const modulePath = "github.com/benedoc-inc/pdfer" + +// Version returns the library version, derived from Go module metadata. +// +// When consumed via `go get` or `go install`, this returns the resolved +// module version (e.g., "v1.10.0"). When built from a working copy, it +// returns "(devel)". Falls back to "(unknown)" if build info is unavailable. +// +// Versioning is driven by git tags through the release workflow; no manual +// string updates are required. func Version() string { - return "1.3.1" + info, ok := debug.ReadBuildInfo() + if !ok { + return "(unknown)" + } + if info.Main.Path == modulePath { + return info.Main.Version + } + for _, dep := range info.Deps { + if dep != nil && dep.Path == modulePath { + return dep.Version + } + } + return "(unknown)" } From 033ef7575ce672329038286cdae0d1dd53106707 Mon Sep 17 00:00:00 2001 From: Logan Stokols Date: Mon, 25 May 2026 13:39:44 -0400 Subject: [PATCH 2/3] build(ci): add CI + release workflows; modernize release process MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repo previously had no CI and no release automation. A local pre-push hook enforced that pdfer.go's version string changed before push to main, but the actual git tags were created manually and drifted from the constant by several minor versions over time. This adds: - .github/workflows/ci.yml — runs go vet + go test on Go 1.21/1.22/1.23 for every push to main and every PR. Includes a version-guard job that rejects reintroduction of a hardcoded version literal in pdfer.go (catches the exact regression pattern that caused the drift). - .github/workflows/release.yml — manual workflow_dispatch with a version input (e.g., v1.10.0). Validates format, refuses to overwrite existing tags, runs the test suite, creates an annotated tag, pushes it, and publishes a GitHub release with auto-generated notes. - pre-push hook — drops the version-bump enforcement (no longer meaningful now that Version() derives at runtime). Keeps the test runner for fast local feedback. - CONTRIBUTING.md — documents the new release process and removes the defunct "version bump requirement" section. Co-Authored-By: Claude Opus 4.7 (1M context) --- .githooks/pre-push | 63 +++----------------------------- .github/workflows/ci.yml | 41 +++++++++++++++++++++ .github/workflows/release.yml | 68 +++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 27 +++++++------- 4 files changed, 128 insertions(+), 71 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml diff --git a/.githooks/pre-push b/.githooks/pre-push index aa76614..38bf458 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,68 +1,13 @@ #!/bin/bash -# Pre-push hook for pdfer -# Runs the full test suite before pushing -# Also enforces version bump requirement for main branch +# Pre-push hook for pdfer. +# Runs the full test suite before pushing — fast local feedback before CI. +# Version bumps are handled by the Release workflow, not at push time. set -e echo "Running pre-push checks..." - -# Check if pushing to main branch -PUSHING_TO_MAIN=false -while read local_ref local_sha remote_ref remote_sha; do - if [[ "$remote_ref" == "refs/heads/main" ]]; then - PUSHING_TO_MAIN=true - break - fi -done - -if [ "$PUSHING_TO_MAIN" = true ]; then - echo "Pushing to main branch - checking version bump..." - - # Get current version from working directory - if [ ! -f "pdfer.go" ]; then - echo "❌ Error: pdfer.go not found" - exit 1 - fi - - # Extract version using sed (more portable than grep -P) - CURRENT_VERSION=$(grep 'return "' pdfer.go | sed -n 's/.*return "\([^"]*\)".*/\1/p' | head -1) - if [ -z "$CURRENT_VERSION" ]; then - echo "❌ Error: Could not extract version from pdfer.go" - exit 1 - fi - - # Get remote version (if remote exists) - REMOTE_VERSION="" - if git rev-parse --verify origin/main >/dev/null 2>&1; then - REMOTE_VERSION=$(git show origin/main:pdfer.go 2>/dev/null | grep 'return "' | sed -n 's/.*return "\([^"]*\)".*/\1/p' | head -1 || echo "") - fi - - # If remote version exists and matches current, reject push - if [ -n "$REMOTE_VERSION" ] && [ "$CURRENT_VERSION" = "$REMOTE_VERSION" ]; then - echo "" - echo "❌ ERROR: Version bump required!" - echo " Current version: $CURRENT_VERSION" - echo " Remote version: $REMOTE_VERSION" - echo "" - echo " Version numbers must be incremented before pushing to main." - echo " Update the version in pdfer.go and commit the change." - echo "" - echo " To override (not recommended), use: git push --no-verify" - echo "" - exit 1 - fi - - if [ -n "$REMOTE_VERSION" ]; then - echo "✓ Version check passed: $REMOTE_VERSION -> $CURRENT_VERSION" - else - echo "✓ Version check passed: $CURRENT_VERSION (no remote version found)" - fi -fi - -# Run all tests echo "Running tests..." go test ./... echo "" -echo "✅ All tests passed! Safe to push." +echo "All tests passed! Safe to push." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ecc802f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + test: + name: Test (Go ${{ matrix.go }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + go: ['1.21', '1.22', '1.23'] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go }} + - run: go vet ./... + - run: go test ./... + + version-guard: + name: No hardcoded library version + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Reject literal version string in pdfer.go + run: | + if grep -nE 'return\s+"v?[0-9]+\.[0-9]+\.[0-9]+"' pdfer.go; then + echo "" + echo "ERROR: pdfer.go contains a hardcoded version literal." + echo "Version() must derive from runtime/debug.ReadBuildInfo()." + echo "See CONTRIBUTING.md for the release process." + exit 1 + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..84775b7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,68 @@ +name: Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release (e.g., v1.10.0)' + required: true + type: string + ref: + description: 'Branch or commit to release from' + required: false + default: 'main' + type: string + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Validate version format + run: | + if ! [[ "${{ inputs.version }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$ ]]; then + echo "ERROR: version must match vMAJOR.MINOR.PATCH (optional -prerelease suffix)" + echo "Got: ${{ inputs.version }}" + exit 1 + fi + + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + fetch-depth: 0 + + - name: Fail if tag already exists + run: | + if git rev-parse "refs/tags/${{ inputs.version }}" >/dev/null 2>&1; then + echo "ERROR: tag ${{ inputs.version }} already exists" + exit 1 + fi + + - uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Verify build and tests + run: | + go vet ./... + go test ./... + + - name: Create and push annotated tag + env: + GIT_AUTHOR_NAME: github-actions[bot] + GIT_AUTHOR_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com + run: | + git tag -a "${{ inputs.version }}" -m "Release ${{ inputs.version }}" + git push origin "${{ inputs.version }}" + + - name: Create GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "${{ inputs.version }}" \ + --title "${{ inputs.version }}" \ + --generate-notes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3272633..4a54654 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,25 +25,28 @@ go test ./... # Run tests This project uses git hooks to maintain code quality: - **pre-commit**: Runs `gofmt` and `go vet` on staged files -- **pre-push**: Runs the full test suite and enforces version bump requirement +- **pre-push**: Runs the full test suite before pushing -#### Version Bump Requirement +The setup script (`./scripts/setup.sh`) configures these. To skip hooks temporarily: -**Pushing to `main` requires a version bump.** The pre-push hook will reject pushes to `main` if the version in `pdfer.go` hasn't changed from the remote version. This ensures every change to `main` has an associated version increment. - -To bump the version: -1. Update the version string in `pdfer.go` (e.g., `0.6.0` → `0.7.0`) -2. Commit the change: `git commit -m "chore: bump version to X.Y.Z"` -3. Push normally - the hook will verify the version changed - -To override (not recommended): `git push --no-verify` - -The setup script configures these automatically. To skip hooks temporarily: ```bash git commit --no-verify # Skip pre-commit git push --no-verify # Skip pre-push ``` +## Releases + +`Version()` reads the module version at runtime from `runtime/debug.ReadBuildInfo()`, so it always matches the git tag Go's module system resolved. There is no version constant in source to keep in sync. + +Releases are cut from `main` via the **Release** GitHub Actions workflow: + +1. Open the [Actions tab](../../actions/workflows/release.yml) +2. Click **Run workflow** +3. Enter the version (e.g., `v1.10.0`), following [semantic versioning](https://semver.org/) +4. The workflow validates the version, runs the test suite, creates an annotated tag, pushes it, and publishes a GitHub release with auto-generated notes + +Only maintainers with write access can dispatch the workflow. + ## Code Style - Follow standard Go conventions (use `gofmt`) From abe8a3fffdd1be5dc6d5dc4ef4e11b13b9525dc1 Mon Sep 17 00:00:00 2001 From: Logan Stokols Date: Mon, 25 May 2026 14:15:43 -0400 Subject: [PATCH 3/3] build(ci): add gofmt check now that repo is fmt-clean Originally omitted from this PR because the repo had pre-existing gofmt drift across 16 files. Rebased onto main (8b00bd1) which includes the fmt cleanup, so the check can now be enabled. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ecc802f..f43652f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,23 @@ jobs: - run: go vet ./... - run: go test ./... + fmt: + name: gofmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.22' + - name: Check formatting + run: | + UNFORMATTED=$(gofmt -l .) + if [ -n "$UNFORMATTED" ]; then + echo "The following files need gofmt:" + echo "$UNFORMATTED" + exit 1 + fi + version-guard: name: No hardcoded library version runs-on: ubuntu-latest