Setting up Zenodo DOI minting for a request catalog (GitLab CI)

This guide walks you through adding automatic Zenodo DOI minting to a request catalog hosted on GitLab. After setup, every deployed solution gets a DOI published on Zenodo before the merge request is merged.

Zenodo-backed releases are the most durable rung of Album’s sharing ladder (see catalog-development): each deployed solution version gets a citable, immutable DOI, completing the FAIR Findable and Accessible principles for your catalog. For an end-to-end example of catalog-governed dissemination see case-studies, scenario D.

Prerequisite: You already have a request catalog created from the catalog-request or catalog-request-gatsby template. If not, see catalog-development.

Overview

When a user runs album deploy <solution> <catalog>, album pushes the solution files to a branch and creates a merge request. The GitLab CI pipeline on that MR then:

  1. upload — Uploads the solution to Zenodo as a draft deposit (receives a DOI).

  2. publish — Updates the catalog index, commits, publishes the Zenodo deposit, and merges.

The publish job requires manual approval — this gives the catalog administrator a chance to review the solution before it receives a permanent DOI and is merged into the catalog.

A lightweight merge job auto-completes MRs that only touch the catalog index database (.db files), which happens when the pipeline itself updates the index.

Step 1 — Create a GitLab Project Access Token

The CI pipeline needs to push commits and merge branches back into the repository.

  1. Go to your catalog project on GitLab.

  2. Navigate to Settings → Access Tokens.

  3. Create a new token:

    • Name: e.g. album-ci

    • Role: Maintainer

    • Scopes: write_repository

  4. Copy the token value.

Step 2 — Create a Zenodo Access Token

  1. Go to zenodo.org (or sandbox.zenodo.org for testing).

  2. Navigate to Applications → Personal access tokens → New token.

  3. Scopes: deposit:write and deposit:actions.

  4. Copy the token value.

Note: Sandbox and production tokens are separate — you cannot use a sandbox token with zenodo.org or vice versa.

Step 3 — Configure CI/CD variables

In your GitLab catalog project, go to Settings → CI/CD → Variables and add:

Variable

Value

Protected

Masked

CI_AUTH_TOKEN

The Project Access Token from Step 1

Yes

Yes

ZENODO_ACCESS_TOKEN

The Zenodo API token from Step 2

Yes

Yes

ZENODO_BASE_URL

https://zenodo.org or https://sandbox.zenodo.org

No

No

Optionally, override git commit authorship (defaults are shown in the CI file):

Variable

Default

Description

CI_USER_NAME

Album CI Bot

Git author name for CI commits

CI_USER_EMAIL

ci@album.solutions

Git author email for CI commits

Step 4 — Add the .gitlab-ci.yml

Create or replace the .gitlab-ci.yml in the root of your catalog repository with the following content:

stages:
  - upload_publish


cache:
  paths:
    - vendor/

#----------------------------
# Authentication
#----------------------------
# Uses a Project Access Token (Maintainer role, write_repository scope).
# Store the token value as CI/CD variable CI_AUTH_TOKEN (see docs).

.linux_base_template:
  image: ubuntu:latest
  before_script:
    - apt-get update -y && apt-get install -yqqf ca-certificates openssh-client git unzip sshpass rsync bzip2 curl --fix-missing

    # setup micromamba & album
    - mkdir -p $CI_PROJECT_DIR/bin
    - curl -fsSL --retry 5 --retry-delay 3 https://micro.mamba.pm/api/micromamba/linux-64/latest -o /tmp/micromamba.tar.bz2
    - tar -xvjf /tmp/micromamba.tar.bz2 -C $CI_PROJECT_DIR/bin/ --strip-components=1 bin/micromamba
    - export MICROMAMBA_EXECUTABLE=$CI_PROJECT_DIR/bin/micromamba
    - $MICROMAMBA_EXECUTABLE --version
    - eval "$($MICROMAMBA_EXECUTABLE shell hook -s bash)"
    - micromamba create -y -n album -c conda-forge python=3.13
    - source ~/.bashrc
    - micromamba activate album
    - python -V
    - which python
    - pip install git+https://gitlab.com/album-app/album.git@main

    # build auth URL using Project Access Token
    - AUTH_URL="${CI_PROJECT_URL/https:\/\//https://${CI_USER_NAME}:${CI_AUTH_TOKEN}@}"
  variables:
    PIP_CACHE_DIR: $CI_PROJECT_DIR/.cache/pip
    CONDA_ENV_NAME: album
    CONDA_PREFIX: /opt/conda
    PREFIX: $CONDA_PREFIX/envs/$CONDA_ENV_NAME
    # git commit authorship — override in CI/CD variables if needed
    CI_USER_NAME: "Album CI Bot"
    CI_USER_EMAIL: "ci@album.solutions"
  cache:
    key: one-key-to-rule-them-all-linux
    paths:
      - ${CONDA_PREFIX}/pkgs/*.tar.bz2
      - ${CONDA_PREFIX}/pkgs/urls.txt


upload:
  extends: .linux_base_template
  stage: upload_publish
  script:
    - album-catalog-admin upload $CI_PROJECT_NAME /opt/catalog $AUTH_URL --branch-name=$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME --force-retrieve=True --report-file=/opt/catalog/report.yml
    # create environment variables from report
    - awk '{print $0}' /opt/catalog/report.yml | sed -e 's/:[^:\/\/]/=/g;s/$//g;s/ *=/=/g' > variables.env
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      changes:
        - "*.db"
      when: never
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      changes:
        - "solutions/**/*"
  artifacts:
    reports:
      dotenv: variables.env

publish:
  extends: .linux_base_template
  stage: upload_publish
  needs:
    - job: upload
      artifacts: true
  script:
    # using environment variables from upload job
    - echo "publishing doi $DOI of zenodo deposit with id $DEPOSIT_ID"
    # prepare repository to push changes
    - album-catalog-admin configure-repo $CI_PROJECT_NAME /opt/catalog $AUTH_URL --force-retrieve=True
    # update the index file of the catalog
    - album-catalog-admin update $CI_PROJECT_NAME /opt/catalog $AUTH_URL --branch-name=$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME --doi=$DOI --deposit-id=$DEPOSIT_ID
    # commit changes
    - album-catalog-admin commit $CI_PROJECT_NAME /opt/catalog $AUTH_URL --branch-name=$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
    # publish the deposit
    - album-catalog-admin publish $CI_PROJECT_NAME /opt/catalog $AUTH_URL --branch-name=$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
    # merge the results to main
    - album-catalog-admin merge $CI_PROJECT_NAME /opt/catalog $AUTH_URL --branch-name $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME --push-option merge_request.merge_when_pipeline_succeeds --log DEBUG
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      changes:
        - "*.db"
      when: never
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      changes:
        - "solutions/**/*"
      when: manual

merge:
  image: bash
  stage: upload_publish
  script:
    - echo "Auto merge enabled! Success!"
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      changes:
        - "*.db"
      when: always

If your catalog uses Gatsby pages, add a pages stage after upload_publish:

stages:
  - upload_publish
  - pages

# ... (keep all upload/publish/merge jobs from above) ...

pages:
  image: node:23
  stage: pages
  cache:
    paths:
      - gatsby/node_modules/
      - gatsby/.cache/
      - public/
  script:
    - rm -f -R public
    - cd gatsby
    - npm install
    - ./node_modules/.bin/gatsby build --prefix-paths
  artifacts:
    paths:
      - public
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      exists:
      - "album_catalog_index.db"

How the pipeline works

Job rules

Trigger

Changed files

Job triggered

Merge request

solutions/**/*

upload (auto) → publish (manual)

Merge request

*.db only

merge (auto-approve)

Push to main

album_catalog_index.db exists

pages (Gatsby only)

The *.db guard prevents the pipeline from re-running the upload/publish cycle when the pipeline itself updates the catalog index database and pushes.

Pipeline idempotency

Every step is safe to re-run after a partial failure:

  • upload: Detects if the same version is already on Zenodo and skips re-upload.

  • publish: If the deposit is already published, skips publishing and still tags.

  • commit: Uses allow_empty=True so re-runs with no changes succeed.

  • merge: Uses force-push for tags; fails fast if main advanced since update (re-running the pipeline resolves this safely).

Testing with Zenodo Sandbox

For initial testing, use the Zenodo sandbox:

  1. Create an account at sandbox.zenodo.org.

  2. Generate an API token there.

  3. Set ZENODO_BASE_URL to https://sandbox.zenodo.org in your CI variables.

  4. Deploy a test solution and verify the pipeline runs correctly.

  5. Once satisfied, switch ZENODO_BASE_URL to https://zenodo.org and use a production token.

Troubleshooting

Pipeline fails at publish with authentication error

  • Verify CI_AUTH_TOKEN has write_repository scope and Maintainer role.

  • Check the token has not expired.

Zenodo upload fails with 401

  • Verify ZENODO_ACCESS_TOKEN is correct and not expired.

  • Ensure you are not mixing sandbox/production tokens and URLs.

Merge fails with “main has advanced”

This is expected when another deploy merged while this pipeline was running. Simply re-run the failed pipelineupload will skip (already published), and update will rebase onto the current main before regenerating the index.

Pipeline runs twice (upload + merge)

This is correct behavior. The first run (upload/publish) processes the solution. The second run (merge job only) auto-approves the index database update that the first run pushed.