diff --git a/.gitea/workflows/build_publish-artifact.yml b/.gitea/workflows/build_publish-artifact.yml new file mode 100644 index 0000000..54c3393 --- /dev/null +++ b/.gitea/workflows/build_publish-artifact.yml @@ -0,0 +1,124 @@ +name: Build & Publish Artifact +on: + workflow_call: + inputs: + env_json: + required: true + type: string + bats-image: + required: true + type: string + cucumber-node-image: + required: true + type: string + 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: + check: + runs-on: ubuntu-latest + outputs: + artifact_exists: ${{ steps.check.outputs.artifact_exists }} + version: ${{ steps.check.outputs.version }} + steps: + - uses: actions/checkout@v4 + + - name: Check existing artifact + id: check + run: | + VERSION=$(jq -r '.version' package.json) + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + TAG=$(curl -s "$GITEA_API_URL/api/v1/repos/$GITHUB_REPOSITORY/tags" | \ + jq -r '.[] | select(.commit.sha == "'"$GITHUB_SHA"'") | .name' | head -1) + if [ -n "$TAG" ]; then + echo "artifact_exists=true" >> "$GITHUB_OUTPUT" + echo "Commit already tagged as $TAG, skipping build" + else + echo "artifact_exists=false" >> "$GITHUB_OUTPUT" + fi + + quality-gate: + needs: [check] + if: needs.check.outputs.artifact_exists == 'false' + uses: niko/gitea-ci-library/.gitea/workflows/quality-gate.yml@main + secrets: inherit + with: + env_json: ${{ inputs.env_json }} + bats-image: ${{ inputs.bats-image }} + cucumber-node-image: ${{ inputs.cucumber-node-image }} + + build: + needs: [check, quality-gate] + if: needs.check.outputs.artifact_exists == 'false' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build container + run: | + docker build -t "minimal:${{ needs.check.outputs.version }}" . + mkdir -p /tmp/image + docker save "minimal:${{ needs.check.outputs.version }}" -o /tmp/image/artifact.tar + + - name: Save Docker image for next job + uses: actions/upload-artifact@v4 + with: + name: docker-image + path: /tmp/image/artifact.tar + + push: + needs: [check, build] + runs-on: ubuntu-latest + steps: + - name: Load saved Docker image + uses: actions/download-artifact@v4 + with: + name: docker-image + path: /tmp/image + + - name: Push to Gitea Packages + run: | + VERSION="${{ needs.check.outputs.version }}" + docker load -i /tmp/image/artifact.tar + REGISTRY=$(echo "$GITEA_API_URL" | sed 's|https://||') + IMAGE="$REGISTRY/niko/gitea-ci-library/minimal:$VERSION" + docker tag "minimal:$VERSION" "$IMAGE" + docker login "$REGISTRY" -u niko -p "$GITEA_TOKEN" + docker push "$IMAGE" + docker logout "$REGISTRY" + + tag-commit: + needs: [check, push] + runs-on: ubuntu-latest + steps: + - name: Create git tag + run: | + VERSION="${{ needs.check.outputs.version }}" + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + "$GITEA_API_URL/api/v1/repos/$GITHUB_REPOSITORY/tags" \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"tag_name\": \"$VERSION\", + \"message\": \"Build #$GITHUB_RUN_NUMBER\", + \"target\": \"$GITHUB_SHA\" + }") + + if [ "$HTTP_CODE" = "201" ]; then + echo "Tag $VERSION created" + elif [ "$HTTP_CODE" = "409" ]; then + echo "Tag $VERSION already exists (parallel build won), skipping" + else + echo "Failed to create tag: HTTP $HTTP_CODE" + exit 1 + fi diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 2bd2a01..449892a 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -6,14 +6,16 @@ on: jobs: load-config: + name: Load gitea-env.conf to pipeline env uses: niko/gitea-ci-library/.gitea/workflows/config-provider.yml@main with: config_path: .gitea/workflows/gitea-env.conf feature: + name: Quality Gate if: github.ref != 'refs/heads/main' needs: [load-config] - uses: niko/gitea-ci-library/.gitea/workflows/build-feature.yml@main + uses: niko/gitea-ci-library/.gitea/workflows/quality-gate.yml@main secrets: inherit with: env_json: ${{ needs.load-config.outputs.env_json }} @@ -21,9 +23,10 @@ jobs: cucumber-node-image: node:22 main: + name: Build & Push Artifact if: github.ref == 'refs/heads/main' needs: [load-config] - uses: niko/gitea-ci-library/.gitea/workflows/build-feature.yml@main + uses: niko/gitea-ci-library/.gitea/workflows/build_publish-artifact.yml@main secrets: inherit with: env_json: ${{ needs.load-config.outputs.env_json }} diff --git a/.gitea/workflows/build-feature.yml b/.gitea/workflows/quality-gate.yml similarity index 73% rename from .gitea/workflows/build-feature.yml rename to .gitea/workflows/quality-gate.yml index b6c9b9a..40bc17e 100644 --- a/.gitea/workflows/build-feature.yml +++ b/.gitea/workflows/quality-gate.yml @@ -1,4 +1,4 @@ -name: Build Feature +name: Quality Gate on: workflow_call: inputs: @@ -56,9 +56,7 @@ jobs: docker run --rm \ -v bats-workspace:/data \ --entrypoint bash ${{ inputs.bats-image }} \ - -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/' \ + -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" @@ -91,53 +89,32 @@ jobs: repository: niko/gitea-ci-library path: .ci - - name: Prepare cucumber - id: prepare-cucumber - shell: bash - run: | - apt-get update -qq && apt-get install -y -qq --no-install-recommends lsof jq - if npm install @cucumber/cucumber > /dev/null 2>&1 && \ - npx --package @cucumber/cucumber cucumber-js --dry-run tests/features/ > /dev/null 2>&1; then - echo "TOOL_OK=true" >> "${GITHUB_ENV}" - else - echo "TOOL_OK=false" >> "${GITHUB_ENV}" - fi - - name: Run cucumber tests - if: always() id: cucumber-tests shell: bash run: | - if [ "${TOOL_OK}" != "true" ]; then - echo "CUCUMBER_EXIT=1" >> "${GITHUB_ENV}" - exit 0 - fi + apt-get update -qq && apt-get install -y -qq --no-install-recommends lsof jq + npm install @cucumber/cucumber > /dev/null 2>&1 mkdir -p "reports/${GITHUB_SHA:0:8}/cucumber" set +e npx cucumber-js \ --format json:"reports/${GITHUB_SHA:0:8}/cucumber/report.json" \ --format html:"reports/${GITHUB_SHA:0:8}/cucumber/index.html" 2>&1 CUCUMBER_EXIT=$? - echo "CUCUMBER_EXIT=${CUCUMBER_EXIT}" >> "${GITHUB_ENV}" + + STATE="success" + [ "${CUCUMBER_EXIT}" != "0" ] && STATE="failure" + if [ -f "reports/${GITHUB_SHA:0:8}/cucumber/index.html" ]; then + bash .ci/scripts/report-status.sh "${STATE}" "Cucumber tests" ci-cucumber cucumber + else + bash .ci/scripts/report-status.sh "${STATE}" "Cucumber tests" ci-cucumber + fi + exit ${CUCUMBER_EXIT} - name: Publish cucumber reports if: always() - run: | - if [ "${TOOL_OK}" = "true" ]; then - bash .ci/scripts/publish-git-pages.sh cucumber - fi - - - name: Set cucumber commit status - if: always() - run: | - if [ "${TOOL_OK}" != "true" ]; then - bash .ci/scripts/report-status.sh failure "Cucumber tool unavailable" ci-cucumber - elif [ "${CUCUMBER_EXIT}" = "0" ]; then - bash .ci/scripts/report-status.sh success "Cucumber tests passed" ci-cucumber cucumber - else - bash .ci/scripts/report-status.sh failure "Cucumber tests FAILED" ci-cucumber cucumber - fi + run: bash .ci/scripts/publish-git-pages.sh cucumber build: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 7de0cc5..ab0d526 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ AGENTS.md .ai node_modules/ tmp/ +coverage/ +.DS_Store +reports/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b09b037 --- /dev/null +++ b/Dockerfile @@ -0,0 +1 @@ +FROM alpine:latest diff --git a/docs/ci-pipeline-practices.md b/docs/ci-pipeline-practices.md index 0115681..e3e6ae0 100644 --- a/docs/ci-pipeline-practices.md +++ b/docs/ci-pipeline-practices.md @@ -111,7 +111,109 @@ Avainkomponentit: - Jokainen testisetti omassa jobissaan - Finalize/build voi kerätä yhteenvedon (ei julkaista summarya jos kenelläkään ei ole linkkiä) -## 7. Inline Logic Threshold +## 7. Commit Status Before Exit + +Commit status (`ci-bats`, `ci-cucumber`) on asetettava **ennen** stepin +`exit`-komentoa, samassa shell-prosessissa. Ei `GITHUB_ENV`-propagointiin +luottamista stepien välillä — Gitea Actions ei välttämättä prosessoi +`GITHUB_ENV`-tiedostoa epäonnistuneen stepin jälkeen. + +Käytäntö testi-stepissä: + +``` +testit_ajoon +EXIT=$? + +STATE="success" +[ "${EXIT}" != "0" ] && STATE="failure" + +# Jos raportti on kirjoitettu levylle → linkki git-pagesiin +if [ -f "reports/${SHA8}/sute/index.html" ]; then + bash .ci/scripts/report-status.sh "${STATE}" "Kuvaus" ci-{suite} {suite} +else + # Muuten linkki Gitea Actions logiin + bash .ci/scripts/report-status.sh "${STATE}" "Kuvaus" ci-{suite} +fi + +exit ${EXIT} +``` + +Tämä takaa: + +- Aina commit status riippumatta siitä, onko kyseessä tool- vai test error +- Oikea URL: raportti git-pagesissa (jos tiedosto on kirjoitettu) tai Gitea Actions logeissa +- PR merge-esto toimii luotettavasti: branch protection näkee statuksen + aina, koska se kirjoitetaan ennen stepin failaamista + +Julkaisu (`publish-git-pages.sh`) jää edelleen omaksi stepikseen `if: always()`:lla. + +## 8. Pipeline Exit Code Safety (validated 2026-06-14) + +Pipeline (`cmd1 | cmd2 | cmd3`) asettaa `$?`:ksi **viimeisen** komennon exit-koodin. Jos `tee` tai muu aina-onnistuva komento on viimeisenä, testin todellinen exit-koodi katoaa. + +### Dangerous patterns + +| Pattern | `$?` captures | Result | +|---------|---------------|--------| +| `docker run … \| tee file` | `tee`:n exit (0) | ❌ test error kadotettu | +| `tar \| docker \| tee` | `tee`:n exit (0) | ❌ test error kadotettu | +| `docker run … 2>&1 \| tee file` | `tee`:n exit (0) | ❌ stderr-ohjaus ei auta | +| `set -o pipefail` + pipeline | viimeisen epäonnistuneen exit | ⚠️ pipefail riippuu bash-versiosta ja PIPESTATUS resetoituu helposti | + +### Safe patterns + +| Pattern | `$?` captures | Verified | +|---------|---------------|----------| +| `docker run … > file 2>&1` | suoraan kontin exit | ✅ lokaali + CI | +| `docker volume` + `tar \| alpine tar x` (data transfer) + `docker run > file` | suoraan kontin exit | ✅ lokaali + CI | + +### Why volume-based approach works + +Kolmen erillisen komennon ketju ilman testiä putkittavaa pipeä: + +``` +docker volume create ws # 1. volyymi +tar c . | docker run … alpine … # 2. data volyymiin (tämä on pipe, mutta data transfer) +docker run -v ws:/data … > file # 3. testit → exit koodi $?:iin puhtaana +``` + +Vaihe 2 on pipe, mutta se on **data transfer** — sen exit-koodilla ei ole väliä. Vaihe 3 on suora `docker run > file` ilman pipeä, joten `$?` on aina kontin exit. + +### Debug-näkyvyys ilman tee:tä + +`tee` antaa real-time logit, mutta tappaa exit-koodin. Ratkaisu: jaa kahteen steppiin: + +```yaml +- name: Run tests + run: | + docker run … > results.txt 2>&1 + EXIT=$? + echo "EXIT=${EXIT}" >> "${GITHUB_ENV}" + exit ${EXIT} + +- name: Publish reports + if: always() + run: publish.sh + +- name: Set commit status + if: always() + run: | + [ "${EXIT}" = "0" ] && report-status.sh success … || report-status.sh failure … +``` + +`if: always()` julkaisee raportit ja asettaa commit statuksen riippumatta testin lopputuloksesta. `GITHUB_ENV`:iin talletettu exit-koodi on luettavissa myöhemmissä stepeissä. + +### Oppitunti + +Pienikin muutos — kuten `| tee` lisääminen debug-näkyvyyttä varten — voi murtaa error propagationin huomaamatta. Ainoa tapa varmistua on testata lokaalisti kontissa: + +```bash +docker run … > results.txt 2>&1 +EXIT=$? +echo $EXIT # pitää olla 1 jos testit failaa +``` + +## 9. Inline Logic Threshold Logiikka workflow YAML:ssa on hauras: YAML:n sisennys, heredocit ja kenoviivat tuottavat helposti toimimattomia steppejä. diff --git a/docs/workflows.md b/docs/workflows.md index 1544372..5d64830 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -1,8 +1,7 @@ # Reusable workflowt -> ⚠️ **POC-vaihe.** Tämä dokumentti kuvaa suunniteltuja workflow'ta -> (ci-feature, ci-master, deploy, test). POCissa on toteutettu -> `build-feature.yml`. Uudelleenkirjoitus odottaa. +> ⚠️ **POC-vaihe.** Toteutettu: `quality-gate.yml`. Suunnitteilla: +> `ci-master.yml`, `deploy.yml`, `test.yml`. --- @@ -16,136 +15,271 @@ Kaikki workflowt: --- -## `ci-feature.yml` — Feature-branch +## `quality-gate.yml` — Merge-portti -**Trigger:** `push` mihin tahansa branchiin paitsi `master` +**Trigger:** `workflow_call` — consumer kutsuu `uses:`-direktiivillä -**Elinkaari:** +**Rooli:** Laatuportti, joka ajetaan branch protection -sääntönä ennen PR:n +sulkemista mainiin. Pipeline on ajettava (`run > 1`) eikä yhtään jobia +saa failata. -``` -start → unit-test → code-coverage → html-reports → end -``` +**Provider-Consumer-malli (ADR 0005):** Provider tarjoaa orkestroinnin +(validointi, raporttien julkaisu, commit-status). Consumer omistaa +pipeline-stepit — valitsee testityökalunsa, mahdolliset laatu- ja +tietoturva-analyy sit sekä niiden järjestyksen. Alla oleva esimerkki +kuvaa tyypillistä Java-mikropalvelua Mavenilla; consumer korvaa nämä +omalla tekniikkapinollaan. -### Inputs +### Inputs (providerin rajapinta) -| Parametri | Pakollinen | Kuvaus | -|-----------|------------|--------| -| `config-file` | Kyllä | Polku `ci-flow-values.yaml`:aan (yleensä `ci-flow-values.yaml`) | -| `containers` | Ei | Kuvaus konteista: avain = nimi, arvo = image. Steppi valitsee missä kontissa ajaa. | +| Parametri | Pakollinen | Tyyppi | Kuvaus | +|-----------|------------|--------|--------| +| `env_json` | Kyllä | string | JSON-muotoiset ympäristömuuttujat (`GITEA_API_URL`, `GIT_PAGES_URL`) | +| `*` | — | — | Consumer lisää omat parametrinsa (`maven-image`, `docker-image`, jne.) | -### Steppi-kaavio +### Secrets + +| Secret | Pakollinen | Kuvaus | +|--------|------------|--------| +| `GITEA_TOKEN` | Kyllä | Gitea API-kutsuihin (commit-status) | +| `GIT_PAGES_PUBLISH_TOKEN` | Kyllä | Raporttien julkaisuun git-pagesiin | + +### Steppi-kaavio (Java-esimerkki) ```mermaid %%{init: {'theme': 'base', 'flowchart': {'arrowheadScale': 2}}}%% flowchart TD - START(["checkout + start - POST INPROGRESS"]) --> UNIT["unit-test - aja testit, generoi raportit"] - UNIT --> COV["code-coverage - jacoco / vastaava"] - COV --> HTML["publish-reports - vie raportit git-pagesiin"] - HTML --> END(["end - POST lopullinen status"]) + VAL["validate + provider: tarkista + CI-konfiguraatio"] --> TEST["test + consumer: mvn test + → testiraportit + coverage"] - FAIL("fail") -. "catch" .-> END + VAL --> AI_SCAN["ai-scan \[optional\] + consumer: tietoturva- + tai laatu-skannaus"] - style START fill:#2563eb,color:#ffffff - style UNIT fill:#059669,color:#ffffff - style COV fill:#059669,color:#ffffff - style HTML fill:#7c3aed,color:#ffffff - style END fill:#2563eb,color:#ffffff + TEST --> SONAR["sonarqube \[optional\] + consumer: mvn sonar:sonar + → laatupoikkeamat"] + TEST --> PUB["publish-reports + provider: vie raportit + git-pagesiin"] + + SONAR --> PUB + AI_SCAN --> PUB + + PUB --> STATUS["commit-status + provider: aseta status + linkillä raporttiin"] + + FAIL("fail") -. "if: always()" .-> PUB + + style VAL fill:#2563eb,color:#ffffff + style TEST fill:#059669,color:#ffffff + style SONAR fill:#7c3aed,color:#ffffff + style AI_SCAN fill:#7c3aed,color:#ffffff + style PUB fill:#0891b2,color:#ffffff + style STATUS fill:#f59e0b,color:#111827 style FAIL fill:#dc2626,color:#ffffff linkStyle default stroke:#9ca3af,stroke-width:3px ``` +Consumerin omat stepit (test, sonarqube, ai-scan) ovat esimerkki. +Vastaava rakenne toimii millä tahansa kielellä tai työkalulla. + +### Optionaaliset laatu- ja tietoturvaskannaukset + +Consumer voi lisätä pipelineen omia skannaussteppejä testien rinnalle. +Nämä ajetaan rinnakkain `validate`-vaiheen jälkeen ja syöttävät +raporttinsa providerin `publish-reports`-palveluun. Jokainen skannaus +on oma Gitea Actions -jobinsa. + +```mermaid +%%{init: {'theme': 'base', 'flowchart': {'arrowheadScale': 2}}}%% +flowchart LR + VAL["validate"] --> SAST["sast + semgrep / codeql"] + VAL --> SCA["sca + snyk / owasp dc"] + VAL --> SECRETS["secret-scan + gitleaks"] + VAL --> LICENSE["license + fossa / scancode"] + VAL --> AI_REVIEW["ai-review + code quality"] + + SAST --> PUB + SCA --> PUB + SECRETS --> PUB + LICENSE --> PUB + AI_REVIEW --> PUB + + PUB["publish-reports + commit-status"] + + style VAL fill:#2563eb,color:#ffffff + style SAST fill:#7c3aed,color:#ffffff + style SCA fill:#7c3aed,color:#ffffff + style SECRETS fill:#7c3aed,color:#ffffff + style LICENSE fill:#7c3aed,color:#ffffff + style AI_REVIEW fill:#7c3aed,color:#ffffff + style PUB fill:#0891b2,color:#ffffff + linkStyle default stroke:#9ca3af,stroke-width:3px +``` + +| Kategoria | Esimerkki | Kuvaus | +|-----------|-----------|--------| +| **SAST** | Semgrep, CodeQL | Staattinen analyysi — bugit ja haavoittuvuudet koodista | +| **SCA** | Snyk, OWASP Dependency-Check | Riippuvuuksien tunnetut haavoittuvuudet | +| **Secret scan** | Gitleaks, TruffleHog | API-avaimet, tokenit ja salasanat repossa | +| **Lisenssit** | FOSSA, ScanCode | Riippuvuuksien lisenssien yhteensopivuus | +| **AI review** | — | Automaattinen koodikatselmointi | + ### Error handling -Workflow käyttää Gitea Actionsin natiivia `jobs..continue-on-error` ja `if: failure()` -ehtoja. Ei erillistä `fail(e)`-kutsua kuten Jenkinsissä. Epäonnistunut steppi asettaa statuksen `failure` ja jatkaa `end`-steppiin, joka raportoi lopullisen statuksen. +Providerin julkaisu- ja status-stepit käyttävät `if: always()`-ehtoa, +jotta raportit ja commit-status päivittyvät myös failaavista ajoista. +Consumerin omat stepit voivat vapaasti päättää `continue-on-error`- tai +`if: failure()`-logiikastaan. Provider ei määrittele virheidenkäsittelyä +consumerin pipelineen. ---- +### Merge-portti -## `ci-master.yml` — Master / release-branch +Branch protection -säännössä Giteassa vaaditaan ennen PR:n sulkemista: +- **Pipeline on ajettu** (`run > 1`, ei "never run" -tila) +- **Kaikki commit-statukset vihreitä** — validate, testit, laatuportit +- Jos joku steppi failaa, status asettuu `failure`-tilaan ja PR:n + sulkeminen estyy -**Trigger:** `push` `master`-branchiin tai `workflow_dispatch` +### Optionaalinen PR-ympäristö (preview app) + +Consumer voi halutessaan buildata kontin ja deployata sen väliaikaiseen +PR-ympäristöön. Tämä on optionaalinen continuation-haara, joka +aktivoituu ehdolla: + +- PR:ssä on tietty label (esim. `preview`) +- Commit message sisältää triggerisanan (esim. `[preview]`) **Elinkaari:** -``` -start → isContainerBuilt? ──kyllä──→ continueToTestFlow - │ - ei - ↓ -unit-test → quality-gate → build-jar → build-docker → push-docker → tag-commit → continueToTestFlow → end +```mermaid +%%{init: {'theme': 'base', 'flowchart': {'arrowheadScale': 2}}}%% +flowchart LR + QG["quality-gate + testit + skannaukset + ok"] --> BUILD["build-container + tag: pr-42"] + BUILD --> DEPLOY["deploy-pr-env + väliaikainen ympäristö"] + + DEPLOY --> STATUS["commit-status + linkki PR-ympäristöön"] + + PR_CLOSE["PR merged / closed"] --> CLEANUP["cleanup-pr-env + tuhoa ympäristö"] + + style QG fill:#059669,color:#ffffff + style BUILD fill:#0891b2,color:#ffffff + style DEPLOY fill:#7c3aed,color:#ffffff + style STATUS fill:#f59e0b,color:#111827 + style PR_CLOSE fill:#dc2626,color:#ffffff + style CLEANUP fill:#dc2626,color:#ffffff + linkStyle default stroke:#9ca3af,stroke-width:3px ``` -### Inputs +1. Quality-gate läpäisty (testit + skannaukset ok) +2. Buildaa kontti, tagi sisältää PR-numeron (`pr-42`) +3. Deployaa PR-ympäristöön (preview/review app) +4. Asettaa commit-statuksen linkillä ympäristöön +5. **PR:n sulkeutuessa** (merge/close): cleanup-job tuhoaa ympäristön -| Parametri | Pakollinen | Kuvaus | -|-----------|------------|--------| -| `config-file` | Kyllä | Polku `ci-flow-values.yaml`:aan | -| `containers` | Ei | Kuvaus konteista: avain = nimi, arvo = image | +Tämä on **consumerin vastuulla** — provider tarjoaa tarvittavat +skriptit (`publish-git-pages.sh`, `report-status.sh`), mutta +trigger-ehto, kontin buildaus ja ympäristön hallinta kuuluvat +consumerin pipelineen. -### isContainerBuilt-check +--- -```yaml -- name: Check if container already built - run: | - TAG=$(git tag --points-at HEAD | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -1) - if [ -n "$TAG" ]; then - echo "container_already_built=true" >> $GITHUB_ENV - echo "container_version=$TAG" >> $GITHUB_ENV - fi +## `ci-master.yml` — Main-branch build + +**Trigger:** `workflow_call` — kutsutaan main-branchiin pushattaessa + +**Rooli:** Buildaa artifaktin (kontti, JAR, npm-paketti tms.) ja julkaisee +sen rekisteriin. Jos sama commit on jo buildattu (version tag on olemassa), +build skipataan ja siirrytään suoraan test flow'hun. + +**Provider-Consumer-malli (ADR 0005):** Provider orkestroi idempotent +build-logiikan (`isArtifactBuilt`-tarkistus), mutta consumer omistaa +build-stepit — valitsee työkalut ja artifaktityypin. + +### isArtifactBuilt-check + +Ennen buildia tarkistetaan, onko tälle commitille jo olemassa versiotagi: + +```bash +TAG=$(git tag --points-at HEAD | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -1) +if [ -n "$TAG" ]; then + echo "artifact_already_built=true" >> $GITHUB_ENV + echo "artifact_version=$TAG" >> $GITHUB_ENV +fi ``` -Jos `container_already_built == true`, build- ja push-steppit skipataan. Siirrytään suoraan `continueToTestFlow`:hun. +Jos tagi löytyy, build- ja push-stepit skipataan. Committia vastaan on +jo olemassa artifakti rekisterissä — uudelleenbuildaus aiheuttaisi +versiokonflikteja ja tuhlaisi CI-aikaa. ### Steppi-kaavio ```mermaid %%{init: {'theme': 'base', 'flowchart': {'arrowheadScale': 2}}}%% flowchart TD - START(["start"]) --> CHECK{"isContainerBuilt? + CHECK{"isArtifactBuilt? git tag --points-at HEAD"} - - CHECK -- "ei" --> UNIT["unit-test"] - UNIT --> SONAR["quality-gate - SonarQube"] - SONAR --> JAR["build-jar - ArtifactType.JAR"] - JAR --> DOCKER["build-docker - ArtifactType.DOCKER - + Docker-labelit"] - DOCKER --> PUSH["push-docker - ArtifactType.DOCKER"] + + CHECK -- "ei" --> QG["quality-gate + testit + skannaukset"] + QG --> BUILD["build-artifact + consumer: docker build / + mvn package / npm build"] + BUILD --> PUSH["push registry + gitea packages / + docker registry"] PUSH --> TAG["tag-commit tagittaa commitin - versiolla"] - - CHECK -- "kyllä" --> CTF["continueToTestFlow"] - TAG --> CTF - CTF --> HTML["publish-reports - vie raportit git-pagesiin"] - HTML --> END(["end - lopullinen status"]) + versiolla (esim. 1.2.3.${RUN})"] - FAIL("fail") -. "catch" .-> END + CHECK -- "kyllä" --> K8S["continueToTestFlow + (future: K8s-testit + test plan -mukaan)"] + TAG --> K8S + + FAIL("fail") -. "quality-gate + ei läpäisty" .-> END + + K8S --> END(["end + commit-status"]) - style START fill:#2563eb,color:#ffffff style CHECK fill:#f59e0b,color:#111827 - style UNIT fill:#059669,color:#ffffff - style SONAR fill:#7c3aed,color:#ffffff - style JAR fill:#0891b2,color:#ffffff - style DOCKER fill:#0891b2,color:#ffffff + style QG fill:#059669,color:#ffffff + style BUILD fill:#0891b2,color:#ffffff style PUSH fill:#dc2626,color:#ffffff style TAG fill:#f59e0b,color:#111827 - style CTF fill:#f59e0b,color:#111827 - style HTML fill:#7c3aed,color:#ffffff - style END fill:#2563eb,color:#ffffff + style K8S fill:#7c3aed,color:#ffffff style FAIL fill:#dc2626,color:#ffffff + style END fill:#2563eb,color:#ffffff linkStyle default stroke:#9ca3af,stroke-width:3px ``` +### Elinkaari + +1. **isArtifactBuilt?** — tarkista onko tagi olemassa +2. **quality-gate** — jos ei tagia, aja `quality-gate.yml` (testit, skannaukset) +3. **build-artifact** — jos quality-gate läpäisty, buildaa artifakti +4. **push registry** — julkaise rekisteriin (Gitea Packages, Docker registry, jne.) +5. **tag-commit** — tagittaa commitin versiolla (esim. `1.2.3.`) +6. **continueToTestFlow** — *(future)* aja K8s-testit test plan -mukaan +7. **commit-status** — aseta lopullinen status + ### Concurrency ```yaml @@ -154,7 +288,8 @@ concurrency: cancel-in-progress: false ``` -Vain yksi master-build kerrallaan per repo. Ei cancel-in-progress — käynnissä olevan buildin annetaan valmistua. +Vain yksi master-build kerrallaan per repo. Ei cancel-in-progress — +käynnissä olevan buildin annetaan valmistua. --- diff --git a/git-pages/files/retention-cleanup.sh b/git-pages/files/retention-cleanup.sh index 11b9f27..6f9aece 100644 --- a/git-pages/files/retention-cleanup.sh +++ b/git-pages/files/retention-cleanup.sh @@ -13,7 +13,7 @@ curl_with_host() { [ -f "$CONFIG" ] || { echo "ERROR: config missing: $CONFIG" >&2; exit 1; } -BRANCH_CACHE="" +declare -A BRANCH_CACHE branch_exists() { local owner="$1" repo="$2" branch="$3" key="${owner}/${repo}/${branch}" local status @@ -21,7 +21,7 @@ branch_exists() { [ -z "$GITEA_API_URL" ] && return 0 [ -z "$GITEA_TOKEN" ] && return 0 - if grep -q "^${key}$" <<< "$BRANCH_CACHE" 2>/dev/null; then + if [ "${BRANCH_CACHE[$key]:-}" = "1" ]; then return 0 fi @@ -30,7 +30,7 @@ branch_exists() { "${GITEA_API_URL}/api/v1/repos/${owner}/${repo}/branches/${branch}" 2>/dev/null || echo "000") if [ "$status" = "200" ]; then - BRANCH_CACHE="${BRANCH_CACHE}${key}"$'\n' + BRANCH_CACHE[$key]=1 return 0 fi return 1 @@ -79,9 +79,15 @@ fi echo "" echo "=== Phase 1: collect reports ===" +declare -A SEEN_REPORTS declare -a REPORTS while IFS= read -r meta_path; do report_dir=$(dirname "$meta_path") + + # Skip duplicates - same report dir already processed + [ -z "${SEEN_REPORTS[$report_dir]:-}" ] || continue + SEEN_REPORTS[$report_dir]=1 + parse_path "$report_dir" meta_content=$(curl_with_host "${PAGES_URL}/${meta_path}" 2>/dev/null || true) [ -n "$meta_content" ] || { echo " WARN: could not fetch $meta_path"; continue; } @@ -121,6 +127,7 @@ done echo "" echo "=== Phase 3: apply retention rules to remaining reports ===" +declare -A BRANCH_COUNTS if [ "${#KEEP[@]}" -gt 0 ]; then IFS=$'\n' for entry in $(printf '%s\n' "${KEEP[@]}" | sort -t'|' -k4,4 -k5,5rn); do diff --git a/package.json b/package.json index 10809ed..ad2f5b8 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,27 @@ { - "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" - } -} + "name": "gitea-ci-library", + "version": "0.1.0", + "description": "", + "main": "cucumber.js", + "directories": { + "doc": "docs", + "test": "tests" + }, + "scripts": { + "test": "npm run test:bats && npm run test:cucumber", + "test:bats": "mkdir -p reports && docker run --rm -v \"$(pwd):/repo:ro\" -v \"$(pwd)/reports:/repo/reports\" -w /repo --entrypoint bash bats/bats:latest -c 'apk add -q python3 curl jq lsof ruby && gem install bashcov -q > /dev/null 2>&1; bats tests/'", + "test:bats:coverage": "mkdir -p reports && docker run --rm -v \"$(pwd):/repo\" -v \"$(pwd)/reports:/repo/reports\" -w /repo --entrypoint bash bats/bats:latest -c 'apk add -q python3 curl jq lsof ruby && gem install bashcov -q > /dev/null 2>&1; bashcov -- bats tests/'", + "test:cucumber": "docker run --rm -v \"$(pwd):/repo:ro\" -v \"$(pwd)/node_modules:/repo/node_modules\" -w /repo --entrypoint bash node:22 -c 'apt-get update -qq && apt-get install -y -qq jq lsof && npm ci && npx cucumber-js tests/features/ --tags @mock and ~@wip'" + }, + "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" + } +} \ No newline at end of file