Compare commits
4
Commits
5471054adb
..
next
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eda8e80844 | ||
|
|
e584718eb5 | ||
|
|
a8d226942e | ||
|
|
6df1fc7875 |
+1
-3
@@ -72,10 +72,8 @@
|
||||
**/*.sublime-*
|
||||
|
||||
# This repository's own host-built artifacts: the binary `make build`
|
||||
# writes, and the Alpine.js file `make assets` extracts from 3p/ (the
|
||||
# build extracts its own).
|
||||
# writes.
|
||||
/bin
|
||||
/static/js/alpine.min.js
|
||||
|
||||
# SQLite databases, which hold the session key and webhook payloads, at
|
||||
# any depth.
|
||||
|
||||
@@ -77,7 +77,3 @@ data/
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# Alpine.js, extracted by `make assets` from its tarball in 3p/, which is
|
||||
# what is committed.
|
||||
/static/js/alpine.min.js
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
yarn.lock
|
||||
static/js/alpine.min.js
|
||||
|
||||
Binary file not shown.
+2
-11
@@ -92,10 +92,6 @@ COPY . .
|
||||
# fmt-check` is the markdown-check stage above.
|
||||
RUN if [ -n "$(gofmt -s -l .)" ]; then echo "gofmt needed on:"; gofmt -s -l .; exit 1; fi
|
||||
|
||||
# static/static.go embeds the Alpine.js file this extracts from 3p/; without
|
||||
# it the static package does not compile and cannot be linted.
|
||||
RUN script/assets
|
||||
|
||||
# The golangci-lint steps run with --network=none. `golangci-lint config
|
||||
# verify` is documented as fetching its JSON schema over HTTPS; this pinned
|
||||
# image resolves the schema without any network, and --network=none enforces
|
||||
@@ -130,9 +126,6 @@ RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
# static/static.go embeds the Alpine.js file this extracts from 3p/.
|
||||
RUN script/assets
|
||||
|
||||
# -timeout applies to each package on its own, so 90s has only to clear the
|
||||
# slowest one. -p 4 -parallel 8 keep the run under 2 GB of memory: at most
|
||||
# four test binaries build or run at once, each with at most eight parallel
|
||||
@@ -211,10 +204,8 @@ RUN version="$(make version VERSION="$VERSION")"; \
|
||||
esac; \
|
||||
fi
|
||||
|
||||
# Builds through the Makefile's build target, which runs script/assets
|
||||
# (Alpine.js, extracted from its tarball in 3p/) first. The target builds
|
||||
# with cgo off, so the binary is statically linked and runs on the Alpine
|
||||
# runtime image below as it is.
|
||||
# The Makefile's build target builds with cgo off, so the binary is
|
||||
# statically linked and runs on the Alpine runtime image below.
|
||||
RUN make build VERSION="$VERSION"
|
||||
|
||||
# Runtime stage
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ COPY . .
|
||||
# The test binary embeds the templates and static files, so the browser
|
||||
# stage needs nothing else. It is built with cgo off and -p 4 keeps the
|
||||
# compile's memory down, both as in the test phase of Dockerfile.
|
||||
RUN make assets && CGO_ENABLED=0 go test -c -p 4 -tags browser -o /browser.test ./internal/server
|
||||
RUN CGO_ENABLED=0 go test -c -p 4 -tags browser -o /browser.test ./internal/server
|
||||
|
||||
# chromedp/headless-shell:151.0.7922.109 (Debian trixie), 2026-08-11. The
|
||||
# browser is on PATH as headless-shell, where the test's browser library
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: bootstrap setup assets test test-browser lint fmt fmt-check check build run dev deps docker clean hooks css css-check version
|
||||
.PHONY: bootstrap setup test test-browser lint fmt fmt-check check build run dev deps docker clean hooks css css-check version
|
||||
|
||||
# Default target
|
||||
.DEFAULT_GOAL := check
|
||||
@@ -22,9 +22,6 @@ bootstrap:
|
||||
setup:
|
||||
@script/setup
|
||||
|
||||
assets:
|
||||
@script/assets
|
||||
|
||||
test:
|
||||
@script/test
|
||||
|
||||
@@ -47,13 +44,13 @@ check:
|
||||
# REPO_POLICIES.md does: the binary carries no build paths, symbol table or
|
||||
# debug information, and on Linux, the image's included, it is statically
|
||||
# linked.
|
||||
build: assets
|
||||
build:
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags '-s -w -X main.version=$(VERSION)' -o bin/webhooker ./cmd/webhooker
|
||||
|
||||
run: build
|
||||
./bin/webhooker
|
||||
|
||||
dev: assets
|
||||
dev:
|
||||
go run ./cmd/webhooker
|
||||
|
||||
deps:
|
||||
|
||||
@@ -1,72 +1,77 @@
|
||||
# Workflow
|
||||
|
||||
One issue per unit of work, one branch and one PR per issue:
|
||||
One issue per unit of work, and one branch and one PR per issue:
|
||||
|
||||
- ensure a tracked issue exists with a definition of done
|
||||
- branch from `next` (never from `main`)
|
||||
- do the work; open a PR based on `next` (never on `main`)
|
||||
- pass an independent review, then the manager squash-merges into `next`
|
||||
- push; nothing stays local-only
|
||||
- make sure a tracked issue exists with a definition of done
|
||||
- branch from `next`, never from `main`
|
||||
- do the work and push the branch
|
||||
- open a PR with base `next`, never `main`
|
||||
- pass an independent review
|
||||
- the repository manager squash-merges it into `next`
|
||||
|
||||
`next` is the branch for the next milestone and must stay green and mergeable to
|
||||
`main` without notice. One `next` -> `main` PR accumulates the milestone;
|
||||
releases are cut from `main` separately.
|
||||
The repository manager is the role that merges each issue's PR into `next`.
|
||||
Nothing stays local-only: every commit is pushed.
|
||||
|
||||
The tracker (https://git.eeqj.de/sneak/webhooker/issues) is the record of what
|
||||
is done and what is under way. This file keeps no list of either.
|
||||
`next` is the branch for the next milestone. It must stay green and mergeable
|
||||
into `main` at all times. A milestone's work reaches `main` through one PR from
|
||||
`next`. The owner, @sneak, merges that PR. Cutting a release from `main` is a
|
||||
separate step after that.
|
||||
|
||||
The tracker (https://git.eeqj.de/sneak/webhooker/issues) records what is done
|
||||
and what is under way. This file keeps no list of either.
|
||||
|
||||
# Status
|
||||
|
||||
What is still open for 1.0.0 is in its milestone
|
||||
(https://git.eeqj.de/sneak/webhooker/milestone/9). `v1.0.0` is not tagged, and
|
||||
only @sneak tags it.
|
||||
(https://git.eeqj.de/sneak/webhooker/milestone/9). `v1.0.0` is not tagged. Only
|
||||
@sneak tags it.
|
||||
|
||||
Delivery is at-least-once by design, not by accident: a send whose result row
|
||||
does not land is attempted again, so a receiver can see a duplicate. That is
|
||||
deliberate — the alternative is a silent lost delivery — and the README says so
|
||||
under Rationale. It is not a defect to re-file.
|
||||
Delivery is at least once by design. When webhooker cannot record the outcome of
|
||||
a delivery, it sends that delivery again. The target can then receive the same
|
||||
event twice. The alternative is a lost delivery that nothing reports. The README
|
||||
says so. Such a duplicate is not a defect to re-file.
|
||||
|
||||
# Next Step
|
||||
|
||||
@sneak runs webhooker in limited production, then tags `v1.0.0`. Merging `next`
|
||||
into `main` is a separate act from tagging and waits on neither of those: `next`
|
||||
is kept mergeable at all times, which is the point of the branch.
|
||||
into `main` is a separate act. It waits on neither the production run nor the
|
||||
tag.
|
||||
|
||||
# Future Steps
|
||||
|
||||
- Per-webhook rate limits, set in the web UI and enforced in the receiver
|
||||
handler, on top of the receiver's existing limits, which `RECEIVER_RATE_LIMIT`
|
||||
sets for the whole server (see Rate Limiting in the README)
|
||||
- API key authentication for programmatic access (the `APIKey` model exists, but
|
||||
nothing creates a key and no middleware checks one)
|
||||
- REST API v1
|
||||
- CRUD for webhooks, entrypoints, targets
|
||||
- Per-webhook rate limits, set in the web UI and enforced at the entrypoint.
|
||||
Today `RECEIVER_RATE_LIMIT` is the only rate-limit setting. It is how many
|
||||
requests per minute one client address may send to one entrypoint. The same
|
||||
address is also capped at ten times that across all entrypoints together.
|
||||
- API key authentication for programmatic access. The `APIKey` model and its
|
||||
`api_keys` table exist. Nothing creates a key and nothing checks one.
|
||||
- REST API v1. `/api/v1` is mounted with no routes today.
|
||||
- CRUD for webhooks, entrypoints and targets
|
||||
- event viewing and filtering endpoints
|
||||
- event redelivery endpoint
|
||||
- OpenAPI specification
|
||||
- Analytics in the web UI: response times summarised per webhook or per target,
|
||||
and figures for all webhooks together. The webhook page already shows event,
|
||||
delivery and failure counts and failure percentages, the target list counts
|
||||
each target's deliveries, the event log and each event's page show every
|
||||
attempt's duration, and `/metrics` carries server-wide counts and delivery
|
||||
durations
|
||||
- A remember-me option at login. Every session already survives a browser
|
||||
restart: it lasts up to 7 days, and ends sooner after `SESSION_IDLE_TIMEOUT`
|
||||
(default 24 hours) without activity
|
||||
- Password reset flow for a forgotten password over the web. The authenticated
|
||||
password _change_ flow already landed, and a lost password is recoverable from
|
||||
the console with `webhooker resetpw`
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/208)
|
||||
and figures for all webhooks together. Today the webhook page shows event,
|
||||
delivery and failure counts and failure percentages. The target list counts
|
||||
each target's deliveries. The event log shows every attempt's duration.
|
||||
`/metrics` carries server-wide counts and delivery durations.
|
||||
- A remember-me option at login. Today every session survives a browser restart.
|
||||
It lasts up to 7 days. It ends sooner after `SESSION_IDLE_TIMEOUT` (default 24
|
||||
hours) without use.
|
||||
- Password reset over the web for a forgotten password. Today a signed-in user
|
||||
can change their password. A lost password is reset from the command line with
|
||||
`webhooker resetpw` (https://git.eeqj.de/sneak/webhooker/issues/208).
|
||||
- Later, nice to have
|
||||
- email delivery target type
|
||||
- SNS and S3 delivery targets
|
||||
- data transformations (e.g. a configurable Slack message format; the Slack
|
||||
target sends one fixed format today)
|
||||
- JSONL file delivery with periodic S3 upload (the `database` target already
|
||||
archives events to SQLite files, rotated hourly, daily or monthly if set,
|
||||
and the webhook page offers them as one gzipped JSON download)
|
||||
- data transformations, such as a configurable Slack message format. The
|
||||
Slack target sends one fixed format today.
|
||||
- JSONL file delivery with periodic S3 upload. Today an archive target
|
||||
writes events to SQLite files. An archive target can rotate those files
|
||||
hourly, daily or monthly. The webhook page offers them as one gzipped JSON
|
||||
download.
|
||||
- webhook event search, and filtering beyond the event log's failed and
|
||||
pending views
|
||||
- multi-user with role-based access control (each webhook already belongs to
|
||||
one account, but there is only the account made at first boot and no way
|
||||
to add another)
|
||||
- multi-user with role-based access control. Today each webhook belongs to
|
||||
one account. Only the account made at first boot exists. There is no way
|
||||
to add another.
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
// REPO_POLICIES.md states that a linter can check: const for everything,
|
||||
// let only for a variable that is reassigned, never var.
|
||||
export default [
|
||||
// Alpine.js, extracted from 3p/ by make assets; not ours to lint.
|
||||
// Alpine.js, committed as published; not ours to lint.
|
||||
{ ignores: ["static/js/alpine.min.js"] },
|
||||
{
|
||||
// The pages load static/js/app.js as a classic script, not a module.
|
||||
|
||||
@@ -13,9 +13,6 @@ import (
|
||||
|
||||
// TestBaseTemplateScriptsAreServed walks every /s/ script the base
|
||||
// template loads on each page and fetches it through the real router.
|
||||
// Alpine.js is extracted from its tarball in 3p/ at build time, so the
|
||||
// file is not in the tree: this is the check that the page still gets
|
||||
// the JavaScript it asks for.
|
||||
func TestBaseTemplateScriptsAreServed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/sh
|
||||
# script/assets: extract Alpine.js from its npm package tarball, committed
|
||||
# in 3p/, to static/js/alpine.min.js, where go:embed reads it. The package
|
||||
# is @alpinejs/csp, Alpine's build for pages whose Content-Security-Policy
|
||||
# forbids eval. The extracted file is not committed. make build, make dev
|
||||
# and the Dockerfile's lint, test and build stages run this first.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
tar -xzOf 3p/alpinejs-csp-3.14.9.tgz package/dist/cdn.min.js \
|
||||
>static/js/alpine.min.js
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Vendored
+11
File diff suppressed because one or more lines are too long
+5
-3
@@ -6,9 +6,11 @@ import (
|
||||
)
|
||||
|
||||
// Static holds the CSS and JavaScript files the web UI's pages load. They
|
||||
// are named one by one so that a missing js/alpine.min.js, which make
|
||||
// assets extracts and git does not track, fails the build instead of
|
||||
// leaving the pages without Alpine.js.
|
||||
// are named one by one, so a missing file fails the build instead of
|
||||
// leaving the pages without it.
|
||||
//
|
||||
// js/alpine.min.js is Alpine.js, committed unchanged. It is
|
||||
// package/dist/cdn.min.js from the npm package @alpinejs/csp 3.14.9.
|
||||
//
|
||||
//go:embed css/tailwind.css css/style.css js/app.js js/alpine.min.js
|
||||
var Static embed.FS
|
||||
|
||||
Reference in New Issue
Block a user