Compare commits

..

22 Commits

Author SHA1 Message Date
moilanik 077d98edb8 any branch
CI Feature / Bats tests (push) Has been cancelled
CI Feature / Cucumber tests (push) Has been cancelled
CI Feature / Report Summary (push) Has been cancelled
CI Feature / Load example-gitea-env.conf to pipeline env (push) Has been cancelled
CI Main / Load example-gitea-env.conf to pipeline env (push) Successful in 22s
CI Main / Check existing artifact (push) Successful in 19s
unit-tests Bats test report
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 1m41s
CI Main / Bats tests (push) Successful in 1m29s
ci-docker-build-push Docker push 0.2.24
CI Main / Build & Push Docker (push) Successful in 42s
CI Main / GitOps (push) Failing after 0s
CI Main / Move provider version tag (push) Has been skipped
CI Main / Report Summary (push) Successful in 5s
2026-06-22 10:24:31 +03:00
moilanik a99a8a28c6 fix
CI Feature / Bats tests (push) Has been cancelled
CI Feature / Cucumber tests (push) Has been cancelled
CI Feature / Report Summary (push) Has been cancelled
CI Feature / Load example-gitea-env.conf to pipeline env (push) Has been cancelled
CI Main / Load example-gitea-env.conf to pipeline env (push) Successful in 22s
CI Main / Check existing artifact (push) Successful in 19s
unit-tests Bats test report
CI Main / Bats tests (push) Successful in 1m28s
acc-tests Cucumber test report
CI Main / Cucumber tests (push) Successful in 1m38s
ci-docker-build-push Docker push 0.2.23
CI Main / Build & Push Docker (push) Successful in 46s
CI Main / New docker to the cluster (push) Failing after 0s
CI Main / Move provider version tag (push) Has been skipped
CI Main / Report Summary (push) Successful in 5s
2026-06-22 10:12:15 +03:00
moilanik 6ae4766581 clean
CI Feature / Bats tests (push) Has been cancelled
CI Feature / Cucumber tests (push) Has been cancelled
CI Feature / Report Summary (push) Has been cancelled
CI Feature / Load example-gitea-env.conf to pipeline env (push) Has been cancelled
CI Main / Load example-gitea-env.conf to pipeline env (push) Successful in 21s
CI Main / Move provider version tag (push) Successful in 16s
CI Main / Check existing artifact (push) Successful in 19s
CI Main / Bats tests (push) Has been skipped
CI Main / Cucumber tests (push) Has been skipped
CI Main / Build & Push Docker (push) Has been skipped
CI Main / Update docker (push) Has been skipped
CI Main / Report Summary (push) Successful in 6s
2026-06-22 10:10:09 +03:00
moilanik bcac84f2fd siistimistä, router pipelien clean
CI Main / Load example-gitea-env.conf to pipeline env (push) Successful in 22s
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 24s
CI Feature / Bats tests (push) Successful in 1m26s
CI Feature / Cucumber tests (push) Successful in 1m38s
CI Main / Check existing artifact (push) Successful in 18s
CI Feature / Report Summary (push) Successful in 4s
unit-tests Bats test report
acc-tests Cucumber test report
CI Main / Bats tests (push) Successful in 1m27s
CI Main / Cucumber tests (push) Successful in 1m27s
CI Main / Build & Push Helm (push) Failing after 31s
ci-docker-build-push Docker push 0.2.22
CI Main / Build & Push Docker (push) Successful in 54s
CI Main / Update docker (push) Failing after 1s
CI Main / Move provider version tag (push) Has been skipped
CI Main / Report Summary (push) Successful in 7s
2026-06-22 09:55:25 +03:00
moilanik db9d6daebb fix
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 19s
unit-tests Bats test report
CI Feature / Bats tests (push) Successful in 1m29s
acc-tests Cucumber test report
CI Feature / Cucumber tests (push) Successful in 1m38s
CI Feature / Report Summary (push) Successful in 5s
2026-06-22 07:53:38 +03:00
moilanik ba16e9e4eb fix
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 23s
acc-tests Cucumber test report
CI Feature / Cucumber tests (push) Failing after 1m23s
unit-tests Bats test report
CI Feature / Bats tests (push) Successful in 1m29s
CI Feature / Report Summary (push) Successful in 6s
2026-06-22 07:41:32 +03:00
moilanik 6463dad6d7 testi korjaukset
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 20s
unit-tests Bats test report
CI Feature / Bats tests (push) Successful in 1m28s
acc-tests Cucumber test report
CI Feature / Cucumber tests (push) Failing after 1m35s
CI Feature / Report Summary (push) Successful in 5s
2026-06-22 07:15:38 +03:00
moilanik 21a6ef7ab1 fix
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 20s
unit-tests Bats test report
CI Feature / Bats tests (push) Failing after 1m27s
acc-tests Cucumber test report
CI Feature / Cucumber tests (push) Failing after 1m37s
CI Feature / Report Summary (push) Successful in 6s
2026-06-22 07:01:05 +03:00
moilanik 84978784fe clean up
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 20s
acc-tests Cucumber test report
CI Feature / Cucumber tests (push) Failing after 26s
unit-tests Bats test report
CI Feature / Bats tests (push) Failing after 1m25s
CI Feature / Report Summary (push) Successful in 5s
2026-06-22 06:57:09 +03:00
moilanik f58497f5e8 skill & docs
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 23s
POC GitOps E2E / e2e (push) Successful in 53s
unit-tests Bats test report
CI Feature / Bats tests (push) Failing after 1m32s
acc-tests Cucumber test report
CI Feature / Cucumber tests (push) Failing after 1m45s
CI Feature / Report Summary (push) Successful in 5s
2026-06-22 06:34:37 +03:00
moilanik fa57a152e4 self commit status about gitops
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 23s
acc-tests Cucumber test report
unit-tests Bats test report
CI Feature / Cucumber tests (push) Failing after 1m22s
CI Feature / Bats tests (push) Failing after 1m31s
CI Feature / Report Summary (push) Successful in 5s
POC GitOps E2E / e2e (push) Successful in 29s
2026-06-22 06:03:51 +03:00
moilanik a0cdf377f6 multi commit status support to same commit in gitops repo
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 22s
POC GitOps E2E / e2e (push) Successful in 43s
unit-tests Bats test report
acc-tests Cucumber test report
CI Feature / Bats tests (push) Failing after 1m29s
CI Feature / Cucumber tests (push) Failing after 1m20s
CI Feature / Report Summary (push) Successful in 6s
2026-06-22 05:56:21 +03:00
moilanik 028fd748a6 multi commit status gitops puolelle
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 32s
POC GitOps E2E / e2e (push) Successful in 49s
unit-tests Bats test report
CI Feature / Bats tests (push) Failing after 1m36s
acc-tests Cucumber test report
CI Feature / Cucumber tests (push) Failing after 1m37s
CI Feature / Report Summary (push) Successful in 6s
2026-06-22 05:47:36 +03:00
moilanik 7f53e2c303 jos jo haluttu versio - ei commit
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 24s
POC GitOps E2E / e2e (push) Failing after 49s
unit-tests Bats test report
CI Feature / Bats tests (push) Failing after 1m44s
acc-tests Cucumber test report
CI Feature / Cucumber tests (push) Failing after 1m48s
CI Feature / Report Summary (push) Successful in 7s
2026-06-22 05:43:00 +03:00
moilanik ec22d49039 monorepossa komponetti parametrina
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 22s
POC GitOps E2E / e2e (push) Successful in 52s
unit-tests Bats test report
CI Feature / Bats tests (push) Failing after 1m29s
acc-tests Cucumber test report
CI Feature / Cucumber tests (push) Failing after 1m40s
CI Feature / Report Summary (push) Successful in 6s
2026-06-22 05:33:11 +03:00
moilanik 13493de7b2 git username & email
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 22s
POC GitOps E2E / e2e (push) Successful in 53s
unit-tests Bats test report
CI Feature / Bats tests (push) Failing after 1m35s
acc-tests Cucumber test report
CI Feature / Cucumber tests (push) Failing after 1m35s
CI Feature / Report Summary (push) Successful in 6s
2026-06-22 05:09:13 +03:00
moilanik 47df5a8017 poc
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 23s
unit-tests Bats test report
CI Feature / Bats tests (push) Failing after 1m34s
acc-tests Cucumber test report
CI Feature / Cucumber tests (push) Failing after 1m35s
CI Feature / Report Summary (push) Successful in 5s
POC GitOps E2E / e2e (push) Failing after 35s
2026-06-22 04:55:09 +03:00
moilanik e84e37c9f8 poc
POC GitOps Dispatch / orchestrator (push) Successful in 10s
POC GitOps Dispatch / test (push) Has been skipped
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 31s
unit-tests Bats test report
acc-tests Cucumber test report
CI Feature / Bats tests (push) Successful in 1m33s
CI Feature / Cucumber tests (push) Successful in 1m35s
CI Feature / Report Summary (push) Successful in 4s
2026-06-22 04:25:05 +03:00
moilanik 9105675591 poc
POC GitOps Dispatch / echo (push) Successful in 13s
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 33s
unit-tests Bats test report
CI Feature / Bats tests (push) Successful in 1m34s
acc-tests Cucumber test report
CI Feature / Cucumber tests (push) Successful in 1m48s
CI Feature / Report Summary (push) Successful in 5s
2026-06-22 04:17:07 +03:00
moilanik 1385afcca6 teet
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 31s
unit-tests Bats test report
CI Feature / Bats tests (push) Successful in 1m34s
acc-tests Cucumber test report
CI Feature / Cucumber tests (push) Successful in 1m47s
CI Feature / Report Summary (push) Successful in 6s
2026-06-21 17:14:27 +03:00
moilanik 5c9df73a66 cucumber testejä lisää
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 34s
acc-tests Cucumber test report
CI Feature / Cucumber tests (push) Failing after 45s
unit-tests Bats test report
CI Feature / Bats tests (push) Successful in 1m36s
CI Feature / Report Summary (push) Successful in 5s
2026-06-21 16:10:41 +03:00
moilanik 86e73d87d3 gitops init
CI Feature / Load example-gitea-env.conf to pipeline env (push) Successful in 24s
acc-tests Cucumber test report
CI Feature / Cucumber tests (push) Failing after 1m10s
unit-tests Bats test report
CI Feature / Bats tests (push) Successful in 1m35s
CI Feature / Report Summary (push) Successful in 6s
2026-06-21 15:44:31 +03:00
22 changed files with 1348 additions and 58 deletions
+1
View File
@@ -4,3 +4,4 @@ DOCKER_REGISTRY=gitea.app.keskikuja.site/niko
DOCKER_IMAGE_NAME=gitea-ci-library-test-image
DOCKER_UI_URL=https://gitea.app.keskikuja.site/niko/-/packages/container
#DOCKERFILE=Dockerfile.platform
+18 -3
View File
@@ -3,6 +3,7 @@ on:
push:
branches:
- main
- feature/gitops
workflow_dispatch:
jobs:
@@ -39,7 +40,7 @@ jobs:
with:
env_json: ${{ needs.load-config.outputs.env_json }}
build-push:
docker-build-push:
name: Build & Push Docker
needs: [load-config, check-version, bats, cucumber]
if: needs.check-version.outputs.artifact_exists != 'true'
@@ -49,18 +50,32 @@ jobs:
env_json: ${{ needs.load-config.outputs.env_json }}
version: ${{ needs.check-version.outputs.version }}
docker-gitops:
name: GitOps
needs: [docker-build-push]
uses: niko/gitea-ci-library/.gitea/workflows/gitops-dispatch.yml
secrets: inherit
with:
env_json: ${{ needs.load-config.outputs.env_json }}
version: ${{ needs.check-version.outputs.version }}
GITOPS_FILE: dev/values.yaml
GITOPS_YQ_TPL: '.service.tag = "{{VERSION}}"'
GITOPS_REPO: niko/gitea-ci-gitops-tests
report-summary:
name: Report Summary
needs: [load-config, build-push]
needs: [load-config, check-version, docker-build-push, docker-gitops]
if: always()
uses: niko/gitea-ci-library/.gitea/workflows/report-summary.yml@main
with:
env_json: ${{ needs.load-config.outputs.env_json }}
suites: bats cucumber
gitops: |
${{ needs.docker-gitops.outputs.summary }}
tag-maintenance:
name: Move provider version tag
needs: [build-push]
needs: [docker-gitops]
if: success()
uses: niko/gitea-ci-library/.gitea/workflows/tag-maintenance.yml@main
secrets: inherit
+15 -2
View File
@@ -5,7 +5,6 @@ on:
- main
paths:
- git-pages/**
- .gitea/workflows/helm-build-push.yml
- .gitea/workflows/git-pages.*
workflow_dispatch:
@@ -36,11 +35,25 @@ jobs:
version: ${{ needs.check-version.outputs.version }}
chart_path: git-pages
chart-gitops:
name: Update chart to the cluster
needs: [helm-push]
uses: niko/gitea-ci-library/.gitea/workflows/gitops-dispatch.yml
secrets: inherit
with:
env_json: ${{ needs.load-config.outputs.env_json }}
version: ${{ needs.check-version.outputs.version }}
GITOPS_FILE: dev/Chart.yaml
GITOPS_YQ_TPL: '(.dependencies[] | select(.name == "git-pages") | .version) = "{{VERSION}}"'
GITOPS_REPO: niko/gitea-ci-gitops-tests
report-summary:
name: Report Summary
needs: [load-config, helm-push]
needs: [load-config, helm-push, chart-gitops]
if: always()
uses: niko/gitea-ci-library/.gitea/workflows/report-summary.yml@main
with:
env_json: ${{ needs.load-config.outputs.env_json }}
suites: ""
gitops: |
${{ needs.chart-gitops.outputs.summary }}
@@ -3,3 +3,4 @@ HELM_REGISTRY=gitea.app.keskikuja.site/niko
HELM_UI_URL=https://gitea.app.keskikuja.site/niko/-/packages/container
GIT_TAG_PREFIX=git-pages/
VERSION_FILE=git-pages/Chart.yaml
+58
View File
@@ -0,0 +1,58 @@
name: GitOps Dispatch
on:
workflow_call:
inputs:
env_json:
required: true
type: string
version:
required: true
type: string
GITOPS_FILE:
required: true
type: string
GITOPS_YQ_TPL:
required: true
type: string
GITOPS_REPO:
required: true
type: string
secrets:
GITOPS_DISPATCH_TOKEN:
required: true
outputs:
summary:
description: 'Pipe-format: component|version|status|commit_sha|repo'
value: ${{ jobs.dispatch.outputs.summary }}
env:
GITOPS_VERSION: ${{ inputs.version }}
GITOPS_FILE: ${{ inputs.GITOPS_FILE }}
GITOPS_YQ_TPL: ${{ inputs.GITOPS_YQ_TPL }}
GITOPS_REPO: ${{ inputs.GITOPS_REPO }}
GITOPS_SOURCE_REPO: ${{ github.repository }}
GITOPS_SOURCE_COMMIT: ${{ github.sha }}
GITEA_API_URL: ${{ fromJson(inputs.env_json).GITEA_API_URL }}
GITOPS_TAG_PREFIX: ${{ fromJson(inputs.env_json).GIT_TAG_PREFIX || '' }}
GITOPS_WORKFLOW: gitops-service.yaml
jobs:
dispatch:
runs-on: ubuntu-latest
outputs:
summary: ${{ steps.run.outputs.GITOPS_SUMMARY }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: niko/gitea-ci-library
path: .ci
- name: Run gitops dispatch
id: run
env:
GITEA_TOKEN: ${{ secrets.GITOPS_DISPATCH_TOKEN }}
run: |
OUTPUT=$(bash .ci/scripts/gitops-dispatch.sh)
echo "$OUTPUT"
SUMMARY=$(awk -F= '/^GITOPS_SUMMARY=/ {print $2}' <<<"$OUTPUT")
echo "GITOPS_SUMMARY=$SUMMARY" >> "$GITHUB_OUTPUT"
+23
View File
@@ -9,6 +9,10 @@ on:
required: true
type: string
description: Space-separated suite names published to git-pages
gitops:
required: false
type: string
description: 'Pipe-separated rows: component|version|status|commit_sha|repo'
env:
GIT_PAGES_URL: ${{ fromJson(inputs.env_json).GIT_PAGES_URL }}
@@ -32,3 +36,22 @@ jobs:
echo "| ${suite} | [View report](${BASE}/${suite}/) |"
done
} >> "${GITHUB_STEP_SUMMARY}"
if [ -n "${{ inputs.gitops }}" ]; then
GITEA_URL="${{ fromJson(inputs.env_json).GITEA_API_URL }}"
{
echo ""
echo "## GitOps updates"
echo ""
echo "| Component | Version | Status | GitOps commit |"
echo "|-----------|---------|--------|--------------|"
echo '${{ inputs.gitops }}' | while IFS='|' read -r comp ver status sha repo; do
[ -z "$comp" ] && continue
if [ -n "$sha" ]; then
echo "| $comp | $ver | $status | [link]($GITEA_URL/$repo/commit/$sha) |"
else
echo "| $comp | $ver | $status | — |"
fi
done
} >> "${GITHUB_STEP_SUMMARY}"
fi
+16 -1
View File
@@ -4,6 +4,8 @@ Reusable workflow -kirjasto Gitea Actionsille. Lisätietoja: [docs/](docs/)
**Consumer-käyttöönotto:** [skills/consumer-pipelines/SKILL.md](skills/consumer-pipelines/SKILL.md) — pipeline-standardit ja säännöt consumer-projekteille
**GitOps-päivitys:** [skills/gitops-update/SKILL.md](skills/gitops-update/SKILL.md) — GitOps-repon job-template, dispatch ja token-ohjeet
**Single repo & monorepo:** Kirjasto toimii molemmissa. Monorepo-tuki
polkusuodatuksella, komponenttikohtaisilla versioilla ja git-tägien
etuliitteillä — jokainen komponentti julkaistaan itsenäisesti omassa
@@ -220,7 +222,9 @@ Consumer-repossa on oltava seuraavat asetukset:
|--------|--------|
| `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.
`GITEA_TOKEN` on Gitean automaattisesti jokaiselle workflow-runille generoima token (`secrets.GITEA_TOKEN`). Se on scopeutettu **siihen repoon**, jossa workflow ajaa — ei toimi toiseen repoon dispatchaukseen eikä toisen repon commit-statusin asettamiseen. Ei tarvitse erikseen luoda.
Jos workflow tarvitsee oikeuksia **toiseen** repoon (esim. dispatch GitOps-repoon), tarvitaan manuaalinen token. Katso [skills/gitops-update/SKILL.md](skills/gitops-update/SKILL.md).
### Config-tiedosto (`.gitea/workflows/gitea-env.conf`)
@@ -256,6 +260,17 @@ Jokaisen jobin alussa `ci-validate.sh` tarkistaa:
Jos validointi epäonnistuu, job keskeytyy exit-koodilla 1 ja Gitean commit-status näyttää epäonnistumisen linkkinä lokiin.
### GitOps-päivitys
Artifact buildin jälkeen voidaan dispatchata GitOps-repoon, joka päivittää
konfiguraatiotiedoston (esim. Chart.yaml version) ja pushaa muutoksen.
Kaksi skriptiä:
- `scripts/dispatch-workflow.sh` — lähettää workflow_dispatch-pyynnön ja pollaa valmistumista
- `scripts/gitops-update.sh` — kloonaa, päivittää yq:llä, committaa ja pushaa
Tarkka asennus: [skills/gitops-update/SKILL.md](skills/gitops-update/SKILL.md)
### Muuta
| Muuttuja | Kuvaus |
+7 -1
View File
@@ -60,9 +60,15 @@ Salaisuudet eivät ole `.conf`-tiedostossa. Ne määritellään Gitean
organization/repository secrets -mekanismissa ja välitetään workflowlle
`secrets: inherit` -direktiivillä.
**`secrets.GITEA_TOKEN` on Gitean automaattisesti generoima token,
scopeutuu siihen repoon jossa workflow ajaa.** Se ei oikeuta
dispatchaamaan toiseen repoon eikä kirjoittamaan toisen repon
commit-statusta. Cross-repo-operaatioihin tarvitaan manuaalinen
org-tason token.
| Secret | Pakollinen | Käyttäjä |
|---|---|---|
| `GITEA_TOKEN` | Kyllä | `report-status.sh`, `check-version.yml`, `docker-build-push.yml` |
| `GITEA_TOKEN` | Kyllä | `report-status.sh`, `check-version.yml`, `docker-build-push.yml`, `gitops-update.sh` (GitOps-repossa) |
| `GIT_PAGES_PUBLISH_TOKEN` | Kyllä | `publish-git-pages.sh`, `config-provider.yml` (validointi) |
| `DOCKER_USERNAME` | Ei | `docker-build-push.yml` (oletus: `github.actor`, ei pakollinen kaikissa registryissä) |
| `DOCKER_PASSWORD` | Kyllä | `docker-build-push.yml` |
+25 -4
View File
@@ -113,17 +113,38 @@ Lukee tiedoston polun `CI_CONF_FILE`-env-muuttujasta (oletus: `.gitea/workflows/
Dispatchaa workflow'n toisessa repossa ja pollaa sen valmistumista synkronisesti.
Käytetään GitOps-deploymentissa ja klusteritestien ketjutuksessa (tuleva).
Generoi automaattisesti `dispatch_id`-tunnisteen, lisää sen dispatch-
inputteihin ja tunnistaa workflow-runin kohdereposta `display_title`-
kentän perusteella. Toimii luotettavasti vaikka samassa repossa olisi
useita samanaikaisia ajoja.
**Kohde-workflow'ssa on oltava `dispatch_id`-input ja `run-name`-kenttä
`display_title`-matchausta varten.** Katso `skills/gitops-update/SKILL.md`.
### Rajapinta
```bash
dispatch-workflow.sh <target_repo> <workflow_file> <ref> <inputs_json> [timeout_minutes]
dispatch-workflow.sh <target_repo> <workflow_file> <ref> <inputs_json> <gitea_api_url> <gitea_token> [timeout_minutes]
```
| Parametri | Pakollinen | Kuvaus |
|-----------|------------|--------|
| `target_repo` | Kyllä | `owner/repo` |
| `workflow_file` | Kyllä | Workflow-tiedosto (esim. `ci-main.yml`) |
| `ref` | Kyllä | Branch |
| `inputs_json` | Kyllä | JSON-objekti dispatch-inputteina |
| `gitea_api_url` | Kyllä | Gitean API-URL |
| `gitea_token` | Kyllä | Gitea API -token (write kohderepoon) |
| `timeout_minutes` | Ei | Aikakatkaisu (oletus 360) |
### Toiminta
1. **Dispatch:** `POST /api/v1/repos/{target_repo}/actions/workflows/{workflow_file}/dispatches`
2. **Poll:** `GET /api/v1/repos/{target_repo}/actions/runs` → odota valmistumista
3. **Palauta:** `conclusion` (`success`/`failure`/`timeout`)
1. **Generoi `dispatch_id`** — 8-hex uniikki tunniste
2. **Injektoi** `dispatch_id` inputteihin
3. **Dispatch:** `POST /api/v1/repos/{target_repo}/actions/workflows/{workflow_file}/dispatches`
4. **Etsi run:** pollaa rinnakkaisia `workflow_dispatch`-runeja, matchaa `display_title` sisältää `dispatch_id`:n
5. **Poll:** `GET /api/v1/repos/{target_repo}/actions/runs/{run_id}` — odota valmistumista
6. **Palauta:** exit 0 (success), exit 1 (failure), exit 124 (timeout)
---
+117 -7
View File
@@ -136,6 +136,29 @@ eikä toimi air gap -ympäristössä. Korvaa tarvittaessa custom-kontilla
---
### `gitops-dispatch.yml` — GitOps-päivityksen dispatch
**Trigger:** `workflow_call`
**Inputit:**
| Parametri | Pakollinen | Kuvaus |
|-----------|------------|--------|
| `env_json` | Kyllä | Konffi, josta luetaan `GITOPS_FILE`, `GITOPS_YQ_TPL`, `GITOPS_REPO`, `GIT_TAG_PREFIX` |
| `version` | Kyllä | Päivitettävä versio (check-version output) |
| `component` | Kyllä | `chart` tai `container` — tunniste summary-riville |
**Secretit:** `GITOPS_TOKEN`
**Outputit:** `summary` — pipe-formaatti: `{component}|{version}|{status}|{commit_sha}|{repo}`
**Steppi-kaavio:**
```
checkout → gitops-dispatch.sh → dispatch-workflow.sh → GITOPS_SUMMARY output
```
---
## Consumer-esimerkki (`example-*`)
### `example-feature.yml` — Feature-haaran CI
@@ -151,11 +174,23 @@ load-config → bats + cucumber → report-summary (always)
**Trigger:** `push` [branches: main]
```
load-config → check-version →
[artifact exists] → done
[no artifact] → bats + cucumber → report-summary (always) → docker-build-push
load-config ───────────────────────────────────────────────────────┐
load-config-helm ───────────────────────────────────────────┐ │
│ │
check-version ←─────────────────────────────────────────────┘ │
│ │
└→ bats + cucumber │
├─ docker-build-push → gitops-container ─┐ │
└─ helm-build-push → gitops-chart ──────┤ │
├→ report-summary ←┘
tag-maintenance ←────────────────────────┘
```
GitOps-jobit (`gitops-chart`, `gitops-container`) käyttävät
`gitops-dispatch.yml`-provider-workflowia. Kaksisuuntainen track:
dispatch-workflow.sh → GITOPS_COMMIT + GITOPS_SUMMARY.
Katso [skills/gitops-update/SKILL.md](../skills/gitops-update/SKILL.md).
### `example-bats-tests.yml` — Bats unit-testit
**Trigger:** `workflow_call`
@@ -174,7 +209,12 @@ commit-statuksen linkillä raporttiin.
**Trigger:** `workflow_call` — ajetaan `if: always()` testien jälkeen
**Inputs:** `env_json`, `suites` (space-separated lista suite-nimistä)
**Inputs:** `env_json`, `suites` (space-separated lista suite-nimistä), `gitops` (optional JSON array)
**GitOps-tuki:** Jos `gitops` input on annettu (JSON array objekteilla
`component`, `version`, `status`, `commit`, `repo`), workflow lisää
GitOps-päivitystaulukon testiraporttien perään. Jokaiselle riville
muodostuu linkki GitOps-repon committiin.
Generoi Markdown-taulukon `GITHUB_STEP_SUMMARY`:yn kaikista julkaistuista
raporteista. Renderöityy HTML:ksi Gitea 1.27+ Summary-välilehdellä.
@@ -182,7 +222,77 @@ Forward-compatibeli — ei haittaa vanhemmilla Gitea-versioilla.
---
## Suunnitteilla
## Provider-skriptit
- `deploy.yml` — GitOps-deployment (dispatch-workflow.sh-pohjainen)
- `test.yml` — Klusteritason test flow
### `gitops-update.sh` — GitOps-version päivitys
**Riippuvuudet:** `yq`, `scripts/report-status.sh`, `git`
Päivittää GitOps-repon konfiguraatiotiedoston versionumeron `yq`:lla,
committaa muutoksen ja asettaa commit-statuksen molempiin repoihin
(kaksisuuntainen track):
| Status | Mihin repo | Context | Linkki |
|---|---|---|---|
| ✅ | **GitOps-repo** | `source/{repo}` | Code-repon committiin |
| ✅ | **Code-repo** (dispatchin jälkeen) | `gitops/{repo} {RUN_ID}` | GitOps-repon committiin |
**Input-ympäristömuuttujat (ajetaan GitOps-repon workflow'ssa):**
| Muuttuja | Pakollinen | Kuvaus |
|---|---|---|
| `INPUT_FILE` | Kyllä | Tiedosto GitOps-repossa (esim. `dev/Chart.yaml`) |
| `YQ_TPL` | Kyllä | `yq`-lauseke `{{VERSION}}`-placeholderilla |
| `VERSION` | Kyllä | Uusi versio (esim. `0.2.3`) |
| `SOURCE_REPO` | Kyllä | Lähdekoodirepo (esim. `org/app`) |
| `SOURCE_COMMIT` | Kyllä | Lähdekoodin commit-SHA |
| `GITOPS_REPO` | Kyllä | GitOps-repo slug |
| `GITEA_API_URL` | Kyllä | Gitean API-URL |
| `GITEA_TOKEN` | Kyllä | Gitea API-token (write GitOps-repoon) |
| `GITOPS_BRANCH` | Ei | GitOps-repon branch (oletus `main`) |
| `GIT_TAG_PREFIX` | Ei | Komponentin tag-prefix status-nimeämiseen |
**Commit-status (GitOps-repoon):**
| Kenttä | Formaatti | Esimerkki |
|--------|-----------|-----------|
| Context | `source/{repo}` | `source/gitea-ci-library` |
| Description | `Install to {env} {version}` | `Install to dev 0.2.0` |
| Target URL | Linkki code-repon committiin | `/org/repo/commit/sha` |
`{env}` parsitaan `INPUT_FILE`:stä (`dev/Chart.yaml``dev`).
**Steppikuvaus:**
1. Korvaa `YQ_TPL`:n `{{VERSION}}` versiolla
2. Muodostaa `CLONE_URL` tokenilla ja hostilla
3. Kloonaa GitOps-repon
4. Ajaa `yq eval -i` päivittääkseen tiedoston
5. Jos muutoksia: commit + push `[skip ci]`, muuten status `— no change`
6. Asettaa commit-statuksen GitOps-repoon (source-konteksti, linkki code-repoon)
**Scriptiä ei ajeta code reposta.** Se ajaa GitOps-repon workflow'ssa.
### Code-repon commit-status (dispatchin jälkeen)
GitOps-päivityksen valmistuttua `dispatch-workflow.sh` tulostaa
`GITOPS_COMMIT=<sha>` (GitOps-repon commitin SHA). Code repo asettaa
oman commit-statusinsa linkillä GitOps-committiin:
| Kenttä | Formaatti | Esimerkki |
|--------|-----------|-----------|
| Context | `gitops/{repo} {RUN_ID}` | `gitops/gitea-ci-library 473` |
| Description | `Install to {env} {version}` | `Install to dev 0.2.0` |
| Target URL | Linkki GitOps-repon committiin | `/niko/gitea-ci-gitops-tests/commit/def456` |
### Loppuraportti (GITHUB_STEP_SUMMARY)
`report-summary.yml` (optio `gitops`-inputti) lisää GitOps-rivit
GITHUB_STEP_SUMMARYyn:
| Component | Version | Status | GitOps commit |
|---|---|---|---|
| helm | 0.2.0 | success | [link](...) |
Kokonainen esimerkki molemmista puolista: [skills/gitops-update/SKILL.md](../skills/gitops-update/SKILL.md)
ja [.gitea/workflows/example-main.yml](../.gitea/workflows/example-main.yml).
---
+32 -10
View File
@@ -17,6 +17,11 @@ POLL_INTERVAL="${DISPATCH_POLL_INTERVAL:-10}"
[ -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
# Generate unique dispatch_id for display_title matching
# Can be overridden via DISPATCH_ID env var (for tests)
DISPATCH_ID="${DISPATCH_ID:-$(xxd -l 4 -p /dev/urandom 2>/dev/null || openssl rand -hex 4 2>/dev/null || od -An -N4 -tx1 /dev/urandom | tr -d ' \n')}"
INPUTS_JSON=$(echo "$INPUTS_JSON" | jq --arg id "$DISPATCH_ID" '. + {dispatch_id: $id}')
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}')
@@ -32,19 +37,30 @@ if [ "$DISPATCH_CODE" != "201" ]; then
exit 1
fi
RUNS_URL="$GITEA_API_URL/api/v1/repos/$TARGET_REPO/actions/runs?status=running"
RUNS_RESP=$(curl -s --connect-timeout 5 --max-time 10 \
-H "Authorization: token $GITEA_TOKEN" "$RUNS_URL")
RUN_ID=$(echo "$RUNS_RESP" | jq -r '.workflow_runs[0].id // empty')
if [ -z "$RUN_ID" ] || [ "$RUN_ID" = "null" ]; then
echo "ERROR: Could not find dispatched workflow run" >&2
exit 1
fi
# Poll: find dispatched run by display_title matching
RUN_ID=""
TIMEOUT_SECONDS=$(awk "BEGIN {printf \"%.3f\", $TIMEOUT_MINUTES * 60}")
START_TIME=$(date +%s)
while [ -z "$RUN_ID" ]; do
NOW=$(date +%s)
ELAPSED=$((NOW - START_TIME))
if awk -v e="$ELAPSED" -v t="$TIMEOUT_SECONDS" 'BEGIN { exit !(e >= t) }'; then
echo "ERROR: Timeout after ${TIMEOUT_MINUTES} minutes — run not found" >&2
exit 124
fi
RUNS_RESP=$(curl -s --connect-timeout 5 --max-time 10 \
"$GITEA_API_URL/api/v1/repos/$TARGET_REPO/actions/runs?event=workflow_dispatch&limit=10" \
-H "Authorization: token $GITEA_TOKEN")
RUN_ID=$(echo "$RUNS_RESP" | jq -r --arg id "$DISPATCH_ID" \
'[.workflow_runs[] | select(.display_title | contains($id))] | .[0].id // empty')
[ -z "$RUN_ID" ] && sleep "$POLL_INTERVAL"
done
# Poll: wait for run to complete
while true; do
NOW=$(date +%s)
ELAPSED=$((NOW - START_TIME))
@@ -61,6 +77,12 @@ while true; do
if [ "$STATUS" = "completed" ]; then
CONCLUSION=$(echo "$RUN_RESP" | jq -r '.conclusion // "failure"')
if [ "$CONCLUSION" = "success" ]; then
GITOPS_COMMIT=""
BRANCH_RESP=$(curl -s --connect-timeout 5 --max-time 10 \
"$GITEA_API_URL/api/v1/repos/$TARGET_REPO/branches/$REF" \
-H "Authorization: token $GITEA_TOKEN") || true
GITOPS_COMMIT=$(echo "$BRANCH_RESP" | jq -r '.commit.id // empty')
echo "GITOPS_COMMIT=$GITOPS_COMMIT"
exit 0
fi
echo "ERROR: Workflow completed with conclusion: $CONCLUSION" >&2
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail
: "${GITOPS_FILE:?}"
: "${GITOPS_YQ_TPL:?}"
: "${GITOPS_VERSION:?}"
: "${GITOPS_SOURCE_REPO:?}"
: "${GITOPS_SOURCE_COMMIT:?}"
: "${GITOPS_REPO:?}"
: "${GITOPS_WORKFLOW:?}"
: "${GITEA_API_URL:?}"
: "${GITEA_TOKEN:?}"
TIMEOUT="${GITOPS_DISPATCH_TIMEOUT:-30}"
INPUTS=$(jq -nc \
--arg file "$GITOPS_FILE" \
--arg yq_tpl "$GITOPS_YQ_TPL" \
--arg version "$GITOPS_VERSION" \
--arg source_repo "$GITOPS_SOURCE_REPO" \
--arg source_commit "$GITOPS_SOURCE_COMMIT" \
--arg git_tag_prefix "${GITOPS_TAG_PREFIX:-}" \
'{file: $file, yq_tpl: $yq_tpl, version: $version, source_repo: $source_repo, source_commit: $source_commit, git_tag_prefix: $git_tag_prefix}')
DIR="$(cd "$(dirname "$0")" && pwd)"
set +e
OUTPUT=$(bash "$DIR/dispatch-workflow.sh" \
"$GITOPS_REPO" "$GITOPS_WORKFLOW" "main" \
"$INPUTS" "$GITEA_API_URL" "$GITEA_TOKEN" "$TIMEOUT" 2>&1)
EXIT=$?
set -e
echo "$OUTPUT"
STATUS="failure"
GITOPS_SHA=""
if [ "$EXIT" = "0" ]; then
STATUS="success"
GITOPS_SHA=$(echo "$OUTPUT" | grep '^GITOPS_COMMIT=' | cut -d= -f2)
fi
COMPONENT="${GITOPS_TAG_PREFIX:-${GITOPS_FILE}}"
echo "GITOPS_SUMMARY=${COMPONENT}|${GITOPS_VERSION}|${STATUS}|${GITOPS_SHA}|${GITOPS_REPO}"
exit "$EXIT"
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
_gitops_fail() {
local MSG="${1:-GitOps update failed}"
echo "[ERROR] ${MSG}" >&2
if [ -n "${GITOPS_REPO:-}" ] && [ -n "${GITOPS_SHA:-}" ] && \
[ -n "${SOURCE_REPO:-}" ] && [ -n "${SOURCE_COMMIT:-}" ] && \
[ -n "${GITEA_API_URL:-}" ] && [ -n "${GITEA_TOKEN:-}" ]; then
local env repo context
env=$(dirname "${INPUT_FILE}")
repo=$(basename "${SOURCE_REPO}")
context="${repo} ${GITHUB_RUN_ID:-unknown}"
[ -n "${GIT_TAG_PREFIX:-}" ] && context="${repo}/${GIT_TAG_PREFIX} ${GITHUB_RUN_ID:-unknown}"
local SOURCE_URL="${GITEA_API_URL}/${SOURCE_REPO}/commit/${SOURCE_COMMIT}"
ROOT_REPO="${GITOPS_REPO}" ROOT_COMMIT="${GITOPS_SHA}" \
GITEA_API_URL="${GITEA_API_URL}" GITEA_TOKEN="${GITEA_TOKEN}" \
bash "${SCRIPT_DIR}/report-status.sh" failure "Install to ${env} ${VERSION}" \
"${context}" "" "${SOURCE_URL}" 2>/dev/null || true
fi
exit 1
}
_gitops_validate() {
[ -n "${INPUT_FILE:-}" ] || _gitops_fail "INPUT_FILE is required"
[ -n "${YQ_TPL:-}" ] || _gitops_fail "YQ_TPL is required"
[ -n "${VERSION:-}" ] || _gitops_fail "VERSION is required"
[ -n "${SOURCE_REPO:-}" ] || _gitops_fail "SOURCE_REPO is required"
[ -n "${SOURCE_COMMIT:-}" ] || _gitops_fail "SOURCE_COMMIT is required"
[ -n "${GITOPS_REPO:-}" ] || _gitops_fail "GITOPS_REPO is required"
[ -n "${GITEA_TOKEN:-}" ] || _gitops_fail "GITEA_TOKEN is required"
[ -n "${GITEA_API_URL:-}" ] || _gitops_fail "GITEA_API_URL is required"
}
_gitops_success() {
local env repo context
env=$(dirname "${INPUT_FILE}")
repo=$(basename "${SOURCE_REPO}")
context="${repo} ${GITHUB_RUN_ID:-unknown}"
[ -n "${GIT_TAG_PREFIX:-}" ] && context="${repo}/${GIT_TAG_PREFIX} ${GITHUB_RUN_ID:-unknown}"
local SOURCE_URL="${GITEA_API_URL}/${SOURCE_REPO}/commit/${SOURCE_COMMIT}"
ROOT_REPO="${GITOPS_REPO}" ROOT_COMMIT="${GITOPS_SHA}" \
GITEA_API_URL="${GITEA_API_URL}" GITEA_TOKEN="${GITEA_TOKEN}" \
bash "${SCRIPT_DIR}/report-status.sh" success \
"Install to ${env} ${VERSION}" \
"${context}" "" "${SOURCE_URL}"
}
_gitops_nochange() {
local env repo context
env=$(dirname "${INPUT_FILE}")
repo=$(basename "${SOURCE_REPO}")
context="${repo} ${GITHUB_RUN_ID:-unknown}"
[ -n "${GIT_TAG_PREFIX:-}" ] && context="${repo}/${GIT_TAG_PREFIX} ${GITHUB_RUN_ID:-unknown}"
local SOURCE_URL="${GITEA_API_URL}/${SOURCE_REPO}/commit/${SOURCE_COMMIT}"
ROOT_REPO="${GITOPS_REPO}" ROOT_COMMIT="${GITOPS_SHA}" \
GITEA_API_URL="${GITEA_API_URL}" GITEA_TOKEN="${GITEA_TOKEN}" \
bash "${SCRIPT_DIR}/report-status.sh" success \
"Install to ${env} ${VERSION} — no change" \
"${context}" "" "${SOURCE_URL}"
}
_gitops_substitute() {
echo "$1" | sed "s/{{VERSION}}/$2/g"
}
_gitops_update() {
local CLONE_DIR="${GITOPS_TARGET_DIR:-$(mktemp -d)}"
if [ -n "${GITOPS_CLONE_URL:-}" ]; then
git clone "${GITOPS_CLONE_URL}" "${CLONE_DIR}" || _gitops_fail "Failed to clone GitOps repo"
else
git clone "${CLONE_URL}" "${CLONE_DIR}" || _gitops_fail "Failed to clone GitOps repo"
fi
cd "${CLONE_DIR}" || _gitops_fail "Failed to enter clone directory"
yq eval -i "${YQ_EXPR}" "${INPUT_FILE}" || _gitops_fail "Failed to update ${INPUT_FILE}"
git add "${INPUT_FILE}" || _gitops_fail "Failed to stage ${INPUT_FILE}"
if git diff --cached --quiet; then
echo "No changes — ${INPUT_FILE} already at ${VERSION}"
GITOPS_SHA="$(git rev-parse HEAD)"
_gitops_nochange
exit 0
fi
git -c user.name="gitea-ci-bot" \
-c user.email="ci@keskikuja.site" \
commit -m "[skip ci] gitops: update version to ${VERSION}" || _gitops_fail "Failed to commit"
GITOPS_SHA="$(git rev-parse HEAD)"
git push || _gitops_fail "Failed to push"
_gitops_success
}
_gitops_validate
YQ_EXPR=$(_gitops_substitute "${YQ_TPL}" "${VERSION}")
GITEA_HOST=$(echo "${GITEA_API_URL}" | sed 's|https://||' | sed 's|http://||')
CLONE_URL="${GITOPS_CLONE_URL:-https://${GITEA_TOKEN}@${GITEA_HOST}/${GITOPS_REPO}.git}"
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
_gitops_update
fi
+410
View File
@@ -0,0 +1,410 @@
---
name: gitops-update
description: |
Setting up GitOps version updates: GitOps-repo workflow template, code
repo dispatch, secret requirements, and two-repo commit-status pattern.
Activates when the user needs to wire up artifact builds to GitOps
configuration updates.
activation-gate: |
User mentions GitOps update, gitops-update, dispatch to another repo,
two-repo version bump, cross-repo deployment, or wiring build output to
config repo.
category: ci
impact: high
---
# GitOps Update — Provider-palvelu
`scripts/gitops-update.sh` ja `scripts/dispatch-workflow.sh` muodostavat
GitOps-päivityspalvelun. Artifact buildataan code repossa, minkä jälkeen
code repo dispatchaa GitOps-repoon, joka päivittää konfiguraatiotiedoston
ja pushaa muutoksen.
## Arkkitehtuuri
Kaksi erillistä repoa, eristetyt oikeudet:
```
Code repo GitOps repo
(build & push artifact) (konfiguraatiot)
build & push onnistuu (v0.2.3)
│ dispatch ci-main.yml
│ {file, yq_tpl, version, source_repo, source_commit}
└────────────────────────────────────→┐
dispatch-workflow.sh pollaa ←─────────┘
code repo asettaa │ git clone, yq update,
oman commit-statusnsa │ git commit + push
dispatchin exit-koodilla │ status GitOps-repoon
```
**Token-periaate:** Vain GitOps-repoon kirjoitetaan. Code repo asettaa
oman commit-statusnsa dispatch-kutsun exit-koodin perusteella omalla
auto-tokenillaan. GitOps-repon auto-token ei tarvitse oikeuksia code
repoon.
## GitOps-repon workflow (ci-main.yml)
GitOps-repoon luodaan `.gitea/workflows/ci-main.yml`:
```yaml
name: GitOps Update
run-name: "GitOps Service (${{ inputs.dispatch_id || 'manual' }})"
on:
workflow_dispatch:
inputs:
file:
required: true
type: string
yq_tpl:
required: true
type: string
version:
required: true
type: string
source_repo:
required: true
type: string
source_commit:
required: true
type: string
dispatch_id:
required: false
type: string
git_tag_prefix:
required: false
type: string
env:
INPUT_FILE: ${{ inputs.file }}
YQ_TPL: ${{ inputs.yq_tpl }}
VERSION: ${{ inputs.version }}
SOURCE_REPO: ${{ inputs.source_repo }}
SOURCE_COMMIT: ${{ inputs.source_commit }}
GITOPS_REPO: ${{ github.repository }}
GITOPS_BRANCH: ${{ github.ref_name }}
GITEA_API_URL: ${{ gitea.server_url }}
GIT_TAG_PREFIX: ${{ inputs.git_tag_prefix || '' }}
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: niko/gitea-ci-library
path: .ci
- name: Install yq
run: |
wget -qO /usr/local/bin/yq \
https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
chmod +x /usr/local/bin/yq
- name: Run GitOps update
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
bash .ci/scripts/gitops-update.sh
```
**Huomiot:**
- `GITEA_TOKEN` on Gitean auto-token — scopeutuu GitOps-repoon, riittää
cloneen, committiin, pushiin ja commit-statusiin GitOps-repossa
- `run-name` ja `dispatch_id` mahdollistavat dispatchaavan skriptin tunnistaa
tämän workflow-runin yksiselitteisesti `display_title`-kentästä, vaikka
samassa repossa olisi samanaikaisia ajoja
- yq ladataan lennossa (kompromissi, ks. "Tuleva CI-kontti")
### Tulossa: custom CI-kontti
Nykyinen job lataa yq:n lennossa. Myöhemmin rakennetaan oma kontti
(`ci-gitops`), jossa on nodejs + git + yq valmiina. Sama patterni kuin
`ci-bats` ja `ci-cucumber`. Ks. `skills/ci-container-build/SKILL.md`.
## Code-repon dispatch-step
Code repo dispatchaa GitOps-repon workflown artifact buildin onnistuttua:
```yaml
gitops-update:
needs: [helm-build-push]
if: success()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: niko/gitea-ci-library
path: .ci
- name: Dispatch GitOps update
id: dispatch
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
INPUTS=$(jq -nc \
--arg file "dev/Chart.yaml" \
--arg yq_tpl '(.dependencies[] | select(.name == "agent-platform-helm") | .version) = "{{VERSION}}"' \
--arg version "${{ needs.check-version.outputs.version }}" \
--arg source_repo "${{ github.repository }}" \
--arg source_commit "${{ github.sha }}" \
'{file: $file, yq_tpl: $yq_tpl, version: $version, source_repo: $source_repo, source_commit: $source_commit}')
OUTPUT=$(bash .ci/scripts/dispatch-workflow.sh \
"niko/agent-platform-gitops" \
"ci-main.yml" \
"main" \
"$INPUTS" \
"${{ fromJson(needs.load-config.outputs.env_json).GITEA_API_URL }}" \
"${{ secrets.GITEA_TOKEN }}" \
"30")
echo "$OUTPUT"
GITOPS_COMMIT=$(echo "$OUTPUT" | grep '^GITOPS_COMMIT=' | cut -d= -f2)
echo "gitops_commit=$GITOPS_COMMIT" >> "$GITHUB_OUTPUT"
```
### Multi-artifact pipeline (kontti + helm)
Yksi main-haaran build tuottaa usein sekä Docker-imagen että Helm-chartin.
Kumpikin artefakti dispatchaa oman GitOps-päivityksensä rinnakkain:
```yaml
gitops-helm:
needs: [helm-build-push]
if: success()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: niko/gitea-ci-library
path: .ci
- name: Update helm version
id: helm
run: |
INPUTS=$(jq -nc \
--arg file "dev/Chart.yaml" \
--arg yq_tpl '(.dependencies[] | select(.name == "git-pages") | .version) = "{{VERSION}}"' \
--arg version "${{ needs.check-version.outputs.version }}" \
--arg source_repo "${{ github.repository }}" \
--arg source_commit "${{ github.sha }}" \
--arg git_tag_prefix "helm" \
'{dispatch_id: "", file: $file, yq_tpl: $yq_tpl, version: $version, source_repo: $source_repo, source_commit: $source_commit, git_tag_prefix: $git_tag_prefix}')
OUTPUT=$(bash .ci/scripts/dispatch-workflow.sh \
"niko/gitea-ci-gitops-tests" "gitops-service.yaml" "main" \
"$INPUTS" "${{ fromJson(needs.load-config.outputs.env_json).GITEA_API_URL }}" \
"${{ secrets.GITOPS_DISPATCH_TOKEN }}" "30")
echo "$OUTPUT"
echo "helm_commit=$(echo "$OUTPUT" | grep '^GITOPS_COMMIT=' | cut -d= -f2)" >> "$GITHUB_OUTPUT"
gitops-docker:
needs: [docker-build-push]
if: success()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: niko/gitea-ci-library
path: .ci
- name: Update docker tag
id: docker
run: |
INPUTS=$(jq -nc \
--arg file "dev/values.yaml" \
--arg yq_tpl '.service.tag = "{{VERSION}}"' \
--arg version "${{ needs.check-version.outputs.version }}" \
--arg source_repo "${{ github.repository }}" \
--arg source_commit "${{ github.sha }}" \
--arg git_tag_prefix "docker" \
'{dispatch_id: "", file: $file, yq_tpl: $yq_tpl, version: $version, source_repo: $source_repo, source_commit: $source_commit, git_tag_prefix: $git_tag_prefix}')
OUTPUT=$(bash .ci/scripts/dispatch-workflow.sh \
"niko/gitea-ci-gitops-tests" "gitops-service.yaml" "main" \
"$INPUTS" "${{ fromJson(needs.load-config.outputs.env_json).GITEA_API_URL }}" \
"${{ secrets.GITOPS_DISPATCH_TOKEN }}" "30")
echo "$OUTPUT"
echo "docker_commit=$(echo "$OUTPUT" | grep '^GITOPS_COMMIT=' | cut -d= -f2)" >> "$GITHUB_OUTPUT"
```
Kaksi dispatchia, kaksi eri tiedostoa, kaksi eri `GIT_TAG_PREFIX`-arvoa.
Kummallakin on oma commit-status-linja ja oma summary-rivi.
`dispatch-workflow.sh` hoitaa rinnakkaisuuden `display_title`-matchauksella.
**GITEA_TOKEN dispatch-vaiheessa:** Tarvitaan manuaalinen token,
jolla on **write-oikeus GitOps-repoon** (esim. org-tason token).
Code-repon auto-token ei oikeuta dispatchaamaan toiseen repoon.
Token luodaan Giteassa: `Settings → Applications → Generate Token`
ja asetetaan code-repoon Actions Secretiksi.
### Commit-status dispatchin perusteella
`dispatch-workflow.sh` tulostaa `GITOPS_COMMIT=<sha>` stdoutiin onnistuneen
GitOps-päivityksen jälkeen. Code repo parsii sen ja asettaa commit-statusin
linkillä GitOps-committiin:
```yaml
- name: Set commit-status with GitOps link
if: always()
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_API_URL: ${{ fromJson(needs.load-config.outputs.env_json).GITEA_API_URL }}
GITOPS_COMMIT: ${{ steps.dispatch.outputs.gitops_commit }}
VERSION: ${{ needs.check-version.outputs.version }}
run: |
GITOPS_URL="${GITEA_API_URL}/niko/agent-platform-gitops/commit/${GITOPS_COMMIT}"
CTX="gitops/$(basename ${{ github.repository }})"
DESC="Deploy to dev ${VERSION}"
if [ -n "$GITOPS_COMMIT" ]; then
bash .ci/scripts/report-status.sh success "$DESC" "$CTX" "" "$GITOPS_URL"
else
bash .ci/scripts/report-status.sh success "$DESC" "$CTX"
fi
```
`dispatch-workflow.sh` palauttaa:
- exit 0 = GitOps-päivitys onnistui (+ `GITOPS_COMMIT=<sha>`)
- exit 1 = GitOps-päivitys failasi
- exit 124 = aikakatkaisu (360 min oletus)
### Loppuraportti (report-summary)
Code-repon viimeinen job (`report-summary`) lisää GitOps-päivityksestä
rivin GITHUB_STEP_SUMMARYyn:
```yaml
- name: GitOps summary
if: always()
env:
GITEA_API_URL: ${{ fromJson(needs.load-config.outputs.env_json).GITEA_API_URL }}
GITOPS_COMMIT: ${{ steps.dispatch.outputs.gitops_commit }}
VERSION: ${{ needs.check-version.outputs.version }}
run: |
if [ -n "$GITOPS_COMMIT" ]; then
LINK="${GITEA_API_URL}/niko/agent-platform-gitops/commit/${GITOPS_COMMIT}"
else
LINK="#"
fi
cat >> "$GITHUB_STEP_SUMMARY" << 'GITOPS'
## GitOps updates
| Component | Version | Status | Commit |
|-----------|---------|--------|--------|
| agent-platform-helm | __VERSION__ | __STATUS__ | [link](__LINK__) |
GITOPS
sed -i "s|__VERSION__|${VERSION}|; s|__STATUS__|${{ job.status }}|; s|__LINK__|${LINK}|" \
"$GITHUB_STEP_SUMMARY"
```
## Secretit ja tokenit
| Secret | Missä | Scope | Kuvaus |
|--------|-------|-------|--------|
| `GITEA_TOKEN` (auto) | Code repo | Vain code repo | Asettaa commit-statusin dispatchin jälkeen |
| `GITEA_TOKEN` (auto) | GitOps repo | Vain GitOps repo | Klooni, push, commit-status GitOps-repossa |
| `GITOPS_DISPATCH_TOKEN` (manuaalinen) | Code repo | Write GitOps-repoon | Dispatchaa GitOps-repon workflow |
**Tokenin luonti:**
1. Gitea → `Settings``Applications``Generate Token`
2. Valitse repo-oikeudet: valitse GitOps-repo, anna write-oikeudet
3. Token asetetaan code-repoon: `{repo} → Settings → Actions Secrets`
4. Salaisuuden nimi: esim. `GITOPS_DISPATCH_TOKEN`
## Provider-skriptit
### `scripts/gitops-update.sh`
Ajaan GitOps-repon workflow'ssa. Päivittää konfiguraatiotiedoston yq:llä,
committaa ja pushaa. Asettaa commit-statuksen vain GitOps-repoon.
**Input-ympäristömuuttujat:**
| Muuttuja | Pakollinen | Kuvaus |
|---|---|---|
| `INPUT_FILE` | Kyllä | Tiedosto GitOps-repossa (esim. `dev/Chart.yaml`) |
| `YQ_TPL` | Kyllä | yq-lauseke `{{VERSION}}`-placeholderilla |
| `VERSION` | Kyllä | Uusi versio (esim. `0.2.3`) |
| `SOURCE_REPO` | Kyllä | Code-repo slug (esim. `org/app`) |
| `SOURCE_COMMIT` | Kyllä | Code-repon commit SHA |
| `GITOPS_REPO` | Kyllä | GitOps-repo slug |
| `GITEA_API_URL` | Kyllä | Gitean API-URL |
| `GITEA_TOKEN` | Kyllä | Gitea API-token (write GitOps-repoon) |
| `GITOPS_BRANCH` | Ei | Branch (oletus `main`) |
| `GIT_TAG_PREFIX` | Ei | Komponentin tag-prefix status-nimeämiseen (esim. `agent-platform-helm`) |
| `GITOPS_CLONE_URL` | Ei | Yliajaa clone-URL (esim. eri protokolla) |
| `GITOPS_TARGET_DIR` | Ei | Yliajaa clone-kohdehakemisto |
**Commit-status muoto:**
GitOps-repoon asetetaan commit-status:
| Kenttä | Formaatti | Esimerkki |
|--------|-----------|-----------|
| Context | `{repo}/{GIT_TAG_PREFIX} {RUN_ID}` tai `{repo} {RUN_ID}` | `gitea-ci-library/agent-platform-helm 473` |
| Description | `Install to {env} {version}` | `Install to dev 0.2.0` |
| Target URL | Linkki code-repon committiin | `/niko/gitea-ci-library/commit/abc123` |
Jos tiedosto on jo halutussa versiossa (ei muutoksia), status saa descriptionin `Install to {env} {version} — no change`. Commit-pushia ei tehdä, GitOps-repo pysyy muuttumattomana.
- `{env}` parsitaan `INPUT_FILE`:stä (`dev/Chart.yaml``dev`)
- `{repo}` parsitaan `SOURCE_REPO`:sta (`niko/gitea-ci-library``gitea-ci-library`)
- `{GIT_TAG_PREFIX}` tulee env-varista (sama kuin `gitea-env.conf`:ssa)
### `scripts/dispatch-workflow.sh`
Dispatchaa workflow_dispatchin kohderepoon ja pollaa valmistumista.
Generoi automaattisesti `dispatch_id`-tunnisteen, lisää sen dispatch-
inputteihin ja tunnistaa workflow-runin kohdereposta `display_title`-
kentän perusteella. Toimii luotettavasti vaikka samassa repossa olisi
useita samanaikaisia dispatch-attribuutioita.
**Argumentit:**
| # | Pakollinen | Kuvaus |
|---|------------|--------|
| 1 | Kyllä | Kohderepo (esim. `niko/agent-platform-gitops`) |
| 2 | Kyllä | Workflow-tiedosto (esim. `ci-main.yml`) |
| 3 | Kyllä | Branch/ref |
| 4 | Kyllä | Inputs JSON |
| 5 | Kyllä | Gitea API URL |
| 6 | Kyllä | Gitea token |
| 7 | Ei | Aikakatkaisu minuutteina (oletus 360) |
Kutsujan ei tarvitse välittää `dispatch_id`:tä — skripti generoi sen
itse ja lisää inputteihin ennen dispatchia.
## [skip ci]
Commit-viestissä on `[skip ci]`, joka estää GitActions-runneria
triggeröimästä uutta CI-ajoa GitOps-repoon pushista. Näin vältetään
ääretön trigger-loop.
## Race condition
`dispatch-workflow.sh` tunnistaa jokaisen dispatchatun runin uniikilla
`dispatch_id`-tunnisteella `display_title`-kentästä. Vaikka useampi
artifakti dispatchaisi samaan aikaan ja useita workflow-runeja olisi
käynnissä rinnakkain, jokainen skripti löytää oikean runinsa.
## Sääntöjä
1. **Token ei kirjoita code repoon.** GitOps-repon workflow ei tarvitse
oikeuksia code repoon. Kaikki status-kutsut kohdistuvat vain
GitOps-repoon. Code repo asettaa oman statusnsa itse.
2. **Ei provider-workflowta.** GitOps-päivitys ei ole reusable workflow.
GitOps-repo ajaa `scripts/gitops-update.sh`:n suoraan.
3. **Vain `workflow_dispatch`.** GitOps-repon workflow:ta ei triggeröidä
pushista — se laukeaa vain dispatch-kutsusta.
4. **Dispatch ei palauta tarkkaa SHA:**ta. Code repo ei tiedä GitOps-
commitin SHA:ta ennen dispatch-valmistumista. Status asetetaan
dispatchin exit-koodin perusteella, ei GitOps-commitin tiedoilla.
5. **`dispatch_id` on pakollinen kohde-workflow'ssa** — ilman sitä
`dispatch-workflow.sh` ei löydä oikeaa runia moniajo-tilanteessa.
6. **`[skip ci]` commit-viestissä.** Pakollinen trigger-loopin estoon.
+20 -19
View File
@@ -3,6 +3,7 @@
setup() {
source tests/helpers/mock-api.sh
export DISPATCH_POLL_INTERVAL="0.1"
export DISPATCH_ID="test123"
}
teardown() {
@@ -12,8 +13,7 @@ teardown() {
@test "dispatch succeeds: POST 201, poll running x3 then success → exit 0" {
mock_set_sequence '[
{"code":201},
{"code":200,"body":{"workflow_runs":[{"id":1,"status":"running"}]}},
{"code":200,"body":{"id":1,"status":"running"}},
{"code":200,"body":{"workflow_runs":[{"id":1,"display_title":"POC (test123)","run_number":42,"status":"running"}]}},
{"code":200,"body":{"id":1,"status":"running"}},
{"code":200,"body":{"id":1,"status":"running"}},
{"code":200,"body":{"id":1,"status":"completed","conclusion":"success"}}
@@ -26,7 +26,7 @@ teardown() {
@test "dispatch: poll returns failure conclusion → exit 1" {
mock_set_sequence '[
{"code":201},
{"code":200,"body":{"workflow_runs":[{"id":1,"status":"running"}]}},
{"code":200,"body":{"workflow_runs":[{"id":1,"display_title":"POC (test123)","run_number":42,"status":"running"}]}},
{"code":200,"body":{"id":1,"status":"running"}},
{"code":200,"body":{"id":1,"status":"completed","conclusion":"failure"}}
]'
@@ -38,7 +38,7 @@ teardown() {
@test "dispatch: poll returns cancelled conclusion → exit 1" {
mock_set_sequence '[
{"code":201},
{"code":200,"body":{"workflow_runs":[{"id":1,"status":"running"}]}},
{"code":200,"body":{"workflow_runs":[{"id":1,"display_title":"POC (test123)","run_number":42,"status":"running"}]}},
{"code":200,"body":{"id":1,"status":"running"}},
{"code":200,"body":{"id":1,"status":"completed","conclusion":"cancelled"}}
]'
@@ -47,18 +47,18 @@ teardown() {
[ "$status" -eq 1 ]
}
@test "timeout: poll never completes, exceeds timeout_minutes → exit 124" {
@test "timeout: no matching run found, exceeds timeout_minutes → exit 124" {
mock_set_sequence '[
{"code":201},
{"code":200,"body":{"workflow_runs":[{"id":1,"status":"running"}]}},
{"code":200,"body":{"id":1,"status":"running"}},
{"code":200,"body":{"id":1,"status":"running"}},
{"code":200,"body":{"id":1,"status":"running"}},
{"code":200,"body":{"id":1,"status":"running"}},
{"code":200,"body":{"id":1,"status":"running"}},
{"code":200,"body":{"id":1,"status":"running"}},
{"code":200,"body":{"id":1,"status":"running"}},
{"code":200,"body":{"id":1,"status":"running"}}
{"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"workflow_runs":[]}},
{"code":200,"body":{"workflow_runs":[]}}
]'
mock_start
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" '{"version":"1.2.3"}' "http://localhost:18080" "test-token-abc123" "0.001"
@@ -77,7 +77,7 @@ teardown() {
@test "POST dispatch is called with correct URL and payload" {
mock_set_sequence '[
{"code":201},
{"code":200,"body":{"workflow_runs":[{"id":1,"status":"running"}]}},
{"code":200,"body":{"workflow_runs":[{"id":1,"display_title":"POC (test123)","run_number":42,"status":"running"}]}},
{"code":200,"body":{"id":1,"status":"completed","conclusion":"success"}}
]'
mock_start
@@ -91,6 +91,7 @@ teardown() {
[[ "$body" == *'"ref":"main"'* ]]
[[ "$body" == *'"inputs"'* ]]
[[ "$body" == *'"version":"1.2.3"'* ]]
[[ "$body" == *'"dispatch_id":"test123"'* ]]
}
@test "missing gitea_api_url argument → exit 1 with error message" {
@@ -120,15 +121,15 @@ teardown() {
[ "$status" -eq 1 ]
}
@test "dispatch: no workflow run found after dispatch → exit 1" {
@test "dispatch: no workflow run found after dispatch → exit 124 (timeout)" {
mock_set_sequence '[
{"code":201},
{"code":200,"body":{"workflow_runs":[]}}
]'
mock_start
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" '{}' "http://localhost:18080" "test-token-abc123"
[ "$status" -eq 1 ]
[[ "$output" == *"ERROR"* ]]
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" '{}' "http://localhost:18080" "test-token-abc123" "0.001"
[ "$status" -eq 124 ]
[[ "$output" == *"ERROR"* || "$output" == *"Timeout"* ]]
}
@test "missing inputs_json argument → exit 1" {
+42
View File
@@ -0,0 +1,42 @@
Feature: GitOps update
As a GitOps repository
I want to update version references and report results to the caller
So that the deployment chain is traceable from source to GitOps commit
Background:
Given a project repository exists in Gitea
And a commit has been pushed to the repository
@mock
Scenario: Not enough env vars — script fails, no status set
Given insufficient environment variables are provided for the GitOps update
When the GitOps update script runs
Then the script exits with error
@mock
Scenario: GitOps job fails — no status set (SHA not yet known)
Given the GitOps repository clone will fail
When the GitOps update script runs
Then the script exits with error
@mock
Scenario: Everything succeeds — GitOps repo gets success status with link to caller
Given a valid GitOps update dispatch
When the GitOps update script runs
Then the script exits successfully
And the GitOps repo commit shows a success status with a link to the caller commit
@mock
Scenario: GitOps push fails — GitOps repo gets failure status
Given the GitOps repo push will fail after the version is committed
When the GitOps update script runs
Then the script exits with error
And the GitOps repo commit shows a failure status linking to the caller commit
@mock
Scenario: No changes — GitOps repo gets "no change" status
Given the version file already has the target version
When the GitOps update script runs
Then the script exits successfully
And the GitOps repo commit shows a "no change" status
And no Git commit or push was performed
@@ -0,0 +1,172 @@
const { spawnSync, execSync } = require('child_process');
const { Before, After, Given, When, Then } = require('@cucumber/cucumber');
const path = require('path');
const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..');
const MOCK_SCRIPT = path.join(PROJECT_ROOT, 'tests', 'helpers', 'mock-api.sh');
const GITOPS_SCRIPT = path.join(PROJECT_ROOT, 'scripts', 'gitops-update.sh');
const MOCK_HELPERS = path.join(PROJECT_ROOT, 'tests', 'helpers');
const REQ_FILE = '/tmp/gitops-mock-requests.log';
const BASE_ENV = {
INPUT_FILE: 'dev/Chart.yaml',
YQ_TPL: '(.version) = "{{VERSION}}"',
VERSION: '0.2.3',
SOURCE_REPO: 'niko/app',
SOURCE_COMMIT: 'abc123def456',
GITOPS_REPO: 'niko/app-gitops',
GITEA_API_URL: 'http://localhost:18080',
GITEA_TOKEN: 'test-token',
};
Before({ tags: '@mock' }, function () {
process.env.PATH = `${MOCK_HELPERS}:${process.env.PATH}`;
try { execSync('rm -f /tmp/gitops-mock-requests.log', { stdio: 'ignore' }); } catch (_) {}
// Restart mock with known request file path
const result = spawnSync('bash', ['-c', `
source "${MOCK_SCRIPT}"
mock_stop 2>/dev/null
MOCK_REQUEST_FILE="${REQ_FILE}"
mock_start
sleep 0.5
curl -s -o /dev/null -w "%{http_code}" --max-time 3 http://localhost:18080/api/v1/repos/health
`], {
cwd: PROJECT_ROOT, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe']
});
const code = result.stdout.trim();
if (!code.startsWith('2') && !code.startsWith('4')) {
throw new Error(`GitOps mock restart failed: ${result.stderr.substring(0,200)}`);
}
});
After({ tags: '@mock' }, function () {
spawnSync('bash', ['-c', `source "${MOCK_SCRIPT}" && mock_stop 2>/dev/null`], { stdio: 'ignore' });
try { execSync('rm -f /tmp/gitops-mock-requests.log /tmp/gitops-git-calls.log', { stdio: 'ignore' }); } catch (_) {}
});
function bash(cmd) {
const result = spawnSync('bash', ['-c', cmd], {
cwd: PROJECT_ROOT,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
});
return { status: result.status, stdout: result.stdout || '', stderr: result.stderr || '' };
}
function getFirstBody() {
return bash(`grep -A1 '^POST ' "${REQ_FILE}" 2>/dev/null | head -2 | tail -1 || echo ""`).stdout.trim();
}
function getFirstPath() {
return bash(`grep '^POST ' "${REQ_FILE}" 2>/dev/null | head -1 | awk '{print $2}' || echo ""`).stdout.trim();
}
function getLastBody() {
return bash(`grep -A1 '^POST ' "${REQ_FILE}" 2>/dev/null | grep -v '^POST ' | tail -1 || echo ""`).stdout.trim();
}
function getLastPath() {
return bash(`grep '^POST ' "${REQ_FILE}" 2>/dev/null | tail -1 | awk '{print $2}' || echo ""`).stdout.trim();
}
function requestCount() {
return parseInt(bash(`grep -c '^POST ' "${REQ_FILE}" 2>/dev/null || echo 0`).stdout.trim(), 10) || 0;
}
function gitCalls() {
const callsFile = process.env.GIT_CALLS_FILE || '/dev/null';
const out = bash(`cat "${callsFile}" 2>/dev/null || echo ""`).stdout;
return out.split('\n').filter(l => l.length > 0);
}
function runScript(envOverrides) {
const env = { ...BASE_ENV, ...envOverrides };
const scriptPath = `/tmp/gitops-run-${Date.now()}.sh`;
const exports = Object.entries(env)
.map(([k, v]) => `export ${k}="${v.replace(/"/g, '\\"')}"`)
.join('\n');
require('fs').writeFileSync(scriptPath, `${exports}\nexport PATH="${MOCK_HELPERS}:$PATH"\nset -euo pipefail\nbash "${GITOPS_SCRIPT}"\nsync\n`, 'utf8');
try {
return bash(`bash "${scriptPath}"`);
} finally {
require('fs').unlinkSync(scriptPath);
}
}
Given('insufficient environment variables are provided for the GitOps update', function () {
this.envOverrides = { INPUT_FILE: '' };
});
Given('the GitOps repository clone will fail', function () {
this.envOverrides = { GIT_MOCK_FAIL: '1' };
});
Given('a valid GitOps update dispatch', function () {
this.envOverrides = {};
});
Given('the GitOps repo push will fail after the version is committed', function () {
this.envOverrides = { GIT_MOCK_FAIL_PUSH: '1' };
});
Given('the version file already has the target version', function () {
this.envOverrides = {
GIT_MOCK_DIFF_NO_CHANGES: '1',
GIT_CALLS_FILE: '/tmp/gitops-git-calls.log',
};
});
When('the GitOps update script runs', function () {
this.result = runScript(this.envOverrides || {});
});
Then('the script exits with error', function () {
if (this.result.status === 0) throw new Error(`Expected non-zero exit, got 0. stderr: ${this.result.stderr.substring(0,200)}`);
});
Then('the script exits successfully', function () {
if (this.result.status !== 0) throw new Error(`Expected exit 0, got ${this.result.status}: ${this.result.stderr.substring(0,200)}`);
});
Then('the GitOps repo commit shows a success status with a link to the caller commit', function () {
const count = requestCount();
if (count < 1) throw new Error(`Expected at least 1 request, got ${count}`);
const body = getFirstBody();
if (!body.includes('"state":"success"')) throw new Error(`Expected success state, body: ${body.substring(0,200)}`);
if (!body.includes('"context":"app ')) throw new Error(`Expected context "app unknown", body: ${body.substring(0,200)}`);
if (!body.includes('"description":"Install to dev 0.2.3"')) throw new Error(`Expected description, body: ${body.substring(0,200)}`);
if (!body.includes('niko/app/commit/abc123def456')) throw new Error(`Expected link to caller commit, body: ${body.substring(0,200)}`);
const pathStr = getFirstPath();
if (!pathStr.includes('/repos/niko/app-gitops/statuses/')) throw new Error(`Expected gitops repo path, got: ${pathStr}`);
});
Then('the GitOps repo commit shows a failure status linking to the caller commit', function () {
const count = requestCount();
if (count < 1) throw new Error(`Expected at least 1 request, got ${count}`);
const body = getFirstBody();
if (!body.includes('"state":"failure"')) throw new Error(`Expected failure state, body: ${body.substring(0,200)}`);
if (!body.includes('"context":"app ')) throw new Error(`Expected context "app unknown", body: ${body.substring(0,200)}`);
if (!body.includes('"description":"Install to dev 0.2.3"')) throw new Error(`Expected description, body: ${body.substring(0,200)}`);
if (!body.includes('niko/app/commit/abc123def456')) throw new Error(`Expected link to caller commit, body: ${body.substring(0,200)}`);
const pathStr = getFirstPath();
if (!pathStr.includes('/repos/niko/app-gitops/statuses/')) throw new Error(`Expected gitops repo path, got: ${pathStr}`);
});
Then('the GitOps repo commit shows a "no change" status', function () {
const count = requestCount();
if (count < 1) throw new Error(`Expected at least 1 request, got ${count}`);
const body = getFirstBody();
if (!body.includes('"state":"success"')) throw new Error(`Expected success state, body: ${body.substring(0,200)}`);
if (!body.includes('"description":"Install to dev 0.2.3 \u2014 no change"')) {
throw new Error(`Expected "no change" description, body: ${body.substring(0,200)}`);
}
const pathStr = getFirstPath();
if (!pathStr.includes('/repos/niko/app-gitops/statuses/')) throw new Error(`Expected gitops repo path, got: ${pathStr}`);
});
Then('no Git commit or push was performed', function () {
const calls = gitCalls();
if (calls.some(l => l.includes(' commit ') || l.includes(' push '))) {
throw new Error(`Expected no commit or push, got: ${calls.join(', ')}`);
}
});
@@ -15,7 +15,7 @@ function bash(cmd) {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
});
return { status: 0, stdout: out };
return { status: 0, stdout: out, stderr: '' };
} catch (e) {
return { status: e.status, stdout: e.stdout || '', stderr: e.stderr || '' };
}
@@ -54,7 +54,7 @@ function setupMock(seqJson) {
}
function runDispatch(args) {
return bash(`export DISPATCH_POLL_INTERVAL="0.1"; bash "${DISPATCH_SCRIPT}" ${args}`);
return bash(`export DISPATCH_ID="test123"; export DISPATCH_POLL_INTERVAL="0.1"; bash "${DISPATCH_SCRIPT}" ${args}`);
}
Given('a deployment has completed in the target environment', function () {
@@ -66,7 +66,7 @@ Given('the test project repository exists with test definitions', function () {
When('a test workflow is dispatched to a test project', function () {
setupMock(JSON.stringify([
{ code: 201 },
{ code: 200, body: { workflow_runs: [{ id: 1, status: 'running' }] } },
{ code: 200, body: { workflow_runs: [{ id: 1, display_title: 'Workflow (test123)', run_number: 42, status: 'running' }] } },
{ code: 200, body: { id: 1, status: 'completed', conclusion: 'success' } },
]));
const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\' "http://localhost:18080" "test-token-abc123"');
@@ -84,7 +84,7 @@ Then('the pipeline continues only after receiving a success result', function ()
When('a test workflow is dispatched and the tests fail', function () {
setupMock(JSON.stringify([
{ code: 201 },
{ code: 200, body: { workflow_runs: [{ id: 1, status: 'running' }] } },
{ code: 200, body: { workflow_runs: [{ id: 1, display_title: 'Workflow (test123)', run_number: 42, status: 'running' }] } },
{ code: 200, body: { id: 1, status: 'completed', conclusion: 'failure' } },
]));
const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\' "http://localhost:18080" "test-token-abc123"');
@@ -98,15 +98,19 @@ Then('the calling pipeline reports failure', function () {
When('a test workflow is dispatched but does not finish within the allowed time', function () {
setupMock(JSON.stringify([
{ code: 201 },
{ code: 200, body: { workflow_runs: [{ id: 1, status: 'running' }] } },
{ code: 200, body: { id: 1, status: 'running' } },
{ code: 200, body: { id: 1, status: 'running' } },
{ code: 200, body: { id: 1, status: 'running' } },
{ code: 200, body: { workflow_runs: [] } },
{ code: 200, body: { workflow_runs: [] } },
{ code: 200, body: { workflow_runs: [] } },
{ code: 200, body: { workflow_runs: [] } },
{ code: 200, body: { workflow_runs: [] } },
]));
const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\' "http://localhost:18080" "test-token-abc123" "0.001"');
const r = runDispatch('"test-owner/test-repo" "test.yml" "main" \'{"version":"1.2.3"}\' "http://localhost:18080" "test-token-abc123" "0.05"');
this.dispatchResult = r.status;
this.dispatchStderr = r.stderr;
});
Then('the calling pipeline reports a timeout error', function () {
if (this.dispatchResult !== 124) throw new Error(`Expected timeout exit 124, got ${this.dispatchResult}`);
if (this.dispatchResult !== 124) {
throw new Error(`Expected timeout exit 124, got ${this.dispatchResult}. stderr: ${(this.dispatchStderr || '').substring(0,300)}`);
}
});
+176
View File
@@ -0,0 +1,176 @@
#!/usr/bin/env bats
setup() {
export INPUT_FILE=dev/Chart.yaml
export YQ_TPL='version = "{{VERSION}}"'
export VERSION=1.0.0
export SOURCE_REPO=niko/app
export SOURCE_COMMIT=abc123def456
export GITOPS_REPO=niko/app-gitops
export GITEA_TOKEN=test-token
export GITEA_API_URL=http://localhost:18080
}
teardown() {
if type mock_stop &>/dev/null 2>&1; then
mock_stop 2>/dev/null || true
fi
}
@test "missing GITEA_API_URL causes exit 1" {
unset GITEA_API_URL
run bash scripts/gitops-update.sh
[ "$status" -eq 1 ]
[[ "$output" == *"GITEA_API_URL"* ]]
}
@test "missing GITEA_TOKEN causes exit 1" {
unset GITEA_TOKEN
run bash scripts/gitops-update.sh
[ "$status" -eq 1 ]
[[ "$output" == *"GITEA_TOKEN"* ]]
}
@test "missing INPUT_FILE causes exit 1" {
unset INPUT_FILE
run bash scripts/gitops-update.sh
[ "$status" -eq 1 ]
[[ "$output" == *"INPUT_FILE"* ]]
}
@test "missing YQ_TPL causes exit 1" {
unset YQ_TPL
run bash scripts/gitops-update.sh
[ "$status" -eq 1 ]
[[ "$output" == *"YQ_TPL"* ]]
}
@test "missing VERSION causes exit 1" {
unset VERSION
run bash scripts/gitops-update.sh
[ "$status" -eq 1 ]
[[ "$output" == *"VERSION"* ]]
}
@test "missing SOURCE_REPO causes exit 1" {
unset SOURCE_REPO
run bash scripts/gitops-update.sh
[ "$status" -eq 1 ]
[[ "$output" == *"SOURCE_REPO"* ]]
}
@test "missing SOURCE_COMMIT causes exit 1" {
unset SOURCE_COMMIT
run bash scripts/gitops-update.sh
[ "$status" -eq 1 ]
[[ "$output" == *"SOURCE_COMMIT"* ]]
}
@test "_gitops_substitute replaces {{VERSION}}" {
run bash -c '
source scripts/gitops-update.sh >/dev/null 2>&1
_gitops_substitute "(.version) = \"{{VERSION}}\"" "0.2.3"
'
[ "$status" -eq 0 ]
[[ "$output" == '(.version) = "0.2.3"' ]]
}
@test "CLONE_URL is constructed correctly from GITEA_API_URL" {
export GITEA_API_URL=https://gitea.app.keskikuja.site
export GITEA_TOKEN=secret123
export GITOPS_REPO=niko/app-gitops
run bash -c '
source scripts/gitops-update.sh >/dev/null 2>&1
echo "$CLONE_URL"
'
[ "$status" -eq 0 ]
[ "$output" = "https://secret123@gitea.app.keskikuja.site/niko/app-gitops.git" ]
}
@test "CLONE_URL works with http:// URL" {
export GITEA_API_URL=http://localhost:18080
export GITEA_TOKEN=token
export GITOPS_REPO=owner/repo
run bash -c '
source scripts/gitops-update.sh >/dev/null 2>&1
echo "$CLONE_URL"
'
[ "$status" -eq 0 ]
[ "$output" = "https://token@localhost:18080/owner/repo.git" ]
}
@test "_gitops_substitute handles multiple {{VERSION}} occurrences" {
run bash -c '
source scripts/gitops-update.sh >/dev/null 2>&1
_gitops_substitute "version = \"{{VERSION}}\"; tag = \"v{{VERSION}}\"" "1.2.3"
'
[ "$status" -eq 0 ]
[[ "$output" == 'version = "1.2.3"; tag = "v1.2.3"' ]]
}
@test "git flow: clone yq add commit push" {
source tests/helpers/mock-api.sh
mock_set_sequence '[
{"code":201},
{"code":201}
]'
mock_start
export GIT_CALLS_FILE=$(mktemp)
export YQ_CALLS_FILE=$(mktemp)
export PATH="${BATS_TEST_DIRNAME}/helpers:$PATH"
export INPUT_FILE=dev/Chart.yaml
export YQ_TPL='(.version) = "{{VERSION}}"'
export VERSION=0.2.3
export SOURCE_REPO=niko/app
export SOURCE_COMMIT=abc123def456
export GITOPS_REPO=niko/app-gitops
export GITEA_API_URL=http://localhost:18080
export GITEA_TOKEN=test-token
run bash scripts/gitops-update.sh
[ "$status" -eq 0 ]
git_calls=$(cat "$GIT_CALLS_FILE")
[[ "$git_calls" == *"clone"* ]]
[[ "$git_calls" == *"add"* ]]
[[ "$git_calls" == *"commit"* ]]
[[ "$git_calls" == *"push"* ]]
yq_calls=$(cat "$YQ_CALLS_FILE")
[[ "$yq_calls" == *"eval -i"* ]]
rm -f "$GIT_CALLS_FILE" "$YQ_CALLS_FILE"
mock_stop
}
@test "one commit-status call: gitops-repo only" {
source tests/helpers/mock-api.sh
mock_set_sequence '[
{"code":201}
]'
mock_start
export GIT_CALLS_FILE=$(mktemp)
export YQ_CALLS_FILE=$(mktemp)
export PATH="${BATS_TEST_DIRNAME}/helpers:$PATH"
export INPUT_FILE=dev/Chart.yaml
export YQ_TPL='(.version) = "{{VERSION}}"'
export VERSION=0.2.3
export SOURCE_REPO=niko/app
export SOURCE_COMMIT=abc123def456
export GITOPS_REPO=niko/app-gitops
export GITEA_API_URL=http://localhost:18080
export GITEA_TOKEN=test-token
run bash scripts/gitops-update.sh
[ "$status" -eq 0 ]
path=$(mock_get_first_request_path)
body=$(mock_get_first_request_body)
[[ "$path" == *"/repos/niko/app-gitops/statuses/"* ]]
[[ "$body" == *'"context":"app '* ]]
[[ "$body" == *'"description":"Install to dev 0.2.3"'* ]]
[[ "$body" == *'"state":"success"'* ]]
rm -f "$GIT_CALLS_FILE" "$YQ_CALLS_FILE"
mock_stop
}
@test "missing GITOPS_REPO causes exit 1" {
unset GITOPS_REPO
run bash scripts/gitops-update.sh
[ "$status" -eq 1 ]
[[ "$output" == *"GITOPS_REPO"* ]]
}
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
echo "git $*" >> "${GIT_CALLS_FILE:-/dev/null}"
[ -z "${GIT_MOCK_FAIL:-}" ] || { echo "git: mock forced failure" >&2; exit 1; }
if [ "${1:-}" = "push" ] && [ -n "${GIT_MOCK_FAIL_PUSH:-}" ]; then
echo "git: mock push failure" >&2
exit 1
fi
# Skip -c config arguments
while [ "${1:-}" = "-c" ]; do
shift 2
done
case "$1" in
clone)
TARGET_DIR="${@: -1}"
mkdir -p "${TARGET_DIR}/$(dirname "$INPUT_FILE")"
echo 'version: 0.1.0' > "${TARGET_DIR}/${INPUT_FILE}"
echo "Cloning into '$TARGET_DIR'..."
;;
add|commit|push|config|init)
;;
diff)
# Default: exit 1 = has changes → proceed to commit
# GIT_MOCK_DIFF_NO_CHANGES=1 → exit 0 = no changes → "no change" path
if [ -n "${GIT_MOCK_DIFF_NO_CHANGES:-}" ]; then
exit 0
fi
exit 1
;;
rev-parse)
echo "mock-sha-9876543210fedcba9876543210fedcba98765432"
;;
*)
echo "git: unknown command: $*" >&2
exit 1
;;
esac
+1 -1
View File
@@ -46,7 +46,7 @@ mock_clear_sequence() {
mock_start() {
MOCK_RESPONSE_CODE="${MOCK_RESPONSE_CODE:-201}"
MOCK_REQUEST_FILE=$(mktemp)
MOCK_REQUEST_FILE="${MOCK_REQUEST_FILE:-$(mktemp)}"
echo "$MOCK_REQUEST_FILE" > "$MOCK_STATE_FILE"
MOCK_CONFIG_FILE=$(mktemp)
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env bash
echo "yq $*" >> "${YQ_CALLS_FILE:-/dev/null}"