From 9e4b82075725236a709f07ce54bb0097a5d048fa Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 14 Apr 2025 13:19:55 +0200 Subject: [PATCH 1/4] configure bumpver for versoning --- .forgejo/workflows/build-deploy-prod.yaml | 63 +++++++++++++++++++++++ README.md | 29 ++++++++++- pyproject.toml | 18 +++++++ src/servala/__about__.py | 1 + uv.lock | 35 +++++++++++++ 5 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 .forgejo/workflows/build-deploy-prod.yaml create mode 100644 src/servala/__about__.py diff --git a/.forgejo/workflows/build-deploy-prod.yaml b/.forgejo/workflows/build-deploy-prod.yaml new file mode 100644 index 0000000..b314540 --- /dev/null +++ b/.forgejo/workflows/build-deploy-prod.yaml @@ -0,0 +1,63 @@ +name: Build and Deploy Production + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + container: catthehacker/ubuntu:act-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ vars.CONTAINER_REGISTRY }} + username: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} + password: ${{ secrets.CONTAINER_REGISTRY_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ vars.CONTAINER_REGISTRY }}/${{ vars.CONTAINER_IMAGE_NAME }}:latest + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + needs: build + runs-on: ubuntu-latest + container: catthehacker/ubuntu:act-latest + environment: + name: staging + url: https://staging.portal.servala.com/ + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Deploy to OpenShift + uses: docker://quay.io/appuio/oc:v4.16 + with: + entrypoint: /bin/bash + args: | + -c "oc login --token=${OPENSHIFT_TOKEN} --server=${OPENSHIFT_URL} && \ + oc -n ${NAMESPACE} apply --overwrite -k deployment/kustomize/overlays/staging && \ + oc -n ${NAMESPACE} rollout restart deployment/servala" + env: + NAMESPACE: ${{ vars.NAMESPACE_PORTAL_STAGING }} + KUBECONFIG: /tmp/kube_config + OPENSHIFT_TOKEN: ${{ secrets.OPENSHIFT_TOKEN_STAGING }} + OPENSHIFT_URL: ${{ secrets.OPENSHIFT_URL }} diff --git a/README.md b/README.md index ba6fdcd..56a39d7 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ The Servala Self-Service Portal +Latest release: 2025.04.14-0 + ## Documentation Available at https://docs.servala.com/. @@ -86,11 +88,34 @@ Then access it with http://localhost:8080/ and the Django admin with http://loca ## Deployment +Deployment files are in the `deployment/kustomize` folder and makes use of [Kustomize](https://kustomize.io/) to account for differences between the deployment stages. +Stages are configured with overlays in `deployment/kustomize/overlays/$environment`. + +### Staging + The code is automatically built and deployed on a push to the main branch. See `.forgejo/workflows/build-deploy-staging.yaml` for the actual workflow. -Deployment files are in the `deployment/kustomize` folder and makes use of [Kustomize](https://kustomize.io/) to account for differences between the deployment stages. -Stages are configured with overlays in `deployment/kustomize/overlays/$environment`. +### Production + +Building and deployment for production happens when a Git tag is pushed. + +### Versioning + +We're using `CalVer` as the versioning scheme. +The tool [bumpver](https://github.com/mbarkhau/bumpver) helps us to automate the process. + +To cut a new release run the following command to check what will happen: + +```bash +uv run bumpver update -d +``` + +The run the following command to create a release: + +```bash +uv run bumpver update +``` ## Maintenance and management commands diff --git a/pyproject.toml b/pyproject.toml index 27d42c7..d919325 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ [dependency-groups] dev = [ "black>=25.1.0", + "bumpver>=2024.1130", "coverage>=7.7.0", "djlint>=1.36.4", "flake8>=7.1.2", @@ -53,3 +54,20 @@ DJANGO_SETTINGS_MODULE = "servala.settings" addopts = "-p no:doctest -p no:pastebin -p no:nose --cov=./ --cov-report=term-missing:skip-covered" testpaths = "src/tests" pythonpath = "src" + +[tool.bumpver] +current_version = "2025.04.14-0" +version_pattern = "YYYY.0M.0D-INC0" +commit_message = "bump version {old_version} -> {new_version}" +tag_message = "{new_version}" +tag_scope = "default" +pre_commit_hook = "" +post_commit_hook = "" +commit = true +tag = true +push = true + +[tool.bumpver.file_patterns] +"pyproject.toml" = ['current_version = "{version}"'] +"src/servala/__about__.py" = ['^__version__ = "{version}"$'] +"README.md" = ['{version}'] diff --git a/src/servala/__about__.py b/src/servala/__about__.py new file mode 100644 index 0000000..e803502 --- /dev/null +++ b/src/servala/__about__.py @@ -0,0 +1 @@ +__version__ = "2025.04.14-0" diff --git a/uv.lock b/uv.lock index 0592903..ff9dc60 100644 --- a/uv.lock +++ b/uv.lock @@ -77,6 +77,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 }, ] +[[package]] +name = "bumpver" +version = "2024.1130" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama" }, + { name = "lexid" }, + { name = "toml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/a9/becf78cc86211bd2287114c4f990a3bed450816696f14810cc59d7815bb5/bumpver-2024.1130.tar.gz", hash = "sha256:74f7ebc294b2240f346e99748cc6f238e57b050999d7428db75d76baf2bf1437", size = 115102 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/34/57d038ae30374976ce4ec57db9dea95bf55d1b5543b35e77aa9ce3543198/bumpver-2024.1130-py2.py3-none-any.whl", hash = "sha256:8e54220aefe7db25148622f45959f7beb6b8513af0b0429b38b9072566665a49", size = 65273 }, +] + [[package]] name = "cachetools" version = "5.5.2" @@ -529,6 +544,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/10/9f8af3e6f569685ce3af7faab51c8dd9d93b9c38eba339ca31c746119447/kubernetes-32.0.1-py2.py3-none-any.whl", hash = "sha256:35282ab8493b938b08ab5526c7ce66588232df00ef5e1dbe88a419107dc10998", size = 1988070 }, ] +[[package]] +name = "lexid" +version = "2021.1006" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/60/0b/28a3f9abc75abbf1fa996eb2dd77e1e33a5d1aac62566e3f60a8ec8b8a22/lexid-2021.1006.tar.gz", hash = "sha256:509a3a4cc926d3dbf22b203b18a4c66c25e6473fb7c0e0d30374533ac28bafe5", size = 11525 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/e3/35764404a4b7e2021be1f88f42264c2e92e0c4720273559a62461ce64a47/lexid-2021.1006-py2.py3-none-any.whl", hash = "sha256:5526bb5606fd74c7add23320da5f02805bddd7c77916f2dc1943e6bada8605ed", size = 7587 }, +] + [[package]] name = "mccabe" version = "0.7.0" @@ -969,6 +993,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "black" }, + { name = "bumpver" }, { name = "coverage" }, { name = "djlint" }, { name = "flake8" }, @@ -1002,6 +1027,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "black", specifier = ">=25.1.0" }, + { name = "bumpver", specifier = ">=2024.1130" }, { name = "coverage", specifier = ">=7.7.0" }, { name = "djlint", specifier = ">=1.36.4" }, { name = "flake8", specifier = ">=7.1.2" }, @@ -1031,6 +1057,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415 }, ] +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, +] + [[package]] name = "tqdm" version = "4.67.1" From 771753a3cb0af5a20dfe8bea2dc019e018563a52 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 14 Apr 2025 15:09:43 +0200 Subject: [PATCH 2/4] add production deployment --- .forgejo/workflows/build-deploy-prod.yaml | 75 ++++++++++++++++--- .../overlays/production/ingress.yaml | 22 ++++++ .../overlays/production/kustomization.yaml | 13 ++++ .../production/portal-deployment.yaml | 14 ++++ 4 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 deployment/kustomize/overlays/production/ingress.yaml create mode 100644 deployment/kustomize/overlays/production/kustomization.yaml create mode 100644 deployment/kustomize/overlays/production/portal-deployment.yaml diff --git a/.forgejo/workflows/build-deploy-prod.yaml b/.forgejo/workflows/build-deploy-prod.yaml index b314540..2dda71e 100644 --- a/.forgejo/workflows/build-deploy-prod.yaml +++ b/.forgejo/workflows/build-deploy-prod.yaml @@ -2,8 +2,8 @@ name: Build and Deploy Production on: push: - branches: [main] - workflow_dispatch: + tags: + - "*" jobs: build: @@ -27,12 +27,27 @@ jobs: username: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} password: ${{ secrets.CONTAINER_REGISTRY_TOKEN }} + - name: Determine image tag + id: determine-tag + run: | + case "${{ github.ref }}" in + refs/tags/*) + TAG_NAME=${{ github.ref }} + TAG_NAME=${TAG_NAME##*/} + echo "::set-output name=tag::${TAG_NAME}" + ;; + *) + echo "Unsupported ref: ${{ github.ref }}" + exit 1 + ;; + esac + - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . push: true - tags: ${{ vars.CONTAINER_REGISTRY }}/${{ vars.CONTAINER_IMAGE_NAME }}:latest + tags: ${{ vars.CONTAINER_REGISTRY }}/${{ vars.CONTAINER_IMAGE_NAME }}:${{ steps.determine-tag.outputs.tag }} cache-from: type=gha cache-to: type=gha,mode=max @@ -41,23 +56,65 @@ jobs: runs-on: ubuntu-latest container: catthehacker/ubuntu:act-latest environment: - name: staging - url: https://staging.portal.servala.com/ + name: production + url: https://portal.servala.com/ steps: - name: Checkout repository uses: actions/checkout@v4 + - name: Determine image tag + id: determine-tag + run: | + case "${{ github.ref }}" in + refs/tags/*) + TAG_NAME=${{ github.ref }} + TAG_NAME=${TAG_NAME##*/} + echo "::set-output name=tag::${TAG_NAME}" + ;; + *) + echo "Unsupported ref: ${{ github.ref }}" + exit 1 + ;; + esac + - name: Deploy to OpenShift uses: docker://quay.io/appuio/oc:v4.16 with: entrypoint: /bin/bash args: | - -c "oc login --token=${OPENSHIFT_TOKEN} --server=${OPENSHIFT_URL} && \ - oc -n ${NAMESPACE} apply --overwrite -k deployment/kustomize/overlays/staging && \ + -c "set -e && oc login --token=${OPENSHIFT_TOKEN} --server=${OPENSHIFT_URL} && \ + pushd deployment/kustomize/overlays/production && \ + kustomize edit set image ${{ vars.CONTAINER_REGISTRY }}/${{ vars.CONTAINER_IMAGE_NAME }}:${{ steps.determine-tag.outputs.tag }} && \ + cat kustomization.yaml && popd && \ + oc -n ${NAMESPACE} apply --overwrite -k deployment/kustomize/overlays/production && \ oc -n ${NAMESPACE} rollout restart deployment/servala" env: - NAMESPACE: ${{ vars.NAMESPACE_PORTAL_STAGING }} + NAMESPACE: ${{ vars.NAMESPACE_PORTAL_PRODUCTION }} KUBECONFIG: /tmp/kube_config - OPENSHIFT_TOKEN: ${{ secrets.OPENSHIFT_TOKEN_STAGING }} + OPENSHIFT_TOKEN: ${{ secrets.OPENSHIFT_TOKEN_PRODUCTION }} + OPENSHIFT_URL: ${{ secrets.OPENSHIFT_URL }} + + - name: Verify deployment + uses: docker://quay.io/appuio/oc:v4.16 + with: + entrypoint: /bin/bash + args: | + -c "set -e && oc login --token=${OPENSHIFT_TOKEN} --server=${OPENSHIFT_URL} && \ + echo 'Waiting for deployment to complete...' && \ + oc -n ${NAMESPACE} rollout status deployment/servala --timeout=300s && \ + echo 'Checking pod status...' && \ + oc -n ${NAMESPACE} get pods -l app=servala && \ + READY_PODS=$(oc -n ${NAMESPACE} get pods -l app=servala -o jsonpath='{.items[*].status.containerStatuses[0].ready}' | grep -o 'true' | wc -l) && \ + TOTAL_PODS=$(oc -n ${NAMESPACE} get pods -l app=servala --no-headers | wc -l) && \ + echo \"Ready pods: $READY_PODS/$TOTAL_PODS\" && \ + if [ \"$READY_PODS\" -eq \"$TOTAL_PODS\" ]; then \ + echo '✅ Deployment verified successfully!' && exit 0; \ + else \ + echo '❌ Deployment verification failed!' && exit 1; \ + fi" + env: + NAMESPACE: ${{ vars.NAMESPACE_PORTAL_PRODUCTION }} + KUBECONFIG: /tmp/kube_config + OPENSHIFT_TOKEN: ${{ secrets.OPENSHIFT_TOKEN_PRODUCTION }} OPENSHIFT_URL: ${{ secrets.OPENSHIFT_URL }} diff --git a/deployment/kustomize/overlays/production/ingress.yaml b/deployment/kustomize/overlays/production/ingress.yaml new file mode 100644 index 0000000..149b3e9 --- /dev/null +++ b/deployment/kustomize/overlays/production/ingress.yaml @@ -0,0 +1,22 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + cert-manager.io/cluster-issuer: letsencrypt-production + name: servala +spec: + rules: + - host: portal.servala.com + http: + paths: + - pathType: Prefix + path: / + backend: + service: + name: servala + port: + number: 8080 + tls: + - hosts: + - portal.servala.com + secretName: ingress-cert diff --git a/deployment/kustomize/overlays/production/kustomization.yaml b/deployment/kustomize/overlays/production/kustomization.yaml new file mode 100644 index 0000000..d746bb1 --- /dev/null +++ b/deployment/kustomize/overlays/production/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +labels: + - includeSelectors: true + pairs: + app.kubernetes.io/instance: test + app.kubernetes.io/name: servala +resources: + - ../../base/portal + - ../../base/database + - ingress.yaml +patches: + - path: portal-deployment.yaml diff --git a/deployment/kustomize/overlays/production/portal-deployment.yaml b/deployment/kustomize/overlays/production/portal-deployment.yaml new file mode 100644 index 0000000..ce8c03a --- /dev/null +++ b/deployment/kustomize/overlays/production/portal-deployment.yaml @@ -0,0 +1,14 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: servala +spec: + template: + spec: + containers: + - name: servala + env: + - name: SERVALA_ENVIRONMENT + value: production + - name: SERVALA_ALLOWED_HOSTS + value: portal.servala.com From 4ec571aca7877805f9b244f2a09fcbaedba1b078 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 14 Apr 2025 15:10:35 +0200 Subject: [PATCH 3/4] allow to manually trigger workflow --- .forgejo/workflows/build-deploy-prod.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.forgejo/workflows/build-deploy-prod.yaml b/.forgejo/workflows/build-deploy-prod.yaml index 2dda71e..369272d 100644 --- a/.forgejo/workflows/build-deploy-prod.yaml +++ b/.forgejo/workflows/build-deploy-prod.yaml @@ -4,6 +4,7 @@ on: push: tags: - "*" + workflow_dispatch: jobs: build: From 8a7044cdc66f2a506e58ffd4153390ddfdd44c94 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 14 Apr 2025 15:12:06 +0200 Subject: [PATCH 4/4] note where to find workflow --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 56a39d7..2bfbb94 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ See `.forgejo/workflows/build-deploy-staging.yaml` for the actual workflow. ### Production Building and deployment for production happens when a Git tag is pushed. +See `.forgejo/workflows/build-deploy-prod.yaml` for the actual workflow. ### Versioning