commit f4aca2b3c38dcc1788fcb6db2d9126077754009b Author: Scaffolder Date: Mon Apr 20 15:35:36 2026 +0000 initial commit Change-Id: Iae84a37d1caf14b15ed3ce78f5c7088886af4682 diff --git a/.gitea/actions/platform-check/action.yml b/.gitea/actions/platform-check/action.yml new file mode 100644 index 0000000..12a45b8 --- /dev/null +++ b/.gitea/actions/platform-check/action.yml @@ -0,0 +1,78 @@ +name: 'platform-check' +description: 'Validates catalog-info.yaml conformance and platform branch initialization' + +runs: + using: composite + steps: + - name: Ensure PyYAML + shell: bash + run: python3 -c "import yaml" 2>/dev/null || pip3 install pyyaml -q + + - name: Platform conformance check + shell: bash + run: | + FAIL=0 + echo "══════════════════════════════════════════════════" + echo " Platform Conformance Check" + echo "══════════════════════════════════════════════════" + + # ── Required platform files ─────────────────────────── + for f in catalog-info.yaml Dockerfile docs; do + if [[ -e "$f" ]]; then + echo " ✓ $f" + else + echo " ✗ $f MISSING" + FAIL=1 + fi + done + + # ── Platform branch model initialization ───────────────────────────────── + if [[ -f ".platform/initialized.md" ]]; then + echo " ✓ .platform/initialized.md (GitOps branch model active)" + else + echo " ✗ .platform/initialized.md MISSING" + echo " This file is committed to the dev branch by gitea:setup-branches." + echo " Its presence proves this PR originated from the platform-managed dev branch." + FAIL=1 + fi + + # ── catalog-info.yaml field validation ─────────────── + if python3 - <<'PYEOF' + import yaml, sys + try: + # catalog-info.yaml may be multi-document (Component + API); load the Component doc + docs = list(yaml.safe_load_all(open('catalog-info.yaml'))) + doc = next((d for d in docs if isinstance(d, dict) and d.get('kind') == 'Component'), docs[0] if docs else {}) + except Exception as e: + print(f" ✗ catalog-info.yaml: parse error: {e}") + sys.exit(1) + fields = [ + ('apiVersion', lambda d: d.get('apiVersion')), + ('kind', lambda d: d.get('kind')), + ('metadata.name', lambda d: (d.get('metadata') or {}).get('name')), + ('spec.type', lambda d: (d.get('spec') or {}).get('type')), + ('spec.owner', lambda d: (d.get('spec') or {}).get('owner')), + ] + fail = False + for field, getter in fields: + val = getter(doc) + if val: + print(f" ✓ catalog-info.yaml/{field}: {val}") + else: + print(f" ✗ catalog-info.yaml/{field}: missing or empty") + fail = True + sys.exit(1 if fail else 0) + PYEOF + then + echo " ✓ catalog-info.yaml is conformant" + else + FAIL=1 + fi + + echo "──────────────────────────────────────────────────" + if [[ $FAIL -eq 0 ]]; then + echo " ✓ Platform conformance: PASSED" + else + echo " ✗ Platform conformance: FAILED" + exit 1 + fi diff --git a/.gitea/workflows/build-push.yml b/.gitea/workflows/build-push.yml new file mode 100644 index 0000000..a204b70 --- /dev/null +++ b/.gitea/workflows/build-push.yml @@ -0,0 +1,138 @@ +name: Build and Push to ACR + +on: + push: + branches: [ dev ] + workflow_dispatch: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + AZURE_FEDERATED_TOKEN_FILE: /var/run/secrets/azure/tokens/azure-identity-token + +jobs: + build: + name: Build and Push + runs-on: ubuntu-latest + if: >- + github.ref != 'refs/heads/main' && ( + github.event_name == 'workflow_dispatch' || + (github.event_name == 'push' && github.event.before != '0000000000000000000000000000000000000000') + ) + permissions: + contents: read + id-token: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build and test + run: | + RUNTIME=$(grep '^runtime:' .platform/config.yaml | sed 's/^runtime: //' | tr -d ' \r') + echo "Runtime: $RUNTIME" + echo "PLATFORM_RUNTIME=$RUNTIME" >> $GITHUB_ENV + + case "$RUNTIME" in + go) + # Install Go if the runner doesn't have it + if ! command -v go &>/dev/null; then + GO_VERSION=1.23.6 + wget -q "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -O /tmp/go.tar.gz + rm -rf /usr/local/go && tar -C /usr/local -xzf /tmp/go.tar.gz + export PATH=$PATH:/usr/local/go/bin + fi + go mod tidy + go test ./... + ;; + java-springboot|java-liberty) + # Install JDK if not present (Debian runner) + if ! command -v java &>/dev/null; then + apt-get update -qq + apt-get install -y --no-install-recommends openjdk-17-jdk-headless + fi + export JAVA_HOME=$(dirname $(dirname $(readlink -f "$(which java)"))) + java -version 2>&1 + # Install Maven if not present + if ! command -v mvn &>/dev/null; then + MVN_VERSION=3.9.9 + wget -q "https://archive.apache.org/dist/maven/maven-3/${MVN_VERSION}/binaries/apache-maven-${MVN_VERSION}-bin.tar.gz" + tar -xzf "apache-maven-${MVN_VERSION}-bin.tar.gz" -C /opt + ln -sf "/opt/apache-maven-${MVN_VERSION}/bin/mvn" /usr/local/bin/mvn + fi + mvn --version + mvn clean package -DskipTests -B + echo "✓ Build complete" + mvn test -B + ;; + nodejs-express|typescript-nestjs) + npm install + npm test + ;; + python-fastapi) + pip install -r requirements.txt -q + pytest app/ -v --tb=short || echo "✓ tests done" + ;; + *) + echo "Unknown runtime '$RUNTIME' — skipping pre-build step" + ;; + esac + + - name: Install Azure CLI + run: | + command -v az &>/dev/null || curl -sL https://aka.ms/InstallAzureCLIDeb | bash + + - name: Install Docker CLI + run: | + command -v docker &>/dev/null || (apt-get update -qq && apt-get install -y docker.io) + docker --version + + - name: Azure login (OIDC) + run: | + az login \ + --service-principal \ + --username "$AZURE_CLIENT_ID" \ + --tenant "$AZURE_TENANT_ID" \ + --federated-token "$(cat $AZURE_FEDERATED_TOKEN_FILE)" + echo "✓ Azure login successful" + + - name: Get ACR details + id: acr + run: | + ACR_NAME=$(az acr list --query "[0].name" -o tsv) + ACR_NAME="${ACR_NAME:-bstagecjotdevacr}" + echo "ACR_NAME=$ACR_NAME" >> $GITHUB_ENV + echo "ACR_LOGIN_SERVER=${ACR_NAME}.azurecr.io" >> $GITHUB_ENV + echo "✓ Using ACR: ${ACR_NAME}.azurecr.io" + + - name: ACR Login + run: | + ACR_TOKEN=$(az acr login --name "$ACR_NAME" --expose-token --output tsv --query accessToken) + docker login "$ACR_LOGIN_SERVER" \ + --username 00000000-0000-0000-0000-000000000000 \ + --password "$ACR_TOKEN" + echo "✓ ACR login successful" + + - name: Build and Push Docker image + run: | + IMAGE_TAG="${{ gitea.sha }}" + IMAGE_FULL="${ACR_LOGIN_SERVER}/test-for-174--007:${IMAGE_TAG}" + IMAGE_LATEST="${ACR_LOGIN_SERVER}/test-for-174--007:latest" + docker build -t "$IMAGE_FULL" -t "$IMAGE_LATEST" . + docker push "$IMAGE_FULL" + docker push "$IMAGE_LATEST" + echo "IMAGE_FULL=$IMAGE_FULL" >> $GITHUB_ENV + echo "✓ Pushed: $IMAGE_FULL" + + - name: Build Summary + run: | + RUNTIME=$(grep '^runtime:' .platform/config.yaml | sed 's/^runtime: //' | tr -d ' \r') + echo "### ✅ Build Successful" >> $GITHUB_STEP_SUMMARY + echo "| | |" >> $GITHUB_STEP_SUMMARY + echo "|---|---|" >> $GITHUB_STEP_SUMMARY + echo "| **Service** | test-for-174--007 |" >> $GITHUB_STEP_SUMMARY + echo "| **Runtime** | $RUNTIME |" >> $GITHUB_STEP_SUMMARY + echo "| **Commit** | ${{ gitea.sha }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Image** | $IMAGE_FULL |" >> $GITHUB_STEP_SUMMARY diff --git a/.gitea/workflows/deploy-humanitec.yml b/.gitea/workflows/deploy-humanitec.yml new file mode 100644 index 0000000..75db160 --- /dev/null +++ b/.gitea/workflows/deploy-humanitec.yml @@ -0,0 +1,243 @@ +name: Deploy to Humanitec + +on: + workflow_run: + workflows: ["Build and Push to ACR"] + types: + - completed + branches: [ dev, staging, prod ] + workflow_dispatch: + inputs: + environment: + description: 'Target environment' + required: true + default: 'dev' + type: choice + options: + - dev + - staging + - prod + +concurrency: + group: -deploy-humanitec + cancel-in-progress: true + +env: + APP_ID: test-for-174--007 + PROJECT_ID: cjot-platform + AZURE_FEDERATED_TOKEN_FILE: /var/run/secrets/azure/tokens/azure-identity-token + DEFAULT_ENV_ID: dev + +jobs: + deploy: + name: Deploy to Humanitec + runs-on: ubuntu-latest + if: >- + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || + (github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/main') + permissions: + contents: read + id-token: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + # No `ref:` override — Gitea's act_runner mangles inline expressions in + # `with:` inputs. For workflow_run events this checks out main (the + # default branch), which is correct: score.yaml and other platform files + # live on main. The image SHA is resolved separately below. + + - name: Install CLI tools + run: | + # jq (needed for github API call) + command -v jq &>/dev/null || (apt-get update -qq && apt-get install -y -qq jq) + # hctl (Humanitec Platform Orchestrator v2 CLI) + HCTL_VERSION=$(curl -s https://api.github.com/repos/humanitec/hctl/releases/latest | jq -r '.tag_name') + curl -fLO "https://github.com/humanitec/hctl/releases/download/${HCTL_VERSION}/hctl_${HCTL_VERSION#v}_linux_amd64.tar.gz" + tar -xzf "hctl_${HCTL_VERSION#v}_linux_amd64.tar.gz" + install -m 755 hctl /usr/local/bin/hctl + hctl --version + echo "✓ Tools installed" + + - name: Install Azure CLI + run: command -v az &>/dev/null || curl -sL https://aka.ms/InstallAzureCLIDeb | bash + + - name: Azure login (OIDC) + run: | + az login \ + --service-principal \ + --username "$AZURE_CLIENT_ID" \ + --tenant "$AZURE_TENANT_ID" \ + --federated-token "$(cat $AZURE_FEDERATED_TOKEN_FILE)" + echo "✓ Azure login successful" + + - name: Get image reference + id: image + run: | + ACR_LOGIN_SERVER=$(az acr list --query "[0].loginServer" -o tsv) + # Read the dev-branch SHA from the event payload. Using $GITHUB_EVENT_PATH + # avoids act_runner expression mangling in with: inputs. The checkout + # above is on main (platform files); we still need the dev SHA for the + # image tag that was built and pushed by build-push.yml. + SHA=$(python3 -c "import json,os; e=json.load(open(os.environ['GITHUB_EVENT_PATH'])); print(e.get('workflow_run',{}).get('head_sha') or '')" 2>/dev/null || true) + if [ -z "$SHA" ]; then SHA=$(git rev-parse HEAD); fi + echo "IMAGE_FULL=${ACR_LOGIN_SERVER}/test-for-174--007:${SHA}" >> $GITHUB_ENV + echo "✓ Image: ${ACR_LOGIN_SERVER}/test-for-174--007:${SHA}" + + - name: Set environment + id: set-env + run: | + if [ "${{ gitea.event_name }}" = "workflow_dispatch" ]; then + ENV_ID="${{ gitea.event.inputs.environment }}" + else + # Derive from the branch that triggered the upstream build-push run. + # workflow_run.head_branch carries 'dev', 'staging', or 'prod'. + BRANCH="${{ github.event.workflow_run.head_branch }}" + case "$BRANCH" in + staging) ENV_ID="staging" ;; + prod) ENV_ID="prod" ;; + *) ENV_ID="$DEFAULT_ENV_ID" ;; + esac + fi + ENV_ID="${ENV_ID:-dev}" + echo "ENV_ID=$ENV_ID" >> $GITHUB_OUTPUT + echo "ENV_ID=$ENV_ID" >> $GITHUB_ENV + echo "✓ Target environment: $ENV_ID" + + - name: Get Humanitec credentials from Key Vault + run: | + HUMANITEC_TOKEN=$(az keyvault secret show \ + --vault-name "bstage-cjot-dev-core-kv" \ + --name "humanitec-api-token-v2" \ + --query "value" -o tsv) + [ -z "$HUMANITEC_TOKEN" ] && echo "❌ Failed to retrieve HUMANITEC_TOKEN" && exit 1 + echo "HUMANITEC_AUTH_TOKEN=$HUMANITEC_TOKEN" >> $GITHUB_ENV + + HUMANITEC_ORG=$(az keyvault secret show \ + --vault-name "bstage-cjot-dev-core-kv" \ + --name "humanitec-org-id" \ + --query "value" -o tsv) + [ -z "$HUMANITEC_ORG" ] && echo "❌ Failed to retrieve HUMANITEC_ORG" && exit 1 + echo "HUMANITEC_ORG=$HUMANITEC_ORG" >> $GITHUB_ENV + echo "✓ Credentials retrieved" + + - name: Deploy via Humanitec Score + run: | + # Pre-flight: wait for any prior deployment to finish before calling hctl. + # hctl refuses to start a new deployment while one is still executing. + echo "Pre-flight: checking for in-progress deployments..." + PREFLIGHT_WAITED=0 + while [ $PREFLIGHT_WAITED -lt 300 ]; do + PREFLIGHT_STATUS=$(curl -sf \ + -H "Authorization: Bearer $HUMANITEC_AUTH_TOKEN" \ + "https://api.humanitec.dev/orgs/$HUMANITEC_ORG/last-deployments?env_id=$ENV_ID&project_id=$PROJECT_ID&state_change_only=true" \ + | jq -r '.items[0].status // "none"' 2>/dev/null || echo "none") + if [ "$PREFLIGHT_STATUS" != "in progress" ] && [ "$PREFLIGHT_STATUS" != "pending" ] && [ "$PREFLIGHT_STATUS" != "executing" ]; then + echo "Pre-flight passed (status=$PREFLIGHT_STATUS). Proceeding." + break + fi + echo " Prior deployment still running ($PREFLIGHT_WAITED s elapsed, status=$PREFLIGHT_STATUS)..." + sleep 15 + PREFLIGHT_WAITED=$((PREFLIGHT_WAITED + 15)) + done + # First deploy — provisions all resources. On a brand-new Humanitec project the + # dns-k8s-ingress Terraform module runs before the K8s Service exists, so the + # ingress backend port falls back to 3000. A second deploy (below) corrects it + # once the Service is up, which is essential for apps not running on port 3000. + HCTL_EXIT=0 + timeout 300 hctl score deploy "$PROJECT_ID" "$ENV_ID" score.yaml \ + --no-prompt \ + --default-image "$IMAGE_FULL" || HCTL_EXIT=$? + if [ "$HCTL_EXIT" -eq 0 ]; then + echo "✓ First deployment complete to $ENV_ID" + elif [ "$HCTL_EXIT" -eq 124 ]; then + echo "✓ First deployment submitted (polling timed out — waiting for K8s to settle)" + else + echo "✗ hctl failed with exit code $HCTL_EXIT" + exit $HCTL_EXIT + fi + # Poll Humanitec API until the first deployment is no longer in-progress before + # re-deploying. A flat sleep is unreliable — Terraform DNS modules can take 4-6 min. + echo "Waiting for first deployment to finish (polling Humanitec API)..." + MAX_WAIT=360 + WAITED=0 + while [ $WAITED -lt $MAX_WAIT ]; do + DEPLOY_STATUS=$(curl -sf \ + -H "Authorization: Bearer $HUMANITEC_AUTH_TOKEN" \ + "https://api.humanitec.dev/orgs/$HUMANITEC_ORG/last-deployments?env_id=$ENV_ID&project_id=$PROJECT_ID&state_change_only=true" \ + | jq -r '.items[0].status // "unknown"' 2>/dev/null || echo "unknown") + if [ "$DEPLOY_STATUS" != "in progress" ] && [ "$DEPLOY_STATUS" != "pending" ] && [ "$DEPLOY_STATUS" != "executing" ]; then + echo "First deployment finished with status: $DEPLOY_STATUS" + break + fi + echo " Still running ($WAITED s elapsed, status=$DEPLOY_STATUS)..." + sleep 15 + WAITED=$((WAITED + 15)) + done + if [ $WAITED -ge $MAX_WAIT ]; then + echo "Warning: first deployment still running after $MAX_WAIT s — proceeding anyway" + fi + # Second deploy — dns module now reads the real K8s Service port, fixing the + # ingress backend. This is a no-op for apps already routed correctly (e.g. port 3000). + HCTL_EXIT2=0 + timeout 120 hctl score deploy "$PROJECT_ID" "$ENV_ID" score.yaml \ + --no-prompt \ + --default-image "$IMAGE_FULL" || HCTL_EXIT2=$? + if [ "$HCTL_EXIT2" -eq 0 ]; then + echo "✓ Deployment finalised to $ENV_ID" + elif [ "$HCTL_EXIT2" -eq 124 ]; then + echo "✓ Second deployment submitted (polling timed out)" + else + echo "✗ Second hctl deploy failed with exit code $HCTL_EXIT2" + exit $HCTL_EXIT2 + fi + + - name: Deployment Summary + run: | + DEPLOY_URL="https://console.humanitec.dev/orgs/${HUMANITEC_ORG}/projects/$APP_ID" + echo "### 🚀 Deployment Complete" >> $GITHUB_STEP_SUMMARY + echo "| | |" >> $GITHUB_STEP_SUMMARY + echo "|---|---|" >> $GITHUB_STEP_SUMMARY + echo "| **Service** | $APP_ID |" >> $GITHUB_STEP_SUMMARY + echo "| **Environment** | $ENV_ID |" >> $GITHUB_STEP_SUMMARY + echo "| **Image** | $IMAGE_FULL |" >> $GITHUB_STEP_SUMMARY + echo "| **Humanitec** | [$DEPLOY_URL]($DEPLOY_URL) |" >> $GITHUB_STEP_SUMMARY + + - name: OTel traffic warmup (dev only) + if: env.ENV_ID == 'dev' + run: | + SERVICE=$(grep '^service:' .platform/config.yaml | sed 's/^service: //' | tr -d ' \r') + HEALTH_PATH=$(grep '^health_path:' .platform/config.yaml | sed 's/^health_path: //' | tr -d ' \r') + HEALTH_PATH="${HEALTH_PATH:-/health}" + APP_URL="https://${SERVICE}.kyndemo.live" + + echo "Waiting for ${APP_URL}${HEALTH_PATH} to be live (up to 120s)..." + HTTP_CODE="000" + DEADLINE=$(($(date +%s) + 120)) + while [ $(date +%s) -lt $DEADLINE ]; do + HTTP_CODE=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 5 "${APP_URL}${HEALTH_PATH}" || echo "000") + [ "$HTTP_CODE" = "200" ] && echo "✓ App live at ${APP_URL}" && break + echo " waiting... (HTTP $HTTP_CODE)"; sleep 5 + done + + if [ "$HTTP_CODE" != "200" ]; then + echo "App did not come live within 120s — skipping OTel warmup" + exit 0 + fi + + # Send a burst of requests across common paths to generate OTel spans/metrics. + # All runtimes expose /health; the remaining paths are best-effort (404s still + # produce spans and are counted by the OTel HTTP instrumentation). + echo "Sending traffic burst (50 requests) to generate OTel telemetry..." + PATHS=("${HEALTH_PATH}" "/" "/api" "/api/status" "/api/health" "${HEALTH_PATH}" "/" "/api") + SUCCESS=0; TOTAL=0 + for i in $(seq 1 7); do + for RPATH in "${PATHS[@]}"; do + CODE=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 5 "${APP_URL}${RPATH}" || echo "000") + TOTAL=$((TOTAL + 1)) + case "$CODE" in 2*|3*) SUCCESS=$((SUCCESS + 1)) ;; esac + done + sleep 1 + done + echo "✓ Warmup complete: ${SUCCESS}/${TOTAL} 2xx/3xx — OTel spans visible in Grafana within ~30s" + echo "| **OTel warmup** | ${SUCCESS}/${TOTAL} requests → ${APP_URL} |" >> $GITHUB_STEP_SUMMARY diff --git a/.gitea/workflows/integration-test.yml b/.gitea/workflows/integration-test.yml new file mode 100644 index 0000000..df881b4 --- /dev/null +++ b/.gitea/workflows/integration-test.yml @@ -0,0 +1,193 @@ + +name: Integration Test + +on: + pull_request: + branches: [ "main" ] + workflow_dispatch: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ── Job 1: Platform Conformance ─────────────────────────────────────────── + platform-check: + name: Platform Conformance + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Platform conformance + uses: ./.gitea/actions/platform-check + + # ── Job 2: Unit Tests + Container Smoke ─────────────────────────────────── + smoke-test: + name: Unit Tests + Container Smoke + runs-on: ubuntu-latest + needs: platform-check + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Free disk space + run: | + docker system prune -f 2>/dev/null || true + df -h / 2>/dev/null || true + + - name: Unit tests + run: | + RUNTIME=$(grep '^runtime:' .platform/config.yaml | sed 's/^runtime: //' | tr -d ' \r') + echo "PLATFORM_RUNTIME=$RUNTIME" >> $GITHUB_ENV + + case "$RUNTIME" in + go) + if ! command -v go &>/dev/null; then + GO_VERSION=1.23.6 + wget -q "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -O /tmp/go.tar.gz + rm -rf /usr/local/go && tar -C /usr/local -xzf /tmp/go.tar.gz + export PATH=$PATH:/usr/local/go/bin + fi + go mod tidy + go test ./... -v + ;; + java-springboot|java-liberty) + # Install JDK if not present (Debian runner) + if ! command -v java &>/dev/null; then + apt-get update -qq + apt-get install -y --no-install-recommends openjdk-17-jdk-headless + fi + export JAVA_HOME=$(dirname $(dirname $(readlink -f "$(which java)"))) + if ! command -v mvn &>/dev/null; then + MVN_VERSION=3.9.9 + wget -q "https://archive.apache.org/dist/maven/maven-3/${MVN_VERSION}/binaries/apache-maven-${MVN_VERSION}-bin.tar.gz" + tar -xzf "apache-maven-${MVN_VERSION}-bin.tar.gz" -C /opt + ln -sf "/opt/apache-maven-${MVN_VERSION}/bin/mvn" /usr/local/bin/mvn + fi + # package produces the build artifact (WAR/JAR) needed by the Dockerfile + mvn package -B + ;; + nodejs-express|typescript-nestjs) + npm install + npm test + ;; + python-fastapi) + pip install -r requirements.txt -q + # exit 5 = no tests collected — treat as pass + pytest app/ -v --tb=short || [ $? -eq 5 ] + ;; + *) + echo "Unknown runtime '$RUNTIME' — skipping unit tests" + ;; + esac + + - name: Install Docker CLI + run: command -v docker &>/dev/null || (apt-get update -qq && apt-get install -y docker.io) + + - name: Build container image + run: docker build -t ci-image:test . + + - name: Start service and wait for health + run: | + # Unique name per run — prevents cross-job container conflicts on shared Docker daemon + CONTAINER_NAME="ci-${GITHUB_RUN_ID}" + PORT=$(grep '^container_port:' .platform/config.yaml | sed 's/^container_port: //' | tr -d ' \r') + HEALTH_PATH=$(grep '^health_path:' .platform/config.yaml | sed 's/^health_path: //' | tr -d ' \r') + PORT="${PORT:-8080}" + HEALTH_PATH="${HEALTH_PATH:-/health}" + echo "CONTAINER_NAME=${CONTAINER_NAME}" >> $GITHUB_ENV + echo "CONTAINER_PORT=${PORT}" >> $GITHUB_ENV + echo "HEALTH_PATH=${HEALTH_PATH}" >> $GITHUB_ENV + + # No port binding — connect to container's bridge IP directly. + # The runner itself runs inside a container (DinD via socket); the ci-service + # container is placed on the same Docker bridge and its IP is reachable from + # the runner container. Using localhost:HOST_PORT would hit the runner's own + # loopback, not the host's port mapping. + # + # OTEL_SDK_DISABLED=true: disables the OTel Java agent's class-transformation + # and exporter threads in CI where no collector is running. Without this, + # Spring Boot + OTel agent takes 60-90s to start, exceeding the health timeout. + docker run -d --name "${CONTAINER_NAME}" -e OTEL_SDK_DISABLED=true ci-image:test + + # Wait for bridge IP to be assigned (< 2s normally) + for i in $(seq 1 10); do + CONTAINER_IP=$(docker inspect "${CONTAINER_NAME}" --format '{{.NetworkSettings.IPAddress}}' 2>/dev/null) + [ -n "${CONTAINER_IP}" ] && break + sleep 1 + done + echo "CONTAINER_IP=${CONTAINER_IP}" >> $GITHUB_ENV + echo "Container IP: ${CONTAINER_IP}" + + echo "Waiting for ${HEALTH_PATH} on ${CONTAINER_IP}:${PORT} (up to 180s)..." + DEADLINE=$(($(date +%s) + 180)) + while true; do + if curl -sf "http://${CONTAINER_IP}:${PORT}${HEALTH_PATH}" >/dev/null 2>&1; then + break + fi + if [ $(date +%s) -ge $DEADLINE ]; then + echo "Timeout waiting for health check" + echo "Container logs:" + docker logs "${CONTAINER_NAME}" 2>&1 | tail -30 + exit 1 + fi + if ! docker ps --filter "name=${CONTAINER_NAME}" --format '{{.Status}}' | grep -q Up; then + echo "Container exited:" + docker logs "${CONTAINER_NAME}" 2>&1 | tail -30 + exit 1 + fi + echo " still waiting..."; sleep 3 + done + echo "✓ Service started at ${CONTAINER_IP}:${PORT}, health endpoint: ${HEALTH_PATH}" + + - name: Validate health response + run: | + curl -sf "http://${CONTAINER_IP}:${CONTAINER_PORT}${HEALTH_PATH}" > /tmp/health.json + echo "Health response:" + cat /tmp/health.json + python3 - <<'PYEOF' + import json, sys + body = json.load(open('/tmp/health.json')) + status = str(body.get('status') or '').upper() + if status not in ('UP', 'OK', 'HEALTHY'): + print(f" ✗ unexpected health status: {body.get('status')!r}") + sys.exit(1) + print(f" ✓ health status: {body['status']}") + PYEOF + echo "✓ Container smoke test: PASSED" + + - name: Write job summary + if: always() + run: | + COMPONENT=$(python3 -c "import yaml; docs=list(yaml.safe_load_all(open('catalog-info.yaml'))); d=next((x for x in docs if isinstance(x,dict) and x.get('kind')=='Component'),docs[0]); print(d['metadata']['name'])" 2>/dev/null || echo "test-for-174--007") + RUNTIME=$(grep '^runtime:' .platform/config.yaml | sed 's/^runtime: //' | tr -d ' \r' 2>/dev/null || echo "unknown") + echo "## Integration Test: \`${COMPONENT}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Stage | Detail |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Platform conformance | catalog-info.yaml ✓ \`.platform/initialized.md\` ✓ |" >> $GITHUB_STEP_SUMMARY + echo "| Unit tests | runtime: \`${RUNTIME}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Container smoke | \`GET ${HEALTH_PATH:-/health}\` → HTTP 200 |" >> $GITHUB_STEP_SUMMARY + + - name: Cleanup + if: always() + run: docker rm -f "ci-${GITHUB_RUN_ID}" 2>/dev/null || true + + - name: Post commit status + if: always() + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + JOB_STATUS: ${{ job.status }} + run: | + STATE=$([[ "$JOB_STATUS" == "success" ]] && echo "success" || echo "failure") + DESC=$([[ "$STATE" == "success" ]] && echo "All checks passed" || echo "Some checks failed") + curl -sf -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/statuses/${GITHUB_SHA}" \ + -d "{\"state\":\"${STATE}\",\"context\":\"Integration Test / Unit Tests + Container Smoke (workflow_dispatch)\",\"description\":\"${DESC}\"}" \ + || true + diff --git a/.gitea/workflows/k6-apply.yml b/.gitea/workflows/k6-apply.yml new file mode 100644 index 0000000..dd15081 --- /dev/null +++ b/.gitea/workflows/k6-apply.yml @@ -0,0 +1,97 @@ + +name: Apply k6 Load Test ConfigMap + +on: + workflow_run: + workflows: ["Deploy to Humanitec"] + types: + - completed + branches: [ dev, staging, prod ] + workflow_dispatch: + inputs: + namespace: + description: 'Target namespace override' + required: false + type: string + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + AZURE_FEDERATED_TOKEN_FILE: /var/run/secrets/azure/tokens/azure-identity-token + +jobs: + apply-k6: + name: Apply k6 ConfigMap + runs-on: ubuntu-latest + if: >- + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || + github.event_name == 'workflow_dispatch' + permissions: + contents: read + id-token: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Azure CLI + run: command -v az &>/dev/null || curl -sL https://aka.ms/InstallAzureCLIDeb | bash + + - name: Azure login (OIDC) + run: | + az login \ + --service-principal \ + --username "$AZURE_CLIENT_ID" \ + --tenant "$AZURE_TENANT_ID" \ + --federated-token "$(cat $AZURE_FEDERATED_TOKEN_FILE)" + echo "✓ Azure login successful" + + - name: Get AKS credentials + run: | + AKS_RG=$(az aks list --query "[0].resourceGroup" -o tsv) + AKS_NAME=$(az aks list --query "[0].name" -o tsv) + az aks get-credentials -g "$AKS_RG" -n "$AKS_NAME" --overwrite-existing + echo "✓ AKS credentials configured" + + - name: Determine target namespace + id: ns + run: | + # Manual override takes priority + MANUAL_NS="${{ github.event.inputs.namespace }}" + if [ -n "$MANUAL_NS" ]; then + echo "NAMESPACE=$MANUAL_NS" >> $GITHUB_ENV + echo "✓ Using manual namespace: $MANUAL_NS" + exit 0 + fi + # Derive from branch + BRANCH="${{ github.event.workflow_run.head_branch }}" + if [ -z "$BRANCH" ]; then + BRANCH=$(git rev-parse --abbrev-ref HEAD) + fi + case "$BRANCH" in + staging) NAMESPACE="staging" ;; + prod) NAMESPACE="prod" ;; + *) NAMESPACE="dev" ;; + esac + echo "NAMESPACE=$NAMESPACE" >> $GITHUB_ENV + echo "✓ Target namespace: $NAMESPACE (from branch: $BRANCH)" + + - name: Apply k6 ConfigMap + run: | + if [ ! -f k6/configmap.yaml ]; then + echo "⚠ k6/configmap.yaml not found — skipping" + exit 0 + fi + kubectl apply -f k6/configmap.yaml -n "$NAMESPACE" + echo "✓ k6 ConfigMap applied to namespace $NAMESPACE" + + - name: Summary + run: | + echo "### ⚡ k6 Load Test ConfigMap Applied" >> $GITHUB_STEP_SUMMARY + echo "| | |" >> $GITHUB_STEP_SUMMARY + echo "|---|---|" >> $GITHUB_STEP_SUMMARY + echo "| **Namespace** | \`$NAMESPACE\` |" >> $GITHUB_STEP_SUMMARY + echo "| **ConfigMap** | \`k6-test-test-for-174--007\` |" >> $GITHUB_STEP_SUMMARY + diff --git a/.gitea/workflows/sonar.yml b/.gitea/workflows/sonar.yml new file mode 100644 index 0000000..955167f --- /dev/null +++ b/.gitea/workflows/sonar.yml @@ -0,0 +1,206 @@ + +name: SonarQube Analysis + +on: + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ gitea.workflow }}-${{ gitea.ref }} + cancel-in-progress: true + +jobs: + sonarqube: + name: Build, Test & Analyse + runs-on: ubuntu-latest + timeout-minutes: 15 + + env: + SONAR_PROJECT_KEY: test-for-174--007 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Make Maven wrapper executable + run: chmod +x mvnw + + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: maven-${{ runner.os }}-${{ hashFiles('**/pom.xml') }} + restore-keys: maven-${{ runner.os }}- + + - name: Cache SonarQube analysis data + uses: actions/cache@v4 + with: + path: ~/.sonar/cache + key: sonar-${{ runner.os }} + restore-keys: sonar-${{ runner.os }} + + # SONAR_TOKEN replaced by SONAR_ADMIN_TOKEN — the admin token is used + # only for provisioning; the scan uses an ephemeral PROJECT_ANALYSIS_TOKEN + # generated in the bootstrap step below. + - name: Validate required secrets + env: + SONAR_ADMIN_TOKEN: ${{ secrets.SONAR_ADMIN_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + run: | + [[ -n "$SONAR_ADMIN_TOKEN" ]] || { echo "::error::SONAR_ADMIN_TOKEN is not set"; exit 1; } + [[ -n "$SONAR_HOST_URL" ]] || { echo "::error::SONAR_HOST_URL is not set"; exit 1; } + [[ -n "$SONAR_PROJECT_KEY" ]] || { echo "::error::SONAR_PROJECT_KEY is not set"; exit 1; } + + - name: Bootstrap SonarQube project and generate scan token + id: sonar-bootstrap + env: + SONAR_ADMIN_TOKEN: ${{ secrets.SONAR_ADMIN_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + run: | + # ---- 1. Create the project if it doesn't already exist ---- + PROJECT_COUNT=$(curl -sf \ + -u "${SONAR_ADMIN_TOKEN}:" \ + "${SONAR_HOST_URL}/api/projects/search?projects=${SONAR_PROJECT_KEY}" \ + | jq '.paging.total') + + if [[ "$PROJECT_COUNT" == "0" ]]; then + echo "Project '${SONAR_PROJECT_KEY}' not found — creating it..." + curl -sf -X POST \ + -u "${SONAR_ADMIN_TOKEN}:" \ + "${SONAR_HOST_URL}/api/projects/create" \ + --data-urlencode "name=${SONAR_PROJECT_KEY}" \ + --data-urlencode "project=${SONAR_PROJECT_KEY}" \ + --data-urlencode "mainBranch=main" \ + --data-urlencode "visibility=private" + echo "✅ Project created." + else + echo "✅ Project '${SONAR_PROJECT_KEY}' already exists — skipping creation." + fi + + # ---- 2. Revoke any leftover token from a previous failed run ---- + TOKEN_NAME="gitea-scan-${SONAR_PROJECT_KEY}" + curl -sf -X POST \ + -u "${SONAR_ADMIN_TOKEN}:" \ + "${SONAR_HOST_URL}/api/user_tokens/revoke" \ + --data-urlencode "name=${TOKEN_NAME}" \ + > /dev/null 2>&1 || true + + # ---- 3. Generate a fresh PROJECT_ANALYSIS_TOKEN for this run ---- + SCAN_TOKEN=$(curl -sf -X POST \ + -u "${SONAR_ADMIN_TOKEN}:" \ + "${SONAR_HOST_URL}/api/user_tokens/generate" \ + --data-urlencode "name=${TOKEN_NAME}" \ + --data-urlencode "type=PROJECT_ANALYSIS_TOKEN" \ + --data-urlencode "projectKey=${SONAR_PROJECT_KEY}" \ + | jq -r '.token') + + [[ -n "$SCAN_TOKEN" ]] || { echo "::error::Failed to generate scan token"; exit 1; } + + echo "✅ Scan token generated for project '${SONAR_PROJECT_KEY}'" + echo "::add-mask::${SCAN_TOKEN}" + echo "SCAN_TOKEN=${SCAN_TOKEN}" >> "$GITHUB_ENV" + - name: Build and test + run: | + ./mvnw -B verify \ + -Dtest='!PostgresIntegrationTests,!MySqlIntegrationTests' + + - name: SonarQube analysis + env: + SCAN_TOKEN: ${{ env.SCAN_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + run: | + ./mvnw -B org.sonarsource.scanner.maven:sonar-maven-plugin:4.0.0.4121:sonar \ + -Dsonar.projectKey="${SONAR_PROJECT_KEY}" \ + -Dsonar.host.url="${SONAR_HOST_URL}" \ + -Dsonar.token="${SCAN_TOKEN}" \ + -Dsonar.java.source=17 \ + -Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml + + - name: Quality Gate check + env: + SCAN_TOKEN: ${{ env.SCAN_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + run: | + echo "Waiting for SonarQube to process the analysis..." + for i in $(seq 1 24); do + RESPONSE=$(curl -sf -u "${SCAN_TOKEN}:" \ + "${SONAR_HOST_URL}/api/qualitygates/project_status?projectKey=${SONAR_PROJECT_KEY}" || true) + STATUS=$(echo "$RESPONSE" | jq -r '.projectStatus.status' 2>/dev/null || echo "NONE") + if [[ "$STATUS" =~ ^(OK|ERROR|WARN)$ ]]; then break; fi + echo " Status: ${STATUS:-pending} — retrying in 5s..." + sleep 5 + done + + echo "" + echo "══════════════════════════════════════════" + echo " Quality Gate: $STATUS" + echo "══════════════════════════════════════════" + + echo "$RESPONSE" | jq -r ' + .projectStatus.conditions[] | + if .status == "ERROR" then " ❌ \(.metricKey): \(.actualValue) (threshold: \(.errorThreshold), comparator: \(.comparator))" + elif .status == "WARN" then " ⚠️ \(.metricKey): \(.actualValue) (threshold: \(.errorThreshold), comparator: \(.comparator))" + else " ✅ \(.metricKey): \(.actualValue)" + end' + + echo "══════════════════════════════════════════" + echo "" + + FAILED=$(echo "$RESPONSE" | jq '[.projectStatus.conditions[] | select(.status == "ERROR")] | length') + if [[ "$FAILED" -gt 0 ]]; then + echo "::error::Quality Gate FAILED — $FAILED metric(s) did not meet threshold" + exit 1 + fi + + - name: Notify Backstage on quality gate failure + if: failure() + env: + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + BACKSTAGE_TOKEN: ${{ secrets.BACKSTAGE_TOKEN }} + BACKSTAGE_URL: ${{ secrets.BACKSTAGE_URL }} + run: | + echo "Sending quality gate failure notification to Backstage..." + HTTP_CODE=$(curl -s -o /tmp/bs-notify-response.json -w "%{http_code}" \ + -X POST "${BACKSTAGE_URL}/api/notifications" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${BACKSTAGE_TOKEN}" \ + -d "{ + \"recipients\": { + \"type\": \"entity\", + \"entityRef\": \"group:default/platform-engineering\" + }, + \"payload\": { + \"title\": \"SonarQube Quality Gate Failed\", + \"description\": \"Quality gate failed for ${SONAR_PROJECT_KEY} on branch ${GITHUB_REF_NAME}. Review the SonarQube dashboard for details.\", + \"link\": \"${SONAR_HOST_URL}/dashboard?id=${SONAR_PROJECT_KEY}\", + \"severity\": \"high\", + \"topic\": \"sonarqube-quality-gate\" + } + }") + echo "HTTP status: ${HTTP_CODE}" + cat /tmp/bs-notify-response.json 2>/dev/null || true + if [[ "$HTTP_CODE" -ge 200 && "$HTTP_CODE" -lt 300 ]]; then + echo "✅ Backstage notification sent" + else + echo "⚠️ Backstage notification failed (HTTP ${HTTP_CODE})" + fi + + - name: Revoke scan token + if: always() + env: + SONAR_ADMIN_TOKEN: ${{ secrets.SONAR_ADMIN_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + run: | + curl -sf -X POST \ + -u "${SONAR_ADMIN_TOKEN}:" \ + "${SONAR_HOST_URL}/api/user_tokens/revoke" \ + --data-urlencode "name=gitea-scan-${SONAR_PROJECT_KEY}" \ + && echo "✅ Scan token revoked" \ + || echo "⚠️ Token revocation failed (may have already been removed)" diff --git a/.gitea/workflows/techdocs.yml b/.gitea/workflows/techdocs.yml new file mode 100644 index 0000000..217cb94 --- /dev/null +++ b/.gitea/workflows/techdocs.yml @@ -0,0 +1,90 @@ +name: Build and Publish TechDocs + +on: + push: + branches: [main] + paths: + - "docs/**" + - "mkdocs.yml" + - "catalog-info.yaml" + workflow_dispatch: {} + +env: + AZURE_FEDERATED_TOKEN_FILE: /var/run/secrets/azure/tokens/azure-identity-token + AZURE_ACCOUNT_NAME: "bstagecjotdevsttechdocs" + ENTITY_NAMESPACE: default + ENTITY_KIND: component + ENTITY_NAME: test-for-174--007 + +jobs: + build-and-publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Bootstrap pip + run: | + python3 --version + if python3 -m pip --version 2>/dev/null; then + echo "pip already available" + elif python3 -m ensurepip --version 2>/dev/null; then + python3 -m ensurepip --upgrade + else + apt-get update -qq + apt-get install -y python3-pip + fi + python3 -m pip install --upgrade pip + python3 -m pip --version + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + python3 -m pip install \ + mkdocs-techdocs-core==1.* \ + mkdocs-git-revision-date-localized-plugin \ + mkdocs-awesome-pages-plugin + + npm install -g @techdocs/cli + + - name: Validate MkDocs config + run: mkdocs build --strict --site-dir /tmp/mkdocs-validate + + - name: Build TechDocs site + run: | + techdocs-cli generate \ + --source-dir . \ + --output-dir ./site \ + --no-docker \ + --verbose + + - name: Install Azure CLI + run: | + if command -v az &>/dev/null; then + echo "Azure CLI already installed: $(az version --query '"azure-cli"' -o tsv)" + else + curl -sL https://aka.ms/InstallAzureCLIDeb | bash + fi + + - name: Azure login (OIDC) + run: | + az login \ + --service-principal \ + --username "$AZURE_CLIENT_ID" \ + --tenant "$AZURE_TENANT_ID" \ + --federated-token "$(cat $AZURE_FEDERATED_TOKEN_FILE)" + + echo "Azure login successful" + + - name: Publish TechDocs site + run: | + techdocs-cli publish \ + --publisher-type azureBlobStorage \ + --storage-name "techdocs" \ + --azureAccountName "$AZURE_ACCOUNT_NAME" \ + --entity "$ENTITY_NAMESPACE/$ENTITY_KIND/$ENTITY_NAME" diff --git a/.platform/config.yaml b/.platform/config.yaml new file mode 100644 index 0000000..012a35b --- /dev/null +++ b/.platform/config.yaml @@ -0,0 +1,14 @@ +# Platform configuration — read by CI/CD workflows at runtime. +# DO NOT DELETE: required for platform-managed GitOps and CI/CD. + +runtime: java-springboot + +# Health check endpoint used by the integration-test smoke test. +# Java Spring Boot uses /actuator/health; Liberty uses /health; all others use /health. +health_path: /actuator/health + +# Port the service listens on inside the container. +# Python FastAPI defaults to 8000; Node/TypeScript to 3000; Liberty to 9080; everything else to 8080. +container_port: 8080 + +service: test-for-174--007 diff --git a/.platform/initialized.md b/.platform/initialized.md new file mode 100644 index 0000000..9f495ee --- /dev/null +++ b/.platform/initialized.md @@ -0,0 +1,25 @@ +# Platform Initialization + +| Field | Value | +|-------|-------| +| Component | `test-for-174--007` | +| Repository | `demo-platform/test-repo-174--007` | +| Runtime | `java-springboot` | +| Branch model | `dev` → `staging` → `prod` (→ `main` SoR) | +| Code lives on | `dev` (push to dev triggers build + deploy to dev env) | +| Protected | `main`, `staging`, `prod` (require PR + passing CI + 1 approval) | +| Free push | `dev` (inner loop — no PR, no approval required) | + +## Promotion flow + +Push to `dev` freely. The CI/CD pipeline builds and deploys to the dev +environment automatically on every commit. + +Promotion to higher environments requires a PR — the integration test must +pass and 1 reviewer must approve before the merge is allowed: + +- **dev → staging** — opens a PR; CI gate + 1 approval required +- **staging → prod** — opens a PR; CI gate + 1 approval required + +`main` is the system of record. It contains only workflow files and +platform metadata. Application code is promoted to it from `prod` via PR. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d695c0 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# test-for-174--007 + +> **System-of-Record branch** — application code lives on `dev`. + +| Branch | Purpose | +|-----------|---------| +| `dev` | Inner loop — push freely, deploys to dev env automatically | +| `staging` | Staging env — promoted from dev via PR (CI gate + 1 approval) | +| `prod` | Production — promoted from staging via PR (CI gate + 1 approval) | +| `main` | System of record — receives merges from prod after releases | + +## Platform CI/CD + +Workflows in `.gitea/workflows/` are **runtime-agnostic** — they read +`.platform/config.yaml` at run-time to determine build toolchain, container +port, and health endpoint. No workflow files need updating when this service +is migrated to a different runtime. + +| Config key | Value | +|------------|-------| +| `runtime` | `java-springboot` | +| `health_path` | `/actuator/health` | +| `container_port` | `8080` | diff --git a/catalog-info.yaml b/catalog-info.yaml new file mode 100644 index 0000000..75489fd --- /dev/null +++ b/catalog-info.yaml @@ -0,0 +1,139 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: test-for-174--007 + description: Test for issue 174 PR + annotations: + humanitec.com/orgId: skillful-wild-chicken-2617 + humanitec.com/projectId: cjot-platform + cjot.io/target-domain: + backstage.io/techdocs-ref: dir:. + backstage.io/kubernetes-namespace: dev + backstage.io/kubernetes-namespaces: "dev" + backstage.io/kubernetes-label-selector: "app=test-for-174--007" + gitea.kyndemo.live/repo-slug: "demo-platform/test-repo-174--007" + sonarqube.org/project-key: test-for-174--007 + prometheus.io/scrape: "true" + prometheus.io/port: "8080" + prometheus.io/path: "/actuator/prometheus" + grafana/grafana-instance: default + grafana/alert-label-selector: "app=test-for-174--007" + + grafana/dashboard-selector: "uid == 'otel-app-observability-v2'" + grafana.com/alert-label-selector: "app=test-for-174--007" + grafana.com/dashboard-url: "https://grafana.kyndemo.live/d/otel-app-observability-v2/opentelemetry-application-observability?orgId=1&var-app=test-for-174--007" + chaos-mesh/enabled: "true" + + + k6/enabled: "true" + k6/test-configmap: "k6-test-test-for-174--007" + k6/test-namespace: "dev" + k6/target-service: "test-for-174--007" + k6/target-port: "8080" + + tags: + - microservice + - golden-path + - java-springboot + - stateless + + - opentelemetry + + + - load-testing + - k6 + + links: + - url: https://console.humanitec.dev/orgs/skillful-wild-chicken-2617/projects/cjot-platform + title: Humanitec Console + icon: dashboard + + - url: https://grafana.kyndemo.live/d/otel-app-observability-v2/opentelemetry-application-observability?orgId=1&var-app=test-for-174--007 + title: Grafana Dashboard + icon: dashboard + + - url: https://gitea.kyndemo.live/demo-platform/test-repo-174--007 + title: Source Repository + icon: github +spec: + type: service + owner: group:default/platform-lead + + lifecycle: experimental + providesApis: + - test-for-174--007-api +--- +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: test-for-174--007-api + description: REST API for test-for-174--007 + annotations: + backstage.io/techdocs-ref: dir:. +spec: + type: openapi + lifecycle: experimental + owner: group:default/platform-lead + + definition: | + openapi: "3.0.0" + info: + title: test-for-174--007 + version: "0.1.0" + description: "Test for issue 174 PR" + servers: + - url: https://test-for-174--007.kyndemo.live + paths: + /api/items: + get: + summary: List all items + responses: + "200": + description: OK + post: + summary: Create item + responses: + "201": + description: Created + /api/items/{id}: + get: + summary: Get by ID + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + "200": + description: OK + "404": + description: Not found + put: + summary: Update item + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + "200": + description: OK + delete: + summary: Update item + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + "200": + description: Deleted + /actuator/health: + get: + summary: Health check + responses: + "200": + description: "{ status: UP }" diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..f1da8b6 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,20 @@ +# API Reference + +Base URL: `https://test-for-174--007.kyndemo.live` + +## Items `/api/items` + +| Method | Path | Body | Response | +|--------|------|------|----------| +| GET | `/api/items` | — | Array of items | +| POST | `/api/items` | `{"name":"...","description":"..."}` | Created item (201) | +| GET | `/api/items/{id}` | — | Item or 404 | +| PUT | `/api/items/{id}` | `{"name":"...","description":"..."}` | Updated item | +| DELETE | `/api/items/{id}` | — | `{"deleted":id}` | + +## Observability + +| Path | Description | +|------|-------------| +| `/actuator/health` | Health check — `{"status":"UP"}` | +| `/metrics` | Prometheus exposition format | diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..d9910f6 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,30 @@ +# Architecture + +`test-for-174--007` is a `java-springboot` service. + +| Component | Detail | +|-----------|--------| +| Runtime | `java-springboot` | +| Container port | `8080` | +| Health check | `/actuator/health` | +| Metrics | `/metrics` (Prometheus format) | +| Image | Azure Container Registry (`bstagecjotdevacr`) | + +## Deployment flow + +``` +Push to dev/staging/prod + → build-push.yml (build + test → push image to ACR) + → deploy-humanitec.yml (hctl score deploy) +``` + +## Branch model + +| Branch | Purpose | +|--------|---------| +| `main` | System of record — catalog, docs, score.yaml, platform config | +| `dev` | Active development | +| `staging` | Pre-production (lazy-created on first promote) | +| `prod` | Production (lazy-created on first promote) | + +Promotions between environments are triggered from the Backstage CI/CD tab. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..43b9986 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,45 @@ +# test-for-174--007 + +Test for issue 174 PR + +## Overview + +This service was scaffolded from the **Create Microservice** golden-path template on the Kyndryl Platform. + +- **Runtime**: `java-springboot` +- **Owner**: group:default/platform-lead +- **Deployment profile**: `stateless` + +## Architecture + +See [architecture.md](architecture.md) for the full architecture diagram. + +At a glance: + +``` +┌──────────────────────────────────────┐ +│ Gitea Actions CI/CD │ +│ ┌─────────────┐ ┌───────────────┐ │ +│ │ build-push │→ │ deploy- │ │ +│ │ .yml │ │ humanitec.yml │ │ +│ └─────────────┘ └───────────────┘ │ +└──────────────────────────────────────┘ + │ │ + ▼ ▼ + Azure ACR Humanitec API + │ + ▼ + AKS (via Score) + │ + ▼ + ┌──────────────────────────┐ + │ test-for-174--007 │ + │ :8080 │ + │ /api/items │ + │ /actuator/health │ + └──────────────────────────┘ +``` + +## API Reference + +See [api.md](api.md) for the full OpenAPI reference. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..108de48 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,11 @@ +site_name: test-for-174--007 +site_description: Test for issue 174 PR +docs_dir: docs + +nav: + - Home: index.md + - Architecture: architecture.md + - API Reference: api.md + +plugins: + - techdocs-core diff --git a/score.yaml b/score.yaml new file mode 100644 index 0000000..adc0933 --- /dev/null +++ b/score.yaml @@ -0,0 +1,51 @@ +apiVersion: score.dev/v1b1 +metadata: + name: test-for-174--007 + +containers: + app: + image: . + variables: + OTEL_SERVICE_NAME: "test-for-174--007" + OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.monitoring.svc.cluster.local:4318" + OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf" + OTEL_RESOURCE_ATTRIBUTES: "service.name=test-for-174--007" + OTEL_METRICS_EXPORTER: "otlp" + OTEL_TRACES_EXPORTER: "otlp" + OTEL_LOGS_EXPORTER: "none" + livenessProbe: + httpGet: + path: /actuator/health/liveness + port: 8080 + readinessProbe: + httpGet: + path: /actuator/health/readiness + port: 8080 + resources: + limits: + memory: "128Mi" + cpu: "200m" + requests: + memory: "32Mi" + cpu: "25m" + +service: + ports: + http: + port: 80 + targetPort: 8080 + +resources: + env: + type: environment + + dns: + type: dns + + # ----- Uncomment to add a PostgreSQL database (deployment_profile: db-only or db+cache) ----- + # db: + # type: postgres + + # ----- Uncomment to add Redis cache (deployment_profile: db+cache) ----- + # cache: + # type: redis