From 86e73d87d3a3f350da034afe3b752759d4e967b1 Mon Sep 17 00:00:00 2001 From: moilanik Date: Sun, 21 Jun 2026 15:44:31 +0300 Subject: [PATCH 01/23] gitops init --- docs/config-model.md | 2 +- docs/workflows.md | 50 ++++- scripts/gitops-update.sh | 46 +++++ tests/features/gitops-update.feature | 15 ++ .../step_definitions/gitops-update.steps.js | 71 +++++++ tests/gitops-update.bats | 179 ++++++++++++++++++ tests/helpers/git | 27 +++ tests/helpers/yq | 2 + 8 files changed, 388 insertions(+), 4 deletions(-) create mode 100755 scripts/gitops-update.sh create mode 100644 tests/features/gitops-update.feature create mode 100644 tests/features/step_definitions/gitops-update.steps.js create mode 100644 tests/gitops-update.bats create mode 100755 tests/helpers/git create mode 100755 tests/helpers/yq diff --git a/docs/config-model.md b/docs/config-model.md index 0f7503f..8288206 100644 --- a/docs/config-model.md +++ b/docs/config-model.md @@ -62,7 +62,7 @@ organization/repository secrets -mekanismissa ja välitetään workflowlle | 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` | | `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` | diff --git a/docs/workflows.md b/docs/workflows.md index 1ab629e..a9de492 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -182,7 +182,51 @@ 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. + +**Input-ympäristömuuttujat:** + +| 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-konfiguraatiorepo (esim. `org/app-gitops`) | +| `GITEA_API_URL` | Kyllä | Gitean API-URL | +| `GITEA_TOKEN` | Kyllä | Gitea API-token | +| `GITOPS_BRANCH` | Ei | GitOps-repon branch (oletus `main`) | + +**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. Commit + push `[skip ci]` +6. Asettaa commit-statuksen: code-repoon (gitops-konteksti) ja GitOps-repoon (source-konteksti) + +**Esimerkki dispatchistä:** +```yaml +- name: Update GitOps + run: | + export INPUT_FILE=dev/Chart.yaml + export YQ_TPL='(.version) = "{{VERSION}}"' + export VERSION=0.2.3 + export SOURCE_REPO=org/app + export SOURCE_COMMIT=${{ github.sha }} + export GITOPS_REPO=org/app-gitops + bash scripts/gitops-update.sh + env: + GITEA_API_URL: ${{ vars.GITEA_API_URL }} + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} +``` + +--- diff --git a/scripts/gitops-update.sh b/scripts/gitops-update.sh new file mode 100755 index 0000000..bf7a0ad --- /dev/null +++ b/scripts/gitops-update.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -euo pipefail + +INPUT_FILE="${INPUT_FILE:?}" +YQ_TPL="${YQ_TPL:?}" +VERSION="${VERSION:?}" +SOURCE_REPO="${SOURCE_REPO:?}" +SOURCE_COMMIT="${SOURCE_COMMIT:?}" +GITOPS_REPO="${GITOPS_REPO:?}" +GITEA_TOKEN="${GITEA_TOKEN:?}" +GITEA_API_URL="${GITEA_API_URL:?}" +GITOPS_BRANCH="${GITOPS_BRANCH:-main}" + +_gitops_substitute() { + echo "$1" | sed "s/{{VERSION}}/$2/g" +} + +YQ_EXPR=$(_gitops_substitute "${YQ_TPL}" "${VERSION}") + +GITEA_HOST=$(echo "${GITEA_API_URL}" | sed 's|https://||' | sed 's|http://||') +CLONE_URL="https://${GITEA_TOKEN}@${GITEA_HOST}/${GITOPS_REPO}.git" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +_gitops_update() { + CLONE_DIR=$(mktemp -d) + git clone "$CLONE_URL" "$CLONE_DIR" + cd "$CLONE_DIR" + yq eval -i "$YQ_EXPR" "$INPUT_FILE" + git add "$INPUT_FILE" + git commit -m "[skip ci] gitops: update version to ${VERSION}" + GITOPS_SHA=$(git rev-parse HEAD) + git push + ROOT_REPO="${SOURCE_REPO}" ROOT_COMMIT="${SOURCE_COMMIT}" \ + bash "${SCRIPT_DIR}/report-status.sh" success "GitOps updated to ${VERSION}" \ + "gitops/${SOURCE_REPO}" "" \ + "${GITEA_API_URL}/${GITOPS_REPO}/commits/${GITOPS_SHA}" + ROOT_REPO="${GITOPS_REPO}" ROOT_COMMIT="${GITOPS_SHA}" \ + bash "${SCRIPT_DIR}/report-status.sh" success "Source build" \ + "source/${SOURCE_REPO}" "" \ + "${GITEA_API_URL}/${SOURCE_REPO}/commits/${SOURCE_COMMIT}" +} + +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + _gitops_update +fi diff --git a/tests/features/gitops-update.feature b/tests/features/gitops-update.feature new file mode 100644 index 0000000..303647c --- /dev/null +++ b/tests/features/gitops-update.feature @@ -0,0 +1,15 @@ +Feature: GitOps version update + As a developer + I want to automatically update version references in a GitOps repo + So that deployment is triggered with the correct artifact version + + Background: + Given a project repository exists in Gitea + And a commit has been pushed to the repository + + @mock @real + Scenario: GitOps repo receives version bump dispatch + When a build completes successfully and dispatches a GitOps update + Then the GitOps repo has a new commit with the updated version + And the code repo shows a gitops status link to the GitOps commit + And the GitOps repo shows a source status link to the code commit diff --git a/tests/features/step_definitions/gitops-update.steps.js b/tests/features/step_definitions/gitops-update.steps.js new file mode 100644 index 0000000..71f71cf --- /dev/null +++ b/tests/features/step_definitions/gitops-update.steps.js @@ -0,0 +1,71 @@ +const { execSync } = require('child_process'); +const { 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'); + +function bash(cmd) { + try { + const out = execSync(`bash -c '${cmd}'`, { + cwd: PROJECT_ROOT, + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + }); + return { status: 0, stdout: out }; + } catch (e) { + return { status: e.status, stdout: e.stdout || '', stderr: e.stderr || '' }; + } +} + +function getFirstBody() { + return bash(`source "${MOCK_SCRIPT}" && _get_request_file && mock_get_first_request_body`).stdout.trim(); +} + +function getFirstPath() { + return bash(`source "${MOCK_SCRIPT}" && _get_request_file && mock_get_first_request_path`).stdout.trim(); +} + +function getLastBody() { + return bash(`source "${MOCK_SCRIPT}" && _get_request_file && mock_get_request_body`).stdout.trim(); +} + +function getLastPath() { + return bash(`source "${MOCK_SCRIPT}" && _get_request_file && mock_get_request_path`).stdout.trim(); +} + +When('a build completes successfully and dispatches a GitOps update', function () { + const 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', + ].join(' '); + const r = bash(`${env} bash "${GITOPS_SCRIPT}"`); + if (r.status !== 0) throw new Error(`Expected exit 0, got ${r.status}: ${r.stderr}`); +}); + +Then('the GitOps repo has a new commit with the updated version', function () { + const out = bash(`git -C /tmp log --oneline -1 2>/dev/null || echo "no-git-log"`); +}); + +Then('the code repo shows a gitops status link to the GitOps commit', function () { + const body = getFirstBody(); + if (!body.includes('"state":"success"')) throw new Error('Expected success status'); + if (!body.includes('"context":"gitops/niko/app"')) throw new Error('Expected gitops context'); + const pathStr = getFirstPath(); + if (!pathStr.includes('/repos/niko/app/statuses/')) throw new Error('Expected source repo status path'); +}); + +Then('the GitOps repo shows a source status link to the code commit', function () { + const body = getLastBody(); + if (!body.includes('"state":"success"')) throw new Error('Expected success status'); + if (!body.includes('"context":"source/niko/app"')) throw new Error('Expected source context'); + const pathStr = getLastPath(); + if (!pathStr.includes('/repos/niko/app-gitops/statuses/')) throw new Error('Expected gitops repo status path'); +}); diff --git a/tests/gitops-update.bats b/tests/gitops-update.bats new file mode 100644 index 0000000..30d163f --- /dev/null +++ b/tests/gitops-update.bats @@ -0,0 +1,179 @@ +#!/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 "two commit-status calls: code-repo and gitops-repo" { + 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 ] + path1=$(mock_get_first_request_path) + body1=$(mock_get_first_request_body) + [[ "$path1" == *"/repos/niko/app/statuses/"* ]] + [[ "$body1" == *'"context":"gitops/niko/app"'* ]] + path2=$(mock_get_request_path) + body2=$(mock_get_request_body) + [[ "$path2" == *"/repos/niko/app-gitops/statuses/"* ]] + [[ "$body2" == *'"context":"source/niko/app"'* ]] + 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"* ]] +} diff --git a/tests/helpers/git b/tests/helpers/git new file mode 100755 index 0000000..37ac5d9 --- /dev/null +++ b/tests/helpers/git @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +echo "git $*" >> "${GIT_CALLS_FILE:-/dev/null}" + +case "$1" in + clone) + TARGET_DIR="${@: -1}" + mkdir -p "$TARGET_DIR" + cd "$TARGET_DIR" + git init --initial-branch=main 2>/dev/null + git config user.email "mock@test.com" + git config user.name "Mock Test" + mkdir -p "$(dirname "$INPUT_FILE")" + echo 'version: 0.1.0' > "$INPUT_FILE" + git add -A 2>/dev/null + git commit -m "initial" 2>/dev/null + echo "Cloning into '$TARGET_DIR'..." + ;; + add|commit|push|config|init) + ;; + rev-parse) + echo "mock-sha-9876543210fedcba9876543210fedcba98765432" + ;; + *) + echo "git: unknown command: $*" >&2 + exit 1 + ;; +esac diff --git a/tests/helpers/yq b/tests/helpers/yq new file mode 100755 index 0000000..b23a267 --- /dev/null +++ b/tests/helpers/yq @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +echo "yq $*" >> "${YQ_CALLS_FILE:-/dev/null}" -- 2.52.0 From 5c9df73a6686ba3f97275dccc373951c7795c7cd Mon Sep 17 00:00:00 2001 From: moilanik Date: Sun, 21 Jun 2026 16:10:41 +0300 Subject: [PATCH 02/23] =?UTF-8?q?cucumber=20testej=C3=A4=20lis=C3=A4=C3=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/gitops-update.sh | 104 +++++++++---- tests/features/gitops-update.feature | 49 ++++-- .../step_definitions/gitops-update.steps.js | 140 ++++++++++++++---- tests/helpers/git | 7 + 4 files changed, 232 insertions(+), 68 deletions(-) diff --git a/scripts/gitops-update.sh b/scripts/gitops-update.sh index bf7a0ad..3537d04 100755 --- a/scripts/gitops-update.sh +++ b/scripts/gitops-update.sh @@ -1,45 +1,87 @@ #!/usr/bin/env bash set -euo pipefail -INPUT_FILE="${INPUT_FILE:?}" -YQ_TPL="${YQ_TPL:?}" -VERSION="${VERSION:?}" -SOURCE_REPO="${SOURCE_REPO:?}" -SOURCE_COMMIT="${SOURCE_COMMIT:?}" -GITOPS_REPO="${GITOPS_REPO:?}" -GITEA_TOKEN="${GITEA_TOKEN:?}" -GITEA_API_URL="${GITEA_API_URL:?}" -GITOPS_BRANCH="${GITOPS_BRANCH:-main}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +_gitops_fail() { + local MSG="${1:-GitOps update failed}" + echo "[ERROR] ${MSG}" >&2 + + if [ -n "${SOURCE_REPO:-}" ] && [ -n "${SOURCE_COMMIT:-}" ] && \ + [ -n "${GITEA_API_URL:-}" ] && [ -n "${GITEA_TOKEN:-}" ]; then + local GITOPS_URL="${GITEA_API_URL}/${GITOPS_REPO:-unknown}/commits/${GITOPS_SHA:-unknown}" + ROOT_REPO="${SOURCE_REPO}" ROOT_COMMIT="${SOURCE_COMMIT}" \ + GITEA_API_URL="${GITEA_API_URL}" GITEA_TOKEN="${GITEA_TOKEN}" \ + bash "${SCRIPT_DIR}/report-status.sh" failure "${MSG}" \ + "gitops/${SOURCE_REPO}" "" "${GITOPS_URL}" 2>/dev/null || true + + if [ -n "${GITOPS_REPO:-}" ] && [ -n "${GITOPS_SHA:-}" ]; then + local SOURCE_URL="${GITEA_API_URL}/${SOURCE_REPO}/commits/${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 "${MSG}" \ + "source/${SOURCE_REPO}" "" "${SOURCE_URL}" 2>/dev/null || true + fi + 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 GITOPS_URL="${GITEA_API_URL}/${GITOPS_REPO}/commits/${GITOPS_SHA}" + local SOURCE_URL="${GITEA_API_URL}/${SOURCE_REPO}/commits/${SOURCE_COMMIT}" + + ROOT_REPO="${SOURCE_REPO}" ROOT_COMMIT="${SOURCE_COMMIT}" \ + GITEA_API_URL="${GITEA_API_URL}" GITEA_TOKEN="${GITEA_TOKEN}" \ + bash "${SCRIPT_DIR}/report-status.sh" success "GitOps updated to ${VERSION}" \ + "gitops/${SOURCE_REPO}" "" "${GITOPS_URL}" + + 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 "Source build" \ + "source/${SOURCE_REPO}" "" "${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}" + git 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="https://${GITEA_TOKEN}@${GITEA_HOST}/${GITOPS_REPO}.git" - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -_gitops_update() { - CLONE_DIR=$(mktemp -d) - git clone "$CLONE_URL" "$CLONE_DIR" - cd "$CLONE_DIR" - yq eval -i "$YQ_EXPR" "$INPUT_FILE" - git add "$INPUT_FILE" - git commit -m "[skip ci] gitops: update version to ${VERSION}" - GITOPS_SHA=$(git rev-parse HEAD) - git push - ROOT_REPO="${SOURCE_REPO}" ROOT_COMMIT="${SOURCE_COMMIT}" \ - bash "${SCRIPT_DIR}/report-status.sh" success "GitOps updated to ${VERSION}" \ - "gitops/${SOURCE_REPO}" "" \ - "${GITEA_API_URL}/${GITOPS_REPO}/commits/${GITOPS_SHA}" - ROOT_REPO="${GITOPS_REPO}" ROOT_COMMIT="${GITOPS_SHA}" \ - bash "${SCRIPT_DIR}/report-status.sh" success "Source build" \ - "source/${SOURCE_REPO}" "" \ - "${GITEA_API_URL}/${SOURCE_REPO}/commits/${SOURCE_COMMIT}" -} +CLONE_URL="${GITOPS_CLONE_URL:-https://${GITEA_TOKEN}@${GITEA_HOST}/${GITOPS_REPO}.git}" if [ "${BASH_SOURCE[0]}" = "${0}" ]; then _gitops_update diff --git a/tests/features/gitops-update.feature b/tests/features/gitops-update.feature index 303647c..409df30 100644 --- a/tests/features/gitops-update.feature +++ b/tests/features/gitops-update.feature @@ -1,15 +1,44 @@ -Feature: GitOps version update - As a developer - I want to automatically update version references in a GitOps repo - So that deployment is triggered with the correct artifact version +Feature: GitOps update + As a GitOps repository + I want to update version references and report results back 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 @real - Scenario: GitOps repo receives version bump dispatch - When a build completes successfully and dispatches a GitOps update - Then the GitOps repo has a new commit with the updated version - And the code repo shows a gitops status link to the GitOps commit - And the GitOps repo shows a source status link to the code commit + @mock + Scenario: Not enough env vars — caller commit gets failure status + Given insufficient environment variables are provided for the GitOps update + When the GitOps update script runs + Then the caller commit shows a failure status with the missing variable name + And the script exits with error + + @mock + Scenario: GitOps job fails — caller commit gets failure status + Given the GitOps repository clone will fail + When the GitOps update script runs + Then the caller commit shows a failure status + And the script exits with error + + @mock + Scenario: Everything succeeds — caller and GitOps get success + Given a valid GitOps update dispatch + When the GitOps update script runs + Then the script exits successfully + And the caller commit shows a success status with a link to the GitOps commit + And the GitOps repo commit shows a success status with a link to the caller + + @mock + Scenario: GitOps push fails — both repos get 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 caller commit shows a failure status + And the GitOps repo commit shows a failure status linking to the caller + + @mock + Scenario: GitOps update succeeds — this repo commit status links to caller + Given a valid GitOps update dispatch + When the GitOps update script runs + Then the GitOps repo commit shows a source context status linking to the caller commit diff --git a/tests/features/step_definitions/gitops-update.steps.js b/tests/features/step_definitions/gitops-update.steps.js index 71f71cf..f466833 100644 --- a/tests/features/step_definitions/gitops-update.steps.js +++ b/tests/features/step_definitions/gitops-update.steps.js @@ -1,10 +1,26 @@ const { execSync } = require('child_process'); -const { When, Then } = require('@cucumber/cucumber'); +const { Before, 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 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}`; +}); function bash(cmd) { try { @@ -13,7 +29,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 || '' }; } @@ -35,37 +51,107 @@ function getLastPath() { return bash(`source "${MOCK_SCRIPT}" && _get_request_file && mock_get_request_path`).stdout.trim(); } -When('a build completes successfully and dispatches a GitOps update', function () { - const 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', - ].join(' '); - const r = bash(`${env} bash "${GITOPS_SCRIPT}"`); - if (r.status !== 0) throw new Error(`Expected exit 0, got ${r.status}: ${r.stderr}`); +function requestCount() { + const rf = bash(`source "${MOCK_SCRIPT}" && _get_request_file`).stdout.trim(); + const count = bash(`grep -c '^POST ' "${rf}" 2>/dev/null || echo 0`).stdout.trim(); + return parseInt(count, 10) || 0; +} + +function runScript(envOverrides) { + const env = { ...BASE_ENV, ...envOverrides }; + const envStr = Object.entries(env) + .map(([k, v]) => { + const escaped = v.replace(/'/g, "'\\''"); + return `${k}='${escaped}'`; + }) + .join(' '); + return bash(`${envStr} bash "${GITOPS_SCRIPT}"`); +} + +Given('insufficient environment variables are provided for the GitOps update', function () { + this.missingVar = 'INPUT_FILE'; + this.envOverrides = {}; + this.envOverrides.INPUT_FILE = ''; }); -Then('the GitOps repo has a new commit with the updated version', function () { - const out = bash(`git -C /tmp log --oneline -1 2>/dev/null || echo "no-git-log"`); +Given('the GitOps repository clone will fail', function () { + this.envOverrides = { GIT_MOCK_FAIL: '1' }; }); -Then('the code repo shows a gitops status link to the GitOps commit', function () { +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' }; +}); + +When('the GitOps update script runs', function () { + this.result = runScript(this.envOverrides || {}); +}); + +Then('the caller commit shows a failure status with the missing variable name', function () { const body = getFirstBody(); - if (!body.includes('"state":"success"')) throw new Error('Expected success status'); - if (!body.includes('"context":"gitops/niko/app"')) throw new Error('Expected gitops context'); + if (!body.includes('"state":"failure"')) throw new Error(`Expected failure state, body: ${body.substring(0,200)}`); + if (!body.includes('"context":"gitops/niko/app"')) throw new Error(`Expected gitops context, body: ${body.substring(0,200)}`); + if (!body.includes(`"description":"${this.missingVar} is required`)) { + throw new Error(`Expected description mentioning ${this.missingVar}, body: ${body.substring(0,200)}`); + } const pathStr = getFirstPath(); - if (!pathStr.includes('/repos/niko/app/statuses/')) throw new Error('Expected source repo status path'); + if (!pathStr.includes('/repos/niko/app/statuses/')) throw new Error(`Expected source repo path, got: ${pathStr}`); }); -Then('the GitOps repo shows a source status link to the code commit', function () { - const body = getLastBody(); - if (!body.includes('"state":"success"')) throw new Error('Expected success status'); - if (!body.includes('"context":"source/niko/app"')) throw new Error('Expected source context'); - const pathStr = getLastPath(); - if (!pathStr.includes('/repos/niko/app-gitops/statuses/')) throw new Error('Expected gitops repo status path'); +Then('the caller commit shows a failure status', function () { + const body = getFirstBody(); + if (!body.includes('"state":"failure"')) throw new Error(`Expected failure state, body: ${body.substring(0,200)}`); + if (!body.includes('"context":"gitops/niko/app"')) throw new Error(`Expected gitops context, body: ${body.substring(0,200)}`); + const pathStr = getFirstPath(); + if (!pathStr.includes('/repos/niko/app/statuses/')) throw new Error(`Expected source repo path, got: ${pathStr}`); +}); + +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}`); +}); + +Then('the script exits successfully', function () { + if (this.result.status !== 0) throw new Error(`Expected exit 0, got ${this.result.status}: ${this.result.stderr}`); +}); + +Then('the caller commit shows a success status with a link to the GitOps commit', function () { + const body = getFirstBody(); + if (!body.includes('"state":"success"')) throw new Error(`Expected success state, body: ${body.substring(0,200)}`); + if (!body.includes('"context":"gitops/niko/app"')) throw new Error(`Expected gitops context, body: ${body.substring(0,200)}`); + if (!body.includes('niko/app-gitops/commits/')) throw new Error(`Expected link to GitOps commit, body: ${body.substring(0,200)}`); + const pathStr = getFirstPath(); + if (!pathStr.includes('/repos/niko/app/statuses/')) throw new Error(`Expected source repo path, got: ${pathStr}`); +}); + +Then('the GitOps repo commit shows a success status with a link to the caller', function () { + if (requestCount() < 2) throw new Error(`Expected at least 2 requests, got ${requestCount()}`); + const body = getLastBody(); + if (!body.includes('"state":"success"')) throw new Error(`Expected success state, body: ${body.substring(0,200)}`); + if (!body.includes('"context":"source/niko/app"')) throw new Error(`Expected source context, body: ${body.substring(0,200)}`); + if (!body.includes('niko/app/commits/abc123def456')) throw new Error(`Expected link to caller commit, body: ${body.substring(0,200)}`); + const pathStr = getLastPath(); + 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', function () { + if (requestCount() < 2) throw new Error(`Expected at least 2 requests, got ${requestCount()}`); + const body = getLastBody(); + if (!body.includes('"state":"failure"')) throw new Error(`Expected failure state, body: ${body.substring(0,200)}`); + if (!body.includes('"context":"source/niko/app"')) throw new Error(`Expected source context, body: ${body.substring(0,200)}`); + if (!body.includes('niko/app/commits/abc123def456')) throw new Error(`Expected link to caller commit, body: ${body.substring(0,200)}`); + const pathStr = getLastPath(); + if (!pathStr.includes('/repos/niko/app-gitops/statuses/')) throw new Error(`Expected gitops repo path, got: ${pathStr}`); +}); + +Then('the GitOps repo commit shows a source context status linking to the caller commit', function () { + if (requestCount() < 2) throw new Error(`Expected at least 2 requests, got ${requestCount()}`); + const body = getLastBody(); + if (!body.includes('"state":"success"')) throw new Error(`Expected success state, body: ${body.substring(0,200)}`); + if (!body.includes('"context":"source/niko/app"')) throw new Error(`Expected source context, body: ${body.substring(0,200)}`); + if (!body.includes('niko/app/commits/abc123def456')) throw new Error(`Expected link to caller, body: ${body.substring(0,200)}`); + const pathStr = getLastPath(); + if (!pathStr.includes('/repos/niko/app-gitops/statuses/')) throw new Error(`Expected gitops repo path, got: ${pathStr}`); }); diff --git a/tests/helpers/git b/tests/helpers/git index 37ac5d9..3afe3ed 100755 --- a/tests/helpers/git +++ b/tests/helpers/git @@ -1,6 +1,13 @@ #!/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 + case "$1" in clone) TARGET_DIR="${@: -1}" -- 2.52.0 From 1385afcca61f3f4055ef6c3124d95bfc450e5593 Mon Sep 17 00:00:00 2001 From: moilanik Date: Sun, 21 Jun 2026 17:14:27 +0300 Subject: [PATCH 03/23] teet --- .../step_definitions/gitops-update.steps.js | 78 ++++++++++++------- tests/helpers/git | 11 +-- tests/helpers/mock-api.sh | 2 +- 3 files changed, 51 insertions(+), 40 deletions(-) diff --git a/tests/features/step_definitions/gitops-update.steps.js b/tests/features/step_definitions/gitops-update.steps.js index f466833..e690433 100644 --- a/tests/features/step_definitions/gitops-update.steps.js +++ b/tests/features/step_definitions/gitops-update.steps.js @@ -1,11 +1,12 @@ -const { execSync } = require('child_process'); -const { Before, When, Then } = require('@cucumber/cucumber'); +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', @@ -20,58 +21,75 @@ const BASE_ENV = { 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', { stdio: 'ignore' }); } catch (_) {} }); function bash(cmd) { - try { - const out = execSync(`bash -c '${cmd}'`, { - cwd: PROJECT_ROOT, - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - }); - return { status: 0, stdout: out, stderr: '' }; - } catch (e) { - return { status: e.status, stdout: e.stdout || '', stderr: e.stderr || '' }; - } + 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(`source "${MOCK_SCRIPT}" && _get_request_file && mock_get_first_request_body`).stdout.trim(); + return bash(`grep -A1 '^POST ' "${REQ_FILE}" 2>/dev/null | head -2 | tail -1 || echo ""`).stdout.trim(); } function getFirstPath() { - return bash(`source "${MOCK_SCRIPT}" && _get_request_file && mock_get_first_request_path`).stdout.trim(); + return bash(`grep '^POST ' "${REQ_FILE}" 2>/dev/null | head -1 | awk '{print $2}' || echo ""`).stdout.trim(); } function getLastBody() { - return bash(`source "${MOCK_SCRIPT}" && _get_request_file && mock_get_request_body`).stdout.trim(); + return bash(`grep -A1 '^POST ' "${REQ_FILE}" 2>/dev/null | grep -v '^POST ' | tail -1 || echo ""`).stdout.trim(); } function getLastPath() { - return bash(`source "${MOCK_SCRIPT}" && _get_request_file && mock_get_request_path`).stdout.trim(); + return bash(`grep '^POST ' "${REQ_FILE}" 2>/dev/null | tail -1 | awk '{print $2}' || echo ""`).stdout.trim(); } function requestCount() { - const rf = bash(`source "${MOCK_SCRIPT}" && _get_request_file`).stdout.trim(); - const count = bash(`grep -c '^POST ' "${rf}" 2>/dev/null || echo 0`).stdout.trim(); - return parseInt(count, 10) || 0; + return parseInt(bash(`grep -c '^POST ' "${REQ_FILE}" 2>/dev/null || echo 0`).stdout.trim(), 10) || 0; } function runScript(envOverrides) { const env = { ...BASE_ENV, ...envOverrides }; - const envStr = Object.entries(env) - .map(([k, v]) => { - const escaped = v.replace(/'/g, "'\\''"); - return `${k}='${escaped}'`; - }) - .join(' '); - return bash(`${envStr} bash "${GITOPS_SCRIPT}"`); + 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.missingVar = 'INPUT_FILE'; - this.envOverrides = {}; - this.envOverrides.INPUT_FILE = ''; + this.envOverrides = { INPUT_FILE: '' }; }); Given('the GitOps repository clone will fail', function () { @@ -110,11 +128,11 @@ Then('the caller commit shows a failure status', function () { }); 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}`); + 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}`); + if (this.result.status !== 0) throw new Error(`Expected exit 0, got ${this.result.status}: ${this.result.stderr.substring(0,200)}`); }); Then('the caller commit shows a success status with a link to the GitOps commit', function () { diff --git a/tests/helpers/git b/tests/helpers/git index 3afe3ed..207c5a2 100755 --- a/tests/helpers/git +++ b/tests/helpers/git @@ -11,15 +11,8 @@ fi case "$1" in clone) TARGET_DIR="${@: -1}" - mkdir -p "$TARGET_DIR" - cd "$TARGET_DIR" - git init --initial-branch=main 2>/dev/null - git config user.email "mock@test.com" - git config user.name "Mock Test" - mkdir -p "$(dirname "$INPUT_FILE")" - echo 'version: 0.1.0' > "$INPUT_FILE" - git add -A 2>/dev/null - git commit -m "initial" 2>/dev/null + 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 --git a/tests/helpers/mock-api.sh b/tests/helpers/mock-api.sh index e6ad4b5..826ccb1 100644 --- a/tests/helpers/mock-api.sh +++ b/tests/helpers/mock-api.sh @@ -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.52.0 From 9105675591b3ec156f611250819b979c7d0a3781 Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 04:17:07 +0300 Subject: [PATCH 04/23] poc --- .gitea/workflows/poc-dispatch.yml | 20 ++++ scripts/poc-dispatch-match.sh | 189 ++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 .gitea/workflows/poc-dispatch.yml create mode 100755 scripts/poc-dispatch-match.sh diff --git a/.gitea/workflows/poc-dispatch.yml b/.gitea/workflows/poc-dispatch.yml new file mode 100644 index 0000000..3037826 --- /dev/null +++ b/.gitea/workflows/poc-dispatch.yml @@ -0,0 +1,20 @@ +name: POC GitOps Dispatch +run-name: "POC (${{ inputs.dispatch_id }})" +on: + push: + branches: + - feature/gitops + workflow_dispatch: + inputs: + dispatch_id: + required: true + type: string + type: + required: true + type: string + +jobs: + echo: + runs-on: ubuntu-latest + steps: + - run: echo "POC dispatch_id=${{ inputs.dispatch_id }} type=${{ inputs.type }}" diff --git a/scripts/poc-dispatch-match.sh b/scripts/poc-dispatch-match.sh new file mode 100755 index 0000000..e182f6e --- /dev/null +++ b/scripts/poc-dispatch-match.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +set -euo pipefail + +# +# POC: testaa display_title-matchingia dispatchatuille workflow runeille. +# +# Ajaa kaksi testiä: +# A — dispatchaa gitea-ci-library:n poc-dispatch.yml (tämä repo, feature-haara) +# B — dispatchaa gitea-ci-gitops-tests:n gitops-service.yaml +# +# Käyttö: +# export GITEA_API_URL=https://gitea.app.keskikuja.site +# export GITEA_TOKEN=... +# bash scripts/poc-dispatch-match.sh +# + +GITEA_API_URL="${GITEA_API_URL:?GITEA_API_URL is required}" +GITEA_TOKEN="${GITEA_TOKEN:?GITEA_TOKEN is required}" +BRANCH="${BRANCH:-feature/gitops}" +TIMEOUT_SECONDS="${TIMEOUT_SECONDS:-60}" +POLL_INTERVAL="${POLL_INTERVAL:-5}" +GITOPS_REPO="${GITOPS_REPO:-niko/gitea-ci-gitops-tests}" +GITOPS_WORKFLOW="${GITOPS_WORKFLOW:-gitops-service.yaml}" +LIB_REPO="${LIB_REPO:-niko/gitea-ci-library}" +LIB_WORKFLOW="${LIB_WORKFLOW:-poc-dispatch.yml}" + +PASS=0 +FAIL=0 + +_ts() { + date -u +%Y-%m-%dT%H:%M:%SZ +} + +_report() { + local label="$1" status="$2" detail="$3" + if [ "$status" = "PASS" ]; then + PASS=$((PASS + 1)) + echo " ✅ $label: $detail" + else + FAIL=$((FAIL + 1)) + echo " ❌ $label: $detail" + fi +} + +_dispatch_and_poll() { + local label="$1" target_repo="$2" workflow_file="$3" ref="$4" dispatch_id="$5" + local inputs_json="$6" + + echo "" + echo "=== Test: $label ===" + echo " target: $target_repo" + echo " workflow: $workflow_file" + echo " ref: $ref" + echo " dispatch_id: $dispatch_id" + + # 1. Ennen dispatchia: ota snapshot viimeisimmästä run_number:sta + local before_resp before_run before_count + before_resp=$(curl -s --connect-timeout 5 --max-time 10 \ + "$GITEA_API_URL/api/v1/repos/$target_repo/actions/runs?event=workflow_dispatch&limit=1" \ + -H "Authorization: token $GITEA_TOKEN") + + before_run=$(echo "$before_resp" | jq -r '.workflow_runs[0].run_number // 0') + before_count=$(echo "$before_resp" | jq -r '.total_count // 0') + echo " before: $before_count runs, latest run_number=$before_run" + + # 2. Dispatch + local dispatch_url="$GITEA_API_URL/api/v1/repos/$target_repo/actions/workflows/$workflow_file/dispatches" + local dispatch_body + dispatch_body=$(jq -nc --arg ref "$ref" --argjson inputs "$inputs_json" '{ref: $ref, inputs: $inputs}') + + local dispatch_code + dispatch_code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 --max-time 10 \ + -X POST "$dispatch_url" \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$dispatch_body") + + if [ "$dispatch_code" != "201" ] && [ "$dispatch_code" != "204" ]; then + _report "$label" "FAIL" "Dispatch failed with HTTP $dispatch_code" + return + fi + echo " dispatch: HTTP $dispatch_code — started" + + # 3. Pollaa: etsi run jossa display_title sisältää dispatch_id:n + local start_time elapsed found="" + start_time=$(date +%s) + + while true; do + elapsed=$(( $(date +%s) - start_time )) + if [ "$elapsed" -ge "$TIMEOUT_SECONDS" ]; then + break + fi + + local runs_resp found_id found_display found_status found_run_num + runs_resp=$(curl -s --connect-timeout 5 --max-time 10 \ + "$GITEA_API_URL/api/v1/repos/$target_repo/actions/runs?event=workflow_dispatch&limit=20" \ + -H "Authorization: token $GITEA_TOKEN") + + found_id=$(echo "$runs_resp" | jq -r --arg id "$dispatch_id" \ + '[.workflow_runs[] | select(.display_title | contains($id))] | .[0].id // empty') + + if [ -n "$found_id" ] && [ "$found_id" != "null" ]; then + found_display=$(echo "$runs_resp" | jq -r --arg id "$dispatch_id" \ + '[.workflow_runs[] | select(.display_title | contains($id))] | .[0].display_title // "?"') + found_status=$(echo "$runs_resp" | jq -r --arg id "$dispatch_id" \ + '[.workflow_runs[] | select(.display_title | contains($id))] | .[0].status // "?"') + found_run_num=$(echo "$runs_resp" | jq -r --arg id "$dispatch_id" \ + '[.workflow_runs[] | select(.display_title | contains($id))] | .[0].run_number // "?"') + + echo " found: id=$found_id run_number=$found_run_num status=$found_status display_title=\"$found_display\" (after ${elapsed}s)" + + # Odota että run on completed + local poll_url="$GITEA_API_URL/api/v1/repos/$target_repo/actions/runs/$found_id" + while [ "$elapsed" -lt "$TIMEOUT_SECONDS" ]; do + local run_resp run_status run_conclusion + run_resp=$(curl -s --connect-timeout 5 --max-time 10 \ + "$poll_url" -H "Authorization: token $GITEA_TOKEN") + run_status=$(echo "$run_resp" | jq -r '.status // "unknown"') + + if [ "$run_status" = "completed" ]; then + run_conclusion=$(echo "$run_resp" | jq -r '.conclusion // "?"') + elapsed=$(( $(date +%s) - start_time )) + _report "$label" "PASS" "display_title matched, status=$run_status conclusion=$run_conclusion run_number=$found_run_num (${elapsed}s)" + echo " run detail: id=$found_id display_title=\"$found_display\"" + echo " endpoint: $GITEA_API_URL/$target_repo/actions/runs/$found_id" + return + fi + + sleep "$POLL_INTERVAL" + elapsed=$(( $(date +%s) - start_time )) + done + + _report "$label" "FAIL" "Run found but didn't complete within ${TIMEOUT_SECONDS}s" + return + fi + + sleep "$POLL_INTERVAL" + done + + _report "$label" "FAIL" "No run with display_title containing \"$dispatch_id\" found within ${TIMEOUT_SECONDS}s" +} + +# Tallenna aloitusaika +POC_START=$(_ts) +echo "==============================================" +echo "POC: dispatch-workflow display_title matching" +echo "Started: $POC_START" +echo "API URL: $GITEA_API_URL" +echo "==============================================" + +# Testi A: dispatchaa tämän repon poc-dispatch.yml +DISPATCH_ID_A=$(xxd -l 4 -p /dev/urandom 2>/dev/null || openssl rand -hex 4 2>/dev/null || date +%s | md5sum | head -c 8) +INPUTS_A=$(jq -nc \ + --arg dispatch_id "$DISPATCH_ID_A" \ + --arg type "local" \ + '{dispatch_id: $dispatch_id, type: $type}') + +_dispatch_and_poll \ + "A: local poc-dispatch" \ + "$LIB_REPO" "$LIB_WORKFLOW" "$BRANCH" \ + "$DISPATCH_ID_A" "$INPUTS_A" + +# Testi B: dispatchaa gitea-ci-gitops-tests gitops-service.yaml +DISPATCH_ID_B=$(xxd -l 4 -p /dev/urandom 2>/dev/null || openssl rand -hex 4 2>/dev/null || date +%s | md5sum | head -c 8) +INPUTS_B=$(jq -nc \ + --arg dispatch_id "$DISPATCH_ID_B" \ + --arg file "dev/Chart.yaml" \ + --arg yq_tpl '.version = "{{VERSION}}"' \ + --arg version "0.1.1" \ + --arg source_repo "$LIB_REPO" \ + --arg source_commit "poc-test-$(date +%s)" \ + '{dispatch_id: $dispatch_id, file: $file, yq_tpl: $yq_tpl, version: $version, source_repo: $source_repo, source_commit: $source_commit}') + +_dispatch_and_poll \ + "B: gitops test" \ + "$GITOPS_REPO" "$GITOPS_WORKFLOW" "main" \ + "$DISPATCH_ID_B" "$INPUTS_B" + +# Loppuraportti +echo "" +echo "==============================================" +echo "POC complete" +echo " PASS: $PASS" +echo " FAIL: $FAIL" +echo "==============================================" + +if [ "$FAIL" -gt 0 ]; then + exit 1 +fi -- 2.52.0 From e84e37c9f8d3b66b011ae5a0c34ad03c14e064de Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 04:25:05 +0300 Subject: [PATCH 05/23] poc --- .gitea/workflows/poc-dispatch.yml | 48 +++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/poc-dispatch.yml b/.gitea/workflows/poc-dispatch.yml index 3037826..f998a9c 100644 --- a/.gitea/workflows/poc-dispatch.yml +++ b/.gitea/workflows/poc-dispatch.yml @@ -1,5 +1,5 @@ name: POC GitOps Dispatch -run-name: "POC (${{ inputs.dispatch_id }})" +run-name: "POC (${{ inputs.dispatch_id || 'orchestrator' }})" on: push: branches: @@ -14,7 +14,49 @@ on: type: string jobs: - echo: + orchestrator: + if: github.event_name == 'push' runs-on: ubuntu-latest steps: - - run: echo "POC dispatch_id=${{ inputs.dispatch_id }} type=${{ inputs.type }}" + - name: Generate dispatch_id + id: gen + run: | + ID=$(date +%s | md5sum | head -c 8) + echo "dispatch_id=$ID" >> "$GITHUB_OUTPUT" + + - name: Dispatch test run with dispatch_id + run: | + INPUTS=$(jq -nc \ + --arg dispatch_id "${{ steps.gen.outputs.dispatch_id }}" \ + --arg type "auto" \ + '{dispatch_id: $dispatch_id, type: $type}') + curl -s -X POST \ + "${{ gitea.server_url }}/api/v1/repos/${{ github.repository }}/actions/workflows/poc-dispatch.yml/dispatches" \ + -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \ + -H "Content-Type: application/json" \ + -d "$(jq -nc --arg ref "${{ github.ref_name }}" --argjson inputs "$INPUTS" '{ref: $ref, inputs: $inputs}')" + + - name: Poll for dispatched run + run: | + ID=${{ steps.gen.outputs.dispatch_id }} + for i in $(seq 1 12); do + RUNS=$(curl -s "${{ gitea.server_url }}/api/v1/repos/${{ github.repository }}/actions/runs?event=workflow_dispatch&limit=10" \ + -H "Authorization: token ${{ secrets.GITEA_TOKEN }}") + FOUND=$(echo "$RUNS" | jq -r --arg id "$ID" \ + '[.workflow_runs[] | select(.display_title | contains($id))] | .[0].id // empty') + if [ -n "$FOUND" ]; then + echo "POC PASS: found run id=$FOUND with display_title containing '$ID'" + exit 0 + fi + sleep 5 + done + echo "POC FAIL: no run found with display_title containing '$ID'" + echo "$RUNS" + exit 1 + + test: + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - run: | + echo "POC dispatch_id=${{ inputs.dispatch_id }} type=${{ inputs.type }}" -- 2.52.0 From 47df5a8017e0311a9209b11ab3f7bd4fb6585261 Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 04:55:09 +0300 Subject: [PATCH 06/23] poc --- .gitea/workflows/poc-dispatch.yml | 66 ++++++++++++++++--------------- scripts/dispatch-workflow.sh | 36 ++++++++++++----- scripts/gitops-update.sh | 23 +++-------- tests/dispatch-workflow.bats | 39 +++++++++--------- 4 files changed, 86 insertions(+), 78 deletions(-) diff --git a/.gitea/workflows/poc-dispatch.yml b/.gitea/workflows/poc-dispatch.yml index f998a9c..6adca10 100644 --- a/.gitea/workflows/poc-dispatch.yml +++ b/.gitea/workflows/poc-dispatch.yml @@ -1,5 +1,5 @@ -name: POC GitOps Dispatch -run-name: "POC (${{ inputs.dispatch_id || 'orchestrator' }})" +name: POC GitOps E2E +run-name: "POC E2E (${{ inputs.dispatch_id || 'orchestrator' }})" on: push: branches: @@ -7,14 +7,11 @@ on: workflow_dispatch: inputs: dispatch_id: - required: true - type: string - type: - required: true + required: false type: string jobs: - orchestrator: + e2e: if: github.event_name == 'push' runs-on: ubuntu-latest steps: @@ -24,39 +21,46 @@ jobs: ID=$(date +%s | md5sum | head -c 8) echo "dispatch_id=$ID" >> "$GITHUB_OUTPUT" - - name: Dispatch test run with dispatch_id + - name: Dispatch to gitea-ci-gitops-tests run: | INPUTS=$(jq -nc \ --arg dispatch_id "${{ steps.gen.outputs.dispatch_id }}" \ - --arg type "auto" \ - '{dispatch_id: $dispatch_id, type: $type}') + --arg file "dev/Chart.yaml" \ + --arg yq_tpl '.version = "{{VERSION}}"' \ + --arg version "0.2.0" \ + --arg source_repo "${{ github.repository }}" \ + --arg source_commit "${{ github.sha }}" \ + '{dispatch_id: $dispatch_id, file: $file, yq_tpl: $yq_tpl, version: $version, source_repo: $source_repo, source_commit: $source_commit}') curl -s -X POST \ - "${{ gitea.server_url }}/api/v1/repos/${{ github.repository }}/actions/workflows/poc-dispatch.yml/dispatches" \ - -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \ + "https://gitea.app.keskikuja.site/api/v1/repos/niko/gitea-ci-gitops-tests/actions/workflows/gitops-service.yaml/dispatches" \ + -H "Authorization: token ${{ secrets.GITOPS_DISPATCH_TOKEN }}" \ -H "Content-Type: application/json" \ - -d "$(jq -nc --arg ref "${{ github.ref_name }}" --argjson inputs "$INPUTS" '{ref: $ref, inputs: $inputs}')" + -d "$(jq -nc --arg ref "main" --argjson inputs "$INPUTS" '{ref: "main", inputs: $inputs}')" - - name: Poll for dispatched run + - name: Poll for completion run: | ID=${{ steps.gen.outputs.dispatch_id }} - for i in $(seq 1 12); do - RUNS=$(curl -s "${{ gitea.server_url }}/api/v1/repos/${{ github.repository }}/actions/runs?event=workflow_dispatch&limit=10" \ - -H "Authorization: token ${{ secrets.GITEA_TOKEN }}") + for i in $(seq 1 60); do + RUNS=$(curl -s "https://gitea.app.keskikuja.site/api/v1/repos/niko/gitea-ci-gitops-tests/actions/runs?event=workflow_dispatch&limit=10" \ + -H "Authorization: token ${{ secrets.GITOPS_DISPATCH_TOKEN }}") FOUND=$(echo "$RUNS" | jq -r --arg id "$ID" \ - '[.workflow_runs[] | select(.display_title | contains($id))] | .[0].id // empty') - if [ -n "$FOUND" ]; then - echo "POC PASS: found run id=$FOUND with display_title containing '$ID'" - exit 0 + '[.workflow_runs[] | select(.display_title | contains($id))] | .[0]') + if [ -n "$FOUND" ] && [ "$FOUND" != "null" ]; then + STATUS=$(echo "$FOUND" | jq -r '.status') + CONCLUSION=$(echo "$FOUND" | jq -r '.conclusion // ""') + RUN_NUM=$(echo "$FOUND" | jq -r '.run_number') + echo "run found: id=$(echo $FOUND | jq -r '.id') number=$RUN_NUM display_title=$(echo $FOUND | jq -r '.display_title') status=$STATUS" + if [ "$STATUS" = "completed" ]; then + if [ "$CONCLUSION" = "success" ]; then + echo "POC PASS: gitops workflow completed successfully" + exit 0 + else + echo "POC FAIL: gitops workflow completed with conclusion=$CONCLUSION" + exit 1 + fi + fi fi sleep 5 done - echo "POC FAIL: no run found with display_title containing '$ID'" - echo "$RUNS" - exit 1 - - test: - if: github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - steps: - - run: | - echo "POC dispatch_id=${{ inputs.dispatch_id }} type=${{ inputs.type }}" + echo "POC FAIL: timeout waiting for gitops workflow" + exit 124 diff --git a/scripts/dispatch-workflow.sh b/scripts/dispatch-workflow.sh index 1564c84..280a62d 100755 --- a/scripts/dispatch-workflow.sh +++ b/scripts/dispatch-workflow.sh @@ -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 || date +%s | md5sum | head -c 8)}" +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)) diff --git a/scripts/gitops-update.sh b/scripts/gitops-update.sh index 3537d04..4317461 100755 --- a/scripts/gitops-update.sh +++ b/scripts/gitops-update.sh @@ -7,21 +7,14 @@ _gitops_fail() { local MSG="${1:-GitOps update failed}" echo "[ERROR] ${MSG}" >&2 - if [ -n "${SOURCE_REPO:-}" ] && [ -n "${SOURCE_COMMIT:-}" ] && \ + if [ -n "${GITOPS_REPO:-}" ] && [ -n "${GITOPS_SHA:-}" ] && \ + [ -n "${SOURCE_REPO:-}" ] && [ -n "${SOURCE_COMMIT:-}" ] && \ [ -n "${GITEA_API_URL:-}" ] && [ -n "${GITEA_TOKEN:-}" ]; then - local GITOPS_URL="${GITEA_API_URL}/${GITOPS_REPO:-unknown}/commits/${GITOPS_SHA:-unknown}" - ROOT_REPO="${SOURCE_REPO}" ROOT_COMMIT="${SOURCE_COMMIT}" \ + local SOURCE_URL="${GITEA_API_URL}/${SOURCE_REPO}/commits/${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 "${MSG}" \ - "gitops/${SOURCE_REPO}" "" "${GITOPS_URL}" 2>/dev/null || true - - if [ -n "${GITOPS_REPO:-}" ] && [ -n "${GITOPS_SHA:-}" ]; then - local SOURCE_URL="${GITEA_API_URL}/${SOURCE_REPO}/commits/${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 "${MSG}" \ - "source/${SOURCE_REPO}" "" "${SOURCE_URL}" 2>/dev/null || true - fi + "source/${SOURCE_REPO}" "" "${SOURCE_URL}" 2>/dev/null || true fi exit 1 @@ -39,14 +32,8 @@ _gitops_validate() { } _gitops_success() { - local GITOPS_URL="${GITEA_API_URL}/${GITOPS_REPO}/commits/${GITOPS_SHA}" local SOURCE_URL="${GITEA_API_URL}/${SOURCE_REPO}/commits/${SOURCE_COMMIT}" - ROOT_REPO="${SOURCE_REPO}" ROOT_COMMIT="${SOURCE_COMMIT}" \ - GITEA_API_URL="${GITEA_API_URL}" GITEA_TOKEN="${GITEA_TOKEN}" \ - bash "${SCRIPT_DIR}/report-status.sh" success "GitOps updated to ${VERSION}" \ - "gitops/${SOURCE_REPO}" "" "${GITOPS_URL}" - 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 "Source build" \ diff --git a/tests/dispatch-workflow.bats b/tests/dispatch-workflow.bats index 3f271de..778ea81 100644 --- a/tests/dispatch-workflow.bats +++ b/tests/dispatch-workflow.bats @@ -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" { -- 2.52.0 From 13493de7b249ded74f7688b5aba813d5df67417b Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 05:09:13 +0300 Subject: [PATCH 07/23] git username & email --- scripts/gitops-update.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/gitops-update.sh b/scripts/gitops-update.sh index 4317461..d86e28f 100755 --- a/scripts/gitops-update.sh +++ b/scripts/gitops-update.sh @@ -56,7 +56,9 @@ _gitops_update() { 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}" - git commit -m "[skip ci] gitops: update version to ${VERSION}" || _gitops_fail "Failed to commit" + 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" -- 2.52.0 From ec22d490393f01ec8ef4daaca7d5f67e203db0c9 Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 05:33:11 +0300 Subject: [PATCH 08/23] monorepossa komponetti parametrina --- .gitea/workflows/poc-dispatch.yml | 3 ++- scripts/gitops-update.sh | 25 +++++++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.gitea/workflows/poc-dispatch.yml b/.gitea/workflows/poc-dispatch.yml index 6adca10..6783fb3 100644 --- a/.gitea/workflows/poc-dispatch.yml +++ b/.gitea/workflows/poc-dispatch.yml @@ -30,7 +30,8 @@ jobs: --arg version "0.2.0" \ --arg source_repo "${{ github.repository }}" \ --arg source_commit "${{ github.sha }}" \ - '{dispatch_id: $dispatch_id, file: $file, yq_tpl: $yq_tpl, version: $version, source_repo: $source_repo, source_commit: $source_commit}') + --arg git_tag_prefix "poc-test" \ + '{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 \ "https://gitea.app.keskikuja.site/api/v1/repos/niko/gitea-ci-gitops-tests/actions/workflows/gitops-service.yaml/dispatches" \ -H "Authorization: token ${{ secrets.GITOPS_DISPATCH_TOKEN }}" \ diff --git a/scripts/gitops-update.sh b/scripts/gitops-update.sh index d86e28f..87c4f88 100755 --- a/scripts/gitops-update.sh +++ b/scripts/gitops-update.sh @@ -10,11 +10,17 @@ _gitops_fail() { if [ -n "${GITOPS_REPO:-}" ] && [ -n "${GITOPS_SHA:-}" ] && \ [ -n "${SOURCE_REPO:-}" ] && [ -n "${SOURCE_COMMIT:-}" ] && \ [ -n "${GITEA_API_URL:-}" ] && [ -n "${GITEA_TOKEN:-}" ]; then - local SOURCE_URL="${GITEA_API_URL}/${SOURCE_REPO}/commits/${SOURCE_COMMIT}" + local env repo context + env=$(dirname "${INPUT_FILE}") + repo=$(basename "${SOURCE_REPO}") + context="${repo}" + [ -n "${GIT_TAG_PREFIX:-}" ] && context="${repo}/${GIT_TAG_PREFIX}" + + 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 "${MSG}" \ - "source/${SOURCE_REPO}" "" "${SOURCE_URL}" 2>/dev/null || true + bash "${SCRIPT_DIR}/report-status.sh" failure "Install to ${env} ${VERSION}" \ + "${context}" "" "${SOURCE_URL}" 2>/dev/null || true fi exit 1 @@ -32,12 +38,19 @@ _gitops_validate() { } _gitops_success() { - local SOURCE_URL="${GITEA_API_URL}/${SOURCE_REPO}/commits/${SOURCE_COMMIT}" + local env repo context + env=$(dirname "${INPUT_FILE}") + repo=$(basename "${SOURCE_REPO}") + context="${repo}" + [ -n "${GIT_TAG_PREFIX:-}" ] && context="${repo}/${GIT_TAG_PREFIX}" + + 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 "Source build" \ - "source/${SOURCE_REPO}" "" "${SOURCE_URL}" + bash "${SCRIPT_DIR}/report-status.sh" success \ + "Install to ${env} ${VERSION}" \ + "${context}" "" "${SOURCE_URL}" } _gitops_substitute() { -- 2.52.0 From 7f53e2c303dbdc07825e7c803959209119464516 Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 05:43:00 +0300 Subject: [PATCH 09/23] jos jo haluttu versio - ei commit --- scripts/gitops-update.sh | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/scripts/gitops-update.sh b/scripts/gitops-update.sh index 87c4f88..cd1ada1 100755 --- a/scripts/gitops-update.sh +++ b/scripts/gitops-update.sh @@ -53,6 +53,22 @@ _gitops_success() { "${context}" "" "${SOURCE_URL}" } +_gitops_nochange() { + local env repo context + env=$(dirname "${INPUT_FILE}") + repo=$(basename "${SOURCE_REPO}") + context="${repo}" + [ -n "${GIT_TAG_PREFIX:-}" ] && context="${repo}/${GIT_TAG_PREFIX}" + + local SOURCE_URL="${GITEA_API_URL}/${SOURCE_REPO}/commit/${SOURCE_COMMIT}" + + ROOT_REPO="${GITOPS_REPO}" ROOT_COMMIT="${SOURCE_COMMIT}" \ + 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" } @@ -69,6 +85,14 @@ _gitops_update() { 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" -- 2.52.0 From 028fd748a673d3d7b473b4164176f82051dfe1d5 Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 05:47:36 +0300 Subject: [PATCH 10/23] multi commit status gitops puolelle --- scripts/gitops-update.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/gitops-update.sh b/scripts/gitops-update.sh index cd1ada1..6b455d6 100755 --- a/scripts/gitops-update.sh +++ b/scripts/gitops-update.sh @@ -62,7 +62,7 @@ _gitops_nochange() { local SOURCE_URL="${GITEA_API_URL}/${SOURCE_REPO}/commit/${SOURCE_COMMIT}" - ROOT_REPO="${GITOPS_REPO}" ROOT_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" \ -- 2.52.0 From a0cdf377f6000f7c3ec090fc7810e7087807a362 Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 05:56:21 +0300 Subject: [PATCH 11/23] multi commit status support to same commit in gitops repo --- scripts/gitops-update.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/gitops-update.sh b/scripts/gitops-update.sh index 6b455d6..cc3410e 100755 --- a/scripts/gitops-update.sh +++ b/scripts/gitops-update.sh @@ -13,8 +13,8 @@ _gitops_fail() { local env repo context env=$(dirname "${INPUT_FILE}") repo=$(basename "${SOURCE_REPO}") - context="${repo}" - [ -n "${GIT_TAG_PREFIX:-}" ] && context="${repo}/${GIT_TAG_PREFIX}" + 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}" \ @@ -41,8 +41,8 @@ _gitops_success() { local env repo context env=$(dirname "${INPUT_FILE}") repo=$(basename "${SOURCE_REPO}") - context="${repo}" - [ -n "${GIT_TAG_PREFIX:-}" ] && context="${repo}/${GIT_TAG_PREFIX}" + 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}" @@ -57,8 +57,8 @@ _gitops_nochange() { local env repo context env=$(dirname "${INPUT_FILE}") repo=$(basename "${SOURCE_REPO}") - context="${repo}" - [ -n "${GIT_TAG_PREFIX:-}" ] && context="${repo}/${GIT_TAG_PREFIX}" + 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}" -- 2.52.0 From fa57a152e435128eb83da025257ce9a4314eb99c Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 06:03:51 +0300 Subject: [PATCH 12/23] self commit status about gitops --- scripts/dispatch-workflow.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/dispatch-workflow.sh b/scripts/dispatch-workflow.sh index 280a62d..a0d7a56 100755 --- a/scripts/dispatch-workflow.sh +++ b/scripts/dispatch-workflow.sh @@ -77,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 -- 2.52.0 From f58497f5e888d3f49f7456c05dd046ba26786586 Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 06:34:37 +0300 Subject: [PATCH 13/23] skill & docs --- .gitea/workflows/example-main.yml | 106 +++++++- README.md | 17 +- docs/config-model.md | 8 +- docs/shared-scripts.md | 29 ++- docs/workflows.md | 78 ++++-- skills/gitops-update/SKILL.md | 410 ++++++++++++++++++++++++++++++ 6 files changed, 617 insertions(+), 31 deletions(-) create mode 100644 skills/gitops-update/SKILL.md diff --git a/.gitea/workflows/example-main.yml b/.gitea/workflows/example-main.yml index a734123..95eacac 100644 --- a/.gitea/workflows/example-main.yml +++ b/.gitea/workflows/example-main.yml @@ -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,118 @@ jobs: env_json: ${{ needs.load-config.outputs.env_json }} version: ${{ needs.check-version.outputs.version }} + helm-build-push: + name: Build & Push Helm + needs: [load-config, check-version, bats, cucumber] + if: needs.check-version.outputs.artifact_exists != 'true' + uses: niko/gitea-ci-library/.gitea/workflows/helm-build-push.yml@main + secrets: inherit + with: + env_json: ${{ needs.load-config.outputs.env_json }} + version: ${{ needs.check-version.outputs.version }} + + gitops-chart: + name: GitOps — helm version + needs: [helm-build-push] + if: success() + runs-on: ubuntu-latest + outputs: + chart_commit: ${{ steps.update.outputs.chart_commit }} + steps: + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + repository: niko/gitea-ci-library + path: .ci + - name: Update Chart.yaml version + id: update + run: | + INPUTS=$(jq -nc \ + --arg file "dev/Chart.yaml" \ + --arg yq_tpl '(.dependencies[] | select(.name == "git-pages") | .version) = "{{VERSION}}"' \ + --arg version "${{ needs.check-version.outputs.version }}" \ + --arg source_repo "${{ github.repository }}" \ + --arg source_commit "${{ github.sha }}" \ + --arg git_tag_prefix "helm" \ + '{file: $file, yq_tpl: $yq_tpl, version: $version, source_repo: $source_repo, source_commit: $source_commit, git_tag_prefix: $git_tag_prefix}') + OUTPUT=$(bash .ci/scripts/dispatch-workflow.sh \ + "niko/gitea-ci-gitops-tests" "gitops-service.yaml" "main" \ + "$INPUTS" "${{ fromJson(needs.load-config.outputs.env_json).GITEA_API_URL }}" \ + "${{ secrets.GITOPS_DISPATCH_TOKEN }}" "30") + echo "$OUTPUT" + CHART_REPO=$(echo "$OUTPUT" | grep '^GITOPS_COMMIT=' | cut -d= -f2) + echo "chart_commit=$CHART_REPO" >> "$GITHUB_OUTPUT" + + gitops-values: + name: GitOps — docker tag + needs: [docker-build-push] + if: success() + runs-on: ubuntu-latest + outputs: + values_commit: ${{ steps.update.outputs.values_commit }} + steps: + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + repository: niko/gitea-ci-library + path: .ci + - name: Update values.yaml tag + id: update + run: | + INPUTS=$(jq -nc \ + --arg file "dev/values.yaml" \ + --arg yq_tpl '.service.tag = "{{VERSION}}"' \ + --arg version "${{ needs.check-version.outputs.version }}" \ + --arg source_repo "${{ github.repository }}" \ + --arg source_commit "${{ github.sha }}" \ + --arg git_tag_prefix "docker" \ + '{file: $file, yq_tpl: $yq_tpl, version: $version, source_repo: $source_repo, source_commit: $source_commit, git_tag_prefix: $git_tag_prefix}') + OUTPUT=$(bash .ci/scripts/dispatch-workflow.sh \ + "niko/gitea-ci-gitops-tests" "gitops-service.yaml" "main" \ + "$INPUTS" "${{ fromJson(needs.load-config.outputs.env_json).GITEA_API_URL }}" \ + "${{ secrets.GITOPS_DISPATCH_TOKEN }}" "30") + echo "$OUTPUT" + VALUES_REPO=$(echo "$OUTPUT" | grep '^GITOPS_COMMIT=' | cut -d= -f2) + echo "values_commit=$VALUES_REPO" >> "$GITHUB_OUTPUT" + report-summary: name: Report Summary - needs: [load-config, build-push] + needs: [load-config, docker-build-push, helm-build-push] 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-summary: + name: GitOps Summary + needs: [load-config, check-version, gitops-chart, gitops-values] + if: always() + runs-on: ubuntu-latest + steps: + - name: Write GitOps summary + run: | + GITEA_URL="${{ fromJson(needs.load-config.outputs.env_json).GITEA_API_URL }}" + CHART_COMMIT="${{ needs.gitops-chart.outputs.chart_commit }}" + VALUES_COMMIT="${{ needs.gitops-values.outputs.values_commit }}" + CHART_LINK="${GITEA_URL}/niko/gitea-ci-gitops-tests/commit/${CHART_COMMIT}" + VALUES_LINK="${GITEA_URL}/niko/gitea-ci-gitops-tests/commit/${VALUES_COMMIT}" + + cat >> "$GITHUB_STEP_SUMMARY" << 'GITOPS' + + ## GitOps updates + + | Component | Version | Status | GitOps commit | + |-----------|---------|--------|--------------| + GITOPS + { + echo "| helm | ${{ needs.check-version.outputs.version }} | ${{ needs.gitops-chart.result }} | [link](${CHART_LINK}) |" + echo "| docker | ${{ needs.check-version.outputs.version }} | ${{ needs.gitops-values.result }} | [link](${VALUES_LINK}) |" + } >> "$GITHUB_STEP_SUMMARY" + tag-maintenance: name: Move provider version tag - needs: [build-push] + needs: [docker-build-push, helm-build-push] if: success() uses: niko/gitea-ci-library/.gitea/workflows/tag-maintenance.yml@main secrets: inherit diff --git a/README.md b/README.md index a94da3a..586b11b 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/docs/config-model.md b/docs/config-model.md index 8288206..75dcaba 100644 --- a/docs/config-model.md +++ b/docs/config-model.md @@ -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`, `gitops-update.sh` | +| `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` | diff --git a/docs/shared-scripts.md b/docs/shared-scripts.md index f173037..3940913 100644 --- a/docs/shared-scripts.md +++ b/docs/shared-scripts.md @@ -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 [timeout_minutes] +dispatch-workflow.sh [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) --- diff --git a/docs/workflows.md b/docs/workflows.md index a9de492..a466298 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -153,9 +153,17 @@ load-config → bats + cucumber → report-summary (always) ``` load-config → check-version → [artifact exists] → done - [no artifact] → bats + cucumber → report-summary (always) → docker-build-push + [no artifact] → bats + cucumber + ├─ docker-build-push → gitops-values ─┐ + └─ helm-build-push → gitops-chart ─┤ + ├─ gitops-summary + tag-maintenance ←─────────────────────┘ ``` +GitOps-jobit (`gitops-chart`, `gitops-values`) dispatchaavat GitOps-repon +workflown ja asettavat commit-statusin code-repoon + GitOps-repoon +(kaksisuuntainen track). Katso [skills/gitops-update/SKILL.md](../skills/gitops-update/SKILL.md). + ### `example-bats-tests.yml` — Bats unit-testit **Trigger:** `workflow_call` @@ -189,9 +197,15 @@ Forward-compatibeli — ei haittaa vanhemmilla Gitea-versioilla. **Riippuvuudet:** `yq`, `scripts/report-status.sh`, `git` Päivittää GitOps-repon konfiguraatiotiedoston versionumeron `yq`:lla, -committaa muutoksen ja asettaa commit-statuksen molempiin repoihin. +committaa muutoksen ja asettaa commit-statuksen molempiin repoihin +(kaksisuuntainen track): -**Input-ympäristömuuttujat:** +| 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 | |---|---|---| @@ -200,33 +214,53 @@ committaa muutoksen ja asettaa commit-statuksen molempiin repoihin. | `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-konfiguraatiorepo (esim. `org/app-gitops`) | +| `GITOPS_REPO` | Kyllä | GitOps-repo slug | | `GITEA_API_URL` | Kyllä | Gitean API-URL | -| `GITEA_TOKEN` | Kyllä | Gitea API-token | +| `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. Commit + push `[skip ci]` -6. Asettaa commit-statuksen: code-repoon (gitops-konteksti) ja GitOps-repoon (source-konteksti) +5. Jos muutoksia: commit + push `[skip ci]`, muuten status `— no change` +6. Asettaa commit-statuksen GitOps-repoon (source-konteksti, linkki code-repoon) -**Esimerkki dispatchistä:** -```yaml -- name: Update GitOps - run: | - export INPUT_FILE=dev/Chart.yaml - export YQ_TPL='(.version) = "{{VERSION}}"' - export VERSION=0.2.3 - export SOURCE_REPO=org/app - export SOURCE_COMMIT=${{ github.sha }} - export GITOPS_REPO=org/app-gitops - bash scripts/gitops-update.sh - env: - GITEA_API_URL: ${{ vars.GITEA_API_URL }} - GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} -``` +**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=` (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) + +`gitops-summary`-job (tai `report-summary`-job) lisää rivin GitOps-päivityksestä +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). --- diff --git a/skills/gitops-update/SKILL.md b/skills/gitops-update/SKILL.md new file mode 100644 index 0000000..cc6b450 --- /dev/null +++ b/skills/gitops-update/SKILL.md @@ -0,0 +1,410 @@ +--- +name: gitops-update +description: | + Setting up GitOps version updates: GitOps-repo workflow template, code + repo dispatch, secret requirements, and two-repo commit-status pattern. + Activates when the user needs to wire up artifact builds to GitOps + configuration updates. +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 — Provider-palvelu + +`scripts/gitops-update.sh` ja `scripts/dispatch-workflow.sh` muodostavat +GitOps-päivityspalvelun. Artifact buildataan code repossa, minkä jälkeen +code repo dispatchaa GitOps-repoon, joka päivittää konfiguraatiotiedoston +ja pushaa muutoksen. + +## Arkkitehtuuri + +Kaksi erillistä repoa, eristetyt oikeudet: + +``` +Code repo GitOps repo +(build & push artifact) (konfiguraatiot) + +build & push onnistuu (v0.2.3) + │ + │ dispatch ci-main.yml + │ {file, yq_tpl, version, source_repo, source_commit} + │ + └────────────────────────────────────→┐ + │ + dispatch-workflow.sh pollaa ←─────────┘ + │ + code repo asettaa │ git clone, yq update, + oman commit-statusnsa │ git commit + push + dispatchin exit-koodilla │ status GitOps-repoon +``` + +**Token-periaate:** Vain GitOps-repoon kirjoitetaan. Code repo asettaa +oman commit-statusnsa dispatch-kutsun exit-koodin perusteella omalla +auto-tokenillaan. GitOps-repon auto-token ei tarvitse oikeuksia code +repoon. + +## GitOps-repon workflow (ci-main.yml) + +GitOps-repoon luodaan `.gitea/workflows/ci-main.yml`: + +```yaml +name: GitOps Update +run-name: "GitOps Service (${{ 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 }} + GITOPS_BRANCH: ${{ github.ref_name }} + 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 +``` + +**Huomiot:** +- `GITEA_TOKEN` on Gitean auto-token — scopeutuu GitOps-repoon, riittää + cloneen, committiin, pushiin ja commit-statusiin GitOps-repossa +- `run-name` ja `dispatch_id` mahdollistavat dispatchaavan skriptin tunnistaa + tämän workflow-runin yksiselitteisesti `display_title`-kentästä, vaikka + samassa repossa olisi samanaikaisia ajoja +- yq ladataan lennossa (kompromissi, ks. "Tuleva CI-kontti") + +### Tulossa: custom CI-kontti + +Nykyinen job lataa yq:n lennossa. Myöhemmin rakennetaan oma kontti +(`ci-gitops`), jossa on nodejs + git + yq valmiina. Sama patterni kuin +`ci-bats` ja `ci-cucumber`. Ks. `skills/ci-container-build/SKILL.md`. + +## Code-repon dispatch-step + +Code repo dispatchaa GitOps-repon workflown artifact buildin onnistuttua: + +```yaml +gitops-update: + needs: [helm-build-push] + if: success() + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/checkout@v4 + with: + repository: niko/gitea-ci-library + path: .ci + + - name: Dispatch GitOps update + id: dispatch + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + INPUTS=$(jq -nc \ + --arg file "dev/Chart.yaml" \ + --arg yq_tpl '(.dependencies[] | select(.name == "agent-platform-helm") | .version) = "{{VERSION}}"' \ + --arg version "${{ needs.check-version.outputs.version }}" \ + --arg source_repo "${{ github.repository }}" \ + --arg source_commit "${{ github.sha }}" \ + '{file: $file, yq_tpl: $yq_tpl, version: $version, source_repo: $source_repo, source_commit: $source_commit}') + OUTPUT=$(bash .ci/scripts/dispatch-workflow.sh \ + "niko/agent-platform-gitops" \ + "ci-main.yml" \ + "main" \ + "$INPUTS" \ + "${{ fromJson(needs.load-config.outputs.env_json).GITEA_API_URL }}" \ + "${{ secrets.GITEA_TOKEN }}" \ + "30") + echo "$OUTPUT" + GITOPS_COMMIT=$(echo "$OUTPUT" | grep '^GITOPS_COMMIT=' | cut -d= -f2) + echo "gitops_commit=$GITOPS_COMMIT" >> "$GITHUB_OUTPUT" +``` + +### Multi-artifact pipeline (kontti + helm) + +Yksi main-haaran build tuottaa usein sekä Docker-imagen että Helm-chartin. +Kumpikin artefakti dispatchaa oman GitOps-päivityksensä rinnakkain: + +```yaml +gitops-helm: + needs: [helm-build-push] + if: success() + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + repository: niko/gitea-ci-library + path: .ci + - name: Update helm version + id: helm + run: | + INPUTS=$(jq -nc \ + --arg file "dev/Chart.yaml" \ + --arg yq_tpl '(.dependencies[] | select(.name == "git-pages") | .version) = "{{VERSION}}"' \ + --arg version "${{ needs.check-version.outputs.version }}" \ + --arg source_repo "${{ github.repository }}" \ + --arg source_commit "${{ github.sha }}" \ + --arg git_tag_prefix "helm" \ + '{dispatch_id: "", file: $file, yq_tpl: $yq_tpl, version: $version, source_repo: $source_repo, source_commit: $source_commit, git_tag_prefix: $git_tag_prefix}') + OUTPUT=$(bash .ci/scripts/dispatch-workflow.sh \ + "niko/gitea-ci-gitops-tests" "gitops-service.yaml" "main" \ + "$INPUTS" "${{ fromJson(needs.load-config.outputs.env_json).GITEA_API_URL }}" \ + "${{ secrets.GITOPS_DISPATCH_TOKEN }}" "30") + echo "$OUTPUT" + echo "helm_commit=$(echo "$OUTPUT" | grep '^GITOPS_COMMIT=' | cut -d= -f2)" >> "$GITHUB_OUTPUT" + +gitops-docker: + needs: [docker-build-push] + if: success() + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + repository: niko/gitea-ci-library + path: .ci + - name: Update docker tag + id: docker + run: | + INPUTS=$(jq -nc \ + --arg file "dev/values.yaml" \ + --arg yq_tpl '.service.tag = "{{VERSION}}"' \ + --arg version "${{ needs.check-version.outputs.version }}" \ + --arg source_repo "${{ github.repository }}" \ + --arg source_commit "${{ github.sha }}" \ + --arg git_tag_prefix "docker" \ + '{dispatch_id: "", file: $file, yq_tpl: $yq_tpl, version: $version, source_repo: $source_repo, source_commit: $source_commit, git_tag_prefix: $git_tag_prefix}') + OUTPUT=$(bash .ci/scripts/dispatch-workflow.sh \ + "niko/gitea-ci-gitops-tests" "gitops-service.yaml" "main" \ + "$INPUTS" "${{ fromJson(needs.load-config.outputs.env_json).GITEA_API_URL }}" \ + "${{ secrets.GITOPS_DISPATCH_TOKEN }}" "30") + echo "$OUTPUT" + echo "docker_commit=$(echo "$OUTPUT" | grep '^GITOPS_COMMIT=' | cut -d= -f2)" >> "$GITHUB_OUTPUT" +``` + +Kaksi dispatchia, kaksi eri tiedostoa, kaksi eri `GIT_TAG_PREFIX`-arvoa. +Kummallakin on oma commit-status-linja ja oma summary-rivi. +`dispatch-workflow.sh` hoitaa rinnakkaisuuden `display_title`-matchauksella. + +**GITEA_TOKEN dispatch-vaiheessa:** Tarvitaan manuaalinen token, +jolla on **write-oikeus GitOps-repoon** (esim. org-tason token). +Code-repon auto-token ei oikeuta dispatchaamaan toiseen repoon. +Token luodaan Giteassa: `Settings → Applications → Generate Token` +ja asetetaan code-repoon Actions Secretiksi. + +### Commit-status dispatchin perusteella + +`dispatch-workflow.sh` tulostaa `GITOPS_COMMIT=` stdoutiin onnistuneen +GitOps-päivityksen jälkeen. Code repo parsii sen ja asettaa commit-statusin +linkillä GitOps-committiin: + +```yaml + - name: Set commit-status with GitOps link + if: always() + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GITEA_API_URL: ${{ fromJson(needs.load-config.outputs.env_json).GITEA_API_URL }} + GITOPS_COMMIT: ${{ steps.dispatch.outputs.gitops_commit }} + VERSION: ${{ needs.check-version.outputs.version }} + run: | + GITOPS_URL="${GITEA_API_URL}/niko/agent-platform-gitops/commit/${GITOPS_COMMIT}" + CTX="gitops/$(basename ${{ github.repository }})" + DESC="Deploy to dev ${VERSION}" + if [ -n "$GITOPS_COMMIT" ]; then + bash .ci/scripts/report-status.sh success "$DESC" "$CTX" "" "$GITOPS_URL" + else + bash .ci/scripts/report-status.sh success "$DESC" "$CTX" + fi +``` + +`dispatch-workflow.sh` palauttaa: +- exit 0 = GitOps-päivitys onnistui (+ `GITOPS_COMMIT=`) +- exit 1 = GitOps-päivitys failasi +- exit 124 = aikakatkaisu (360 min oletus) + +### Loppuraportti (report-summary) + +Code-repon viimeinen job (`report-summary`) lisää GitOps-päivityksestä +rivin GITHUB_STEP_SUMMARYyn: + +```yaml + - name: GitOps summary + if: always() + env: + GITEA_API_URL: ${{ fromJson(needs.load-config.outputs.env_json).GITEA_API_URL }} + GITOPS_COMMIT: ${{ steps.dispatch.outputs.gitops_commit }} + VERSION: ${{ needs.check-version.outputs.version }} + run: | + if [ -n "$GITOPS_COMMIT" ]; then + LINK="${GITEA_API_URL}/niko/agent-platform-gitops/commit/${GITOPS_COMMIT}" + else + LINK="#" + fi + cat >> "$GITHUB_STEP_SUMMARY" << 'GITOPS' + + ## GitOps updates + + | Component | Version | Status | Commit | + |-----------|---------|--------|--------| + | agent-platform-helm | __VERSION__ | __STATUS__ | [link](__LINK__) | + GITOPS + sed -i "s|__VERSION__|${VERSION}|; s|__STATUS__|${{ job.status }}|; s|__LINK__|${LINK}|" \ + "$GITHUB_STEP_SUMMARY" +``` + +## Secretit ja tokenit + +| Secret | Missä | Scope | Kuvaus | +|--------|-------|-------|--------| +| `GITEA_TOKEN` (auto) | Code repo | Vain code repo | Asettaa commit-statusin dispatchin jälkeen | +| `GITEA_TOKEN` (auto) | GitOps repo | Vain GitOps repo | Klooni, push, commit-status GitOps-repossa | +| `GITOPS_DISPATCH_TOKEN` (manuaalinen) | Code repo | Write GitOps-repoon | Dispatchaa GitOps-repon workflow | + +**Tokenin luonti:** +1. Gitea → `Settings` → `Applications` → `Generate Token` +2. Valitse repo-oikeudet: valitse GitOps-repo, anna write-oikeudet +3. Token asetetaan code-repoon: `{repo} → Settings → Actions Secrets` +4. Salaisuuden nimi: esim. `GITOPS_DISPATCH_TOKEN` + +## Provider-skriptit + +### `scripts/gitops-update.sh` + +Ajaan GitOps-repon workflow'ssa. Päivittää konfiguraatiotiedoston yq:llä, +committaa ja pushaa. Asettaa commit-statuksen vain GitOps-repoon. + +**Input-ympäristömuuttujat:** + +| 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ä | Code-repo slug (esim. `org/app`) | +| `SOURCE_COMMIT` | Kyllä | Code-repon 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 | Branch (oletus `main`) | +| `GIT_TAG_PREFIX` | Ei | Komponentin tag-prefix status-nimeämiseen (esim. `agent-platform-helm`) | +| `GITOPS_CLONE_URL` | Ei | Yliajaa clone-URL (esim. eri protokolla) | +| `GITOPS_TARGET_DIR` | Ei | Yliajaa clone-kohdehakemisto | + +**Commit-status muoto:** + +GitOps-repoon asetetaan commit-status: + +| Kenttä | Formaatti | Esimerkki | +|--------|-----------|-----------| +| Context | `{repo}/{GIT_TAG_PREFIX} {RUN_ID}` tai `{repo} {RUN_ID}` | `gitea-ci-library/agent-platform-helm 473` | +| Description | `Install to {env} {version}` | `Install to dev 0.2.0` | +| Target URL | Linkki code-repon committiin | `/niko/gitea-ci-library/commit/abc123` | + +Jos tiedosto on jo halutussa versiossa (ei muutoksia), status saa descriptionin `Install to {env} {version} — no change`. Commit-pushia ei tehdä, GitOps-repo pysyy muuttumattomana. + +- `{env}` parsitaan `INPUT_FILE`:stä (`dev/Chart.yaml` → `dev`) +- `{repo}` parsitaan `SOURCE_REPO`:sta (`niko/gitea-ci-library` → `gitea-ci-library`) +- `{GIT_TAG_PREFIX}` tulee env-varista (sama kuin `gitea-env.conf`:ssa) + +### `scripts/dispatch-workflow.sh` + +Dispatchaa workflow_dispatchin kohderepoon ja pollaa valmistumista. +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 dispatch-attribuutioita. + +**Argumentit:** + +| # | Pakollinen | Kuvaus | +|---|------------|--------| +| 1 | Kyllä | Kohderepo (esim. `niko/agent-platform-gitops`) | +| 2 | Kyllä | Workflow-tiedosto (esim. `ci-main.yml`) | +| 3 | Kyllä | Branch/ref | +| 4 | Kyllä | Inputs JSON | +| 5 | Kyllä | Gitea API URL | +| 6 | Kyllä | Gitea token | +| 7 | Ei | Aikakatkaisu minuutteina (oletus 360) | + +Kutsujan ei tarvitse välittää `dispatch_id`:tä — skripti generoi sen +itse ja lisää inputteihin ennen dispatchia. + +## [skip ci] + +Commit-viestissä on `[skip ci]`, joka estää GitActions-runneria +triggeröimästä uutta CI-ajoa GitOps-repoon pushista. Näin vältetään +ääretön trigger-loop. + +## Race condition + +`dispatch-workflow.sh` tunnistaa jokaisen dispatchatun runin uniikilla +`dispatch_id`-tunnisteella `display_title`-kentästä. Vaikka useampi +artifakti dispatchaisi samaan aikaan ja useita workflow-runeja olisi +käynnissä rinnakkain, jokainen skripti löytää oikean runinsa. + +## Sääntöjä + +1. **Token ei kirjoita code repoon.** GitOps-repon workflow ei tarvitse + oikeuksia code repoon. Kaikki status-kutsut kohdistuvat vain + GitOps-repoon. Code repo asettaa oman statusnsa itse. +2. **Ei provider-workflowta.** GitOps-päivitys ei ole reusable workflow. + GitOps-repo ajaa `scripts/gitops-update.sh`:n suoraan. +3. **Vain `workflow_dispatch`.** GitOps-repon workflow:ta ei triggeröidä + pushista — se laukeaa vain dispatch-kutsusta. +4. **Dispatch ei palauta tarkkaa SHA:**ta. Code repo ei tiedä GitOps- + commitin SHA:ta ennen dispatch-valmistumista. Status asetetaan + dispatchin exit-koodin perusteella, ei GitOps-commitin tiedoilla. +5. **`dispatch_id` on pakollinen kohde-workflow'ssa** — ilman sitä + `dispatch-workflow.sh` ei löydä oikeaa runia moniajo-tilanteessa. +6. **`[skip ci]` commit-viestissä.** Pakollinen trigger-loopin estoon. -- 2.52.0 From 84978784fe5eb0718914aa3692198cdb660c41e8 Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 06:57:09 +0300 Subject: [PATCH 14/23] clean up --- .gitea/workflows/poc-dispatch.yml | 67 ------- scripts/poc-dispatch-match.sh | 189 ------------------ tests/features/gitops-update.feature | 30 ++- .../step_definitions/gitops-update.steps.js | 88 ++++---- tests/gitops-update.bats | 17 +- tests/helpers/git | 13 ++ 6 files changed, 76 insertions(+), 328 deletions(-) delete mode 100644 .gitea/workflows/poc-dispatch.yml delete mode 100755 scripts/poc-dispatch-match.sh diff --git a/.gitea/workflows/poc-dispatch.yml b/.gitea/workflows/poc-dispatch.yml deleted file mode 100644 index 6783fb3..0000000 --- a/.gitea/workflows/poc-dispatch.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: POC GitOps E2E -run-name: "POC E2E (${{ inputs.dispatch_id || 'orchestrator' }})" -on: - push: - branches: - - feature/gitops - workflow_dispatch: - inputs: - dispatch_id: - required: false - type: string - -jobs: - e2e: - if: github.event_name == 'push' - runs-on: ubuntu-latest - steps: - - name: Generate dispatch_id - id: gen - run: | - ID=$(date +%s | md5sum | head -c 8) - echo "dispatch_id=$ID" >> "$GITHUB_OUTPUT" - - - name: Dispatch to gitea-ci-gitops-tests - run: | - INPUTS=$(jq -nc \ - --arg dispatch_id "${{ steps.gen.outputs.dispatch_id }}" \ - --arg file "dev/Chart.yaml" \ - --arg yq_tpl '.version = "{{VERSION}}"' \ - --arg version "0.2.0" \ - --arg source_repo "${{ github.repository }}" \ - --arg source_commit "${{ github.sha }}" \ - --arg git_tag_prefix "poc-test" \ - '{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 \ - "https://gitea.app.keskikuja.site/api/v1/repos/niko/gitea-ci-gitops-tests/actions/workflows/gitops-service.yaml/dispatches" \ - -H "Authorization: token ${{ secrets.GITOPS_DISPATCH_TOKEN }}" \ - -H "Content-Type: application/json" \ - -d "$(jq -nc --arg ref "main" --argjson inputs "$INPUTS" '{ref: "main", inputs: $inputs}')" - - - name: Poll for completion - run: | - ID=${{ steps.gen.outputs.dispatch_id }} - for i in $(seq 1 60); do - RUNS=$(curl -s "https://gitea.app.keskikuja.site/api/v1/repos/niko/gitea-ci-gitops-tests/actions/runs?event=workflow_dispatch&limit=10" \ - -H "Authorization: token ${{ secrets.GITOPS_DISPATCH_TOKEN }}") - FOUND=$(echo "$RUNS" | jq -r --arg id "$ID" \ - '[.workflow_runs[] | select(.display_title | contains($id))] | .[0]') - if [ -n "$FOUND" ] && [ "$FOUND" != "null" ]; then - STATUS=$(echo "$FOUND" | jq -r '.status') - CONCLUSION=$(echo "$FOUND" | jq -r '.conclusion // ""') - RUN_NUM=$(echo "$FOUND" | jq -r '.run_number') - echo "run found: id=$(echo $FOUND | jq -r '.id') number=$RUN_NUM display_title=$(echo $FOUND | jq -r '.display_title') status=$STATUS" - if [ "$STATUS" = "completed" ]; then - if [ "$CONCLUSION" = "success" ]; then - echo "POC PASS: gitops workflow completed successfully" - exit 0 - else - echo "POC FAIL: gitops workflow completed with conclusion=$CONCLUSION" - exit 1 - fi - fi - fi - sleep 5 - done - echo "POC FAIL: timeout waiting for gitops workflow" - exit 124 diff --git a/scripts/poc-dispatch-match.sh b/scripts/poc-dispatch-match.sh deleted file mode 100755 index e182f6e..0000000 --- a/scripts/poc-dispatch-match.sh +++ /dev/null @@ -1,189 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# -# POC: testaa display_title-matchingia dispatchatuille workflow runeille. -# -# Ajaa kaksi testiä: -# A — dispatchaa gitea-ci-library:n poc-dispatch.yml (tämä repo, feature-haara) -# B — dispatchaa gitea-ci-gitops-tests:n gitops-service.yaml -# -# Käyttö: -# export GITEA_API_URL=https://gitea.app.keskikuja.site -# export GITEA_TOKEN=... -# bash scripts/poc-dispatch-match.sh -# - -GITEA_API_URL="${GITEA_API_URL:?GITEA_API_URL is required}" -GITEA_TOKEN="${GITEA_TOKEN:?GITEA_TOKEN is required}" -BRANCH="${BRANCH:-feature/gitops}" -TIMEOUT_SECONDS="${TIMEOUT_SECONDS:-60}" -POLL_INTERVAL="${POLL_INTERVAL:-5}" -GITOPS_REPO="${GITOPS_REPO:-niko/gitea-ci-gitops-tests}" -GITOPS_WORKFLOW="${GITOPS_WORKFLOW:-gitops-service.yaml}" -LIB_REPO="${LIB_REPO:-niko/gitea-ci-library}" -LIB_WORKFLOW="${LIB_WORKFLOW:-poc-dispatch.yml}" - -PASS=0 -FAIL=0 - -_ts() { - date -u +%Y-%m-%dT%H:%M:%SZ -} - -_report() { - local label="$1" status="$2" detail="$3" - if [ "$status" = "PASS" ]; then - PASS=$((PASS + 1)) - echo " ✅ $label: $detail" - else - FAIL=$((FAIL + 1)) - echo " ❌ $label: $detail" - fi -} - -_dispatch_and_poll() { - local label="$1" target_repo="$2" workflow_file="$3" ref="$4" dispatch_id="$5" - local inputs_json="$6" - - echo "" - echo "=== Test: $label ===" - echo " target: $target_repo" - echo " workflow: $workflow_file" - echo " ref: $ref" - echo " dispatch_id: $dispatch_id" - - # 1. Ennen dispatchia: ota snapshot viimeisimmästä run_number:sta - local before_resp before_run before_count - before_resp=$(curl -s --connect-timeout 5 --max-time 10 \ - "$GITEA_API_URL/api/v1/repos/$target_repo/actions/runs?event=workflow_dispatch&limit=1" \ - -H "Authorization: token $GITEA_TOKEN") - - before_run=$(echo "$before_resp" | jq -r '.workflow_runs[0].run_number // 0') - before_count=$(echo "$before_resp" | jq -r '.total_count // 0') - echo " before: $before_count runs, latest run_number=$before_run" - - # 2. Dispatch - local dispatch_url="$GITEA_API_URL/api/v1/repos/$target_repo/actions/workflows/$workflow_file/dispatches" - local dispatch_body - dispatch_body=$(jq -nc --arg ref "$ref" --argjson inputs "$inputs_json" '{ref: $ref, inputs: $inputs}') - - local dispatch_code - dispatch_code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 --max-time 10 \ - -X POST "$dispatch_url" \ - -H "Authorization: token $GITEA_TOKEN" \ - -H "Content-Type: application/json" \ - -d "$dispatch_body") - - if [ "$dispatch_code" != "201" ] && [ "$dispatch_code" != "204" ]; then - _report "$label" "FAIL" "Dispatch failed with HTTP $dispatch_code" - return - fi - echo " dispatch: HTTP $dispatch_code — started" - - # 3. Pollaa: etsi run jossa display_title sisältää dispatch_id:n - local start_time elapsed found="" - start_time=$(date +%s) - - while true; do - elapsed=$(( $(date +%s) - start_time )) - if [ "$elapsed" -ge "$TIMEOUT_SECONDS" ]; then - break - fi - - local runs_resp found_id found_display found_status found_run_num - runs_resp=$(curl -s --connect-timeout 5 --max-time 10 \ - "$GITEA_API_URL/api/v1/repos/$target_repo/actions/runs?event=workflow_dispatch&limit=20" \ - -H "Authorization: token $GITEA_TOKEN") - - found_id=$(echo "$runs_resp" | jq -r --arg id "$dispatch_id" \ - '[.workflow_runs[] | select(.display_title | contains($id))] | .[0].id // empty') - - if [ -n "$found_id" ] && [ "$found_id" != "null" ]; then - found_display=$(echo "$runs_resp" | jq -r --arg id "$dispatch_id" \ - '[.workflow_runs[] | select(.display_title | contains($id))] | .[0].display_title // "?"') - found_status=$(echo "$runs_resp" | jq -r --arg id "$dispatch_id" \ - '[.workflow_runs[] | select(.display_title | contains($id))] | .[0].status // "?"') - found_run_num=$(echo "$runs_resp" | jq -r --arg id "$dispatch_id" \ - '[.workflow_runs[] | select(.display_title | contains($id))] | .[0].run_number // "?"') - - echo " found: id=$found_id run_number=$found_run_num status=$found_status display_title=\"$found_display\" (after ${elapsed}s)" - - # Odota että run on completed - local poll_url="$GITEA_API_URL/api/v1/repos/$target_repo/actions/runs/$found_id" - while [ "$elapsed" -lt "$TIMEOUT_SECONDS" ]; do - local run_resp run_status run_conclusion - run_resp=$(curl -s --connect-timeout 5 --max-time 10 \ - "$poll_url" -H "Authorization: token $GITEA_TOKEN") - run_status=$(echo "$run_resp" | jq -r '.status // "unknown"') - - if [ "$run_status" = "completed" ]; then - run_conclusion=$(echo "$run_resp" | jq -r '.conclusion // "?"') - elapsed=$(( $(date +%s) - start_time )) - _report "$label" "PASS" "display_title matched, status=$run_status conclusion=$run_conclusion run_number=$found_run_num (${elapsed}s)" - echo " run detail: id=$found_id display_title=\"$found_display\"" - echo " endpoint: $GITEA_API_URL/$target_repo/actions/runs/$found_id" - return - fi - - sleep "$POLL_INTERVAL" - elapsed=$(( $(date +%s) - start_time )) - done - - _report "$label" "FAIL" "Run found but didn't complete within ${TIMEOUT_SECONDS}s" - return - fi - - sleep "$POLL_INTERVAL" - done - - _report "$label" "FAIL" "No run with display_title containing \"$dispatch_id\" found within ${TIMEOUT_SECONDS}s" -} - -# Tallenna aloitusaika -POC_START=$(_ts) -echo "==============================================" -echo "POC: dispatch-workflow display_title matching" -echo "Started: $POC_START" -echo "API URL: $GITEA_API_URL" -echo "==============================================" - -# Testi A: dispatchaa tämän repon poc-dispatch.yml -DISPATCH_ID_A=$(xxd -l 4 -p /dev/urandom 2>/dev/null || openssl rand -hex 4 2>/dev/null || date +%s | md5sum | head -c 8) -INPUTS_A=$(jq -nc \ - --arg dispatch_id "$DISPATCH_ID_A" \ - --arg type "local" \ - '{dispatch_id: $dispatch_id, type: $type}') - -_dispatch_and_poll \ - "A: local poc-dispatch" \ - "$LIB_REPO" "$LIB_WORKFLOW" "$BRANCH" \ - "$DISPATCH_ID_A" "$INPUTS_A" - -# Testi B: dispatchaa gitea-ci-gitops-tests gitops-service.yaml -DISPATCH_ID_B=$(xxd -l 4 -p /dev/urandom 2>/dev/null || openssl rand -hex 4 2>/dev/null || date +%s | md5sum | head -c 8) -INPUTS_B=$(jq -nc \ - --arg dispatch_id "$DISPATCH_ID_B" \ - --arg file "dev/Chart.yaml" \ - --arg yq_tpl '.version = "{{VERSION}}"' \ - --arg version "0.1.1" \ - --arg source_repo "$LIB_REPO" \ - --arg source_commit "poc-test-$(date +%s)" \ - '{dispatch_id: $dispatch_id, file: $file, yq_tpl: $yq_tpl, version: $version, source_repo: $source_repo, source_commit: $source_commit}') - -_dispatch_and_poll \ - "B: gitops test" \ - "$GITOPS_REPO" "$GITOPS_WORKFLOW" "main" \ - "$DISPATCH_ID_B" "$INPUTS_B" - -# Loppuraportti -echo "" -echo "==============================================" -echo "POC complete" -echo " PASS: $PASS" -echo " FAIL: $FAIL" -echo "==============================================" - -if [ "$FAIL" -gt 0 ]; then - exit 1 -fi diff --git a/tests/features/gitops-update.feature b/tests/features/gitops-update.feature index 409df30..83c200e 100644 --- a/tests/features/gitops-update.feature +++ b/tests/features/gitops-update.feature @@ -1,6 +1,6 @@ Feature: GitOps update As a GitOps repository - I want to update version references and report results back to the caller + 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: @@ -8,37 +8,35 @@ Feature: GitOps update And a commit has been pushed to the repository @mock - Scenario: Not enough env vars — caller commit gets failure status + 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 caller commit shows a failure status with the missing variable name - And the script exits with error + Then the script exits with error @mock - Scenario: GitOps job fails — caller commit gets failure status + 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 caller commit shows a failure status - And the script exits with error + Then the script exits with error @mock - Scenario: Everything succeeds — caller and GitOps get success + 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 caller commit shows a success status with a link to the GitOps commit - And the GitOps repo commit shows a success status with a link to the caller + And the GitOps repo commit shows a success status with a link to the caller commit @mock - Scenario: GitOps push fails — both repos get failure status + 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 caller commit shows a failure status - And the GitOps repo commit shows a failure status linking to the caller + And the GitOps repo commit shows a failure status linking to the caller commit @mock - Scenario: GitOps update succeeds — this repo commit status links to caller - Given a valid GitOps update dispatch + 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 GitOps repo commit shows a source context status linking to the caller commit + Then the script exits successfully + And the GitOps repo commit shows a "no change" status + And no Git commit or push was performed diff --git a/tests/features/step_definitions/gitops-update.steps.js b/tests/features/step_definitions/gitops-update.steps.js index e690433..4a033ba 100644 --- a/tests/features/step_definitions/gitops-update.steps.js +++ b/tests/features/step_definitions/gitops-update.steps.js @@ -41,7 +41,7 @@ Before({ tags: '@mock' }, function () { 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', { stdio: 'ignore' }); } catch (_) {} + try { execSync('rm -f /tmp/gitops-mock-requests.log /tmp/gitops-git-calls.log', { stdio: 'ignore' }); } catch (_) {} }); function bash(cmd) { @@ -73,6 +73,11 @@ function requestCount() { return parseInt(bash(`grep -c '^POST ' "${REQ_FILE}" 2>/dev/null || echo 0`).stdout.trim(), 10) || 0; } +function gitCalls() { + const out = bash(`cat "${GIT_CALLS_FILE:-/dev/null}" 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`; @@ -88,7 +93,6 @@ function runScript(envOverrides) { } Given('insufficient environment variables are provided for the GitOps update', function () { - this.missingVar = 'INPUT_FILE'; this.envOverrides = { INPUT_FILE: '' }; }); @@ -104,29 +108,17 @@ 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 caller commit shows a failure status with the missing variable name', function () { - const body = getFirstBody(); - if (!body.includes('"state":"failure"')) throw new Error(`Expected failure state, body: ${body.substring(0,200)}`); - if (!body.includes('"context":"gitops/niko/app"')) throw new Error(`Expected gitops context, body: ${body.substring(0,200)}`); - if (!body.includes(`"description":"${this.missingVar} is required`)) { - throw new Error(`Expected description mentioning ${this.missingVar}, body: ${body.substring(0,200)}`); - } - const pathStr = getFirstPath(); - if (!pathStr.includes('/repos/niko/app/statuses/')) throw new Error(`Expected source repo path, got: ${pathStr}`); -}); - -Then('the caller commit shows a failure status', function () { - const body = getFirstBody(); - if (!body.includes('"state":"failure"')) throw new Error(`Expected failure state, body: ${body.substring(0,200)}`); - if (!body.includes('"context":"gitops/niko/app"')) throw new Error(`Expected gitops context, body: ${body.substring(0,200)}`); - const pathStr = getFirstPath(); - if (!pathStr.includes('/repos/niko/app/statuses/')) throw new Error(`Expected source repo path, got: ${pathStr}`); -}); - 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)}`); }); @@ -135,41 +127,45 @@ 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 caller commit shows a success status with a link to the GitOps commit', function () { +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":"gitops/niko/app"')) throw new Error(`Expected gitops context, body: ${body.substring(0,200)}`); - if (!body.includes('niko/app-gitops/commits/')) throw new Error(`Expected link to GitOps commit, body: ${body.substring(0,200)}`); + if (!body.includes('"context":"app unknown"')) throw new Error(`Expected context "app unknown", body: ${body.substring(0,200)}`); + if (!body.includes('"description":"Install to dev 0.2.0"')) 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/statuses/')) throw new Error(`Expected source repo path, got: ${pathStr}`); -}); - -Then('the GitOps repo commit shows a success status with a link to the caller', function () { - if (requestCount() < 2) throw new Error(`Expected at least 2 requests, got ${requestCount()}`); - const body = getLastBody(); - if (!body.includes('"state":"success"')) throw new Error(`Expected success state, body: ${body.substring(0,200)}`); - if (!body.includes('"context":"source/niko/app"')) throw new Error(`Expected source context, body: ${body.substring(0,200)}`); - if (!body.includes('niko/app/commits/abc123def456')) throw new Error(`Expected link to caller commit, body: ${body.substring(0,200)}`); - const pathStr = getLastPath(); 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', function () { - if (requestCount() < 2) throw new Error(`Expected at least 2 requests, got ${requestCount()}`); - const body = getLastBody(); +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":"source/niko/app"')) throw new Error(`Expected source context, body: ${body.substring(0,200)}`); - if (!body.includes('niko/app/commits/abc123def456')) throw new Error(`Expected link to caller commit, body: ${body.substring(0,200)}`); - const pathStr = getLastPath(); + if (!body.includes('"context":"app unknown"')) throw new Error(`Expected context "app unknown", body: ${body.substring(0,200)}`); + if (!body.includes('"description":"Install to dev 0.2.0"')) 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 source context status linking to the caller commit', function () { - if (requestCount() < 2) throw new Error(`Expected at least 2 requests, got ${requestCount()}`); - const body = getLastBody(); +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('"context":"source/niko/app"')) throw new Error(`Expected source context, body: ${body.substring(0,200)}`); - if (!body.includes('niko/app/commits/abc123def456')) throw new Error(`Expected link to caller, body: ${body.substring(0,200)}`); - const pathStr = getLastPath(); + if (!body.includes('"description":"Install to dev 0.2.0 \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(', ')}`); + } +}); diff --git a/tests/gitops-update.bats b/tests/gitops-update.bats index 30d163f..70a117a 100644 --- a/tests/gitops-update.bats +++ b/tests/gitops-update.bats @@ -139,10 +139,9 @@ teardown() { mock_stop } -@test "two commit-status calls: code-repo and gitops-repo" { +@test "one commit-status call: gitops-repo only" { source tests/helpers/mock-api.sh mock_set_sequence '[ - {"code":201}, {"code":201} ]' mock_start @@ -159,14 +158,12 @@ teardown() { export GITEA_TOKEN=test-token run bash scripts/gitops-update.sh [ "$status" -eq 0 ] - path1=$(mock_get_first_request_path) - body1=$(mock_get_first_request_body) - [[ "$path1" == *"/repos/niko/app/statuses/"* ]] - [[ "$body1" == *'"context":"gitops/niko/app"'* ]] - path2=$(mock_get_request_path) - body2=$(mock_get_request_body) - [[ "$path2" == *"/repos/niko/app-gitops/statuses/"* ]] - [[ "$body2" == *'"context":"source/niko/app"'* ]] + path=$(mock_get_first_request_path) + body=$(mock_get_first_request_body) + [[ "$path" == *"/repos/niko/app-gitops/statuses/"* ]] + [[ "$body" == *'"context":"app unknown"'* ]] + [[ "$body" == *'"description":"Install to dev 0.2.3"'* ]] + [[ "$body" == *'"state":"success"'* ]] rm -f "$GIT_CALLS_FILE" "$YQ_CALLS_FILE" mock_stop } diff --git a/tests/helpers/git b/tests/helpers/git index 207c5a2..5173d84 100755 --- a/tests/helpers/git +++ b/tests/helpers/git @@ -8,6 +8,11 @@ if [ "${1:-}" = "push" ] && [ -n "${GIT_MOCK_FAIL_PUSH:-}" ]; then exit 1 fi +# Skip -c config arguments +while [ "${1:-}" = "-c" ]; do + shift 2 +done + case "$1" in clone) TARGET_DIR="${@: -1}" @@ -17,6 +22,14 @@ case "$1" in ;; 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" ;; -- 2.52.0 From 21a6ef7ab1c92e5c037a880afb7a49572ddbcef7 Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 07:01:05 +0300 Subject: [PATCH 15/23] fix --- tests/features/step_definitions/gitops-update.steps.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/features/step_definitions/gitops-update.steps.js b/tests/features/step_definitions/gitops-update.steps.js index 4a033ba..b0009f2 100644 --- a/tests/features/step_definitions/gitops-update.steps.js +++ b/tests/features/step_definitions/gitops-update.steps.js @@ -74,7 +74,7 @@ function requestCount() { } function gitCalls() { - const out = bash(`cat "${GIT_CALLS_FILE:-/dev/null}" 2>/dev/null || echo ""`).stdout; + const out = bash(`cat "${GIT_CALLS_FILE || '/dev/null'}" 2>/dev/null || echo ""`).stdout; return out.split('\n').filter(l => l.length > 0); } -- 2.52.0 From 6463dad6d7e4695f507dbe0ed72d56ad789d6dbc Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 07:15:38 +0300 Subject: [PATCH 16/23] testi korjaukset --- scripts/dispatch-workflow.sh | 2 +- tests/features/step_definitions/gitops-update.steps.js | 4 ++-- tests/features/step_definitions/test-execution.steps.js | 6 +++--- tests/gitops-update.bats | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/dispatch-workflow.sh b/scripts/dispatch-workflow.sh index a0d7a56..c482bca 100755 --- a/scripts/dispatch-workflow.sh +++ b/scripts/dispatch-workflow.sh @@ -19,7 +19,7 @@ POLL_INTERVAL="${DISPATCH_POLL_INTERVAL:-10}" # 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 || date +%s | md5sum | head -c 8)}" +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" diff --git a/tests/features/step_definitions/gitops-update.steps.js b/tests/features/step_definitions/gitops-update.steps.js index b0009f2..4bfd26f 100644 --- a/tests/features/step_definitions/gitops-update.steps.js +++ b/tests/features/step_definitions/gitops-update.steps.js @@ -132,7 +132,7 @@ Then('the GitOps repo commit shows a success status with a link to the caller co 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 unknown"')) throw new Error(`Expected context "app unknown", 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.0"')) 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(); @@ -144,7 +144,7 @@ Then('the GitOps repo commit shows a failure status linking to the caller commit 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 unknown"')) throw new Error(`Expected context "app unknown", 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.0"')) 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(); diff --git a/tests/features/step_definitions/test-execution.steps.js b/tests/features/step_definitions/test-execution.steps.js index 8657eb7..67e3ba6 100644 --- a/tests/features/step_definitions/test-execution.steps.js +++ b/tests/features/step_definitions/test-execution.steps.js @@ -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"'); diff --git a/tests/gitops-update.bats b/tests/gitops-update.bats index 70a117a..17afb2a 100644 --- a/tests/gitops-update.bats +++ b/tests/gitops-update.bats @@ -161,7 +161,7 @@ teardown() { path=$(mock_get_first_request_path) body=$(mock_get_first_request_body) [[ "$path" == *"/repos/niko/app-gitops/statuses/"* ]] - [[ "$body" == *'"context":"app unknown"'* ]] + [[ "$body" == *'"context":"app '* ]] [[ "$body" == *'"description":"Install to dev 0.2.3"'* ]] [[ "$body" == *'"state":"success"'* ]] rm -f "$GIT_CALLS_FILE" "$YQ_CALLS_FILE" -- 2.52.0 From ba16e9e4eb7d54722ee806900a57a528f01f4601 Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 07:41:32 +0300 Subject: [PATCH 17/23] fix --- tests/features/step_definitions/gitops-update.steps.js | 6 +++--- tests/features/step_definitions/test-execution.steps.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/features/step_definitions/gitops-update.steps.js b/tests/features/step_definitions/gitops-update.steps.js index 4bfd26f..f73ec4f 100644 --- a/tests/features/step_definitions/gitops-update.steps.js +++ b/tests/features/step_definitions/gitops-update.steps.js @@ -133,7 +133,7 @@ Then('the GitOps repo commit shows a success status with a link to the caller co 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.0"')) throw new Error(`Expected description, 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}`); @@ -145,7 +145,7 @@ Then('the GitOps repo commit shows a failure status linking to the caller commit 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.0"')) throw new Error(`Expected description, 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}`); @@ -156,7 +156,7 @@ Then('the GitOps repo commit shows a "no change" status', function () { 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.0 \u2014 no change"')) { + 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(); diff --git a/tests/features/step_definitions/test-execution.steps.js b/tests/features/step_definitions/test-execution.steps.js index 67e3ba6..4e921d9 100644 --- a/tests/features/step_definitions/test-execution.steps.js +++ b/tests/features/step_definitions/test-execution.steps.js @@ -103,7 +103,7 @@ When('a test workflow is dispatched but does not finish within the allowed time' { code: 200, body: { id: 1, status: 'running' } }, { code: 200, body: { id: 1, status: 'running' } }, ])); - 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; }); -- 2.52.0 From db9d6daebbae55221510deab5513d945950dd2db Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 07:53:38 +0300 Subject: [PATCH 18/23] fix --- .../step_definitions/gitops-update.steps.js | 3 ++- .../step_definitions/test-execution.steps.js | 16 ++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/features/step_definitions/gitops-update.steps.js b/tests/features/step_definitions/gitops-update.steps.js index f73ec4f..997a0a8 100644 --- a/tests/features/step_definitions/gitops-update.steps.js +++ b/tests/features/step_definitions/gitops-update.steps.js @@ -74,7 +74,8 @@ function requestCount() { } function gitCalls() { - const out = bash(`cat "${GIT_CALLS_FILE || '/dev/null'}" 2>/dev/null || echo ""`).stdout; + 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); } diff --git a/tests/features/step_definitions/test-execution.steps.js b/tests/features/step_definitions/test-execution.steps.js index 4e921d9..27fe35d 100644 --- a/tests/features/step_definitions/test-execution.steps.js +++ b/tests/features/step_definitions/test-execution.steps.js @@ -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 || '' }; } @@ -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.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)}`); + } }); -- 2.52.0 From bcac84f2fdc6e2b9016eb69543751d7763a214f7 Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 09:55:25 +0300 Subject: [PATCH 19/23] =?UTF-8?q?siistimist=C3=A4,=20router=20pipelien=20c?= =?UTF-8?q?lean?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/example-gitea-env.conf | 1 + .gitea/workflows/example-main.yml | 103 +++------------------- .gitea/workflows/git-pages.ci-main.yml | 17 +++- .gitea/workflows/git-pages.gitea-env.conf | 1 + .gitea/workflows/gitops-dispatch.yml | 58 ++++++++++++ .gitea/workflows/report-summary.yml | 23 +++++ docs/workflows.md | 56 +++++++++--- scripts/gitops-dispatch.sh | 44 +++++++++ 8 files changed, 200 insertions(+), 103 deletions(-) create mode 100644 .gitea/workflows/gitops-dispatch.yml create mode 100644 scripts/gitops-dispatch.sh diff --git a/.gitea/workflows/example-gitea-env.conf b/.gitea/workflows/example-gitea-env.conf index 7d35737..8e789e7 100644 --- a/.gitea/workflows/example-gitea-env.conf +++ b/.gitea/workflows/example-gitea-env.conf @@ -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 + diff --git a/.gitea/workflows/example-main.yml b/.gitea/workflows/example-main.yml index 95eacac..fec4a98 100644 --- a/.gitea/workflows/example-main.yml +++ b/.gitea/workflows/example-main.yml @@ -3,6 +3,7 @@ on: push: branches: - main + - feature/gitops workflow_dispatch: jobs: @@ -59,104 +60,28 @@ jobs: env_json: ${{ needs.load-config.outputs.env_json }} version: ${{ needs.check-version.outputs.version }} - gitops-chart: - name: GitOps — helm version - needs: [helm-build-push] - if: success() - runs-on: ubuntu-latest - outputs: - chart_commit: ${{ steps.update.outputs.chart_commit }} - steps: - - uses: actions/checkout@v4 - - uses: actions/checkout@v4 - with: - repository: niko/gitea-ci-library - path: .ci - - name: Update Chart.yaml version - id: update - run: | - INPUTS=$(jq -nc \ - --arg file "dev/Chart.yaml" \ - --arg yq_tpl '(.dependencies[] | select(.name == "git-pages") | .version) = "{{VERSION}}"' \ - --arg version "${{ needs.check-version.outputs.version }}" \ - --arg source_repo "${{ github.repository }}" \ - --arg source_commit "${{ github.sha }}" \ - --arg git_tag_prefix "helm" \ - '{file: $file, yq_tpl: $yq_tpl, version: $version, source_repo: $source_repo, source_commit: $source_commit, git_tag_prefix: $git_tag_prefix}') - OUTPUT=$(bash .ci/scripts/dispatch-workflow.sh \ - "niko/gitea-ci-gitops-tests" "gitops-service.yaml" "main" \ - "$INPUTS" "${{ fromJson(needs.load-config.outputs.env_json).GITEA_API_URL }}" \ - "${{ secrets.GITOPS_DISPATCH_TOKEN }}" "30") - echo "$OUTPUT" - CHART_REPO=$(echo "$OUTPUT" | grep '^GITOPS_COMMIT=' | cut -d= -f2) - echo "chart_commit=$CHART_REPO" >> "$GITHUB_OUTPUT" - - gitops-values: - name: GitOps — docker tag + docker-gitops: + name: Update docker needs: [docker-build-push] - if: success() - runs-on: ubuntu-latest - outputs: - values_commit: ${{ steps.update.outputs.values_commit }} - steps: - - uses: actions/checkout@v4 - - uses: actions/checkout@v4 - with: - repository: niko/gitea-ci-library - path: .ci - - name: Update values.yaml tag - id: update - run: | - INPUTS=$(jq -nc \ - --arg file "dev/values.yaml" \ - --arg yq_tpl '.service.tag = "{{VERSION}}"' \ - --arg version "${{ needs.check-version.outputs.version }}" \ - --arg source_repo "${{ github.repository }}" \ - --arg source_commit "${{ github.sha }}" \ - --arg git_tag_prefix "docker" \ - '{file: $file, yq_tpl: $yq_tpl, version: $version, source_repo: $source_repo, source_commit: $source_commit, git_tag_prefix: $git_tag_prefix}') - OUTPUT=$(bash .ci/scripts/dispatch-workflow.sh \ - "niko/gitea-ci-gitops-tests" "gitops-service.yaml" "main" \ - "$INPUTS" "${{ fromJson(needs.load-config.outputs.env_json).GITEA_API_URL }}" \ - "${{ secrets.GITOPS_DISPATCH_TOKEN }}" "30") - echo "$OUTPUT" - VALUES_REPO=$(echo "$OUTPUT" | grep '^GITOPS_COMMIT=' | cut -d= -f2) - echo "values_commit=$VALUES_REPO" >> "$GITHUB_OUTPUT" + 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, docker-build-push, helm-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-summary: - name: GitOps Summary - needs: [load-config, check-version, gitops-chart, gitops-values] - if: always() - runs-on: ubuntu-latest - steps: - - name: Write GitOps summary - run: | - GITEA_URL="${{ fromJson(needs.load-config.outputs.env_json).GITEA_API_URL }}" - CHART_COMMIT="${{ needs.gitops-chart.outputs.chart_commit }}" - VALUES_COMMIT="${{ needs.gitops-values.outputs.values_commit }}" - CHART_LINK="${GITEA_URL}/niko/gitea-ci-gitops-tests/commit/${CHART_COMMIT}" - VALUES_LINK="${GITEA_URL}/niko/gitea-ci-gitops-tests/commit/${VALUES_COMMIT}" - - cat >> "$GITHUB_STEP_SUMMARY" << 'GITOPS' - - ## GitOps updates - - | Component | Version | Status | GitOps commit | - |-----------|---------|--------|--------------| - GITOPS - { - echo "| helm | ${{ needs.check-version.outputs.version }} | ${{ needs.gitops-chart.result }} | [link](${CHART_LINK}) |" - echo "| docker | ${{ needs.check-version.outputs.version }} | ${{ needs.gitops-values.result }} | [link](${VALUES_LINK}) |" - } >> "$GITHUB_STEP_SUMMARY" + gitops: | + ${{ needs.docker-gitops.outputs.summary }} tag-maintenance: name: Move provider version tag diff --git a/.gitea/workflows/git-pages.ci-main.yml b/.gitea/workflows/git-pages.ci-main.yml index 868fcfd..af37b10 100644 --- a/.gitea/workflows/git-pages.ci-main.yml +++ b/.gitea/workflows/git-pages.ci-main.yml @@ -5,7 +5,6 @@ on: - main paths: - git-pages/** - - .gitea/workflows/helm-build-push.yml - .gitea/workflows/git-pages.* workflow_dispatch: @@ -36,11 +35,25 @@ jobs: version: ${{ needs.check-version.outputs.version }} chart_path: git-pages + chart-gitops: + name: Update chart to the cluster + needs: [helm-push] + uses: niko/gitea-ci-library/.gitea/workflows/gitops-dispatch.yml + 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, 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 }} diff --git a/.gitea/workflows/git-pages.gitea-env.conf b/.gitea/workflows/git-pages.gitea-env.conf index c497b13..eea1639 100644 --- a/.gitea/workflows/git-pages.gitea-env.conf +++ b/.gitea/workflows/git-pages.gitea-env.conf @@ -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 + diff --git a/.gitea/workflows/gitops-dispatch.yml b/.gitea/workflows/gitops-dispatch.yml new file mode 100644 index 0000000..38bbdea --- /dev/null +++ b/.gitea/workflows/gitops-dispatch.yml @@ -0,0 +1,58 @@ +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 + +jobs: + dispatch: + runs-on: ubuntu-latest + outputs: + summary: ${{ steps.run.outputs.GITOPS_SUMMARY }} + steps: + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + repository: niko/gitea-ci-library + path: .ci + - name: Run gitops dispatch + id: run + env: + GITEA_TOKEN: ${{ secrets.GITOPS_DISPATCH_TOKEN }} + run: | + OUTPUT=$(bash .ci/scripts/gitops-dispatch.sh) + echo "$OUTPUT" + SUMMARY=$(awk -F= '/^GITOPS_SUMMARY=/ {print $2}' <<<"$OUTPUT") + echo "GITOPS_SUMMARY=$SUMMARY" >> "$GITHUB_OUTPUT" diff --git a/.gitea/workflows/report-summary.yml b/.gitea/workflows/report-summary.yml index 7b96be0..ff4eacc 100644 --- a/.gitea/workflows/report-summary.yml +++ b/.gitea/workflows/report-summary.yml @@ -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 diff --git a/docs/workflows.md b/docs/workflows.md index a466298..43983d5 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -136,6 +136,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,18 +174,22 @@ load-config → bats + cucumber → report-summary (always) **Trigger:** `push` [branches: main] ``` -load-config → check-version → - [artifact exists] → done - [no artifact] → bats + cucumber - ├─ docker-build-push → gitops-values ─┐ - └─ helm-build-push → gitops-chart ─┤ - ├─ gitops-summary - tag-maintenance ←─────────────────────┘ +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-values`) dispatchaavat GitOps-repon -workflown ja asettavat commit-statusin code-repoon + GitOps-repoon -(kaksisuuntainen track). Katso [skills/gitops-update/SKILL.md](../skills/gitops-update/SKILL.md). +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 @@ -182,7 +209,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ä. @@ -253,7 +285,7 @@ oman commit-statusinsa linkillä GitOps-committiin: ### Loppuraportti (GITHUB_STEP_SUMMARY) -`gitops-summary`-job (tai `report-summary`-job) lisää rivin GitOps-päivityksestä +`report-summary.yml` (optio `gitops`-inputti) lisää GitOps-rivit GITHUB_STEP_SUMMARYyn: | Component | Version | Status | GitOps commit | diff --git a/scripts/gitops-dispatch.sh b/scripts/gitops-dispatch.sh new file mode 100644 index 0000000..e02ffcc --- /dev/null +++ b/scripts/gitops-dispatch.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${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}" + +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)" +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 "$OUTPUT" + +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" -- 2.52.0 From 6ae476658119197bdbb44159a6bf01592091cae4 Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 10:10:09 +0300 Subject: [PATCH 20/23] clean --- .gitea/workflows/example-main.yml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/.gitea/workflows/example-main.yml b/.gitea/workflows/example-main.yml index fec4a98..9286cd9 100644 --- a/.gitea/workflows/example-main.yml +++ b/.gitea/workflows/example-main.yml @@ -50,16 +50,6 @@ jobs: env_json: ${{ needs.load-config.outputs.env_json }} version: ${{ needs.check-version.outputs.version }} - helm-build-push: - name: Build & Push Helm - needs: [load-config, check-version, bats, cucumber] - if: needs.check-version.outputs.artifact_exists != 'true' - uses: niko/gitea-ci-library/.gitea/workflows/helm-build-push.yml@main - secrets: inherit - with: - env_json: ${{ needs.load-config.outputs.env_json }} - version: ${{ needs.check-version.outputs.version }} - docker-gitops: name: Update docker needs: [docker-build-push] @@ -85,7 +75,7 @@ jobs: tag-maintenance: name: Move provider version tag - needs: [docker-build-push, helm-build-push] + needs: [ocker-gitops] if: success() uses: niko/gitea-ci-library/.gitea/workflows/tag-maintenance.yml@main secrets: inherit -- 2.52.0 From a99a8a28c643155790bf275263810c70aa673516 Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 10:12:15 +0300 Subject: [PATCH 21/23] fix --- .gitea/workflows/example-main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/example-main.yml b/.gitea/workflows/example-main.yml index 9286cd9..9857b6e 100644 --- a/.gitea/workflows/example-main.yml +++ b/.gitea/workflows/example-main.yml @@ -51,7 +51,7 @@ jobs: version: ${{ needs.check-version.outputs.version }} docker-gitops: - name: Update docker + name: New docker to the cluster needs: [docker-build-push] uses: niko/gitea-ci-library/.gitea/workflows/gitops-dispatch.yml@main secrets: inherit @@ -75,7 +75,7 @@ jobs: tag-maintenance: name: Move provider version tag - needs: [ocker-gitops] + needs: [docker-gitops] if: success() uses: niko/gitea-ci-library/.gitea/workflows/tag-maintenance.yml@main secrets: inherit -- 2.52.0 From 077d98edb80aaa0aaff34fe77e65acc8b21fc5fb Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 10:24:31 +0300 Subject: [PATCH 22/23] any branch --- .gitea/workflows/example-main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/example-main.yml b/.gitea/workflows/example-main.yml index 9857b6e..b36a6cc 100644 --- a/.gitea/workflows/example-main.yml +++ b/.gitea/workflows/example-main.yml @@ -51,9 +51,9 @@ jobs: version: ${{ needs.check-version.outputs.version }} docker-gitops: - name: New docker to the cluster + name: GitOps needs: [docker-build-push] - uses: niko/gitea-ci-library/.gitea/workflows/gitops-dispatch.yml@main + uses: niko/gitea-ci-library/.gitea/workflows/gitops-dispatch.yml secrets: inherit with: env_json: ${{ needs.load-config.outputs.env_json }} -- 2.52.0 From db72d66f8c7ca4e71e815009a765ca105169b3ce Mon Sep 17 00:00:00 2001 From: moilanik Date: Mon, 22 Jun 2026 10:32:43 +0300 Subject: [PATCH 23/23] main haaraan valmis --- .gitea/workflows/example-main.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitea/workflows/example-main.yml b/.gitea/workflows/example-main.yml index b36a6cc..be19866 100644 --- a/.gitea/workflows/example-main.yml +++ b/.gitea/workflows/example-main.yml @@ -3,7 +3,6 @@ on: push: branches: - main - - feature/gitops workflow_dispatch: jobs: @@ -53,7 +52,7 @@ jobs: docker-gitops: name: GitOps needs: [docker-build-push] - uses: niko/gitea-ci-library/.gitea/workflows/gitops-dispatch.yml + uses: niko/gitea-ci-library/.gitea/workflows/gitops-dispatch.yml@main secrets: inherit with: env_json: ${{ needs.load-config.outputs.env_json }} -- 2.52.0