conductor

CI task system
git clone git://git.finwo.net/app/conductor
Log | Files | Refs | README | LICENSE

commit d3b0ef3904416797c50829677052757079d6a1ad
parent 3d75d459b107891ee9ba0d846567567c39efc199
Author: finwo <finwo@pm.me>
Date:   Sun, 20 Sep 2026 22:30:51 +0200

Simplified post-receive hook

Diffstat:
Ahooks/conductor.conf.example | 5+++++
Mhooks/post-receive | 127++++++++++++++++++++++++++++++++++++-------------------------------------------
2 files changed, 62 insertions(+), 70 deletions(-)

diff --git a/hooks/conductor.conf.example b/hooks/conductor.conf.example @@ -0,0 +1,5 @@ +# conductor.conf - webhook configuration for post-receive hook +# Format: url secret (one per line, comments start with #) + +# https://ci.example.com/api/v1/projects/prod/trigger secret123 +# https://ci.staging.com/api/v1/projects/staging/trigger staging_secret diff --git a/hooks/post-receive b/hooks/post-receive @@ -1,47 +1,27 @@ #!/bin/sh -# hooks/post-receive - notify a conductor of pushed commits +# hooks/post-receive - notify conductors of pushed commits # # Install into a bare repository as hooks/post-receive, chmod +x. # Reads "<old> <new> <ref>" per pushed ref on stdin. # -# Required: -# CONDUCTOR_URL base url, for example https://ci.example.com -# CONDUCTOR_PROJECT project id registered with the conductor -# -# Optional: -# CONDUCTOR_SECRET trigger secret, or a path to a file holding it. -# Required whenever the project has one set. -# CONDUCTOR_REFS space separated refs to build. Default: every -# branch. Example: "refs/heads/main refs/heads/dev" -# CONDUCTOR_TIMEOUT seconds to wait per request, default 10 -# -# The hook decides nothing about the build. It reports which commit was -# pushed and the conductor reads the pipeline from the repository itself. -# A failure here never blocks the push. +# Expects conductor.conf in the parent directory with lines: +# <trigger_url> <secret> +# Blank lines and lines starting with # are ignored. set -eu -CONDUCTOR_URL="${CONDUCTOR_URL:-}" -CONDUCTOR_PROJECT="${CONDUCTOR_PROJECT:-}" -CONDUCTOR_SECRET="${CONDUCTOR_SECRET:-}" -CONDUCTOR_REFS="${CONDUCTOR_REFS:-}" -CONDUCTOR_TIMEOUT="${CONDUCTOR_TIMEOUT:-10}" - ZERO="0000000000000000000000000000000000000000" +DIRSELF=$(dirname $(realpath $0)) +CONFIG="${DIRSELF}/../conductor.conf" + log() { printf '[conductor] %s\n' "$*" >&2; } -if [ -z "${CONDUCTOR_URL}" ] || [ -z "${CONDUCTOR_PROJECT}" ]; then - log "CONDUCTOR_URL and CONDUCTOR_PROJECT must be set; not triggering" +if [ ! -f "${CONFIG}" ]; then + log "no conductor.conf found; not triggering" exit 0 fi -# The secret may be given inline or as a file, so it can stay out of the -# process environment on shared hosts. -if [ -n "${CONDUCTOR_SECRET}" ] && [ -f "${CONDUCTOR_SECRET}" ]; then - CONDUCTOR_SECRET=$(cat "${CONDUCTOR_SECRET}") -fi - if command -v curl >/dev/null 2>&1; then http_client=curl elif command -v wget >/dev/null 2>&1; then @@ -51,15 +31,39 @@ else exit 0 fi -want_ref() { - [ -z "${CONDUCTOR_REFS}" ] && case "$1" in - refs/heads/*) return 0 ;; - *) return 1 ;; - esac - for candidate in ${CONDUCTOR_REFS}; do - [ "${candidate}" = "$1" ] && return 0 - done - return 1 +trigger() { + _url="$1" + _secret="$2" + _payload="$3" + _ref="$4" + + if [ -n "${_secret}" ]; then + digest=$(printf '%s' "${_payload}" | openssl dgst -sha256 -hmac "${_secret}" | sed 's/^.*[= ]//') + signature="sha256=${digest}" + else + signature="" + log "warning: no secret for ${_url}, sending unsigned" + fi + + if [ "${http_client}" = curl ]; then + response=$(curl -fsS -m 10 -X POST "${_url}" \ + -H 'Content-Type: application/json' \ + -H "X-Hub-Signature-256: ${signature}" \ + -d "${_payload}" 2>&1) || { + log "trigger failed for ${_ref}: ${response}" + return + } + else + response=$(wget -qO- --timeout=10 \ + --header='Content-Type: application/json' \ + --header="X-Hub-Signature-256: ${signature}" \ + --post-data="${_payload}" "${_url}" 2>&1) || { + log "trigger failed for ${_ref}: ${response}" + return + } + fi + + log "${response}" } while read -r old new ref; do @@ -68,12 +72,11 @@ while read -r old new ref; do continue fi - if ! want_ref "${ref}"; then - log "skipping ${ref}" - continue - fi + case "${ref}" in + refs/heads/*) ;; + *) log "skipping ${ref}"; continue ;; + esac - # A new branch reports an all zero parent, which is not a commit. if [ "${old}" = "${ZERO}" ]; then base="" else @@ -83,36 +86,20 @@ while read -r old new ref; do payload=$(printf '{"sha":"%s","base":"%s","ref":"%s","actor":"%s"}' \ "${new}" "${base}" "${ref}" "${USER:-git}") - if [ -n "${CONDUCTOR_SECRET}" ]; then - digest=$(printf '%s' "${payload}" | openssl dgst -sha256 -hmac "${CONDUCTOR_SECRET}" | sed 's/^.*[= ]//') - signature="sha256=${digest}" - else - signature="" - log "warning: no CONDUCTOR_SECRET set, sending unsigned" - fi - - url="${CONDUCTOR_URL%/}/api/v1/projects/${CONDUCTOR_PROJECT}/trigger" log "triggering ${ref} at $(echo "${new}" | cut -c1-12)" - if [ "${http_client}" = curl ]; then - response=$(curl -fsS -m "${CONDUCTOR_TIMEOUT}" -X POST "${url}" \ - -H 'Content-Type: application/json' \ - -H "X-Hub-Signature-256: ${signature}" \ - -d "${payload}" 2>&1) || { - log "trigger failed for ${ref}: ${response}" - continue - } - else - response=$(wget -qO- --timeout="${CONDUCTOR_TIMEOUT}" \ - --header='Content-Type: application/json' \ - --header="X-Hub-Signature-256: ${signature}" \ - --post-data="${payload}" "${url}" 2>&1) || { - log "trigger failed for ${ref}: ${response}" - continue - } - fi + while IFS= read -r line; do + case "${line}" in + ''|\#*) continue ;; + esac - log "${response}" + url=$(printf '%s' "${line}" | awk '{print $1}') + secret=$(printf '%s' "${line}" | awk '{print $2}') + + [ -z "${url}" ] && continue + + trigger "${url}" "${secret}" "${payload}" "${ref}" "${new}" + done < "${CONFIG}" done exit 0