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
73 changes: 73 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 14 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
164 changes: 164 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -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 — <https://app.hackagon.dev.renku.ch>

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://<repository>` 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.
Loading
Loading