Feature/gitops (#37)
CI Git-Pages Main / Load git-pages.gitea-env.conf to pipeline env (push) Successful in 34s
CI Main / Check existing artifact (push) Successful in 22s
CI Git-Pages Main / Build & Push Helm chart (push) Successful in 48s
CI Main / Bats tests (push) Successful in 1m34s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 1m45s
CI Main / Load example-gitea-env.conf to pipeline env (push) Successful in 34s
CI Git-Pages Main / Check existing artifact (push) Successful in 21s
ci-helm-build-push Helm push 0.1.5
unit-tests Bats test report
CI Git-Pages Main / Update chart to the cluster (push) Failing after 0s
ci-docker-build-push Docker push 0.2.25
CI Git-Pages Main / Report Summary (push) Successful in 7s
CI Main / Build & Push Docker (push) Successful in 44s
CI Main / GitOps (push) Failing after 22s
CI Main / Move provider version tag (push) Has been skipped
CI Main / Report Summary (push) Successful in 6s
CI Git-Pages Main / Load git-pages.gitea-env.conf to pipeline env (push) Successful in 34s
CI Main / Check existing artifact (push) Successful in 22s
CI Git-Pages Main / Build & Push Helm chart (push) Successful in 48s
CI Main / Bats tests (push) Successful in 1m34s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 1m45s
CI Main / Load example-gitea-env.conf to pipeline env (push) Successful in 34s
CI Git-Pages Main / Check existing artifact (push) Successful in 21s
ci-helm-build-push Helm push 0.1.5
unit-tests Bats test report
CI Git-Pages Main / Update chart to the cluster (push) Failing after 0s
ci-docker-build-push Docker push 0.2.25
CI Git-Pages Main / Report Summary (push) Successful in 7s
CI Main / Build & Push Docker (push) Successful in 44s
CI Main / GitOps (push) Failing after 22s
CI Main / Move provider version tag (push) Has been skipped
CI Main / Report Summary (push) Successful in 6s
Co-authored-by: moilanik <niko.moilanen@tietoevry.com> Reviewed-on: #37
This commit was merged in pull request #37.
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
Feature: GitOps update
|
||||
As a GitOps repository
|
||||
I want to update version references and report results to the caller
|
||||
So that the deployment chain is traceable from source to GitOps commit
|
||||
|
||||
Background:
|
||||
Given a project repository exists in Gitea
|
||||
And a commit has been pushed to the repository
|
||||
|
||||
@mock
|
||||
Scenario: Not enough env vars — script fails, no status set
|
||||
Given insufficient environment variables are provided for the GitOps update
|
||||
When the GitOps update script runs
|
||||
Then the script exits with error
|
||||
|
||||
@mock
|
||||
Scenario: GitOps job fails — no status set (SHA not yet known)
|
||||
Given the GitOps repository clone will fail
|
||||
When the GitOps update script runs
|
||||
Then the script exits with error
|
||||
|
||||
@mock
|
||||
Scenario: Everything succeeds — GitOps repo gets success status with link to caller
|
||||
Given a valid GitOps update dispatch
|
||||
When the GitOps update script runs
|
||||
Then the script exits successfully
|
||||
And the GitOps repo commit shows a success status with a link to the caller commit
|
||||
|
||||
@mock
|
||||
Scenario: GitOps push fails — GitOps repo gets failure status
|
||||
Given the GitOps repo push will fail after the version is committed
|
||||
When the GitOps update script runs
|
||||
Then the script exits with error
|
||||
And the GitOps repo commit shows a failure status linking to the caller commit
|
||||
|
||||
@mock
|
||||
Scenario: No changes — GitOps repo gets "no change" status
|
||||
Given the version file already has the target version
|
||||
When the GitOps update script runs
|
||||
Then the script exits successfully
|
||||
And the GitOps repo commit shows a "no change" status
|
||||
And no Git commit or push was performed
|
||||
@@ -0,0 +1,172 @@
|
||||
const { spawnSync, execSync } = require('child_process');
|
||||
const { Before, After, Given, When, Then } = require('@cucumber/cucumber');
|
||||
const path = require('path');
|
||||
|
||||
const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..');
|
||||
const MOCK_SCRIPT = path.join(PROJECT_ROOT, 'tests', 'helpers', 'mock-api.sh');
|
||||
const GITOPS_SCRIPT = path.join(PROJECT_ROOT, 'scripts', 'gitops-update.sh');
|
||||
const MOCK_HELPERS = path.join(PROJECT_ROOT, 'tests', 'helpers');
|
||||
const REQ_FILE = '/tmp/gitops-mock-requests.log';
|
||||
|
||||
const BASE_ENV = {
|
||||
INPUT_FILE: 'dev/Chart.yaml',
|
||||
YQ_TPL: '(.version) = "{{VERSION}}"',
|
||||
VERSION: '0.2.3',
|
||||
SOURCE_REPO: 'niko/app',
|
||||
SOURCE_COMMIT: 'abc123def456',
|
||||
GITOPS_REPO: 'niko/app-gitops',
|
||||
GITEA_API_URL: 'http://localhost:18080',
|
||||
GITEA_TOKEN: 'test-token',
|
||||
};
|
||||
|
||||
Before({ tags: '@mock' }, function () {
|
||||
process.env.PATH = `${MOCK_HELPERS}:${process.env.PATH}`;
|
||||
try { execSync('rm -f /tmp/gitops-mock-requests.log', { stdio: 'ignore' }); } catch (_) {}
|
||||
// Restart mock with known request file path
|
||||
const result = spawnSync('bash', ['-c', `
|
||||
source "${MOCK_SCRIPT}"
|
||||
mock_stop 2>/dev/null
|
||||
MOCK_REQUEST_FILE="${REQ_FILE}"
|
||||
mock_start
|
||||
sleep 0.5
|
||||
curl -s -o /dev/null -w "%{http_code}" --max-time 3 http://localhost:18080/api/v1/repos/health
|
||||
`], {
|
||||
cwd: PROJECT_ROOT, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
const code = result.stdout.trim();
|
||||
if (!code.startsWith('2') && !code.startsWith('4')) {
|
||||
throw new Error(`GitOps mock restart failed: ${result.stderr.substring(0,200)}`);
|
||||
}
|
||||
});
|
||||
|
||||
After({ tags: '@mock' }, function () {
|
||||
spawnSync('bash', ['-c', `source "${MOCK_SCRIPT}" && mock_stop 2>/dev/null`], { stdio: 'ignore' });
|
||||
try { execSync('rm -f /tmp/gitops-mock-requests.log /tmp/gitops-git-calls.log', { stdio: 'ignore' }); } catch (_) {}
|
||||
});
|
||||
|
||||
function bash(cmd) {
|
||||
const result = spawnSync('bash', ['-c', cmd], {
|
||||
cwd: PROJECT_ROOT,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
return { status: result.status, stdout: result.stdout || '', stderr: result.stderr || '' };
|
||||
}
|
||||
|
||||
function getFirstBody() {
|
||||
return bash(`grep -A1 '^POST ' "${REQ_FILE}" 2>/dev/null | head -2 | tail -1 || echo ""`).stdout.trim();
|
||||
}
|
||||
|
||||
function getFirstPath() {
|
||||
return bash(`grep '^POST ' "${REQ_FILE}" 2>/dev/null | head -1 | awk '{print $2}' || echo ""`).stdout.trim();
|
||||
}
|
||||
|
||||
function getLastBody() {
|
||||
return bash(`grep -A1 '^POST ' "${REQ_FILE}" 2>/dev/null | grep -v '^POST ' | tail -1 || echo ""`).stdout.trim();
|
||||
}
|
||||
|
||||
function getLastPath() {
|
||||
return bash(`grep '^POST ' "${REQ_FILE}" 2>/dev/null | tail -1 | awk '{print $2}' || echo ""`).stdout.trim();
|
||||
}
|
||||
|
||||
function requestCount() {
|
||||
return parseInt(bash(`grep -c '^POST ' "${REQ_FILE}" 2>/dev/null || echo 0`).stdout.trim(), 10) || 0;
|
||||
}
|
||||
|
||||
function gitCalls() {
|
||||
const callsFile = process.env.GIT_CALLS_FILE || '/dev/null';
|
||||
const out = bash(`cat "${callsFile}" 2>/dev/null || echo ""`).stdout;
|
||||
return out.split('\n').filter(l => l.length > 0);
|
||||
}
|
||||
|
||||
function runScript(envOverrides) {
|
||||
const env = { ...BASE_ENV, ...envOverrides };
|
||||
const scriptPath = `/tmp/gitops-run-${Date.now()}.sh`;
|
||||
const exports = Object.entries(env)
|
||||
.map(([k, v]) => `export ${k}="${v.replace(/"/g, '\\"')}"`)
|
||||
.join('\n');
|
||||
require('fs').writeFileSync(scriptPath, `${exports}\nexport PATH="${MOCK_HELPERS}:$PATH"\nset -euo pipefail\nbash "${GITOPS_SCRIPT}"\nsync\n`, 'utf8');
|
||||
try {
|
||||
return bash(`bash "${scriptPath}"`);
|
||||
} finally {
|
||||
require('fs').unlinkSync(scriptPath);
|
||||
}
|
||||
}
|
||||
|
||||
Given('insufficient environment variables are provided for the GitOps update', function () {
|
||||
this.envOverrides = { INPUT_FILE: '' };
|
||||
});
|
||||
|
||||
Given('the GitOps repository clone will fail', function () {
|
||||
this.envOverrides = { GIT_MOCK_FAIL: '1' };
|
||||
});
|
||||
|
||||
Given('a valid GitOps update dispatch', function () {
|
||||
this.envOverrides = {};
|
||||
});
|
||||
|
||||
Given('the GitOps repo push will fail after the version is committed', function () {
|
||||
this.envOverrides = { GIT_MOCK_FAIL_PUSH: '1' };
|
||||
});
|
||||
|
||||
Given('the version file already has the target version', function () {
|
||||
this.envOverrides = {
|
||||
GIT_MOCK_DIFF_NO_CHANGES: '1',
|
||||
GIT_CALLS_FILE: '/tmp/gitops-git-calls.log',
|
||||
};
|
||||
});
|
||||
|
||||
When('the GitOps update script runs', function () {
|
||||
this.result = runScript(this.envOverrides || {});
|
||||
});
|
||||
|
||||
Then('the script exits with error', function () {
|
||||
if (this.result.status === 0) throw new Error(`Expected non-zero exit, got 0. stderr: ${this.result.stderr.substring(0,200)}`);
|
||||
});
|
||||
|
||||
Then('the script exits successfully', function () {
|
||||
if (this.result.status !== 0) throw new Error(`Expected exit 0, got ${this.result.status}: ${this.result.stderr.substring(0,200)}`);
|
||||
});
|
||||
|
||||
Then('the GitOps repo commit shows a success status with a link to the caller commit', function () {
|
||||
const count = requestCount();
|
||||
if (count < 1) throw new Error(`Expected at least 1 request, got ${count}`);
|
||||
const body = getFirstBody();
|
||||
if (!body.includes('"state":"success"')) throw new Error(`Expected success state, body: ${body.substring(0,200)}`);
|
||||
if (!body.includes('"context":"app ')) throw new Error(`Expected context "app unknown", body: ${body.substring(0,200)}`);
|
||||
if (!body.includes('"description":"Install to dev 0.2.3"')) throw new Error(`Expected description, body: ${body.substring(0,200)}`);
|
||||
if (!body.includes('niko/app/commit/abc123def456')) throw new Error(`Expected link to caller commit, body: ${body.substring(0,200)}`);
|
||||
const pathStr = getFirstPath();
|
||||
if (!pathStr.includes('/repos/niko/app-gitops/statuses/')) throw new Error(`Expected gitops repo path, got: ${pathStr}`);
|
||||
});
|
||||
|
||||
Then('the GitOps repo commit shows a failure status linking to the caller commit', function () {
|
||||
const count = requestCount();
|
||||
if (count < 1) throw new Error(`Expected at least 1 request, got ${count}`);
|
||||
const body = getFirstBody();
|
||||
if (!body.includes('"state":"failure"')) throw new Error(`Expected failure state, body: ${body.substring(0,200)}`);
|
||||
if (!body.includes('"context":"app ')) throw new Error(`Expected context "app unknown", body: ${body.substring(0,200)}`);
|
||||
if (!body.includes('"description":"Install to dev 0.2.3"')) throw new Error(`Expected description, body: ${body.substring(0,200)}`);
|
||||
if (!body.includes('niko/app/commit/abc123def456')) throw new Error(`Expected link to caller commit, body: ${body.substring(0,200)}`);
|
||||
const pathStr = getFirstPath();
|
||||
if (!pathStr.includes('/repos/niko/app-gitops/statuses/')) throw new Error(`Expected gitops repo path, got: ${pathStr}`);
|
||||
});
|
||||
|
||||
Then('the GitOps repo commit shows a "no change" status', function () {
|
||||
const count = requestCount();
|
||||
if (count < 1) throw new Error(`Expected at least 1 request, got ${count}`);
|
||||
const body = getFirstBody();
|
||||
if (!body.includes('"state":"success"')) throw new Error(`Expected success state, body: ${body.substring(0,200)}`);
|
||||
if (!body.includes('"description":"Install to dev 0.2.3 \u2014 no change"')) {
|
||||
throw new Error(`Expected "no change" description, body: ${body.substring(0,200)}`);
|
||||
}
|
||||
const pathStr = getFirstPath();
|
||||
if (!pathStr.includes('/repos/niko/app-gitops/statuses/')) throw new Error(`Expected gitops repo path, got: ${pathStr}`);
|
||||
});
|
||||
|
||||
Then('no Git commit or push was performed', function () {
|
||||
const calls = gitCalls();
|
||||
if (calls.some(l => l.includes(' commit ') || l.includes(' push '))) {
|
||||
throw new Error(`Expected no commit or push, got: ${calls.join(', ')}`);
|
||||
}
|
||||
});
|
||||
@@ -15,7 +15,7 @@ function bash(cmd) {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
return { status: 0, stdout: out };
|
||||
return { status: 0, stdout: out, stderr: '' };
|
||||
} catch (e) {
|
||||
return { status: e.status, stdout: e.stdout || '', stderr: e.stderr || '' };
|
||||
}
|
||||
@@ -54,7 +54,7 @@ function setupMock(seqJson) {
|
||||
}
|
||||
|
||||
function runDispatch(args) {
|
||||
return bash(`export DISPATCH_POLL_INTERVAL="0.1"; bash "${DISPATCH_SCRIPT}" ${args}`);
|
||||
return bash(`export DISPATCH_ID="test123"; export DISPATCH_POLL_INTERVAL="0.1"; bash "${DISPATCH_SCRIPT}" ${args}`);
|
||||
}
|
||||
|
||||
Given('a deployment has completed in the target environment', function () {
|
||||
@@ -66,7 +66,7 @@ Given('the test project repository exists with test definitions', function () {
|
||||
When('a test workflow is dispatched to a test project', function () {
|
||||
setupMock(JSON.stringify([
|
||||
{ code: 201 },
|
||||
{ code: 200, body: { workflow_runs: [{ id: 1, status: 'running' }] } },
|
||||
{ code: 200, body: { workflow_runs: [{ id: 1, display_title: 'Workflow (test123)', run_number: 42, status: 'running' }] } },
|
||||
{ code: 200, body: { id: 1, status: 'completed', conclusion: 'success' } },
|
||||
]));
|
||||
const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\' "http://localhost:18080" "test-token-abc123"');
|
||||
@@ -84,7 +84,7 @@ Then('the pipeline continues only after receiving a success result', function ()
|
||||
When('a test workflow is dispatched and the tests fail', function () {
|
||||
setupMock(JSON.stringify([
|
||||
{ code: 201 },
|
||||
{ code: 200, body: { workflow_runs: [{ id: 1, status: 'running' }] } },
|
||||
{ code: 200, body: { workflow_runs: [{ id: 1, display_title: 'Workflow (test123)', run_number: 42, status: 'running' }] } },
|
||||
{ code: 200, body: { id: 1, status: 'completed', conclusion: 'failure' } },
|
||||
]));
|
||||
const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\' "http://localhost:18080" "test-token-abc123"');
|
||||
@@ -98,15 +98,19 @@ Then('the calling pipeline reports failure', function () {
|
||||
When('a test workflow is dispatched but does not finish within the allowed time', function () {
|
||||
setupMock(JSON.stringify([
|
||||
{ code: 201 },
|
||||
{ code: 200, body: { workflow_runs: [{ id: 1, status: 'running' }] } },
|
||||
{ code: 200, body: { id: 1, status: 'running' } },
|
||||
{ code: 200, body: { id: 1, status: 'running' } },
|
||||
{ code: 200, body: { id: 1, status: 'running' } },
|
||||
{ code: 200, body: { workflow_runs: [] } },
|
||||
{ code: 200, body: { workflow_runs: [] } },
|
||||
{ code: 200, body: { workflow_runs: [] } },
|
||||
{ code: 200, body: { workflow_runs: [] } },
|
||||
{ code: 200, body: { workflow_runs: [] } },
|
||||
]));
|
||||
const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\' "http://localhost:18080" "test-token-abc123" "0.001"');
|
||||
const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\' "http://localhost:18080" "test-token-abc123" "0.05"');
|
||||
this.dispatchResult = r.status;
|
||||
this.dispatchStderr = r.stderr;
|
||||
});
|
||||
|
||||
Then('the calling pipeline reports a timeout error', function () {
|
||||
if (this.dispatchResult !== 124) throw new Error(`Expected timeout exit 124, got ${this.dispatchResult}`);
|
||||
if (this.dispatchResult !== 124) {
|
||||
throw new Error(`Expected timeout exit 124, got ${this.dispatchResult}. stderr: ${(this.dispatchStderr || '').substring(0,300)}`);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user