#!/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 <&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=\"\"" 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