Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc6bb78973 | |||
| a5947551d4 | |||
| 4910565547 | |||
| 2bef079d03 | |||
| 4f20f5ae2f | |||
| bd93ef2f8f | |||
| f06cb112d8 | |||
| d57f56f196 | |||
| 277c0f882d | |||
| cc7f4f0976 | |||
| 6621b3f336 | |||
| c0012ba6fa | |||
| 908aee9f8b | |||
| ad4c2cd570 | |||
| 1e10633e60 |
@@ -0,0 +1,20 @@
|
|||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
node_modules/
|
||||||
|
reports/
|
||||||
|
coverage/
|
||||||
|
.ai/
|
||||||
|
.cursor/
|
||||||
|
.vscode/
|
||||||
|
tmp/
|
||||||
|
.DS_Store
|
||||||
|
*.md
|
||||||
|
docs/
|
||||||
|
guides/
|
||||||
|
.simplecov
|
||||||
|
cucumber.js
|
||||||
|
package-lock.json
|
||||||
|
package.json
|
||||||
|
CURRENT_PROVIDER_VERSION
|
||||||
|
README.md
|
||||||
|
AGENTS.md
|
||||||
@@ -27,54 +27,17 @@ jobs:
|
|||||||
version: ${{ steps.set-outputs.outputs.version }}
|
version: ${{ steps.set-outputs.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: niko/gitea-ci-library
|
||||||
|
path: .ci
|
||||||
|
|
||||||
- name: Check existing artifact and calculate version
|
- name: Check existing artifact and calculate version
|
||||||
run: |
|
env:
|
||||||
if [ -n "${VERSION_FILE}" ]; then
|
SERVER_URL: ${{ gitea.server_url }}
|
||||||
if echo "${VERSION_FILE}" | grep -q '\.json$'; then
|
REPO: ${{ github.repository }}
|
||||||
RAW_VERSION=$(jq -r '.version' "${VERSION_FILE}")
|
SHA: ${{ github.sha }}
|
||||||
else
|
run: bash .ci/scripts/check-version.sh
|
||||||
RAW_VERSION=$(cat "${VERSION_FILE}" | tr -d '[:space:]')
|
|
||||||
fi
|
|
||||||
elif [ -f VERSION ]; then
|
|
||||||
RAW_VERSION=$(cat VERSION | tr -d '[:space:]')
|
|
||||||
elif [ -f package.json ]; then
|
|
||||||
RAW_VERSION=$(jq -r '.version' package.json)
|
|
||||||
elif [ -f pom.xml ]; then
|
|
||||||
RAW_VERSION=$(grep -oP '<version>\K[^<]+' pom.xml | head -1)
|
|
||||||
else
|
|
||||||
echo "ERROR: No VERSION file, package.json, or pom.xml found" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
BASE_VERSION=$(echo "$RAW_VERSION" | cut -d'.' -f1-2)
|
|
||||||
echo "gitea-ci-library - Tunnistettu Major.Minor versio: $BASE_VERSION"
|
|
||||||
|
|
||||||
TAGS_JSON=$(curl -s -f -H "Authorization: token $GITEA_TOKEN" \
|
|
||||||
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/tags")
|
|
||||||
|
|
||||||
TAG=$(echo "$TAGS_JSON" | jq -r --arg prefix "${GIT_TAG_PREFIX}" '
|
|
||||||
if type == "array" then
|
|
||||||
.[] | select(.commit.sha == "${{ github.sha }}" and (.name | startswith($prefix))) | .name
|
|
||||||
else empty end' | head -1)
|
|
||||||
|
|
||||||
mkdir -p /tmp/build-ctx
|
|
||||||
|
|
||||||
if [ -n "$TAG" ]; then
|
|
||||||
echo "ARTIFACT_EXISTS=true" > /tmp/build-ctx/build.env
|
|
||||||
echo "NEXT_VERSION=$TAG" >> /tmp/build-ctx/build.env
|
|
||||||
echo "gitea-ci-library - Artefakti löytyi jo tagilla: $TAG."
|
|
||||||
else
|
|
||||||
echo "ARTIFACT_EXISTS=false" > /tmp/build-ctx/build.env
|
|
||||||
|
|
||||||
HIGHEST_PATCH=$(echo "$TAGS_JSON" | jq -r --arg prefix "${GIT_TAG_PREFIX}" --arg bv "${GIT_TAG_PREFIX}${BASE_VERSION}." '
|
|
||||||
if type == "array" then .[] | .name | select(startswith($bv)) | sub($bv; "") | tonumber else empty end' | sort -rn | head -1)
|
|
||||||
|
|
||||||
if [ -z "$HIGHEST_PATCH" ]; then NEXT_PATCH=0; else NEXT_PATCH=$((HIGHEST_PATCH + 1)); fi
|
|
||||||
FULL_VERSION="${BASE_VERSION}.${NEXT_PATCH}"
|
|
||||||
|
|
||||||
echo "NEXT_VERSION=$FULL_VERSION" >> /tmp/build-ctx/build.env
|
|
||||||
echo "gitea-ci-library - Uusi vapaa versio: $FULL_VERSION"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Set job outputs
|
- name: Set job outputs
|
||||||
id: set-outputs
|
id: set-outputs
|
||||||
|
|||||||
@@ -33,20 +33,22 @@ jobs:
|
|||||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME || github.actor }}
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME || github.actor }}
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
REGISTRY="${DOCKER_REGISTRY:?DOCKER_REGISTRY not set in conf}"
|
if [ -z "${DOCKER_REGISTRY}" ]; then echo "ERROR: DOCKER_REGISTRY not set in conf"; exit 1; fi
|
||||||
|
REGISTRY="${DOCKER_REGISTRY}"
|
||||||
|
REGISTRY_HOST="${REGISTRY%%/*}"
|
||||||
DOCKERFILE="${{ inputs.dockerfile_path }}"
|
DOCKERFILE="${{ inputs.dockerfile_path }}"
|
||||||
IMAGE_NAME="${{ inputs.image_name }}"
|
IMAGE_NAME="${{ inputs.image_name }}"
|
||||||
TAG="${{ inputs.tag }}"
|
TAG="${{ inputs.tag }}"
|
||||||
|
|
||||||
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
CONTEXT_DIR=$(dirname "${DOCKERFILE}")
|
||||||
docker build \
|
docker build \
|
||||||
--label "git.commit=${{ github.sha }}" \
|
--label "git.commit=${{ github.sha }}" \
|
||||||
--label "git.commitBy=${{ github.actor }}" \
|
--label "git.commitBy=${{ github.actor }}" \
|
||||||
--label "build.date=${NOW}" \
|
--label "build.date=${NOW}" \
|
||||||
-f "${DOCKERFILE}" \
|
-f "${DOCKERFILE}" \
|
||||||
-t "${IMAGE_NAME}:${TAG}" .
|
-t "${IMAGE_NAME}:${TAG}" \
|
||||||
|
"${CONTEXT_DIR}"
|
||||||
REGISTRY_HOST="${REGISTRY%%/*}"
|
|
||||||
|
|
||||||
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
|
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
|
||||||
echo "Pushing ${FULL_IMAGE} ..."
|
echo "Pushing ${FULL_IMAGE} ..."
|
||||||
|
|||||||
@@ -45,29 +45,33 @@ jobs:
|
|||||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME || github.actor }}
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME || github.actor }}
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
|
if [ -z "${DOCKER_REGISTRY}" ]; then echo "ERROR: DOCKER_REGISTRY not set in env.conf"; exit 1; fi
|
||||||
|
if [ -z "${DOCKER_IMAGE_NAME}" ]; then echo "ERROR: DOCKER_IMAGE_NAME not set in env.conf"; exit 1; fi
|
||||||
|
REGISTRY="${DOCKER_REGISTRY}"
|
||||||
|
IMAGE="${DOCKER_IMAGE_NAME}"
|
||||||
|
REGISTRY_HOST="${REGISTRY%%/*}"
|
||||||
|
|
||||||
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
CONTEXT_DIR=$(dirname "${DOCKERFILE}")
|
||||||
docker build \
|
docker build \
|
||||||
--label "git.commit=${{ github.sha }}" \
|
--label "git.commit=${{ github.sha }}" \
|
||||||
--label "git.commitBy=${{ github.actor }}" \
|
--label "git.commitBy=${{ github.actor }}" \
|
||||||
--label "build.date=${NOW}" \
|
--label "build.date=${NOW}" \
|
||||||
-f "${DOCKERFILE}" \
|
-f "${DOCKERFILE}" \
|
||||||
-t "${DOCKER_IMAGE_NAME}:${VERSION}" \
|
-t "${IMAGE}:${VERSION}" \
|
||||||
-t "${DOCKER_IMAGE_NAME}:latest" .
|
-t "${IMAGE}:latest" \
|
||||||
|
"${CONTEXT_DIR}"
|
||||||
REGISTRY="${DOCKER_REGISTRY:?DOCKER_REGISTRY not set in env.conf}"
|
|
||||||
IMAGE="${DOCKER_IMAGE_NAME:?DOCKER_IMAGE_NAME not set in env.conf}"
|
|
||||||
REGISTRY_HOST="${REGISTRY%%/*}"
|
|
||||||
|
|
||||||
FULL_IMAGE="${REGISTRY}/${IMAGE}:${VERSION}"
|
FULL_IMAGE="${REGISTRY}/${IMAGE}:${VERSION}"
|
||||||
echo "Pushing ${FULL_IMAGE} ..."
|
echo "Pushing ${FULL_IMAGE} ..."
|
||||||
|
|
||||||
docker tag "${DOCKER_IMAGE_NAME}:${VERSION}" "$FULL_IMAGE"
|
docker tag "${IMAGE}:${VERSION}" "$FULL_IMAGE"
|
||||||
echo "$DOCKER_PASSWORD" | docker login "$REGISTRY_HOST" -u "$DOCKER_USERNAME" --password-stdin
|
echo "$DOCKER_PASSWORD" | docker login "$REGISTRY_HOST" -u "$DOCKER_USERNAME" --password-stdin
|
||||||
docker push "$FULL_IMAGE"
|
docker push "$FULL_IMAGE"
|
||||||
|
|
||||||
FULL_LATEST="${REGISTRY}/${IMAGE}:latest"
|
FULL_LATEST="${REGISTRY}/${IMAGE}:latest"
|
||||||
echo "Pushing ${FULL_LATEST} ..."
|
echo "Pushing ${FULL_LATEST} ..."
|
||||||
docker tag "${DOCKER_IMAGE_NAME}:latest" "$FULL_LATEST"
|
docker tag "${IMAGE}:latest" "$FULL_LATEST"
|
||||||
docker push "$FULL_LATEST"
|
docker push "$FULL_LATEST"
|
||||||
|
|
||||||
docker logout "$REGISTRY_HOST"
|
docker logout "$REGISTRY_HOST"
|
||||||
@@ -79,11 +83,12 @@ jobs:
|
|||||||
if [ -n "${DOCKER_UI_URL:-}" ] && [ -n "${VERSION:-}" ]; then
|
if [ -n "${DOCKER_UI_URL:-}" ] && [ -n "${VERSION:-}" ]; then
|
||||||
CONTAINER_URL="${DOCKER_UI_URL}/${DOCKER_IMAGE_NAME}/${VERSION}"
|
CONTAINER_URL="${DOCKER_UI_URL}/${DOCKER_IMAGE_NAME}/${VERSION}"
|
||||||
fi
|
fi
|
||||||
bash .ci/scripts/report-status.sh success "Docker build & push ${VERSION} OK" ci-docker-build-push "" "$CONTAINER_URL"
|
DIR=$(dirname "${DOCKERFILE}")
|
||||||
|
if [ "$DIR" != "." ]; then
|
||||||
- name: Report status FAILURE
|
bash .ci/scripts/report-status.sh success "${DIR}: Docker push ${VERSION}" "${DIR}-ci-docker-build-push" "" "$CONTAINER_URL"
|
||||||
if: failure()
|
else
|
||||||
run: bash .ci/scripts/report-status.sh failure "Docker build & push ${VERSION} FAILED" ci-docker-build-push
|
bash .ci/scripts/report-status.sh success "Docker push ${VERSION}" ci-docker-build-push "" "$CONTAINER_URL"
|
||||||
|
fi
|
||||||
|
|
||||||
tag-commit:
|
tag-commit:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ on:
|
|||||||
bats-image:
|
bats-image:
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: gitea.app.keskikuja.site/niko/ci-bats:latest
|
default: gitea.app.keskikuja.site/niko/ci-bats:git
|
||||||
secrets:
|
secrets:
|
||||||
GITEA_TOKEN:
|
GITEA_TOKEN:
|
||||||
required: true
|
required: true
|
||||||
@@ -48,4 +48,4 @@ jobs:
|
|||||||
|
|
||||||
- name: Report
|
- name: Report
|
||||||
if: always()
|
if: always()
|
||||||
run: bash .ci/scripts/ci-report.sh "Bats test report" unit-tests bats
|
run: bash .ci/scripts/ci-report.sh "Bats test report" unit-tests bats ${{ job.status }}
|
||||||
|
|||||||
@@ -1,21 +1,41 @@
|
|||||||
name: Build CI Bats Container (Manual)
|
name: CI Container Build Bats
|
||||||
on: workflow_dispatch
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
config_path:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
default: '.gitea/workflows/example-gitea-env.conf'
|
||||||
|
description: 'Polku .gitea-env.conf-tiedostoon'
|
||||||
|
dockerfile_path:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
default: 'Dockerfile.ci-bats'
|
||||||
|
description: 'Polku Dockerfileen'
|
||||||
|
image_name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
default: 'ci-bats'
|
||||||
|
description: 'Kontin nimi ilman registry-polkua'
|
||||||
|
tag:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
default: 'latest'
|
||||||
|
description: 'Image-tägi'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
load-config:
|
load-config:
|
||||||
name: Load config
|
|
||||||
uses: niko/gitea-ci-library/.gitea/workflows/config-provider.yml@main
|
uses: niko/gitea-ci-library/.gitea/workflows/config-provider.yml@main
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
config_path: .gitea/workflows/example-gitea-env.conf
|
config_path: ${{ inputs.config_path }}
|
||||||
|
|
||||||
build-push:
|
build-push:
|
||||||
name: Build & Push
|
|
||||||
needs: [load-config]
|
needs: [load-config]
|
||||||
uses: niko/gitea-ci-library/.gitea/workflows/ci-container-build-push.yml@main
|
uses: niko/gitea-ci-library/.gitea/workflows/ci-container-build-push.yml@main
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
env_json: ${{ needs.load-config.outputs.env_json }}
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
dockerfile_path: Dockerfile.ci-bats
|
dockerfile_path: ${{ inputs.dockerfile_path }}
|
||||||
image_name: ci-bats
|
image_name: ${{ inputs.image_name }}
|
||||||
tag: latest
|
tag: ${{ inputs.tag }}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ on:
|
|||||||
cucumber-node-image:
|
cucumber-node-image:
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: gitea.app.keskikuja.site/niko/ci-cucumber:latest
|
default: gitea.app.keskikuja.site/niko/ci-cucumber:with-python
|
||||||
secrets:
|
secrets:
|
||||||
GITEA_TOKEN:
|
GITEA_TOKEN:
|
||||||
required: true
|
required: true
|
||||||
@@ -38,10 +38,10 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p reports/cucumber
|
mkdir -p reports/cucumber
|
||||||
npx cucumber-js \
|
npx cucumber-js \
|
||||||
--format json:reports/cucumber/report.json \
|
--format json:reports/cucumber/results.json \
|
||||||
--format html:reports/cucumber/report.html 2>&1
|
--format html:reports/cucumber/test-report.html 2>&1
|
||||||
|
|
||||||
- name: Report
|
- name: Report
|
||||||
if: always()
|
if: always()
|
||||||
shell: bash
|
shell: bash
|
||||||
run: bash .ci/scripts/ci-report.sh "Cucumber test report" acc-tests cucumber
|
run: bash .ci/scripts/ci-report.sh "Cucumber test report" acc-tests cucumber ${{ job.status }}
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ DOCKER_REGISTRY=gitea.app.keskikuja.site/niko
|
|||||||
DOCKER_IMAGE_NAME=gitea-ci-library-test-image
|
DOCKER_IMAGE_NAME=gitea-ci-library-test-image
|
||||||
DOCKER_UI_URL=https://gitea.app.keskikuja.site/niko/-/packages/container
|
DOCKER_UI_URL=https://gitea.app.keskikuja.site/niko/-/packages/container
|
||||||
#DOCKERFILE=Dockerfile.platform
|
#DOCKERFILE=Dockerfile.platform
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
env_json: ${{ needs.load-config.outputs.env_json }}
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
|
||||||
build-push:
|
docker-build-push:
|
||||||
name: Build & Push Docker
|
name: Build & Push Docker
|
||||||
needs: [load-config, check-version, bats, cucumber]
|
needs: [load-config, check-version, bats, cucumber]
|
||||||
if: needs.check-version.outputs.artifact_exists != 'true'
|
if: needs.check-version.outputs.artifact_exists != 'true'
|
||||||
@@ -49,18 +49,32 @@ jobs:
|
|||||||
env_json: ${{ needs.load-config.outputs.env_json }}
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
version: ${{ needs.check-version.outputs.version }}
|
version: ${{ needs.check-version.outputs.version }}
|
||||||
|
|
||||||
|
docker-gitops:
|
||||||
|
name: GitOps
|
||||||
|
needs: [docker-build-push]
|
||||||
|
uses: niko/gitea-ci-library/.gitea/workflows/gitops-dispatch.yml@main
|
||||||
|
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:
|
report-summary:
|
||||||
name: Report Summary
|
name: Report Summary
|
||||||
needs: [load-config, build-push]
|
needs: [load-config, check-version, docker-build-push, docker-gitops]
|
||||||
if: always()
|
if: always()
|
||||||
uses: niko/gitea-ci-library/.gitea/workflows/report-summary.yml@main
|
uses: niko/gitea-ci-library/.gitea/workflows/report-summary.yml@main
|
||||||
with:
|
with:
|
||||||
env_json: ${{ needs.load-config.outputs.env_json }}
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
suites: bats cucumber
|
suites: bats cucumber
|
||||||
|
gitops: |
|
||||||
|
${{ needs.docker-gitops.outputs.summary }}
|
||||||
|
|
||||||
tag-maintenance:
|
tag-maintenance:
|
||||||
name: Move provider version tag
|
name: Move provider version tag
|
||||||
needs: [build-push]
|
needs: [docker-gitops]
|
||||||
if: success()
|
if: success()
|
||||||
uses: niko/gitea-ci-library/.gitea/workflows/tag-maintenance.yml@main
|
uses: niko/gitea-ci-library/.gitea/workflows/tag-maintenance.yml@main
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
name: CI Git-Pages Main
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- git-pages/**
|
||||||
|
- .gitea/workflows/git-pages.*
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
load-config:
|
||||||
|
name: Load git-pages.gitea-env.conf to pipeline env
|
||||||
|
uses: niko/gitea-ci-library/.gitea/workflows/config-provider.yml@main
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
config_path: .gitea/workflows/git-pages.gitea-env.conf
|
||||||
|
|
||||||
|
check-version:
|
||||||
|
name: Check existing artifact
|
||||||
|
needs: [load-config]
|
||||||
|
uses: niko/gitea-ci-library/.gitea/workflows/check-version.yml@main
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
|
||||||
|
helm-push:
|
||||||
|
name: Build & Push Helm chart
|
||||||
|
needs: [load-config, check-version]
|
||||||
|
if: needs.check-version.outputs.artifact_exists != 'true'
|
||||||
|
uses: niko/gitea-ci-library/.gitea/workflows/helm-build-push.yml@main
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
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, 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 }}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
GITEA_API_URL=https://gitea.app.keskikuja.site
|
||||||
|
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
|
||||||
|
|
||||||
@@ -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"
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
name: Helm Build & Push
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
env_json:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
version:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
secrets:
|
||||||
|
GITEA_TOKEN:
|
||||||
|
required: true
|
||||||
|
HELM_USER:
|
||||||
|
required: false
|
||||||
|
HELM_PASSWORD:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_API_URL: ${{ fromJson(inputs.env_json).GITEA_API_URL }}
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
HELM_REGISTRY: ${{ fromJson(inputs.env_json).HELM_REGISTRY || '' }}
|
||||||
|
HELM_UI_URL: ${{ fromJson(inputs.env_json).HELM_UI_URL || '' }}
|
||||||
|
GIT_TAG_PREFIX: ${{ fromJson(inputs.env_json).GIT_TAG_PREFIX || '' }}
|
||||||
|
CHART_FILE: ${{ fromJson(inputs.env_json).VERSION_FILE || 'Chart.yaml' }}
|
||||||
|
VERSION: ${{ inputs.version }}
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: alpine/helm:3.19.0
|
||||||
|
steps:
|
||||||
|
- name: Install Node.js for actions/checkout
|
||||||
|
# COMPROMISE: Requires internet access.
|
||||||
|
# Does NOT work in air-gapped environments.
|
||||||
|
# Replace with a custom image (e.g., extending alpine/helm + nodejs) if needed.
|
||||||
|
run: apk add --no-cache nodejs
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: niko/gitea-ci-library
|
||||||
|
path: .ci
|
||||||
|
|
||||||
|
- name: Package Helm chart
|
||||||
|
run: |
|
||||||
|
CHART_DIR=$(dirname "${CHART_FILE}")
|
||||||
|
helm dependency update "${CHART_DIR}"
|
||||||
|
helm package "${CHART_DIR}" \
|
||||||
|
--version "${VERSION}" \
|
||||||
|
--app-version "${VERSION}" \
|
||||||
|
--destination /tmp/helm-packages
|
||||||
|
|
||||||
|
- name: Push to OCI registry
|
||||||
|
env:
|
||||||
|
HELM_USER: ${{ secrets.HELM_USER || github.actor }}
|
||||||
|
HELM_PASSWORD: ${{ secrets.HELM_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
REGISTRY="${HELM_REGISTRY:?HELM_REGISTRY not set in env.conf}"
|
||||||
|
echo "$HELM_PASSWORD" | helm registry login "${REGISTRY}" \
|
||||||
|
-u "$HELM_USER" \
|
||||||
|
--password-stdin
|
||||||
|
helm push /tmp/helm-packages/*.tgz "oci://${REGISTRY}"
|
||||||
|
helm registry logout "${REGISTRY}"
|
||||||
|
|
||||||
|
- name: Report status with UI link
|
||||||
|
if: success() && env.HELM_UI_URL != ''
|
||||||
|
run: |
|
||||||
|
CHART_NAME=$(grep '^name:' "${CHART_FILE}" | awk '{print $2}')
|
||||||
|
UI_URL="${HELM_UI_URL}/${CHART_NAME}/${VERSION}"
|
||||||
|
if [ "${CHART_PATH}" != "." ] && [ -n "${CHART_PATH}" ]; then
|
||||||
|
bash .ci/scripts/report-status.sh success "${CHART_PATH}: Helm push ${VERSION}" "${CHART_PATH}-ci-helm-build-push" "" "$UI_URL"
|
||||||
|
else
|
||||||
|
bash .ci/scripts/report-status.sh success "Helm push ${VERSION}" ci-helm-build-push "" "$UI_URL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
tag-commit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build-push]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Create git tag
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
SERVER_URL: ${{ gitea.server_url }}
|
||||||
|
RUN_NUMBER: ${{ github.run_number }}
|
||||||
|
SHA: ${{ github.sha }}
|
||||||
|
run: |
|
||||||
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||||||
|
"$SERVER_URL/api/v1/repos/${{ github.repository }}/tags" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\": \"${GIT_TAG_PREFIX}${VERSION}\", \"message\": \"Build #$RUN_NUMBER\", \"target\": \"$SHA\"}")
|
||||||
|
|
||||||
|
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "409" ]; then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -9,6 +9,10 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
description: Space-separated suite names published to git-pages
|
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:
|
env:
|
||||||
GIT_PAGES_URL: ${{ fromJson(inputs.env_json).GIT_PAGES_URL }}
|
GIT_PAGES_URL: ${{ fromJson(inputs.env_json).GIT_PAGES_URL }}
|
||||||
@@ -32,3 +36,22 @@ jobs:
|
|||||||
echo "| ${suite} | [View report](${BASE}/${suite}/) |"
|
echo "| ${suite} | [View report](${BASE}/${suite}/) |"
|
||||||
done
|
done
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
} >> "${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
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
FROM bats/bats:1.11.0
|
FROM bats/bats:1.11.0
|
||||||
RUN apk add --no-cache lsof python3 jq curl ruby && \
|
RUN apk add --no-cache lsof python3 jq curl ruby nodejs git && \
|
||||||
gem install bashcov -v 3.3.0
|
gem install bashcov -v 3.3.0
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
FROM node:22
|
FROM node:22
|
||||||
RUN apt-get update -qq && \
|
RUN apt-get update -qq && \
|
||||||
apt-get install -y -qq --no-install-recommends lsof jq && \
|
apt-get install -y -qq --no-install-recommends lsof jq python3 && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
npm install -g @cucumber/cucumber
|
npm install -g @cucumber/cucumber
|
||||||
|
|||||||
@@ -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
|
**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
|
**Single repo & monorepo:** Kirjasto toimii molemmissa. Monorepo-tuki
|
||||||
polkusuodatuksella, komponenttikohtaisilla versioilla ja git-tägien
|
polkusuodatuksella, komponenttikohtaisilla versioilla ja git-tägien
|
||||||
etuliitteillä — jokainen komponentti julkaistaan itsenäisesti omassa
|
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. |
|
| `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`)
|
### 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.
|
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
|
### Muuta
|
||||||
|
|
||||||
| Muuttuja | Kuvaus |
|
| Muuttuja | Kuvaus |
|
||||||
|
|||||||
+27
-7
@@ -1,6 +1,6 @@
|
|||||||
# AI Context: Gitea Actions CI -kirjasto
|
# AI Context: Gitea Actions CI -kirjasto
|
||||||
|
|
||||||
**Updated**: 2026-06-15 (siivottu, provider/consumer-erottelu valmis)
|
**Updated**: 2026-06-19 (provider/consumer dual role, Project Skills -aktivointi)
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
Gitea Actions reusable workflow -kirjasto mikropalveluiden build-, testaus-,
|
Gitea Actions reusable workflow -kirjasto mikropalveluiden build-, testaus-,
|
||||||
@@ -11,8 +11,8 @@ käyttävät kirjastoa `uses:`-direktiivillä.
|
|||||||
## Monorepo: kaksi erillistä kokonaisuutta
|
## Monorepo: kaksi erillistä kokonaisuutta
|
||||||
|
|
||||||
### 1. Juuri (`gitea-ci-library`)
|
### 1. Juuri (`gitea-ci-library`)
|
||||||
Provider-kirjasto: reusable workflowt, scriptit, ADRt, dokumentaatio.
|
Provider- ja consumer-kirjasto: reusable workflowt, scriptit, ADRt, dokumentaatio,
|
||||||
Consumer kutsuu provider-workflowta `uses:`-direktiivillä.
|
ja consumer-esimerkit (dogfood). Consumer kutsuu provider-workflowta `uses:`-direktiivillä.
|
||||||
|
|
||||||
### 2. `git-pages/` — oma kokonaisuus
|
### 2. `git-pages/` — oma kokonaisuus
|
||||||
Helm-chartti Codeberg git-pagesille. Täysin itsenäinen — oma dokumentaatio,
|
Helm-chartti Codeberg git-pagesille. Täysin itsenäinen — oma dokumentaatio,
|
||||||
@@ -31,13 +31,13 @@ kuuluu `git-pages/docs/`-alle, ei juuren `docs/`-kansioon.
|
|||||||
| `scripts/` | Provider-skriptit: `report-status.sh`, `publish-git-pages.sh`, `ci-validate.sh` |
|
| `scripts/` | Provider-skriptit: `report-status.sh`, `publish-git-pages.sh`, `ci-validate.sh` |
|
||||||
| `.gitea/scripts/` | **Consumer-skriptit**: `bats-coverage.sh`, `bats-report.sh` |
|
| `.gitea/scripts/` | **Consumer-skriptit**: `bats-coverage.sh`, `bats-report.sh` |
|
||||||
| `docs/` | Arkkitehtuuri, ADRt (0004–0008) |
|
| `docs/` | Arkkitehtuuri, ADRt (0004–0008) |
|
||||||
| `skills/consumer-pipelines/` | Consumer-pipeline-standardit — AI:n pakottavat säännöt consumer-CI:lle |
|
| `skills/consumer-pipelines/` | Consumer-pipeline-standardit (ks. Project Skills). Koskee vain consumer-puolta |
|
||||||
| `skills/ci-container-build/` | CI-kontin build-workflow'n template — `ci-container-build-push.yml` |
|
| `skills/ci-container-build/` | CI-kontin build-workflow'n template (ks. Project Skills) |
|
||||||
| `docs/adr/` | Architecture Decision Records |
|
| `docs/adr/` | Architecture Decision Records |
|
||||||
| `git-pages/` | Raporttien hostaus (Helm-chartti) |
|
| `git-pages/` | Raporttien hostaus (Helm-chartti) |
|
||||||
| `tests/` | Bats-testit skripteille |
|
| `tests/` | Bats-testit skripteille |
|
||||||
|
|
||||||
### Provider workflowt (5 kpl)
|
### Provider workflowt (6 kpl)
|
||||||
|
|
||||||
| Workflow | Input | Output | Kuvaus |
|
| Workflow | Input | Output | Kuvaus |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
@@ -46,6 +46,7 @@ kuuluu `git-pages/docs/`-alle, ei juuren `docs/`-kansioon.
|
|||||||
| `docker-build-push.yml` | `env_json`, `version` | — | Buildaa Docker-imagen, puskea rekisteriin, tagittaa commitin. |
|
| `docker-build-push.yml` | `env_json`, `version` | — | Buildaa Docker-imagen, puskea rekisteriin, tagittaa commitin. |
|
||||||
| `ci-container-build-push.yml` | `env_json`, `dockerfile_path`, `image_name`, `tag` | — | Buildaa CI-työkalukontin, puskea rekisteriin. Ei versiointia eikä git-tägäystä. |
|
| `ci-container-build-push.yml` | `env_json`, `dockerfile_path`, `image_name`, `tag` | — | Buildaa CI-työkalukontin, puskea rekisteriin. Ei versiointia eikä git-tägäystä. |
|
||||||
| `report-summary.yml` | `env_json`, `suites` | — | Generoi `GITHUB_STEP_SUMMARY`-taulukon raporttilinkeillä (Gitea 1.27+) |
|
| `report-summary.yml` | `env_json`, `suites` | — | Generoi `GITHUB_STEP_SUMMARY`-taulukon raporttilinkeillä (Gitea 1.27+) |
|
||||||
|
| `helm-build-push.yml` | `env_json`, `version` | — | Pakkaa + puskea Helm chartin OCI-registryyn, tagittaa commitin. **Tekninen velka:** asentaa node.js:n runtime-vaiheessa (`apk add --no-cache nodejs` ennen checkouttia) koska `alpine/helm`-kontissa ei ole nodea. Rikkoo Offline Container -periaatetta. Ratkaistaan myöhemmin: proper multi-tool CI-kontti (helm + nodejs + git) docker hubiin. Ei consumerin ongelma. |
|
||||||
|
|
||||||
### Example-tiedostot (consumer-referenssi)
|
### Example-tiedostot (consumer-referenssi)
|
||||||
|
|
||||||
@@ -57,9 +58,28 @@ kuuluu `git-pages/docs/`-alle, ei juuren `docs/`-kansioon.
|
|||||||
| `example-cucumber-tests.yml` | workflow_call | Hyväksymätestit Cucumberilla, raportit git-pagesiin, status linkillä |
|
| `example-cucumber-tests.yml` | workflow_call | Hyväksymätestit Cucumberilla, raportit git-pagesiin, status linkillä |
|
||||||
| `example-gitea-env.conf` | — | KEY=VALUE config tälle repolle |
|
| `example-gitea-env.conf` | — | KEY=VALUE config tälle repolle |
|
||||||
|
|
||||||
|
## Provider & Consumer Dual Role
|
||||||
|
|
||||||
|
Tämä repo on **yhtä aikaa sekä provider että consumer**. Eri puolilla on eri säännöt:
|
||||||
|
|
||||||
|
- **Provider-puoli**: `.gitea/workflows/*.yml` (pl. `example-*`), `scripts/` — reusable workflowt joita muut projektit kutsuvat. Saa käyttää `docker run` -komentoja (esim. `docker-build-push.yml`). Consumer-pipeline-standardit (`skills/consumer-pipelines/`) eivät koske provideria.
|
||||||
|
- **Consumer-puoli**: `.gitea/workflows/example-*`, `.gitea/scripts/` — tämän repon oma CI (dogfood), toimii consumer-esimerkkinä. Käyttää `@main`-refiä provider-viittauksissa (sama repo). Noudattaa `skills/consumer-pipelines/`-sääntöjä.
|
||||||
|
- **Ulkoiset consumerit** käyttävät `@v1`-tagia provider-viittauksissa.
|
||||||
|
|
||||||
|
## Project Skills (skills/)
|
||||||
|
|
||||||
|
Tämä projekti sisältää omia `.ai/skills/`-järjestelmästä riippumattomia skillejä `skills/`-kansiossa. Jokainen alihakemisto sisältää `SKILL.md`:n jossa on `activation-gate`-kenttä.
|
||||||
|
|
||||||
|
**Sääntö:** Uuden tehtävän alussa skannaa `skills/*/SKILL.md` ja arvioi jokaisen `activation-gate` annettua tehtävää vasten. Jos gate matchaa, lataa skill aktiiviseksi ohjeeksi ennen toimenpiteitä.
|
||||||
|
|
||||||
|
| Skill | Gate | Kuvaus |
|
||||||
|
|---|---|---|
|
||||||
|
| `skills/consumer-pipelines/` | Consumer-pipeline-muutokset | Consumer-pipeline-standardit: reitittimen puhtaus, exit-koodi, konttipolitiikka, raportointi, nimeäminen. Koskee vain consumer-puolta. |
|
||||||
|
| `skills/ci-container-build/` | CI-kontin build | CI-kontin build-workflown template ja Dockerfile-ohjeet |
|
||||||
|
|
||||||
## Key Technical Decisions
|
## Key Technical Decisions
|
||||||
|
|
||||||
- **Provider & Consumer -malli**: `example-*`-tiedostot ovat consumer-esimerkkejä, provider-workflowt reusableja. ADR 0005.
|
- **Provider & Consumer -malli**: Tämä repo on sekä provider että consumer. Provider-workflowt reusableja muille, `example-*`-tiedostot tämän repon oma consumer-CI (dogfood). ADR 0005.
|
||||||
- **Vain Gitea, vain reusable workflowt**: ei custom actioneita, ei multi-platform
|
- **Vain Gitea, vain reusable workflowt**: ei custom actioneita, ei multi-platform
|
||||||
- **Commit-status API vain raporttilinkeille**: Tool-jobit luottavat natiiviin. Test-jobit käyttävät API:a koska se on ainoa tapa upottaa raporttilinkki. ADR 0004, 0007.
|
- **Commit-status API vain raporttilinkeille**: Tool-jobit luottavat natiiviin. Test-jobit käyttävät API:a koska se on ainoa tapa upottaa raporttilinkki. ADR 0004, 0007.
|
||||||
- **Exit-koodi on ainoa onnistumisen mittari**: Ei pipeä, ei tiedostoheuristiikkaa. ADR 0008.
|
- **Exit-koodi on ainoa onnistumisen mittari**: Ei pipeä, ei tiedostoheuristiikkaa. ADR 0008.
|
||||||
|
|||||||
@@ -60,9 +60,15 @@ Salaisuudet eivät ole `.conf`-tiedostossa. Ne määritellään Gitean
|
|||||||
organization/repository secrets -mekanismissa ja välitetään workflowlle
|
organization/repository secrets -mekanismissa ja välitetään workflowlle
|
||||||
`secrets: inherit` -direktiivillä.
|
`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ä |
|
| 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) |
|
| `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_USERNAME` | Ei | `docker-build-push.yml` (oletus: `github.actor`, ei pakollinen kaikissa registryissä) |
|
||||||
| `DOCKER_PASSWORD` | Kyllä | `docker-build-push.yml` |
|
| `DOCKER_PASSWORD` | Kyllä | `docker-build-push.yml` |
|
||||||
|
|||||||
+25
-4
@@ -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.
|
Dispatchaa workflow'n toisessa repossa ja pollaa sen valmistumista synkronisesti.
|
||||||
Käytetään GitOps-deploymentissa ja klusteritestien ketjutuksessa (tuleva).
|
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
|
### Rajapinta
|
||||||
|
|
||||||
```bash
|
```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
|
### Toiminta
|
||||||
|
|
||||||
1. **Dispatch:** `POST /api/v1/repos/{target_repo}/actions/workflows/{workflow_file}/dispatches`
|
1. **Generoi `dispatch_id`** — 8-hex uniikki tunniste
|
||||||
2. **Poll:** `GET /api/v1/repos/{target_repo}/actions/runs` → odota valmistumista
|
2. **Injektoi** `dispatch_id` inputteihin
|
||||||
3. **Palauta:** `conclusion` (`success`/`failure`/`timeout`)
|
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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -38,4 +38,4 @@ suoraan Gitea UI:ssa.
|
|||||||
| **Multi-Git-platform** | Vain Gitea — yksi alusta kunnolla (periaate 10) |
|
| **Multi-Git-platform** | Vain Gitea — yksi alusta kunnolla (periaate 10) |
|
||||||
| **Custom actionit** | Reusable workflow on kevyempi ja natiivimpi (periaate 2) |
|
| **Custom actionit** | Reusable workflow on kevyempi ja natiivimpi (periaate 2) |
|
||||||
| **Ulkoinen orkestraattori** | Gitean `needs` + `if` hoitaa ohjauksen |
|
| **Ulkoinen orkestraattori** | Gitean `needs` + `if` hoitaa ohjauksen |
|
||||||
| **Artifactory/Nexus** | Gitea Packages riittää MVP:ssä |
|
| **Artifactory/Nexus** | Build & push toimii Docker-standardilla. UI-tason linkitys (`report-summary`) vaatii Nexus/Artifactory-spesifin URL-rakenteen — ei vielä toteutettu, toteutetaan tarvittaessa |
|
||||||
|
|||||||
+181
-9
@@ -63,13 +63,98 @@ checkout → laske versio package.json + git-tageista → output
|
|||||||
|
|
||||||
**Trigger:** `workflow_call`
|
**Trigger:** `workflow_call`
|
||||||
|
|
||||||
**Inputs:** `env_json`, `version`
|
**Inputs:**
|
||||||
|
|
||||||
|
| Parametri | Pakollinen | Kuvaus |
|
||||||
|
|-----------|------------|--------|
|
||||||
|
| `env_json` | Kyllä | Konffi `gitea-env.conf`:stä |
|
||||||
|
| `version` | Kyllä | Version string (check-version output) |
|
||||||
|
|
||||||
|
**`env_json`-avaimet:**
|
||||||
|
|
||||||
|
| Avain | Pakollinen | Kuvaus |
|
||||||
|
|-------|------------|--------|
|
||||||
|
| `DOCKER_REGISTRY` | Kyllä | Registry (esim. `gitea.app.keskikuja.site/niko`) |
|
||||||
|
| `DOCKER_IMAGE_NAME` | Kyllä | Kuvan nimi ilman registry-polkua |
|
||||||
|
| `DOCKER_UI_URL` | Ei | Registry UI -linkki raportointia varten |
|
||||||
|
| `DOCKERFILE` | Ei | Dockerfile-polku, oletus `Dockerfile` |
|
||||||
|
| `GITEA_API_URL` | Kyllä | Gitean API-URL |
|
||||||
|
| `GIT_TAG_PREFIX` | Ei | Tag-prefix (esim. `docker/`) |
|
||||||
|
|
||||||
**Secrets:** `GITEA_TOKEN`, `DOCKER_USERNAME`, `DOCKER_PASSWORD`
|
**Secrets:** `GITEA_TOKEN`, `DOCKER_USERNAME`, `DOCKER_PASSWORD`
|
||||||
|
|
||||||
**Steppi-kaavio:**
|
**Steppi-kaavio:**
|
||||||
```
|
```
|
||||||
build-push (build + push samassa jobissa, ei levyn kautta) → tag-commit
|
build-push (build + push, labelit: commit+date) → tag-commit (git-tagin luonti)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Huomio:** Ei käytä `container:`-direktiiviä — ajaa suoraan runnerilla,
|
||||||
|
joten `actions/checkout` toimii ilman node-asennuksia.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `helm-build-push.yml` — Helm chart build & push
|
||||||
|
|
||||||
|
**Trigger:** `workflow_call`
|
||||||
|
|
||||||
|
**Inputs:**
|
||||||
|
|
||||||
|
| Parametri | Pakollinen | Kuvaus |
|
||||||
|
|-----------|------------|--------|
|
||||||
|
| `env_json` | Kyllä | Konffi `gitea-env.conf`:stä |
|
||||||
|
| `version` | Kyllä | Version string (check-version output) |
|
||||||
|
| `chart_path` | Ei | Polku Chart.yaml-hakemistoon, oletus `.` |
|
||||||
|
|
||||||
|
**`env_json`-avaimet:**
|
||||||
|
|
||||||
|
| Avain | Pakollinen | Kuvaus |
|
||||||
|
|-------|------------|--------|
|
||||||
|
| `HELM_REGISTRY` | Kyllä | OCI-registry (esim. `gitea.app.keskikuja.site/niko`) |
|
||||||
|
| `HELM_UI_URL` | Ei | Registry UI -linkki raportointia varten |
|
||||||
|
| `GITEA_API_URL` | Kyllä | Gitean API-URL |
|
||||||
|
| `GIT_TAG_PREFIX` | Ei | Tag-prefix (esim. `helm/`) |
|
||||||
|
|
||||||
|
**Secrets:** `GITEA_TOKEN`, `HELM_USER`, `HELM_PASSWORD`
|
||||||
|
|
||||||
|
**Steppi-kaavio:**
|
||||||
|
```
|
||||||
|
build-push (helm package → helm push OCI) → tag-commit (git-tagin luonti)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Steppien kuvaus `build-push`-jobissa:**
|
||||||
|
1. **Node.js-asennus** — `apk add --no-cache nodejs` (vaaditaan `actions/checkout`-actionia varten)
|
||||||
|
2. **Checkout** — sovellusrepo ja gitea-ci-library `.ci/`-polkuun
|
||||||
|
3. **Package** — `helm package` versiolla `$VERSION`
|
||||||
|
4. **Push OCI** — `helm push` registryyn autentikoinnilla
|
||||||
|
5. **Report status** — commit-status + UI-linkki
|
||||||
|
|
||||||
|
**Kompromissi:** Kontti `alpine/helm` ei sisällä node.js:ää, mutta
|
||||||
|
`actions/checkout@v4` on JavaScript-action ja vaatii sen. Siksi nodejs
|
||||||
|
asennetaan lennossa ennen checkouttia. Tämä vaatii internet-yhteyden
|
||||||
|
eikä toimi air gap -ympäristössä. Korvaa tarvittaessa custom-kontilla
|
||||||
|
(jossa helm + nodejs, ks. `skills/ci-container-build/SKILL.md`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `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
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -89,11 +174,23 @@ load-config → bats + cucumber → report-summary (always)
|
|||||||
**Trigger:** `push` [branches: main]
|
**Trigger:** `push` [branches: main]
|
||||||
|
|
||||||
```
|
```
|
||||||
load-config → check-version →
|
load-config ───────────────────────────────────────────────────────┐
|
||||||
[artifact exists] → done
|
load-config-helm ───────────────────────────────────────────┐ │
|
||||||
[no artifact] → bats + cucumber → report-summary (always) → docker-build-push
|
│ │
|
||||||
|
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
|
### `example-bats-tests.yml` — Bats unit-testit
|
||||||
|
|
||||||
**Trigger:** `workflow_call`
|
**Trigger:** `workflow_call`
|
||||||
@@ -112,7 +209,12 @@ commit-statuksen linkillä raporttiin.
|
|||||||
|
|
||||||
**Trigger:** `workflow_call` — ajetaan `if: always()` testien jälkeen
|
**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
|
Generoi Markdown-taulukon `GITHUB_STEP_SUMMARY`:yn kaikista julkaistuista
|
||||||
raporteista. Renderöityy HTML:ksi Gitea 1.27+ Summary-välilehdellä.
|
raporteista. Renderöityy HTML:ksi Gitea 1.27+ Summary-välilehdellä.
|
||||||
@@ -120,7 +222,77 @@ Forward-compatibeli — ei haittaa vanhemmilla Gitea-versioilla.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Suunnitteilla
|
## Provider-skriptit
|
||||||
|
|
||||||
- `deploy.yml` — GitOps-deployment (dispatch-workflow.sh-pohjainen)
|
### `gitops-update.sh` — GitOps-version päivitys
|
||||||
- `test.yml` — Klusteritason test flow
|
|
||||||
|
**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).
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -176,46 +176,90 @@ if [ "${#TO_DELETE[@]}" -eq 0 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Phase 4: whiteout deletion ==="
|
echo "=== Phase 4: full site rebuild ==="
|
||||||
echo "Creating whiteout tar for ${#TO_DELETE[@]} report(s)..."
|
echo "Rebuilding site (${#TO_DELETE[@]} report(s) to delete)..."
|
||||||
|
|
||||||
WHITEOUT_TAR=$(mktemp)
|
ARCHIVE_FILE=$(mktemp)
|
||||||
trap 'rm -f "$WHITEOUT_TAR"' EXIT
|
SITE_DIR=$(mktemp -d)
|
||||||
|
NEW_TAR=$(mktemp)
|
||||||
|
cleanup_phase4() {
|
||||||
|
rm -f "$ARCHIVE_FILE" "$NEW_TAR"
|
||||||
|
rm -rf "$SITE_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup_phase4 EXIT
|
||||||
|
|
||||||
python3 -c "
|
# Try archive.tar first
|
||||||
import tarfile, sys
|
echo "Downloading archive.tar..."
|
||||||
|
HTTP_CODE=$(curl_with_host -o "$ARCHIVE_FILE" -w "%{http_code}" -sS "${PAGES_URL}/.git-pages/archive.tar")
|
||||||
|
|
||||||
tar = tarfile.open(name='${WHITEOUT_TAR}', mode='w')
|
if [ "$HTTP_CODE" = "200" ] && tar -tf "$ARCHIVE_FILE" >/dev/null 2>&1; then
|
||||||
|
echo "Extracting archive..."
|
||||||
|
tar -xf "$ARCHIVE_FILE" -C "$SITE_DIR"
|
||||||
|
|
||||||
dirs = set()
|
for dir in "${TO_DELETE[@]}"; do
|
||||||
for d in sys.argv[1:]:
|
if [ -d "$SITE_DIR/$dir" ]; then
|
||||||
dirs.add(d.strip())
|
echo " Removing: $dir"
|
||||||
|
rm -rf "$SITE_DIR/$dir"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "archive.tar failed (HTTP ${HTTP_CODE}) - falling back to manifest-based rebuild"
|
||||||
|
|
||||||
tarinfo = tarfile.TarInfo()
|
ALL_PATHS=$(echo "$MANIFEST" | jq -r '.contents | keys[]' 2>/dev/null || true)
|
||||||
tarinfo.type = tarfile.CHRTYPE
|
|
||||||
tarinfo.devmajor = 0
|
|
||||||
tarinfo.devminor = 0
|
|
||||||
|
|
||||||
for d in sorted(dirs, key=len, reverse=True):
|
if [ -z "$ALL_PATHS" ]; then
|
||||||
info = tarinfo
|
echo "ERROR: no files in manifest - cannot rebuild" >&2
|
||||||
info.name = d
|
exit 1
|
||||||
tar.addfile(info)
|
fi
|
||||||
|
|
||||||
tar.close()
|
EXCLUDE_GREP=""
|
||||||
" "${TO_DELETE[@]}"
|
for dir in "${TO_DELETE[@]}"; do
|
||||||
|
EXCLUDE_GREP="${EXCLUDE_GREP}${EXCLUDE_GREP:+|}^${dir}/"
|
||||||
|
done
|
||||||
|
|
||||||
echo "Patching ${PAGES_URL}/ with whiteout tar..."
|
if [ -n "$EXCLUDE_GREP" ]; then
|
||||||
HTTP_CODE=$(curl_with_host -X PATCH "${PAGES_URL}/" \
|
KEEP_PATHS=$(echo "$ALL_PATHS" | grep -v -E "$EXCLUDE_GREP" || true)
|
||||||
|
else
|
||||||
|
KEEP_PATHS="$ALL_PATHS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$KEEP_PATHS" ]; then
|
||||||
|
echo "No files to keep - site will be empty"
|
||||||
|
mkdir -p "$SITE_DIR/__placeholder__"
|
||||||
|
echo "placeholder" > "$SITE_DIR/__placeholder__/index.html"
|
||||||
|
else
|
||||||
|
FILE_COUNT=$(echo "$KEEP_PATHS" | wc -l | tr -d ' ')
|
||||||
|
echo "Downloading ${FILE_COUNT} file(s)..."
|
||||||
|
while IFS= read -r path; do
|
||||||
|
[ -z "$path" ] && continue
|
||||||
|
dir=$(dirname "$SITE_DIR/$path")
|
||||||
|
mkdir -p "$dir"
|
||||||
|
curl_with_host -o "$SITE_DIR/$path" -sS "${PAGES_URL}/${path}" || {
|
||||||
|
echo " WARN: failed to download ${path}"
|
||||||
|
}
|
||||||
|
done <<< "$KEEP_PATHS"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$(ls -A "$SITE_DIR" 2>/dev/null)" ]; then
|
||||||
|
echo "Site is empty - creating placeholder"
|
||||||
|
mkdir -p "$SITE_DIR/__placeholder__"
|
||||||
|
echo "placeholder" > "$SITE_DIR/__placeholder__/index.html"
|
||||||
|
fi
|
||||||
|
|
||||||
|
tar -cf "$NEW_TAR" -C "$SITE_DIR" .
|
||||||
|
|
||||||
|
echo "PUT: replacing site contents..."
|
||||||
|
HTTP_CODE=$(curl_with_host -X PUT "${PAGES_URL}/" \
|
||||||
-H "Content-Type: application/x-tar" \
|
-H "Content-Type: application/x-tar" \
|
||||||
-H "Atomic: no" \
|
--data-binary @"${NEW_TAR}" \
|
||||||
--data-binary @"${WHITEOUT_TAR}" \
|
|
||||||
-w "%{http_code}" \
|
-w "%{http_code}" \
|
||||||
-o /dev/null)
|
-o /dev/null)
|
||||||
|
|
||||||
echo "HTTP $HTTP_CODE"
|
echo "HTTP ${HTTP_CODE}"
|
||||||
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "204" ]; then
|
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "204" ]; then
|
||||||
echo "Retention cleanup finished."
|
echo "Site rebuild completed."
|
||||||
else
|
else
|
||||||
echo "ERROR: retention HTTP ${HTTP_CODE}" >&2
|
echo "ERROR: PUT HTTP ${HTTP_CODE}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ metadata:
|
|||||||
labels:
|
labels:
|
||||||
{{- include "git-pages.componentLabels" . | nindent 4 }}
|
{{- include "git-pages.componentLabels" . | nindent 4 }}
|
||||||
annotations:
|
annotations:
|
||||||
"helm.sh/hook": post-install, post-upgrade
|
"helm.sh/hook": post-install
|
||||||
"helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation
|
"helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation
|
||||||
spec:
|
spec:
|
||||||
backoffLimit: 5
|
backoffLimit: 5
|
||||||
@@ -32,6 +32,16 @@ spec:
|
|||||||
-H "Host: {{ .Values.ingress.host }}" \
|
-H "Host: {{ .Values.ingress.host }}" \
|
||||||
-o /dev/null "http://git-pages:3000/.git-pages/health"
|
-o /dev/null "http://git-pages:3000/.git-pages/health"
|
||||||
do sleep 2; done
|
do sleep 2; done
|
||||||
|
echo "Init: checking if site already exists..."
|
||||||
|
MANIFEST=$(curl -sf \
|
||||||
|
-H "Host: {{ .Values.ingress.host }}" \
|
||||||
|
"http://git-pages:3000/.git-pages/manifest.json" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if echo "$MANIFEST" | grep -q '"contents"'; then
|
||||||
|
echo "Init: site already initialized, skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Init: creating placeholder site..."
|
echo "Init: creating placeholder site..."
|
||||||
WORK=$(mktemp -d)
|
WORK=$(mktemp -d)
|
||||||
mkdir -p "$WORK/__init__"
|
mkdir -p "$WORK/__init__"
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Helm Registry Setup (OCI)
|
||||||
|
|
||||||
|
Pipeline paketoi Helm chartin OCI-artefaktiksi ja pushee sen OCI-rekisteriin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Konfiguroi `gitea-env.conf`
|
||||||
|
|
||||||
|
```
|
||||||
|
# HELM_REGISTRY on muotoa: registry.example.com/org
|
||||||
|
#
|
||||||
|
# host+org: registry.example.com/org
|
||||||
|
#
|
||||||
|
# Pipeline rakentaa OCI-refin: oci://${HELM_REGISTRY}/<chart-name>:${VERSION}
|
||||||
|
# (chart-name tulee Chart.yaml:n name-kentästä)
|
||||||
|
|
||||||
|
HELM_REGISTRY=gitea.app.keskikuja.site/niko # PAKOLLINEN — tyhjä ei käy
|
||||||
|
HELM_UI_URL= # valinnainen — tarkista Giteasta kontin oma UI-osoite, workflow liittää perään /chart-name/VERSION
|
||||||
|
GIT_TAG_PREFIX=git-pages/ # valinnainen — monorepo-tägäys
|
||||||
|
VERSION_FILE=git-pages/Chart.yaml # valinnainen — jos Chart.yaml ei rootissa
|
||||||
|
```
|
||||||
|
|
||||||
|
| Kenttä | Pakollinen | Kuvaus |
|
||||||
|
|---|---|---|
|
||||||
|
| `HELM_REGISTRY` | **kyllä** | Registry host + owner (esim. `gitea.app.site/niko`). **Tyhjä pysäyttää workflow'n.** |
|
||||||
|
| `HELM_UI_URL` | ei | Base-URL OCI-paketin UI-sivulle (ilman chart-nimeä ja versiota). Osoite riippuu onko paketti linkitetty repoon vai ei — tarkista Giteasta. Workflow liittää perään `/chart-name/VERSION`. Jos tyhjä, commit-statusia ei erikseen aseteta. |
|
||||||
|
| `GIT_TAG_PREFIX` | ei | Etuliite git-tägille. Pakollinen monorepossa, jotta tagit eivät sekoitu muihin komponentteihin. |
|
||||||
|
| `VERSION_FILE` | ei | Polku version lähteeseen (Chart.yaml, package.json, VERSION). Oletus: juuren `Chart.yaml`. |
|
||||||
|
|
||||||
|
**OCI-ref = `oci://${HELM_REGISTRY}/<chart-name>:${VERSION}`**
|
||||||
|
Esim. `oci://gitea.app.keskikuja.site/niko/git-pages:1.2.3`
|
||||||
|
|
||||||
|
Chartin nimi (`<chart-name>`) määräytyy `Chart.yaml`-tiedoston `name`-kentästä.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Luo PAT (Personal Access Token) Giteassa
|
||||||
|
|
||||||
|
**Gitea → oma profiili (oikea yläkulma) → Settings → Applications → Manage Access Tokens → Generate New Token**
|
||||||
|
|
||||||
|
Valitse scope:
|
||||||
|
|
||||||
|
| Scope | Pääsy |
|
||||||
|
|---|---|
|
||||||
|
| `package` | **Read and Write** |
|
||||||
|
|
||||||
|
> Tämä token toimii salasanana `helm registry login` -komennossa. Muut scopet (kuten `repository`) eivät riitä — konttirekisteri vaatii nimenomaan `package`-scopen.
|
||||||
|
|
||||||
|
Tokenin arvo näytetään **vain kerran** luomisen yhteydessä. Kopioi se talteen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Tallenna PAT repositoryn Secretsiin
|
||||||
|
|
||||||
|
Nämä ovat kaksi eri paikkaa:
|
||||||
|
- **Access Tokenit** (User Settings) = missä luot tokenin
|
||||||
|
- **Repository Secrets** (Repository Settings) = minne talletat sen workflow'n käyttöön
|
||||||
|
|
||||||
|
**Repository → Settings → Actions → Secrets → Add new secret**
|
||||||
|
|
||||||
|
| Secret | Arvo |
|
||||||
|
|---|---|
|
||||||
|
| `HELM_PASSWORD` | Edellisessä vaiheessa luotu PAT |
|
||||||
|
|
||||||
|
`HELM_USER`-secretiä **ei tarvita**. Workflow käyttää automaattisesti `${{ github.actor }}` (workflowin käynnistäjä).
|
||||||
|
|
||||||
|
Jos registry vaatii eri käyttäjätunnuksen kuin `github.actor` (esim. Harbor, Artifactory), lisää myös:
|
||||||
|
|
||||||
|
| Secret | Arvo |
|
||||||
|
|---|---|
|
||||||
|
| `HELM_USER` | Registryn käyttäjätunnus |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Tarkistuslista ennen ajoa
|
||||||
|
|
||||||
|
- [ ] `HELM_REGISTRY` asetettu `gitea-env.conf`issa
|
||||||
|
- [ ] (tarvittaessa) `HELM_UI_URL` asetettu — ilman tätä commit-statusia ei erikseen aseteta
|
||||||
|
- [ ] PAT luotu Giteassa scopella `package` Read and Write
|
||||||
|
- [ ] `HELM_PASSWORD`-secret tallennettu repositoryn Secretsiin (se PAT)
|
||||||
|
- [ ] (tarvittaessa) `HELM_USER`-secret — oletus `github.actor`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Esimerkkejä eri polkurakenteista
|
||||||
|
|
||||||
|
### 5a. Hosti + org — Gitea user-taso
|
||||||
|
|
||||||
|
```
|
||||||
|
HELM_REGISTRY=gitea.app.keskikuja.site/niko
|
||||||
|
```
|
||||||
|
|
||||||
|
- OCI-ref: `oci://gitea.app.keskikuja.site/niko/git-pages:1.2.3`
|
||||||
|
- Paketti käyttäjän `niko` alla. Linkitys repoon tehdään Gitean UI:sta: paketin sivulta (Package → Settings) → linkitä repositoryyn.
|
||||||
|
- `HELM_PASSWORD` = Gitea PAT scopella `package`
|
||||||
|
|
||||||
|
### 5b. Hosti + org — Harbor
|
||||||
|
|
||||||
|
```
|
||||||
|
HELM_REGISTRY=harbor.example.com/projekti
|
||||||
|
```
|
||||||
|
|
||||||
|
- `HELM_USER` = Harbor-käyttäjä
|
||||||
|
- `HELM_PASSWORD` = Harbor-token
|
||||||
|
|
||||||
|
### 5c. Artifactory
|
||||||
|
|
||||||
|
```
|
||||||
|
HELM_REGISTRY=artifactory.example.com/helm-local
|
||||||
|
```
|
||||||
|
|
||||||
|
- `HELM_USER` = service account
|
||||||
|
- `HELM_PASSWORD` = API-token
|
||||||
Executable
+60
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
RAW_VERSION=""
|
||||||
|
|
||||||
|
if [ -n "${VERSION_FILE-}" ] && [ -f "${VERSION_FILE-}" ]; then
|
||||||
|
RAW_VERSION=$(tr -d "$(printf '\xef\xbb\xbf')" < "${VERSION_FILE}" | sed -n 's/^version:[[:space:]]*\([^[:space:]]*\).*/\1/p')
|
||||||
|
if [ -z "${RAW_VERSION}" ]; then
|
||||||
|
if echo "${VERSION_FILE}" | grep -q -E '\.json$'; then
|
||||||
|
RAW_VERSION=$(jq -r '.version' "${VERSION_FILE}")
|
||||||
|
else
|
||||||
|
RAW_VERSION=$(cat "${VERSION_FILE}" | tr -d '[:space:]')
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${RAW_VERSION}" ]; then
|
||||||
|
if [ -f VERSION ]; then
|
||||||
|
RAW_VERSION=$(cat VERSION | tr -d '[:space:]')
|
||||||
|
elif [ -f package.json ]; then
|
||||||
|
RAW_VERSION=$(jq -r '.version' package.json)
|
||||||
|
elif [ -f pom.xml ]; then
|
||||||
|
RAW_VERSION=$(grep -oP '<version>\K[^<]+' pom.xml | head -1)
|
||||||
|
elif [ -f Chart.yaml ]; then
|
||||||
|
RAW_VERSION=$(tr -d "$(printf '\xef\xbb\xbf')" < Chart.yaml | sed -n 's/^version:[[:space:]]*\([^[:space:]]*\).*/\1/p')
|
||||||
|
else
|
||||||
|
echo "ERROR: No version source found (VERSION_FILE, VERSION, package.json, pom.xml, Chart.yaml)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
BASE_VERSION=$(echo "$RAW_VERSION" | cut -d'.' -f1-2)
|
||||||
|
echo "gitea-ci-library - Tunnistettu Major.Minor versio: $BASE_VERSION"
|
||||||
|
|
||||||
|
TAGS_JSON=$(curl -s -f -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${SERVER_URL}/api/v1/repos/${REPO}/tags")
|
||||||
|
|
||||||
|
TAG=$(echo "$TAGS_JSON" | jq -r --arg prefix "${GIT_TAG_PREFIX-}" --arg sha "${SHA}" '
|
||||||
|
if type == "array" then
|
||||||
|
.[] | select(.commit.sha == $sha and (.name | startswith($prefix))) | .name
|
||||||
|
else empty end' | head -1)
|
||||||
|
|
||||||
|
mkdir -p /tmp/build-ctx
|
||||||
|
|
||||||
|
if [ -n "$TAG" ]; then
|
||||||
|
echo "ARTIFACT_EXISTS=true" > /tmp/build-ctx/build.env
|
||||||
|
echo "NEXT_VERSION=$TAG" >> /tmp/build-ctx/build.env
|
||||||
|
echo "gitea-ci-library - Artefakti löytyi jo tagilla: $TAG."
|
||||||
|
else
|
||||||
|
echo "ARTIFACT_EXISTS=false" > /tmp/build-ctx/build.env
|
||||||
|
|
||||||
|
HIGHEST_PATCH=$(echo "$TAGS_JSON" | jq -r --arg prefix "${GIT_TAG_PREFIX-}" --arg bv "${GIT_TAG_PREFIX-}${BASE_VERSION}." '
|
||||||
|
if type == "array" then .[] | .name | select(startswith($bv)) | sub($bv; "") | tonumber else empty end' | sort -rn | head -1)
|
||||||
|
|
||||||
|
if [ -z "$HIGHEST_PATCH" ]; then NEXT_PATCH=0; else NEXT_PATCH=$((HIGHEST_PATCH + 1)); fi
|
||||||
|
FULL_VERSION="${BASE_VERSION}.${NEXT_PATCH}"
|
||||||
|
|
||||||
|
echo "NEXT_VERSION=$FULL_VERSION" >> /tmp/build-ctx/build.env
|
||||||
|
echo "gitea-ci-library - Uusi vapaa versio: $FULL_VERSION"
|
||||||
|
fi
|
||||||
+64
-40
@@ -1,9 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env sh
|
||||||
set -euo pipefail
|
set -eu
|
||||||
|
|
||||||
DESCRIPTION="${1:-}"
|
DESCRIPTION="${1:-}"
|
||||||
CONTEXT="${2:-}"
|
CONTEXT="${2:-}"
|
||||||
SUITE="${3:-}"
|
SUITE="${3:-}"
|
||||||
|
STATUS="${4:-success}"
|
||||||
|
|
||||||
[ -n "$DESCRIPTION" ] || { echo "ERROR: description argument required" >&2; exit 1; }
|
[ -n "$DESCRIPTION" ] || { echo "ERROR: description argument required" >&2; exit 1; }
|
||||||
[ -n "$CONTEXT" ] || { echo "ERROR: context argument required" >&2; exit 1; }
|
[ -n "$CONTEXT" ] || { echo "ERROR: context argument required" >&2; exit 1; }
|
||||||
@@ -13,53 +14,71 @@ REPORT_DIR="reports/${SUITE}"
|
|||||||
|
|
||||||
if [ ! -d "$REPORT_DIR" ]; then
|
if [ ! -d "$REPORT_DIR" ]; then
|
||||||
echo "ERROR: $REPORT_DIR not found" >&2
|
echo "ERROR: $REPORT_DIR not found" >&2
|
||||||
bash .ci/scripts/report-status.sh failure "$DESCRIPTION" "$CONTEXT"
|
sh .ci/scripts/report-status.sh failure "$DESCRIPTION" "$CONTEXT"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FILES=()
|
FILE_COUNT=0
|
||||||
while IFS= read -r -d '' f; do
|
SUBDIR_COUNT=0
|
||||||
FILES+=("$(basename "$f")")
|
ENTRIES=""
|
||||||
done < <(find "$REPORT_DIR" -maxdepth 1 -type f ! -name index.html -print0 2>/dev/null || true)
|
|
||||||
|
|
||||||
SUBDIRS=()
|
for f in "$REPORT_DIR"/*; do
|
||||||
while IFS= read -r -d '' d; do
|
[ -f "$f" ] || continue
|
||||||
name="${d#$REPORT_DIR/}"
|
base=$(basename "$f")
|
||||||
[ -f "$d/index.html" ] && SUBDIRS+=("$name")
|
[ "$base" = "index.html" ] && continue
|
||||||
done < <(find "$REPORT_DIR" -maxdepth 1 -type d ! -name . -print0 2>/dev/null || true)
|
FILE_COUNT=$((FILE_COUNT + 1))
|
||||||
|
ENTRIES="${ENTRIES}file:${base}
|
||||||
|
"
|
||||||
|
done
|
||||||
|
|
||||||
TOTAL=$(( ${#FILES[@]} + ${#SUBDIRS[@]} ))
|
for d in "$REPORT_DIR"/*/; do
|
||||||
|
[ -d "$d" ] || continue
|
||||||
|
base=$(basename "$d")
|
||||||
|
[ -f "$d/index.html" ] || continue
|
||||||
|
SUBDIR_COUNT=$((SUBDIR_COUNT + 1))
|
||||||
|
ENTRIES="${ENTRIES}dir:${base}
|
||||||
|
"
|
||||||
|
done
|
||||||
|
|
||||||
|
TOTAL=$((FILE_COUNT + SUBDIR_COUNT))
|
||||||
|
|
||||||
if [ "$TOTAL" -eq 0 ]; then
|
if [ "$TOTAL" -eq 0 ]; then
|
||||||
echo "ERROR: no reportable items in $REPORT_DIR" >&2
|
echo "ERROR: no reportable items in $REPORT_DIR" >&2
|
||||||
bash .ci/scripts/report-status.sh failure "$DESCRIPTION" "$CONTEXT"
|
sh .ci/scripts/report-status.sh failure "$DESCRIPTION" "$CONTEXT"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SHA8="${GITHUB_SHA:0:8}"
|
SHA8=$(echo "${GITHUB_SHA:-xxxxxxxx}" | cut -c1-8)
|
||||||
|
|
||||||
humanize() {
|
humanize() {
|
||||||
local name="$1"
|
name="$1"
|
||||||
name="${name%.*}"
|
name=$(echo "$name" | sed -e 's/\.[^.]*$//' -e 's/[-_]/ /g')
|
||||||
name="${name//-/ }"
|
first=$(echo "$name" | cut -c1 | tr '[:lower:]' '[:upper:]')
|
||||||
name="${name//_/ }"
|
rest=$(echo "$name" | cut -c2-)
|
||||||
echo "${name^}"
|
echo "${first}${rest}"
|
||||||
}
|
}
|
||||||
|
|
||||||
generate_index() {
|
generate_index() {
|
||||||
local html
|
{
|
||||||
html='<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">'
|
echo '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">'
|
||||||
html+="<title>$DESCRIPTION</title>"
|
echo "<title>$DESCRIPTION</title>"
|
||||||
html+='<style>body{font-family:sans-serif;margin:2em;max-width:960px}h1{color:#1e293b}ul{list-style:none;padding:0}li{margin:.5em 0;padding:.5em;background:#f8fafc;border-radius:6px}a{color:#2563eb;text-decoration:none}a:hover{text-decoration:underline}</style>'
|
echo '<style>body{font-family:sans-serif;margin:2em;max-width:960px}h1{color:#1e293b}ul{list-style:none;padding:0}li{margin:.5em 0;padding:.5em;background:#f8fafc;border-radius:6px}a{color:#2563eb;text-decoration:none}a:hover{text-decoration:underline}</style>'
|
||||||
html+="</head><body><h1>$DESCRIPTION</h1><ul>"
|
echo "</head><body><h1>$DESCRIPTION</h1><ul>"
|
||||||
for f in "${FILES[@]}"; do
|
|
||||||
html+="<li><a href=\"$f\">$(humanize "$f")</a></li>"
|
echo "$ENTRIES" | while IFS= read -r entry; do
|
||||||
|
[ -z "$entry" ] && continue
|
||||||
|
entry_type=$(echo "$entry" | cut -d: -f1)
|
||||||
|
entry_name=$(echo "$entry" | cut -d: -f2-)
|
||||||
|
if [ "$entry_type" = "file" ]; then
|
||||||
|
echo "<li><a href=\"$entry_name\">$(humanize "$entry_name")</a></li>"
|
||||||
|
else
|
||||||
|
cap=$(echo "$entry_name" | sed 's/\(.\).*/\1/' | tr '[:lower:]' '[:upper:]')$(echo "$entry_name" | sed 's/.//')
|
||||||
|
echo "<li><a href=\"$entry_name/index.html\">${cap}</a></li>"
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
for d in "${SUBDIRS[@]}"; do
|
|
||||||
html+="<li><a href=\"$d/index.html\">${d^}</a></li>"
|
echo '</ul></body></html>'
|
||||||
done
|
} > "$REPORT_DIR/index.html"
|
||||||
html+='</ul></body></html>'
|
|
||||||
printf '%s' "$html" > "$REPORT_DIR/index.html"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
STAGED="reports/${SHA8}/${SUITE}"
|
STAGED="reports/${SHA8}/${SUITE}"
|
||||||
@@ -67,20 +86,25 @@ mkdir -p "$STAGED"
|
|||||||
|
|
||||||
if [ "$TOTAL" -eq 1 ]; then
|
if [ "$TOTAL" -eq 1 ]; then
|
||||||
cp -a "$REPORT_DIR/." "$STAGED/"
|
cp -a "$REPORT_DIR/." "$STAGED/"
|
||||||
bash .ci/scripts/publish-git-pages.sh "$SUITE"
|
sh .ci/scripts/publish-git-pages.sh "$SUITE"
|
||||||
|
|
||||||
if [ ${#FILES[@]} -eq 1 ]; then
|
first_entry=$(echo "$ENTRIES" | head -1)
|
||||||
ENTRY="${FILES[0]}"
|
first_type=$(echo "$first_entry" | cut -d: -f1)
|
||||||
|
first_name=$(echo "$first_entry" | cut -d: -f2-)
|
||||||
|
|
||||||
|
if [ "$first_type" = "file" ]; then
|
||||||
|
SINGLE_ENTRY="$first_name"
|
||||||
else
|
else
|
||||||
ENTRY="${SUBDIRS[0]}/index.html"
|
SINGLE_ENTRY="${first_name}/index.html"
|
||||||
fi
|
fi
|
||||||
URL="${GIT_PAGES_URL}/${GITHUB_REPOSITORY}/reports/${SHA8}/${SUITE}/${ENTRY}"
|
|
||||||
bash .ci/scripts/report-status.sh success "$DESCRIPTION" "$CONTEXT" "" "$URL"
|
URL="${GIT_PAGES_URL}/${GITHUB_REPOSITORY}/reports/${SHA8}/${SUITE}/${SINGLE_ENTRY}"
|
||||||
|
sh .ci/scripts/report-status.sh "$STATUS" "$DESCRIPTION" "$CONTEXT" "" "$URL"
|
||||||
else
|
else
|
||||||
generate_index
|
generate_index
|
||||||
cp -a "$REPORT_DIR/." "$STAGED/"
|
cp -a "$REPORT_DIR/." "$STAGED/"
|
||||||
bash .ci/scripts/publish-git-pages.sh "$SUITE"
|
sh .ci/scripts/publish-git-pages.sh "$SUITE"
|
||||||
bash .ci/scripts/report-status.sh success "$DESCRIPTION" "$CONTEXT" "$SUITE"
|
sh .ci/scripts/report-status.sh "$STATUS" "$DESCRIPTION" "$CONTEXT" "$SUITE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -rf "$STAGED"
|
rm -rf "$STAGED"
|
||||||
|
|||||||
@@ -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_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
|
[ -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_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}')
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
RUNS_URL="$GITEA_API_URL/api/v1/repos/$TARGET_REPO/actions/runs?status=running"
|
# Poll: find dispatched run by display_title matching
|
||||||
RUNS_RESP=$(curl -s --connect-timeout 5 --max-time 10 \
|
RUN_ID=""
|
||||||
-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
|
|
||||||
|
|
||||||
TIMEOUT_SECONDS=$(awk "BEGIN {printf \"%.3f\", $TIMEOUT_MINUTES * 60}")
|
TIMEOUT_SECONDS=$(awk "BEGIN {printf \"%.3f\", $TIMEOUT_MINUTES * 60}")
|
||||||
START_TIME=$(date +%s)
|
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
|
while true; do
|
||||||
NOW=$(date +%s)
|
NOW=$(date +%s)
|
||||||
ELAPSED=$((NOW - START_TIME))
|
ELAPSED=$((NOW - START_TIME))
|
||||||
@@ -61,6 +77,12 @@ while true; do
|
|||||||
if [ "$STATUS" = "completed" ]; then
|
if [ "$STATUS" = "completed" ]; then
|
||||||
CONCLUSION=$(echo "$RUN_RESP" | jq -r '.conclusion // "failure"')
|
CONCLUSION=$(echo "$RUN_RESP" | jq -r '.conclusion // "failure"')
|
||||||
if [ "$CONCLUSION" = "success" ]; then
|
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
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo "ERROR: Workflow completed with conclusion: $CONCLUSION" >&2
|
echo "ERROR: Workflow completed with conclusion: $CONCLUSION" >&2
|
||||||
|
|||||||
@@ -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"
|
||||||
Executable
+114
@@ -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
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env sh
|
||||||
set -euo pipefail
|
set -eu
|
||||||
|
|
||||||
SUITE_PATH="${1:-}"
|
SUITE_PATH="${1:-}"
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ SUITE_PATH="${1:-}"
|
|||||||
|
|
||||||
OWNER="${GITHUB_REPOSITORY%%/*}"
|
OWNER="${GITHUB_REPOSITORY%%/*}"
|
||||||
REPO="${GITHUB_REPOSITORY##*/}"
|
REPO="${GITHUB_REPOSITORY##*/}"
|
||||||
SHA8="${GITHUB_SHA:0:8}"
|
SHA8=$(echo "$GITHUB_SHA" | cut -c1-8)
|
||||||
PAGES_USER="${GIT_PAGES_PUBLISH_USER:-publish}"
|
PAGES_USER="${GIT_PAGES_PUBLISH_USER:-publish}"
|
||||||
REPORT_DIR="reports/${SHA8}/${SUITE_PATH%/}"
|
REPORT_DIR="reports/${SHA8}/${SUITE_PATH%/}"
|
||||||
REPORT_BASE="${GIT_PAGES_URL}/${OWNER}/${REPO}/reports/${SHA8}"
|
REPORT_BASE="${GIT_PAGES_URL}/${OWNER}/${REPO}/reports/${SHA8}"
|
||||||
@@ -33,17 +33,30 @@ else
|
|||||||
fi
|
fi
|
||||||
mkdir -p "$TARGET"
|
mkdir -p "$TARGET"
|
||||||
cp -a "$REPORT_DIR/." "$TARGET/"
|
cp -a "$REPORT_DIR/." "$TARGET/"
|
||||||
if [ ! -f "$TARGET/index.html" ]; then
|
|
||||||
items=()
|
|
||||||
while IFS= read -r -d '' f; do
|
|
||||||
items+=("$(basename "$f")")
|
|
||||||
done < <(find "$TARGET" -maxdepth 1 -type f ! -name index.html -print0 2>/dev/null || true)
|
|
||||||
while IFS= read -r -d '' d; do
|
|
||||||
name=$(basename "$d")
|
|
||||||
[ -f "$d/index.html" ] && items+=("$name")
|
|
||||||
done < <(find "$TARGET" -maxdepth 1 -type d ! -name . -print0 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ ${#items[@]} -gt 1 ]; then
|
if [ ! -f "$TARGET/index.html" ]; then
|
||||||
|
ITEM_LIST=""
|
||||||
|
ITEM_COUNT=0
|
||||||
|
|
||||||
|
for f in "$TARGET"/*; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
base=$(basename "$f")
|
||||||
|
[ "$base" = "index.html" ] && continue
|
||||||
|
ITEM_LIST="${ITEM_LIST}file:${base}
|
||||||
|
"
|
||||||
|
ITEM_COUNT=$((ITEM_COUNT + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
for d in "$TARGET"/*/; do
|
||||||
|
[ -d "$d" ] || continue
|
||||||
|
base=$(basename "$d")
|
||||||
|
[ -f "$d/index.html" ] || continue
|
||||||
|
ITEM_LIST="${ITEM_LIST}dir:${base}
|
||||||
|
"
|
||||||
|
ITEM_COUNT=$((ITEM_COUNT + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$ITEM_COUNT" -gt 1 ]; then
|
||||||
{
|
{
|
||||||
echo '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">'
|
echo '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">'
|
||||||
echo "<title>Test report ${SHA8}</title>"
|
echo "<title>Test report ${SHA8}</title>"
|
||||||
@@ -53,16 +66,21 @@ if [ ! -f "$TARGET/index.html" ]; then
|
|||||||
echo 'a{color:#2563eb;text-decoration:none}a:hover{text-decoration:underline}'
|
echo 'a{color:#2563eb;text-decoration:none}a:hover{text-decoration:underline}'
|
||||||
echo '</style></head><body>'
|
echo '</style></head><body>'
|
||||||
echo "<h1>Test report <code>${SHA8}</code></h1><ul>"
|
echo "<h1>Test report <code>${SHA8}</code></h1><ul>"
|
||||||
for item in "${items[@]}"; do
|
|
||||||
label="${item%.*}"
|
echo "$ITEM_LIST" | while IFS= read -r item; do
|
||||||
label="${label//-/ }"
|
[ -z "$item" ] && continue
|
||||||
label="${label//_/ }"
|
item_type=$(echo "$item" | cut -d: -f1)
|
||||||
if [ -f "$TARGET/$item" ]; then
|
item_name=$(echo "$item" | cut -d: -f2-)
|
||||||
echo "<li><a href=\"$item\">${label^}</a></li>"
|
label=$(echo "$item_name" | sed -e 's/\.[^.]*$//' -e 's/[-_]/ /g')
|
||||||
|
first=$(echo "$label" | cut -c1 | tr '[:lower:]' '[:upper:]')
|
||||||
|
rest=$(echo "$label" | cut -c2-)
|
||||||
|
if [ "$item_type" = "file" ]; then
|
||||||
|
echo "<li><a href=\"$item_name\">${first}${rest}</a></li>"
|
||||||
else
|
else
|
||||||
echo "<li><a href=\"$item/index.html\">${label^}</a></li>"
|
echo "<li><a href=\"$item_name/index.html\">${first}${rest}</a></li>"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
echo '</ul></body></html>'
|
echo '</ul></body></html>'
|
||||||
} > "$TARGET/index.html"
|
} > "$TARGET/index.html"
|
||||||
fi
|
fi
|
||||||
@@ -74,7 +92,7 @@ EOF
|
|||||||
find "$WORK/$OWNER" \( -type f -o -type l \) -print | sed "s|^${WORK}/||" | tar -cf "$TAR" -C "$WORK" -T -
|
find "$WORK/$OWNER" \( -type f -o -type l \) -print | sed "s|^${WORK}/||" | tar -cf "$TAR" -C "$WORK" -T -
|
||||||
|
|
||||||
publish() {
|
publish() {
|
||||||
local method="$1"
|
method="$1"
|
||||||
curl -sS -X "$method" "$PUBLISH_SITE_URL" \
|
curl -sS -X "$method" "$PUBLISH_SITE_URL" \
|
||||||
-u "${PAGES_USER}:${GIT_PAGES_PUBLISH_TOKEN}" \
|
-u "${PAGES_USER}:${GIT_PAGES_PUBLISH_TOKEN}" \
|
||||||
-H "Content-Type: application/x-tar" \
|
-H "Content-Type: application/x-tar" \
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env sh
|
||||||
set -euo pipefail
|
set -eu
|
||||||
|
|
||||||
# https://docs.gitea.com/api/next/#tag/repository/operation/repoCreateStatus
|
|
||||||
|
|
||||||
STATE="${1:-}"
|
STATE="${1:-}"
|
||||||
DESCRIPTION="${2:-}"
|
DESCRIPTION="${2:-}"
|
||||||
KEY="${3:-commit-${GITHUB_SHA:0:8}}"
|
SHA8=$(echo "${GITHUB_SHA:-}" | cut -c1-8)
|
||||||
|
KEY="${3:-commit-${SHA8}}"
|
||||||
SUITE="${4:-}"
|
SUITE="${4:-}"
|
||||||
CUSTOM_URL="${5:-}"
|
CUSTOM_URL="${5:-}"
|
||||||
|
|
||||||
@@ -18,7 +17,8 @@ if [ -n "$CUSTOM_URL" ]; then
|
|||||||
URL="$CUSTOM_URL"
|
URL="$CUSTOM_URL"
|
||||||
elif [ -n "$SUITE" ]; then
|
elif [ -n "$SUITE" ]; then
|
||||||
SUITE="${SUITE%/}/"
|
SUITE="${SUITE%/}/"
|
||||||
URL="${GIT_PAGES_URL}/${GITHUB_REPOSITORY}/reports/${GITHUB_SHA:0:8}/${SUITE}"
|
SHA8_CUT=$(echo "$GITHUB_SHA" | cut -c1-8)
|
||||||
|
URL="${GIT_PAGES_URL}/${GITHUB_REPOSITORY}/reports/${SHA8_CUT}/${SUITE}"
|
||||||
else
|
else
|
||||||
URL="${GITEA_API_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
URL="${GITEA_API_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -32,6 +32,31 @@ Kun kontti on pushattu registryyn, se on muiden pipeline-jobien käytettävissä
|
|||||||
`latest`-tägillä — rebuild = käyttöönotto. Mitään versioviittauksia ei tarvitse
|
`latest`-tägillä — rebuild = käyttöönotto. Mitään versioviittauksia ei tarvitse
|
||||||
päivittää.
|
päivittää.
|
||||||
|
|
||||||
|
## Offline-periaate (DoD)
|
||||||
|
|
||||||
|
CI-kontin **Definition of Done**:
|
||||||
|
|
||||||
|
> Kontti ei lataa mitään pipeline-vaiheessa (`workflow run` -stepit) eikä kontin
|
||||||
|
> runtime-prosessissa (`container:` / `docker run`). Kaikki riippuvuudet
|
||||||
|
> (kielikohtaiset paketit, työkalut, binäärit) on joko:
|
||||||
|
> - Pre-cachattu kontin **build-vaiheessa** Dockerfilessä, TAI
|
||||||
|
> - Kopioitu multi-stage buildilla toisesta imagesta (`COPY --from`)
|
||||||
|
>
|
||||||
|
> Ainoa sallittu lataushetki on `docker build`. Sen jälkeen kontti toimii
|
||||||
|
> ilman verkkoyhteyttä.
|
||||||
|
|
||||||
|
**Miksi:** Toistettavuus, air gap -yhteensopivuus, nopeus. Pipeline ei saa
|
||||||
|
epäonnistua sen takia että ulkoinen registry on alhaalla tai että `go mod download`
|
||||||
|
joutuu latamaan 100 modulia jokaisella testiajolla.
|
||||||
|
|
||||||
|
**Kielikohtaiset pre-cachet:** Jos kontissa ajetaan kielikohtaista testiä
|
||||||
|
(Go, Java, Node, Python, ...), kaikki kielikohtaiset riippuvuudet on
|
||||||
|
pre-cachattava Dockerfilessä build-vaiheessa:
|
||||||
|
- Go: `COPY go.mod go.sum ./` → `RUN go mod download`
|
||||||
|
- Java/Maven: `COPY pom.xml ./` → `RUN mvn dependency:go-offline`
|
||||||
|
- Node: `COPY package.json package-lock.json ./` → `RUN npm ci --omit=dev`
|
||||||
|
- Python: `COPY requirements.txt ./` → `RUN pip wheel --wheel-dir=/wheels -r requirements.txt` → `COPY --from` käyttöön
|
||||||
|
|
||||||
## Nimeäminen
|
## Nimeäminen
|
||||||
|
|
||||||
CI-kontin build-workflow noudattaa samaa nimeämiskonventiota kuin muutkin
|
CI-kontin build-workflow noudattaa samaa nimeämiskonventiota kuin muutkin
|
||||||
@@ -123,27 +148,80 @@ tag: latest
|
|||||||
|
|
||||||
### Dockerfile
|
### Dockerfile
|
||||||
|
|
||||||
Dockerfile yhdistää tarvitut työkalut yhteen konttiin. Molemmat tavat kelpaavat:
|
Dockerfile yhdistää tarvitut työkalut yhteen konttiin.
|
||||||
|
**Kaikki riippuvuudet ladataan build-vaiheessa — kontti on täysin itseriittoinen.**
|
||||||
|
|
||||||
```dockerfile
|
```dockerfile
|
||||||
# Tapa A: COPY --from toisesta imagesta
|
# Tapa A: COPY --from toisesta imagesta
|
||||||
FROM __BASE_IMAGE__:__VERSION__
|
FROM __BASE_IMAGE__:__VERSION__
|
||||||
COPY --from=__SOURCE_IMAGE__:__VERSION__ /path/to/binary /usr/local/bin/
|
COPY --from=__SOURCE_IMAGE__:__VERSION__ /path/to/binary /usr/local/bin/
|
||||||
RUN apk add --no-cache __PAKETIT__ # Ei koskaan git:iä — kloonaus kuuluu pipelinelle
|
RUN apk add --no-cache __PAKETIT__
|
||||||
|
|
||||||
# Tapa B: curl-lataus (normaali Dockerfilessa)
|
# Tapa B: Build-vaiheen curl-lataus
|
||||||
FROM __BASE_IMAGE__:__VERSION__
|
FROM __BASE_IMAGE__:__VERSION__
|
||||||
RUN apk add --no-cache curl __PAKETIT__ && \
|
RUN apk add --no-cache curl __PAKETIT__ && \
|
||||||
curl -fsSL __URL__/__BINARY__.tar.gz | tar xz -C /usr/local/bin && \
|
curl -fsSL __URL__/__BINARY__.tar.gz | tar xz -C /usr/local/bin && \
|
||||||
apk del curl
|
apk del curl
|
||||||
|
|
||||||
|
# Tapa C: Multi-stage + kielikohtainen pre-cache
|
||||||
|
FROM __BASE_IMAGE__:__VERSION__ AS deps
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
FROM deps AS build
|
||||||
|
COPY . .
|
||||||
|
RUN go test -c -o /tmp/test.bin ./...
|
||||||
|
|
||||||
|
FROM __BASE_IMAGE__:__VERSION__
|
||||||
|
COPY --from=deps /go/pkg/mod /go/pkg/mod
|
||||||
|
COPY --from=build /tmp/test.bin /usr/local/bin/test
|
||||||
```
|
```
|
||||||
|
|
||||||
`COPY --from` on kevyempi (ei curl-asennusta). `curl` on selkeämpi kun binääri
|
`COPY --from` on kevyempi (ei curl-asennusta). `curl` (Tapa B) on sallittu
|
||||||
tulee suoraan GitHub Releasesista tai vastaavasta.
|
vain build-vaiheessa — `apk del curl` poistaa työkalun ennen runtimea.
|
||||||
|
Tapa C pre-cacheaa kielikohtaiset riippuvuudet ja tuottaa täysin
|
||||||
|
offline-runtime-kontin.
|
||||||
|
|
||||||
|
## Testaus ennen julkaisua
|
||||||
|
|
||||||
|
Konttia ei saa pushata registryyn ennen kuin se on validoitu.
|
||||||
|
|
||||||
|
### 1. Aja testit kontin sisällä
|
||||||
|
|
||||||
|
Testit on ajettava **kontin sisällä**, ei suoraan lokaalilla koneella.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# OIKEIN — kontin sisällä
|
||||||
|
docker build -t ci-tyokalu:test .
|
||||||
|
docker run --rm -v "$(pwd):/repo" -w /repo ci-tyokalu:test bash -c 'bats tests/'
|
||||||
|
|
||||||
|
# VÄÄRIN — lokaalit binäärit vs kontti
|
||||||
|
bats tests/ # eri bash/työkalut kuin kontissa
|
||||||
|
bashcov -- bats tests/ # eri ruby-versio kuin kontissa
|
||||||
|
```
|
||||||
|
|
||||||
|
Lokaali ympäristö (macOS, eri kirjastoversiot) poikkeaa aina kontista.
|
||||||
|
Testi voi mennä läpi lokaalissa mutta failata CI:ssä, tai päinvastoin.
|
||||||
|
|
||||||
|
### 2. Fragile-testien seulonta (10x ajo)
|
||||||
|
|
||||||
|
Aja koko testipaketti **10 kertaa peräkkäin** kontin sisällä ennen pushausta:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for i in $(seq 1 10); do
|
||||||
|
echo "=== RUN $i ==="
|
||||||
|
docker run --rm -v "$(pwd):/repo" -w /repo ci-tyokalu:test \
|
||||||
|
bash -c 'bats tests/' || exit 1
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Jos yksikin ajo failaa, kontissa on fragile testi — korjaa ennen pushausta.
|
||||||
|
Fragile testit syövät devaukseen käytettyä aikaa turhilla uusinta-ajoilla.
|
||||||
|
|
||||||
## Mitä EI kannata tehdä
|
## Mitä EI kannata tehdä
|
||||||
|
|
||||||
- Älä lisää `workflow_call`-triggariä — CI-konttia ei koskaan buildata automaattisesti
|
- Älä lisää `workflow_call`-triggariä — CI-konttia ei koskaan buildata automaattisesti
|
||||||
- Älä poista `<komponentti>.`-prefiksiä olemassaolevista tiedostoista — ne kuuluvat monorepo-nimeämiskonventioon
|
- Älä poista `<komponentti>.`-prefiksiä olemassaolevista tiedostoista — ne kuuluvat monorepo-nimeämiskonventioon
|
||||||
- Älä sisällytä CI-konttiin mitään sovelluskoodia — vain työkalut
|
- Älä sisällytä CI-konttiin mitään sovelluskoodia — vain työkalut
|
||||||
- Älä koskaan asenna `git`:iä CI-konttiin — repon kloonaus ja checkout ovat Gitea Actionsin natiiveja operaatioita, eivät kontin vastuulla. Git paisuttaa konttia turhaan ja luo harhan että kontti hallitsee repoa
|
- Älä koskaan lataa mitään pipeline- tai runtime-vaiheessa — kaikki lataukset kuuluvat `docker build` -vaiheeseen (Offline-periaate)
|
||||||
|
- Älä jätä kielikohtaisia riippuvuuksia pre-cachaamatta — `go mod download`, `npm install`, `mvn dependency:go-offline` jne. ajetaan Dockerfilessä, ei pipelinessä
|
||||||
|
|||||||
@@ -0,0 +1,636 @@
|
|||||||
|
# Consumer Pipelines — Reference
|
||||||
|
|
||||||
|
Mallipohjat, esimerkit ja konfiguraatiot. Katso säännöt `SKILL.md`:stä.
|
||||||
|
|
||||||
|
## Pre-cache-esimerkit (Offline Container)
|
||||||
|
|
||||||
|
Alla Dockerfile-esimerkit kielikohtaisista pre-cacheista. Kaikki ajetaan
|
||||||
|
build-vaiheessa — kontti on täysin itseriittoinen eikä lataa mitään
|
||||||
|
pipeline- tai runtime-vaiheessa.
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM golang:1.24-alpine AS deps
|
||||||
|
WORKDIR /build
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
FROM deps AS test-build
|
||||||
|
COPY . .
|
||||||
|
RUN go test -c -o /tmp/test.bin ./...
|
||||||
|
|
||||||
|
FROM alpine:3.21
|
||||||
|
RUN apk add --no-cache git nodejs
|
||||||
|
COPY --from=deps /go/pkg/mod /go/pkg/mod
|
||||||
|
COPY --from=test-build /tmp/test.bin /usr/local/bin/test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node.js
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM node:22-alpine AS deps
|
||||||
|
WORKDIR /build
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
|
FROM node:22-alpine
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
COPY --from=deps /build/node_modules /app/node_modules
|
||||||
|
COPY . /app
|
||||||
|
WORKDIR /app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Java / Maven
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM maven:3.9-eclipse-temurin-21 AS deps
|
||||||
|
WORKDIR /build
|
||||||
|
COPY pom.xml ./
|
||||||
|
RUN mvn dependency:go-offline -B
|
||||||
|
|
||||||
|
FROM maven:3.9-eclipse-temurin-21 AS build
|
||||||
|
COPY --from=deps /root/.m2 /root/.m2
|
||||||
|
COPY . .
|
||||||
|
RUN mvn package -B -DskipTests
|
||||||
|
|
||||||
|
FROM eclipse-temurin:21-jre
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY --from=build /build/target/*.jar /app/app.jar
|
||||||
|
WORKDIR /app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM python:3.12-alpine AS deps
|
||||||
|
WORKDIR /build
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip wheel --wheel-dir=/wheels -r requirements.txt
|
||||||
|
|
||||||
|
FROM python:3.12-alpine
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
COPY --from=deps /build/wheels /wheels
|
||||||
|
COPY --from=deps /build/requirements.txt /
|
||||||
|
RUN pip install --no-index --find-links=/wheels -r /requirements.txt && rm -rf /wheels
|
||||||
|
COPY . /app
|
||||||
|
WORKDIR /app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Helm + Node.js (korvaa helm-build-push.yml:n runtime-apk)
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM alpine/helm:3.16.0 AS helm-bin
|
||||||
|
FROM node:22-alpine
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
COPY --from=helm-bin /usr/bin/helm /usr/local/bin/helm
|
||||||
|
```
|
||||||
|
|
||||||
|
Tämä kontti korvaa `helm-build-push.yml`:n `alpine/helm:3.19.0`-image-riippuvuuden
|
||||||
|
ja poistaa tarpeen asentaa node.js runtime-vaiheessa.
|
||||||
|
|
||||||
|
## Reititin — täydellinen esimerkki
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
jobs:
|
||||||
|
load-config:
|
||||||
|
uses: <owner>/gitea-ci-library/.gitea/workflows/config-provider.yml@v1
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
<test-1>:
|
||||||
|
needs: [load-config]
|
||||||
|
uses: ./.gitea/workflows/<component>.<test-1>.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
|
||||||
|
<test-2>:
|
||||||
|
needs: [load-config]
|
||||||
|
uses: ./.gitea/workflows/<component>.<test-2>.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
|
||||||
|
report-summary:
|
||||||
|
needs: [load-config, <test-1>, <test-2>]
|
||||||
|
if: always()
|
||||||
|
uses: <owner>/gitea-ci-library/.gitea/workflows/report-summary.yml@v1
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
suites: <suite-1> <suite-2>
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI-kontin build — parametroitu workflow
|
||||||
|
|
||||||
|
CI-kontin build on `workflow_dispatch`-triggeröity job, joka näkyy Gitea Actionsissa kuten Jenkinsin
|
||||||
|
parametroitu job — käyttäjä antaa inputit UI:sta ennen ajoa.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: CI Container Build <työkalu>
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
config_path:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
default: '.gitea/workflows/<komponentti>.gitea-env.conf'
|
||||||
|
description: 'Polku .gitea-env.conf-tiedostoon'
|
||||||
|
dockerfile_path:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
default: '<komponentti>/Dockerfile.ci-<työkalu>'
|
||||||
|
description: 'Polku Dockerfileen'
|
||||||
|
image_name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
default: 'ci-<työkalu>'
|
||||||
|
description: 'Kontin nimi ilman registry-polkua'
|
||||||
|
tag:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
default: 'latest'
|
||||||
|
description: 'Image-tägi'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
load-config:
|
||||||
|
uses: <owner>/gitea-ci-library/.gitea/workflows/config-provider.yml@v1
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
config_path: ${{ inputs.config_path }}
|
||||||
|
|
||||||
|
build-push:
|
||||||
|
needs: [load-config]
|
||||||
|
uses: <owner>/gitea-ci-library/.gitea/workflows/ci-container-build-push.yml@v1
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
dockerfile_path: ${{ inputs.dockerfile_path }}
|
||||||
|
image_name: ${{ inputs.image_name }}
|
||||||
|
tag: ${{ inputs.tag }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CI-kontin ajaminen testijobissa
|
||||||
|
|
||||||
|
**Ainoa sallittu tapa** consumer-puolella on `container:`-direktiivi. `docker run` komennolla
|
||||||
|
kontin käynnistäminen stepin sisällä on anti-pattern. `container:`-direktiivillä kaikki stepit
|
||||||
|
ajetaan samassa kontissa — tiedostot ovat suoraan filesystemillä eikä erillistä
|
||||||
|
volyyminhallintaa tarvita.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
jobs:
|
||||||
|
<työkalu>:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: ${{ inputs.<image-name> }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: <owner>/gitea-ci-library
|
||||||
|
path: .ci
|
||||||
|
|
||||||
|
- name: Run <työkalu>
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p "reports/<suite>"
|
||||||
|
<komento> > "reports/<suite>/results.txt" 2>&1
|
||||||
|
|
||||||
|
- name: Post-process reports
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
<mahdollinen_raporttien_jälkikäsittely>
|
||||||
|
|
||||||
|
- name: Report
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
bash .ci/scripts/ci-report.sh "<Test type> test report" <context> <suite> ${{ job.status }}
|
||||||
|
```
|
||||||
|
|
||||||
|
Monorepossa context ja description sisältävät komponentin nimen:
|
||||||
|
```yaml
|
||||||
|
- name: Report
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
bash .ci/scripts/ci-report.sh "<Komponentti>: <Test type> test report" <komponentti>.<context> <suite> ${{ job.status }}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usean runnerin cache-ongelma:** Jos eri kerroilla käynnistyy eri runnereita,
|
||||||
|
niillä voi olla eri versio `latest`-imagen digesteistä. Ratkaisuja:
|
||||||
|
- Rebuildaa kontti ja aja `docker pull <image>` manuaalisesti kaikilla runnereilla
|
||||||
|
- Käytä versioitua tagia (`v2`, `v3`, ...) ja päivitä workflow'n default buildauksen jälkeen
|
||||||
|
|
||||||
|
**Mallit:**
|
||||||
|
- `example-cucumber-tests.yml` — ei post-processia
|
||||||
|
- `example-bats-tests.yml` — post-process coverage + report
|
||||||
|
|
||||||
|
## Raporttitasot — tarkat YAML-mallit
|
||||||
|
|
||||||
|
### Taso 1: Ei jälkikäsittelyä
|
||||||
|
|
||||||
|
Single repo:
|
||||||
|
```yaml
|
||||||
|
- name: Run tests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p "reports/<suite>"
|
||||||
|
<testikomento>
|
||||||
|
|
||||||
|
- name: Report
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
bash .ci/scripts/ci-report.sh "<Test type> test report" <context> <suite> ${{ job.status }}
|
||||||
|
```
|
||||||
|
|
||||||
|
Monorepo:
|
||||||
|
```yaml
|
||||||
|
- name: Run tests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p "reports/<suite>"
|
||||||
|
<testikomento>
|
||||||
|
|
||||||
|
- name: Report
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
bash .ci/scripts/ci-report.sh "<Komponentti>: <Test type> test report" <komponentti>.<context> <suite> ${{ job.status }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Taso 2: Jälkikäsittely tarvitaan
|
||||||
|
|
||||||
|
Single repo:
|
||||||
|
```yaml
|
||||||
|
- name: Run tests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p "reports/<suite>"
|
||||||
|
<testikomento> > "reports/<suite>/results.txt" 2>&1
|
||||||
|
|
||||||
|
- name: Post-process coverage
|
||||||
|
if: always()
|
||||||
|
run: <siirrä coverage-data reports/<suite>/coverage/-hakemistoon>
|
||||||
|
|
||||||
|
- name: Post-process test report
|
||||||
|
if: always()
|
||||||
|
run: <HTML-generointi raa'asta outputista>
|
||||||
|
|
||||||
|
- name: Report
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
bash .ci/scripts/ci-report.sh "<Test type> test report" <context> <suite> ${{ job.status }}
|
||||||
|
```
|
||||||
|
|
||||||
|
Monorepo:
|
||||||
|
```yaml
|
||||||
|
- name: Run tests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p "reports/<suite>"
|
||||||
|
<testikomento> > "reports/<suite>/results.txt" 2>&1
|
||||||
|
|
||||||
|
- name: Post-process coverage
|
||||||
|
if: always()
|
||||||
|
run: <siirrä coverage-data reports/<suite>/coverage/-hakemistoon>
|
||||||
|
|
||||||
|
- name: Post-process test report
|
||||||
|
if: always()
|
||||||
|
run: <HTML-generointi raa'asta outputista>
|
||||||
|
|
||||||
|
- name: Report
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
bash .ci/scripts/ci-report.sh "<Komponentti>: <Test type> test report" <komponentti>.<context> <suite> ${{ job.status }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Väärin vs oikein — yksi asia per step
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# VÄÄRIN — helm template fail → kubeconform jää ajamatta, report jää tekemättä
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
helm template ... > /tmp/manifests.yaml
|
||||||
|
kubeconform ... > results.txt 2>&1
|
||||||
|
|
||||||
|
# OIKEIN — erilliset stepit
|
||||||
|
- name: Helm template
|
||||||
|
run: helm template platform-helm/ -f values.yaml > /tmp/manifests.yaml 2>&1
|
||||||
|
|
||||||
|
- name: Kubeconform
|
||||||
|
if: success()
|
||||||
|
run: |
|
||||||
|
mkdir -p reports/kubeconform
|
||||||
|
kubeconform ... > reports/kubeconform/results.txt 2>&1
|
||||||
|
|
||||||
|
- name: Report
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
bash .ci/scripts/ci-report.sh "Helm kubeconform" helm-test kubeconform ${{ job.status }}
|
||||||
|
```
|
||||||
|
|
||||||
|
Monorepossa:
|
||||||
|
```yaml
|
||||||
|
- name: Report
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
bash .ci/scripts/ci-report.sh "<Komponentti>: Helm kubeconform" <komponentti>.helm-test kubeconform ${{ job.status }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Väärin vs oikein — post-process
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# VÄÄRIN — jos coverage epäonnistuu, report jää generoimatta
|
||||||
|
- name: Post-process reports
|
||||||
|
run: |
|
||||||
|
bash .ci/.gitea/scripts/bats-coverage.sh reports/bats
|
||||||
|
bash .ci/.gitea/scripts/bats-report.sh reports/bats
|
||||||
|
|
||||||
|
# OIKEIN — erilliset stepit if: always()
|
||||||
|
- name: Post-process coverage
|
||||||
|
if: always()
|
||||||
|
run: bash .ci/.gitea/scripts/bats-coverage.sh reports/bats
|
||||||
|
|
||||||
|
- name: Post-process test report
|
||||||
|
if: always()
|
||||||
|
run: bash .ci/.gitea/scripts/bats-report.sh reports/bats
|
||||||
|
```
|
||||||
|
|
||||||
|
## Raportin julkaisukelpoisuus
|
||||||
|
|
||||||
|
`ci-report.sh` päättää onko raportti julkaisukelpoinen skannaamalla `reports/<suite>/`-hakemistoa.
|
||||||
|
|
||||||
|
### Mitä skannataan
|
||||||
|
|
||||||
|
| Mitä | Sääntö |
|
||||||
|
|---|---|
|
||||||
|
| **Tiedostot (FILES)** | Kaikki `reports/<suite>/`-juuressa olevat tiedostot paitsi `index.html` |
|
||||||
|
| **Alihakemistot (SUBDIRS)** | Vain ne, joissa on `index.html` |
|
||||||
|
|
||||||
|
### Julkaisukelpoisuus
|
||||||
|
|
||||||
|
| Tila | Seuraus |
|
||||||
|
|---|---|
|
||||||
|
| `FILES + SUBDIRS = 0` | **Failure** — `ci-report.sh` palauttaa virheen, raporttia ei julkaista |
|
||||||
|
| `FILES + SUBDIRS = 1` | Suora linkki itemiin — ei generoi index-sivua |
|
||||||
|
| `FILES + SUBDIRS > 1` | Generoi `reports/<suite>/index.html`-sivun, linkit kaikkiin itemeihin |
|
||||||
|
|
||||||
|
### Hakemistorakenne
|
||||||
|
|
||||||
|
```
|
||||||
|
reports/<suite>/
|
||||||
|
├── results.txt ← testin stdout (skannataan FILES)
|
||||||
|
├── test-report.html ← generoitu HTML (skannataan FILES)
|
||||||
|
└── <mikä tahansa>/ ← alihakemisto (skannataan SUBDIRS)
|
||||||
|
└── index.html ← VAIN jos tämä on olemassa
|
||||||
|
```
|
||||||
|
|
||||||
|
### Esimerkki: coverage-näkymä
|
||||||
|
|
||||||
|
```
|
||||||
|
reports/<suite>/coverage/index.html ← on olemassa
|
||||||
|
```
|
||||||
|
|
||||||
|
Coverage-dataa ei siirretä automaattisesti. Testin tai post-process-stepin pitää
|
||||||
|
siirtää coverage `reports/<suite>/coverage/`-hakemistoon ja varmistaa että `index.html` on mukana.
|
||||||
|
|
||||||
|
**Provider vastuulla:** `ci-report.sh` (provider-skripti) hoitaa sekä hakemistorakenteen
|
||||||
|
skannauksen, `index.html`-generoinnin että julkaisun git-pagesiin. Consumer tuottaa
|
||||||
|
vain raakatiedostot `reports/<suite>/`-hakemistoon — `ci-report.sh` päättää
|
||||||
|
julkaisukelpoisuuden ja generoi tarvittavan navigaation.
|
||||||
|
|
||||||
|
## Debug-ohje: raportti ei näy
|
||||||
|
|
||||||
|
### 1. Aja lokaalisti samalla komennolla kuin CI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p reports/bats
|
||||||
|
bashcov -- bats tests/ > reports/bats/results.txt 2>&1
|
||||||
|
echo "exit: $?"
|
||||||
|
ls -la reports/bats/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Lisää `echo "DEBUG: ..." >&2` ennen ja jälkeen kriittisen operaation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "DEBUG: coverage exists? $([ -d coverage ] && echo YES || echo NO)" >&2
|
||||||
|
echo "DEBUG: target/index.html exists? $([ -f reports/suite/coverage/index.html ] && echo YES || echo NO)" >&2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Tarkista kutsuparametrit
|
||||||
|
|
||||||
|
Yleisin virhe: skripti odottaa `$1` = X, mutta kutsuja antaa `$1` = Y ja `$2` = X.
|
||||||
|
|
||||||
|
### 4. Tarkista tiedostopolut
|
||||||
|
|
||||||
|
1. Onko lähdetiedosto olemassa ennen kopiointia?
|
||||||
|
2. Onko kohde olemassa kopioinnin jälkeen?
|
||||||
|
3. Onko `index.html` subdirissä (vaaditaan `ci-report.sh`:lle)?
|
||||||
|
|
||||||
|
### 5. Poista debug-echot kun ongelma on korjattu
|
||||||
|
|
||||||
|
### 6. Älä kokeile — debuggaa
|
||||||
|
|
||||||
|
Kokeilu = arvaus. Debuggaus = lisää echo, aja, lue logi, eristä ongelma. Vasta sitten korjaa.
|
||||||
|
|
||||||
|
## Konfiguraatiotiedosto (.gitea-env.conf)
|
||||||
|
|
||||||
|
Tiedosto on `key=value`-muotoinen (kuten `.env`). Kommentit ja tyhjät rivit sallittuja.
|
||||||
|
|
||||||
|
### Single repo
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# .gitea/workflows/gitea-env.conf
|
||||||
|
GITEA_API_URL=https://gitea.example.com
|
||||||
|
GIT_PAGES_URL=https://reports.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker-artifaktin buildaavat projektit
|
||||||
|
|
||||||
|
```ini
|
||||||
|
DOCKER_REGISTRY=gitea.example.com/myorg
|
||||||
|
DOCKER_IMAGE_NAME=my-service
|
||||||
|
DOCKER_UI_URL=https://gitea.example.com/myorg/-/packages/container
|
||||||
|
#DOCKERFILE=Dockerfile.platform # valinnainen, oletus Dockerfile
|
||||||
|
```
|
||||||
|
|
||||||
|
`DOCKER_UI_URL` ei sisällä image-nimeä — se on puhdas container-registryn osoite.
|
||||||
|
Image-nimi lisätään automaattisesti URL:iin `docker-build-push.yml`:ssä.
|
||||||
|
|
||||||
|
### Helm-artifaktin buildaavat projektit
|
||||||
|
|
||||||
|
```ini
|
||||||
|
HELM_REGISTRY=gitea.example.com/myorg
|
||||||
|
GIT_TAG_PREFIX=git-pages/
|
||||||
|
VERSION_FILE=git-pages/Chart.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
| Kenttä | Pakollinen | Kuvaus |
|
||||||
|
|---|---|---|
|
||||||
|
| `HELM_REGISTRY` | **kyllä** | Registry host + owner, esim. `gitea.example.com/myorg`. **Tyhjä pysäyttää workflow'n.** |
|
||||||
|
| `GIT_TAG_PREFIX` | ei | Etuliite git-tägille. Pakollinen monorepossa. |
|
||||||
|
| `VERSION_FILE` | ei | Polku version lähteeseen. Oletus: juuren `Chart.yaml`. |
|
||||||
|
|
||||||
|
### Salaisuudet (Gitea Settings → Secrets)
|
||||||
|
|
||||||
|
| Secret | Pakollinen |
|
||||||
|
|---|---|
|
||||||
|
| `GITEA_TOKEN` | Aina (Gitean sisäinen, automaattisesti saatavilla) |
|
||||||
|
| `GIT_PAGES_PUBLISH_TOKEN` | Aina |
|
||||||
|
| `DOCKER_USERNAME` | Vain jos buildaat kontteja |
|
||||||
|
| `DOCKER_PASSWORD` | Vain jos buildaat kontteja |
|
||||||
|
| `HELM_USER` | Vain jos pushaat Helm chartin OCI-rekisteriin (oletus `github.actor`) |
|
||||||
|
| `HELM_PASSWORD` | Vain jos pushaat Helm chartin OCI-rekisteriin |
|
||||||
|
|
||||||
|
## Monorepo
|
||||||
|
|
||||||
|
Monorepossa yhdessä repossa asuu useampi julkaistava komponentti. Jokaiselle komponentille
|
||||||
|
oma conf-tiedosto `.gitea/workflows/<komponentti>.gitea-env.conf`.
|
||||||
|
|
||||||
|
### Suositus: komponentit omiin juurihakemistoihin
|
||||||
|
|
||||||
|
On suositeltavaa sijoittaa jokaisen komponentin koko lähdekoodi omaan juuritason
|
||||||
|
hakemistoonsa (`api/`, `frontend/`, `shared/`). Tämä helpottaa `paths:`-filtteröintiä,
|
||||||
|
pitää komponentit selkeästi erillään, ja tekee repossa navigoinnista suoraviivaista.
|
||||||
|
|
||||||
|
### Ongelmat ja ratkaisut
|
||||||
|
|
||||||
|
| Ongelma | Ratkaisu |
|
||||||
|
|---|---|
|
||||||
|
| Monta komponenttia, yksi repo — mikä triggeröi? | `paths:`-filtteri: komponentin hakemisto + sen CI-workflow't ja conf-tiedosto |
|
||||||
|
| Jokaisella komponentilla oma versio | `VERSION_FILE=<komponentti>/package.json` confissa |
|
||||||
|
| Git-tägit sekaisin ellei nimiavaruutta | `GIT_TAG_PREFIX=<komponentti>/` confissa → tägi `<komponentti>/1.2.3` |
|
||||||
|
| Eri julkaisutahdit | Riippumattomat CI-triggerit, omat versiopolut |
|
||||||
|
|
||||||
|
### Komponenttikohtainen conf
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# .gitea/workflows/<komponentti>.gitea-env.conf
|
||||||
|
GITEA_API_URL=https://gitea.example.com
|
||||||
|
GIT_PAGES_URL=https://reports.example.com
|
||||||
|
DOCKER_REGISTRY=gitea.example.com/myorg
|
||||||
|
DOCKER_IMAGE_NAME=<image-nimi>
|
||||||
|
DOCKER_UI_URL=https://gitea.example.com/myorg/-/packages/container
|
||||||
|
GIT_TAG_PREFIX=<komponentti>/
|
||||||
|
# Jompikumpi — JSON (.version-kenttä) tai plain text:
|
||||||
|
VERSION_FILE=<komponentti>/package.json
|
||||||
|
#VERSION_FILE=<komponentti>/VERSION
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monorepo reititin
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: CI <Komponentti> Main
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- <komponentti>/**
|
||||||
|
- .gitea/workflows/<komponentti>.*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
load-config:
|
||||||
|
uses: <owner>/gitea-ci-library/.gitea/workflows/config-provider.yml@v1
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
config_path: .gitea/workflows/<komponentti>.gitea-env.conf
|
||||||
|
|
||||||
|
check-version:
|
||||||
|
needs: [load-config]
|
||||||
|
uses: <owner>/gitea-ci-library/.gitea/workflows/check-version.yml@v1
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
|
||||||
|
<testit>:
|
||||||
|
needs: [load-config, check-version]
|
||||||
|
if: needs.check-version.outputs.artifact_exists != 'true'
|
||||||
|
uses: ./.gitea/workflows/<komponentti>.<testi>.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
|
||||||
|
build-push:
|
||||||
|
needs: [load-config, check-version, <testit>]
|
||||||
|
if: needs.check-version.outputs.artifact_exists != 'true'
|
||||||
|
uses: <owner>/gitea-ci-library/.gitea/workflows/docker-build-push.yml@v1
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
version: ${{ needs.check-version.outputs.version }}
|
||||||
|
|
||||||
|
report-summary:
|
||||||
|
name: Report Summary
|
||||||
|
needs: [load-config, build-push]
|
||||||
|
if: always()
|
||||||
|
uses: <owner>/gitea-ci-library/.gitea/workflows/report-summary.yml@v1
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
suites: '<suite-1> <suite-2>'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commit status -kontekstit monorepossa:** Testiraporttien `ci-report.sh`-kutsussa
|
||||||
|
context ja description sisältävät komponentin nimen:
|
||||||
|
```yaml
|
||||||
|
- name: Report
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
bash .ci/scripts/ci-report.sh "<Komponentti>: Unit test report" <komponentti>.unit-tests bats ${{ job.status }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version elinkaari per komponentti
|
||||||
|
|
||||||
|
`GIT_TAG_PREFIX` takaa että eri komponenttien versiohistoria pysyy erillään.
|
||||||
|
Git-tägi `<komponentti>/0.2.3` ei sekoitu toisen komponentin tägeihin.
|
||||||
|
|
||||||
|
`check-version.yml` suodattaa ja laskee seuraavan patchin vain kyseisen
|
||||||
|
komponentin etuliitteellä. Idempotenttius toimii komponenttikohtaisesti:
|
||||||
|
jos commitilla on jo tägi, pipeline skipataan `if: artifact_exists != 'true'`.
|
||||||
|
|
||||||
|
### Mitä EI kannata tehdä monorepossa
|
||||||
|
|
||||||
|
- Älä aja kaikkia komponentteja samasta triggeristä — `paths:` pitää CI:t erillisinä
|
||||||
|
- Älä käytä samaa versionhallintatiedostoa usealle komponentille
|
||||||
|
- Älä anna monorepo-parametreja pipeline-overrideina — kaikki kuuluu conf-tiedostoon
|
||||||
|
- Älä rajaa `paths:` pelkkään komponentin hakemistoon — CI ei triggeröidy workflow- tai conf-muutoksista
|
||||||
|
|
||||||
|
## Versionhallinta
|
||||||
|
|
||||||
|
`check-version.yml` lukee version automaattisesti prioriteettijärjestyksessä:
|
||||||
|
|
||||||
|
| # | Lähde | Formaatti |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `VERSION_FILE` confissa | Määritelty polku |
|
||||||
|
| 2 | `VERSION`-tiedosto (root) | Plain text |
|
||||||
|
| 3 | `package.json` (root) | `.version`-kenttä |
|
||||||
|
| 4 | `pom.xml` (root) | `<version>`-elementti |
|
||||||
|
|
||||||
|
`major.minor` otetaan tästä. Patch lasketaan automaattisesti git-tageista.
|
||||||
|
Esim. `VERSION` = `0.2`, tagit = `0.2.0`, `0.2.1` → seuraava `0.2.2`.
|
||||||
|
|
||||||
|
## Branch protection (PR-gate)
|
||||||
|
|
||||||
|
Gitean Settings → Branches → Add Rule:
|
||||||
|
|
||||||
|
- **Branch:** `main`
|
||||||
|
- **Enable Require Status Checks:** päälle
|
||||||
|
- **Status checks:** valitse testijobien nimet
|
||||||
|
|
||||||
|
## Provider-rajapinnat — referenssi
|
||||||
|
|
||||||
|
### Workflowt
|
||||||
|
|
||||||
|
| Workflow | Käyttötarkoitus |
|
||||||
|
|---|---|
|
||||||
|
| `config-provider.yml` | Lataa + validoi `.conf`, tuottaa `env_json` |
|
||||||
|
| `check-version.yml` | Tarkistaa onko commit buildattu, laskee version |
|
||||||
|
| `docker-build-push.yml` | Buildaa + puskea Docker-imagen, tagittaa commitin |
|
||||||
|
| `helm-build-push.yml` | Paketoi + puskea Helm chartin OCI-rekisteriin, tagittaa commitin |
|
||||||
|
| `report-summary.yml` | `GITHUB_STEP_SUMMARY`-taulukko raporttilinkeillä (Gitea 1.27+) |
|
||||||
|
|
||||||
|
### Skriptit (kutsutaan `.ci/scripts/`-polun kautta)
|
||||||
|
|
||||||
|
| Skripti | Käyttötarkoitus |
|
||||||
|
|---|---|
|
||||||
|
| `ci-report.sh` | Yhdistetty raportointi: julkaisee git-pagesiin ja asettaa commit-statuksen. Korvaa erilliset `publish-git-pages.sh` + `report-status.sh` -kutsut. Käyttö: `bash .ci/scripts/ci-report.sh "<kuvaus>" <context> <suite> ${{ job.status }}` |
|
||||||
|
| `report-status.sh` | POSTaa commit-statuksen linkillä (kutsutaan `ci-report.sh`:n sisältä) |
|
||||||
|
| `publish-git-pages.sh` | Julkaisee raporttihakemiston git-pagesiin (kutsutaan `ci-report.sh`:n sisältä) |
|
||||||
|
| `ci-validate.sh` | Validoi `.conf`-tiedoston (kutsutaan `config-provider.yml`:stä) |
|
||||||
+385
-450
@@ -19,6 +19,8 @@ impact: high
|
|||||||
Säännöt joilla consumer-projektit rakentavat CI-pipelinejä `gitea-ci-library`-kirjaston päälle.
|
Säännöt joilla consumer-projektit rakentavat CI-pipelinejä `gitea-ci-library`-kirjaston päälle.
|
||||||
Nämä eivät ole provider-kirjaston sääntöjä — ne kuvaavat miten consumerin kuuluu käyttää kirjastoa oikein.
|
Nämä eivät ole provider-kirjaston sääntöjä — ne kuvaavat miten consumerin kuuluu käyttää kirjastoa oikein.
|
||||||
|
|
||||||
|
Katso tarkat mallipohjat ja esimerkit `REFERENCE.md`:stä.
|
||||||
|
|
||||||
## 1. Reitittimen puhtaus
|
## 1. Reitittimen puhtaus
|
||||||
|
|
||||||
Reitittimet (`ci-feature.yml`, `ci-main.yml`) eivät sisällä `run:`-steppejä. Ne koostuvat vain:
|
Reitittimet (`ci-feature.yml`, `ci-main.yml`) eivät sisällä `run:`-steppejä. Ne koostuvat vain:
|
||||||
@@ -36,36 +38,7 @@ with:
|
|||||||
Jokainen job vastaa yhtä loogista testiä tai operaatiota. Reititin on orkestraattori — kaikki suorittava
|
Jokainen job vastaa yhtä loogista testiä tai operaatiota. Reititin on orkestraattori — kaikki suorittava
|
||||||
logiikka on omassa `workflow_call`-tiedostossaan.
|
logiikka on omassa `workflow_call`-tiedostossaan.
|
||||||
|
|
||||||
**Esimerkki:**
|
Katso täydellinen esimerkki `REFERENCE.md`:stä.
|
||||||
|
|
||||||
```yaml
|
|
||||||
jobs:
|
|
||||||
load-config:
|
|
||||||
uses: <owner>/gitea-ci-library/.gitea/workflows/config-provider.yml@v1
|
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
<test-1>:
|
|
||||||
needs: [load-config]
|
|
||||||
uses: ./.gitea/workflows/<component>.<test-1>.yml
|
|
||||||
secrets: inherit
|
|
||||||
with:
|
|
||||||
env_json: ${{ needs.load-config.outputs.env_json }}
|
|
||||||
|
|
||||||
<test-2>:
|
|
||||||
needs: [load-config]
|
|
||||||
uses: ./.gitea/workflows/<component>.<test-2>.yml
|
|
||||||
secrets: inherit
|
|
||||||
with:
|
|
||||||
env_json: ${{ needs.load-config.outputs.env_json }}
|
|
||||||
|
|
||||||
report-summary:
|
|
||||||
needs: [load-config, <test-1>, <test-2>]
|
|
||||||
if: always()
|
|
||||||
uses: <owner>/gitea-ci-library/.gitea/workflows/report-summary.yml@v1
|
|
||||||
with:
|
|
||||||
env_json: ${{ needs.load-config.outputs.env_json }}
|
|
||||||
suites: <suite-1> <suite-2>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Yksi asia per tiedosto
|
## 2. Yksi asia per tiedosto
|
||||||
|
|
||||||
@@ -89,263 +62,172 @@ ja exit-koodi välittyy natiivisti. Ylimääräistä `EXIT=$?` + `echo >> GITHUB
|
|||||||
<testikomento> > results.txt 2>&1
|
<testikomento> > results.txt 2>&1
|
||||||
```
|
```
|
||||||
|
|
||||||
**Miksi ei pipeä (`| tee`):**
|
**Miksi ei pipeä (`| tee`):** `|` syö exit-koodin. Käytä redirectiä `>`.
|
||||||
|
|
||||||
```bash
|
**Yksi asia per step:** Älä koskaan niputa useaa komentoa samaan `run:`-blockiin. `bash -e` pysäyttää
|
||||||
# VÄÄRIN — pipe syö exit-koodin
|
koko stepin ensimmäisellä failaavalla komennolla, ja loput jäävät ajamatta. Sama pätee post-process-steppeihin.
|
||||||
<komento> | tee results.txt
|
|
||||||
|
|
||||||
# OIKEIN — redirect tiedostoon
|
|
||||||
<komento> > results.txt 2>&1
|
|
||||||
```
|
|
||||||
|
|
||||||
`set -e` ei pelasta pipe-tilanteessa — `|` syö exit-koodin kuten ennenkin. Redirectillä exit-koodi
|
|
||||||
välittyy luonnollisesti.
|
|
||||||
|
|
||||||
**Yksi asia per step:** Älä koskaan niputa useaa post-process-komentoa samaan `run:`-blockiin.
|
|
||||||
`bash -e` pysäyttää koko stepin jos yksi komento epäonnistuu — seuraavat jäävät ajamatta.
|
|
||||||
Käytä erillisiä steppejä `if: always()`:lla, jotta jokainen vaihe ajetaan itsenäisesti:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# VÄÄRIN — jos coverage epäonnistuu, report jää generoimatta
|
|
||||||
- name: Post-process reports
|
|
||||||
run: |
|
|
||||||
bash .ci/.gitea/scripts/bats-coverage.sh reports/bats
|
|
||||||
bash .ci/.gitea/scripts/bats-report.sh reports/bats
|
|
||||||
|
|
||||||
# OIKEIN — erilliset stepit if: always()
|
|
||||||
- name: Post-process coverage
|
|
||||||
if: always()
|
|
||||||
run: bash .ci/.gitea/scripts/bats-coverage.sh reports/bats
|
|
||||||
|
|
||||||
- name: Post-process test report
|
|
||||||
if: always()
|
|
||||||
run: bash .ci/.gitea/scripts/bats-report.sh reports/bats
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Konttipolitiikka
|
## 4. Konttipolitiikka
|
||||||
|
|
||||||
1. **Julkiset registry-kontit kiinteällä versiolla** — `alpine/helm:3.19.0`, `node:22`, `maven:3.9-eclipse-temurin-21`.
|
1. **Julkiset registry-kontit kiinteällä versiolla** — `alpine/helm:3.19.0`, `node:22`, `maven:3.9-eclipse-temurin-21`.
|
||||||
Toistettavuus ja turvallisuus eivät saa riippua ulkoisesta `latest`:sta
|
Toistettavuus ja turvallisuus eivät saa riippua ulkoisesta `latest`:sta.
|
||||||
2. **Projektin omat CI-kontit `latest`-tägillä** — buildattu `ci-container-build-<kontti>.yml`:llä.
|
2. **Projektin omat CI-kontit `latest`-tägillä** — buildattu `ci-container-build-<kontti>.yml`:llä.
|
||||||
Kontin build-pipeline päivittää `latest`:n automaattisesti. Rebuild = käyttöönotto
|
Kontin build-pipeline päivittää `latest`:n automaattisesti. Rebuild = käyttöönotto
|
||||||
kaikissa pipelineissa ilman versioviittauksien päivittelyä.
|
kaikissa pipelineissa ilman versioviittauksien päivittelyä.
|
||||||
`latest` on näille paras käytäntö, ei kompromissi
|
|
||||||
3. **Ei koskaan `curl`-latauksia CI-ajon sisällä** — työkalujen asennus CI-stepeissä hidastaa,
|
3. **Ei koskaan `curl`-latauksia CI-ajon sisällä** — työkalujen asennus CI-stepeissä hidastaa,
|
||||||
epäluotettavaa, ja vaikeuttaa toistettavuutta
|
epäluotettavaa, ja vaikeuttaa toistettavuutta.
|
||||||
4. **Konttikuva hallitaan workflow'ssa, ei kutsujassa** — jos workflow vaatii tietyn
|
4. **Konttikuva hallitaan workflow'ssa, ei kutsujassa** — jos workflow vaatii tietyn
|
||||||
konttikuvan, se määritellään oletuksena (`default:`) workflow'n inputissa.
|
konttikuvan, se määritellään oletuksena (`default:`) workflow'n inputissa.
|
||||||
Kutsujan ei tarvitse tietää eikä välittää image-nimeä ellei halua ylikirjoittaa.
|
Kutsujan ei tarvitse tietää eikä välittää image-nimeä ellei halua ylikirjoittaa.
|
||||||
|
|
||||||
CI-kontin build-workflow'n template: [skills/ci-container-build/SKILL.md](../ci-container-build/SKILL.md) — sisältää
|
CI-kontin build-workflow'n template: `skills/ci-container-build/SKILL.md`.
|
||||||
valmiin `ci-container-build-<kontti>.yml`-pohjan jossa `workflow_dispatch`-tuki manuaaliajoon.
|
|
||||||
|
|
||||||
### 4.1 CI-kontin ajaminen jobissa
|
### 4.1 Offline Container -vaatimus (DoD)
|
||||||
|
|
||||||
|
CI-kontin (ja kaikkien pipeline-konttien) on oltava täysin itseriittoisia:
|
||||||
|
|
||||||
|
> Kontti ei lataa mitään pipeline-vaiheessa (`workflow run` -stepit) eikä
|
||||||
|
> kontin runtime-prosessissa (`container:` / `docker run`). Kaikki
|
||||||
|
> riippuvuudet pre-cachataan `docker build` -vaiheessa.
|
||||||
|
> Ainoa sallittu lataushetki on `docker build`.
|
||||||
|
|
||||||
|
**Esimerkkejä rikkomuksista:**
|
||||||
|
- `apk add`, `apt-get install`, `npm install`, `go mod download`, `pip install`
|
||||||
|
pipeline-stepissä
|
||||||
|
- `curl <url> | tar xz` runtime-vaiheessa
|
||||||
|
- Node.js-konttikuva ilman nodea (joudutaan asentamaan lennossa)
|
||||||
|
|
||||||
|
### 4.2 Kielikohtainen pre-cache
|
||||||
|
|
||||||
|
Kun kontissa testataan kielikohtaista koodia, kaikki riippuvuudet on
|
||||||
|
pre-cachattava Dockerfilessä, ei pipeline-stepissä:
|
||||||
|
|
||||||
|
| Kieli | Pre-cache Dockerfilessä |
|
||||||
|
|---|---|
|
||||||
|
| Go | `COPY go.mod go.sum ./` → `RUN go mod download` |
|
||||||
|
| Java/Maven | `COPY pom.xml ./` → `RUN mvn dependency:go-offline` |
|
||||||
|
| Node | `COPY package.json package-lock.json ./` → `RUN npm ci --omit=dev` |
|
||||||
|
| Python | `COPY requirements.txt ./` → `RUN pip install -r requirements.txt` |
|
||||||
|
|
||||||
|
Katso tarkat Dockerfile-esimerkit `REFERENCE.md`:stä.
|
||||||
|
|
||||||
|
### 4.3 CI-kontin ajaminen jobissa
|
||||||
|
|
||||||
Ainoa sallittu tapa on `container:`-direktiivi. `docker run` komennolla kontin
|
Ainoa sallittu tapa on `container:`-direktiivi. `docker run` komennolla kontin
|
||||||
käynnistäminen stepin sisällä on anti-pattern.
|
käynnistäminen stepin sisällä on anti-pattern.
|
||||||
|
|
||||||
**Miksi:** `docker run` erilliskonttina aiheuttaa:
|
Katso CI-kontin template `REFERENCE.md`:stä.
|
||||||
- Tiedostojen jako vaatii erillisen volyyminhallinnan (`docker volume create`)
|
|
||||||
- Coverage-data jää volyymiin, ei filesystemille → post-process-skriptit eivät löydä sitä
|
|
||||||
- Ylimääräisiä siirtoja (`tar`, `docker cp`), jotka voivat epäonnistua hiljaa
|
|
||||||
- Vaikeampi debugata (data on kontissa, ei CWD:ssä)
|
|
||||||
|
|
||||||
`container:`-direktiivillä kaikki ajetaan samassa kontissa — tiedostot ovat suoraan
|
**Huomio `actions/checkout@v4`:stä:** `container:`-direktiivillä kaikki stepit
|
||||||
filesystemillä, post-process-skriptit näkevät ne ilman erillistä siirtoa.
|
ajetaan kontin *sisällä* — myös `actions/checkout@v4`. Se on JavaScript-action
|
||||||
|
joka vaatii sekä `nodejs` että `git`. Varmista että CI-kontin Dockerfilessä on
|
||||||
|
molemmat — muuten checkout ei toimi ja pipeline failaa.
|
||||||
|
|
||||||
```yaml
|
### 4.4 Build-konteksti, `.dockerignore` ja `COPY`
|
||||||
jobs:
|
|
||||||
<työkalu>:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: ${{ inputs.<image-name> }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: <owner>/gitea-ci-library
|
|
||||||
path: .ci
|
|
||||||
|
|
||||||
- name: Run <työkalu>
|
**Build-konteksti** on aina tiedoston (Dockerfile, Chart.yaml) oman hakemiston
|
||||||
shell: bash
|
juuri (`dirname "${DOCKERFILE}"` / `dirname "${CHART_FILE}"`). Kaikki
|
||||||
run: |
|
suhteelliset polut — ignore-tiedosto, `COPY`, `ADD` — ovat suhteessa tähän
|
||||||
mkdir -p "reports/<suite>"
|
kontekstiin.
|
||||||
<komento> > "reports/<suite>/results.txt" 2>&1
|
|
||||||
|
|
||||||
- name: Post-process reports
|
| Tiedosto | Konteksti | Ignore-tiedosto | Käyttö |
|
||||||
if: always()
|
|---|---|---|---|
|
||||||
run: |
|
| `Dockerfile` | `.` | `./.dockerignore` | `docker build` / `COPY src/ src/` |
|
||||||
<mahdollinen_raporttien_jälkikäsittely>
|
| `api/Dockerfile` | `api/` | `api/.dockerignore` | `docker build` / `COPY src/ src/` |
|
||||||
|
| `Chart.yaml` (`VERSION_FILE`) | `.` | `./.helmignore` | `helm package` |
|
||||||
|
| `api/Chart.yaml` (`VERSION_FILE`) | `api/` | `api/.helmignore` | `helm package` |
|
||||||
|
|
||||||
- name: Report
|
Helm chartin polku luetaan confin `VERSION_FILE`-kentästä — sama rivi jota
|
||||||
if: always()
|
`check-version.yml` käyttää version lähteenä. Yksi conf-rivi ohjaa molempia:
|
||||||
run: bash .ci/scripts/ci-report.sh "<kuvaus>" <context> <suite>
|
sekä versionlaskentaa että chartin sijaintia.
|
||||||
|
|
||||||
|
**Mitä ignore-tiedosto sisältää:** Kaikki mikä EI ole konttiin tai chart-pakettiin
|
||||||
|
tarkoitettua koodia tai resurssia, ON oltava ignore-tiedostossa:
|
||||||
|
|
||||||
|
- Git- ja CI-historia (`.git/`, `.gitea/`, `.github/`)
|
||||||
|
- Testikoodi, testidata, testiraportit (`tests/`, `reports/`, `coverage/`)
|
||||||
|
- Dokumentaatio (`docs/`, `guides/`, `*.md`, `CHANGELOG`, `README`)
|
||||||
|
- Editori- ja työkalukonfiguraatio (`.vscode/`, `.cursor/`, `.idea/`, `.DS_Store`)
|
||||||
|
- Riippuvuudet jotka asennetaan Dockerfilessä (`node_modules/`)
|
||||||
|
- Väliaikaistiedostot (`tmp/`, `*.log`)
|
||||||
|
- Projektikohtaiset konfiguraatiot (`.env`, `*.conf`, `CURRENT_PROVIDER_VERSION`)
|
||||||
|
|
||||||
|
**Miksi:** Build-kontekstin koko vaikuttaa suoraan `docker build` -nopeuteen.
|
||||||
|
Raskas konteksti (etenkin `.git/` ja `node_modules/`) hidastaa buildia ja
|
||||||
|
kuluttaa runnerin resursseja turhaan. Ylimääräiset tiedostot kontissa ovat
|
||||||
|
**tietoturvariski** — tokenit, `.env` ja sensitiivinen data voivat päätyä
|
||||||
|
kontin layeriin jos `.dockerignore` ei ole kattava.
|
||||||
|
|
||||||
|
### 4.5 `COPY`-kuri — kopioi vain tarvittava
|
||||||
|
|
||||||
|
`COPY . .` on kielletty. Jokainen `COPY` kopioi vain tarvittavat tiedostot
|
||||||
|
tai hakemistot:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# VÄÄRIN
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# OIKEIN
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
COPY src/ src/
|
||||||
|
COPY public/ public/
|
||||||
```
|
```
|
||||||
|
|
||||||
Jos testi tuottaa raportteja suoraan ilman jälkikäsittelyä, Post-process-steppiä ei tarvita.
|
**Miksi:**
|
||||||
Jos jälkikäsittely on tarpeen (coverage-siirto, HTML-generointi raa'asta outputista),
|
- Layer-cache: `COPY . .` rikkoo välimuistin — mikä tahansa muutos
|
||||||
se tehdään omassa stepissä `if: always()` — katso tarkemmin [Raporttitasot](#5-raporttitasot).
|
tiedostossa tyhjentää koko layerin
|
||||||
|
- Tietoturva: konttiin voi päätyä ylimääräisiä tiedostoja vaikka
|
||||||
|
`.dockerignore` olisi kattava (unohtunut ignore-rivi, uusi työkalu
|
||||||
|
joka luo tiedostoja build-kontekstiin)
|
||||||
|
- Luettavuus: `COPY . .` ei kerro mitä kontti todella sisältää
|
||||||
|
- Kontin koko: eksplisiittinen `COPY` pitää image-koon kurissa
|
||||||
|
|
||||||
**Mallit:**
|
### 4.6 `.helmignore` — pidä chart-paketti siistinä
|
||||||
- `example-cucumber-tests.yml` — ei post-processia
|
|
||||||
- `example-bats-tests.yml` — post-process coverage + report
|
`helm package` käyttää `.helmignore`-tiedostoa samalla periaatteella kuin
|
||||||
|
`docker build` käyttää `.dockerignore`a:
|
||||||
|
|
||||||
|
- Chart-hakemisto luetaan confin `VERSION_FILE`-kentästä (`dirname "${VERSION_FILE}"`)
|
||||||
|
- ignore-tiedosto luetaan chart-hakemiston juuresta (sama konteksti kuin
|
||||||
|
`Chart.yaml`, ks. 4.4)
|
||||||
|
- Kaikki turha (testit, docs, git, CI-konffit, kuvat) on poissuljettava
|
||||||
|
- Jos `.helmignore` puuttuu, `helm package` paketoi mukaan kaikki
|
||||||
|
chart-hakemiston tiedostot — turhaa bulkkia registryyn
|
||||||
|
|
||||||
|
**`.helmignore` on pakollinen** jokaiselle chartille. Minimisisältö:
|
||||||
|
|
||||||
|
```
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
tests/
|
||||||
|
docs/
|
||||||
|
*.md
|
||||||
|
.DS_Store
|
||||||
|
```
|
||||||
|
|
||||||
## 5. Raporttitasot
|
## 5. Raporttitasot
|
||||||
|
|
||||||
Testi tuottaa raportin `reports/<suite>/`-hakemistoon. Yksi `ci-report.sh`-kutsu hoitaa sekä
|
Testi tuottaa raportin `reports/<suite>/`-hakemistoon. Yksi `ci-report.sh`-kutsu hoitaa sekä
|
||||||
julkaisun että commit-statuksen — erillistä Publish + Report Status -kaksivaiheisuutta ei tarvita.
|
julkaisun että commit-statuksen.
|
||||||
|
|
||||||
### Taso 1: Ei jälkikäsittelyä
|
### Taso 1: Ei jälkikäsittelyä
|
||||||
|
|
||||||
Kun testi tuottaa raportit suoraan (kuten `pytest --html` tai `cucumber-js --format html`):
|
Kun testi tuottaa raportit suoraan (kuten `pytest --html` tai `cucumber-js --format html`):
|
||||||
|
- testi kirjoittaa `reports/<suite>/`-hakemistoon
|
||||||
```yaml
|
- `ci-report.sh` julkaisee ja asettaa commit-statuksen
|
||||||
- name: Run tests
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
mkdir -p "reports/<suite>"
|
|
||||||
<testikomento>
|
|
||||||
|
|
||||||
- name: Report
|
|
||||||
if: always()
|
|
||||||
run: bash .ci/scripts/ci-report.sh "<kuvaus>" <context> <suite>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Taso 2: Jälkikäsittely tarvitaan
|
### Taso 2: Jälkikäsittely tarvitaan
|
||||||
|
|
||||||
Kun testi tuottaa raakadataa (stdout, coverage-tiedostot) joka pitää muuntaa tai siirtää
|
Kun testi tuottaa raakadataa (stdout, coverage-tiedostot) joka pitää muuntaa tai siirtää
|
||||||
`reports/<suite>/`-hakemistoon, käytetään Post-process-steppejä. **Jokainen operaatio
|
`reports/<suite>/`-hakemistoon. **Jokainen operaatio omassa stepissään** `if: always()`.
|
||||||
omassa stepissään** — älä koskaan niputa useaa post-process-komentoa samaan `run:`-blockiin:
|
|
||||||
|
|
||||||
```yaml
|
Tarkat YAML-mallit molemmista tasoista: `REFERENCE.md`.
|
||||||
- name: Run tests
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
mkdir -p "reports/<suite>"
|
|
||||||
<testikomento> > "reports/<suite>/results.txt" 2>&1
|
|
||||||
|
|
||||||
- name: Post-process coverage
|
|
||||||
if: always()
|
|
||||||
run: <siirrä coverage-data reports/<suite>/coverage/-hakemistoon>
|
|
||||||
|
|
||||||
- name: Post-process test report
|
|
||||||
if: always()
|
|
||||||
run: <HTML-generointi raa'asta outputista>
|
|
||||||
|
|
||||||
- name: Report
|
|
||||||
if: always()
|
|
||||||
run: bash .ci/scripts/ci-report.sh "<kuvaus>" <context> <suite>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Huomio subdir-sisällöstä:** Jos testi tuottaa dataa alihakemistoon (esim.
|
|
||||||
coverage `./coverage/`-kansioon), se pitää erikseen SIIRTÄÄ
|
|
||||||
`reports/<suite>/<subdir>/`-hakemistoon ennen `ci-report.sh`:n ajoa.
|
|
||||||
Ilman siirtoa sisältö ei näy index-sivulla, vaikka työkalu tuottaisi sen oikein.
|
|
||||||
Subdir vaatii lisäksi `index.html`:n, jotta `ci-report.sh` löytää sen.
|
|
||||||
|
|
||||||
**Miksi:** Gitea Actions käyttää `bash -e`-oletusta. Jos yksi post-process-komento
|
|
||||||
epäonnistuu (esim. `set -euo pipefail`-skripti), koko stepi pysähtyy eivätkä seuraavat
|
|
||||||
komennot käynnisty — raportti jää julkaisematta. Erilliset stepit `if: always()` takaavat
|
|
||||||
että jokainen post-process-vaihe ajetaan itsenäisesti.
|
|
||||||
|
|
||||||
### Monta raportoitavaa tiedostoa
|
|
||||||
|
|
||||||
Kun `reports/<suite>/`-hakemistossa on useita tiedostoja tai alihakemistoja,
|
|
||||||
`ci-report.sh` generoi automaattisesti `reports/<suite>/index.html` jos hakemistossa
|
|
||||||
on enemmän kuin yksi raportoitava item.
|
|
||||||
|
|
||||||
```
|
|
||||||
reports/<suite>/
|
|
||||||
├── results.txt ← testin stdout (skannataan FILES)
|
|
||||||
├── test-report.html ← generoitu HTML (skannataan FILES)
|
|
||||||
└── <mikä tahansa>/ ← alihakemisto (skannataan SUBDIRS)
|
|
||||||
└── index.html ← VAIN jos tämä on olemassa
|
|
||||||
```
|
|
||||||
|
|
||||||
**Subdir-sääntö:** Alihakemisto näkyy indexissä VAIN jos se sisältää `index.html`:n.
|
**Subdir-sääntö:** Alihakemisto näkyy indexissä VAIN jos se sisältää `index.html`:n.
|
||||||
Pelkkä tyhjä subdir ilman `index.html`:ää ei näy — tämä on yleisin syy miksi
|
|
||||||
jokin raportin osa (kuten coverage) puuttuu indexistä.
|
|
||||||
|
|
||||||
## 6. Raportin julkaisukelpoisuus
|
## 6. Nimeäminen
|
||||||
|
|
||||||
`ci-report.sh` päättää onko raportti julkaisukelpoinen skannaamalla
|
Tiedostonimet `.gitea/workflows/`-kansiossa noudattavat yhtenäistä rakennetta:
|
||||||
`reports/<suite>/`-hakemistoa.
|
|
||||||
|
|
||||||
### Mitä skannataan
|
|
||||||
|
|
||||||
| Mitä | Sääntö |
|
|
||||||
|---|---|
|
|
||||||
| **Tiedostot (FILES)** | Kaikki `reports/<suite>/`-juuressa olevat tiedostot paitsi `index.html` |
|
|
||||||
| **Alihakemistot (SUBDIRS)** | Vain ne, joissa on `index.html` |
|
|
||||||
|
|
||||||
### Julkaisukelpoisuus
|
|
||||||
|
|
||||||
| Tila | Seuraus |
|
|
||||||
|---|---|
|
|
||||||
| `FILES + SUBDIRS = 0` | **Failure** — `ci-report.sh` palauttaa virheen, raporttia ei julkaista |
|
|
||||||
| `FILES + SUBDIRS = 1` | Suora linkki itemiin — ei generoi index-sivua |
|
|
||||||
| `FILES + SUBDIRS > 1` | Generoi `reports/<suite>/index.html`-sivun, linkit kaikkiin itemeihin |
|
|
||||||
|
|
||||||
### Esimerkki: coverage-näkymä
|
|
||||||
|
|
||||||
```
|
|
||||||
reports/<suite>/coverage/index.html ← on olemassa
|
|
||||||
```
|
|
||||||
|
|
||||||
Coverage-dataa ei siirretä automaattisesti. Testin tai post-process-stepin pitää
|
|
||||||
siirtää coverage `reports/<suite>/coverage/`-hakemistoon ja varmistaa että
|
|
||||||
`index.html` on mukana. Sama periaate pätee mihin tahansa subdir-sisältöön.
|
|
||||||
|
|
||||||
## 7. Debug-ohje: raportti ei näy
|
|
||||||
|
|
||||||
### 1. Aja lokaalisti samalla komennolla kuin CI
|
|
||||||
|
|
||||||
Näet mitä tiedostoja syntyy, mihin ne tulevat ja mikä on työkalun exit-koodi.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Esimerkki
|
|
||||||
mkdir -p reports/bats
|
|
||||||
bashcov -- bats tests/ > reports/bats/results.txt 2>&1
|
|
||||||
echo "exit: $?"
|
|
||||||
ls -la reports/bats/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Lisää `echo "DEBUG: ..." >&2` ennen ja jälkeen kriittisen operaation
|
|
||||||
|
|
||||||
Debuggaus stderriin (`>&2`) näkyy CI-logissa eikä häiritse skriptin normaalia outputia.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "DEBUG: coverage exists? $([ -d coverage ] && echo YES || echo NO)" >&2
|
|
||||||
echo "DEBUG: target/index.html exists? $([ -f reports/suite/coverage/index.html ] && echo YES || echo NO)" >&2
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Tarkista kutsuparametrit
|
|
||||||
|
|
||||||
Yleisin virhe: skripti odottaa `$1` = X, mutta kutsuja antaa `$1` = Y ja `$2` = X.
|
|
||||||
Skripti lukee väärän parametrin ja etsii dataa väärästä paikasta.
|
|
||||||
|
|
||||||
### 4. Tarkista tiedostopolut
|
|
||||||
|
|
||||||
1. Onko lähdetiedosto olemassa ennen kopiointia?
|
|
||||||
2. Onko kohde olemassa kopioinnin jälkeen?
|
|
||||||
3. Onko `index.html` subdirissä (vaaditaan `ci-report.sh`:lle)?
|
|
||||||
|
|
||||||
Jos vastaus johonkin on "ei" — tiedät mikä pitää korjata.
|
|
||||||
|
|
||||||
### 5. Poista debug-echot kun ongelma on korjattu
|
|
||||||
|
|
||||||
Debug-rivit eivät kuulu tuotantoon.
|
|
||||||
|
|
||||||
### 6. Älä kokeile — debuggaa
|
|
||||||
|
|
||||||
Kokeilu = arvaus. Debuggaus = lisää echo, aja, lue logi, eristä ongelma.
|
|
||||||
Vasta sitten korjaa. Tämä on nopeampi tie oikeaan ratkaisuun.
|
|
||||||
|
|
||||||
## 8. Nimeäminen
|
|
||||||
|
|
||||||
Tiedostonimet `.gitea/workflows/`-kansiossa noudattavat yhtenäistä rakennetta, jotta
|
|
||||||
tiedostot löytyvät nopeasti ja niiden rooli on selvillä:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
<komponentti>.ci-feature.yml ← feature-haaran reititin
|
<komponentti>.ci-feature.yml ← feature-haaran reititin
|
||||||
@@ -355,216 +237,64 @@ tiedostot löytyvät nopeasti ja niiden rooli on selvillä:
|
|||||||
<komponentti>.gitea-env.conf ← komponenttikohtainen konfiguraatio
|
<komponentti>.gitea-env.conf ← komponenttikohtainen konfiguraatio
|
||||||
```
|
```
|
||||||
|
|
||||||
Single repossa `<komponentti>` jätetään pois — tiedostot ovat suoraan `ci-feature.yml`,
|
Single repossa `<komponentti>` jätetään pois.
|
||||||
`ci-main.yml`, `<testityyppi>.yml`, `ci-container-build-<kontti>.yml`.
|
Monorepossa prefiksi pitää komponentin tiedostot yhdessä.
|
||||||
|
|
||||||
Monorepossa prefiksi pitää komponentin tiedostot yhdessä: `ls <komponentti>.*` löytää kaikki
|
### 6.1 Commit status -nimeäminen
|
||||||
kerralla.
|
|
||||||
|
|
||||||
## 9. Artifact-kuri
|
`ci-report.sh`-kutsun `description` (2. argumentti) ja `context` (3. argumentti)
|
||||||
|
noudattavat seuraavaa kaavaa:
|
||||||
|
|
||||||
|
**Single repo:**
|
||||||
|
```
|
||||||
|
context: <testityyppi> (esim. unit-tests, acc-tests)
|
||||||
|
description: <Test type> test report (esim. Unit test report)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Monorepo:**
|
||||||
|
```
|
||||||
|
context: <komponentti>.<testityyppi> (esim. library.unit-tests)
|
||||||
|
description: <Komponentti>: <Test type> test report (esim. Library: Unit test report)
|
||||||
|
```
|
||||||
|
|
||||||
|
> Gitea YAML: `run:` laita lainausmerkeillä `run: |`-blockiin — Gitea ei tue lainausmerkkejä yhden rivin `run:`-komennoissa.
|
||||||
|
>
|
||||||
|
> ```yaml
|
||||||
|
> - name: Report
|
||||||
|
> if: always()
|
||||||
|
> run: |
|
||||||
|
> bash .ci/scripts/ci-report.sh "<Komponentti>: <Test type> test report" <komponentti>.<context> <suite> ${{ job.status }}
|
||||||
|
> ```
|
||||||
|
|
||||||
|
Build/push-status (Docker, Helm) on providerin hallussa — consumer ei vaikuta
|
||||||
|
niiden nimeämiseen.
|
||||||
|
|
||||||
|
## 7. Artifact-kuri
|
||||||
|
|
||||||
Gitea Actionsin `upload-artifact` jättää pysyvän tiedoston. Artifakteja ei käytetä
|
Gitea Actionsin `upload-artifact` jättää pysyvän tiedoston. Artifakteja ei käytetä
|
||||||
workflow_call:ien väliseen datan siirtoon ellei se ole teknisesti välttämätöntä.
|
`workflow_call`:ien väliseen datan siirtoon ellei se ole teknisesti välttämätöntä.
|
||||||
|
|
||||||
**Ensisijainen ratkaisu:** jokainen testi tuottaa tarvitsemansa datan itse. Ei
|
**Ensisijainen ratkaisu:** jokainen testi tuottaa tarvitsemansa datan itse. Ei
|
||||||
`upload-artifact` + `download-artifact` -riippuvuuksia.
|
`upload-artifact` + `download-artifact` -riippuvuuksia.
|
||||||
|
|
||||||
```yaml
|
## 8. Report-Summary — pakollinen jokaisen pipelinen lopuksi
|
||||||
# OIKEIN — molemmat testit tuottavat oman datansa
|
|
||||||
- name: Prepare data
|
|
||||||
run: <komento> > /tmp/data
|
|
||||||
- name: Validate data
|
|
||||||
run: <validointikomento> /tmp/data
|
|
||||||
```
|
|
||||||
|
|
||||||
**Miksi:**
|
Jokaisen reitittimen (oli se `ci-main.yml`, `ci-feature.yml` tai mikä tahansa) viimeinen job on `report-summary`.
|
||||||
- Testit pysyvät itsenäisinä — yhden testin fail ei estä muita
|
|
||||||
- Ei "artifact expired" -virheitä myöhemmin
|
|
||||||
- Ei pysyviä artifakteja siivoamatta
|
|
||||||
|
|
||||||
---
|
**Säännöt:**
|
||||||
|
- `needs:` sisältää **kaikki edeltävät jobit** — summary odottaa että kaikki on valmis (onnistui tai ei)
|
||||||
|
- `if: always()` — ajetaan aina, vaikka pipeline olisi keskeytetty tai joku jobi failannut
|
||||||
|
- `suites:` on välilyönnein eroteltu lista suiten nimistä (esim. `bats cucumber`). Tyhjä merkkijono sallittu jos testisuiteja ei ole.
|
||||||
|
- Provider (`report-summary.yml`) hoitaa summaryn logiikan — reititin vain kutsuu
|
||||||
|
|
||||||
## Konfiguraatiotiedosto (.gitea-env.conf)
|
**Miksi aina:**
|
||||||
|
- Gitea 1.27+ näyttää `GITHUB_STEP_SUMMARY`:n Actions UI:ssa. Ilman summarya pipeline näyttää epätäydelliseltä.
|
||||||
|
- Summaryyn voidaan myöhemmin lisätä muutakin kuin testilinkkejä (build-artefaktit, deploy-tiedot).
|
||||||
|
- Yhtenäinen rakenne jokaisessa pipeline-parissa vähentää kysymyksiä.
|
||||||
|
|
||||||
Tiedosto on `key=value`-muotoinen (kuten `.env`). Kommentit ja tyhjät rivit sallittuja.
|
YAML-malli: `REFERENCE.md`.
|
||||||
|
|
||||||
### Single repo
|
## 9. ADR-yhteenveto — consumerin kannalta oleelliset säännöt
|
||||||
|
|
||||||
```ini
|
|
||||||
# .gitea/workflows/gitea-env.conf
|
|
||||||
GITEA_API_URL=https://gitea.example.com
|
|
||||||
GIT_PAGES_URL=https://reports.example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker-artifaktin buildaavat projektit
|
|
||||||
|
|
||||||
```ini
|
|
||||||
DOCKER_REGISTRY=gitea.example.com/myorg
|
|
||||||
DOCKER_IMAGE_NAME=my-service
|
|
||||||
DOCKER_UI_URL=https://gitea.example.com/myorg/-/packages/container
|
|
||||||
#DOCKERFILE=Dockerfile.platform # valinnainen, oletus Dockerfile
|
|
||||||
```
|
|
||||||
|
|
||||||
`DOCKER_UI_URL` ei sisällä image-nimeä — se on puhdas container-registryn osoite.
|
|
||||||
Image-nimi lisätään automaattisesti URL:iin `docker-build-push.yml`:ssä.
|
|
||||||
|
|
||||||
### Salaisuudet (Gitea Settings → Secrets)
|
|
||||||
|
|
||||||
| Secret | Pakollinen |
|
|
||||||
|---|---|
|
|
||||||
| `GITEA_TOKEN` | Aina (Gitean sisäinen, automaattisesti saatavilla) |
|
|
||||||
| `GIT_PAGES_PUBLISH_TOKEN` | Aina |
|
|
||||||
| `DOCKER_USERNAME` | Vain jos buildaat kontteja |
|
|
||||||
| `DOCKER_PASSWORD` | Vain jos buildaat kontteja |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Monorepo
|
|
||||||
|
|
||||||
Monorepossa yhdessä repossa asuu useampi julkaistava komponentti. Jokaiselle komponentille
|
|
||||||
oma conf-tiedosto `.gitea/workflows/<komponentti>.gitea-env.conf`, jossa on kaikki
|
|
||||||
komponenttikohtainen tieto.
|
|
||||||
|
|
||||||
### Suositus: komponentit omiin juurihakemistoihin
|
|
||||||
|
|
||||||
On suositeltavaa sijoittaa jokaisen komponentin koko lähdekoodi omaan juuritason
|
|
||||||
hakemistoonsa (`api/`, `frontend/`, `shared/`). Tämä helpottaa `paths:`-filtteröintiä,
|
|
||||||
pitää komponentit selkeästi erillään, ja tekee repossa navigoinnista suoraviivaista.
|
|
||||||
Tämä on kuitenkin vain suositus — ei pakottava sääntö.
|
|
||||||
|
|
||||||
### Ongelmat ja ratkaisut
|
|
||||||
|
|
||||||
| Ongelma | Ratkaisu |
|
|
||||||
|---|---|
|
|
||||||
| Monta komponenttia, yksi repo — mikä triggeröi? | `paths:`-filtteri: `push: { paths: ['<komponentti>/**'] }` |
|
|
||||||
| Jokaisella komponentilla oma versio | `VERSION_FILE=<komponentti>/package.json` confissa |
|
|
||||||
| Git-tägit sekaisin ellei nimiavaruutta | `GIT_TAG_PREFIX=<komponentti>/` confissa → tägi `<komponentti>/1.2.3` |
|
|
||||||
| Eri julkaisutahdit | Riippumattomat CI-triggerit, omat versiopolut |
|
|
||||||
|
|
||||||
### Komponenttikohtainen conf
|
|
||||||
|
|
||||||
```ini
|
|
||||||
# .gitea/workflows/<komponentti>.gitea-env.conf
|
|
||||||
GITEA_API_URL=https://gitea.example.com
|
|
||||||
GIT_PAGES_URL=https://reports.example.com
|
|
||||||
DOCKER_REGISTRY=gitea.example.com/myorg
|
|
||||||
DOCKER_IMAGE_NAME=<image-nimi>
|
|
||||||
DOCKER_UI_URL=https://gitea.example.com/myorg/-/packages/container
|
|
||||||
GIT_TAG_PREFIX=<komponentti>/
|
|
||||||
# Jompikumpi — JSON (.version-kenttä) tai plain text:
|
|
||||||
VERSION_FILE=<komponentti>/package.json
|
|
||||||
#VERSION_FILE=<komponentti>/VERSION
|
|
||||||
```
|
|
||||||
|
|
||||||
### Monorepo reititin
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: CI <Komponentti> Main
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- '<komponentti>/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
load-config:
|
|
||||||
uses: <owner>/gitea-ci-library/.gitea/workflows/config-provider.yml@v1
|
|
||||||
secrets: inherit
|
|
||||||
with:
|
|
||||||
config_path: .gitea/workflows/<komponentti>.gitea-env.conf
|
|
||||||
|
|
||||||
check-version:
|
|
||||||
needs: [load-config]
|
|
||||||
uses: <owner>/gitea-ci-library/.gitea/workflows/check-version.yml@v1
|
|
||||||
secrets: inherit
|
|
||||||
with:
|
|
||||||
env_json: ${{ needs.load-config.outputs.env_json }}
|
|
||||||
|
|
||||||
<testit>:
|
|
||||||
needs: [load-config, check-version]
|
|
||||||
if: needs.check-version.outputs.artifact_exists != 'true'
|
|
||||||
uses: ./.gitea/workflows/<komponentti>.<testi>.yml
|
|
||||||
secrets: inherit
|
|
||||||
with:
|
|
||||||
env_json: ${{ needs.load-config.outputs.env_json }}
|
|
||||||
|
|
||||||
build-push:
|
|
||||||
needs: [load-config, check-version, <testit>]
|
|
||||||
if: needs.check-version.outputs.artifact_exists != 'true'
|
|
||||||
uses: <owner>/gitea-ci-library/.gitea/workflows/docker-build-push.yml@v1
|
|
||||||
secrets: inherit
|
|
||||||
with:
|
|
||||||
env_json: ${{ needs.load-config.outputs.env_json }}
|
|
||||||
version: ${{ needs.check-version.outputs.version }}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Version elinkaari per komponentti
|
|
||||||
|
|
||||||
`GIT_TAG_PREFIX` takaa että eri komponenttien versiohistoria pysyy erillään.
|
|
||||||
Git-tägi `<komponentti>/0.2.3` ei sekoitu toisen komponentin tägeihin.
|
|
||||||
|
|
||||||
`check-version.yml` suodattaa ja laskee seuraavan patchin vain kyseisen
|
|
||||||
komponentin etuliitteellä. Idempotenttius toimii komponenttikohtaisesti:
|
|
||||||
jos commitilla on jo tägi, pipeline skipataan `if: artifact_exists != 'true'`.
|
|
||||||
|
|
||||||
### Mitä EI kannata tehdä monorepossa
|
|
||||||
|
|
||||||
- Älä aja kaikkia komponentteja samasta triggeristä — `paths:` pitää CI:t erillisinä
|
|
||||||
- Älä käytä samaa versionhallintatiedostoa usealle komponentille
|
|
||||||
- Älä anna monorepo-parametreja pipeline-overrideina — kaikki kuuluu conf-tiedostoon
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Versionhallinta
|
|
||||||
|
|
||||||
`check-version.yml` lukee version automaattisesti prioriteettijärjestyksessä:
|
|
||||||
|
|
||||||
| # | Lähde | Formaatti |
|
|
||||||
|---|---|---|
|
|
||||||
| 1 | `VERSION_FILE` confissa | Määritelty polku |
|
|
||||||
| 2 | `VERSION`-tiedosto (root) | Plain text |
|
|
||||||
| 3 | `package.json` (root) | `.version`-kenttä |
|
|
||||||
| 4 | `pom.xml` (root) | `<version>`-elementti |
|
|
||||||
|
|
||||||
`major.minor` otetaan tästä. Patch lasketaan automaattisesti git-tageista.
|
|
||||||
Esim. `VERSION` = `0.2`, tagit = `0.2.0`, `0.2.1` → seuraava `0.2.2`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Branch protection (PR-gate)
|
|
||||||
|
|
||||||
Gitean Settings → Branches → Add Rule:
|
|
||||||
|
|
||||||
- **Branch:** `main`
|
|
||||||
- **Enable Require Status Checks:** päälle
|
|
||||||
- **Status checks:** valitse testijobien nimet
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Provider-rajapinnat — referenssi
|
|
||||||
|
|
||||||
### Workflowt
|
|
||||||
|
|
||||||
| Workflow | Käyttötarkoitus |
|
|
||||||
|---|---|
|
|
||||||
| `config-provider.yml` | Lataa + validoi `.conf`, tuottaa `env_json` |
|
|
||||||
| `check-version.yml` | Tarkistaa onko commit buildattu, laskee version |
|
|
||||||
| `docker-build-push.yml` | Buildaa + puskea Docker-imagen, tagittaa commitin |
|
|
||||||
| `report-summary.yml` | `GITHUB_STEP_SUMMARY`-taulukko raporttilinkeillä (Gitea 1.27+) |
|
|
||||||
|
|
||||||
### Skriptit (kutsutaan `.ci/scripts/`-polun kautta)
|
|
||||||
|
|
||||||
| Skripti | Käyttötarkoitus |
|
|
||||||
|---|---|
|
|
||||||
| `ci-report.sh` | Yhdistetty raportointi: julkaisee git-pagesiin ja asettaa commit-statuksen. Korvaa erilliset `publish-git-pages.sh` + `report-status.sh` -kutsut. Käyttö: `bash .ci/scripts/ci-report.sh "<kuvaus>" <context> <suite>` |
|
|
||||||
| `report-status.sh` | POSTaa commit-statuksen linkillä (kutsutaan `ci-report.sh`:n sisältä) |
|
|
||||||
| `publish-git-pages.sh` | Julkaisee raporttihakemiston git-pagesiin (kutsutaan `ci-report.sh`:n sisältä) |
|
|
||||||
| `ci-validate.sh` | Validoi `.conf`-tiedoston (kutsutaan `config-provider.yml`:stä) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ADR-yhteenveto — consumerin kannalta oleelliset säännöt
|
|
||||||
|
|
||||||
### Reititin ei sisällä suorittavaa koodia (ADR 0010)
|
### Reititin ei sisällä suorittavaa koodia (ADR 0010)
|
||||||
|
|
||||||
@@ -584,26 +314,231 @@ sisäiseen dogfood-käyttöön. Breaking changet kielletty — `v1`-rajapinta on
|
|||||||
### Paikalliset `uses:` eivät käytä refiä
|
### Paikalliset `uses:` eivät käytä refiä
|
||||||
|
|
||||||
Gitea act runner v1.0.8 muodostaa paikallisista `uses: ./.gitea/workflows/*.yml@main`-viittauksista
|
Gitea act runner v1.0.8 muodostaa paikallisista `uses: ./.gitea/workflows/*.yml@main`-viittauksista
|
||||||
epävalidin git-refin `main@<sha>`, joka aiheuttaa virheen `Revision invalid : reference must
|
epävalidin git-refin `main@<sha>`.
|
||||||
be defined once at the beginning`.
|
|
||||||
|
|
||||||
Paikallisista `uses:`-direktiiveistä EI koskaan käytetä `@main`- tai muuta ref-päätettä:
|
Paikallisista `uses:`-direktiiveistä EI koskaan käytetä `@main`- tai muuta ref-päätettä:
|
||||||
- `uses: ./.gitea/workflows/chart.helm-lint.yml` ← oikein
|
- `uses: ./.gitea/workflows/chart.helm-lint.yml` ← oikein
|
||||||
- `uses: ./.gitea/workflows/chart.helm-lint.yml@main` ← väärin
|
- `uses: ./.gitea/workflows/chart.helm-lint.yml@main` ← väärin
|
||||||
|
|
||||||
Ilman refiä runner käyttää workflow'ta triggeröivästä commitista. Ulkoisten repojen
|
Ilman refiä runner käyttää workflow'ta triggeröivästä commitista.
|
||||||
viittauksissa (`niko/...@v1`) pääte pysyy. Nämä resolvoidaan eri reittiä ja toimivat oikein.
|
|
||||||
|
|
||||||
### Exit-koodi on ainoa onnistumisen mittari (ADR 0008)
|
### Exit-koodi on ainoa onnistumisen mittari (ADR 0008)
|
||||||
|
|
||||||
Ei pipeä (`|`) komennon perässä — se syö exit-koodin. Käytä redirectiä (`> file 2>&1`).
|
Ei pipeä (`|`) komennon perässä — se syö exit-koodin. Käytä redirectiä (`> file 2>&1`).
|
||||||
|
|
||||||
### Commit-status vain raporttilinkille (ADR 0007)
|
|
||||||
|
|
||||||
`ci-report.sh`-skriptiä käytetään VAIN kun on raportti linkitettäväksi.
|
|
||||||
Tool-jobit (build, deploy) luottavat Gitean natiiviin job-statukseen.
|
|
||||||
|
|
||||||
### Providerin checkout ei kuulu consumerille
|
### Providerin checkout ei kuulu consumerille
|
||||||
|
|
||||||
Providerin scriptit haetaan `actions/checkout`-stepillä `.ci/`-polkuun.
|
Providerin scriptit haetaan `actions/checkout`-stepillä `.ci/`-polkuun.
|
||||||
Consumer ei kopioi eikä muokkaa providerin tiedostoja.
|
Consumer ei kopioi eikä muokkaa providerin tiedostoja.
|
||||||
|
|
||||||
|
## 10. Build & Push -providerit
|
||||||
|
|
||||||
|
### `docker-build-push.yml` — Docker image build & push
|
||||||
|
|
||||||
|
Buildaa ja pushee Docker-imagen OCI-registryyn. Ajaa suoraan runnerilla
|
||||||
|
(ei `container:`-direktiiviä), joten `actions/checkout` toimii natiivisti.
|
||||||
|
|
||||||
|
**`env_json`-avaimet (pakolliset):**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
DOCKER_REGISTRY: gitea.app.keskikuja.site/niko
|
||||||
|
DOCKER_IMAGE_NAME: my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
**Käyttö reitittimessä:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
docker-build-push:
|
||||||
|
uses: OWNER/gitea-ci-library/.gitea/workflows/docker-build-push.yml@v1
|
||||||
|
needs: [check-version]
|
||||||
|
if: needs.check-version.outputs.artifact_exists == 'false'
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
version: ${{ needs.check-version.outputs.version }}
|
||||||
|
```
|
||||||
|
|
||||||
|
Tarkka input/secret-lista: `docs/workflows.md`.
|
||||||
|
|
||||||
|
### `helm-build-push.yml` — Helm chart build & push
|
||||||
|
|
||||||
|
Pakkaa ja pushee Helm-chartin OCI-registryyn. Käyttää `alpine/helm`-konttia.
|
||||||
|
|
||||||
|
**`env_json`-avaimet (pakolliset):**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
HELM_REGISTRY: gitea.app.keskikuja.site/niko
|
||||||
|
VERSION_FILE: platform-helm/Chart.yaml # chart-hakemisto + versionlähde
|
||||||
|
```
|
||||||
|
|
||||||
|
**Käyttö reitittimessä:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
helm-build-push:
|
||||||
|
uses: OWNER/gitea-ci-library/.gitea/workflows/helm-build-push.yml@v1
|
||||||
|
needs: [check-version]
|
||||||
|
if: needs.check-version.outputs.artifact_exists == 'false'
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
version: ${{ needs.check-version.outputs.version }}
|
||||||
|
```
|
||||||
|
|
||||||
|
Chart-hakemisto johdetaan `VERSION_FILE`-polusta: `dirname "${VERSION_FILE}"`.
|
||||||
|
Jos `VERSION_FILE` on `Chart.yaml`, konteksti on juuri. Jos `platform-helm/Chart.yaml`,
|
||||||
|
konteksti on `platform-helm/`.
|
||||||
|
|
||||||
|
**Yksittäisten Helm-UI-linkkien raportointi:** `HELM_UI_URL` on
|
||||||
|
tarkoitettu yleiselle registry UI:lle — provider muodostaa linkin
|
||||||
|
`${HELM_UI_URL}/${CHART_NAME}/${VERSION}` automaattisesti.
|
||||||
|
|
||||||
|
Tarkka input/secret-lista: `docs/workflows.md`.
|
||||||
|
|
||||||
|
## 11. Multi-artifact monorepo -komponentti
|
||||||
|
|
||||||
|
Yksi monorepo-komponentti voi tuottaa useita artefakteja (esim. Docker image
|
||||||
|
+ Helm chart). Kukin artefakti on **omassa reitittimessään** — ei yhtä
|
||||||
|
monoliittista pipelinea. Tämä on tietoinen arkkitehtuurivalinta:
|
||||||
|
|
||||||
|
- Reitittimet ovat itsenäisiä: eri `paths:`-triggerit, eri tagit, eri confit
|
||||||
|
- Yksi commit voi triggeröidä molemmat rinnakkain
|
||||||
|
- Yhden artefaktin build tai testi ei estä toista
|
||||||
|
|
||||||
|
### Esimerkki: `platform-helm` joka tuottaa Docker-imagen ja Helm chartin
|
||||||
|
|
||||||
|
```
|
||||||
|
.gitea/workflows/
|
||||||
|
├── platform-helm.ci-main.yml # Docker build & push
|
||||||
|
├── platform-helm.gitea-env.conf # Docker-konffi
|
||||||
|
├── platform-helm.helm-ci-main.yml # Helm build & push
|
||||||
|
├── platform-helm.helm-gitea-env.conf # Helm-konffi
|
||||||
|
├── platform-helm.helm-chart-lint.yml # Chart-testi
|
||||||
|
└── platform-helm.ci-container-build-helm.yml # CI-kontin build
|
||||||
|
```
|
||||||
|
|
||||||
|
### `platform-helm.gitea-env.conf` (Docker)
|
||||||
|
|
||||||
|
```ini
|
||||||
|
DOCKER_REGISTRY=gitea.app.keskikuja.site/niko
|
||||||
|
DOCKER_IMAGE_NAME=platform-helm
|
||||||
|
GIT_TAG_PREFIX=platform-helm/
|
||||||
|
```
|
||||||
|
|
||||||
|
### `platform-helm.helm-gitea-env.conf` (Helm)
|
||||||
|
|
||||||
|
```ini
|
||||||
|
HELM_REGISTRY=gitea.app.keskikuja.site/niko
|
||||||
|
VERSION_FILE=platform-helm/Chart.yaml
|
||||||
|
GIT_TAG_PREFIX=chart/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reitittimet
|
||||||
|
|
||||||
|
**`platform-helm.ci-main.yml`** — Docker-buildi, testit, oma tagi:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: platform-helm CI Main
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- platform-helm/**
|
||||||
|
- .gitea/workflows/platform-helm.*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
load-config:
|
||||||
|
uses: OWNER/gitea-ci-library/.gitea/workflows/config-provider.yml@v1
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
config_path: .gitea/workflows/platform-helm.gitea-env.conf
|
||||||
|
|
||||||
|
check-version:
|
||||||
|
needs: [load-config]
|
||||||
|
uses: OWNER/gitea-ci-library/.gitea/workflows/check-version.yml@v1
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
|
||||||
|
test:
|
||||||
|
needs: [load-config, check-version]
|
||||||
|
uses: ./.gitea/workflows/platform-helm.sbom-lint.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
|
||||||
|
build-push:
|
||||||
|
needs: [load-config, check-version, test]
|
||||||
|
if: needs.check-version.outputs.artifact_exists == 'false'
|
||||||
|
uses: OWNER/gitea-ci-library/.gitea/workflows/docker-build-push.yml@v1
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
version: ${{ needs.check-version.outputs.version }}
|
||||||
|
|
||||||
|
report-summary:
|
||||||
|
needs: [load-config, test, build-push]
|
||||||
|
if: always()
|
||||||
|
uses: OWNER/gitea-ci-library/.gitea/workflows/report-summary.yml@v1
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
suites: ''
|
||||||
|
```
|
||||||
|
|
||||||
|
**`platform-helm.helm-ci-main.yml`** — Helm-buildi, chart-testi, oma tagi:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: platform-helm Helm CI Main
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- platform-helm/**
|
||||||
|
- .gitea/workflows/platform-helm.helm*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
load-config:
|
||||||
|
uses: OWNER/gitea-ci-library/.gitea/workflows/config-provider.yml@v1
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
config_path: .gitea/workflows/platform-helm.helm-gitea-env.conf
|
||||||
|
|
||||||
|
check-version:
|
||||||
|
needs: [load-config]
|
||||||
|
uses: OWNER/gitea-ci-library/.gitea/workflows/check-version.yml@v1
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
|
||||||
|
chart-lint:
|
||||||
|
needs: [load-config, check-version]
|
||||||
|
uses: ./.gitea/workflows/platform-helm.helm-chart-lint.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
|
||||||
|
helm-build-push:
|
||||||
|
needs: [load-config, check-version, chart-lint]
|
||||||
|
if: needs.check-version.outputs.artifact_exists == 'false'
|
||||||
|
uses: OWNER/gitea-ci-library/.gitea/workflows/helm-build-push.yml@v1
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
version: ${{ needs.check-version.outputs.version }}
|
||||||
|
|
||||||
|
report-summary:
|
||||||
|
needs: [load-config, chart-lint, helm-build-push]
|
||||||
|
if: always()
|
||||||
|
uses: OWNER/gitea-ci-library/.gitea/workflows/report-summary.yml@v1
|
||||||
|
with:
|
||||||
|
env_json: ${{ needs.load-config.outputs.env_json }}
|
||||||
|
suites: ''
|
||||||
|
```
|
||||||
|
|
||||||
|
### Säännöt
|
||||||
|
|
||||||
|
- Jokaisella artefaktilla on oma reititin, oma conf, omat testit
|
||||||
|
- Conf-tiedoston nimi erottaa artefaktit: `<komponentti>.gitea-env.conf` vs
|
||||||
|
`<komponentti>.helm-gitea-env.conf`
|
||||||
|
- `<komponentti>.helm-`-prefiksi erottaa Helm-artefaktin tiedostot
|
||||||
|
- `GIT_TAG_PREFIX` pitää tagit erillään: `platform-helm/1.2.3` vs `chart/1.2.3`
|
||||||
|
- Molemmat reitittimet voivat triggeröityä samasta commitista
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
source "$BATS_TEST_DIRNAME/helpers/mock-api.sh"
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
export GITEA_TOKEN=test-token
|
||||||
|
export GIT_TAG_PREFIX=""
|
||||||
|
export SERVER_URL="http://localhost:18080"
|
||||||
|
export REPO="niko/test"
|
||||||
|
export SHA="abc123"
|
||||||
|
rm -rf /tmp/build-ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
mock_stop 2>/dev/null || true
|
||||||
|
rm -rf /tmp/build-ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "VERSION_FILE=Chart.yaml extracts version from YAML" {
|
||||||
|
mock_set_sequence '[{"code": 200, "body": []}]'
|
||||||
|
mock_start
|
||||||
|
|
||||||
|
export VERSION_FILE="$BATS_TEST_DIRNAME/fixtures/check-version/Chart.yaml"
|
||||||
|
run bash scripts/check-version.sh
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
source /tmp/build-ctx/build.env
|
||||||
|
[ "$ARTIFACT_EXISTS" = "false" ]
|
||||||
|
[ "$NEXT_VERSION" = "0.3.0" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "VERSION_FILE=VERSION extracts version from plain text" {
|
||||||
|
mock_set_sequence '[{"code": 200, "body": []}]'
|
||||||
|
mock_start
|
||||||
|
|
||||||
|
export VERSION_FILE="$BATS_TEST_DIRNAME/fixtures/check-version/VERSION"
|
||||||
|
run bash scripts/check-version.sh
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
source /tmp/build-ctx/build.env
|
||||||
|
[ "$ARTIFACT_EXISTS" = "false" ]
|
||||||
|
[ "$NEXT_VERSION" = "0.3.0" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "VERSION_FILE=package.json extracts version from JSON" {
|
||||||
|
mock_set_sequence '[{"code": 200, "body": []}]'
|
||||||
|
mock_start
|
||||||
|
|
||||||
|
export VERSION_FILE="$BATS_TEST_DIRNAME/fixtures/check-version/package.json"
|
||||||
|
run bash scripts/check-version.sh
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
source /tmp/build-ctx/build.env
|
||||||
|
[ "$ARTIFACT_EXISTS" = "false" ]
|
||||||
|
[ "$NEXT_VERSION" = "0.3.0" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "VERSION_FILE=subdir/Chart.yaml extracts version from monorepo" {
|
||||||
|
mock_set_sequence '[{"code": 200, "body": []}]'
|
||||||
|
mock_start
|
||||||
|
|
||||||
|
export VERSION_FILE="$BATS_TEST_DIRNAME/fixtures/check-version/subdir/Chart.yaml"
|
||||||
|
run bash scripts/check-version.sh
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
source /tmp/build-ctx/build.env
|
||||||
|
[ "$ARTIFACT_EXISTS" = "false" ]
|
||||||
|
[ "$NEXT_VERSION" = "0.4.0" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "no VERSION_FILE, root VERSION found" {
|
||||||
|
mock_set_sequence '[{"code": 200, "body": []}]'
|
||||||
|
mock_start
|
||||||
|
|
||||||
|
WORKDIR=$(mktemp -d)
|
||||||
|
cp "$BATS_TEST_DIRNAME/fixtures/check-version/VERSION" "$WORKDIR/VERSION"
|
||||||
|
|
||||||
|
SCRIPT="$PWD/scripts/check-version.sh"
|
||||||
|
run bash -c "cd '$WORKDIR' && exec bash '$SCRIPT'"
|
||||||
|
|
||||||
|
rm -rf "$WORKDIR"
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
source /tmp/build-ctx/build.env
|
||||||
|
[ "$NEXT_VERSION" = "0.3.0" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "no VERSION_FILE, root Chart.yaml found" {
|
||||||
|
mock_set_sequence '[{"code": 200, "body": []}]'
|
||||||
|
mock_start
|
||||||
|
|
||||||
|
WORKDIR=$(mktemp -d)
|
||||||
|
cp "$BATS_TEST_DIRNAME/fixtures/check-version/Chart.yaml" "$WORKDIR/Chart.yaml"
|
||||||
|
|
||||||
|
SCRIPT="$PWD/scripts/check-version.sh"
|
||||||
|
run bash -c "cd '$WORKDIR' && exec bash '$SCRIPT'"
|
||||||
|
|
||||||
|
rm -rf "$WORKDIR"
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
source /tmp/build-ctx/build.env
|
||||||
|
[ "$NEXT_VERSION" = "0.3.0" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "tag exists for commit sets ARTIFACT_EXISTS=true" {
|
||||||
|
mock_set_sequence '[{"code": 200, "body": [{"name": "0.3.0", "commit": {"sha": "abc123"}}]}]'
|
||||||
|
mock_start
|
||||||
|
|
||||||
|
export VERSION_FILE="$BATS_TEST_DIRNAME/fixtures/check-version/VERSION"
|
||||||
|
run bash scripts/check-version.sh
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
source /tmp/build-ctx/build.env
|
||||||
|
[ "$ARTIFACT_EXISTS" = "true" ]
|
||||||
|
[ "$NEXT_VERSION" = "0.3.0" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "tag with prefix filters correctly" {
|
||||||
|
mock_set_sequence '[{"code": 200, "body": [{"name": "git-pages/0.3.0", "commit": {"sha": "abc123"}}, {"name": "docker/0.3.0", "commit": {"sha": "abc123"}}]}]'
|
||||||
|
mock_start
|
||||||
|
|
||||||
|
export GIT_TAG_PREFIX="git-pages/"
|
||||||
|
export VERSION_FILE="$BATS_TEST_DIRNAME/fixtures/check-version/VERSION"
|
||||||
|
run bash scripts/check-version.sh
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
source /tmp/build-ctx/build.env
|
||||||
|
[ "$ARTIFACT_EXISTS" = "true" ]
|
||||||
|
[ "$NEXT_VERSION" = "git-pages/0.3.0" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "no tag, new version calculated" {
|
||||||
|
mock_set_sequence '[{"code": 200, "body": []}]'
|
||||||
|
mock_start
|
||||||
|
|
||||||
|
export VERSION_FILE="$BATS_TEST_DIRNAME/fixtures/check-version/VERSION"
|
||||||
|
run bash scripts/check-version.sh
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
source /tmp/build-ctx/build.env
|
||||||
|
[ "$ARTIFACT_EXISTS" = "false" ]
|
||||||
|
[ "$NEXT_VERSION" = "0.3.0" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "highest patch calculated correctly" {
|
||||||
|
mock_set_sequence '[{"code": 200, "body": [{"name": "0.3.0", "commit": {"sha": "def456"}}, {"name": "0.3.1", "commit": {"sha": "def456"}}]}]'
|
||||||
|
mock_start
|
||||||
|
|
||||||
|
export VERSION_FILE="$BATS_TEST_DIRNAME/fixtures/check-version/VERSION"
|
||||||
|
run bash scripts/check-version.sh
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
source /tmp/build-ctx/build.env
|
||||||
|
[ "$ARTIFACT_EXISTS" = "false" ]
|
||||||
|
[ "$NEXT_VERSION" = "0.3.2" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "VERSION_FILE=Chart-umbrella.yaml extracts only top-level version" {
|
||||||
|
mock_set_sequence '[{"code": 200, "body": []}]'
|
||||||
|
mock_start
|
||||||
|
|
||||||
|
export VERSION_FILE="$BATS_TEST_DIRNAME/fixtures/check-version/Chart-umbrella.yaml"
|
||||||
|
run bash scripts/check-version.sh
|
||||||
|
|
||||||
|
echo "STATUS=$status"
|
||||||
|
echo "OUTPUT=$output"
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
source /tmp/build-ctx/build.env
|
||||||
|
echo "NEXT_VERSION=$NEXT_VERSION"
|
||||||
|
[ "$NEXT_VERSION" = "0.1.0" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "no version source exits with error" {
|
||||||
|
mock_set_sequence '[{"code": 200, "body": []}]'
|
||||||
|
mock_start
|
||||||
|
|
||||||
|
WORKDIR=$(mktemp -d)
|
||||||
|
SCRIPT="$PWD/scripts/check-version.sh"
|
||||||
|
run bash -c "cd '$WORKDIR' && exec bash '$SCRIPT'"
|
||||||
|
|
||||||
|
rm -rf "$WORKDIR"
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
[[ "$output" == *"ERROR"* ]]
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
setup() {
|
setup() {
|
||||||
source tests/helpers/mock-api.sh
|
source tests/helpers/mock-api.sh
|
||||||
export DISPATCH_POLL_INTERVAL="0.1"
|
export DISPATCH_POLL_INTERVAL="0.1"
|
||||||
|
export DISPATCH_ID="test123"
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown() {
|
teardown() {
|
||||||
@@ -12,8 +13,7 @@ teardown() {
|
|||||||
@test "dispatch succeeds: POST 201, poll running x3 then success → exit 0" {
|
@test "dispatch succeeds: POST 201, poll running x3 then success → exit 0" {
|
||||||
mock_set_sequence '[
|
mock_set_sequence '[
|
||||||
{"code":201},
|
{"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":"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":"completed","conclusion":"success"}}
|
{"code":200,"body":{"id":1,"status":"completed","conclusion":"success"}}
|
||||||
@@ -26,7 +26,7 @@ teardown() {
|
|||||||
@test "dispatch: poll returns failure conclusion → exit 1" {
|
@test "dispatch: poll returns failure conclusion → exit 1" {
|
||||||
mock_set_sequence '[
|
mock_set_sequence '[
|
||||||
{"code":201},
|
{"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":"running"}},
|
||||||
{"code":200,"body":{"id":1,"status":"completed","conclusion":"failure"}}
|
{"code":200,"body":{"id":1,"status":"completed","conclusion":"failure"}}
|
||||||
]'
|
]'
|
||||||
@@ -38,7 +38,7 @@ teardown() {
|
|||||||
@test "dispatch: poll returns cancelled conclusion → exit 1" {
|
@test "dispatch: poll returns cancelled conclusion → exit 1" {
|
||||||
mock_set_sequence '[
|
mock_set_sequence '[
|
||||||
{"code":201},
|
{"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":"running"}},
|
||||||
{"code":200,"body":{"id":1,"status":"completed","conclusion":"cancelled"}}
|
{"code":200,"body":{"id":1,"status":"completed","conclusion":"cancelled"}}
|
||||||
]'
|
]'
|
||||||
@@ -47,18 +47,18 @@ teardown() {
|
|||||||
[ "$status" -eq 1 ]
|
[ "$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 '[
|
mock_set_sequence '[
|
||||||
{"code":201},
|
{"code":201},
|
||||||
{"code":200,"body":{"workflow_runs":[{"id":1,"status":"running"}]}},
|
{"code":200,"body":{"workflow_runs":[]}},
|
||||||
{"code":200,"body":{"id":1,"status":"running"}},
|
{"code":200,"body":{"workflow_runs":[]}},
|
||||||
{"code":200,"body":{"id":1,"status":"running"}},
|
{"code":200,"body":{"workflow_runs":[]}},
|
||||||
{"code":200,"body":{"id":1,"status":"running"}},
|
{"code":200,"body":{"workflow_runs":[]}},
|
||||||
{"code":200,"body":{"id":1,"status":"running"}},
|
{"code":200,"body":{"workflow_runs":[]}},
|
||||||
{"code":200,"body":{"id":1,"status":"running"}},
|
{"code":200,"body":{"workflow_runs":[]}},
|
||||||
{"code":200,"body":{"id":1,"status":"running"}},
|
{"code":200,"body":{"workflow_runs":[]}},
|
||||||
{"code":200,"body":{"id":1,"status":"running"}},
|
{"code":200,"body":{"workflow_runs":[]}},
|
||||||
{"code":200,"body":{"id":1,"status":"running"}}
|
{"code":200,"body":{"workflow_runs":[]}}
|
||||||
]'
|
]'
|
||||||
mock_start
|
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"
|
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" {
|
@test "POST dispatch is called with correct URL and payload" {
|
||||||
mock_set_sequence '[
|
mock_set_sequence '[
|
||||||
{"code":201},
|
{"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"}}
|
{"code":200,"body":{"id":1,"status":"completed","conclusion":"success"}}
|
||||||
]'
|
]'
|
||||||
mock_start
|
mock_start
|
||||||
@@ -91,6 +91,7 @@ teardown() {
|
|||||||
[[ "$body" == *'"ref":"main"'* ]]
|
[[ "$body" == *'"ref":"main"'* ]]
|
||||||
[[ "$body" == *'"inputs"'* ]]
|
[[ "$body" == *'"inputs"'* ]]
|
||||||
[[ "$body" == *'"version":"1.2.3"'* ]]
|
[[ "$body" == *'"version":"1.2.3"'* ]]
|
||||||
|
[[ "$body" == *'"dispatch_id":"test123"'* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "missing gitea_api_url argument → exit 1 with error message" {
|
@test "missing gitea_api_url argument → exit 1 with error message" {
|
||||||
@@ -120,15 +121,15 @@ teardown() {
|
|||||||
[ "$status" -eq 1 ]
|
[ "$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 '[
|
mock_set_sequence '[
|
||||||
{"code":201},
|
{"code":201},
|
||||||
{"code":200,"body":{"workflow_runs":[]}}
|
{"code":200,"body":{"workflow_runs":[]}}
|
||||||
]'
|
]'
|
||||||
mock_start
|
mock_start
|
||||||
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" '{}' "http://localhost:18080" "test-token-abc123"
|
run bash scripts/dispatch-workflow.sh "test-owner/test-repo" "test.yml" "main" '{}' "http://localhost:18080" "test-token-abc123" "0.001"
|
||||||
[ "$status" -eq 1 ]
|
[ "$status" -eq 124 ]
|
||||||
[[ "$output" == *"ERROR"* ]]
|
[[ "$output" == *"ERROR"* || "$output" == *"Timeout"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "missing inputs_json argument → exit 1" {
|
@test "missing inputs_json argument → exit 1" {
|
||||||
|
|||||||
@@ -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',
|
encoding: 'utf-8',
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
});
|
});
|
||||||
return { status: 0, stdout: out };
|
return { status: 0, stdout: out, stderr: '' };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { status: e.status, stdout: e.stdout || '', stderr: e.stderr || '' };
|
return { status: e.status, stdout: e.stdout || '', stderr: e.stderr || '' };
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ function setupMock(seqJson) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function runDispatch(args) {
|
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 () {
|
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 () {
|
When('a test workflow is dispatched to a test project', function () {
|
||||||
setupMock(JSON.stringify([
|
setupMock(JSON.stringify([
|
||||||
{ code: 201 },
|
{ 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' } },
|
{ 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"');
|
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 () {
|
When('a test workflow is dispatched and the tests fail', function () {
|
||||||
setupMock(JSON.stringify([
|
setupMock(JSON.stringify([
|
||||||
{ code: 201 },
|
{ 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' } },
|
{ 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"');
|
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 () {
|
When('a test workflow is dispatched but does not finish within the allowed time', function () {
|
||||||
setupMock(JSON.stringify([
|
setupMock(JSON.stringify([
|
||||||
{ code: 201 },
|
{ code: 201 },
|
||||||
{ code: 200, body: { workflow_runs: [{ id: 1, status: 'running' }] } },
|
{ code: 200, body: { workflow_runs: [] } },
|
||||||
{ code: 200, body: { id: 1, status: 'running' } },
|
{ code: 200, body: { workflow_runs: [] } },
|
||||||
{ code: 200, body: { id: 1, status: 'running' } },
|
{ code: 200, body: { workflow_runs: [] } },
|
||||||
{ code: 200, body: { id: 1, status: 'running' } },
|
{ 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.dispatchResult = r.status;
|
||||||
|
this.dispatchStderr = r.stderr;
|
||||||
});
|
});
|
||||||
|
|
||||||
Then('the calling pipeline reports a timeout error', function () {
|
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)}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: agent-platform
|
||||||
|
description: Agent Platform umbrella chart
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
|
dependencies:
|
||||||
|
- name: vikunja
|
||||||
|
version: "0.1.0"
|
||||||
|
repository: oci://registry.example.com
|
||||||
|
- name: langfuse
|
||||||
|
version: "0.2.0"
|
||||||
|
repository: oci://registry.example.com
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: test-chart
|
||||||
|
description: Test chart for version extraction
|
||||||
|
type: application
|
||||||
|
version: 0.3.0
|
||||||
|
appVersion: "1.0.0"
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
0.3.0
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version": "0.3.0"}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
<project><version>0.3.0</version></project>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: subdir-chart
|
||||||
|
description: Chart in subdirectory for monorepo testing
|
||||||
|
type: application
|
||||||
|
version: 0.4.0
|
||||||
|
appVersion: "1.0.0"
|
||||||
@@ -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"* ]]
|
||||||
|
}
|
||||||
Binary file not shown.
Executable
+40
@@ -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
|
||||||
@@ -12,8 +12,10 @@ MOCK_CONFIG_FILE=""
|
|||||||
_kill_port() {
|
_kill_port() {
|
||||||
local pids
|
local pids
|
||||||
pids=$(lsof -ti ":$MOCK_PORT" 2>/dev/null) || true
|
pids=$(lsof -ti ":$MOCK_PORT" 2>/dev/null) || true
|
||||||
[ -n "$pids" ] && kill -9 $pids 2>/dev/null || true
|
if [ -n "$pids" ]; then
|
||||||
|
kill -9 $pids 2>/dev/null || true
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
_wait_port_free() {
|
_wait_port_free() {
|
||||||
@@ -26,7 +28,7 @@ _wait_port_free() {
|
|||||||
|
|
||||||
_wait_port_ready() {
|
_wait_port_ready() {
|
||||||
local i=0
|
local i=0
|
||||||
while ! lsof -ti ":$MOCK_PORT" >/dev/null 2>&1 && [ $i -lt 5 ]; do
|
while ! lsof -ti ":$MOCK_PORT" >/dev/null 2>&1 && [ $i -lt 30 ]; do
|
||||||
sleep 0.2
|
sleep 0.2
|
||||||
i=$((i + 1))
|
i=$((i + 1))
|
||||||
done
|
done
|
||||||
@@ -44,7 +46,7 @@ mock_clear_sequence() {
|
|||||||
|
|
||||||
mock_start() {
|
mock_start() {
|
||||||
MOCK_RESPONSE_CODE="${MOCK_RESPONSE_CODE:-201}"
|
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"
|
echo "$MOCK_REQUEST_FILE" > "$MOCK_STATE_FILE"
|
||||||
MOCK_CONFIG_FILE=$(mktemp)
|
MOCK_CONFIG_FILE=$(mktemp)
|
||||||
|
|
||||||
|
|||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "yq $*" >> "${YQ_CALLS_FILE:-/dev/null}"
|
||||||
Reference in New Issue
Block a user