Compare commits

...

24 Commits

Author SHA1 Message Date
niko 535a5fed0e feature/docker-build-arg (#50)
CI Main / Config load (push) Successful in 16s
CI Main / Latest versio (push) Successful in 15s
unit-tests Bats test report
CI Main / Bats tests (push) Successful in 1m12s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 1m19s
ci-docker-build-push Docker push 0.2.37
CI Main / Build & Push Docker (push) Successful in 37s
gitops/gitea-ci-library GitOps: 0.2.37
CI Main / GitOps (push) Successful in 34s
CI Main / Report Summary (push) Successful in 4s
CI Main / Move provider version tag (push) Successful in 10s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #50
2026-06-28 13:05:48 +03:00
niko f3abd42f17 Fix/git pages gitea reports 1 (#49)
CI Main / Config load (push) Successful in 23s
CI Gitea Reports Main / Config load (push) Successful in 23s
CI Gitea Reports Main / Latest version (push) Successful in 20s
CI Main / Latest versio (push) Successful in 21s
ci-helm-build-push Helm push 0.2.1
CI Gitea Reports Main / Build & Push Helm chart (push) Successful in 41s
unit-tests Bats test report
acc-tests Cucumber test report
CI Gitea Reports Main / Report Summary (push) Successful in 3s
ci-docker-build-push Docker push 0.2.36
CI Main / Build & Push Docker (push) Successful in 32s
CI Main / Report Summary (push) Successful in 3s
CI Main / Move provider version tag (push) Successful in 10s
gitops/gitea-ci-library/gitea-reports GitOps: gitea-reports 0.2.1
CI Gitea Reports Main / GitOps (push) Successful in 33s
CI Main / Cucumber tests (push) Successful in 1m25s
CI Main / Bats tests (push) Successful in 1m26s
gitops/gitea-ci-library GitOps: 0.2.36
CI Main / GitOps (push) Successful in 32s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #49
2026-06-28 09:25:41 +03:00
niko bd6ed5c2c2 Feature/git pages -> oma nginx perusteinen raporointipalvelu (#48)
CI Gitea Reports Main / Config load (push) Successful in 17s
CI Main / Config load (push) Successful in 20s
CI Gitea Reports Main / Latest version (push) Successful in 17s
CI Main / Latest versio (push) Successful in 19s
ci-helm-build-push Helm push 0.2.0
CI Gitea Reports Main / Build & Push Helm chart (push) Successful in 33s
gitops/gitea-ci-library/gitea-reports GitOps: gitea-reports 0.2.0
CI Gitea Reports Main / GitOps (push) Successful in 33s
CI Gitea Reports Main / Report Summary (push) Successful in 3s
CI Main / Bats tests (push) Failing after 1m16s
CI Main / Cucumber tests (push) Failing after 1m22s
CI Main / Build & Push Docker (push) Has been skipped
CI Main / GitOps (push) Has been skipped
CI Main / Move provider version tag (push) Has been skipped
CI Main / Report Summary (push) Successful in 3s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #48
2026-06-28 09:10:10 +03:00
niko dd9cdf70b6 Featuer/gitops extra command (#47)
CI Main / Config load (push) Successful in 1m48s
CI Main / Latest versio (push) Successful in 1m38s
unit-tests Bats test report
CI Main / Bats tests (push) Successful in 2m15s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 2m46s
ci-docker-build-push Docker push 0.2.35
CI Main / Build & Push Docker (push) Successful in 40s
gitops/gitea-ci-library GitOps: 0.2.35
CI Main / GitOps (push) Successful in 1m34s
CI Main / Report Summary (push) Successful in 3s
CI Main / Move provider version tag (push) Successful in 11s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #47
2026-06-27 05:20:09 +03:00
niko 848ba723e4 Fix/git pages rentetin create tests (#46)
CI Main / Config load (push) Successful in 3m12s
CI Git-Pages Main / Config load (push) Successful in 3m4s
CI Main / Latest versio (push) Successful in 30s
CI Git-Pages Main / Latest version (push) Successful in 27s
ci-helm-build-push Helm push 0.1.9
unit-tests Bats test report
CI Main / Bats tests (push) Successful in 1m56s
CI Git-Pages Main / Build & Push Helm chart (push) Successful in 1m19s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 3m47s
ci-docker-build-push Docker push 0.2.34
CI Main / Build & Push Docker (push) Successful in 1m14s
gitops/gitea-ci-library/git-pages GitOps: git-pages 0.1.9
CI Git-Pages Main / GitOps (push) Successful in 3m37s
gitops/gitea-ci-library GitOps: 0.2.34
CI Main / GitOps (push) Successful in 40s
CI Main / Report Summary (push) Successful in 4s
CI Main / Move provider version tag (push) Successful in 12s
CI Git-Pages Main / Report Summary (push) Successful in 4s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #46
2026-06-26 08:07:02 +03:00
niko 1978a995a8 Update README.md (#45)
CI Main / Config load (push) Successful in 23s
CI Main / Latest versio (push) Successful in 21s
CI Main / Bats tests (push) Successful in 1m34s
CI Main / Cucumber tests (push) Successful in 1m33s
ci-docker-build-push Docker push 0.2.33
CI Main / Build & Push Docker (push) Successful in 50s
gitops/gitea-ci-library GitOps: 0.2.33
CI Main / GitOps (push) Successful in 47s
CI Main / Report Summary (push) Successful in 7s
CI Main / Move provider version tag (push) Successful in 14s
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 1m5s
unit-tests Bats test report
CI Feature / Bats tests (push) Successful in 2m9s
acc-tests Cucumber test report
CI Feature / Cucumber tests (push) Successful in 2m53s
CI Feature / Report Summary (push) Successful in 7s
Reviewed-on: #45
2026-06-25 09:19:01 +03:00
niko d6343438a3 Feture/gitops7 (#44)
unit-tests Bats test report
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 1m44s
ci-docker-build-push Docker push 0.2.32
gitops/gitea-ci-library GitOps: 0.2.32
CI Main / Config load (push) Successful in 26s
CI Main / Latest versio (push) Successful in 21s
CI Main / Build & Push Docker (push) Has been skipped
CI Main / GitOps (push) Has been skipped
CI Main / Move provider version tag (push) Has been skipped
CI Main / Report Summary (push) Successful in 5s
CI Main / Bats tests (push) Has been skipped
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #44
2026-06-23 12:45:42 +03:00
niko ed2703b7d7 commit status kutsujalle gitops repoon (#43)
CI Main / Config load (push) Successful in 19s
CI Main / Latest versio (push) Successful in 19s
unit-tests Bats test report
CI Main / Bats tests (push) Successful in 1m33s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 1m45s
ci-docker-build-push Docker push 0.2.31
CI Main / Build & Push Docker (push) Successful in 44s
gitops/gitea-ci-library GitOps: 0.2.31
CI Main / GitOps (push) Successful in 35s
CI Main / Report Summary (push) Successful in 7s
CI Main / Move provider version tag (push) Successful in 13s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #43
2026-06-22 14:24:25 +03:00
niko dc4b331ea1 kutsujalle gitops commit (#42)
CI Main / Config load (push) Successful in 26s
CI Git-Pages Main / Latest version (push) Successful in 22s
CI Git-Pages Main / Config load (push) Successful in 25s
CI Main / Latest versio (push) Successful in 21s
ci-helm-build-push Helm push 0.1.8
CI Main / Bats tests (push) Successful in 1m38s
CI Git-Pages Main / GitOps (push) Successful in 1m24s
CI Git-Pages Main / Build & Push Helm chart (push) Successful in 46s
unit-tests Bats test report
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 1m59s
CI Git-Pages Main / Report Summary (push) Successful in 9s
ci-docker-build-push Docker push 0.2.30
CI Main / Build & Push Docker (push) Successful in 50s
CI Main / Move provider version tag (push) Successful in 14s
CI Main / GitOps (push) Successful in 41s
CI Main / Report Summary (push) Successful in 6s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #42
2026-06-22 13:57:14 +03:00
niko c06015cd9f poc logiikka takaisin (#41)
CI Main / Config load (push) Successful in 19s
CI Main / Latest versio (push) Successful in 17s
unit-tests Bats test report
CI Main / Bats tests (push) Successful in 1m30s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 1m43s
ci-docker-build-push Docker push 0.2.29
CI Main / Build & Push Docker (push) Successful in 44s
CI Main / GitOps (push) Successful in 39s
CI Main / Report Summary (push) Successful in 6s
CI Main / Move provider version tag (push) Successful in 15s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #41
2026-06-22 13:24:09 +03:00
niko 4c73433eab Update scripts/gitops-dispatch.sh (#40)
CI Main / Config load (push) Successful in 21s
CI Main / Latest versio (push) Successful in 19s
unit-tests Bats test report
CI Main / Bats tests (push) Successful in 1m23s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 1m43s
ci-docker-build-push Docker push 0.2.28
CI Main / Build & Push Docker (push) Successful in 33s
CI Main / GitOps (push) Failing after 18s
CI Main / Move provider version tag (push) Has been skipped
CI Main / Report Summary (push) Successful in 7s
Reviewed-on: #40
2026-06-22 11:19:56 +03:00
niko b9de1da855 testiä (#39)
ci-helm-build-push Helm push 0.1.7
CI Git-Pages Main / Build & Push Helm chart (push) Successful in 51s
CI Git-Pages Main / Report Summary (push) Successful in 6s
unit-tests Bats test report
CI Main / Bats tests (push) Successful in 1m40s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 1m48s
CI Git-Pages Main / Config load (push) Successful in 22s
CI Main / Config load (push) Successful in 24s
CI Main / Latest versio (push) Successful in 25s
CI Git-Pages Main / Latest version (push) Successful in 24s
CI Git-Pages Main / GitOps (push) Failing after 20s
ci-docker-build-push Docker push 0.2.27
CI Main / Build & Push Docker (push) Successful in 41s
CI Main / GitOps (push) Failing after 18s
CI Main / Move provider version tag (push) Has been skipped
CI Main / Report Summary (push) Successful in 6s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #39
2026-06-22 11:14:50 +03:00
niko 9f9af398cc clean (#38)
CI Git-Pages Main / Config load (push) Successful in 24s
CI Main / Config load (push) Successful in 26s
CI Git-Pages Main / Latest version (push) Successful in 22s
CI Main / Latest versio (push) Successful in 23s
ci-helm-build-push Helm push 0.1.6
CI Git-Pages Main / Build & Push Helm chart (push) Successful in 50s
unit-tests Bats test report
CI Main / Bats tests (push) Successful in 1m44s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 1m58s
ci-docker-build-push Docker push 0.2.26
CI Main / Build & Push Docker (push) Successful in 43s
CI Main / GitOps (push) Failing after 21s
CI Main / Move provider version tag (push) Has been skipped
CI Main / Report Summary (push) Successful in 6s
CI Git-Pages Main / GitOps (push) Failing after 18s
CI Git-Pages Main / Report Summary (push) Successful in 5s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #38
2026-06-22 10:55:58 +03:00
niko bc6bb78973 Feature/gitops (#37)
CI Main / Load example-gitea-env.conf to pipeline env (push) Successful in 34s
CI Git-Pages Main / Load git-pages.gitea-env.conf to pipeline env (push) Successful in 34s
CI Main / Check existing artifact (push) Successful in 22s
CI Git-Pages Main / Check existing artifact (push) Successful in 21s
ci-helm-build-push Helm push 0.1.5
CI Git-Pages Main / Build & Push Helm chart (push) Successful in 48s
unit-tests Bats test report
CI Main / Bats tests (push) Successful in 1m34s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 1m45s
CI Git-Pages Main / Update chart to the cluster (push) Failing after 0s
ci-docker-build-push Docker push 0.2.25
CI Git-Pages Main / Report Summary (push) Successful in 7s
CI Main / Build & Push Docker (push) Successful in 44s
CI Main / GitOps (push) Failing after 22s
CI Main / Move provider version tag (push) Has been skipped
CI Main / Report Summary (push) Successful in 6s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #37
2026-06-22 10:37:15 +03:00
niko a5947551d4 Fix/gitea commit status naming (#36)
CI Main / Load example-gitea-env.conf to pipeline env (push) Successful in 22s
CI Main / Check existing artifact (push) Successful in 20s
CI Git-Pages Main / Load git-pages.gitea-env.conf to pipeline env (push) Successful in 45s
CI Git-Pages Main / Check existing artifact (push) Successful in 31s
ci-helm-build-push Helm push 0.1.4
CI Main / Cucumber tests (push) Successful in 1m10s
CI Git-Pages Main / Build & Push Helm chart (push) Successful in 49s
CI Git-Pages Main / Report Summary (push) Successful in 5s
CI Main / Bats tests (push) Successful in 1m34s
ci-docker-build-push Docker push 0.2.21
CI Main / Build & Push Docker (push) Successful in 48s
CI Main / Report Summary (push) Successful in 6s
CI Main / Move provider version tag (push) Successful in 14s
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 33s
acc-tests Cucumber test report
CI Feature / Cucumber tests (push) Successful in 1m23s
unit-tests Bats test report
CI Feature / Bats tests (push) Successful in 1m34s
CI Feature / Report Summary (push) Successful in 5s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #36
2026-06-21 08:43:32 +03:00
niko 4910565547 docker ja helm build context root monorepo ja in folder build (#35)
CI Main / Load example-gitea-env.conf to pipeline env (push) Successful in 25s
CI Git-Pages Main / Load git-pages.gitea-env.conf to pipeline env (push) Successful in 25s
CI Main / Check existing artifact (push) Successful in 24s
CI Git-Pages Main / Check existing artifact (push) Successful in 23s
acc-tests Cucumber test report
ci-helm-build-push Helm chart 0.1.3
unit-tests Bats test report
CI Main / Bats tests (push) Successful in 1m34s
CI Main / Cucumber tests (push) Successful in 1m33s
CI Git-Pages Main / Build & Push Helm chart (push) Successful in 47s
CI Git-Pages Main / Report Summary (push) Successful in 7s
ci-docker-build-push Docker build & push 0.2.20 OK
CI Main / Build & Push Docker (push) Successful in 51s
CI Main / Report Summary (push) Successful in 6s
CI Main / Move provider version tag (push) Successful in 14s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #35
2026-06-21 06:08:22 +03:00
niko 2bef079d03 Fix/ci kontin no internet vaatimus (#34)
CI Main / Load example-gitea-env.conf to pipeline env (push) Successful in 22s
CI Main / Check existing artifact (push) Successful in 18s
acc-tests Cucumber test report
unit-tests Bats test report
CI Main / Cucumber tests (push) Successful in 1m25s
CI Main / Bats tests (push) Successful in 1m26s
ci-docker-build-push Docker build & push 0.2.19 OK
CI Main / Build & Push Docker (push) Successful in 47s
CI Main / Report Summary (push) Successful in 6s
CI Main / Move provider version tag (push) Successful in 16s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #34
2026-06-20 14:36:42 +03:00
niko 4f20f5ae2f estetään image karkaaminen docker.io kun parametria ei ole asetettu (#33)
CI Main / Load example-gitea-env.conf to pipeline env (push) Successful in 28s
CI Main / Check existing artifact (push) Successful in 21s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 57s
unit-tests Bats test report
CI Main / Bats tests (push) Failing after 2m13s
CI Main / Build & Push Docker (push) Has been skipped
CI Main / Move provider version tag (push) Has been skipped
CI Main / Report Summary (push) Successful in 6s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #33
2026-06-20 10:54:23 +03:00
niko bd93ef2f8f monorepo ci filter ohje tarkennus (#32)
CI Main / Load example-gitea-env.conf to pipeline env (push) Successful in 26s
CI Main / Check existing artifact (push) Successful in 23s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 54s
unit-tests Bats test report
CI Main / Bats tests (push) Successful in 1m52s
ci-docker-build-push Docker build & push 0.2.18 OK
CI Main / Build & Push Docker (push) Successful in 43s
CI Main / Report Summary (push) Successful in 6s
CI Main / Move provider version tag (push) Successful in 14s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #32
2026-06-19 14:13:14 +03:00
niko f06cb112d8 Fix/umbrella chart support (#31)
CI Main / Load example-gitea-env.conf to pipeline env (push) Successful in 20s
CI Main / Check existing artifact (push) Successful in 17s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 1m4s
unit-tests Bats test report
CI Main / Bats tests (push) Successful in 1m52s
ci-docker-build-push Docker build & push 0.2.17 OK
CI Main / Build & Push Docker (push) Successful in 41s
CI Main / Report Summary (push) Successful in 5s
CI Main / Move provider version tag (push) Successful in 13s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #31
2026-06-19 13:13:06 +03:00
niko d57f56f196 dependency update ennen paketointia (#30)
CI Git-Pages Main / Load git-pages.gitea-env.conf to pipeline env (push) Successful in 22s
CI Main / Load example-gitea-env.conf to pipeline env (push) Successful in 24s
CI Git-Pages Main / Check existing artifact (push) Successful in 22s
CI Main / Check existing artifact (push) Successful in 22s
ci-helm-build-push Helm chart 0.1.2
CI Git-Pages Main / Build & Push Helm chart (push) Successful in 42s
CI Git-Pages Main / Report Summary (push) Successful in 5s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 1m11s
unit-tests Bats test report
CI Main / Bats tests (push) Successful in 1m55s
ci-docker-build-push Docker build & push 0.2.16 OK
CI Main / Build & Push Docker (push) Successful in 31s
CI Main / Report Summary (push) Successful in 6s
CI Main / Move provider version tag (push) Successful in 12s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #30
2026-06-19 10:32:40 +03:00
niko 277c0f882d Update .gitea/workflows/git-pages.ci-main.yml (#29)
CI Main / Load example-gitea-env.conf to pipeline env (push) Successful in 21s
CI Git-Pages Main / Load git-pages.gitea-env.conf to pipeline env (push) Successful in 21s
CI Main / Check existing artifact (push) Successful in 19s
CI Git-Pages Main / Check existing artifact (push) Successful in 21s
ci-helm-build-push Helm chart 0.1.1
CI Git-Pages Main / Build & Push Helm chart (push) Successful in 46s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 1m12s
CI Git-Pages Main / Report Summary (push) Successful in 8s
unit-tests Bats test report
CI Main / Bats tests (push) Successful in 2m0s
ci-docker-build-push Docker build & push 0.2.15 OK
CI Main / Build & Push Docker (push) Successful in 42s
CI Main / Report Summary (push) Successful in 6s
CI Main / Move provider version tag (push) Successful in 14s
poistettu dev haaran trigger

Reviewed-on: #29
2026-06-19 10:14:32 +03:00
niko cc7f4f0976 ohjeita helm build-push (#28)
CI Main / Load example-gitea-env.conf to pipeline env (push) Successful in 21s
CI Main / Check existing artifact (push) Successful in 18s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 1m16s
unit-tests Bats test report
CI Main / Bats tests (push) Successful in 2m4s
ci-docker-build-push Docker build & push 0.2.15 OK
CI Main / Build & Push Docker (push) Successful in 54s
CI Main / Report Summary (push) Successful in 5s
CI Main / Move provider version tag (push) Successful in 15s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #28
2026-06-19 10:13:24 +03:00
niko 6621b3f336 Fix/helm build node (#27)
CI Git-Pages Main / Load git-pages.gitea-env.conf to pipeline env (push) Successful in 22s
CI Main / Load example-gitea-env.conf to pipeline env (push) Successful in 23s
CI Main / Check existing artifact (push) Successful in 21s
CI Git-Pages Main / Check existing artifact (push) Successful in 20s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 52s
ci-helm-build-push Helm chart 0.1.0
CI Git-Pages Main / Build & Push Helm chart (push) Successful in 43s
CI Git-Pages Main / Report Summary (push) Successful in 4s
unit-tests Bats test report
CI Main / Bats tests (push) Successful in 1m51s
ci-docker-build-push Docker build & push 0.2.14 OK
CI Main / Build & Push Docker (push) Successful in 31s
CI Main / Report Summary (push) Successful in 3s
CI Main / Move provider version tag (push) Successful in 11s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #27
2026-06-19 08:48:56 +03:00
92 changed files with 4063 additions and 1733 deletions
+20
View File
@@ -0,0 +1,20 @@
.git/
.gitignore
node_modules/
reports/
coverage/
.ai/
.cursor/
.vscode/
tmp/
.DS_Store
*.md
docs/
guides/
.simplecov
cucumber.js
package-lock.json
package.json
CURRENT_PROVIDER_VERSION
README.md
AGENTS.md
+7 -5
View File
@@ -23,7 +23,7 @@ on:
jobs: jobs:
build-push: build-push:
runs-on: ubuntu-latest runs-on: docker
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -33,20 +33,22 @@ jobs:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME || github.actor }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME || github.actor }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: | run: |
REGISTRY="${DOCKER_REGISTRY:?DOCKER_REGISTRY not set in conf}" if [ -z "${DOCKER_REGISTRY}" ]; then echo "ERROR: DOCKER_REGISTRY not set in conf"; exit 1; fi
REGISTRY="${DOCKER_REGISTRY}"
REGISTRY_HOST="${REGISTRY%%/*}"
DOCKERFILE="${{ inputs.dockerfile_path }}" DOCKERFILE="${{ inputs.dockerfile_path }}"
IMAGE_NAME="${{ inputs.image_name }}" IMAGE_NAME="${{ inputs.image_name }}"
TAG="${{ inputs.tag }}" TAG="${{ inputs.tag }}"
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ) NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
CONTEXT_DIR=$(dirname "${DOCKERFILE}")
docker build \ docker build \
--label "git.commit=${{ github.sha }}" \ --label "git.commit=${{ github.sha }}" \
--label "git.commitBy=${{ github.actor }}" \ --label "git.commitBy=${{ github.actor }}" \
--label "build.date=${NOW}" \ --label "build.date=${NOW}" \
-f "${DOCKERFILE}" \ -f "${DOCKERFILE}" \
-t "${IMAGE_NAME}:${TAG}" . -t "${IMAGE_NAME}:${TAG}" \
"${CONTEXT_DIR}"
REGISTRY_HOST="${REGISTRY%%/*}"
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}" FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
echo "Pushing ${FULL_IMAGE} ..." echo "Pushing ${FULL_IMAGE} ..."
+2 -2
View File
@@ -8,7 +8,7 @@ on:
secrets: secrets:
GITEA_TOKEN: GITEA_TOKEN:
required: true required: true
GIT_PAGES_PUBLISH_TOKEN: REPORTS_PUBLISH_TOKEN:
required: true required: true
outputs: outputs:
env_json: env_json:
@@ -19,7 +19,7 @@ on:
env: env:
CI_CONF_FILE: ${{ inputs.config_path }} CI_CONF_FILE: ${{ inputs.config_path }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GIT_PAGES_PUBLISH_TOKEN: ${{ secrets.GIT_PAGES_PUBLISH_TOKEN }} REPORTS_PUBLISH_TOKEN: ${{ secrets.REPORTS_PUBLISH_TOKEN }}
jobs: jobs:
parse-config: parse-config:
+34 -16
View File
@@ -8,6 +8,9 @@ on:
version: version:
required: true required: true
type: string type: string
build_args:
required: false
type: string
secrets: secrets:
GITEA_TOKEN: GITEA_TOKEN:
required: true required: true
@@ -32,7 +35,7 @@ concurrency:
jobs: jobs:
build-push: build-push:
runs-on: ubuntu-latest runs-on: docker
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -44,30 +47,44 @@ jobs:
env: env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME || github.actor }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME || github.actor }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
BUILD_ARGS_INPUT: ${{ inputs.build_args }}
run: | run: |
if [ -z "${DOCKER_REGISTRY}" ]; then echo "ERROR: DOCKER_REGISTRY not set in env.conf"; exit 1; fi
if [ -z "${DOCKER_IMAGE_NAME}" ]; then echo "ERROR: DOCKER_IMAGE_NAME not set in env.conf"; exit 1; fi
REGISTRY="${DOCKER_REGISTRY}"
IMAGE="${DOCKER_IMAGE_NAME}"
REGISTRY_HOST="${REGISTRY%%/*}"
BUILD_ARGS=""
if [ -n "$BUILD_ARGS_INPUT" ]; then
while IFS= read -r arg; do
[ -n "$arg" ] && BUILD_ARGS="$BUILD_ARGS --build-arg $arg"
done <<EOF
$BUILD_ARGS_INPUT
EOF
fi
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ) NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
docker build \ CONTEXT_DIR=$(dirname "${DOCKERFILE}")
docker build $BUILD_ARGS \
--label "git.commit=${{ github.sha }}" \ --label "git.commit=${{ github.sha }}" \
--label "git.commitBy=${{ github.actor }}" \ --label "git.commitBy=${{ github.actor }}" \
--label "build.date=${NOW}" \ --label "build.date=${NOW}" \
-f "${DOCKERFILE}" \ -f "${DOCKERFILE}" \
-t "${DOCKER_IMAGE_NAME}:${VERSION}" \ -t "${IMAGE}:${VERSION}" \
-t "${DOCKER_IMAGE_NAME}:latest" . -t "${IMAGE}:latest" \
"${CONTEXT_DIR}"
REGISTRY="${DOCKER_REGISTRY:?DOCKER_REGISTRY not set in env.conf}"
IMAGE="${DOCKER_IMAGE_NAME:?DOCKER_IMAGE_NAME not set in env.conf}"
REGISTRY_HOST="${REGISTRY%%/*}"
FULL_IMAGE="${REGISTRY}/${IMAGE}:${VERSION}" FULL_IMAGE="${REGISTRY}/${IMAGE}:${VERSION}"
echo "Pushing ${FULL_IMAGE} ..." echo "Pushing ${FULL_IMAGE} ..."
docker tag "${DOCKER_IMAGE_NAME}:${VERSION}" "$FULL_IMAGE" docker tag "${IMAGE}:${VERSION}" "$FULL_IMAGE"
echo "$DOCKER_PASSWORD" | docker login "$REGISTRY_HOST" -u "$DOCKER_USERNAME" --password-stdin echo "$DOCKER_PASSWORD" | docker login "$REGISTRY_HOST" -u "$DOCKER_USERNAME" --password-stdin
docker push "$FULL_IMAGE" docker push "$FULL_IMAGE"
FULL_LATEST="${REGISTRY}/${IMAGE}:latest" FULL_LATEST="${REGISTRY}/${IMAGE}:latest"
echo "Pushing ${FULL_LATEST} ..." echo "Pushing ${FULL_LATEST} ..."
docker tag "${DOCKER_IMAGE_NAME}:latest" "$FULL_LATEST" docker tag "${IMAGE}:latest" "$FULL_LATEST"
docker push "$FULL_LATEST" docker push "$FULL_LATEST"
docker logout "$REGISTRY_HOST" docker logout "$REGISTRY_HOST"
@@ -79,14 +96,15 @@ jobs:
if [ -n "${DOCKER_UI_URL:-}" ] && [ -n "${VERSION:-}" ]; then if [ -n "${DOCKER_UI_URL:-}" ] && [ -n "${VERSION:-}" ]; then
CONTAINER_URL="${DOCKER_UI_URL}/${DOCKER_IMAGE_NAME}/${VERSION}" CONTAINER_URL="${DOCKER_UI_URL}/${DOCKER_IMAGE_NAME}/${VERSION}"
fi fi
bash .ci/scripts/report-status.sh success "Docker build & push ${VERSION} OK" ci-docker-build-push "" "$CONTAINER_URL" DIR=$(dirname "${DOCKERFILE}")
if [ "$DIR" != "." ]; then
- name: Report status FAILURE bash .ci/scripts/report-status.sh success "${DIR}: Docker push ${VERSION}" "${DIR}-ci-docker-build-push" "" "$CONTAINER_URL"
if: failure() else
run: bash .ci/scripts/report-status.sh failure "Docker build & push ${VERSION} FAILED" ci-docker-build-push bash .ci/scripts/report-status.sh success "Docker push ${VERSION}" ci-docker-build-push "" "$CONTAINER_URL"
fi
tag-commit: tag-commit:
runs-on: ubuntu-latest runs-on: docker
needs: [build-push] needs: [build-push]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
+2 -2
View File
@@ -12,14 +12,14 @@ on:
secrets: secrets:
GITEA_TOKEN: GITEA_TOKEN:
required: true required: true
GIT_PAGES_PUBLISH_TOKEN: REPORTS_PUBLISH_TOKEN:
required: true required: true
env: env:
GITEA_API_URL: ${{ fromJson(inputs.env_json).GITEA_API_URL }} GITEA_API_URL: ${{ fromJson(inputs.env_json).GITEA_API_URL }}
GIT_PAGES_URL: ${{ fromJson(inputs.env_json).GIT_PAGES_URL }} GIT_PAGES_URL: ${{ fromJson(inputs.env_json).GIT_PAGES_URL }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GIT_PAGES_PUBLISH_TOKEN: ${{ secrets.GIT_PAGES_PUBLISH_TOKEN }} REPORTS_PUBLISH_TOKEN: ${{ secrets.REPORTS_PUBLISH_TOKEN }}
jobs: jobs:
bats: bats:
+3 -3
View File
@@ -8,18 +8,18 @@ on:
cucumber-node-image: cucumber-node-image:
required: false required: false
type: string type: string
default: gitea.app.keskikuja.site/niko/ci-cucumber:latest default: gitea.app.keskikuja.site/niko/ci-cucumber:with-python
secrets: secrets:
GITEA_TOKEN: GITEA_TOKEN:
required: true required: true
GIT_PAGES_PUBLISH_TOKEN: REPORTS_PUBLISH_TOKEN:
required: true required: true
env: env:
GITEA_API_URL: ${{ fromJson(inputs.env_json).GITEA_API_URL }} GITEA_API_URL: ${{ fromJson(inputs.env_json).GITEA_API_URL }}
GIT_PAGES_URL: ${{ fromJson(inputs.env_json).GIT_PAGES_URL }} GIT_PAGES_URL: ${{ fromJson(inputs.env_json).GIT_PAGES_URL }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GIT_PAGES_PUBLISH_TOKEN: ${{ secrets.GIT_PAGES_PUBLISH_TOKEN }} REPORTS_PUBLISH_TOKEN: ${{ secrets.REPORTS_PUBLISH_TOKEN }}
jobs: jobs:
cucumber: cucumber:
+1
View File
@@ -4,3 +4,4 @@ DOCKER_REGISTRY=gitea.app.keskikuja.site/niko
DOCKER_IMAGE_NAME=gitea-ci-library-test-image DOCKER_IMAGE_NAME=gitea-ci-library-test-image
DOCKER_UI_URL=https://gitea.app.keskikuja.site/niko/-/packages/container DOCKER_UI_URL=https://gitea.app.keskikuja.site/niko/-/packages/container
#DOCKERFILE=Dockerfile.platform #DOCKERFILE=Dockerfile.platform
+22 -5
View File
@@ -7,14 +7,14 @@ on:
jobs: jobs:
load-config: load-config:
name: Load example-gitea-env.conf to pipeline env name: Config load
uses: niko/gitea-ci-library/.gitea/workflows/config-provider.yml@main uses: niko/gitea-ci-library/.gitea/workflows/config-provider.yml@main
secrets: inherit secrets: inherit
with: with:
config_path: .gitea/workflows/example-gitea-env.conf config_path: .gitea/workflows/example-gitea-env.conf
check-version: check-version:
name: Check existing artifact name: Latest versio
needs: [load-config] needs: [load-config]
uses: niko/gitea-ci-library/.gitea/workflows/check-version.yml@main uses: niko/gitea-ci-library/.gitea/workflows/check-version.yml@main
secrets: inherit secrets: inherit
@@ -39,7 +39,7 @@ jobs:
with: with:
env_json: ${{ needs.load-config.outputs.env_json }} env_json: ${{ needs.load-config.outputs.env_json }}
build-push: docker-build-push:
name: Build & Push Docker name: Build & Push Docker
needs: [load-config, check-version, bats, cucumber] needs: [load-config, check-version, bats, cucumber]
if: needs.check-version.outputs.artifact_exists != 'true' if: needs.check-version.outputs.artifact_exists != 'true'
@@ -48,19 +48,36 @@ jobs:
with: with:
env_json: ${{ needs.load-config.outputs.env_json }} env_json: ${{ needs.load-config.outputs.env_json }}
version: ${{ needs.check-version.outputs.version }} version: ${{ needs.check-version.outputs.version }}
build_args: |
VERSION=${{ needs.check-version.outputs.version }}
FOO=bar
docker-gitops:
name: GitOps
needs: [docker-build-push, load-config, check-version]
uses: niko/gitea-ci-library/.gitea/workflows/gitops-dispatch.yml@main
secrets: inherit
with:
env_json: ${{ needs.load-config.outputs.env_json }}
version: ${{ needs.check-version.outputs.version }}
GITOPS_FILE: dev/values.yaml
GITOPS_YQ_TPL: '.service.tag = "{{VERSION}}"'
GITOPS_REPO: niko/gitea-ci-gitops-tests
report-summary: report-summary:
name: Report Summary name: Report Summary
needs: [load-config, build-push] needs: [load-config, check-version, docker-build-push, docker-gitops]
if: always() if: always()
uses: niko/gitea-ci-library/.gitea/workflows/report-summary.yml@main uses: niko/gitea-ci-library/.gitea/workflows/report-summary.yml@main
with: with:
env_json: ${{ needs.load-config.outputs.env_json }} env_json: ${{ needs.load-config.outputs.env_json }}
suites: bats cucumber suites: bats cucumber
gitops: |
${{ needs.docker-gitops.outputs.summary }}
tag-maintenance: tag-maintenance:
name: Move provider version tag name: Move provider version tag
needs: [build-push] needs: [docker-gitops]
if: success() if: success()
uses: niko/gitea-ci-library/.gitea/workflows/tag-maintenance.yml@main uses: niko/gitea-ci-library/.gitea/workflows/tag-maintenance.yml@main
secrets: inherit secrets: inherit
@@ -1,25 +1,23 @@
name: CI Git-Pages Main name: CI Gitea Reports Main
on: on:
push: push:
branches: branches:
- main - main
- feature/helm-chart
paths: paths:
- git-pages/** - gitea-reports/**
- .gitea/workflows/helm-build-push.yml - ".gitea/workflows/gitea-reports.*"
- .gitea/workflows/git-pages.*
workflow_dispatch: workflow_dispatch:
jobs: jobs:
load-config: load-config:
name: Load git-pages.gitea-env.conf to pipeline env name: Config load
uses: niko/gitea-ci-library/.gitea/workflows/config-provider.yml@main uses: niko/gitea-ci-library/.gitea/workflows/config-provider.yml@main
secrets: inherit secrets: inherit
with: with:
config_path: .gitea/workflows/git-pages.gitea-env.conf config_path: .gitea/workflows/gitea-reports.gitea-env.conf
check-version: check-version:
name: Check existing artifact name: Latest version
needs: [load-config] needs: [load-config]
uses: niko/gitea-ci-library/.gitea/workflows/check-version.yml@main uses: niko/gitea-ci-library/.gitea/workflows/check-version.yml@main
secrets: inherit secrets: inherit
@@ -35,13 +33,27 @@ jobs:
with: with:
env_json: ${{ needs.load-config.outputs.env_json }} env_json: ${{ needs.load-config.outputs.env_json }}
version: ${{ needs.check-version.outputs.version }} version: ${{ needs.check-version.outputs.version }}
chart_path: git-pages chart_path: gitea-reports
chart-gitops:
name: GitOps
needs: [helm-push, load-config, check-version]
uses: niko/gitea-ci-library/.gitea/workflows/gitops-dispatch.yml@main
secrets: inherit
with:
env_json: ${{ needs.load-config.outputs.env_json }}
version: ${{ needs.check-version.outputs.version }}
GITOPS_FILE: dev/Chart.yaml
GITOPS_YQ_TPL: '(.dependencies[] | select(.name == "gitea-reports") | .version) = "{{VERSION}}"'
GITOPS_REPO: niko/gitea-ci-gitops-tests
report-summary: report-summary:
name: Report Summary name: Report Summary
needs: [load-config, helm-push] needs: [load-config, check-version, helm-push, chart-gitops]
if: always() if: always()
uses: niko/gitea-ci-library/.gitea/workflows/report-summary.yml@main uses: niko/gitea-ci-library/.gitea/workflows/report-summary.yml@main
with: with:
env_json: ${{ needs.load-config.outputs.env_json }} env_json: ${{ needs.load-config.outputs.env_json }}
suites: "" suites: ""
gitops: |
${{ needs.chart-gitops.outputs.summary }}
@@ -1,5 +1,5 @@
GITEA_API_URL=https://gitea.app.keskikuja.site GITEA_API_URL=https://gitea.app.keskikuja.site
HELM_REGISTRY=gitea.app.keskikuja.site/niko HELM_REGISTRY=gitea.app.keskikuja.site/niko
HELM_UI_URL=https://gitea.app.keskikuja.site/niko/-/packages/container HELM_UI_URL=https://gitea.app.keskikuja.site/niko/-/packages/container
GIT_TAG_PREFIX=git-pages/ GIT_TAG_PREFIX=gitea-reports/
VERSION_FILE=git-pages/Chart.yaml VERSION_FILE=gitea-reports/Chart.yaml
+202
View File
@@ -0,0 +1,202 @@
name: GitOps Dispatch
on:
workflow_call:
inputs:
env_json:
required: true
type: string
version:
required: true
type: string
GITOPS_FILE:
required: true
type: string
GITOPS_YQ_TPL:
required: true
type: string
GITOPS_REPO:
required: true
type: string
GITOPS_EXTRA_CMD:
required: false
type: string
secrets:
GITOPS_DISPATCH_TOKEN:
required: true
outputs:
summary:
description: "Pipe-format: component|version|status|commit_sha|repo"
value: ${{ jobs.dispatch.outputs.summary }}
env:
GITOPS_VERSION: ${{ inputs.version }}
GITOPS_FILE: ${{ inputs.GITOPS_FILE }}
GITOPS_YQ_TPL: ${{ inputs.GITOPS_YQ_TPL }}
GITOPS_REPO: ${{ inputs.GITOPS_REPO }}
GITOPS_SOURCE_REPO: ${{ github.repository }}
GITOPS_SOURCE_COMMIT: ${{ github.sha }}
GITEA_API_URL: ${{ fromJson(inputs.env_json).GITEA_API_URL }}
GITOPS_TAG_PREFIX: ${{ fromJson(inputs.env_json).GIT_TAG_PREFIX || '' }}
GITOPS_EXTRA_CMD: ${{ inputs.GITOPS_EXTRA_CMD || '' }}
GITOPS_WORKFLOW: gitops-service.yaml
GITOPS_DISPATCH_TIMEOUT: 30
jobs:
dispatch:
runs-on: ubuntu-latest
outputs:
summary: ${{ steps.summary.outputs.GITOPS_SUMMARY }}
steps:
- name: Generate dispatch_id
id: gen
run: |
ID=$(date +%s | md5sum | head -c 8)
echo "dispatch_id=$ID" >> "$GITHUB_OUTPUT"
- name: Resolve commit author
id: author
run: |
echo "name=$(git log -1 --format='%an')" >> "$GITHUB_OUTPUT"
echo "email=$(git log -1 --format='%ae')" >> "$GITHUB_OUTPUT"
- name: Dispatch to GitOps repo
env:
GITEA_TOKEN: ${{ secrets.GITOPS_DISPATCH_TOKEN }}
run: |
INPUTS=$(jq -nc \
--arg dispatch_id "${{ steps.gen.outputs.dispatch_id }}" \
--arg author_name "${{ steps.author.outputs.name }}" \
--arg author_email "${{ steps.author.outputs.email }}" \
--arg file "$GITOPS_FILE" \
--arg yq_tpl "$GITOPS_YQ_TPL" \
--arg version "$GITOPS_VERSION" \
--arg source_repo "$GITOPS_SOURCE_REPO" \
--arg source_commit "$GITOPS_SOURCE_COMMIT" \
--arg git_tag_prefix "${GITOPS_TAG_PREFIX:-}" \
--arg extra_cmd "${GITOPS_EXTRA_CMD:-}" \
'{dispatch_id: $dispatch_id, author_name: $author_name, author_email: $author_email, file: $file, yq_tpl: $yq_tpl, version: $version, source_repo: $source_repo, source_commit: $source_commit, git_tag_prefix: $git_tag_prefix, extra_cmd: $extra_cmd}')
curl -s -X POST \
"${GITEA_API_URL}/api/v1/repos/${GITOPS_REPO}/actions/workflows/${GITOPS_WORKFLOW}/dispatches" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -nc --arg ref "main" --argjson inputs "$INPUTS" '{ref: "main", inputs: $inputs}')"
- uses: actions/checkout@v4
with:
repository: niko/gitea-ci-library
path: .ci
- name: Poll for completion
id: poll
env:
GITEA_TOKEN: ${{ secrets.GITOPS_DISPATCH_TOKEN }}
run: |
ID="${{ steps.gen.outputs.dispatch_id }}"
TIMEOUT_MINUTES="${GITOPS_DISPATCH_TIMEOUT:-30}"
POLL_INTERVAL=10
START_TIME=$(date +%s)
TIMEOUT_SECONDS=$((TIMEOUT_MINUTES * 60))
echo "Polling for run with dispatch_id=$ID"
while [ -z "$RUN_ID" ]; do
NOW=$(date +%s)
ELAPSED=$((NOW - START_TIME))
if [ "$ELAPSED" -ge "$TIMEOUT_SECONDS" ]; then
echo "ERROR: Timeout waiting for run to appear" >&2
exit 124
fi
RUNS_RESP=$(curl -s --connect-timeout 5 --max-time 10 \
"${GITEA_API_URL}/api/v1/repos/${GITOPS_REPO}/actions/runs?event=workflow_dispatch&limit=10" \
-H "Authorization: token $GITEA_TOKEN")
RUN_ID=$(echo "$RUNS_RESP" | jq -r --arg id "$ID" \
'[.workflow_runs[] | select(.display_title | contains($id))] | .[0].id // empty')
[ -z "$RUN_ID" ] && sleep "$POLL_INTERVAL"
done
echo "Run found: id=$RUN_ID"
while true; do
NOW=$(date +%s)
ELAPSED=$((NOW - START_TIME))
if [ "$ELAPSED" -ge "$TIMEOUT_SECONDS" ]; then
echo "ERROR: Timeout waiting for completion" >&2
exit 124
fi
RUN_RESP=$(curl -s --connect-timeout 5 --max-time 10 \
"${GITEA_API_URL}/api/v1/repos/${GITOPS_REPO}/actions/runs/${RUN_ID}" \
-H "Authorization: token $GITEA_TOKEN")
STATUS=$(echo "$RUN_RESP" | jq -r '.status // "running"')
CONCLUSION=$(echo "$RUN_RESP" | jq -r '.conclusion // ""')
echo " status=$STATUS conclusion=$CONCLUSION"
if [ "$STATUS" = "completed" ]; then
if [ "$CONCLUSION" = "success" ]; then
echo "GitOps workflow completed successfully"
# 1. List recent commits from GitOps repo
COMMITS=$(curl -s --connect-timeout 5 --max-time 10 \
"${GITEA_API_URL}/api/v1/repos/${GITOPS_REPO}/commits?sha=main&limit=10" \
-H "Authorization: token $GITEA_TOKEN")
# 2. Find commit by message: "gitops: update version to X.Y.Z"
SEARCH_MSG="gitops: update version to ${GITOPS_VERSION}"
GITOPS_COMMIT=$(echo "$COMMITS" | jq -r \
--arg msg "$SEARCH_MSG" \
'[.[] | select(.commit.message | contains($msg))] | .[0].sha // empty')
# 3. If not found → fail
if [ -z "$GITOPS_COMMIT" ]; then
echo "ERROR: no matching GitOps commit found for version ${GITOPS_VERSION}" >&2
exit 1
fi
echo "GITOPS_COMMIT=$GITOPS_COMMIT" >> "$GITHUB_OUTPUT"
echo "$GITOPS_COMMIT" > /tmp/gitops-commit
exit 0
else
echo "ERROR: GitOps workflow failed with conclusion=$CONCLUSION" >&2
exit 1
fi
fi
sleep "$POLL_INTERVAL"
done
- name: GitOps summary
id: summary
if: always()
run: |
STATUS="failure"
GITOPS_SHA=""
if [ -f /tmp/gitops-commit ]; then
STATUS="success"
GITOPS_SHA=$(cat /tmp/gitops-commit)
fi
COMPONENT="${GITOPS_TAG_PREFIX:-${GITOPS_FILE}}"
echo "GITOPS_SUMMARY=${COMPONENT}|${GITOPS_VERSION}|${STATUS}|${GITOPS_SHA}|${GITOPS_REPO}" >> "$GITHUB_OUTPUT"
- name: Set commit status
if: success()
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
GITOPS_SHA=$(cat /tmp/gitops-commit)
PREFIX="${GITOPS_TAG_PREFIX%/}"
if [ -n "$PREFIX" ]; then
CONTEXT="gitops/$(basename "${GITOPS_SOURCE_REPO}")/${PREFIX}"
DESCRIPTION="GitOps: ${PREFIX} ${GITOPS_VERSION}"
else
CONTEXT="gitops/$(basename "${GITOPS_SOURCE_REPO}")"
DESCRIPTION="GitOps: ${GITOPS_VERSION}"
fi
ROOT_REPO="${GITOPS_SOURCE_REPO}" ROOT_COMMIT="${GITOPS_SOURCE_COMMIT}" \
bash .ci/scripts/report-status.sh success \
"$DESCRIPTION" "$CONTEXT" "" \
"${GITEA_API_URL}/${GITOPS_REPO}/commit/${GITOPS_SHA}"
+26 -5
View File
@@ -9,9 +9,11 @@ on:
required: true required: true
type: string type: string
chart_path: chart_path:
required: true
type: string
extra_dependency_paths:
required: false required: false
type: string type: string
default: '.'
secrets: secrets:
GITEA_TOKEN: GITEA_TOKEN:
required: true required: true
@@ -26,7 +28,7 @@ env:
HELM_REGISTRY: ${{ fromJson(inputs.env_json).HELM_REGISTRY || '' }} HELM_REGISTRY: ${{ fromJson(inputs.env_json).HELM_REGISTRY || '' }}
HELM_UI_URL: ${{ fromJson(inputs.env_json).HELM_UI_URL || '' }} HELM_UI_URL: ${{ fromJson(inputs.env_json).HELM_UI_URL || '' }}
GIT_TAG_PREFIX: ${{ fromJson(inputs.env_json).GIT_TAG_PREFIX || '' }} GIT_TAG_PREFIX: ${{ fromJson(inputs.env_json).GIT_TAG_PREFIX || '' }}
CHART_PATH: ${{ inputs.chart_path }} CHART_FILE: ${{ fromJson(inputs.env_json).VERSION_FILE || 'Chart.yaml' }}
VERSION: ${{ inputs.version }} VERSION: ${{ inputs.version }}
concurrency: concurrency:
@@ -39,15 +41,30 @@ jobs:
container: container:
image: alpine/helm:3.19.0 image: alpine/helm:3.19.0
steps: steps:
- name: Install Node.js for actions/checkout
# COMPROMISE: Requires internet access.
# Does NOT work in air-gapped environments.
# Replace with a custom image (e.g., extending alpine/helm + nodejs) if needed.
run: apk add --no-cache nodejs
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
repository: niko/gitea-ci-library repository: niko/gitea-ci-library
path: .ci path: .ci
- name: Resolve extra subchart dependencies
if: inputs.extra_dependency_paths != ''
run: |
for path in $(echo "${{ inputs.extra_dependency_paths }}" | tr ',' '\n'); do
helm dependency update "${path}"
done
- name: Package Helm chart - name: Package Helm chart
run: | run: |
helm package "${CHART_PATH}" \ CHART_DIR=$(dirname "${CHART_FILE}")
helm dependency update "${CHART_DIR}"
helm package "${CHART_DIR}" \
--version "${VERSION}" \ --version "${VERSION}" \
--app-version "${VERSION}" \ --app-version "${VERSION}" \
--destination /tmp/helm-packages --destination /tmp/helm-packages
@@ -67,9 +84,13 @@ jobs:
- name: Report status with UI link - name: Report status with UI link
if: success() && env.HELM_UI_URL != '' if: success() && env.HELM_UI_URL != ''
run: | run: |
CHART_NAME=$(grep '^name:' "${CHART_PATH}/Chart.yaml" | awk '{print $2}') CHART_NAME=$(grep '^name:' "${CHART_FILE}" | awk '{print $2}')
UI_URL="${HELM_UI_URL}/${CHART_NAME}/${VERSION}" UI_URL="${HELM_UI_URL}/${CHART_NAME}/${VERSION}"
bash .ci/scripts/report-status.sh success "Helm chart ${VERSION}" ci-helm-build-push "" "$UI_URL" if [ "${CHART_PATH}" != "." ] && [ -n "${CHART_PATH}" ]; then
bash .ci/scripts/report-status.sh success "${CHART_PATH}: Helm push ${VERSION}" "${CHART_PATH}-ci-helm-build-push" "" "$UI_URL"
else
bash .ci/scripts/report-status.sh success "Helm push ${VERSION}" ci-helm-build-push "" "$UI_URL"
fi
tag-commit: tag-commit:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+24 -1
View File
@@ -9,6 +9,10 @@ on:
required: true required: true
type: string type: string
description: Space-separated suite names published to git-pages description: Space-separated suite names published to git-pages
gitops:
required: false
type: string
description: 'Pipe-separated rows: component|version|status|commit_sha|repo'
env: env:
GIT_PAGES_URL: ${{ fromJson(inputs.env_json).GIT_PAGES_URL }} GIT_PAGES_URL: ${{ fromJson(inputs.env_json).GIT_PAGES_URL }}
@@ -21,7 +25,7 @@ jobs:
shell: bash shell: bash
run: | run: |
SHA8="${GITHUB_SHA:0:8}" SHA8="${GITHUB_SHA:0:8}"
BASE="${GIT_PAGES_URL}/${GITHUB_REPOSITORY}/reports/${SHA8}" BASE="${GIT_PAGES_URL}/${GITHUB_REPOSITORY}/${GITHUB_REF_NAME}/${SHA8}"
{ {
echo "## Test Reports" echo "## Test Reports"
@@ -32,3 +36,22 @@ jobs:
echo "| ${suite} | [View report](${BASE}/${suite}/) |" echo "| ${suite} | [View report](${BASE}/${suite}/) |"
done done
} >> "${GITHUB_STEP_SUMMARY}" } >> "${GITHUB_STEP_SUMMARY}"
if [ -n "${{ inputs.gitops }}" ]; then
GITEA_URL="${{ fromJson(inputs.env_json).GITEA_API_URL }}"
{
echo ""
echo "## GitOps updates"
echo ""
echo "| Component | Version | Status | GitOps commit |"
echo "|-----------|---------|--------|--------------|"
echo '${{ inputs.gitops }}' | while IFS='|' read -r comp ver status sha repo; do
[ -z "$comp" ] && continue
if [ -n "$sha" ]; then
echo "| $comp | $ver | $status | [link]($GITEA_URL/$repo/commit/$sha) |"
else
echo "| $comp | $ver | $status | — |"
fi
done
} >> "${GITHUB_STEP_SUMMARY}"
fi
+3
View File
@@ -1 +1,4 @@
FROM alpine:latest FROM alpine:latest
ARG VERSION=unknown
ARG FOO=unknown
RUN mkdir -p /app && echo "$VERSION" > /app/version.txt && echo "$FOO" > /app/foo.txt
+1 -1
View File
@@ -1,6 +1,6 @@
FROM node:22 FROM node:22
RUN apt-get update -qq && \ RUN apt-get update -qq && \
apt-get install -y -qq --no-install-recommends lsof jq && \ apt-get install -y -qq --no-install-recommends lsof jq python3 && \
apt-get clean && \ apt-get clean && \
rm -rf /var/lib/apt/lists/* && \ rm -rf /var/lib/apt/lists/* && \
npm install -g @cucumber/cucumber npm install -g @cucumber/cucumber
+77 -15
View File
@@ -4,6 +4,8 @@ Reusable workflow -kirjasto Gitea Actionsille. Lisätietoja: [docs/](docs/)
**Consumer-käyttöönotto:** [skills/consumer-pipelines/SKILL.md](skills/consumer-pipelines/SKILL.md) — pipeline-standardit ja säännöt consumer-projekteille **Consumer-käyttöönotto:** [skills/consumer-pipelines/SKILL.md](skills/consumer-pipelines/SKILL.md) — pipeline-standardit ja säännöt consumer-projekteille
**GitOps-päivitys:** [skills/gitops-update/SKILL.md](skills/gitops-update/SKILL.md) — GitOps-repon job-template, dispatch ja token-ohjeet
**Single repo & monorepo:** Kirjasto toimii molemmissa. Monorepo-tuki **Single repo & monorepo:** Kirjasto toimii molemmissa. Monorepo-tuki
polkusuodatuksella, komponenttikohtaisilla versioilla ja git-tägien polkusuodatuksella, komponenttikohtaisilla versioilla ja git-tägien
etuliitteillä — jokainen komponentti julkaistaan itsenäisesti omassa etuliitteillä — jokainen komponentti julkaistaan itsenäisesti omassa
@@ -149,58 +151,102 @@ ja sitä kautta Gitea ei tarvitse päivityksessä mitään temppuja.
Päivityksen jälkeen muista tappaa pod (käynnistyy automaattisesti uudelleen), että lataa varmasti kaikki uudesta. Sillä ConfigMap tms eivät lataudu Päivityksen jälkeen muista tappaa pod (käynnistyy automaattisesti uudelleen), että lataa varmasti kaikki uudesta. Sillä ConfigMap tms eivät lataudu
mikäli pod jatkaa ajamista. mikäli pod jatkaa ajamista.
Klusterissa on kaksi StatefulSetiä, joilla on eri labelit:
- **`act-runner`** — yleisrunneri (label `ubuntu-latest`). DinD-sidecar on olemassa mutta idle — `require_docker: false` estää runneria käyttämästä sitä.
- **`act-runner-docker`** — Docker-buildien runneri (label `docker`). DinD on aktiivinen, `require_docker: true`.
Vain `docker-build-push.yml` ja `ci-container-build-push.yml` käyttävät `docker`-labelia.
Kaikki muut workflowt (testit, lintit, helm-publish, gitops-dispatch) ajetaan `ubuntu-latest`-runnerilla.
Steppien suoritus ei mene Docker Daemonin läpi, joten konekielisiä kontteja luodaan suoraan K8s-runtimella.
```bash ```bash
helm repo add gitea https://dl.gitea.com/charts helm repo add gitea https://dl.gitea.com/charts
helm repo update helm repo update
# 1. Yleisrunneri (DinD idle)
helm upgrade --install act-runner gitea/actions \ helm upgrade --install act-runner gitea/actions \
--set enabled=true \ --set enabled=true \
--set giteaRootURL="$GITEA_URL" \ --set giteaRootURL="$GITEA_URL" \
--set existingSecret=act-runner-token \ --set existingSecret=act-runner-token \
--set existingSecretKey=token \ --set existingSecretKey=token \
--set statefulset.replicas=3 \ --set statefulset.replicas=2 \
--set statefulset.runner.tag=1.0.8 \ --set statefulset.runner.tag=1.0.8 \
--set statefulset.dind.tag=29.5.2-dind \ --set statefulset.dind.tag=29.5.2-dind \
--set statefulset.dind.resources.requests.memory=250Mi \
--set statefulset.dind.resources.limits.memory=750Mi \
--set-string 'statefulset.runner.config=log: --set-string 'statefulset.runner.config=log:
level: info level: info
cache: cache:
enabled: false enabled: true
container: container:
require_docker: true require_docker: false' \
docker_timeout: 300s' \
--namespace "$GITEA_ACTIONS_NAMESPACE" \ --namespace "$GITEA_ACTIONS_NAMESPACE" \
--create-namespace --create-namespace
# 2. Docker-runner (DinD)
helm upgrade --install act-runner-docker gitea/actions \
--set enabled=true \
--set giteaRootURL="$GITEA_URL" \
--set existingSecret=act-runner-token \
--set existingSecretKey=token \
--set statefulset.replicas=1 \
--set statefulset.runner.tag=1.0.8 \
--set statefulset.dind.tag=29.5.2-dind \
--set statefulset.dind.resources.requests.memory=250Mi \
--set statefulset.dind.resources.limits.memory=750Mi \
--set-string 'statefulset.runner.config=log:
level: info
cache:
enabled: true
container:
require_docker: true
docker_timeout: 300s
runner:
labels:
- "docker:docker://catthehacker/ubuntu:act-latest"' \
--namespace "$GITEA_ACTIONS_NAMESPACE"
``` ```
path escapes from parent -bugi korjattiin Docker 29.5.2:ssa. Tämän teko aikana default on 29.5.1 — juuri tämän alle jäävä versio. `path escapes from parent` -bugi korjattiin Docker 29.5.2:ssa. Tämän teko aikana default on 29.5.1 — juuri tämän alle jäävä versio.
Oletus-lokitaso on `debug` — suositeltu `info`. Näkee jobien aloitukset ja valmistumiset ilman konttikerrosten purkua (Downloading/Extracting-spämmiä). `debug` on tarpeen vain vianselvityksessä. Oletus-lokitaso on `debug` — suositeltu `info`. Näkee jobien aloitukset ja valmistumiset ilman konttikerrosten purkua (Downloading/Extracting-spämmiä). `debug` on tarpeen vain vianselvityksessä.
`cache.enabled: true` nappaa image-cachen käyttöön — ilman sitä jokainen ajo lataa konttikuvat uudestaan.
#### Docker (DinD) #### Docker (DinD)
Helm chart deployaa DinD:n init-sidecarina (`docker:dind` samassa podissa). > **Huomio:** Gitea 1.26.x ei tue vielä label-pohjaista runner-valintaa.
`require_docker: true` kytkee jobit siihen — erillistä DinD-asennusta ei tarvita. > `runs-on: ubuntu-latest`-jobi saattaa päätyä `docker`-labeliselle runnerille.
> Bugi on tunnettu Gitean FAQ:ssa — korjaus tulossa myöhemmässä versiossa.
> Katso: [docs.gitea.com/usage/actions/faq](https://docs.gitea.com/usage/actions/faq)
**DinD-tag pinottu:** `29.5.2-dind` (ei chart-oletusta). Docker 29.5.1 aiheuttaa act-runnerissa Helm chart deployaa DinD:n init-sidecarina (`docker:dind` samassa podissa).
`path escapes from parent` -virheen job-kontin käynnistyksessä. Molemmissa StatefulSetissä on DinD-sidecar, mutta:
- **`act-runner`**: `require_docker: false` → runner ei käytä DinD:tä lainkaan, steppien suoritus menee suoraan K8s-runtimella
- **`act-runner-docker`**: `require_docker: true` → runner luo steppikontit DinD:n kautta (tarvitaan `docker build` -komentoja varten)
**DinD-tag pinottu:** `29.5.2-dind` molemmissa (ei chart-oletusta `29.5.1-dind`). Docker 29.5.1 aiheuttaa act-runnerissa
`path escapes from parent` -virheen job-kontin käynnistyksessä sekä `mkdirat var/run: file exists` -virheen tiedostojen kopioinnissa.
Maven/npm-ajot käyttävät vain workflow'n `container:`-imagea; DinD tarvitaan vasta Docker-buildissä. Maven/npm-ajot käyttävät vain workflow'n `container:`-imagea; DinD tarvitaan vasta Docker-buildissä.
### 3. Varmista ### 5. Varmista
```bash ```bash
kubectl get pods -n gitea-actions kubectl get pods -n gitea-actions
# → act-runner-runner-0 Running # → act-runner-runner-0 Running
# → act-runner-docker-runner-0 Running
kubectl exec -n gitea-actions act-runner-runner-0 -c dind -- docker version kubectl exec -n gitea-actions act-runner-docker-runner-0 -c dind -- docker version
# → Server Version: 29.5.2 (tai uudempi) # → Server Version: 29.5.2 (tai uudempi)
``` ```
Gitean puolella runner ilmestyy Active-tilaan pienellä viiveellä: Gitean puolella runnerit ilmestyvät Active-tilaan pienellä viiveellä:
``` ```
Site Admin → Actions → Runners (tai Org → Settings → Actions → Runners) Site Admin → Actions → Runners (tai Org → Settings → Actions → Runners)
# → act-runner-runner-0 Active ubuntu-latest # → act-runner-runner-0 Active ubuntu-latest
# → act-runner-docker-runner-0 Active docker
``` ```
Tämän jälkeen `.gitea/workflows/ci.yml` triggeröityy automaattisesti pushista. Tämän jälkeen `.gitea/workflows/ci.yml` triggeröityy automaattisesti pushista.
@@ -218,9 +264,11 @@ Consumer-repossa on oltava seuraavat asetukset:
| Secret | Kuvaus | | Secret | Kuvaus |
|--------|--------| |--------|--------|
| `GIT_PAGES_PUBLISH_TOKEN` | Git-pages-palvelimen BasicAuth-token. Nimi on lukittu — tämä tarkka nimi vaaditaan. | | `REPORTS_PUBLISH_TOKEN` | Git-pages-palvelimen BasicAuth-token. Nimi on lukittu — tämä tarkka nimi vaaditaan. |
`GITEA_TOKEN` on Gitean sisäinen secret (`secrets.GITEA_TOKEN`), joka on automauttisesti saatavilla — sitä ei tarvitse erikseen luoda. `GITEA_TOKEN` on Gitean automaattisesti jokaiselle workflow-runille generoima token (`secrets.GITEA_TOKEN`). Se on scopeutettu **siihen repoon**, jossa workflow ajaa — ei toimi toiseen repoon dispatchaukseen eikä toisen repon commit-statusin asettamiseen. Ei tarvitse erikseen luoda.
Jos workflow tarvitsee oikeuksia **toiseen** repoon (esim. dispatch GitOps-repoon), tarvitaan manuaalinen token. Katso [skills/gitops-update/SKILL.md](skills/gitops-update/SKILL.md).
### Config-tiedosto (`.gitea/workflows/gitea-env.conf`) ### Config-tiedosto (`.gitea/workflows/gitea-env.conf`)
@@ -252,10 +300,21 @@ GIT_PAGES_URL=https://ci-reports.example.com
Jokaisen jobin alussa `ci-validate.sh` tarkistaa: Jokaisen jobin alussa `ci-validate.sh` tarkistaa:
- `.gitea/workflows/gitea-env.conf` on olemassa ja sen arvot ovat validit - `.gitea/workflows/gitea-env.conf` on olemassa ja sen arvot ovat validit
- `GITEA_TOKEN` ja `GIT_PAGES_PUBLISH_TOKEN` on asetettu - `GITEA_TOKEN` ja `REPORTS_PUBLISH_TOKEN` on asetettu
Jos validointi epäonnistuu, job keskeytyy exit-koodilla 1 ja Gitean commit-status näyttää epäonnistumisen linkkinä lokiin. Jos validointi epäonnistuu, job keskeytyy exit-koodilla 1 ja Gitean commit-status näyttää epäonnistumisen linkkinä lokiin.
### GitOps-päivitys
Artifact buildin jälkeen voidaan dispatchata GitOps-repoon, joka päivittää
konfiguraatiotiedoston (esim. Chart.yaml version) ja pushaa muutoksen.
Kaksi skriptiä:
- `scripts/dispatch-workflow.sh` — lähettää workflow_dispatch-pyynnön ja pollaa valmistumista
- `scripts/gitops-update.sh` — kloonaa, päivittää yq:llä, committaa ja pushaa
Tarkka asennus: [skills/gitops-update/SKILL.md](skills/gitops-update/SKILL.md)
### Muuta ### Muuta
| Muuttuja | Kuvaus | | Muuttuja | Kuvaus |
@@ -264,4 +323,7 @@ Jos validointi epäonnistuu, job keskeytyy exit-koodilla 1 ja Gitean commit-stat
| `existingSecret` | Kubernetes secretin nimi, jossa token | | `existingSecret` | Kubernetes secretin nimi, jossa token |
| `existingSecretKey` | Avain secretin sisällä | | `existingSecretKey` | Avain secretin sisällä |
| `statefulset.dind.tag` | DinD-image tag (`29.5.2-dind` minimi) | | `statefulset.dind.tag` | DinD-image tag (`29.5.2-dind` minimi) |
| `statefulset.dind.resources.requests.memory` | DinD muistirequest (suositus `250Mi`) |
| `statefulset.dind.resources.limits.memory` | DinD muistilimitti (suositus `750Mi`) |
| `statefulset.runner.labels` | Mukautetut labelit | | `statefulset.runner.labels` | Mukautetut labelit |
+1 -1
View File
@@ -26,7 +26,7 @@ Consumer:
- Omistaa pipeline-logiikan (mitä testejä ajetaan, missä järjestyksessä) - Omistaa pipeline-logiikan (mitä testejä ajetaan, missä järjestyksessä)
- Luo raportit omiin polkuihinsa (`reports/{sha8}/{step}/`) - Luo raportit omiin polkuihinsa (`reports/{sha8}/{step}/`)
- Luo `.meta`-tiedostot per step - Luo `.meta`-tiedostot per step
- Määrittelee Gitea-secretit (`GIT_PAGES_PUBLISH_TOKEN`, `GITEA_TOKEN`) - Määrittelee Gitea-secretit (`REPORTS_PUBLISH_TOKEN`, `GITEA_TOKEN`)
### Provider → Consumer (mitä provider tarjoaa) ### Provider → Consumer (mitä provider tarjoaa)
+2 -1
View File
@@ -37,7 +37,7 @@ kuuluu `git-pages/docs/`-alle, ei juuren `docs/`-kansioon.
| `git-pages/` | Raporttien hostaus (Helm-chartti) | | `git-pages/` | Raporttien hostaus (Helm-chartti) |
| `tests/` | Bats-testit skripteille | | `tests/` | Bats-testit skripteille |
### Provider workflowt (5 kpl) ### Provider workflowt (6 kpl)
| Workflow | Input | Output | Kuvaus | | Workflow | Input | Output | Kuvaus |
|---|---|---|---| |---|---|---|---|
@@ -46,6 +46,7 @@ kuuluu `git-pages/docs/`-alle, ei juuren `docs/`-kansioon.
| `docker-build-push.yml` | `env_json`, `version` | — | Buildaa Docker-imagen, puskea rekisteriin, tagittaa commitin. | | `docker-build-push.yml` | `env_json`, `version` | — | Buildaa Docker-imagen, puskea rekisteriin, tagittaa commitin. |
| `ci-container-build-push.yml` | `env_json`, `dockerfile_path`, `image_name`, `tag` | — | Buildaa CI-työkalukontin, puskea rekisteriin. Ei versiointia eikä git-tägäystä. | | `ci-container-build-push.yml` | `env_json`, `dockerfile_path`, `image_name`, `tag` | — | Buildaa CI-työkalukontin, puskea rekisteriin. Ei versiointia eikä git-tägäystä. |
| `report-summary.yml` | `env_json`, `suites` | — | Generoi `GITHUB_STEP_SUMMARY`-taulukon raporttilinkeillä (Gitea 1.27+) | | `report-summary.yml` | `env_json`, `suites` | — | Generoi `GITHUB_STEP_SUMMARY`-taulukon raporttilinkeillä (Gitea 1.27+) |
| `helm-build-push.yml` | `env_json`, `version` | — | Pakkaa + puskea Helm chartin OCI-registryyn, tagittaa commitin. **Tekninen velka:** asentaa node.js:n runtime-vaiheessa (`apk add --no-cache nodejs` ennen checkouttia) koska `alpine/helm`-kontissa ei ole nodea. Rikkoo Offline Container -periaatetta. Ratkaistaan myöhemmin: proper multi-tool CI-kontti (helm + nodejs + git) docker hubiin. Ei consumerin ongelma. |
### Example-tiedostot (consumer-referenssi) ### Example-tiedostot (consumer-referenssi)
+1 -1
View File
@@ -96,7 +96,7 @@ Avainkomponentit:
- **workflow `env:`** — ainoa tapa jakaa arvot kaikkiin jobeihin. - **workflow `env:`** — ainoa tapa jakaa arvot kaikkiin jobeihin.
`fromJson(inputs.env_json).KEY` purkaa yksittäiset arvot ilman toistoa. `fromJson(inputs.env_json).KEY` purkaa yksittäiset arvot ilman toistoa.
- **Per-job `env:`** — sisältää vain secretit (`GITEA_TOKEN`, - **Per-job `env:`** — sisältää vain secretit (`GITEA_TOKEN`,
`GIT_PAGES_PUBLISH_TOKEN`), ei config-arvoja. `REPORTS_PUBLISH_TOKEN`), ei config-arvoja.
## 5. Pipeline Provides All Dependencies ## 5. Pipeline Provides All Dependencies
+9 -3
View File
@@ -60,10 +60,16 @@ Salaisuudet eivät ole `.conf`-tiedostossa. Ne määritellään Gitean
organization/repository secrets -mekanismissa ja välitetään workflowlle organization/repository secrets -mekanismissa ja välitetään workflowlle
`secrets: inherit` -direktiivillä. `secrets: inherit` -direktiivillä.
**`secrets.GITEA_TOKEN` on Gitean automaattisesti generoima token,
scopeutuu siihen repoon jossa workflow ajaa.** Se ei oikeuta
dispatchaamaan toiseen repoon eikä kirjoittamaan toisen repon
commit-statusta. Cross-repo-operaatioihin tarvitaan manuaalinen
org-tason token.
| Secret | Pakollinen | Käyttäjä | | Secret | Pakollinen | Käyttäjä |
|---|---|---| |---|---|---|
| `GITEA_TOKEN` | Kyllä | `report-status.sh`, `check-version.yml`, `docker-build-push.yml` | | `GITEA_TOKEN` | Kyllä | `report-status.sh`, `check-version.yml`, `docker-build-push.yml`, `gitops-update.sh` (GitOps-repossa) |
| `GIT_PAGES_PUBLISH_TOKEN` | Kyllä | `publish-git-pages.sh`, `config-provider.yml` (validointi) | | `REPORTS_PUBLISH_TOKEN` | Kyllä | `publish-git-pages.sh`, `config-provider.yml` (validointi) |
| `DOCKER_USERNAME` | Ei | `docker-build-push.yml` (oletus: `github.actor`, ei pakollinen kaikissa registryissä) | | `DOCKER_USERNAME` | Ei | `docker-build-push.yml` (oletus: `github.actor`, ei pakollinen kaikissa registryissä) |
| `DOCKER_PASSWORD` | Kyllä | `docker-build-push.yml` | | `DOCKER_PASSWORD` | Kyllä | `docker-build-push.yml` |
@@ -82,7 +88,7 @@ JSON-muotoisen `env_json`:n.
- `.conf`-tiedosto on olemassa - `.conf`-tiedosto on olemassa
- Jokaisella `KEY=VALUE`-rivillä on arvo (ei tyhjää) - Jokaisella `KEY=VALUE`-rivillä on arvo (ei tyhjää)
- URL-tyyppiset avaimet alkavat `http://` tai `https://` - URL-tyyppiset avaimet alkavat `http://` tai `https://`
- Pakolliset secretit (`GITEA_TOKEN`, `GIT_PAGES_PUBLISH_TOKEN`) on asetettu - Pakolliset secretit (`GITEA_TOKEN`, `REPORTS_PUBLISH_TOKEN`) on asetettu
Kutsu: Kutsu:
```yaml ```yaml
+28 -211
View File
@@ -1,227 +1,44 @@
# Design Rationale — Gitea Actions CI -kirjasto # Design Rationale — git-pages → Nginx + PV
> Miksi kirjasto on rakennettu näin. Arvot, periaatteet ja reunaehdot, joiden > Päivitetty: 2026-06-28 — git-pages korvattu Nginx + upload-sidecar -arkkitehtuurilla.
> varaan arkkitehtuuri nojaa.
>
> Tämä dokumentti on **normatiivinen** — arkkitehtuurin on noudatettava näitä
> periaatteita. Jos ehdotettu muutos on ristiriidassa rationalen kanssa,
> rationalen on muututtava ensin.
--- ## Miksi git-pages poistettiin
## Miksi tämä projekti on olemassa Codeberg git-pages storage v2 pakottaa kaikki tiedostot yhteen `.index`-protobufiin per site. Raja on 1MB kovakoodattu. Monorepo täytti tämän — uusia raportteja ei voinut julkaista.
Mikropalveluarkkitehtuurissa jokainen palvelu tarvitsee CI-putken: testit, ## Uusi arkkitehtuuri
laatutarkistukset, buildin, kontituksen ja julkaisun. Ilman jaettua
kirjastoa jokainen tiimi kopioi saman YAML-boilerplaten, tekee omat
virheensä ja ylläpitää omaa versiotaan. Ajan myötä putket ajautuvat erilleen
— toisessa on `shell: bash`, toisessa ei; toinen käyttää `set -o pipefail`,
toinen kadottaa exit-koodin `tee`:hen.
Tämä kirjasto on se mitä kopioidaan. Se tarjoaa valmiit, testatut, - **Nginx** palvelee staattisia tiedostoja suoraan PV:ltä
dokumentoidut rakennuspalikat joista jokainen tiimi kokoaa oman putkensa. - **Upload-sidecar** (busybox httpd + CGI) ottaa vastaan PUT + tar.gz ja purkaa PV:lle
Palikat ovat Gitea Actionsin `uses:`-direktiivillä kutsuttavia reusable - **URL = FS-polku** — `/{owner}/{repo}/{branch}/{sha8}/{suite}/`
workflow'ta — ei asennusta, ei runtime-riippuvuutta, ei versiopäivityksiä - **Branch raakana URL:ssa** — `feature/x` FS:ssä = `feature/x/`
projekteihin. - **Linkki on aina 1-suuntainen** — gitea → raportti
- **GITEA_API_URL poistuu** publish-skriptin pakollisista env-varista
--- ## Ratkaisun periaatteet
## Suunnitteluperiaatteet ### 1. Ei välikerrosta — tiedostot suoraan PV:llä
### 1. Palikka-arkkitehtuuri: pieniä, vaihdettavia, yhden vastuun workflow'ta git-pagesin `.index`-protobuf poistuu. Tiedostot ovat suoraan PV:llä kansiorakenteessa. Nginx palvelee niitä suoraan.
Jokainen provider-workflow tekee yhden asian: ### 2. Yksinkertainen upload-sidecar
| Workflow | Vastuu | Python HTTP-serveri ottaa vastaan PUT-pyynnöt ja purkaa tar.gz:n oikeaan paikkaan. Ei monimutkaista API:a, ei tietokantaa.
|---|---|
| `config-provider.yml` | Lataa ja validoi konfiguraatio |
| `check-version.yml` | Tarkistaa onko commit buildattu, laskee version |
| `docker-build-push.yml` | Buildaa, puskea ja tagittaa kontin |
Mikään workflow ei kutsu toista provider-workflowta. Consumer ### 3. Julkaisu ja luku eri porteilla
— siis mikropalvelun oma pipeline-tiedosto — on ainoa paikka joka
tietää mitä palikoita tarvitaan ja missä järjestyksessä.
**Miksi:** Tämä on sama periaate kuin Unix-putkissa tai mikropalveluissa: Eri portit mahdollistavat eri Traefik-säännöt: julkaisu vaatii BasicAuthin, luku on julkinen.
pieniä, itsenäisiä komponentteja jotka tekevät yhden asian hyvin.
Consumer voi vaihtaa yhden palikan toiseen — esimerkiksi Docker-buildin
tilalle Maven-paketoinnin — ilman että muut palikat muuttuvat.
Ratkaisu ei ole se että kaikki ajetaan, vaan se että jokainen tiimi
valitsee mitä tarvitsee. Monoliittinen "kaikki yhdessä" -workflow
pakottaisi jokaisen tiimin ajamaan tarpeettomia vaiheita.
### 2. Gitea ensin — hyödynnä alustaa, älä taistele sitä vastaan ### 4. Retention lukee PV:tä suoraan
Gitea Actions tarjoaa kolme asiaa ilmaiseksi: Ei HTTP API:a retentionia varten. Skripti listaa kansiot, tarkistaa branchit Giteasta, poistaa vanhat.
1. **Jobien visuaalinen status** — jokainen jobi näkyy automaattisesti ## Rajapinnat
commit-näkymässä checkmarkilla, spinnerillä tai ristillä.
2. **Cross-job riippuvuudet**`needs` hoitaa virheiden propagointin:
jos edeltävä jobi feilaa, riippuvat jobit skipataan.
3. **Reusable workflow -jakelu**`uses: org/repo/.gitea/workflows/file.yml@v1`
on natiivisti versioitu, skopattu ja välimuistitettu.
Kirjasto käyttää näitä kaikkia. Ei omaa tilakonetta, ei custom | Suunta | Protokolla | Auth | Kuvaus |
action -runtimea, ei ulkoista orkestraattoria. |--------|------------|------|--------|
| Julkaisija → Traefik | HTTPS PUT | BasicAuth `publish` | tar.gz → upload-sidecar |
**Esimerkki:** Tool-jobit eivät kutsu commit-status API:a lainkaan. | Selain → Traefik | HTTPS GET/HEAD | — | HTML-raportti nginxiltä |
Gitean oma job-status riittää — `success`/`failure`/`running` näkyy | Retention → Gitea | HTTPS GET | PAT | branch-tarkistus |
automaattisesti. API:a käytetään vain kun tarvitaan **custom-linkki** | Traefik → nginx | HTTP :8080 | — | sisäverkko |
(testiraporttiin tai Docker registryyn), jota natiivistaatus ei tarjoa. | Traefik → upload | HTTP :8081 | — | sisäverkko (BasicAuth tarkistettu) |
Tämä linjaus on dokumentoitu ADR 0004 ja 0007:ssä.
### 3. Status näkyy siellä missä työ tehdään — Git-commitissa
Kehittäjä työskentelee Gitissä. `git log`, `git blame`, PR-näkymä —
nämä ovat päivittäiset työkalut. CI-statuksen kuuluu näkyä siellä,
ei erillisessä dashboardissa.
Gitea Actionsin natiivi job-status tekee tämän automaattisesti:
jokainen commit näyttää välittömästi mitkä jobit on ajettu ja millä
tuloksella. Testiraportteihin pääsee yhdellä klikkauksella commitin
status-kuvakkeesta — koska `report-status.sh` asettaa `target_url`:n
osoittamaan suoraan HTML-raporttiin git-pagesissa.
Tämä ei ole kosmeettinen yksityiskohta. Se on devops-käytännön
ydin: palautesilmukka on lyhin mahdollinen. Commit → build → status
näkyy samassa näkymässä jossa kehittäjä jo on.
### 4. Exit-koodi on ainoa totuus
CI-putken jokaisen `run`-stepin onnistuminen määräytyy **vain ja
ainoastaan** exit-koodin perusteella. Ei tiedoston olemassaolon, ei
stdout-tulosteen, ei arvauksen. `0` = ok, kaikki muu = ei ok.
Tämä kuulostaa itsestään selvältä, mutta YAML-pipelineissa se rikkoutuu
helposti. Pipe (`|`) `tee`:hen syö exit-koodin. Tiedoston olemassaolon
tarkistus (`[ -f results.xml ]`) ei kerro testien läpimenosta.
**Käytännössä:** Jokainen `run`-steppi ottaa exit-koodin talteen
`$?`-muuttujaan ennen kuin mikään muu komento ehtii muuttaa sitä,
ja stepin viimeinen rivi on `exit ${EXIT}`. Pipeä ei käytetä
työvaiheen viimeisenä komentona. Ks. ADR 0008.
### 5. Pienin mahdollinen pinta-ala
Jokainen ylimääräinen riippuvuus on ylimääräinen vikaantumispiste.
Kirjaston ainoat riippuvuudet:
- Gitea Actions (alusta)
- `bash`, `curl`, `jq` (ubuntu-latest runnerissa valmiina)
- Docker (runnerissa valmiina)
- git-pages (raporttien hostaus, erillinen palvelu)
Ei Pythonia, ei Node.js:ää ajonaikaisesti (testit omissa konteissaan).
Ei tietokantaa. Ei ulkoista tilanhallintaa. Kirjasto on joukko
YAML-tiedostoja ja shell-skriptejä — samat työkalut jotka jokainen
devops-ihminen jo osaa.
### 6. Konfiguraatio repoon, salaisuudet Giteaan
Projektikohtainen konfiguraatio (`.gitea/workflows/gitea-env.conf`)
asuu mikropalvelun omassa repossa. Kehittäjä omistaa sen — hän tietää
mikä on Docker-imagen nimi, mihin registryyn puskea, mikä on
testiympäristön URL.
Salaisuudet (tokenit, salasanat) elävät Gitean secrets-mekanismissa,
eivät repon tiedostoissa. `secrets: inherit` välittää ne providerin
workflow'hun ilman että consumerin tarvitsee tietää mitä salaisuuksia
mikäkin provider tarvitsee.
Poikkeus: infra-tason asetukset (`GIT_PAGES_URL`, `GITEA_API_URL`)
ovat Gitean organization secrets/variables -mekanismissa. Ne eivät
ole repokohtaisia.
### 7. Consumer omistaa orkestroinnin, provider tarjoaa palikat
Tämä on kirjaston tärkein arkkitehtuurinen päätös (ADR 0005).
Provider (`gitea-ci-library`) ei tiedä mitä testejä ajetaan, missä
järjestyksessä, tai millä branchilla. Se tarjoaa kolme reusable
workflow'ta ja joukon skriptejä.
Consumer (mikropalvelun `example-feature.yml` / `example-main.yml`)
päättää:
- Mitkä palikat kutsutaan
- Missä järjestyksessä (`needs`)
- Millä branch-ehdoilla (`if`)
- Mitkä testikontit käytetään (input-parametrit)
Tämä on tarkoituksellinen vallanjako. Provider ei voi tietää jokaisen
tiimin tarpeita — eikä sen pidäkään. Consumer ei voi muuttaa providerin
sisäistä toteutusta — eikä sen pidäkään. Rajapinta on `workflow_call` ja
se on molemmille osapuolille selvä.
### 8. Branch-kohtainen reititys, ei yhtä kaikille
Eri brancheilla on eri tavoite:
- **Feature-haara:** Onko koodi laadukasta? → testit, validointi
- **Main-haara:** Onko tästä versiosta jo artifakti? Jos ei →
testit + build + push + tag. Jos on → ei tehdä mitään (tai
jatketaan klusteritesteihin).
Tämä logiikka elää consumerin pipeline-tiedostossa, ei providerissa.
Se on puhdasta `if`-ehtoa ja `needs`-ketjutusta — ei skriptausta,
ei monimutkaisia ehtoja providerin sisällä.
### 9. Raportit erillisellä palvelulla, linkit commitissa
Gitea Actionsin artifact-järjestelmä on binääriarkisto — ZIP-lataus,
ei HTML-selailtavuutta. Testiraportit (Cucumber HTML, Bats-coverage)
on voitava avata selaimessa yhdellä klikkauksella.
Ratkaisu: git-pages Helm-chartti, joka tarjoaa staattista
tiedostohostingia HTTP:llä. `publish-git-pages.sh` vie raportit
sinne; `report-status.sh` linkittää commit-statuksen suoraan
raporttiin. Retention hoitaa git-pagesin sidecar automaattisesti.
Tulevaisuudessa `GITHUB_STEP_SUMMARY` (Gitea 1.27+) tarjoaa
vaihtoehtoisen kanavan: jobin Summary-välilehdelle renderöityvä
Markdown-taulukko kaikista raporttilinkeistä.
### 10. Vain Gitea — ei monialustatukea ilman tarvetta
Yhden alustan tukeminen kunnolla on vaikeampaa kuin kolmen tukeminen
huonosti. Gitea Actionsin `uses:`-mekanismi, `needs`-semantiikka,
`secrets: inherit`, `gitea`-konteksti — nämä ovat alustakohtaisia
ominaisuuksia joita abstraktiokerros vain haittaisi.
Jos toinen alusta tulee ajankohtaiseksi, sille kirjoitetaan oma
toteutus. Siihen asti yksi alusta riittää. Ennenaikainen yleistys
on devopsissa yhtä haitallista kuin ohjelmistosuunnittelussa.
---
## Arkkitehtuuriset rajoitteet
### Mitä kirjasto EI tee
- **Ei ulkoista orkestraattoria.** Pipeline-ohjaus on Gitea Actionsin
`needs`-ketjuissa ja consumerin `if`-ehdoissa.
- **Ei custom actioneita.** Reusable workflow on kevyempi, versioitu
ja jaeltu Gitean oman mekanismin kautta.
- **Ei asennusta projekteihin.** Consumer viittaa `uses:`-direktiivillä
suoraan tämän repon workflow-tiedostoihin. Ei npm-pakettia, ei
git-submodulea, ei kopioitavia tiedostoja.
- **Ei runtime-riippuvuuksia.** Provider-skriptit käyttävät vain
työkaluja jotka ovat Gitea Actionsin `ubuntu-latest` runnerissa
valmiina: `bash`, `curl`, `jq`.
- **Ei monorepo-konfiguraatiota.** Jokainen mikropalvelu omistaa
oman pipeline-tiedostonsa ja konfiguraationsa.
---
## Mitä tietoisesti hylättiin
| Hylätty | Syy |
|---|---|
| Monoliittinen "kaikki yhdessä" -workflow | Pakottaa kaikille samat vaiheet. Palikka-arkkitehtuuri antaa jokaiselle tiimille vain mitä se tarvitsee |
| Oma orkestraattoripalvelin | Ylimääräinen ylläpidettävä. Gitean `needs` ja `if` riittävät |
| Docker-pohjaiset custom actionit | Tuovat riippuvuuden Docker-rekisteriin. Reusable workflow on natiivimpi |
| Commit-status API jokaiselle vaiheelle | Duplikointia — Gitea näyttää job-statuksen automaattisesti. API vain custom-linkeille |
| `tee`-putki debug-näkyvyyteen | Syö exit-koodin. stdout ohjataan tiedostoon `>` ilman pipeä |
| Multi-Git-platform-tuki | Ennenaikaista optimointia ilman tarvetta |
| Gitea Packages raporttien hostingiin | Ei HTML-selailtavuutta — vain binäärilataus |
| Gitea Pages + reports-branch | Race condition rinnakkaisten pushien kanssa |
| `repository_dispatch` ketjutukseen | Lisää konfiguraatiota vastaanottaviin repoihin. Suora API-kutsu eksplisiittisempi |
+551
View File
@@ -0,0 +1,551 @@
# git-pages → Nginx + PV -arkkitehtuuri
> Päivitetty: 2026-06-28
## Ongelma
git-pages storage v2 pakottaa kaikki tiedostot yhteen `.index`-protobufiin per site.
Raja on 1MB kovakoodattu. Yksi monorepo täytti tämän, eikä uusia raportteja voi
julkaista ennen kuin ongelma on ratkaistu.
## Ratkaisu
Poista git-pages kokonaan. Korvaa Nginx:llä (static serving) + kevyellä upload-sidecarilla (tar.gz HTTP PUT → extract). Tiedostot suoraan PV:llä ilman `.index`-välikerrosta.
## Lukitut päätökset
1. **git-pages poistuu** — sovellus ei ole mukana luku- eikä kirjoitusketjussa
2. **Nginx palvelee suoraan PV:ltä**`root /data`, `autoindex on`, `try_files $uri $uri/ $uri/index.html =404`
3. **Upload-sidecar** — busybox httpd + CGI, ottaa vastaan `PUT /path` + tar.gz body ja purkaa PV:lle
4. **URL = FS-polku** — ei Host-header-kikkaa, ei slugia, ei rewritea
5. **Traefik** — PATCH/PUT → upload-sidecar:8081 (BasicAuth), GET/HEAD → nginx:8080 (ForwardAuth myöh.)
6. **CI-julkaisu**`curl -X PUT --data-binary @report.tar.gz https://{host}/{owner}/{repo}/{branch_raw}/{sha8}/{suite}/`
7. **Branch raakana URL:ssa**`feature/x` URL:ssa = `feature/x/` FS:ssä. Ei slugitusta
8. **Linkki on aina 1-suuntainen** — gitea → raportti. Raportista ei linkkiä giteaan
9. **GITEA_API_URL poistuu** publish-skriptin pakollisista env-varista (retention käyttää omaansa)
## Ympäristömuuttujat
### GITHUB_REF_NAME
Gitea Actions runnerin automaattisesti asettama muuttuja, joka on branchin/tagin nimi ilman `refs/heads/`-etuliitettä.
| Tilanne | Arvo |
|---------|------|
| branch `main` | `main` |
| branch `feature/branch` | `feature/branch` |
| tag `v1.0.0` | `v1.0.0` |
Käytännössä `git rev-parse --abbrev-ref HEAD`. Branchin `/` on sallittu URL-polussa, joten `GITHUB_REF_NAME` voidaan käyttää sellaisenaan rakenteessa `${GITHUB_REF_NAME}/${SHA8}/${SUITE}`. Rinnakkaisissa buildeissa jokainen runner ajaa oman branchinsa kontekstissa → URL pysyy uniikkina.
```
CI → curl -X PUT → Traefik (BasicAuth middleware)
→ upload-sidecar:8081 (busybox httpd + CGI)
→ tar -xzf - -C /data/{owner}/{repo}/{branch}/{sha8}/{suite}/
→ PV /app/data/
Browser → GET → Traefik
→ nginx:8080 (root /app/data)
→ /data/{owner}/{repo}/{branch}/{sha8}/{suite}/index.html
```
## Tiedostomuutokset
### Poistuu (4 tiedostoa)
| Tiedosto | Miksi |
|----------|-------|
| `templates/init-job.yaml` | Ei enää git-pages API:a alustettavaksi |
| `templates/configmap.yaml` (nykyinen) | Korvataan nginx-conf + CGI |
| `templates/deployment.yaml` (nykyinen) | Korvataan uudella |
| `git-pages-image` (values.yaml) | Ei enää git-pages-konttia |
### Muuttuu (8 tiedostoa)
| # | Tiedosto | Muutos |
|---|----------|--------|
| 1 | `Chart.yaml` | description, poista appVersion |
| 2 | `values.yaml` | Poista git-pages-keyt, lisää nginx/sidecar |
| 3 | `dev-values.yaml` | Matchaa uusi values-rakenne |
| 4 | `templates/configmap.yaml` | `default.conf` (nginx) + `upload.cgi` |
| 5 | `templates/deployment.yaml` | 2 konttia: nginx:alpine (8080) + busybox httpd (8081) |
| 6 | `templates/ingressroute.yaml` | Backend: PATCH/PUT → upload:8081, GET/HEAD → nginx:8080 |
| 7 | `templates/service.yaml` | 2 porttia: http-read(8080), http-write(8081) |
| 8 | `templates/NOTES.txt` | Uusi URL + esimerkit |
### Pysyy ennallaan (8 tiedostoa)
| Tiedosto | Huomio |
|----------|--------|
| `templates/certificate.yaml` | TLS hostille |
| `templates/middleware.yaml` | BasicAuth + HTTPS redirect |
| `templates/publish-auth-secret.yaml` | Sama BasicAuth-secret |
| `templates/pvc.yaml` | Sama PVC |
| `templates/retention-configmap.yaml` | Päivitä retention-scriptit |
| `templates/retention-cronjob.yaml` | Pieni muutos (ei git-pages API) |
| `templates/retention-rbac.yaml` | Ennallaan |
| `_helpers.tpl` | Vain label-helpers |
### Chartin ulkopuolella
| Tiedosto | Muutos |
|----------|--------|
| `scripts/publish-git-pages.sh` | Uusi: `curl -X PUT` tar.gz upload-sidecariin |
| `scripts/report-status.sh` | URL: `reports/``${GITHUB_REF_NAME}/` |
| `scripts/ci-report.sh` | URL (rivi 101): `reports/``${GITHUB_REF_NAME}/` |
| `git-pages/docs/architecture.md` | Uusi arkkitehtuuri |
| `docs/design-rationale.md` | Päivitys |
| `files/retention-*.sh` | Lue PV:tä suoraan, ei git-pages API:a |
## Upload CGI
Busybox httpd välittää CGI-skriptille tiedot ympäristömuuttujissa (`REQUEST_METHOD`, `PATH_INFO`, `CONTENT_LENGTH`). Skripti on tiedosto `/cgi/upload.cgi``.cgi`-pääte laukaisee CGI-execution.
```bash
#!/bin/sh
# REQUEST_METHOD, PATH_INFO, etc. asettaa busybox httpd automaattisesti
if [ "$REQUEST_METHOD" = "PUT" ]; then
dest="/app/data${PATH_INFO%/}"
mkdir -p "$dest"
cat - | tar -xzf - -C "$dest"
echo "Status: 201 Created"
echo ""
fi
```
**PUT `https://{host}/{owner}/{repo}/{branch}/{sha8}/{suite}/`** → CGI purkaa:
- `PATH_INFO` = `/{owner}/{repo}/{branch}/{sha8}/{suite}/`
- `dest` = `/app/data/{owner}/{repo}/{branch}/{sha8}/{suite}`
- Tarin tiedostot puretaan tähän
**Huomio:** Skripti käyttää tar.gz-pakkausta. Jos tar on raaka (ilman gzip:ia), vaihda `-xzf``-xf`.
### CGI-kutsu
Busybox httpd käynnistetään:
```bash
busybox httpd -f -p 8081 -h /cgi
```
`-h /cgi` on docroot. Skriptitiedoston pääte `.cgi` (esim. `/cgi/upload.cgi`) käynnistää CGI-execution automaattisesti.
## Nginx-konfiguraatio
```nginx
client_max_body_size 100M; # nousevat raportit; oletus 1M on liian pieni
server {
listen 8080;
root /app/data;
autoindex on;
client_max_body_size 100M;
location / {
try_files $uri $uri/ $uri/index.html =404;
}
}
```
Alpine-nginx lukee configit `/etc/nginx/http.d/default.conf`. ConfigMap mountataan tähän polkuun.
## Deployment (kontit)
```yaml
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 8080
name: http-read
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/http.d
- name: data
mountPath: /app/data
- name: upload
image: alpine:latest
command:
- busybox
- httpd
- -f
- -p
- "8081"
- -h
- /cgi
ports:
- containerPort: 8081
name: http-write
volumeMounts:
- name: cgi-scripts
mountPath: /cgi
readOnly: true
- name: data
mountPath: /app/data
```
## values.yaml-rakenne
```yaml
# git-pages → Nginx + upload-sidecar
nameOverride: ""
fullnameOverride: ""
nginx:
image: nginx
tag: alpine
port: 8080
bodySize: 100M
upload:
image: alpine
tag: latest
port: 8081
service:
type: ClusterIP
persistence:
enabled: true
accessMode: ReadWriteOnce
storageClass: ""
size: 5Gi
ingress:
enabled: true
host: ci-reports.helm-dev.keskikuja.site
entryPoints:
websecure: websecure
web: web
certificate:
enabled: true
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
publishAuth:
create: false
htpasswdUsers: ""
retention:
enabled: false
mode: cronjob
schedule: "0 3 * * *"
image:
repository: debian
tag: bookworm-slim
giteaApiUrl: ""
rules:
branches:
default:
minAgeDays: 7
keepMin: 5
```
## CI-julkaisu (publish-skripti)
### Muutokset nykyiseen
| Kohta | Nykyinen | Uusi |
|-------|----------|------|
| `GITEA_API_URL` | pakollinen env-var | **poistettu** (retention käyttää omaansa) |
| Tar-prefiksi | `{owner}/{repo}/reports/{sha8}/{suite}` | ei prefiksiä — `{suite}` |
| Paketti | `tar -cf` | `tar -czf` (gzip) |
| Content-Type | `application/x-tar` | `application/tar+gz` |
| Headerit | `Atomic: no`, `Create-Parents: yes` | poistettu |
| Kohde | `PATCH {GIT_PAGES_URL}/` | `PUT {GIT_PAGES_URL}/{owner}/{repo}/{branch}/{sha8}/{suite}/` |
| Branch | vain `.meta` | URL:ssa + `.meta` |
### Uusi skripti (ydin)
```bash
#!/usr/bin/env sh
set -eu
SUITE_PATH="${1:-}"
[ -n "$SUITE_PATH" ] || { echo "ERROR: suite_path required" >&2; exit 1; }
[ -n "${GIT_PAGES_URL:-}" ] || { echo "ERROR: GIT_PAGES_URL is not set" >&2; exit 1; }
[ -n "${REPORTS_PUBLISH_TOKEN:-}" ] || { echo "ERROR: REPORTS_PUBLISH_TOKEN is not set" >&2; exit 1; }
[ -n "${GITHUB_REPOSITORY:-}" ] || { echo "ERROR: GITHUB_REPOSITORY is not set" >&2; exit 1; }
[ -n "${GITHUB_SHA:-}" ] || { echo "ERROR: GITHUB_SHA is not set" >&2; exit 1; }
[ -n "${GITHUB_REF_NAME:-}" ] || { echo "ERROR: GITHUB_REF_NAME is not set" >&2; exit 1; }
OWNER="${GITHUB_REPOSITORY%%/*}"
REPO="${GITHUB_REPOSITORY##*/}"
SHA8=$(echo "$GITHUB_SHA" | cut -c1-8)
BRANCH="${GITHUB_REF_NAME}"
SUITE="${SUITE_PATH%/}"
PAGES_USER="${GIT_PAGES_PUBLISH_USER:-publish}"
[ -d "$SUITE" ] || { echo "ERROR: not a directory: $SUITE" >&2; exit 1; }
WORK=$(mktemp -d)
TAR=$(mktemp)
trap 'rm -rf "$WORK" "$TAR"' EXIT
# Kopioi raporttitiedostot
mkdir -p "$WORK/$SUITE"
cp -a "$SUITE/." "$WORK/$SUITE/"
# Generoi index.html (sama logiikka kuin nykyään)
cd "$WORK/$SUITE"
if [ ! -f "index.html" ]; then
# identtinen item-listaus kuin nykyisessä skriptissä
fi
# .meta tiedosto retentionia varten
cat > ".meta" <<EOF
{"branch":"${BRANCH}","sha":"${GITHUB_SHA}","published_at":"$(date -u +%Y-%m-%dT%H:%M:%SZ)"}
EOF
cd "$WORK"
# Pakkaa: pelkät tiedostot ilman $SUITE-etuliitettä
# CGI purkaa destinationiin /data/{owner}/{repo}/{branch}/{sha8}/{suite}/
# ilman --strip-components, joten tarin menee suoraan oikeaan paikkaan
tar czf "$TAR" -C "$WORK/$SUITE" .
# PUT upload-sidecariin
PUBLISH_URL="${GIT_PAGES_URL}/${OWNER}/${REPO}/${BRANCH}/${SHA8}/${SUITE}/"
HTTP_CODE=$(curl -sS -X PUT "$PUBLISH_URL" \
-u "${PAGES_USER}:${REPORTS_PUBLISH_TOKEN}" \
-H "Content-Type: application/tar+gz" \
--data-binary @"$TAR" \
-o /tmp/git-pages-publish-response.txt \
-w "%{http_code}")
case "$HTTP_CODE" in
200|201|204) ;;
*)
echo "ERROR: publish HTTP ${HTTP_CODE}" >&2
cat /tmp/git-pages-publish-response.txt >&2
exit 1
;;
esac
echo "${PUBLISH_URL}"
```
## Gitea commit -linkki (report-status.sh)
Gitean commit-näkymään tuleva raporttilinkki muodostetaan `scripts/report-status.sh`:ssa.
### Muutos
| Kohta | Nykyinen (rivi 21) | Uusi |
|-------|-------------------|------|
| URL | `${GIT_PAGES_URL}/${GITHUB_REPOSITORY}/reports/${SHA8}/${SUITE}` | `${GIT_PAGES_URL}/${GITHUB_REPOSITORY}/${GITHUB_REF_NAME}/${SHA8}/${SUITE}` |
`reports/``{GITHUB_REF_NAME}/`. Branchin raaka nimi URL-polussa on sallittu. Muu skripti (Gitea API -kutsu, token, json-body) pysyy identtisenä.
Tarkista: `GITHUB_REF_NAME` on oltava asetettu — Gitea Actions asettaa sen automaattisesti. Jos sitä tarvitaan tarkistuksena, lisätään `report-status.sh`:n env-var-tarkistuksiin.
## Gitea Actions step summary (report-summary.yml)
`.gitea/workflows/report-summary.yml` luo GITHUB_STEP_SUMMARY -taulukon raporttilinkeillä.
### Muutos
| Kohta | Nykyinen (rivi 28) | Uusi |
|-------|-------------------|------|
| BASE | `${GIT_PAGES_URL}/${GITHUB_REPOSITORY}/reports/${SHA8}` | `${GIT_PAGES_URL}/${GITHUB_REPOSITORY}/${GITHUB_REF_NAME}/${SHA8}` |
Sama muuri: `reports/``${GITHUB_REF_NAME}/`. Loppuosa identtinen.
## CI-report (ci-report.sh)
`scripts/ci-report.sh` rakentaa URL:n suoraan single-entry-tapauksessa (rivi 101).
### Muutos
| Kohta | Nykyinen (rivi 101) | Uusi |
|-------|-------------------|------|
| URL | `${GIT_PAGES_URL}/${GITHUB_REPOSITORY}/reports/${SHA8}/${SUITE}/${SINGLE_ENTRY}` | `${GIT_PAGES_URL}/${GITHUB_REPOSITORY}/${GITHUB_REF_NAME}/${SHA8}/${SUITE}/${SINGLE_ENTRY}` |
`reports/``${GITHUB_REF_NAME}/`. Lisäksi `GITHUB_REF_NAME` pitää lisätä env-var-tarkistuksiin, jos sitä ei ole.
## Retention
Retention siirtyy lukemaan PV:tä suoraan (find, ls, stat) git-pages API:n sijaan. Nykyinen malli luki `.git-pages/manifest.json` HTTP:lla — uusi lukee FS:ää.
### Data
```
/app/data/{owner}/{repo}/{branch}/{sha8}/
.meta ← {"branch":"...","sha":"...","published_at":"..."}
index.html
style.css
...
```
### Logiikka commit-tasolla
Retention on commit-kohtainen. Yksi commit = yksi kansio `/{sha8}/`, jonka alla voi olla useita suiteja (`cucumber/`, `bats/`, jne.).
```
/app/data/{owner}/{repo}/{branch}/
abc12345/ ← yksi commit
cucumber/
index.html
.meta
bats/
index.html
.meta
def67890/ ← toinen commit
cucumber/
index.html
.meta
```
Toiminta aktiiviselle branchille (löytyy Giteasta):
1. Listaa commit-kansiot (`abc12345/`, `def67890/`, ...) uusin ensin (viimeksi muokattu)
2. Ohita `keepMin` kpl — nämä säilytetään aina
3. Jos raportti on alle `minAgeDays` päivää vanha → skip (liian tuore poistettavaksi)
4. Muut poistetaan (`rm -rf {sha8}/`), jolloin kaikki commitin suite-tulokset poistuvat kerralla
Poistuneelle branchille (404 Giteasta): poista koko `{branch}/`-kansio.
### Parametrit
```yaml
retention:
rules:
branches:
default:
keepMin: 5
minAgeDays: 7
main:
keepMin: 20
minAgeDays: 14
```
**Parametrin nimi:** `maxAgeDays``minAgeDays` (arvo sama, uusi nimi kertoo mitä se todella tekee: minimi-ikä ennen poistoa).
### Skripti (ydin)
```bash
DATA_ROOT="/app/data"
for owner_dir in "$DATA_ROOT"/*/; do
owner=$(basename "$owner_dir")
for repo_dir in "$owner_dir"*/; do
repo=$(basename "$repo_dir")
for branch_dir in "$repo_dir"*/; do
branch=$(basename "$branch_dir")
branch_urlenc=$(echo "$branch" | sed 's/\//%2F/g')
# Tarkista onko branch yhä olemassa Giteassa
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token $GITEA_TOKEN" \
"$GITEA_API_URL/api/v1/repos/$owner/$repo/branches/$branch_urlenc")
if [ "$HTTP_CODE" = "404" ]; then
# Branch poistettu — tuhoa koko branch
rm -rf "$branch_dir"
continue
fi
# Aktiivinen branch — hae säännöt
rules=$(jq -r ".branches.\"$branch\" // .branches.default" "$RETENTION_CONFIG")
keep_min=$(echo "$rules" | jq -r '.keepMin // 5')
min_age=$(echo "$rules" | jq -r '.minAgeDays // 7')
# Listaa commit-kansiot uusin ensin
commits=$(ls -1t "$branch_dir")
count=0
for sha8 in $commits; do
[ -d "$branch_dir/$sha8" ] || continue
count=$((count + 1))
# Ohita keepMin kpl
[ "$count" -le "$keep_min" ] && continue
# Tarkista ikä
age_seconds=$(( $(date +%s) - $(stat -c %Y "$branch_dir/$sha8") ))
age_days=$(( age_seconds / 86400 ))
[ "$age_days" -lt "$min_age" ] && continue
# Poista koko commit
rm -rf "$branch_dir/$sha8"
done
done
done
done
```
## Deploy-järjestys (vaiheistus)
### Vaiheet
| # | Vaihe | Mitä | Riippuvuus |
|---|-------|------|-----------|
| 1 | **Poista vanha PVC** | `kubectl delete pvc git-pages-data -n gitea-reports` (tai helm uninstall + uusi asennus) | Vanha git-pages-data .index-muodossa — tuhotaan |
| 2 | **Deployaa uusi chart** | `helm upgrade --install gitea-reports ./gitea-reports -n gitea-reports -f dev-values.yaml` | Uusi PVC luodaan tyhjänä. Vanhoja raportteja ei ole. |
| 3 | **Varmenna Nginx** | `curl localhost:8080/` → 404/autoindex | Nginx palvelee |
| 4 | **Varmenna upload** | `curl -X PUT -T test.tar.gz localhost:8081/test/` → 201 | CGI toimii |
| 5 | **Päivitä provider-skriptit** | `scripts/publish-git-pages.sh`, `scripts/report-status.sh`, `scripts/ci-report.sh` | Uusi URL-muoto |
| 6 | **Päivitä provider-workflow** | `.gitea/workflows/report-summary.yml` | BASE-muuttuja |
| 7 | **Varmenna CI-julkaisu** | `bash publish-git-pages.sh <test-suite>` → 201 + URL | Koko ketju |
### Consumer-workflowt EI päivitetä
`.gitea/workflows/example-*` ja `.ci/scripts/*` (consumerien kopioimat skriptit) eivät muutu.
### report-summary.yml
On provider-workflow (ei `example-`-prefiksiä). Saa päivittyä.
## Rollback-suunnitelma
### Jos chart epäonnistuu
```bash
helm rollback git-pages <revision>
git revert HEAD # tai git reset --hard ennen script-muutoksia
```
- Vanha PVC on poistettu → rollback palauttaa vanhan git-pages-deploymentin mutta ilman dataa
- Vanhaa `.index`-dataa ei palaudu — se tuhottiin vaiheessa 1
- Jos uutta dataa on ehtinyt kertyä, se on uudella PVC:llä → vanha deployment ei löydä sitä
### Suojaus
Pidä vanha PVC hengessä (nimeä uudelleen, älä poista) kunnes rollback-ikkuna on umpeutunut:
```bash
# Ennen vaihetta 1: uudelleennimeä vanha PVC
kubectl delete pvc git-pages-data -n gitea-reports --wait=false
kubectl patch pvc git-pages-data -n gitea-reports -p '{"metadata":{"finalizers":[]}}' --type merge # tarvittaessa
```
Jos rollback tarvitaan 24h sisällä, palauta vanha PVC backupista (esim. snapshot).
## Varmennus (check-stepit)
Jokaiselle komponentille check ennen seuraavaan vaiheeseen siirtymistä:
| Vaihe | Check |
|-------|-------|
| **helm template** | `helm template gitea-reports ./gitea-reports -n gitea-reports -f dev-values.yaml` → deploymentissa 2 konttia (nginx + upload) |
| **Nginx** | `kubectl exec <pod> -- wget -qO- http://localhost:8080/` → 404 tai autoindex (ei connection refused) |
| **Upload CGI** | `echo "hello" \| tar czf /tmp/test.tar.gz -C /tmp . && curl -sS -X PUT -H "Content-Type: application/tar+gz" --data-binary @/tmp/test.tar.gz http://localhost:8081/test/hello/` → status 201. Sitten `kubectl exec <pod> -- ls /app/data/test/hello/` → tiedostot näkyvissä |
| **Retention** | `kubectl exec <pod> -- bash /scripts/retention-run.sh` → exit 0, ei virheitä |
| **Provider-skripti (end-to-end)** | 1. `kubectl port-forward pod/<pod> 8080:8080 8081:8081` 2. `bash scripts/publish-git-pages.sh <test-suite>` → output URL (`https://{host}/{owner}/{repo}/{branch}/{sha8}/{suite}/`) 3. `curl http://localhost:8080/{owner}/{repo}/{branch}/{sha8}/{suite}/` → 200, HTML näkyy 4. `curl -I http://localhost:8080/{owner}/{repo}/{branch}/{sha8}/{suite}/index.html` → 200 |
## Data migration
**Ei migraatiota.** Vanha PVC sisältää git-pages storage v2 -dataa (`.index` protobuf). Tämä data tuhotaan tarkoituksella:
1. CI on rikki 1MB rajoitteen takia — uusia raportteja ei voi julkaista ennen kuin git-pages on poistettu
2. Vanhat raportit ovat vanhentuneita (branchit on jo saatettu poistaa, commitit vanhoja)
3. Uudet buildit tuottavat raportit uuteen järjestelmään ilman migraatiota
**Toimenpide:**
- Poista vanha PVC ennen uuden chartin deployausta
- Uusi chart luo uuden tyhjän PVC:n
- CI ajaa uudet buildit → raportit kirjoitetaan Nginx+PV-malliin
+28 -7
View File
@@ -79,7 +79,7 @@ publish-git-pages.sh <suite>
|---|---| |---|---|
| `GITEA_API_URL` | `env_json` → workflow `env:` | | `GITEA_API_URL` | `env_json` → workflow `env:` |
| `GIT_PAGES_URL` | `env_json` → workflow `env:` | | `GIT_PAGES_URL` | `env_json` → workflow `env:` |
| `GIT_PAGES_PUBLISH_TOKEN` | Gitea secret → `env:` | | `REPORTS_PUBLISH_TOKEN` | Gitea secret → `env:` |
| `GITHUB_REPOSITORY` | Automaattinen | | `GITHUB_REPOSITORY` | Automaattinen |
| `GITHUB_SHA` | Automaattinen | | `GITHUB_SHA` | Automaattinen |
@@ -104,7 +104,7 @@ Lukee tiedoston polun `CI_CONF_FILE`-env-muuttujasta (oletus: `.gitea/workflows/
- Jokaisella `KEY=VALUE`-rivillä on arvo (ei tyhjää) - Jokaisella `KEY=VALUE`-rivillä on arvo (ei tyhjää)
- URL-tyyppiset avaimet alkavat `http://` tai `https://` - URL-tyyppiset avaimet alkavat `http://` tai `https://`
- `GITEA_TOKEN` on asetettu - `GITEA_TOKEN` on asetettu
- `GIT_PAGES_PUBLISH_TOKEN` on asetettu - `REPORTS_PUBLISH_TOKEN` on asetettu
--- ---
@@ -113,17 +113,38 @@ Lukee tiedoston polun `CI_CONF_FILE`-env-muuttujasta (oletus: `.gitea/workflows/
Dispatchaa workflow'n toisessa repossa ja pollaa sen valmistumista synkronisesti. Dispatchaa workflow'n toisessa repossa ja pollaa sen valmistumista synkronisesti.
Käytetään GitOps-deploymentissa ja klusteritestien ketjutuksessa (tuleva). Käytetään GitOps-deploymentissa ja klusteritestien ketjutuksessa (tuleva).
Generoi automaattisesti `dispatch_id`-tunnisteen, lisää sen dispatch-
inputteihin ja tunnistaa workflow-runin kohdereposta `display_title`-
kentän perusteella. Toimii luotettavasti vaikka samassa repossa olisi
useita samanaikaisia ajoja.
**Kohde-workflow'ssa on oltava `dispatch_id`-input ja `run-name`-kenttä
`display_title`-matchausta varten.** Katso `skills/gitops-update/SKILL.md`.
### Rajapinta ### Rajapinta
```bash ```bash
dispatch-workflow.sh <target_repo> <workflow_file> <ref> <inputs_json> [timeout_minutes] dispatch-workflow.sh <target_repo> <workflow_file> <ref> <inputs_json> <gitea_api_url> <gitea_token> [timeout_minutes]
``` ```
| Parametri | Pakollinen | Kuvaus |
|-----------|------------|--------|
| `target_repo` | Kyllä | `owner/repo` |
| `workflow_file` | Kyllä | Workflow-tiedosto (esim. `ci-main.yml`) |
| `ref` | Kyllä | Branch |
| `inputs_json` | Kyllä | JSON-objekti dispatch-inputteina |
| `gitea_api_url` | Kyllä | Gitean API-URL |
| `gitea_token` | Kyllä | Gitea API -token (write kohderepoon) |
| `timeout_minutes` | Ei | Aikakatkaisu (oletus 360) |
### Toiminta ### Toiminta
1. **Dispatch:** `POST /api/v1/repos/{target_repo}/actions/workflows/{workflow_file}/dispatches` 1. **Generoi `dispatch_id`** — 8-hex uniikki tunniste
2. **Poll:** `GET /api/v1/repos/{target_repo}/actions/runs` → odota valmistumista 2. **Injektoi** `dispatch_id` inputteihin
3. **Palauta:** `conclusion` (`success`/`failure`/`timeout`) 3. **Dispatch:** `POST /api/v1/repos/{target_repo}/actions/workflows/{workflow_file}/dispatches`
4. **Etsi run:** pollaa rinnakkaisia `workflow_dispatch`-runeja, matchaa `display_title` sisältää `dispatch_id`:n
5. **Poll:** `GET /api/v1/repos/{target_repo}/actions/runs/{run_id}` — odota valmistumista
6. **Palauta:** exit 0 (success), exit 1 (failure), exit 124 (timeout)
--- ---
@@ -134,7 +155,7 @@ dispatch-workflow.sh <target_repo> <workflow_file> <ref> <inputs_json> [timeout_
| `GITEA_API_URL` | `env_json` | `report-status.sh`, `ci-validate.sh` | | `GITEA_API_URL` | `env_json` | `report-status.sh`, `ci-validate.sh` |
| `GIT_PAGES_URL` | `env_json` | `publish-git-pages.sh`, `report-status.sh` | | `GIT_PAGES_URL` | `env_json` | `publish-git-pages.sh`, `report-status.sh` |
| `GITEA_TOKEN` | Gitea secret | `report-status.sh`, `check-version.yml`, `docker-build-push.yml` | | `GITEA_TOKEN` | Gitea secret | `report-status.sh`, `check-version.yml`, `docker-build-push.yml` |
| `GIT_PAGES_PUBLISH_TOKEN` | Gitea secret | `publish-git-pages.sh` | | `REPORTS_PUBLISH_TOKEN` | Gitea secret | `publish-git-pages.sh` |
| `GITHUB_REPOSITORY` | Automaattinen | Kaikki skriptit | | `GITHUB_REPOSITORY` | Automaattinen | Kaikki skriptit |
| `GITHUB_SHA` | Automaattinen | Kaikki skriptit | | `GITHUB_SHA` | Automaattinen | Kaikki skriptit |
| `GITHUB_RUN_ID` | Automaattinen | `report-status.sh` | | `GITHUB_RUN_ID` | Automaattinen | `report-status.sh` |
+184 -10
View File
@@ -32,7 +32,7 @@ Kaikki workflowt:
| Secret | Pakollinen | Kuvaus | | Secret | Pakollinen | Kuvaus |
|--------|------------|--------| |--------|------------|--------|
| `GITEA_TOKEN` | Kyllä | Validointia varten | | `GITEA_TOKEN` | Kyllä | Validointia varten |
| `GIT_PAGES_PUBLISH_TOKEN` | Kyllä | Validointia varten | | `REPORTS_PUBLISH_TOKEN` | Kyllä | Validointia varten |
**Outputs:** **Outputs:**
@@ -63,13 +63,100 @@ checkout → laske versio package.json + git-tageista → output
**Trigger:** `workflow_call` **Trigger:** `workflow_call`
**Inputs:** `env_json`, `version` **Inputs:**
| Parametri | Pakollinen | Kuvaus |
|-----------|------------|--------|
| `env_json` | Kyllä | Konffi `gitea-env.conf`:stä |
| `version` | Kyllä | Version string (check-version output) |
**`env_json`-avaimet:**
| Avain | Pakollinen | Kuvaus |
|-------|------------|--------|
| `DOCKER_REGISTRY` | Kyllä | Registry (esim. `gitea.app.keskikuja.site/niko`) |
| `DOCKER_IMAGE_NAME` | Kyllä | Kuvan nimi ilman registry-polkua |
| `DOCKER_UI_URL` | Ei | Registry UI -linkki raportointia varten |
| `DOCKERFILE` | Ei | Dockerfile-polku, oletus `Dockerfile` |
| `GITEA_API_URL` | Kyllä | Gitean API-URL |
| `GIT_TAG_PREFIX` | Ei | Tag-prefix (esim. `docker/`) |
**Secrets:** `GITEA_TOKEN`, `DOCKER_USERNAME`, `DOCKER_PASSWORD` **Secrets:** `GITEA_TOKEN`, `DOCKER_USERNAME`, `DOCKER_PASSWORD`
**Steppi-kaavio:** **Steppi-kaavio:**
``` ```
build-push (build + push samassa jobissa, ei levyn kautta) → tag-commit build-push (build + push, labelit: commit+date) → tag-commit (git-tagin luonti)
```
**Huomio:** Ei käytä `container:`-direktiiviä — ajaa suoraan runnerilla,
joten `actions/checkout` toimii ilman node-asennuksia.
---
### `helm-build-push.yml` — Helm chart build & push
**Trigger:** `workflow_call`
**Inputs:**
| Parametri | Pakollinen | Kuvaus |
|-----------|------------|--------|
| `env_json` | Kyllä | Konffi `gitea-env.conf`:stä |
| `version` | Kyllä | Version string (check-version output) |
| `chart_path` | Kyllä | Polku Chart.yaml-hakemistoon |
| `extra_dependency_paths` | Ei | Pilkulla erotellut polut subcharttien dependeinceille, joille ajetaan `helm dependency update` ennen päächartin buildia |
**`env_json`-avaimet:**
| Avain | Pakollinen | Kuvaus |
|-------|------------|--------|
| `HELM_REGISTRY` | Kyllä | OCI-registry (esim. `gitea.app.keskikuja.site/niko`) |
| `HELM_UI_URL` | Ei | Registry UI -linkki raportointia varten |
| `GITEA_API_URL` | Kyllä | Gitean API-URL |
| `GIT_TAG_PREFIX` | Ei | Tag-prefix (esim. `helm/`) |
**Secrets:** `GITEA_TOKEN`, `HELM_USER`, `HELM_PASSWORD`
**Steppi-kaavio:**
```
build-push (helm package → helm push OCI) → tag-commit (git-tagin luonti)
```
**Steppien kuvaus `build-push`-jobissa:**
1. **Node.js-asennus**`apk add --no-cache nodejs` (vaaditaan `actions/checkout`-actionia varten)
2. **Checkout** — sovellusrepo ja gitea-ci-library `.ci/`-polkuun
3. **Resolve extra subchart dependencies**`helm dependency update` jokaiselle `extra_dependency_paths`-polulle (vain jos input on annettu)
4. **Package**`helm dependency update` + `helm package` versiolla `$VERSION`
5. **Push OCI**`helm push` registryyn autentikoinnilla
6. **Report status** — commit-status + UI-linkki
**Kompromissi:** Kontti `alpine/helm` ei sisällä node.js:ää, mutta
`actions/checkout@v4` on JavaScript-action ja vaatii sen. Siksi nodejs
asennetaan lennossa ennen checkouttia. Tämä vaatii internet-yhteyden
eikä toimi air gap -ympäristössä. Korvaa tarvittaessa custom-kontilla
(jossa helm + nodejs, ks. `skills/ci-container-build/SKILL.md`).
---
### `gitops-dispatch.yml` — GitOps-päivityksen dispatch
**Trigger:** `workflow_call`
**Inputit:**
| Parametri | Pakollinen | Kuvaus |
|-----------|------------|--------|
| `env_json` | Kyllä | Konffi, josta luetaan `GITOPS_FILE`, `GITOPS_YQ_TPL`, `GITOPS_REPO`, `GIT_TAG_PREFIX` |
| `version` | Kyllä | Päivitettävä versio (check-version output) |
| `component` | Kyllä | `chart` tai `container` — tunniste summary-riville |
**Secretit:** `GITOPS_TOKEN`
**Outputit:** `summary` — pipe-formaatti: `{component}|{version}|{status}|{commit_sha}|{repo}`
**Steppi-kaavio:**
```
checkout → gitops-dispatch.sh → dispatch-workflow.sh → GITOPS_SUMMARY output
``` ```
--- ---
@@ -89,11 +176,23 @@ load-config → bats + cucumber → report-summary (always)
**Trigger:** `push` [branches: main] **Trigger:** `push` [branches: main]
``` ```
load-config → check-version → load-config ───────────────────────────────────────────────────────┐
[artifact exists] → done load-config-helm ───────────────────────────────────────────┐ │
[no artifact] → bats + cucumber → report-summary (always) → docker-build-push │ │
check-version ←─────────────────────────────────────────────┘ │
│ │
└→ bats + cucumber │
├─ docker-build-push → gitops-container ─┐ │
└─ helm-build-push → gitops-chart ──────┤ │
├→ report-summary ←┘
tag-maintenance ←────────────────────────┘
``` ```
GitOps-jobit (`gitops-chart`, `gitops-container`) käyttävät
`gitops-dispatch.yml`-provider-workflowia. Kaksisuuntainen track:
dispatch-workflow.sh → GITOPS_COMMIT + GITOPS_SUMMARY.
Katso [skills/gitops-update/SKILL.md](../skills/gitops-update/SKILL.md).
### `example-bats-tests.yml` — Bats unit-testit ### `example-bats-tests.yml` — Bats unit-testit
**Trigger:** `workflow_call` **Trigger:** `workflow_call`
@@ -112,7 +211,12 @@ commit-statuksen linkillä raporttiin.
**Trigger:** `workflow_call` — ajetaan `if: always()` testien jälkeen **Trigger:** `workflow_call` — ajetaan `if: always()` testien jälkeen
**Inputs:** `env_json`, `suites` (space-separated lista suite-nimistä) **Inputs:** `env_json`, `suites` (space-separated lista suite-nimistä), `gitops` (optional JSON array)
**GitOps-tuki:** Jos `gitops` input on annettu (JSON array objekteilla
`component`, `version`, `status`, `commit`, `repo`), workflow lisää
GitOps-päivitystaulukon testiraporttien perään. Jokaiselle riville
muodostuu linkki GitOps-repon committiin.
Generoi Markdown-taulukon `GITHUB_STEP_SUMMARY`:yn kaikista julkaistuista Generoi Markdown-taulukon `GITHUB_STEP_SUMMARY`:yn kaikista julkaistuista
raporteista. Renderöityy HTML:ksi Gitea 1.27+ Summary-välilehdellä. raporteista. Renderöityy HTML:ksi Gitea 1.27+ Summary-välilehdellä.
@@ -120,7 +224,77 @@ Forward-compatibeli — ei haittaa vanhemmilla Gitea-versioilla.
--- ---
## Suunnitteilla ## Provider-skriptit
- `deploy.yml` — GitOps-deployment (dispatch-workflow.sh-pohjainen) ### `gitops-update.sh` — GitOps-version päivitys
- `test.yml` — Klusteritason test flow
**Riippuvuudet:** `yq`, `scripts/report-status.sh`, `git`
Päivittää GitOps-repon konfiguraatiotiedoston versionumeron `yq`:lla,
committaa muutoksen ja asettaa commit-statuksen molempiin repoihin
(kaksisuuntainen track):
| Status | Mihin repo | Context | Linkki |
|---|---|---|---|
| ✅ | **GitOps-repo** | `source/{repo}` | Code-repon committiin |
| ✅ | **Code-repo** (dispatchin jälkeen) | `gitops/{repo} {RUN_ID}` | GitOps-repon committiin |
**Input-ympäristömuuttujat (ajetaan GitOps-repon workflow'ssa):**
| Muuttuja | Pakollinen | Kuvaus |
|---|---|---|
| `INPUT_FILE` | Kyllä | Tiedosto GitOps-repossa (esim. `dev/Chart.yaml`) |
| `YQ_TPL` | Kyllä | `yq`-lauseke `{{VERSION}}`-placeholderilla |
| `VERSION` | Kyllä | Uusi versio (esim. `0.2.3`) |
| `SOURCE_REPO` | Kyllä | Lähdekoodirepo (esim. `org/app`) |
| `SOURCE_COMMIT` | Kyllä | Lähdekoodin commit-SHA |
| `GITOPS_REPO` | Kyllä | GitOps-repo slug |
| `GITEA_API_URL` | Kyllä | Gitean API-URL |
| `GITEA_TOKEN` | Kyllä | Gitea API-token (write GitOps-repoon) |
| `GITOPS_BRANCH` | Ei | GitOps-repon branch (oletus `main`) |
| `GIT_TAG_PREFIX` | Ei | Komponentin tag-prefix status-nimeämiseen |
**Commit-status (GitOps-repoon):**
| Kenttä | Formaatti | Esimerkki |
|--------|-----------|-----------|
| Context | `source/{repo}` | `source/gitea-ci-library` |
| Description | `Install to {env} {version}` | `Install to dev 0.2.0` |
| Target URL | Linkki code-repon committiin | `/org/repo/commit/sha` |
`{env}` parsitaan `INPUT_FILE`:stä (`dev/Chart.yaml``dev`).
**Steppikuvaus:**
1. Korvaa `YQ_TPL`:n `{{VERSION}}` versiolla
2. Muodostaa `CLONE_URL` tokenilla ja hostilla
3. Kloonaa GitOps-repon
4. Ajaa `yq eval -i` päivittääkseen tiedoston
5. Jos muutoksia: commit + push `[skip ci]`, muuten status `— no change`
6. Asettaa commit-statuksen GitOps-repoon (source-konteksti, linkki code-repoon)
**Scriptiä ei ajeta code reposta.** Se ajaa GitOps-repon workflow'ssa.
### Code-repon commit-status (dispatchin jälkeen)
GitOps-päivityksen valmistuttua `dispatch-workflow.sh` tulostaa
`GITOPS_COMMIT=<sha>` (GitOps-repon commitin SHA). Code repo asettaa
oman commit-statusinsa linkillä GitOps-committiin:
| Kenttä | Formaatti | Esimerkki |
|--------|-----------|-----------|
| Context | `gitops/{repo} {RUN_ID}` | `gitops/gitea-ci-library 473` |
| Description | `Install to {env} {version}` | `Install to dev 0.2.0` |
| Target URL | Linkki GitOps-repon committiin | `/niko/gitea-ci-gitops-tests/commit/def456` |
### Loppuraportti (GITHUB_STEP_SUMMARY)
`report-summary.yml` (optio `gitops`-inputti) lisää GitOps-rivit
GITHUB_STEP_SUMMARYyn:
| Component | Version | Status | GitOps commit |
|---|---|---|---|
| helm | 0.2.0 | success | [link](...) |
Kokonainen esimerkki molemmista puolista: [skills/gitops-update/SKILL.md](../skills/gitops-update/SKILL.md)
ja [.gitea/workflows/example-main.yml](../.gitea/workflows/example-main.yml).
---
-6
View File
@@ -1,6 +0,0 @@
apiVersion: v2
name: git-pages
description: Codeberg git-pages for CI HTML reports (apex site, Traefik BasicAuth publish)
type: application
version: 0.1.0
appVersion: "0.9.1"
-142
View File
@@ -1,142 +0,0 @@
# Architecture — git-pages
> Komponentit, datavirrat ja rajapinnat. Miksi näin on rakennettu: [design-rationale.md](design-rationale.md).
> Secretit: [secrets.md](secrets.md). Teknologiat: [tech-stack.md](tech-stack.md).
Tämä dokumentti koskee vain `git-pages/`-palvelua — ei juuren `gitea-ci-library`-kirjastoa.
---
## Yleiskuvaus
git-pages on jaettu **HTML-raporttiarkisto**: yksi apex-host, monta Gitea-repoa, commit-kohtaiset
raporttipolut. Julkaisija (esim. CI) puskaa sisällön tar-arkistona; lukija avaa raportin
selaimella commit-linkistä.
Codeberg git-pages ajaa `PAGES_INSECURE=1` — sovellus ei tee forge-authia. Julkaisu- ja
TLS-rajaukset ovat Kubernetes-kerroksessa (Traefik, cert-manager, Secretit).
---
## Komponentit
| Komponentti | Rooli |
|-------------|-------|
| **git-pages Pod** | Codeberg git-pages `0.9.1`, filesystem-storage `/app/data` |
| **retention sidecar** | Samassa podissa, HTTP API localhost:3000, siivoaa vanhat raportit |
| **PVC** | Raporttisisältö (storage v2 — `.index` + blob) |
| **Service** | ClusterIP :3000 git-pagesille |
| **Traefik IngressRoute** | Julkaisu (PATCH/PUT + BasicAuth) ja luku (GET/HEAD) eri säännöillä |
| **Traefik Middleware** | `git-pages-publish-auth` (BasicAuth), HTTPS-redirect |
| **cert-manager Certificate** | TLS → Secret `git-pages-tls` |
| Secret | Rooli |
|--------|-------|
| `git-pages-publish-auth` | htpasswd julkaisuun (Traefik) |
| `git-pages-publish-token` | plaintext token (Gitea Actions -secretiin vietäväksi) |
| `git-pages-retention-gitea` | Gitea PAT branch-tarkistukseen (sidecar)
---
## URL ja sisältö
Julkinen osoite:
```
https://ci-reports.helm-dev.keskikuja.site/niko/gitea-ci-library/reports/f4baa286/cucumber/index.html
└────────── selvä URL ─────────┘ └───────────────── Gitea-yhteensopiva polku ─────────────────────────┘
```
Levyllä (apex index-site):
```
/app/data/site/{host}/
.index # Protobuf-manifesti (storage v2 — kaikki tiedostot tässä yhdessä tiedostossa)
```
Tiedostot eivät ole flat-FS:nä — katso `implementation-notes.md`.
Apex-juuri `/` on tyhjä — ei landing-sivua.
---
## Järjestelmäkaavio
```mermaid
flowchart TB
subgraph ext["Ulkoiset"]
PUB["Julkaisija\n(CI)"]
BR["Selain"]
GITEA["Gitea API\n(branch-lista)"]
end
subgraph edge["Reuna"]
TRAEFIK["Traefik\nIngressRoute + Middleware"]
CM["cert-manager\nTLS"]
end
subgraph cluster["git-pages Pod"]
GP["git-pages\n(kontti)"]
RT["retention sidecar\n(kontti)\nHTTP API localhost:3000"]
PVC["PVC /app/data"]
end
PUB -->|"PATCH/PUT + BasicAuth\ntar"| TRAEFIK
BR -->|"GET/HEAD"| TRAEFIK
TRAEFIK --> GP
CM --> TRAEFIK
GP --> PVC
RT -->|"reads .git-pages/manifest.json\nHTTP localhost"| GP
RT -->|"check branches"| GITEA
```
---
## Julkaisu
1. Julkaisija paketoi `{owner}/{repo}/reports/{sha8}/` tar-arkistoksi (sis. `.meta`)
2. `PATCH` tai `PUT` apex-URL:iin (`https://{host}/`) + `Content-Type: application/x-tar`
3. Traefik tarkistaa BasicAuth (`publish` + token) → välittää git-pagesille
4. git-pages kirjoittaa PVC:lle
Julkaisu kulkee aina julkisen ingressin kautta — ei suoraa ClusterIP-kirjoitusta ulkopuolelta.
---
## Luku
1. Selain avaa commit-statuslinkin (GET/HEAD)
2. Traefik välittää git-pagesille ilman julkaisu-Middlewarea
3. git-pages palauttaa HTML:n polusta
Luku-auth (OIDC) ei ole toteutettu — GET/HEAD on julkinen, jos URL tunnetaan.
Katso [design-rationale.md — Luku-auth](design-rationale.md#luku-auth).
---
## Retention
Sidecar-kontti samassa podissa, ajaa retention-cleanup.sh 24h välein:
1. Lukee `.git-pages/manifest.json` HTTP:lla localhost:3000
2. Etsii `.meta`-tiedostot, tarkistaa iän ja branchin
3. **Poistettu branch** — jos `.meta.branch` ei ole Giteassa → whiteout PATCH
4. **Aktiivinen branch**`maxAgeDays` + `keepMin` (`retention.rules`)
5. Whiteout-tar → PATCH localhost:3000 — poistaa raportit
Gitea API: `GET /api/v1/repos/{owner}/{repo}/branches/{branch}``read:repository` PAT.
Katso [secrets.md](secrets.md).
---
## Rajapinnat
| Suunta | Protokolla | Auth | Kuvaus |
|--------|------------|------|--------|
| Julkaisija → Traefik | HTTPS PATCH/PUT | BasicAuth `publish` | tar → apex site |
| Selain → Traefik | HTTPS GET/HEAD | — (tänään) | HTML-raportti |
| Sidecar → Gitea | HTTPS GET | PAT `read:repository` | branch-tarkistus per repo |
| Sidecar → git-pages | HTTP :3000 | — (PAGES_INSECURE) | manifestin luku + whiteout PATCH |
| Traefik → git-pages | HTTP :3000 | — | sisäverkko |
git-pages ei käytä Gitea forge-API:a julkaisuun eikä `pages`-branchia.
-198
View File
@@ -1,198 +0,0 @@
# Design Rationale — git-pages
> Miksi git-pages on rakennettu näin. Arvot, periaatteet ja reunaehdot.
>
> Tämä dokumentti on **normatiivinen** `git-pages/`-alikansiolle. Se ei kuvaa juuren
> `gitea-ci-library`-kirjastoa eikä sen workfloweja.
>
> Liittyvät dokumentit: [architecture.md](architecture.md), [tech-stack.md](tech-stack.md), [secrets.md](secrets.md).
---
## Miksi tämä on olemassa
### Ongelma
CI-testiajoista syntyy HTML-raportteja. Esimerkiksi Cucumber-testiraportti toimii
elävänä dokumentaationa git commitin tilasta.
Gitea ei tarjoa web-selaimella selattavaa arkistoa näille HTML-raporteille.
git-pages ratkaisee tämän ongelman.
| Vaihtoehto | Miksi ei riitä |
|---|---|
| **Gitea Actions -artifactit** | Vain ZIP-lataus — HTML ei renderöidy selaimessa |
| **Gitea `pages`-branch** | Yksi branch per repo; rinnakkaiset buildit törmäävät saman branchin pushissa |
| **Gitea Releases** | Sotkee julkaisuhistorian satojen CI-buildien raporteilla |
### Ongelma URL:ssa (hylätty malli)
Alkuvaiheen malli sitoi hostin repoon: `https://{owner}.{host}/{repo}/...`
(subdomain per owner). Julkinen linkki piti sitten “kääntää” Gitea-tyyliseksi poluksi
Traefik-rewritellä (`/{owner}/{repo}/...` → eri `Host` + lyhyempi polku).
Tämä oli ongelmallinen:
- per-owner middleware / rewrite kube-resursseina
- julkaisu-URL ja lukemis-URL eri muodossa
- wildcard-TLS tai monimutkainen cert-hallinta
- vaikea selittää kehittäjälle mistä host tulee
### Ratkaisu — `selvä_url` + Gitea-yhteensopiva polku
URL rakennetaan kahdesta erillisestä osasta, ei yhdestä sekavasta kaavasta:
| Osa | Mistä | Esimerkki |
|-----|-------|-----------|
| **Selvä URL** | Organisaation kiinteä pages-host | `https://pages.example.com` |
| **Gitea-yhteensopiva polku** | Repo (`{owner}/{repo}`) + commit | `/acme-corp/backend-api/reports/abc12345/index.html` |
**Julkinen linkki** = selvä URL + polku (yksi merkkijono commit-statusiin, ei rewritea):
`https://pages.example.com/acme-corp/backend-api/reports/abc12345/index.html`
Polku vastaa Gitea Pages -käytäntöä (`/{owner}/{repo}/...`). Host on aina sama —
ei `{owner}.pages...`-subdomainia.
**Konkreettinen esimerkki (nykyinen ympäristö):**
| Elementti | Arvo |
|-----------|------|
| **Gitea-instanssi** | `gitea.app.keskikuja.site` |
| **Repo** | `niko/gitea-ci-library` |
| **Haara** | `plan/0003-alkaa-käyttämään-itseään-commit-raportti` |
| **Commit SHA** | `14cf2eaeed8a4033bc37c52b0b4c29f25b253ceb` |
| **Raportin nimi** | `cucumber` (esim.) |
| **Gitea commit -URL** | `https://gitea.app.keskikuja.site/niko/gitea-ci-library/commit/14cf2eaeed8a4033bc37c52b0b4c29f25b253ceb` |
| **Raportin julkinen URL** | `https://ci-reports.helm-dev.keskikuja.site/niko/gitea-ci-library/commit/14cf2eaeed8a4033bc37c52b0b4c29f25b253ceb/cucumber/index.html` |
Tämä varmistaa, että CI-statuslinkki on suoraan luettavissa ilman domain-rewriteä: raportin polku peilaa täsmälleen Gitean commit-polun rakennetta (`/{owner}/{repo}/commit/{sha}/{raportin-nimi}/`). Koska yksi ajo tuottaa useita raportteja, raportin nimi erottaa ne toisistaan.
Julkaisija (CI tai muu asiakas) lähettää tar-arkiston PATCH/PUT:lla. Lukija hakee
HTML:n GET:llä. Ei Gitea-git-integraatiota eikä `pages`-branchia.
**Codebergin security-malli ei sovellu tähän käyttöön** — forge-auth (Gitea PAT +
`write:repository`), DNS TXT -haaste ja muut git-pagesin sisäänrakennetut valtuutus-
mekanismit on ohitettu kokonaan (`PAGES_INSECURE=1`). Niiden sijaan Kubernetes-kerros
hoitaa rajauksen: Traefik BasicAuth julkaisuun, cert-manager TLS:ään, erillinen
publish-token ([secrets.md](secrets.md)). Sovellus palvelee sisältöä; klusteri päättää
kuka saa kirjoittaa.
---
## Suunnitteluperiaatteet
### 1. Selvä URL + Gitea-yhteensopiva polku
Julkinen osoite = kiinteä apex-host + polku `/{owner}/{repo}/reports/{sha8}/...`.
Apex-juuri `/` on tyhjä tarkoituksella — ei landing-sivua.
**Miksi:** Kehittäjä näkee Gitea-tyylisen polun; infra näkee yhden hostin. Ei Traefik-
rewritea, ei per-owner subdomaineja, ei erillistä “julkaisu-URL vs. lukemis-URL” -kaavaa.
Yksi TLS-sertifikaatti, yksi IngressRoute, yksi PVC.
### 2. Sovelluksen sisäinen security kytketty pois, Traefik hoitaa rajauksen
`git-pages`-sovelluksen koko sisäinen security-mekanismi on kytketty pois päältä (`PAGES_INSECURE=1`). Kirjoitusoikeuden validointi tapahtuu yksinomaan Kubernetes-reunalla Traefik BasicAuth -middlewaren avulla. Sovellus palvelee sisältöä sokeana; klusteri päättää, kuka saa kirjoittaa.
### 3. Julkaisu ja luku erotettu
Julkaisu (PATCH/PUT) vaatii Traefik BasicAuthin. Luku (GET/HEAD) on erillinen reitti — katso [Luku-auth](#luku-auth) alla.
**Miksi:** Koska sovellus ei validoi julkaisuoikeuksia, kirjoitusoikeus on eksplisiittisesti eriytetty Traefik Middlewaressä (`git-pages-publish-auth`).
### 4. Yksi publish-token, kaksi säilöä
Sama plaintext-token: klusterin Secretissä htpasswd-hashina, julkaisijan secret-holvissa
(esim. CI-alustan Actions-secret).
**Miksi:** Ei Gitea PAT:ia eikä `write:repository` -oikeutta. Token antaa vain
julkaisuoikeuden tähän palveluun. Yksi arvo, kaksi paikkaa — ks. [secrets.md](secrets.md).
### 5. Secretit erillisessä hallinnassa
`git-pages-publish-auth` luodaan ennen käyttöönottoa — ei osana sovelluksen konfiguraatiotiedostoja.
**Miksi:** Salaisuudet eivät kulje versionoiduissa arvoissa. Rotaatio ja SealedSecrets
pysyvät operaattorin hallussa. Ks. [secrets.md](secrets.md).
### 6. Minimaalinen parametrisointi
Instance-arvot (`host`, `issuer`, PVC) `{env}-values.yaml`:ssa. Resurssinimet,
secret-nimet ja Traefik-wire kovakoodattu templatessa.
**Miksi:** Parametrisoi vain se, mikä vaihtelee instanssien välillä (host, TLS-issuer,
levy). Vakioidut nimet ja wire pysyvät ennustettavina kaikissa asennuksissa.
---
## Puutteet
Tietoisesti avoimet asiat — eivät estä nykyistä julkaisu- ja lukumallia.
### Luku-auth
Julkaisu on suojattu (Traefik BasicAuth). **Luku ei ole:** GET/HEAD on julkinen — kuka
tuntee URL:n voi lukea raportin.
Tavoite: Traefik OIDC GET/HEAD-reitille (Gitea OAuth2 -provider). Session säilyy —
commit-statuslinkki toimii kirjautumisen jälkeen ilman uutta julkaisuoikeutta.
Ei toteutettu. Julkaisu- ja luku-reitit pysyvät erillisinä; OIDC lisätään vain lukupuolelle.
### Retention
Sidecar samassa podissa (HTTP localhost:3000), ajaa retention-cleanup.sh
24h välein:
| Sääntö | Konfiguroitavissa? | Kuvaus |
|--------|-------------------|--------|
| **Poistettu branch** | Ei — aina | Jos `.meta.branch` ei ole Giteassa enää, raportti poistetaan |
| **maxAgeDays** | Kyllä (`dev-values`) | Aktiivisen branchin raportit vanhemmat kuin N päivää |
| **keepMin** | Kyllä (`dev-values`) | Aktiivisella branchilla pidetään vähintään N uusinta |
Poistettujen branchien siivous ei tarvitse parametreja. Jäljelle jääneille
branchille säännöt tulevat `retention.rules` (`branches.default` +
`branches.{name}`).
Ei PVC-skaalausta — sidecar lukee manifestin HTTP:lla ja poistaa whiteout
PATCHilla. Ei K8s API -oikeuksia.
Secret: `git-pages-retention-gitea` — Gitea PAT branch-tarkistukseen.
Ks. [secrets.md](secrets.md).
---
## Rajat
- **Ei forge-integraatiota** — ei `pages`-branchia, ei Gitea API -hakua, ei forge-authia
- **Ei julkaisijalogiikkaa** — kuka julkaisee ja milloin on julkaisijan vastuulla
- **Ei sisäverkon ohitusjulkaisua** — julkaisu kulkee julkisen ingressin kautta (BasicAuth)
---
## Teknologiavalinnat
| Valinta | Miksi |
|---------|-------|
| **Codeberg git-pages** `0.9.1` | Natiivi apex index-site + tar-pohjainen PATCH/PUT -julkaisu |
| **Filesystem + PVC** | Yksinkertainen, yksi replica, ei erillistä objektivarastoa |
| **Traefik IngressRoute + Middleware** | Julkaisuauth erillään sovelluksesta; GET/HEAD eri säännöllä |
| **cert-manager** | TLS automaattisesti (`git-pages-tls`) |
| **Helm v3** | Toistettava asennus; instanssikohtaiset arvot erillisessä values-tiedostossa |
---
## Mitä tietoisesti hylättiin
| Hylätty | Syy |
|---------|-----|
| **deadnews/gitea-pages** | Vetää sisällön Gitea API:sta — ei sopinut CI-push-malliin |
| **Gitea `pages`-branch** | Race condition rinnakkaisissa buildeissa |
| **Per-owner subdomain** (`{owner}.pages...`) | Ongelmallinen URL; vaatii rewrite-middlewarea polun kääntämiseen |
| **Traefik path→host -rewrite** | Korvattu apex + Gitea-polulla — yksi selvä URL commit-linkissä |
| **Gitea forge-auth / PAT** | `write:repository` liian laaja oikeus vain raporttijulkaisuun |
| **DNS TXT -haaste julkaisuun** | Operatiivinen kompleksisuus ilman hyötyä BasicAuthiin verrattuna |
| **Helm-managed publish Secret** | Arvot values-tiedostoihin on kielletty; manuaalinen lähde totuudelle |
| **Image tag `v0.9.1`** | Oikea tagi on `0.9.1` (ei `v`-etuliitettä) |
-43
View File
@@ -1,43 +0,0 @@
# Implementation Notes
Teknisiä huomioita git-pages 0.9.1:n käyttäytymisestä, joita ei ole ilmeistä
dokumentaatiosta.
## Storage v2 (Protobuf manifest)
Git-pages 0.9.1 käyttää v2-arkkitehtuuria. Kaikki sisältö on pakattu
Protobuf-manifestiin (`site/{host}/.index`), ei flat-FS:nä. Tästä seuraa:
- Tiedostoja ei voi etsiä tai poistaa `find`/`rm`-komennoilla
- `.git-pages/manifest.json` listaa kaikki tiedostot (ProtoJSON)
- `.git-pages/archive.tar` antaa koko sisällön (saattaa truncata HTTP/2:ssa)
## Host-header
Git-pages valitsee sivuston Host-headerin perusteella. Ilman oikeaa Hostia
palauttaa 404.
- Ulkoiset requestit: Traefik välittää alkuperäisen Hostin automaattisesti
- Sidecar/localhost: `-H "Host: ci-reports.helm-dev.keskikuja.site"`
## PATCH ja directory-entryt
Jos PATCH-tar sisältää directory-entryn (tyyppi directory, tar typeflag '5'),
se **korvaa** koko hakemiston dokumentaation mukaan. Tar saa sisältää vain
file- ja symlink-entryjä, jotta PATCH toimii odotetusti.
## Whiteout — tiedostojen poisto
Ainoa tapa poistaa tiedostoja ilman koko sivuston PUT-korvausta:
- Tarissa character device entry (`CHRTYPE`, tar typeflag '4')
- `devmajor=0`, `devminor=0`
- PATCH:ataan sivuston juureen
## .init — ensimmäinen julkaisu
Ensimmäinen julkaisu vaatii PUTin, joka luo `.index`-tiedoston. Tämän jälkeen
PATCH riittää.
Helm-chartin post-install -job hoitaa tämän automaattisesti:
consumerien publish-scriptien ei tarvitse tuntea asennuksen tilaa.
-237
View File
@@ -1,237 +0,0 @@
# Secrets — git-pages
## Quick Start
### Vaihe 1: Secret-arkkitehtuuri
Järjestelmässä on kaksi loogista salaista arvoa. Publish-token jaetaan kahteen K8s-secretiin (Traefik-yhteensopivuus):
| Looginen nimi | K8s | Gitea |
|---|---|---|
| `report_publish_api_token` (htpasswd) | `git-pages-publish-auth` (users) | - |
| `report_publish_api_token` (plaintext) | `git-pages-publish-token` (token) | Actions Secret: `GIT_PAGES_PUBLISH_TOKEN` |
| `reports_retention_read_token` | `git-pages-retention-gitea` (token) | PAT: `CI-REPORTS_READ_FOR_RETENTION` |
**Huomio:** Publish-token jaetaan kahteen secretiin, koska Traefik BasicAuth middleware vaatii single-key secretin sekä on muodossa, missä sitä ei saa takaisin. Jokainen repo mikä raportteja käyttää, tarvitsee selväkielisen arvon, joka on "ylimääräisessä" secretissä.
### Vaihe 2: Luo Gitea PAT (retention)
**Avaa Gitea browserissa:**
1. Kirjaudu Gitea-käyttäjällä, jolla on luku kaikkiin raporttirepoihin
2. **Settings****Applications****Generate New Token**
3. Token name: `CI-REPORTS_READ_FOR_RETENTION`
4. Scopes: valitse vain **`read:repository`**
5. **Generate Token****kopioi token heti** (näytetään vain kerran)
6. Tallenna token talteen (`GITEA_RETENTION_TOKEN`)
### Vaihe 3: Generoi publish-token
**Palaa terminaalille:**
```bash
GITEA_RETENTION_TOKEN="<from Gitea>"
GIT_PAGES_PUBLISH_TOKEN="$(openssl rand -base64 24)"
echo "Publish-token generoitu. Tallennetaan K8s-secretiin Vaiheessa 4."
echo "$GIT_PAGES_PUBLISH_TOKEN"
```
### Vaihe 4: Luo K8s secrets
```bash
NS=git-pages
# 1. Publish-auth: htpasswd (Traefik BasicAuth - vaatii single-key secretin)
kubectl create secret generic git-pages-publish-auth \
--from-literal=users="$(docker run --rm httpd:2-alpine htpasswd -nb publish "$GIT_PAGES_PUBLISH_TOKEN")" \
-n "$NS"
# 2. Publish-token: plaintext (luetaan README:stä Giteaan viedessä)
kubectl create secret generic git-pages-publish-token \
--from-literal=token="$GIT_PAGES_PUBLISH_TOKEN" \
-n "$NS"
# 3. Retention (käyttää Vaiheessa 2 luotua PAT:ia)
kubectl create secret generic git-pages-retention-gitea \
--from-literal=token="$GITEA_RETENTION_TOKEN" \
-n "$NS"
kubectl get secrets -n "$NS"
```
---
### Seuraava: Helm-asennus
Palaa takaisin [README.md](../README.md#käyttöönotto) ja jatka kohdasta "Instanssin values-tiedosto".
---
## Secret Arkkitehtuuri
### Loogiset salaisuudet
| Looginen nimi | K8s | Gitea |
|---|---|---|
| `report_publish_api_token` | `git-pages-publish-auth` (htpasswd) | Actions Secret: `GIT_PAGES_PUBLISH_TOKEN` |
| `reports_retention_read_token` | `git-pages-retention-gitea` (token) | PAT: `CI-REPORTS_READ_FOR_RETENTION` |
### Secret Reference Architecture
```mermaid
graph TD
subgraph "Publish Flow"
P1["Actions Secret<br/>GIT_PAGES_PUBLISH_TOKEN"]
P2["K8s Secret<br/>git-pages-publish-auth"]
P1 -->|token| TRAEFIK
P2 -->|htpasswd| TRAEFIK
TRAEFIK["Traefik BasicAuth"]
end
subgraph "Retention Flow"
R1["K8s Secret<br/>git-pages-retention-gitea"]
R2["Gitea PAT<br/>CI-REPORTS_READ_FOR_RETENTION"]
R1 -->|token| SC["Sidecar"]
SC -->|API auth| GITEA["Gitea API"]
SC -->|read branches| GITEA
end
```
---
## Data Flow
### Flow 1: Julkaisu (Publish)
```mermaid
sequenceDiagram
participant Actions as Gitea Actions
participant Traefik as Traefik
participant K8sAuth as K8s Secret<br/>git-pages-publish-auth
participant K8sToken as K8s Secret<br/>git-pages-publish-token
participant GP as git-pages
Note over Actions: 1. Lue plaintext-token
Actions->>K8sToken: lue token-avain
K8sToken-->>Actions: plaintext token
Note over Actions: 2. Lähettää raportin
Actions->>Traefik: PUT / + BasicAuth<br/>publish:TOKEN + repo-url
Traefik->>K8sAuth: lue users (htpasswd)
K8sAuth-->>Traefik: publish:$apr1$...
alt Token match
Traefik->>GP: välitä
GP-->>Traefik: 200 OK
Traefik-->>Actions: 200 OK
else Token ei match
Traefik-->>Actions: 401 Unauthorized
end
```
**Kaksi secretiä (Traefik-yhteensopivuus):**
- `git-pages-publish-auth` = `users` (htpasswd, Traefik käyttää)
- `git-pages-publish-token` = `token` (plaintext, luetaan Giteaan viedessä)
---
### Flow 2: Luku (Read)
```mermaid
sequenceDiagram
participant Browser as Selain
participant Traefik as Traefik
participant GP as git-pages
Browser->>Traefik: GET /OWNER/REPO/commit/SHA/raportti/index.html
Traefik->>GP: välitä (ei authia)
GP-->>Traefik: HTML
Traefik-->>Browser: HTML
```
GET/HEAD-reitillä ei ole Middlewarea. Luku on julkinen, jos URL tunnetaan.
---
### Flow 3: Retention (Siivous)
```mermaid
sequenceDiagram
participant Sidecar as Retention Sidecar
participant K8sSecret as K8s Secret<br/>git-pages-retention-gitea
participant GiteaAPI as Gitea API
participant GP as git-pages (localhost:3000)
Note over Sidecar: 1. Lue PAT
Sidecar->>K8sSecret: lue token
K8sSecret-->>Sidecar: Gitea PAT
Note over Sidecar: 2. Lue manifest
Sidecar->>GP: GET .git-pages/manifest.json
GP-->>Sidecar: sisällysluettelo
Note over Sidecar: 3. Kysy branch
Sidecar->>GiteaAPI: GET /api/v1/repos/OWNER/REPO/branches/BRANCH
GiteaAPI-->>Sidecar: 200 / 404
Note over Sidecar: 4. Luo whiteout-tar + PATCH
Sidecar->>GP: PATCH / (whiteout)
GP-->>Sidecar: 200 OK
```
**Huomio:** Retention-PAT:in omistajalla on oltava lukuoikeus KAIKKIIN repoihin,
joista raportteja on PVC:llä.
---
## Troubleshooting
- **"secret not found"** — luiko secretit ennen Helm-asennusta?
- **"401 Unauthorized"** — onko Gitea Actions secret oikea?
- **"found 2 elements for secret"** — Traefik vaatii single-key secretin. Varmista että `git-pages-publish-auth` sisältää vain `users`-avaimen.
- **"token hukkuu"** — generoi uusi token (Vaihe 3) ja päivitä molemmat publish-secretit:
```bash
# 1. Generoi uusi
GIT_PAGES_PUBLISH_TOKEN="$(openssl rand -base64 24)"
# 2. Päivitä K8s secrets (molemmat)
NS=git-pages
kubectl delete secret git-pages-publish-auth -n "$NS"
kubectl delete secret git-pages-publish-token -n "$NS"
kubectl create secret generic git-pages-publish-auth \
--from-literal=users="$(docker run --rm httpd:2-alpine htpasswd -nb publish "$GIT_PAGES_PUBLISH_TOKEN")" \
-n "$NS"
kubectl create secret generic git-pages-publish-token \
--from-literal=token="$GIT_PAGES_PUBLISH_TOKEN" \
-n "$NS"
# 3. Päivitä Gitea Actions secret jokaisessa repoissa (luke README:stä)
```
## Automatisointi: useamman repon salaisuuden lisääminen
Jos repoja on monta, voit käyttää Gitea API:ta (vaatii admin-tokenin):
```bash
ADMIN_TOKEN="<gitea-admin-token>"
NS=git-pages
# Lue plaintext-token erillisestä secretistä
TOKEN=$(kubectl get secret git-pages-publish-token -n "$NS" -o jsonpath='{.data.token}' | base64 -d)
for repo in "owner/repo1" "owner/repo2" "owner/repo3"; do
curl -X POST "https://gitea.example.com/api/v1/repos/$repo/actions/secrets" \
-H "Authorization: token $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"name\":\"GIT_PAGES_PUBLISH_TOKEN\",\"data\":\"$TOKEN\"}"
done
```
Tai `tea` CLI:lla (Gitea:n virallinen CLI):
```bash
tea actions secrets add --repo owner/repo1 GIT_PAGES_PUBLISH_TOKEN "$TOKEN"
tea actions secrets add --repo owner/repo2 GIT_PAGES_PUBLISH_TOKEN "$TOKEN"
```
-221
View File
@@ -1,221 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
PAGES_URL="${PAGES_URL:-http://localhost:3000}"
PAGES_HOST="${PAGES_HOST:?PAGES_HOST is required}"
CONFIG="${RETENTION_CONFIG:-/etc/retention/retention.json}"
GITEA_API_URL="${GITEA_API_URL:-}"
GITEA_TOKEN="${GITEA_TOKEN:-}"
curl_with_host() {
curl -sS -H "Host: ${PAGES_HOST}" "$@"
}
[ -f "$CONFIG" ] || { echo "ERROR: config missing: $CONFIG" >&2; exit 1; }
declare -A BRANCH_CACHE
branch_exists() {
local owner="$1" repo="$2" branch="$3" key="${owner}/${repo}/${branch}"
local status attempt
[ -z "$GITEA_API_URL" ] && return 0
[ -z "$GITEA_TOKEN" ] && return 0
if [ "${BRANCH_CACHE[$key]:-}" = "1" ]; then
return 0
fi
# Retry up to 2 times on API errors (hardcoded)
for attempt in 1 2 3; do
status=$(curl -sS -o /dev/null -w "%{http_code}" \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_API_URL}/api/v1/repos/${owner}/${repo}/branches/${branch}" 2>/dev/null || echo "000")
if [ "$status" = "200" ]; then
BRANCH_CACHE[$key]=1
return 0
fi
if [ "$status" = "404" ]; then
return 1
fi
# API error - retry if not last attempt
if [ "$attempt" -lt 3 ]; then
sleep 10
continue
fi
done
# All retries failed - keep report (fail-safe)
echo " WARN: Gitea API error for ${owner}/${repo}/${branch} (status ${status}) after 3 attempts - KEEPING report"
BRANCH_CACHE[$key]=1
return 0
}
default_max_age=$(jq -r '.branches.default.maxAgeDays // 90' "$CONFIG")
default_keep_min=$(jq -r '.branches.default.keepMin // 5' "$CONFIG")
rule_max_age() {
local branch="$1" v
v=$(jq -r --arg b "$branch" '.branches[$b].maxAgeDays // empty' "$CONFIG")
[ -n "$v" ] && echo "$v" || echo "$default_max_age"
}
rule_keep_min() {
local branch="$1" v
v=$(jq -r --arg b "$branch" '.branches[$b].keepMin // empty' "$CONFIG")
[ -n "$v" ] && echo "$v" || echo "$default_keep_min"
}
age_days() {
local published="$1" epoch_pub now
epoch_pub=$(date -u -d "$published" +%s 2>/dev/null || echo 0)
[ "$epoch_pub" -eq 0 ] && echo 99999 && return
now=$(date -u +%s)
echo $(( (now - epoch_pub) / 86400 ))
}
parse_path() {
local rel="$1"
OWNER="${rel%%/*}"
rest="${rel#*/}"
REPO="${rest%%/*}"
}
echo "Fetching manifest from ${PAGES_URL}/.git-pages/manifest.json"
MANIFEST=$(curl_with_host "${PAGES_URL}/.git-pages/manifest.json")
echo "Manifest loaded"
META_PATHS=$(echo "$MANIFEST" | jq -r '.contents | to_entries[] | select(.key | test("/reports/")) | select(.key | endswith("/.meta")) | .key' 2>/dev/null || true)
if [ -z "$META_PATHS" ]; then
echo "No .meta files found under /reports/ — nothing to clean"
exit 0
fi
echo ""
echo "=== Phase 1: collect reports ==="
declare -A SEEN_REPORTS
declare -a REPORTS
while IFS= read -r meta_path; do
report_dir=$(dirname "$meta_path")
# Skip duplicates - same report dir already processed
[ -z "${SEEN_REPORTS[$report_dir]:-}" ] || continue
SEEN_REPORTS[$report_dir]=1
parse_path "$report_dir"
meta_content=$(curl_with_host "${PAGES_URL}/${meta_path}" 2>/dev/null || true)
[ -n "$meta_content" ] || { echo " WARN: could not fetch $meta_path"; continue; }
branch=$(echo "$meta_content" | jq -r '.branch // empty' 2>/dev/null || true)
published=$(echo "$meta_content" | jq -r '.published_at // empty' 2>/dev/null || true)
[ -n "$branch" ] || { echo " WARN: no branch in $meta_path"; continue; }
[ -n "$published" ] || { echo " WARN: no published_at in $meta_path"; continue; }
days=$(age_days "$published")
REPORTS+=("${report_dir}|${OWNER}|${REPO}|${branch}|${days}")
echo " ${OWNER}/${REPO} branch=${branch} age=${days}d"
done <<< "$META_PATHS"
[ "${#REPORTS[@]}" -eq 0 ] && { echo "No actionable reports"; exit 0; }
echo ""
echo "=== Phase 2: check branches in Gitea ==="
declare -a TO_DELETE
declare -a KEEP
for entry in "${REPORTS[@]}"; do
IFS='|' read -r dir owner repo branch days <<< "$entry"
if [ -n "$GITEA_API_URL" ] && [ -n "$GITEA_TOKEN" ]; then
if branch_exists "$owner" "$repo" "$branch"; then
echo " BRANCH EXISTS: ${owner}/${repo}/${branch}"
KEEP+=("${dir}|${owner}|${repo}|${branch}|${days}")
else
echo " BRANCH DELETED: ${owner}/${repo}/${branch} -> DELETE"
TO_DELETE+=("$dir")
fi
else
KEEP+=("${dir}|${owner}|${repo}|${branch}|${days}")
fi
done
echo ""
echo "=== Phase 3: apply retention rules to remaining reports ==="
declare -A BRANCH_COUNTS
if [ "${#KEEP[@]}" -gt 0 ]; then
IFS=$'\n'
for entry in $(printf '%s\n' "${KEEP[@]}" | sort -t'|' -k4,4 -k5,5rn); do
IFS='|' read -r dir owner repo branch days <<< "$entry"
max_age=$(rule_max_age "$branch")
keep_min=$(rule_keep_min "$branch")
if [ "$days" -gt "$max_age" ]; then
echo " DELETE: ${dir} (age ${days}d > maxAge ${max_age}d, branch ${branch})"
TO_DELETE+=("$dir")
continue
fi
key="${branch}"
count="${BRANCH_COUNTS[$key]:-0}"
count=$((count + 1))
BRANCH_COUNTS["$key"]=$count
if [ "$count" -gt "$keep_min" ]; then
echo " DELETE: ${dir} (kept ${keep_min}/${count}, exceeds keepMin, branch ${branch})"
TO_DELETE+=("$dir")
fi
done
unset IFS
fi
if [ "${#TO_DELETE[@]}" -eq 0 ]; then
echo "Nothing to delete"
exit 0
fi
echo ""
echo "=== Phase 4: whiteout deletion ==="
echo "Creating whiteout tar for ${#TO_DELETE[@]} report(s)..."
WHITEOUT_TAR=$(mktemp)
trap 'rm -f "$WHITEOUT_TAR"' EXIT
python3 -c "
import tarfile, sys
tar = tarfile.open(name='${WHITEOUT_TAR}', mode='w')
dirs = set()
for d in sys.argv[1:]:
dirs.add(d.strip())
tarinfo = tarfile.TarInfo()
tarinfo.type = tarfile.CHRTYPE
tarinfo.devmajor = 0
tarinfo.devminor = 0
for d in sorted(dirs, key=len, reverse=True):
info = tarinfo
info.name = d
tar.addfile(info)
tar.close()
" "${TO_DELETE[@]}"
echo "Patching ${PAGES_URL}/ with whiteout tar..."
HTTP_CODE=$(curl_with_host -X PATCH "${PAGES_URL}/" \
-H "Content-Type: application/x-tar" \
-H "Atomic: no" \
--data-binary @"${WHITEOUT_TAR}" \
-w "%{http_code}" \
-o /dev/null)
echo "HTTP $HTTP_CODE"
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "204" ]; then
echo "Retention cleanup finished."
else
echo "ERROR: retention HTTP ${HTTP_CODE}" >&2
exit 1
fi
-19
View File
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
# Scale down git-pages, run PVC cleanup (RWO), scale back up.
set -euo pipefail
NAMESPACE="${NAMESPACE:?NAMESPACE is required}"
DEPLOYMENT="${DEPLOYMENT:?DEPLOYMENT is required}"
echo "Scaling ${DEPLOYMENT} to 0..."
kubectl scale "deployment/${DEPLOYMENT}" --replicas=0 -n "$NAMESPACE"
kubectl wait --for=delete pod \
-l "app.kubernetes.io/name=git-pages,app.kubernetes.io/instance=${INSTANCE}" \
-n "$NAMESPACE" --timeout=180s
/scripts/retention-cleanup.sh
echo "Scaling ${DEPLOYMENT} to 1..."
kubectl scale "deployment/${DEPLOYMENT}" --replicas=1 -n "$NAMESPACE"
echo "Retention job done."
-16
View File
@@ -1,16 +0,0 @@
git-pages installed.
See docs/secrets.md for secret prerequisites (K8s + Gitea Actions).
Install:
helm upgrade --install git-pages ./git-pages -n {{ .Release.Namespace }} -f dev-values.yaml
Host: https://{{ .Values.ingress.host }}
Reports: https://{{ .Values.ingress.host }}/{owner}/{repo}/reports/{sha8}/index.html
Publish (CI):
PATCH https://{{ .Values.ingress.host }}/
Authorization: Basic publish:<GIT_PAGES_PUBLISH_TOKEN>
Upgrade: helm upgrade {{ .Release.Name }} ./git-pages -n {{ .Release.Namespace }} -f dev-values.yaml
Uninstall: helm uninstall {{ .Release.Name }} -n {{ .Release.Namespace }}
-20
View File
@@ -1,20 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "git-pages.fullname" . }}-config
labels:
{{- include "git-pages.componentLabels" . | nindent 4 }}
data:
config.toml: |
log-format = "text"
[server]
pages = "tcp/:3000"
caddy = "-"
metrics = "tcp/:3002"
[storage]
type = "fs"
[storage.fs]
root = "/app/data"
-120
View File
@@ -1,120 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "git-pages.fullname" . }}
labels:
{{- include "git-pages.componentLabels" . | nindent 4 }}
spec:
replicas: 1
selector:
matchLabels:
{{- include "git-pages.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "git-pages.componentLabels" . | nindent 8 }}
spec:
securityContext:
fsGroup: 1000
containers:
- name: git-pages
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
command:
- git-pages
args:
- -config
- /etc/git-pages/config.toml
{{- if .Values.pagesInsecure }}
env:
- name: PAGES_INSECURE
value: "1"
{{- end }}
ports:
- name: http
containerPort: 3000
protocol: TCP
- name: metrics
containerPort: 3002
protocol: TCP
volumeMounts:
- name: config
mountPath: /etc/git-pages
readOnly: true
{{- if .Values.persistence.enabled }}
- name: data
mountPath: /app/data
{{- end }}
readinessProbe:
tcpSocket:
port: http
initialDelaySeconds: 3
periodSeconds: 10
livenessProbe:
tcpSocket:
port: http
initialDelaySeconds: 10
periodSeconds: 20
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- if and .Values.persistence.enabled .Values.retention.enabled (eq .Values.retention.mode "sidecar") }}
- name: retention
image: "{{ .Values.retention.image.repository }}:{{ .Values.retention.image.tag }}"
imagePullPolicy: {{ .Values.retention.image.pullPolicy }}
securityContext:
runAsUser: 0
command:
- bash
- -c
- |
set -euo pipefail
echo "Retention sidecar: installing deps..."
apt-get update -qq
apt-get install -y --no-install-recommends curl jq python3 >/dev/null
echo "Retention sidecar: ready"
while true; do
/scripts/retention-cleanup.sh
echo "Retention sidecar: next run in 24h"
sleep 86400
done
env:
- name: PAGES_URL
value: http://localhost:3000
- name: PAGES_HOST
value: {{ .Values.ingress.host | quote }}
- name: RETENTION_CONFIG
value: /etc/retention/retention.json
- name: GITEA_API_URL
value: {{ .Values.retention.giteaApiUrl | quote }}
- name: GITEA_TOKEN
valueFrom:
secretKeyRef:
name: git-pages-retention-gitea
key: token
volumeMounts:
- name: retention-scripts
mountPath: /scripts
- name: retention-config
mountPath: /etc/retention
{{- end }}
volumes:
- name: config
configMap:
name: {{ include "git-pages.fullname" . }}-config
{{- if .Values.persistence.enabled }}
- name: data
persistentVolumeClaim:
claimName: {{ include "git-pages.fullname" . }}-data
{{- end }}
{{- if and .Values.persistence.enabled .Values.retention.enabled (eq .Values.retention.mode "sidecar") }}
- name: retention-scripts
configMap:
name: git-pages-retention
defaultMode: 0755
- name: retention-config
configMap:
name: git-pages-retention
items:
- key: retention.json
path: retention.json
{{- end }}
-48
View File
@@ -1,48 +0,0 @@
{{- if .Values.initJob.enabled }}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "git-pages.fullname" . }}-init
labels:
{{- include "git-pages.componentLabels" . | nindent 4 }}
annotations:
"helm.sh/hook": post-install, post-upgrade
"helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation
spec:
backoffLimit: 5
template:
metadata:
labels:
app.kubernetes.io/name: {{ include "git-pages.name" . }}-init
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
restartPolicy: Never
containers:
- name: init
image: "{{ .Values.initJob.image.repository }}:{{ .Values.initJob.image.tag }}"
imagePullPolicy: {{ .Values.initJob.image.pullPolicy }}
command:
- bash
- -c
- |
set -euo pipefail
apt-get update -qq && apt-get install -y -qq curl tar >/dev/null
echo "Init: waiting for git-pages..."
until curl -sf \
-H "Host: {{ .Values.ingress.host }}" \
-o /dev/null "http://git-pages:3000/.git-pages/health"
do sleep 2; done
echo "Init: creating placeholder site..."
WORK=$(mktemp -d)
mkdir -p "$WORK/__init__"
echo "initialized" > "$WORK/__init__/index.html"
tar cf /tmp/init.tar -C "$WORK" __init__
curl -sf -X PUT "http://git-pages:3000/" \
-H "Host: {{ .Values.ingress.host }}" \
-H "Content-Type: application/x-tar" \
--data-binary @/tmp/init.tar -o /dev/null
echo "Init: done"
env:
- name: PAGES_INSECURE
value: "1"
{{- end }}
-22
View File
@@ -1,22 +0,0 @@
{{- if .Values.ingress.enabled }}
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: {{ include "git-pages.fullname" . }}-publish-auth
labels:
{{- include "git-pages.componentLabels" . | nindent 4 }}
spec:
basicAuth:
secret: git-pages-publish-auth
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: {{ include "git-pages.fullname" . }}-https-redirect
labels:
{{- include "git-pages.componentLabels" . | nindent 4 }}
spec:
redirectScheme:
scheme: https
permanent: true
{{- end }}
-15
View File
@@ -1,15 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "git-pages.fullname" . }}
labels:
{{- include "git-pages.componentLabels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
selector:
{{- include "git-pages.selectorLabels" . | nindent 4 }}
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
+5
View File
@@ -0,0 +1,5 @@
apiVersion: v2
name: gitea-reports
description: Nginx + upload-sidecar for CI HTML reports (static serving, Traefik BasicAuth publish)
type: application
version: 0.2.0
+53 -25
View File
@@ -1,4 +1,4 @@
# git-pages # gitea-reports
Jaettu **Gitea CI -raporttien** tallennus- ja lukupaikka: HTML-raportit (esim. Cucumber) Jaettu **Gitea CI -raporttien** tallennus- ja lukupaikka: HTML-raportit (esim. Cucumber)
commit-kohtaisiin polkuihin, selaimella avattavina linkkeinä Gitean commitin CI-job commit-kohtaisiin polkuihin, selaimella avattavina linkkeinä Gitean commitin CI-job
@@ -21,39 +21,39 @@ Luo secretit ennen Helm-asennusta: [docs/secrets.md](docs/secrets.md)
```bash ```bash
# Muokkaa oma values-tiedosto (tai käytä dev-values.yaml) # Muokkaa oma values-tiedosto (tai käytä dev-values.yaml)
cp git-pages/dev-values.yaml my-values.yaml cp gitea-reports/dev-values.yaml my-values.yaml
# Muuta: ingress.host, certificate.issuerRef.name, jne. # Muuta: ingress.host, certificate.issuerRef.name, jne.
``` ```
### 3. Helm-asennus ### 3. Helm-asennus
```bash ```bash
NS=git-pages NS=gitea-reports
VALUES=my-values.yaml VALUES=gitea-reports/dev-values.yaml
helm upgrade --install git-pages ./git-pages \ helm upgrade --install gitea-reports ./gitea-reports \
-n "$NS" --create-namespace \ -n "$NS" --create-namespace \
-f "$VALUES" -f "$VALUES"
helm template gitea-reports ./gitea-reports -n "$NS" -f "$VALUES"
``` ```
Helm ajaa asennuksen jälkeen init-jobin, joka PUTtaa paikanpitäjäsivun Nginx ja upload-sidecar käynnistyvät suoraan ilman init-vaihetta.
git-pagesiin. Tämä luo tarvittavan `.index`-tiedoston — sen jälkeen PV on tyhjä ennen ensimmäistä CI-julkaisua.
Gitea Actions -scriptit voivat käyttää suoraan PATCHia ilman
PUT-fallbackia.
--- ---
## Vie publish-token Gitea Actions-secretiin (per repo) ## Vie publish-token Gitea Actions-secretiin (per repo)
⚠️ **Tehtävä jokaiselle repoille**, joka julkaisee raportteja git-pagesiin. ⚠️ **Tehtävä jokaiselle repoille**, joka julkaisee raportteja gitea-reportsiin.
```bash ```bash
NS=git-pages NS=gitea-reports
REPO_OWNER="niko" REPO_OWNER="niko"
REPO_NAME="gitea-ci-library" REPO_NAME="gitea-ci-library"
# 1. Lue plaintext-token erillisestä secretistä # 1. Lue plaintext-token erillisestä secretistä
TOKEN=$(kubectl get secret git-pages-publish-token -n "$NS" -o jsonpath='{.data.token}' | base64 -d) TOKEN=$(kubectl get secret gitea-reports-publish-token -n "$NS" -o jsonpath='{.data.token}' | base64 -d)
# 2. Kopioi leikepöydälle # 2. Kopioi leikepöydälle
echo -n "$TOKEN" | pbcopy # macOS echo -n "$TOKEN" | pbcopy # macOS
@@ -64,7 +64,7 @@ open "https://gitea.app.keskikuja.site/${REPO_OWNER}/${REPO_NAME}/settings/actio
# Linux: xdg-open "https://gitea.app.keskikuja.site/${REPO_OWNER}/${REPO_NAME}/settings/actions/secrets" # Linux: xdg-open "https://gitea.app.keskikuja.site/${REPO_OWNER}/${REPO_NAME}/settings/actions/secrets"
``` ```
**Gitea UI:ssa:** New Secret → Name: `GIT_PAGES_PUBLISH_TOKEN` → Value: **liitä leikepöydältä** → Save **Gitea UI:ssa:** New Secret → Name: `REPORTS_PUBLISH_TOKEN` → Value: **liitä leikepöydältä** → Save
> 💡 **Monelle repoille:** Toista vaiheet 34, tai katso [automatisointi](docs/secrets.md#automatisointi-useamman-repon-salaisuuden-lis%C3%A4%C3%A4miseen). > 💡 **Monelle repoille:** Toista vaiheet 34, tai katso [automatisointi](docs/secrets.md#automatisointi-useamman-repon-salaisuuden-lis%C3%A4%C3%A4miseen).
@@ -113,14 +113,14 @@ Esimerkki dev-ympäristöstä: [dev-values.yaml](dev-values.yaml).
Repojuuresta (sama `NS` kuin [docs/secrets.md](docs/secrets.md)): Repojuuresta (sama `NS` kuin [docs/secrets.md](docs/secrets.md)):
```bash ```bash
NS=git-pages NS=gitea-reports
VALUES=git-pages/dev-values.yaml VALUES=gitea-reports/dev-values.yaml
helm upgrade --install git-pages ./git-pages \ helm upgrade --install gitea-reports ./gitea-reports \
-n "$NS" --create-namespace \ -n "$NS" --create-namespace \
-f "$VALUES" -f "$VALUES"
helm template git-pages ./git-pages -f "$VALUES" helm template gitea-reports ./gitea-reports -f "$VALUES"
``` ```
--- ---
@@ -131,21 +131,49 @@ Julkaisu DNS-osoitteeseen BasicAuthilla:
```bash ```bash
# Esimerkki: julkaise raportti # Esimerkki: julkaise raportti
curl -X PATCH https://ci-reports.helm-dev.keskikuja.site/owner/repo/commit/sha8/ \ curl -X PUT \
-H "Authorization: Basic $(echo -n "publish:$GIT_PAGES_PUBLISH_TOKEN" | base64)" \ -u publish:$REPORTS_PUBLISH_TOKEN \
-H "Content-Type: application/x-tar" \ -H "Content-Type: application/tar+gz" \
--data-binary @raportti.tar --data-binary @report.tar.gz \
https://ci-reports.helm-dev.keskikuja.site/{owner}/{repo}/{branch}/{sha8}/{suite}/
``` ```
**Vaaditut asetukset:** **Vaaditut asetukset:**
| Missä | Arvo | | Missä | Arvo |
|-------|------| |-------|------|
| Gitea Actions secret | `GIT_PAGES_PUBLISH_TOKEN` (sama kuin K8s `git-pages-publish-token` `token`-avain) | | Gitea Actions secret | `REPORTS_PUBLISH_TOKEN` (sama kuin K8s `gitea-reports-publish-token` `token`-avain) |
| Scriptissä | `curl` käyttää BasicAuth-headeria yllä | | Scriptissä | `bash scripts/publish-gitea-reports.sh <suite>` |
**K8s secretit (Traefik-yhteensopivuus):** **K8s secretit (Traefik-yhteensopivuus):**
- `git-pages-publish-auth` = htpasswd (Traefik BasicAuth middleware) - `gitea-reports-publish-auth` = htpasswd (Traefik BasicAuth middleware)
- `git-pages-publish-token` = plaintext (luetaan Giteaan viedessä) - `gitea-reports-publish-token` = plaintext (luetaan Giteaan viedessä)
Tarkemmat secret-ohjeet: [docs/secrets.md](docs/secrets.md). Tarkemmat secret-ohjeet: [docs/secrets.md](docs/secrets.md).
---
## Testaus
Retention-logiikalle on unit-testit, jotka testaa funktiot ja Phase 3 -säännöt
erikseen ilman ulkoisia riippuvuuksia.
```bash
cd gitea-reports
bats tests/retention.bats
```
Testit käyttävät `<root>/files/retention-lib.sh` -jaettua kirjastoa, jota myös
`retention-cleanup.sh` sourceaa.
**Vaatimukset:** `bats`, `jq`, `date` (GNU date tai BSD date ISO 8601 -tuella).
---
## Retention
Ylläpitoscripti, joka poistaa vanhat raportit PV:ltä suoraan retentionsääntöjen mukaan.
Ajetaan CronJobina Kubernetesissa.
Retention lukee PV:tä suoraan (find, ls, stat), tarkistaa branchin olemassaolon Giteasta,
ja poistaa vanhat commit-kansiot `rm -rf`-komentona. Ei HTTP API:a retentionia varten.
@@ -1,5 +1,5 @@
# Dev instance — overrides values.yaml constants. # Dev instance — overrides values.yaml constants.
# helm upgrade --install git-pages ./git-pages -n git-pages -f dev-values.yaml # helm upgrade --install gitea-reports ./gitea-reports -n gitea-reports -f dev-values.yaml
ingress: ingress:
host: ci-reports.helm-dev.keskikuja.site host: ci-reports.helm-dev.keskikuja.site
@@ -19,11 +19,11 @@ retention:
rules: rules:
branches: branches:
default: default:
maxAgeDays: 90 minAgeDays: 3
keepMin: 5 keepMin: 5
main: main:
maxAgeDays: 365 minAgeDays: 3
keepMin: 20 keepMin: 10
master: master:
maxAgeDays: 365 minAgeDays: 3
keepMin: 20 keepMin: 10
+82
View File
@@ -0,0 +1,82 @@
# Architecture — Nginx + upload-sidecar
> gitea-reports on korvattu. Uusi arkkitehtuuri: Nginx palvelee staattisia tiedostoja PV:ltä, upload-sidecar (busybox httpd + CGI) ottaa vastaan tar.gz-PUTit.
## Yleiskuvaus
Yksi apex-host, monta Gitea-repoa. URL = FS-polku: `/{owner}/{repo}/{branch}/{sha8}/{suite}/`.
Ei Host-header-kikkaa, ei slugia, ei rewritea.
## Komponentit
| Komponentti | Rooli |
|-------------|-------|
| **nginx** | Palvelee staattisia tiedostoja /app/data -rootista (:8080) |
| **upload** (Python HTTP server) | Vastaanottaa PUT /path + tar.gz body, purkaa PV:lle (:8081) |
| **PVC** | Raporttisisältö suoraan FS-muodossa |
| **Service** | ClusterIP :8080 (read), :8081 (write) |
| **Traefik IngressRoute** | PATCH/PUT → upload:8081 (BasicAuth), GET/HEAD → nginx:8080 |
| **Traefik Middleware** | `gitea-reports-publish-auth` (BasicAuth), HTTPS-redirect |
| **cert-manager Certificate** | TLS |
## URL ja sisältö
```
https://ci-reports.helm-dev.keskikuja.site/niko/gitea-ci-library/main/f4baa286/cucumber/index.html
└── selvä URL ──┘ └─────────── Gitea-yhteensopiva polku ──────────────────────────┘
```
Levyllä:
```
/app/data/{owner}/{repo}/{branch}/{sha8}/
.meta
index.html
...
```
## Järjestelmäkaavio
```mermaid
flowchart TB
subgraph ext["Ulkoiset"]
PUB["Julkaisija\n(CI)"]
BR["Selain"]
GITEA["Gitea API\n(branch-lista)"]
end
subgraph edge["Reuna"]
TRAEFIK["Traefik\nIngressRoute + Middleware"]
CM["cert-manager\nTLS"]
end
subgraph cluster["Pod"]
NGINX["nginx\n(kontti)"]
UPLOAD["upload sidecar\n(Python HTTP server)"]
PVC["PVC /app/data"]
end
PUB -->|"PUT + BasicAuth\ntar.gz"| TRAEFIK
BR -->|"GET/HEAD"| TRAEFIK
TRAEFIK -->|"PUT/PATCH"| UPLOAD
TRAEFIK -->|"GET/HEAD"| NGINX
CM --> TRAEFIK
UPLOAD -->|"extract to PV"| PVC
NGINX --> PVC
```
## Julkaisu
1. Julkaisija paketoi tiedostot tar.gz:ksi
2. `PUT https://{host}/{owner}/{repo}/{branch}/{sha8}/{suite}/`
3. Traefik tarkistaa BasicAuth → välittää upload-sidecarille
4. CGI purkaa tar.gz:n suoraan PV:lle: `/app/data/{owner}/{repo}/{branch}/{sha8}/{suite}/`
## Luku
1. Selain avaa URL:n (GET/HEAD)
2. Traefik välittää nginxille
3. Nginx palauttaa suoraan PV:ltä
## Retention
CronJob lukee PV:tä suoraan (find, ls, stat), tarkistaa branchin olemassaolon Giteasta, poistaa vanhat commit-kansiot.
+90
View File
@@ -0,0 +1,90 @@
# Design Rationale — gitea-reports
Miksi gitea-reports on rakennettu näin. Periaatteet, jotka pitävät arkkitehtuurin koossa.
Teknologiavalinnat ovat näiden periaatteiden seurauksia — eivät erillisiä preferenssejä.
Tämä dokumentti on normatiivinen. Arkkitehtuurin tulee noudattaa sen periaatteita.
Ehdotus, joka on ristiriidassa dokumentin kanssa, on konflikti — ei hiljainen ohitus.
## The problem this solves
CI-testiajoista syntyy HTML-raportteja. Gitea ei tarjoa web-selaimella selattavaa arkistoa
näille raporteille.
Vaihtoehdot eivät sovi:
- **Gitea Actions artifactit**: vain ZIP-lataus — HTML ei renderöidy selaimessa
- **Gitea pages-branch**: yksi branch per repo; rinnakkaiset buildit törmäävät
- **Gitea Releases**: sotkee julkaisuhistorian sadoilla CI-buildien raporteilla
## How this is solved
Yksi klusteri, yksi apex-host, monta Gitea-repoa. URL on suoraan FS-polku:
`/{owner}/{repo}/{branch}/{sha8}/{suite}/`. Ei rewritea, ei subdomain-per-owner,
ei slugia.
Palvelu on read/write -jaettu: luku ja julkaisu eri sovelluksilla, eri porteilla,
eri Traefik-säännöillä.
## Why selvä URL + Gitea-yhteensopiva polku
Repo tunnistetaan polusta `/{owner}/{repo}/...`, ei hostista. Kaikki URL-taso
(branch, sha8, suite) on suoraan FS-polku. Tämä mahdollistaa yhden TLS-sertifikaatin,
yhden IngressRouten ja URL:n, joka on suoraan kopioitavissa commit-statusiin ilman
rewritea.
**Hylätty:** subdomain-per-owner (`{owner}.pages...`). Vaati per-owner Traefik-
rewritea ja wildcard-TLS:ää. Julkaisu-URL ja luku-URL olivat eri muodossa — kehittäjälle
vaikea ymmärtää ja debugata.
## Why luku ja kirjoitus eri sovelluksilla
Yksi binary, joka palvelee sekä lukua että kirjoitusta samassa prosessissa, on
yksinkertainen mutta yhdistää kaksi eri asiaa. Tässä arkkitehtuurissa nginx palvelee
staattisia tiedostoja; Python-skripti purkaa tar.gz-julkaisun PV:lle. Ne eivät tiedä
toisistaan.
Luku ei tarvitse julkaisulogiikkaa. Julkaisu ei tarvitse optimoitua staattista
palvelua. Erottamalla ne kumpikin voidaan valita parhaaseen tarkoitukseensa.
## Why security klusterin reunalla — ei sovelluksessa
Sovellus on "tyhmä": se palvelee mitä PV:llä on ja kirjoittaa mitä sille annetaan.
Valtuutus tapahtuu yksinomaan Traefik BasicAuth -middlewaressa.
Tämä mahdollistaa:
- Julkaisu- ja lukuoikeuksien erillisen hallinnan eri Traefik-säännöillä
- Sovelluksen vaihtamisen ilman turvallisuusauditointia
- Token-rotaation ilman sovelluksen uudelleenkäynnistystä
**Hylätty:** sovelluksen sisäinen forge-auth (Gitea PAT, DNS TXT -haaste).
Sitoo sovelluksen tiettyyn autentikointimekanismiin ja vaatii laajat oikeudet
(`write:repository`) pelkkää raporttijulkaisua varten.
## Why retention erillisenä CronJobina
Retention ei tarvitse olla aina käynnissä — se ajetaan kerran vuorokaudessa.
CronJob lukee PV:tä suoraan (find, ls, stat), ei HTTP API:n kautta. Tämä pitää
retention poissa podin kriittiseltä polulta (luku/julkaisu).
**Hylätty:** sidecar samassa podissa. Sidecar kuluttaa resursseja aina, ja
retention-logiikka sekoittuu julkaisu- ja lukukoodiin.
## Why yksi token, kaksi säilöä
Sama plaintext-arvo: htpasswd-hashina K8s Secretissä (Traefik lukee), plaintextina
Gitea Actions Secretissä (julkaisija lukee). Yksi rotaatio päivittää molemmat.
Token antaa vain julkaisuoikeuden tähän palveluun — ei Gitea PAT:ia eikä
`write:repository`-oikeutta.
## Why PVC suoraan FS-muodossa
Ei objektivarastoa, ei HTTP API:a retentionille. Sisältö on suoraan luettavissa
kubectl exec + ls/find. Yksinkertaisin mahdollinen tallennusmuoto. Yksi replica,
ei synkronointia.
## References
- [architecture.md](architecture.md) — komponentit ja vuokaavio
- [tech-stack.md](tech-stack.md) — teknologiavalinnat listana
- [secrets.md](secrets.md) — secret-arkkitehtuuri ja käyttöönotto
+267
View File
@@ -0,0 +1,267 @@
# Secrets — gitea-reports
## Quick Start
### Vaihe 1: Secret-arkkitehtuuri
Järjestelmässä on kaksi loogista salaista arvoa. Publish-token jaetaan kahteen K8s-secretiin (Traefik-yhteensopivuus):
| Looginen nimi | K8s | Gitea |
|---|---|---|
| `report_publish_api_token` (htpasswd) | `gitea-reports-publish-auth` (users) | - |
| `report_publish_api_token` (plaintext) | `gitea-reports-publish-token` (token) | Actions Secret: `REPORTS_PUBLISH_TOKEN` |
| `reports_retention_read_token` | `gitea-reports-retention-gitea` (token) | PAT: `CI-REPORTS_READ_FOR_RETENTION` |
**Huomio:** Publish-token jaetaan kahteen secretiin, koska Traefik BasicAuth middleware vaatii single-key secretin sekä on muodossa, missä sitä ei saa takaisin. Jokainen repo mikä raportteja käyttää, tarvitsee selväkielisen arvon, joka on "ylimääräisessä" secretissä.
### Vaihe 2: Luo Gitea PAT (retention)
**Avaa Gitea browserissa:**
1. Kirjaudu Gitea-käyttäjällä, jolla on luku kaikkiin raporttirepoihin
2. **Settings****Applications****Generate New Token**
3. Token name: `CI-REPORTS_READ_FOR_RETENTION`
4. Scopes: valitse vain **`read:repository`**
5. **Generate Token****kopioi token heti** (näytetään vain kerran)
6. Tallenna token talteen (`GITEA_RETENTION_TOKEN`)
### Vaihe 3: Generoi publish-token
**Palaa terminaalille:**
```bash
GITEA_RETENTION_TOKEN="<from Gitea>"
REPORTS_PUBLISH_TOKEN="$(openssl rand -base64 24)"
echo "Publish-token generoitu. Tallennetaan K8s-secretiin Vaiheessa 4."
echo "$REPORTS_PUBLISH_TOKEN"
```
### Vaihe 4: Luo K8s secrets
```bash
NS=gitea-reports
# 1. Publish-auth: htpasswd (Traefik BasicAuth - vaatii single-key secretin)
kubectl create secret generic gitea-reports-publish-auth \
--from-literal=users="$(docker run --rm httpd:2-alpine htpasswd -nb publish "$REPORTS_PUBLISH_TOKEN")" \
-n "$NS"
# 2. Publish-token: plaintext (luetaan README:stä Giteaan viedessä)
kubectl create secret generic gitea-reports-publish-token \
--from-literal=token="$REPORTS_PUBLISH_TOKEN" \
-n "$NS"
# 3. Retention (käyttää Vaiheessa 2 luotua PAT:ia)
kubectl create secret generic gitea-reports-retention-gitea \
--from-literal=token="$GITEA_RETENTION_TOKEN" \
-n "$NS"
kubectl get secrets -n "$NS"
```
---
### Vaihe 5: Vie publish-token Gitea Actions-secretiin
Token näkyy terminaalissa Vaiheen 3 `echo`sta. Jos se on jo kadonnut näkyvistä, hae se K8s-secretistä:
```bash
NS=gitea-reports
kubectl get secret gitea-reports-publish-token -n "$NS" -o jsonpath='{.data.token}' | base64 -d; echo
```
Vie kopioitu secret Gitea Actions Secrets:
Kannattaa viedä tämä token ORG tason secretiksi Gitea Actions. Muuten joudut tekemään tämän jokaiselle rapositorylle erikseen.
Luo `REPORTS_PUBLISH_TOKEN` secret, ja terminaalista kopioitu arvo `values` kenttään.
Description voi olla esim:
> Julkaisuun liittyvä salaisuus, jonka gitea-pages auth tarkistaa, että saako julkaista raporttia.
**Gitea UI:ssa:** New Secret → Name: `REPORTS_PUBLISH_TOKEN` → Value: **liitä token** → Save
Toista jokaiselle repolle, joka julkaisee raportteja.
---
### Seuraava: Helm-asennus
Palaa takaisin [README.md](../README.md#käyttöönotto) ja jatka kohdasta "Instanssin values-tiedosto".
---
## Secret Arkkitehtuuri
### Loogiset salaisuudet
| Looginen nimi | K8s | Gitea |
|---|---|---|
| `report_publish_api_token` | `gitea-reports-publish-auth` (htpasswd) | Actions Secret: `REPORTS_PUBLISH_TOKEN` |
| `reports_retention_read_token` | `gitea-reports-retention-gitea` (token) | PAT: `CI-REPORTS_READ_FOR_RETENTION` |
### Secret Reference Architecture
```mermaid
graph TD
subgraph "Publish Flow"
P1["Actions Secret<br/>REPORTS_PUBLISH_TOKEN"]
P2["K8s Secret<br/>gitea-reports-publish-auth"]
P1 -->|token| TRAEFIK
P2 -->|htpasswd| TRAEFIK
TRAEFIK["Traefik BasicAuth"]
end
subgraph "Retention Flow"
R1["K8s Secret<br/>gitea-reports-retention-gitea"]
R2["Gitea PAT<br/>CI-REPORTS_READ_FOR_RETENTION"]
R1 -->|token| SC["Sidecar"]
SC -->|API auth| GITEA["Gitea API"]
SC -->|read branches| GITEA
end
```
---
## Data Flow
### Flow 1: Julkaisu (Publish)
```mermaid
sequenceDiagram
participant Actions as Gitea Actions
participant Traefik as Traefik
participant K8sAuth as K8s Secret<br/>gitea-reports-publish-auth
participant K8sToken as K8s Secret<br/>gitea-reports-publish-token
participant Upload as upload-sidecar
participant PV as PVC /app/data
Note over Actions: 1. Lue plaintext-token
Actions->>K8sToken: lue token-avain
K8sToken-->>Actions: plaintext token
Note over Actions: 2. Lähettää raportin
Actions->>Traefik: PUT /{polku} + BasicAuth<br/>publish:TOKEN + tar.gz
Traefik->>K8sAuth: lue users (htpasswd)
K8sAuth-->>Traefik: publish:$apr1$...
alt Token match
Traefik->>Upload: PUT /{path}
Upload->>PV: extract tar.gz
PV-->>Upload: 201 Created
Upload-->>Traefik: 201 Created
Traefik-->>Actions: 201 Created
else Token ei match
Traefik-->>Actions: 401 Unauthorized
end
```
**Kaksi secretiä (Traefik-yhteensopivuus):**
- `gitea-reports-publish-auth` = `users` (htpasswd, Traefik käyttää)
- `gitea-reports-publish-token` = `token` (plaintext, luetaan Giteaan viedessä)
---
### Flow 2: Luku (Read)
```mermaid
sequenceDiagram
participant Browser as Selain
participant Traefik as Traefik
participant Nginx as nginx
participant PV as PVC /app/data
Browser->>Traefik: GET /OWNER/REPO/branch/SHA/suite/index.html
Traefik->>Nginx: välitä (ei authia)
Nginx->>PV: lue tiedosto
PV-->>Nginx: HTML
Nginx-->>Traefik: HTML
Traefik-->>Browser: HTML
```
GET/HEAD-reitillä ei ole Middlewarea. Luku on julkinen, jos URL tunnetaan.
---
### Flow 3: Retention (Siivous)
```mermaid
sequenceDiagram
participant CronJob as Retention CronJob
participant K8sSecret as K8s Secret<br/>gitea-reports-retention-gitea
participant GiteaAPI as Gitea API
participant PV as PVC /app/data
Note over CronJob: 1. Lue PAT
CronJob->>K8sSecret: lue token
K8sSecret-->>CronJob: Gitea PAT
Note over CronJob: 2. Lue PV suoraan
CronJob->>PV: ls /app/data/{owner}/{repo}/{branch}/
PV-->>CronJob: commit-kansiot
Note over CronJob: 3. Kysy branch
CronJob->>GiteaAPI: GET /api/v1/repos/OWNER/REPO/branches/BRANCH
GiteaAPI-->>CronJob: 200 / 404
Note over CronJob: 4. Poista vanhat
CronJob->>PV: rm -rf vanhat commit-kansiot
```
**Huomio:** Retention-PAT:in omistajalla on oltava lukuoikeus KAIKKIIN repoihin,
joista raportteja on PVC:llä.
---
## Troubleshooting
- **"secret not found"** — luiko secretit ennen Helm-asennusta?
- **"401 Unauthorized"** — onko Gitea Actions secret oikea?
- **"found 2 elements for secret"** — Traefik vaatii single-key secretin. Varmista että `gitea-reports-publish-auth` sisältää vain `users`-avaimen.
- **"token hukkuu"** — generoi uusi token (Vaihe 3) ja päivitä molemmat publish-secretit:
```bash
# 1. Generoi uusi
REPORTS_PUBLISH_TOKEN="$(openssl rand -base64 24)"
# 2. Päivitä K8s secrets (molemmat)
NS=gitea-reports
kubectl delete secret gitea-reports-publish-auth -n "$NS"
kubectl delete secret gitea-reports-publish-token -n "$NS"
kubectl create secret generic gitea-reports-publish-auth \
--from-literal=users="$(docker run --rm httpd:2-alpine htpasswd -nb publish "$REPORTS_PUBLISH_TOKEN")" \
-n "$NS"
kubectl create secret generic gitea-reports-publish-token \
--from-literal=token="$REPORTS_PUBLISH_TOKEN" \
-n "$NS"
# 3. Päivitä Gitea Actions secret jokaisessa repoissa (luke README:stä)
```
## Automatisointi: useamman repon salaisuuden lisääminen
Jos repoja on monta, voit käyttää Gitea API:ta (vaatii admin-tokenin):
```bash
ADMIN_TOKEN="<gitea-admin-token>"
NS=gitea-reports
# Lue plaintext-token erillisestä secretistä
TOKEN=$(kubectl get secret gitea-reports-publish-token -n "$NS" -o jsonpath='{.data.token}' | base64 -d)
for repo in "owner/repo1" "owner/repo2" "owner/repo3"; do
curl -X POST "https://gitea.example.com/api/v1/repos/$repo/actions/secrets" \
-H "Authorization: token $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"name\":\"REPORTS_PUBLISH_TOKEN\",\"data\":\"$TOKEN\"}"
done
```
Tai `tea` CLI:lla (Gitea:n virallinen CLI):
```bash
tea actions secrets add --repo owner/repo1 REPORTS_PUBLISH_TOKEN "$TOKEN"
tea actions secrets add --repo owner/repo2 REPORTS_PUBLISH_TOKEN "$TOKEN"
```
@@ -1,7 +1,7 @@
# Tech Stack — git-pages # Tech Stack — gitea-reports
> Mitä teknologioita `git-pages/` Helm chart käyttää ja edellyttää. Tämä dokumentti koskee vain > Mitä teknologioita `gitea-reports/` Helm chart käyttää ja edellyttää. Tämä dokumentti koskee vain
> `git-pages/`-alikansiota monorepossa — ei juuren `gitea-ci-library`-kirjastoa. > `gitea-reports/`-alikansiota monorepossa — ei juuren `gitea-ci-library`-kirjastoa.
--- ---
@@ -9,13 +9,9 @@
| Teknologia | Versio | Käyttö | | Teknologia | Versio | Käyttö |
|---|---|---| |---|---|---|
| **git-pages** (Codeberg) | `0.9.1` | Staattinen sisältö, apex index-site (`/.index`), HTTP PATCH/PUT -julkaisu | | **nginx** | alpine | Staattinen tiedostopalvelin (GET/HEAD) |
| **Filesystem storage** | — | Sisältö PVC:llä (`/app/data`) | | **Python** | 3-alpine | PUT-vastaanotto + tar.gz-purku PV:lle |
| **TOML** | — | Sovellusconfig ConfigMapissa (`config.toml`) | | **Filesystem storage** | — | Sisältö PVC:llä |
Image: `codeberg.org/git-pages/git-pages:0.9.1` (ei `v`-etuliitettä tagissa).
Chart ajaa `PAGES_INSECURE=1` — julkaisuvaltuutus Traefik Middlewaressä, ei forge-authia.
--- ---
@@ -26,7 +22,7 @@ Chart ajaa `PAGES_INSECURE=1` — julkaisuvaltuutus Traefik Middlewaressä, ei f
| **Kubernetes** | 1.24+ | Deployment, Service, PVC, Secret | | **Kubernetes** | 1.24+ | Deployment, Service, PVC, Secret |
| **Helm** | v3 | Chart asennus ja päivitys | | **Helm** | v3 | Chart asennus ja päivitys |
| **Traefik** | CRD `traefik.io/v1alpha1` | IngressRoute, Middleware (`basicAuth`, HTTPS-redirect) | | **Traefik** | CRD `traefik.io/v1alpha1` | IngressRoute, Middleware (`basicAuth`, HTTPS-redirect) |
| **cert-manager** | — | TLS-sertifikaatti (`git-pages-tls`) | | **cert-manager** | — | TLS-sertifikaatti (`gitea-reports-tls`) |
--- ---
@@ -43,8 +39,8 @@ Chart ajaa `PAGES_INSECURE=1` — julkaisuvaltuutus Traefik Middlewaressä, ei f
| Resurssi | Lähde | Dokumentti | | Resurssi | Lähde | Dokumentti |
|---|---|---| |---|---|---|
| `git-pages-publish-auth` | Manuaalinen Secret | [secrets.md](secrets.md) | | `gitea-reports-publish-auth` | Manuaalinen Secret | [secrets.md](secrets.md) |
| `git-pages-tls` | cert-manager Certificate | Automaattinen asennuksessa | | `gitea-reports-tls` | cert-manager Certificate | Automaattinen asennuksessa |
| `ClusterIssuer` | Klusteri (esim. `letsencrypt-prod`) | `dev-values.yaml` | | `ClusterIssuer` | Klusteri (esim. `letsencrypt-prod`) | `dev-values.yaml` |
--- ---
@@ -66,7 +62,7 @@ Chart ajaa `PAGES_INSECURE=1` — julkaisuvaltuutus Traefik Middlewaressä, ei f
| `values.yaml` | Chart-vakiot | Image, resurssit, Traefik entrypointit | | `values.yaml` | Chart-vakiot | Image, resurssit, Traefik entrypointit |
| `{env}-values.yaml` | Instanssi | Host, issuer, PVC koko/storageClass | | `{env}-values.yaml` | Instanssi | Host, issuer, PVC koko/storageClass |
Esimerkki: `helm upgrade --install git-pages ./git-pages -n git-pages -f dev-values.yaml` Esimerkki: `helm upgrade --install gitea-reports ./gitea-reports -n gitea-reports -f dev-values.yaml`
--- ---
@@ -74,8 +70,8 @@ Esimerkki: `helm upgrade --install git-pages ./git-pages -n git-pages -f dev-val
| Teknologia | Syy | | Teknologia | Syy |
|---|---| |---|---|
| **Gitea `pages`-branch** | Ei Gitea git-pages -integraatiota | | **Gitea `pages`-branch** | Ei Gitea gitea-reports -integraatiota |
| **deadnews/gitea-pages** | Ei Gitea API -hakua | | **deadnews/gitea-pages** | Ei Gitea API -hakua |
| **MinIO / S3** | Erillinen raporttivarasto — ei tämän chartin scope | | **MinIO / S3** | Erillinen raporttivarasto — ei tämän chartin scope |
| **Gitea forge-auth / PAT** | Julkaisu BasicAuth-tokenilla (`git-pages-publish-auth`) | | **Gitea forge-auth / PAT** | Julkaisu BasicAuth-tokenilla (`gitea-reports-publish-auth`) |
| **Helm-managed publish Secret** | `publishAuth.create: false` — secret manuaalisesti | | **Helm-managed publish Secret** | `publishAuth.create: false` — secret manuaalisesti |
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Retention cleanup: read PV directly, no git-pages API.
# Removes old commit-level report dirs based on branch existence + age rules.
set -euo pipefail
DATA_ROOT="${DATA_ROOT:-/app/data}"
CONFIG="${RETENTION_CONFIG:-/etc/retention/retention.json}"
GITEA_API_URL="${GITEA_API_URL:-}"
GITEA_TOKEN="${GITEA_TOKEN:-}"
[ -f "$CONFIG" ] || { echo "ERROR: config missing: $CONFIG" >&2; exit 1; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/retention-lib.sh"
DELETED_COUNT=0
ERROR_COUNT=0
for owner_dir in "$DATA_ROOT"/*/; do
[ -d "$owner_dir" ] || continue
owner=$(basename "$owner_dir")
for repo_dir in "$owner_dir"*/; do
[ -d "$repo_dir" ] || continue
repo=$(basename "$repo_dir")
for branch_dir in "$repo_dir"*/; do
[ -d "$branch_dir" ] || continue
branch=$(basename "$branch_dir")
echo "=== ${owner}/${repo}/${branch} ==="
if ! branch_exists "$owner" "$repo" "$branch"; then
echo " Branch does not exist in Gitea -> DELETE ALL"
rm -rf "$branch_dir"
DELETED_COUNT=$((DELETED_COUNT + 1))
continue
fi
rules=$(jq -r ".branches.\"$branch\" // .branches.default" "$CONFIG")
keep_min=$(echo "$rules" | jq -r '.keepMin // 5')
min_age=$(echo "$rules" | jq -r '.minAgeDays // 7')
commits=$(ls -1t "$branch_dir" 2>/dev/null || true)
count=0
for sha8 in $commits; do
[ -d "$branch_dir/$sha8" ] || continue
count=$((count + 1))
[ "$count" -le "$keep_min" ] && continue
age=$(age_days_from_stat "$branch_dir/$sha8")
[ "$age" -lt "$min_age" ] && continue
echo " DELETE: ${owner}/${repo}/${branch}/${sha8} (age=${age}d, keepMin=${keep_min})"
rm -rf "$branch_dir/$sha8"
DELETED_COUNT=$((DELETED_COUNT + 1))
done
done
done
done
echo ""
echo "=== Summary ==="
echo " Deleted: ${DELETED_COUNT}"
echo " Errors: ${ERROR_COUNT}"
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Shared functions for retention-cleanup.sh
# Can be sourced by tests for unit testing
age_days_from_stat() {
local dir="$1" epoch_dir now
epoch_dir=$(stat -c %Y "$dir" 2>/dev/null || stat -f %m "$dir" 2>/dev/null || echo 0)
[ "$epoch_dir" -eq 0 ] && echo 99999 && return
now=$(date +%s)
echo $(( (now - epoch_dir) / 86400 ))
}
read_rule() {
local config="$1" branch="$2" key="$3" default="$4"
v=$(jq -r --arg b "$branch" --arg k "$key" '.branches[$b][$k] // empty' "$config")
[ -n "$v" ] && echo "$v" || echo "$default"
}
branch_exists() {
local owner="$1" repo="$2" branch="$3"
local branch_urlenc code
[ -z "$GITEA_API_URL" ] && return 0
[ -z "$GITEA_TOKEN" ] && return 0
branch_urlenc=$(echo "$branch" | sed 's/\//%2F/g')
code=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token $GITEA_TOKEN" \
"$GITEA_API_URL/api/v1/repos/$owner/$repo/branches/$branch_urlenc" 2>/dev/null || echo "000")
[ "$code" = "200" ]
}
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
# Run retention cleanup on PVC directly (no git-pages API).
set -euo pipefail
/scripts/retention-cleanup.sh
echo "Retention job done."
+19
View File
@@ -0,0 +1,19 @@
Nginx + upload-sidecar installed.
See docs/secrets.md for secret prerequisites (K8s + Gitea Actions).
Install:
helm upgrade --install gitea-reports ./gitea-reports -n {{ .Release.Namespace }} --create-namespace -f dev-values.yaml
Host: https://{{ .Values.ingress.host }}
Reports: https://{{ .Values.ingress.host }}/{owner}/{repo}/{branch}/{sha8}/{suite}/
Publish (CI):
curl -X PUT \
-u publish:<token> \
-H "Content-Type: application/tar+gz" \
--data-binary @report.tar.gz \
https://{{ .Values.ingress.host }}/{owner}/{repo}/{branch}/{sha8}/{suite}/
Upgrade: helm upgrade {{ .Release.Name }} ./gitea-reports -n {{ .Release.Namespace }} -f dev-values.yaml
Uninstall: helm uninstall {{ .Release.Name }} -n {{ .Release.Namespace }}
@@ -1,8 +1,8 @@
{{- define "git-pages.name" -}} {{- define "gitea-reports.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }} {{- end }}
{{- define "git-pages.fullname" -}} {{- define "gitea-reports.fullname" -}}
{{- if .Values.fullnameOverride }} {{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }} {{- else }}
@@ -15,25 +15,25 @@
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- define "git-pages.chart" -}} {{- define "gitea-reports.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }} {{- end }}
{{- define "git-pages.labels" -}} {{- define "gitea-reports.labels" -}}
helm.sh/chart: {{ include "git-pages.chart" . }} helm.sh/chart: {{ include "gitea-reports.chart" . }}
{{ include "git-pages.selectorLabels" . }} {{ include "gitea-reports.selectorLabels" . }}
{{- if .Chart.AppVersion }} {{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }} {{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }} {{- end }}
{{- define "git-pages.selectorLabels" -}} {{- define "gitea-reports.selectorLabels" -}}
app.kubernetes.io/name: {{ include "git-pages.name" . }} app.kubernetes.io/name: {{ include "gitea-reports.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }} {{- end }}
{{- define "git-pages.componentLabels" -}} {{- define "gitea-reports.componentLabels" -}}
{{ include "git-pages.labels" . }} {{ include "gitea-reports.labels" . }}
app.kubernetes.io/component: pages-server app.kubernetes.io/component: pages-server
{{- end }} {{- end }}
@@ -2,11 +2,11 @@
apiVersion: cert-manager.io/v1 apiVersion: cert-manager.io/v1
kind: Certificate kind: Certificate
metadata: metadata:
name: {{ include "git-pages.fullname" . }}-tls name: {{ include "gitea-reports.fullname" . }}-tls
labels: labels:
{{- include "git-pages.componentLabels" . | nindent 4 }} {{- include "gitea-reports.componentLabels" . | nindent 4 }}
spec: spec:
secretName: git-pages-tls secretName: {{ include "gitea-reports.fullname" . }}-tls
dnsNames: dnsNames:
- {{ .Values.ingress.host | quote }} - {{ .Values.ingress.host | quote }}
issuerRef: issuerRef:
+43
View File
@@ -0,0 +1,43 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "gitea-reports.fullname" . }}-config
labels:
{{- include "gitea-reports.componentLabels" . | nindent 4 }}
data:
default.conf: |
client_max_body_size {{ .Values.nginx.bodySize }};
server {
listen {{ .Values.nginx.port }};
root /app/data;
autoindex on;
client_max_body_size {{ .Values.nginx.bodySize }};
location / {
try_files $uri $uri/ $uri/index.html =404;
}
}
upload-server.py: |
#!/usr/bin/env python3
import http.server, os, sys, tarfile, io, urllib.parse
PORT = int(os.environ.get("UPLOAD_PORT", "8081"))
DATA_ROOT = "/app/data"
class Handler(http.server.BaseHTTPRequestHandler):
def do_PUT(self):
path = urllib.parse.unquote(self.path)
dest = DATA_ROOT + path.rstrip("/")
os.makedirs(dest, exist_ok=True)
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
with tarfile.open(fileobj=io.BytesIO(body), mode="r:gz") as tar:
tar.extractall(dest)
self.send_response(201)
self.end_headers()
def log_message(self, fmt, *args):
sys.stderr.write("%s - %s\n" % (self.client_address[0], fmt % args))
http.server.HTTPServer(("", PORT), Handler).serve_forever()
+75
View File
@@ -0,0 +1,75 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "gitea-reports.fullname" . }}
labels:
{{- include "gitea-reports.componentLabels" . | nindent 4 }}
spec:
replicas: 1
selector:
matchLabels:
{{- include "gitea-reports.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "gitea-reports.componentLabels" . | nindent 8 }}
spec:
securityContext:
fsGroup: 1000
containers:
- name: nginx
image: "{{ .Values.nginx.image }}:{{ .Values.nginx.tag }}"
imagePullPolicy: IfNotPresent
ports:
- name: http-read
containerPort: {{ .Values.nginx.port }}
protocol: TCP
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/conf.d
readOnly: true
{{- if .Values.persistence.enabled }}
- name: data
mountPath: /app/data
{{- end }}
resources:
{{- toYaml .Values.nginx.resources | nindent 12 }}
- name: upload
image: "{{ .Values.upload.image }}:{{ .Values.upload.tag }}"
imagePullPolicy: IfNotPresent
command:
- python3
- /upload/upload-server.py
ports:
- name: http-write
containerPort: {{ .Values.upload.port }}
protocol: TCP
volumeMounts:
- name: upload-script
mountPath: /upload
readOnly: true
{{- if .Values.persistence.enabled }}
- name: data
mountPath: /app/data
{{- end }}
resources:
{{- toYaml .Values.upload.resources | nindent 12 }}
volumes:
- name: nginx-conf
configMap:
name: {{ include "gitea-reports.fullname" . }}-config
items:
- key: default.conf
path: default.conf
- name: upload-script
configMap:
name: {{ include "gitea-reports.fullname" . }}-config
defaultMode: 0755
items:
- key: upload-server.py
path: upload-server.py
{{- if .Values.persistence.enabled }}
- name: data
persistentVolumeClaim:
claimName: {{ include "gitea-reports.fullname" . }}-data
{{- end }}
@@ -2,9 +2,9 @@
apiVersion: traefik.io/v1alpha1 apiVersion: traefik.io/v1alpha1
kind: IngressRoute kind: IngressRoute
metadata: metadata:
name: {{ include "git-pages.fullname" . }} name: {{ include "gitea-reports.fullname" . }}
labels: labels:
{{- include "git-pages.componentLabels" . | nindent 4 }} {{- include "gitea-reports.componentLabels" . | nindent 4 }}
spec: spec:
entryPoints: entryPoints:
- {{ .Values.ingress.entryPoints.websecure }} - {{ .Values.ingress.entryPoints.websecure }}
@@ -14,24 +14,24 @@ spec:
(Method(`PATCH`) || Method(`PUT`)) (Method(`PATCH`) || Method(`PUT`))
kind: Rule kind: Rule
middlewares: middlewares:
- name: {{ include "git-pages.fullname" . }}-publish-auth - name: {{ include "gitea-reports.fullname" . }}-publish-auth
services: services:
- name: {{ include "git-pages.fullname" . }} - name: {{ include "gitea-reports.fullname" . }}
port: {{ .Values.service.port }} port: {{ .Values.upload.port }}
- match: Host(`{{ .Values.ingress.host }}`) && (Method(`GET`) || Method(`HEAD`)) - match: Host(`{{ .Values.ingress.host }}`) && (Method(`GET`) || Method(`HEAD`))
kind: Rule kind: Rule
services: services:
- name: {{ include "git-pages.fullname" . }} - name: {{ include "gitea-reports.fullname" . }}
port: {{ .Values.service.port }} port: {{ .Values.nginx.port }}
tls: tls:
secretName: git-pages-tls secretName: {{ include "gitea-reports.fullname" . }}-tls
--- ---
apiVersion: traefik.io/v1alpha1 apiVersion: traefik.io/v1alpha1
kind: IngressRoute kind: IngressRoute
metadata: metadata:
name: {{ include "git-pages.fullname" . }}-http name: {{ include "gitea-reports.fullname" . }}-http
labels: labels:
{{- include "git-pages.componentLabels" . | nindent 4 }} {{- include "gitea-reports.componentLabels" . | nindent 4 }}
spec: spec:
entryPoints: entryPoints:
- {{ .Values.ingress.entryPoints.web }} - {{ .Values.ingress.entryPoints.web }}
@@ -41,8 +41,8 @@ spec:
!PathPrefix(`/.well-known/acme-challenge/`) !PathPrefix(`/.well-known/acme-challenge/`)
kind: Rule kind: Rule
middlewares: middlewares:
- name: {{ include "git-pages.fullname" . }}-https-redirect - name: {{ include "gitea-reports.fullname" . }}-https-redirect
services: services:
- name: {{ include "git-pages.fullname" . }} - name: {{ include "gitea-reports.fullname" . }}
port: {{ .Values.service.port }} port: {{ .Values.nginx.port }}
{{- end }} {{- end }}
+22
View File
@@ -0,0 +1,22 @@
{{- if .Values.ingress.enabled }}
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: {{ include "gitea-reports.fullname" . }}-publish-auth
labels:
{{- include "gitea-reports.componentLabels" . | nindent 4 }}
spec:
basicAuth:
secret: {{ include "gitea-reports.fullname" . }}-publish-auth
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: {{ include "gitea-reports.fullname" . }}-https-redirect
labels:
{{- include "gitea-reports.componentLabels" . | nindent 4 }}
spec:
redirectScheme:
scheme: https
permanent: true
{{- end }}
@@ -2,9 +2,9 @@
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
metadata: metadata:
name: git-pages-publish-auth name: {{ include "gitea-reports.fullname" . }}-publish-auth
labels: labels:
{{- include "git-pages.componentLabels" . | nindent 4 }} {{- include "gitea-reports.componentLabels" . | nindent 4 }}
type: Opaque type: Opaque
stringData: stringData:
users: | users: |
@@ -2,9 +2,9 @@
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
name: {{ include "git-pages.fullname" . }}-data name: {{ include "gitea-reports.fullname" . }}-data
labels: labels:
{{- include "git-pages.componentLabels" . | nindent 4 }} {{- include "gitea-reports.componentLabels" . | nindent 4 }}
spec: spec:
accessModes: accessModes:
- {{ .Values.persistence.accessMode }} - {{ .Values.persistence.accessMode }}
@@ -2,12 +2,14 @@
apiVersion: v1 apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
name: git-pages-retention name: {{ include "gitea-reports.fullname" . }}-retention
labels: labels:
{{- include "git-pages.componentLabels" . | nindent 4 }} {{- include "gitea-reports.componentLabels" . | nindent 4 }}
data: data:
retention.json: | retention.json: |
{{- .Values.retention.rules | toJson | nindent 4 }} {{- .Values.retention.rules | toJson | nindent 4 }}
retention-lib.sh: |
{{- .Files.Get "files/retention-lib.sh" | nindent 4 }}
retention-cleanup.sh: | retention-cleanup.sh: |
{{- .Files.Get "files/retention-cleanup.sh" | nindent 4 }} {{- .Files.Get "files/retention-cleanup.sh" | nindent 4 }}
retention-run.sh: | retention-run.sh: |
@@ -2,9 +2,9 @@
apiVersion: batch/v1 apiVersion: batch/v1
kind: CronJob kind: CronJob
metadata: metadata:
name: git-pages-retention name: {{ include "gitea-reports.fullname" . }}-retention
labels: labels:
{{- include "git-pages.componentLabels" . | nindent 4 }} {{- include "gitea-reports.componentLabels" . | nindent 4 }}
spec: spec:
schedule: {{ .Values.retention.schedule | quote }} schedule: {{ .Values.retention.schedule | quote }}
concurrencyPolicy: Forbid concurrencyPolicy: Forbid
@@ -16,10 +16,10 @@ spec:
template: template:
metadata: metadata:
labels: labels:
app.kubernetes.io/name: git-pages-retention app.kubernetes.io/name: {{ include "gitea-reports.fullname" . }}-retention
app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/instance: {{ .Release.Name }}
spec: spec:
serviceAccountName: git-pages-retention serviceAccountName: {{ include "gitea-reports.fullname" . }}-retention
restartPolicy: OnFailure restartPolicy: OnFailure
containers: containers:
- name: retention - name: retention
@@ -34,15 +34,9 @@ spec:
set -euo pipefail set -euo pipefail
apt-get update -qq apt-get update -qq
apt-get install -y --no-install-recommends curl jq >/dev/null apt-get install -y --no-install-recommends curl jq >/dev/null
chmod +x /scripts/retention-run.sh /scripts/retention-cleanup.sh chmod +x /scripts/retention-run.sh
/scripts/retention-run.sh /scripts/retention-run.sh
env: env:
- name: NAMESPACE
value: {{ .Release.Namespace | quote }}
- name: DEPLOYMENT
value: {{ include "git-pages.fullname" . | quote }}
- name: INSTANCE
value: {{ .Release.Name | quote }}
- name: DATA_ROOT - name: DATA_ROOT
value: /app/data value: /app/data
- name: RETENTION_CONFIG - name: RETENTION_CONFIG
@@ -52,7 +46,7 @@ spec:
- name: GITEA_TOKEN - name: GITEA_TOKEN
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: git-pages-retention-gitea name: {{ include "gitea-reports.fullname" . }}-retention-gitea
key: token key: token
volumeMounts: volumeMounts:
- name: data - name: data
@@ -64,14 +58,14 @@ spec:
volumes: volumes:
- name: data - name: data
persistentVolumeClaim: persistentVolumeClaim:
claimName: {{ include "git-pages.fullname" . }}-data claimName: {{ include "gitea-reports.fullname" . }}-data
- name: scripts - name: scripts
configMap: configMap:
name: git-pages-retention name: {{ include "gitea-reports.fullname" . }}-retention
defaultMode: 0755 defaultMode: 0755
- name: config - name: config
configMap: configMap:
name: git-pages-retention name: {{ include "gitea-reports.fullname" . }}-retention
items: items:
- key: retention.json - key: retention.json
path: retention.json path: retention.json
@@ -2,16 +2,16 @@
apiVersion: v1 apiVersion: v1
kind: ServiceAccount kind: ServiceAccount
metadata: metadata:
name: git-pages-retention name: {{ include "gitea-reports.fullname" . }}-retention
labels: labels:
{{- include "git-pages.componentLabels" . | nindent 4 }} {{- include "gitea-reports.componentLabels" . | nindent 4 }}
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
kind: Role kind: Role
metadata: metadata:
name: git-pages-retention name: {{ include "gitea-reports.fullname" . }}-retention
labels: labels:
{{- include "git-pages.componentLabels" . | nindent 4 }} {{- include "gitea-reports.componentLabels" . | nindent 4 }}
rules: rules:
- apiGroups: ["apps"] - apiGroups: ["apps"]
resources: ["deployments", "deployments/scale"] resources: ["deployments", "deployments/scale"]
@@ -23,15 +23,15 @@ rules:
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding kind: RoleBinding
metadata: metadata:
name: git-pages-retention name: {{ include "gitea-reports.fullname" . }}-retention
labels: labels:
{{- include "git-pages.componentLabels" . | nindent 4 }} {{- include "gitea-reports.componentLabels" . | nindent 4 }}
roleRef: roleRef:
apiGroup: rbac.authorization.k8s.io apiGroup: rbac.authorization.k8s.io
kind: Role kind: Role
name: git-pages-retention name: {{ include "gitea-reports.fullname" . }}-retention
subjects: subjects:
- kind: ServiceAccount - kind: ServiceAccount
name: git-pages-retention name: {{ include "gitea-reports.fullname" . }}-retention
namespace: {{ .Release.Namespace }} namespace: {{ .Release.Namespace }}
{{- end }} {{- end }}
+19
View File
@@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "gitea-reports.fullname" . }}
labels:
{{- include "gitea-reports.componentLabels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
selector:
{{- include "gitea-reports.selectorLabels" . | nindent 4 }}
ports:
- name: http-read
port: {{ .Values.nginx.port }}
targetPort: http-read
protocol: TCP
- name: http-write
port: {{ .Values.upload.port }}
targetPort: http-write
protocol: TCP
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env bats
setup() {
source "$(dirname "$BATS_TEST_DIRNAME")/files/retention-lib.sh"
CONFIG=$(mktemp)
}
teardown() {
rm -f "$CONFIG"
}
write_config() {
cat > "$CONFIG"
}
# ---------------------------------------------------------------------------
# read_rule
# ---------------------------------------------------------------------------
@test "read_rule returns default when branch has no override" {
write_config <<'EOF'
{"branches":{"default":{"minAgeDays":90,"keepMin":5}}}
EOF
result=$(read_rule "$CONFIG" "nonexistent" "minAgeDays" 90)
[ "$result" = "90" ]
}
@test "read_rule returns branch-specific value" {
write_config <<'EOF'
{"branches":{"default":{"minAgeDays":90,"keepMin":5},"main":{"minAgeDays":365,"keepMin":20}}}
EOF
result=$(read_rule "$CONFIG" "main" "keepMin" 5)
[ "$result" = "20" ]
}
@test "read_rule returns default for undefined key even if branch exists" {
write_config <<'EOF'
{"branches":{"default":{"minAgeDays":90,"keepMin":5},"main":{"minAgeDays":365}}}
EOF
result=$(read_rule "$CONFIG" "main" "keepMin" 5)
[ "$result" = "5" ]
}
# ---------------------------------------------------------------------------
# age_days_from_stat
# ---------------------------------------------------------------------------
@test "age_days_from_stat returns 0 for newly created dir" {
local tmpdir=$(mktemp -d)
result=$(age_days_from_stat "$tmpdir")
[ "$result" -eq 0 ] || [ "$result" -eq 1 ]
rm -rf "$tmpdir"
}
@test "age_days_from_stat returns 99999 for nonexistent dir" {
result=$(age_days_from_stat "/nonexistent/path")
[ "$result" = "99999" ]
}
@test "age_days_from_stat returns positive for old dir" {
local tmpdir=$(mktemp -d)
# Touch mtime back 2 days
touch -t "$(date -v-2d +%Y%m%d%H%M.%S 2>/dev/null || date -d '2 days ago' +%Y%m%d%H%M.%S 2>/dev/null)" "$tmpdir" 2>/dev/null || true
result=$(age_days_from_stat "$tmpdir")
[ "$result" -ge 1 ]
rm -rf "$tmpdir"
}
# ---------------------------------------------------------------------------
# branch_exists — edge cases (no mock needed)
# ---------------------------------------------------------------------------
@test "branch_exists: empty GITEA_API_URL → return 0 (skip)" {
GITEA_API_URL=""
GITEA_TOKEN="test-token"
run branch_exists "owner" "repo" "any-branch"
[ "$status" -eq 0 ]
}
@test "branch_exists: empty GITEA_TOKEN → return 0 (skip)" {
GITEA_API_URL="https://gitea.example.com"
GITEA_TOKEN=""
run branch_exists "owner" "repo" "any-branch"
[ "$status" -eq 0 ]
}
# ---------------------------------------------------------------------------
# retention-cleanup integration — mock PV tree
# ---------------------------------------------------------------------------
@test "retention-cleanup: deletes only old commits beyond keepMin" {
local dataroot=$(mktemp -d)
local config=$(mktemp)
cat > "$config" <<'EOF'
{"branches":{"default":{"minAgeDays":0,"keepMin":2}}}
EOF
# Create mock PV: owner/repo/branch/{c1,c2,c3}
mkdir -p "$dataroot/owner/repo/main/c1/suite"
mkdir -p "$dataroot/owner/repo/main/c2/suite"
mkdir -p "$dataroot/owner/repo/main/c3/suite"
DATA_ROOT="$dataroot"
RETENTION_CONFIG="$config"
GITEA_API_URL=""
GITEA_TOKEN=""
# Run main loop (subset of retention-cleanup.sh logic)
for branch_dir in "$dataroot/owner/repo/main/"; do
branch=$(basename "$branch_dir")
rules=$(jq -r ".branches.\"$branch\" // .branches.default" "$config")
keep_min=$(echo "$rules" | jq -r '.keepMin // 5')
min_age=$(echo "$rules" | jq -r '.minAgeDays // 7')
commits=$(ls -1t "$branch_dir" 2>/dev/null || true)
count=0
for sha8 in $commits; do
[ -d "$branch_dir/$sha8" ] || continue
count=$((count + 1))
[ "$count" -le "$keep_min" ] && continue
age=$(age_days_from_stat "$branch_dir/$sha8")
[ "$age" -lt "$min_age" ] && continue
rm -rf "$branch_dir/$sha8"
done
done
# c1 (oldest, 3rd in ls -1t) should be deleted, c2 and c3 kept
[ -d "$dataroot/owner/repo/main/c2" ]
[ -d "$dataroot/owner/repo/main/c3" ]
[ ! -d "$dataroot/owner/repo/main/c1" ]
rm -rf "$dataroot" "$config"
}
@test "retention-cleanup: deletes entire branch dir when branch not in Gitea" {
local dataroot=$(mktemp -d)
mkdir -p "$dataroot/owner/repo/deleted-branch/c1/suite"
mkdir -p "$dataroot/owner/repo/active-branch/c2/suite"
DATA_ROOT="$dataroot"
RETENTION_CONFIG="/dev/null"
GITEA_API_URL=""
GITEA_TOKEN=""
# With empty GITEA_API_URL, branch_exists returns 0 → no deletion
# (can't mock Gitea 404 without external deps)
rm -rf "$dataroot"
}
@@ -1,33 +1,42 @@
# Constants — shared across all instances. Do not put per-env values here. # Constants — shared across all instances. Do not put per-env values here.
# Per instance: use {env}-values.yaml (e.g. dev-values.yaml): # Per instance: use {env}-values.yaml (e.g. dev-values.yaml):
# helm install git-pages ./git-pages -n git-pages -f dev-values.yaml # helm install gitea-reports ./gitea-reports -n gitea-reports -f dev-values.yaml
nameOverride: "" nameOverride: ""
fullnameOverride: "" fullnameOverride: ""
image: nginx:
repository: codeberg.org/git-pages/git-pages image: nginx
tag: "0.9.1" tag: alpine
pullPolicy: IfNotPresent port: 8080
bodySize: 100M
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 200m
memory: 128Mi
pagesInsecure: true upload:
image: python
tag: 3-alpine
port: 8081
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
service: service:
type: ClusterIP type: ClusterIP
port: 3000
persistence: persistence:
enabled: true enabled: true
accessMode: ReadWriteOnce accessMode: ReadWriteOnce
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 1000m
memory: 512Mi
ingress: ingress:
enabled: true enabled: true
entryPoints: entryPoints:
@@ -37,15 +46,6 @@ ingress:
certificate: certificate:
enabled: true enabled: true
# Post-install init job: creates placeholder site so .index exists.
# Consumers can use PATCH directly without PUT fallback.
initJob:
enabled: true
image:
repository: debian
tag: bookworm-slim
pullPolicy: IfNotPresent
# Optional Helm-managed secret — prefer manual create (see docs/secrets.md). # Optional Helm-managed secret — prefer manual create (see docs/secrets.md).
publishAuth: publishAuth:
create: false create: false
@@ -53,7 +53,7 @@ publishAuth:
retention: retention:
enabled: false enabled: false
mode: sidecar mode: cronjob
schedule: "0 3 * * *" schedule: "0 3 * * *"
image: image:
repository: debian repository: debian
@@ -63,5 +63,5 @@ retention:
rules: rules:
branches: branches:
default: default:
maxAgeDays: 90 minAgeDays: 7
keepMin: 5 keepMin: 5
+2 -2
View File
@@ -4,7 +4,7 @@ set -e
RAW_VERSION="" RAW_VERSION=""
if [ -n "${VERSION_FILE-}" ] && [ -f "${VERSION_FILE-}" ]; then if [ -n "${VERSION_FILE-}" ] && [ -f "${VERSION_FILE-}" ]; then
RAW_VERSION=$(sed -n 's/^version:[[:space:]]*\([^[:space:]]*\).*/\1/p' "${VERSION_FILE}") RAW_VERSION=$(tr -d "$(printf '\xef\xbb\xbf')" < "${VERSION_FILE}" | sed -n 's/^version:[[:space:]]*\([^[:space:]]*\).*/\1/p')
if [ -z "${RAW_VERSION}" ]; then if [ -z "${RAW_VERSION}" ]; then
if echo "${VERSION_FILE}" | grep -q -E '\.json$'; then if echo "${VERSION_FILE}" | grep -q -E '\.json$'; then
RAW_VERSION=$(jq -r '.version' "${VERSION_FILE}") RAW_VERSION=$(jq -r '.version' "${VERSION_FILE}")
@@ -22,7 +22,7 @@ if [ -z "${RAW_VERSION}" ]; then
elif [ -f pom.xml ]; then elif [ -f pom.xml ]; then
RAW_VERSION=$(grep -oP '<version>\K[^<]+' pom.xml | head -1) RAW_VERSION=$(grep -oP '<version>\K[^<]+' pom.xml | head -1)
elif [ -f Chart.yaml ]; then elif [ -f Chart.yaml ]; then
RAW_VERSION=$(sed -n 's/^version:[[:space:]]*\([^[:space:]]*\).*/\1/p' Chart.yaml) RAW_VERSION=$(tr -d "$(printf '\xef\xbb\xbf')" < Chart.yaml | sed -n 's/^version:[[:space:]]*\([^[:space:]]*\).*/\1/p')
else else
echo "ERROR: No version source found (VERSION_FILE, VERSION, package.json, pom.xml, Chart.yaml)" >&2 echo "ERROR: No version source found (VERSION_FILE, VERSION, package.json, pom.xml, Chart.yaml)" >&2
exit 1 exit 1
+67 -45
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env sh
set -euo pipefail set -eu
DESCRIPTION="${1:-}" DESCRIPTION="${1:-}"
CONTEXT="${2:-}" CONTEXT="${2:-}"
@@ -14,74 +14,96 @@ REPORT_DIR="reports/${SUITE}"
if [ ! -d "$REPORT_DIR" ]; then if [ ! -d "$REPORT_DIR" ]; then
echo "ERROR: $REPORT_DIR not found" >&2 echo "ERROR: $REPORT_DIR not found" >&2
bash .ci/scripts/report-status.sh failure "$DESCRIPTION" "$CONTEXT" sh .ci/scripts/report-status.sh failure "$DESCRIPTION" "$CONTEXT"
exit 1 exit 1
fi fi
FILES=() FILE_COUNT=0
while IFS= read -r -d '' f; do SUBDIR_COUNT=0
FILES+=("$(basename "$f")") ENTRIES=""
done < <(find "$REPORT_DIR" -maxdepth 1 -type f ! -name index.html -print0 2>/dev/null || true)
SUBDIRS=() for f in "$REPORT_DIR"/*; do
while IFS= read -r -d '' d; do [ -f "$f" ] || continue
name="${d#$REPORT_DIR/}" base=$(basename "$f")
[ -f "$d/index.html" ] && SUBDIRS+=("$name") [ "$base" = "index.html" ] && continue
done < <(find "$REPORT_DIR" -maxdepth 1 -type d ! -name . -print0 2>/dev/null || true) FILE_COUNT=$((FILE_COUNT + 1))
ENTRIES="${ENTRIES}file:${base}
"
done
TOTAL=$(( ${#FILES[@]} + ${#SUBDIRS[@]} )) for d in "$REPORT_DIR"/*/; do
[ -d "$d" ] || continue
base=$(basename "$d")
[ -f "$d/index.html" ] || continue
SUBDIR_COUNT=$((SUBDIR_COUNT + 1))
ENTRIES="${ENTRIES}dir:${base}
"
done
TOTAL=$((FILE_COUNT + SUBDIR_COUNT))
if [ "$TOTAL" -eq 0 ]; then if [ "$TOTAL" -eq 0 ]; then
echo "ERROR: no reportable items in $REPORT_DIR" >&2 echo "ERROR: no reportable items in $REPORT_DIR" >&2
bash .ci/scripts/report-status.sh failure "$DESCRIPTION" "$CONTEXT" sh .ci/scripts/report-status.sh failure "$DESCRIPTION" "$CONTEXT"
exit 1 exit 1
fi fi
SHA8="${GITHUB_SHA:0:8}" SHA8=$(echo "${GITHUB_SHA:-xxxxxxxx}" | cut -c1-8)
humanize() { humanize() {
local name="$1" name="$1"
name="${name%.*}" name=$(echo "$name" | sed -e 's/\.[^.]*$//' -e 's/[-_]/ /g')
name="${name//-/ }" first=$(echo "$name" | cut -c1 | tr '[:lower:]' '[:upper:]')
name="${name//_/ }" rest=$(echo "$name" | cut -c2-)
echo "${name^}" echo "${first}${rest}"
} }
generate_index() { generate_index() {
local html {
html='<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">' echo '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">'
html+="<title>$DESCRIPTION</title>" echo "<title>$DESCRIPTION</title>"
html+='<style>body{font-family:sans-serif;margin:2em;max-width:960px}h1{color:#1e293b}ul{list-style:none;padding:0}li{margin:.5em 0;padding:.5em;background:#f8fafc;border-radius:6px}a{color:#2563eb;text-decoration:none}a:hover{text-decoration:underline}</style>' echo '<style>body{font-family:sans-serif;margin:2em;max-width:960px}h1{color:#1e293b}ul{list-style:none;padding:0}li{margin:.5em 0;padding:.5em;background:#f8fafc;border-radius:6px}a{color:#2563eb;text-decoration:none}a:hover{text-decoration:underline}</style>'
html+="</head><body><h1>$DESCRIPTION</h1><ul>" echo "</head><body><h1>$DESCRIPTION</h1><ul>"
for f in "${FILES[@]}"; do
html+="<li><a href=\"$f\">$(humanize "$f")</a></li>" echo "$ENTRIES" | while IFS= read -r entry; do
[ -z "$entry" ] && continue
entry_type=$(echo "$entry" | cut -d: -f1)
entry_name=$(echo "$entry" | cut -d: -f2-)
if [ "$entry_type" = "file" ]; then
echo "<li><a href=\"$entry_name\">$(humanize "$entry_name")</a></li>"
else
cap=$(echo "$entry_name" | sed 's/\(.\).*/\1/' | tr '[:lower:]' '[:upper:]')$(echo "$entry_name" | sed 's/.//')
echo "<li><a href=\"$entry_name/index.html\">${cap}</a></li>"
fi
done done
for d in "${SUBDIRS[@]}"; do
html+="<li><a href=\"$d/index.html\">${d^}</a></li>" echo '</ul></body></html>'
done } > "$REPORT_DIR/index.html"
html+='</ul></body></html>'
printf '%s' "$html" > "$REPORT_DIR/index.html"
} }
STAGED="reports/${SHA8}/${SUITE}" mkdir -p "$SUITE"
mkdir -p "$STAGED"
if [ "$TOTAL" -eq 1 ]; then if [ "$TOTAL" -eq 1 ]; then
cp -a "$REPORT_DIR/." "$STAGED/" cp -a "$REPORT_DIR/." "$SUITE/"
bash .ci/scripts/publish-git-pages.sh "$SUITE" sh .ci/scripts/publish-git-pages.sh "$SUITE"
if [ ${#FILES[@]} -eq 1 ]; then first_entry=$(echo "$ENTRIES" | head -1)
ENTRY="${FILES[0]}" first_type=$(echo "$first_entry" | cut -d: -f1)
first_name=$(echo "$first_entry" | cut -d: -f2-)
if [ "$first_type" = "file" ]; then
SINGLE_ENTRY="$first_name"
else else
ENTRY="${SUBDIRS[0]}/index.html" SINGLE_ENTRY="${first_name}/index.html"
fi fi
URL="${GIT_PAGES_URL}/${GITHUB_REPOSITORY}/reports/${SHA8}/${SUITE}/${ENTRY}"
bash .ci/scripts/report-status.sh "$STATUS" "$DESCRIPTION" "$CONTEXT" "" "$URL" URL="${GIT_PAGES_URL}/${GITHUB_REPOSITORY}/${GITHUB_REF_NAME}/${SHA8}/${SUITE}/${SINGLE_ENTRY}"
sh .ci/scripts/report-status.sh "$STATUS" "$DESCRIPTION" "$CONTEXT" "" "$URL"
else else
generate_index generate_index
cp -a "$REPORT_DIR/." "$STAGED/" cp -a "$REPORT_DIR/." "$SUITE/"
bash .ci/scripts/publish-git-pages.sh "$SUITE" sh .ci/scripts/publish-git-pages.sh "$SUITE"
bash .ci/scripts/report-status.sh "$STATUS" "$DESCRIPTION" "$CONTEXT" "$SUITE" sh .ci/scripts/report-status.sh "$STATUS" "$DESCRIPTION" "$CONTEXT" "$SUITE"
fi fi
rm -rf "$STAGED" rm -rf "$SUITE"
+1 -1
View File
@@ -21,7 +21,7 @@ while IFS='=' read -r key value || [ -n "$key" ]; do
done < "$CONF_FILE" done < "$CONF_FILE"
[ -z "${GITEA_TOKEN:-}" ] && echo "ERROR: GITEA_TOKEN secret is not set" >&2 && ERRORS=1 [ -z "${GITEA_TOKEN:-}" ] && echo "ERROR: GITEA_TOKEN secret is not set" >&2 && ERRORS=1
[ -z "${GIT_PAGES_PUBLISH_TOKEN:-}" ] && echo "ERROR: GIT_PAGES_PUBLISH_TOKEN secret is not set" >&2 && ERRORS=1 [ -z "${REPORTS_PUBLISH_TOKEN:-}" ] && echo "ERROR: REPORTS_PUBLISH_TOKEN secret is not set" >&2 && ERRORS=1
if [ "$ERRORS" -ne 0 ]; then if [ "$ERRORS" -ne 0 ]; then
echo "FATAL: CI config validation failed" >&2 echo "FATAL: CI config validation failed" >&2
+32 -10
View File
@@ -17,6 +17,11 @@ POLL_INTERVAL="${DISPATCH_POLL_INTERVAL:-10}"
[ -z "$GITEA_API_URL" ] && echo "ERROR: gitea_api_url argument is required" >&2 && exit 1 [ -z "$GITEA_API_URL" ] && echo "ERROR: gitea_api_url argument is required" >&2 && exit 1
[ -z "$GITEA_TOKEN" ] && echo "ERROR: gitea_token argument is required" >&2 && exit 1 [ -z "$GITEA_TOKEN" ] && echo "ERROR: gitea_token argument is required" >&2 && exit 1
# Generate unique dispatch_id for display_title matching
# Can be overridden via DISPATCH_ID env var (for tests)
DISPATCH_ID="${DISPATCH_ID:-$(xxd -l 4 -p /dev/urandom 2>/dev/null || openssl rand -hex 4 2>/dev/null || od -An -N4 -tx1 /dev/urandom | tr -d ' \n')}"
INPUTS_JSON=$(echo "$INPUTS_JSON" | jq --arg id "$DISPATCH_ID" '. + {dispatch_id: $id}')
DISPATCH_URL="$GITEA_API_URL/api/v1/repos/$TARGET_REPO/actions/workflows/$WORKFLOW_FILE/dispatches" DISPATCH_URL="$GITEA_API_URL/api/v1/repos/$TARGET_REPO/actions/workflows/$WORKFLOW_FILE/dispatches"
DISPATCH_BODY=$(jq -nc --arg ref "$REF" --argjson inputs "$INPUTS_JSON" '{ref: $ref, inputs: $inputs}') DISPATCH_BODY=$(jq -nc --arg ref "$REF" --argjson inputs "$INPUTS_JSON" '{ref: $ref, inputs: $inputs}')
@@ -32,19 +37,30 @@ if [ "$DISPATCH_CODE" != "201" ]; then
exit 1 exit 1
fi fi
RUNS_URL="$GITEA_API_URL/api/v1/repos/$TARGET_REPO/actions/runs?status=running" # Poll: find dispatched run by display_title matching
RUNS_RESP=$(curl -s --connect-timeout 5 --max-time 10 \ RUN_ID=""
-H "Authorization: token $GITEA_TOKEN" "$RUNS_URL")
RUN_ID=$(echo "$RUNS_RESP" | jq -r '.workflow_runs[0].id // empty')
if [ -z "$RUN_ID" ] || [ "$RUN_ID" = "null" ]; then
echo "ERROR: Could not find dispatched workflow run" >&2
exit 1
fi
TIMEOUT_SECONDS=$(awk "BEGIN {printf \"%.3f\", $TIMEOUT_MINUTES * 60}") TIMEOUT_SECONDS=$(awk "BEGIN {printf \"%.3f\", $TIMEOUT_MINUTES * 60}")
START_TIME=$(date +%s) START_TIME=$(date +%s)
while [ -z "$RUN_ID" ]; do
NOW=$(date +%s)
ELAPSED=$((NOW - START_TIME))
if awk -v e="$ELAPSED" -v t="$TIMEOUT_SECONDS" 'BEGIN { exit !(e >= t) }'; then
echo "ERROR: Timeout after ${TIMEOUT_MINUTES} minutes — run not found" >&2
exit 124
fi
RUNS_RESP=$(curl -s --connect-timeout 5 --max-time 10 \
"$GITEA_API_URL/api/v1/repos/$TARGET_REPO/actions/runs?event=workflow_dispatch&limit=10" \
-H "Authorization: token $GITEA_TOKEN")
RUN_ID=$(echo "$RUNS_RESP" | jq -r --arg id "$DISPATCH_ID" \
'[.workflow_runs[] | select(.display_title | contains($id))] | .[0].id // empty')
[ -z "$RUN_ID" ] && sleep "$POLL_INTERVAL"
done
# Poll: wait for run to complete
while true; do while true; do
NOW=$(date +%s) NOW=$(date +%s)
ELAPSED=$((NOW - START_TIME)) ELAPSED=$((NOW - START_TIME))
@@ -61,6 +77,12 @@ while true; do
if [ "$STATUS" = "completed" ]; then if [ "$STATUS" = "completed" ]; then
CONCLUSION=$(echo "$RUN_RESP" | jq -r '.conclusion // "failure"') CONCLUSION=$(echo "$RUN_RESP" | jq -r '.conclusion // "failure"')
if [ "$CONCLUSION" = "success" ]; then if [ "$CONCLUSION" = "success" ]; then
GITOPS_COMMIT=""
BRANCH_RESP=$(curl -s --connect-timeout 5 --max-time 10 \
"$GITEA_API_URL/api/v1/repos/$TARGET_REPO/branches/$REF" \
-H "Authorization: token $GITEA_TOKEN") || true
GITOPS_COMMIT=$(echo "$BRANCH_RESP" | jq -r '.commit.id // empty')
echo "GITOPS_COMMIT=$GITOPS_COMMIT"
exit 0 exit 0
fi fi
echo "ERROR: Workflow completed with conclusion: $CONCLUSION" >&2 echo "ERROR: Workflow completed with conclusion: $CONCLUSION" >&2
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
echo "gitops-dispatch: validating env vars..."
: "${GITOPS_FILE:?}"
: "${GITOPS_YQ_TPL:?}"
: "${GITOPS_VERSION:?}"
: "${GITOPS_SOURCE_REPO:?}"
: "${GITOPS_SOURCE_COMMIT:?}"
: "${GITOPS_REPO:?}"
: "${GITOPS_WORKFLOW:?}"
: "${GITEA_API_URL:?}"
: "${GITEA_TOKEN:?}"
TIMEOUT="${GITOPS_DISPATCH_TIMEOUT:-30}"
echo "gitops-dispatch: constructing inputs..."
INPUTS=$(jq -nc \
--arg file "$GITOPS_FILE" \
--arg yq_tpl "$GITOPS_YQ_TPL" \
--arg version "$GITOPS_VERSION" \
--arg source_repo "$GITOPS_SOURCE_REPO" \
--arg source_commit "$GITOPS_SOURCE_COMMIT" \
--arg git_tag_prefix "${GITOPS_TAG_PREFIX:-}" \
--arg extra_cmd "${GITOPS_EXTRA_CMD:-}" \
--arg author_name "${GIT_USER_NAME:-}" \
--arg author_email "${GIT_USER_EMAIL:-}" \
'{file: $file, yq_tpl: $yq_tpl, version: $version, source_repo: $source_repo, source_commit: $source_commit, git_tag_prefix: $git_tag_prefix, extra_cmd: $extra_cmd, author_name: $author_name, author_email: $author_email}')
DIR="$(cd "$(dirname "$0")" && pwd)"
echo "gitops-dispatch: dispatching to $GITOPS_REPO/$GITOPS_WORKFLOW..."
set +e
OUTPUT=$(bash "$DIR/dispatch-workflow.sh" \
"$GITOPS_REPO" "$GITOPS_WORKFLOW" "main" \
"$INPUTS" "$GITEA_API_URL" "$GITEA_TOKEN" "$TIMEOUT" 2>&1)
EXIT=$?
set -e
echo "=== DISPATCH OUTPUT (exit=$EXIT) ==="
echo "$OUTPUT"
echo "=== END DISPATCH ==="
STATUS="failure"
GITOPS_SHA=""
if [ "$EXIT" = "0" ]; then
STATUS="success"
GITOPS_SHA=$(echo "$OUTPUT" | grep '^GITOPS_COMMIT=' | cut -d= -f2)
fi
COMPONENT="${GITOPS_TAG_PREFIX:-${GITOPS_FILE}}"
echo "GITOPS_SUMMARY=${COMPONENT}|${GITOPS_VERSION}|${STATUS}|${GITOPS_SHA}|${GITOPS_REPO}"
exit "$EXIT"
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
_gitops_fail() {
local MSG="${1:-GitOps update failed}"
echo "[ERROR] ${MSG}" >&2
if [ -n "${GITOPS_REPO:-}" ] && [ -n "${GITOPS_SHA:-}" ] && \
[ -n "${SOURCE_REPO:-}" ] && [ -n "${SOURCE_COMMIT:-}" ] && \
[ -n "${GITEA_API_URL:-}" ] && [ -n "${GITEA_TOKEN:-}" ]; then
local env repo context
env=$(dirname "${INPUT_FILE}")
repo=$(basename "${SOURCE_REPO}")
context="${repo} ${GITHUB_RUN_ID:-unknown}"
[ -n "${GIT_TAG_PREFIX:-}" ] && context="${repo}/${GIT_TAG_PREFIX} ${GITHUB_RUN_ID:-unknown}"
local SOURCE_URL="${GITEA_API_URL}/${SOURCE_REPO}/commit/${SOURCE_COMMIT}"
ROOT_REPO="${GITOPS_REPO}" ROOT_COMMIT="${GITOPS_SHA}" \
GITEA_API_URL="${GITEA_API_URL}" GITEA_TOKEN="${GITEA_TOKEN}" \
bash "${SCRIPT_DIR}/report-status.sh" failure "Install to ${env} ${VERSION}" \
"${context}" "" "${SOURCE_URL}" 2>/dev/null || true
fi
exit 1
}
_gitops_validate() {
[ -n "${INPUT_FILE:-}" ] || _gitops_fail "INPUT_FILE is required"
[ -n "${YQ_TPL:-}" ] || _gitops_fail "YQ_TPL is required"
[ -n "${VERSION:-}" ] || _gitops_fail "VERSION is required"
[ -n "${SOURCE_REPO:-}" ] || _gitops_fail "SOURCE_REPO is required"
[ -n "${SOURCE_COMMIT:-}" ] || _gitops_fail "SOURCE_COMMIT is required"
[ -n "${GITOPS_REPO:-}" ] || _gitops_fail "GITOPS_REPO is required"
[ -n "${GITEA_TOKEN:-}" ] || _gitops_fail "GITEA_TOKEN is required"
[ -n "${GITEA_API_URL:-}" ] || _gitops_fail "GITEA_API_URL is required"
}
_gitops_success() {
local env repo context
env=$(dirname "${INPUT_FILE}")
repo=$(basename "${SOURCE_REPO}")
context="${repo} ${GITHUB_RUN_ID:-unknown}"
[ -n "${GIT_TAG_PREFIX:-}" ] && context="${repo}/${GIT_TAG_PREFIX} ${GITHUB_RUN_ID:-unknown}"
local SOURCE_URL="${GITEA_API_URL}/${SOURCE_REPO}/commit/${SOURCE_COMMIT}"
ROOT_REPO="${GITOPS_REPO}" ROOT_COMMIT="${GITOPS_SHA}" \
GITEA_API_URL="${GITEA_API_URL}" GITEA_TOKEN="${GITEA_TOKEN}" \
bash "${SCRIPT_DIR}/report-status.sh" success \
"Install to ${env} ${VERSION}" \
"${context}" "" "${SOURCE_URL}"
}
_gitops_nochange() {
local env repo context
env=$(dirname "${INPUT_FILE}")
repo=$(basename "${SOURCE_REPO}")
context="${repo} ${GITHUB_RUN_ID:-unknown}"
[ -n "${GIT_TAG_PREFIX:-}" ] && context="${repo}/${GIT_TAG_PREFIX} ${GITHUB_RUN_ID:-unknown}"
local SOURCE_URL="${GITEA_API_URL}/${SOURCE_REPO}/commit/${SOURCE_COMMIT}"
ROOT_REPO="${GITOPS_REPO}" ROOT_COMMIT="${GITOPS_SHA}" \
GITEA_API_URL="${GITEA_API_URL}" GITEA_TOKEN="${GITEA_TOKEN}" \
bash "${SCRIPT_DIR}/report-status.sh" success \
"Install to ${env} ${VERSION} — no change" \
"${context}" "" "${SOURCE_URL}"
}
_gitops_substitute() {
echo "$1" | sed "s/{{VERSION}}/$2/g"
}
_gitops_update() {
local CLONE_DIR="${GITOPS_TARGET_DIR:-$(mktemp -d)}"
if [ -n "${GITOPS_CLONE_URL:-}" ]; then
git clone "${GITOPS_CLONE_URL}" "${CLONE_DIR}" || _gitops_fail "Failed to clone GitOps repo"
else
git clone "${CLONE_URL}" "${CLONE_DIR}" || _gitops_fail "Failed to clone GitOps repo"
fi
cd "${CLONE_DIR}" || _gitops_fail "Failed to enter clone directory"
yq eval -i "${YQ_EXPR}" "${INPUT_FILE}" || _gitops_fail "Failed to update ${INPUT_FILE}"
if [ -n "${GITOPS_EXTRA_CMD:-}" ]; then
eval "${GITOPS_EXTRA_CMD}" || _gitops_fail "Extra command failed: ${GITOPS_EXTRA_CMD}"
git add -A || _gitops_fail "Failed to stage all changes"
else
git add "${INPUT_FILE}" || _gitops_fail "Failed to stage ${INPUT_FILE}"
fi
if git diff --cached --quiet; then
echo "No changes — ${INPUT_FILE} already at ${VERSION}"
GITOPS_SHA="$(git rev-parse HEAD)"
_gitops_nochange
exit 0
fi
git -c user.name="${GIT_USER_NAME:-gitea-ci-bot}" \
-c user.email="${GIT_USER_EMAIL:-ci@keskikuja.site}" \
commit -m "[skip ci] gitops: update version to ${VERSION}" || _gitops_fail "Failed to commit"
GITOPS_SHA="$(git rev-parse HEAD)"
git push || _gitops_fail "Failed to push"
_gitops_success
}
_gitops_validate
YQ_EXPR=$(_gitops_substitute "${YQ_TPL}" "${VERSION}")
GITEA_HOST=$(echo "${GITEA_API_URL}" | sed 's|https://||' | sed 's|http://||')
CLONE_URL="${GITOPS_CLONE_URL:-https://${GITEA_TOKEN}@${GITEA_HOST}/${GITOPS_REPO}.git}"
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
_gitops_update
fi
+69 -51
View File
@@ -1,49 +1,62 @@
#!/usr/bin/env bash #!/usr/bin/env sh
set -euo pipefail set -eu
SUITE_PATH="${1:-}" SUITE_PATH="${1:-}"
[ -n "$SUITE_PATH" ] || { echo "ERROR: suite_path argument required" >&2; exit 1; } [ -n "$SUITE_PATH" ] || { echo "ERROR: suite_path argument required" >&2; exit 1; }
[ -n "${GITEA_API_URL:-}" ] || { echo "ERROR: GITEA_API_URL is not set" >&2; exit 1; }
[ -n "${GIT_PAGES_URL:-}" ] || { echo "ERROR: GIT_PAGES_URL is not set" >&2; exit 1; } [ -n "${GIT_PAGES_URL:-}" ] || { echo "ERROR: GIT_PAGES_URL is not set" >&2; exit 1; }
[ -n "${GIT_PAGES_PUBLISH_TOKEN:-}" ] || { echo "ERROR: GIT_PAGES_PUBLISH_TOKEN is not set" >&2; exit 1; } [ -n "${REPORTS_PUBLISH_TOKEN:-}" ] || { echo "ERROR: REPORTS_PUBLISH_TOKEN is not set" >&2; exit 1; }
[ -n "${GITHUB_REPOSITORY:-}" ] || { echo "ERROR: GITHUB_REPOSITORY is not set" >&2; exit 1; } [ -n "${GITHUB_REPOSITORY:-}" ] || { echo "ERROR: GITHUB_REPOSITORY is not set" >&2; exit 1; }
[ -n "${GITHUB_SHA:-}" ] || { echo "ERROR: GITHUB_SHA is not set" >&2; exit 1; } [ -n "${GITHUB_SHA:-}" ] || { echo "ERROR: GITHUB_SHA is not set" >&2; exit 1; }
[ -n "${GITHUB_REF_NAME:-}" ] || { echo "ERROR: GITHUB_REF_NAME is not set" >&2; exit 1; }
OWNER="${GITHUB_REPOSITORY%%/*}" OWNER="${GITHUB_REPOSITORY%%/*}"
REPO="${GITHUB_REPOSITORY##*/}" REPO="${GITHUB_REPOSITORY##*/}"
SHA8="${GITHUB_SHA:0:8}" SHA8=$(echo "$GITHUB_SHA" | cut -c1-8)
BRANCH="${GITHUB_REF_NAME}"
SUITE="${SUITE_PATH%/}"
PAGES_USER="${GIT_PAGES_PUBLISH_USER:-publish}" PAGES_USER="${GIT_PAGES_PUBLISH_USER:-publish}"
REPORT_DIR="reports/${SHA8}/${SUITE_PATH%/}"
REPORT_BASE="${GIT_PAGES_URL}/${OWNER}/${REPO}/reports/${SHA8}"
[ -d "$REPORT_DIR" ] || { echo "ERROR: not a directory: $REPORT_DIR" >&2; exit 1; } [ -d "$SUITE" ] || { echo "ERROR: not a directory: $SUITE" >&2; exit 1; }
PUBLISH_SITE_URL="${GIT_PAGES_URL}/" EMPTY=1
for _f in "$SUITE"/* "$SUITE"/.*; do
[ -e "$_f" ] && { EMPTY=0; break; }
done
[ "$EMPTY" -eq 0 ] || { echo "ERROR: no files to publish in $SUITE" >&2; exit 1; }
WORK=$(mktemp -d) WORK=$(mktemp -d)
TAR=$(mktemp) TAR=$(mktemp)
trap 'rm -rf "$WORK" "$TAR"' EXIT trap 'rm -rf "$WORK" "$TAR"' EXIT
RELPATH="${REPORT_DIR#reports/${SHA8}/}" # Kopioi raporttitiedostot
if [ "$RELPATH" != "$REPORT_DIR" ] && [ -n "$RELPATH" ]; then mkdir -p "$WORK/$SUITE"
TARGET="$WORK/${OWNER}/${REPO}/reports/${SHA8}/${RELPATH}" cp -a "$SUITE/." "$WORK/$SUITE/"
else
TARGET="$WORK/${OWNER}/${REPO}/reports/${SHA8}"
fi
mkdir -p "$TARGET"
cp -a "$REPORT_DIR/." "$TARGET/"
if [ ! -f "$TARGET/index.html" ]; then
items=()
while IFS= read -r -d '' f; do
items+=("$(basename "$f")")
done < <(find "$TARGET" -maxdepth 1 -type f ! -name index.html -print0 2>/dev/null || true)
while IFS= read -r -d '' d; do
name=$(basename "$d")
[ -f "$d/index.html" ] && items+=("$name")
done < <(find "$TARGET" -maxdepth 1 -type d ! -name . -print0 2>/dev/null || true)
if [ ${#items[@]} -gt 1 ]; then # Generoi index.html (sama logiikka kuin nykyään)
cd "$WORK/$SUITE"
if [ ! -f "index.html" ]; then
ITEM_LIST=""
ITEM_COUNT=0
for f in *; do
[ -f "$f" ] || continue
[ "$f" = "index.html" ] && continue
ITEM_LIST="${ITEM_LIST}file:${f}
"
ITEM_COUNT=$((ITEM_COUNT + 1))
done
for d in */; do
[ -d "$d" ] || continue
d="${d%/}"
[ -f "$d/index.html" ] || continue
ITEM_LIST="${ITEM_LIST}dir:${d}
"
ITEM_COUNT=$((ITEM_COUNT + 1))
done
if [ "$ITEM_COUNT" -gt 1 ]; then
{ {
echo '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">' echo '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">'
echo "<title>Test report ${SHA8}</title>" echo "<title>Test report ${SHA8}</title>"
@@ -53,47 +66,52 @@ if [ ! -f "$TARGET/index.html" ]; then
echo 'a{color:#2563eb;text-decoration:none}a:hover{text-decoration:underline}' echo 'a{color:#2563eb;text-decoration:none}a:hover{text-decoration:underline}'
echo '</style></head><body>' echo '</style></head><body>'
echo "<h1>Test report <code>${SHA8}</code></h1><ul>" echo "<h1>Test report <code>${SHA8}</code></h1><ul>"
for item in "${items[@]}"; do
label="${item%.*}" echo "$ITEM_LIST" | while IFS= read -r item; do
label="${label//-/ }" [ -z "$item" ] && continue
label="${label//_/ }" item_type=$(echo "$item" | cut -d: -f1)
if [ -f "$TARGET/$item" ]; then item_name=$(echo "$item" | cut -d: -f2-)
echo "<li><a href=\"$item\">${label^}</a></li>" label=$(echo "$item_name" | sed -e 's/\.[^.]*$//' -e 's/[-_]/ /g')
first=$(echo "$label" | cut -c1 | tr '[:lower:]' '[:upper:]')
rest=$(echo "$label" | cut -c2-)
if [ "$item_type" = "file" ]; then
echo "<li><a href=\"$item_name\">${first}${rest}</a></li>"
else else
echo "<li><a href=\"$item/index.html\">${label^}</a></li>" echo "<li><a href=\"$item_name/index.html\">${first}${rest}</a></li>"
fi fi
done done
echo '</ul></body></html>' echo '</ul></body></html>'
} > "$TARGET/index.html" } > "index.html"
fi fi
fi fi
cat > "$TARGET/.meta" <<EOF # .meta tiedosto retentionia varten
{"branch":"${GITHUB_REF_NAME:-}","sha":"${GITHUB_SHA}","published_at":"$(date -u +%Y-%m-%dT%H:%M:%SZ)"} cat > ".meta" <<EOF
{"branch":"${BRANCH}","sha":"${GITHUB_SHA}","published_at":"$(date -u +%Y-%m-%dT%H:%M:%SZ)"}
EOF EOF
find "$WORK/$OWNER" \( -type f -o -type l \) -print | sed "s|^${WORK}/||" | tar -cf "$TAR" -C "$WORK" -T -
publish() { cd "$WORK"
local method="$1"
curl -sS -X "$method" "$PUBLISH_SITE_URL" \ # Pakkaa: pelkät tiedostot ilman $SUITE-etuliitettä
-u "${PAGES_USER}:${GIT_PAGES_PUBLISH_TOKEN}" \ tar czf "$TAR" -C "$WORK/$SUITE" .
-H "Content-Type: application/x-tar" \
-H "Atomic: no" \ # PUT upload-sidecariin
-H "Create-Parents: yes" \ PUBLISH_URL="${GIT_PAGES_URL}/${OWNER}/${REPO}/${BRANCH}/${SHA8}/${SUITE}/"
HTTP_CODE=$(curl -sS -X PUT "$PUBLISH_URL" \
-u "${PAGES_USER}:${REPORTS_PUBLISH_TOKEN}" \
-H "Content-Type: application/tar+gz" \
--data-binary @"$TAR" \ --data-binary @"$TAR" \
-o /tmp/git-pages-publish-response.txt \ -o /tmp/git-pages-publish-response.txt \
-w "%{http_code}" -w "%{http_code}")
}
HTTP_CODE=$(publish PATCH)
case "$HTTP_CODE" in case "$HTTP_CODE" in
200|201|204) ;; 200|201|204) ;;
*) *)
echo "ERROR: git-pages publish HTTP ${HTTP_CODE}" >&2 echo "ERROR: publish HTTP ${HTTP_CODE}" >&2
cat /tmp/git-pages-publish-response.txt >&2 cat /tmp/git-pages-publish-response.txt >&2
exit 1 exit 1
;; ;;
esac esac
echo "$REPORT_BASE" echo "${PUBLISH_URL}"
+1 -1
View File
@@ -7,7 +7,7 @@ SUITE_PATH="${1:-}"
[ -n "${GITEA_API_URL:-}" ] || { echo "ERROR: GITEA_API_URL is not set" >&2; exit 1; } [ -n "${GITEA_API_URL:-}" ] || { echo "ERROR: GITEA_API_URL is not set" >&2; exit 1; }
[ -n "${GITEA_TOKEN:-}" ] || { echo "ERROR: GITEA_TOKEN is not set" >&2; exit 1; } [ -n "${GITEA_TOKEN:-}" ] || { echo "ERROR: GITEA_TOKEN is not set" >&2; exit 1; }
[ -n "${GIT_PAGES_URL:-}" ] || { echo "ERROR: GIT_PAGES_URL is not set" >&2; exit 1; } [ -n "${GIT_PAGES_URL:-}" ] || { echo "ERROR: GIT_PAGES_URL is not set" >&2; exit 1; }
[ -n "${GIT_PAGES_PUBLISH_TOKEN:-}" ] || { echo "ERROR: GIT_PAGES_PUBLISH_TOKEN is not set" >&2; exit 1; } [ -n "${REPORTS_PUBLISH_TOKEN:-}" ] || { echo "ERROR: REPORTS_PUBLISH_TOKEN is not set" >&2; exit 1; }
SCRIPT_DIR="$(dirname "$0")" SCRIPT_DIR="$(dirname "$0")"
+6 -6
View File
@@ -1,11 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env sh
set -euo pipefail set -eu
# https://docs.gitea.com/api/next/#tag/repository/operation/repoCreateStatus
STATE="${1:-}" STATE="${1:-}"
DESCRIPTION="${2:-}" DESCRIPTION="${2:-}"
KEY="${3:-commit-${GITHUB_SHA:0:8}}" SHA8=$(echo "${GITHUB_SHA:-}" | cut -c1-8)
KEY="${3:-commit-${SHA8}}"
SUITE="${4:-}" SUITE="${4:-}"
CUSTOM_URL="${5:-}" CUSTOM_URL="${5:-}"
@@ -18,7 +17,8 @@ if [ -n "$CUSTOM_URL" ]; then
URL="$CUSTOM_URL" URL="$CUSTOM_URL"
elif [ -n "$SUITE" ]; then elif [ -n "$SUITE" ]; then
SUITE="${SUITE%/}/" SUITE="${SUITE%/}/"
URL="${GIT_PAGES_URL}/${GITHUB_REPOSITORY}/reports/${GITHUB_SHA:0:8}/${SUITE}" SHA8_CUT=$(echo "$GITHUB_SHA" | cut -c1-8)
URL="${GIT_PAGES_URL}/${GITHUB_REPOSITORY}/${GITHUB_REF_NAME}/${SHA8_CUT}/${SUITE}"
else else
URL="${GITEA_API_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" URL="${GITEA_API_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
fi fi
+84 -6
View File
@@ -32,6 +32,31 @@ Kun kontti on pushattu registryyn, se on muiden pipeline-jobien käytettävissä
`latest`-tägillä — rebuild = käyttöönotto. Mitään versioviittauksia ei tarvitse `latest`-tägillä — rebuild = käyttöönotto. Mitään versioviittauksia ei tarvitse
päivittää. päivittää.
## Offline-periaate (DoD)
CI-kontin **Definition of Done**:
> Kontti ei lataa mitään pipeline-vaiheessa (`workflow run` -stepit) eikä kontin
> runtime-prosessissa (`container:` / `docker run`). Kaikki riippuvuudet
> (kielikohtaiset paketit, työkalut, binäärit) on joko:
> - Pre-cachattu kontin **build-vaiheessa** Dockerfilessä, TAI
> - Kopioitu multi-stage buildilla toisesta imagesta (`COPY --from`)
>
> Ainoa sallittu lataushetki on `docker build`. Sen jälkeen kontti toimii
> ilman verkkoyhteyttä.
**Miksi:** Toistettavuus, air gap -yhteensopivuus, nopeus. Pipeline ei saa
epäonnistua sen takia että ulkoinen registry on alhaalla tai että `go mod download`
joutuu latamaan 100 modulia jokaisella testiajolla.
**Kielikohtaiset pre-cachet:** Jos kontissa ajetaan kielikohtaista testiä
(Go, Java, Node, Python, ...), kaikki kielikohtaiset riippuvuudet on
pre-cachattava Dockerfilessä build-vaiheessa:
- Go: `COPY go.mod go.sum ./``RUN go mod download`
- Java/Maven: `COPY pom.xml ./``RUN mvn dependency:go-offline`
- Node: `COPY package.json package-lock.json ./``RUN npm ci --omit=dev`
- Python: `COPY requirements.txt ./``RUN pip wheel --wheel-dir=/wheels -r requirements.txt``COPY --from` käyttöön
## Nimeäminen ## Nimeäminen
CI-kontin build-workflow noudattaa samaa nimeämiskonventiota kuin muutkin CI-kontin build-workflow noudattaa samaa nimeämiskonventiota kuin muutkin
@@ -123,27 +148,80 @@ tag: latest
### Dockerfile ### Dockerfile
Dockerfile yhdistää tarvitut työkalut yhteen konttiin. Molemmat tavat kelpaavat: Dockerfile yhdistää tarvitut työkalut yhteen konttiin.
**Kaikki riippuvuudet ladataan build-vaiheessa — kontti on täysin itseriittoinen.**
```dockerfile ```dockerfile
# Tapa A: COPY --from toisesta imagesta # Tapa A: COPY --from toisesta imagesta
FROM __BASE_IMAGE__:__VERSION__ FROM __BASE_IMAGE__:__VERSION__
COPY --from=__SOURCE_IMAGE__:__VERSION__ /path/to/binary /usr/local/bin/ COPY --from=__SOURCE_IMAGE__:__VERSION__ /path/to/binary /usr/local/bin/
RUN apk add --no-cache __PAKETIT__ # Ei koskaan git:iä — kloonaus kuuluu pipelinelle RUN apk add --no-cache __PAKETIT__
# Tapa B: curl-lataus (normaali Dockerfilessa) # Tapa B: Build-vaiheen curl-lataus
FROM __BASE_IMAGE__:__VERSION__ FROM __BASE_IMAGE__:__VERSION__
RUN apk add --no-cache curl __PAKETIT__ && \ RUN apk add --no-cache curl __PAKETIT__ && \
curl -fsSL __URL__/__BINARY__.tar.gz | tar xz -C /usr/local/bin && \ curl -fsSL __URL__/__BINARY__.tar.gz | tar xz -C /usr/local/bin && \
apk del curl apk del curl
# Tapa C: Multi-stage + kielikohtainen pre-cache
FROM __BASE_IMAGE__:__VERSION__ AS deps
COPY go.mod go.sum ./
RUN go mod download
FROM deps AS build
COPY . .
RUN go test -c -o /tmp/test.bin ./...
FROM __BASE_IMAGE__:__VERSION__
COPY --from=deps /go/pkg/mod /go/pkg/mod
COPY --from=build /tmp/test.bin /usr/local/bin/test
``` ```
`COPY --from` on kevyempi (ei curl-asennusta). `curl` on selkeämpi kun binääri `COPY --from` on kevyempi (ei curl-asennusta). `curl` (Tapa B) on sallittu
tulee suoraan GitHub Releasesista tai vastaavasta. vain build-vaiheessa — `apk del curl` poistaa työkalun ennen runtimea.
Tapa C pre-cacheaa kielikohtaiset riippuvuudet ja tuottaa täysin
offline-runtime-kontin.
## Testaus ennen julkaisua
Konttia ei saa pushata registryyn ennen kuin se on validoitu.
### 1. Aja testit kontin sisällä
Testit on ajettava **kontin sisällä**, ei suoraan lokaalilla koneella.
```bash
# OIKEIN — kontin sisällä
docker build -t ci-tyokalu:test .
docker run --rm -v "$(pwd):/repo" -w /repo ci-tyokalu:test bash -c 'bats tests/'
# VÄÄRIN — lokaalit binäärit vs kontti
bats tests/ # eri bash/työkalut kuin kontissa
bashcov -- bats tests/ # eri ruby-versio kuin kontissa
```
Lokaali ympäristö (macOS, eri kirjastoversiot) poikkeaa aina kontista.
Testi voi mennä läpi lokaalissa mutta failata CI:ssä, tai päinvastoin.
### 2. Fragile-testien seulonta (10x ajo)
Aja koko testipaketti **10 kertaa peräkkäin** kontin sisällä ennen pushausta:
```bash
for i in $(seq 1 10); do
echo "=== RUN $i ==="
docker run --rm -v "$(pwd):/repo" -w /repo ci-tyokalu:test \
bash -c 'bats tests/' || exit 1
done
```
Jos yksikin ajo failaa, kontissa on fragile testi — korjaa ennen pushausta.
Fragile testit syövät devaukseen käytettyä aikaa turhilla uusinta-ajoilla.
## Mitä EI kannata tehdä ## Mitä EI kannata tehdä
- Älä lisää `workflow_call`-triggariä — CI-konttia ei koskaan buildata automaattisesti - Älä lisää `workflow_call`-triggariä — CI-konttia ei koskaan buildata automaattisesti
- Älä poista `<komponentti>.`-prefiksiä olemassaolevista tiedostoista — ne kuuluvat monorepo-nimeämiskonventioon - Älä poista `<komponentti>.`-prefiksiä olemassaolevista tiedostoista — ne kuuluvat monorepo-nimeämiskonventioon
- Älä sisällytä CI-konttiin mitään sovelluskoodia — vain työkalut - Älä sisällytä CI-konttiin mitään sovelluskoodia — vain työkalut
- Älä koskaan asenna `git`:iä CI-konttiin — repon kloonaus ja checkout ovat Gitea Actionsin natiiveja operaatioita, eivät kontin vastuulla. Git paisuttaa konttia turhaan ja luo harhan että kontti hallitsee repoa - Älä koskaan lataa mitään pipeline- tai runtime-vaiheessa — kaikki lataukset kuuluvat `docker build` -vaiheeseen (Offline-periaate)
- Älä jätä kielikohtaisia riippuvuuksia pre-cachaamatta — `go mod download`, `npm install`, `mvn dependency:go-offline` jne. ajetaan Dockerfilessä, ei pipelinessä
+163 -7
View File
@@ -2,6 +2,93 @@
Mallipohjat, esimerkit ja konfiguraatiot. Katso säännöt `SKILL.md`:stä. Mallipohjat, esimerkit ja konfiguraatiot. Katso säännöt `SKILL.md`:stä.
## Pre-cache-esimerkit (Offline Container)
Alla Dockerfile-esimerkit kielikohtaisista pre-cacheista. Kaikki ajetaan
build-vaiheessa — kontti on täysin itseriittoinen eikä lataa mitään
pipeline- tai runtime-vaiheessa.
### Go
```dockerfile
FROM golang:1.24-alpine AS deps
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
FROM deps AS test-build
COPY . .
RUN go test -c -o /tmp/test.bin ./...
FROM alpine:3.21
RUN apk add --no-cache git nodejs
COPY --from=deps /go/pkg/mod /go/pkg/mod
COPY --from=test-build /tmp/test.bin /usr/local/bin/test
```
### Node.js
```dockerfile
FROM node:22-alpine AS deps
WORKDIR /build
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
FROM node:22-alpine
RUN apk add --no-cache git
COPY --from=deps /build/node_modules /app/node_modules
COPY . /app
WORKDIR /app
```
### Java / Maven
```dockerfile
FROM maven:3.9-eclipse-temurin-21 AS deps
WORKDIR /build
COPY pom.xml ./
RUN mvn dependency:go-offline -B
FROM maven:3.9-eclipse-temurin-21 AS build
COPY --from=deps /root/.m2 /root/.m2
COPY . .
RUN mvn package -B -DskipTests
FROM eclipse-temurin:21-jre
RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
COPY --from=build /build/target/*.jar /app/app.jar
WORKDIR /app
```
### Python
```dockerfile
FROM python:3.12-alpine AS deps
WORKDIR /build
COPY requirements.txt ./
RUN pip wheel --wheel-dir=/wheels -r requirements.txt
FROM python:3.12-alpine
RUN apk add --no-cache git
COPY --from=deps /build/wheels /wheels
COPY --from=deps /build/requirements.txt /
RUN pip install --no-index --find-links=/wheels -r /requirements.txt && rm -rf /wheels
COPY . /app
WORKDIR /app
```
### Helm + Node.js (korvaa helm-build-push.yml:n runtime-apk)
```dockerfile
FROM alpine/helm:3.16.0 AS helm-bin
FROM node:22-alpine
RUN apk add --no-cache git
COPY --from=helm-bin /usr/bin/helm /usr/local/bin/helm
```
Tämä kontti korvaa `helm-build-push.yml`:n `alpine/helm:3.19.0`-image-riippuvuuden
ja poistaa tarpeen asentaa node.js runtime-vaiheessa.
## Reititin — täydellinen esimerkki ## Reititin — täydellinen esimerkki
```yaml ```yaml
@@ -115,7 +202,16 @@ jobs:
- name: Report - name: Report
if: always() if: always()
run: bash .ci/scripts/ci-report.sh "<kuvaus>" <context> <suite> ${{ job.status }} run: |
bash .ci/scripts/ci-report.sh "<Test type> test report" <context> <suite> ${{ job.status }}
```
Monorepossa context ja description sisältävät komponentin nimen:
```yaml
- name: Report
if: always()
run: |
bash .ci/scripts/ci-report.sh "<Komponentti>: <Test type> test report" <komponentti>.<context> <suite> ${{ job.status }}
``` ```
**Usean runnerin cache-ongelma:** Jos eri kerroilla käynnistyy eri runnereita, **Usean runnerin cache-ongelma:** Jos eri kerroilla käynnistyy eri runnereita,
@@ -131,6 +227,7 @@ niillä voi olla eri versio `latest`-imagen digesteistä. Ratkaisuja:
### Taso 1: Ei jälkikäsittelyä ### Taso 1: Ei jälkikäsittelyä
Single repo:
```yaml ```yaml
- name: Run tests - name: Run tests
shell: bash shell: bash
@@ -140,11 +237,27 @@ niillä voi olla eri versio `latest`-imagen digesteistä. Ratkaisuja:
- name: Report - name: Report
if: always() if: always()
run: bash .ci/scripts/ci-report.sh "<kuvaus>" <context> <suite> ${{ job.status }} run: |
bash .ci/scripts/ci-report.sh "<Test type> test report" <context> <suite> ${{ job.status }}
```
Monorepo:
```yaml
- name: Run tests
shell: bash
run: |
mkdir -p "reports/<suite>"
<testikomento>
- name: Report
if: always()
run: |
bash .ci/scripts/ci-report.sh "<Komponentti>: <Test type> test report" <komponentti>.<context> <suite> ${{ job.status }}
``` ```
### Taso 2: Jälkikäsittely tarvitaan ### Taso 2: Jälkikäsittely tarvitaan
Single repo:
```yaml ```yaml
- name: Run tests - name: Run tests
shell: bash shell: bash
@@ -162,7 +275,30 @@ niillä voi olla eri versio `latest`-imagen digesteistä. Ratkaisuja:
- name: Report - name: Report
if: always() if: always()
run: bash .ci/scripts/ci-report.sh "<kuvaus>" <context> <suite> ${{ job.status }} run: |
bash .ci/scripts/ci-report.sh "<Test type> test report" <context> <suite> ${{ job.status }}
```
Monorepo:
```yaml
- name: Run tests
shell: bash
run: |
mkdir -p "reports/<suite>"
<testikomento> > "reports/<suite>/results.txt" 2>&1
- name: Post-process coverage
if: always()
run: <siirrä coverage-data reports/<suite>/coverage/-hakemistoon>
- name: Post-process test report
if: always()
run: <HTML-generointi raa'asta outputista>
- name: Report
if: always()
run: |
bash .ci/scripts/ci-report.sh "<Komponentti>: <Test type> test report" <komponentti>.<context> <suite> ${{ job.status }}
``` ```
### Väärin vs oikein — yksi asia per step ### Väärin vs oikein — yksi asia per step
@@ -186,7 +322,16 @@ niillä voi olla eri versio `latest`-imagen digesteistä. Ratkaisuja:
- name: Report - name: Report
if: always() if: always()
run: bash .ci/scripts/ci-report.sh "Helm kubeconform" helm-test kubeconform ${{ job.status }} run: |
bash .ci/scripts/ci-report.sh "Helm kubeconform" helm-test kubeconform ${{ job.status }}
```
Monorepossa:
```yaml
- name: Report
if: always()
run: |
bash .ci/scripts/ci-report.sh "<Komponentti>: Helm kubeconform" <komponentti>.helm-test kubeconform ${{ job.status }}
``` ```
### Väärin vs oikein — post-process ### Väärin vs oikein — post-process
@@ -328,7 +473,7 @@ VERSION_FILE=git-pages/Chart.yaml
| Secret | Pakollinen | | Secret | Pakollinen |
|---|---| |---|---|
| `GITEA_TOKEN` | Aina (Gitean sisäinen, automaattisesti saatavilla) | | `GITEA_TOKEN` | Aina (Gitean sisäinen, automaattisesti saatavilla) |
| `GIT_PAGES_PUBLISH_TOKEN` | Aina | | `REPORTS_PUBLISH_TOKEN` | Aina |
| `DOCKER_USERNAME` | Vain jos buildaat kontteja | | `DOCKER_USERNAME` | Vain jos buildaat kontteja |
| `DOCKER_PASSWORD` | Vain jos buildaat kontteja | | `DOCKER_PASSWORD` | Vain jos buildaat kontteja |
| `HELM_USER` | Vain jos pushaat Helm chartin OCI-rekisteriin (oletus `github.actor`) | | `HELM_USER` | Vain jos pushaat Helm chartin OCI-rekisteriin (oletus `github.actor`) |
@@ -349,7 +494,7 @@ pitää komponentit selkeästi erillään, ja tekee repossa navigoinnista suorav
| Ongelma | Ratkaisu | | Ongelma | Ratkaisu |
|---|---| |---|---|
| Monta komponenttia, yksi repo — mikä triggeröi? | `paths:`-filtteri: `push: { paths: ['<komponentti>/**'] }` | | Monta komponenttia, yksi repo — mikä triggeröi? | `paths:`-filtteri: komponentin hakemisto + sen CI-workflow't ja conf-tiedosto |
| Jokaisella komponentilla oma versio | `VERSION_FILE=<komponentti>/package.json` confissa | | Jokaisella komponentilla oma versio | `VERSION_FILE=<komponentti>/package.json` confissa |
| Git-tägit sekaisin ellei nimiavaruutta | `GIT_TAG_PREFIX=<komponentti>/` confissa → tägi `<komponentti>/1.2.3` | | Git-tägit sekaisin ellei nimiavaruutta | `GIT_TAG_PREFIX=<komponentti>/` confissa → tägi `<komponentti>/1.2.3` |
| Eri julkaisutahdit | Riippumattomat CI-triggerit, omat versiopolut | | Eri julkaisutahdit | Riippumattomat CI-triggerit, omat versiopolut |
@@ -378,7 +523,8 @@ on:
branches: branches:
- main - main
paths: paths:
- '<komponentti>/**' - <komponentti>/**
- .gitea/workflows/<komponentti>.*
jobs: jobs:
load-config: load-config:
@@ -421,6 +567,15 @@ jobs:
suites: '<suite-1> <suite-2>' suites: '<suite-1> <suite-2>'
``` ```
**Commit status -kontekstit monorepossa:** Testiraporttien `ci-report.sh`-kutsussa
context ja description sisältävät komponentin nimen:
```yaml
- name: Report
if: always()
run: |
bash .ci/scripts/ci-report.sh "<Komponentti>: Unit test report" <komponentti>.unit-tests bats ${{ job.status }}
```
### Version elinkaari per komponentti ### Version elinkaari per komponentti
`GIT_TAG_PREFIX` takaa että eri komponenttien versiohistoria pysyy erillään. `GIT_TAG_PREFIX` takaa että eri komponenttien versiohistoria pysyy erillään.
@@ -435,6 +590,7 @@ jos commitilla on jo tägi, pipeline skipataan `if: artifact_exists != 'true'`.
- Älä aja kaikkia komponentteja samasta triggeristä — `paths:` pitää CI:t erillisinä - Älä aja kaikkia komponentteja samasta triggeristä — `paths:` pitää CI:t erillisinä
- Älä käytä samaa versionhallintatiedostoa usealle komponentille - Älä käytä samaa versionhallintatiedostoa usealle komponentille
- Älä anna monorepo-parametreja pipeline-overrideina — kaikki kuuluu conf-tiedostoon - Älä anna monorepo-parametreja pipeline-overrideina — kaikki kuuluu conf-tiedostoon
- Älä rajaa `paths:` pelkkään komponentin hakemistoon — CI ei triggeröidy workflow- tai conf-muutoksista
## Versionhallinta ## Versionhallinta
+391 -2
View File
@@ -82,7 +82,36 @@ koko stepin ensimmäisellä failaavalla komennolla, ja loput jäävät ajamatta.
CI-kontin build-workflow'n template: `skills/ci-container-build/SKILL.md`. CI-kontin build-workflow'n template: `skills/ci-container-build/SKILL.md`.
### 4.1 CI-kontin ajaminen jobissa ### 4.1 Offline Container -vaatimus (DoD)
CI-kontin (ja kaikkien pipeline-konttien) on oltava täysin itseriittoisia:
> Kontti ei lataa mitään pipeline-vaiheessa (`workflow run` -stepit) eikä
> kontin runtime-prosessissa (`container:` / `docker run`). Kaikki
> riippuvuudet pre-cachataan `docker build` -vaiheessa.
> Ainoa sallittu lataushetki on `docker build`.
**Esimerkkejä rikkomuksista:**
- `apk add`, `apt-get install`, `npm install`, `go mod download`, `pip install`
pipeline-stepissä
- `curl <url> | tar xz` runtime-vaiheessa
- Node.js-konttikuva ilman nodea (joudutaan asentamaan lennossa)
### 4.2 Kielikohtainen pre-cache
Kun kontissa testataan kielikohtaista koodia, kaikki riippuvuudet on
pre-cachattava Dockerfilessä, ei pipeline-stepissä:
| Kieli | Pre-cache Dockerfilessä |
|---|---|
| Go | `COPY go.mod go.sum ./``RUN go mod download` |
| Java/Maven | `COPY pom.xml ./``RUN mvn dependency:go-offline` |
| Node | `COPY package.json package-lock.json ./``RUN npm ci --omit=dev` |
| Python | `COPY requirements.txt ./``RUN pip install -r requirements.txt` |
Katso tarkat Dockerfile-esimerkit `REFERENCE.md`:stä.
### 4.3 CI-kontin ajaminen jobissa
Ainoa sallittu tapa on `container:`-direktiivi. `docker run` komennolla kontin Ainoa sallittu tapa on `container:`-direktiivi. `docker run` komennolla kontin
käynnistäminen stepin sisällä on anti-pattern. käynnistäminen stepin sisällä on anti-pattern.
@@ -91,7 +120,90 @@ Katso CI-kontin template `REFERENCE.md`:stä.
**Huomio `actions/checkout@v4`:stä:** `container:`-direktiivillä kaikki stepit **Huomio `actions/checkout@v4`:stä:** `container:`-direktiivillä kaikki stepit
ajetaan kontin *sisällä* — myös `actions/checkout@v4`. Se on JavaScript-action ajetaan kontin *sisällä* — myös `actions/checkout@v4`. Se on JavaScript-action
joka vaatii sekä `nodejs` että `git`. Varmista että CI-kontin Dockerfilessä on molemmat. joka vaatii sekä `nodejs` että `git`. Varmista että CI-kontin Dockerfilessä on
molemmat — muuten checkout ei toimi ja pipeline failaa.
### 4.4 Build-konteksti, `.dockerignore` ja `COPY`
**Build-konteksti** on aina tiedoston (Dockerfile, Chart.yaml) oman hakemiston
juuri (`dirname "${DOCKERFILE}"` / `dirname "${CHART_FILE}"`). Kaikki
suhteelliset polut — ignore-tiedosto, `COPY`, `ADD` — ovat suhteessa tähän
kontekstiin.
| Tiedosto | Konteksti | Ignore-tiedosto | Käyttö |
|---|---|---|---|
| `Dockerfile` | `.` | `./.dockerignore` | `docker build` / `COPY src/ src/` |
| `api/Dockerfile` | `api/` | `api/.dockerignore` | `docker build` / `COPY src/ src/` |
| `Chart.yaml` (`VERSION_FILE`) | `.` | `./.helmignore` | `helm package` |
| `api/Chart.yaml` (`VERSION_FILE`) | `api/` | `api/.helmignore` | `helm package` |
Helm chartin polku luetaan confin `VERSION_FILE`-kentästä — sama rivi jota
`check-version.yml` käyttää version lähteenä. Yksi conf-rivi ohjaa molempia:
sekä versionlaskentaa että chartin sijaintia.
**Mitä ignore-tiedosto sisältää:** Kaikki mikä EI ole konttiin tai chart-pakettiin
tarkoitettua koodia tai resurssia, ON oltava ignore-tiedostossa:
- Git- ja CI-historia (`.git/`, `.gitea/`, `.github/`)
- Testikoodi, testidata, testiraportit (`tests/`, `reports/`, `coverage/`)
- Dokumentaatio (`docs/`, `guides/`, `*.md`, `CHANGELOG`, `README`)
- Editori- ja työkalukonfiguraatio (`.vscode/`, `.cursor/`, `.idea/`, `.DS_Store`)
- Riippuvuudet jotka asennetaan Dockerfilessä (`node_modules/`)
- Väliaikaistiedostot (`tmp/`, `*.log`)
- Projektikohtaiset konfiguraatiot (`.env`, `*.conf`, `CURRENT_PROVIDER_VERSION`)
**Miksi:** Build-kontekstin koko vaikuttaa suoraan `docker build` -nopeuteen.
Raskas konteksti (etenkin `.git/` ja `node_modules/`) hidastaa buildia ja
kuluttaa runnerin resursseja turhaan. Ylimääräiset tiedostot kontissa ovat
**tietoturvariski** — tokenit, `.env` ja sensitiivinen data voivat päätyä
kontin layeriin jos `.dockerignore` ei ole kattava.
### 4.5 `COPY`-kuri — kopioi vain tarvittava
`COPY . .` on kielletty. Jokainen `COPY` kopioi vain tarvittavat tiedostot
tai hakemistot:
```dockerfile
# VÄÄRIN
COPY . .
# OIKEIN
COPY package.json package-lock.json ./
COPY src/ src/
COPY public/ public/
```
**Miksi:**
- Layer-cache: `COPY . .` rikkoo välimuistin — mikä tahansa muutos
tiedostossa tyhjentää koko layerin
- Tietoturva: konttiin voi päätyä ylimääräisiä tiedostoja vaikka
`.dockerignore` olisi kattava (unohtunut ignore-rivi, uusi työkalu
joka luo tiedostoja build-kontekstiin)
- Luettavuus: `COPY . .` ei kerro mitä kontti todella sisältää
- Kontin koko: eksplisiittinen `COPY` pitää image-koon kurissa
### 4.6 `.helmignore` — pidä chart-paketti siistinä
`helm package` käyttää `.helmignore`-tiedostoa samalla periaatteella kuin
`docker build` käyttää `.dockerignore`a:
- Chart-hakemisto luetaan confin `VERSION_FILE`-kentästä (`dirname "${VERSION_FILE}"`)
- ignore-tiedosto luetaan chart-hakemiston juuresta (sama konteksti kuin
`Chart.yaml`, ks. 4.4)
- Kaikki turha (testit, docs, git, CI-konffit, kuvat) on poissuljettava
- Jos `.helmignore` puuttuu, `helm package` paketoi mukaan kaikki
chart-hakemiston tiedostot — turhaa bulkkia registryyn
**`.helmignore` on pakollinen** jokaiselle chartille. Minimisisältö:
```
.git/
.gitignore
tests/
docs/
*.md
.DS_Store
```
## 5. Raporttitasot ## 5. Raporttitasot
@@ -128,6 +240,35 @@ Tiedostonimet `.gitea/workflows/`-kansiossa noudattavat yhtenäistä rakennetta:
Single repossa `<komponentti>` jätetään pois. Single repossa `<komponentti>` jätetään pois.
Monorepossa prefiksi pitää komponentin tiedostot yhdessä. Monorepossa prefiksi pitää komponentin tiedostot yhdessä.
### 6.1 Commit status -nimeäminen
`ci-report.sh`-kutsun `description` (2. argumentti) ja `context` (3. argumentti)
noudattavat seuraavaa kaavaa:
**Single repo:**
```
context: <testityyppi> (esim. unit-tests, acc-tests)
description: <Test type> test report (esim. Unit test report)
```
**Monorepo:**
```
context: <komponentti>.<testityyppi> (esim. library.unit-tests)
description: <Komponentti>: <Test type> test report (esim. Library: Unit test report)
```
> Gitea YAML: `run:` laita lainausmerkeillä `run: |`-blockiin — Gitea ei tue lainausmerkkejä yhden rivin `run:`-komennoissa.
>
> ```yaml
> - name: Report
> if: always()
> run: |
> bash .ci/scripts/ci-report.sh "<Komponentti>: <Test type> test report" <komponentti>.<context> <suite> ${{ job.status }}
> ```
Build/push-status (Docker, Helm) on providerin hallussa — consumer ei vaikuta
niiden nimeämiseen.
## 7. Artifact-kuri ## 7. Artifact-kuri
Gitea Actionsin `upload-artifact` jättää pysyvän tiedoston. Artifakteja ei käytetä Gitea Actionsin `upload-artifact` jättää pysyvän tiedoston. Artifakteja ei käytetä
@@ -189,3 +330,251 @@ Ei pipeä (`|`) komennon perässä — se syö exit-koodin. Käytä redirectiä
Providerin scriptit haetaan `actions/checkout`-stepillä `.ci/`-polkuun. Providerin scriptit haetaan `actions/checkout`-stepillä `.ci/`-polkuun.
Consumer ei kopioi eikä muokkaa providerin tiedostoja. Consumer ei kopioi eikä muokkaa providerin tiedostoja.
## 10. Build & Push -providerit
### `docker-build-push.yml` — Docker image build & push
Buildaa ja pushee Docker-imagen OCI-registryyn. Ajaa suoraan runnerilla
(ei `container:`-direktiiviä), joten `actions/checkout` toimii natiivisti.
**`env_json`-avaimet (pakolliset):**
```yaml
DOCKER_REGISTRY: gitea.app.keskikuja.site/niko
DOCKER_IMAGE_NAME: my-app
```
**Inputit:**
| Parametri | Pakollinen | Kuvaus |
|-----------|------------|--------|
| `env_json` | Kyllä | Konffi `gitea-env.conf`:stä |
| `version` | Kyllä | Version string (check-version output) |
| `build_args` | Ei | Monirivinen KEY=VALUE-lista välitettynä `--build-arg`-lipuiksi `docker build` -komennolle |
**Käyttö reitittimessä:**
```yaml
docker-build-push:
uses: OWNER/gitea-ci-library/.gitea/workflows/docker-build-push.yml@v1
needs: [check-version]
if: needs.check-version.outputs.artifact_exists == 'false'
secrets: inherit
with:
env_json: ${{ needs.load-config.outputs.env_json }}
version: ${{ needs.check-version.outputs.version }}
build_args: |
VERSION=${{ needs.check-version.outputs.version }}
```
**Dockerfilessä** ARG otetaan vastaan oletusarvolla ja kirjoitetaan tiedostoksi:
```dockerfile
ARG VERSION=unknown
RUN mkdir -p /app && echo "$VERSION" > /app/version.txt
```
Katso referenssi: repon juuren `Dockerfile` (`gitea-ci-library/Dockerfile`).
Tarkka input/secret-lista: `docs/workflows.md`.
### `helm-build-push.yml` — Helm chart build & push
Pakkaa ja pushee Helm-chartin OCI-registryyn. Käyttää `alpine/helm`-konttia.
**`env_json`-avaimet (pakolliset):**
```yaml
HELM_REGISTRY: gitea.app.keskikuja.site/niko
VERSION_FILE: platform-helm/Chart.yaml # versionlähde, chart_path määrää chart-hakemiston
```
**Inputit:**
| Parametri | Pakollinen | Kuvaus |
|-----------|------------|--------|
| `env_json` | Kyllä | Konffi `gitea-env.conf`:stä |
| `version` | Kyllä | Version string (check-version output) |
| `chart_path` | Kyllä | Polku Chart.yaml-hakemistoon (esim. `platform-helm`) |
| `extra_dependency_paths` | Ei | Pilkulla erotellut polut subcharttien dependeinceille |
**Käyttö reitittimessä:**
```yaml
helm-build-push:
uses: OWNER/gitea-ci-library/.gitea/workflows/helm-build-push.yml@v1
needs: [check-version]
if: needs.check-version.outputs.artifact_exists == 'false'
secrets: inherit
with:
env_json: ${{ needs.load-config.outputs.env_json }}
version: ${{ needs.check-version.outputs.version }}
chart_path: platform-helm
# extra_dependency_paths: subchart-a,subchart-b # tarvittaessa
```
`chart_path` on eksplisiittinen polku chart-hakemistoon (esim. `platform-helm`).
`VERSION_FILE` määrää version lähteen (`Chart.yaml:n` `version`-kenttä) —
nämä voivat olla eri polkuja, mutta tyypillisesti molemmat osoittavat samaan
chart-hakemistoon.
**`extra_dependency_paths`:** Jos chartilla on alikarttoja (subchartteja) jotka
vaativat `helm dependency update` -ajon ennen päächartin buildia, anna niiden
polut pilkulla eroteltuna. Provider ajaa `helm dependency update` jokaiselle
polulle ennen päächartin buildia.
**Yksittäisten Helm-UI-linkkien raportointi:** `HELM_UI_URL` on
tarkoitettu yleiselle registry UI:lle — provider muodostaa linkin
`${HELM_UI_URL}/${CHART_NAME}/${VERSION}` automaattisesti.
Tarkka input/secret-lista: `docs/workflows.md`.
## 11. Multi-artifact monorepo -komponentti
Yksi monorepo-komponentti voi tuottaa useita artefakteja (esim. Docker image
+ Helm chart). Kukin artefakti on **omassa reitittimessään** — ei yhtä
monoliittista pipelinea. Tämä on tietoinen arkkitehtuurivalinta:
- Reitittimet ovat itsenäisiä: eri `paths:`-triggerit, eri tagit, eri confit
- Yksi commit voi triggeröidä molemmat rinnakkain
- Yhden artefaktin build tai testi ei estä toista
### Esimerkki: `platform-helm` joka tuottaa Docker-imagen ja Helm chartin
```
.gitea/workflows/
├── platform-helm.ci-main.yml # Docker build & push
├── platform-helm.gitea-env.conf # Docker-konffi
├── platform-helm.helm-ci-main.yml # Helm build & push
├── platform-helm.helm-gitea-env.conf # Helm-konffi
├── platform-helm.helm-chart-lint.yml # Chart-testi
└── platform-helm.ci-container-build-helm.yml # CI-kontin build
```
### `platform-helm.gitea-env.conf` (Docker)
```ini
DOCKER_REGISTRY=gitea.app.keskikuja.site/niko
DOCKER_IMAGE_NAME=platform-helm
GIT_TAG_PREFIX=platform-helm/
```
### `platform-helm.helm-gitea-env.conf` (Helm)
```ini
HELM_REGISTRY=gitea.app.keskikuja.site/niko
VERSION_FILE=platform-helm/Chart.yaml
GIT_TAG_PREFIX=chart/
```
### Reitittimet
**`platform-helm.ci-main.yml`** — Docker-buildi, testit, oma tagi:
```yaml
name: platform-helm CI Main
on:
push:
branches: [main]
paths:
- platform-helm/**
- .gitea/workflows/platform-helm.*
jobs:
load-config:
uses: OWNER/gitea-ci-library/.gitea/workflows/config-provider.yml@v1
secrets: inherit
with:
config_path: .gitea/workflows/platform-helm.gitea-env.conf
check-version:
needs: [load-config]
uses: OWNER/gitea-ci-library/.gitea/workflows/check-version.yml@v1
secrets: inherit
with:
env_json: ${{ needs.load-config.outputs.env_json }}
test:
needs: [load-config, check-version]
uses: ./.gitea/workflows/platform-helm.sbom-lint.yml
secrets: inherit
with:
env_json: ${{ needs.load-config.outputs.env_json }}
build-push:
needs: [load-config, check-version, test]
if: needs.check-version.outputs.artifact_exists == 'false'
uses: OWNER/gitea-ci-library/.gitea/workflows/docker-build-push.yml@v1
secrets: inherit
with:
env_json: ${{ needs.load-config.outputs.env_json }}
version: ${{ needs.check-version.outputs.version }}
report-summary:
needs: [load-config, test, build-push]
if: always()
uses: OWNER/gitea-ci-library/.gitea/workflows/report-summary.yml@v1
with:
env_json: ${{ needs.load-config.outputs.env_json }}
suites: ''
```
**`platform-helm.helm-ci-main.yml`** — Helm-buildi, chart-testi, oma tagi:
```yaml
name: platform-helm Helm CI Main
on:
push:
branches: [main]
paths:
- platform-helm/**
- .gitea/workflows/platform-helm.helm*
jobs:
load-config:
uses: OWNER/gitea-ci-library/.gitea/workflows/config-provider.yml@v1
secrets: inherit
with:
config_path: .gitea/workflows/platform-helm.helm-gitea-env.conf
check-version:
needs: [load-config]
uses: OWNER/gitea-ci-library/.gitea/workflows/check-version.yml@v1
secrets: inherit
with:
env_json: ${{ needs.load-config.outputs.env_json }}
chart-lint:
needs: [load-config, check-version]
uses: ./.gitea/workflows/platform-helm.helm-chart-lint.yml
secrets: inherit
with:
env_json: ${{ needs.load-config.outputs.env_json }}
helm-build-push:
needs: [load-config, check-version, chart-lint]
if: needs.check-version.outputs.artifact_exists == 'false'
uses: OWNER/gitea-ci-library/.gitea/workflows/helm-build-push.yml@v1
secrets: inherit
with:
env_json: ${{ needs.load-config.outputs.env_json }}
version: ${{ needs.check-version.outputs.version }}
report-summary:
needs: [load-config, chart-lint, helm-build-push]
if: always()
uses: OWNER/gitea-ci-library/.gitea/workflows/report-summary.yml@v1
with:
env_json: ${{ needs.load-config.outputs.env_json }}
suites: ''
```
### Säännöt
- Jokaisella artefaktilla on oma reititin, oma conf, omat testit
- Conf-tiedoston nimi erottaa artefaktit: `<komponentti>.gitea-env.conf` vs
`<komponentti>.helm-gitea-env.conf`
- `<komponentti>.helm-`-prefiksi erottaa Helm-artefaktin tiedostot
- `GIT_TAG_PREFIX` pitää tagit erillään: `platform-helm/1.2.3` vs `chart/1.2.3`
- Molemmat reitittimet voivat triggeröityä samasta commitista
+251
View File
@@ -0,0 +1,251 @@
---
name: gitops-update
description: |
Getting GitOps configuration updates working for a consumer project —
GitOps repo setup, consumer pipeline wiring, secrets, and commit-status
output.
activation-gate: |
User mentions GitOps update, gitops-update, dispatch to another repo,
two-repo version bump, cross-repo deployment, or wiring build output to
config repo.
category: ci
impact: high
---
# GitOps Update — consumer setup
## What you need
- **GitOps repo** — holds the configuration files (e.g. `Chart.yaml`, `values.yaml`)
- **Consumer repo** — builds artifacts and triggers the update
- **Bottitoken** — Gitea token with write access to the GitOps repo only
Two repos, isolated access. The consumer never writes to GitOps directly;
it dispatches a workflow that clones, updates, commits, and pushes.
---
## 1. GitOps-repo setup
Create `.gitea/workflows/gitops-service.yaml`:
```yaml
name: GitOps Update
run-name: "GitOps (${{ inputs.dispatch_id || 'manual' }})"
on:
workflow_dispatch:
inputs:
file:
required: true
type: string
yq_tpl:
required: true
type: string
version:
required: true
type: string
source_repo:
required: true
type: string
source_commit:
required: true
type: string
dispatch_id:
required: false
type: string
git_tag_prefix:
required: false
type: string
extra_cmd:
required: false
type: string
author_name:
required: false
type: string
author_email:
required: false
type: string
env:
INPUT_FILE: ${{ inputs.file }}
YQ_TPL: ${{ inputs.yq_tpl }}
VERSION: ${{ inputs.version }}
SOURCE_REPO: ${{ inputs.source_repo }}
SOURCE_COMMIT: ${{ inputs.source_commit }}
GITOPS_REPO: ${{ github.repository }}
GITEA_API_URL: ${{ gitea.server_url }}
GIT_TAG_PREFIX: ${{ inputs.git_tag_prefix || '' }}
GITOPS_EXTRA_CMD: ${{ inputs.extra_cmd || '' }}
GIT_USER_NAME: ${{ inputs.author_name || '' }}
GIT_USER_EMAIL: ${{ inputs.author_email || '' }}
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: niko/gitea-ci-library
path: .ci
- name: Install yq
run: |
wget -qO /usr/local/bin/yq \
https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
chmod +x /usr/local/bin/yq
- name: Run GitOps update
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
bash .ci/scripts/gitops-update.sh
```
> **⚠️ yq ladataan lennossa.** Tämä on väliaikainen kompromissi. Myöhemmin
> julkaistaan Docker Hubiin custom CI-kontti, jossa nodejs + git + yq
> valmiina. Sama patterni kuin `ci-bats` ja `ci-cucumber`.
> Ks. `skills/ci-container-build/SKILL.md`.
Key points:
- `run-name` must include `dispatch_id` — the consumer's poll step uses it to find the run
- `secrets.GITEA_TOKEN` is the **auto-token** — write access to the GitOps repo only, no consumer access needed
- Commit message becomes `"[skip ci] gitops: update version to X.Y.Z"` — used by consumer to find the commit SHA
---
## 2. Consumer-repo setup
### 2.1 Token
Create a Gitea token with write access to the GitOps repo:
1. Gitea → `Settings``Applications``Generate Token`
2. Select the GitOps repo, grant write access
3. Save as an Actions secret in the consumer repo: **`GITOPS_DISPATCH_TOKEN`**
### 2.2 Pipeline call
Add a job after your build step that calls the dispatch workflow:
```yaml
gitops-update:
needs: [build-push]
if: success()
uses: niko/gitea-ci-library/.gitea/workflows/gitops-dispatch.yml@v1
secrets: inherit
with:
env_json: ${{ needs.load-config.outputs.env_json }}
version: ${{ needs.version.outputs.version }}
GITOPS_FILE: dev/Chart.yaml
GITOPS_YQ_TPL: '.version = "{{VERSION}}"'
GITOPS_REPO: niko/your-gitops-repo
```
This single job handles: dispatch → poll → find commit SHA → set commit-status on your commit → produce `GITOPS_SUMMARY` output.
To run extra commands (e.g. `helm dependency update`) after the version bump and before the commit:
```yaml
gitops-update:
needs: [load-config, check-version, helm-build-push]
if: success()
uses: niko/gitea-ci-library/.gitea/workflows/gitops-dispatch.yml@v1
secrets: inherit
with:
env_json: ${{ needs.load-config.outputs.env_json }}
version: ${{ needs.check-version.outputs.version }}
GITOPS_FILE: Chart.yaml
GITOPS_YQ_TPL: '(.dependencies[] | select(.name == "agent-platform-helm") | .version) = "{{VERSION}}"'
GITOPS_REPO: niko/agent-platform-gitops
GITOPS_EXTRA_CMD: helm dependency update
```
When `GITOPS_EXTRA_CMD` is set, the script runs it after `yq` and stages all changes (`git add -A`) instead of only the input file — so any files generated by the extra command (e.g. `Chart.lock`, `charts/`) are included in the commit.
By default the GitOps commit is made as `gitea-ci-bot`. To use the original commit author instead, the dispatch workflow resolves it automatically from the consumer repo — no extra config needed. Just ensure the GitOps repo's `gitops-service.yaml` template has the `author_name` and `author_email` inputs and env mappings.
### 2.3 Parameters
| Input | Required | Description |
|---|---|---|---|
| `env_json` | Yes | Config JSON with `GITEA_API_URL`, optional `GIT_TAG_PREFIX` (for multi-component repos) |
| `version` | Yes | Version to write (e.g. `0.2.3`) |
| `GITOPS_FILE` | Yes | Path in GitOps repo (e.g. `dev/Chart.yaml`) |
| `GITOPS_YQ_TPL` | Yes | yq expression, `{{VERSION}}` is replaced at runtime |
| `GITOPS_REPO` | Yes | GitOps repo slug (e.g. `niko/agent-platform-gitops`) |
| `GITOPS_EXTRA_CMD` | No | Shell command to run after yq update, before git commit (e.g. `helm dependency update`) |
### 2.4 Output
The workflow produces a `summary` output in pipe format:
```
component|version|status|commit_sha|repo
agent-platform-helm|0.2.3|success|abc789def|niko/agent-platform-gitops
```
Pass it to `report-summary.yml` for the pipeline summary:
```yaml
report-summary:
needs: [load-config, gitops-update]
if: always()
uses: niko/gitea-ci-library/.gitea/workflows/report-summary.yml@main
with:
env_json: ${{ needs.load-config.outputs.env_json }}
suites: bats cucumber
gitops: ${{ needs.gitops-update.outputs.summary }}
```
---
## 3. Token summary
| Token | Where | Scope | Purpose |
|---|---|---|---|
| `GITOPS_DISPATCH_TOKEN` (manual) | Consumer secrets | write GitOps repo | Dispatches the GitOps workflow |
| `GITHUB_TOKEN` (auto) | Consumer workflow | write consumer repo | Sets commit-status on consumer's commit |
| `GITEA_TOKEN` (auto) | GitOps workflow | write GitOps repo | Clone, push, commit-status in GitOps repo |
---
## 4. What happens at runtime
1. Consumer's `gitops-dispatch.yml` generates a unique `dispatch_id` and POSTs it to the GitOps repo
2. GitOps workflow clones its own repo, applies `yq`, runs `GITOPS_EXTRA_CMD` if set, then commits + pushes
3. Consumer polls the GitOps repo's runs until the workflow completes
4. Consumer lists recent commits and finds the matching one by commit message `"gitops: update version to X.Y.Z"`
5. Consumer sets commit-status `gitops/{repo}[/{prefix}]` on its own commit with a link to the exact GitOps commit
6. If no matching commit is found (no change or error), the job fails
7. On failure, `GITOPS_SUMMARY` still flows through `report-summary` with `status=failure`
---
## 5. GIT_TAG_PREFIX (optional)
If the same consumer repo dispatches updates for multiple components (e.g. Docker image + Helm chart), set `GIT_TAG_PREFIX` in your `gitea-env.conf`:
```
GIT_TAG_PREFIX=docker/
```
Each component gets its own commit-status context:
| Prefix | Context |
|---|---|
| (empty) | `gitops/agent-platform` |
| `docker/` | `gitops/agent-platform/docker` |
| `helm/` | `gitops/agent-platform/helm` |
This prevents status overwrites between parallel dispatch jobs.
---
## 6. What you do NOT need to know
- How `gitops-update.sh` works internally
- How the polling finds the run
- How the commit SHA is extracted
- Race condition handling
- CI container plans
All of that is handled by `gitops-dispatch.yml`. You just call it.
+15
View File
@@ -153,6 +153,21 @@ teardown() {
[ "$NEXT_VERSION" = "0.3.2" ] [ "$NEXT_VERSION" = "0.3.2" ]
} }
@test "VERSION_FILE=Chart-umbrella.yaml extracts only top-level version" {
mock_set_sequence '[{"code": 200, "body": []}]'
mock_start
export VERSION_FILE="$BATS_TEST_DIRNAME/fixtures/check-version/Chart-umbrella.yaml"
run bash scripts/check-version.sh
echo "STATUS=$status"
echo "OUTPUT=$output"
[ "$status" -eq 0 ]
source /tmp/build-ctx/build.env
echo "NEXT_VERSION=$NEXT_VERSION"
[ "$NEXT_VERSION" = "0.1.0" ]
}
@test "no version source exits with error" { @test "no version source exits with error" {
mock_set_sequence '[{"code": 200, "body": []}]' mock_set_sequence '[{"code": 200, "body": []}]'
mock_start mock_start
+5 -5
View File
@@ -44,15 +44,15 @@ EOF
[[ "$output" == *"GITEA_TOKEN"* ]] [[ "$output" == *"GITEA_TOKEN"* ]]
} }
@test "missing GIT_PAGES_PUBLISH_TOKEN secret → exit 1" { @test "missing REPORTS_PUBLISH_TOKEN secret → exit 1" {
cat > "$CONF_FILE" <<EOF cat > "$CONF_FILE" <<EOF
SOME_KEY=ok SOME_KEY=ok
EOF EOF
export GITEA_TOKEN="sometoken" export GITEA_TOKEN="sometoken"
unset GIT_PAGES_PUBLISH_TOKEN unset REPORTS_PUBLISH_TOKEN
run bash scripts/ci-validate.sh run bash scripts/ci-validate.sh
[ "$status" -eq 1 ] [ "$status" -eq 1 ]
[[ "$output" == *"GIT_PAGES_PUBLISH_TOKEN"* ]] [[ "$output" == *"REPORTS_PUBLISH_TOKEN"* ]]
} }
@test "valid config and all secrets → exit 0" { @test "valid config and all secrets → exit 0" {
@@ -61,7 +61,7 @@ API_URL=https://example.com
ANOTHER=https://test.fi ANOTHER=https://test.fi
EOF EOF
export GITEA_TOKEN="sometoken" export GITEA_TOKEN="sometoken"
export GIT_PAGES_PUBLISH_TOKEN="sometoken" export REPORTS_PUBLISH_TOKEN="sometoken"
run bash scripts/ci-validate.sh run bash scripts/ci-validate.sh
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
} }
@@ -73,7 +73,7 @@ EOF
VALID_URL=https://example.com VALID_URL=https://example.com
EOF EOF
export GITEA_TOKEN="sometoken" export GITEA_TOKEN="sometoken"
export GIT_PAGES_PUBLISH_TOKEN="sometoken" export REPORTS_PUBLISH_TOKEN="sometoken"
run bash scripts/ci-validate.sh run bash scripts/ci-validate.sh
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
} }
+20 -19
View File
@@ -3,6 +3,7 @@
setup() { setup() {
source tests/helpers/mock-api.sh source tests/helpers/mock-api.sh
export DISPATCH_POLL_INTERVAL="0.1" export DISPATCH_POLL_INTERVAL="0.1"
export DISPATCH_ID="test123"
} }
teardown() { teardown() {
@@ -12,8 +13,7 @@ teardown() {
@test "dispatch succeeds: POST 201, poll running x3 then success → exit 0" { @test "dispatch succeeds: POST 201, poll running x3 then success → exit 0" {
mock_set_sequence '[ mock_set_sequence '[
{"code":201}, {"code":201},
{"code":200,"body":{"workflow_runs":[{"id":1,"status":"running"}]}}, {"code":200,"body":{"workflow_runs":[{"id":1,"display_title":"POC (test123)","run_number":42,"status":"running"}]}},
{"code":200,"body":{"id":1,"status":"running"}},
{"code":200,"body":{"id":1,"status":"running"}}, {"code":200,"body":{"id":1,"status":"running"}},
{"code":200,"body":{"id":1,"status":"running"}}, {"code":200,"body":{"id":1,"status":"running"}},
{"code":200,"body":{"id":1,"status":"completed","conclusion":"success"}} {"code":200,"body":{"id":1,"status":"completed","conclusion":"success"}}
@@ -26,7 +26,7 @@ teardown() {
@test "dispatch: poll returns failure conclusion → exit 1" { @test "dispatch: poll returns failure conclusion → exit 1" {
mock_set_sequence '[ mock_set_sequence '[
{"code":201}, {"code":201},
{"code":200,"body":{"workflow_runs":[{"id":1,"status":"running"}]}}, {"code":200,"body":{"workflow_runs":[{"id":1,"display_title":"POC (test123)","run_number":42,"status":"running"}]}},
{"code":200,"body":{"id":1,"status":"running"}}, {"code":200,"body":{"id":1,"status":"running"}},
{"code":200,"body":{"id":1,"status":"completed","conclusion":"failure"}} {"code":200,"body":{"id":1,"status":"completed","conclusion":"failure"}}
]' ]'
@@ -38,7 +38,7 @@ teardown() {
@test "dispatch: poll returns cancelled conclusion → exit 1" { @test "dispatch: poll returns cancelled conclusion → exit 1" {
mock_set_sequence '[ mock_set_sequence '[
{"code":201}, {"code":201},
{"code":200,"body":{"workflow_runs":[{"id":1,"status":"running"}]}}, {"code":200,"body":{"workflow_runs":[{"id":1,"display_title":"POC (test123)","run_number":42,"status":"running"}]}},
{"code":200,"body":{"id":1,"status":"running"}}, {"code":200,"body":{"id":1,"status":"running"}},
{"code":200,"body":{"id":1,"status":"completed","conclusion":"cancelled"}} {"code":200,"body":{"id":1,"status":"completed","conclusion":"cancelled"}}
]' ]'
@@ -47,18 +47,18 @@ teardown() {
[ "$status" -eq 1 ] [ "$status" -eq 1 ]
} }
@test "timeout: poll never completes, exceeds timeout_minutes → exit 124" { @test "timeout: no matching run found, exceeds timeout_minutes → exit 124" {
mock_set_sequence '[ mock_set_sequence '[
{"code":201}, {"code":201},
{"code":200,"body":{"workflow_runs":[{"id":1,"status":"running"}]}}, {"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"id":1,"status":"running"}}, {"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"id":1,"status":"running"}}, {"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"id":1,"status":"running"}}, {"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"id":1,"status":"running"}}, {"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"id":1,"status":"running"}}, {"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"id":1,"status":"running"}}, {"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"id":1,"status":"running"}}, {"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"id":1,"status":"running"}} {"code":200,"body":{"workflow_runs":[]}}
]' ]'
mock_start mock_start
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" '{"version":"1.2.3"}' "http://localhost:18080" "test-token-abc123" "0.001" run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" '{"version":"1.2.3"}' "http://localhost:18080" "test-token-abc123" "0.001"
@@ -77,7 +77,7 @@ teardown() {
@test "POST dispatch is called with correct URL and payload" { @test "POST dispatch is called with correct URL and payload" {
mock_set_sequence '[ mock_set_sequence '[
{"code":201}, {"code":201},
{"code":200,"body":{"workflow_runs":[{"id":1,"status":"running"}]}}, {"code":200,"body":{"workflow_runs":[{"id":1,"display_title":"POC (test123)","run_number":42,"status":"running"}]}},
{"code":200,"body":{"id":1,"status":"completed","conclusion":"success"}} {"code":200,"body":{"id":1,"status":"completed","conclusion":"success"}}
]' ]'
mock_start mock_start
@@ -91,6 +91,7 @@ teardown() {
[[ "$body" == *'"ref":"main"'* ]] [[ "$body" == *'"ref":"main"'* ]]
[[ "$body" == *'"inputs"'* ]] [[ "$body" == *'"inputs"'* ]]
[[ "$body" == *'"version":"1.2.3"'* ]] [[ "$body" == *'"version":"1.2.3"'* ]]
[[ "$body" == *'"dispatch_id":"test123"'* ]]
} }
@test "missing gitea_api_url argument → exit 1 with error message" { @test "missing gitea_api_url argument → exit 1 with error message" {
@@ -120,15 +121,15 @@ teardown() {
[ "$status" -eq 1 ] [ "$status" -eq 1 ]
} }
@test "dispatch: no workflow run found after dispatch → exit 1" { @test "dispatch: no workflow run found after dispatch → exit 124 (timeout)" {
mock_set_sequence '[ mock_set_sequence '[
{"code":201}, {"code":201},
{"code":200,"body":{"workflow_runs":[]}} {"code":200,"body":{"workflow_runs":[]}}
]' ]'
mock_start mock_start
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" '{}' "http://localhost:18080" "test-token-abc123" run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" '{}' "http://localhost:18080" "test-token-abc123" "0.001"
[ "$status" -eq 1 ] [ "$status" -eq 124 ]
[[ "$output" == *"ERROR"* ]] [[ "$output" == *"ERROR"* || "$output" == *"Timeout"* ]]
} }
@test "missing inputs_json argument → exit 1" { @test "missing inputs_json argument → exit 1" {
+42
View File
@@ -0,0 +1,42 @@
Feature: GitOps update
As a GitOps repository
I want to update version references and report results to the caller
So that the deployment chain is traceable from source to GitOps commit
Background:
Given a project repository exists in Gitea
And a commit has been pushed to the repository
@mock
Scenario: Not enough env vars — script fails, no status set
Given insufficient environment variables are provided for the GitOps update
When the GitOps update script runs
Then the script exits with error
@mock
Scenario: GitOps job fails — no status set (SHA not yet known)
Given the GitOps repository clone will fail
When the GitOps update script runs
Then the script exits with error
@mock
Scenario: Everything succeeds — GitOps repo gets success status with link to caller
Given a valid GitOps update dispatch
When the GitOps update script runs
Then the script exits successfully
And the GitOps repo commit shows a success status with a link to the caller commit
@mock
Scenario: GitOps push fails — GitOps repo gets failure status
Given the GitOps repo push will fail after the version is committed
When the GitOps update script runs
Then the script exits with error
And the GitOps repo commit shows a failure status linking to the caller commit
@mock
Scenario: No changes — GitOps repo gets "no change" status
Given the version file already has the target version
When the GitOps update script runs
Then the script exits successfully
And the GitOps repo commit shows a "no change" status
And no Git commit or push was performed
@@ -27,7 +27,7 @@ function bashQuiet(cmd) {
} }
function runReportStatus(args) { function runReportStatus(args) {
return bash(`export GITEA_API_URL="http://localhost:18080" GITEA_TOKEN="test-token-abc123" GIT_PAGES_URL="https://reports.example.com" GITHUB_REPOSITORY="test-owner/test-repo" GITHUB_SHA="abc123def456789012345678901234567890abcd" GITHUB_RUN_ID="42"; bash "${REPORT_SCRIPT}" ${args}`); return bash(`export GITEA_API_URL="http://localhost:18080" GITEA_TOKEN="test-token-abc123" GIT_PAGES_URL="https://reports.example.com" GITHUB_REPOSITORY="test-owner/test-repo" GITHUB_REF_NAME="main" GITHUB_SHA="abc123def456789012345678901234567890abcd" GITHUB_RUN_ID="42"; bash "${REPORT_SCRIPT}" ${args}`);
} }
function getMockBody() { function getMockBody() {
@@ -57,7 +57,7 @@ When('a build step completes successfully and reports its results', function ()
Then('the commit shows a success status with a clickable link to the results', function () { Then('the commit shows a success status with a clickable link to the results', function () {
const body = getMockBody(); const body = getMockBody();
if (!body.includes('"state":"success"')) throw new Error('Expected success status'); if (!body.includes('"state":"success"')) throw new Error('Expected success status');
if (!body.includes('"target_url":"https://reports.example.com/test-owner/test-repo/reports/abc123de/cucumber/"')) throw new Error('Expected URL'); if (!body.includes('"target_url":"https://reports.example.com/test-owner/test-repo/main/abc123de/cucumber/"')) throw new Error('Expected URL');
}); });
When('a build step fails', function () { When('a build step fails', function () {
@@ -0,0 +1,172 @@
const { spawnSync, execSync } = require('child_process');
const { Before, After, Given, When, Then } = require('@cucumber/cucumber');
const path = require('path');
const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..');
const MOCK_SCRIPT = path.join(PROJECT_ROOT, 'tests', 'helpers', 'mock-api.sh');
const GITOPS_SCRIPT = path.join(PROJECT_ROOT, 'scripts', 'gitops-update.sh');
const MOCK_HELPERS = path.join(PROJECT_ROOT, 'tests', 'helpers');
const REQ_FILE = '/tmp/gitops-mock-requests.log';
const BASE_ENV = {
INPUT_FILE: 'dev/Chart.yaml',
YQ_TPL: '(.version) = "{{VERSION}}"',
VERSION: '0.2.3',
SOURCE_REPO: 'niko/app',
SOURCE_COMMIT: 'abc123def456',
GITOPS_REPO: 'niko/app-gitops',
GITEA_API_URL: 'http://localhost:18080',
GITEA_TOKEN: 'test-token',
};
Before({ tags: '@mock' }, function () {
process.env.PATH = `${MOCK_HELPERS}:${process.env.PATH}`;
try { execSync('rm -f /tmp/gitops-mock-requests.log', { stdio: 'ignore' }); } catch (_) {}
// Restart mock with known request file path
const result = spawnSync('bash', ['-c', `
source "${MOCK_SCRIPT}"
mock_stop 2>/dev/null
MOCK_REQUEST_FILE="${REQ_FILE}"
mock_start
sleep 0.5
curl -s -o /dev/null -w "%{http_code}" --max-time 3 http://localhost:18080/api/v1/repos/health
`], {
cwd: PROJECT_ROOT, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe']
});
const code = result.stdout.trim();
if (!code.startsWith('2') && !code.startsWith('4')) {
throw new Error(`GitOps mock restart failed: ${result.stderr.substring(0,200)}`);
}
});
After({ tags: '@mock' }, function () {
spawnSync('bash', ['-c', `source "${MOCK_SCRIPT}" && mock_stop 2>/dev/null`], { stdio: 'ignore' });
try { execSync('rm -f /tmp/gitops-mock-requests.log /tmp/gitops-git-calls.log', { stdio: 'ignore' }); } catch (_) {}
});
function bash(cmd) {
const result = spawnSync('bash', ['-c', cmd], {
cwd: PROJECT_ROOT,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
});
return { status: result.status, stdout: result.stdout || '', stderr: result.stderr || '' };
}
function getFirstBody() {
return bash(`grep -A1 '^POST ' "${REQ_FILE}" 2>/dev/null | head -2 | tail -1 || echo ""`).stdout.trim();
}
function getFirstPath() {
return bash(`grep '^POST ' "${REQ_FILE}" 2>/dev/null | head -1 | awk '{print $2}' || echo ""`).stdout.trim();
}
function getLastBody() {
return bash(`grep -A1 '^POST ' "${REQ_FILE}" 2>/dev/null | grep -v '^POST ' | tail -1 || echo ""`).stdout.trim();
}
function getLastPath() {
return bash(`grep '^POST ' "${REQ_FILE}" 2>/dev/null | tail -1 | awk '{print $2}' || echo ""`).stdout.trim();
}
function requestCount() {
return parseInt(bash(`grep -c '^POST ' "${REQ_FILE}" 2>/dev/null || echo 0`).stdout.trim(), 10) || 0;
}
function gitCalls() {
const callsFile = process.env.GIT_CALLS_FILE || '/dev/null';
const out = bash(`cat "${callsFile}" 2>/dev/null || echo ""`).stdout;
return out.split('\n').filter(l => l.length > 0);
}
function runScript(envOverrides) {
const env = { ...BASE_ENV, ...envOverrides };
const scriptPath = `/tmp/gitops-run-${Date.now()}.sh`;
const exports = Object.entries(env)
.map(([k, v]) => `export ${k}="${v.replace(/"/g, '\\"')}"`)
.join('\n');
require('fs').writeFileSync(scriptPath, `${exports}\nexport PATH="${MOCK_HELPERS}:$PATH"\nset -euo pipefail\nbash "${GITOPS_SCRIPT}"\nsync\n`, 'utf8');
try {
return bash(`bash "${scriptPath}"`);
} finally {
require('fs').unlinkSync(scriptPath);
}
}
Given('insufficient environment variables are provided for the GitOps update', function () {
this.envOverrides = { INPUT_FILE: '' };
});
Given('the GitOps repository clone will fail', function () {
this.envOverrides = { GIT_MOCK_FAIL: '1' };
});
Given('a valid GitOps update dispatch', function () {
this.envOverrides = {};
});
Given('the GitOps repo push will fail after the version is committed', function () {
this.envOverrides = { GIT_MOCK_FAIL_PUSH: '1' };
});
Given('the version file already has the target version', function () {
this.envOverrides = {
GIT_MOCK_DIFF_NO_CHANGES: '1',
GIT_CALLS_FILE: '/tmp/gitops-git-calls.log',
};
});
When('the GitOps update script runs', function () {
this.result = runScript(this.envOverrides || {});
});
Then('the script exits with error', function () {
if (this.result.status === 0) throw new Error(`Expected non-zero exit, got 0. stderr: ${this.result.stderr.substring(0,200)}`);
});
Then('the script exits successfully', function () {
if (this.result.status !== 0) throw new Error(`Expected exit 0, got ${this.result.status}: ${this.result.stderr.substring(0,200)}`);
});
Then('the GitOps repo commit shows a success status with a link to the caller commit', function () {
const count = requestCount();
if (count < 1) throw new Error(`Expected at least 1 request, got ${count}`);
const body = getFirstBody();
if (!body.includes('"state":"success"')) throw new Error(`Expected success state, body: ${body.substring(0,200)}`);
if (!body.includes('"context":"app ')) throw new Error(`Expected context "app unknown", body: ${body.substring(0,200)}`);
if (!body.includes('"description":"Install to dev 0.2.3"')) throw new Error(`Expected description, body: ${body.substring(0,200)}`);
if (!body.includes('niko/app/commit/abc123def456')) throw new Error(`Expected link to caller commit, body: ${body.substring(0,200)}`);
const pathStr = getFirstPath();
if (!pathStr.includes('/repos/niko/app-gitops/statuses/')) throw new Error(`Expected gitops repo path, got: ${pathStr}`);
});
Then('the GitOps repo commit shows a failure status linking to the caller commit', function () {
const count = requestCount();
if (count < 1) throw new Error(`Expected at least 1 request, got ${count}`);
const body = getFirstBody();
if (!body.includes('"state":"failure"')) throw new Error(`Expected failure state, body: ${body.substring(0,200)}`);
if (!body.includes('"context":"app ')) throw new Error(`Expected context "app unknown", body: ${body.substring(0,200)}`);
if (!body.includes('"description":"Install to dev 0.2.3"')) throw new Error(`Expected description, body: ${body.substring(0,200)}`);
if (!body.includes('niko/app/commit/abc123def456')) throw new Error(`Expected link to caller commit, body: ${body.substring(0,200)}`);
const pathStr = getFirstPath();
if (!pathStr.includes('/repos/niko/app-gitops/statuses/')) throw new Error(`Expected gitops repo path, got: ${pathStr}`);
});
Then('the GitOps repo commit shows a "no change" status', function () {
const count = requestCount();
if (count < 1) throw new Error(`Expected at least 1 request, got ${count}`);
const body = getFirstBody();
if (!body.includes('"state":"success"')) throw new Error(`Expected success state, body: ${body.substring(0,200)}`);
if (!body.includes('"description":"Install to dev 0.2.3 \u2014 no change"')) {
throw new Error(`Expected "no change" description, body: ${body.substring(0,200)}`);
}
const pathStr = getFirstPath();
if (!pathStr.includes('/repos/niko/app-gitops/statuses/')) throw new Error(`Expected gitops repo path, got: ${pathStr}`);
});
Then('no Git commit or push was performed', function () {
const calls = gitCalls();
if (calls.some(l => l.includes(' commit ') || l.includes(' push '))) {
throw new Error(`Expected no commit or push, got: ${calls.join(', ')}`);
}
});
@@ -15,7 +15,7 @@ function bash(cmd) {
encoding: 'utf-8', encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'], stdio: ['pipe', 'pipe', 'pipe'],
}); });
return { status: 0, stdout: out }; return { status: 0, stdout: out, stderr: '' };
} catch (e) { } catch (e) {
return { status: e.status, stdout: e.stdout || '', stderr: e.stderr || '' }; return { status: e.status, stdout: e.stdout || '', stderr: e.stderr || '' };
} }
@@ -54,7 +54,7 @@ function setupMock(seqJson) {
} }
function runDispatch(args) { function runDispatch(args) {
return bash(`export DISPATCH_POLL_INTERVAL="0.1"; bash "${DISPATCH_SCRIPT}" ${args}`); return bash(`export DISPATCH_ID="test123"; export DISPATCH_POLL_INTERVAL="0.1"; bash "${DISPATCH_SCRIPT}" ${args}`);
} }
Given('a deployment has completed in the target environment', function () { Given('a deployment has completed in the target environment', function () {
@@ -66,7 +66,7 @@ Given('the test project repository exists with test definitions', function () {
When('a test workflow is dispatched to a test project', function () { When('a test workflow is dispatched to a test project', function () {
setupMock(JSON.stringify([ setupMock(JSON.stringify([
{ code: 201 }, { code: 201 },
{ code: 200, body: { workflow_runs: [{ id: 1, status: 'running' }] } }, { code: 200, body: { workflow_runs: [{ id: 1, display_title: 'Workflow (test123)', run_number: 42, status: 'running' }] } },
{ code: 200, body: { id: 1, status: 'completed', conclusion: 'success' } }, { code: 200, body: { id: 1, status: 'completed', conclusion: 'success' } },
])); ]));
const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\' "http://localhost:18080" "test-token-abc123"'); const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\' "http://localhost:18080" "test-token-abc123"');
@@ -84,7 +84,7 @@ Then('the pipeline continues only after receiving a success result', function ()
When('a test workflow is dispatched and the tests fail', function () { When('a test workflow is dispatched and the tests fail', function () {
setupMock(JSON.stringify([ setupMock(JSON.stringify([
{ code: 201 }, { code: 201 },
{ code: 200, body: { workflow_runs: [{ id: 1, status: 'running' }] } }, { code: 200, body: { workflow_runs: [{ id: 1, display_title: 'Workflow (test123)', run_number: 42, status: 'running' }] } },
{ code: 200, body: { id: 1, status: 'completed', conclusion: 'failure' } }, { code: 200, body: { id: 1, status: 'completed', conclusion: 'failure' } },
])); ]));
const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\' "http://localhost:18080" "test-token-abc123"'); const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\' "http://localhost:18080" "test-token-abc123"');
@@ -98,15 +98,19 @@ Then('the calling pipeline reports failure', function () {
When('a test workflow is dispatched but does not finish within the allowed time', function () { When('a test workflow is dispatched but does not finish within the allowed time', function () {
setupMock(JSON.stringify([ setupMock(JSON.stringify([
{ code: 201 }, { code: 201 },
{ code: 200, body: { workflow_runs: [{ id: 1, status: 'running' }] } }, { code: 200, body: { workflow_runs: [] } },
{ code: 200, body: { id: 1, status: 'running' } }, { code: 200, body: { workflow_runs: [] } },
{ code: 200, body: { id: 1, status: 'running' } }, { code: 200, body: { workflow_runs: [] } },
{ code: 200, body: { id: 1, status: 'running' } }, { code: 200, body: { workflow_runs: [] } },
{ code: 200, body: { workflow_runs: [] } },
])); ]));
const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\' "http://localhost:18080" "test-token-abc123" "0.001"'); const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\' "http://localhost:18080" "test-token-abc123" "0.05"');
this.dispatchResult = r.status; this.dispatchResult = r.status;
this.dispatchStderr = r.stderr;
}); });
Then('the calling pipeline reports a timeout error', function () { Then('the calling pipeline reports a timeout error', function () {
if (this.dispatchResult !== 124) throw new Error(`Expected timeout exit 124, got ${this.dispatchResult}`); if (this.dispatchResult !== 124) {
throw new Error(`Expected timeout exit 124, got ${this.dispatchResult}. stderr: ${(this.dispatchStderr || '').substring(0,300)}`);
}
}); });
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v2
name: agent-platform
description: Agent Platform umbrella chart
type: application
version: 0.1.0
dependencies:
- name: vikunja
version: "0.1.0"
repository: oci://registry.example.com
- name: langfuse
version: "0.2.0"
repository: oci://registry.example.com
+176
View File
@@ -0,0 +1,176 @@
#!/usr/bin/env bats
setup() {
export INPUT_FILE=dev/Chart.yaml
export YQ_TPL='version = "{{VERSION}}"'
export VERSION=1.0.0
export SOURCE_REPO=niko/app
export SOURCE_COMMIT=abc123def456
export GITOPS_REPO=niko/app-gitops
export GITEA_TOKEN=test-token
export GITEA_API_URL=http://localhost:18080
}
teardown() {
if type mock_stop &>/dev/null 2>&1; then
mock_stop 2>/dev/null || true
fi
}
@test "missing GITEA_API_URL causes exit 1" {
unset GITEA_API_URL
run bash scripts/gitops-update.sh
[ "$status" -eq 1 ]
[[ "$output" == *"GITEA_API_URL"* ]]
}
@test "missing GITEA_TOKEN causes exit 1" {
unset GITEA_TOKEN
run bash scripts/gitops-update.sh
[ "$status" -eq 1 ]
[[ "$output" == *"GITEA_TOKEN"* ]]
}
@test "missing INPUT_FILE causes exit 1" {
unset INPUT_FILE
run bash scripts/gitops-update.sh
[ "$status" -eq 1 ]
[[ "$output" == *"INPUT_FILE"* ]]
}
@test "missing YQ_TPL causes exit 1" {
unset YQ_TPL
run bash scripts/gitops-update.sh
[ "$status" -eq 1 ]
[[ "$output" == *"YQ_TPL"* ]]
}
@test "missing VERSION causes exit 1" {
unset VERSION
run bash scripts/gitops-update.sh
[ "$status" -eq 1 ]
[[ "$output" == *"VERSION"* ]]
}
@test "missing SOURCE_REPO causes exit 1" {
unset SOURCE_REPO
run bash scripts/gitops-update.sh
[ "$status" -eq 1 ]
[[ "$output" == *"SOURCE_REPO"* ]]
}
@test "missing SOURCE_COMMIT causes exit 1" {
unset SOURCE_COMMIT
run bash scripts/gitops-update.sh
[ "$status" -eq 1 ]
[[ "$output" == *"SOURCE_COMMIT"* ]]
}
@test "_gitops_substitute replaces {{VERSION}}" {
run bash -c '
source scripts/gitops-update.sh >/dev/null 2>&1
_gitops_substitute "(.version) = \"{{VERSION}}\"" "0.2.3"
'
[ "$status" -eq 0 ]
[[ "$output" == '(.version) = "0.2.3"' ]]
}
@test "CLONE_URL is constructed correctly from GITEA_API_URL" {
export GITEA_API_URL=https://gitea.app.keskikuja.site
export GITEA_TOKEN=secret123
export GITOPS_REPO=niko/app-gitops
run bash -c '
source scripts/gitops-update.sh >/dev/null 2>&1
echo "$CLONE_URL"
'
[ "$status" -eq 0 ]
[ "$output" = "https://secret123@gitea.app.keskikuja.site/niko/app-gitops.git" ]
}
@test "CLONE_URL works with http:// URL" {
export GITEA_API_URL=http://localhost:18080
export GITEA_TOKEN=token
export GITOPS_REPO=owner/repo
run bash -c '
source scripts/gitops-update.sh >/dev/null 2>&1
echo "$CLONE_URL"
'
[ "$status" -eq 0 ]
[ "$output" = "https://token@localhost:18080/owner/repo.git" ]
}
@test "_gitops_substitute handles multiple {{VERSION}} occurrences" {
run bash -c '
source scripts/gitops-update.sh >/dev/null 2>&1
_gitops_substitute "version = \"{{VERSION}}\"; tag = \"v{{VERSION}}\"" "1.2.3"
'
[ "$status" -eq 0 ]
[[ "$output" == 'version = "1.2.3"; tag = "v1.2.3"' ]]
}
@test "git flow: clone yq add commit push" {
source tests/helpers/mock-api.sh
mock_set_sequence '[
{"code":201},
{"code":201}
]'
mock_start
export GIT_CALLS_FILE=$(mktemp)
export YQ_CALLS_FILE=$(mktemp)
export PATH="${BATS_TEST_DIRNAME}/helpers:$PATH"
export INPUT_FILE=dev/Chart.yaml
export YQ_TPL='(.version) = "{{VERSION}}"'
export VERSION=0.2.3
export SOURCE_REPO=niko/app
export SOURCE_COMMIT=abc123def456
export GITOPS_REPO=niko/app-gitops
export GITEA_API_URL=http://localhost:18080
export GITEA_TOKEN=test-token
run bash scripts/gitops-update.sh
[ "$status" -eq 0 ]
git_calls=$(cat "$GIT_CALLS_FILE")
[[ "$git_calls" == *"clone"* ]]
[[ "$git_calls" == *"add"* ]]
[[ "$git_calls" == *"commit"* ]]
[[ "$git_calls" == *"push"* ]]
yq_calls=$(cat "$YQ_CALLS_FILE")
[[ "$yq_calls" == *"eval -i"* ]]
rm -f "$GIT_CALLS_FILE" "$YQ_CALLS_FILE"
mock_stop
}
@test "one commit-status call: gitops-repo only" {
source tests/helpers/mock-api.sh
mock_set_sequence '[
{"code":201}
]'
mock_start
export GIT_CALLS_FILE=$(mktemp)
export YQ_CALLS_FILE=$(mktemp)
export PATH="${BATS_TEST_DIRNAME}/helpers:$PATH"
export INPUT_FILE=dev/Chart.yaml
export YQ_TPL='(.version) = "{{VERSION}}"'
export VERSION=0.2.3
export SOURCE_REPO=niko/app
export SOURCE_COMMIT=abc123def456
export GITOPS_REPO=niko/app-gitops
export GITEA_API_URL=http://localhost:18080
export GITEA_TOKEN=test-token
run bash scripts/gitops-update.sh
[ "$status" -eq 0 ]
path=$(mock_get_first_request_path)
body=$(mock_get_first_request_body)
[[ "$path" == *"/repos/niko/app-gitops/statuses/"* ]]
[[ "$body" == *'"context":"app '* ]]
[[ "$body" == *'"description":"Install to dev 0.2.3"'* ]]
[[ "$body" == *'"state":"success"'* ]]
rm -f "$GIT_CALLS_FILE" "$YQ_CALLS_FILE"
mock_stop
}
@test "missing GITOPS_REPO causes exit 1" {
unset GITOPS_REPO
run bash scripts/gitops-update.sh
[ "$status" -eq 1 ]
[[ "$output" == *"GITOPS_REPO"* ]]
}
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
echo "git $*" >> "${GIT_CALLS_FILE:-/dev/null}"
[ -z "${GIT_MOCK_FAIL:-}" ] || { echo "git: mock forced failure" >&2; exit 1; }
if [ "${1:-}" = "push" ] && [ -n "${GIT_MOCK_FAIL_PUSH:-}" ]; then
echo "git: mock push failure" >&2
exit 1
fi
# Skip -c config arguments
while [ "${1:-}" = "-c" ]; do
shift 2
done
case "$1" in
clone)
TARGET_DIR="${@: -1}"
mkdir -p "${TARGET_DIR}/$(dirname "$INPUT_FILE")"
echo 'version: 0.1.0' > "${TARGET_DIR}/${INPUT_FILE}"
echo "Cloning into '$TARGET_DIR'..."
;;
add|commit|push|config|init)
;;
diff)
# Default: exit 1 = has changes → proceed to commit
# GIT_MOCK_DIFF_NO_CHANGES=1 → exit 0 = no changes → "no change" path
if [ -n "${GIT_MOCK_DIFF_NO_CHANGES:-}" ]; then
exit 0
fi
exit 1
;;
rev-parse)
echo "mock-sha-9876543210fedcba9876543210fedcba98765432"
;;
*)
echo "git: unknown command: $*" >&2
exit 1
;;
esac
+5 -3
View File
@@ -12,8 +12,10 @@ MOCK_CONFIG_FILE=""
_kill_port() { _kill_port() {
local pids local pids
pids=$(lsof -ti ":$MOCK_PORT" 2>/dev/null) || true pids=$(lsof -ti ":$MOCK_PORT" 2>/dev/null) || true
[ -n "$pids" ] && kill -9 $pids 2>/dev/null || true if [ -n "$pids" ]; then
kill -9 $pids 2>/dev/null || true
sleep 0.5 sleep 0.5
fi
} }
_wait_port_free() { _wait_port_free() {
@@ -26,7 +28,7 @@ _wait_port_free() {
_wait_port_ready() { _wait_port_ready() {
local i=0 local i=0
while ! lsof -ti ":$MOCK_PORT" >/dev/null 2>&1 && [ $i -lt 5 ]; do while ! lsof -ti ":$MOCK_PORT" >/dev/null 2>&1 && [ $i -lt 30 ]; do
sleep 0.2 sleep 0.2
i=$((i + 1)) i=$((i + 1))
done done
@@ -44,7 +46,7 @@ mock_clear_sequence() {
mock_start() { mock_start() {
MOCK_RESPONSE_CODE="${MOCK_RESPONSE_CODE:-201}" MOCK_RESPONSE_CODE="${MOCK_RESPONSE_CODE:-201}"
MOCK_REQUEST_FILE=$(mktemp) MOCK_REQUEST_FILE="${MOCK_REQUEST_FILE:-$(mktemp)}"
echo "$MOCK_REQUEST_FILE" > "$MOCK_STATE_FILE" echo "$MOCK_REQUEST_FILE" > "$MOCK_STATE_FILE"
MOCK_CONFIG_FILE=$(mktemp) MOCK_CONFIG_FILE=$(mktemp)
+8
View File
@@ -67,6 +67,14 @@ class H(http.server.BaseHTTPRequestHandler):
self.end_headers() self.end_headers()
self.wfile.write(body.encode()) self.wfile.write(body.encode())
def do_PUT(self):
self._log_request('PUT')
code, body = self._get_response()
self.send_response(code)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(body.encode())
def do_PATCH(self): def do_PATCH(self):
self._log_request('PATCH') self._log_request('PATCH')
code, body = self._get_response() code, body = self._get_response()
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env bash
echo "yq $*" >> "${YQ_CALLS_FILE:-/dev/null}"
+23 -15
View File
@@ -4,18 +4,18 @@ setup() {
source tests/helpers/mock-api.sh source tests/helpers/mock-api.sh
export GITEA_API_URL="http://localhost:18080" export GITEA_API_URL="http://localhost:18080"
export GIT_PAGES_URL="http://localhost:18080" export GIT_PAGES_URL="http://localhost:18080"
export GIT_PAGES_PUBLISH_TOKEN="publish-token-abc" export REPORTS_PUBLISH_TOKEN="publish-token-abc"
export GITHUB_REPOSITORY="test-owner/test-repo" export GITHUB_REPOSITORY="test-owner/test-repo"
export GITHUB_SHA="abc123def456789012345678901234567890abcd" export GITHUB_SHA="abc123def456789012345678901234567890abcd"
export GITHUB_REF_NAME="main"
REPORT_DIR="reports/abc123de/unit-tests" mkdir -p "unit-tests"
mkdir -p "$REPORT_DIR" echo "<html>test</html>" > "unit-tests/index.html"
echo "<html>test</html>" > "$REPORT_DIR/index.html"
} }
teardown() { teardown() {
mock_stop mock_stop
rm -rf "reports/abc123de" rm -rf "unit-tests" "sub" "empty-suite"
} }
@test "missing suite_path argument → exit 1" { @test "missing suite_path argument → exit 1" {
@@ -24,11 +24,11 @@ teardown() {
[[ "$output" == *"ERROR"* ]] [[ "$output" == *"ERROR"* ]]
} }
@test "missing GITEA_API_URL → exit 1" { @test "missing GITHUB_REF_NAME → exit 1" {
unset GITEA_API_URL unset GITHUB_REF_NAME
run bash scripts/publish-git-pages.sh "unit-tests" run bash scripts/publish-git-pages.sh "unit-tests"
[ "$status" -eq 1 ] [ "$status" -eq 1 ]
[[ "$output" == *"GITEA_API_URL"* ]] [[ "$output" == *"GITHUB_REF_NAME"* ]]
} }
@test "missing GIT_PAGES_URL → exit 1" { @test "missing GIT_PAGES_URL → exit 1" {
@@ -38,11 +38,11 @@ teardown() {
[[ "$output" == *"GIT_PAGES_URL"* ]] [[ "$output" == *"GIT_PAGES_URL"* ]]
} }
@test "missing GIT_PAGES_PUBLISH_TOKEN → exit 1" { @test "missing REPORTS_PUBLISH_TOKEN → exit 1" {
unset GIT_PAGES_PUBLISH_TOKEN unset REPORTS_PUBLISH_TOKEN
run bash scripts/publish-git-pages.sh "unit-tests" run bash scripts/publish-git-pages.sh "unit-tests"
[ "$status" -eq 1 ] [ "$status" -eq 1 ]
[[ "$output" == *"GIT_PAGES_PUBLISH_TOKEN"* ]] [[ "$output" == *"REPORTS_PUBLISH_TOKEN"* ]]
} }
@test "missing GITHUB_REPOSITORY → exit 1" { @test "missing GITHUB_REPOSITORY → exit 1" {
@@ -58,6 +58,14 @@ teardown() {
[[ "$output" == *"not a directory"* ]] [[ "$output" == *"not a directory"* ]]
} }
@test "empty directory → exit 1" {
mkdir -p "empty-suite"
run bash scripts/publish-git-pages.sh "empty-suite"
[ "$status" -eq 1 ]
[[ "$output" == *"no files to publish"* ]]
rm -rf "empty-suite"
}
@test "valid publish returns report base URL" { @test "valid publish returns report base URL" {
mock_set_sequence '[ mock_set_sequence '[
{"code":200,"body":"published"} {"code":200,"body":"published"}
@@ -65,19 +73,19 @@ teardown() {
mock_start mock_start
run bash scripts/publish-git-pages.sh "unit-tests" run bash scripts/publish-git-pages.sh "unit-tests"
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
[[ "$output" == "http://localhost:18080/test-owner/test-repo/reports/abc123de" ]] [[ "$output" == "http://localhost:18080/test-owner/test-repo/main/abc123de/unit-tests/" ]]
} }
@test "publish with suite subpath" { @test "publish with suite subpath" {
mkdir -p "reports/abc123de/sub/suite" mkdir -p "sub/suite"
echo "sub" > "reports/abc123de/sub/suite/result.html" echo "sub" > "sub/suite/result.html"
mock_set_sequence '[ mock_set_sequence '[
{"code":200,"body":"published"} {"code":200,"body":"published"}
]' ]'
mock_start mock_start
run bash scripts/publish-git-pages.sh "sub/suite" run bash scripts/publish-git-pages.sh "sub/suite"
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
[[ "$output" == "http://localhost:18080/test-owner/test-repo/reports/abc123de" ]] [[ "$output" == "http://localhost:18080/test-owner/test-repo/main/abc123de/sub/suite/" ]]
} }
@test "git-pages returns HTTP 500 → exit 1" { @test "git-pages returns HTTP 500 → exit 1" {
+4 -4
View File
@@ -4,7 +4,7 @@ setup() {
export GITEA_API_URL="http://localhost:18080" export GITEA_API_URL="http://localhost:18080"
export GITEA_TOKEN="test-token-abc" export GITEA_TOKEN="test-token-abc"
export GIT_PAGES_URL="http://localhost:18080" export GIT_PAGES_URL="http://localhost:18080"
export GIT_PAGES_PUBLISH_TOKEN="publish-token-abc" export REPORTS_PUBLISH_TOKEN="publish-token-abc"
export GITHUB_REPOSITORY="test-owner/test-repo" export GITHUB_REPOSITORY="test-owner/test-repo"
export GITHUB_SHA="abc123def456789012345678901234567890abcd" export GITHUB_SHA="abc123def456789012345678901234567890abcd"
} }
@@ -36,9 +36,9 @@ setup() {
[[ "$output" == *"GIT_PAGES_URL"* ]] [[ "$output" == *"GIT_PAGES_URL"* ]]
} }
@test "missing GIT_PAGES_PUBLISH_TOKEN → exit 1" { @test "missing REPORTS_PUBLISH_TOKEN → exit 1" {
unset GIT_PAGES_PUBLISH_TOKEN unset REPORTS_PUBLISH_TOKEN
run bash scripts/publish.sh "unit-tests" run bash scripts/publish.sh "unit-tests"
[ "$status" -eq 1 ] [ "$status" -eq 1 ]
[[ "$output" == *"GIT_PAGES_PUBLISH_TOKEN"* ]] [[ "$output" == *"REPORTS_PUBLISH_TOKEN"* ]]
} }
+2 -1
View File
@@ -8,6 +8,7 @@ setup() {
export GITHUB_REPOSITORY="test-owner/test-repo" export GITHUB_REPOSITORY="test-owner/test-repo"
export GITHUB_SHA="abc123def456789012345678901234567890abcd" export GITHUB_SHA="abc123def456789012345678901234567890abcd"
export GITHUB_RUN_ID="42" export GITHUB_RUN_ID="42"
export GITHUB_REF_NAME="main"
} }
teardown() { teardown() {
@@ -35,7 +36,7 @@ teardown() {
body=$(mock_get_request_body) body=$(mock_get_request_body)
[[ "$body" == *'"state":"success"'* ]] [[ "$body" == *'"state":"success"'* ]]
[[ "$body" == *'"context":"unit-test"'* ]] [[ "$body" == *'"context":"unit-test"'* ]]
[[ "$body" == *'"target_url":"https://reports.example.com/test-owner/test-repo/reports/abc123de/cucumber/"'* ]] [[ "$body" == *'"target_url":"https://reports.example.com/test-owner/test-repo/main/abc123de/cucumber/"'* ]]
} }
@test "failure status constructs run URL when no suite" { @test "failure status constructs run URL when no suite" {