Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .githooks/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/sh
# Reject AI co-authorship / attribution in commit messages.
# Applies to everyone (humans and agents). Enable once per clone:
# git config core.hooksPath .githooks
msg_file="$1"
if grep -qiE 'co-authored-by|generated with .*(claude|code)|🤖|noreply@anthropic' "$msg_file"; then
echo "commit-msg: AI / co-author attribution is not allowed in this repo." >&2
echo "Remove Co-Authored-By and 'Generated with ...' lines, then commit again." >&2
exit 1
fi
exit 0
13 changes: 13 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh
# Basic "no slop" gate: staged Go must be gofmt-clean (generated code excluded).
# Enable once per clone: git config core.hooksPath .githooks
files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.go$' | grep -v '/internal/generated/')
[ -z "$files" ] && exit 0
bad=$(gofmt -l $files 2>/dev/null)
if [ -n "$bad" ]; then
echo "pre-commit: these Go files are not gofmt-formatted:" >&2
echo "$bad" >&2
echo "Run: gofmt -w <files>" >&2
exit 1
fi
exit 0
50 changes: 50 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!--
Conventional Commits drive releases. Keep commit messages — and, if you
squash-merge, the PR title — in `type(scope): summary` form.
No AI / co-author attribution (the commit-msg hook enforces this).
-->

## Summary

<!-- What does this PR do, and why? 1–3 sentences. -->

## Type of change

- [ ] `feat` — new feature
- [ ] `fix` — bug fix
- [ ] `refactor` — no behaviour change
- [ ] `docs`
- [ ] `ci` / `build` — pipeline or tooling
- [ ] `test`
- [ ] `chore`
- [ ] **Breaking change** (`!` / `BREAKING CHANGE:` footer)

## Changes

<!-- Bullet the notable changes, grouped by area: api / worker / ui / cli / ci / docs. -->

-

## Breaking changes

<!-- Describe the break and how consumers migrate. Delete this section if none. -->

## Test plan

- [ ] `cd api && go test ./... -count=1`
- [ ] `cd agent && go test ./... -count=1`
- [ ] `cd cli && go test ./... -count=1`
- [ ] `cd ui && npx tsc --noEmit` (if the UI changed)
- [ ] Manual check: <!-- what you clicked / ran -->

## Checklist

- [ ] Commits follow Conventional Commits
- [ ] No `Co-Authored-By` / AI attribution (enforced by the `commit-msg` hook)
- [ ] `gofmt`-clean and `go vet ./...` passes
- [ ] Docs updated (`docs/`, `README.md`) where relevant
- [ ] No secrets or `.env` files committed

## Deployment notes

<!-- New env vars, secrets, migrations, or host steps? Otherwise write "none". -->
54 changes: 54 additions & 0 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Docs

on:
push:
branches: [main]
paths:
- "docs/**"
- "mkdocs.yml"
- "deploy/Caddyfile"
pull_request:
paths:
- "docs/**"
- "mkdocs.yml"

# Don't let two docs deploys run at once on the host.
concurrency:
group: docs-deploy
cancel-in-progress: true

jobs:
build:
name: Build docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: mkdocs build
run: |
docker run --rm -v "$PWD:/docs" squidfunk/mkdocs-material:latest \
build -f /docs/mkdocs.yml -d /docs/_site

deploy:
name: Rebuild docs on homelab
needs: build
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Join Netbird VPN
# Managed Netbird cloud — default management URL, no self-hosted URL needed.
run: |
curl -fsSL https://pkgs.netbird.io/install.sh | sudo sh
sudo netbird up --setup-key "${{ secrets.NETBIRD_SETUP_KEY }}"
sudo netbird status
- name: Rebuild and serve docs over SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
password: ${{ secrets.SSH_PASSWORD }}
# The docs-build one-shot re-renders the markdown into the docs_site
# volume; the already-running Caddy serves it live (no restart needed).
script: |
cd ~/pulse
git fetch origin main && git pull --ff-only
docker compose -f deploy/docker-compose.homelab.yml run --rm docs-build
19 changes: 19 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Release Please

on:
push:
branches: [main]

permissions:
contents: write
pull-requests: write

jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
with:
token: ${{ secrets.PULSE_TOKEN }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
29 changes: 26 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,30 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Create GitHub release
uses: softprops/action-gh-release@v2

# Deploy runs only after the image is built and pushed, so the homelab never
# pulls a tag that does not exist yet. The GitHub release + changelog are
# created by release-please (see release-please.yml), not here.
deploy:
name: Deploy to homelab
needs: release
runs-on: ubuntu-latest
steps:
- name: Join Netbird VPN
# Managed Netbird cloud — uses the default management URL, no self-hosted URL needed.
run: |
curl -fsSL https://pkgs.netbird.io/install.sh | sudo sh
sudo netbird up --setup-key "${{ secrets.NETBIRD_SETUP_KEY }}"
sudo netbird status
- name: Pull and restart on host over SSH
uses: appleboy/ssh-action@v1
with:
generate_release_notes: true
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
password: ${{ secrets.SSH_PASSWORD }}
script: |
cd ~/pulse
git fetch --tags && git pull --ff-only
docker compose -f deploy/docker-compose.homelab.yml pull
docker compose -f deploy/docker-compose.homelab.yml up -d
docker image prune -f
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "0.0.0"
}
75 changes: 75 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Contributing to Pulse

Thanks for contributing. This guide covers the dev setup, the commit policy
(enforced by git hooks), and how to open a pull request.

## Development setup

Pulse is a Go workspace (`go.work`) with three modules — `api`, `agent`, `cli` —
plus a Next.js admin UI in `ui/`.

```sh
# Build the binary (embeds the admin UI) and run it on :8080
make run

# Run the test suites
cd api && go test ./... -count=1
cd agent && go test ./... -count=1
cd cli && go test ./... -count=1

# Regenerate DB access code after editing api/internal/db/queries or migrations
make sqlc # requires sqlc v1.31.1

# UI
cd ui && npm ci && npm run build
```

## Branching

Branch off `main` with a type prefix: `feat/...`, `fix/...`, `docs/...`,
`refactor/...`, `ci/...`. Do not commit directly to `main`.

## Commit policy (enforced)

These rules apply to everyone, including AI assistants:

- **Conventional Commits.** `type(scope): summary` (`feat`, `fix`, `refactor`,
`docs`, `build`, `ci`, `test`, `chore`). Mark breaking changes with a `!` after
the type or a `BREAKING CHANGE:` footer. This drives automatic versioning.
- **No AI / co-author attribution.** No `Co-Authored-By`, no "Generated with …"
lines, no bot trailers.
- **No force-pushes to `main`.**
- **No slop.** Code is `gofmt`-clean, passes `go vet ./...`, and passes the test
suite before it is committed.

### Enable the hooks (once per clone)

The repo ships hooks under `.githooks/` that enforce the rules above:

```sh
git config core.hooksPath .githooks
```

`commit-msg` rejects AI/co-author trailers; `pre-commit` rejects un-`gofmt`'d Go.
(`--no-verify` bypasses git hooks — don't.)

## Pull requests

1. Push your branch and open a PR against `main`. The
[PR template](.github/PULL_REQUEST_TEMPLATE.md) loads automatically — fill in
every section (summary, type, changes, breaking changes, test plan, checklist).
2. CI must be green (vet, gofmt, tests, UI build) before review.
3. Keep PRs focused; one logical change per commit even if the PR bundles several.
4. **Merge-commit** the PR (don't squash) so each Conventional Commit reaches
`main` — release-please relies on them for the changelog and the version bump.
If you must squash, the PR title itself must be a Conventional Commit and carry
any `BREAKING CHANGE:` footer.

## Releases & deployment

Versioning is automated with
[release-please](https://github.com/googleapis/release-please): merging
Conventional Commits to `main` maintains a release PR; merging that PR tags
`vX.Y.Z`, publishes a GitHub release + `CHANGELOG.md`, builds and pushes the
image to GHCR, and deploys to the homelab. See
[docs/deployment.md](docs/deployment.md) for the full flow and required secrets.
21 changes: 21 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# MIT License

Copyright (c) 2026 memetics19

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Pulse is an open-source, self-hosted status page and monitoring tool. It ships a

A live status page runs at [status.shreeda.xyz](https://status.shreeda.xyz). The full documentation is at [docs.shreeda.xyz](https://docs.shreeda.xyz).

<!--![Pulse status page](docs/assets/status-page.gif)-->

## Highlights

- **Single binary and SQLite.** One Go binary plus one SQLite file. The monitoring worker runs in-process.
Expand Down Expand Up @@ -48,4 +50,4 @@ Full documentation is at [docs.shreeda.xyz](https://docs.shreeda.xyz). The Markd

## License

Pulse is open source. See the repository for license details.
Pulse is released under the MIT License. See [LICENSE.md](LICENSE.md).
2 changes: 1 addition & 1 deletion agent/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ RUN go work sync && go mod download -modfile agent/go.mod
# Copy full source
COPY agent/ ./agent/
COPY api/ ./api/
COPY worker/ ./worker/


# Build the agent binary (CGO disabled → fully static)
RUN CGO_ENABLED=0 go build -o /pulse-agent ./agent/cmd/agent
Expand Down
3 changes: 2 additions & 1 deletion api/internal/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package db
import (
"database/sql"
"embed"
"errors"

"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/sqlite"
Expand Down Expand Up @@ -39,7 +40,7 @@ func runMigrations(conn *sql.DB) error {
if err != nil {
return err
}
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return err
}
return nil
Expand Down
5 changes: 5 additions & 0 deletions api/internal/db/sqlc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ sql:
out: "../generated"
emit_json_tags: true
emit_pointers_for_null_types: true
overrides:
- column: "monitors.is_active"
go_type: "bool"
- column: "infra_agents.is_active"
go_type: "bool"
4 changes: 2 additions & 2 deletions api/internal/generated/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions api/internal/generated/monitors.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion api/internal/handlers/ingest.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package handlers

import (
"encoding/json"
"log"
"net/http"
"strings"
"time"
Expand Down Expand Up @@ -55,7 +56,7 @@ func (h *Ingest) PostMetrics(w http.ResponseWriter, r *http.Request) {

if err := h.q.UpdateAgentLastSeen(r.Context(), agent.ID); err != nil {
// Non-fatal: log but continue
_ = err
log.Printf("failed to update agent last seen: %v", err)
}

w.WriteHeader(http.StatusNoContent)
Expand Down
2 changes: 1 addition & 1 deletion api/internal/handlers/monitors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func TestMonitorsCRUD(t *testing.T) {
"timeout_seconds": 10,
"degraded_threshold_ms": 500,
"down_threshold_ms": 2000,
"is_active": 1,
"is_active": true,
})
req := httptest.NewRequest(http.MethodPost, "/api/monitors", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
Expand Down
Loading
Loading