Contributing

How we work on Astrarium. Short on purpose — a contributing file nobody reads is worse than none.

Prerequisites

Contributor tooling, in addition to whatever you already use to write code:

Day-to-day commands (npm run dev, npm run build, npm test, npm run lint, npm run format) are documented in README.md.

Discussion

Synchronous discussion, design / scoping conversations, and decision-making happen on Discord:

Astrarium Discord

The GitLab issue tracker remains the asynchronous source of truth — every decision still gets recorded on the relevant issue or MR. The channel is for the live conversation that gets us there. GitLab → Discord notifications for MR / issue / pipeline activity are wired up in the same channel (#49), so events flow into Discord automatically; this link is the human entry point.

Slash commands

Astrarium is driven by an AI pair-programmer (see the About this project note in README.md). The day-to-day workflow is automated by a small set of project-level slash commands shipped under .claude/commands/. They are shortcuts for the manual glab flow described below — they don’t replace it, they accelerate the parts that are mechanical.

Run /init first, every fresh checkout. It loads CLAUDE.md, CONTRIBUTING.md, and the surrounding context into the AI driver so every subsequent command operates against an accurate mental model of the project, and runs a drift check against src/ — if CLAUDE.md has fallen behind the codebase, /init auto-drafts a task workitem documenting the gap and asks you to confirm before filing. Skipping /init leads the driver to hallucinate file paths and ignore conventions documented here. This isn’t a suggestion.

Project-level commands (one Markdown prompt each, lives in .claude/commands/):

Command Phase it accelerates What it does
/init Session start Orients Claude to the project (loads CLAUDE.md, walks src/), runs a drift check, and auto-drafts a task workitem if CLAUDE.md no longer matches src/. Confirm-before-create.
/workitem File a new idea Drafts a new GitLab issue from a free-form description, picks a label, asks for confirmation before filing.
/groom-plan Backlog grooming Walks the open backlog, surfaces 0–3 maintainer decisions, queues mechanical fixes, publishes a grooming-YYYY-MM-DD wiki page. Read-only on the tracker.
/groom-apply Backlog grooming Reads the maintainer-annotated grooming page and applies the agreed-on adjustments to issues.
/next Pick a ticket Picks the top item from the latest grooming page’s Next up bucket, runs per-issue verification, optionally self-assigns.
/work Start work Runs the git-flow ceremony for #N: self-assign, branch off up-to-date main, empty starter commit, push, draft MR. Aborts on any precondition failure; never auto-recovers with destructive git ops.

Also useful, shipped by Claude Code rather than this project:

If a command is added or removed in .claude/commands/, this section is the one that needs updating.

Git workflow

One issue, one merge request, one branch. Every change must be traceable to a filed GitLab issue.

Single long-lived branch: main. Feature work targets main via MRs. The production site (astrarium42.gitlab.io/astrarium/) deploys on vX.Y.Z tag pushes — cutting the tag is the release.

  1. File the issue first. Agree on scope before writing code. Small fixes still need an issue — it’s the unit of work, not the commit.
  2. Self-assign the issue so the board reflects who’s working on it: glab issue update <N> --assignee "@me". An unassigned open issue means nobody’s working on it. If an issue already has an assignee who isn’t you, don’t take it — coordinate on Discord first, have the current assignee unassign, or ask the maintainer to reassign. The --assignee "@me" call replaces the assignee silently; the rule lives here, not in GitLab. /next enforces this automatically; manual self-assigns rely on you reading the page first.
  3. Branch off main with the name issue-<N>-<slug> (e.g. issue-13-conventions). Pull main first: git checkout main && git pull --ff-only. Never commit directly to main.
  4. Open the draft MR before writing any code. Make an empty initial commit and push so GitLab has something to attach the MR to:
    git commit --allow-empty -m "Start work on #<N>"
    git push -u origin issue-<N>-<slug>
    glab mr create --draft --assignee "@me" \
      --squash-before-merge --remove-source-branch --target-branch main \
      --title "<short description>" \
      --description "Closes #<N>"
    
    The --draft flag prefixes Draft: on its own — don’t add it to --title or you’ll end up with Draft: Draft: …. The MR is visible from the first keystroke, mirroring how a human would scaffold the work. Real commits land on top. Closes #<N> in the description auto-closes the issue on merge.
  5. Commit freely on the branch with short, descriptive subjects in the imperative mood. Branch commits get collapsed into a single hand-authored commit at pre-squash time (step 6), so subjects can be working notes — they don’t need to read well in the final history.
  6. Pre-squash before merge. GitLab’s squash-on-merge auto-message concatenates branch commit subjects, which is rarely the right summary. Squash locally and write the merge commit yourself:
    git fetch origin
    git rebase -i origin/main
    # In the editor: pick the first commit, fixup (f) the rest.
    # Then amend the surviving commit's message:
    git commit --amend
    git push --force-with-lease
    
    The MR is now a single commit. GitLab’s squash-on-merge stays enabled as a safety net, but with one commit on the branch the toggle is a no-op.
  7. If CI fails, fix forward with a new commit, then re-squash before merge.
  8. Ready MRs are frozen. Once an MR is marked ready, no further changes are accepted on it. If a change becomes necessary — reviewer-requested edit, rebase, scope tweak — first re-mark the MR as draft (glab mr update <N> --draft), push the change, and only then re-request readiness. Keeps ready a meaningful signal: a ready MR is the exact diff being asked to merge, not a moving target.
  9. Merge needs at least one approval. GitLab won’t let an MR merge without it. Treat ready and approved as separate states — the second is a real gate, not a formality. Don’t propose merging until the approval is in.
  10. Scope discipline. If a new idea surfaces mid-MR, file a separate issue for it. Only fold it in if the reviewer explicitly agrees.

Issue labels

Three labels are in use: bug, feature, task. The criterion is does the deployed site change?

This keeps the label honest as a release-notes signal: every bug and feature ships as a release, every task may not.

Commit messages

Docs match code

If your change affects anything documented in docs/, update the doc in the same commit. If the change invalidates a doc entirely, rewrite it or remove it — do not leave stale copy.

File Covers
docs/architecture.md Directory layout, module boundaries, page structure
docs/physics.md Formulas and their derivations, accuracy targets
docs/scales.md Per-view scaling rationale and compressions
docs/releasing.md Release process, tagging, versioning conventions
CHANGELOG.md One [Unreleased] entry per bug / feature MR, written on that same MR (see below)

This rule is advisory, not CI-enforced. Rely on reviewer judgement.

If your MR’s label is bug or feature, add a one-paragraph entry under [Unreleased] in CHANGELOG.md as part of the same MR. The release MR’s job is to rename [Unreleased] to [X.Y.Z] — date and add the compare-link — not to backfill content. If the maintainer routinely has to write entries during the release cut, the rule isn’t being followed. task MRs may add an entry if the change is worth surfacing in release notes (e.g. workflow changes that affect contributors), but aren’t required to.

Why glab, not a wrapper?

We invoke glab directly rather than adopting a wrapper like shipit-cli-go (which does support GitLab via a project-root .shipit.yml). Recorded so the question doesn’t keep coming back (discussion):

Shipit’s main value is helping humans remember the multi-step GitLab flow (branch → push → MR). Claude bridges that gap well enough here that the wrapper doesn’t pay its keep. Calling glab directly also keeps contributor tooling to one CLI instead of two.

Not a knock on shipit — a judgement call about what fits this project’s workflow. Revisit if Claude stops being the primary driver.

Releasing

SemVer (vX.Y.Z). Every merged bug fix or feature must ship as a new patch or minor release — do not leave fixes sitting in [Unreleased] after merge. Three sources of truth must agree at release time: the annotated git tag, src/version.js (VERSION), and the CHANGELOG.md entry. The deploy is gated on the tag push, so cutting the tag is the release.

See docs/releasing.md for the full process (-dev suffix conventions, the glab release create invocation).

Deployment

GitLab CI (.gitlab-ci.yml) has a single pages job that fires on vX.Y.Z tag pushes only. It checks out the tag, runs npm run build, and serves the resulting dist/ at https://astrarium42.gitlab.io/astrarium/.

Merging to main does not deploy on its own — push a vX.Y.Z tag to ship.

Force HTTPS is enabled in Settings → Deploy → Pages (project setting, not in this repo): plain HTTP requests to the deployed site — including the custom domain — get a 301 to the HTTPS equivalent. The toggle relies on a valid TLS certificate; if the cert ever expires or the domain config breaks, the site starts erroring on HTTP. (#109)

Other pointers

← Astrarium