From 897a8eeeb29691c6b0b0a811b5e2448bdbb7646d Mon Sep 17 00:00:00 2001 From: Sabine Maennel <5292683+sabinem@users.noreply.github.com> Date: Wed, 9 Sep 2026 14:41:26 +0200 Subject: [PATCH 1/3] docs: record user-visible changes in a changelog The repo had no changelog, so the only account of what a release contains was the commit messages. Keep a Changelog format, hand-maintained, one line per PR. The 0.8.0 entry summarises what is in production rather than reconstructing it commit by commit; it predates the file and says so. Deliberately not generated from commit messages. --- CHANGELOG.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..025aaefa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,73 @@ +# Changelog + +All notable changes to Hackagon are recorded here. The format follows +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses +[semantic versioning](https://semver.org/spec/v2.0.0.html). + +Entries are written by hand, one line per user-visible change, added in the same +pull request that makes the change. Write for someone who has never opened the +codebase: say what a person can now do, not which function changed. Anything +invisible to a user of the platform — a refactor, a test, a CI tweak — does not +need an entry. + +`just version::bump` stamps the accumulated `[Unreleased]` section with the new +version and today's date, so the release notes for a version are whatever was +written while it was being built. See [RELEASING.md](RELEASING.md). + +## [Unreleased] + +### Added + +### Changed + +### Fixed + +## [0.8.0](https://github.com/SwissDataScienceCenter/hackagon/releases/tag/v0.8.0) - 2026-09-08 + +The first tagged release, and the one currently in production. It predates this +changelog, so this entry summarises what the release contains rather than +reconstructing its 735 commits. + +### Added + +- **Authentication and authorisation**: Keycloak for login; casbin for + per-hackathon roles, with global admin as an override. +- **Hackathons**: Organisers can create a hackathon and are then owners of that + hackathon, but can also give that role to other registered users. +- **Hackathon Owners**: edit it, and drive a hackathon. Capability switches + decide what participants may do. A hackathon has phases, but capabilities are + independent of phases. Tracks are optional. A hackathon can have more than one + owner. +- **A public page per hackathon**: Owners can publish a markdown page on the + hackathon which is visible on the public part of the website. +- **Registration**: Owners create a registration form and decide which answers + will be visible to other participants. For public hackathons registered users + can ask to join the hackathon by clicking a button on the public website. They + will then be redirected to fill in the registration form. The owner sees the + answers and can approve or reject their participation. +- **Private hackathons by invitation link**: For private hackathons a link to an + unlisted page is distributed where participants can ask join. Registration to + a private hackathon follows otherwise the same process as registration to a + public hackathon. +- **Participants**: A roster that can be handed to a mailing tool, so the owner + can communicate with the participants of his hackathon. +- **Projects**: Projects can be either proposed by the hackathon owner or by the + Participants depending on the settings for the hackathon. In case Participants + can propose projects the owners can approve or reject them. +- **Teams**: Participants can set preferences for projects they want to work on + during the hackathon. Then owners can use the preferences and the the answers + on the registration form to build balanced teams by either downloading the + data and uploading team assignments or by using a platform tool to make a + suggestion that they can refine. Team assignments can be updated on bulk or as + individual records. **Submissions** Any member of a team can make a versioned + submission on behalf of the team. A submission is a weblink (to a github repo + or other artifacts). A submission can be declared as final. That will make + visible to other teams during the voting. +- **Voting**: The owners can setup voting categories and methods and allow + participants to vote for other teams. +- **Pages**: The owners can add markdown pages and decide whether they should be + visible to participants. + +- **Deployment**: A Helm chart published as an OCI artifact, container images + built and pushed by CI, a gRPC health check, and the deployed version shown in + the footer. From b5135196336a0d8a2e47afa1c76c6cd2bdee1be0 Mon Sep 17 00:00:00 2001 From: Sabine Maennel <5292683+sabinem@users.noreply.github.com> Date: Wed, 9 Sep 2026 14:58:46 +0200 Subject: [PATCH 2/3] feat(just): version::bump stamps the changelog with the release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A hand-maintained changelog rots the moment someone forgets to move [Unreleased] on release day. bump now lifts whatever sits under that heading into a dated heading for the version being cut, and leaves [Unreleased] empty again — so a version's notes are exactly what was written while it was being built. --- tools/just/version.just | 111 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/tools/just/version.just b/tools/just/version.just index 309c8ebd..e62582fd 100644 --- a/tools/just/version.just +++ b/tools/just/version.just @@ -4,6 +4,8 @@ set shell := ["bash", "-cue"] root_dir := `git rev-parse --show-toplevel` version_file := root_dir + "/VERSION" pkg_file := root_dir + "/components/frontend/package.json" +changelog_file := root_dir + "/CHANGELOG.md" +repo_url := "https://github.com/SwissDataScienceCenter/hackagon" # Components whose `.component.yaml` version becomes an OCI image tag. Not # `tools/quitsh`: vendored, carries its own upstream version, builds no image. @@ -32,7 +34,8 @@ help: echo -e " ${dim}Fail if any component version has drifted from VERSION${reset}" echo "" echo -e " ${cyan}just version::bump${reset} patch|minor|major" - echo -e " ${dim}Edit VERSION, propagate it to every component, commit, and tag${reset}" + echo -e " ${dim}Edit VERSION, propagate it to every component, roll the${reset}" + echo -e " ${dim}changelog, commit, and tag${reset}" echo "" echo -e " ${cyan}just version::tag${reset}" echo -e " ${dim}Tag HEAD with the version already declared in VERSION${reset}" @@ -40,6 +43,10 @@ help: echo -e " ${dim}VERSION covers the app only: the per-component copies become the${reset}" echo -e " ${dim}published image tags. The Helm chart versions itself — see just helm::help.${reset}" echo "" + echo -e " ${dim}bump also stamps CHANGELOG.md: whatever sits under [Unreleased]${reset}" + echo -e " ${dim}becomes the notes for the version being cut. Write the entry in the${reset}" + echo -e " ${dim}pull request that makes the change — see RELEASING.md.${reset}" + echo "" # Print the declared version and the string a build stamps into the footer. [group('version')] @@ -117,7 +124,7 @@ check: echo "All versions agree on $declared." -# Bump VERSION, propagate it to the components, commit, and tag. +# Bump VERSION, propagate it to the components, roll CHANGELOG.md, commit and tag. # Usage: just version::bump patch|minor|major [group('version')] bump part="patch": @@ -185,6 +192,103 @@ bump part="patch": exit 1 fi + # Stamp the accumulated [Unreleased] section with this version, so the + # release notes for v$next are whatever was written while it was built. + # A missing CHANGELOG.md is not fatal — a checkout without one can still + # release — but a CHANGELOG.md we cannot parse is, because silently + # publishing a version with no notes is worse than a failed release. + changelog_note="" + if [ -f "{{ changelog_file }}" ]; then + today=$(date +%F) + tmp=$(mktemp) + + # Lift the body out of [Unreleased], put a fresh empty [Unreleased] in + # its place, and re-head the body with the version, its compare link + # and today's date. Subsection headings nobody wrote under are dropped + # rather than shipped empty. + awk -v ver="$next" -v date="$today" \ + -v link="{{ repo_url }}/compare/v$current...v$next" ' + function flush() { + print "## [Unreleased]" + print "" + print "### Added" + print "" + print "### Changed" + print "" + print "### Fixed" + print "" + # The compare link lives in the heading, not in a link + # definition at the foot of the file: prettier hard-wraps a + # definition line past 80 columns but never wraps a heading, + # and a wrapped definition is one the next release cannot find. + print "## [" ver "](" link ") - " date + + for (i = 1; i <= n; i++) { + if (body[i] ~ /^### /) { + keep = 0 + for (j = i + 1; j <= n; j++) { + if (body[j] ~ /^### /) break + if (body[j] !~ /^[[:space:]]*$/) { keep = 1; break } + } + skip = !keep + if (skip) continue + } + if (skip && body[i] ~ /^[[:space:]]*$/) continue + out[++m] = body[i] + } + + while (m > 0 && out[m] ~ /^[[:space:]]*$/) m-- + s = 1 + while (s <= m && out[s] ~ /^[[:space:]]*$/) s++ + + print "" + for (i = s; i <= m; i++) print out[i] + if (m >= s) print "" + } + state == 0 { + if ($0 ~ /^## \[Unreleased\]/) { state = 1; found = 1; next } + print + next + } + state == 1 { + # The next top-level heading closes the section; so does EOF. + if ($0 ~ /^## /) { flush(); state = 2; print; next } + body[++n] = $0 + next + } + state == 2 { print } + END { + if (state == 1) flush() + if (!found) exit 3 + # Report an empty release section to the caller, not an error: + # a release with no user-visible changes is unusual, not wrong. + if (m >= s) exit 0 + exit 4 + } + ' "{{ changelog_file }}" >"$tmp" || rc=$? + rc=${rc:-0} + + if [ "$rc" = "3" ]; then + rm -f "$tmp" + echo "{{ changelog_file }} has no '## [Unreleased]' heading, so there" >&2 + echo "is nothing to stamp as v$next. Restore the heading, or delete" >&2 + echo "the file if this repo no longer keeps a changelog." >&2 + exit 1 + elif [ "$rc" != "0" ] && [ "$rc" != "4" ]; then + rm -f "$tmp" + echo "Failed to roll {{ changelog_file }} (awk exited $rc)." >&2 + exit 1 + fi + + if [ "$rc" = "4" ]; then + changelog_note=" (empty — nobody recorded a change)" + fi + + mv "$tmp" "{{ changelog_file }}" + + git -C "{{ root_dir }}" add "{{ changelog_file }}" + fi + git -C "{{ root_dir }}" add "{{ version_file }}" "{{ pkg_file }}" git -C "{{ root_dir }}" commit -m "chore(release): v$next" git -C "{{ root_dir }}" tag -a "v$next" -m "v$next" @@ -194,6 +298,9 @@ bump part="patch": for comp in {{ image_comps }}; do echo " image $comp-service:$next" done + if [ -n "$changelog_note" ] || [ -f "{{ changelog_file }}" ]; then + echo " notes CHANGELOG.md [Unreleased] -> [$next]$changelog_note" + fi echo " commit chore(release): v$next" echo " tag v$next (annotated)" echo "" From 35de60e1f4ba045101c5b02951f4cd097437606d Mon Sep 17 00:00:00 2001 From: Sabine Maennel <5292683+sabinem@users.noreply.github.com> Date: Thu, 10 Sep 2026 07:19:34 +0200 Subject: [PATCH 3/3] docs: write down the release process and what a merge already does RELEASING.md describes the current release process and what happens if a feature is merged to main. It also decribes the branching model and the versioning of App and Charts. --- CLAUDE.md | 18 ++++-- README.md | 5 ++ RELEASING.md | 164 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 RELEASING.md diff --git a/CLAUDE.md b/CLAUDE.md index 111993d8..2e88e535 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,7 +68,8 @@ components/backend/ └── Schema.md # human-readable DB reference (auto-generated) components/frontend/ # SvelteKit; generated gRPC clients under src/lib/server/grpc/generated/ helm-chart/ # Helm chart; published as an OCI artifact on a v* tag -mydocs/docs/backend-tickets/ # known gaps, one file per issue (README inside) +CHANGELOG.md # user-visible changes; the entry lands in the PR +RELEASING.md # branch model + how a change reaches a cluster tools/helm/lint-values.yaml # throwaway values so the chart can be rendered in CI tools/nix/ # Nix flake + process-compose config (toolchain.nix) tools/just/*.just # just modules — see Dev commands @@ -150,10 +151,19 @@ These hold across the whole codebase; the skills explain the mechanisms. - **`VERSION` at the repo root is the app's only declared version.** Bump it with `just version::bump`, never by hand — the recipe moves `VERSION`, both `components/*/.component.yaml` versions and `components/frontend/package.json` - together, then commits and tags. A component version _is_ its published image - tag, so `just version::check` (a CI stage) fails a tree where these disagree. + together, rolls `CHANGELOG.md`'s `[Unreleased]` section into a heading for the + new version, then commits and tags. A component version _is_ its published + image tag, so `just version::check` (a CI stage) fails where these disagree. The frontend reads `VERSION` at build time (`vite.config.ts` → `$lib/version` - → the footer), so editing it without a rebuild changes nothing. + → the footer) and displays it. +- **`main` is the only long-lived branch, and merging to it reaches the running + app.** One focused pull request per change, each carrying its `CHANGELOG.md` + entry. Every push to `main` moves `temporary/*:latest`, and a deployment at + running https://app.hackagon.dev.renku.ch/ that tag with + `imagePullPolicy: Always` picks it up on _any_ container restart. This + deployment is just temporary. The real deployment happens via Chart where an + `appVersion` is pinned. Charts have there own decoupled Chart version. See + `RELEASING.md`. - **The chart versions itself; `VERSION` never touches it.** `helm-chart/Chart.yaml` holds two hand-edited numbers: `version` is the chart's own release, `appVersion` is the app release it deploys. diff --git a/README.md b/README.md index 4cbb9820..12e00c9e 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,11 @@ report identifies the code that produced it. Reading `VERSION` from a file rather than from `git describe` means a shallow clone or an unpacked tarball still builds with a truthful version. +`bump` also stamps [CHANGELOG.md](CHANGELOG.md): whatever has accumulated under +`[Unreleased]` becomes the notes for the version being cut, so write the entry +in the pull request that makes the change. [RELEASING.md](RELEASING.md) covers +the branch model and how a release reaches a cluster. + ## Component READMEs - [Frontend](components/frontend/README.md) diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..5b6fdf47 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,164 @@ +# Releasing Hackagon + +This documents how a change gets from a pull request to a running app: which +version number moves, what a merge already does on its own, and which steps a +person has to decide. + +The mechanics of the pipeline itself — stages, image registries, caching — are +in [.github/ci-cd.md](.github/ci-cd.md). This file is the process on top of +them. + +## Branches + +`main` is the only long-lived branch. Work happens on a short-lived branch — +`feat/…`, `fix/…`, `docs/…`, `chore/…` — and merges into `main` through a pull +request, with its changelog entry in the same pull request. + +There is no integration branch. One focused pull request per change, reviewed +and merged on its own, is what keeps the history readable and the changelog +honest. + +A pull request runs the full CI suite: codegen drift, lint, version consistency, +chart lint, build, tests, and an image build. That is the only gate in front of +`main` which matters for the reason below. + +## Deployment + +There are two deployments. Only one of them exists today. + +### The temporary one — + +It runs `temporary/*:latest` with `imagePullPolicy: Always`. Every push to +`main` rebuilds that tag, so this site serves whatever was merged last — from +the next container restart onwards. + +``` +merge to main ──► CI rebuilds temporary/*:latest ──► next container restart + serves the new code +``` + +A restart is not always something you chose: an eviction or a node drain +restarts the container too. To restart it on purpose: + +```bash +kubectl rollout restart deploy/hackagon-frontend +``` + +To see which build it is serving, read the version at the bottom of the page. + +Every push also publishes an immutable tag beside `latest` — the version plus +the first 12 characters of the commit, e.g. `0.8.0-efc7c9ace429`. `latest` gets +overwritten; those never do, so a specific build can be pinned: + +```bash +--set frontend.image.tag=0.8.0-efc7c9ace429 +--set frontend.image.pullPolicy=IfNotPresent +``` + +`skopeo list-tags docker://` shows which tags exist. + +### The real one — not set up yet + +dev and prod will be installed from the Helm chart, which names the app release +it deploys in `appVersion`. Such a deployment ignores `latest` completely and +changes only when someone installs a new chart version. How it will be set up is +not decided yet, so this file does not describe it. + +## Three version numbers + +Two of them are the app, one is the chart, and they move independently. Read +[.github/ci-cd.md](.github/ci-cd.md#two-release-trains) for why. + +| Number | Lives in | Moved by | Means | +| ------------------ | ------------------------ | -------------------- | -------------------------------------- | +| App version | `VERSION` (+ mirrors) | `just version::bump` | The app release. Becomes the image tag | +| Chart version | `Chart.yaml: version` | By hand | The chart's own release | +| Chart `appVersion` | `Chart.yaml: appVersion` | By hand | The app release a cluster will run | + +`VERSION` is mirrored into both `components/*/.component.yaml` files and +`components/frontend/package.json`, because a component version _is_ its +published image tag. CI fails a tree where they disagree, so always move them +with `just version::bump` rather than by hand. + +That check exists because the drift is real and silent: an image tagged `0.7.0` +whose footer reads `v0.1.0` came from a tree where those two files disagreed, +and nothing complained at the time. + +## Writing the changelog + +Add the entry in the same pull request that makes the change, under +`## [Unreleased]` in [CHANGELOG.md](CHANGELOG.md). One line, in the language a +user of the platform would use — what they can now do, not which function +changed. A refactor, a test or a CI tweak needs no entry. + +`just version::bump` lifts whatever sits under `[Unreleased]` into a heading for +the version being cut, dates it, and leaves `[Unreleased]` empty again. So the +release notes for a version are exactly what was written while it was being +built — nobody reconstructs them afterwards from the commit log. If nobody wrote +anything, `bump` says so in its summary and releases anyway. + +Concurrent pull requests both appending under `[Unreleased]` will conflict on +those lines. The conflict is trivial to resolve — keep both entries — and is the +price of a file anyone can read without tooling. + +## Cutting an app release + +From an up-to-date, clean `main`: + +```bash +just version::check # the versions already agree +just version::bump minor # or patch / major +git show # read the release commit before it leaves +git push && git push origin v0.9.0 +``` + +`bump` refuses a dirty tree, edits every version file, rolls the changelog, +commits as `chore(release): v0.9.0` and creates the annotated tag. It pushes +nothing — it prints the two `git push` commands so the release is never a side +effect of running it. + +Pushing the tag builds and pushes `release/backend-service:0.9.0` and +`release/frontend-service:0.9.0`. A release image is never overwritten, so a +botched tag is fixed by bumping again, not by re-tagging. + +If `main` has moved past something you cannot ship and a released version needs +a fix, branch from the tag rather than from `main`: + +```bash +git switch -c hotfix/thing v0.8.0 +``` + +Open the pull request against `main` if the fix belongs there too. Create such a +branch when a fix actually needs one — not in advance. + +## Making a release deployable + +A tagged release is not installable from the chart until the chart points at it. +That means two hand-edited numbers in `helm-chart/Chart.yaml`: + +```yaml +version: 0.3.0 # the chart's own release — bump it, CI checks that you did +appVersion: "0.9.0" # the app release this chart deploys +``` + +CI wants that `version` bump for _any_ change under `helm-chart/`, comments +included. On merge, `just helm::publish` pushes the chart to +`oci://ghcr.io/swissdatasciencecenter/hackagon/charts/hackagon`, and skips +without failing when the version is already published or when `appVersion` has +no images yet. + +Publishing a chart is not deploying it. Once a real deployment exists, +installing that chart version is the step that changes what runs. + +## A first release checklist + +Worth knowing once, then never again: + +- **New GHCR packages are private by default.** The first `v*` tag creates the + `release/*` packages and the first chart publish creates `charts/hackagon`. + Until someone sets them public in the org's package settings, a cluster needs + an `imagePullSecret`. The `temporary/*` packages are already public. +- **The chart cannot render with its own defaults.** Several values — admin id, + passwords, hostnames — are intentionally empty. `just helm::lint` renders it + against [tools/helm/lint-values.yaml](tools/helm/lint-values.yaml), which + exists only so the chart can be linted in CI. It is not a deployment example.