{"id":164945,"date":"2026-03-28T12:11:13","date_gmt":"2026-03-28T09:11:13","guid":{"rendered":"https:\/\/computingforgeeks.com\/gitlab-cicd-variables-inputs\/"},"modified":"2026-03-28T12:55:29","modified_gmt":"2026-03-28T09:55:29","slug":"gitlab-cicd-variables-inputs","status":"publish","type":"post","link":"https:\/\/computingforgeeks.com\/gitlab-cicd-variables-inputs\/","title":{"rendered":"Configure GitLab CI\/CD Variables and Inputs with Real Examples"},"content":{"rendered":"\n<p>GitLab CI\/CD variables control how pipelines behave. They inject configuration, credentials, and runtime data into your jobs without hardcoding values in <code>.gitlab-ci.yml<\/code>. The problem is that GitLab has multiple variable types, scopes, and precedence rules, and the documentation scatters them across eight different pages. This guide consolidates everything into one practical reference with working examples from a real GitLab 18 server.<\/p>\n\n\n\n<p><em>Tested <strong>March 2026<\/strong> | GitLab CE 18.10.1 on Ubuntu 24.04 LTS, GitLab Runner 18.10.0, shell executor<\/em><\/p>\n\n\n\n<p>Every example in this article ran on a live GitLab instance. The pipeline outputs, screenshots, and job logs are real, not fabricated. If you want to follow along, you&#8217;ll need a GitLab instance with at least one registered runner. Our guides cover <a href=\"https:\/\/computingforgeeks.com\/install-gitlab-ce-ubuntu-debian\/\" target=\"_blank\" rel=\"noreferrer noopener\">installing GitLab on Ubuntu\/Debian<\/a> and <a href=\"https:\/\/computingforgeeks.com\/install-gitlab-on-rocky-almalinux\/\" target=\"_blank\" rel=\"noreferrer noopener\">Rocky Linux\/AlmaLinux<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How GitLab Variables Work<\/h2>\n\n\n\n<p>Every CI\/CD job runs in an environment where variables are injected as standard environment variables. There are three sources:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Predefined variables<\/strong>: GitLab injects 150+ variables automatically (commit SHA, branch name, pipeline ID, project path, runner info, and more)<\/li>\n\n\n\n<li><strong>Custom variables<\/strong>: you define them in <code>.gitlab-ci.yml<\/code>, the project\/group settings UI, or via the API<\/li>\n\n\n\n<li><strong>Dotenv variables<\/strong>: jobs create them at runtime and pass them to downstream jobs through artifacts<\/li>\n<\/ol>\n\n\n\n<p>GitLab 17+ also introduced <strong>inputs<\/strong>, which are typed parameters resolved at pipeline creation time. Unlike variables (which can change during execution), inputs are immutable once the pipeline starts. They use a different syntax (<code>$[[ inputs.name ]]<\/code> vs <code>$VARIABLE<\/code>) and serve a different purpose: making reusable pipeline templates.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Predefined Variables<\/h2>\n\n\n\n<p>GitLab automatically sets variables for every pipeline and job. You don&#8217;t need to define them. Here&#8217;s a pipeline job that prints the most useful ones:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>show-predefined-vars:\n  stage: info\n  script:\n    - echo \"Commit SHA   - $CI_COMMIT_SHA\"\n    - echo \"Short SHA    - $CI_COMMIT_SHORT_SHA\"\n    - echo \"Branch       - $CI_COMMIT_REF_NAME\"\n    - echo \"Pipeline ID  - $CI_PIPELINE_ID\"\n    - echo \"Source        - $CI_PIPELINE_SOURCE\"\n    - echo \"Project      - $CI_PROJECT_NAME\"\n    - echo \"Job ID       - $CI_JOB_ID\"\n    - echo \"Runner       - $CI_RUNNER_DESCRIPTION\"\n    - echo \"Triggered by - $GITLAB_USER_LOGIN\"<\/code><\/pre>\n\n\n\n<p>The actual output from our GitLab server:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Commit SHA   - f46a0e647fbd830b2c10c4b10ec4d9988a63d3f6\nShort SHA    - f46a0e64\nBranch       - main\nPipeline ID  - 2\nSource        - push\nProject      - ci-variables-demo\nJob ID       - 17\nRunner       - Shell runner on GitLab server\nTriggered by - root<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-predefined-vars-output.png\" alt=\"GitLab job output showing predefined CI\/CD variables\" class=\"wp-image-164941\" width=\"1366\" height=\"768\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-predefined-vars-output.png 1366w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-predefined-vars-output-300x169.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-predefined-vars-output-1024x576.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-predefined-vars-output-768x432.png 768w\" sizes=\"auto, (max-width: 1366px) 100vw, 1366px\" \/><\/figure>\n\n\n\n<p>The most commonly used predefined variables:<\/p>\n\n\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Variable<\/th>\n<th>Contains<\/th>\n<th>Available in rules:if?<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>CI_COMMIT_SHA<\/code><\/td>\n<td>Full 40-character commit hash<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td><code>CI_COMMIT_SHORT_SHA<\/code><\/td>\n<td>First 8 characters of the commit hash<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td><code>CI_COMMIT_REF_NAME<\/code><\/td>\n<td>Branch or tag name<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td><code>CI_COMMIT_BRANCH<\/code><\/td>\n<td>Branch name (empty in MR pipelines)<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td><code>CI_PIPELINE_SOURCE<\/code><\/td>\n<td>How the pipeline was triggered (push, web, schedule, api, trigger, merge_request_event)<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td><code>CI_PIPELINE_ID<\/code><\/td>\n<td>Unique pipeline ID across the instance<\/td>\n<td>No (persisted)<\/td>\n<\/tr>\n<tr>\n<td><code>CI_JOB_ID<\/code><\/td>\n<td>Unique job ID<\/td>\n<td>No (persisted)<\/td>\n<\/tr>\n<tr>\n<td><code>CI_JOB_TOKEN<\/code><\/td>\n<td>Token for authenticating API calls within the job<\/td>\n<td>No (persisted)<\/td>\n<\/tr>\n<tr>\n<td><code>CI_PROJECT_ID<\/code><\/td>\n<td>Numeric project ID<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td><code>CI_DEFAULT_BRANCH<\/code><\/td>\n<td>Default branch name (usually main)<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td><code>CI_REGISTRY_IMAGE<\/code><\/td>\n<td>Container registry address for the project<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td><code>GITLAB_USER_LOGIN<\/code><\/td>\n<td>Username of who triggered the pipeline<\/td>\n<td>Yes<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n\n\n<p>Variables marked &#8220;No&#8221; under <code>rules:if<\/code> are <strong>persisted variables<\/strong>. They contain tokens or sensitive data and are only available inside running jobs, not during pipeline creation. Trying to use <code>CI_JOB_ID<\/code> in a <code>rules:if<\/code> expression will always evaluate to empty.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Custom Variables in .gitlab-ci.yml<\/h2>\n\n\n\n<p>Define variables at two levels: globally (available to all jobs) or per-job (scoped to that job only).<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>variables:\n  APP_NAME: \"my-web-app\"\n  APP_VERSION: \"2.1.0\"\n  DEPLOY_ENV:\n    value: \"staging\"\n    description: \"Target environment (staging or production)\"\n\nshow-custom-vars:\n  variables:\n    JOB_SPECIFIC_VAR: \"only-in-this-job\"\n    APP_VERSION: \"3.0.0-override\"\n  script:\n    - echo \"APP_NAME    - $APP_NAME\"\n    - echo \"APP_VERSION - $APP_VERSION\"\n    - echo \"JOB_VAR     - $JOB_SPECIFIC_VAR\"\n    - echo \"DEPLOY_ENV  - $DEPLOY_ENV\"<\/code><\/pre>\n\n\n\n<p>Output from our test pipeline:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>=== Global Variables ===\nAPP_NAME    - my-web-app\nDEPLOY_ENV  - staging\n\n=== Job-Level Override ===\nAPP_VERSION - 3.0.0-override  (was 2.1.0 globally, overridden to 3.0.0)\nJOB_VAR     - only-in-this-job<\/code><\/pre>\n\n\n\n<p>The <code>value:<\/code> and <code>description:<\/code> syntax for <code>DEPLOY_ENV<\/code> creates a prefilled variable when someone triggers the pipeline manually from the GitLab UI. The description appears as a label next to the input field.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Project Variables in the GitLab UI<\/h2>\n\n\n\n<p>Go to <strong>Settings &gt; CI\/CD &gt; Variables<\/strong> in your project to add variables through the web interface. This is where you store credentials, API keys, and environment-specific configuration that should never appear in your repository.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-cicd-variables-settings.png\" alt=\"GitLab CI\/CD Variables Settings page\" class=\"wp-image-164940\" width=\"1366\" height=\"768\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-cicd-variables-settings.png 1366w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-cicd-variables-settings-300x169.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-cicd-variables-settings-1024x576.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-cicd-variables-settings-768x432.png 768w\" sizes=\"auto, (max-width: 1366px) 100vw, 1366px\" \/><\/figure>\n\n\n\n<p>Each UI variable has these properties:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Type<\/strong>: <code>Variable<\/code> (standard env var) or <code>File<\/code> (value written to a temp file, variable holds the file path)<\/li>\n\n\n\n<li><strong>Protected<\/strong>: only available on protected branches and tags<\/li>\n\n\n\n<li><strong>Masked<\/strong>: value replaced with <code>[MASKED]<\/code> in job logs. Requires 8+ characters, no spaces<\/li>\n\n\n\n<li><strong>Masked and hidden<\/strong> (GitLab 17.6+): also hidden from the Settings UI itself<\/li>\n\n\n\n<li><strong>Environment scope<\/strong> (Premium): restrict to specific environments like <code>staging<\/code> or <code>production\/*<\/code><\/li>\n<\/ul>\n\n\n\n<p>In our demo, we created a masked <code>API_SECRET_KEY<\/code> variable. The job log shows exactly how masking works:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>=== UI-Defined Variables ===\nDEPLOY_SERVER  - 192.168.1.100\nAPI_SECRET_KEY - &#91;MASKED]  (should show &#91;MASKED])<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-custom-vars-output.png\" alt=\"GitLab job showing custom and masked variables\" class=\"wp-image-164942\" width=\"1366\" height=\"768\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-custom-vars-output.png 1366w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-custom-vars-output-300x169.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-custom-vars-output-1024x576.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-custom-vars-output-768x432.png 768w\" sizes=\"auto, (max-width: 1366px) 100vw, 1366px\" \/><\/figure>\n\n\n\n<p>The actual value (<code>sk-prod-a8f3e2d1c4b5a697<\/code>) is injected into the job&#8217;s environment but never appears in the log output.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">File-Type Variables<\/h2>\n\n\n\n<p>File-type variables are commonly misunderstood. When you create a file-type variable, GitLab writes the value to a temporary file and sets the environment variable to the <strong>file path<\/strong>, not the contents. This is essential for SSH keys, TLS certificates, and kubeconfig files.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>show-file-var:\n  script:\n    - echo \"Path: $SSH_PRIVATE_KEY\"\n    - head -3 \"$SSH_PRIVATE_KEY\"<\/code><\/pre>\n\n\n\n<p>Output from our test:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>=== File-Type Variable ===\nSSH_PRIVATE_KEY variable contains a file path:\nPath - \/home\/gitlab-runner\/builds\/e_ZfGc5EI\/0\/root\/ci-variables-demo.tmp\/SSH_PRIVATE_KEY\n\nFile contents (first 3 lines):\n-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0\ndemo-key-content-for-article-purposes-only<\/code><\/pre>\n\n\n\n<p>The runner creates the temp file before the job starts and deletes it after. To use it with SSH:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>deploy:\n  script:\n    - chmod 600 \"$SSH_PRIVATE_KEY\"\n    - ssh -i \"$SSH_PRIVATE_KEY\" user@server \"echo connected\"<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Passing Variables Between Jobs with dotenv<\/h2>\n\n\n\n<p>Jobs run in isolation. If a build job generates a version number or artifact path, the next job doesn&#8217;t automatically know about it. Dotenv artifacts solve this by letting one job write key-value pairs to a file that later jobs can read as environment variables.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>build-app:\n  stage: build\n  script:\n    - BUILD_ID=\"build-$(date +%s)\"\n    - BUILD_ARTIFACT=\"$APP_NAME-$APP_VERSION.tar.gz\"\n    - COMMIT_SHORT=$(echo $CI_COMMIT_SHA | cut -c1-8)\n    - echo \"BUILD_ID=$BUILD_ID\" &gt;&gt; build.env\n    - echo \"BUILD_ARTIFACT=$BUILD_ARTIFACT\" &gt;&gt; build.env\n    - echo \"BUILD_COMMIT=$COMMIT_SHORT\" &gt;&gt; build.env\n  artifacts:\n    reports:\n      dotenv: build.env\n\ntest-with-build-vars:\n  stage: test\n  needs: &#91;build-app]\n  script:\n    - echo \"BUILD_ID       - $BUILD_ID\"\n    - echo \"BUILD_ARTIFACT - $BUILD_ARTIFACT\"\n    - echo \"BUILD_COMMIT   - $BUILD_COMMIT\"<\/code><\/pre>\n\n\n\n<p>The build job writes three variables to <code>build.env<\/code> and declares it as a dotenv artifact. The test job receives them automatically:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>=== Variables received from build-app via dotenv ===\nBUILD_ID       - build-1774688488\nBUILD_ARTIFACT - my-web-app-2.1.0.tar.gz\nBUILD_COMMIT   - f46a0e64\n\nThese were NOT defined in this job.\nThey came from the build-app job's dotenv artifact.\n\n=== Global vars still available too ===\nAPP_NAME    - my-web-app\nAPP_VERSION - 2.1.0  (global value, not overridden)<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-dotenv-output.png\" alt=\"GitLab job showing dotenv variables passed between jobs\" class=\"wp-image-164943\" width=\"1366\" height=\"768\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-dotenv-output.png 1366w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-dotenv-output-300x169.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-dotenv-output-1024x576.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-dotenv-output-768x432.png 768w\" sizes=\"auto, (max-width: 1366px) 100vw, 1366px\" \/><\/figure>\n\n\n\n<p>Dotenv files must follow strict formatting rules: UTF-8 encoding, no empty lines, no comments, no multiline values, max 5 KB. Variable names can only contain ASCII letters, digits, and underscores.<\/p>\n\n\n\n<p>Control which dotenv artifacts a job inherits with <code>dependencies:<\/code> or <code>needs:<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Inherit dotenv from specific jobs only\ntest-job:\n  needs: &#91;build-app]\n  script: echo \"$BUILD_ID\"\n\n# Block all dotenv inheritance\nindependent-job:\n  needs:\n    - job: build-app\n      artifacts: false\n  script: echo \"No dotenv vars here\"<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Variable Precedence<\/h2>\n\n\n\n<p>When the same variable name is defined at multiple levels, GitLab uses a strict precedence order. This catches people off guard because UI variables beat YAML definitions. We tested this by defining <code>DEPLOY_SERVER<\/code> in both the project UI (value: <code>192.168.1.100<\/code>) and the job-level YAML (value: <code>job-level-server.local<\/code>).<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>=== Variable Precedence Demo ===\nDEPLOY_SERVER is defined in THREE places:\n  1. Project UI variable  = 192.168.1.100\n  2. Job-level .gitlab-ci.yml = job-level-server.local\n\nActual value - 192.168.1.100\n\nThe UI project variable WINS over the job-level YAML variable.<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-precedence-output.png\" alt=\"GitLab job demonstrating variable precedence\" class=\"wp-image-164944\" width=\"1366\" height=\"768\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-precedence-output.png 1366w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-precedence-output-300x169.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-precedence-output-1024x576.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-precedence-output-768x432.png 768w\" sizes=\"auto, (max-width: 1366px) 100vw, 1366px\" \/><\/figure>\n\n\n\n<p>The full precedence order (highest wins):<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Pipeline variables (manual trigger, API, schedule, downstream)<\/li>\n\n\n\n<li>Project variables (Settings &gt; CI\/CD &gt; Variables)<\/li>\n\n\n\n<li>Group variables (inherited from parent groups)<\/li>\n\n\n\n<li>Instance variables (self-managed only, set by admins)<\/li>\n\n\n\n<li>Dotenv report variables<\/li>\n\n\n\n<li>Job-level <code>.gitlab-ci.yml<\/code> variables<\/li>\n\n\n\n<li>Global <code>.gitlab-ci.yml<\/code> variables<\/li>\n\n\n\n<li>Predefined variables<\/li>\n<\/ol>\n\n\n\n<p>This means a project-level UI variable will always override the same key defined in your YAML. If your pipeline ignores a YAML variable and you can&#8217;t figure out why, check whether a UI variable with the same name exists.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Using Variables in rules:<\/h2>\n\n\n\n<p>The <code>rules:if<\/code> keyword evaluates variables to decide whether a job runs. This is how you create conditional pipelines:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>deploy-staging:\n  rules:\n    - if: $DEPLOY_ENV == \"staging\"\n      when: always\n    - when: never\n  script:\n    - echo \"Deploying to staging...\"\n\ndeploy-production:\n  rules:\n    - if: $DEPLOY_ENV == \"production\"\n      when: manual\n    - when: never\n  script:\n    - echo \"Deploying to production...\"\n\nonly-on-tags:\n  rules:\n    - if: $CI_COMMIT_TAG\n  script:\n    - echo \"This runs only when a tag is pushed\"\n\nonly-merge-requests:\n  rules:\n    - if: $CI_PIPELINE_SOURCE == \"merge_request_event\"\n  script:\n    - echo \"This runs only in MR pipelines\"<\/code><\/pre>\n\n\n\n<p>Two important gotchas with <code>rules:if<\/code>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Persisted variables don&#8217;t work<\/strong>: <code>CI_JOB_ID<\/code>, <code>CI_JOB_TOKEN<\/code>, <code>CI_PIPELINE_ID<\/code>, and similar variables are not available during pipeline creation. Using them in <code>rules:if<\/code> always evaluates to empty<\/li>\n\n\n\n<li><strong>No variable expansion in <code>rules:changes<\/code> or <code>rules:exists<\/code><\/strong>: these keywords don&#8217;t support <code>$variable<\/code> syntax at all<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Reusable Templates with spec:inputs<\/h2>\n\n\n\n<p>Inputs (introduced in GitLab 17) are typed parameters for pipeline templates. Unlike variables, inputs are resolved at pipeline creation time and cannot change during execution. They use the <code>$[[ inputs.name ]]<\/code> interpolation syntax.<\/p>\n\n\n\n<p>Create a reusable template file (<code>templates\/deploy-service.yml<\/code>):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>spec:\n  inputs:\n    service_name:\n      type: string\n      description: \"Name of the service to deploy\"\n    target_port:\n      type: number\n      default: 8080\n    run_tests:\n      type: boolean\n      default: true\n    environment:\n      type: string\n      options: &#91;\"dev\", \"staging\", \"production\"]\n      default: \"dev\"\n---\n\ntest-$&#91;&#91; inputs.service_name ]]:\n  stage: test\n  rules:\n    - if: $&#91;&#91; inputs.run_tests ]] == true\n  script:\n    - echo \"Testing $&#91;&#91; inputs.service_name ]] on port $&#91;&#91; inputs.target_port ]]\"\n    - echo \"Environment: $&#91;&#91; inputs.environment ]]\"\n\ndeploy-$&#91;&#91; inputs.service_name ]]:\n  stage: deploy\n  script:\n    - echo \"Deploying $&#91;&#91; inputs.service_name ]] to $&#91;&#91; inputs.environment ]]\"\n    - echo \"Branch: $CI_COMMIT_REF_NAME\"<\/code><\/pre>\n\n\n\n<p>The <code>---<\/code> YAML document separator is required between the <code>spec:<\/code> block and the job definitions. Input types can be <code>string<\/code>, <code>number<\/code>, <code>boolean<\/code>, or <code>array<\/code>. Use <code>options:<\/code> to restrict values and <code>regex:<\/code> for pattern validation.<\/p>\n\n\n\n<p>Include the template multiple times with different parameters in your main <code>.gitlab-ci.yml<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>include:\n  - local: templates\/deploy-service.yml\n    inputs:\n      service_name: \"api-gateway\"\n      target_port: 3000\n      environment: \"staging\"\n\n  - local: templates\/deploy-service.yml\n    inputs:\n      service_name: \"frontend\"\n      target_port: 8080\n      run_tests: false\n      environment: \"production\"\n\nstages:\n  - test\n  - deploy<\/code><\/pre>\n\n\n\n<p>This generates four jobs from one template: <code>test-api-gateway<\/code>, <code>deploy-api-gateway<\/code>, <code>test-frontend<\/code> (skipped because <code>run_tests: false<\/code>), and <code>deploy-frontend<\/code>. Each job has its inputs baked in at pipeline creation time.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Variables vs Inputs: when to use which<\/h3>\n\n\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Aspect<\/th>\n<th>Variables (<code>$VAR<\/code>)<\/th>\n<th>Inputs (<code>$[[ inputs.name ]]<\/code>)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Resolved when<\/td>\n<td>During job execution (dynamic)<\/td>\n<td>At pipeline creation (static)<\/td>\n<\/tr>\n<tr>\n<td>Can change at runtime<\/td>\n<td>Yes<\/td>\n<td>No<\/td>\n<\/tr>\n<tr>\n<td>Type validation<\/td>\n<td>None (all strings)<\/td>\n<td>String, number, boolean, array<\/td>\n<\/tr>\n<tr>\n<td>Scope<\/td>\n<td>Global, job, project, group<\/td>\n<td>Only the file where defined<\/td>\n<\/tr>\n<tr>\n<td>Best for<\/td>\n<td>Credentials, runtime config, environment-specific values<\/td>\n<td>Reusable templates, pipeline parameters<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n\n\n<h2 class=\"wp-block-heading\">The Full Demo Pipeline<\/h2>\n\n\n\n<p>Here&#8217;s the complete pipeline we ran on our GitLab 18 server. It demonstrates all variable types in a single <code>.gitlab-ci.yml<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>stages:\n  - info\n  - build\n  - test\n  - deploy\n\nvariables:\n  APP_NAME: \"my-web-app\"\n  APP_VERSION: \"2.1.0\"\n  DEPLOY_ENV:\n    value: \"staging\"\n    description: \"Target environment (staging or production)\"\n\nshow-predefined-vars:\n  stage: info\n  script:\n    - echo \"Commit SHA   - $CI_COMMIT_SHA\"\n    - echo \"Branch       - $CI_COMMIT_REF_NAME\"\n    - echo \"Pipeline ID  - $CI_PIPELINE_ID\"\n    - echo \"Source        - $CI_PIPELINE_SOURCE\"\n    - echo \"Project      - $CI_PROJECT_NAME\"\n    - echo \"Triggered by - $GITLAB_USER_LOGIN\"\n\nshow-custom-vars:\n  stage: info\n  variables:\n    JOB_SPECIFIC_VAR: \"only-in-this-job\"\n    APP_VERSION: \"3.0.0-override\"\n  script:\n    - echo \"APP_NAME    - $APP_NAME\"\n    - echo \"APP_VERSION - $APP_VERSION\"\n    - echo \"JOB_VAR     - $JOB_SPECIFIC_VAR\"\n    - echo \"DEPLOY_SERVER - $DEPLOY_SERVER\"\n    - echo \"API_SECRET_KEY - $API_SECRET_KEY\"\n\nshow-file-var:\n  stage: info\n  script:\n    - echo \"Path - $SSH_PRIVATE_KEY\"\n    - head -3 \"$SSH_PRIVATE_KEY\"\n\nbuild-app:\n  stage: build\n  script:\n    - BUILD_ID=\"build-$(date +%s)\"\n    - BUILD_ARTIFACT=\"$APP_NAME-$APP_VERSION.tar.gz\"\n    - COMMIT_SHORT=$(echo $CI_COMMIT_SHA | cut -c1-8)\n    - echo \"BUILD_ID=$BUILD_ID\" &gt;&gt; build.env\n    - echo \"BUILD_ARTIFACT=$BUILD_ARTIFACT\" &gt;&gt; build.env\n    - echo \"BUILD_COMMIT=$COMMIT_SHORT\" &gt;&gt; build.env\n  artifacts:\n    reports:\n      dotenv: build.env\n\ntest-with-build-vars:\n  stage: test\n  needs: &#91;build-app]\n  script:\n    - echo \"BUILD_ID       - $BUILD_ID\"\n    - echo \"BUILD_ARTIFACT - $BUILD_ARTIFACT\"\n    - echo \"BUILD_COMMIT   - $BUILD_COMMIT\"\n\nprecedence-demo:\n  stage: test\n  variables:\n    DEPLOY_SERVER: \"job-level-server.local\"\n  script:\n    - echo \"Actual value - $DEPLOY_SERVER\"\n    - echo \"UI project variable WINS over job-level YAML\"\n\ndeploy-staging:\n  stage: deploy\n  rules:\n    - if: $DEPLOY_ENV == \"staging\"\n      when: always\n    - when: never\n  script:\n    - echo \"Deploying $APP_NAME v$APP_VERSION to staging\"\n    - echo \"Server - $DEPLOY_SERVER\"\n\ndeploy-production:\n  stage: deploy\n  rules:\n    - if: $DEPLOY_ENV == \"production\"\n      when: manual\n    - when: never\n  script:\n    - echo \"Deploying to production\"\n\ndynamic-artifacts:\n  stage: build\n  script:\n    - mkdir -p \"output\/$CI_COMMIT_REF_NAME\"\n    - echo \"Built from $CI_COMMIT_SHORT_SHA\" &gt; \"output\/$CI_COMMIT_REF_NAME\/build-info.txt\"\n  artifacts:\n    paths:\n      - output\/$CI_COMMIT_REF_NAME\/\n    expire_in: 1 hour<\/code><\/pre>\n\n\n\n<p>This single file produced the pipeline below with 8 jobs across 4 stages, all passing:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-pipeline-graph.png\" alt=\"GitLab pipeline graph showing all stages and jobs\" class=\"wp-image-164939\" width=\"1366\" height=\"768\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-pipeline-graph.png 1366w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-pipeline-graph-300x169.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-pipeline-graph-1024x576.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/03\/gitlab-pipeline-graph-768x432.png 768w\" sizes=\"auto, (max-width: 1366px) 100vw, 1366px\" \/><\/figure>\n\n\n\n<p>The pipeline ran 8 jobs across 4 stages (info, build, test, deploy), all passing. The <code>deploy-production<\/code> job was correctly skipped because <code>DEPLOY_ENV<\/code> was set to &#8220;staging&#8221; (the default). To trigger it, you&#8217;d run the pipeline manually and change <code>DEPLOY_ENV<\/code> to &#8220;production&#8221;.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Where Variables Can and Cannot Be Used<\/h2>\n\n\n\n<p>Variables expand through three mechanisms: GitLab internal expansion (before the runner gets the job), runner expansion (when the runner processes job config), and shell expansion (during script execution). Not all keywords support all expansion types.<\/p>\n\n\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Keyword<\/th>\n<th>Variables work?<\/th>\n<th>Expanded by<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>script<\/code>, <code>before_script<\/code>, <code>after_script<\/code><\/td>\n<td>Yes<\/td>\n<td>Shell<\/td>\n<\/tr>\n<tr>\n<td><code>image<\/code>, <code>services:name<\/code><\/td>\n<td>Yes<\/td>\n<td>Runner<\/td>\n<\/tr>\n<tr>\n<td><code>variables<\/code><\/td>\n<td>Yes<\/td>\n<td>GitLab, then Runner<\/td>\n<\/tr>\n<tr>\n<td><code>rules:if<\/code><\/td>\n<td>Partial<\/td>\n<td>GitLab (no persisted vars)<\/td>\n<\/tr>\n<tr>\n<td><code>rules:changes<\/code>, <code>rules:exists<\/code><\/td>\n<td>No<\/td>\n<td>N\/A<\/td>\n<\/tr>\n<tr>\n<td><code>include<\/code><\/td>\n<td>Yes (limited)<\/td>\n<td>GitLab<\/td>\n<\/tr>\n<tr>\n<td><code>environment:name<\/code>, <code>environment:url<\/code><\/td>\n<td>Yes<\/td>\n<td>GitLab<\/td>\n<\/tr>\n<tr>\n<td><code>artifacts:name\/paths<\/code><\/td>\n<td>Yes<\/td>\n<td>Runner<\/td>\n<\/tr>\n<tr>\n<td><code>cache:key<\/code><\/td>\n<td>Yes<\/td>\n<td>Runner<\/td>\n<\/tr>\n<tr>\n<td><code>tags<\/code><\/td>\n<td>Yes<\/td>\n<td>GitLab<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n\n\n<p>One subtle trap: <code>after_script<\/code> runs in a completely separate shell context. Variables exported in <code>script<\/code> or <code>before_script<\/code> are not available in <code>after_script<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Troubleshooting<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Variable is empty when it shouldn&#8217;t be<\/h3>\n\n\n\n<p>Check the precedence order. A project-level UI variable with the same name overrides your YAML definition. Also check if the variable is protected (only available on protected branches) and you&#8217;re running on an unprotected branch.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Masked value not masked in logs<\/h3>\n\n\n\n<p>Masking only catches exact matches. If your script URL-encodes, base64-encodes, or otherwise transforms the value, the modified form won&#8217;t be masked. Also, masking requires the value to be at least 8 characters with no spaces.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">YAML parses numbers incorrectly<\/h3>\n\n\n\n<p>YAML interprets unquoted numbers. <code>VAR: 012345<\/code> becomes octal <code>5349<\/code>. Always quote variable values: <code>VAR: \"012345\"<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Debug mode for variable inspection<\/h3>\n\n\n\n<p>Set <code>CI_DEBUG_TRACE: \"true\"<\/code> as a pipeline variable to see all variable values in job logs. This is a security risk because it exposes every variable including secrets. Only use it temporarily and restrict log access.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>variables:\n  CI_DEBUG_TRACE: \"true\"<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">&#8220;argument list too long&#8221; error<\/h3>\n\n\n\n<p>This happens when the total size of all variables exceeds the shell&#8217;s <code>ARG_MAX<\/code> limit. Move large values (certificates, JSON blobs) to file-type variables instead of standard env vars.<\/p>\n\n\n\n<p>For the full list of <a href=\"https:\/\/docs.gitlab.com\/ci\/variables\/predefined_variables\/\" target=\"_blank\" rel=\"noreferrer noopener\">predefined variables<\/a>, the official <a href=\"https:\/\/docs.gitlab.com\/ci\/variables\/\" target=\"_blank\" rel=\"noreferrer noopener\">CI\/CD variables reference<\/a>, and the <a href=\"https:\/\/docs.gitlab.com\/ci\/inputs\/\" target=\"_blank\" rel=\"noreferrer noopener\">inputs documentation<\/a>, see the GitLab docs. To get started with GitLab itself, follow our <a href=\"https:\/\/computingforgeeks.com\/install-gitlab-ce-on-ubuntu-debian\/\" target=\"_blank\" rel=\"noreferrer noopener\">installation guide for Ubuntu\/Debian<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>GitLab CI\/CD variables control how pipelines behave. They inject configuration, credentials, and runtime data into your jobs without hardcoding values in .gitlab-ci.yml. The problem is that GitLab has multiple variable types, scopes, and precedence rules, and the documentation scatters them across eight different pages. This guide consolidates everything into one practical reference with working examples &#8230; <a title=\"Configure GitLab CI\/CD Variables and Inputs with Real Examples\" class=\"read-more\" href=\"https:\/\/computingforgeeks.com\/gitlab-cicd-variables-inputs\/\" aria-label=\"Read more about Configure GitLab CI\/CD Variables and Inputs with Real Examples\">Read more<\/a><\/p>\n","protected":false},"author":3,"featured_media":164946,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[329,35913,590,39796,299,50],"tags":[],"class_list":["post-164945","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-automation","category-devops","category-git","category-gitlab","category-how-to","category-linux-tutorials"],"_links":{"self":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/164945","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/comments?post=164945"}],"version-history":[{"count":2,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/164945\/revisions"}],"predecessor-version":[{"id":164958,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/164945\/revisions\/164958"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media\/164946"}],"wp:attachment":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media?parent=164945"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/categories?post=164945"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/tags?post=164945"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}