Jaypore CI

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

commit 1aa9821cd9b6a63a1061aae84af5eda682d0b250
parent 93b9f6689441b680d72a109f39fb3cf02d104b81
Author: Arjoonn Sharma <arjoonn@midpathsoftware.com>
Date:   Fri, 27 Mar 2026 11:19:38 +0000

Publish via cli (!14)

Reviewed-on: https://gitea.midpathsoftware.com/midpath/jayporeci/pulls/14
Co-authored-by: Arjoonn Sharma <arjoonn@midpathsoftware.com>
Co-committed-by: Arjoonn Sharma <arjoonn@midpathsoftware.com>

Diffstat:
M.jci/run.sh | 6+++---
MREADME.md | 12+++++++++---
Mscripts/build_image.sh | 2+-
Mscripts/publish_site.sh | 36++++++++++++++++++++++++------------
Asecrets/.gitignore | 3+++
Asecrets/bin/.gitignore | 3+++
Asecrets/bin/create_envfile.sh | 8++++++++
Asecrets/bin/edit_env.sh | 30++++++++++++++++++++++++++++++
Asecrets/bin/ensure_bins.sh | 34++++++++++++++++++++++++++++++++++
Asecrets/bin/set_env.sh | 8++++++++
Asecrets/prod.enc | 9+++++++++
11 files changed, 132 insertions(+), 19 deletions(-)

diff --git a/.jci/run.sh b/.jci/run.sh @@ -215,9 +215,9 @@ echo "" # --------------------------------------------------------------------------- # Step 5: Build site (sequential — needs the binaries to be present) # --------------------------------------------------------------------------- -echo "--- Step 5: Building site inside jci container ---" -run_step "Docs & Site" "scripts/build_site.sh" \ - docker run --rm -v "$PWD:/tmp/Jaypore CI" jci "/tmp/Jaypore CI/scripts/build_site.sh" +echo "--- Step 5: Building and publishing site ---" +run_step "Docs & Site" "scripts/publish_site.sh" \ + docker run --rm -v "$PWD:/tmp/Jaypore CI" jci "/tmp/Jaypore CI/scripts/publish_site.sh" echo "" echo "All steps completed successfully!" diff --git a/README.md b/README.md @@ -86,7 +86,13 @@ chmod +x .jci/run.sh 0 0 * * * branch:main name:nightly # named midnight build on main ``` -After editing `.jci/crontab`, run `git jci cron sync` to install the entries into your system's crontab. Cron jobs run with the repository root as the working directory and have access to all three `JCI_*` environment variables. +After editing `.jci/crontab`, run `git jci cron sync` to install the entries into your system's crontab. Each installed entry runs `git-jci run` from the repository root. For example, the `branch:main name:nightly` entry above becomes: + +``` +0 0 * * * cd '/path/to/repo' && git fetch --quiet 2>/dev/null; git checkout --quiet main 2>/dev/null && git pull --quiet 2>/dev/null; git-jci run # JCI:<repoID> [nightly] +``` + +Cron jobs run with the repository root as the working directory and have access to all three `JCI_*` environment variables. ## Environment Vars @@ -714,7 +720,7 @@ run_js=false run_go=false if [ -z "$changed_files" ]; then - echo "No diff detected (first commit or shallow clone) running all sub-pipelines." + echo "No diff detected (first commit or shallow clone) — running all sub-pipelines." run_python=true run_js=true run_go=true @@ -757,7 +763,7 @@ if $run_python; 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." + 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 diff --git a/scripts/build_image.sh b/scripts/build_image.sh @@ -12,7 +12,7 @@ echo "Building Docker image '${IMAGE_NAME}'..." # The repo is NOT baked into the image; it is mounted at runtime via -v. docker build -t "${IMAGE_NAME}" - <<'DOCKERFILE' FROM alpine:latest -RUN apk add --no-cache git ca-certificates stagit bash lowdown +RUN apk add --no-cache git ca-certificates stagit bash lowdown curl python3 zip WORKDIR /work CMD ["/bin/sh"] DOCKERFILE diff --git a/scripts/publish_site.sh b/scripts/publish_site.sh @@ -4,21 +4,33 @@ set -o errexit set -o nounset set -o pipefail +export REPO_DIR="/tmp/Jaypore CI" +export PUBLIC_DIR="${REPO_DIR}/www_jci/public" + publish() { echo "Publishing site" + cd "$REPO_DIR" pwd - cd website - md5sum secrets/ci.key - source secrets/bin/set_env.sh ci - - cd /vol/www && zip -r ../website.zip . - - echo Pushing build - curl -H "Content-Type: application/zip" \ - -H "Authorization: Bearer $NETLIFY_TOKEN" \ - --data-binary "@/vol/website.zip" \ - https://api.netlify.com/api/v1/sites/$NETLIFY_SITEID/deploys | python3 -m json.tool + md5sum secrets/prod.key + md5sum secrets/prod.enc + source secrets/bin/set_env.sh prod + echo "Build site" + bash scripts/build_site.sh + echo "---========================---" + cp -r "$PUBLIC_DIR" / + (cd /build && ls -al .) + echo "---========================---" + echo "Creating zip" + ( + cd /public \ + && zip -r /website.zip ./ \ + && cd / \ + && echo "Publishing Site ID: $NETLIFY_SITE_ID" \ + && curl -H "Content-Type: application/zip" \ + -H "Authorization: Bearer $NETLIFY_API_TOKEN" \ + --data-binary "@website.zip" \ + https://api.netlify.com/api/v1/sites/$NETLIFY_SITE_ID/deploys + ) } (publish) - diff --git a/secrets/.gitignore b/secrets/.gitignore @@ -0,0 +1,3 @@ +*.key +*.plaintext* +!dev.key diff --git a/secrets/bin/.gitignore b/secrets/bin/.gitignore @@ -0,0 +1,3 @@ +age +age-keygen +sops diff --git a/secrets/bin/create_envfile.sh b/secrets/bin/create_envfile.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +BIN=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +SECRETS=$(echo "$BIN/..") +NAME=$1 +(bash $BIN/ensure_bins.sh) +PATH="$PATH:$HOME/.local/bin:$BIN" +SOPS_AGE_KEY_FILE=$SECRETS/$NAME.key sops --decrypt --input-type dotenv --output-type dotenv $SECRETS/$NAME.enc > secrets/$NAME.plaintext.env diff --git a/secrets/bin/edit_env.sh b/secrets/bin/edit_env.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -o errexit +set -o pipefail + +main (){ + NAME=$1 + BIN=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + SECRETS=$(echo "$BIN/..") + KEY_FILE=$(echo "$SECRETS/$NAME.key") + ENC_FILE=$(echo "$SECRETS/$NAME.enc") + PLAINTEXT_FILE=$(echo "$SECRETS/$NAME.plaintext") + export SOPS_AGE_KEY_FILE=$KEY_FILE + echo "BIN = $BIN" + echo "SECRETS = $SECRETS" + echo "KEY = $KEY_FILE" + echo "SOPS KEY = $SOPS_AGE_KEY_FILE" + echo "ENC = $ENC_FILE" + echo "PLAIN = $PLAINTEXT_FILE" + (bash $BIN/ensure_bins.sh) + PATH="$PATH:$HOME/.local/bin:$BIN" + + if [[ -f "$ENC_FILE" ]]; then + sops --decrypt --input-type dotenv --output-type dotenv "$ENC_FILE" > "$PLAINTEXT_FILE" + fi + ${EDITOR:-nano} "$PLAINTEXT_FILE" + sops --input-type dotenv --output-type dotenv --encrypt --age $(age-keygen -y "$KEY_FILE") "$PLAINTEXT_FILE" > "$ENC_FILE" + rm "$PLAINTEXT_FILE" +} +(main $1) diff --git a/secrets/bin/ensure_bins.sh b/secrets/bin/ensure_bins.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -o errexit +set -o pipefail + +get_sops(){ + if sops --version > /dev/null + then + return + else + echo "SOPS not found in PATH. Downloading..." + curl -L -o $HOME/.local/bin/sops https://github.com/getsops/sops/releases/download/v3.8.1/sops-v3.8.1.linux.amd64 + chmod u+x $HOME/.local/bin/sops + fi +} + +get_age(){ + if age --version > /dev/null + then + return + else + echo "AGE not found in PATH. Downloading..." + curl -L -o /tmp/age.tar.gz https://github.com/FiloSottile/age/releases/download/v1.1.1/age-v1.1.1-linux-amd64.tar.gz + (cd /tmp && tar xf age.tar.gz && cd age && cp age $HOME/.local/bin && cp age-keygen $HOME/.local/bin) + fi +} + +main (){ + mkdir -p $HOME/.local/bin + PATH="$PATH:$HOME/.local/bin" + get_age + get_sops +} +(main) diff --git a/secrets/bin/set_env.sh b/secrets/bin/set_env.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +BIN=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +SECRETS=$(echo "$BIN/..") +NAME=$1 +(bash "$BIN/ensure_bins.sh") +PATH="$PATH:$HOME/.local/bin:$BIN" +export $(SOPS_AGE_KEY_FILE="$SECRETS/$NAME.key" sops --decrypt --input-type dotenv --output-type dotenv "$SECRETS/$NAME.enc" | xargs) diff --git a/secrets/prod.enc b/secrets/prod.enc @@ -0,0 +1,9 @@ +ENV=ENC[AES256_GCM,data:7hbwjQ==,iv:ibu94jXJgUCIOe1XghxoquzPfU8lzkUuFWom4GNt2/o=,tag:wI5HU40TNKTOFFfo/R2/Kw==,type:str] +NETLIFY_SITE_ID=ENC[AES256_GCM,data:GElDtor7JvsI2Vdxxz79lNAAT/JBX7rnB3hw0Ukm7EPrwEBX,iv:vWYahbVnodsiCZbdodT+EkrFuT/vttN5MOHWdXXXSBE=,tag:6FUvVMzraMRQLv/yAPscGA==,type:str] +NETLIFY_API_TOKEN=ENC[AES256_GCM,data:P0rLxyxDPBz8CSl4JuByBBIH7ftZcGNYNOS01vRgqsdcnB0p61zHpQ==,iv:sjAAPpiRoPMgT35OjZIRgyEzR+mvLf7q5/ShIZxW0Nk=,tag:+Pab5wkh8gXgp+b7wwVmFw==,type:str] +sops_mac=ENC[AES256_GCM,data:1MOQQMGmwJvXG0OevC/DyVV74l7eGCdI5vA5HDDXuSIsxiyHOgIaw8oidmFnxCbPjvYOq9VD8OJy3KBvRRu1JG/6jNE2k/u4e7lLEkaqYo/nIZaL3+LkA9BfI5b45qK1S73WjvKrwdsVgKQBKs6E+jnBAdG0kAHZNEWxoN+780w=,iv:rApwN9Bvkpk/jmPKsaIOyXdDErl0YGx//+A9wuzs0fQ=,tag:U/ZnJay2FQNxxne+TudXbQ==,type:str] +sops_age__list_0__map_recipient=age19ql6rxw38v44a665h2fdvzq3r3c57huzfdz84wfnu0fhn5z0458q4d2rn4 +sops_lastmodified=2026-03-27T08:08:47Z +sops_unencrypted_suffix=_unencrypted +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkNjNnNURUU1J3T3ZiWE1m\nb1NOL3BZWlNkK1lrK3kwQVNXdWxLbXkreFh3CmxGUStYWkdmVWVvTUIzWk9idGZy\nbW1MK29nODZacC9FVWE0NVJPa3ZORUEKLS0tIENrS3U1M3VCd1FKTGE4NHU1eVdu\nT2VadzU2eWdHQzN2VHo0b2J3dG9QWEUKsfTExhi5fxH7F8CUkva2XLIhAii7r6F2\nVfMD2CIGWqWJbSPPJ6W4E5XemgAdupgvp6RQWZRo4PsC7G8980ldkg==\n-----END AGE ENCRYPTED FILE-----\n +sops_version=3.7.3