Gitlab CI/CD 在管道之间传递 artifacts/variables

Gitlab CI/CD Pass artifacts/variables between pipelines

tl;博士

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

所以(在我的例子中)这个:

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

我有两个阶段,stagingdeploystaging 中的 building 作业构建应用程序并创建“审查应用程序”(为简单起见,没有单独的构建阶段)。 deploy 中的 deploying 作业然后上传新应用程序。

每当打开合并请求时,包含 building 作业 运行 的管道。通过这种方式构建应用程序,开发人员可以单击合并请求中的“审查应用程序”图标。 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 行所示:“删除 build.env”。我尝试将 build.env 添加到 .gitignore 但它仍然被删除。

经过数小时的搜索,我在 this gitlab issue comment and this Whosebug post 中发现 artifacts.reports.dotenv 不适用于 dependenciesneeds 关键字。

删除 dependencies 无效。仅使用 needs 也不起作用。不允许同时使用两者。

有谁知道如何让它工作的方法吗?我觉得这就是它应该的工作方式。

将工件作为文件获取

Whosebug post 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 不存在。 (build.env 是否在 .gitignore 中并不重要,两者都测试过)

从 API

获取工件

我还找到了 Whosebug post Use artifacts from merge request job in GitLab CI 的答案,建议将 API 与 $CI_JOB_TOKEN 一起使用。但是因为我需要非合并请求管道中的工件,所以我不能使用建议的 CI_MERGE_REQUEST_REF_PATH.

我尝试使用 $CI_COMMIT_REF_NAMEyml 的(重要部分)是:

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 Not Found”。由于 commit SHAs are not supported$CI_COMMIT_BEFORE_SHA$CI_COMMIT_SHA 也不起作用。

使用needs

更新:我在 gitlab 文档中找到了 Artifact downloads between pipelines in the same project 部分,这正是我想要的。但是:我无法让它工作。

从文档中复制 more less 后,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_VERSIONdeploying 的工作。如果除了我尝试过的方法之外还有其他方法,我很高兴听到它们。提前致谢。


例子.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

这是你可以通过文件传递的东西。

在构建作业中创建新变量:

 variables:
     CONFIG: "anyname"

然后在脚本中对文件执行 export/copy,例如:

- echo $BUILD_VERSION > $CI_PROJECT_DIR/$CONFIG

在工件中添加路径:

artifacts:
   paths:
   - $CONFIG

然后在部署作业中

variables:
     CONFIG: "anyname"

并获取它

- source $CI_PROJECT_DIR/$CONFIG

为了让它工作,只需尝试解决传递的问题,保持依赖关系并保持工件只使用“需要”,避免在作业中清除工件

您不能使用 CI/CD 在完全不相关的管道之间传递工件。 “building”在定义合并请求的分支上 运行,而“deploying”在合并结果上 运行,这并不意味着“deploying”只是下一阶段.如果中间合并了另一个MR呢?如果有合并冲突怎么办?

换句话说,你不能因为构建了开发分支就跳过主分支的“构建”。让“构建”一直发生,并将“部署”限制在主分支。在此设置中,您可以轻松地将工件从“构建”传递到“部署”。

或者,如果您希望合并事件实际使用版本状态更新主分支,只需使用源代码控制的 VERSION 文件。这就是 git 的用途。合并时,main 将从分支中获取 VERSION。如果另一个分支首先进入,您将必须解决冲突,这是您应该做的。

根据 Gitlab 文档,如果尚未过期,应该可以通过 URL 下载任何作业的工件。

此外,您也可以使用 Gitlab API 从其他项目下载(未过期的)工件;并且您可以使用 Gitlab API 将作业的工件标记为保持不管过期策略,或删除工件。

不过我自己没试过。

对于您的情况,假设 'building' 和 'deploying' 作业都 运行 在 main b运行ch 上,您有望通过工件像这样。

如果您有其他方法可以在 deploying 作业中找到 运行ch name X building 作业 运行 上的什么,那么您可以下载来自 b运行ch 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'.

文档的相关部分,带有链接和摘录:

Access a branch or tag's latest job artifacts by URL

要浏览或下载 b运行ch 的最新作品,请使用这两个 url 之一。 [我认为 /file/ 变体用于 Gitlab Pages 工件,但我不确定。 --埃斯蒂斯]

  • 浏览工件:
    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 b运行ch 的工件,作业 coverage,文件路径 review/index.htmlhttps://gitlab.com/gitlab-org/gitlab/-/jobs/artifacts/main/raw/review/index.html?job=coverage

Config setting: Keep artifacts from each branch's most recent succesful jobs

  • 此选项默认启用
  • AFAICT 它保留最近的工件
    • 每个活动的 b运行ch 或标签 (a.k.a.a 'ref');
    • 如果该引用上有多个管道 运行,则最后一个管道的工件会覆盖早期管道产生的工件。
  • 所有其他工件仍受生成它们的 .gitlab-ci.yml 中的 expire_in 设置控制。

Gitlab API for job artifacts

使用Gitlab的好处API是如果你能拿到合适的token,你还可以从其他项目下载artifacts。如果您的脚本在 Gitlab 中 运行ning CI.

,则您需要数字项目 ID——即 $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"

我假设我们一开始就知道要检索其工件的提交哈希。

这是计划:

commit hash --> job id --> artifact archive --> extract artifact

  1. Gitlab 的 GraphQL API 可以在 JSON 中获取项目的作业列表 + 每个作业的工件 urls。
  2. 您可以过滤 JSON 列表以获得您想要的提交 + 作业名。无法直接在 GraphQL 中执行,所以我在 Python.
  3. 中执行
  4. 然后打印作业 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 将 return 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 -> 路径部分。那一点肯定有效。

get-jobs-as-json.sh: (token, project name) --> 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=""
    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 ""

json2jobinfo.py: (joblist, job name, commit SHA) --> (slug of) archive url

这是一个 Python 脚本,它将从标准输入读取作业列表 JSON,并打印您指定的作业 + 提交组合的工件存档路径。

#!/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 个输入,并产生 1 个输出:(token, API URL, job name, commit sha, artefact path) -> artefact file。欢迎编辑。现在,我使用了 shell 以及 Python.

另外,理想情况下,有人会尝试上面的代码并留下评论是否可以正常工作。我可能会自己测试一下。但不是今天。