Skip to content
Closed
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ written while it was being built. See [RELEASING.md](RELEASING.md).

### Fixed

## [0.12.2](https://github.com/SwissDataScienceCenter/hackagon/compare/v0.12.1...v0.12.2) - 2026-10-28

### Fixed

- Participants can see published results after voting has closed; the results
page used to fail with an error once an organizer turned voting off.

## [0.12.1](https://github.com/SwissDataScienceCenter/hackagon/compare/v0.12.0...v0.12.1) - 2026-10-27

## [0.12.0](https://github.com/SwissDataScienceCenter/hackagon/compare/v0.11.0...v0.12.0) - 2026-10-23
Expand Down
55 changes: 55 additions & 0 deletions HOTFIX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Hotfixing to the current release branch

## Release branch

For the release that is currently on Prod a release branch is kept:
`release/0.x`. It starts at tag `v0.x.0`.

## Hotfixes

Hotfixes are tagged `v0.x.1`, `v0.x.2`, etc.

For each hotfix tag, images are built as `release/backend-service:0.x.1` and
`release/frontend-service:0.x.1`, etc. Once the images are there, the chart can
be deployed first to dev and then to prod.

## Order of steps:

### Make the hotfix

Check out the release branch `release/0.x`. It should contain all previous
hotfix tags.

Make the fix on a `hotfix/some-fix` branch and also add a changelog entry.

### Test the hotfix

Consider that the db schema might have changed and that you have to reset the db
in order to test the hotfix.

### Merge the hotfix

Open a PR to merge the hotfix into `release/0.x` — not into `main`, which GitHub
suggests. In the PR, add a second commit that only changes the helm chart
`helm-chart/Chart.yaml`: from `version: 0.y.m` and `appVersion: "0.x.n"` to
`version: 0.y.m+1` and `appVersion: "0.x.n+1"`. Merge the PR with "Rebase and
merge", not "Squash and merge", so the two commits stay separate.

Then update your local release branch, so the release contains the fix:

`git switch release/0.x && git pull --ff-only`

Then `just version::bump patch`. Versions should now all be on `0.x.n+1`.

Push the tag:

`git push && git push origin v0.x.n+1`.

The images and the chart should build now. Install the new chart version on prod
through `sdsc-ordes/cloud-infra`.

### Bring the change to main:

Open a branch from `main` and cherry pick only the fix commit onto that branch,
not the helm chart commit. Open a PR and merge into `main`. Dev gets the fix
after its next restart.
11 changes: 2 additions & 9 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,15 +144,8 @@ git reset --hard origin/main
git tag -d v0.9.0
```

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.
A fix prod cannot wait for goes onto this release branch, not through `main` —
see [HOTFIX.md](HOTFIX.md) for the protocol.

## Making a release deployable

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.12.1
0.12.2
2 changes: 1 addition & 1 deletion components/backend/.component.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: backend
version: 0.12.1
version: 0.12.2
language: go

targets:
Expand Down
21 changes: 18 additions & 3 deletions components/backend/internal/service/vote_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,28 @@ func (s *VoteService) ListVoteCategories(
return nil, status.Errorf(codes.InvalidArgument, "invalid hackathon_id: %v", err)
}

if err := s.enforcer.RequirePermission(
// Results are fetched per category, so anyone who may read results must
// also be able to list the categories, even once voting is off.
canReadCategories, err := s.enforcer.Enforce(
ctx,
hackathonID.String(),
m.VoteCategory,
m.Read,
); err != nil {
return nil, err
)
if err != nil {
slog.Error("enforce list vote categories", "err", err)

return nil, status.Error(codes.Internal, "authorization error")
}
if !canReadCategories {
if err := s.enforcer.RequirePermission(
ctx,
hackathonID.String(),
m.VoteResult,
m.Read,
); err != nil {
return nil, err
}
}

categories, err := s.dbClient.VoteCategory.Query().
Expand Down
49 changes: 49 additions & 0 deletions components/backend/internal/service/vote_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,55 @@ var _ = Describe("VoteService", func() {
Expect(err).NotTo(HaveOccurred())
Expect(resp.GetVoteCategories()).To(BeEmpty())
})

Context("as a member", func() {
var memberCtx context.Context
BeforeEach(func() {
createVoteCategory(
hackathonID,
"Best",
voteEntities.VotingMethod_VOTING_METHOD_SINGLE_CHOICE,
voteEntities.VoterType_VOTER_TYPE_ALL_PARTICIPANTS,
nil,
)
_, kc := createAndJoin(hackathonID, "member")
memberCtx = metadata.NewOutgoingContext(
context.Background(),
metadata.Pairs("authorization", "Bearer "+testutils.CreateTestJWTToken(kc)),
)
})

It("lists categories while voting is enabled", func() {
setCapabilities(hackathonID, hackathonEntities.Capability_CAPABILITY_VOTE)
resp, err := voteClient.ListVoteCategories(
memberCtx,
&voteMsgs.ListVoteCategoriesRequest{HackathonId: hackathonID},
)
Expect(err).NotTo(HaveOccurred())
Expect(resp.GetVoteCategories()).To(HaveLen(1))
})

It("lists categories when only results are visible", func() {
setCapabilities(
hackathonID,
hackathonEntities.Capability_CAPABILITY_VIEW_RESULTS,
)
resp, err := voteClient.ListVoteCategories(
memberCtx,
&voteMsgs.ListVoteCategoriesRequest{HackathonId: hackathonID},
)
Expect(err).NotTo(HaveOccurred())
Expect(resp.GetVoteCategories()).To(HaveLen(1))
})

It("is denied when neither voting nor results are enabled", func() {
_, err := voteClient.ListVoteCategories(
memberCtx,
&voteMsgs.ListVoteCategoriesRequest{HackathonId: hackathonID},
)
Expect(status.Code(err)).To(Equal(codes.PermissionDenied))
})
})
})

Describe("EditVoteCategory", func() {
Expand Down
2 changes: 1 addition & 1 deletion components/frontend/.component.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: frontend
version: 0.12.1
version: 0.12.2
language: typescript

targets:
Expand Down
2 changes: 1 addition & 1 deletion components/frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "frontend-v3",
"private": true,
"version": "0.12.1",
"version": "0.12.2",
"type": "module",
"scripts": {
"dev": "vite dev",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,25 @@ export const load: PageServerLoad = async (event) => {
// Every category, including ranked and jury ones the booth cannot cast a
// vote for — results are results however they were produced, and a category
// decided elsewhere (a jury meeting off the platform, say) still deserves to
// show its podium. Reading a result needs only `vote_result:read`, which the
// capability grants to every member.
const { voteCategories } = await vote.listVoteCategories({
hackathonId: event.params.id,
})
// show its podium. Listing categories takes `vote_category:read` *or*
// `vote_result:read`, and the capability grants the latter to every member,
// so this works after voting has closed.
//
// The catch is a guard, same as the per-category one below: a refusal here
// once took the whole page down with a 500. Losing the podiums still leaves
// the entries directory.
let voteCategories: Awaited<
ReturnType<typeof vote.listVoteCategories>
>["voteCategories"] = []
try {
;({ voteCategories } = await vote.listVoteCategories({
hackathonId: event.params.id,
}))
} catch (e) {
if (!(e instanceof ClientError && e.code === Status.PERMISSION_DENIED)) {
throw e
}
}

const categories = await Promise.all(
voteCategories.map(async (c) => {
Expand Down
4 changes: 2 additions & 2 deletions helm-chart/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ type: application

# The chart's own version. Bumped by hand when the chart changes, and
# independent of the app: the CI publishes whatever it finds here.
version: 0.8.1
version: 0.8.2
# The app release this chart deploys. A new app release does
# not become deployable until someone points the chart at it.
appVersion: "0.12.1"
appVersion: "0.12.2"

dependencies:
- name: postgresql
Expand Down
Loading