Thanks for your interest in CloudTunnels! This document covers the day-to-day mechanics of contributing. By participating you agree to abide by the Code of Conduct.
- macOS 13+
- Xcode 15+ (Swift 5.9)
- CLI tools:
gcloud,aws,kubectl(only needed for manual testing, not formake test)
git clone https://github.com/FournineCS/cloud-tunnels.git
cd cloud-tunnels
make test # must pass before you open a PR- Fork the repo and create a branch (
git checkout -b feat/my-feature) - Make your changes
- Run
make test— all tests must pass - For UI / bundle / resource changes, also run
make app && open build/CloudTunnels.appand confirm the app actually launches (see Release process for why this matters) - Open a pull request against
main
We follow Conventional Commits. The commit/PR title prefix drives changelog categorization and tooling:
| Prefix | Use for |
|---|---|
feat: |
User-visible new feature |
fix: |
User-visible bug fix |
refactor: |
Internal change with no behavior change |
docs: |
Documentation only |
test: |
Tests only |
ci: / build: |
CI workflow or build-system changes |
chore: |
Tooling, deps, housekeeping |
Examples:
feat(ssh): support multiple -L forwards per tunnelfix(branding): load PNGs via Bundle.main, not SwiftPM Bundle.moduleci(release): notarize per-arch zips before stapling
Keep the subject line under 72 characters. Use the body for the why — the diff already shows the what.
- Bugs and feature requests — open an issue at https://github.com/FournineCS/cloud-tunnels/issues. Include macOS version (
sw_vers), CPU arch (uname -m), CloudTunnels version (Aboutmenu orctun --version), and the relevant slice oflog stream --predicate 'subsystem == "com.fourninecloud.cloud-tunnels"' --info. - Security issues — please do not file a public issue. Email the maintainers privately first.
The system is built around a tagged union ProviderConfig enum paired with a TunnelLauncher protocol. Touch these eight sites in order:
Sources/TunnelCore/Models/ProviderConfig.swift— newTunnelProvidercase + config struct +Codable/kind/targetDescription/accountTagswitchesSources/TunnelCore/Models/Tunnel.swift—accountKey,validate(), andallLocalPorts()switchesSources/TunnelCore/<Name>Launcher.swift(new) — implementTunnelLauncherSources/TunnelCore/TunnelLauncher.swift— addLauncherFactory.launcher(for:)caseSources/CloudTunnels/Core/TunnelManager.swift— only if the provider needs lifecycle side effectsSources/CloudTunnels/UI/AddEditTunnelView.swift— per-provider@State, form branch,<name>FieldsViewBuilder,save()caseSources/CloudTunnels/UI/MenuBarView.swift— tab button + three exhaustive switchesTests/CloudTunnelsTests/<Name>LauncherTests.swift— mirrorSSHLauncherTestsas template
See CLAUDE.md for full details on each site.
Tools are registered in Sources/CloudTunnels/Tools/Tool.swift (ToolRegistry.all) and dispatched in Sources/CloudTunnels/Tools/ToolsRootView.swift. Pattern: pure helper (no SwiftUI, testable) + thin SwiftUI view + tests. Wrap the view body in ScrollView to prevent popover footer overlap.
- No comments unless the why is non-obvious — well-named identifiers carry the what
- Immutable patterns: create new values instead of mutating in place
- Functions under 50 lines, files under 800 lines
- New providers and tools need launcher argument tests; pure argv builders must be unit-tested without shelling out
Ad-hoc signing (SIGN_IDENTITY=-) is the default and is fine for all changes that don't touch privacy-sensitive frameworks (Calendar, Contacts, Photos, etc.). For those, you'll need a real Apple Developer identity — see the SIGN_IDENTITY note in the Makefile.
make test runs the full XCTest suite (~365 tests). No real kubectl, gcloud, or aws invocations happen in tests. Use swift test --filter <ClassName> to run a single class.
Pure argv builders, config parsers, and reducer-style helpers are the primary unit-test targets — anything that shells out should be factored into a pure static function and tested without spawning a process.
Releases are tag-driven. Pushing a tag matching v* triggers
.github/workflows/release.yml, which builds universal + per-arch zips,
signs with the FOURNINE Developer ID, notarizes via notarytool, staples,
and publishes a GitHub Release with SHA256SUMS.txt.
Before tagging:
make test— full suite greenmake app && open build/CloudTunnels.app— confirm the app actually launches and the menu-bar icon appears. A green build does not guarantee a working.app; SwiftPM resource bundles, code-signing entitlements, and TCC requirements only surface at launch. The v1.0.1 release shipped a fatal crash because this step was skipped.- Update relevant docs (
README.md,CLAUDE.md) if behavior changed. - Tag:
git tag -a vX.Y.Z -m "Release vX.Y.Z" && git push public vX.Y.Z - Watch the run:
gh run watch <run-id> --repo FournineCS/cloud-tunnels --exit-status
If a release ships with a runtime bug, do not retag the same version —
cut a patch release (vX.Y.(Z+1)) so anyone who already downloaded the
broken build can tell what they have.
Versioning is SemVer:
MAJOR— incompatible config schema or CLI changesMINOR— new provider, new tool, new user-visible featurePATCH— bug fixes, doc updates, build-system fixes