gitlab-ci-vars-setter/set-gitlab-vars

230 lines
8.1 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
# Universal script for setting GitLab CI/CD variables via API
# Print help and exit if --help or -h is passed, or no arguments
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" || $# -eq 0 ]]; then
cat <<EOF
set-gitlab-vars — universal script for setting GitLab CI/CD variables via API
Usage:
1. Bulk from YAML file:
export GITLAB_URL="https://gitlab.example.com"
export PROJECT=4 # or export PROJECT=owner_or_group/repo
export GITLAB_TOKEN=...
set-gitlab-vars path/to/file.yml
2. Single variable via arguments:
set-gitlab-vars VAR_NAME VAR_VALUE [ENV_SCOPE] [VAR_TYPE] [PROTECTED] [RAW] [MASKED] [MASKED_AND_HIDDEN]
# Example:
set-gitlab-vars MY_SECRET "supervalue" "Production" "file" true true false false
3. Help:
set-gitlab-vars --help
YAML file structure:
VAR_NAME:
value: "..." # required
environment_scope: "Dev" # optional
variable_type: "env_var" # or "file", optional (default: env_var)
protected: false # optional (default: false)
raw: true # optional (default: true)
masked: false # optional (default: false)
masked_and_hidden: false # optional (default: false)
description: "..." # optional (default: null)
Required environment variables:
GITLAB_URL — GitLab address
PROJECT — project ID or path (e.g. 4 or owner_or_group/repo)
GITLAB_TOKEN — token with api scope
Notes:
- All boolean options (protected, raw, masked, masked_and_hidden) default to false except raw (default: true).
- description is only supported when using YAML input.
- For file variables, multiline and special characters are supported.
- For env_var variables, multiline values are supported via form-data.
- All options are supported both in YAML and via CLI (except description, which is YAML only).
EOF
exit 0
fi
# Error/info helpers
err() { echo "[ERROR] $*" >&2; }
info() { echo "[INFO] $*"; }
# Check required environment variables
if [[ -z "${GITLAB_URL:-}" ]]; then
err "GITLAB_URL is not set. Export GitLab address, e.g.: export GITLAB_URL=\"https://gitlab.example.com\""
exit 1
fi
if [[ -z "${PROJECT:-}" ]]; then
err "PROJECT is not set. Export project ID or path, e.g.: export PROJECT=4 or export PROJECT=owner_or_group/repo"
exit 1
fi
if [[ -z "${GITLAB_TOKEN:-}" ]]; then
err "GITLAB_TOKEN is not set. Export access token, e.g.: export GITLAB_TOKEN=\"<your_token>\""
exit 1
fi
urlencode() {
local LANG=C
local length="${#1}"
for (( i = 0; i < length; i++ )); do
local c="${1:i:1}"
case $c in
[a-zA-Z0-9.~_-]) printf "$c" ;;
*) printf '%%%02X' "'${c}" ;;
esac
done
}
PROJECT_ENC="$(urlencode "$PROJECT")"
CURL_BASE=(curl -sS --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}")
# JSON escaping for file variables without python3
escape_json() {
local s="$1"
s="${s//\\/\\\\}" # \
s="${s//\"/\\\"}" # "
s="${s//$'\n'/\\n}" # newlines
s="${s//$'\t'/\\t}" # tabs
echo -n "$s"
}
set_var() {
VAR_NAME="$1"
VAR_VALUE="$2"
ENVIRONMENT_SCOPE="${3:-}"
VARIABLE_TYPE="${4:-env_var}"
PROTECTED="${5:-false}"
RAW="${6:-true}"
MASKED="${7:-false}"
MASKED_AND_HIDDEN="${8:-false}"
DESCRIPTION="${9:-null}"
info "Setting variable: $VAR_NAME, scope: $ENVIRONMENT_SCOPE, type: $VARIABLE_TYPE, protected: $PROTECTED, raw: $RAW, masked: $MASKED, masked_and_hidden: $MASKED_AND_HIDDEN, description: $DESCRIPTION"
# Prepare request body and headers
if [[ "$VARIABLE_TYPE" == "file" ]]; then
# For file variables, use JSON for correct escaping (no python3)
ESCAPED_VALUE=$(escape_json "$VAR_VALUE")
JSON_BODY="{\"key\":\"$VAR_NAME\",\"value\":\"$ESCAPED_VALUE\",\"variable_type\":\"$VARIABLE_TYPE\",\"protected\":$PROTECTED,\"raw\":$RAW,\"masked\":$MASKED,\"masked_and_hidden\":$MASKED_AND_HIDDEN"
if [[ "$DESCRIPTION" != "null" ]]; then
JSON_BODY+=" ,\"description\":\"$DESCRIPTION\""
fi
if [[ -n "$ENVIRONMENT_SCOPE" ]]; then
JSON_BODY+=" ,\"environment_scope\":\"$ENVIRONMENT_SCOPE\""
fi
JSON_BODY+="}"
REQUEST_ARGS=(--header "Content-Type: application/json" --data "$JSON_BODY")
else
# For env_var, use form-data to allow multiline
REQUEST_ARGS=(
--form "key=$VAR_NAME"
--form "value=$VAR_VALUE"
--form "variable_type=$VARIABLE_TYPE"
--form "protected=$PROTECTED"
--form "raw=$RAW"
--form "masked=$MASKED"
--form "masked_and_hidden=$MASKED_AND_HIDDEN"
)
if [[ "$DESCRIPTION" != "null" ]]; then
REQUEST_ARGS+=(--form "description=$DESCRIPTION")
fi
if [[ -n "$ENVIRONMENT_SCOPE" ]]; then
REQUEST_ARGS+=(--form "environment_scope=$ENVIRONMENT_SCOPE")
fi
fi
# Try to create variable
RESPONSE_BODY=$(mktemp)
RESPONSE_CODE=$("${CURL_BASE[@]}" -o "$RESPONSE_BODY" -w "%{http_code}" \
--request POST "$GITLAB_URL/api/v4/projects/$PROJECT_ENC/variables" \
"${REQUEST_ARGS[@]}")
if [[ "$RESPONSE_CODE" == "201" ]]; then
info "Variable $VAR_NAME created successfully."
rm -f "$RESPONSE_BODY"
return 0
elif [[ "$RESPONSE_CODE" == "409" || "$RESPONSE_CODE" == "400" && $(grep -q 'has already been taken' "$RESPONSE_BODY" && echo 1) == 1 ]]; then
info "Variable already exists, trying to update..."
# Update variable
UPDATE_URL="$GITLAB_URL/api/v4/projects/$PROJECT_ENC/variables/$VAR_NAME"
if [[ -n "$ENVIRONMENT_SCOPE" ]]; then
UPDATE_URL+="?filter%5Benvironment_scope%5D=$ENVIRONMENT_SCOPE"
fi
UPDATE_BODY=$(mktemp)
if [[ "$VARIABLE_TYPE" == "file" ]]; then
UPDATE_ARGS=(--header "Content-Type: application/json" --data "$JSON_BODY")
else
UPDATE_ARGS=(
--form "key=$VAR_NAME"
--form "value=$VAR_VALUE"
--form "variable_type=$VARIABLE_TYPE"
--form "protected=$PROTECTED"
--form "raw=true"
)
if [[ -n "$ENVIRONMENT_SCOPE" ]]; then
UPDATE_ARGS+=(--form "environment_scope=$ENVIRONMENT_SCOPE")
fi
fi
RESPONSE2=$("${CURL_BASE[@]}" -o "$UPDATE_BODY" -w "%{http_code}" \
--request PUT "$UPDATE_URL" "${UPDATE_ARGS[@]}")
if [[ "$RESPONSE2" == "200" ]]; then
info "Variable $VAR_NAME updated."
rm -f "$RESPONSE_BODY" "$UPDATE_BODY"
return 0
else
err "Failed to update variable $VAR_NAME (HTTP $RESPONSE2). GitLab API response: $(cat "$UPDATE_BODY")"
rm -f "$RESPONSE_BODY" "$UPDATE_BODY"
return 1
fi
elif [[ "$RESPONSE_CODE" == "400" ]]; then
err "Failed to create variable $VAR_NAME (HTTP 400)."
err "GitLab API response: $(cat "$RESPONSE_BODY")"
rm -f "$RESPONSE_BODY"
return 1
else
err "Failed to create variable $VAR_NAME (HTTP $RESPONSE_CODE)"
rm -f "$RESPONSE_BODY"
return 1
fi
}
# If a yaml file is passed as an argument, load variables from the file
if [[ -n "${1:-}" && -f "$1" ]]; then
if command -v yq >/dev/null 2>&1; then
VARS_FILE="$1"
VAR_NAMES=( $(yq 'keys | .[]' "$VARS_FILE") )
for VAR in "${VAR_NAMES[@]}"; do
[[ "$VAR" == "\$schema" ]] && continue
VALUE=$(yq -r ".${VAR}.value" "$VARS_FILE")
ENV_SCOPE=$(yq -r ".${VAR}.environment_scope // \"\"" "$VARS_FILE")
VAR_TYPE=$(yq -r ".${VAR}.variable_type // \"env_var\"" "$VARS_FILE")
PROTECTED=$(yq -r ".${VAR}.protected // false" "$VARS_FILE")
RAW=$(yq -r ".${VAR}.raw // true" "$VARS_FILE")
MASKED=$(yq -r ".${VAR}.masked // false" "$VARS_FILE")
MASKED_AND_HIDDEN=$(yq -r ".${VAR}.masked_and_hidden // false" "$VARS_FILE")
DESCRIPTION=$(yq -r ".${VAR}.description // null" "$VARS_FILE")
set_var "$VAR" "$VALUE" "$ENV_SCOPE" "$VAR_TYPE" "$PROTECTED" "$RAW" "$MASKED" "$MASKED_AND_HIDDEN" "$DESCRIPTION"
done
exit 0
else
err "yq is required to work with yaml file (https://mikefarah.gitbook.io/yq/)"
exit 2
fi
fi
# If 2-5 positional arguments are passed, set a single variable
if [[ $# -ge 2 && $# -le 5 ]]; then
VAR_NAME="$1"
VAR_VALUE="$2"
ENV_SCOPE="${3:-}"
VAR_TYPE="${4:-env_var}"
PROTECTED="${5:-false}"
set_var "$VAR_NAME" "$VAR_VALUE" "$ENV_SCOPE" "$VAR_TYPE" "$PROTECTED"
exit $?
fi
err "Invalid arguments. Run with --help for usage."
exit 1