Gitlab CI/CD:在流水线之间传递构建产物/变量

28

简短概述

在Gitlab CI中,如何在不同流水线的作业之间传递数据,例如$BUILD_VERSION变量?

我的情况是这样的:

Pipeline 1 on push ect.            Pipeline 2 after merge

    `building` job ...                `deploying` job
          │                                ▲
          └─────── $BUILD_VERSION ─────────┘

背景

考虑以下示例(完整的yml如下):

building:
    stage: staging
    # only on merge requests
    rules:
        # execute when a merge request is open
        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
          when: always
        - when: never
    script:
        - echo "BUILD_VERSION=1.2.3" > build.env
    artifacts:
        reports:
            dotenv: build.env

deploying:
    stage: deploy
    # after merge request is merged
    rules:
        # execute when a branch was merged to staging
        - if: $CI_COMMIT_BRANCH == $STAGING_BRANCH
          when: always
        - when: never
    dependencies: 
        - building
    script:
        - echo $BUILD_VERSION

我有两个阶段,分别是stagingdeploy。在staging中,building工作构建应用程序并创建“Review App”(为简单起见,没有单独的构建阶段)。然后,在deploy中,deploying工作上传新的应用程序。
包含building工作的流水线在打开合并请求时运行。这样,应用程序就会被构建,并且开发人员可以在合并请求中点击“Review App”图标。合并请求合并后,deploying工作立即运行。思路如下:
                      *staging* stage (pipeline 1)        *deploy* stage (pipeline 2)

<open merge request> -> `building` job (and show)   ...   <merge> -> `deploying` job
                             │                                            ▲
                             └───────────── $BUILD_VERSION ───────────────┘

对我来说问题是,staging/building会创建一些数据,例如$BUILD_VERSION。我希望在deploy/deploying中使用这个$BUILD_VERSION,例如通过Gitlab API创建一个新的发布版本。
所以我的问题是:如何将$BUILD_VERSION(和其他数据)从staging/building传递到deploy/deploying

我迄今为止尝试过的方法

artifacts.reports.dotenv

所述案例在 Pass an environment variable to another job 中的gitlab文档中更详细地处理。此外,下面显示的 yml 文件受到这个示例的很大启发。然而,它并不起作用。 build.env 构件是在 building 中创建的,但每当执行 deploying 任务时,build.env 文件都会被删除,如第15行所示:"Removing build.env"。我试图将 build.env 添加到 .gitignore 中,但它仍然会被删除。

Preparing environment - Running on runner- via gitlab-runner... - Getting source from Git repository - Fetching changes with git depth set to 50... - Reinitialized existing Git repository in  - Checking out as staging... - Removing build.env - Skipping Git submodules setup - Executing "step_script" stage of the job script - Using docker image - echo $BUILD_VERSION - Job succeeded

经过多次搜索,我在这个Gitlab issue评论这篇stackoverflow帖子中发现artifacts.reports.dotenv不能与dependenciesneeds关键字一起使用。
删除dependencies也无法解决问题。仅使用needs也不行。同时使用两者是不允许的。
有没有人知道如何使其正常工作?我觉得它应该这样工作。
将构件作为文件获取
这篇stackoverflow帖子中的回答Gitlab ci cd removes artifact for merge requests建议将build.env作为普通文件使用。我也尝试了这种方法。关键的yml如下:
building:
    # ...
    artifacts:
        paths:
            - build.env

deploying:
    # ...
    before_script:
        - source build.env

结果与上面相同。 build.env 被删除。然后 source build.env 命令失败,因为 build.env 不存在。(无论是否在.gitignore中,都经过测试)。

从API获取工件

我还发现了stackoverflow帖子Use artifacts from merge request job in GitLab CI的答案,建议使用API和 $CI_JOB_TOKEN一起使用。但由于我需要在非合并请求管道中使用工件,因此无法使用建议的CI_MERGE_REQUEST_REF_PATH
我尝试使用$CI_COMMIT_REF_NAME。 然后(重要部分的)yml如下:
deploying:
    # ...
    script:
        - url=$CI_API_V4_URL/projects/jobs/artifacts/$CI_COMMIT_REF_NAME/download?job=building
        - echo "Downloading $url"
        - 'curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --output $url'
        # ...

但是这个API请求被拒绝并显示“404未找到”。由于提交SHA不受支持, 因此$CI_COMMIT_BEFORE_SHA$CI_COMMIT_SHA也无法工作。

使用needs

更新:我在GitLab文档中找到了同一项目中的管道之间的构件下载部分,这正是我想要的。但是:我无法使其工作。

在从文档中复制了更多或更少的内容后,yml如下所示:

building:
    # ...
    artifacts:
        paths:
            - version
        expire_in: never

deploying:
    # ...
    needs:
        - project: $CI_PROJECT_PATH
          job: building
          ref: staging # building runs on staging branch, main doesn't work either
          artifacts: true

现在,deploying任务立即失败,我收到以下错误提示:

This job depends on other jobs with expired/erased artifacts:
Please refer to https://docs.gitlab.com/ee/ci/yaml/README.html#dependencies

我尝试设置artifacts.expire_in = never(如图所示),但仍然遇到相同的错误。此外,在 设置 CI/CD Artifacts 中选择了“保留来自最近成功工作的构件”。因此,应该存在构件。我错过了什么?根据文档,这应该可以正常工作!


我希望有人能帮助我将$BUILD_VERSION传递到deploying任务中。如果有比我尝试过的其他方法,我非常乐意听取。提前感谢。


示例.gitlab-ci.yml

stages:
    - staging
    - deploy

building:
    tags: 
        - docker
    image: bash
    stage: staging
    rules:
        - if: ($CI_PIPELINE_SOURCE == "merge_request_event") && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "staging"
          when: always
        - when: never
    script:
        - echo "BUILD_VERSION=1.2.3" > build.env
    artifacts:
        reports:
            dotenv: build.env
    environment:
        name: Example
        url: https://example.com

deploying:
    tags: 
        - docker
    image: bash
    stage: deploy
    rules:
        - if: $CI_COMMIT_BRANCH == "staging"
          when: always
        - when: never
    dependencies:
        - building
    script:
        echo $BUILD_VERSION

构件/依赖项应该正常工作。去掉 reports: dotenv: 并改用 paths:。然后将 dependencies: 保持不变,以在部署管道中恢复 build.env。 - Peter
@Peter 很遗憾,这个方法不起作用。如截图所示,我再次收到“删除build.env”的消息。我复制了我的答案中的 yml 并仅更改了 buildingartifacts.reportsartifacts.paths: -build.env - miile7
抱歉,我错过了您试图在某些构建中跳过“构建”阶段并尝试在不相关的流水线之间传递数据的部分。请参见我的答案。 - Peter
5个回答

8
根据Gitlab文档,如果任何作业的工件尚未过期,则可以通过URL下载该工件。
此外,您还可以使用Gitlab API下载其他项目(未过期)的工件;并且您可以使用Gitlab API标记作业的工件以保留其期限策略,或者删除工件。
但我自己没有尝试过这个。
对于您的情况,假设“构建”和“部署”作业都在“main”分支上运行,您可以像下面这样传递工件。
如果您有其他方法可以在“部署”作业中找出“构建”作业运行的分支名称X,那么您可以从分支X而不是始终从“main”下载工件。
# Assuming
# domain: example.com
# namespace: mygroup
# project: myproject

building:
    # on latest commit on `main`, because we need a predictable
    # branch name to save/retrieve the artifact.
    stage: staging
    script:
        - echo "BUILD_VERSION=1.2.3" > build.env
    artifacts:
        # 'paths', not 'reports'
        paths:
            - build.env

deploying:
    # after merge request is merged
    stage: deploy
    dependencies: 
        - building
    script:
        # could use ${CI_PROJECT_URL} to get https://example.com/mygroup/myproj
        - curl https://${CI_SERVER_HOST}/${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}/-/jobs/artifacts/main/raw/build.env?job=building > build.env
        - source build.env
        - echo $BUILD_VERSION  # should print '1.2.3'.

文档中相关部分,包括链接和摘录:

通过URL访问分支或标签的最新作业构件

要浏览或下载分支的最新构件,请使用以下两个URL之一。[我认为/file/变体用于Gitlab Pages构件,但我不确定。--Esteis]

  • 浏览构件:
    https://example.com/<namespace>/<project>/-/jobs/artifacts/<ref>/browse?job=<job_name>
  • 下载所有构件的zip:
    https://example.com/<namespace>/<project>/-/jobs/artifacts/<ref>/download?job=<job_name>
  • 下载一个构件文件:
    https://example.com/<namespace>/<project>/-/jobs/artifacts/<ref>/raw/<path/to/file>?job=<job_name>
  • 下载一个构件文件(与Gitlab Pages有关?)
    https://example.com/<namespace>/<project>/-/jobs/artifacts/<ref>/file/<path>?job=<job_name>

例如,要下载带有域gitlab.com、命名空间为gitlab-org、项目为gitlab、最新提交为main分支、作业为coverage、文件路径为review/index.html的构件: https://gitlab.com/gitlab-org/gitlab/-/jobs/artifacts/main/raw/review/index.html?job=coverage

配置设置:保留每个分支最近成功作业的构件

  • 此选项默认打开。
  • AFAICT,它会保留每个活动分支或标签(也称为“ref”)的最新构件; 如果在该ref上运行了多个Pipeline,则最后一个Pipeline的构件将覆盖先前Pipeline生成的构件。
  • 所有其他构件仍受.gitlab-ci.yml中产生它们的expire_in设置的管理。

GitLab作业工件的API

使用GitLab API的优点是,如果您能获得正确的令牌,您还可以从其他项目下载工件。您需要数字项目ID —— 如果您的脚本在GitLab CI中运行,则为$CI_PROJECT_ID。

要下载工件归档:

  • curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/${CI_PROJECT_ID}/jobs/artifacts/main/download?job=test"

要下载单个工件文件:

  • curl --location --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/${CI_PROJECT_ID}/jobs/artifacts/main/raw/some/release/file.pdf?job=pdf"

3
谢谢你的回答。我曾经尝试过这个方法,但没有成功。该 API 需要上一个任务的作业 ID,而我在寻找它时遇到了很大的麻烦。也许可以通过某种方式获取给定分支的最后一次运行任务,但我不记得了。我已经通过标记提交来解决了我的问题(标记可以被拉取,因此很容易获得)。 - miile7
2
是的,手动标记提交可能是使其工作最简单的方法。这个答案的最终API URL看起来像是自动解析给定分支的最后一次运行作业,也许它们仍然有效?一旦我再次操作Gitlab,我会尝试一下。同样适用于我下面的另一个答案:未经测试,但可能有效,并且到目前为止的研究可能会为某人节省一些工作。如果我将来有时间测试,我会更新我的答案。 - Esteis
请提供一个经过测试的解决方案,而不是基于阅读文档的理论。 - undefined

6
你不能使用CI/CD在完全不相关的流水线之间传递构件。 "构建"运行在定义合并请求的分支上,“部署”运行在合并结果上,并不意味着“部署”只是下一个阶段。如果在两者之间合并了另一个MR,或者发生了合并冲突,该怎么办?
换句话说,你不能跳过主要分支上的"构建",仅因为你在开发分支上进行了构建。让"构建"一直发生,并将"部署"限制在主分支上。在这种设置中,你可以轻松地将构件从"构建"传递到"部署"。
或者,如果你想让合并事件实际上使用源控制的VERSION文件更新主分支的版本状态,那就用git来实现。当你合并时,主分支将采用分支的VERSION。如果另一个分支先被合并,你必须解决冲突,就像你应该做的那样。

非常感谢。我想这就是我的问题的答案:“它不起作用”。尽管这不是我想听到的。有没有办法使管道“相关”?考虑到提交历史记录,我认为它们已经相关了。一个管道在父提交中运行,下一个管道在接下来的提交中运行。 - miile7
把构件存储在 git 日志校验和 (git log | md5sum) 下如何?如果您在主/主分支上运行部署流水线,它可以生成 git 日志校验和并尝试获取构件。如果它们存在,则使用它们。如果不存在,则运行构建步骤。 - peterjaap

4
我假设我们已经知道要检索工件的提交哈希。
计划如下:
提交哈希 -> 作业 ID -> 工件归档 -> 提取工件
Gitlab的GraphQL API使得可以获取JSON格式的项目作业列表以及每个作业的工件URL。
您可以筛选该JSON列表以获取所需的提交和作业名称。虽然无法直接在GraphQL中执行此操作,但我正在使用Python实现它。
然后打印作业ID或工件归档URL。在我们的情况下,我们直接获取工件归档URL;但其他人可能想将作业ID用作其他API调用的输入。
首先,让我们仅查看GraphQL查询及其结果,以了解可用数据的感觉。
GraphQL查询:项目作业和工件
以下是获取项目作业列表的查询。您可以通过将其粘贴到Gitlab's GraphQL explorer中来尝试它。

query {
  # FIXME: your project path goes here
  project(fullPath: "gitlab-org/gitlab") {
    # First page of jobs. To get the next page, change the head to
    # jobs(after: "123_my_endCursor") { ... }
    # You can find the endCursor in pageInfo
    jobs {
      pageInfo {
        endCursor
        startCursor
      }
      # No, we can't filter on `nodes(name: "my-job-name")`,
      # nor on `edges{ node(name: "my-job-name") }`. :-(
      nodes {
        id
        name
        commitPath
        artifacts {
          edges {
            node {
              downloadPath
              fileType
            }
          }
        }
      }
    }
  }
}

GraphQL 结果

GraphQL API 将返回以下 JSON。它包含用于分页的游标名称和作业列表。在此示例中,第一个作业没有工件,第二个作业有。实际上,此列表将包含 100 个作业。

{
  "data": {
    "project": {
      "jobs": {
        "pageInfo": {
          "endCursor": "eyJpZCI6IjE1NDExMjgwNDAifQ",
          "startCursor": "eyJpZCI6IjE1NDExNTY0NzEifQ"
        },
        "nodes": [
          {
            "id": "gid://gitlab/Ci::Build/1541156471",
            "name": "review-docs-cleanup",
            "refName": "refs/merge-requests/67466/merge",
            "refPath": "/gitlab-org/gitlab/-/commits/refs/merge-requests/67466/merge",
            "commitPath": "/gitlab-org/gitlab/-/commit/5ec616f5e8f3268c23ff06dc52ef098f76352a7f",
            "artifacts": {
              "edges": []
            }
          },
          {
            "id": "gid://gitlab/Ci::Build/1541128174",
            "name": "static-analysis 4/4",
            "refName": "refs/merge-requests/67509/merge",
            "refPath": "/gitlab-org/gitlab/-/commits/refs/merge-requests/67509/merge",
            "commitPath": "/gitlab-org/gitlab/-/commit/41f949d3a398968edb67e22526c93c2f5292c23d",
            "artifacts": {
              "edges": [
                {
                  "node": {
                    "downloadPath": "/gitlab-org/gitlab/-/jobs/1541128174/artifacts/download?file_type=metadata",
                    "fileType": "METADATA"
                  }
                },
                {
                  "node": {
                    "downloadPath": "/gitlab-org/gitlab/-/jobs/1541128174/artifacts/download?file_type=archive",
                    "fileType": "ARCHIVE"
                  }
                }
              ]
            }
          },
        ]
      }
    }
  }
}

可能实际运行的代码

请注意,以下脚本:

  • 不处理分页
  • 未在CI容器内运行
    • 初始GraphQL API请求脚本未经测试
    • 下载和提取存档的最终命令未经测试
    • 只有JSON -> path部分经过测试。那一部分肯定有效。

get-jobs-as-json.sh:(token,项目名称)--> joblist

#!/bin/sh

# Usage:
#
#   GRAPHQL_TOKEN=mysecret get-jobs-as-json.sh gitlab-org/gitlab
#
# You can authorize your request by generating a personal access token to use
# as a bearer token.
# https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html

main() {
    # We want curl to see ` \" `, so we type ` \\\" ` when we define $QUERY.
    # I type        : \\\"$group_and_project\\\"
    # QUERY contains: \"asdf/jkl\"
    # I type        : --data "{\"query\": \"$QUERY\"}
    # Curl sees     : '{"query": "...\"asdf/jkl\"...}"
    group_and_project="$1"
    QUERY="
      query {
        # Project path goes here
        project(fullPath: \\\"$group_and_project\\\") {

          # First page of jobs. To get the next page, change the head to
          # jobs(after: \\\"123_my_endCursor\\\") { ... }
          # You can find the endCursor in pageInfo
          jobs {
            pageInfo {
              endCursor
              startCursor
            }
            # No, you can't filter on nodes(name: \\\"my-job-name\\\"),
            # nor on edges{ node(name: \\\"my-job-name\\\") }.
            nodes {
              id
              name
              refName
              refPath
              commitPath
              artifacts {
                edges {
                  node {
                    downloadPath
                    fileType
                  }
                }
              }
            }
          }
        }
      }
    "
    curl "https://gitlab.com/api/graphql"
        --header "Authorization: Bearer $GRAPHQL_TOKEN" \
        --header "Content-Type: application/json" \
        --request POST \
        --data "{\"query\": \"$QUERY\"}"
}

main "$1"

这是一个Python脚本,它可以从标准输入中读取joblist JSON,并打印出您指定的作业+提交组合的artifact存档路径。

#!/usr/bin/python3

# json2jobinfo.py

"""Read JSON from stdin, print archive path of job with correct (jobname, commit) combo.

The commit SHA does not have to be the full hash, just the start is enough.

Usage:
    json2jobinfo.py JOBNAME GITHASH

Example:
    json2jobinfo.py 'static-analysis 4/4' 41f949
    json2jobinfo.py 'static-analysis 4/4' 41f949d3a398968edb67e22526c93c2f5292c23d
"""


import sys, json
from pprint import pprint
from typing import List, Dict, Tuple


def matches_sha(commitPath: str, pattern: str) -> bool:
    """True if this commitPath's commit hash starts with {pattern}"""
    commit_part = commitPath.split('/')[-1]
    # print(commit_part)
    # print(pattern)
    return commit_part.startswith(pattern)


def filter_commit_job(jobs: List[Dict], jobname: str, commit: str) -> List[Dict]:
    """Given list of job dicts, find job with correct jobname and commit SHA"""
    return [
        j for j in jobs
        if matches_sha(j['commitPath'], commit)
        and j['name'] == jobname
    ]


def get_archive_url(job: Dict) -> str:
    """Given job dict, return download path of 'ARCHIVE' artifact"""
    archive = [
        arti for arti in job['artifacts']['edges']
        if arti['node']['fileType'] == 'ARCHIVE'
    ][0]
    return archive['node']['downloadPath']


def main_sans_io(graphql_reply: str, jobname: str, commit: str) -> Tuple[str, str]:
    """Return job id, artifact archive download path"""
    jobs = json.loads(graphql_reply)['data']['project']['jobs']['nodes']
    job = filter_commit_job(jobs, jobname, commit)[0]
    job_id = job['id'].split('/')[-1]
    archive_url = get_archive_url(job)
    return job_id, archive_url


def main(args):
    """Read stdin; look for job with correct jobname and commit; print
    download path of artifacts archive"""
    if len(args) == 3:
        jobname, commit = args[1], args[2]
    else:
        # hardcoded for example purposes
        jobname = 'static-analysis 4/4'
        commit = '41f949d3a398968edb67e22526c93c2f5292c23d'

    graphql_reply = sys.stdin.read()
    job_id, job_archive_url = main_sans_io(graphql_reply, jobname, commit)
    print(job_archive_url)

    # If you want to see the json, instead:
    # pprint(job)

if __name__ == '__main__':
    main(sys.argv)

组合使用:

# First, ensure $GRAPHQL_TOKEN contains your personal access token

# Save current directory
oldpwd=$(pwd)

# cd to a temporary directory
cd $(mktemp -d)

zip_path=$( \
    ./get-jobs-as-json.sh gitlab-org/gitlab \
    | ./json2jobinfo.py 'static-analysis 4/4' 41f949 \
)
curl \
    --location \
    --header "PRIVATE-TOKEN: <your_access_token>" \
    $zip_path > archive.zip
unzip archive.zip

# Extract the file we want
cp FILE/YOU/WANT $oldpwd

# Go back to where we were
cd $oldpwd

理想情况下,上述代码将被合并到一个单独的Python脚本中,该脚本在一个地方接收5个输入,并产生一个输出:(token, API URL, job name, commit sha, artefact path) -> artefact file。欢迎编辑。目前,我使用了Shell和Python。
同样理想的是,有人将尝试上面的代码并留下评论,他们是否能够使其正常工作。我可能会自己测试它。但不是今天。

2

这是可以通过文件传递的内容。

在构建任务中创建新变量:

 variables:
     CONFIG: "anyname"

然后在脚本中将其导出/复制到文件中,例如:

- echo $BUILD_VERSION > $CI_PROJECT_DIR/$CONFIG

在构件中添加路径:

artifacts:
   paths:
   - $CONFIG

在部署任务中。
variables:
     CONFIG: "anyname"

并获取源代码

- source $CI_PROJECT_DIR/$CONFIG

要使其工作,只需尝试解决传递问题,保留依赖项,并使用“needs”来保留构件,避免在任务中清除构件。

2
谢谢你的回答。但不幸的是,这并没有起作用。我得到了与我的问题截图中显示的相同的输出。它在第15行中再次说“删除任何名称”。而且deploy作业失败,因为源文件不存在。(顺便说一句,在artifacts.paths中的$CONFIG_VERSION是一个笔误,对吧?尽管如此,管道仍然失败。) - miile7
是的,抱歉,我只是在看build_version并复制了它。好的,如果要擦除它,那么您需要使用"needs"选项或开始使用类似这样的阶段:https://docs.gitlab.com/ee/ci/yaml/#stages,如果GitLab管道很广泛,那么源代码是大多数用例的问题解决方法。 - anynewscom
1
我已经在使用阶段和 needs/dependencies 了。您能否举个例子,说明您想要对我在问题末尾发布的 yml 进行哪些更改? - miile7

0
我和你有同样的问题。我还在考虑最好的解决方案,但我有一个可能行得通的想法。
在我的情况下,唯一有效的终点是https://gitlab.com/api/v4/projects/<project_id>/jobs/<job_id>/artifacts。由于我不知道第二个流水线中的job_id,所以无法使用它,但我们可以通过其他终点获取该ID。

https://gitlab.com/api/v4/projects/<project_id>/jobs?scope[]=success

然后你有工作列表,也许可以使用筛选器找到ID。
主要问题是,你只能获取最后的20个工作,但是你可以通过参数page获取下一页。

我很想要一个例子,我也是这么想的。但是我不知道如何在.gitlab-ci.yml文件中实现这个。 - St. Jan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接