commit 28bc8f6c323879446053b8f7d54714f1c6307d0b parent 076d3d7bc877aaa49f4542f7feea68518133d85b Author: Arjoonn Sharma <arjoonn@midpathsoftware.com> Date: Sat, 7 Mar 2026 17:01:31 +0000 Add examples (!11) Reviewed-on: https://gitea.midpathsoftware.com/midpath/jayporeci/pulls/11 Co-authored-by: Arjoonn Sharma <arjoonn@midpathsoftware.com> Co-committed-by: Arjoonn Sharma <arjoonn@midpathsoftware.com> Diffstat:
| M | README.md | | | 244 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- |
| A | examples/00-golang-lint-build-test/.jci/run.sh | | | 63 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/00-golang-lint-build-test/README.md | | | 40 | ++++++++++++++++++++++++++++++++++++++++ |
| A | examples/00-golang-lint-build-test/main.go | | | 35 | +++++++++++++++++++++++++++++++++++ |
| A | examples/00-golang-lint-build-test/main_test.go | | | 46 | ++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/01-pylint-pytest-coverage/.jci/run.sh | | | 47 | +++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/01-pylint-pytest-coverage/README.md | | | 59 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/02-docker-compose-api-tests/.jci/run.sh | | | 64 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/02-docker-compose-api-tests/Dockerfile | | | 15 | +++++++++++++++ |
| A | examples/02-docker-compose-api-tests/README.md | | | 65 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/02-docker-compose-api-tests/docker-compose.yml | | | 39 | +++++++++++++++++++++++++++++++++++++++ |
| A | examples/02-docker-compose-api-tests/test_api.sh | | | 84 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/03-midnight-build-telegram/.jci/crontab | | | 1 | + |
| A | examples/03-midnight-build-telegram/.jci/run.sh | | | 46 | ++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/03-midnight-build-telegram/README.md | | | 51 | +++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/04-trufflehog-scan/.jci/crontab | | | 1 | + |
| A | examples/04-trufflehog-scan/.jci/run.sh | | | 37 | +++++++++++++++++++++++++++++++++++++ |
| A | examples/04-trufflehog-scan/README.md | | | 58 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/05-lint-fix-precommit/.jci/run.sh | | | 110 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/05-lint-fix-precommit/README.md | | | 72 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/05-lint-fix-precommit/install-hook.sh | | | 36 | ++++++++++++++++++++++++++++++++++++ |
| A | examples/06-sub-pipelines/.jci/run.sh | | | 180 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/06-sub-pipelines/README.md | | | 73 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/06-sub-pipelines/go-app/go-app | | | 0 | |
| A | examples/06-sub-pipelines/go-app/go.mod | | | 3 | +++ |
| A | examples/06-sub-pipelines/go-app/main.go | | | 21 | +++++++++++++++++++++ |
| A | examples/06-sub-pipelines/go-app/main_test.go | | | 27 | +++++++++++++++++++++++++++ |
| A | examples/06-sub-pipelines/js-app/index.js | | | 13 | +++++++++++++ |
| A | examples/06-sub-pipelines/js-app/package.json | | | 10 | ++++++++++ |
| A | examples/06-sub-pipelines/python-app/app.py | | | 11 | +++++++++++ |
| A | examples/06-sub-pipelines/python-app/test_app.py | | | 19 | +++++++++++++++++++ |
| A | examples/07-secrets-telegram/.jci/run.sh | | | 120 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/07-secrets-telegram/README.md | | | 167 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/07-secrets-telegram/secrets.example.json | | | 4 | ++++ |
| A | examples/08-mail-on-failure/.jci/crontab | | | 1 | + |
| A | examples/08-mail-on-failure/.jci/run.sh | | | 56 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/08-mail-on-failure/README.md | | | 40 | ++++++++++++++++++++++++++++++++++++++++ |
| A | examples/09-auto-update-deps/.jci/crontab | | | 1 | + |
| A | examples/09-auto-update-deps/.jci/run.sh | | | 52 | ++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/09-auto-update-deps/README.md | | | 45 | +++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/10-build-publish-docker/.jci/run.sh | | | 104 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/10-build-publish-docker/Dockerfile | | | 20 | ++++++++++++++++++++ |
| A | examples/10-build-publish-docker/README.md | | | 91 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/11-upstream-trigger/README.md | | | 54 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/12-downstream-trigger/README.md | | | 47 | +++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/13-jekyll-netlify/.jci/run.sh | | | 77 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/13-jekyll-netlify/README.md | | | 44 | ++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/13-jekyll-netlify/site/_config.yml | | | 4 | ++++ |
| A | examples/13-jekyll-netlify/site/_layouts/default.html | | | 10 | ++++++++++ |
| A | examples/13-jekyll-netlify/site/index.md | | | 6 | ++++++ |
| A | examples/14-docusaurus-s3/.jci/run.sh | | | 71 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/14-docusaurus-s3/README.md | | | 57 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/14-docusaurus-s3/build.sh | | | 78 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | examples/14-docusaurus-s3/docs/index.md | | | 21 | +++++++++++++++++++++ |
54 files changed, 2725 insertions(+), 15 deletions(-)
diff --git a/README.md b/README.md @@ -144,18 +144,232 @@ part of the git repository. ## Examples -### Lint, Build, Test, Publish a golang project -### Pylint, Pytest, Coverage report -### Build Jekyll and publish to netlify -### Build Docusaurus and publish to S3 bucket -### Run a docker compose of redis, postgres, django, and run API tests against it. -### Schedule a midnight build and push status to telegram -### Run trufflehog scan on repo every hour -### 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 -### Set and use Secrets to publish messages to telegram -### Send mail on scheduled pipe failures -### Midnight auto-update dependencies and ensure tests are passing after update -### Build and publish docker images -### Run pipelines on this repo, when changes happen in upstream projects -### Run pipelines on another repo, when changes affect downstream projects +### 00 — Golang Lint, Build & Test + +``` +00-golang-lint-build-test/ +├── .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 + +``` +01-pylint-pytest-coverage/ +├── .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 + +``` +02-docker-compose-api-tests/ +├── .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 + +``` +03-midnight-build-telegram/ +├── .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 + +``` +04-trufflehog-scan/ +├── .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 + +``` +05-lint-fix-precommit/ +├── .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 + +``` +06-sub-pipelines/ +├── .jci/ +│ └── run.sh +├── README.md +├── go-app/ +│ ├── go-app +│ ├── go.mod +│ ├── main.go +│ └── main_test.go +├── js-app/ +│ ├── index.js +│ └── package.json +└── python-app/ + ├── app.py + └── test_app.py +``` + +- [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 + +``` +07-secrets-telegram/ +├── .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 + +``` +08-mail-on-failure/ +├── .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 + +``` +09-auto-update-deps/ +├── .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 + +``` +10-build-publish-docker/ +├── .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 + +``` +11-upstream-trigger/ +└── README.md +``` + +- [examples/11-upstream-trigger/README.md](/releases/files/examples/11-upstream-trigger/README.md) + +### 12 — Downstream Trigger + +``` +12-downstream-trigger/ +└── README.md +``` + +- [examples/12-downstream-trigger/README.md](/releases/files/examples/12-downstream-trigger/README.md) + +### 13 — Jekyll + Netlify + +``` +13-jekyll-netlify/ +├── .jci/ +│ └── run.sh +├── README.md +└── site/ + ├── _config.yml + ├── _layouts/ + │ └── default.html + └── index.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) + +### 14 — Build Docusaurus & Publish to S3 + +``` +14-docusaurus-s3/ +├── .jci/ +│ └── run.sh +├── README.md +├── build.sh +└── docs/ + └── index.md +``` + +- [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/examples/00-golang-lint-build-test/.jci/run.sh b/examples/00-golang-lint-build-test/.jci/run.sh @@ -0,0 +1,63 @@ +#!/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 "========================================" diff --git a/examples/00-golang-lint-build-test/README.md b/examples/00-golang-lint-build-test/README.md @@ -0,0 +1,40 @@ +# 00 — Golang Lint, Build & Test + +A minimal Jaypore CI example that lints, builds, tests, and optionally +publishes a Go HTTP server. + +## What's in the box + +| File | Purpose | +|---|---| +| `main.go` | Tiny HTTP server with a `/health` endpoint (`{"status":"ok"}`) | +| `main_test.go` | Tests for status code, content-type, and response body | +| `.jci/run.sh` | CI pipeline script executed by Jaypore CI | + +## Pipeline steps + +1. **Module init** — runs `go mod init` if no `go.mod` is present. +2. **Lint** — `go vet ./...` (report saved to `$JCI_OUTPUT_DIR/vet-report.txt`). +3. **Format check** — `gofmt -l .` fails the build if any file is unformatted. +4. **Build** — compiles the binary to `$JCI_OUTPUT_DIR/server`. +5. **Test** — `go test -v -cover ./...` (results saved to `$JCI_OUTPUT_DIR/test-results.txt`). +6. **Publish** — copies the binary to `$PUBLISH_DIR` when the variable is set; + skips gracefully otherwise. + +## Environment variables + +| Variable | Provided by | Description | +|---|---|---| +| `JCI_COMMIT` | Jaypore CI | Git commit SHA being built | +| `JCI_REPO_ROOT` | Jaypore CI | Absolute path to the repository root | +| `JCI_OUTPUT_DIR` | Jaypore CI | Directory for build artefacts (also the initial cwd) | +| `PUBLISH_DIR` | User (optional) | If set, the binary is copied here after a successful build | + +## Running locally + +```bash +export JCI_COMMIT=$(git rev-parse HEAD) +export JCI_REPO_ROOT=$(pwd) +export JCI_OUTPUT_DIR=$(mktemp -d) +bash 00-golang-lint-build-test/.jci/run.sh +``` diff --git a/examples/00-golang-lint-build-test/main.go b/examples/00-golang-lint-build-test/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "os" +) + +// HealthResponse is the JSON payload returned by the health endpoint. +type HealthResponse struct { + Status string `json:"status"` +} + +// healthHandler writes a JSON health-check response. +func healthHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(HealthResponse{Status: "ok"}) +} + +func main() { + addr := ":8080" + if port := os.Getenv("PORT"); port != "" { + addr = ":" + port + } + + mux := http.NewServeMux() + mux.HandleFunc("/health", healthHandler) + + log.Printf("listening on %s", addr) + if err := http.ListenAndServe(addr, mux); err != nil { + log.Fatalf("server error: %v", err) + } +} diff --git a/examples/00-golang-lint-build-test/main_test.go b/examples/00-golang-lint-build-test/main_test.go @@ -0,0 +1,46 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestHealthHandler_StatusCode(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/health", nil) + rec := httptest.NewRecorder() + + healthHandler(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rec.Code) + } +} + +func TestHealthHandler_ContentType(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/health", nil) + rec := httptest.NewRecorder() + + healthHandler(rec, req) + + ct := rec.Header().Get("Content-Type") + if ct != "application/json" { + t.Fatalf("expected Content-Type application/json, got %q", ct) + } +} + +func TestHealthHandler_Body(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/health", nil) + rec := httptest.NewRecorder() + + healthHandler(rec, req) + + var resp HealthResponse + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("failed to decode response: %v", err) + } + if resp.Status != "ok" { + t.Fatalf("expected status %q, got %q", "ok", resp.Status) + } +} diff --git a/examples/01-pylint-pytest-coverage/.jci/run.sh b/examples/01-pylint-pytest-coverage/.jci/run.sh @@ -0,0 +1,47 @@ +#!/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" diff --git a/examples/01-pylint-pytest-coverage/README.md b/examples/01-pylint-pytest-coverage/README.md @@ -0,0 +1,59 @@ +# Example 01 — Pylint + Pytest + Coverage + +This Jaypore CI example runs three checks on a Django project: + +| Step | Tool | What it does | +|------|------|--------------| +| 1 | **Pylint** | Static analysis of the `core/` app. | +| 2 | **Pytest** | Runs the test suite. | +| 3 | **Coverage** | Generates a terminal summary and an HTML coverage report. | + +## Artifacts produced + +| File | Description | +|------|-------------| +| `pylint-report.txt` | Full Pylint output. | +| `pytest-results.txt` | Pytest output including the coverage summary. | +| `htmlcov/index.html` | Browsable HTML coverage report. | + +## Project layout assumed + +``` +your-repo/ +├── manage.py +├── mysite/ +│ └── settings.py +├── core/ # the Django app under test +└── .jci/ + └── run.sh # ← this script +``` + +## How to use + +1. Copy the `.jci/` directory into your Django project's repository root: + + ```bash + cp -r 01-pylint-pytest-coverage/.jci /path/to/your-repo/.jci + ``` + +2. Make sure the required Python packages are installed: + + ```bash + pip install pylint pytest pytest-cov pytest-django + ``` + +3. Run Jaypore CI: + + ```bash + git jci run + ``` + + Jaypore CI will execute `.jci/run.sh`, and the generated artifacts will be + available in the CI output directory. + +## Customisation + +- **Different app name** — replace `core` with your app name in `run.sh`. +- **Different settings module** — change `mysite.settings` to match yours. +- **Fail on lint score** — remove `|| true` after the `pylint` command to make + the build fail when Pylint reports issues. diff --git a/examples/02-docker-compose-api-tests/.jci/run.sh b/examples/02-docker-compose-api-tests/.jci/run.sh @@ -0,0 +1,64 @@ +#!/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 diff --git a/examples/02-docker-compose-api-tests/Dockerfile b/examples/02-docker-compose-api-tests/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY manage.py . +COPY setup.cfg . +COPY mysite/ mysite/ +COPY core/ core/ + +EXPOSE 8000 + +CMD ["python3", "manage.py", "runserver", "0.0.0.0:8000"] diff --git a/examples/02-docker-compose-api-tests/README.md b/examples/02-docker-compose-api-tests/README.md @@ -0,0 +1,65 @@ +# 02 — Docker Compose API Tests + +This Jaypore CI example shows how to bring up a multi-service stack with +**Docker Compose** and run API tests against it. + +## What’s in the stack + +| Service | Image | Purpose | +|------------|----------------------|-------------------------------| +| `postgres` | `postgres:16-alpine` | Primary database | +| `redis` | `redis:7-alpine` | Cache / message broker | +| `web` | Built from repo | Django app serving the API | + +All three services have Docker health checks so the CI script can wait +until everything is ready before running tests. + +## Files + +``` +02-docker-compose-api-tests/ +├── .jci/ +│ └── run.sh # Jaypore CI entry point +├── docker-compose.yml # Service definitions +├── Dockerfile # Django app image +├── test_api.sh # Curl-based API test suite +└── README.md # This file +``` + +## How it works + +1. **`.jci/run.sh`** is executed by Jaypore CI. + It receives `JCI_COMMIT`, `JCI_REPO_ROOT`, and `JCI_OUTPUT_DIR` as + environment variables. + +2. The script runs `docker compose up -d --build` to start postgres, + redis, and the Django web service. + +3. It polls the Docker health checks until all three services report + healthy (up to 120 s). + +4. **`test_api.sh`** fires `curl` requests at the Django app: + - `GET /health/` — expects `{"status": "ok"}` + - `GET /items/` — expects a JSON list of items + - `GET /nonexistent/` — expects a 404 + +5. Results are saved into `$JCI_OUTPUT_DIR` so they become CI artifacts: + - `api-test-results.txt` — pass/fail summary + - `test-output.log` — full test console output + - `compose-logs.log` — container logs for debugging + - `compose-up.log` — docker compose build/start output + +6. `docker compose down` tears everything down (via a `trap` so it runs + even on failure). + +## Running locally + +```bash +export JCI_COMMIT=$(git rev-parse HEAD) +export JCI_REPO_ROOT=$(git rev-parse --show-toplevel) +export JCI_OUTPUT_DIR=$(mktemp -d) + +bash 02-docker-compose-api-tests/.jci/run.sh + +ls "$JCI_OUTPUT_DIR" # see the artifacts +``` diff --git a/examples/02-docker-compose-api-tests/docker-compose.yml b/examples/02-docker-compose-api-tests/docker-compose.yml @@ -0,0 +1,39 @@ +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: mysite + POSTGRES_USER: mysite + POSTGRES_PASSWORD: mysite + healthcheck: + test: ["CMD-SHELL", "pg_isready -U mysite"] + interval: 5s + timeout: 3s + retries: 5 + + redis: + image: redis:7-alpine + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + + web: + build: + context: .. + dockerfile: 02-docker-compose-api-tests/Dockerfile + ports: + - "0:8000" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/')"] + interval: 5s + timeout: 5s + retries: 10 + command: > + sh -c "python3 manage.py migrate --run-syncdb && python3 manage.py runserver 0.0.0.0:8000" diff --git a/examples/02-docker-compose-api-tests/test_api.sh b/examples/02-docker-compose-api-tests/test_api.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# test_api.sh — Run API tests against the Django app +# +# Usage: ./test_api.sh <base_url> <output_dir> + +set -euo pipefail + +BASE_URL="${1:-http://localhost:8000}" +OUTPUT_DIR="${2:-.}" + +PASSED=0 +FAILED=0 +RESULTS="" + +run_test() { + local name="$1" + local url="$2" + local expected_code="$3" + local expected_body="$4" # substring to look for in response body + + echo -n "TEST: $name ... " + + local http_code body + body=$(curl -s -w '\n%{http_code}' "$url" 2>&1) || true + http_code=$(echo "$body" | tail -1) + body=$(echo "$body" | sed '$d') + + local status="PASS" + local detail="" + + if [ "$http_code" != "$expected_code" ]; then + status="FAIL" + detail="expected HTTP $expected_code, got $http_code" + elif [ -n "$expected_body" ] && ! echo "$body" | grep -q "$expected_body"; then + status="FAIL" + detail="response body missing '$expected_body'" + fi + + if [ "$status" = "PASS" ]; then + echo "PASS" + PASSED=$((PASSED + 1)) + else + echo "FAIL ($detail)" + FAILED=$((FAILED + 1)) + fi + + RESULTS+="$status $name (HTTP $http_code) $detail\n" +} + +echo "========================================" +echo "API Tests — $BASE_URL" +echo "========================================" +echo + +# --- Health endpoint --- +run_test "GET /health/ returns 200" "$BASE_URL/health/" 200 '"status"' +run_test "GET /health/ contains ok" "$BASE_URL/health/" 200 '"ok"' + +# --- Items endpoint --- +run_test "GET /items/ returns 200" "$BASE_URL/items/" 200 '"items"' +run_test "GET /items/ returns JSON array" "$BASE_URL/items/" 200 'items' + +# --- Not Found --- +run_test "GET /nonexistent/ returns 404" "$BASE_URL/nonexistent/" 404 '' + +echo +echo "========================================" +echo "Results: $PASSED passed, $FAILED failed" +echo "========================================" + +# Write results file +{ + echo "API Test Results" + echo "================" + echo "Base URL: $BASE_URL" + echo "Date: $(date -u)" + echo + echo -e "$RESULTS" + echo "Total: $PASSED passed, $FAILED failed" +} > "$OUTPUT_DIR/api-test-results.txt" + +if [ "$FAILED" -gt 0 ]; then + exit 1 +fi diff --git a/examples/03-midnight-build-telegram/.jci/crontab b/examples/03-midnight-build-telegram/.jci/crontab @@ -0,0 +1 @@ +0 0 * * * run diff --git a/examples/03-midnight-build-telegram/.jci/run.sh b/examples/03-midnight-build-telegram/.jci/run.sh @@ -0,0 +1,46 @@ +#!/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" diff --git a/examples/03-midnight-build-telegram/README.md b/examples/03-midnight-build-telegram/README.md @@ -0,0 +1,51 @@ +# Midnight Build with Telegram Notifications + +Schedule a nightly build at midnight and push pass/fail status to a Telegram +chat. + +## How it works + +The file `.jci/crontab` contains a single cron entry: + +``` +0 0 * * * run +``` + +This tells Jaypore CI to execute `.jci/run.sh` every day at midnight. + +To install (or update) this schedule into the system crontab, run: + +```bash +git jci cron sync +``` + +`cron sync` reads `.jci/crontab`, translates each line into a real crontab +entry that invokes `git jci run` inside the repository, and writes it to the +current user's crontab. Run the command again after changing `.jci/crontab` to +pick up new schedules. + +## Setting up Telegram + +1. Create a bot via [BotFather](https://t.me/BotFather) and note the **bot + token**. +2. Get your **chat ID** by sending a message to the bot and visiting + `https://api.telegram.org/bot<TOKEN>/getUpdates`. +3. Export both values so Jaypore CI can see them at runtime: + +```bash +export TELEGRAM_BOT_TOKEN="123456:ABC-DEF..." +export TELEGRAM_CHAT_ID="-100..." +``` + +You can persist these in `~/.bashrc`, a `.env` file sourced by your shell, or +whatever secrets mechanism you prefer. + +## What the build does + +1. `cd` into the repo root. +2. Run `python3 manage.py test core` and capture the exit code. +3. Save test output and exit code to `$JCI_OUTPUT_DIR`. +4. Send a Telegram message with the repo name, short commit hash, pass/fail + status, and timestamp. +5. Exit with the test exit code so Jaypore CI records the run as passed or + failed. diff --git a/examples/04-trufflehog-scan/.jci/crontab b/examples/04-trufflehog-scan/.jci/crontab @@ -0,0 +1 @@ +0 * * * * run diff --git a/examples/04-trufflehog-scan/.jci/run.sh b/examples/04-trufflehog-scan/.jci/run.sh @@ -0,0 +1,37 @@ +#!/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 diff --git a/examples/04-trufflehog-scan/README.md b/examples/04-trufflehog-scan/README.md @@ -0,0 +1,58 @@ +# TruffleHog Secret Scan + +Automatically scan your repository for leaked secrets every hour using +[trufflehog3](https://github.com/feeltheajf/trufflehog3). + +## What is TruffleHog? + +TruffleHog scans a Git repository for accidentally committed secrets—API keys, +tokens, passwords, private keys, and other high-entropy strings that should +never appear in source control. It checks both the current working tree and the +full commit history, so even secrets that were "deleted" in a later commit are +caught. + +## How the hourly scan works + +The file `.jci/crontab` contains: + +``` +0 * * * * run +``` + +This tells Jaypore CI to execute `.jci/run.sh` once every hour (at minute 0). + +To install (or update) this schedule into the system crontab, run: + +```bash +git jci cron sync +``` + +`cron sync` reads `.jci/crontab`, translates each line into a real crontab +entry that invokes `git jci run` inside the repository, and writes it to the +current user's crontab. Run the command again after changing `.jci/crontab` to +pick up new schedules. + +## What the scan does + +1. `cd` into the repo root. +2. Run `trufflehog3` against the current working tree to find secrets in + checked-out files. +3. Run `trufflehog3` against the commit history to find secrets that were ever + committed. +4. Merge both results into `$JCI_OUTPUT_DIR/trufflehog-report.txt`. +5. If any secrets are found, print the report and exit with code **1** so + Jaypore CI records the run as failed. +6. If the repo is clean, exit with code **0**. + +## Customisation + +You can tweak the scan by editing `.jci/run.sh`: + +- **Severity filter** – add `--severity HIGH` to only flag high-confidence + findings. +- **Limit history depth** – add `--depth 100` to scan only the last 100 + commits. +- **Custom rules** – pass `--rules /path/to/rules.yaml` to use your own + patterns. +- **JSON output** – change `--format TEXT` to `--format JSON` for + machine-readable results. diff --git a/examples/05-lint-fix-precommit/.jci/run.sh b/examples/05-lint-fix-precommit/.jci/run.sh @@ -0,0 +1,110 @@ +#!/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" diff --git a/examples/05-lint-fix-precommit/README.md b/examples/05-lint-fix-precommit/README.md @@ -0,0 +1,72 @@ +# 05 — Lint & Fix on Pre-Commit + +Run lint checks automatically before every `git commit` using Jaypore CI. +This example targets a Django project (Python-heavy) but includes stubs for +Go and JavaScript files in the same repo. + +## What it does + +| Language | Tool | Action | +|------------|----------------------------|-------------------------------------| +| Python | `py_compile`, `black` | Syntax-check all `.py` files; verify formatting | +| Go | `gofmt` | List unformatted `.go` files | +| JavaScript | `eslint --fix` | Auto-fix and report remaining errors | + +Results are saved to `$JCI_OUTPUT_DIR/lint-results.txt` so Jaypore CI can +render them in its dashboard. + +## Setup + +### 1. Copy the CI script into your project + +```bash +mkdir -p .jci +cp .jci/run.sh /path/to/your/project/.jci/run.sh +``` + +### 2. Install the pre-commit hook + +```bash +./install-hook.sh # uses the current repo +# or +./install-hook.sh /path/to/your/project +``` + +This creates `.git/hooks/pre-commit` which calls `git jci run` before each +commit. If any lint check fails the commit is blocked. + +### 3. Commit as usual + +```bash +git add . +git commit -m "my changes" +# Jaypore CI runs automatically; commit proceeds only if all checks pass. +``` + +## How it works + +1. Git fires the **pre-commit** hook before creating a commit object. +2. The hook runs `git jci run`, which executes `.jci/run.sh`. +3. `run.sh` walks through Python, Go, and JS checks, recording output to + `lint-results.txt`. +4. If **any** check fails, `run.sh` exits non-zero → the hook exits non-zero + → Git aborts the commit. +5. Fix the reported issues, `git add` the fixes, and commit again. + +## Skipping the hook + +In an emergency you can bypass the pre-commit hook: + +```bash +git commit --no-verify -m "skip lint this time" +``` + +## Files + +``` +05-lint-fix-precommit/ +├── .jci/ +│ └── run.sh # Jaypore CI script — lint checks +├── install-hook.sh # Helper to install the git hook +└── README.md # This file +``` diff --git a/examples/05-lint-fix-precommit/install-hook.sh b/examples/05-lint-fix-precommit/install-hook.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -euo pipefail + +# Install a git pre-commit hook that runs Jaypore CI before each commit. +# +# Usage: +# ./install-hook.sh # install in the current repo +# ./install-hook.sh /path # install in a specific repo + +REPO_ROOT="${1:-$(git rev-parse --show-toplevel)}" +HOOK="${REPO_ROOT}/.git/hooks/pre-commit" + +if [ -f "$HOOK" ]; then + echo "A pre-commit hook already exists at $HOOK" + echo "Back it up or remove it, then re-run this script." + exit 1 +fi + +mkdir -p "$(dirname "$HOOK")" + +cat > "$HOOK" << 'EOF' +#!/bin/bash +# Pre-commit hook installed by install-hook.sh +# Runs Jaypore CI lint checks before allowing a commit. + +echo "Running Jaypore CI lint checks..." +if ! git jci run; then + echo "" + echo "Commit blocked: lint checks failed." + echo "Fix the issues above, stage your changes, and try again." + exit 1 +fi +EOF + +chmod +x "$HOOK" +echo "Installed pre-commit hook at $HOOK" diff --git a/examples/06-sub-pipelines/.jci/run.sh b/examples/06-sub-pipelines/.jci/run.sh @@ -0,0 +1,180 @@ +#!/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)" diff --git a/examples/06-sub-pipelines/README.md b/examples/06-sub-pipelines/README.md @@ -0,0 +1,73 @@ +# 06 — Sub-pipelines + +Run only the CI steps that matter for each commit by detecting which parts of a +monorepo actually changed. + +## The problem + +In a monorepo with independent services—say `python-app/`, `js-app/`, and +`go-app/`—running every linter, build, and test suite on every commit wastes +time. A one-line docs fix in `python-app/` shouldn’t trigger the Go compiler. + +## The pattern + +``` +git diff --name-only HEAD~1 HEAD +``` + +gives you the list of files touched in the latest commit. Grep that list for +each top-level folder and conditionally execute the matching sub-pipeline: + +```bash +changed=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || true) + +if echo "$changed" | grep -q '^python-app/'; then + # run python lint + tests +fi +``` + +When the diff is empty (first commit, shallow clone, force-push) the script +falls back to running **all** sub-pipelines so nothing is silently skipped. + +## What the script does + +| Folder changed | Actions performed | Results file | +|----------------|------------------------------------|-----------------------| +| `python-app/` | Lint (ruff/flake8) + pytest | `python-results.txt` | +| `js-app/` | npm install + eslint | `js-results.txt` | +| `go-app/` | `go build` + `go test` | `go-results.txt` | + +Each sub-pipeline writes its output to a dedicated results file in +`$JCI_OUTPUT_DIR` so you can inspect them independently. + +## Adapting this to your project + +- **Add more folders** — copy a sub-pipeline block and change the grep pattern. +- **Use path globs** — match `docs/` or `*.md` to trigger a docs-build step. +- **Parallel execution** — background each sub-pipeline and `wait` to run them + concurrently. +- **Deeper diffs** — use `HEAD~N` or compare against a base branch + (`git diff origin/main...HEAD`) for pull-request workflows. + +## File layout + +``` +06-sub-pipelines/ +├── .jci/ +│ └── run.sh # Main CI entry point +├── python-app/ +│ ├── app.py # Tiny Python module (add, greet) +│ └── test_app.py # pytest tests +├── js-app/ +│ ├── index.js # Node module (add, greet) +│ └── package.json # Minimal package manifest +├── go-app/ +│ ├── main.go # Go package (Add, Greet) +│ ├── main_test.go # Go tests +│ └── go.mod # Go module definition +└── README.md # This file +``` + +Each sub-app is a self-contained, minimal demo—no external dependencies +required. They exist purely to give the sub-pipeline script something real to +lint, build, and test. diff --git a/examples/06-sub-pipelines/go-app/go-app b/examples/06-sub-pipelines/go-app/go-app Binary files differ. diff --git a/examples/06-sub-pipelines/go-app/go.mod b/examples/06-sub-pipelines/go-app/go.mod @@ -0,0 +1,3 @@ +module go-app + +go 1.21 diff --git a/examples/06-sub-pipelines/go-app/main.go b/examples/06-sub-pipelines/go-app/main.go @@ -0,0 +1,21 @@ +// Package main is a tiny demo for the sub-pipeline example. +package main + +import "fmt" + +// Add returns the sum of a and b. +func Add(a, b int) int { + return a + b +} + +// Greet returns a greeting string. +func Greet(name string) string { + if name == "" { + name = "world" + } + return fmt.Sprintf("Hello, %s!", name) +} + +func main() { + fmt.Println(Greet("")) +} diff --git a/examples/06-sub-pipelines/go-app/main_test.go b/examples/06-sub-pipelines/go-app/main_test.go @@ -0,0 +1,27 @@ +package main + +import "testing" + +func TestAdd(t *testing.T) { + if got := Add(2, 3); got != 5 { + t.Errorf("Add(2, 3) = %d; want 5", got) + } +} + +func TestAddNegative(t *testing.T) { + if got := Add(-1, 1); got != 0 { + t.Errorf("Add(-1, 1) = %d; want 0", got) + } +} + +func TestGreetDefault(t *testing.T) { + if got := Greet(""); got != "Hello, world!" { + t.Errorf("Greet(\"\") = %q; want \"Hello, world!\"", got) + } +} + +func TestGreetName(t *testing.T) { + if got := Greet("CI"); got != "Hello, CI!" { + t.Errorf("Greet(\"CI\") = %q; want \"Hello, CI!\"", got) + } +} diff --git a/examples/06-sub-pipelines/js-app/index.js b/examples/06-sub-pipelines/js-app/index.js @@ -0,0 +1,13 @@ +/** + * A tiny demo module for the sub-pipeline example. + */ + +function add(a, b) { + return a + b; +} + +function greet(name) { + return `Hello, ${name || "world"}!`; +} + +module.exports = { add, greet }; diff --git a/examples/06-sub-pipelines/js-app/package.json b/examples/06-sub-pipelines/js-app/package.json @@ -0,0 +1,10 @@ +{ + "name": "js-app", + "version": "0.1.0", + "description": "Minimal JS demo for the sub-pipeline example", + "main": "index.js", + "scripts": { + "test": "node -e \"const {add,greet}=require('./index');console.assert(add(2,3)===5);console.assert(greet()==='Hello, world!');console.log('All tests passed.')\"" + }, + "license": "MIT" +} diff --git a/examples/06-sub-pipelines/python-app/app.py b/examples/06-sub-pipelines/python-app/app.py @@ -0,0 +1,11 @@ +"""A tiny demo module for the sub-pipeline example.""" + + +def add(a, b): + """Return the sum of *a* and *b*.""" + return a + b + + +def greet(name="world"): + """Return a greeting string.""" + return f"Hello, {name}!" diff --git a/examples/06-sub-pipelines/python-app/test_app.py b/examples/06-sub-pipelines/python-app/test_app.py @@ -0,0 +1,19 @@ +"""Tests for app.py — runnable with pytest.""" + +from app import add, greet + + +def test_add(): + assert add(2, 3) == 5 + + +def test_add_negative(): + assert add(-1, 1) == 0 + + +def test_greet_default(): + assert greet() == "Hello, world!" + + +def test_greet_name(): + assert greet("CI") == "Hello, CI!" diff --git a/examples/07-secrets-telegram/.jci/run.sh b/examples/07-secrets-telegram/.jci/run.sh @@ -0,0 +1,120 @@ +#!/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" diff --git a/examples/07-secrets-telegram/README.md b/examples/07-secrets-telegram/README.md @@ -0,0 +1,167 @@ +# Example 07 — Secrets with SOPS + Telegram + +This Jaypore CI example shows how to keep secrets (API tokens, credentials) +encrypted inside your repository using [Mozilla SOPS](https://github.com/getsops/sops) +and decrypt them at CI time. The decrypted values are used to send a Telegram +notification with the build result. + +## How it works + +| Step | What happens | +|------|-------------| +| 1 | If `sops` is installed and `secrets.enc.json` exists, decrypt it to obtain `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID`. | +| 2 | If SOPS is unavailable, fall back to plain environment variables. | +| 3 | Run `python3 manage.py test core` and capture the result. | +| 4 | Send a Markdown-formatted message to Telegram with the build status. | +| 5 | Save all artifacts to `$JCI_OUTPUT_DIR`. | + +## Artifacts produced + +| File | Description | +|------|-------------| +| `test_output.txt` | Full test output. | +| `exit_code.txt` | Exit code from the test run. | +| `telegram_response.json` | Telegram API response (when notification is sent). | + +## Setting up SOPS + +### 1. Install SOPS + +```bash +# Debian / Ubuntu +curl -LO https://github.com/getsops/sops/releases/download/v3.9.4/sops_3.9.4_amd64.deb +sudo dpkg -i sops_3.9.4_amd64.deb + +# macOS +brew install sops +``` + +### 2. Choose an encryption backend + +SOPS supports **age**, **GPG**, **AWS KMS**, **GCP KMS**, and **Azure Key Vault**. +For local / small-team use, [age](https://github.com/FiloSottile/age) is the +simplest option: + +```bash +# Install age +sudo apt install age # Debian/Ubuntu +brew install age # macOS + +# Generate a key pair +age-keygen -o ~/.config/sops/age/keys.txt +# Note the public key printed to stdout (starts with "age1...") +``` + +### 3. Create a `.sops.yaml` config (optional but recommended) + +Place this in your repository root so SOPS knows which key to use: + +```yaml +creation_rules: + - path_regex: secrets\.enc\.json$ + age: "age1your-public-key-here" +``` + +### 4. Create and encrypt the secrets file + +Start from the provided template: + +```bash +cp secrets.example.json secrets.json +``` + +Edit `secrets.json` with your real values: + +```json +{ + "TELEGRAM_BOT_TOKEN": "123456789:ABCdefGHI-JKLmnoPQRstUVwxyz", + "TELEGRAM_CHAT_ID": "-1001234567890" +} +``` + +Encrypt it: + +```bash +sops -e secrets.json > secrets.enc.json +``` + +Now commit `secrets.enc.json` (the encrypted version) and **delete the +plaintext** `secrets.json`: + +```bash +rm secrets.json +git add secrets.enc.json +git commit -m "Add encrypted secrets" +``` + +> **Never commit the plaintext `secrets.json`.** Add it to `.gitignore`. + +### 5. Decrypt (what run.sh does) + +At CI time the script runs: + +```bash +sops -d secrets.enc.json +``` + +This prints the decrypted JSON to stdout. The script parses it with Python to +extract individual values into environment variables. + +Decryption requires the private key. For `age`, the key file at +`~/.config/sops/age/keys.txt` is used automatically. + +## Alternative: plain environment variables + +If you prefer not to use SOPS, export the variables before running CI: + +```bash +export TELEGRAM_BOT_TOKEN="123456789:ABCdefGHI-JKLmnoPQRstUVwxyz" +export TELEGRAM_CHAT_ID="-1001234567890" +git jci run +``` + +The script detects that SOPS is absent (or that the encrypted file is missing) +and falls back to whatever `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID` are +already set in the environment. + +## Setting up Telegram + +1. Create a bot via [BotFather](https://t.me/BotFather) and note the **bot + token**. +2. Get your **chat ID** by sending a message to the bot and visiting + `https://api.telegram.org/bot<TOKEN>/getUpdates`. +3. Put both values into `secrets.json` and encrypt with SOPS (see above), or + export them as environment variables. + +## Project layout + +``` +your-repo/ +├── manage.py +├── mysite/ +│ └── settings.py +├── core/ # the Django app under test +├── secrets.enc.json # encrypted secrets (committed) +├── secrets.example.json # template with placeholder values +├── .sops.yaml # SOPS config (optional) +└── .jci/ + └── run.sh # ← this script +``` + +## How to use + +1. Copy the `.jci/` directory and `secrets.example.json` into your repo: + + ```bash + cp -r 07-secrets-telegram/.jci /path/to/your-repo/.jci + cp 07-secrets-telegram/secrets.example.json /path/to/your-repo/ + ``` + +2. Set up SOPS and encrypt your secrets (see above), or export them as + environment variables. + +3. Run Jaypore CI: + + ```bash + git jci run + ``` diff --git a/examples/07-secrets-telegram/secrets.example.json b/examples/07-secrets-telegram/secrets.example.json @@ -0,0 +1,4 @@ +{ + "TELEGRAM_BOT_TOKEN": "123456789:ABCdefGHI-JKLmnoPQRstUVwxyz", + "TELEGRAM_CHAT_ID": "-1001234567890" +} diff --git a/examples/08-mail-on-failure/.jci/crontab b/examples/08-mail-on-failure/.jci/crontab @@ -0,0 +1 @@ +0 0 * * * run diff --git a/examples/08-mail-on-failure/.jci/run.sh b/examples/08-mail-on-failure/.jci/run.sh @@ -0,0 +1,56 @@ +#!/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" diff --git a/examples/08-mail-on-failure/README.md b/examples/08-mail-on-failure/README.md @@ -0,0 +1,40 @@ +# 08 — Mail on failure + +Send an email whenever a scheduled CI run fails. + +## What it does + +A cron entry (`0 0 * * *`) triggers `run.sh` once a day at midnight. +The script runs `python3 manage.py test core` and, only when the tests +fail, sends an email containing: + +- repository name +- commit hash +- timestamp +- the tail of the test output + +Test output is always saved to `$JCI_OUTPUT_DIR/test-output.txt` so you +can inspect it later regardless of pass/fail. + +## Required environment variables + +| Variable | Purpose | +|---|---| +| `SMTP_HOST` | SMTP server hostname (e.g. `smtp.mailgun.org`) | +| `SMTP_PORT` | SMTP port, typically `587` for STARTTLS | +| `SMTP_USER` | SMTP login username | +| `SMTP_PASS` | SMTP login password or API key | +| `MAIL_FROM` | Sender address (e.g. `ci@example.com`) | +| `MAIL_TO` | Recipient address | + +Set these in your CI environment or in a `.env` file that your Jaypore CI +setup sources before running the pipeline. + +## How it works + +1. `run.sh` changes into `$JCI_REPO_ROOT` and runs the Django test suite. +2. Output is piped to both stdout and `$JCI_OUTPUT_DIR/test-output.txt`. +3. If the exit code is non-zero, a `send_mail()` helper uses Python's + `smtplib` to deliver a failure report over SMTP/STARTTLS. +4. The script exits with the original test exit code so Jaypore CI + records the run as failed. diff --git a/examples/09-auto-update-deps/.jci/crontab b/examples/09-auto-update-deps/.jci/crontab @@ -0,0 +1 @@ +0 0 * * * run diff --git a/examples/09-auto-update-deps/.jci/run.sh b/examples/09-auto-update-deps/.jci/run.sh @@ -0,0 +1,52 @@ +#!/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 diff --git a/examples/09-auto-update-deps/README.md b/examples/09-auto-update-deps/README.md @@ -0,0 +1,45 @@ +# Auto-Update Dependencies + +Keep your project's Python dependencies fresh with a nightly CI job that +upgrades packages, runs the test suite, and commits the result. + +## How it works + +`.jci/crontab` schedules a run at midnight every day: + +``` +0 0 * * * run +``` + +When Jaypore CI fires the job, `.jci/run.sh`: + +1. **Snapshots** the current `pip3 freeze` output so you have a baseline. +2. **Upgrades** every package listed in `requirements.txt` to its latest + compatible version. +3. **Snapshots again** and produces a diff showing exactly what changed. +4. **Runs the Django test suite** (`manage.py test`) against the updated + environment. +5. **On success** — freezes the new versions into `requirements.txt` and + commits them automatically. +6. **On failure** — writes a report listing the updated packages and the + test output so you can see what broke. + +All intermediate artifacts (`before-update.txt`, `after-update.txt`, +`dep-diff.txt`, `test-results.txt`, and the failure report when applicable) +are saved to `$JCI_OUTPUT_DIR` for inspection. + +## Why nightly updates? + +* **Security** — patches land within hours, not weeks. +* **Small diffs** — a daily upgrade rarely touches more than a handful of + packages, making breakage easy to diagnose. +* **No surprises** — if a new release breaks your tests you find out + immediately instead of during a deadline-day deploy. + +## Adapting this example + +* Swap `pip3` commands for `npm`, `cargo`, `go get -u`, etc. +* Replace `manage.py test` with your project's test runner. +* Adjust the cron schedule (`0 0 * * 1` for weekly, for example). +* Add notifications (email, Slack) by extending `run.sh` after the + success/failure branches. diff --git a/examples/10-build-publish-docker/.jci/run.sh b/examples/10-build-publish-docker/.jci/run.sh @@ -0,0 +1,104 @@ +#!/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" diff --git a/examples/10-build-publish-docker/Dockerfile b/examples/10-build-publish-docker/Dockerfile @@ -0,0 +1,20 @@ +FROM python:3.12-slim + +WORKDIR /app + +# Install build dependencies for common Python packages +RUN apt-get update && apt-get install -y --no-install-recommends \ + libpq-dev gcc && \ + rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt gunicorn + +COPY manage.py . +COPY mysite/ mysite/ +COPY core/ core/ +COPY setup.cfg . + +EXPOSE 8000 + +CMD ["gunicorn", "mysite.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "2"] diff --git a/examples/10-build-publish-docker/README.md b/examples/10-build-publish-docker/README.md @@ -0,0 +1,91 @@ +# Example 10 — Build & Publish Docker Images + +This Jaypore CI example builds a production Docker image for the Django +project, runs the test suite inside the container, and optionally pushes the +image to a Docker registry. + +| Step | What it does | +|------|--------------| +| 1 | **Build** the Docker image, tagged with the commit SHA and `latest`. | +| 2 | **Test** — run `manage.py test` inside the freshly built container. | +| 3 | **Inspect** — save `docker inspect` output and image size info. | +| 4 | **Push** — if `DOCKER_REGISTRY` is set, log in and push both tags. | + +## Dockerfile + +The included `Dockerfile` creates a slim production image: + +- **Base** — `python:3.12-slim` +- **Dependencies** — installs `requirements.txt` plus `gunicorn` +- **App code** — copies `manage.py`, `mysite/`, `core/`, and `setup.cfg` +- **Entrypoint** — runs Gunicorn on port 8000 with 2 workers + +## Artifacts produced + +| File | Description | +|------|-------------| +| `docker-build.log` | Full `docker build` output. | +| `docker-test.log` | Test suite output from inside the container. | +| `image-inspect.json` | `docker inspect` metadata for the built image. | +| `image-info.txt` | Image repository, tag, ID, and size. | +| `docker-push.log` | Registry push output (only when pushing). | + +## Environment variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `DOCKER_REGISTRY` | No | Registry hostname (e.g. `registry.example.com`). If unset, the image is built and tested locally but not pushed. | +| `DOCKER_USERNAME` | No | Username for `docker login`. | +| `DOCKER_PASSWORD` | No | Password / token for `docker login`. | + +## Project layout assumed + +``` +your-repo/ +├── manage.py +├── requirements.txt +├── setup.cfg +├── mysite/ +│ └── settings.py +├── core/ +└── 10-build-publish-docker/ + ├── Dockerfile + └── .jci/ + └── run.sh # ← this script +``` + +## How to use + +1. Copy the `.jci/` directory and `Dockerfile` into your repository: + + ```bash + cp -r 10-build-publish-docker/.jci /path/to/your-repo/.jci + cp 10-build-publish-docker/Dockerfile /path/to/your-repo/Dockerfile + ``` + +2. Make sure Docker is available on the CI runner. + +3. Run Jaypore CI: + + ```bash + git jci run + ``` + + To also push to a registry, export the environment variables first: + + ```bash + export DOCKER_REGISTRY=registry.example.com + export DOCKER_USERNAME=deploy-bot + export DOCKER_PASSWORD=secret-token + git jci run + ``` + +## Customisation + +- **Image name** — change `IMAGE_NAME` in `run.sh` to match your project. +- **Build context** — adjust the `-f` flag and build context path if your + `Dockerfile` lives elsewhere. +- **Multi-platform builds** — replace `docker build` with `docker buildx build + --platform linux/amd64,linux/arm64` for multi-arch images. +- **Gunicorn workers** — edit the `CMD` in `Dockerfile` or set the + `WEB_CONCURRENCY` environment variable at runtime. diff --git a/examples/11-upstream-trigger/README.md b/examples/11-upstream-trigger/README.md @@ -0,0 +1,54 @@ +# 11 — Upstream Trigger + +Re-run your app’s tests whenever an upstream library gets new commits. + +## Repos + +| Repo | Path | Role | +|------|------|------| +| **upstream-lib** | `/home/exedev/upstream-lib/` | Shared library (`mathlib.py` — `add`, `multiply`) | +| **downstream-app** | `/home/exedev/downstream-app/` | App that depends on the library | + +## The symlink + +``` +downstream-app/.jci/upstream-lib → /home/exedev/upstream-lib +``` + +This symlink is how the app knows where its upstream dependency lives. + +## How it works + +1. `downstream-app/.jci/run.sh` resolves the `upstream-lib` symlink. +2. It reads the upstream repo’s `HEAD` commit. +3. It compares that against `.jci/upstream-last-commit` (saved from the previous run). +4. **If upstream has changed** — app tests run to verify compatibility. +5. **If upstream is unchanged** — CI exits early, nothing to do. + +## Try it + +```bash +# First run — upstream is "new", tests run +cd /home/exedev/downstream-app +git jci run + +# Second run — upstream unchanged, skips +git jci run + +# Simulate an upstream change +cd /home/exedev/upstream-lib +echo '# update' >> mathlib.py && git add mathlib.py && git commit -m 'update' + +# Third run — detects upstream change, tests run again +cd /home/exedev/downstream-app +git jci run +``` + +## Adding more upstream repos + +```bash +cd /home/exedev/downstream-app/.jci +ln -s /path/to/another-lib upstream-other +``` + +Extend `run.sh` to loop over all `upstream-*` symlinks if needed. diff --git a/examples/12-downstream-trigger/README.md b/examples/12-downstream-trigger/README.md @@ -0,0 +1,47 @@ +# 12 — Downstream Trigger + +After a shared library’s tests pass, push CI into every downstream repo +that depends on it. + +## Repos + +| Repo | Path | Role | +|------|------|------| +| **core-lib** | `/home/exedev/core-lib/` | Shared library (`corelib.py` — `validate_email`, `slugify`) | +| **consumer-svc** | `/home/exedev/consumer-svc/` | Service that imports core-lib | + +## The symlink + +``` +core-lib/.jci/downstream-consumer → /home/exedev/consumer-svc +``` + +This symlink tells core-lib where to push CI signals. + +## How it works + +1. `core-lib/.jci/run.sh` runs core-lib’s own tests. +2. If they pass, it finds every `.jci/downstream-*` symlink. +3. For each one, it resolves the symlink and runs `git jci run` in that repo. +4. If any downstream CI fails, the overall run fails. + +## Try it + +```bash +cd /home/exedev/core-lib +git jci run +``` + +You’ll see core-lib tests run, then consumer-svc CI gets triggered +automatically. + +## Adding more downstream repos + +```bash +cd /home/exedev/core-lib/.jci +ln -s /path/to/billing-svc downstream-billing +ln -s /path/to/admin-app downstream-admin +``` + +The `run.sh` discovers all `downstream-*` symlinks automatically — no +config file to edit. diff --git a/examples/13-jekyll-netlify/.jci/run.sh b/examples/13-jekyll-netlify/.jci/run.sh @@ -0,0 +1,77 @@ +#!/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." diff --git a/examples/13-jekyll-netlify/README.md b/examples/13-jekyll-netlify/README.md @@ -0,0 +1,44 @@ +# 13 — Jekyll + Netlify + +Build a Jekyll static site and (optionally) deploy it to Netlify. + +## What this example does + +1. Checks that Jekyll is installed; installs it via `gem` if missing. +2. Runs `jekyll build` to generate the static site into `$JCI_OUTPUT_DIR/_site`. +3. Verifies the build succeeded and lists output files. +4. If `NETLIFY_AUTH_TOKEN` and `NETLIFY_SITE_ID` are set, zips the output and + deploys it to Netlify via the Netlify API. Otherwise it skips the deploy + gracefully. +5. Writes a build log to `$JCI_OUTPUT_DIR/build.log`. + +## Files + +| Path | Purpose | +|------|---------| +| `.jci/run.sh` | CI entry point | +| `site/index.md` | Sample Jekyll page | +| `site/_config.yml` | Minimal Jekyll configuration | +| `site/_layouts/default.html` | Bare-bones HTML layout | + +## Environment variables + +| Variable | Description | +|----------|-------------| +| `JCI_COMMIT` | Git commit SHA being built | +| `JCI_REPO_ROOT` | Root of the repository checkout | +| `JCI_OUTPUT_DIR` | Directory for build artifacts (also the initial cwd) | +| `NETLIFY_AUTH_TOKEN` | *(optional)* Netlify personal access token | +| `NETLIFY_SITE_ID` | *(optional)* Target Netlify site ID | + +## Running locally + +```bash +export JCI_COMMIT=$(git rev-parse HEAD) +export JCI_REPO_ROOT=$(pwd) +export JCI_OUTPUT_DIR=$(mktemp -d) + +bash 13-jekyll-netlify/.jci/run.sh +``` + +To deploy, also set `NETLIFY_AUTH_TOKEN` and `NETLIFY_SITE_ID` before running. diff --git a/examples/13-jekyll-netlify/site/_config.yml b/examples/13-jekyll-netlify/site/_config.yml @@ -0,0 +1,4 @@ +title: Jekyll Netlify Example +baseurl: "" +url: "" +markdown: kramdown diff --git a/examples/13-jekyll-netlify/site/_layouts/default.html b/examples/13-jekyll-netlify/site/_layouts/default.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>{{ page.title | default: site.title }}</title> +</head> +<body> + {{ content }} +</body> +</html> diff --git a/examples/13-jekyll-netlify/site/index.md b/examples/13-jekyll-netlify/site/index.md @@ -0,0 +1,6 @@ +--- +layout: default +title: Home +--- +# Hello from Jekyll +Built by Jaypore CI. diff --git a/examples/14-docusaurus-s3/.jci/run.sh b/examples/14-docusaurus-s3/.jci/run.sh @@ -0,0 +1,71 @@ +#!/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 "========================================" diff --git a/examples/14-docusaurus-s3/README.md b/examples/14-docusaurus-s3/README.md @@ -0,0 +1,57 @@ +# 14 — Build Docusaurus & Publish to S3 + +This example shows how to build a documentation site and deploy the static +output to an AWS S3 bucket using Jaypore CI. + +## How it works + +1. **Build** — The CI script checks for a real Docusaurus project + (`package.json` containing `docusaurus`). If found it runs `npm run build`; + otherwise it falls back to a lightweight `build.sh` that converts Markdown + to HTML with pandoc (or plain sed when pandoc is unavailable). + +2. **Artifact collection** — The generated HTML is copied to + `$JCI_OUTPUT_DIR/build/` so it is available as a CI artifact. + +3. **S3 deploy** — If AWS credentials and an S3 bucket are configured the + output is synced to S3 with `aws s3 sync`. If the variables are missing + the deploy step is skipped gracefully. + +## Using a real Docusaurus project + +For a production setup, replace the simple `docs/` + `build.sh` scaffold with +a full Docusaurus project: + +```bash +npx create-docusaurus@latest my-docs classic +``` + +Then place the generated project files in this directory. The CI script will +automatically detect `package.json` and run `npm run build` instead of the +fallback. + +## Configuring S3 credentials + +Set these environment variables (e.g. in your Jaypore CI secrets): + +| Variable | Required | Description | +|-------------------------|----------|--------------------------------------| +| `AWS_ACCESS_KEY_ID` | Yes | AWS access key | +| `AWS_SECRET_ACCESS_KEY` | Yes | AWS secret key | +| `S3_BUCKET` | Yes | Target bucket, e.g. `s3://my-docs` | +| `AWS_REGION` | No | AWS region (default `us-east-1`) | + +Without these variables the pipeline still succeeds — it simply skips the +deploy step and prints a message. + +## Files + +``` +14-docusaurus-s3/ +├── .jci/ +│ └── run.sh # Jaypore CI entry point +├── docs/ +│ └── index.md # Sample documentation page +├── build.sh # Lightweight Markdown → HTML builder +└── README.md # This file +``` diff --git a/examples/14-docusaurus-s3/build.sh b/examples/14-docusaurus-s3/build.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# Simple build script that converts Markdown docs to static HTML. +# This simulates what Docusaurus (or any static-site generator) would produce. +# +# Usage: ./build.sh [source_dir] [output_dir] +# source_dir — directory containing .md files (default: docs) +# output_dir — where to write HTML output (default: build) + +set -euo pipefail + +SRC_DIR="${1:-docs}" +OUT_DIR="${2:-build}" + +rm -rf "$OUT_DIR" +mkdir -p "$OUT_DIR" + +# ── helper: wrap markdown in a minimal HTML page ────────────────────────── +md_to_html() { + local md_file="$1" + local title + # Pull the first H1 as the page title, fall back to the filename. + title=$(grep -m1 '^# ' "$md_file" | sed 's/^# //' || basename "$md_file" .md) + + cat <<-HEADER +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>${title}</title> + <style> + body { font-family: system-ui, -apple-system, sans-serif; max-width: 48rem; margin: 2rem auto; padding: 0 1rem; line-height: 1.6; color: #1a1a1a; } + h1,h2,h3 { margin-top: 1.5em; } + code { background: #f4f4f4; padding: .15em .3em; border-radius: 3px; } + pre { background: #f4f4f4; padding: 1em; overflow-x: auto; border-radius: 4px; } + a { color: #0969da; } + </style> +</head> +<body> +HEADER + + if command -v pandoc &>/dev/null; then + pandoc --from=markdown --to=html "$md_file" + else + # Bare-bones Markdown → HTML using sed (handles headings, bold, + # inline code, links, list items, and paragraphs). + sed -E \ + -e 's|^### (.+)|<h3>\1</h3>|' \ + -e 's|^## (.+)|<h2>\1</h2>|' \ + -e 's|^# (.+)|<h1>\1</h1>|' \ + -e 's|\*\*([^*]+)\*\*|<strong>\1</strong>|g' \ + -e 's|`([^`]+)`|<code>\1</code>|g' \ + -e 's|\[([^]]+)\]\(([^)]+)\)|<a href="\2">\1</a>|g' \ + -e 's|^- (.+)|<li>\1</li>|' \ + -e 's|^[0-9]+\. (.+)|<li>\1</li>|' \ + -e '/^$/s|.*|<br>|' \ + "$md_file" + fi + + cat <<-FOOTER +</body> +</html> +FOOTER +} + +# ── convert every .md file ──────────────────────────────────────────────── +count=0 +while IFS= read -r -d '' md; do + rel="${md#"$SRC_DIR"/}" + html_path="$OUT_DIR/${rel%.md}.html" + mkdir -p "$(dirname "$html_path")" + md_to_html "$md" > "$html_path" + echo " ✓ $md → $html_path" + count=$((count + 1)) +done < <(find "$SRC_DIR" -name '*.md' -print0 | sort -z) + +echo "" +echo "Build complete: $count page(s) written to $OUT_DIR/" diff --git a/examples/14-docusaurus-s3/docs/index.md b/examples/14-docusaurus-s3/docs/index.md @@ -0,0 +1,21 @@ +# Welcome to My Docs + +This is a sample documentation site built with a Docusaurus-style pipeline. + +## Getting Started + +Follow these steps to get up and running: + +1. Write your documentation in Markdown files under `docs/`. +2. Run the build to generate static HTML. +3. Deploy to an S3 bucket for hosting. + +## Features + +- **Simple Markdown authoring** — write docs in plain Markdown. +- **Static HTML output** — fast, cacheable, and easy to host. +- **S3 deployment** — publish directly to an AWS S3 bucket. + +## Learn More + +See the [Jaypore CI documentation](https://jay.sdf.org) for more examples.