Compare commits

...

10 Commits

Author SHA1 Message Date
niko d6343438a3 Feture/gitops7 (#44)
CI Main / Build & Push Docker (push) Successful in 56s
gitops/gitea-ci-library GitOps: 0.2.32
CI Main / GitOps (push) Successful in 45s
CI Main / Move provider version tag (push) Successful in 16s
CI Main / Report Summary (push) Successful in 7s
CI Main / Latest versio (push) Successful in 24s
CI Main / Bats tests (push) Successful in 1m38s
CI Main / Cucumber tests (push) Successful in 1m44s
CI Main / Config load (push) Successful in 21s
unit-tests Bats test report
acc-tests Cucumber test report
ci-docker-build-push Docker push 0.2.32
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 Main / GitOps (push) Successful in 35s
CI Main / Report Summary (push) Successful in 7s
CI Main / Move provider version tag (push) Successful in 13s
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
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 / GitOps (push) Successful in 39s
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 / 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 / Latest versio (push) Successful in 19s
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 / Config load (push) Successful in 21s
unit-tests Bats test report
CI Main / Bats tests (push) Successful in 1m23s
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 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 Git-Pages Main / Config load (push) Successful in 24s
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 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 / Build & Push Helm chart (push) Successful in 48s
CI Main / Bats tests (push) Successful in 1m34s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 1m45s
CI Main / Load example-gitea-env.conf to pipeline env (push) Successful in 34s
CI Git-Pages Main / Check existing artifact (push) Successful in 21s
ci-helm-build-push Helm push 0.1.5
unit-tests Bats test report
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 / Check existing artifact (push) Successful in 24s
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 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
32 changed files with 1789 additions and 122 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
+3 -1
View File
@@ -41,12 +41,14 @@ jobs:
TAG="${{ inputs.tag }}"
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
CONTEXT_DIR=$(dirname "${DOCKERFILE}")
docker build \
--label "git.commit=${{ github.sha }}" \
--label "git.commitBy=${{ github.actor }}" \
--label "build.date=${NOW}" \
-f "${DOCKERFILE}" \
-t "${IMAGE_NAME}:${TAG}" .
-t "${IMAGE_NAME}:${TAG}" \
"${CONTEXT_DIR}"
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
echo "Pushing ${FULL_IMAGE} ..."
+9 -6
View File
@@ -52,13 +52,15 @@ jobs:
REGISTRY_HOST="${REGISTRY%%/*}"
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
CONTEXT_DIR=$(dirname "${DOCKERFILE}")
docker build \
--label "git.commit=${{ github.sha }}" \
--label "git.commitBy=${{ github.actor }}" \
--label "build.date=${NOW}" \
-f "${DOCKERFILE}" \
-t "${IMAGE}:${VERSION}" \
-t "${IMAGE}:latest" .
-t "${IMAGE}:latest" \
"${CONTEXT_DIR}"
FULL_IMAGE="${REGISTRY}/${IMAGE}:${VERSION}"
echo "Pushing ${FULL_IMAGE} ..."
@@ -81,11 +83,12 @@ jobs:
if [ -n "${DOCKER_UI_URL:-}" ] && [ -n "${VERSION:-}" ]; then
CONTAINER_URL="${DOCKER_UI_URL}/${DOCKER_IMAGE_NAME}/${VERSION}"
fi
bash .ci/scripts/report-status.sh success "Docker build & push ${VERSION} OK" ci-docker-build-push "" "$CONTAINER_URL"
- name: Report status FAILURE
if: failure()
run: bash .ci/scripts/report-status.sh failure "Docker build & push ${VERSION} FAILED" ci-docker-build-push
DIR=$(dirname "${DOCKERFILE}")
if [ "$DIR" != "." ]; then
bash .ci/scripts/report-status.sh success "${DIR}: Docker push ${VERSION}" "${DIR}-ci-docker-build-push" "" "$CONTAINER_URL"
else
bash .ci/scripts/report-status.sh success "Docker push ${VERSION}" ci-docker-build-push "" "$CONTAINER_URL"
fi
tag-commit:
runs-on: ubuntu-latest
+1
View File
@@ -4,3 +4,4 @@ DOCKER_REGISTRY=gitea.app.keskikuja.site/niko
DOCKER_IMAGE_NAME=gitea-ci-library-test-image
DOCKER_UI_URL=https://gitea.app.keskikuja.site/niko/-/packages/container
#DOCKERFILE=Dockerfile.platform
+19 -5
View File
@@ -7,14 +7,14 @@ on:
jobs:
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
secrets: inherit
with:
config_path: .gitea/workflows/example-gitea-env.conf
check-version:
name: Check existing artifact
name: Latest versio
needs: [load-config]
uses: niko/gitea-ci-library/.gitea/workflows/check-version.yml@main
secrets: inherit
@@ -39,7 +39,7 @@ jobs:
with:
env_json: ${{ needs.load-config.outputs.env_json }}
build-push:
docker-build-push:
name: Build & Push Docker
needs: [load-config, check-version, bats, cucumber]
if: needs.check-version.outputs.artifact_exists != 'true'
@@ -49,18 +49,32 @@ jobs:
env_json: ${{ needs.load-config.outputs.env_json }}
version: ${{ needs.check-version.outputs.version }}
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:
name: Report Summary
needs: [load-config, build-push]
needs: [load-config, check-version, docker-build-push, docker-gitops]
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.docker-gitops.outputs.summary }}
tag-maintenance:
name: Move provider version tag
needs: [build-push]
needs: [docker-gitops]
if: success()
uses: niko/gitea-ci-library/.gitea/workflows/tag-maintenance.yml@main
secrets: inherit
+17 -4
View File
@@ -5,20 +5,19 @@ on:
- main
paths:
- git-pages/**
- .gitea/workflows/helm-build-push.yml
- .gitea/workflows/git-pages.*
workflow_dispatch:
jobs:
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
secrets: inherit
with:
config_path: .gitea/workflows/git-pages.gitea-env.conf
check-version:
name: Check existing artifact
name: Latest version
needs: [load-config]
uses: niko/gitea-ci-library/.gitea/workflows/check-version.yml@main
secrets: inherit
@@ -36,11 +35,25 @@ jobs:
version: ${{ needs.check-version.outputs.version }}
chart_path: git-pages
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 == "git-pages") | .version) = "{{VERSION}}"'
GITOPS_REPO: niko/gitea-ci-gitops-tests
report-summary:
name: Report Summary
needs: [load-config, helm-push]
needs: [load-config, check-version, helm-push, chart-gitops]
if: always()
uses: niko/gitea-ci-library/.gitea/workflows/report-summary.yml@main
with:
env_json: ${{ needs.load-config.outputs.env_json }}
suites: ""
gitops: |
${{ needs.chart-gitops.outputs.summary }}
@@ -3,3 +3,4 @@ HELM_REGISTRY=gitea.app.keskikuja.site/niko
HELM_UI_URL=https://gitea.app.keskikuja.site/niko/-/packages/container
GIT_TAG_PREFIX=git-pages/
VERSION_FILE=git-pages/Chart.yaml
+189
View File
@@ -0,0 +1,189 @@
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
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_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: Dispatch to GitOps repo
env:
GITEA_TOKEN: ${{ secrets.GITOPS_DISPATCH_TOKEN }}
run: |
INPUTS=$(jq -nc \
--arg dispatch_id "${{ steps.gen.outputs.dispatch_id }}" \
--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:-}" \
'{dispatch_id: $dispatch_id, file: $file, yq_tpl: $yq_tpl, version: $version, source_repo: $source_repo, source_commit: $source_commit, git_tag_prefix: $git_tag_prefix}')
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}"
+20 -6
View File
@@ -9,9 +9,11 @@ on:
required: true
type: string
chart_path:
required: true
type: string
extra_dependency_paths:
required: false
type: string
default: '.'
secrets:
GITEA_TOKEN:
required: true
@@ -26,7 +28,7 @@ env:
HELM_REGISTRY: ${{ fromJson(inputs.env_json).HELM_REGISTRY || '' }}
HELM_UI_URL: ${{ fromJson(inputs.env_json).HELM_UI_URL || '' }}
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 }}
concurrency:
@@ -51,10 +53,18 @@ jobs:
repository: niko/gitea-ci-library
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
run: |
helm dependency update "${CHART_PATH}"
helm package "${CHART_PATH}" \
CHART_DIR=$(dirname "${CHART_FILE}")
helm dependency update "${CHART_DIR}"
helm package "${CHART_DIR}" \
--version "${VERSION}" \
--app-version "${VERSION}" \
--destination /tmp/helm-packages
@@ -74,9 +84,13 @@ jobs:
- name: Report status with UI link
if: success() && env.HELM_UI_URL != ''
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}"
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:
runs-on: ubuntu-latest
+23
View File
@@ -9,6 +9,10 @@ on:
required: true
type: string
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:
GIT_PAGES_URL: ${{ fromJson(inputs.env_json).GIT_PAGES_URL }}
@@ -32,3 +36,22 @@ jobs:
echo "| ${suite} | [View report](${BASE}/${suite}/) |"
done
} >> "${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
+16 -1
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
**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
polkusuodatuksella, komponenttikohtaisilla versioilla ja git-tägien
etuliitteillä — jokainen komponentti julkaistaan itsenäisesti omassa
@@ -220,7 +222,9 @@ Consumer-repossa on oltava seuraavat asetukset:
|--------|--------|
| `GIT_PAGES_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`)
@@ -256,6 +260,17 @@ Jokaisen jobin alussa `ci-validate.sh` tarkistaa:
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
| Muuttuja | Kuvaus |
+2 -1
View File
@@ -37,7 +37,7 @@ kuuluu `git-pages/docs/`-alle, ei juuren `docs/`-kansioon.
| `git-pages/` | Raporttien hostaus (Helm-chartti) |
| `tests/` | Bats-testit skripteille |
### Provider workflowt (5 kpl)
### Provider workflowt (6 kpl)
| 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. |
| `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+) |
| `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)
+7 -1
View File
@@ -60,9 +60,15 @@ Salaisuudet eivät ole `.conf`-tiedostossa. Ne määritellään Gitean
organization/repository secrets -mekanismissa ja välitetään workflowlle
`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ä |
|---|---|---|
| `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) |
| `DOCKER_USERNAME` | Ei | `docker-build-push.yml` (oletus: `github.actor`, ei pakollinen kaikissa registryissä) |
| `DOCKER_PASSWORD` | Kyllä | `docker-build-push.yml` |
+25 -4
View File
@@ -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.
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
```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
1. **Dispatch:** `POST /api/v1/repos/{target_repo}/actions/workflows/{workflow_file}/dispatches`
2. **Poll:** `GET /api/v1/repos/{target_repo}/actions/runs` → odota valmistumista
3. **Palauta:** `conclusion` (`success`/`failure`/`timeout`)
1. **Generoi `dispatch_id`** — 8-hex uniikki tunniste
2. **Injektoi** `dispatch_id` inputteihin
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)
---
+123 -11
View File
@@ -103,7 +103,8 @@ joten `actions/checkout` toimii ilman node-asennuksia.
|-----------|------------|--------|
| `env_json` | Kyllä | Konffi `gitea-env.conf`:stä |
| `version` | Kyllä | Version string (check-version output) |
| `chart_path` | Ei | Polku Chart.yaml-hakemistoon, oletus `.` |
| `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:**
@@ -124,9 +125,10 @@ 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. **Package**`helm package` versiolla `$VERSION`
4. **Push OCI**`helm push` registryyn autentikoinnilla
5. **Report status** — commit-status + UI-linkki
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
@@ -136,6 +138,29 @@ eikä toimi air gap -ympäristössä. Korvaa tarvittaessa custom-kontilla
---
### `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
```
---
## Consumer-esimerkki (`example-*`)
### `example-feature.yml` — Feature-haaran CI
@@ -151,11 +176,23 @@ load-config → bats + cucumber → report-summary (always)
**Trigger:** `push` [branches: main]
```
load-config → check-version →
[artifact exists] → done
[no artifact] → bats + cucumber → report-summary (always) → docker-build-push
load-config ───────────────────────────────────────────────────────┐
load-config-helm ───────────────────────────────────────────┐ │
│ │
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
**Trigger:** `workflow_call`
@@ -174,7 +211,12 @@ commit-statuksen linkillä raporttiin.
**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
raporteista. Renderöityy HTML:ksi Gitea 1.27+ Summary-välilehdellä.
@@ -182,7 +224,77 @@ Forward-compatibeli — ei haittaa vanhemmilla Gitea-versioilla.
---
## Suunnitteilla
## Provider-skriptit
- `deploy.yml` — GitOps-deployment (dispatch-workflow.sh-pohjainen)
- `test.yml` — Klusteritason test flow
### `gitops-update.sh` — GitOps-version päivitys
**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).
---
+72 -28
View File
@@ -176,46 +176,90 @@ if [ "${#TO_DELETE[@]}" -eq 0 ]; then
fi
echo ""
echo "=== Phase 4: whiteout deletion ==="
echo "Creating whiteout tar for ${#TO_DELETE[@]} report(s)..."
echo "=== Phase 4: full site rebuild ==="
echo "Rebuilding site (${#TO_DELETE[@]} report(s) to delete)..."
WHITEOUT_TAR=$(mktemp)
trap 'rm -f "$WHITEOUT_TAR"' EXIT
ARCHIVE_FILE=$(mktemp)
SITE_DIR=$(mktemp -d)
NEW_TAR=$(mktemp)
cleanup_phase4() {
rm -f "$ARCHIVE_FILE" "$NEW_TAR"
rm -rf "$SITE_DIR"
}
trap cleanup_phase4 EXIT
python3 -c "
import tarfile, sys
# Try archive.tar first
echo "Downloading archive.tar..."
HTTP_CODE=$(curl_with_host -o "$ARCHIVE_FILE" -w "%{http_code}" -sS "${PAGES_URL}/.git-pages/archive.tar")
tar = tarfile.open(name='${WHITEOUT_TAR}', mode='w')
if [ "$HTTP_CODE" = "200" ] && tar -tf "$ARCHIVE_FILE" >/dev/null 2>&1; then
echo "Extracting archive..."
tar -xf "$ARCHIVE_FILE" -C "$SITE_DIR"
dirs = set()
for d in sys.argv[1:]:
dirs.add(d.strip())
for dir in "${TO_DELETE[@]}"; do
if [ -d "$SITE_DIR/$dir" ]; then
echo " Removing: $dir"
rm -rf "$SITE_DIR/$dir"
fi
done
else
echo "archive.tar failed (HTTP ${HTTP_CODE}) - falling back to manifest-based rebuild"
tarinfo = tarfile.TarInfo()
tarinfo.type = tarfile.CHRTYPE
tarinfo.devmajor = 0
tarinfo.devminor = 0
ALL_PATHS=$(echo "$MANIFEST" | jq -r '.contents | keys[]' 2>/dev/null || true)
for d in sorted(dirs, key=len, reverse=True):
info = tarinfo
info.name = d
tar.addfile(info)
if [ -z "$ALL_PATHS" ]; then
echo "ERROR: no files in manifest - cannot rebuild" >&2
exit 1
fi
tar.close()
" "${TO_DELETE[@]}"
EXCLUDE_GREP=""
for dir in "${TO_DELETE[@]}"; do
EXCLUDE_GREP="${EXCLUDE_GREP}${EXCLUDE_GREP:+|}^${dir}/"
done
echo "Patching ${PAGES_URL}/ with whiteout tar..."
HTTP_CODE=$(curl_with_host -X PATCH "${PAGES_URL}/" \
if [ -n "$EXCLUDE_GREP" ]; then
KEEP_PATHS=$(echo "$ALL_PATHS" | grep -v -E "$EXCLUDE_GREP" || true)
else
KEEP_PATHS="$ALL_PATHS"
fi
if [ -z "$KEEP_PATHS" ]; then
echo "No files to keep - site will be empty"
mkdir -p "$SITE_DIR/__placeholder__"
echo "placeholder" > "$SITE_DIR/__placeholder__/index.html"
else
FILE_COUNT=$(echo "$KEEP_PATHS" | wc -l | tr -d ' ')
echo "Downloading ${FILE_COUNT} file(s)..."
while IFS= read -r path; do
[ -z "$path" ] && continue
dir=$(dirname "$SITE_DIR/$path")
mkdir -p "$dir"
curl_with_host -o "$SITE_DIR/$path" -sS "${PAGES_URL}/${path}" || {
echo " WARN: failed to download ${path}"
}
done <<< "$KEEP_PATHS"
fi
fi
if [ -z "$(ls -A "$SITE_DIR" 2>/dev/null)" ]; then
echo "Site is empty - creating placeholder"
mkdir -p "$SITE_DIR/__placeholder__"
echo "placeholder" > "$SITE_DIR/__placeholder__/index.html"
fi
tar -cf "$NEW_TAR" -C "$SITE_DIR" .
echo "PUT: replacing site contents..."
HTTP_CODE=$(curl_with_host -X PUT "${PAGES_URL}/" \
-H "Content-Type: application/x-tar" \
-H "Atomic: no" \
--data-binary @"${WHITEOUT_TAR}" \
--data-binary @"${NEW_TAR}" \
-w "%{http_code}" \
-o /dev/null)
echo "HTTP $HTTP_CODE"
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "204" ]; then
echo "Retention cleanup finished."
echo "HTTP ${HTTP_CODE}"
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "204" ]; then
echo "Site rebuild completed."
else
echo "ERROR: retention HTTP ${HTTP_CODE}" >&2
echo "ERROR: PUT HTTP ${HTTP_CODE}" >&2
exit 1
fi
+11 -1
View File
@@ -6,7 +6,7 @@ metadata:
labels:
{{- include "git-pages.componentLabels" . | nindent 4 }}
annotations:
"helm.sh/hook": post-install, post-upgrade
"helm.sh/hook": post-install
"helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation
spec:
backoffLimit: 5
@@ -32,6 +32,16 @@ spec:
-H "Host: {{ .Values.ingress.host }}" \
-o /dev/null "http://git-pages:3000/.git-pages/health"
do sleep 2; done
echo "Init: checking if site already exists..."
MANIFEST=$(curl -sf \
-H "Host: {{ .Values.ingress.host }}" \
"http://git-pages:3000/.git-pages/manifest.json" 2>/dev/null || echo "")
if echo "$MANIFEST" | grep -q '"contents"'; then
echo "Init: site already initialized, skipping"
exit 0
fi
echo "Init: creating placeholder site..."
WORK=$(mktemp -d)
mkdir -p "$WORK/__init__"
+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_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_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
fi
RUNS_URL="$GITEA_API_URL/api/v1/repos/$TARGET_REPO/actions/runs?status=running"
RUNS_RESP=$(curl -s --connect-timeout 5 --max-time 10 \
-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
# Poll: find dispatched run by display_title matching
RUN_ID=""
TIMEOUT_SECONDS=$(awk "BEGIN {printf \"%.3f\", $TIMEOUT_MINUTES * 60}")
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
NOW=$(date +%s)
ELAPSED=$((NOW - START_TIME))
@@ -61,6 +77,12 @@ while true; do
if [ "$STATUS" = "completed" ]; then
CONCLUSION=$(echo "$RUN_RESP" | jq -r '.conclusion // "failure"')
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
fi
echo "ERROR: Workflow completed with conclusion: $CONCLUSION" >&2
+49
View File
@@ -0,0 +1,49 @@
#!/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:-}" \
'{file: $file, yq_tpl: $yq_tpl, version: $version, source_repo: $source_repo, source_commit: $source_commit, git_tag_prefix: $git_tag_prefix}')
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"
+114
View File
@@ -0,0 +1,114 @@
#!/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}"
git add "${INPUT_FILE}" || _gitops_fail "Failed to stage ${INPUT_FILE}"
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="gitea-ci-bot" \
-c 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
+71 -4
View File
@@ -202,7 +202,16 @@ jobs:
- name: Report
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,
@@ -218,6 +227,7 @@ niillä voi olla eri versio `latest`-imagen digesteistä. Ratkaisuja:
### Taso 1: Ei jälkikäsittelyä
Single repo:
```yaml
- name: Run tests
shell: bash
@@ -227,11 +237,27 @@ niillä voi olla eri versio `latest`-imagen digesteistä. Ratkaisuja:
- name: Report
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
Single repo:
```yaml
- name: Run tests
shell: bash
@@ -249,7 +275,30 @@ niillä voi olla eri versio `latest`-imagen digesteistä. Ratkaisuja:
- name: Report
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
@@ -273,7 +322,16 @@ niillä voi olla eri versio `latest`-imagen digesteistä. Ratkaisuja:
- name: Report
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
@@ -509,6 +567,15 @@ jobs:
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
`GIT_TAG_PREFIX` takaa että eri komponenttien versiohistoria pysyy erillään.
+280 -8
View File
@@ -123,6 +123,88 @@ ajetaan kontin *sisällä* — myös `actions/checkout@v4`. Se on JavaScript-act
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
Testi tuottaa raportin `reports/<suite>/`-hakemistoon. Yksi `ci-report.sh`-kutsu hoitaa sekä
@@ -158,6 +240,35 @@ Tiedostonimet `.gitea/workflows/`-kansiossa noudattavat yhtenäistä rakennetta:
Single repossa `<komponentti>` jätetään pois.
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
Gitea Actionsin `upload-artifact` jättää pysyvän tiedoston. Artifakteja ei käytetä
@@ -257,8 +368,18 @@ Pakkaa ja pushee Helm-chartin OCI-registryyn. Käyttää `alpine/helm`-konttia.
```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
@@ -270,20 +391,171 @@ helm-build-push:
with:
env_json: ${{ needs.load-config.outputs.env_json }}
version: ${{ needs.check-version.outputs.version }}
# chart_path: '.' # oletus, vaihda jos Chart.yaml on alihakemistossa
chart_path: platform-helm
# extra_dependency_paths: subchart-a,subchart-b # tarvittaessa
```
**Vanhentunut käytäntö:** Nykyinen `helm-build-push.yml` asentaa node.js:n
lennossa `apk add --no-cache nodejs` ennen checkouttia — tämä rikkoo
Offline Container -vaatimusta (4.1).
`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.
**Korjaustoimenpide:** Rakenna custom CI-kontti `ci-container-build`-skillillä
jossa on helm + nodejs + git (katso pre-cache-esimerkit `REFERENCE.md`:stä),
päivitä workflow'n `container: image:` osoittamaan omaan konttiin, ja poista
runtime-apk.
**`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
+217
View File
@@ -0,0 +1,217 @@
---
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
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 || '' }}
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.
### 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`) |
### 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`, 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.
+20 -19
View File
@@ -3,6 +3,7 @@
setup() {
source tests/helpers/mock-api.sh
export DISPATCH_POLL_INTERVAL="0.1"
export DISPATCH_ID="test123"
}
teardown() {
@@ -12,8 +13,7 @@ teardown() {
@test "dispatch succeeds: POST 201, poll running x3 then success → exit 0" {
mock_set_sequence '[
{"code":201},
{"code":200,"body":{"workflow_runs":[{"id":1,"status":"running"}]}},
{"code":200,"body":{"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":"success"}}
@@ -26,7 +26,7 @@ teardown() {
@test "dispatch: poll returns failure conclusion → exit 1" {
mock_set_sequence '[
{"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":"completed","conclusion":"failure"}}
]'
@@ -38,7 +38,7 @@ teardown() {
@test "dispatch: poll returns cancelled conclusion → exit 1" {
mock_set_sequence '[
{"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":"completed","conclusion":"cancelled"}}
]'
@@ -47,18 +47,18 @@ teardown() {
[ "$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 '[
{"code":201},
{"code":200,"body":{"workflow_runs":[{"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":"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":{"workflow_runs":[]}},
{"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"workflow_runs":[]}}
]'
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"
@@ -77,7 +77,7 @@ teardown() {
@test "POST dispatch is called with correct URL and payload" {
mock_set_sequence '[
{"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"}}
]'
mock_start
@@ -91,6 +91,7 @@ teardown() {
[[ "$body" == *'"ref":"main"'* ]]
[[ "$body" == *'"inputs"'* ]]
[[ "$body" == *'"version":"1.2.3"'* ]]
[[ "$body" == *'"dispatch_id":"test123"'* ]]
}
@test "missing gitea_api_url argument → exit 1 with error message" {
@@ -120,15 +121,15 @@ teardown() {
[ "$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 '[
{"code":201},
{"code":200,"body":{"workflow_runs":[]}}
]'
mock_start
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" '{}' "http://localhost:18080" "test-token-abc123"
[ "$status" -eq 1 ]
[[ "$output" == *"ERROR"* ]]
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" '{}' "http://localhost:18080" "test-token-abc123" "0.001"
[ "$status" -eq 124 ]
[[ "$output" == *"ERROR"* || "$output" == *"Timeout"* ]]
}
@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
@@ -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',
stdio: ['pipe', 'pipe', 'pipe'],
});
return { status: 0, stdout: out };
return { status: 0, stdout: out, stderr: '' };
} catch (e) {
return { status: e.status, stdout: e.stdout || '', stderr: e.stderr || '' };
}
@@ -54,7 +54,7 @@ function setupMock(seqJson) {
}
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 () {
@@ -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 () {
setupMock(JSON.stringify([
{ 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' } },
]));
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 () {
setupMock(JSON.stringify([
{ 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' } },
]));
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 () {
setupMock(JSON.stringify([
{ code: 201 },
{ code: 200, body: { workflow_runs: [{ 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: { workflow_runs: [] } },
{ code: 200, body: { workflow_runs: [] } },
{ code: 200, body: { workflow_runs: [] } },
{ 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.dispatchStderr = r.stderr;
});
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)}`);
}
});
+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
+1 -1
View File
@@ -46,7 +46,7 @@ mock_clear_sequence() {
mock_start() {
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"
MOCK_CONFIG_FILE=$(mktemp)
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env bash
echo "yq $*" >> "${YQ_CALLS_FILE:-/dev/null}"