def update_github_statuses(self, trigger): descriptions = { "pending": "Pipeline in-progress", "success": "Pipeline success", "error": "Pipeline in error or canceled", "failure": "Pipeline failed" } gitlab_project_id = trigger['ci_project_id'] github_repo = trigger['github_repo'] sha = trigger['sha'] installation_id = trigger['installation_id'] context = trigger['context'] ref = trigger['ci_ref'] pending = False gitlabclient = GitlabClient() githubclient = GithubClient(installation_id=installation_id) try: pipelines = {} project = gitlabclient.get_project(gitlab_project_id) project_url = project['web_url'] pipelines = gitlabclient.get_pipelines(int(gitlab_project_id), ref=ref) if not pipelines: logger.info('no pipelines') raise self.retry(countdown=60) pipe = pipelines[0] state = GITHUB_STATUS_MAP[pipe['status']] pending = state == "pending" pipeline_body = { "state": state, "target_url": project_url + "/pipelines/%s" % pipe['id'], "description": descriptions[state], "context": "%s/pipeline" % context } resp = [] resp.append(githubclient.post_status(pipeline_body, github_repo, sha)) builds = gitlabclient.get_jobs(project['id'], pipe['id']) if not builds: logger.info('no builds') raise self.retry(countdown=60) pdict = {'builds': {}} for build in builds: if build['name'] not in pdict['builds']: pdict[build['name']] = [] pdict[build['name']].append(build) for _, builds in pdict['builds'].items(): build = sorted(builds, key=lambda x: x['id'], reverse=True)[0] if build['status'] not in ['skipped', 'created']: resp.append( update_github_status(project, build, github_repo, sha, installation_id, context)) if pending: logger.info('still pending: retry') time.sleep(10) raise self.retry(exc=None, countdown=60) return resp except requests.exceptions.RequestException as exc: logger.error('Error request') raise self.retry(countdown=60, exc=exc)
def update_pipeline_status(gitlab_project_id, pipeline_id): ''' Queries GitLab to get the pipeline status and then update the GitHub statuses ''' gitlabclient = GitlabClient() project = gitlabclient.get_project(gitlab_project_id) pipeline_attr = gitlabclient.get_pipeline_status(gitlab_project_id, pipeline_id) return post_pipeline_status(project, pipeline_attr)
def update_build_status(self, params): try: gitlab_project_id = params['ci_project_id'] github_repo = params['github_repo'] sha = params['sha'] installation_id = params['installation_id'] build_id = params['build_id'] gitlabclient = GitlabClient() project = gitlabclient.get_project(gitlab_project_id) build = gitlabclient.get_job(project['id'], build_id) return update_github_status(project, build, github_repo, sha, installation_id) except Exception as exc: self.retry(countdown=60, exc=exc)
def retry_build(self, external_id, sha=None): project_id = external_id['project_id'] object_id = external_id['object_id'] kind = external_id['object_kind'] gitlabclient = GitlabClient() try: if kind == "build": return gitlabclient.retry_build(project_id, object_id) elif kind == "pipeline": return gitlabclient.retry_pipeline(project_id, sha) except requests.exceptions.RequestException as exc: logger.error('Error request') raise self.retry(countdown=60, exc=exc)
def update_github_check(event): gitlabclient = GitlabClient() checkstatus = CheckStatus(event) installation_id = gitlabclient.get_variable( checkstatus.project_id, 'GITHUB_INSTALLATION_ID')['value'] github_repo = gitlabclient.get_variable(checkstatus.project_id, 'GITHUB_REPO')['value'] githubclient = GithubClient(installation_id=installation_id) # Skip queued builds as they could be 'manual' if checkstatus.status == "queued" and checkstatus.object_kind == "build": return None if checkstatus.object_kind == "pipeline": githubclient.post_status(checkstatus.render_pipeline_status(), github_repo, checkstatus.sha) return githubclient.create_check(github_repo, checkstatus.render_check())
def post_pipeline_status(project, pipeline_attr): ''' POST the pipeline status on GitHub ''' descriptions = { "pending": "Pipeline in-progress", "success": "Pipeline success", "error": "Pipeline in error or canceled", "failure": "Pipeline failed" } gitlabclient = GitlabClient() installation_id = gitlabclient.get_variable( project['id'], 'GITHUB_INSTALLATION_ID')['value'] github_repo = gitlabclient.get_variable(project['id'], 'GITHUB_REPO')['value'] githubclient = GithubClient(installation_id=installation_id) sha = pipeline_attr['sha'] context = FFCONFIG.github['context'] state = GITHUB_STATUS_MAP[pipeline_attr['status']] pipeline_body = { "state": state, "target_url": project['web_url'] + "/pipelines/%s" % pipeline_attr['id'], "description": descriptions[state], "context": "%s/pipeline" % context } resync_body = { "state": "success", "target_url": FFCONFIG.failfast['failfast_url'] + "/api/v1/resync/%s/%s" % (project['id'], pipeline_attr['id']), "description": "resync-gitlab status", "context": "%s/resync-gitlab" % context } githubclient.post_status(resync_body, github_repo, sha) return githubclient.post_status(pipeline_body, github_repo, sha)
def migrate_variables(project_source_id, project_target_id): client = GitlabClient() variables = client.get_variables(project_source_id)
def trigger_pipeline(self): gevent = self.ghevent gitlab_user = self.config.gitlab['robot-user'] dirpath = tempfile.mkdtemp() repo_path = os.path.join(dirpath, "repo") gitbin = self._checkout_repo(gevent, repo_path) try: ci_file = self._get_ci_file(repo_path) except ResourceNotFound: raise Unexpected("Could not find a CI config file in: %s" % (repo_path)) try: content = self._parse_ci_file(ci_file['content'], ci_file['file']) except YAMLComposeError: raise Unexpected("Could not parse CI file: %s" % (ci_file['file']), {}) lint_resp = GitlabClient().gitlabci_lint(ci_file['content']) logger.error(lint_resp) logger.error(content) if 'status' not in lint_resp or lint_resp['status'] != 'valid': raise Unexpected(".gitlab-ci.yml syntax error", {}) variables = content.get('variables', dict()) namespace = variables.get('FAILFASTCI_NAMESPACE', self.config.gitlab.get('namespace', None)) repo = variables.get('GITLAB_REPOSITORY', None) reponame = gevent.repo.replace("/", "_") if repo: namespace, reponame = repo.split('/') gitlab_endpoint = variables.get( 'GITLAB_URL', self.config.gitlab.get('gitlab_url', None)) self.gitlab = GitlabClient(gitlab_endpoint, config=self.config) ci_project = self.gitlab.initialize_project(reponame, namespace) # @Todo(ant31) check if clone_url is required # clone_url = clone_url_with_auth(gevent.clone_url, "bot:%s" % self.github.token) target_url = clone_url_with_auth( ci_project['http_url_to_repo'], "%s:%s" % (gitlab_user, self.gitlab.gitlab_token)) gitbin.remote('add', 'target', target_url) variables.update({ 'EVENT': gevent.event_type, 'PR_ID': str(gevent.pr_id), 'SHA': gevent.head_sha, 'SHA8': gevent.head_sha[0:8], 'FAILFASTCI_STATUS_API': ('%s/api/v1/github_status' % (self.config.failfast['failfast_url'], )), 'SOURCE_REF': gevent.refname, 'REF_NAME': gevent.refname, 'CI_REF': gevent.target_refname, 'GITHUB_INSTALLATION_ID': str(gevent.installation_id), 'GITHUB_REPO': gevent.repo }) content['variables'] = variables self.gitlab.set_variables( ci_project['id'], { 'GITHUB_INSTALLATION_ID': str(gevent.installation_id), 'GITHUB_REPO': gevent.repo }) perform_sync = variables.get("FAILFAST_SYNC_REPO", "false") if ((perform_sync == "true") or (DEFAULT_MODE == "sync")): # Full synchronize the repo) gitbin.push("target", 'HEAD:%s' % gevent.target_refname, "-f") ci_sha = str(gitbin.rev_parse('HEAD')) return { # NOTE: the GitHub reference details for subsequent tasks. 'sha': gevent.head_sha, 'ci_sha': ci_sha, 'ref': gevent.refname, 'ci_ref': gevent.target_refname, 'ci_project_id': ci_project['id'], 'installation_id': gevent.installation_id, 'github_repo': gevent.repo, 'context': self.config.github['context'] } else: self.sync_only_ci_file(gevent, content, ci_project, ci_file)
class Pipeline(object): def __init__(self, git_event, config=None): if config is None: config = FFCONFIG self.ghevent = git_event self.config = config self.github = GithubClient( installation_id=self.ghevent.installation_id) def _parse_ci_file(self, content, filepath): if filepath == ".gitlab-ci.yml": return yaml.safe_load(content) def _checkout_repo(self, gevent, repo_path): clone_url = clone_url_with_auth(gevent.clone_url, "bot:%s" % self.github.token) try_count = 0 while try_count < 3: try: time.sleep(1) gitbin = Repo.clone_from(clone_url, repo_path).git break except Exception: try_count = try_count + 1 if try_count >= 3: raise gitbin.config("http.postBuffer", "1524288000") gitbin.config("--local", "user.name", "FailFast-ci Bot") gitbin.config("--local", "user.email", "*****@*****.**") if gevent.pr_id == "": gitbin.checkout(gevent.refname) else: pr_branch = "pr-%s" % gevent.pr_id gitbin.fetch('origin', "pull/%s/head:%s" % (gevent.pr_id, pr_branch)) gitbin.checkout(pr_branch) if not gitbin.rev_parse('HEAD') == gevent.head_sha: raise Unexpected("git sha don't match", { 'expected_sha': gevent.head_sha, 'sha': gitbin.rev_parse('HEAD') }) return gitbin def _get_ci_file(self, repo_path): content = None for filepath in [".gitlab-ci.yml", ".failfast-ci.jsonnet"]: path = os.path.join(repo_path, filepath) if not os.path.exists(path): continue with open(path, 'r') as f: content = f.read() return {"content": content, "file": filepath} if content is None: raise ResourceNotFound("no .gitlab-ci.yml or .failfast-ci.jsonnet") def trigger_pipeline(self): gevent = self.ghevent gitlab_user = self.config.gitlab['robot-user'] dirpath = tempfile.mkdtemp() repo_path = os.path.join(dirpath, "repo") gitbin = self._checkout_repo(gevent, repo_path) try: ci_file = self._get_ci_file(repo_path) except ResourceNotFound: raise Unexpected("Could not find a CI config file in: %s" % (repo_path)) try: content = self._parse_ci_file(ci_file['content'], ci_file['file']) except YAMLComposeError: raise Unexpected("Could not parse CI file: %s" % (ci_file['file']), {}) lint_resp = GitlabClient().gitlabci_lint(ci_file['content']) logger.error(lint_resp) logger.error(content) if 'status' not in lint_resp or lint_resp['status'] != 'valid': raise Unexpected(".gitlab-ci.yml syntax error", {}) variables = content.get('variables', dict()) namespace = variables.get('FAILFASTCI_NAMESPACE', self.config.gitlab.get('namespace', None)) repo = variables.get('GITLAB_REPOSITORY', None) reponame = gevent.repo.replace("/", "_") if repo: namespace, reponame = repo.split('/') gitlab_endpoint = variables.get( 'GITLAB_URL', self.config.gitlab.get('gitlab_url', None)) self.gitlab = GitlabClient(gitlab_endpoint, config=self.config) ci_project = self.gitlab.initialize_project(reponame, namespace) # @Todo(ant31) check if clone_url is required # clone_url = clone_url_with_auth(gevent.clone_url, "bot:%s" % self.github.token) target_url = clone_url_with_auth( ci_project['http_url_to_repo'], "%s:%s" % (gitlab_user, self.gitlab.gitlab_token)) gitbin.remote('add', 'target', target_url) variables.update({ 'EVENT': gevent.event_type, 'PR_ID': str(gevent.pr_id), 'SHA': gevent.head_sha, 'SHA8': gevent.head_sha[0:8], 'FAILFASTCI_STATUS_API': ('%s/api/v1/github_status' % (self.config.failfast['failfast_url'], )), 'SOURCE_REF': gevent.refname, 'REF_NAME': gevent.refname, 'CI_REF': gevent.target_refname, 'GITHUB_INSTALLATION_ID': str(gevent.installation_id), 'GITHUB_REPO': gevent.repo }) content['variables'] = variables self.gitlab.set_variables( ci_project['id'], { 'GITHUB_INSTALLATION_ID': str(gevent.installation_id), 'GITHUB_REPO': gevent.repo }) perform_sync = variables.get("FAILFAST_SYNC_REPO", "false") if ((perform_sync == "true") or (DEFAULT_MODE == "sync")): # Full synchronize the repo) gitbin.push("target", 'HEAD:%s' % gevent.target_refname, "-f") ci_sha = str(gitbin.rev_parse('HEAD')) return { # NOTE: the GitHub reference details for subsequent tasks. 'sha': gevent.head_sha, 'ci_sha': ci_sha, 'ref': gevent.refname, 'ci_ref': gevent.target_refname, 'ci_project_id': ci_project['id'], 'installation_id': gevent.installation_id, 'github_repo': gevent.repo, 'context': self.config.github['context'] } else: self.sync_only_ci_file(gevent, content, ci_project, ci_file) # @TODO this is a partial implem def sync_only_ci_file(self, gevent, content, ci_project, ci_file): """ Push only the .failfast-ci.yaml and set token to clone """ tokenkey = "GH_TOKEN_%s" % str.upper(uuid.uuid4().hex) clone_url = gevent.clone_url.replace("https://", "https://bot:$%s:" % tokenkey) ci_branch = gevent.refname content['variables'].update({'SOURCE_REPO': clone_url}) self.gitlab.set_variables(ci_project['id'], {tokenkey: self.github.token}) return self.gitlab.push_file(project_id=ci_project['id'], file_path=ci_file['file'], file_content=yaml.safe_dump(content), branch=ci_branch, message=gevent.commit_message)
class Pipeline(object): def __init__(self, git_event): self.ghevent = git_event self.github = GithubClient( installation_id=self.ghevent.installation_id) def _parse_ci_file(self, content, filepath): if filepath == ".gitlab-ci.yml": return yaml.safe_load(content) def _checkout_repo(self, gevent, repo_path): clone_url = clone_url_with_auth(gevent.clone_url, "bot:%s" % self.github.token) try_count = 0 while try_count < 3: try: time.sleep(1) gitbin = Repo.clone_from(clone_url, repo_path).git break except Exception: try_count = try_count + 1 if try_count >= 3: raise gitbin.config("http.postBuffer", "1524288000") gitbin.config("--local", "user.name", "FailFast-ci Bot") gitbin.config("--local", "user.email", "*****@*****.**") if gevent.pr_id == "": gitbin.checkout(gevent.refname) else: pr_branch = "pr-%s" % gevent.pr_id gitbin.fetch('origin', "pull/%s/head:%s" % (gevent.pr_id, pr_branch)) gitbin.checkout(pr_branch) if not gitbin.rev_parse('HEAD') == gevent.head_sha: raise Unexpected("git sha don't match", { 'expected_sha': gevent.head_sha, 'sha': gitbin.rev_parse('HEAD') }) return gitbin def _get_ci_file(self, repo_path): content = None for filepath in [".gitlab-ci.yml", ".failfast-ci.jsonnet"]: path = os.path.join(repo_path, filepath) if not os.path.exists(path): continue with open(path, 'r') as f: content = f.read() return {"content": content, "file": filepath} if content is None: raise ResourceNotFound( "n o .gitlab-ci.yml or .failfail-ci.jsonnet") def _append_update_stage(self, content): stage_name = "github-status-update" url = FAILFASTCI_API + "/api/v1/github_statuses" update_status = { "ci_project_id": "$CI_PROJECT_ID", "ci_sha": "$CI_BUILD_REF", "sha": "$SHA", "ci_ref": "$CI_COMMIT_REF_NAME", "github_repo": "$GITHUB_REPO", "installation_id": "$GITHUB_INSTALLATION_ID", "delay": 150 } update_status_30 = deepcopy(update_status) update_status_30['delay'] = 30 params_150 = json.dumps(update_status) params_30 = json.dumps(update_status_30) job = { "image": "python:2.7", "stage": stage_name, "before_script": [], "after_script": [ "curl -XPOST %s -d \"%s\" || true" % (url, params_30.replace('"', '\\\"')), "curl -XPOST %s -d \"%s\" || true" % (url, params_150.replace('"', '\\\"')) ], "script": [ "echo curl -XPOST %s -d \"%s\" || true" % (url, params_30.replace('"', '\\\"')), "echo curl -XPOST %s -d \"%s\" || true" % (url, params_150.replace('"', '\\\"')) ], "when": "always" } if isinstance(FAILFASTCI_REQUIRE_RUNNER_TAG, str) and \ bool(FAILFASTCI_REQUIRE_RUNNER_TAG): job['tags'] = [FAILFASTCI_REQUIRE_RUNNER_TAG] content['stages'].append(stage_name) content['report-status'] = job def _append_update_build(self, content): params = json.dumps({ "ci_project_id": "$CI_PROJECT_ID", "ci_sha": "$CI_BUILD_REF", "sha": "$SHA", "build_id": "$CI_BUILD_ID", "github_repo": "$GITHUB_REPO", "installation_id": "$GITHUB_INSTALLATION_ID", "delay": 45 }) url = FAILFASTCI_API + "/api/v1/github_status" task = "curl -m 45 --connect-timeout 45 -XPOST %s -d \"%s\" || true" % ( url, params.replace('"', '\\\"')) for key, job in content.items(): if key in GITLAB_CI_KEYS or key[0] == ".": continue if "after_script" not in job: job['after_script'] = [] if task not in job: if task not in job['after_script']: job['after_script'].append(task) def trigger_pipeline(self): gevent = self.ghevent gitlab_user = GITLAB_USER dirpath = tempfile.mkdtemp() repo_path = os.path.join(dirpath, "repo") gitbin = self._checkout_repo(gevent, repo_path) try: ci_file = self._get_ci_file(repo_path) except ResourceNotFound as e: raise Unexpected("Could not find a CI config file in: %s" % (repo_path, )) try: content = self._parse_ci_file(ci_file['content'], ci_file['file']) except YAMLComposeError as e: raise Unexpected("Could not parse CI file: %s" % (ci_file['file'], )) variables = content.get('variables', dict()) namespace = variables.get('FAILFASTCI_NAMESPACE', None) gitlab_endpoint = variables.get('GITLAB_URL', None) self.gitlab = GitlabClient(gitlab_endpoint) ci_project = self.gitlab.initialize_project( gevent.repo.replace("/", "_"), namespace) # @Todo(ant31) check if clone_url is required # clone_url = clone_url_with_auth(gevent.clone_url, "bot:%s" % self.github.token) target_url = clone_url_with_auth( ci_project['http_url_to_repo'], "%s:%s" % (gitlab_user, self.gitlab.gitlab_token)) gitbin.remote('add', 'target', target_url) variables.update({ 'EVENT': gevent.event_type, 'PR_ID': str(gevent.pr_id), 'SHA': gevent.head_sha, 'SHA8': gevent.head_sha[0:8], 'FAILFASTCI_STATUS_API': ('%s/api/v1/github_status' % (FAILFASTCI_API, )), 'SOURCE_REF': gevent.refname, 'REF_NAME': gevent.refname, 'CI_REF': gevent.target_refname, 'GITHUB_INSTALLATION_ID': str(gevent.installation_id), 'GITHUB_REPO': gevent.repo }) content['variables'] = variables self._append_update_stage(content) perform_sync = variables.get("FAILFAST_SYNC_REPO", "false") if ((perform_sync == "true") or (DEFAULT_MODE == "sync")): # Full synchronize the repo path = os.path.join(repo_path, ".gitlab-ci.yml") with open(path, 'w') as gitlabcifile: gitlabcifile.write( yaml.safe_dump(content, default_style='"', width=float("inf"))) gitbin.commit( "-a", "-m", "build %s \n\n @ %s" % (gevent.head_sha, gevent.commit_url)) gitbin.push("target", 'HEAD:%s' % gevent.target_refname, "-f") ci_sha = str(gitbin.rev_parse('HEAD')) return { # NOTE: the GitHub reference details for subsequent tasks. 'sha': gevent.head_sha, 'ci_sha': ci_sha, 'ref': gevent.refname, 'ci_ref': gevent.target_refname, 'ci_project_id': ci_project['id'], 'installation_id': gevent.installation_id, 'github_repo': gevent.repo } else: self.sync_only_ci_file(gevent, content, ci_project, ci_file) # @TODO this is a partial implem def sync_only_ci_file(self, gevent, content, ci_project, ci_file): """ Push only the .failfast-ci.yaml and set token to clone """ tokenkey = "GH_TOKEN_%s" % str.upper(uuid.uuid4().hex) clone_url = gevent.clone_url.replace("https://", "https://bot:$%s:" % tokenkey) ci_branch = gevent.refname content['variables'].update({'SOURCE_REPO': clone_url}) self.gitlab.set_variables(ci_project['id'], {tokenkey: self.github.token}) return self.gitlab.push_file(project_id=ci_project['id'], file_path=ci_file['file'], file_content=yaml.safe_dump(content), branch=ci_branch, message=gevent.commit_message)
def trigger_pipeline(self): gevent = self.ghevent gitlab_user = GITLAB_USER dirpath = tempfile.mkdtemp() repo_path = os.path.join(dirpath, "repo") gitbin = self._checkout_repo(gevent, repo_path) try: ci_file = self._get_ci_file(repo_path) except ResourceNotFound as e: raise Unexpected("Could not find a CI config file in: %s" % (repo_path, )) try: content = self._parse_ci_file(ci_file['content'], ci_file['file']) except YAMLComposeError as e: raise Unexpected("Could not parse CI file: %s" % (ci_file['file'], )) variables = content.get('variables', dict()) namespace = variables.get('FAILFASTCI_NAMESPACE', None) gitlab_endpoint = variables.get('GITLAB_URL', None) self.gitlab = GitlabClient(gitlab_endpoint) ci_project = self.gitlab.initialize_project( gevent.repo.replace("/", "_"), namespace) # @Todo(ant31) check if clone_url is required # clone_url = clone_url_with_auth(gevent.clone_url, "bot:%s" % self.github.token) target_url = clone_url_with_auth( ci_project['http_url_to_repo'], "%s:%s" % (gitlab_user, self.gitlab.gitlab_token)) gitbin.remote('add', 'target', target_url) variables.update({ 'EVENT': gevent.event_type, 'PR_ID': str(gevent.pr_id), 'SHA': gevent.head_sha, 'SHA8': gevent.head_sha[0:8], 'FAILFASTCI_STATUS_API': ('%s/api/v1/github_status' % (FAILFASTCI_API, )), 'SOURCE_REF': gevent.refname, 'REF_NAME': gevent.refname, 'CI_REF': gevent.target_refname, 'GITHUB_INSTALLATION_ID': str(gevent.installation_id), 'GITHUB_REPO': gevent.repo }) content['variables'] = variables self._append_update_stage(content) perform_sync = variables.get("FAILFAST_SYNC_REPO", "false") if ((perform_sync == "true") or (DEFAULT_MODE == "sync")): # Full synchronize the repo path = os.path.join(repo_path, ".gitlab-ci.yml") with open(path, 'w') as gitlabcifile: gitlabcifile.write( yaml.safe_dump(content, default_style='"', width=float("inf"))) gitbin.commit( "-a", "-m", "build %s \n\n @ %s" % (gevent.head_sha, gevent.commit_url)) gitbin.push("target", 'HEAD:%s' % gevent.target_refname, "-f") ci_sha = str(gitbin.rev_parse('HEAD')) return { # NOTE: the GitHub reference details for subsequent tasks. 'sha': gevent.head_sha, 'ci_sha': ci_sha, 'ref': gevent.refname, 'ci_ref': gevent.target_refname, 'ci_project_id': ci_project['id'], 'installation_id': gevent.installation_id, 'github_repo': gevent.repo } else: self.sync_only_ci_file(gevent, content, ci_project, ci_file)