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
45 changes: 45 additions & 0 deletions .github/workflows/changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Changelog

on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]

# Least privilege: the gate only reads the tree and the PR labels on the event.
permissions:
contents: read
pull-requests: read

concurrency:
group: changelog-${{ github.ref }}
cancel-in-progress: true

# Actions are pinned to commit SHAs (with the version in a comment) so a
# moved or compromised tag can't change what runs here.

jobs:
check:
name: Require CHANGELOG.md
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false

- uses: pnpm/action-setup@d9184bf108216479bc5a137cc391f4d7b14c870b # v6.1.0

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .nvmrc
cache: pnpm

- uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4.0.0

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Require a changelog entry unless skip-changelog
env:
SKIP_CHANGELOG: ${{ contains(github.event.pull_request.labels.*.name, 'skip-changelog') }}
run: just check-changelog --base ${{ github.event.pull_request.base.sha }}
60 changes: 60 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Release

# On each push to main (and when someone runs this by hand), cut a version
# from CHANGELOG [Unreleased] and bump package.json. Added → minor;
# Fixed/Changed → patch. Never major before 1.0. No-ops when Unreleased is empty.
on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: write

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

# Actions are pinned to commit SHAs (with the version in a comment) so a
# moved or compromised tag can't change what runs here.

jobs:
bump:
name: Version bump
if: ${{ github.event_name == 'workflow_dispatch' || !startsWith(github.event.head_commit.message, 'Release v') }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- uses: pnpm/action-setup@d9184bf108216479bc5a137cc391f4d7b14c870b # v6.1.0

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .nvmrc
cache: pnpm

- uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4.0.0

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Cut Unreleased into a version
env:
GIT_AUTHOR_NAME: github-actions[bot]
GIT_AUTHOR_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
GIT_COMMITTER_NAME: github-actions[bot]
GIT_COMMITTER_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
run: |
set -euo pipefail
before="$(git rev-parse HEAD)"
just bump --commit
after="$(git rev-parse HEAD)"
if [ "${before}" = "${after}" ]; then
echo "No version bump."
exit 0
fi
git push origin HEAD:${{ github.ref_name }}
git push origin --tags
8 changes: 6 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ just check # lint + typecheck + tests with coverage thresholds; must
just test # tests only
just fmt # auto-format (Biome, and Terraform if `terraform` is on PATH)
just validate-config # validate the access list files
just check-changelog # fail if this branch does not change CHANGELOG.md vs origin/main
just bump # cut [Unreleased] into a dated version and bump package.json
just tf-validate # terraform fmt -check and validate bootstrap + envs/dev
just tf-plan dev # plan the dev stack (needs Azure login; does not apply)
just tf-apply dev # apply the dev stack — only when a maintainer says so
Expand All @@ -55,8 +57,9 @@ src/
config/ # *.example.yaml committed; real admins.yaml / members.yaml gitignored
data/ # runtime state; gitignored. Safe to delete EXCEPT schedules.yaml (the scheduled posts)
content/info/ # markdown for /info topics, one file per topic; PUBLIC, reviewed
scripts/ # register-commands, validate-config
docs/ # architecture, identity, infra overview, ADRs
scripts/ # register-commands, validate-config, changelog check/bump
docs/ # architecture, identity, infra overview, ADRs, runbook
CHANGELOG.md # Keep a Changelog 1.1.0; most PRs add an Unreleased entry
infra/ # Terraform. bootstrap/ is #8; modules/pixel and envs/dev are #9 (do not apply until asked)
```

Expand Down Expand Up @@ -99,6 +102,7 @@ infra/ # Terraform. bootstrap/ is #8; modules/pixel and envs/de
- **Coverage:** `vitest.config.ts` sets an 80% overall floor, with strict floors (about 98%) for `core/`, `services/`, the Discord handlers and the observability helpers. Keep platform client code thin: put decisions in plain functions (see `adapters/discord/handlers.ts`) so they can be tested. Never lower a threshold to make a change pass. Add tests instead, or explain why in the PR.
- **Style:** Biome. Don't hand-format.
- **Runbook:** [`docs/runbook.md`](docs/runbook.md) is how volunteers run Pixel. A change to how Pixel is built, configured, deployed, monitored or recovered (an environment variable, a file it reads or writes, a command to run after a deploy, infrastructure) must update the matching section of the runbook in the same PR. The `🚧 Azure` markers are filled in as the infrastructure lands.
- **Changelog:** [`CHANGELOG.md`](CHANGELOG.md) follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/). Every user-facing or infra change (commands, behaviour, config, deploy, monitoring, operator docs that change how volunteers run Pixel) must add a short entry under `## [Unreleased]` in the matching category (`Added`, `Changed`, `Fixed`, …). Do not invent a version date; `just bump` (and the Release workflow on `main`) writes those when Unreleased is cut. Pure chores (typos, CI-only, no user or operator impact) may apply or request the `skip-changelog` label instead of an entry. Until 1.0, bumps are minor (`Added`) or patch (`Fixed`/`Changed`/other types) only — never major.
- **Dependencies:** keep them minimal. Explain why in the PR when you add one.

## Things to avoid
Expand Down
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Until 1.0.0, releases stay on 0.x: `Added` bumps minor, `Fixed`/`Changed` (and the other Keep a Changelog types) bump patch. The major version is never bumped automatically.

## [Unreleased]

### Added

- Keep a Changelog, automatic `package.json` version bumps from Unreleased notes, and a CI gate that requires a changelog entry unless the pull request is labeled `skip-changelog`.

## [0.1.0] - 2026-10-09

Phase 1 as shipped on `main` through 2026-10-09. Item dates are merge or commit dates from git.

### Added

- Phase 1 core, access lists and Discord adapter: `/help`, `/ping`, `/whoami`, `/admin` (2026-10-03, #1).
- `/status` from SpaceAPI and open/closed announcements (2026-10-03, #20, #21).
- `/events` from the guild's scheduled events (2026-10-03, #22).
- `/info` answers from reviewed markdown topics (2026-10-03, #23).
- Admin member and capability commands, plus Discord role mirroring (2026-10-03 to 2026-10-04, #30, #32, #33, #34, #35).
- Autocomplete for command options (2026-10-04, #44).
- `/feedback` to the maintainers via Sentry (2026-10-04, #46).
- Home Assistant: device allow-list, `/ha list` / `status` / `set` / `open`, capabilities, and an admin door switch (2026-10-04 to 2026-10-08, #45, #49, #50, #51, #52, #53, #54, #57, #64).
- `/schedule` for channel messages and polls (2026-10-07, #62).
- Online/offline posts for Pixel itself, and a Sentry cron check-in (2026-10-07, #58, #59).

### Changed

- Structured logs to the console, a rotating file and Sentry Logs (2026-10-04, #55).
- Operations runbook for volunteers (2026-10-05, #56).
- Azure Terraform bootstrap, Container Apps `dev` stack, and GHCR CD that keeps `main` off prod (2026-10-08, #65, #66, #67, #68).
- Sentry traces, profiles, sessions and runtime metrics (2026-10-08, #69).
- Infra overview with Azure and GitHub diagrams (2026-10-09, #75).

[Unreleased]: https://github.com/pixelbar/pixel/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/pixelbar/pixel/releases/tag/v0.1.0
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ Run `just` to list every recipe.
| `just build` / `just start` | Compile to `dist/` and run the build |
| `just fmt` | Auto-format |
| `just validate-config` | Validate the access list files |
| `just check-changelog` | Fail if this branch does not change `CHANGELOG.md` vs `origin/main` |
| `just bump` | Cut `[Unreleased]` into a dated version and bump `package.json` |
| `just register` | Register slash commands with Discord |
| `just command-access` | Show admin-tier commands to the admins in `admins.yaml` (see [`docs/discord-command-visibility.md`](docs/discord-command-visibility.md)) |
| `just docker-build` | Build the container image |
Expand All @@ -147,7 +149,7 @@ Run `just` to list every recipe.

## CI

Every pull request and push to `main` runs [CI](.github/workflows/ci.yml). It runs `just check` (lint, type-check, tests with coverage thresholds) and the production build, and checks that the Docker image builds. [Terraform](.github/workflows/terraform.yml) validates bootstrap and `infra/envs/dev` only when `infra/`, that workflow, or the `justfile` change, then plans `dev` (fails closed without Environment `dev` `ARM_*`). [CD](.github/workflows/cd.yml) on `main` publishes `ghcr.io/pixelbar/pixel:<sha>` and uploads Sentry source maps (fails closed without org/token). It does **not** deploy prod. Azure `dev` is `just deploy-dev` from a local build. No Terraform apply on merge.
Every pull request and push to `main` runs [CI](.github/workflows/ci.yml). It runs `just check` (lint, type-check, tests with coverage thresholds) and the production build, and checks that the Docker image builds. Pull requests also run a [changelog](.github/workflows/changelog.yml) job that fails unless `CHANGELOG.md` changed, or the PR has the `skip-changelog` label. [Terraform](.github/workflows/terraform.yml) validates bootstrap and `infra/envs/dev` only when `infra/`, that workflow, or the `justfile` change, then plans `dev` (fails closed without Environment `dev` `ARM_*`). [CD](.github/workflows/cd.yml) on `main` publishes `ghcr.io/pixelbar/pixel:<sha>` and uploads Sentry source maps (fails closed without org/token). It does **not** deploy prod. A [Release](.github/workflows/release.yml) job on `main` cuts `CHANGELOG.md` Unreleased into a dated version and bumps `package.json` when there are notes. Azure `dev` is `just deploy-dev` from a local build. No Terraform apply on merge.

## Deployment

Expand All @@ -163,7 +165,7 @@ Pixel decides who gets member-level access, so its security matters. If you find

## Contributing

Pixel is a Pixelbar community project. If you use an AI coding agent, point it at [`AGENTS.md`](AGENTS.md).
Pixel is a Pixelbar community project. If you use an AI coding agent, point it at [`AGENTS.md`](AGENTS.md). User-facing and operator-facing changes belong in [`CHANGELOG.md`](CHANGELOG.md).

## License

Expand Down
26 changes: 24 additions & 2 deletions docs/runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Before you change anything on **prod**, read "Rules of thumb" below.
- [Azure Container App (`dev`)](#azure-container-app-dev)
- [Secrets](#secrets)
- [Deploys](#deploys)
- [Changelog and versions](#changelog-and-versions)
- [Incidents](#incidents)
- [Sentry](#sentry)
- [Privacy requests](#privacy-requests)
Expand Down Expand Up @@ -59,8 +60,10 @@ Before you change anything on **prod**, read "Rules of thumb" below.
| Check the config files without starting Pixel | `just validate-config` |
| Make Discord pick up new or changed commands | `just register` (after a deploy that changes commands) |
| Look up a person | `/admin level get user:` (admins only) |
| See what changed between versions | [`CHANGELOG.md`](../CHANGELOG.md) |
| Cut a package version from Unreleased notes | `just bump` (or wait for the Release workflow on `main`) |

Useful `just` recipes: `just dev`, `just check`, `just validate-config`, `just register`, `just sentry-release`, `just command-access`, `just docker-build`, `just deploy-dev`, `just tf-validate`. Run `just` to list them all.
Useful `just` recipes: `just dev`, `just check`, `just validate-config`, `just check-changelog`, `just bump`, `just register`, `just sentry-release`, `just command-access`, `just docker-build`, `just deploy-dev`, `just tf-validate`. Run `just` to list them all.

## Running Pixel today

Expand Down Expand Up @@ -259,6 +262,25 @@ Every secret lives in the secret store for its environment (a local `.env` for d
- If `pixel-dev` is up, do not start the Mini bot (`just dev`) on the same Pixel Dev token.
- **Sentry releases:** `just sentry-release` / the CD job. Needs secret `SENTRY_AUTH_TOKEN` and variables `SENTRY_ORG`, `SENTRY_PROJECT`. The CD job **fails closed** if any are missing. Local `just sentry-release` still skips so `just deploy-dev` can roll without them.

## Changelog and versions

User-facing and operator-facing changes are recorded in [`CHANGELOG.md`](../CHANGELOG.md), in [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/) format. The version people see in `/ping` and the online post is `package.json` (plus the git SHA the image was built from).

**On every pull request** that users or operators would notice (commands, behaviour, config, deploy, monitoring, this runbook): add a short bullet under `## [Unreleased]` in the right category (`Added`, `Changed`, `Fixed`, …). CI fails the PR if `CHANGELOG.md` is untouched. Pure chores (typos, CI-only, no user or operator impact) may use the `skip-changelog` label instead; adding or removing that label re-runs the check. Create the label on the repo once if it does not exist yet.

**To cut a version** (move Unreleased into a dated section and bump `package.json`):

```sh
just bump # writes the files; you commit
just bump --commit # also commits and tags vX.Y.Z
```

`Added` is a minor bump (`0.1.0` → `0.2.0`). `Fixed`, `Changed` and the other Keep a Changelog types are a patch (`0.1.0` → `0.1.1`). Until 1.0 the major version is never bumped this way. Do not invent the version date; the recipe uses today's UTC date.

The **Release** workflow (`.github/workflows/release.yml`) runs the same `just bump --commit` on each push to `main` when Unreleased has notes, then pushes the commit and tag (`vX.Y.Z`). If the previous version was never tagged, it also tags that parent commit so the changelog compare links work. That commit is created with `GITHUB_TOKEN`, so it does **not** start another CD run. The new `package.json` version is what the next publish (`just deploy-dev`, or the next merge that CD builds) will show. `PIXEL_VERSION` in the image remains the git SHA.

Do not run `just bump` on a feature branch unless you are deliberately cutting a release there.

## Incidents

**The bot is offline or doesn't answer**
Expand Down Expand Up @@ -360,7 +382,7 @@ If `members.yaml` is lost and there's no backup, Pixel won't start (it fails clo

## Keeping this up to date

- **Change infrastructure, change this file.** A pull request that changes how Pixel is built, configured, deployed or monitored must update the matching section here (and `architecture.md`). This is noted in `AGENTS.md`.
- **Change infrastructure, change this file.** A pull request that changes how Pixel is built, configured, deployed or monitored must update the matching section here (and `architecture.md`). This is noted in `AGENTS.md`. The same change also needs a `CHANGELOG.md` Unreleased entry unless it is a pure chore (`skip-changelog`).
- **When Terraform lands:** search for `🚧` and replace each one with real steps, keeping the "Today" steps only where they still apply. Add the real environment names, links and the on-call contact to "Who and what".
- **Every procedure should be followed once by someone other than its author, in dev,** before it's trusted. Fix whatever they trip over, and note the date here if you like.
- **After an incident,** add what you learned: a missing step, a better check, a new entry under [Incidents](#incidents).
11 changes: 11 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ typecheck:
# Everything CI runs for the Node app: lint, types, tests with coverage thresholds
check: lint typecheck coverage

# Fail unless this branch updates CHANGELOG.md vs the base (default origin/main).
# CI sets SKIP_CHANGELOG when the PR has the skip-changelog label.
check-changelog *args:
pnpm exec tsx scripts/check-changelog.ts {{args}}

# Cut [Unreleased] into a dated version and bump package.json.
# Added → minor; Fixed/Changed → patch. Never major before 1.0.
# `--commit` also creates the release commit and tag (used by CI).
bump *args:
pnpm exec tsx scripts/bump-version.ts {{args}}

# Format-check and validate bootstrap + envs/dev (needs Terraform 1.11+; CI runs this when infra/ changes)
tf-validate:
terraform fmt -check -recursive infra
Expand Down
Loading
Loading