From c422825bf01d20cd903b5b54dfbda0582a1b8799 Mon Sep 17 00:00:00 2001 From: niko Date: Sun, 14 Jun 2026 03:26:44 +0300 Subject: [PATCH] pipeline siivous ja testikattavuuden nosto (#9) Co-authored-by: moilanik Reviewed-on: https://gitea.app.keskikuja.site/niko/gitea-ci-library/pulls/9 --- .gitea/scripts/bats-coverage.sh | 37 +++++ .gitea/scripts/bats-report.sh | 36 +++++ .gitea/scripts/generate-report-index.sh | 30 ++++ .gitea/workflows/build-feature.yml | 132 ++++++------------ .gitea/workflows/ci-engine.yml | 27 ---- .gitea/workflows/ci.yml | 11 ++ .gitea/workflows/config-provider.yml | 31 ++++ .gitea/workflows/gitea-env.conf | 2 + .simplecov | 5 + README.md | 54 ++++++- config.yaml | 3 - docs/adr/0005-provider-consumer.md | 10 +- docs/adr/0006-directory-ownership.md | 70 ++++++++++ docs/ai-context.md | 15 +- docs/architecture.md | 14 +- docs/ci-pipeline-practices.md | 60 ++++++++ docs/shared-scripts.md | 11 +- docs/tickets/0002-dispatch-workflow-sh.md | 7 +- docs/workflows.md | 4 +- scripts/ci-validate.sh | 31 ++++ scripts/dispatch-workflow.sh | 9 +- scripts/publish-git-pages.sh | 34 ++--- scripts/publish.sh | 16 ++- scripts/report-status.sh | 24 ++-- tests/ci-validate.bats | 79 +++++++++++ tests/dispatch-workflow.bats | 47 ++++--- .../step_definitions/commit-status.steps.js | 35 ++--- .../step_definitions/test-execution.steps.js | 16 +-- tests/helpers/mock-api.sh | 4 +- tests/helpers/mock-server.py | 10 +- tests/publish-git-pages.bats | 91 ++++++++++++ tests/publish.bats | 44 ++++++ tests/report-status.bats | 82 ++++------- 33 files changed, 779 insertions(+), 302 deletions(-) create mode 100755 .gitea/scripts/bats-coverage.sh create mode 100644 .gitea/scripts/bats-report.sh create mode 100644 .gitea/scripts/generate-report-index.sh delete mode 100644 .gitea/workflows/ci-engine.yml create mode 100644 .gitea/workflows/config-provider.yml create mode 100644 .gitea/workflows/gitea-env.conf create mode 100644 .simplecov delete mode 100644 config.yaml create mode 100644 docs/adr/0006-directory-ownership.md create mode 100644 scripts/ci-validate.sh create mode 100644 tests/ci-validate.bats create mode 100644 tests/publish-git-pages.bats create mode 100644 tests/publish.bats diff --git a/.gitea/scripts/bats-coverage.sh b/.gitea/scripts/bats-coverage.sh new file mode 100755 index 0000000..43c76d9 --- /dev/null +++ b/.gitea/scripts/bats-coverage.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +WORKSPACE_VOLUME="${1:-}" +REPORT_DIR="${2:-}" + +[ -n "$WORKSPACE_VOLUME" ] || { echo "ERROR: workspace volume name required" >&2; exit 1; } +[ -n "$REPORT_DIR" ] || { echo "ERROR: report directory required" >&2; exit 1; } + +HAS_COVERAGE=false +COVERAGE_SRC="" +if docker run --rm -v "$WORKSPACE_VOLUME":/data alpine sh -c '[ -d /data/coverage ] && ls -A /data/coverage | grep -q .' 2>/dev/null; then + COVERAGE_SRC="/data/coverage" +fi + +if [ -n "$COVERAGE_SRC" ]; then + mkdir -p "$REPORT_DIR/coverage" + docker run --rm -v "$WORKSPACE_VOLUME":/data alpine tar c -C "$COVERAGE_SRC" . | tar x -C "$REPORT_DIR/coverage" + HAS_COVERAGE=true +fi + +cat > "$REPORT_DIR/index.html" << EOF + +Bats report ${GITHUB_SHA:0:8} + +

Bats report ${GITHUB_SHA:0:8}

+' >> "$REPORT_DIR/index.html" diff --git a/.gitea/scripts/bats-report.sh b/.gitea/scripts/bats-report.sh new file mode 100644 index 0000000..e10e178 --- /dev/null +++ b/.gitea/scripts/bats-report.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPORT_DIR="${1:-reports/bats}" +INPUT="$REPORT_DIR/results.txt" +OUTPUT="$REPORT_DIR/test-report.html" + +[ -f "$INPUT" ] || { echo "ERROR: $INPUT not found" >&2; exit 1; } + +TOTAL=$(grep -cE '^(ok|not ok) ' "$INPUT" 2>/dev/null) || TOTAL=0 +PASS=$(grep -c '^ok ' "$INPUT" 2>/dev/null) || PASS=0 +FAIL=$((TOTAL - PASS)) + +{ + echo '' + echo "Bats test report ${GITHUB_SHA:0:8}" + echo '' + echo "

Bats test report ${GITHUB_SHA:0:8}

" + echo "

Total: ${TOTAL} | Passed: ${PASS} | Failed: ${FAIL}

" + echo '' + + while IFS=' ' read -r status num rest; do + case "$status" in + ok) echo "" ;; + not) echo "" ;; + esac + done < <(grep -E '^(ok|not ok) ' "$INPUT") + + echo '
#StatusTest
${num}PASS${rest}
${num}FAIL${rest}
' +} > "$OUTPUT" + +echo "$OUTPUT" diff --git a/.gitea/scripts/generate-report-index.sh b/.gitea/scripts/generate-report-index.sh new file mode 100644 index 0000000..5e63f7a --- /dev/null +++ b/.gitea/scripts/generate-report-index.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +SHA8="${GITHUB_SHA:0:8}" +REPORTS_DIR="reports/${SHA8}" + +mkdir -p "${REPORTS_DIR}" + +BATS_PASS=$(grep -c 'ok' "${REPORTS_DIR}/bats/results.txt" 2>/dev/null || echo 0) +BATS_FAIL=$(grep -c 'not ok' "${REPORTS_DIR}/bats/results.txt" 2>/dev/null || echo 0) +CUCUMBER_PASS=$(jq '.summary.passed // 0' "${REPORTS_DIR}/cucumber/report.json" 2>/dev/null || echo 0) +CUCUMBER_FAIL=$(jq '.summary.failed // 0' "${REPORTS_DIR}/cucumber/report.json" 2>/dev/null || echo 0) + +{ + echo "" + echo "CI report ${SHA8}" + echo "" + echo "

CI report ${SHA8}

" + echo "

Commit: ${GITHUB_SHA}
Branch: ${GITHUB_REF_NAME}
Run: ${GITHUB_RUN_ID}

" + echo "" + echo "" + echo "" + echo "" + echo "" + echo "
SuitePassedFailedReport
Bats${BATS_PASS}${BATS_FAIL}results.txt" + echo " | junit.xml
Cucumber${CUCUMBER_PASS}${CUCUMBER_FAIL}report" + echo " | json
" +} > "${REPORTS_DIR}/index.html" diff --git a/.gitea/workflows/build-feature.yml b/.gitea/workflows/build-feature.yml index 4a6ce87..b6c9b9a 100644 --- a/.gitea/workflows/build-feature.yml +++ b/.gitea/workflows/build-feature.yml @@ -2,24 +2,42 @@ name: Build Feature on: workflow_call: inputs: + env_json: + required: true + type: string bats-image: - required: false + required: true type: string - default: bats/bats:latest cucumber-node-image: - required: false + required: true type: string - default: node:22 + secrets: + GITEA_TOKEN: + required: true + GIT_PAGES_PUBLISH_TOKEN: + required: true + +env: + GITEA_API_URL: ${{ fromJson(inputs.env_json).GITEA_API_URL }} + GIT_PAGES_URL: ${{ fromJson(inputs.env_json).GIT_PAGES_URL }} + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GIT_PAGES_PUBLISH_TOKEN: ${{ secrets.GIT_PAGES_PUBLISH_TOKEN }} jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + repository: niko/gitea-ci-library + path: .ci + + - name: Validate CI config + run: bash .ci/scripts/ci-validate.sh + bats: runs-on: ubuntu-latest - env: - GITEA_API_URL: https://gitea.app.keskikuja.site - GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} - PAGES_HOST: ci-reports.helm-dev.keskikuja.site - GIT_PAGES_PUBLISH_URL: https://ci-reports.helm-dev.keskikuja.site - GIT_PAGES_PUBLISH_TOKEN: ${{ secrets.GIT_PAGES_PUBLISH_TOKEN }} steps: - uses: actions/checkout@v4 - uses: actions/checkout@v4 @@ -35,56 +53,37 @@ jobs: tar c . | docker run --rm -i -v bats-workspace:/data alpine tar x -C /data mkdir -p "reports/${GITHUB_SHA:0:8}/bats" set +e - docker run --rm -v bats-workspace:/data \ + docker run --rm \ + -v bats-workspace:/data \ --entrypoint bash ${{ inputs.bats-image }} \ - -c 'apk add -q lsof python3 jq curl && \ - cd /data && bats tests/ ' \ + -c 'apk add -q lsof python3 jq curl ruby && cd /data && \ + gem install bashcov -v 3.3.0 2>&1 | tail -1 && \ + bashcov -- bats tests/' \ > "reports/${GITHUB_SHA:0:8}/bats/results.txt" 2>&1 BATS_EXIT=$? + bash .ci/.gitea/scripts/bats-coverage.sh bats-workspace "reports/${GITHUB_SHA:0:8}/bats" docker volume rm bats-workspace > /dev/null 2>&1 - { - echo "

Bats tests

" - } > "reports/${GITHUB_SHA:0:8}/bats/index.html" + bash .ci/.gitea/scripts/bats-report.sh "reports/${GITHUB_SHA:0:8}/bats" echo "BATS_EXIT=${BATS_EXIT}" >> "${GITHUB_ENV}" exit ${BATS_EXIT} - name: Publish bats reports if: always() - shell: bash - run: | - bash .ci/scripts/publish-git-pages.sh "reports/${GITHUB_SHA:0:8}/bats" + run: bash .ci/scripts/publish-git-pages.sh bats - name: Set bats commit status if: always() - shell: bash run: | if [ "${BATS_EXIT}" = "0" ]; then - STATUS="success" - DESC="Bats tests" - URL="https://${PAGES_HOST}/${GITHUB_REPOSITORY}/reports/${GITHUB_SHA:0:8}/bats/" + bash .ci/scripts/report-status.sh success "Bats tests" ci-bats bats else - STATUS="failure" - DESC="Bats tests FAILED" - URL="https://${PAGES_HOST}/${GITHUB_REPOSITORY}/reports/${GITHUB_SHA:0:8}/bats/" + bash .ci/scripts/report-status.sh failure "Bats tests FAILED" ci-bats bats fi - bash .ci/scripts/report-status.sh "$STATUS" "$DESC" "$URL" ci-bats cucumber: runs-on: ubuntu-latest container: image: ${{ inputs.cucumber-node-image }} - env: - GITEA_API_URL: https://gitea.app.keskikuja.site - GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} - PAGES_HOST: ci-reports.helm-dev.keskikuja.site - GIT_PAGES_PUBLISH_URL: https://ci-reports.helm-dev.keskikuja.site - GIT_PAGES_PUBLISH_TOKEN: ${{ secrets.GIT_PAGES_PUBLISH_TOKEN }} steps: - uses: actions/checkout@v4 - uses: actions/checkout@v4 @@ -124,40 +123,25 @@ jobs: - name: Publish cucumber reports if: always() - shell: bash run: | if [ "${TOOL_OK}" = "true" ]; then - bash .ci/scripts/publish-git-pages.sh "reports/${GITHUB_SHA:0:8}/cucumber" + bash .ci/scripts/publish-git-pages.sh cucumber fi - name: Set cucumber commit status if: always() - shell: bash run: | if [ "${TOOL_OK}" != "true" ]; then - STATUS="failure" - DESC="Cucumber tool unavailable" - URL="https://gitea.app.keskikuja.site/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + bash .ci/scripts/report-status.sh failure "Cucumber tool unavailable" ci-cucumber elif [ "${CUCUMBER_EXIT}" = "0" ]; then - STATUS="success" - DESC="Cucumber tests passed" - URL="https://${PAGES_HOST}/${GITHUB_REPOSITORY}/reports/${GITHUB_SHA:0:8}/cucumber/" + bash .ci/scripts/report-status.sh success "Cucumber tests passed" ci-cucumber cucumber else - STATUS="failure" - DESC="Cucumber tests FAILED" - URL="https://${PAGES_HOST}/${GITHUB_REPOSITORY}/reports/${GITHUB_SHA:0:8}/cucumber/" + bash .ci/scripts/report-status.sh failure "Cucumber tests FAILED" ci-cucumber cucumber fi - bash .ci/scripts/report-status.sh "$STATUS" "$DESC" "$URL" ci-cucumber build: runs-on: ubuntu-latest needs: [bats, cucumber] - env: - GITEA_API_URL: https://gitea.app.keskikuja.site - GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} - PAGES_HOST: ci-reports.helm-dev.keskikuja.site - GIT_PAGES_PUBLISH_URL: https://ci-reports.helm-dev.keskikuja.site - GIT_PAGES_PUBLISH_TOKEN: ${{ secrets.GIT_PAGES_PUBLISH_TOKEN }} steps: - uses: actions/checkout@v4 - uses: actions/checkout@v4 @@ -166,35 +150,7 @@ jobs: path: .ci - name: Generate report index - shell: bash - run: | - SHA8="${GITHUB_SHA:0:8}" - mkdir -p "reports/${SHA8}" - BATS_PASS=$(grep -c 'ok' "reports/${SHA8}/bats/results.txt" 2>/dev/null || echo 0) - BATS_FAIL=$(grep -c 'not ok' "reports/${SHA8}/bats/results.txt" 2>/dev/null || echo 0) - CUCUMBER_PASS=$(jq '.summary.passed // 0' "reports/${SHA8}/cucumber/report.json" 2>/dev/null || echo 0) - CUCUMBER_FAIL=$(jq '.summary.failed // 0' "reports/${SHA8}/cucumber/report.json" 2>/dev/null || echo 0) - { - echo "" - echo "CI report ${SHA8}" - echo "" - echo "

CI report ${SHA8}

" - echo "

Commit: ${GITHUB_SHA}
Branch: ${GITHUB_REF_NAME}
Run: ${GITHUB_RUN_ID}

" - echo "" - echo "" - echo "" - echo "" - echo "" - echo "
SuitePassedFailedReport
Bats${BATS_PASS}${BATS_FAIL}results.txt" - echo " | junit.xml
Cucumber${CUCUMBER_PASS}${CUCUMBER_FAIL}report" - echo " | json
" - } > "reports/${SHA8}/index.html" + run: bash .ci/.gitea/scripts/generate-report-index.sh - name: Set build commit status - run: | - bash .ci/scripts/report-status.sh success \ - "Build complete" \ - "https://gitea.app.keskikuja.site/niko/gitea-ci-library/actions/runs/${GITHUB_RUN_ID}" \ - ci-build + run: bash .ci/scripts/report-status.sh success "Build complete" ci-build diff --git a/.gitea/workflows/ci-engine.yml b/.gitea/workflows/ci-engine.yml deleted file mode 100644 index ba66fb6..0000000 --- a/.gitea/workflows/ci-engine.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: CI Engine -on: - workflow_call: - inputs: - config-file: - required: true - type: string - secrets: - GITEA_TOKEN: - required: true - GIT_PAGES_PUBLISH_TOKEN: - required: true - -jobs: - publish: - runs-on: ubuntu-latest - env: - GITEA_API_URL: https://gitea.app.keskikuja.site - PAGES_HOST: ci-reports.helm-dev.keskikuja.site - GIT_PAGES_PUBLISH_URL: https://ci-reports.helm-dev.keskikuja.site - GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} - GIT_PAGES_PUBLISH_TOKEN: ${{ secrets.GIT_PAGES_PUBLISH_TOKEN }} - steps: - - uses: actions/checkout@v4 - - - name: Publish reports - run: bash scripts/publish.sh reports diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 095b40c..2bd2a01 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -5,16 +5,27 @@ on: workflow_dispatch: jobs: + load-config: + uses: niko/gitea-ci-library/.gitea/workflows/config-provider.yml@main + with: + config_path: .gitea/workflows/gitea-env.conf + feature: if: github.ref != 'refs/heads/main' + needs: [load-config] uses: niko/gitea-ci-library/.gitea/workflows/build-feature.yml@main secrets: inherit with: + env_json: ${{ needs.load-config.outputs.env_json }} bats-image: bats/bats:latest + cucumber-node-image: node:22 main: if: github.ref == 'refs/heads/main' + needs: [load-config] uses: niko/gitea-ci-library/.gitea/workflows/build-feature.yml@main secrets: inherit with: + env_json: ${{ needs.load-config.outputs.env_json }} bats-image: bats/bats:latest + cucumber-node-image: node:22 diff --git a/.gitea/workflows/config-provider.yml b/.gitea/workflows/config-provider.yml new file mode 100644 index 0000000..af4ad80 --- /dev/null +++ b/.gitea/workflows/config-provider.yml @@ -0,0 +1,31 @@ +name: Config Provider Library +on: + workflow_call: + inputs: + config_path: + required: true + type: string + outputs: + env_json: + value: ${{ jobs.parse-config.outputs.json_data }} + +jobs: + parse-config: + runs-on: ubuntu-latest + outputs: + json_data: ${{ steps.convert.outputs.JSON_OUT }} + steps: + - uses: actions/checkout@v4 + + - id: convert + run: | + JSON_STRING=$(jq -R -s ' + split("\n") + | map(select(length > 0 and (startswith("#") | not))) + | map(split("=")) + | map({(.[0]): .[1]}) + | add + ' "${{ inputs.config_path }}") + + CLEAN_JSON=$(echo "$JSON_STRING" | jq -c .) + echo "JSON_OUT=$CLEAN_JSON" >> "$GITHUB_OUTPUT" diff --git a/.gitea/workflows/gitea-env.conf b/.gitea/workflows/gitea-env.conf new file mode 100644 index 0000000..72dba50 --- /dev/null +++ b/.gitea/workflows/gitea-env.conf @@ -0,0 +1,2 @@ +GITEA_API_URL=https://gitea.app.keskikuja.site +GIT_PAGES_URL=https://ci-reports.helm-dev.keskikuja.site diff --git a/.simplecov b/.simplecov new file mode 100644 index 0000000..4ee20ec --- /dev/null +++ b/.simplecov @@ -0,0 +1,5 @@ +SimpleCov.start do + add_filter '/tests/' + add_filter '/node_modules/' + add_filter '/git-pages/' +end diff --git a/README.md b/README.md index e6df085..644a580 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,14 @@ Consumer kutsuu provideria `uses:`-viittauksella. Ei discoveryä — polku kovak ### Polun muodostus ``` -{owner}/{repo}/.gitea/workflows/ci-engine.yml@{ref} +{owner}/{repo}/.gitea/workflows/build-feature.yml@{ref} ``` | Osa | Mistä | Esimerkki (homelab) | |-----|-------|---------------------| | `owner` | Repopolun ensimmäinen osa — **käyttäjänimi tai org** | `niko` | | `repo` | Repon nimi | `gitea-ci-library` | -| tiedosto | Providerin workflow | `.gitea/workflows/ci-engine.yml` | +| tiedosto | Providerin workflow | `.gitea/workflows/build-feature.yml` | | `@ref` | Tag tai branch provider-repossa | `@v1` (tuotanto) | **Owner ei ole org-pakotettu.** Homelabissa ei välttämättä ole organisaatiotasoa — silloin owner on @@ -36,7 +36,7 @@ Consumerin `ci.yml`: ```yaml jobs: call-engine: - uses: niko/gitea-ci-library/.gitea/workflows/ci-engine.yml@v1 + uses: niko/gitea-ci-library/.gitea/workflows/build-feature.yml@v1 secrets: inherit ``` @@ -56,7 +56,7 @@ niko/gitea-ci-library ←→ niko/gitea-ci-library (mirror tai push-mirror) niko/gitea-ci-library/... niko/gitea-ci-library/... ``` -Mirror pitää `ci-engine.yml`:n ja tagit (`v1`) saatavilla kulloisellakin palvelimella. Tämä korvaa +Mirror pitää `build-feature.yml`:n ja tagit (`v1`) saatavilla kulloisellakin palvelimella. Tämä korvaa provider-repon checkout-hackit workflowissa — binding hoituu Gitean natiivilla `uses:`-mekanismilla. Periaatteet: [tmp/data-flow-design.md](tmp/data-flow-design.md) @@ -180,6 +180,52 @@ Tarkista ennen ensimmäistä ajoa: [Provider-binding](#provider-binding--miten-c Lisätietoa runnerin toiminnasta, konteista ja DinD:stä: [docs/runner.md](docs/runner.md) +## Vaaditut secretit ja muuttujat + +Consumer-repossa on oltava seuraavat asetukset: + +### Repo Actions Secrets (`{repo} → Settings → Actions → Secrets`) + +| Secret | Kuvaus | +|--------|--------| +| `GIT_PAGES_PUBLISH_TOKEN` | Git-pages-palvelimen BasicAuth-token. Nimi on lukittu — tämä tarkka nimi vaaditaan. | + +`GITEA_TOKEN` on Gitean sisäinen secret (`secrets.GITEA_TOKEN`), joka on automauttisesti saatavilla — sitä ei tarvitse erikseen luoda. + +### Config-tiedosto (`.gitea/workflows/gitea-env.conf`) + +Tiedoston **nimi ja polku on lukittu**: `.gitea/workflows/gitea-env.conf` consumer-repon juuressa. +Tämän tiedoston perusteella `config-provider.yml` tuottaa `env_json`-outputin, joka välitetään +workflowille. + +Tiedosto on `key=value`-muotoinen (kuten `.env`). Kommentit ja tyhjät rivit sallittuja. + +**Vaaditut avaimet:** + +| Avain | Kuvaus | +|-------|--------| +| `GITEA_API_URL` | Gitea-palvelimen base URL (esim. `https://gitea.app.example.com`) | +| `GIT_PAGES_URL` | Git-pages-palvelimen URL ilman trailing slash (esim. `https://ci-reports.example.com`) | + +**Validointisäännöt:** +- Arvot eivät saa olla tyhjiä +- Jos avaimen nimessä on `URL`, arvon on alettava `http://` tai `https://` +- Tiedoston on oltava olemassa (muuten job keskeytyy) + +Esimerkki: +``` +GITEA_API_URL=https://gitea.app.example.com +GIT_PAGES_URL=https://ci-reports.example.com +``` + +### Validaatio + +Jokaisen jobin alussa `ci-validate.sh` tarkistaa: +- `.gitea/workflows/gitea-env.conf` on olemassa ja sen arvot ovat validit +- `GITEA_TOKEN` ja `GIT_PAGES_PUBLISH_TOKEN` on asetettu + +Jos validointi epäonnistuu, job keskeytyy exit-koodilla 1 ja Gitean commit-status näyttää epäonnistumisen linkkinä lokiin. + ### Muuta | Muuttuja | Kuvaus | diff --git a/config.yaml b/config.yaml deleted file mode 100644 index 56d23f8..0000000 --- a/config.yaml +++ /dev/null @@ -1,3 +0,0 @@ -GITEA_API_URL: https://gitea.app.keskikuja.site -PAGES_HOST: ci-reports.helm-dev.keskikuja.site -GIT_PAGES_PUBLISH_URL: https://ci-reports.helm-dev.keskikuja.site diff --git a/docs/adr/0005-provider-consumer.md b/docs/adr/0005-provider-consumer.md index b898ae1..5475a70 100644 --- a/docs/adr/0005-provider-consumer.md +++ b/docs/adr/0005-provider-consumer.md @@ -3,7 +3,7 @@ ## Päätös Provider (gitea-ci-library) ja Consumer (mikropalveluprojekti) erotetaan -selkeällä rajapinnalla: `.gitea/workflows/ci-engine.yml` on ainoa pinta, +selkeällä rajapinnalla: `.gitea/workflows/build-feature.yml` on ainoa pinta, jota consumer kutsuu. Kaikki muu providerin koodi (scriptit, git-pages-helmi, retention) on @@ -18,7 +18,7 @@ riippuvuutta. # .gitea/workflows/ci.yml — consumerin repo jobs: ci: - uses: niko/gitea-ci-library/.gitea/workflows/ci-engine.yml@v1 + uses: niko/gitea-ci-library/.gitea/workflows/build-feature.yml@v1 secrets: inherit ``` @@ -44,13 +44,13 @@ Consumer: - Git-pages Helm-chartti - Retention sidecar - Scriptit ja työkalut (toteutus avoin, uudelleenkirjoitettavissa) -- Kaikki paitsi `ci-engine.yml` on sisäistä toteutusta ja voi muuttua +- Kaikki paitsi `build-feature.yml` on sisäistä toteutusta ja voi muuttua ilman versiopäivitystä ## Periaatteet -1. `ci-engine.yml` on **lukittu rajapinta**. Consumer kutsuu tätä, ei - koskaan providerin scriptejä suoraan. `ci-engine.yml` voi muuttua vain +1. `build-feature.yml` on **lukittu rajapinta**. Consumer kutsuu tätä, ei + koskaan providerin scriptejä suoraan. `build-feature.yml` voi muuttua vain version vaihtuessa. 2. Consumer omistaa pipeline-logiikan. Provider ei tiedä mitä testejä ajetaan, missä järjestyksessä tai millä työkaluilla. diff --git a/docs/adr/0006-directory-ownership.md b/docs/adr/0006-directory-ownership.md new file mode 100644 index 0000000..5df4b4b --- /dev/null +++ b/docs/adr/0006-directory-ownership.md @@ -0,0 +1,70 @@ +# 6. Directory ownership — provider vs consumer + +## Päätös + +Provider-repossa (`gitea-ci-library`) kansioiden omistajuus on seuraava: + +| Kansio / Tiedosto | Omistaja | Tyyppi | +|-------------------|----------|--------| +| `.gitea/workflows/` | Sekoitettu | Providerin reusable workflowt + consumerin pipeline | +| `.gitea/workflows/gitea-env.conf` | Consumer | KEY=VALUE config | +| `.gitea/scripts/` | Consumer | Consumer-skriptit | +| `scripts/` | Provider | Providerin sisäiset työkalut | + +## Reusable workflowt — sijaintipakko + +Gitea Actions vaatii, että `uses:`-direktiivillä kutsuttavat workflowt +ovat muodossa `{owner}/{repo}/.gitea/workflows/{file}@{ref}`. + +**Tämä on Gitea Actionsin asettama tekninen rajoite.** Toimivia +polkuja ovat vain: + +``` +# ✅ kelpaa +uses: org/repo/.gitea/workflows/file.yml@branch + +# ❌ eivät kelpaa +uses: org/repo/workflows/file.yml@branch +uses: org/repo/.gitea/workflows/path/file.yml@branch +uses: org/repo/scripts/workflow.yml@branch +``` + +Tästä syystä providerin reusable workflowt (`config-provider.yml`, +`build-feature.yml`) ovat samassa `.gitea/workflows/`-kansiossa consumerin +pipeline-tiedostojen (`ci.yml`) kanssa. + +Erottelu on nimessä ja dokumentaatiossa, ei kansiorakenteessa: +- `config-provider.yml`, `build-feature.yml` — providerin tarjoamia +- `ci.yml` — consumerin omistamia + +## Providerin `scripts/` (juuressa) + +Providerin sisäiset työkalut. Consumer ei koskaan kutsu näitä suoraan — +vain providerin workflowt kutsuvat tupla checkoutin kautta: +`.ci/scripts/publish-git-pages.sh`. + +Consumerilla ei ole suoraa polkua näihin tiedostoihin ilman providerin +workflowa. + +## Consumerin `.gitea/scripts/` + +Consumerin omat skriptit, osana consumerin pipeline-logiikkaa. +Kutsutaan consumerin workflowista ilman tupla checkouttia: +`.gitea/scripts/bats-report.sh`. + +## Consumerin `.gitea/workflows/gitea-env.conf` + +Consumerin konfiguraatiotiedosto. Providerin `config-provider.yml` +lukee tämän ja muuntaa JSONiksi, mutta consumer omistaa sisällön. + +## Vaikutukset + +- Provider voi muuttaa `scripts/` ja `config-provider.yml` sisältöä + ilman consumerin hyväksyntää (versiovaihdon yhteydessä) +- Consumer voi muuttaa `.gitea/workflows/ci.yml`, + `.gitea/workflows/build-feature.yml` ja `.gitea/scripts/` sisältöä + ilman providerin muutoksia +- Providerin workflowt käyttävät `.ci/scripts/...` -polkua (tupla checkout) +- Consumerin workflowt käyttävät `.gitea/scripts/...` -polkua (natiivi checkout) +- Sekä provider että consumer jakavat `.gitea/workflows/` — tämä on + Gitea Actionsin tekninen rajoite, ei suunnittelupäätös diff --git a/docs/ai-context.md b/docs/ai-context.md index c5b2a0e..ebf5faf 100644 --- a/docs/ai-context.md +++ b/docs/ai-context.md @@ -17,8 +17,7 @@ Tämä repo on käytännössä monorepo, jossa on kaksi itsenäistä osaa: ### 1. Juuri (`gitea-ci-library`) Provider-kirjasto: reusable workflowt, scriptit, ADRt, dokumentaatio. -Rajapinta: `.gitea/workflows/ci-engine.yml` — ainoa pinta, jota consumerit -kutsuvat. +Consumer kutsuu `build-feature.yml`-workflowa `uses:`-direktiivillä. ### 2. `git-pages/` — oma kokonaisuus Helm-chartti Codeberg git-pagesille. Täysin itsenäinen — oma dokumentaatio, @@ -32,7 +31,7 @@ Ohut ja yksiselitteinen: ``` scripts/publish-git-pages.sh - → PATCH tar osoitteeseen PAGES_HOST + → PATCH tar osoitteeseen GIT_PAGES_URL → palauttaa BASE URL:n git-pages tarjoaa: @@ -46,9 +45,9 @@ blob-arkkitehtuuri). Git-pages ei tiedä workflowista, scripteistä tai provider-logiikasta. ## Architecture (POC-tila) -- **Provider & Consumer -malli**: `ci-engine.yml` on lukittu rajapinta. +- **Provider & Consumer -malli**: `build-feature.yml` on lukittu rajapinta. ADR 0005. -- **Raporttien hostaus**: git-pages Helm-chartilla (`git-pages/`). +- **Raporttien hostaus**: git-pages Helm-chartilla (`git-pages/`), `GIT_PAGES_URL` määrittää perusosoitteen. - **Retention**: sidecar samassa podissa, HTTP API localhost:3000, Gitea API branch-check. - **Commit-status**: Gitea Actions näyttää automaattisesti. API vain @@ -59,19 +58,19 @@ provider-logiikasta. | Path | Purpose | |---|---| -| `.gitea/workflows/` | `ci-engine.yml` (ainoa reusable workflow POC-vaiheessa) | +| `.gitea/workflows/` | Reusable workflowt (`build-feature.yml`, `config-provider.yml`) | | `scripts/` | `publish-git-pages.sh`, `report-status.sh`, `dispatch-workflow.sh` | | **`git-pages/`** | **Oma kokonaisuus: Helm-chartti + docs + retention** | | `docs/` | Root-tason arkkitehtuuri, ADRt (0001–0005) | | `docs/adr/` | Architecture Decision Records | | `tests/` | Bats-testit skripteille | -| `.gitea/workflows/ci.yml` | Dogfood — kutsuu `ci-engine.yml`:a | +| `.gitea/workflows/ci.yml` | Dogfood — kutsuu `build-feature.yml`:a | **Tarkemmat git-pages-asiat:** `git-pages/docs/` (implementation-notes, architecture, design-rationale, secrets, tech-stack). ## Key Technical Decisions -- **Provider & Consumer**: `ci-engine.yml` lukittu rajapinta, muu koodi +- **Provider & Consumer**: `build-feature.yml` lukittu rajapinta, muu koodi vapaasti muutettavissa - **Vain Gitea, vain reusable workflowt**: ei custom actioneita, ei multi-platform diff --git a/docs/architecture.md b/docs/architecture.md index 3e6bad3..fa8f619 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,9 +1,6 @@ # Architecture — Gitea Actions CI -kirjasto -> ⚠️ POC-vaihe. Tämä dokumentti kuvaa suunniteltua arkkitehtuuria. Toteutus -> on edelleen kehitysvaiheessa (`ci-engine.yml` on ainoa reusable workflow). -> Odota uudelleenkirjoitusta ennen kuin luotat tähän dokumenttiin. -> +> ⚠️ POC-vaihe. Tämä dokumentti kuvaa suunniteltua arkkitehtuuria. > Normatiivinen lähde: ADR 0004, ADR 0005, `docs/design-rationale.md`. --- @@ -21,7 +18,7 @@ Kirjasto on Gitea-spesifi. Raportit hallinnoidaan git-pages Helm-chartilla | Rooli | Kuvaus | |-------|--------| -| **Provider** | `gitea-ci-library` — tarjoaa `ci-engine.yml`:n (lukittu rajapinta) sekä scriptit | +| **Provider** | `gitea-ci-library` — tarjoaa `build-feature.yml` (lukittu rajapinta) sekä scriptit | | **Consumer** | Mikropalveluprojekti — kutsuu `uses:`-direktiivillä, omistaa pipeline-logiikan | Tarkemmin: ADR 0005. @@ -30,10 +27,10 @@ Tarkemmin: ADR 0005. | Komponentti | Tila | |-------------|------| -| `ci-engine.yml` | Toimii POC-tasolla. Ainoa reusable workflow. | +| `build-feature.yml` | Toimii. Ainoa reusable workflow. | | `publish-git-pages.sh` | Toimii. PATCH tar git-pagesiin. | | `report-status.sh` | Toimii. POSTaa commit-status (vain custom-linkkiin). | -| `dispatch-workflow.sh` | Suunniteltu, ei toteutettu POCissa. | +| `dispatch-workflow.sh` | Toimii. Dispatchee workflown ja pollaa valmistumista. | | `git-pages/` | Helm-chartti raporttien hostaukseen. Oma kokonaisuus, tarkemmin: `git-pages/docs/`. | ## Ulkoiset palvelut @@ -43,10 +40,9 @@ Tarkemmin: ADR 0005. | **Gitea REST API** | Commit-status, workflow-dispatch, run-pollaus | | **Gitea Packages** | Docker-imagen säilytys | | **git-pages** | Raporttien hostaus | -| **SonarQube** | Koodin laadun analyysi (suunniteltu) | ## Arkkitehtuuriset rajoitteet -- `ci-engine.yml` on ainoa consumerin kutsuma rajapinta (ADR 0005) +- `build-feature.yml` on ainoa consumerin kutsuma rajapinta (ADR 0005) - Gitea Actionsin natiivi commit-status on ensisijainen (ADR 0004) - Raportit ovat julkisia URL:lla (osoite tunnettava) diff --git a/docs/ci-pipeline-practices.md b/docs/ci-pipeline-practices.md index 5c05661..0115681 100644 --- a/docs/ci-pipeline-practices.md +++ b/docs/ci-pipeline-practices.md @@ -50,6 +50,53 @@ Käytäntö: - Jos `docker run` tarvitsee env-arvoja, välitä ne eksplisiittisesti `-e VAR`-lipulla - `GITHUB_ENV` on validi tapa välittää arvoja stepien välille samassa jobissa, mutta ei leviä `docker run`-kontteihin ilman `-e`-lippua +### Cross-job config propagation (validated 2026-06-13) + +Config-arvojen vienti kaikkiin jobeihin ilman toistoa vaatii kahden +mekanismin ketjuttamista: + +1. **`needs` + `with:`** — `jobs..with.` tukee + `needs`-kontekstia. Tämä mahdollistaa sen, että yhden jobin outputit + voidaan välittää toiselle reusable workflowille inputeina. +2. **Workflow `env:`** — ainoa natiivi mekanismi, joka tekee arvoista + näkyviä kaikissa jobeissa automaattisesti (POC validioitu). + +Ketju toimii näin: + +``` +gitea-env.conf → config-provider.yml → env_json (yksi JSON-string) + (1) (2) + ↓ + ci.yml with: env_json + ${{ needs.load-config.outputs.env_json }} + (3) + ↓ + build-feature.yml workflow env: + GITEA_API_URL: ${{ fromJson(inputs.env_json).GITEA_API_URL }} + (4) + ↓ + kaikki jobit → $GITEA_API_URL, $GIT_PAGES_URL jne. + (5) +``` + +Vaiheet: +1. Consumer määrittelee arvot `gitea-env.conf`:ssä (KEY=VALUE) +2. `config-provider.yml` lukee confin ja tuottaa yhden JSON-stringin outputina +3. `ci.yml` välittää JSONin `needs` + `with:` -ketjulla +4. `build-feature.yml` purkaa arvot workflow `env:`-tasolle `fromJson()`:lla +5. Kaikki jobit käyttävät valmiita env-muuttujia (`$GIT_PAGES_URL` jne.) + +Avainkomponentit: +- **config-provider.yml** — reusable workflow, joka muuntaa conf-tiedoston + yhdeksi JSON-outputiksi. Yksi output riittää, ei per-key outputteja. +- **`jobs..with`** — tukee `needs`-kontekstia (Gitea Actions, + kuten GitHub Actions). Tämä on kriittinen yksityiskohta: ilman tätä + config-arvoja ei voi välittää reusable workflowille dynaamisesti. +- **workflow `env:`** — ainoa tapa jakaa arvot kaikkiin jobeihin. + `fromJson(inputs.env_json).KEY` purkaa yksittäiset arvot ilman toistoa. +- **Per-job `env:`** — sisältää vain secretit (`GITEA_TOKEN`, + `GIT_PAGES_PUBLISH_TOKEN`), ei config-arvoja. + ## 5. Pipeline Provides All Dependencies - Ei luottamusta runnerin esiasennettuihin työkaluihin @@ -63,3 +110,16 @@ Käytäntö: - Rinnakkaiset jobit (bats + cucumber) — tuloksia saa heti kun valmistuu - Jokainen testisetti omassa jobissaan - Finalize/build voi kerätä yhteenvedon (ei julkaista summarya jos kenelläkään ei ole linkkiä) + +## 7. Inline Logic Threshold + +Logiikka workflow YAML:ssa on hauras: YAML:n sisennys, heredocit ja +kenoviivat tuottavat helposti toimimattomia steppejä. + +**Kynnys siirtää scriptiksi:** heti kun steppiin tulee ehtoja, silmukoita, +tai yli 3 riviä inline-koodia, siirrä omaksi scriptikseen `.gitea/scripts/`- +kansioon. + +Esimerkki: coverage-datan purku ja navigointi-indexin luonti oli aluksi +inline-heredocina workflow YAML:ssa. Siirto omaan `bats-coverage.sh`-scriptiin +teki siitä luettavan, testattavan ja muokattavan ilman YAML-muotoiluriskejä. diff --git a/docs/shared-scripts.md b/docs/shared-scripts.md index be8677a..b50fa25 100644 --- a/docs/shared-scripts.md +++ b/docs/shared-scripts.md @@ -67,7 +67,7 @@ Dispatchaa workflow'n toisessa repossa ja pollaa sen valmistumista synkronisesti ### Rajapinta ```bash -dispatch-workflow.sh [timeout_minutes] +dispatch-workflow.sh [timeout_minutes] ``` | Parametri | Pakollinen | Kuvaus | @@ -76,6 +76,8 @@ dispatch-workflow.sh [timeout_ | `workflow_file` | Kyllä | Workflow-tiedoston nimi (esim. `test.yml`) | | `ref` | Kyllä | Branch | | `inputs_json` | Kyllä | JSON-objekti input-parametreina | +| `gitea_api_url` | Kyllä | Gitean API-URL (esim. `https://gitea.example.com`) | +| `gitea_token` | Kyllä | Gitea API -token | | `timeout_minutes` | Ei | Oletus: 360 (6 tuntia) | ### Toiminta @@ -90,7 +92,8 @@ dispatch-workflow.sh [timeout_ ```bash dispatch-workflow.sh "tests/integration" "test.yml" "main" \ - '{"version":"1.2.3","tags":"@smoke","root_commit":"abc123","root_repo":"services/temperature-store"}' + '{"version":"1.2.3","tags":"@smoke","root_commit":"abc123","root_repo":"services/temperature-store"}' \ + "https://gitea.example.com" "gtp_abc123" ``` --- @@ -181,8 +184,8 @@ Skriptit lukevat nämä Gitea Actionsin ympäristömuuttujat: | Muuttuja | Lähde | Käyttäjä | |----------|-------|----------| -| `GITEA_API_URL` | Org variable | Kaikki skriptit | -| `GITEA_TOKEN` | Org secret | `report-status.sh`, `dispatch-workflow.sh`, `tag-commit.sh` | +| `GITEA_API_URL` | Org variable | `report-status.sh` | +| `GITEA_TOKEN` | Org secret | `report-status.sh`, `tag-commit.sh` | | `MINIO_BASE_URL` | Org variable | `push-reports.sh` | | `MINIO_ACCESS_KEY` | Org secret | `push-reports.sh` | | `MINIO_SECRET_KEY` | Org secret | `push-reports.sh` | diff --git a/docs/tickets/0002-dispatch-workflow-sh.md b/docs/tickets/0002-dispatch-workflow-sh.md index d4012d0..9a5ec11 100644 --- a/docs/tickets/0002-dispatch-workflow-sh.md +++ b/docs/tickets/0002-dispatch-workflow-sh.md @@ -54,7 +54,7 @@ Dispatchaa workflow toisessa repossa ja pollaa sen valmistumista synkronisesti. ## Rajapinta ```bash -dispatch-workflow.sh [timeout_minutes] +dispatch-workflow.sh [timeout_minutes] ``` | Parametri | Pakollinen | Kuvaus | @@ -63,6 +63,8 @@ dispatch-workflow.sh [timeout_ | `workflow_file` | Kyllä | Workflow-tiedosto (esim. `test.yml`) | | `ref` | Kyllä | Branch | | `inputs_json` | Kyllä | JSON-objekti input-parametreina | +| `gitea_api_url` | Kyllä | Gitean API-URL | +| `gitea_token` | Kyllä | Gitea API -token | | `timeout_minutes` | Ei | Oletus: 360 | ## API-kutsut @@ -77,7 +79,8 @@ dispatch-workflow.sh [timeout_ ```bash dispatch-workflow.sh "tests/integration" "test.yml" "main" \ - '{"version":"1.2.3","tags":"@smoke","root_commit":"abc123","root_repo":"services/temperature-store"}' + '{"version":"1.2.3","tags":"@smoke","root_commit":"abc123","root_repo":"services/temperature-store"}' \ + "https://gitea.example.com" "gtp_abc123" ``` ## Verifiointi diff --git a/docs/workflows.md b/docs/workflows.md index a6bd90b..1544372 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -1,8 +1,8 @@ # Reusable workflowt > ⚠️ **POC-vaihe.** Tämä dokumentti kuvaa suunniteltuja workflow'ta -> (ci-feature, ci-master, deploy, test). POCissa on toteutettu vain -> `ci-engine.yml`. Uudelleenkirjoitus odottaa. +> (ci-feature, ci-master, deploy, test). POCissa on toteutettu +> `build-feature.yml`. Uudelleenkirjoitus odottaa. --- diff --git a/scripts/ci-validate.sh b/scripts/ci-validate.sh new file mode 100644 index 0000000..f79cbe0 --- /dev/null +++ b/scripts/ci-validate.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +CONF_FILE="${CI_CONF_FILE:-.gitea/workflows/gitea-env.conf}" +ERRORS=0 + +[ -f "$CONF_FILE" ] || { echo "ERROR: $CONF_FILE not found — checkout missing?" >&2; exit 1; } + +echo "Reading $CONF_FILE..." + +while IFS='=' read -r key value || [ -n "$key" ]; do + key=$(echo "$key" | xargs) + value=$(echo "$value" | xargs) + [ -z "$key" ] && continue + [[ "$key" == "#"* ]] && continue + [ -z "$value" ] && echo "ERROR: $key is empty in $CONF_FILE" >&2 && ERRORS=1 + if [ -n "$value" ] && [[ "$key" == *"URL"* ]] && [[ "$value" != http://* ]] && [[ "$value" != https://* ]]; then + echo "ERROR: $key should be a URL (http/https), got: $value" >&2 + ERRORS=1 + fi +done < "$CONF_FILE" + +[ -z "${GITEA_TOKEN:-}" ] && echo "ERROR: GITEA_TOKEN secret is not set" >&2 && ERRORS=1 +[ -z "${GIT_PAGES_PUBLISH_TOKEN:-}" ] && echo "ERROR: GIT_PAGES_PUBLISH_TOKEN secret is not set" >&2 && ERRORS=1 + +if [ "$ERRORS" -ne 0 ]; then + echo "FATAL: CI config validation failed" >&2 + exit 1 +fi + +echo "OK: all CI env vars validated" diff --git a/scripts/dispatch-workflow.sh b/scripts/dispatch-workflow.sh index 78905b8..1564c84 100755 --- a/scripts/dispatch-workflow.sh +++ b/scripts/dispatch-workflow.sh @@ -1,20 +1,21 @@ #!/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 - TARGET_REPO="${1:-}" WORKFLOW_FILE="${2:-}" REF="${3:-}" INPUTS_JSON="${4:-}" -TIMEOUT_MINUTES="${5:-360}" +GITEA_API_URL="${5:-}" +GITEA_TOKEN="${6:-}" +TIMEOUT_MINUTES="${7:-360}" POLL_INTERVAL="${DISPATCH_POLL_INTERVAL:-10}" [ -z "$TARGET_REPO" ] && echo "ERROR: target_repo argument is required" >&2 && exit 1 [ -z "$WORKFLOW_FILE" ] && echo "ERROR: workflow_file argument is required" >&2 && exit 1 [ -z "$REF" ] && echo "ERROR: ref argument is required" >&2 && exit 1 [ -z "$INPUTS_JSON" ] && echo "ERROR: inputs_json argument is required" >&2 && exit 1 +[ -z "$GITEA_API_URL" ] && echo "ERROR: gitea_api_url argument is required" >&2 && exit 1 +[ -z "$GITEA_TOKEN" ] && echo "ERROR: gitea_token argument is required" >&2 && exit 1 DISPATCH_URL="$GITEA_API_URL/api/v1/repos/$TARGET_REPO/actions/workflows/$WORKFLOW_FILE/dispatches" DISPATCH_BODY=$(jq -nc --arg ref "$REF" --argjson inputs "$INPUTS_JSON" '{ref: $ref, inputs: $inputs}') diff --git a/scripts/publish-git-pages.sh b/scripts/publish-git-pages.sh index 61065ab..81228a7 100755 --- a/scripts/publish-git-pages.sh +++ b/scripts/publish-git-pages.sh @@ -1,29 +1,25 @@ #!/usr/bin/env bash -# Publish a report directory to git-pages apex index-site via Traefik (BasicAuth). -# Public URL: https://{PAGES_HOST}/{owner}/{repo}/reports/{sha8}/index.html set -euo pipefail -REPORT_DIR="${1:-}" -PAGES_HOST="${PAGES_HOST:-}" -GIT_PAGES_PUBLISH_URL="${GIT_PAGES_PUBLISH_URL:-https://pages.helm-dev.keskikuja.site}" -GIT_PAGES_PUBLISH_TOKEN="${GIT_PAGES_PUBLISH_TOKEN:-}" -GIT_PAGES_PUBLISH_USER="${GIT_PAGES_PUBLISH_USER:-publish}" -REPO_SLUG="${GITHUB_REPOSITORY:-}" +SUITE_PATH="${1:-}" -[ -n "$REPORT_DIR" ] || { echo "ERROR: report directory argument required" >&2; exit 1; } -[ -d "$REPORT_DIR" ] || { echo "ERROR: not a directory: $REPORT_DIR" >&2; exit 1; } -[ -n "$PAGES_HOST" ] || { echo "ERROR: PAGES_HOST is not set" >&2; exit 1; } -[ -n "$GIT_PAGES_PUBLISH_TOKEN" ] || { echo "ERROR: GIT_PAGES_PUBLISH_TOKEN is not set" >&2; exit 1; } -[ -n "$REPO_SLUG" ] || { echo "ERROR: GITHUB_REPOSITORY is not set" >&2; exit 1; } +[ -n "$SUITE_PATH" ] || { echo "ERROR: suite_path argument required" >&2; exit 1; } +[ -n "${GITEA_API_URL:-}" ] || { echo "ERROR: GITEA_API_URL is not set" >&2; exit 1; } +[ -n "${GIT_PAGES_URL:-}" ] || { echo "ERROR: GIT_PAGES_URL is not set" >&2; exit 1; } +[ -n "${GIT_PAGES_PUBLISH_TOKEN:-}" ] || { echo "ERROR: GIT_PAGES_PUBLISH_TOKEN is not set" >&2; exit 1; } +[ -n "${GITHUB_REPOSITORY:-}" ] || { echo "ERROR: GITHUB_REPOSITORY is not set" >&2; exit 1; } [ -n "${GITHUB_SHA:-}" ] || { echo "ERROR: GITHUB_SHA is not set" >&2; exit 1; } -OWNER="${REPO_SLUG%%/*}" -REPO="${REPO_SLUG##*/}" +OWNER="${GITHUB_REPOSITORY%%/*}" +REPO="${GITHUB_REPOSITORY##*/}" SHA8="${GITHUB_SHA:0:8}" -REPORT_BASE="https://${PAGES_HOST}/${OWNER}/${REPO}/reports/${SHA8}" +PAGES_USER="${GIT_PAGES_PUBLISH_USER:-publish}" +REPORT_DIR="reports/${SHA8}/${SUITE_PATH%/}" +REPORT_BASE="${GIT_PAGES_URL}/${OWNER}/${REPO}/reports/${SHA8}" -PUBLISH_BASE="${GIT_PAGES_PUBLISH_URL%/}" -PUBLISH_SITE_URL="${PUBLISH_BASE}/" +[ -d "$REPORT_DIR" ] || { echo "ERROR: not a directory: $REPORT_DIR" >&2; exit 1; } + +PUBLISH_SITE_URL="${GIT_PAGES_URL}/" WORK=$(mktemp -d) TAR=$(mktemp) @@ -45,7 +41,7 @@ find "$WORK/$OWNER" \( -type f -o -type l \) -print | sed "s|^${WORK}/||" | tar publish() { local method="$1" curl -sS -X "$method" "$PUBLISH_SITE_URL" \ - -u "${GIT_PAGES_PUBLISH_USER}:${GIT_PAGES_PUBLISH_TOKEN}" \ + -u "${PAGES_USER}:${GIT_PAGES_PUBLISH_TOKEN}" \ -H "Content-Type: application/x-tar" \ -H "Atomic: no" \ -H "Create-Parents: yes" \ diff --git a/scripts/publish.sh b/scripts/publish.sh index 299f3ce..eb9b53e 100644 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -1,10 +1,16 @@ #!/usr/bin/env bash -# Vie raportit git-pagesiin + commit-status linkillä set -euo pipefail -REPORT_DIR="${1:-reports}" -PAGES_HOST="${PAGES_HOST:-ci-reports.helm-dev.keskikuja.site}" +SUITE_PATH="${1:-}" -REPORT_URL=$(bash "$(dirname $0)/publish-git-pages.sh" "$REPORT_DIR") +[ -n "$SUITE_PATH" ] || { echo "ERROR: suite_path argument required" >&2; exit 1; } +[ -n "${GITEA_API_URL:-}" ] || { echo "ERROR: GITEA_API_URL is not set" >&2; exit 1; } +[ -n "${GITEA_TOKEN:-}" ] || { echo "ERROR: GITEA_TOKEN is not set" >&2; exit 1; } +[ -n "${GIT_PAGES_URL:-}" ] || { echo "ERROR: GIT_PAGES_URL is not set" >&2; exit 1; } +[ -n "${GIT_PAGES_PUBLISH_TOKEN:-}" ] || { echo "ERROR: GIT_PAGES_PUBLISH_TOKEN is not set" >&2; exit 1; } + +SCRIPT_DIR="$(dirname "$0")" + +REPORT_URL=$(bash "$SCRIPT_DIR/publish-git-pages.sh" "$SUITE_PATH") echo "Published: $REPORT_URL" -bash "$(dirname $0)/report-status.sh" success "Reports published" "$REPORT_URL" ci-report +bash "$SCRIPT_DIR/report-status.sh" success "Reports published" "ci-report" diff --git a/scripts/report-status.sh b/scripts/report-status.sh index a8d7a95..d2c10ce 100755 --- a/scripts/report-status.sh +++ b/scripts/report-status.sh @@ -1,28 +1,26 @@ #!/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:-}" +KEY="${3:-commit-${GITHUB_SHA:0:8}}" +SUITE="${4:-}" [ -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 +[ -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 -if [ -n "$ROOT_COMMIT" ] && [ -n "$ROOT_REPO" ]; then - REPO="$ROOT_REPO" - COMMIT="$ROOT_COMMIT" +if [ -n "$SUITE" ]; then + SUITE="${SUITE%/}/" + URL="${GIT_PAGES_URL}/${GITHUB_REPOSITORY}/reports/${GITHUB_SHA:0:8}/${SUITE}" else - REPO="${GITHUB_REPOSITORY:-}" - COMMIT="${GITHUB_SHA:-}" + URL="${GITEA_API_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" fi +REPO="${ROOT_REPO:-${GITHUB_REPOSITORY:-}}" +COMMIT="${ROOT_COMMIT:-${GITHUB_SHA:-}}" + [ -z "$REPO" ] && echo "ERROR: GITHUB_REPOSITORY is not set" >&2 && exit 1 [ -z "$COMMIT" ] && echo "ERROR: GITHUB_SHA is not set" >&2 && exit 1 diff --git a/tests/ci-validate.bats b/tests/ci-validate.bats new file mode 100644 index 0000000..46e8f9c --- /dev/null +++ b/tests/ci-validate.bats @@ -0,0 +1,79 @@ +#!/usr/bin/env bats + +setup() { + export CONF_FILE=$(mktemp) + export CI_CONF_FILE="$CONF_FILE" +} + +teardown() { + rm -f "$CONF_FILE" +} + +@test "missing config file → exit 1" { + export CI_CONF_FILE="/nonexistent/path/$(date +%s).conf" + run bash scripts/ci-validate.sh + [ "$status" -eq 1 ] + [[ "$output" == *"ERROR"* ]] +} + +@test "empty value in config → exit 1" { + cat > "$CONF_FILE" < "$CONF_FILE" < "$CONF_FILE" < "$CONF_FILE" < "$CONF_FILE" < "$CONF_FILE" </dev/null | xargs -r kill -9 2>/dev/null || true', { stdio: 'ignore' }); execSync('sleep 0.4', { stdio: 'ignore' }); @@ -62,7 +54,7 @@ function setupMock(seqJson) { } function runDispatch(args) { - return bash(`${envBlock()}; bash "${DISPATCH_SCRIPT}" ${args}`); + return bash(`export DISPATCH_POLL_INTERVAL="0.1"; bash "${DISPATCH_SCRIPT}" ${args}`); } Given('a deployment has completed in the target environment', function () { @@ -77,7 +69,7 @@ When('a test workflow is dispatched to a test project', function () { { code: 200, body: { workflow_runs: [{ id: 1, 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"}\''); + const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\' "http://localhost:18080" "test-token-abc123"'); this.dispatchResult = r.status; }); @@ -95,7 +87,7 @@ When('a test workflow is dispatched and the tests fail', function () { { code: 200, body: { workflow_runs: [{ id: 1, 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"}\''); + const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\' "http://localhost:18080" "test-token-abc123"'); this.dispatchResult = r.status; }); @@ -111,7 +103,7 @@ When('a test workflow is dispatched but does not finish within the allowed time' { code: 200, body: { id: 1, status: 'running' } }, { code: 200, body: { id: 1, status: 'running' } }, ])); - const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\' "0.001"'); + const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\' "http://localhost:18080" "test-token-abc123" "0.001"'); this.dispatchResult = r.status; }); diff --git a/tests/helpers/mock-api.sh b/tests/helpers/mock-api.sh index 7c77d69..1bdc292 100644 --- a/tests/helpers/mock-api.sh +++ b/tests/helpers/mock-api.sh @@ -13,12 +13,12 @@ _kill_port() { local pids pids=$(lsof -ti ":$MOCK_PORT" 2>/dev/null) || true [ -n "$pids" ] && kill -9 $pids 2>/dev/null || true - sleep 0.3 + sleep 0.5 } _wait_port_free() { local i=0 - while lsof -ti ":$MOCK_PORT" >/dev/null 2>&1 && [ $i -lt 30 ]; do + while lsof -ti ":$MOCK_PORT" >/dev/null 2>&1 && [ $i -lt 50 ]; do sleep 0.1 i=$((i + 1)) done diff --git a/tests/helpers/mock-server.py b/tests/helpers/mock-server.py index 2b4cbb1..57249d6 100644 --- a/tests/helpers/mock-server.py +++ b/tests/helpers/mock-server.py @@ -46,7 +46,7 @@ class H(http.server.BaseHTTPRequestHandler): def _log_request(self, method): path = self.path content_len = int(self.headers.get('Content-Length', 0)) - body = self.rfile.read(content_len).decode() if content_len else '' + body = self.rfile.read(content_len).decode(errors='replace') if content_len else '' line = f'{method} {path}\n{body}\n' with open(REQ_FILE, 'a') as f: f.write(line) @@ -67,6 +67,14 @@ class H(http.server.BaseHTTPRequestHandler): self.end_headers() self.wfile.write(body.encode()) + def do_PATCH(self): + self._log_request('PATCH') + code, body = self._get_response() + self.send_response(code) + self.send_header('Content-Type', 'application/json') + self.end_headers() + self.wfile.write(body.encode()) + def log_message(self, format, *args): pass diff --git a/tests/publish-git-pages.bats b/tests/publish-git-pages.bats new file mode 100644 index 0000000..1e62214 --- /dev/null +++ b/tests/publish-git-pages.bats @@ -0,0 +1,91 @@ +#!/usr/bin/env bats + +setup() { + source tests/helpers/mock-api.sh + export GITEA_API_URL="http://localhost:18080" + export GIT_PAGES_URL="http://localhost:18080" + export GIT_PAGES_PUBLISH_TOKEN="publish-token-abc" + export GITHUB_REPOSITORY="test-owner/test-repo" + export GITHUB_SHA="abc123def456789012345678901234567890abcd" + + REPORT_DIR="reports/abc123de/unit-tests" + mkdir -p "$REPORT_DIR" + echo "test" > "$REPORT_DIR/index.html" +} + +teardown() { + mock_stop + rm -rf "reports/abc123de" +} + +@test "missing suite_path argument → exit 1" { + run bash scripts/publish-git-pages.sh "" + [ "$status" -eq 1 ] + [[ "$output" == *"ERROR"* ]] +} + +@test "missing GITEA_API_URL → exit 1" { + unset GITEA_API_URL + run bash scripts/publish-git-pages.sh "unit-tests" + [ "$status" -eq 1 ] + [[ "$output" == *"GITEA_API_URL"* ]] +} + +@test "missing GIT_PAGES_URL → exit 1" { + unset GIT_PAGES_URL + run bash scripts/publish-git-pages.sh "unit-tests" + [ "$status" -eq 1 ] + [[ "$output" == *"GIT_PAGES_URL"* ]] +} + +@test "missing GIT_PAGES_PUBLISH_TOKEN → exit 1" { + unset GIT_PAGES_PUBLISH_TOKEN + run bash scripts/publish-git-pages.sh "unit-tests" + [ "$status" -eq 1 ] + [[ "$output" == *"GIT_PAGES_PUBLISH_TOKEN"* ]] +} + +@test "missing GITHUB_REPOSITORY → exit 1" { + unset GITHUB_REPOSITORY + run bash scripts/publish-git-pages.sh "unit-tests" + [ "$status" -eq 1 ] + [[ "$output" == *"GITHUB_REPOSITORY"* ]] +} + +@test "suite path is not a directory → exit 1" { + run bash scripts/publish-git-pages.sh "nonexistent" + [ "$status" -eq 1 ] + [[ "$output" == *"not a directory"* ]] +} + +@test "valid publish returns report base URL" { + mock_set_sequence '[ + {"code":200,"body":"published"} + ]' + mock_start + run bash scripts/publish-git-pages.sh "unit-tests" + [ "$status" -eq 0 ] + [[ "$output" == "http://localhost:18080/test-owner/test-repo/reports/abc123de" ]] +} + +@test "publish with suite subpath" { + mkdir -p "reports/abc123de/sub/suite" + echo "sub" > "reports/abc123de/sub/suite/result.html" + mock_set_sequence '[ + {"code":200,"body":"published"} + ]' + mock_start + run bash scripts/publish-git-pages.sh "sub/suite" + [ "$status" -eq 0 ] + [[ "$output" == "http://localhost:18080/test-owner/test-repo/reports/abc123de" ]] +} + +@test "git-pages returns HTTP 500 → exit 1" { + mock_set_sequence '[ + {"code":500,"body":"internal error"} + ]' + mock_start + run bash scripts/publish-git-pages.sh "unit-tests" + [ "$status" -eq 1 ] + [[ "$output" == *"500"* ]] +} diff --git a/tests/publish.bats b/tests/publish.bats new file mode 100644 index 0000000..36832b4 --- /dev/null +++ b/tests/publish.bats @@ -0,0 +1,44 @@ +#!/usr/bin/env bats + +setup() { + export GITEA_API_URL="http://localhost:18080" + export GITEA_TOKEN="test-token-abc" + export GIT_PAGES_URL="http://localhost:18080" + export GIT_PAGES_PUBLISH_TOKEN="publish-token-abc" + export GITHUB_REPOSITORY="test-owner/test-repo" + export GITHUB_SHA="abc123def456789012345678901234567890abcd" +} + +@test "missing suite_path argument → exit 1" { + run bash scripts/publish.sh "" + [ "$status" -eq 1 ] + [[ "$output" == *"ERROR"* ]] +} + +@test "missing GITEA_API_URL → exit 1" { + unset GITEA_API_URL + run bash scripts/publish.sh "unit-tests" + [ "$status" -eq 1 ] + [[ "$output" == *"GITEA_API_URL"* ]] +} + +@test "missing GITEA_TOKEN → exit 1" { + unset GITEA_TOKEN + run bash scripts/publish.sh "unit-tests" + [ "$status" -eq 1 ] + [[ "$output" == *"GITEA_TOKEN"* ]] +} + +@test "missing GIT_PAGES_URL → exit 1" { + unset GIT_PAGES_URL + run bash scripts/publish.sh "unit-tests" + [ "$status" -eq 1 ] + [[ "$output" == *"GIT_PAGES_URL"* ]] +} + +@test "missing GIT_PAGES_PUBLISH_TOKEN → exit 1" { + unset GIT_PAGES_PUBLISH_TOKEN + run bash scripts/publish.sh "unit-tests" + [ "$status" -eq 1 ] + [[ "$output" == *"GIT_PAGES_PUBLISH_TOKEN"* ]] +} diff --git a/tests/report-status.bats b/tests/report-status.bats index 35b38a9..786c731 100644 --- a/tests/report-status.bats +++ b/tests/report-status.bats @@ -4,9 +4,9 @@ setup() { source tests/helpers/mock-api.sh export GITEA_API_URL="http://localhost:18080" export GITEA_TOKEN="test-token-abc123" + export GIT_PAGES_URL="https://reports.example.com" export GITHUB_REPOSITORY="test-owner/test-repo" export GITHUB_SHA="abc123def456789012345678901234567890abcd" - export GITHUB_SERVER_URL="https://gitea.example.com" export GITHUB_RUN_ID="42" } @@ -16,68 +16,40 @@ teardown() { @test "pending status is POSTed with correct payload" { mock_start - run bash scripts/report-status.sh pending "Building project" "http://example.com/build/42" + run bash scripts/report-status.sh pending "Building project" [ "$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"'* ]] + [[ "$body" == *'"target_url":"http://localhost:18080/test-owner/test-repo/actions/runs/42"'* ]] method=$(mock_get_request_method) [[ "$method" == "POST" ]] } -@test "success status with url and custom key" { +@test "success status with custom key and suite builds report URL" { mock_start - run bash scripts/report-status.sh success "Unit tests OK" "http://example.com/reports/cucumber.html" "unit-test" + run bash scripts/report-status.sh success "Unit tests OK" unit-test cucumber [ "$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"'* ]] + [[ "$body" == *'"target_url":"https://reports.example.com/test-owner/test-repo/reports/abc123de/cucumber/"'* ]] } -@test "failure status is POSTed correctly" { +@test "failure status constructs run URL when no suite" { mock_start - run bash scripts/report-status.sh failure "Tests failed: 3 of 10" "http://example.com/build/42" + run bash scripts/report-status.sh failure "Tests failed: 3 of 10" [ "$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" ]] + [[ "$body" == *'"target_url":"http://localhost:18080/test-owner/test-repo/actions/runs/42"'* ]] } @test "default key when not provided" { mock_start - run bash scripts/report-status.sh pending "Build started" "http://example.com/build/42" + run bash scripts/report-status.sh pending "Build started" [ "$status" -eq 0 ] body=$(mock_get_request_body) [[ "$body" == *'"context":"commit-abc123de"'* ]] @@ -86,38 +58,46 @@ teardown() { @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" + run bash scripts/report-status.sh success "Should fail" [ "$status" -eq 1 ] } +@test "cross-repo: ROOT_COMMIT and ROOT_REPO override target" { + export ROOT_COMMIT="rootabc123" + export ROOT_REPO="services/temperature-store" + mock_start + run bash scripts/report-status.sh success "Deployed to staging" deploy-staging + [ "$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"'* ]] + unset ROOT_COMMIT ROOT_REPO +} + @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" + run bash scripts/report-status.sh pending "Test" [ "$status" -eq 1 ] - [[ "$output" == *"ERROR"* ]] || [[ "$output" == *"GITEA_API_URL"* ]] + [[ "$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" + run bash scripts/report-status.sh pending "Test" [ "$status" -eq 1 ] - [[ "$output" == *"ERROR"* ]] || [[ "$output" == *"GITEA_TOKEN"* ]] + [[ "$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" + run bash scripts/report-status.sh "" "desc" [ "$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" "" + run bash scripts/report-status.sh pending "" [ "$status" -eq 1 ] }