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-requestorcatalog-request-gatsbytemplate. 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:
upload — Uploads the solution to Zenodo as a draft deposit (receives a DOI).
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.
Go to your catalog project on GitLab.
Navigate to Settings → Access Tokens.
Create a new token:
Name: e.g.
album-ciRole:
MaintainerScopes:
write_repository
Copy the token value.
Step 2 — Create a Zenodo Access Token
Go to zenodo.org (or sandbox.zenodo.org for testing).
Navigate to Applications → Personal access tokens → New token.
Scopes:
deposit:writeanddeposit:actions.Copy the token value.
Note: Sandbox and production tokens are separate — you cannot use a sandbox token with
zenodo.orgor 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 |
|---|---|---|---|
|
The Project Access Token from Step 1 |
Yes |
Yes |
|
The Zenodo API token from Step 2 |
Yes |
Yes |
|
|
No |
No |
Optionally, override git commit authorship (defaults are shown in the CI file):
Variable |
Default |
Description |
|---|---|---|
|
|
Git author name for CI commits |
|
|
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 |
|
|
Merge request |
|
|
Push to |
|
|
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=Trueso re-runs with no changes succeed.merge: Uses force-push for tags; fails fast if
mainadvanced sinceupdate(re-running the pipeline resolves this safely).
Testing with Zenodo Sandbox
For initial testing, use the Zenodo sandbox:
Create an account at sandbox.zenodo.org.
Generate an API token there.
Set
ZENODO_BASE_URLtohttps://sandbox.zenodo.orgin your CI variables.Deploy a test solution and verify the pipeline runs correctly.
Once satisfied, switch
ZENODO_BASE_URLtohttps://zenodo.organd use a production token.
Troubleshooting
Pipeline fails at publish with authentication error
Verify
CI_AUTH_TOKENhaswrite_repositoryscope andMaintainerrole.Check the token has not expired.
Zenodo upload fails with 401
Verify
ZENODO_ACCESS_TOKENis 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 pipeline — upload 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.