CI/CD Setup & Linting Gate Configuration
This document explains the s9s CI/CD pipeline, linting enforcement, and how to configure branch protection rules to maintain code quality.
Table of Contents
- Overview
- GitHub Actions Workflow
- Linting Gate
- Branch Protection Rules
- Local Testing Before Push
- Troubleshooting
- Best Practices
Overview
The s9s project uses GitHub Actions to enforce code quality standards automatically. Every pull request is checked for:
- Code Style & Quality - golangci-lint validates 15+ code quality rules
- Unit Tests - Tested on Go 1.23 and 1.24
- Build Verification - Ensures code compiles for 6 platform combinations
- Security - Trivy and gosec security scanners detect vulnerabilities
All checks must pass before code can be merged to main.
GitHub Actions Workflow
Pipeline Overview
┌─────────────┐ │ Trigger │ (push to main/develop, PR to main) └──────┬──────┘ │ ├─> ┌──────────────────────┐ │ │ Lint Job (5-8 min) │ <-- BLOCKS BUILD IF FAILS │ │ - golangci-lint │ │ │ - 15 linters │ │ └──────────┬───────────┘ │ │ ├─> ┌──────────v────────────┐ │ │ Test Job (8-15 min) │ <-- BLOCKS BUILD IF FAILS │ │ - Go 1.23 & 1.24 │ │ │ - Race detector │ │ │ - Coverage upload │ │ └──────────┬────────────┘ │ │ │ ┌──────────v────────────────────────┐ │ │ Build Job (5-10 min) - DEPENDS ON │ │ │ - Linux x86_64, arm64 │ <-- ONLY RUNS IF │ │ - macOS x86_64, arm64 │ LINT & TEST PASS │ │ - Windows x86_64 │ │ └──────────┬────────────────────────┘ │ │ └─> ┌──────────v─────────────┐ │ Security Job (3-5 min) │ │ - Trivy scanner │ │ - Gosec scanner │ └────────────────────────┘
Key Configuration
File:
.github/workflows/ci.ymlLint Job
jobs: lint: name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.24' - name: Install golangci-lint run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.3.0 - name: Run golangci-lint run: golangci-lint run --timeout 10m
What this does:
- Checks out your code
- Sets up Go 1.24 environment
- Installs golangci-lint v2.3.0
- Runs all 15 enabled linters with 10-minute timeout
- Fails the job if ANY linting violation is found
Build Job Dependencies
jobs: build: name: Build runs-on: ubuntu-latest needs: [lint, test] # CRITICAL: Requires both to pass
The
needs:Linters Enabled
The CI enforces the same linters as
.golangci.ymlCore Linters:
- - Unchecked errors (critical)
errcheck - - Suspicious constructs
govet - - Ineffective assignments
ineffassign - - Static analysis
staticcheck
Quality Linters:
- - Spelling errors
misspell - - HTTP response bodies not closed
bodyclose - - Error wrapping violations
errorlint - - Wasted assignments
wastedassign
Style & Patterns:
- - Code patterns and style
gocritic - - Dead code detection
unused - - Validate nolint directives
nolintlint - - Go idioms enforcement
revive
Advanced:
- - Cognitive complexity (threshold: 50)
gocognit - - Code duplication (threshold: 150 lines)
dupl
See
.golangci.ymlLinting Gate
What Is It?
A linting gate is an automated enforcement mechanism that prevents code merges unless all linting checks pass. The s9s project implements this through:
- GitHub Actions Jobs - The lint job in
.github/workflows/ci.yml - Job Dependencies - Build job depends on lint job passing
- Branch Protection Rules - GitHub setting to require lint job to pass before merge
How It Works
-
Developer pushes code to PR
git push origin feature/my-feature -
GitHub Actions runs automatically
- Lint job runs:
golangci-lint run --timeout 10m - If lint fails: Red X shown on PR
- If lint passes: Green checkmark shown on PR
- Lint job runs:
-
PR cannot be merged until lint passes
- With branch protection enabled (see below)
- GitHub shows: "This branch has 1 failing check"
- Merge button is disabled
-
Developer fixes issues locally
golangci-lint run # Identify issues make fmt # Fix formatting git add . && git commit -m "fix: resolve linting violations" git push origin feature/my-feature -
Lint job runs again
- All checks now pass
- Merge button becomes enabled
- Code can be merged
Current Status
The linting gate is ALREADY IMPLEMENTED in
.github/workflows/ci.yml- Lint job runs on every push and PR
- Build job depends on lint job passing
- Build cannot run if lint fails
Next Step: Configure branch protection rules to formally require the lint check (see below).
Branch Protection Rules
What Are They?
Branch protection rules are GitHub repository settings that enforce policies on branches. For the main branch, we enforce:
- Require status checks to pass before merging
- Lint check must pass
- Test check must pass
- Security check must pass
- Require pull request reviews before merging
- Require branches to be up to date before merging
- Require signed commits
Configuration Steps
Via GitHub Web UI
-
Go to Repository Settings
- Navigate to: https://github.com/jontk/s9s/settings/branches
- Or: Click "Settings" > "Branches" in left sidebar
-
Add Rule for
Branchmain- Click "Add rule"
- Branch name pattern:
main - Click "Create"
-
Configure Required Status Checks
- Scroll to "Require status checks to pass before merging"
- Enable: "Require branches to be up to date before merging"
- Search and select required checks:
- Lint
- Test
- Security Scan (optional but recommended)
-
Require Pull Request Reviews (recommended)
- Enable: "Require pull request reviews before merging"
- Approvals required: 1
- Enable: "Dismiss stale pull request approvals when new commits are pushed"
-
Additional Security Options (recommended)
- Enable: "Require signed commits"
- Enable: "Require status checks to pass before merging"
-
Save Rules
- Click "Save changes" button
Verify Configuration
After enabling branch protection:
-
Create a test PR with intentional linting error
git checkout -b test/lint-gate echo "var unusedVar int" >> cmd/s9s/main.go git add . && git commit -m "test: intentional linting error" git push origin test/lint-gate -
Check GitHub PR page
- Should show: "1 failing check" (Lint job)
- Merge button should be disabled
- Message: "Status checks failing"
-
Fix the error
git checkout test/lint-gate git revert HEAD # Undo the change git push origin test/lint-gate -
Verify merge becomes available
- All checks now pass
- Merge button becomes enabled
- Can now merge the PR
Local Testing Before Push
Pre-commit Hooks (Recommended)
Install pre-commit hooks to catch linting issues before pushing:
# One-time setup pre-commit install # Now before every commit, hooks automatically run: # - gofumpt (formatting) # - goimports (import organization) # - golangci-lint (full linting)
See docs/PRE_COMMIT_SETUP.md for detailed setup guide.
Manual Testing
Run linting locally before pushing:
# Check for violations golangci-lint run # Fix formatting issues make fmt # Run all checks that CI will run make test make lint make build
Recommended workflow:
# 1. Make your changes vim cmd/s9s/main.go # 2. Format and lint locally make fmt golangci-lint run # 3. Fix any issues shown # ... edit files ... # 4. Run tests make test # 5. Build verification make build # 6. Only then commit and push git add . git commit -m "feat: add new feature" git push origin feature/my-feature
Common Linting Violations
Unused variable:
// Wrong - golangci-lint will flag as 'unused' var unusedVar string // Right - Use underscore for intentionally unused _ = unusedVar
Unused function parameter:
// Wrong - parameter 'ctx' not used func processJob(ctx context.Context) error { return nil } // Right - rename to underscore func processJob(_ context.Context) error { return nil }
Missing error check:
// Wrong - error returned but not checked file.Close() // Right - check error if err := file.Close(); err != nil { return fmt.Errorf("failed to close file: %w", err) }
Unclosed HTTP body:
// Wrong - response body not closed resp, _ := http.Get(url) data := resp.Body // bodyclose violation // Right - defer close resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() // Ensures body is closed
Error wrapping:
// Wrong - old error wrapping format return errors.New(fmt.Sprintf("error: %s", err.Error())) // Right - use %w for proper error chaining return fmt.Errorf("failed operation: %w", err)
Troubleshooting
Lint Job Fails on PR
Problem: PR shows red X on "Lint" check
Solution:
-
View the CI logs
- Click on the red X next to "Lint"
- Click "Details" to see full error messages
-
Identify the violation
- Log shows:
cmd/s9s/main.go:23:5: unused-parameter: parameter 'cmd' is unused [unused] - This means variable in file
cmdline 23 is not usedcmd/s9s/main.go
- Log shows:
-
Fix locally and test
git fetch origin git checkout feature/my-feature golangci-lint run # See the same error locally # Fix: rename parameter to underscore vim cmd/s9s/main.go # Change `cmd` to `_` golangci-lint run # Verify fixed -
Push the fix
git add cmd/s9s/main.go git commit -m "fix: remove unused parameter" git push origin feature/my-feature
Different Linting Results Locally vs CI
Problem:
golangci-lint runCauses:
- Different golangci-lint version (CI uses v2.3.0)
- Different Go version (CI uses 1.24, you might have 1.23)
- Incomplete module cache
Solution:
# 1. Check your local version golangci-lint version # 2. If different from v2.3.0, install correct version curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.3.0 # 3. Update Go modules go mod tidy # 4. Clear build cache go clean -cache # 5. Run again golangci-lint run
How to Skip a Linting Check
Use //nolint
// Only for legitimate exceptions, with explanation: var shadowedErr error if err != nil { shadowedErr = err // nolint:shadowed // intentional for deferred handling }
Requirements:
- Must be specific: not just
//nolint:rulename//nolint - Must have explanation:
// nolint:rulename // explanation - nolintlint linter validates these directives
See Linting Standards for complete guidelines.
Merge Blocked by Status Checks
Problem: "This branch has 1 failing check" message, merge button disabled
Causes:
- Lint job is still running (wait for completion)
- Lint job failed (see "Lint Job Fails on PR" above)
- Test job failed (check test logs)
- Branch is out of date with main
Solution:
-
If branch is out of date:
git fetch origin git rebase origin/main # Fix any conflicts git push origin feature/my-feature --force-with-lease -
If tests are failing:
- Click on red X next to "Test"
- Click "Details" to see test output
- Fix the failing test locally
-
Wait for all checks to complete
- GitHub shows progress: "X of 3 checks passing"
- Once all show checkmark, merge button becomes enabled
Best Practices
For Developers
-
Install pre-commit hooks (first-time setup)
pre-commit install -
Run checks before pushing
make fmt make lint make test -
Write meaningful commit messages
- Follow Conventional Commits format (feat:, fix:, docs:, etc.)
- Reference issue numbers when appropriate
-
Respond to CI feedback quickly
- Check failing checks immediately
- Fix and push updated commits same day
- Don't let PRs accumulate unaddressed CI failures
For Code Reviews
-
Check CI status before reviewing
- All status checks should show checkmark
- Don't approve PRs with failing checks
-
Verify linting and tests
- Ensure "Lint" and "Test" are green
- Don't merge if CI red
-
Require updates before merge
- If branch becomes out of date with main
- Have author rebase and push:
git rebase origin/main && git push --force-with-lease
For Maintainers
-
Monitor CI performance
- Lint should take 5-8 minutes
- If slower, investigate golangci-lint timeout issues
-
Keep dependencies current
- Update golangci-lint version quarterly
- Update Go version in
.github/workflows/ci.yml - Keep linter rule set updated
-
Archive old build artifacts
- Builds are uploaded as artifacts
- Archive old artifacts to save storage
Related Documentation
- CONTRIBUTING.md - Contribution guidelines and setup
- LINTING.md - Linting standards and rules
- PRE_COMMIT_SETUP.md - Pre-commit hook configuration
- .golangci.yml - Linting configuration
- .github/workflows/ci.yml - CI/CD workflow
Questions?
- Check existing GitHub Issues
- Review CI workflow logs for recent runs
- Read Contributing Guide