feat(scripts): implement report-status.sh with bats and cucumber tests (#1)
CI — gitea-ci-library / feature (push) Failing after 0s
CI — gitea-ci-library / master (push) Has been skipped

Co-authored-by: moilanik <niko.moilanen@tietoevry.com>
Reviewed-on: #1
This commit is contained in:
2026-06-08 11:33:09 +03:00
parent 9a59cbc185
commit 1379bbf1ee
13 changed files with 1746 additions and 6 deletions
+1
View File
@@ -2,3 +2,4 @@
.github/copilot-instructions.md .github/copilot-instructions.md
AGENTS.md AGENTS.md
.ai .ai
node_modules/
+94
View File
@@ -0,0 +1,94 @@
# Gitea Actions CI -kirjasto
Reusable workflow -kirjasto Gitea Actionsille. Lisätietoja: [docs/](docs/)
## Main-haaran suojaus
Jokaisessa tätä kirjastoa käyttävässä repossa `main`-haara suojataan — koodi päätyy sinne vain PR:n kautta:
```
Repository → Settings → Branches → Branch Protection → Add Rule
```
| Osio | Asetus | Arvo |
|------|--------|------|
| **Patterns** | Protected Branch Name Pattern | `main` |
| **Push** | Disable Push | ✓ |
| **Force Push** | Disable Force Push | ✓ |
| **Pull Request Approvals** | Required approvals | `1` |
| | Dismiss stale approvals | ✓ |
| | Ignore stale approvals | ✓ |
| | Enable Status Check | ✓ (kun CI on olemassa) |
| **Pull Request Merge** | Block merge on rejected reviews | ✓ |
| | Block merge on official review requests | ✓ |
| | Block merge if pull request is outdated | ✓
## Gitea Actions runner (K8s / Helm)
Act runner suorittaa Gitea Actions workflowt. Asennus Kubernetes-klusteriin Helm chartilla:
### 1. Rekisteröintitoken
Hae token Giteasta:
- **Organization-taso:** Org → Settings → Actions → Runners → Create new runner
- **Globaali (site admin):** Site Admin → Actions → Runners → Create new runner
### 2. Asenna runner
```bash
GITEA_URL="https://<gitea-server-url>"
GITEA_ACTIONS_TOKEN="<registration-token>"
GITEA_ACTIONS_NAMESPACE="gitea-actions"
helm repo add gitea https://dl.gitea.com/charts
helm repo update
kubectl create secret generic act-runner-token \
--from-literal=token="$GITEA_ACTIONS_TOKEN" \
--namespace "$GITEA_ACTIONS_NAMESPACE" \
--dry-run=client -o yaml | kubectl apply -f -
helm upgrade --install act-runner gitea/actions \
--set enabled=true \
--set giteaRootURL="$GITEA_URL" \
--set existingSecret=act-runner-token \
--set existingSecretKey=token \
--set-string 'statefulset.runner.config=log:
level: info
cache:
enabled: false
container:
require_docker: true
docker_timeout: 300s' \
--namespace "$GITEA_ACTIONS_NAMESPACE" \
--create-namespace
```
Oletus-lokitaso on `debug` — suositeltu `info`. Näkee jobien aloitukset ja valmistumiset ilman konttikerrosten purkua (Downloading/Extracting-spämmiä). `debug` on tarpeen vain vianselvityksessä.
### 3. Varmista
```bash
kubectl get pods -n gitea-actions
# → act-runner-runner-0 2/2 Running
```
Gitean puolella runner ilmestyy Active-tilaan pienellä viiveellä:
```
Site Admin → Actions → Runners (tai Org → Settings → Actions → Runners)
# → act-runner-runner-0 Active ubuntu-latest
```
Tämän jälkeen `.gitea/workflows/ci.yml` triggeröityy automaattisesti pushista.
Lisätietoa runnerin toiminnasta, konteista ja DinD:stä: [docs/runner.md](docs/runner.md)
### Muuta
| Muuttuja | Kuvaus |
|----------|--------|
| `giteaRootURL` | Gitea-palvelimen osoite (esim. `https://gitea.example.com`) |
| `existingSecret` | Kubernetes secretin nimi, jossa token |
| `existingSecretKey` | Avain secretin sisällä |
| `statefulset.runner.labels` | Mukautetut labelit |
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
default: {
paths: ['tests/features/*.feature'],
require: ['tests/features/step_definitions/*.steps.js'],
format: ['progress-bar'],
tags: 'not @wip',
},
};
+1
View File
@@ -54,6 +54,7 @@ Kirjasto on Gitea-spesifi. Se hyödyntää Gitean REST API:a commit-statusraport
|---------|-------| |---------|-------|
| **Gitea REST API** | Commit-statusraportointi, workflow-dispatch, run-pollaus, taggaus | | **Gitea REST API** | Commit-statusraportointi, workflow-dispatch, run-pollaus, taggaus |
| **Gitea Packages** | Docker-imagen ja NPM-paketin säilytys | | **Gitea Packages** | Docker-imagen ja NPM-paketin säilytys |
| **Gitea act runner** | Suorittaa workflowt. Konteista, DinD:stä ja label-järjestelmästä: [runner.md](runner.md) |
| **MinIO** | Testiraporttien tallennus ja staattinen web-hosting | | **MinIO** | Testiraporttien tallennus ja staattinen web-hosting |
| **SonarQube** | Koodin laadun analyysi ja quality gate | | **SonarQube** | Koodin laadun analyysi ja quality gate |
+111
View File
@@ -0,0 +1,111 @@
# Gitea Actions — Runtime-ympäristö
> Kuuluu arkkitehtuuriin: [architecture.md](architecture.md). Asennusohjeet: [README.md](../README.md).
>
> Tämä dokumentti kuvaa miten Gitea Actions runner suorittaa workflowt ja minkälaisen runtime-ympäristön se tarjoaa.
---
## Runnerin rooli
Runner on **pelkkä suoritin**. Se ei sisällä build-työkaluja (Maven, npm, Docker) — ne tulevat workflow'n määrittelemistä konteista. Runner vastaanottaa työt Gitea-palvelimelta, ajaa ne ja raportoi tulokset takaisin.
```
Gitea → dispatch job → Runner → pull containers → execute steps → report status
```
Runnerilla on yksi vastuu: **suorittaa workflow-steppejä**. Kaikki runtime-ympäristön määrittely tapahtuu workflow-tiedostossa.
## Kontit ja palvelut
Jokainen job voi määritellä käyttämänsä kontit. Tämä vastaa Jenkinsin pod template -konseptia, mutta on yksinkertaisempi:
### `container:` — ajonaikainen ympäristö
Workflow-steppi ajetaan tässä kontissa. Eri jobeilla voi olla eri kontti:
```yaml
jobs:
build-java:
runs-on: ubuntu-latest
container: maven:3.9-eclipse-temurin-21
steps:
- run: mvn verify
build-node:
runs-on: ubuntu-latest
container: node:22
steps:
- run: npm ci && npm test
```
### `services:` — rinnakkaiset palvelut
Palvelukontit käynnistyvät jobin ajaksi ja ovat käytettävissä `localhost`-verkon kautta:
```yaml
jobs:
build-with-docker:
runs-on: ubuntu-latest
container: maven:3.9-eclipse-temurin-21
services:
docker:
image: docker:dind
env:
DOCKER_TLS_CERTDIR: ""
steps:
- run: mvn package
- run: docker build -t my-app .
```
Kontit vedetään ajonaikaisesti registrystä (Docker Hub, Gitea Packages, mikä tahansa). Runnerin ei tarvitse tietää niistä etukäteen.
### Docker-in-Docker
Runner-podilla on kaksi tapaa käyttää Dockeria:
| Tapa | Kuvaus | Sopii |
|------|--------|-------|
| **DinD** (`docker:dind`) | Oma Docker daemon service-kontissa | Suositeltu K8s-ympäristöön |
| **Docker socket** | Jaettu `/var/run/docker.sock` | Yksinkertainen, vähemmän eristetty |
Gitea act runnerin Helm chart tukee DinD:tä oletuksena.
## Label-järjestelmä
Runnerit rekisteröidään labelilla. Workflow valitsee runnerin `runs-on`-kentällä:
```yaml
jobs:
build:
runs-on: ubuntu-latest # ← label
```
Labelit asetetaan runnerin rekisteröinnissä:
```bash
# Helm
--set runner.labels="ubuntu-latest,docker,arm64"
# Binääri
./act_runner register --labels ubuntu-latest,docker
```
Eri labelit mahdollistavat erikoistuneet runnerit (ARM, GPU, Windows), mutta MVP:ssä riittää yksi `ubuntu-latest`.
## Runner-tasot
| Taso | Scope | Riski | Käyttötapaus |
|------|-------|-------|--------------|
| **Global** | Kaikki organisaatiot ja repot | Token-vuoto → hyökkääjä voi ajaa koodia missä tahansa | Jaettu infra, keskitetty hallinta |
| **Organization** | Yhden organisaation repot | Rajoittuu yhteen orgiin | Per organisaatio, eristetty — **suositeltu** |
## Jenkins-vertailu
| Jenkins | Gitea Actions |
|---------|--------------|
| Pod template (YAML) määrittelee kontit | `container:` + `services:` per job |
| Jokaiselle jobille oma pod | Jokaiselle jobille omat konttimääritykset |
| DinD sidecar-podissa | `services: docker:dind` samassa jobissa |
| Agentti = erillinen JVM-prosessi | Runner = kevyt Go-binääri tai K8s-pod |
| Labelit Jenkins-nodessa | Labelit runner-rekisteröinnissä |
+1098
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
{
"name": "gitea-ci-library",
"version": "1.0.0",
"description": "",
"main": "cucumber.js",
"directories": {
"doc": "docs",
"test": "tests"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "ssh://git@gitea.app.keskikuja.site:30009/niko/gitea-ci-library.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"@cucumber/cucumber": "^13.0.0"
}
}
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail
[ -z "${GITEA_API_URL:-}" ] && echo "ERROR: GITEA_API_URL is not set" >&2 && exit 1
[ -z "${GITEA_TOKEN:-}" ] && echo "ERROR: GITEA_TOKEN is not set" >&2 && exit 1
STATE="${1:-}"
DESCRIPTION="${2:-}"
URL="${3:-}"
KEY="${4:-commit-${GITHUB_SHA:0:8}}"
ROOT_COMMIT="${5:-}"
ROOT_REPO="${6:-}"
[ -z "$STATE" ] && echo "ERROR: state argument is required" >&2 && exit 1
[ -z "$DESCRIPTION" ] && echo "ERROR: description argument is required" >&2 && exit 1
[ -z "$URL" ] && echo "ERROR: url argument is required" >&2 && exit 1
if [ -n "$ROOT_COMMIT" ] && [ -n "$ROOT_REPO" ]; then
REPO="$ROOT_REPO"
COMMIT="$ROOT_COMMIT"
else
REPO="${GITHUB_REPOSITORY:-}"
COMMIT="${GITHUB_SHA:-}"
fi
[ -z "$REPO" ] && echo "ERROR: GITHUB_REPOSITORY is not set" >&2 && exit 1
[ -z "$COMMIT" ] && echo "ERROR: GITHUB_SHA is not set" >&2 && exit 1
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "$GITEA_API_URL/api/v1/repos/$REPO/statuses/$COMMIT" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"state\":\"$STATE\",\"target_url\":\"$URL\",\"description\":\"$DESCRIPTION\",\"context\":\"$KEY\"}")
if [ "$HTTP_CODE" = "201" ]; then
exit 0
fi
if [ -z "$HTTP_CODE" ]; then
echo "ERROR: Failed to connect to Gitea API at $GITEA_API_URL" >&2
else
echo "ERROR: API returned HTTP $HTTP_CODE" >&2
fi
exit 1
+6 -6
View File
@@ -11,32 +11,32 @@ Feature: Commit status visibility
# Ticket 0001: report-status.sh — Build status reporting to commits # Ticket 0001: report-status.sh — Build status reporting to commits
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ticket-0001 @mock @wip @real @ticket-0001 @mock @real
Scenario: Build step reports pending status to commit Scenario: Build step reports pending status to commit
When a build step starts executing When a build step starts executing
Then the commit shows a pending status with a description of the step Then the commit shows a pending status with a description of the step
@ticket-0001 @mock @wip @real @ticket-0001 @mock @real
Scenario: Build step reports successful completion with a result link Scenario: Build step reports successful completion with a result link
When a build step completes successfully and reports its results When a build step completes successfully and reports its results
Then the commit shows a success status with a clickable link to the results Then the commit shows a success status with a clickable link to the results
@ticket-0001 @mock @wip @real @ticket-0001 @mock @real
Scenario: Build step reports failure when tests do not pass Scenario: Build step reports failure when tests do not pass
When a build step fails When a build step fails
Then the commit shows a failure status with a description of what went wrong Then the commit shows a failure status with a description of what went wrong
@ticket-0001 @mock @wip @real @ticket-0001 @mock @real
Scenario: Multiple build steps report distinct statuses on the same commit Scenario: Multiple build steps report distinct statuses on the same commit
When several build steps each report their own status to the same commit When several build steps each report their own status to the same commit
Then each status appears under a unique label on the commit Then each status appears under a unique label on the commit
@ticket-0001 @mock @wip @real @ticket-0001 @mock @real
Scenario: Deployment step reports status back to the source commit Scenario: Deployment step reports status back to the source commit
When a deployment finishes for a commit that originated from another repository When a deployment finishes for a commit that originated from another repository
Then the source commit shows the deployment status alongside the build status Then the source commit shows the deployment status alongside the build status
@ticket-0001 @mock @wip @ticket-0001 @mock
Scenario: Status reporting is interrupted when the build system cannot be reached Scenario: Status reporting is interrupted when the build system cannot be reached
When a build step tries to report status but the build system is unavailable When a build step tries to report status but the build system is unavailable
Then the pipeline fails with a clear error message Then the pipeline fails with a clear error message
@@ -0,0 +1,131 @@
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 REPORT_SCRIPT = path.join(PROJECT_ROOT, 'scripts', 'report-status.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 bashQuiet(cmd) {
execSync(`bash -c '${cmd}'`, {
cwd: PROJECT_ROOT,
stdio: 'ignore',
});
}
function envBlock() {
return [
'export GITEA_API_URL="http://localhost:18080"',
'export GITEA_TOKEN="test-token-abc123"',
'export GITHUB_REPOSITORY="test-owner/test-repo"',
'export GITHUB_SHA="abc123def456789012345678901234567890abcd"',
'export GITHUB_SERVER_URL="https://gitea.example.com"',
'export GITHUB_RUN_ID="42"',
].join('; ');
}
function runReportStatus(args) {
return bash(`${envBlock()}; bash "${REPORT_SCRIPT}" ${args}`);
}
function getMockBody() {
return bash(`source "${MOCK_SCRIPT}" && _get_request_file && mock_get_request_body`).stdout.trim();
}
function getMockPath() {
return bash(`source "${MOCK_SCRIPT}" && _get_request_file && mock_get_request_path`).stdout.trim();
}
When('a build step starts executing', function () {
const r = runReportStatus('pending "Building project" "http://example.com/build/42"');
if (r.status !== 0) throw new Error(`Expected exit 0, got ${r.status}: ${r.stderr}`);
});
Then('the commit shows a pending status with a description of the step', function () {
const body = getMockBody();
if (!body.includes('"state":"pending"')) throw new Error('Expected pending status');
if (!body.includes('"description":"Building project"')) throw new Error('Expected description');
});
When('a build step completes successfully and reports its results', function () {
const r = runReportStatus('success "Unit tests OK" "http://example.com/reports/cucumber.html" "unit-test"');
if (r.status !== 0) throw new Error(`Expected exit 0, got ${r.status}`);
});
Then('the commit shows a success status with a clickable link to the results', function () {
const body = getMockBody();
if (!body.includes('"state":"success"')) throw new Error('Expected success status');
if (!body.includes('"target_url":"http://example.com/reports/cucumber.html"')) throw new Error('Expected URL');
});
When('a build step fails', function () {
const r = runReportStatus('failure "Tests failed: 3 of 10" "http://example.com/build/42"');
if (r.status !== 0) throw new Error(`Expected exit 0, got ${r.status}`);
});
Then('the commit shows a failure status with a description of what went wrong', function () {
const body = getMockBody();
if (!body.includes('"state":"failure"')) throw new Error('Expected failure status');
if (!body.includes('"description":"Tests failed: 3 of 10"')) throw new Error('Expected failure description');
});
When('several build steps each report their own status to the same commit', function () {
runReportStatus('pending "Build started" "http://example.com/build/42" "ci-build"');
execSync('sleep 0.3', { stdio: 'ignore' });
runReportStatus('success "Unit tests passed" "http://example.com/reports/unit.html" "unit-test"');
execSync('sleep 0.3', { stdio: 'ignore' });
runReportStatus('success "Integration tests passed" "http://example.com/reports/integration.html" "integration-test"');
});
Then('each status appears under a unique label on the commit', function () {
const requestFile = bash(`source "${MOCK_SCRIPT}" && _get_request_file`).stdout.trim();
if (!requestFile || requestFile === '/dev/null') throw new Error('Mock request file not found');
bashQuiet('sync');
const allRequestsRaw = bash(`cat "${requestFile}"`);
const allRequests = allRequestsRaw.stdout;
const postCount = (allRequests.match(/POST /g) || []).length;
if (postCount < 3) throw new Error(`Expected 3 POST requests, got ${postCount}. Content: ${allRequests.substring(0, 1000)}`);
if (!allRequests.includes('"context":"ci-build"')) throw new Error('Missing ci-build');
if (!allRequests.includes('"context":"unit-test"')) throw new Error('Missing unit-test');
if (!allRequests.includes('"context":"integration-test"')) throw new Error('Missing integration-test');
});
When('a deployment finishes for a commit that originated from another repository', function () {
const r = runReportStatus('success "Deployed to staging" "http://example.com/deploy/42" "deploy-staging" "rootabc123" "services/temperature-store"');
if (r.status !== 0) throw new Error(`Expected exit 0, got ${r.status}`);
});
Then('the source commit shows the deployment status alongside the build status', function () {
const body = getMockBody();
if (!body.includes('"state":"success"')) throw new Error('Expected success');
if (!body.includes('"context":"deploy-staging"')) throw new Error('Expected deploy-staging context');
const pathStr = getMockPath();
if (!pathStr.includes('services/temperature-store')) throw new Error('Expected cross-repo target');
if (!pathStr.includes('rootabc123')) throw new Error('Expected root commit');
});
When('a build step tries to report status but the build system is unavailable', function () {
bashQuiet(`source "${MOCK_SCRIPT}" && mock_stop`);
execSync('sleep 0.3', { stdio: 'ignore' });
bashQuiet(`source "${MOCK_SCRIPT}" && mock_set_response 500 && mock_start`);
const r = runReportStatus('success "Should fail" "http://example.com"');
this.reportStatusFailed = (r.status !== 0);
});
Then('the pipeline fails with a clear error message', function () {
if (!this.reportStatusFailed) throw new Error('Expected pipeline to fail (exit code != 0)');
});
@@ -0,0 +1,28 @@
const { execSync, spawn } = require('child_process');
const { Before, After, Given } = 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');
Before({ tags: '@mock' }, function () {
execSync(`bash -c 'source "${MOCK_SCRIPT}" && mock_start'`, {
cwd: PROJECT_ROOT,
stdio: 'ignore',
});
});
After({ tags: '@mock' }, function () {
try {
execSync(`bash -c 'source "${MOCK_SCRIPT}" && mock_stop'`, {
cwd: PROJECT_ROOT,
stdio: 'ignore',
});
} catch (_) { /* ignore */ }
});
Given('a project repository exists in Gitea', function () {
});
Given('a commit has been pushed to the repository', function () {
});
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
set -euo pipefail
MOCK_PORT=18080
MOCK_PID=""
MOCK_REQUEST_FILE=""
MOCK_RESPONSE_CODE=201
MOCK_STATE_FILE="/tmp/mock_api_state"
_kill_port() {
local pids
pids=$(lsof -ti ":$MOCK_PORT" 2>/dev/null) || true
[ -n "$pids" ] && kill $pids 2>/dev/null || true
sleep 0.2
}
_wait_port_free() {
local i=0
while lsof -ti ":$MOCK_PORT" >/dev/null 2>&1 && [ $i -lt 30 ]; do
sleep 0.1
i=$((i + 1))
done
}
mock_start() {
MOCK_RESPONSE_CODE="${MOCK_RESPONSE_CODE:-201}"
MOCK_REQUEST_FILE=$(mktemp)
echo "$MOCK_REQUEST_FILE" > "$MOCK_STATE_FILE"
_kill_port
_wait_port_free
(
while true; do
printf 'HTTP/1.1 %d OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{"id":1}\n' "$MOCK_RESPONSE_CODE" \
| nc -l "$MOCK_PORT" >> "$MOCK_REQUEST_FILE" 2>/dev/null || break
sleep 0.05
done
) &
MOCK_PID=$!
sleep 0.3
}
mock_stop() {
[ -n "${MOCK_PID:-}" ] && kill "$MOCK_PID" 2>/dev/null || true
_kill_port
_wait_port_free
[ -n "${MOCK_REQUEST_FILE:-}" ] && rm -f "${MOCK_REQUEST_FILE}" 2>/dev/null || true
rm -f "$MOCK_STATE_FILE"
MOCK_PID=""
}
mock_set_response() {
MOCK_RESPONSE_CODE="${1:-201}"
}
_get_request_file() {
if [ -f "$MOCK_STATE_FILE" ]; then
cat "$MOCK_STATE_FILE"
elif [ -n "${MOCK_REQUEST_FILE:-}" ]; then
echo "$MOCK_REQUEST_FILE"
else
echo "/dev/null"
fi
}
mock_get_request_body() {
tail -1 "$(_get_request_file)" 2>/dev/null || true
}
mock_get_request_path() {
head -1 "$(_get_request_file)" 2>/dev/null | awk '{print $2}' || true
}
mock_get_request_method() {
head -1 "$(_get_request_file)" 2>/dev/null | awk '{print $1}' || true
}
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env bats
setup() {
source tests/helpers/mock-api.sh
export GITEA_API_URL="http://localhost:18080"
export GITEA_TOKEN="test-token-abc123"
export GITHUB_REPOSITORY="test-owner/test-repo"
export GITHUB_SHA="abc123def456789012345678901234567890abcd"
export GITHUB_SERVER_URL="https://gitea.example.com"
export GITHUB_RUN_ID="42"
}
teardown() {
mock_stop
}
@test "pending status is POSTed with correct payload" {
mock_start
run bash scripts/report-status.sh pending "Building project" "http://example.com/build/42"
[ "$status" -eq 0 ]
path=$(mock_get_request_path)
[[ "$path" == "/api/v1/repos/test-owner/test-repo/statuses/abc123def456789012345678901234567890abcd" ]]
body=$(mock_get_request_body)
[[ "$body" == *'"state":"pending"'* ]]
[[ "$body" == *'"description":"Building project"'* ]]
[[ "$body" == *'"target_url":"http://example.com/build/42"'* ]]
method=$(mock_get_request_method)
[[ "$method" == "POST" ]]
}
@test "success status with url and custom key" {
mock_start
run bash scripts/report-status.sh success "Unit tests OK" "http://example.com/reports/cucumber.html" "unit-test"
[ "$status" -eq 0 ]
body=$(mock_get_request_body)
[[ "$body" == *'"state":"success"'* ]]
[[ "$body" == *'"description":"Unit tests OK"'* ]]
[[ "$body" == *'"target_url":"http://example.com/reports/cucumber.html"'* ]]
[[ "$body" == *'"context":"unit-test"'* ]]
}
@test "failure status is POSTed correctly" {
mock_start
run bash scripts/report-status.sh failure "Tests failed: 3 of 10" "http://example.com/build/42"
[ "$status" -eq 0 ]
body=$(mock_get_request_body)
[[ "$body" == *'"state":"failure"'* ]]
[[ "$body" == *'"description":"Tests failed: 3 of 10"'* ]]
}
@test "error status is POSTed correctly" {
mock_start
run bash scripts/report-status.sh error "Build timed out" "http://example.com/build/42"
[ "$status" -eq 0 ]
body=$(mock_get_request_body)
[[ "$body" == *'"state":"error"'* ]]
}
@test "cross-repo: root_commit and root_repo override target" {
mock_start
run bash scripts/report-status.sh success "Deployed to staging" "http://example.com/deploy/42" "deploy-staging" "rootabc123" "services/temperature-store"
[ "$status" -eq 0 ]
path=$(mock_get_request_path)
[[ "$path" == "/api/v1/repos/services/temperature-store/statuses/rootabc123" ]]
body=$(mock_get_request_body)
[[ "$body" == *'"state":"success"'* ]]
[[ "$body" == *'"context":"deploy-staging"'* ]]
}
@test "cross-repo: only root_commit without root_repo is ignored" {
mock_start
run bash scripts/report-status.sh success "Partial cross-repo" "http://example.com" "my-key" "abc"
[ "$status" -eq 0 ]
path=$(mock_get_request_path)
[[ "$path" == "/api/v1/repos/test-owner/test-repo/statuses/abc123def456789012345678901234567890abcd" ]]
}
@test "default key when not provided" {
mock_start
run bash scripts/report-status.sh pending "Build started" "http://example.com/build/42"
[ "$status" -eq 0 ]
body=$(mock_get_request_body)
[[ "$body" == *'"context":"commit-abc123de"'* ]]
}
@test "API returns 500 causes exit 1" {
mock_set_response 500
mock_start
run bash scripts/report-status.sh success "Should fail" "http://example.com"
[ "$status" -eq 1 ]
}
@test "missing GITEA_API_URL causes exit 1 with error message" {
unset GITEA_API_URL
run bash scripts/report-status.sh pending "Test" "http://example.com"
[ "$status" -eq 1 ]
[[ "$output" == *"ERROR"* ]] || [[ "$output" == *"GITEA_API_URL"* ]]
}
@test "missing GITEA_TOKEN causes exit 1 with error message" {
unset GITEA_TOKEN
run bash scripts/report-status.sh pending "Test" "http://example.com"
[ "$status" -eq 1 ]
[[ "$output" == *"ERROR"* ]] || [[ "$output" == *"GITEA_TOKEN"* ]]
}
@test "missing required state argument causes exit 1" {
mock_start
run bash scripts/report-status.sh "" "desc" "http://example.com"
[ "$status" -eq 1 ]
}
@test "missing required description argument causes exit 1" {
mock_start
run bash scripts/report-status.sh pending "" "http://example.com"
[ "$status" -eq 1 ]
}
@test "missing required url argument causes exit 1" {
mock_start
run bash scripts/report-status.sh pending "desc" ""
[ "$status" -eq 1 ]
}