From fb298bca0225193b546757980e2e923d73036819 Mon Sep 17 00:00:00 2001 From: Avi Kelman Date: Mon, 18 May 2020 14:23:11 -0400 Subject: [PATCH 1/4] :tada: New release maker CLI --- .gitignore | 35 +++ d3b_release_maker/__init__.py | 0 d3b_release_maker/cli.py | 60 ++++ d3b_release_maker/config.py | 49 ++++ d3b_release_maker/release_maker.py | 436 +++++++++++++++++++++++++++++ requirements.txt | 6 + setup.py | 21 ++ 7 files changed, 607 insertions(+) create mode 100644 .gitignore create mode 100644 d3b_release_maker/__init__.py create mode 100644 d3b_release_maker/cli.py create mode 100644 d3b_release_maker/config.py create mode 100644 d3b_release_maker/release_maker.py create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e0f820d --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Dotfiles/dotdirs +.* + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Environments +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Distribution / packaging +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +*.egg +MANIFEST + +# Sphinx documentation +docs/_build/ diff --git a/d3b_release_maker/__init__.py b/d3b_release_maker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/d3b_release_maker/cli.py b/d3b_release_maker/cli.py new file mode 100644 index 0000000..163a9ef --- /dev/null +++ b/d3b_release_maker/cli.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +import click + +from d3b_release_maker.release_maker import ( + make_release, + new_notes, +) + + +@click.group(context_settings={"help_option_names": ["-h", "--help"]}) +def cli(): + """ + Container for the cli + """ + pass + + +def options(function): + function = click.option( + "--blurb_file", + prompt="Optional markdown file containing a custom message to prepend to the notes for this release", + default="", + help="Optional markdown file containing a custom message to prepend to the notes for this release", + )(function) + function = click.option( + "--repo", + prompt="The github repository (e.g. my-organization/my-project-name)", + help="The github organization/repository to make a release for", + )(function) + return function + + +@click.command( + name="preview", short_help="Preview the changes for a new release" +) +@options +def preview_changelog_cmd(repo, blurb_file): + new_notes(repo, blurb_file) + + +@click.command(name="build", short_help="Generate a new release on GitHub") +@options +@click.option( + "--project_title", + prompt="The title of the project", + default="", + help="This will be put before the release number in the generated notes", +) +@click.option( + "--pre_release_script", + prompt="Shell script to run before pushing the release to GitHub", + default="", + help="Shell script to run before pushing the release to GitHub", +) +def make_release_cmd(repo, project_title, blurb_file, pre_release_script): + make_release(repo, project_title, blurb_file, pre_release_script) + + +cli.add_command(preview_changelog_cmd) +cli.add_command(make_release_cmd) diff --git a/d3b_release_maker/config.py b/d3b_release_maker/config.py new file mode 100644 index 0000000..6b4268f --- /dev/null +++ b/d3b_release_maker/config.py @@ -0,0 +1,49 @@ +""" +Default config values for release maker +""" + +GITHUB_API = "https://api.github.com" +GITHUB_RAW = "https://raw.githubusercontent.com" + +GH_TOKEN_VAR = "GH_TOKEN" +RELEASE_EMOJIS = "πŸ·πŸ”–" +EMOJI_CATEGORIES = { + "Additions": {"✨", "πŸŽ‰", "πŸ“ˆ", "βž•", "🌐", "πŸ”€", "πŸ”Š"}, + "Documentation": {"πŸ’‘", "πŸ“"}, + "Removals": {"πŸ”₯", "βž–", "βͺ", "πŸ”‡", "πŸ—‘"}, + "Fixes": { + "πŸ›", + "πŸš‘", + "πŸ”’", + "🍎", + "🐧", + "🏁", + "πŸ€–", + "🍏", + "🚨", + "✏️", + "πŸ‘½", + "πŸ‘Œ", + "♿️", + "πŸ’¬", + "🚸", + "πŸ₯…", + }, + "Ops": { + "πŸš€", + "πŸ’š", + "⬇️", + "⬆️", + "πŸ“Œ", + "πŸ‘·", + "🐳", + "πŸ“¦", + "πŸ‘₯", + "πŸ™ˆ", + "πŸ“Έ", + "☸️", + "🌱", + "🚩", + }, +} +OTHER_CATEGORY = "Other Changes" diff --git a/d3b_release_maker/release_maker.py b/d3b_release_maker/release_maker.py new file mode 100644 index 0000000..a4f1b43 --- /dev/null +++ b/d3b_release_maker/release_maker.py @@ -0,0 +1,436 @@ +import os +import shutil +import stat +import subprocess +import tempfile +import time +from collections import defaultdict +from datetime import datetime + +import emoji +import regex +import semver +from d3b_utils.requests_retry import Session +from github import Github +from github.GithubException import GithubException, UnknownObjectException + +from d3b_release_maker import config + +GH_API = config.GITHUB_API +GH_RAW = config.GITHUB_RAW + +CHANGEFILE = "CHANGELOG.md" + +MAJOR = "major" +MINOR = "minor" +PATCH = "patch" +RELEASE_OPTIONS = [MAJOR, MINOR, PATCH] + +release_pattern = r"\s*[" + config.RELEASE_EMOJIS + r"]\s*[Rr]elease" +emoji_categories = { + e: category + for category, emoji_set in config.EMOJI_CATEGORIES.items() + for e in emoji_set +} + + +def split_at_pattern(text, pattern): + """ + Split a string where a pattern begins + """ + start = regex.search(pattern, text).start() + return text[0:start], text[start:] + + +def delay_until(datetime_of_reset): + wait_time = int((datetime_of_reset - datetime.now()).total_seconds() + 5.5) + timestr = ( + lambda wait_time: f"{(wait_time//60)} minutes, {wait_time%60} seconds" + ) + print(f"Backing off for {timestr(wait_time)}.") + print( + "If you don't want to wait that long right now, feel free to kill this process." + ) + while wait_time > 0: + time.sleep(5) + wait_time -= 5 + print(f"{timestr(wait_time)} remaining...") + + +class GitHubSession(Session): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.headers.update({"Accept": "application/vnd.github.v3.raw+json"}) + gh_token = os.getenv(config.GH_TOKEN_VAR) + if gh_token: + self.headers.update({"Authorization": "token " + gh_token}) + + def get(self, url, **request_kwargs): + """ + If response.status_code is not 200 then exit program + Otherwise return original response + """ + while True: + response = super().get(url, **request_kwargs) + if response.status_code != 200: + if response.status_code == 404: + raise UnknownObjectException( + response.status_code, response.url + ) + elif response.headers.get("X-Ratelimit-Remaining") == "0": + print( + response.json().get("message"), + "<--- https://developer.github.com/v3/#rate-limiting", + ) + datetime_of_reset = datetime.fromtimestamp( + int(response.headers["X-Ratelimit-Reset"]) + ) + delay_until(datetime_of_reset) + else: + raise GithubException( + response.status_code, + f"Could not fetch {response.url}! Caused by: {response.text}", + ) + else: + break + + return response + + def yield_paginated(self, endpoint, query_params): + """ + Yield from paginated endpoint + """ + query_params.update({"page": 1, "per_page": 100}) + items = True + while items: + items = self.get(endpoint, params=query_params).json() + yield from items + query_params["page"] += 1 + + +class GitHubReleaseNotes: + def __init__(self): + self.session = GitHubSession() + + def _starting_emojis(self, title): + """ + Detect emojis at the start of a PR title (and fix malformed titles) + """ + emojis = set() + graphemes = regex.findall(r"\X", title) + for i, g in enumerate(graphemes): + if any(char in emoji.UNICODE_EMOJI for char in g): + emojis.add(g) + else: # stop after first non-hit + if g != " ": + # fix missing space in malformed titles + title = ( + "".join(graphemes[:i]) + " " + "".join(graphemes[i:]) + ) + break + + return (emojis or {"?"}, title) + + def _get_merged_prs(self, after): + """ + Get all non-release PRs merged into master after the given time + """ + print("Fetching PRs ...") + endpoint = f"{self.base_url}/pulls" + query_params = {"base": "master", "state": "closed"} + prs = [] + for p in self.session.yield_paginated(endpoint, query_params): + if p["merged_at"]: + if p["merged_at"] < after: + break + elif regex.search(release_pattern, p["title"]) is None: + prs.append(p) + return prs + + def _get_commit_date(self, commit_url): + """ + Get date of commit at commit_url + """ + commit = self.session.get(commit_url).json() + return commit["commit"]["committer"]["date"] + + def _get_last_tag(self): + """ + Get latest tag and when it was committed + """ + tags = self.session.get(f"{self.base_url}/tags").json() + + # Get latest commit of last tagged release + if len(tags) > 0: + for t in tags: + try: + prefix, version = split_at_pattern(t["name"], r"\d") + # Raise on non-semver tags so we can skip them + semver.VersionInfo.parse(version) + return { + "name": t["name"], + "date": self._get_commit_date(t["commit"]["url"]), + "commit_sha": t["commit"]["sha"], + } + except ValueError: + pass + else: + return None + + def _next_release_version(self, prev_version, release_type): + """ + Get next release version based on prev version using semver format + """ + prev_version = semver.VersionInfo.parse(prev_version).finalize_version() + if release_type == MAJOR: + new_version = prev_version.bump_major() + elif release_type == MINOR: + new_version = prev_version.bump_minor() + elif release_type == PATCH: + new_version = prev_version.bump_patch() + else: + raise ValueError( + f"Invalid release type: {release_type}! Release type " + f"must be one of {RELEASE_OPTIONS}!" + ) + + return str(new_version) + + def _to_markdown(self, repo, counts, prs): + """ + Converts accumulated information about the project into markdown + """ + messages = [ + "### Summary", + "", + "- Emojis: " + + ", ".join(f"{k} x{v}" for k, v in counts["emojis"].items()), + "- Categories: " + + ", ".join(f"{k} x{v}" for k, v in counts["categories"].items()), + "", + "### New features and changes", + "", + ] + + for p in prs: + userlink = f"[{p['user']['login']}]({p['user']['html_url']})" + sha_link = f"[{p['merge_commit_sha'][:8]}](https://github.com/{repo}/commit/{p['merge_commit_sha']})" + pr_link = f"[#{p['number']}]({p['html_url']})" + messages.append( + f"- {pr_link} - {p['title']} - {sha_link} by {userlink}" + ) + + return "\n".join(messages) + + def build_release_notes(self, repo, blurb=None): + """ + Make release notes + """ + print("\nBegin making release notes ...") + + # Set up session + self.base_url = f"{GH_API}/repos/{repo}" + + # Get tag of last release + print("Fetching latest tag ...") + latest_tag = self._get_last_tag() + + if latest_tag: + print(f"Latest tag: {latest_tag}") + else: + print("No tags found") + latest_tag = {"name": "0.0.0", "date": ""} + + # Get all non-release PRs that were merged into master after the last release + prs = self._get_merged_prs(latest_tag["date"]) + + # Count the emojis and fix missing spaces in titles + counts = {"emojis": defaultdict(int), "categories": defaultdict(int)} + for p in prs: + emojis, p["title"] = self._starting_emojis(p["title"].strip()) + for e in emojis: + counts["emojis"][e] += 1 + counts["categories"][ + emoji_categories.get(e, config.OTHER_CATEGORY) + ] += 1 + + # Compose markdown + markdown = self._to_markdown(repo, counts, prs) + if blurb: + markdown = f"{blurb}\n\n" + markdown + + print("=" * 32 + "BEGIN DELTA" + "=" * 32) + print(markdown) + print("=" * 33 + "END DELTA" + "=" * 33) + + while True: + release_type = input( + f"What type of semantic versioning release is this {RELEASE_OPTIONS}? " + ).lower() + if release_type in RELEASE_OPTIONS: + break + else: + print(f"'{release_type}' is not one of {RELEASE_OPTIONS}") + + # Update release version + prefix, prev_version = split_at_pattern(latest_tag["name"], r"\d") + version = prefix + self._next_release_version( + prev_version, release_type + ) + markdown = f"## Release {version}\n\n" + markdown + + print(f"Previous version: {prev_version}") + print(f"New version: {version}") + + return version, markdown + + +def new_notes(repo, blurb_file): + """ + Build notes for new changes + """ + blurb = None + if blurb_file: + with open(blurb_file, "r") as bf: + blurb = bf.read().strip() + + return GitHubReleaseNotes().build_release_notes(repo=repo, blurb=blurb) + + +def new_changelog(repo, blurb_file): + """ + Creates release notes markdown containing: + - The next release version number + - A changelog of Pull Requests merged into master since the last release + - Emoji and category summaries for Pull Requests in the release + + Then merges that into the existing changelog. + """ + + # Build notes for new changes + + new_version, new_markdown = new_notes(repo, blurb_file) + + if new_version not in new_markdown.partition("\n")[0]: + print( + f"New version '{new_version}' not in release title of new markdown." + ) + return None, None, None + + # Load previous changelog file + + session = GitHubSession() + try: + prev_markdown = session.get(f"{GH_RAW}/{repo}/master/{CHANGEFILE}").text + except UnknownObjectException: + prev_markdown = "" + + # Remove previous title line if not specific to a particular release + + if "\n" in prev_markdown: + prev_title, prev_markdown = prev_markdown.split("\n", 1) + if regex.search(r"[Rr]elease .*\d+\.\d+\.\d+", prev_title): + prev_markdown = "\n".join([prev_title, prev_markdown]) + + # Update changelog with new release notes + + if new_version in prev_markdown.partition("\n")[0]: + print(f"\nNew version '{new_version}' already in {CHANGEFILE}.") + return None, None, None + else: + changelog = "\n\n".join([new_markdown, prev_markdown]).rstrip() + return new_version, new_markdown, changelog + + +def make_release(repo, project_title, blurb_file, pre_release_script): + """ + Generate a new changelog, run the script, and then make a PR on GitHub + """ + gh_token = os.getenv(config.GH_TOKEN_VAR) + + new_version, new_markdown, changelog = new_changelog(repo, blurb_file) + + if changelog: + # Attach project header + changelog = f"# {project_title} Change History\n\n{changelog}" + + # Freshly clone repo + tmp = os.path.join(tempfile.gettempdir(), "release_maker") + shutil.rmtree(tmp, ignore_errors=True) + print(f"Cloning https://github.com/{repo}.git to {tmp} ...") + subprocess.run( + [ + "git", + "clone", + "--depth", + "1", + f"https://{gh_token}@github.com/{repo}.git", + tmp, + ], + check=True, + capture_output=True, + ) + os.chdir(tmp) + + print("Writing updated changelog file ...") + with open(CHANGEFILE, "w") as cl: + cl.write(changelog) + + if pre_release_script: + print(f"Executing pre-release script {pre_release_script} ...") + mode = os.stat(pre_release_script).st_mode + os.chmod(pre_release_script, mode | stat.S_IXUSR) + subprocess.run( + [pre_release_script, new_version], + check=True, + capture_output=True, + ) + + # Create and push new release branch + release_branch_name = f"πŸ”–-release-{new_version}" + print(f"Submitting release branch {release_branch_name} ...") + subprocess.run( + ["git", "checkout", "-b", release_branch_name], + check=True, + capture_output=True, + ) + subprocess.run(["git", "add", "-A"], check=True, capture_output=True) + subprocess.run( + [ + "git", + "commit", + "-m", + f":bookmark: Release {new_version}\n\n{new_markdown}", + ], + check=True, + capture_output=True, + ) + subprocess.run( + ["git", "push", "--force", "origin", release_branch_name], + check=True, + capture_output=True, + ) + + # Create GitHub Pull Request + print("Submitting PR for release ...") + gh_repo = Github(gh_token, base_url=GH_API).get_repo(repo) + pr_title = f"πŸ”– Release {new_version}" + pr_url = None + for p in gh_repo.get_pulls(state="open", base="master"): + if p.title == pr_title: + pr_url = p.html_url + break + + if pr_url: + print(f"Updated release PR: {pr_url}") + else: + pr = gh_repo.create_pull( + title=pr_title, + body=new_markdown, + head=release_branch_name, + base="master", + ) + pr.add_to_labels("release") + print(f"Created release PR: {pr.html_url}") + else: + print("Doing nothing.") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0de4659 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +Click==7.0 +d3b_utils @ git+https://git@github.com/d3b-center/d3b-utils-python.git +emoji +regex +semver +PyGithub diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0fd3e0f --- /dev/null +++ b/setup.py @@ -0,0 +1,21 @@ +import os +from setuptools import setup, find_packages + +root_dir = os.path.dirname(os.path.abspath(__file__)) +req_file = os.path.join(root_dir, "requirements.txt") +with open(req_file) as f: + requirements = f.read().splitlines() + +setup( + name="d3b-release-maker", + version="1.0.0", + description="D3b software release authoring tool", + author=( + "Center for Data Driven Discovery in Biomedicine at the" + " Children's Hospital of Philadelphia" + ), + packages=find_packages(), + entry_points={"console_scripts": ["release=d3b_release_maker.cli:cli"]}, + include_package_data=True, + install_requires=requirements, +) From e601b83f46d21ae83a9c76d7dc97239d04726825 Mon Sep 17 00:00:00 2001 From: Avi Kelman Date: Mon, 18 May 2020 18:25:13 -0400 Subject: [PATCH 2/4] :sparkles: Autoreleasing GH Action --- github_actions_workflow/gh_release.yml | 59 ++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 github_actions_workflow/gh_release.yml diff --git a/github_actions_workflow/gh_release.yml b/github_actions_workflow/gh_release.yml new file mode 100644 index 0000000..9d86828 --- /dev/null +++ b/github_actions_workflow/gh_release.yml @@ -0,0 +1,59 @@ +# This workflow will load Python, run a script to generate assets, and then +# bundle a github release + +name: Release generator + +on: + pull_request: + types: + - closed + +jobs: + create_release: + if: github.base_ref == 'master' && github.event.pull_request.merged && contains( github.event.pull_request.labels.*.name, 'release') + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@v2 + + - name: Set up Python 3.8 + uses: actions/setup-python@v1 + with: + python-version: 3.8 + + - name: Create tag from title and run asset script + id: find_tag_and_prepare_assets + run: | + TAG=$(echo ${{ github.event.pull_request.title }} | sed -E "s/^.*Release (.+\..+\..+)$/\1/g") + echo "::set-output name=tag::$TAG" + + SCRIPT=.github/prepare_assets.sh + if [ -f $SCRIPT ]; then + chmod u+x $SCRIPT + $SCRIPT $TAG + fi + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.find_tag_and_prepare_assets.outputs.tag }} + release_name: ${{ github.event.pull_request.title }} + body: ${{ github.event.pull_request.body }} + draft: false + prerelease: false + + - name: Upload Assets + run: | + upload_url=${{ steps.create_release.outputs.upload_url }} + if [ -f .github/release_assets.txt ]; then + while IFS="" read -r FILE || [ -n "$FILE" ] + do + curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + -H "Content-Type: $(file -b --mime-type $FILE)" \ + --data-binary "@$FILE" \ + "${upload_url%\{*}?name=$(basename $FILE)" + done < .github/release_assets.txt + fi From 6004da94dcce57a13ebfd72cef4c15893ce3e819 Mon Sep 17 00:00:00 2001 From: Avi Kelman Date: Mon, 18 May 2020 18:40:22 -0400 Subject: [PATCH 3/4] :pencil: Populate readme --- .../workflows}/gh_release.yml | 0 README.md | 67 ++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) rename {github_actions_workflow => .github/workflows}/gh_release.yml (100%) diff --git a/github_actions_workflow/gh_release.yml b/.github/workflows/gh_release.yml similarity index 100% rename from github_actions_workflow/gh_release.yml rename to .github/workflows/gh_release.yml diff --git a/README.md b/README.md index c57640a..0d63af0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,65 @@ -# d3b-release-maker -Tool to generate GitHub-based software releases +# D3b Release Maker + +Tool to automate GitHub-based software releases for projects with the following +characteristics: + +- Feature branching with merge-commits into master +- Gitmoji-style PR messages +- Semantic versioning tags for releases () + +## Part 1: CLI that generates release notes and creates a PR on GitHub for review + +Install with pip: +`pip install git+https://github.com/d3b-center/d3b-release-maker.git` + +Create a temporary auth token on GitHub and add it to your environment as +`GH_TOKEN`. + +Then run: `release --help` + +## Part 2: GitHub Actions Workflow that automates tagging releases and asset uploading + +Copy `gh_releases.yml` from this repository's `.github/workflows/` directory +into `.github/workflows/` in your project repository. (Read more about GitHub +Actions and Workflows at ) + +If you want to attach binary assets to your GitHub releases, add a +`.github/prepare_assets.sh` script that receives one input argument with the +new release version and generates your assets and then creates +`.github/release_assets.txt` containing a list of the files you want to upload, +one per line, in the order that you want them to appear. + +## What the parts do + +### What the CLI does + +When you run the `release build` command: + +1. The CLI looks at the most recent git tag that looks like a Semantic Version. +2. Then it looks for all PRs that were merged into master after that tag which + do not themselves look like a release merge. +3. Emojis at the start of the PR titles are grouped and counted according to + the gitmoji emoji guide (). +4. Markdown is generated with links to PRs, merge commits, and committers and is + shown to the user. +5. The user is prompted to provide a type of version update (major, minor, + patch) based on the shown list of changes, and a new version number is + generated. +6. A fresh copy of your repository is cloned to a temporary location. +7. The new changes list is added to the CHANGELOG.md file (one is created if + one doesn't already exist). +8. An optional user-provided script is then run if you need to e.g. add the new + version number to files in your repository. +9. All newly modified files are commited with a special release commit and + pushed to a special release branch, and a Pull Request into master is opened + on GitHub for review. + +### What the GitHub Actions Workflow does + +When a special release branch is merged into master: + +1. Your repository is tagged with the new semantic version, and a GitHub + release is created with the contents of the just-merged Pull Request body. +2. If a `.github/prepare_assets.sh` script exists, it is run. +3. If a `.github/release_assets.txt` file exists, any files listed in it are + then uploaded to the GitHub release. From d07ececa81823c345b83c330da08108035329c8c Mon Sep 17 00:00:00 2001 From: Avi Kelman Date: Mon, 1 Jun 2020 12:41:37 -0400 Subject: [PATCH 4/4] :wrench: Use scm tag for python package version --- requirements.txt | 1 + setup.py | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0de4659..1d4d8f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +setuptools_scm Click==7.0 d3b_utils @ git+https://git@github.com/d3b-center/d3b-utils-python.git emoji diff --git a/setup.py b/setup.py index 0fd3e0f..120d295 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ import os -from setuptools import setup, find_packages + +from setuptools import find_packages, setup root_dir = os.path.dirname(os.path.abspath(__file__)) req_file = os.path.join(root_dir, "requirements.txt") @@ -8,7 +9,11 @@ setup( name="d3b-release-maker", - version="1.0.0", + use_scm_version={ + "local_scheme": "dirty-tag", + "version_scheme": "post-release", + }, + setup_requires=["setuptools_scm"], description="D3b software release authoring tool", author=( "Center for Data Driven Discovery in Biomedicine at the"