Skip to content
Back to skills

1641 Setup Ci Publish Token E86a6578

BSecurity

Create GitLab project access token for CI/CD publishing and add as masked CI variable

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
devopspythonbashgitapici/cddocumentation

Works with

  • cli
  • api

Security analysis

B88/100
  • criticalSends environment variables or credentials to an external URL

Pro shows the line behind each finding and how to fix it

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 1641-setup-ci-publish-token_e86a6578 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 1641 Setup Ci Publish Token E86a6578?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 1641 Setup Ci Publish Token E86a6578
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-1641-setup-ci-publish-token-e86a6578/badge)](https://www.skillsdirectory.com/skills/tools-only-1641-setup-ci-publish-token-e86a6578)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
---
description: Create GitLab project access token for CI/CD publishing and add as masked CI variable
---

# Setup CI Publishing Token

Creates a GitLab project access token with permissions for publishing releases and uploading artifacts, then adds it as a protected, masked CI/CD variable.

## Problem

GitLab CI `CI_JOB_TOKEN` has limited permissions and cannot upload release assets, causing `401 Unauthorized` errors when using `glab release create` with file attachments.

## Solution

Run the setup script which automatically:

1. Verifies your GITLAB_TOKEN has required permissions (api scope + Maintainer access)
2. Checks if `ci-publish-token` project access token exists
3. Checks if `CI_PUBLISH_TOKEN` CI/CD variable exists
4. Takes appropriate action based on current state

## Prerequisites

- `GITLAB_TOKEN` environment variable set with `api` scope and Maintainer+ access to the project
- `jq` installed
- `glab` installed and authenticated
- Running from the git repository root

## Usage

Create the script at `.claude/commands/setup-ci-publish-token.sh`:

```sh
#!/usr/bin/env sh
# setup-ci-publish-token.sh
# Creates or renews CI_PUBLISH_TOKEN for GitLab CI/CD publishing

set -eu

# Constants
TOKEN_NAME="ci-publish-token"
VAR_NAME="CI_PUBLISH_TOKEN"

# Load existing .env if present and non-empty
[ -s .env ] && . ./.env

# Set the token for the environment
GITLAB_TOKEN="${GITLAB_TOKEN:-${GL_TOKEN:-${CI_JOB_TOKEN:-}}}"
export GITLAB_TOKEN

if [ -z "${GITLAB_TOKEN:-}" ]; then
    echo "ERROR: You need a GITLAB_TOKEN set in your environment to do this."
    exit 1
fi

if [ ! -d .git ]; then
    echo "ERROR: You must be in the git root directory to do this."
    exit 1
fi

if ! command -v jq >/dev/null 2>&1; then
    echo "ERROR: You need jq installed. Try 'brew install jq'"
    exit 1
fi

if ! command -v glab >/dev/null 2>&1; then
    echo "ERROR: You need glab installed. Try 'brew install glab'"
    exit 1
fi

in_dotenv() { [ -e .env ] && grep -q "^$1=" .env; }
in_env() { env | grep -q "^$1="; }

# --- Environment variable detection ---

if ! in_env GITLAB_HOST && ! in_dotenv GITLAB_HOST; then
    if [ -z "${CI_SERVER_HOST:-}" ]; then
        GITLAB_HOST="$(sed -n 's/.*url = git@\([^:]*\):.*/\1/p; s/.*url = https:\/\/\([^/]*\)\/.*/\1/p' .git/config | head -1)"
    else
        GITLAB_HOST="${CI_SERVER_HOST}"
    fi
fi

if ! in_env CI_PROJECT_PATH && ! in_dotenv CI_PROJECT_PATH; then
    CI_PROJECT_PATH="$(sed -n 's/.*url = git@[^:]*:\(.*\)\.git$/\1/p; s/.*url = https:\/\/[^/]*\/\(.*\)\.git$/\1/p' .git/config | head -1)"
fi

if ! in_env GITLAB_USER_ID && ! in_dotenv GITLAB_USER_ID; then
    GITLAB_USER_ID="$(glab api user | jq '.id')"
fi

# --- Persist to .env if not in CI ---

if [ -z "${GITLAB_CI:-}" ]; then
    [ ! -e .env ] && touch .env
    [ ! -e .gitignore ] && touch .gitignore
    grep -qE '^\s*/?\.env\s*$' .gitignore || printf "# Ignore localized environment variables\n.env\n" >>.gitignore
    [ -s .env ] && . ./.env
    in_dotenv GITLAB_HOST || echo "GITLAB_HOST=${GITLAB_HOST}" >>.env
    in_dotenv CI_PROJECT_PATH || echo "CI_PROJECT_PATH=${CI_PROJECT_PATH}" >>.env
    in_dotenv GITLAB_USER_ID || echo "GITLAB_USER_ID=${GITLAB_USER_ID}" >>.env
fi

# URL-encode the project path for API calls
CI_PROJECT_PATH_ENCODED=$(printf '%s' "${CI_PROJECT_PATH}" | sed 's/\//%2F/g')

export GITLAB_HOST CI_PROJECT_PATH GITLAB_USER_ID CI_PROJECT_PATH_ENCODED

# --- Permission checks ---

has_api_scope() {
    if glab api personal_access_tokens/self | jq -e '.scopes | index("api")' >/dev/null 2>&1; then
        return 0
    else
        echo "ERROR: The current GITLAB_TOKEN does not have the 'api' scope."
        exit 1
    fi
}

has_maintainer_access() {
    access_level=$(glab api "projects/${CI_PROJECT_PATH_ENCODED}/members/all/${GITLAB_USER_ID}" | jq -r '.access_level')
    if [ "${access_level:-0}" -ge 40 ]; then
        return 0
    else
        echo "ERROR: The current GITLAB_TOKEN does not have Maintainer (40) or higher access to ${CI_PROJECT_PATH}."
        exit 1
    fi
}

has_api_scope
has_maintainer_access

# --- Check token and variable status ---

token_json=$(glab token list --repo "${CI_PROJECT_PATH}" --output json)
token_info=$(echo "${token_json}" | jq -r "if . == null then empty else .[] | select(.name == \"${TOKEN_NAME}\") end")

# Filter out the "Listing variables..." line, default to empty array if no match
var_json=$(glab variable list --repo "${CI_PROJECT_PATH}" --output json 2>/dev/null | grep '^\[' || echo '[]')
if echo "${var_json}" | jq -e ".[] | select(.key == \"${VAR_NAME}\")" >/dev/null 2>&1; then
    var_exists="true"
else
    var_exists="false"
fi

# --- Decision logic ---

# Case 4: No token exists - CREATE NEW
if [ -z "${token_info}" ]; then
    echo "INFO: Creating project access token '${TOKEN_NAME}'..."
    NEW_TOKEN=$(glab token create "${TOKEN_NAME}" \
        --repo "${CI_PROJECT_PATH}" \
        --access-level maintainer \
        --scope api \
        --scope write_repository \
        --duration 8760h \
        --description "CI/CD token for publishing releases and uploading artifacts" \
        --output text)

    echo "INFO: Setting CI variable '${VAR_NAME}'..."
    echo "${NEW_TOKEN}" | glab variable set "${VAR_NAME}" \
        --repo "${CI_PROJECT_PATH}" \
        --masked \
        --protected \
        --description "Project access token for CI/CD release publishing and artifact uploads"

    echo "DONE: Token and variable created."
    exit 0
fi

# Token exists - check expiry
expires_at=$(echo "${token_info}" | jq -r '.expires_at')
today=$(date +%Y-%m-%d)

# Convert YYYY-MM-DD to integer for POSIX-compatible comparison
expires_int=$(echo "${expires_at}" | tr -d '-')
today_int=$(echo "${today}" | tr -d '-')

# Case 2: Token expired - RENEW
if [ "${expires_int}" -lt "${today_int}" ]; then
    echo "INFO: Token expired (${expires_at}). Rotating..."
    NEW_TOKEN=$(glab token rotate "${TOKEN_NAME}" --repo "${CI_PROJECT_PATH}" --output text)

    echo "INFO: Updating CI variable '${VAR_NAME}'..."
    echo "${NEW_TOKEN}" | glab variable update "${VAR_NAME}" --repo "${CI_PROJECT_PATH}"

    echo "DONE: Token rotated and variable updated."
    exit 0
fi

# Case 3: Token valid but variable missing - ROTATE to get new value
if [ "${var_exists}" = "false" ]; then
    echo "INFO: Token '${TOKEN_NAME}' exists (expires ${expires_at}) but CI variable '${VAR_NAME}' is missing."
    echo "INFO: Rotating token to obtain a new value..."
    NEW_TOKEN=$(glab token rotate "${TOKEN_NAME}" --repo "${CI_PROJECT_PATH}" --output text)

    echo "INFO: Setting CI variable '${VAR_NAME}'..."
    echo "${NEW_TOKEN}" | glab variable set "${VAR_NAME}" \
        --repo "${CI_PROJECT_PATH}" \
        --masked \
        --protected \
        --description "Project access token for CI/CD release publishing and artifact uploads"

    echo "DONE: Token rotated and variable created."
    exit 0
fi

# Case 1: Token valid and variable exists - SKIP
echo "OK: Already configured. Token '${TOKEN_NAME}' expires ${expires_at}."
exit 0

```

Run it:

```bash
chmod +x .claude/commands/setup-ci-publish-token.sh && .claude/commands/setup-ci-publish-token.sh
```

## Script Behavior

| Token Exists? | Token Valid? | Variable Exists? | Script Action                                |
| ------------- | ------------ | ---------------- | -------------------------------------------- |
| No            | N/A          | Any              | Creates token and variable                   |
| Yes           | Expired      | Yes              | Rotates token, updates variable              |
| Yes           | Valid        | No               | Rotates token to get value, creates variable |
| Yes           | Valid        | Yes              | No action needed (already configured)        |

## Output Messages

The script uses consistent prefixes for parsing:

- `ERROR:` - Fatal error, script exits with non-zero status
- `INFO:` - Progress information
- `DONE:` - Successful completion with changes made
- `OK:` - Successful completion, no changes needed

## Examples of how to use the new token

In your `.gitlab-ci.yml` or CI scripts, prefer `CI_PUBLISH_TOKEN` for operations requiring elevated permissions. eg.:

```bash
PUBLISH_TOKEN="${CI_PUBLISH_TOKEN:-${GITLAB_TOKEN:-${GL_TOKEN:-}}}"
if [ -n "${PUBLISH_TOKEN}" ]; then
  glab auth login --hostname "${CI_SERVER_HOST}" --token "${PUBLISH_TOKEN}"
else
  glab auth login --hostname "${CI_SERVER_HOST}" --job-token "${CI_JOB_TOKEN}"
fi
```

In Python scripts you can now check for it:

```python
token = os.environ.get("CI_PUBLISH_TOKEN") or \
        os.environ.get("GITLAB_TOKEN") or \
        os.environ.get("GL_TOKEN") or \
        os.environ.get("CI_JOB_TOKEN")
```

## Token Details

The script creates tokens with:

- **Name:** `ci-publish-token`
- **Access level:** Maintainer
- **Scopes:** `api`, `write_repository`
- **Duration:** 1 year (8760h)

The CI variable is created with:

- **Protected:** Yes (only available on protected branches)
- **Masked:** Yes (hidden in job logs)

## Troubleshooting

**ERROR: The current GITLAB_TOKEN does not have the 'api' scope**

Your personal access token needs the `api` scope. Create a new token at Settings > Access Tokens with `api` scope enabled.

**ERROR: The current GITLAB_TOKEN does not have Maintainer (40) or higher access**

You need Maintainer or Owner role on the project to manage project access tokens and CI variables.

**401 Unauthorized errors persist after setup:**

- Verify the job runs on a protected branch (the variable is protected)
- Check token hasn't expired: `glab token list`
- Verify your CI script is using `CI_PUBLISH_TOKEN`, not `CI_JOB_TOKEN`

**Variable not available in job:**

- Protected variables only work on protected branches
- Verify the branch/tag is protected in Settings > Repository > Protected branches

## Related Documentation

- [GitLab Project Access Tokens](https://docs.gitlab.com/user/project/settings/project_access_tokens/)
- [GitLab CI/CD Variables](https://docs.gitlab.com/ci/variables/)
- [glab token documentation](https://gitlab.com/gitlab-org/cli/-/blob/main/docs/source/token/index.md)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.