feat(scripts): implement report-status.sh with bats and cucumber tests (#1)
Co-authored-by: moilanik <niko.moilanen@tietoevry.com> Reviewed-on: #1
This commit is contained in:
@@ -2,3 +2,4 @@
|
|||||||
.github/copilot-instructions.md
|
.github/copilot-instructions.md
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
.ai
|
.ai
|
||||||
|
node_modules/
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
module.exports = {
|
||||||
|
default: {
|
||||||
|
paths: ['tests/features/*.feature'],
|
||||||
|
require: ['tests/features/step_definitions/*.steps.js'],
|
||||||
|
format: ['progress-bar'],
|
||||||
|
tags: 'not @wip',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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
@@ -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ä |
|
||||||
Generated
+1098
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+44
@@ -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
|
||||||
@@ -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 () {
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 ]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user