Skip to content
Snippets Groups Projects

tests: Test the ci-package-builder pipeline

Merged Emanuele Aina requested to merge wip/em/automatically-test-the-ci-itself into master
All threads resolved!
2 files
+ 351
0
Compare changes
  • Side-by-side
  • Inline
Files
2
  • caee0e7f
    Set up a test repository and run the ci-package-builder pipeline on it
    by committing new changes, creating merge requests and monitoring the
    resulting pipelines.
    
    The test currently checks:
    * submitting non-release changes and landing them
    * submitting release commits and landing them
    * blocking commits to frozen stable branches
    
    This pipeline needs some extra setup:
    * a tests/dash> repository, forked from pkg/target/dash>, where we
      force-push changes, create merge requests and monitor pipelines
    * the `OBS_ROOT` CI variable to be set on tests/dash> to point to
      throwaway OBS branches to test the upload and monitorig jobs,
      for instance `home:apertis-gitlab:apertis:v2021dev3:target`
    * the `GITLAB_CI_USER`, `GITLAB_CI_PASSWORD`, `OSC_USERNAME` and
      `OSC_PASSWORD` CI variables to be set on tests/dash>, matching what it
      is used on the pkg/ projects
    * the `GITLAB_AUTH_TOKEN` CI variable to be set on this repository
      to access the GitLab APIs used to issue MRs and monitor pipelines, and
      to push changes via git to the tests/dash> repository
    
    Signed-off-by: Emanuele Aina's avatarEmanuele Aina <emanuele.aina@collabora.com>
+ 323
0
#!/usr/bin/env python3
import argparse
import logging
import subprocess
import time
import urllib.parse
import git
import gitlab
SCRATCH_REPO_DIR = "/tmp/scratchrepo"
TEST_BRANCH = "wip/test/fake"
WAIT_AFTER_PUSH = 5
def _get_branch_prefix(project):
sep = "/"
prefix = project.default_branch.split(sep)[0]
return prefix + sep
def _find_stable_release_branch(project, prefix):
branches = (b.name for b in project.branches.list(all=True))
releases = (b for b in branches if b.startswith(prefix))
stable = (b for b in releases if b[-4:-1] != "dev" and b[-3:] != "pre")
latest = sorted(stable, reverse=True)[0]
return latest
def _monitor_pipeline_for_completion(pipeline):
logging.info(f"monitoring pipeline {pipeline.web_url}")
while pipeline.status in ("running", "pending", "created"):
logging.debug(f"pipeline {pipeline.web_url} status is {pipeline.status}")
pipeline.refresh()
time.sleep(5)
def _commit_and_push(gl, repo, filename, msg):
repo.index.add(filename)
actor = git.Actor(gl.user.name, gl.user.email)
commit = repo.index.commit(msg, author=actor, committer=actor)
logging.info(f"committed {commit.hexsha} to {TEST_BRANCH}")
pushes = repo.remotes.origin.push(TEST_BRANCH, force=True, verbose=True)
assert not any(p.flags & git.remote.PushInfo.ERROR for p in pushes)
time.sleep(
WAIT_AFTER_PUSH
) # give GitLab some time to realize we pushed some changes
return commit
class GitLabToOBSTester:
def __init__(self, testing_pipeline_url):
self.gl = None
self.scratch_gitlab = None
self.testing_pipeline_url = testing_pipeline_url
self.stable_branch = None
def connect(self, gitlab_instance, gitlab_server_url, gitlab_auth_token):
if gitlab_server_url:
logging.info(f'Connecting to the "{gitlab_server_url}" instance')
self.gl = gitlab.Gitlab(gitlab_server_url, private_token=gitlab_auth_token)
else:
logging.info(f'Connecting to the "{gitlab_instance}" configured instance')
self.gl = gitlab.Gitlab.from_config(gitlab_instance)
self.gl.auth()
def prepare_scratch(self, scratch_repository):
logging.info(
f"setting up options on {scratch_repository}, temporarily disable CI"
)
s = self.gl.projects.get(scratch_repository)
self.scratch_gitlab = s
try:
s.branches.delete(TEST_BRANCH)
except gitlab.GitlabDeleteError:
pass
s.merge_requests_access_level = "enabled"
s.merge_method = "ff"
s.builds_access_level = "disabled"
s.lfs_enabled = True
s.save()
url = urllib.parse.urlparse(s.http_url_to_repo)
password = urllib.parse.quote(self.gl.private_token, safe="")
netloc = f"oauth2:{password}@{url.netloc}"
url = url._replace(netloc=netloc)
logging.info(f"checking out {url.geturl()}")
self.scratch_git = git.Repo.clone_from(url.geturl(), SCRATCH_REPO_DIR)
def reset_scratch(self, reference_repository):
logging.info(
f"resetting {self.scratch_gitlab.path_with_namespace} to {reference_repository}"
)
scratch = self.scratch_git
reference = self.gl.projects.get(reference_repository)
url = reference.http_url_to_repo
scratch.create_remote("reference", url)
scratch.remotes.reference.fetch()
logging.debug(
f"switching to the {TEST_BRANCH} branch as {scratch.head.commit.hexsha}"
)
scratch.create_head("wip/test/fake").checkout()
logging.debug("synchronizing branches")
default = reference.default_branch
prefix = _get_branch_prefix(reference)
self.stable_branch = _find_stable_release_branch(reference, prefix)
branches = [default, self.stable_branch, "pristine-lfs", "pristine-lfs-source"]
logging.info(f"synchronizing the {branches} branches")
for branchname in branches:
scratch.create_head(
branchname,
commit=scratch.remotes.reference.refs[branchname],
force=True,
)
logging.debug(
f"pushing {branches} to {self.scratch_gitlab.path_with_namespace}"
)
pushes = scratch.remotes.origin.push(branches, force=True, verbose=True)
assert not any(p.flags & git.remote.PushInfo.ERROR for p in pushes)
logging.debug(
f"setting the default branch on {self.scratch_gitlab.path_with_namespace} to {default} and re-enable CI"
)
self.scratch_gitlab.default_branch = default
self.scratch_gitlab.builds_access_level = "enabled"
self.scratch_gitlab.save()
def point_gitlab_ci_here(self, ci_config_path):
logging.info(f"pointing to the CI definition at {ci_config_path}")
self.scratch_gitlab.ci_config_path = ci_config_path
self.scratch_gitlab.save()
def test_nonrelease_mr(self):
logging.info(f"testing an unreleased version on {TEST_BRANCH}")
assert self.scratch_git.active_branch.name == TEST_BRANCH
subprocess.run(
["dch", "-i", "Fake changes while testing the GitLab-to-OBS pipeline"],
check=True,
cwd=SCRATCH_REPO_DIR,
)
msg = f"Test unreleased changes\n\nPipeline: {self.testing_pipeline_url}"
commit = _commit_and_push(self.gl, self.scratch_git, "debian/changelog", msg)
pipeline = self.scratch_gitlab.pipelines.list(
ref=TEST_BRANCH, sha=commit.hexsha
)[0]
_monitor_pipeline_for_completion(pipeline)
assert (
pipeline.status == "success"
), f"submitted non-release pipeline {pipeline.web_url} didn't succeed: {pipeline.status}"
job_names = [j.name for j in pipeline.jobs.list()]
assert job_names == [
"build-source"
], f"submitted non-release pipeline on unmerged changes expected different jobs: {job_names}"
target = self.scratch_gitlab.default_branch
mr = self.scratch_gitlab.mergerequests.create(
dict(
source_branch=TEST_BRANCH,
target_branch=target,
title="Test non-release",
)
)
logging.info(f"created non-release MR {mr.web_url}")
pipelines = [p for p in mr.pipelines() if p["sha"] == commit.hexsha]
assert (
len(pipelines) == 1
), f"non-release MR has unexpected pipelines: {' '.join(p['web_url'] for p in pipelines)}"
assert mr.pipelines()[0]["id"] == pipeline.id
logging.info(f"landing {mr.web_url}")
mr.merge()
time.sleep(WAIT_AFTER_PUSH)
pipeline = self.scratch_gitlab.pipelines.list(
ref=self.scratch_gitlab.default_branch, sha=commit.hexsha
)[0]
_monitor_pipeline_for_completion(pipeline)
assert (
pipeline.status == "success"
), f"landed non-release pipeline {pipeline.web_url} didn't succeed: {pipeline.status}"
job_names = [j.name for j in pipeline.jobs.list()]
assert job_names == [
"build-source",
"tag-release",
"upload",
], f"landed non-release pipeline expected different jobs: {job_names}"
logging.info("Non-release MR landed successfully ✅")
def test_release_commit(self):
logging.info("submitting a release commit")
subprocess.run(["dch", "-r", "apertis"], check=True, cwd=SCRATCH_REPO_DIR)
msg = f"Test release commit\n\nPipeline: {self.testing_pipeline_url}"
commit = _commit_and_push(self.gl, self.scratch_git, "debian/changelog", msg)
pipeline = self.scratch_gitlab.pipelines.list(
ref=TEST_BRANCH, sha=commit.hexsha
)[0]
_monitor_pipeline_for_completion(pipeline)
assert (
pipeline.status == "success"
), f"submitted release pipeline {pipeline.web_url} didn't succeed: '{pipeline.status}'"
job_names = [j.name for j in pipeline.jobs.list()]
assert job_names == [
"build-source"
], f"submitted release pipeline on unmerged changes expected different jobs: {job_names}"
return commit
def test_release_mr_to_frozen_branch(self, commit):
logging.info("testing MR to frozen branch")
target = self.stable_branch
mr = self.scratch_gitlab.mergerequests.create(
dict(
source_branch=TEST_BRANCH,
target_branch=target,
title="Test release on frozen branch",
)
)
logging.info(f"created release MR {mr.web_url} to frozen branch {target}")
time.sleep(WAIT_AFTER_PUSH)
pipelines = {
p["ref"]: self.scratch_gitlab.pipelines.get(p["id"])
for p in mr.pipelines()
if p["sha"] == commit.hexsha
}
assert (
len(pipelines) == 2
), f"unexpected pipelines: {p['web_url'] for p in pipelines}"
branch_pipeline = pipelines.pop(TEST_BRANCH)
mr_pipeline = next(iter(pipelines.values()))
_monitor_pipeline_for_completion(branch_pipeline)
_monitor_pipeline_for_completion(mr_pipeline)
assert (
branch_pipeline.status == "success"
), f"release branch pipeline {branch_pipeline.web_url} to frozen branch didn't succeed: '{branch_pipeline.status}'"
assert (
mr_pipeline.status == "failed"
), f"release MR pipeline {mr_pipeline.web_url} to frozen branch should have failed: '{mr_pipeline.status}'"
job_names = [j.name for j in mr_pipeline.jobs.list()]
assert job_names == [
"freeze-stable-branches"
], f"submitted non-release pipeline expected different jobs: {job_names}"
mr.state = 'closed'
mr.save()
logging.info("MR on frozen branch blocked successfully ✅")
def test_release_mr_to_default_branch(self, commit):
logging.info("testing MR to default branch")
target = self.scratch_gitlab.default_branch
mr = self.scratch_gitlab.mergerequests.create(
dict(source_branch=TEST_BRANCH, target_branch=target, title="Test release")
)
logging.info(f"created release MR {mr.web_url} to default branch {target}")
pipelines = [p for p in mr.pipelines() if p["sha"] == commit.hexsha]
assert len(pipelines) == 1
assert pipelines[0]["ref"] == TEST_BRANCH
logging.info(f"landing {mr.web_url}")
mr.merge()
time.sleep(WAIT_AFTER_PUSH)
pipeline = self.scratch_gitlab.pipelines.list(
ref=self.scratch_gitlab.default_branch, sha=commit.hexsha
)[0]
_monitor_pipeline_for_completion(pipeline)
assert (
pipeline.status == "success"
), f"landed release pipeline {pipeline.web_url} didn't succeed: '{pipeline.status}'"
job_names = [j.name for j in pipeline.jobs.list()]
assert job_names == [
"build-source",
"tag-release",
"upload",
], f"landed release pipeline on merged changes expected different jobs: {job_names}"
logging.info("Release MR landed successfully ✅")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Test the GitLab-to-OBS pipeline")
parser.add_argument(
"--debug",
action="store_const",
dest="loglevel",
const=logging.DEBUG,
help="print debug information",
)
parser.add_argument(
"--scratch-repository",
type=str,
required=True,
help="the repository on which the pipeline is tested",
)
parser.add_argument(
"--reference-repository",
type=str,
required=True,
help="the scratch repository will be reset to the reference-repository contents",
)
parser.add_argument(
"--gitlab-instance",
type=str,
default="apertis",
help="get connection parameters from this configured instance",
)
parser.add_argument("--gitlab-auth-token", type=str, help="the GitLab authentication token")
parser.add_argument("--gitlab-server-url", type=str, help="the GitLab instance URL")
parser.add_argument(
"--testing-pipeline-url",
type=str,
help="The URL of the pipeline running the test",
)
parser.add_argument(
"--ci-config-path",
type=str,
required=True,
help="The path to the CI config to test",
)
args = parser.parse_args()
logging.basicConfig(level=args.loglevel or logging.INFO)
t = GitLabToOBSTester(args.testing_pipeline_url)
t.connect(args.gitlab_instance, args.gitlab_server_url, args.gitlab_auth_token)
t.prepare_scratch(args.scratch_repository)
t.reset_scratch(args.reference_repository)
t.point_gitlab_ci_here(args.ci_config_path)
t.test_nonrelease_mr()
release_commit = t.test_release_commit()
t.test_release_mr_to_frozen_branch(release_commit)
t.test_release_mr_to_default_branch(release_commit)
logging.info("Test completed successfully ✨")
Loading