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
- Clone the repository
git clone https://github.com/torosent/c9s.git
cd c9s
- Install development tools
make install-tools
This installs:
- gofumpt (code formatter)
- staticcheck (static analysis)
- golangci-lint (linter suite)
- Build the project
make build
The binary will be in bin/c9s.
- 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
- Fork the repository and create a feature branch:
git checkout -b feat/my-feature
-
Make your changes following the coding standards
-
Run the full CI suite:
make ci
Ensure it passes (tests, linting, coverage ≥70%).
-
Commit with a conventional commit message
-
Push to your fork and open a pull request
-
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/*.logif 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:
-
HOMEBREW_TAP_TOKEN secret: Personal access token with
reposcope fortorosent/homebrew-c9s. Set in repository secrets. Required for goreleaser to push formula updates to the tap. -
Apple codesigning (optional): For notarized macOS binaries, set these secrets:
MACOS_SIGN_P12: Base64-encoded Developer ID Application certificate (.p12)MACOS_SIGN_P12_PASSWORD: Certificate passwordMACOS_NOTARY_ISSUER_ID: App Store Connect issuer IDMACOS_NOTARY_KEY_ID: App Store Connect API key IDMACOS_NOTARY_KEY_FILE: Path to.p8key file (or base64 content)
If these are not set, goreleaser will skip notarization and produce unsigned binaries (still functional).
To cut a release:
- Ensure
make cipasses locally with coverage ≥70% - Update
CHANGELOG.md(move[Unreleased]→[X.Y.Z] - YYYY-MM-DD) - Commit:
chore(release): prepare vX.Y.Z - Tag:
git tag -a vX.Y.Z -m "c9s vX.Y.Z" - Push:
git push origin main --tags - Monitor:
gh run list --limit 3— the release workflow should trigger - Verify:
gh release view vX.Y.Z— confirms binaries uploaded - 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
pushtrigger topages.yml, OR - Use Enterprise with Pages enabled, OR
- Build the docs locally with
mkdocs buildand host elsewhere.
Run mkdocs serve for a live local preview.
Questions?
- Open a GitHub Discussion
- File an Issue
Thank you for contributing to c9s! 🎉