commit 522f246bc616fdd5d8fefb415717d4d46a2b2071
parent 28bc8f6c323879446053b8f7d54714f1c6307d0b
Author: Arjoonn Sharma <arjoonn@midpathsoftware.com>
Date: Sat, 7 Mar 2026 17:17:25 +0000
Auto generate exampels (!12)
Reviewed-on: https://gitea.midpathsoftware.com/midpath/jayporeci/pulls/12
Co-authored-by: Arjoonn Sharma <arjoonn@midpathsoftware.com>
Co-committed-by: Arjoonn Sharma <arjoonn@midpathsoftware.com>
Diffstat:
5 files changed, 1290 insertions(+), 145 deletions(-)
diff --git a/.jci/run.sh b/.jci/run.sh
@@ -128,6 +128,17 @@ echo "--- Step 1: Updating code with latest VERSION ---"
run_step "Sync Version" "scripts/sync_version.sh" scripts/sync_version.sh
echo ""
+# ---------------------------------------------------------------------------
+# Step 1b: Render examples into README.md, then regenerate the TOC index
+# ---------------------------------------------------------------------------
+echo "--- Step 1b: Rendering examples section into README.md ---"
+run_step "Render Examples" "scripts/render_examples.sh" scripts/render_examples.sh
+echo ""
+
+echo "--- Step 1c: Regenerating README index ---"
+run_step "README Index" "scripts/generate_readme_index.sh" scripts/generate_readme_index.sh
+echo ""
+
# Step 2: Build Docker image (sequential — other steps depend on it)
# ---------------------------------------------------------------------------
echo "--- Step 2: Building Docker image via scripts/build_image.sh ---"
diff --git a/PLAN.md b/PLAN.md
@@ -1,4 +0,0 @@
-- I want to refactor the APIs in the `jci web` command.
-- The UI should ask for a list of branches, and that should NOT return a complete list of commits.
-- A separate API should be used for a list of commits, which should be paginated by 100 commits per page.
-- The overall goal of this refactor is to retain the same web functionality but use multiple API calls on-demand so that we have a FAST user experience while browsing.
diff --git a/README.md b/README.md
@@ -11,21 +11,21 @@
- [How it works](#how-it-works)
- [FAQ / Needs / Wants / Todos](#faq-needs-wants-todos)
- [Examples](#examples)
- - [Lint, Build, Test, Publish a golang project](#lint-build-test-publish-a-golang-project)
- - [Pylint, Pytest, Coverage report](#pylint-pytest-coverage-report)
- - [Build Jekyll and publish to netlify](#build-jekyll-and-publish-to-netlify)
- - [Build Docusaurus and publish to S3 bucket](#build-docusaurus-and-publish-to-s3-bucket)
- - [Run a docker compose of redis, postgres, django, and run API tests against it.](#run-a-docker-compose-of-redis-postgres-django-and-run-api-tests-against-it)
- - [Schedule a midnight build and push status to telegram](#schedule-a-midnight-build-and-push-status-to-telegram)
- - [Run trufflehog scan on repo every hour](#run-trufflehog-scan-on-repo-every-hour)
- - [Run lint --fix on pre-commit for python, go, JS in the same repo](#run-lint-fix-on-pre-commit-for-python-go-js-in-the-same-repo)
- - [Create sub-pipelines for python / js / go and run when changes are there in any folder](#create-sub-pipelines-for-python-js-go-and-run-when-changes-are-there-in-any-folder)
- - [Set and use Secrets to publish messages to telegram](#set-and-use-secrets-to-publish-messages-to-telegram)
- - [Send mail on scheduled pipe failures](#send-mail-on-scheduled-pipe-failures)
- - [Midnight auto-update dependencies and ensure tests are passing after update](#midnight-auto-update-dependencies-and-ensure-tests-are-passing-after-update)
- - [Build and publish docker images](#build-and-publish-docker-images)
- - [Run pipelines on this repo, when changes happen in upstream projects](#run-pipelines-on-this-repo-when-changes-happen-in-upstream-projects)
- - [Run pipelines on another repo, when changes affect downstream projects](#run-pipelines-on-another-repo-when-changes-affect-downstream-projects)
+ - [Golang Lint Build Test](#golang-lint-build-test)
+ - [Pylint Pytest Coverage](#pylint-pytest-coverage)
+ - [Docker Compose Api Tests](#docker-compose-api-tests)
+ - [Midnight Build Telegram](#midnight-build-telegram)
+ - [Trufflehog Scan](#trufflehog-scan)
+ - [Lint Fix Precommit](#lint-fix-precommit)
+ - [Sub Pipelines](#sub-pipelines)
+ - [Secrets Telegram](#secrets-telegram)
+ - [Mail On Failure](#mail-on-failure)
+ - [Auto Update Deps](#auto-update-deps)
+ - [Build Publish Docker](#build-publish-docker)
+ - [Upstream Trigger](#upstream-trigger)
+ - [Downstream Trigger](#downstream-trigger)
+ - [Jekyll Netlify](#jekyll-netlify)
+ - [Docusaurus S3](#docusaurus-s3)
---
@@ -144,232 +144,1245 @@ part of the git repository.
## Examples
-### 00 — Golang Lint, Build & Test
+### Golang Lint Build Test
```
-00-golang-lint-build-test/
-├── .jci/
+$ tree .
+.
+├── .jci
│ └── run.sh
├── README.md
├── main.go
└── main_test.go
-```
-- [examples/00-golang-lint-build-test/.jci/run.sh](/releases/files/examples/00-golang-lint-build-test/.jci/run.sh)
-- [examples/00-golang-lint-build-test/README.md](/releases/files/examples/00-golang-lint-build-test/README.md)
-- [examples/00-golang-lint-build-test/main.go](/releases/files/examples/00-golang-lint-build-test/main.go)
-- [examples/00-golang-lint-build-test/main_test.go](/releases/files/examples/00-golang-lint-build-test/main_test.go)
-### Example 01 — Pylint + Pytest + Coverage
+$ cat .jci/run.sh
+#!/usr/bin/env bash
+set -euo pipefail
+
+# ── Navigate to the project directory ────────────────────────────────────────
+cd "$JCI_REPO_ROOT/00-golang-lint-build-test"
+echo "==> Working directory: $(pwd)"
+echo "==> Commit: ${JCI_COMMIT:-unknown}"
+echo
+
+# ── Initialise Go module if needed ───────────────────────────────────────────
+if [ ! -f go.mod ]; then
+ echo "==> No go.mod found – initialising module"
+ go mod init example.com/server
+fi
+
+# ── Lint (go vet) ────────────────────────────────────────────────────────────
+echo "==> Running go vet ./..."
+go vet ./... 2>&1 | tee "$JCI_OUTPUT_DIR/vet-report.txt"
+echo " vet report saved to \$JCI_OUTPUT_DIR/vet-report.txt"
+echo
+
+# ── Format check ─────────────────────────────────────────────────────────────
+echo "==> Checking formatting with gofmt"
+unformatted=$(gofmt -l .)
+if [ -n "$unformatted" ]; then
+ echo " ERROR: the following files need gofmt:"
+ echo "$unformatted"
+ exit 1
+fi
+echo " all files formatted correctly"
+echo
+
+# ── Build ────────────────────────────────────────────────────────────────────
+echo "==> Building binary"
+go build -o "$JCI_OUTPUT_DIR/server" .
+echo " binary saved to \$JCI_OUTPUT_DIR/server"
+echo
+
+# ── Test ─────────────────────────────────────────────────────────────────────
+echo "==> Running tests"
+go test -v -cover ./... 2>&1 | tee "$JCI_OUTPUT_DIR/test-results.txt"
+echo " test results saved to \$JCI_OUTPUT_DIR/test-results.txt"
+echo
+
+# ── Publish (optional) ───────────────────────────────────────────────────────
+if [ -n "${PUBLISH_DIR:-}" ]; then
+ echo "==> Publishing binary to $PUBLISH_DIR"
+ mkdir -p "$PUBLISH_DIR"
+ cp "$JCI_OUTPUT_DIR/server" "$PUBLISH_DIR/server"
+ echo " published successfully"
+else
+ echo "==> PUBLISH_DIR not set – skipping publish step"
+fi
+echo
+
+# ── Summary ──────────────────────────────────────────────────────────────────
+echo "========================================"
+echo " Pipeline complete"
+echo " Commit : ${JCI_COMMIT:-unknown}"
+echo " Binary : $JCI_OUTPUT_DIR/server"
+echo " Vet : $JCI_OUTPUT_DIR/vet-report.txt"
+echo " Tests : $JCI_OUTPUT_DIR/test-results.txt"
+echo "========================================"
+```
+
+### Pylint Pytest Coverage
```
-01-pylint-pytest-coverage/
-├── .jci/
+$ tree .
+.
+├── .jci
│ └── run.sh
└── README.md
-```
-- [examples/01-pylint-pytest-coverage/.jci/run.sh](/releases/files/examples/01-pylint-pytest-coverage/.jci/run.sh)
-- [examples/01-pylint-pytest-coverage/README.md](/releases/files/examples/01-pylint-pytest-coverage/README.md)
-### 02 — Docker Compose API Tests
+$ cat .jci/run.sh
+#!/bin/bash
+set -euo pipefail
+
+# Jaypore CI run script
+# ---------------------
+# This script is executed by Jaypore CI.
+#
+# Available environment variables:
+# JCI_COMMIT - The git commit being tested
+# JCI_REPO_ROOT - Absolute path to the repository root
+# JCI_OUTPUT_DIR - Directory for CI artifacts (cwd at start)
+#
+# Any files written to JCI_OUTPUT_DIR become CI artifacts.
+
+echo "=== Jaypore CI: Pylint + Pytest + Coverage ==="
+echo "Commit : $JCI_COMMIT"
+echo "Repo : $JCI_REPO_ROOT"
+echo "Output : $JCI_OUTPUT_DIR"
+echo
+
+cd "$JCI_REPO_ROOT"
+
+# ---- 1. Pylint ----
+echo "--- Running Pylint on core/ ---"
+DJANGO_SETTINGS_MODULE=mysite.settings \
+pylint core/ \
+ --output-format=text \
+ --disable=C0114,C0115,C0116 \
+ | tee "$JCI_OUTPUT_DIR/pylint-report.txt" \
+ || true # don't fail the build on lint warnings
+echo
+
+# ---- 2. Pytest + Coverage ----
+echo "--- Running Pytest with Coverage ---"
+pytest \
+ --cov=core \
+ --cov-report=html:"$JCI_OUTPUT_DIR/htmlcov" \
+ --cov-report=term \
+ | tee "$JCI_OUTPUT_DIR/pytest-results.txt"
+echo
+
+# ---- 3. Summary ----
+echo "=== Summary ==="
+echo "Pylint report : pylint-report.txt"
+echo "Pytest output : pytest-results.txt"
+echo "Coverage HTML : htmlcov/index.html"
+echo "All artifacts are in $JCI_OUTPUT_DIR"
+```
+
+### Docker Compose Api Tests
```
-02-docker-compose-api-tests/
-├── .jci/
+$ tree .
+.
+├── .jci
│ └── run.sh
├── Dockerfile
├── README.md
├── docker-compose.yml
└── test_api.sh
-```
-- [examples/02-docker-compose-api-tests/.jci/run.sh](/releases/files/examples/02-docker-compose-api-tests/.jci/run.sh)
-- [examples/02-docker-compose-api-tests/Dockerfile](/releases/files/examples/02-docker-compose-api-tests/Dockerfile)
-- [examples/02-docker-compose-api-tests/README.md](/releases/files/examples/02-docker-compose-api-tests/README.md)
-- [examples/02-docker-compose-api-tests/docker-compose.yml](/releases/files/examples/02-docker-compose-api-tests/docker-compose.yml)
-- [examples/02-docker-compose-api-tests/test_api.sh](/releases/files/examples/02-docker-compose-api-tests/test_api.sh)
-### Midnight Build with Telegram Notifications
+$ cat .jci/run.sh
+#!/bin/bash
+set -e
+
+echo "=== Jaypore CI: Docker Compose API Tests ==="
+echo "Commit: $JCI_COMMIT"
+echo "Repo root: $JCI_REPO_ROOT"
+echo "Output dir: $JCI_OUTPUT_DIR"
+
+PROJECT_DIR="$JCI_REPO_ROOT/02-docker-compose-api-tests"
+cd "$PROJECT_DIR"
+
+COMPOSE_PROJECT="jci-api-tests-$$"
+
+cleanup() {
+ echo "=== Cleaning up ==="
+ docker compose -p "$COMPOSE_PROJECT" down -v --remove-orphans 2>/dev/null || true
+}
+trap cleanup EXIT
+
+# ---- Start services ----
+echo "=== Starting services ==="
+docker compose -p "$COMPOSE_PROJECT" up -d --build 2>&1 | tee "$JCI_OUTPUT_DIR/compose-up.log"
+
+# ---- Wait for web service to respond ----
+echo "=== Waiting for web service ==="
+MAX_WAIT=90
+ELAPSED=0
+
+# Find the mapped port for web:8000
+while [ $ELAPSED -lt $MAX_WAIT ]; do
+ WEB_PORT=$(docker compose -p "$COMPOSE_PROJECT" port web 8000 2>/dev/null | cut -d: -f2 || true)
+ if [ -n "$WEB_PORT" ]; then
+ # Check if web responds
+ if curl -sf "http://localhost:$WEB_PORT/health/" >/dev/null 2>&1; then
+ echo " Web service healthy on port $WEB_PORT!"
+ break
+ fi
+ fi
+ echo " [$ELAPSED s] waiting..."
+ sleep 5
+ ELAPSED=$((ELAPSED + 5))
+done
+
+if [ $ELAPSED -ge $MAX_WAIT ]; then
+ echo "ERROR: Services did not become healthy within ${MAX_WAIT}s"
+ docker compose -p "$COMPOSE_PROJECT" ps 2>&1 | tee "$JCI_OUTPUT_DIR/compose-ps.log"
+ docker compose -p "$COMPOSE_PROJECT" logs 2>&1 | tee "$JCI_OUTPUT_DIR/compose-logs.log"
+ exit 1
+fi
+
+BASE_URL="http://localhost:${WEB_PORT}"
+echo "=== Web service at $BASE_URL ==="
+
+# ---- Run API tests ----
+echo "=== Running API tests ==="
+bash "$PROJECT_DIR/test_api.sh" "$BASE_URL" "$JCI_OUTPUT_DIR" 2>&1 \
+ | tee "$JCI_OUTPUT_DIR/test-output.log"
+TEST_EXIT=${PIPESTATUS[0]}
+
+# ---- Capture logs for artifacts ----
+docker compose -p "$COMPOSE_PROJECT" logs 2>&1 > "$JCI_OUTPUT_DIR/compose-logs.log"
+
+echo "=== CI Complete (exit $TEST_EXIT) ==="
+exit $TEST_EXIT
+```
+
+### Midnight Build Telegram
```
-03-midnight-build-telegram/
-├── .jci/
+$ tree .
+.
+├── .jci
│ ├── crontab
│ └── run.sh
└── README.md
-```
-- [examples/03-midnight-build-telegram/.jci/crontab](/releases/files/examples/03-midnight-build-telegram/.jci/crontab)
-- [examples/03-midnight-build-telegram/.jci/run.sh](/releases/files/examples/03-midnight-build-telegram/.jci/run.sh)
-- [examples/03-midnight-build-telegram/README.md](/releases/files/examples/03-midnight-build-telegram/README.md)
-### TruffleHog Secret Scan
+$ cat .jci/run.sh
+#!/bin/bash
+set -o pipefail
+
+cd "$JCI_REPO_ROOT" || exit 1
+
+REPO_NAME=$(basename "$JCI_REPO_ROOT")
+TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
+SHORT_COMMIT=$(echo "$JCI_COMMIT" | head -c 7)
+
+# ── Run Django tests ─────────────────────────────────────────
+TEST_OUTPUT=$(python3 manage.py test core 2>&1)
+TEST_EXIT=$?
+
+# ── Save results to output dir ───────────────────────────────
+echo "$TEST_OUTPUT" > "$JCI_OUTPUT_DIR/test_output.txt"
+echo "$TEST_EXIT" > "$JCI_OUTPUT_DIR/exit_code.txt"
+
+if [ "$TEST_EXIT" -eq 0 ]; then
+ STATUS="✅ PASSED"
+else
+ STATUS="❌ FAILED"
+fi
+
+# ── Send Telegram notification ───────────────────────────────
+MESSAGE=$(cat <<EOF
+*Midnight Build Report*
+
+Repo: \`${REPO_NAME}\`
+Commit: \`${SHORT_COMMIT}\`
+Status: ${STATUS}
+Timestamp: ${TIMESTAMP}
+EOF
+)
+
+if [ -n "$TELEGRAM_BOT_TOKEN" ] && [ -n "$TELEGRAM_CHAT_ID" ]; then
+ curl -s -X POST \
+ "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
+ -d chat_id="$TELEGRAM_CHAT_ID" \
+ -d text="$MESSAGE" \
+ -d parse_mode="Markdown" \
+ > /dev/null
+else
+ echo "WARNING: TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID not set, skipping notification"
+fi
+
+exit "$TEST_EXIT"
+```
+
+### Trufflehog Scan
```
-04-trufflehog-scan/
-├── .jci/
+$ tree .
+.
+├── .jci
│ ├── crontab
│ └── run.sh
└── README.md
-```
-- [examples/04-trufflehog-scan/.jci/crontab](/releases/files/examples/04-trufflehog-scan/.jci/crontab)
-- [examples/04-trufflehog-scan/.jci/run.sh](/releases/files/examples/04-trufflehog-scan/.jci/run.sh)
-- [examples/04-trufflehog-scan/README.md](/releases/files/examples/04-trufflehog-scan/README.md)
-### 05 — Lint & Fix on Pre-Commit
+$ cat .jci/run.sh
+#!/bin/bash
+set -o pipefail
+
+cd "$JCI_REPO_ROOT" || exit 1
+
+REPO_NAME=$(basename "$JCI_REPO_ROOT")
+SHORT_COMMIT=$(echo "$JCI_COMMIT" | head -c 7)
+REPORT="$JCI_OUTPUT_DIR/trufflehog-report.txt"
+
+echo "=== TruffleHog Secret Scan ==="
+echo "Repo: $REPO_NAME"
+echo "Commit: $SHORT_COMMIT"
+echo "Time: $(date '+%Y-%m-%d %H:%M:%S')"
+echo
+
+# ── Run trufflehog3 scan (current working tree, no history) ──
+echo "Scanning current working tree..."
+trufflehog3 --no-history . > "$REPORT" 2>&1 || true
+
+echo "Scanning commit history..."
+trufflehog3 --no-current . >> "$REPORT" 2>&1 || true
+
+# ── Report findings ──────────────────────────────────────────
+if [ -s "$REPORT" ]; then
+ FINDINGS=$(grep -c 'MEDIUM\|HIGH\|CRITICAL' "$REPORT" 2>/dev/null || echo "0")
+ echo "⚠️ Found $FINDINGS potential issue(s). See report:"
+ cat "$REPORT"
+ echo
+ echo "Report saved to trufflehog-report.txt"
+ # In production you might: exit 1
+ # For this example, we report but don't fail
+else
+ echo "✅ No secrets found."
+fi
+
+echo "=== Scan Complete ==="
+exit 0
+```
+
+### Lint Fix Precommit
```
-05-lint-fix-precommit/
-├── .jci/
+$ tree .
+.
+├── .jci
│ └── run.sh
├── README.md
└── install-hook.sh
-```
-- [examples/05-lint-fix-precommit/.jci/run.sh](/releases/files/examples/05-lint-fix-precommit/.jci/run.sh)
-- [examples/05-lint-fix-precommit/README.md](/releases/files/examples/05-lint-fix-precommit/README.md)
-- [examples/05-lint-fix-precommit/install-hook.sh](/releases/files/examples/05-lint-fix-precommit/install-hook.sh)
-### 06 — Sub-pipelines
+$ cat .jci/run.sh
+#!/bin/bash
+set -euo pipefail
+
+# Jaypore CI: Lint & fix for a multi-language repo (Python/Go/JS)
+# Intended to run as a pre-commit check via `git jci run`.
+#
+# Environment provided by Jaypore CI:
+# JCI_COMMIT - the commit being checked
+# JCI_REPO_ROOT - root of the git repository
+# JCI_OUTPUT_DIR - directory for build artifacts (cwd at start)
+
+RESULTS="${JCI_OUTPUT_DIR}/lint-results.txt"
+: > "$RESULTS"
+
+cd "$JCI_REPO_ROOT"
+
+overall_status=0
+
+# ── Python ──────────────────────────────────────────────────────────
+echo "=== Python lint ===" | tee -a "$RESULTS"
+
+# Syntax check every .py file
+py_files=$(find . -name '*.py' -not -path './.git/*' -not -path './node_modules/*' || true)
+if [ -n "$py_files" ]; then
+ py_errors=0
+ while IFS= read -r f; do
+ if ! python3 -m py_compile "$f" 2>>"$RESULTS"; then
+ py_errors=$((py_errors + 1))
+ fi
+ done <<< "$py_files"
+
+ if [ "$py_errors" -gt 0 ]; then
+ echo "FAIL: $py_errors Python file(s) have syntax errors" | tee -a "$RESULTS"
+ overall_status=1
+ else
+ echo "OK: all Python files pass syntax check" | tee -a "$RESULTS"
+ fi
+
+ # Formatter check (black)
+ if command -v black &>/dev/null; then
+ echo "--- black --check ---" | tee -a "$RESULTS"
+ if ! black --check . 2>&1 | tee -a "$RESULTS"; then
+ echo "FAIL: black found files that need reformatting" | tee -a "$RESULTS"
+ echo " Run 'black .' to fix, then re-commit." | tee -a "$RESULTS"
+ overall_status=1
+ else
+ echo "OK: black is happy" | tee -a "$RESULTS"
+ fi
+ else
+ echo "SKIP: black not installed" | tee -a "$RESULTS"
+ fi
+else
+ echo "SKIP: no .py files found" | tee -a "$RESULTS"
+fi
+
+# ── Go ──────────────────────────────────────────────────────────────
+echo "" | tee -a "$RESULTS"
+echo "=== Go lint ===" | tee -a "$RESULTS"
+
+go_files=$(find . -name '*.go' -not -path './.git/*' -not -path './vendor/*' || true)
+if [ -n "$go_files" ]; then
+ if command -v gofmt &>/dev/null; then
+ unformatted=$(gofmt -l . 2>&1 || true)
+ if [ -n "$unformatted" ]; then
+ echo "FAIL: the following Go files need formatting:" | tee -a "$RESULTS"
+ echo "$unformatted" | tee -a "$RESULTS"
+ echo " Run 'gofmt -w .' to fix, then re-commit." | tee -a "$RESULTS"
+ overall_status=1
+ else
+ echo "OK: all Go files are formatted" | tee -a "$RESULTS"
+ fi
+ else
+ echo "SKIP: gofmt not installed" | tee -a "$RESULTS"
+ fi
+else
+ echo "SKIP: no .go files found" | tee -a "$RESULTS"
+fi
+
+# ── JavaScript ──────────────────────────────────────────────────────
+echo "" | tee -a "$RESULTS"
+echo "=== JavaScript lint ===" | tee -a "$RESULTS"
+
+js_files=$(find . -name '*.js' -not -path './.git/*' -not -path './node_modules/*' || true)
+if [ -n "$js_files" ]; then
+ if [ -f package.json ] && command -v npx &>/dev/null; then
+ echo "--- eslint --fix ---" | tee -a "$RESULTS"
+ # --fix rewrites files in place; the pre-commit hook should
+ # stage the corrections automatically.
+ if ! npx eslint --fix . 2>&1 | tee -a "$RESULTS"; then
+ echo "FAIL: eslint reported errors that could not be auto-fixed" | tee -a "$RESULTS"
+ overall_status=1
+ else
+ echo "OK: eslint passed (auto-fixable issues were corrected)" | tee -a "$RESULTS"
+ fi
+ else
+ echo "SKIP: npx/package.json not available" | tee -a "$RESULTS"
+ fi
+else
+ echo "SKIP: no .js files found" | tee -a "$RESULTS"
+fi
+
+# ── Summary ─────────────────────────────────────────────────────────
+echo "" | tee -a "$RESULTS"
+if [ "$overall_status" -eq 0 ]; then
+ echo "✅ All lint checks passed." | tee -a "$RESULTS"
+else
+ echo "❌ Some lint checks failed. See above for details." | tee -a "$RESULTS"
+fi
+
+exit "$overall_status"
+```
+
+### Sub Pipelines
```
-06-sub-pipelines/
-├── .jci/
+$ tree .
+.
+├── .jci
│ └── run.sh
-├── README.md
-├── go-app/
+├── go-app
│ ├── go-app
│ ├── go.mod
│ ├── main.go
│ └── main_test.go
-├── js-app/
+├── js-app
│ ├── index.js
│ └── package.json
-└── python-app/
- ├── app.py
- └── test_app.py
-```
+├── python-app
+│ ├── app.py
+│ └── test_app.py
+└── README.md
-- [examples/06-sub-pipelines/.jci/run.sh](/releases/files/examples/06-sub-pipelines/.jci/run.sh)
-- [examples/06-sub-pipelines/README.md](/releases/files/examples/06-sub-pipelines/README.md)
-- [examples/06-sub-pipelines/go-app/go-app](/releases/files/examples/06-sub-pipelines/go-app/go-app)
-- [examples/06-sub-pipelines/go-app/go.mod](/releases/files/examples/06-sub-pipelines/go-app/go.mod)
-- [examples/06-sub-pipelines/go-app/main.go](/releases/files/examples/06-sub-pipelines/go-app/main.go)
-- [examples/06-sub-pipelines/go-app/main_test.go](/releases/files/examples/06-sub-pipelines/go-app/main_test.go)
-- [examples/06-sub-pipelines/js-app/index.js](/releases/files/examples/06-sub-pipelines/js-app/index.js)
-- [examples/06-sub-pipelines/js-app/package.json](/releases/files/examples/06-sub-pipelines/js-app/package.json)
-- [examples/06-sub-pipelines/python-app/app.py](/releases/files/examples/06-sub-pipelines/python-app/app.py)
-- [examples/06-sub-pipelines/python-app/test_app.py](/releases/files/examples/06-sub-pipelines/python-app/test_app.py)
-### Example 07 — Secrets with SOPS + Telegram
+$ cat .jci/run.sh
+#!/bin/bash
+set -euo pipefail
+
+# 06-sub-pipelines: Run CI steps only for parts of the monorepo that changed.
+#
+# Detects which top-level folders were modified in the latest commit and
+# launches the matching sub-pipeline (python / js / go). When no diff is
+# available (e.g. the very first commit) every pipeline runs.
+
+cd "$JCI_REPO_ROOT"
+
+# ── Detect changed folders ───────────────────────────────────────────
+changed_files=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || true)
+
+run_python=false
+run_js=false
+run_go=false
+
+if [ -z "$changed_files" ]; then
+ echo "No diff detected (first commit or shallow clone) — running all sub-pipelines."
+ run_python=true
+ run_js=true
+ run_go=true
+else
+ echo "Changed files:"
+ echo "$changed_files" | sed 's/^/ /'
+ echo
+
+ if echo "$changed_files" | grep -q '^06-sub-pipelines/python-app/'; then
+ run_python=true
+ fi
+ if echo "$changed_files" | grep -q '^06-sub-pipelines/js-app/'; then
+ run_js=true
+ fi
+ if echo "$changed_files" | grep -q '^06-sub-pipelines/go-app/'; then
+ run_go=true
+ fi
+fi
+
+pipelines_ran=0
+summary=""
+
+# ── Python sub-pipeline ──────────────────────────────────────────────
+if $run_python; then
+ echo "═══ Python sub-pipeline ═══"
+ result_file="$JCI_OUTPUT_DIR/python-results.txt"
+ {
+ echo "Python sub-pipeline results"
+ echo "Commit: $JCI_COMMIT"
+ echo "Run at: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
+ echo
+
+ # Lint
+ echo "── Lint ──"
+ if [ -d 06-sub-pipelines/python-app ] && command -v python3 &>/dev/null; then
+ if command -v ruff &>/dev/null; then
+ echo "Running: ruff check 06-sub-pipelines/python-app/"
+ ruff check 06-sub-pipelines/python-app/ 2>&1 && echo "Lint: PASS" || echo "Lint: FAIL"
+ elif command -v flake8 &>/dev/null; then
+ echo "Running: flake8 06-sub-pipelines/python-app/"
+ flake8 06-sub-pipelines/python-app/ 2>&1 && echo "Lint: PASS" || echo "Lint: FAIL"
+ else
+ echo "No Python linter found (ruff/flake8) — checking syntax only."
+ find 06-sub-pipelines/python-app -name '*.py' -exec python3 -m py_compile {} + 2>&1 \
+ && echo "Syntax check: PASS" || echo "Syntax check: FAIL"
+ fi
+ else
+ echo "06-sub-pipelines/python-app/ directory or python3 not found — skipped."
+ fi
+ echo
+
+ # Tests
+ echo "── Tests ──"
+ if [ -d 06-sub-pipelines/python-app ] && command -v python3 &>/dev/null; then
+ if [ -f 06-sub-pipelines/python-app/requirements.txt ]; then
+ echo "Installing dependencies…"
+ pip install -q -r 06-sub-pipelines/python-app/requirements.txt 2>&1 || true
+ fi
+ echo "Running: python3 -m pytest 06-sub-pipelines/python-app/"
+ python3 -m pytest 06-sub-pipelines/python-app/ 2>&1 && echo "Tests: PASS" || echo "Tests: FAIL"
+ else
+ echo "06-sub-pipelines/python-app/ directory or python3 not found — skipped."
+ fi
+ } | tee "$result_file"
+
+ pipelines_ran=$((pipelines_ran + 1))
+ summary="${summary} ✔ python-app\n"
+ echo
+fi
+
+# ── JS sub-pipeline ──────────────────────────────────────────────────
+if $run_js; then
+ echo "═══ JS sub-pipeline ═══"
+ result_file="$JCI_OUTPUT_DIR/js-results.txt"
+ {
+ echo "JS sub-pipeline results"
+ echo "Commit: $JCI_COMMIT"
+ echo "Run at: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
+ echo
+
+ # Lint
+ echo "── Lint ──"
+ if [ -d 06-sub-pipelines/js-app ]; then
+ if [ -f 06-sub-pipelines/js-app/package.json ] && command -v npm &>/dev/null; then
+ echo "Installing dependencies…"
+ (cd 06-sub-pipelines/js-app && npm ci --ignore-scripts 2>&1) || true
+ if [ -x 06-sub-pipelines/js-app/node_modules/.bin/eslint ]; then
+ echo "Running: eslint 06-sub-pipelines/js-app/"
+ 06-sub-pipelines/js-app/node_modules/.bin/eslint 06-sub-pipelines/js-app/ 2>&1 \
+ && echo "Lint: PASS" || echo "Lint: FAIL"
+ else
+ echo "eslint not installed — listing JS files instead."
+ find 06-sub-pipelines/js-app -name '*.js' -o -name '*.ts' | head -20
+ echo "Lint: SKIPPED"
+ fi
+ else
+ echo "No package.json or npm not available — checking syntax with node."
+ if command -v node &>/dev/null; then
+ find 06-sub-pipelines/js-app -name '*.js' -exec node --check {} \; 2>&1 \
+ && echo "Syntax check: PASS" || echo "Syntax check: FAIL"
+ else
+ echo "node not found — skipped."
+ fi
+ fi
+ else
+ echo "06-sub-pipelines/js-app/ directory not found — skipped."
+ fi
+ } | tee "$result_file"
+
+ pipelines_ran=$((pipelines_ran + 1))
+ summary="${summary} ✔ js-app\n"
+ echo
+fi
+
+# ── Go sub-pipeline ──────────────────────────────────────────────────
+if $run_go; then
+ echo "═══ Go sub-pipeline ═══"
+ result_file="$JCI_OUTPUT_DIR/go-results.txt"
+ {
+ echo "Go sub-pipeline results"
+ echo "Commit: $JCI_COMMIT"
+ echo "Run at: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
+ echo
+
+ # Build & test
+ echo "── Build & Test ──"
+ if [ -d 06-sub-pipelines/go-app ]; then
+ if command -v go &>/dev/null; then
+ echo "Running: go build ./06-sub-pipelines/go-app/..."
+ (cd 06-sub-pipelines/go-app && go build ./...) 2>&1 && echo "Build: PASS" || echo "Build: FAIL"
+ echo
+ echo "Running: go test ./06-sub-pipelines/go-app/..."
+ (cd 06-sub-pipelines/go-app && go test ./...) 2>&1 && echo "Test: PASS" || echo "Test: FAIL"
+ else
+ echo "go not found — skipped."
+ fi
+ else
+ echo "06-sub-pipelines/go-app/ directory not found — skipped."
+ fi
+ } | tee "$result_file"
+
+ pipelines_ran=$((pipelines_ran + 1))
+ summary="${summary} ✔ go-app\n"
+ echo
+fi
+
+# ── Summary ──────────────────────────────────────────────────────────
+echo "═══════════════════════════════════════"
+echo "Sub-pipeline summary ($pipelines_ran ran)"
+echo "═══════════════════════════════════════"
+
+if [ $pipelines_ran -eq 0 ]; then
+ echo " No sub-pipelines matched the changed files."
+else
+ printf "$summary"
+fi
+
+echo
+echo "Result files in $JCI_OUTPUT_DIR:"
+ls -1 "$JCI_OUTPUT_DIR"/*.txt 2>/dev/null || echo " (none)"
+```
+
+### Secrets Telegram
```
-07-secrets-telegram/
-├── .jci/
+$ tree .
+.
+├── .jci
│ └── run.sh
├── README.md
└── secrets.example.json
-```
-- [examples/07-secrets-telegram/.jci/run.sh](/releases/files/examples/07-secrets-telegram/.jci/run.sh)
-- [examples/07-secrets-telegram/README.md](/releases/files/examples/07-secrets-telegram/README.md)
-- [examples/07-secrets-telegram/secrets.example.json](/releases/files/examples/07-secrets-telegram/secrets.example.json)
-### 08 — Mail on failure
+$ cat .jci/run.sh
+#!/bin/bash
+set -o pipefail
+
+# Jaypore CI run script
+# ---------------------
+# This script is executed by Jaypore CI.
+#
+# Available environment variables:
+# JCI_COMMIT - The git commit being tested
+# JCI_REPO_ROOT - Absolute path to the repository root
+# JCI_OUTPUT_DIR - Directory for CI artifacts (cwd at start)
+#
+# This example demonstrates managing secrets with Mozilla SOPS.
+# Secrets are stored encrypted in the repo and decrypted at CI time.
+
+echo "=== Jaypore CI: Secrets + Telegram ==="
+echo "Commit : $JCI_COMMIT"
+echo "Repo : $JCI_REPO_ROOT"
+echo "Output : $JCI_OUTPUT_DIR"
+echo
+
+cd "$JCI_REPO_ROOT" || exit 1
+
+REPO_NAME=$(basename "$JCI_REPO_ROOT")
+SHORT_COMMIT=$(echo "$JCI_COMMIT" | head -c 7)
+TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
+
+# ── Load secrets ─────────────────────────────────────────────
+# Strategy:
+# 1. If `sops` is installed and secrets.enc.json exists, decrypt it.
+# 2. Otherwise fall back to plain environment variables.
+
+load_secrets_from_sops() {
+ local secrets_file="secrets.enc.json"
+ if [ ! -f "$secrets_file" ]; then
+ echo "WARNING: $secrets_file not found, skipping SOPS decryption"
+ return 1
+ fi
+ echo "--- Decrypting secrets with SOPS ---"
+ local decrypted
+ decrypted=$(sops -d "$secrets_file" 2>&1)
+ if [ $? -ne 0 ]; then
+ echo "ERROR: sops decryption failed:"
+ echo "$decrypted"
+ return 1
+ fi
+ # Extract values from the decrypted JSON
+ TELEGRAM_BOT_TOKEN=$(echo "$decrypted" | python3 -c "import sys,json; print(json.load(sys.stdin)['TELEGRAM_BOT_TOKEN'])")
+ TELEGRAM_CHAT_ID=$(echo "$decrypted" | python3 -c "import sys,json; print(json.load(sys.stdin)['TELEGRAM_CHAT_ID'])")
+ export TELEGRAM_BOT_TOKEN TELEGRAM_CHAT_ID
+ echo "Secrets loaded from $secrets_file"
+ return 0
+}
+
+if command -v sops &> /dev/null; then
+ load_secrets_from_sops || echo "Falling back to environment variables"
+else
+ echo "--- SOPS not installed ---"
+ echo "Install it to use encrypted secrets:"
+ echo " # Debian/Ubuntu"
+ echo " curl -LO https://github.com/getsops/sops/releases/download/v3.9.4/sops_3.9.4_amd64.deb"
+ echo " sudo dpkg -i sops_3.9.4_amd64.deb"
+ echo ""
+ echo " # macOS"
+ echo " brew install sops"
+ echo ""
+ echo "Falling back to environment variables"
+fi
+
+# ── Run Django tests ─────────────────────────────────────────
+echo
+echo "--- Running Django tests ---"
+TEST_OUTPUT=$(python3 manage.py test core 2>&1)
+TEST_EXIT=$?
+
+echo "$TEST_OUTPUT"
+echo "$TEST_OUTPUT" > "$JCI_OUTPUT_DIR/test_output.txt"
+echo "$TEST_EXIT" > "$JCI_OUTPUT_DIR/exit_code.txt"
+
+if [ "$TEST_EXIT" -eq 0 ]; then
+ STATUS="✅ PASSED"
+else
+ STATUS="❌ FAILED"
+fi
+
+# ── Send Telegram notification ───────────────────────────────
+MESSAGE=$(cat <<EOF
+*CI Build — Secrets Example*
+
+Repo: \`${REPO_NAME}\`
+Commit: \`${SHORT_COMMIT}\`
+Status: ${STATUS}
+Time: ${TIMESTAMP}
+EOF
+)
+
+if [ -n "$TELEGRAM_BOT_TOKEN" ] && [ -n "$TELEGRAM_CHAT_ID" ]; then
+ echo
+ echo "--- Sending Telegram notification ---"
+ RESPONSE=$(curl -s -X POST \
+ "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
+ -d chat_id="$TELEGRAM_CHAT_ID" \
+ -d text="$MESSAGE" \
+ -d parse_mode="Markdown")
+ echo "$RESPONSE" > "$JCI_OUTPUT_DIR/telegram_response.json"
+ echo "Notification sent."
+else
+ echo
+ echo "WARNING: TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID not set, skipping notification"
+fi
+
+# ── Summary ──────────────────────────────────────────────────
+echo
+echo "=== Summary ==="
+echo "Test result : $STATUS"
+echo "Test output : test_output.txt"
+echo "Exit code : exit_code.txt"
+echo "All artifacts in $JCI_OUTPUT_DIR"
+
+exit "$TEST_EXIT"
+```
+
+### Mail On Failure
```
-08-mail-on-failure/
-├── .jci/
+$ tree .
+.
+├── .jci
│ ├── crontab
│ └── run.sh
└── README.md
-```
-- [examples/08-mail-on-failure/.jci/crontab](/releases/files/examples/08-mail-on-failure/.jci/crontab)
-- [examples/08-mail-on-failure/.jci/run.sh](/releases/files/examples/08-mail-on-failure/.jci/run.sh)
-- [examples/08-mail-on-failure/README.md](/releases/files/examples/08-mail-on-failure/README.md)
-### Auto-Update Dependencies
+$ cat .jci/run.sh
+#!/bin/bash
+set -o pipefail
+
+# --- Send mail via Python smtplib ---
+send_mail() {
+ local subject="$1"
+ local body="$2"
+ python3 - "$subject" "$body" <<'PYMAIL'
+import sys, smtplib, os
+from email.mime.text import MIMEText
+
+subject = sys.argv[1]
+body = sys.argv[2]
+
+msg = MIMEText(body)
+msg["Subject"] = subject
+msg["From"] = os.environ["MAIL_FROM"]
+msg["To"] = os.environ["MAIL_TO"]
+
+with smtplib.SMTP(os.environ["SMTP_HOST"], int(os.environ["SMTP_PORT"])) as srv:
+ srv.starttls()
+ srv.login(os.environ["SMTP_USER"], os.environ["SMTP_PASS"])
+ srv.sendmail(msg["From"], [msg["To"]], msg.as_string())
+
+print("Mail sent.")
+PYMAIL
+}
+
+# --- Run tests ---
+cd "$JCI_REPO_ROOT"
+repo_name=$(basename "$JCI_REPO_ROOT")
+timestamp=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
+
+python3 manage.py test core 2>&1 | tee "$JCI_OUTPUT_DIR/test-output.txt"
+exit_code=${PIPESTATUS[0]}
+
+# --- Notify on failure ---
+if [ "$exit_code" -ne 0 ]; then
+ subject="CI failure: ${repo_name} @ ${JCI_COMMIT:0:8}"
+ body="Repository : ${repo_name}
+Commit : ${JCI_COMMIT}
+Timestamp : ${timestamp}
+Exit code : ${exit_code}
+
+--- Test output (last 80 lines) ---
+$(tail -n 80 "$JCI_OUTPUT_DIR/test-output.txt")"
+
+ if [ -n "${SMTP_HOST:-}" ] && [ -n "${MAIL_FROM:-}" ] && [ -n "${MAIL_TO:-}" ]; then
+ send_mail "$subject" "$body"
+ else
+ echo "WARNING: SMTP_HOST, MAIL_FROM, or MAIL_TO not set — skipping email notification"
+ fi
+fi
+
+exit "$exit_code"
```
-09-auto-update-deps/
-├── .jci/
+
+### Auto Update Deps
+
+```
+$ tree .
+.
+├── .jci
│ ├── crontab
│ └── run.sh
└── README.md
-```
-- [examples/09-auto-update-deps/.jci/crontab](/releases/files/examples/09-auto-update-deps/.jci/crontab)
-- [examples/09-auto-update-deps/.jci/run.sh](/releases/files/examples/09-auto-update-deps/.jci/run.sh)
-- [examples/09-auto-update-deps/README.md](/releases/files/examples/09-auto-update-deps/README.md)
-### Example 10 — Build & Publish Docker Images
+$ cat .jci/run.sh
+#!/bin/bash
+set -euo pipefail
+
+# ── Auto-Update Dependencies ─────────────────────────────────────────
+# Scheduled at midnight via .jci/crontab.
+# Updates all pip packages, runs the test suite, and reports results.
+# ─────────────────────────────────────────────────────────────────────
+
+cd "$JCI_REPO_ROOT"
+
+echo "==> Snapshot current dependency versions"
+pip3 freeze 2>/dev/null > "$JCI_OUTPUT_DIR/before-update.txt"
+
+echo "==> Upgrading packages from requirements.txt"
+pip3 install --break-system-packages --upgrade -r requirements.txt 2>&1 | tee "$JCI_OUTPUT_DIR/upgrade-log.txt" || true
+
+echo "==> Snapshot new dependency versions"
+pip3 freeze 2>/dev/null > "$JCI_OUTPUT_DIR/after-update.txt"
+
+echo "==> Dependency diff"
+if diff "$JCI_OUTPUT_DIR/before-update.txt" "$JCI_OUTPUT_DIR/after-update.txt" \
+ > "$JCI_OUTPUT_DIR/dep-diff.txt" 2>&1; then
+ echo "No dependency changes."
+else
+ echo "Changed packages:"
+ cat "$JCI_OUTPUT_DIR/dep-diff.txt"
+fi
+
+echo "==> Running test suite"
+if python3 manage.py test core --no-input 2>&1 | tee "$JCI_OUTPUT_DIR/test-results.txt"; then
+ echo "==> Tests passed after update"
+
+ # Freeze the verified versions back into requirements.txt
+ # In a real setup, you'd commit updated requirements:
+ # pip3 freeze > requirements.txt && git add requirements.txt && git commit -m "chore: auto-update deps"
+ echo "Dependencies verified — would commit in a real workflow"
+
+ echo "SUCCESS" > "$JCI_OUTPUT_DIR/status.txt"
+else
+ echo "==> Tests FAILED after update"
+ echo "The following packages were updated:" > "$JCI_OUTPUT_DIR/failure-report.txt"
+ cat "$JCI_OUTPUT_DIR/dep-diff.txt" >> "$JCI_OUTPUT_DIR/failure-report.txt"
+ echo "" >> "$JCI_OUTPUT_DIR/failure-report.txt"
+ echo "Test output:" >> "$JCI_OUTPUT_DIR/failure-report.txt"
+ cat "$JCI_OUTPUT_DIR/test-results.txt" >> "$JCI_OUTPUT_DIR/failure-report.txt"
+
+ echo "Failure report:"
+ cat "$JCI_OUTPUT_DIR/failure-report.txt"
+
+ echo "FAILURE" > "$JCI_OUTPUT_DIR/status.txt"
+ exit 1
+fi
+```
+
+### Build Publish Docker
```
-10-build-publish-docker/
-├── .jci/
+$ tree .
+.
+├── .jci
│ └── run.sh
├── Dockerfile
└── README.md
-```
-- [examples/10-build-publish-docker/.jci/run.sh](/releases/files/examples/10-build-publish-docker/.jci/run.sh)
-- [examples/10-build-publish-docker/Dockerfile](/releases/files/examples/10-build-publish-docker/Dockerfile)
-- [examples/10-build-publish-docker/README.md](/releases/files/examples/10-build-publish-docker/README.md)
-### 11 — Upstream Trigger
+$ cat .jci/run.sh
+#!/bin/bash
+set -euo pipefail
+
+# Jaypore CI run script
+# ---------------------
+# This script is executed by Jaypore CI.
+#
+# Available environment variables:
+# JCI_COMMIT - The git commit being tested
+# JCI_REPO_ROOT - Absolute path to the repository root
+# JCI_OUTPUT_DIR - Directory for CI artifacts (cwd at start)
+#
+# Optional environment variables:
+# DOCKER_REGISTRY - Registry to push to (e.g. registry.example.com)
+# DOCKER_USERNAME - Registry login username
+# DOCKER_PASSWORD - Registry login password
+#
+# Any files written to JCI_OUTPUT_DIR become CI artifacts.
+
+echo "=== Jaypore CI: Build & Publish Docker Image ==="
+echo "Commit : $JCI_COMMIT"
+echo "Repo : $JCI_REPO_ROOT"
+echo "Output : $JCI_OUTPUT_DIR"
+echo
+
+cd "$JCI_REPO_ROOT"
+
+IMAGE_NAME="mysite"
+SHORT_SHA=$(echo "$JCI_COMMIT" | cut -c1-12)
+TAG_COMMIT="${IMAGE_NAME}:${SHORT_SHA}"
+TAG_LATEST="${IMAGE_NAME}:latest"
+
+# If a registry is configured, prefix the image name
+if [ -n "${DOCKER_REGISTRY:-}" ]; then
+ TAG_COMMIT="${DOCKER_REGISTRY}/${TAG_COMMIT}"
+ TAG_LATEST="${DOCKER_REGISTRY}/${TAG_LATEST}"
+fi
+
+echo "Image tags:"
+echo " $TAG_COMMIT"
+echo " $TAG_LATEST"
+echo
+
+# ---- 1. Build the Docker image ----
+echo "--- Building Docker image ---"
+docker build \
+ -f 10-build-publish-docker/Dockerfile \
+ -t "$TAG_COMMIT" \
+ -t "$TAG_LATEST" \
+ . \
+ 2>&1 | tee "$JCI_OUTPUT_DIR/docker-build.log"
+
+echo
+echo "Build complete."
+echo
+
+# ---- 2. Run tests inside the container ----
+echo "--- Running tests inside the container ---"
+docker run --rm "$TAG_COMMIT" \
+ python3 manage.py test core --verbosity=2 \
+ 2>&1 | tee "$JCI_OUTPUT_DIR/docker-test.log"
+
+echo
+echo "Tests passed."
+echo
+
+# ---- 3. Save image metadata ----
+echo "--- Saving image info ---"
+docker inspect "$TAG_COMMIT" > "$JCI_OUTPUT_DIR/image-inspect.json"
+docker images --filter "reference=${IMAGE_NAME}" \
+ --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Size}}' \
+ | tee "$JCI_OUTPUT_DIR/image-info.txt"
+echo
+
+# ---- 4. Push to registry (if configured) ----
+if [ -n "${DOCKER_REGISTRY:-}" ]; then
+ echo "--- Pushing to $DOCKER_REGISTRY ---"
+
+ if [ -n "${DOCKER_USERNAME:-}" ] && [ -n "${DOCKER_PASSWORD:-}" ]; then
+ echo "$DOCKER_PASSWORD" | docker login "$DOCKER_REGISTRY" \
+ -u "$DOCKER_USERNAME" --password-stdin \
+ 2>&1 | tee -a "$JCI_OUTPUT_DIR/docker-push.log"
+ fi
+
+ docker push "$TAG_COMMIT" 2>&1 | tee -a "$JCI_OUTPUT_DIR/docker-push.log"
+ docker push "$TAG_LATEST" 2>&1 | tee -a "$JCI_OUTPUT_DIR/docker-push.log"
+
+ echo
+ echo "Push complete."
+else
+ echo "--- DOCKER_REGISTRY not set, skipping push ---"
+fi
+
+echo
+
+# ---- 5. Summary ----
+echo "=== Summary ==="
+echo "Image : $TAG_COMMIT"
+echo "Build log : docker-build.log"
+echo "Test log : docker-test.log"
+echo "Image metadata : image-inspect.json"
+echo "Image info : image-info.txt"
+[ -n "${DOCKER_REGISTRY:-}" ] && echo "Push log : docker-push.log"
+echo "All artifacts are in $JCI_OUTPUT_DIR"
+```
+
+### Upstream Trigger
```
-11-upstream-trigger/
+$ tree .
+.
└── README.md
```
-- [examples/11-upstream-trigger/README.md](/releases/files/examples/11-upstream-trigger/README.md)
-
-### 12 — Downstream Trigger
+### Downstream Trigger
```
-12-downstream-trigger/
+$ tree .
+.
└── README.md
```
-- [examples/12-downstream-trigger/README.md](/releases/files/examples/12-downstream-trigger/README.md)
-
-### 13 — Jekyll + Netlify
+### Jekyll Netlify
```
-13-jekyll-netlify/
-├── .jci/
+$ tree .
+.
+├── .jci
│ └── run.sh
-├── README.md
-└── site/
- ├── _config.yml
- ├── _layouts/
- │ └── default.html
- └── index.md
-```
+├── site
+│ ├── _layouts
+│ │ └── default.html
+│ ├── _config.yml
+│ └── index.md
+└── README.md
+
-- [examples/13-jekyll-netlify/.jci/run.sh](/releases/files/examples/13-jekyll-netlify/.jci/run.sh)
-- [examples/13-jekyll-netlify/README.md](/releases/files/examples/13-jekyll-netlify/README.md)
-- [examples/13-jekyll-netlify/site/_config.yml](/releases/files/examples/13-jekyll-netlify/site/_config.yml)
-- [examples/13-jekyll-netlify/site/_layouts/default.html](/releases/files/examples/13-jekyll-netlify/site/_layouts/default.html)
-- [examples/13-jekyll-netlify/site/index.md](/releases/files/examples/13-jekyll-netlify/site/index.md)
+$ cat .jci/run.sh
+#!/bin/bash
+# ------------------------------------------------------------------
+# Jaypore CI — Build Jekyll site and publish to Netlify
+# ------------------------------------------------------------------
+set -euo pipefail
+
+LOG="${JCI_OUTPUT_DIR}/build.log"
+exec > >(tee -a "$LOG") 2>&1
+
+echo "=== Jekyll + Netlify CI ==="
+echo "Commit : ${JCI_COMMIT:-unknown}"
+echo "Repo : ${JCI_REPO_ROOT}"
+echo "Output : ${JCI_OUTPUT_DIR}"
+echo
+
+# ── 1. Navigate to site source ────────────────────────────────────
+SITE_DIR="${JCI_REPO_ROOT}/13-jekyll-netlify/site"
+cd "$SITE_DIR"
+echo "Working directory: $(pwd)"
+
+# ── 2. Ensure Jekyll is available ─────────────────────────────────
+if ! command -v jekyll &>/dev/null; then
+ echo "Jekyll not found — installing…"
+ sudo gem install jekyll bundler --no-document 2>&1 || true
+else
+ echo "Jekyll found: $(jekyll --version)"
+fi
+
+# ── 3. Build ──────────────────────────────────────────────────────
+DEST="${JCI_OUTPUT_DIR}/_site"
+echo
+echo "Building site → $DEST"
+jekyll build --destination "$DEST"
+
+if [ ! -d "$DEST" ]; then
+ echo "ERROR: Build failed — $DEST does not exist."
+ exit 1
+fi
+
+echo
+echo "Build succeeded. Output files:"
+find "$DEST" -type f | sort
+echo
+
+# ── 4. Deploy to Netlify (optional) ──────────────────────────────
+if [ -n "${NETLIFY_AUTH_TOKEN:-}" ] && [ -n "${NETLIFY_SITE_ID:-}" ]; then
+ echo "Deploying to Netlify (site ${NETLIFY_SITE_ID})…"
+
+ ZIP_FILE="${JCI_OUTPUT_DIR}/_site.zip"
+ (cd "$DEST" && zip -r "$ZIP_FILE" .)
+
+ HTTP_CODE=$(curl -s -o "${JCI_OUTPUT_DIR}/netlify_response.json" \
+ -w "%{http_code}" \
+ -H "Content-Type: application/zip" \
+ -H "Authorization: Bearer ${NETLIFY_AUTH_TOKEN}" \
+ --data-binary @"$ZIP_FILE" \
+ "https://api.netlify.com/api/v1/sites/${NETLIFY_SITE_ID}/deploys")
+
+ if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
+ echo "Netlify deploy succeeded (HTTP ${HTTP_CODE})."
+ else
+ echo "WARNING: Netlify deploy returned HTTP ${HTTP_CODE}."
+ cat "${JCI_OUTPUT_DIR}/netlify_response.json"
+ exit 1
+ fi
+else
+ echo "Skipping Netlify deploy: NETLIFY_AUTH_TOKEN or NETLIFY_SITE_ID not set"
+fi
+
+# ── 5. Summary ───────────────────────────────────────────────────
+echo
+echo "=== Summary ==="
+echo "Site source : $SITE_DIR"
+echo "Build output: $DEST"
+echo "Build log : $LOG"
+echo "Files built : $(find "$DEST" -type f | wc -l)"
+echo "Done."
+```
-### 14 — Build Docusaurus & Publish to S3
+### Docusaurus S3
```
-14-docusaurus-s3/
-├── .jci/
+$ tree .
+.
+├── .jci
│ └── run.sh
+├── docs
+│ └── index.md
├── README.md
-├── build.sh
-└── docs/
- └── index.md
+└── build.sh
+
+
+$ cat .jci/run.sh
+#!/bin/bash
+# Jaypore CI — Build Docusaurus (or simple static site) and publish to S3.
+#
+# Environment (provided by Jaypore CI):
+# JCI_COMMIT — current commit SHA
+# JCI_REPO_ROOT — repository root path
+# JCI_OUTPUT_DIR — directory for CI artifacts (cwd at start)
+#
+# Optional environment:
+# AWS_ACCESS_KEY_ID — AWS credentials for S3 deploy
+# AWS_SECRET_ACCESS_KEY — AWS credentials for S3 deploy
+# S3_BUCKET — target bucket (e.g. s3://my-docs-bucket)
+# AWS_REGION — AWS region (default: us-east-1)
+
+set -euo pipefail
+
+PROJECT_DIR="$JCI_REPO_ROOT/14-docusaurus-s3"
+BUILD_LOG="$JCI_OUTPUT_DIR/build.log"
+
+# ── Helpers ──────────────────────────────────────────────────────────
+log() { echo "[jci] $*" | tee -a "$BUILD_LOG"; }
+
+# Start the build log
+echo "=== Build log — $(date -u '+%Y-%m-%dT%H:%M:%SZ') ===" > "$BUILD_LOG"
+log "Commit : ${JCI_COMMIT:-unknown}"
+log "Project: $PROJECT_DIR"
+log ""
+
+cd "$PROJECT_DIR"
+
+# ── 1. Build ─────────────────────────────────────────────────────────
+if [ -f package.json ] && grep -q '"docusaurus"' package.json 2>/dev/null; then
+ log "Detected Docusaurus project — running npm build"
+ npm ci 2>&1 | tee -a "$BUILD_LOG"
+ npm run build 2>&1 | tee -a "$BUILD_LOG"
+ SITE_DIR="$PROJECT_DIR/build"
+else
+ log "No Docusaurus project found — using simple build.sh fallback"
+ bash build.sh 2>&1 | tee -a "$BUILD_LOG"
+ SITE_DIR="$PROJECT_DIR/build"
+fi
+
+# ── 2. Copy build output to CI artifacts ─────────────────────────────
+log ""
+log "Copying build output to \$JCI_OUTPUT_DIR/build/"
+mkdir -p "$JCI_OUTPUT_DIR/build"
+cp -r "$SITE_DIR"/* "$JCI_OUTPUT_DIR/build/"
+log "Artifact files:"
+ls -lR "$JCI_OUTPUT_DIR/build/" 2>&1 | tee -a "$BUILD_LOG"
+
+# ── 3. Deploy to S3 (optional) ───────────────────────────────────────
+log ""
+if [ -n "${AWS_ACCESS_KEY_ID:-}" ] && [ -n "${S3_BUCKET:-}" ]; then
+ AWS_REGION="${AWS_REGION:-us-east-1}"
+ log "Deploying to $S3_BUCKET (region: $AWS_REGION)"
+ aws s3 sync "$JCI_OUTPUT_DIR/build/" "$S3_BUCKET" \
+ --region "$AWS_REGION" \
+ --delete \
+ 2>&1 | tee -a "$BUILD_LOG"
+ log "S3 deploy complete."
+else
+ log "Skipping S3 deploy: AWS credentials or S3_BUCKET not set"
+fi
+
+# ── 4. Summary ───────────────────────────────────────────────────────
+log ""
+log "========================================"
+log " Build & deploy finished successfully"
+log " Pages : $(find "$JCI_OUTPUT_DIR/build" -name '*.html' | wc -l)"
+log " Log : \$JCI_OUTPUT_DIR/build.log"
+log "========================================"
```
-
-- [examples/14-docusaurus-s3/.jci/run.sh](/releases/files/examples/14-docusaurus-s3/.jci/run.sh)
-- [examples/14-docusaurus-s3/README.md](/releases/files/examples/14-docusaurus-s3/README.md)
-- [examples/14-docusaurus-s3/build.sh](/releases/files/examples/14-docusaurus-s3/build.sh)
-- [examples/14-docusaurus-s3/docs/index.md](/releases/files/examples/14-docusaurus-s3/docs/index.md)
diff --git a/scripts/publish_site.sh b/scripts/publish_site.sh
@@ -0,0 +1,24 @@
+#! /bin/bash
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+publish() {
+ echo "Publishing site"
+ pwd
+ cd website
+ md5sum secrets/ci.key
+ source secrets/bin/set_env.sh ci
+
+ cd /vol/www && zip -r ../website.zip .
+
+ echo Pushing build
+ curl -H "Content-Type: application/zip" \
+ -H "Authorization: Bearer $NETLIFY_TOKEN" \
+ --data-binary "@/vol/website.zip" \
+ https://api.netlify.com/api/v1/sites/$NETLIFY_SITEID/deploys | python3 -m json.tool
+}
+
+(publish)
+
diff --git a/scripts/render_examples.sh b/scripts/render_examples.sh
@@ -0,0 +1,101 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+README_FILE="${1:-README.md}"
+
+if [[ ! -f "${README_FILE}" ]]; then
+ echo "README file not found: ${README_FILE}" >&2
+ exit 1
+fi
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
+EXAMPLES_DIR="${REPO_ROOT}/examples"
+
+python3 - "$README_FILE" "$EXAMPLES_DIR" <<'PY'
+import sys
+import os
+from pathlib import Path
+
+readme_path = Path(sys.argv[1])
+examples_dir = Path(sys.argv[2])
+
+# ---------------------------------------------------------------------------
+# Build a tree-style listing for a given directory (mirrors `tree .` output).
+# Hidden files/dirs (dot-prefixed) are included since .jci is relevant.
+# ---------------------------------------------------------------------------
+def build_tree(root: Path) -> str:
+ lines = ["."]
+
+ def walk(directory: Path, prefix: str):
+ entries = sorted(
+ directory.iterdir(),
+ key=lambda p: (p.is_file(), p.name),
+ )
+ for idx, entry in enumerate(entries):
+ connector = "└── " if idx == len(entries) - 1 else "├── "
+ lines.append(f"{prefix}{connector}{entry.name}")
+ if entry.is_dir():
+ extension = " " if idx == len(entries) - 1 else "│ "
+ walk(entry, prefix + extension)
+
+ walk(root, "")
+ return "\n".join(lines)
+
+
+# ---------------------------------------------------------------------------
+# Collect examples
+# ---------------------------------------------------------------------------
+examples = sorted(
+ [d for d in examples_dir.iterdir() if d.is_dir()],
+ key=lambda d: d.name,
+)
+
+section_lines = ["## Examples", ""]
+
+for example in examples:
+ # Header: derive a readable title from the folder name
+ name = example.name
+ # Strip leading digits+dash prefix (e.g. "00-", "13-") for the title
+ parts = name.split("-", 1)
+ title = parts[1].replace("-", " ").title() if len(parts) == 2 and parts[0].isdigit() else name
+ section_lines.append(f"### {title}")
+ section_lines.append("")
+
+ run_sh = example / ".jci" / "run.sh"
+
+ # Single fenced block with tree output + run.sh contents
+ section_lines.append("```")
+ section_lines.append("$ tree .")
+ section_lines.append(build_tree(example))
+
+ if run_sh.exists():
+ section_lines.append("")
+ section_lines.append("")
+ section_lines.append("$ cat .jci/run.sh")
+ section_lines.append(run_sh.read_text(encoding="utf-8").rstrip())
+
+ section_lines.append("```")
+ section_lines.append("")
+
+# ---------------------------------------------------------------------------
+# Replace the ## Examples section (everything from that heading to EOF)
+# ---------------------------------------------------------------------------
+text = readme_path.read_text(encoding="utf-8")
+lines = text.splitlines()
+
+examples_heading_idx = next(
+ (i for i, l in enumerate(lines) if l.strip() == "## Examples"),
+ None,
+)
+if examples_heading_idx is None:
+ raise SystemExit("Could not find '## Examples' heading in README")
+
+new_lines = lines[:examples_heading_idx] + section_lines
+new_text = "\n".join(new_lines)
+if not new_text.endswith("\n"):
+ new_text += "\n"
+
+readme_path.write_text(new_text, encoding="utf-8")
+print(f"Rendered {len(examples)} examples into {readme_path}")
+PY