Skip to content

Instantly share code, notes, and snippets.

@denislemire
Created April 14, 2026 18:58
Show Gist options
  • Select an option

  • Save denislemire/f4119bba6aa1ddd645695c94808e615d to your computer and use it in GitHub Desktop.

Select an option

Save denislemire/f4119bba6aa1ddd645695c94808e615d to your computer and use it in GitHub Desktop.
rotate-oauth-trigger.sh
#!/usr/bin/env sh
# Rotate a GitHub OAuth VCS trigger via CircleCI API v2 (delete + recreate).
# Same effect as Project Setup: removes the GitHub webhook and registers a new one.
#
# Requires: curl, jq
# Auth: https://circleci.com/docs/guides/toolkit/api-developers-guide/
#
# Usage:
# export CIRCLE_TOKEN=... # personal API token; header Circle-Token
# export CIRCLECI_PROJECT_SLUG=gh/your-org/your-repo # OAuth org slug form
# # optional:
# # export PIPELINE_DEFINITION_ID=<uuid> # if omitted, first github_oauth definition is used
# # export TRIGGER_ID=<uuid> # if omitted and multiple OAuth triggers, script exits with error
# # export DRY_RUN=1 # print actions only
# ./rotate-oauth-trigger.sh
#
# API reference (OpenAPI): https://circleci.com/api/v2/openapi.json
# Project admin / triggers: paths under /projects/{project_id}/...
set -eu
API_ROOT="${CIRCLECI_API_ROOT:-https://circleci.com/api/v2}"
die() { printf '%s\n' "$*" >&2; exit 1; }
need_cmd() { command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"; }
need_cmd curl
need_cmd jq
[ -n "${CIRCLE_TOKEN:-}" ] || die "set CIRCLE_TOKEN"
[ -n "${CIRCLECI_PROJECT_SLUG:-}" ] || die "set CIRCLECI_PROJECT_SLUG (e.g. gh/org/repo)"
uri_escape() { jq -nr --arg s "$1" '$s | @uri'; }
curl_cci() {
curl -sS -f \
-H "Circle-Token: ${CIRCLE_TOKEN}" \
-H "Accept: application/json" \
"$@"
}
ENC_SLUG="$(uri_escape "${CIRCLECI_PROJECT_SLUG}")"
# 1) Project UUID from human slug
PROJ_JSON="$(curl_cci "${API_ROOT}/project/${ENC_SLUG}")" || die "failed GET /project (check slug and token)"
PROJECT_ID="$(printf '%s' "$PROJ_JSON" | jq -r '.id')"
[ -n "$PROJECT_ID" ] && [ "$PROJECT_ID" != null ] || die "could not read project id from API response"
# 2) Pipeline definition (github_oauth)
DEFS_JSON="$(curl_cci "${API_ROOT}/projects/${PROJECT_ID}/pipeline-definitions")" || die "failed GET pipeline-definitions"
pick_oauth_def_id() {
printf '%s' "$DEFS_JSON" | jq -r '
.items[]
| select(.config_source.provider == "github_oauth")
| .id' | head -n 1
}
if [ -n "${PIPELINE_DEFINITION_ID:-}" ]; then
PIPELINE_DEF_ID="$PIPELINE_DEFINITION_ID"
else
PIPELINE_DEF_ID="$(pick_oauth_def_id)"
fi
[ -n "$PIPELINE_DEF_ID" ] || die "no pipeline definition with config_source.provider=github_oauth; set PIPELINE_DEFINITION_ID explicitly"
# 3) Triggers on that definition
TRIG_LIST="$(curl_cci "${API_ROOT}/projects/${PROJECT_ID}/pipeline-definitions/${PIPELINE_DEF_ID}/triggers")" || die "failed GET triggers"
oauth_trigger_ids() {
printf '%s' "$TRIG_LIST" | jq -r '
.items[]
| select(.event_source.provider == "github_oauth")
| .id'
}
if [ -n "${TRIGGER_ID:-}" ]; then
TID="$TRIGGER_ID"
else
count="$(oauth_trigger_ids | wc -l | tr -d ' ')"
[ "$count" -ge 1 ] || die "no github_oauth trigger on pipeline definition ${PIPELINE_DEF_ID}"
[ "$count" -eq 1 ] || die "multiple github_oauth triggers; set TRIGGER_ID to one of: $(oauth_trigger_ids | tr '\n' ' ')"
TID="$(oauth_trigger_ids | head -n 1)"
fi
# 4) Full trigger payload (for verbatim-ish recreate)
TRIG_JSON="$(curl_cci "${API_ROOT}/projects/${PROJECT_ID}/triggers/${TID}")" || die "failed GET trigger ${TID}"
# Build createTriggerRequest from GET trigger (strip read-only fields).
CREATE_BODY="$(printf '%s' "$TRIG_JSON" | jq '
{
event_source: {
provider: .event_source.provider,
repo: { external_id: .event_source.repo.external_id }
}
}
+ (if (.event_preset | type) == "string" and .event_preset != "" then { event_preset: .event_preset } else {} end)
+ (if (.checkout_ref | type) == "string" and .checkout_ref != "" then { checkout_ref: .checkout_ref } else {} end)
+ (if (.config_ref | type) == "string" and .config_ref != "" then { config_ref: .config_ref } else {} end)
')"
printf 'Project id: %s\nPipeline definition id: %s\nTrigger id: %s\n' "$PROJECT_ID" "$PIPELINE_DEF_ID" "$TID" >&2
printf 'Recreate body:\n%s\n' "$CREATE_BODY" >&2
if [ -n "${DRY_RUN:-}" ]; then
printf '\nDRY_RUN set — no DELETE/POST performed. Would run:\n' >&2
printf ' DELETE %s/projects/%s/triggers/%s\n' "$API_ROOT" "$PROJECT_ID" "$TID" >&2
printf ' POST %s/projects/%s/pipeline-definitions/%s/triggers\n' "$API_ROOT" "$PROJECT_ID" "$PIPELINE_DEF_ID" >&2
exit 0
fi
# 5) Delete then create
curl_cci -X DELETE "${API_ROOT}/projects/${PROJECT_ID}/triggers/${TID}" >/dev/null || die "DELETE trigger failed"
NEW_TRIG="$(curl_cci -X POST "${API_ROOT}/projects/${PROJECT_ID}/pipeline-definitions/${PIPELINE_DEF_ID}/triggers" \
-H "Content-Type: application/json" \
-d "$CREATE_BODY")" || die "POST create trigger failed (trigger was deleted; re-add via UI if needed)"
printf '%s\n' "$NEW_TRIG" | jq .
printf '\nNew trigger id: %s\n' "$(printf '%s' "$NEW_TRIG" | jq -r '.id')" >&2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment