Feature/gitops #37

Merged
niko merged 23 commits from feature/gitops into main 2026-06-22 10:37:15 +03:00
6 changed files with 76 additions and 328 deletions
Showing only changes of commit 84978784fe - Show all commits
-67
View File
@@ -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
-189
View File
@@ -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
+14 -16
View File
@@ -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
@@ -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(', ')}`);
}
});
+7 -10
View File
@@ -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
}
+13
View File
@@ -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"
;;