I have a repository that builds and supplies artifacts to other projects. In order to get the latest artifacts the GitLab Job Artifacts API offers two options based on the source of the request - inside a CI job or elsewhere. The URL I am using is
https://gitlab.example.com/api/v4/projects/${proj_id}/jobs/artifacts/${branch_tag}/download?job=${job_name}
I have a separate download.cmake
files inside a cmake
directory of my project (all of which is configured using CMake), where FetchContent_Declare()
and FetchContent_MakeAvailable()
are being executed to ensure that the project is setup for both development (locally) and deployment (on-premise premium GitLab instance).
Among other function inside download.cmake
the following is what I use for downloading any artifact from my GitLab:
function(download_file_gitlab_latest name token proj_id branch_tag job_name)
set(_URL "https://gitlab.example.com/api/v4/projects/${proj_id}/jobs/artifacts/${branch_tag}/download?job=${job_name}")
set(_HEADER "PRIVATE-TOKEN: ${token}")
message("GET Request: ${_URL}")
message("Destination prefix: ${name}")
FetchContent_Declare(
${name}
#QUIET
URL "${_URL}"
HTTP_HEADER ${_HEADER}
DOWNLOAD_NO_EXTRACT 0
DOWNLOAD_EXTRACT_TIMESTAMP 1
DOWNLOAD_NO_PROGRESS 0
)
if(NOT ${name}_POPULATED)
FetchContent_MakeAvailable(${name})
endif()
endfunction(download_file_gitlab_latest)
The header of the request contains an single parameter PRIVATE-TOKEN
that contains a group/project/personal access token required for authenticating any request against the GitLab API (more on that parameter later on).
The token is supplied by a token.txt
file, which has a single line with the respective token and is loaded in the CMakeLists.txt
that calls the download function from above líke this:
file(READ "${CMAKE_SOURCE_DIR}/token.txt" TOKEN) #<---- the token is loaded from the token.txt and stored inside the TOKEN variable
...
download_file_gitlab_latest(
"${DOWNLOAD_DIR_PREFIX}"
${TOKEN} #<---------------------------------------- supplied by token.txt
68522
"${config}"
"build-all-${config}"
)
where config
's value is a single value from CMAKE_CONFIGURATION_TYPES
(debug
, release
etc.).
Both locally and in my CI job I configure the project (which also triggers the fetching of the artifacts) using
cmake -Bbuild -G "Visual Studio 16 2019" -S.
The only difference is the passing of CMAKE_CONFIGURATION_TYPES
in the CI job to ensure that only the artifacts for the respective build type are downloaded.
Configuring the project locally shows no signs of a problem and the artifacts are downloaded, extracted, copied to a specific directory, included in the project and the build step is executed, producing a working EXE. However, remotely I am getting the following error:
CMake Error at D:/Software/CMake/share/cmake-3.30/Modules/FetchContent.cmake:1416:EVAL:1:
Parse error. Function missing ending ")". Instead found unterminated
bracket with text "JOB-TOKEN: ��g".
Call Stack (most recent call first):
D:/Software/CMake/share/cmake-3.30/Modules/FetchContent.cmake:1416 (cmake_language)
cmake/download.cmake:32 (FetchContent_Declare)
CMakeLists.txt:41 (download_file_gitlab_latest)
CMake Error at D:/Software/CMake/share/cmake-3.30/Modules/FetchContent.cmake:1416 (cmake_language):
cmake_language unknown error.
Call Stack (most recent call first):
cmake/download.cmake:32 (FetchContent_Declare)
CMakeLists.txt:41 (download_file_gitlab_latest)
-- Configuring incomplete, errors occurred!
Respectively the build steps never takes place.
There are two crucial steps that I have in my CI job that ensure the proper setup for the download and can be seen in the CI job below:
build-imgui-dx11-demo:
stage: build
rules:
- if: $CI_COMMIT_REF_NAME == "main"
allow_failure: true
artifacts:
untracked: true
paths:
- build
- cmake # Allows me to verify that PRIVATE-TOKEN has been replaced with JOB-TOKEN
- token.txt # Allows me to verify that the CI_JOB_TOKEN is stored in the token.txt and it's not just an empty file
when: always # Allows me to see the artifacts even if the CI job fails
before_script:
- echo $CI_JOB_TOKEN > token.txt
- |
(Get-Content ./cmake/download.cmake).Replace('PRIVATE-TOKEN', 'JOB-TOKEN') | Set-Content ./cmake/download.cmake
script:
- cmake -Bbuild -G "Visual Studio 16 2019" -S. -DCMAKE_CONFIGURATION_TYPES="release"
- cmake --build build --target ALL_BUILD --config release
For local download I use PRIVATE-TOKEN
as also described in the download.cmake
function. Since it's really bad practice to upload passwords and personal tokens to a repo, the token.txt
is listed in .gitignore
and therefore needs to be generated in the CI job
echo $CI_JOB_TOKEN > token.txt
Further, since the project is configured in a CI job, I also replace PRIVATE-TOKEN
with JOB-TOKEN
in the header for FetchContent_Declare()
, which I have verified by checking the changed download.cmake
that indeed has
set(_HEADER "JOB-TOKEN: ${token}")
Since I have a premium subscription I do know that I can use needs: project
to interconnect multiple repositories and their artifacts. However, I would like to do it this way since it does not depend on premium features and (with minimal changes) mirrors the local setup.
UPDATE: First I added a message()
inside my download function
message("Token: ${token}") # token is the parameter of the function and not the TOKEN variable where the contents of token.txt are stored!
but got empty message. Even Token:
substring was gone.
I then added a similar message
message("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++")
message("------------------------Found token ${TOKEN}---------------------")
message("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++")
right after the file(READ "${CMAKE_SOURCE_DIR}/token.txt" TOKEN)
. A screen shot of the GitLab CI log can be seen below:
It appears that contents of token.txt
, while correct, are not properly read. I downloaded a couple of versions of that file from different CI job failed runs and it always consists of a different token string and an empty second line. I do believe that the second terminating empty line is what messes up the processing of the token. I need to see how to write the CI_JOB_TOKEN
to a TXT file without automatically adding a new line or use some option provided by file(READ ...)
in CMake.
The problem here was that echo
ing a text to a file automatically adds a new line to it (similar to Out-File
powershell command).
I changed my CI job accordingly:
- |
$CI_JOB_TOKEN | Out-File token.txt -NoNewline
(Get-Content ./cmake/download.cmake).Replace('PRIVATE-TOKEN', 'JOB-TOKEN') | Set-Content ./cmake/download.cmake
Notice the -NoNewline
parameter, which ensures that the file contains a single line (the token).
While this fixes the issue, I made another change to make my CMake project configuration less prone to such an error (I am actually already checking if the file exists and is empty) by replacing file(READ ...)
with
file(
STRINGS
"${CMAKE_SOURCE_DIR}/token.txt"
TOKEN
LIMIT_COUNT 1
)
According to the file()
the STRINGS
mode reads ASCII strings, which in this specific case - GitLab tokens - is not an issue. In addition the LIMIT_COUNT
restricts how many strings are read (documentation doesn't state it but I assume that string == line). This change ensures that token.txt
will not break the configuration if echo
ed or otherwise created locally/inside CI job.
The failing of the download is also due to the project not being whitelisted in the project, where the request for download is targeted at. This can be accomplished by adding it to the Limit access to
list.