Skip to content

Contributing to c9s

Thank you for your interest in contributing to c9s! This guide will help you get started.

Development Environment

Prerequisites

  • Go 1.26 or later
  • Apple Containers CLI (optional, for testing against real containers)
  • make
  • Git

Setup

  1. Clone the repository
git clone https://github.com/torosent/c9s.git
cd c9s
  1. Install development tools
make install-tools

This installs: - gofumpt (code formatter) - staticcheck (static analysis) - golangci-lint (linter suite)

  1. Build the project
make build

The binary will be in bin/c9s.

  1. Run tests
make ci

This runs: - Code formatting check - Linting - Unit tests with race detection - Coverage report (must be ≥70%)

Running Locally

With Demo Data

go run ./cmd/c9s --demo-data

Against Real Containers

go run ./cmd/c9s

Project Structure

c9s/
├── cmd/
│   ├── c9s/              # Main entrypoint
│   └── gen-hotkeys/      # Hotkeys doc generator
├── internal/
│   ├── cli/              # Container CLI wrapper + fake
│   ├── ui/               # Bubble Tea UI components
│   │   ├── screens/      # Full-screen views
│   │   ├── modals/       # Popup modals
│   │   ├── widgets/      # Reusable UI widgets
│   │   ├── keymap/       # Keyboard binding registry
│   │   └── theme/        # Color scheme management
│   ├── jobs/             # Background job manager
│   ├── config/           # Configuration loading
│   └── clock/            # Testable time abstraction
├── docs/                 # Documentation
└── tools/demos/          # VHS tape scripts

Coding Standards

Formatting

All code must be formatted with gofumpt:

make fmt

Linting

Code must pass golangci-lint and staticcheck:

make lint

Testing

  • Write tests for new features
  • Maintain ≥70% code coverage
  • Use table-driven tests where appropriate
  • Mock external dependencies with cli.Fake

Commit Messages

Use conventional commit format:

type(scope): brief description

Longer explanation if needed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Types: feat, fix, docs, style, refactor, test, chore

Examples: - feat(screens): add disk usage screen - fix(logs): handle multi-line log entries correctly - docs(hotkeys): update keyboard reference

Testing

Unit Tests

make test

With Race Detector

make test-race

Coverage Report

make coverage

Generates coverage.out and prints total coverage. Must be ≥70%.

Test Patterns

Screen tests:

func TestContainersScreen(t *testing.T) {
    fake := &cli.Fake{
        ListContainersResp: []cli.Container{...},
    }
    clk := clock.NewFake(time.Now())
    p := theme.DefaultDark()

    screen := containers.New(fake, clk, p)
    // Test Update, View, Hotkeys, etc.
}

CLI tests:

func TestListContainers(t *testing.T) {
    client := cli.NewDefaultClient(cli.WithBinary("cat"))
    containers, err := client.ListContainers(context.Background(), false)
    // assertions...
}

Documentation

Generating Hotkeys Docs

After adding or changing hotkeys:

make docs-hotkeys

This regenerates docs/hotkeys.md.

Running mkdocs Locally

If you have mkdocs-material installed:

pip3 install --user mkdocs-material
mkdocs serve

Then visit http://localhost:8000.

If you don't have Python/pip, just ensure mkdocs.yml is valid YAML:

python3 -c 'import yaml; yaml.safe_load(open("mkdocs.yml"))'

The documentation site is automatically deployed to GitHub Pages on push to main.

VHS Tape Scripts

VHS (https://github.com/charmbracelet/vhs) is used to generate animated GIF demos.

Tape scripts are in tools/demos/*.tape. To render locally (if you have vhs installed):

cd tools/demos
vhs containers.tape

GIFs are automatically regenerated by the .github/workflows/demos.yml workflow.

Pull Request Process

  1. Fork the repository and create a feature branch:
git checkout -b feat/my-feature
  1. Make your changes following the coding standards

  2. Run the full CI suite:

make ci

Ensure it passes (tests, linting, coverage ≥70%).

  1. Commit with a conventional commit message

  2. Push to your fork and open a pull request

  3. Address review feedback if any

Filing Issues

When filing a bug report, include:

  • c9s version (c9s --version)
  • Operating system and terminal emulator
  • Apple Containers CLI version (container version)
  • Steps to reproduce
  • Expected vs actual behavior
  • Logs from ~/.local/state/c9s/errors/*.log if applicable

When filing a feature request, describe:

  • The problem you're trying to solve
  • Your proposed solution
  • Any alternative approaches you considered

Maintainer Notes

Release Process

Releases are managed via goreleaser and triggered automatically when pushing a version tag.

Prerequisites:

  1. HOMEBREW_TAP_TOKEN secret: Personal access token with repo scope for torosent/homebrew-c9s. Set in repository secrets. Required for goreleaser to push formula updates to the tap.

  2. Apple codesigning (optional): For notarized macOS binaries, set these secrets:

  3. MACOS_SIGN_P12: Base64-encoded Developer ID Application certificate (.p12)
  4. MACOS_SIGN_P12_PASSWORD: Certificate password
  5. MACOS_NOTARY_ISSUER_ID: App Store Connect issuer ID
  6. MACOS_NOTARY_KEY_ID: App Store Connect API key ID
  7. MACOS_NOTARY_KEY_FILE: Path to .p8 key file (or base64 content)

If these are not set, goreleaser will skip notarization and produce unsigned binaries (still functional).

To cut a release:

  1. Ensure make ci passes locally with coverage ≥70%
  2. Update CHANGELOG.md (move [Unreleased][X.Y.Z] - YYYY-MM-DD)
  3. Commit: chore(release): prepare vX.Y.Z
  4. Tag: git tag -a vX.Y.Z -m "c9s vX.Y.Z"
  5. Push: git push origin main --tags
  6. Monitor: gh run list --limit 3 — the release workflow should trigger
  7. Verify: gh release view vX.Y.Z — confirms binaries uploaded
  8. Verify tap: gh repo view torosent/homebrew-c9s — confirms formula updated

Snapshot testing (dry-run):

goreleaser --snapshot --clean

This builds all targets locally in dist/ without publishing. Useful for testing the config before a real release.

Known release-time issues

Homebrew tap push failure (HOMEBREW_TAP_TOKEN missing)

Symptom (in the release workflow):

homebrew formula: could not update "c9s.rb": PUT https://api.github.com/repos/torosent/homebrew-c9s/contents/c9s.rb: 403 Resource not accessible by integration

Cause: GITHUB_TOKEN is scoped to the source repo only and cannot push to torosent/homebrew-c9s. Fix: create a fine-grained personal access token with Contents: Read & write permission on torosent/homebrew-c9s, then add it as a repository secret HOMEBREW_TAP_TOKEN on torosent/c9s.

The release itself (binaries + checksums on the GitHub Release page) succeeds even when this push fails. Re-running the workflow after adding the secret will pick up where it left off.

GitHub Pages on private repos

pages.yml is manual-only (workflow_dispatch) because c9s is currently a private repository, and GitHub Pages on private repos requires GitHub Enterprise. To deploy the docs site:

  • Switch the repo to public, then re-add a push trigger to pages.yml, OR
  • Use Enterprise with Pages enabled, OR
  • Build the docs locally with mkdocs build and host elsewhere.

Run mkdocs serve for a live local preview.

Questions?

Thank you for contributing to c9s! 🎉