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 14 code quality rules
- Unit Tests - Tested on Go 1.23 and 1.24
- Build Verification - Ensures code compiles for 5 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 │ │ │ - 14 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.yml
Lint 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: Run golangci-lint uses: golangci/golangci-lint-action@v9 with: version: v2.8.0 args: --timeout 10m
What this does:
- Checks out your code
- Sets up Go 1.24 environment
- Installs golangci-lint v2.8.0 via GitHub Action
- Runs all 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: directive ensures the build only runs if both lint and test jobs complete successfully.
Linters Enabled
The CI enforces the same linters as .golangci.yml:
Core Linters:
errcheck- Unchecked errors (critical)govet- Suspicious constructsineffassign- Ineffective assignmentsstaticcheck- Static analysis
Quality Linters:
misspell- Spelling errorsbodyclose- HTTP response bodies not closederrorlint- Error wrapping violationswastedassign- Wasted assignments
Style & Patterns:
gocritic- Code patterns and styleunused- Dead code detectionnolintlint- Validate nolint directivesrevive- Go idioms enforcement
Advanced:
cyclop- Cyclomatic complexity (max: 10)noctx- Context propagation
See .golangci.yml and Linting Standards for complete configuration.
Linting 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
mainBranch- 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)
Note: A .pre-commit-config.yaml file is not currently included in the repository. The pre-commit hooks above should be set up manually using the commands shown.
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
cmdin filecmd/s9s/main.goline 23 is not used
- 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 run passes locally but fails in CI
Causes:
- Different golangci-lint version (CI uses v2.8.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.8.0, install correct version curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.8.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 directive (when absolutely necessary):
// Only for legitimate exceptions, with explanation: var shadowedErr error if err != nil { shadowedErr = err // nolint:shadowed // intentional for deferred handling }
Requirements:
- Must be specific:
//nolint:rulenamenot just//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-config.yaml- 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