From 57e232a27526c388dd5166627c5442d7e6812c19 Mon Sep 17 00:00:00 2001 From: Robert B Gordon Date: Sat, 16 May 2026 21:45:30 -0500 Subject: [PATCH 1/6] bump container version --- scripts/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install.sh b/scripts/install.sh index 8518d6c..cc65ffd 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -23,7 +23,7 @@ ORG_API_ENDPOINT="" DATA_ENDPOINT="" DATA_TLS="" DOCKER_IMAGE="controltheory/aigent" -DOCKER_IMAGE_TAG="v1.3.31" +DOCKER_IMAGE_TAG="v1.3.33" # Defaults OPERATION="install" From 89af89b34224b6089bad3b0f0533bca91e623d5a Mon Sep 17 00:00:00 2001 From: Robert B Gordon Date: Fri, 5 Jun 2026 22:11:06 -0500 Subject: [PATCH 2/6] Add --debug flag and quiet default output to install.sh Add -d/--debug flag; default output is now minimal start/done messages, with verbose details gated behind debug mode via debug() and run_cmd() helpers. Ignore unknown CLI args instead of erroring. Bump VERSION to v1.4.3. Note: also adds --set image.tag=dev and image.repository=localhost:32000/aigent to both helm installs, which look like local-dev overrides. --- scripts/install.sh | 145 +++++++++++++++++++++++++++------------------ 1 file changed, 87 insertions(+), 58 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index cc65ffd..1b7a4b0 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -3,7 +3,7 @@ set -e # ControlTheory Agent Installation Script # Version -VERSION="v1.4.2" +VERSION="v1.4.3" # Supports both Docker and Kubernetes (Helm) installations # # Usage: @@ -38,6 +38,23 @@ DEPLOYMENT_ENV="" SOURCE_ID="" HELM_VERSION="" HELM_DEVEL="false" +DEBUG="false" + +# Print only in debug mode +debug() { + if [ "$DEBUG" = "true" ]; then + echo "$@" + fi +} + +# Run a command, suppressing stdout unless debug mode (stderr always shown) +run_cmd() { + if [ "$DEBUG" = "true" ]; then + "$@" + else + "$@" > /dev/null + fi +} # Chart references (can be overridden via CT_CHARTS_PATH env var for local dev) AIGENT_DS_CHART="" @@ -52,6 +69,7 @@ Usage: $0 [options] General options: -o, --operation Operation: install, uninstall, status, preflight (default: install) -p, --platform

Target platform: docker, k8s (default: k8s) + -d, --debug Verbose output (default: minimal start/done messages) -h, --help Show this help message -v, --version Show version @@ -169,6 +187,10 @@ while [ $# -gt 0 ]; do HELM_DEVEL="true" shift 1 ;; + -d|--debug) + DEBUG="true" + shift 1 + ;; -h|--help) usage ;; @@ -176,6 +198,9 @@ while [ $# -gt 0 ]; do echo "$VERSION" exit 0 ;; + *) + shift 1 + ;; esac done @@ -263,9 +288,9 @@ CONTAINER_NAME="aigent" # Docker functions # docker_install() { - echo "Installing ControlTheory Agent via Docker..." - echo "Container Name: $CONTAINER_NAME" - echo "" + echo "ControlTheory Agent install started (docker)" + debug "Container Name: $CONTAINER_NAME" + debug "" # Capture hostname for container HOST_NAME=$(hostname) @@ -304,27 +329,29 @@ docker_install() { -p 4318:1758 \ ${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG}" - eval "$DOCKER_CMD" + if [ "$DEBUG" = "true" ]; then + eval "$DOCKER_CMD" + else + eval "$DOCKER_CMD" > /dev/null + fi - echo "" - echo "Docker installation complete!" - echo "Container: $CONTAINER_NAME" - echo "" - echo "To check status:" - echo " docker ps | grep $CONTAINER_NAME" - echo " docker logs $CONTAINER_NAME" + debug "" + debug "Container: $CONTAINER_NAME" + debug "" + debug "To check status:" + debug " docker ps | grep $CONTAINER_NAME" + debug " docker logs $CONTAINER_NAME" + echo "ControlTheory Agent install done" } docker_uninstall() { - echo "Uninstalling ControlTheory Agent from Docker..." - echo "Container Name: $CONTAINER_NAME" - echo "" + echo "ControlTheory Agent uninstall started (docker)" + debug "Container Name: $CONTAINER_NAME" - docker stop "$CONTAINER_NAME" 2>/dev/null || true - docker rm "$CONTAINER_NAME" 2>/dev/null || true + run_cmd docker stop "$CONTAINER_NAME" 2>/dev/null || true + run_cmd docker rm "$CONTAINER_NAME" 2>/dev/null || true - echo "" - echo "Docker uninstallation complete!" + echo "ControlTheory Agent uninstall done" } # @@ -373,7 +400,7 @@ quick_preflight_check() { } k8s_install_ds() { - echo "Installing AIgent DaemonSet (node log collection)..." + debug "Installing AIgent DaemonSet (node log collection)..." quick_preflight_check local HELM_ARGS=( @@ -382,6 +409,8 @@ k8s_install_ds() { --set daemonset.org_api_endpoint="$ORG_API_ENDPOINT" --set daemonset.cluster_name="$CLUSTER_NAME" --set daemonset.deployment_env="$DEPLOYMENT_ENV" + --set image.tag="dev" + --set image.repository="localhost:32000/aigent" --set daemonset.org_dns_id="$ORG_ID" ) @@ -399,7 +428,7 @@ k8s_install_ds() { if [ "$HOST_PORT" = "true" ]; then HELM_ARGS+=(--set hostPort.enabled=true) - echo " Host Port: enabled (1757/1758)" + debug " Host Port: enabled (1757/1758)" fi if [ -n "$HELM_VERSION" ]; then @@ -409,11 +438,11 @@ k8s_install_ds() { HELM_ARGS+=(--devel) fi - $HELM upgrade --install --create-namespace "$RELEASE_NAME_DS" "$AIGENT_DS_CHART" "${HELM_ARGS[@]}" + run_cmd $HELM upgrade --install --create-namespace "$RELEASE_NAME_DS" "$AIGENT_DS_CHART" "${HELM_ARGS[@]}" } k8s_install_cluster() { - echo "Installing AIgent Cluster Agent (k8s events)..." + debug "Installing AIgent Cluster Agent (k8s events)..." local HELM_ARGS=( --namespace="$NAMESPACE" @@ -421,6 +450,8 @@ k8s_install_cluster() { --set deployment.org_api_endpoint="$ORG_API_ENDPOINT" --set deployment.cluster_name="$CLUSTER_NAME" --set deployment.deployment_env="$DEPLOYMENT_ENV" + --set image.tag="dev" + --set image.repository="localhost:32000/aigent" --set deployment.org_dns_id="$ORG_ID" ) @@ -443,39 +474,39 @@ k8s_install_cluster() { HELM_ARGS+=(--devel) fi - $HELM upgrade --install --create-namespace "$RELEASE_NAME_CLUSTER" "$AIGENT_CLUSTER_CHART" "${HELM_ARGS[@]}" + run_cmd $HELM upgrade --install --create-namespace "$RELEASE_NAME_CLUSTER" "$AIGENT_CLUSTER_CHART" "${HELM_ARGS[@]}" } k8s_uninstall_ds() { - echo "Uninstalling AIgent DaemonSet..." - $HELM uninstall "$RELEASE_NAME_DS" --namespace="$NAMESPACE" 2>/dev/null || echo "DaemonSet release not found" + debug "Uninstalling AIgent DaemonSet..." + run_cmd $HELM uninstall "$RELEASE_NAME_DS" --namespace="$NAMESPACE" 2>/dev/null || echo "DaemonSet release not found" } k8s_uninstall_cluster() { - echo "Uninstalling AIgent Cluster Agent..." - $HELM uninstall "$RELEASE_NAME_CLUSTER" --namespace="$NAMESPACE" 2>/dev/null || echo "Cluster Agent release not found" + debug "Uninstalling AIgent Cluster Agent..." + run_cmd $HELM uninstall "$RELEASE_NAME_CLUSTER" --namespace="$NAMESPACE" 2>/dev/null || echo "Cluster Agent release not found" } k8s_install() { - echo "Installing ControlTheory Agent on Kubernetes..." - echo "Cluster Name: $CLUSTER_NAME" - echo "Deployment Environment: $DEPLOYMENT_ENV" - echo "Namespace: $NAMESPACE" - echo "Type: $TYPE" + echo "ControlTheory Agent install started (k8s)" + debug "Cluster Name: $CLUSTER_NAME" + debug "Deployment Environment: $DEPLOYMENT_ENV" + debug "Namespace: $NAMESPACE" + debug "Type: $TYPE" if [ "$HOST_PORT" = "true" ]; then - echo "Host Port: enabled (1757/1758)" + debug "Host Port: enabled (1757/1758)" fi if [ -n "$KUBECONFIG_FILE" ]; then - echo "Kubeconfig: $KUBECONFIG_FILE" + debug "Kubeconfig: $KUBECONFIG_FILE" fi if [ -n "$HELM_VERSION" ]; then - echo "Helm Chart Version: $HELM_VERSION" + debug "Helm Chart Version: $HELM_VERSION" fi # Set up chart references if [ -n "$CT_CHARTS_PATH" ]; then # -- use local charts if you have them handy - echo "Charts: $CT_CHARTS_PATH (local)..." + debug "Charts: $CT_CHARTS_PATH (local)..." AIGENT_DS_CHART="$CT_CHARTS_PATH/aigent-ds" AIGENT_CLUSTER_CHART="$CT_CHARTS_PATH/aigent-cluster" if [ ! -d "$AIGENT_DS_CHART" ] || [ ! -d "$AIGENT_CLUSTER_CHART" ]; then @@ -484,13 +515,13 @@ k8s_install() { fi else # Published repo - echo "Charts: Adding ct-helm repository..." - $HELM repo add ct-helm https://control-theory.github.io/helm-charts 2>/dev/null || true - $HELM repo update ct-helm + debug "Charts: Adding ct-helm repository..." + run_cmd $HELM repo add ct-helm https://control-theory.github.io/helm-charts 2>/dev/null || true + run_cmd $HELM repo update ct-helm AIGENT_DS_CHART="ct-helm/aigent-ds" AIGENT_CLUSTER_CHART="ct-helm/aigent-cluster" fi - echo "" + debug "" case "$TYPE" in ds) @@ -501,33 +532,32 @@ k8s_install() { ;; both) k8s_install_ds - echo "" + debug "" k8s_install_cluster ;; esac - echo "" - echo "Kubernetes installation complete!" - echo "Namespace: $NAMESPACE" + debug "" + debug "Namespace: $NAMESPACE" if [ "$TYPE" = "ds" ] || [ "$TYPE" = "both" ]; then - echo "DaemonSet Release: $RELEASE_NAME_DS" + debug "DaemonSet Release: $RELEASE_NAME_DS" fi if [ "$TYPE" = "cluster" ] || [ "$TYPE" = "both" ]; then - echo "Cluster Agent Release: $RELEASE_NAME_CLUSTER" + debug "Cluster Agent Release: $RELEASE_NAME_CLUSTER" fi - echo "" - echo "To check status:" - echo " $KUBECTL get pods -n $NAMESPACE" + debug "" + debug "To check status:" + debug " $KUBECTL get pods -n $NAMESPACE" + echo "ControlTheory Agent install done" } k8s_uninstall() { - echo "Uninstalling ControlTheory Agent from Kubernetes..." - echo "Namespace: $NAMESPACE" - echo "Type: $TYPE" + echo "ControlTheory Agent uninstall started (k8s)" + debug "Namespace: $NAMESPACE" + debug "Type: $TYPE" if [ -n "$KUBECONFIG_FILE" ]; then - echo "Kubeconfig: $KUBECONFIG_FILE" + debug "Kubeconfig: $KUBECONFIG_FILE" fi - echo "" case "$TYPE" in ds) @@ -542,8 +572,7 @@ k8s_uninstall() { ;; esac - echo "" - echo "Kubernetes uninstallation complete!" + echo "ControlTheory Agent uninstall done" } # @@ -1057,5 +1086,5 @@ else esac fi -echo "" +debug "" echo "Completed: $(date -u '+%Y-%m-%d %H:%M:%S UTC') | $VERSION" From 287d2d44dd4706b695e4f63fc5e163cfb6832147 Mon Sep 17 00:00:00 2001 From: Robert B Gordon Date: Fri, 5 Jun 2026 22:34:38 -0500 Subject: [PATCH 3/6] more infor in a compat way --- scripts/install.sh | 121 ++++++++++++++++++++++++++++++--------------- 1 file changed, 82 insertions(+), 39 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index 1b7a4b0..7bcd2bf 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -3,7 +3,7 @@ set -e # ControlTheory Agent Installation Script # Version -VERSION="v1.4.3" +VERSION="v1.4.4" # Supports both Docker and Kubernetes (Helm) installations # # Usage: @@ -56,6 +56,35 @@ run_cmd() { fi } +# Output formatting +HR_HEAVY="════════════════════════════════════════════════════════" +HR_LIGHT="────────────────────────────────────────────────────────" + +# Run an install step: capture all output to a log and print a single +# "✓ name detail" status line on success. On failure print "✗ name failed:" +# and dump the captured log indented, then exit. Debug mode streams the +# underlying command output instead of capturing it. +run_step() { + local name="$1" detail="$2" + shift 2 + if [ "$DEBUG" = "true" ]; then + "$@" + printf ' ✓ %-17s%s\n' "$name" "$detail" + return + fi + local log + log=$(mktemp) + if "$@" > "$log" 2>&1; then + printf ' ✓ %-17s%s\n' "$name" "$detail" + rm -f "$log" + else + printf ' ✗ %s failed:\n' "$name" + sed 's/^/ /' "$log" + rm -f "$log" + exit 1 + fi +} + # Chart references (can be overridden via CT_CHARTS_PATH env var for local dev) AIGENT_DS_CHART="" AIGENT_CLUSTER_CHART="" @@ -393,14 +422,11 @@ quick_preflight_check() { done if [[ $problem_nodes -gt 0 ]]; then - echo "" - echo "WARNING: $problem_nodes of $total_nodes nodes have insufficient resources (use -o preflight for details)" - echo "" + echo " WARNING: $problem_nodes of $total_nodes nodes have insufficient resources (use -o preflight for details)" fi } k8s_install_ds() { - debug "Installing AIgent DaemonSet (node log collection)..." quick_preflight_check local HELM_ARGS=( @@ -428,7 +454,6 @@ k8s_install_ds() { if [ "$HOST_PORT" = "true" ]; then HELM_ARGS+=(--set hostPort.enabled=true) - debug " Host Port: enabled (1757/1758)" fi if [ -n "$HELM_VERSION" ]; then @@ -438,12 +463,11 @@ k8s_install_ds() { HELM_ARGS+=(--devel) fi - run_cmd $HELM upgrade --install --create-namespace "$RELEASE_NAME_DS" "$AIGENT_DS_CHART" "${HELM_ARGS[@]}" + run_step "AIgent DaemonSet" "$(printf '%-16s— node log collection' "$RELEASE_NAME_DS")" \ + $HELM upgrade --install --create-namespace "$RELEASE_NAME_DS" "$AIGENT_DS_CHART" "${HELM_ARGS[@]}" } k8s_install_cluster() { - debug "Installing AIgent Cluster Agent (k8s events)..." - local HELM_ARGS=( --namespace="$NAMESPACE" --set deployment.controlplane.admission_token="$CLUSTER_ADMISSION_TOKEN" @@ -474,7 +498,8 @@ k8s_install_cluster() { HELM_ARGS+=(--devel) fi - run_cmd $HELM upgrade --install --create-namespace "$RELEASE_NAME_CLUSTER" "$AIGENT_CLUSTER_CHART" "${HELM_ARGS[@]}" + run_step "Cluster Agent" "$(printf '%-16s— k8s events' "$RELEASE_NAME_CLUSTER")" \ + $HELM upgrade --install --create-namespace "$RELEASE_NAME_CLUSTER" "$AIGENT_CLUSTER_CHART" "${HELM_ARGS[@]}" } k8s_uninstall_ds() { @@ -487,41 +512,58 @@ k8s_uninstall_cluster() { run_cmd $HELM uninstall "$RELEASE_NAME_CLUSTER" --namespace="$NAMESPACE" 2>/dev/null || echo "Cluster Agent release not found" } +# Helm repo setup as a single step so run_step can capture its output +helm_repo_setup() { + $HELM repo add ct-helm https://control-theory.github.io/helm-charts 2>/dev/null || true + $HELM repo update ct-helm +} + k8s_install() { - echo "ControlTheory Agent install started (k8s)" - debug "Cluster Name: $CLUSTER_NAME" - debug "Deployment Environment: $DEPLOYMENT_ENV" - debug "Namespace: $NAMESPACE" - debug "Type: $TYPE" + local type_desc="$TYPE" + case "$TYPE" in + ds) type_desc="ds (daemonset)" ;; + cluster) type_desc="cluster (cluster agent)" ;; + both) type_desc="both (daemonset + cluster agent)" ;; + esac + + echo "$HR_HEAVY" + echo " ControlTheory Agent · Kubernetes Install" + echo "$HR_HEAVY" + echo "" + printf ' %-13s: %s\n' "Cluster" "$CLUSTER_NAME" + printf ' %-13s: %s\n' "Environment" "$DEPLOYMENT_ENV" + printf ' %-13s: %s\n' "Namespace" "$NAMESPACE" + printf ' %-13s: %s\n' "Type" "$type_desc" if [ "$HOST_PORT" = "true" ]; then - debug "Host Port: enabled (1757/1758)" - fi - if [ -n "$KUBECONFIG_FILE" ]; then - debug "Kubeconfig: $KUBECONFIG_FILE" + printf ' %-13s: %s\n' "Host Port" "1757/1758" + else + printf ' %-13s: %s\n' "Host Port" "disabled" fi + printf ' %-13s: %s\n' "Kubeconfig" "$KUBECONFIG_FILE" if [ -n "$HELM_VERSION" ]; then - debug "Helm Chart Version: $HELM_VERSION" + printf ' %-13s: %s\n' "Chart Version" "$HELM_VERSION" fi + echo "" + echo "$HR_LIGHT" + echo " Installing" + echo "$HR_LIGHT" # Set up chart references if [ -n "$CT_CHARTS_PATH" ]; then # -- use local charts if you have them handy - debug "Charts: $CT_CHARTS_PATH (local)..." AIGENT_DS_CHART="$CT_CHARTS_PATH/aigent-ds" AIGENT_CLUSTER_CHART="$CT_CHARTS_PATH/aigent-cluster" if [ ! -d "$AIGENT_DS_CHART" ] || [ ! -d "$AIGENT_CLUSTER_CHART" ]; then - echo "Error: CT_CHARTS_PATH is set but charts not found at $CT_CHARTS_PATH" + printf ' ✗ Helm charts not found at %s\n' "$CT_CHARTS_PATH" exit 1 fi + printf ' ✓ %-17s%s\n' "Helm charts" "local ($CT_CHARTS_PATH)" else # Published repo - debug "Charts: Adding ct-helm repository..." - run_cmd $HELM repo add ct-helm https://control-theory.github.io/helm-charts 2>/dev/null || true - run_cmd $HELM repo update ct-helm AIGENT_DS_CHART="ct-helm/aigent-ds" AIGENT_CLUSTER_CHART="ct-helm/aigent-cluster" + run_step "Helm repo" "ct-helm (updated)" helm_repo_setup fi - debug "" case "$TYPE" in ds) @@ -532,23 +574,24 @@ k8s_install() { ;; both) k8s_install_ds - debug "" k8s_install_cluster ;; esac - debug "" - debug "Namespace: $NAMESPACE" - if [ "$TYPE" = "ds" ] || [ "$TYPE" = "both" ]; then - debug "DaemonSet Release: $RELEASE_NAME_DS" - fi - if [ "$TYPE" = "cluster" ] || [ "$TYPE" = "both" ]; then - debug "Cluster Agent Release: $RELEASE_NAME_CLUSTER" + echo "" + echo "$HR_LIGHT" + echo " Install complete" + echo "$HR_LIGHT" + echo "" + + # Only include --kubeconfig in the hint when a non-default path was given + local kubectl_hint="kubectl" + if [ -n "$KUBECONFIG_FILE" ] && [ "$KUBECONFIG_FILE" != "$HOME/.kube/config" ]; then + kubectl_hint="kubectl --kubeconfig $KUBECONFIG_FILE" fi - debug "" - debug "To check status:" - debug " $KUBECTL get pods -n $NAMESPACE" - echo "ControlTheory Agent install done" + echo " Check pod status:" + echo " $kubectl_hint get pods -n $NAMESPACE" + echo "" } k8s_uninstall() { @@ -1087,4 +1130,4 @@ else fi debug "" -echo "Completed: $(date -u '+%Y-%m-%d %H:%M:%S UTC') | $VERSION" +echo " Completed: $(date -u '+%Y-%m-%d %H:%M:%S UTC') $VERSION" From 541f806fd4b620ca62d70bb974407dae0c604dd9 Mon Sep 17 00:00:00 2001 From: Robert B Gordon Date: Wed, 10 Jun 2026 09:45:41 -0500 Subject: [PATCH 4/6] Remove dev image tag and local registry overrides from install.sh Drop the hardcoded `image.tag=dev` and `image.repository=localhost:32000/aigent` --set overrides from both k8s_install_ds and k8s_install_cluster so installs use the chart's default image. --- scripts/install.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index 7bcd2bf..530671e 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -435,8 +435,6 @@ k8s_install_ds() { --set daemonset.org_api_endpoint="$ORG_API_ENDPOINT" --set daemonset.cluster_name="$CLUSTER_NAME" --set daemonset.deployment_env="$DEPLOYMENT_ENV" - --set image.tag="dev" - --set image.repository="localhost:32000/aigent" --set daemonset.org_dns_id="$ORG_ID" ) @@ -474,8 +472,6 @@ k8s_install_cluster() { --set deployment.org_api_endpoint="$ORG_API_ENDPOINT" --set deployment.cluster_name="$CLUSTER_NAME" --set deployment.deployment_env="$DEPLOYMENT_ENV" - --set image.tag="dev" - --set image.repository="localhost:32000/aigent" --set deployment.org_dns_id="$ORG_ID" ) From 11ee8fb872145fc7ff6e787bfc35147df8a0153d Mon Sep 17 00:00:00 2001 From: Robert B Gordon Date: Wed, 10 Jun 2026 09:50:03 -0500 Subject: [PATCH 5/6] bump for vis --- scripts/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install.sh b/scripts/install.sh index 530671e..1879a0a 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -3,7 +3,7 @@ set -e # ControlTheory Agent Installation Script # Version -VERSION="v1.4.4" +VERSION="v1.4.5" # Supports both Docker and Kubernetes (Helm) installations # # Usage: From 94e224dfcae1e7db282ae5687fcf8775e00551c1 Mon Sep 17 00:00:00 2001 From: Robert B Gordon Date: Mon, 29 Jun 2026 18:55:53 -0500 Subject: [PATCH 6/6] Enforce stage-only merges to main and document release workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add CONTRIBUTING.md describing the feature → stage → main branching and release flow, including how install scripts publish to the Pages site root vs /stage/. Add .github/workflows/enforce-stage-only.yml with a source-branch-guard check that fails any PR into main whose source branch isn't stage, intended as a required status check on main. --- .github/workflows/enforce-stage-only.yml | 18 +++++++++++++ CONTRIBUTING.md | 33 ++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 .github/workflows/enforce-stage-only.yml create mode 100644 CONTRIBUTING.md diff --git a/.github/workflows/enforce-stage-only.yml b/.github/workflows/enforce-stage-only.yml new file mode 100644 index 0000000..cbd381f --- /dev/null +++ b/.github/workflows/enforce-stage-only.yml @@ -0,0 +1,18 @@ +name: Enforce stage-only merges to main +# Guards the publish branch: the ONLY branch allowed to open a PR into `main` +# is `stage`. Make `source-branch-guard` a required status check on `main` +# (already configured via branch protection) so this cannot be bypassed. +on: + pull_request: + branches: [main] +jobs: + source-branch-guard: + runs-on: ubuntu-latest + steps: + - name: Require PR source to be stage + run: | + if [ "${{ github.head_ref }}" != "stage" ]; then + echo "::error::PRs into 'main' must come from 'stage' (got '${{ github.head_ref }}'). Land your change on 'stage' first, then merge stage -> main." + exit 1 + fi + echo "OK: source branch is 'stage'." diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0ec3e44 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# Contributing + +## Branching & release workflow + +This repo publishes the **public install scripts** (served at `install.controltheory.com` via GitHub Pages). The `main` branch is what the public gets — do **not** commit to it directly. + +``` +feature branch ──► stage ──► (PR) ──► main + (your work) (test) (public release) +``` + +| Branch | Role | Where it publishes (`deploy.yml`) | +|--------|------|-----------------------------------| +| `stage` | Integration & testing | served under `/stage/` on the Pages site | +| `main` | Public release | served at the site **root** (what users `curl`) | + +> Heads-up: `deploy.yml` builds the site from **both** branches. Pushing to `main` changes the public root immediately; changes you only put on `stage` show up under `/stage/`, **not** at the root. (This is the gotcha that bit us — test on `stage`, release via `main`.) + +### How to land a change + +1. Branch off `stage`, do your work, open a PR **into `stage`**. +2. Merge to `stage` and test against the `/stage/` scripts. +3. When it's good, open a PR **from `stage` into `main`** and merge it. That is the *only* way to release publicly. + +### Enforcement (so we can't forget) + +`main` is protected: + +- **Direct pushes are blocked** — everything goes through a PR. +- **Only `stage` may be merged into `main`.** A required status check (`source-branch-guard`, see `.github/workflows/enforce-stage-only.yml`) fails any PR into `main` whose source branch isn't `stage`. +- Force-pushes and branch deletion are blocked, and the rules apply to admins too. + +If you find yourself wanting to push to `main` directly: don't. Land it on `stage`, then merge `stage` → `main`.