Jaypore CI

> Jaypore CI: Minimal, Offline, Local CI system.
Log | Files | Refs | README | LICENSE

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:
M.jci/run.sh | 11+++++++++++
DPLAN.md | 4----
MREADME.md | 1295++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Ascripts/publish_site.sh | 24++++++++++++++++++++++++
Ascripts/render_examples.sh | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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