Contributing to Osmia¶
Thank you for your interest in contributing to Osmia! This document provides guidelines and information for contributors.
Code of Conduct¶
This project adheres to the Contributor Covenant Code of Conduct. By participating, you are expected to uphold this code.
Getting Started¶
- Fork the repository
- Clone your fork:
git clone https://github.com/YOUR_USERNAME/osmia.git - Install git hooks:
./hack/install-hooks.sh - Create a feature branch:
git checkout -b feat/my-feature - Make your changes
- Commit with a conventional commit message
- Push and open a pull request (hooks run lint and tests automatically)
Development Prerequisites¶
- Go 1.23+
- Docker (for container builds)
kind(for local Kubernetes clusters)kubectl(for cluster interaction)helm(for chart deployment)gofumpt(for code formatting)golangci-lint(for linting)buf(for protobuf linting and generation)
Install development dependencies:
Git Hooks¶
Install the recommended git hooks after cloning:
This installs a pre-push hook that runs golangci-lint and go test -race ./... before every push, catching lint errors and test failures locally before they reach CI.
Local Development Workflow¶
Osmia uses kind for local development and testing. The full workflow is automated via Make targets:
# Verify all prerequisites are installed
make check-prereqs
# Full setup: build the controller binary, build the controller image,
# create the kind cluster, and deploy the local-dev profile
make local-up
# Stream controller logs
make logs
# Run end-to-end smoke tests
make e2e-test
# Fast rebuild and redeploy (reuses existing cluster)
make local-redeploy
# Tear everything down
make local-down
The local-up target creates a two-node kind cluster (control-plane + worker),
builds the controller image with a dev tag, loads it into kind, and deploys
the Helm chart with local-dev overrides. The local-dev profile disables image
pulls and leader election, exposes the controller HTTP endpoint on
localhost:30080 for /healthz, /readyz, and /metrics,
and uses the noop ticketing backend so the controller starts without external
credentials. Use make live-up when you want to exercise real ticketing
backends and engine containers.
Incident.io MCP setup smoke test¶
docker/engine-claude-code/setup-claude.sh conditionally registers the
incident.io remote MCP server in the agent workspace's .mcp.json when
INCIDENT_IO_API_KEY is present. Because the script hardcodes the
container's absolute paths (/etc/claude-code/*.json, /workspace/.mcp.json),
it can only be exercised unmodified inside a container with that layout.
hack/test-incident-mcp-setup.sh runs it in a throwaway alpine container
via Docker and asserts the resulting mcp.json both with and without the
key set:
This is a local developer convenience, not a CI gate — it is not wired
into any GitHub Actions workflow. It requires Docker (with the daemon
running) and jq, and skips cleanly (exit 0) when either is unavailable.
Code Style¶
- Run
gofumpton all Go files before committing - Run
golangci-lint runand fix all warnings - Use British English in comments, documentation, and error messages
- Follow standard Go project layout conventions
- All exported types, functions, and methods must have doc comments
- Prefer table-driven tests with subtests
Commit Messages¶
We use Conventional Commits:
feat:— new featurefix:— bug fixdocs:— documentation onlytest:— adding or updating testsrefactor:— code change that neither fixes a bug nor adds a featurechore:— maintenance tasks
Pull Requests¶
- Keep PRs focused — one logical change per PR
- Update documentation as needed
- Add a changelog fragment (see below)
- Ensure CI passes before requesting review
Changelog fragments¶
Do not edit CHANGELOG.md. Add a new file under changelog.d/ instead,
named +<slug>.<category>.md, where the category is one of added,
changed, deprecated, removed, fixed or security:
The contents are the entry as it should read in the released notes, written
as prose without a leading -. See
changelog.d/README.md
for the full conventions.
One file per change means two PRs in flight never touch the same lines. Appending to a shared "Unreleased" section conflicted every time, and every merge then forced a rebase on everything behind it.
You do not need towncrier installed to add a fragment. It is run once per
release, by a maintainer.
Purely internal changes that no user or operator would notice need no fragment. A refactor that changes observable behaviour does.
Dev Releases (Edge Deployments)¶
Every push to main automatically publishes:
- Edge container images tagged
main(andsha-<hash>) toghcr.io - An edge Helm chart versioned
0.0.0-edgeto the same GitHub Pages repo
The 0.0.0-edge chart is always overwritten with the latest main commit. Its
appVersion is set to the full git SHA so you can trace exactly which commit is
running.
Weekly image rebuild¶
The image workflow also runs on a schedule, Mondays at 05:17 UTC, rebuilding
every image with pull: true so a fresh base layer is fetched. Nothing pins a
base-image digest, so this is the only way a security fix in node:22-slim or
the Go builder reaches the published images during a week with no commits.
The schedule turns itself off if the repository goes quiet. GitHub disables scheduled workflows in a public repository after 60 days with no repository activity. That is the one situation the rebuild is for, so it is worth knowing rather than discovering: a long gap between commits is both when a stale base image matters most and when the rebuild stops running.
There is no way to opt out of that behaviour. If the images look stale, check whether the workflow is disabled:
It can also be re-enabled from the Actions tab. Pushing any commit to the default branch resets the 60-day clock, but adding a keepalive commit purely to hold the schedule open is not worth the noise in the history.
Consuming the edge chart¶
helm repo add osmia https://unitaryai.github.io/osmia
helm repo update
# Inspect the dev chart (--devel required for pre-release versions)
helm show chart osmia/osmia --version 0.0.0-edge --devel
# Install/upgrade with the edge chart
helm upgrade --install osmia osmia/osmia \
--version 0.0.0-edge --devel \
--set image.tag=main \
--set image.pullPolicy=Always
Switching ArgoCD between dev and release mode¶
Dev mode — track main continuously (no semver release needed):
# kustomization.yaml
version: 0.0.0-edge
# values.yaml
image:
tag: "main"
pullPolicy: Always
engines:
claude-code:
image: ghcr.io/unitaryai/osmia/engine-claude-code:main
Release mode — pin to a specific semver release:
# kustomization.yaml
version: X.Y.Z
# values.yaml
image:
tag: "X.Y.Z"
pullPolicy: IfNotPresent
# remove engines.claude-code.image override (uses chart default)
Dev vs full release¶
| Situation | Use |
|---|---|
| Iterating on a fix — chart templates, RBAC, deployment config | Dev edge |
| Sharing a fix with external users or documenting in CHANGELOG | Full semver release |
| Testing a new feature before it's ready to announce | Dev edge |
| Stable, versioned deployment you can roll back to by number | Full semver release |
Manual dev build¶
You can trigger the edge image + chart build without pushing to main via the
GitHub Actions UI: go to Actions → Build and push edge images → Run workflow.
Releasing¶
Osmia uses git tags to trigger the release pipeline. The release.yaml workflow
builds container images and publishes the Helm chart to the GitHub Pages
repository (https://unitaryai.github.io/osmia).
Release checklist¶
- Ensure
mainis clean — all PRs merged, CI passing. - Decide the version — follow Semantic Versioning:
- Patch (
x.y.Z) for bug fixes - Minor (
x.Y.0) for new features (backward-compatible) - Major (
X.0.0) for breaking changes - Build
CHANGELOG.md— assemble the fragments inchangelog.d/into a new release section:
Pass the version without the v prefix, so the heading matches every
prior release. Preview first with --draft, which writes nothing. The
build consumes the fragments, so their deletion is part of the release
commit. Configuration is in towncrier.toml.
Entries are ordered alphabetically within each category, not by importance. Reorder by hand afterwards if a release has an entry that should lead.
On a machine configured against Unitary's private package index, uvx
will not find towncrier on that index. Prefix the command with
UV_NO_CONFIG=1 and add --default-index https://pypi.org/simple.
4. Bump charts/osmia/Chart.yaml — set both version and appVersion to
the new version (without the v prefix). The chart-releaser-action uses
this to decide whether to publish; if it matches an already-published version
the chart release is silently skipped.
5. Commit — chore: release vX.Y.Z
6. Tag — git tag vX.Y.Z
7. Push both — git push && git push origin vX.Y.Z
8. Verify the release pipeline — check GitHub Actions for:
- Container images built and pushed to ghcr.io
- Images signed with cosign
- Helm chart published to the gh-pages branch
9. Verify ArgoCD (if applicable) — confirm the new chart version is
available: helm repo update && helm search repo osmia
Common mistakes¶
- Forgetting
Chart.yaml— the most common failure. IfversioninChart.yamlisn't bumped, the chart-releaser sees the version already exists in thegh-pagesindex and skips publishing. Container images are built but the Helm chart is not released. - Tag without pushing main — the tag must point to a commit that is on
main(or at least pushed to the remote), otherwise the release workflow checks out stale code.
Plugin Contributions¶
If you're building a plugin, see the Writing a Plugin guide.
Licence¶
By contributing, you agree that your contributions will be licensed under the Apache License 2.0.