Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
139 changes: 139 additions & 0 deletions .github/workflows/helm-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
name: Helm OCI release

on:
push:
tags:
- 'helm/v*'

permissions:
contents: read

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

jobs:
publish:
name: Validate and publish Helm chart
runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
contents: read
packages: write
env:
OCI_REPOSITORY: ghcr.io/cattr-app/helm/server
OCI_NAMESPACE: oci://ghcr.io/cattr-app/helm
steps:
- uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
with:
egress-policy: audit

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0

- uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
with:
version: v3.19.0

- uses: imjasonh/setup-crane@feee3b6bb0d4c68370f256a4502498c9227e5c6b # v0.7
with:
version: v0.22.1

- name: Validate tag, chart, image and branch
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
bash .helm/ci/release-metadata.sh "$RELEASE_TAG"
git fetch --no-tags origin main
git merge-base --is-ancestor "$GITHUB_SHA" origin/main || {
echo "::error::Helm releases must point to commits reachable from main"
exit 1
}
image_tag="$(helm show chart .helm | awk '/^appVersion:/ {gsub(/"/, "", $2); print $2}')"
crane manifest "ghcr.io/cattr-app/server:$image_tag" >/dev/null || {
echo "::error::Referenced application image is not available: $image_tag"
exit 1
}

- name: Lint and validate chart
uses: docker://alpine/k8s@sha256:d50424976a0a888d2531f13035dce45c004ec2b6f877f233005ba35c0d61be5d # 1.34.12
with:
entrypoint: sh
args: .helm/ci/check.sh

# A re-run of a failed release reuses the archive packaged by the first attempt:
# a fresh `helm package` is not byte-identical (it records timestamps), so it could
# not be compared with what was already pushed.
- name: Restore archive from an earlier attempt
id: restore
if: github.run_attempt > 1
continue-on-error: true
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: helm-chart-archive
path: dist

- name: Package chart
id: package
if: steps.restore.outcome != 'success'
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
version="${RELEASE_TAG#helm/v}"
mkdir -p dist
helm package .helm --destination dist
test -f "dist/server-$version.tgz"
sha256sum "dist/server-$version.tgz" | tee dist/SHA256SUMS

- name: Preserve packaged archive
if: steps.package.outcome == 'success'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: helm-chart-archive
path: dist/
if-no-files-found: error
retention-days: 14

- name: Authenticate to GHCR
env:
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
printf '%s' "$GHCR_TOKEN" | helm registry login ghcr.io -u "$GITHUB_ACTOR" --password-stdin
printf '%s' "$GHCR_TOKEN" | crane auth login ghcr.io -u "$GITHUB_ACTOR" --password-stdin

- name: Publish immutable OCI chart and verify pull
env:
RELEASE_TAG: ${{ github.ref_name }}
ARCHIVE_RESTORED: ${{ steps.restore.outcome == 'success' }}
run: |
set -euo pipefail
version="${RELEASE_TAG#helm/v}"
sha256sum -c dist/SHA256SUMS
# No overwrite: previously published artifacts must remain immutable.
# Fail closed: only an explicit "not found" from the registry allows the push;
# any other lookup failure (timeout, authorization) aborts the release.
# The one exception is a re-run that restored the archive pushed by an earlier
# attempt of this run: then nothing is pushed and the published chart is only
# verified against that archive.
if lookup="$(crane manifest "$OCI_REPOSITORY:$version" 2>&1)"; then
if [ "$ARCHIVE_RESTORED" != "true" ]; then
echo "::error::Helm chart $version is already published; refusing overwrite"
exit 1
fi
echo "Helm chart $version was pushed by an earlier attempt; verifying without pushing"
elif ! printf '%s' "$lookup" | grep -qE ': (GET|HEAD) [^ ]+: (MANIFEST_UNKNOWN|NAME_UNKNOWN): |: (GET|HEAD) [^ ]+: unexpected status code 404 Not Found'; then
echo "::error::Cannot determine whether Helm chart $version exists: $lookup"
exit 1
else
helm push "dist/server-$version.tgz" "$OCI_NAMESPACE"
fi
mkdir -p pulled
helm pull "oci://$OCI_REPOSITORY" --version "$version" --destination pulled
cmp "dist/server-$version.tgz" "pulled/server-$version.tgz"
helm show chart "oci://$OCI_REPOSITORY" --version "$version"
crane digest "$OCI_REPOSITORY:$version" | tee dist/OCI-DIGEST
44 changes: 44 additions & 0 deletions .github/workflows/helm-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Helm Kind smoke test

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
install:
runs-on: ubuntu-24.04
timeout-minutes: 25
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
with:
version: v3.19.0

- uses: helm/kind-action@1544676c07bb3570fcb70166beaa07c4528fd4aa # v1.15.1

- name: Install demo chart without --wait (setup runs post-install)
run: |
set -euo pipefail
helm install cattr ./.helm --namespace cattr --create-namespace \
-f .helm/ci/demo-mysql-values.yaml --timeout 15m
kubectl -n cattr rollout status deployment/cattr-mysql --timeout=10m
kubectl -n cattr wait --for=condition=complete job/cattr-setup --timeout=10m
kubectl -n cattr rollout status deployment/cattr --timeout=10m
kubectl -n cattr get deployments,pods,jobs,services

- name: Inspect failure
if: failure()
run: |
kubectl -n cattr get pods,jobs,deployments -o wide || true
kubectl -n cattr describe pods || true
kubectl -n cattr logs job/cattr-setup --all-containers --tail=100 || true
kubectl -n cattr logs deployment/cattr-mysql --all-containers --tail=100 || true
kubectl -n cattr logs deployment/cattr-mysql --previous --tail=100 || true
kubectl -n cattr get events --sort-by=.lastTimestamp || true
13 changes: 13 additions & 0 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,19 @@ jobs:
entrypoint: sh
args: .helm/ci/check.sh

helm-release-tags:
name: Helm release metadata
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Validate Helm tags and chart metadata
run: bash .helm/ci/test-release-metadata.sh

zizmor:
name: zizmor
runs-on: ubuntu-24.04
Expand Down
23 changes: 20 additions & 3 deletions .helm/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
apiVersion: v2
name: cattr-server
name: server
description: Cattr is an open-source time-tracking solution for teams
type: application
# The chart is versioned independently of the application. Chart 0.2.x targets
# the Cattr 4.1 runtime image (Caddy + Octane + Reverb + Pebble, explicit roles).
version: 0.2.0-alpha.2
appVersion: "4.1.0-alpha.2"
version: 0.2.0
appVersion: "4.1.1"
kubeVersion: ">=1.27.0-0"
keywords:
- cattr
Expand All @@ -14,5 +14,22 @@ keywords:
- management
- laravel
home: https://cattr.app
icon: https://raw.githubusercontent.com/cattr-app/server-application/main/public/images/logo.svg
maintainers:
- name: Alex
email: inbox@alexv-pro.ru
- name: Cattr Team
email: hi@cattr.app
sources:
- https://github.com/cattr-app/server-application
annotations:
artifacthub.io/license: SSPL-1.0
# Keep the image tag equal to appVersion (checked by ci/release-metadata.sh).
artifacthub.io/images: |
- name: cattr
image: ghcr.io/cattr-app/server:4.1.1
artifacthub.io/links: |
- name: Documentation
url: https://cattr.app/manual
- name: Source
url: https://github.com/cattr-app/server-application
32 changes: 25 additions & 7 deletions .helm/MIGRATION-0.1-to-0.2.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
# Migrating from chart 0.1.x to 0.2.x

Chart 0.2 is a rewrite for the Cattr 4.1 runtime. An in-place `helm upgrade`
of the same release works, but **files are not migrated automatically** and
several values changed. Plan a short downtime.
Chart 0.2 is a rewrite for the Cattr 4.1 runtime. **Default names have changed**:
with release `cattr`, chart 0.2.0 now creates resources such as Deployment
`cattr` and PVC `cattr-storage`, rather than `cattr-cattr-server` and
`cattr-cattr-server-storage`. This is intentionally a breaking change.

An in-place upgrade is **not supported with the new naming defaults**. The
steps below use `nameOverride: cattr-server` explicitly to retain the legacy
resource names for an existing release. Adopting the new names requires a
separately planned migration of resources, Secrets, PVCs, and data. Uploaded
files are **not migrated automatically**; plan downtime.

## What changes

Expand Down Expand Up @@ -38,7 +45,7 @@ which MySQL 8.0 cannot open again (there is no in-place downgrade).
Built-in MySQL:

```sh
NS=cattr; REL=cattr; FULL=cattr-cattr-server # adjust: <release>-cattr-server
NS=cattr; REL=cattr; FULL=cattr-cattr-server # legacy default name (nameOverride=cattr-server); use your release's own name if it set nameOverride/fullnameOverride
kubectl -n $NS exec deploy/$FULL-mysql -- sh -c \
'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --single-transaction --routines --triggers "$MYSQL_DATABASE"' \
> cattr-backup.sql
Expand Down Expand Up @@ -86,10 +93,21 @@ kubectl -n $NS patch deployment $FULL --type=json \

## 5. Upgrade

Write a new values file (do not use `--reuse-values`). APP_KEY and the DB
credentials are read from the existing Secret when left empty:
Write a new values file (do not use `--reuse-values`) that keeps the names of
the existing release. APP_KEY and the DB credentials are read from the existing
Secret when left empty:

```yaml
# Names. The Deployment selector label `app.kubernetes.io/name` comes from
# `nameOverride` (default: `cattr`; the chart-name fallback was `cattr-server` in
# 0.1.x and is `server` now) and is immutable, so it must keep its old value:
# - default names in 0.1.x: `nameOverride: cattr-server`;
# - own `nameOverride`: keep that value;
# - own `fullnameOverride` only: keep it and ALSO set `nameOverride: cattr-server`.
# $FULL in the steps above is the `fullnameOverride` if there is one, otherwise
# the name the old release rendered.
nameOverride: cattr-server
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
# fullnameOverride: my-cattr # only if the release had it
cattr:
appUrl: https://cattr.example.com
reverbAppSecret: "<openssl rand -hex 32>" # new, required once
Expand All @@ -111,7 +129,7 @@ ingress: # if you used the 0.1.x Ingress
```

```sh
helm upgrade $REL ./.helm -n $NS -f values-0.2.yaml --wait --timeout 15m
helm upgrade $REL oci://ghcr.io/cattr-app/helm/server --version 0.2.0 -n $NS -f values-0.2.yaml --wait --timeout 15m
```

The `pre-upgrade` setup Job runs the migrations; then the Pod is recreated.
Expand Down
Loading
Loading