Skip to content

Commit

Permalink
feat: fetch artifacts from GitHub Actions (#973)
Browse files Browse the repository at this point in the history
GitHub requires a token for artifact downloads from their API. (A
logged-in user is required for the normal download link shown on the
Actions page. This needs to be kept in mind when we update the download
comment from the bot.)

I could not find any way to get the workflow run ID or artifacts from
the check run object except parsing the URL.
  • Loading branch information
aliciaaevans authored Apr 1, 2024
1 parent eda64b5 commit 858e1cf
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 8 deletions.
49 changes: 42 additions & 7 deletions bioconda_utils/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def upload_pr_artifacts(config, repo, git_sha, dryrun=False, mulled_upload_targe
# no PR found for the commit
return UploadResult.NO_PR
pr = prs[0]
artifacts = set(fetch_artifacts(pr, artifact_source))
artifacts = set(fetch_artifacts(pr, artifact_source, repo))
if not artifacts:
# no artifacts found, fail and rebuild packages
logger.info("No artifacts found.")
Expand All @@ -54,13 +54,19 @@ def upload_pr_artifacts(config, repo, git_sha, dryrun=False, mulled_upload_targe
# download the artifact
if artifact_source == "azure":
artifact_path = os.path.join(tmpdir, os.path.basename(artifact))
download_artifact(artifact, artifact_path)
download_artifact(artifact, artifact_path, artifact_source)
zipfile.ZipFile(artifact_path).extractall(tmpdir)
elif artifact_source == "circleci":
artifact_dir = os.path.join(tmpdir, *(artifact.split("/")[-4:-1]))
artifact_path = os.path.join(tmpdir, artifact_dir, os.path.basename(artifact))
Path(artifact_dir).mkdir(parents=True, exist_ok=True)
download_artifact(artifact, artifact_path)
Path(artifact_dir).mkdir(parents=True, exist_ok=True)
download_artifact(artifact, artifact_path, artifact_source)
elif artifact_source == "github-actions":
artifact_dir = os.path.join(tmpdir, "artifacts")
artifact_path = os.path.join(artifact_dir, os.path.basename(artifact))
Path(artifact_dir).mkdir(parents=True, exist_ok=True)
download_artifact(artifact, artifact_path, artifact_source)
zipfile.ZipFile(artifact_path).extractall(artifact_dir)

# get all the contained packages and images and upload them
platform_patterns = [repodata.platform2subdir(repodata.native_platform())]
Expand Down Expand Up @@ -110,17 +116,24 @@ def upload_pr_artifacts(config, repo, git_sha, dryrun=False, mulled_upload_targe
backoff.expo,
requests.exceptions.RequestException
)
def download_artifact(url, to_path):
def download_artifact(url, to_path, artifact_source):
logger.info(f"Downloading artifact {url}.")
resp = requests.get(url, stream=True, allow_redirects=True)
headers = {}
if artifact_source == "github-actions":
token = os.environ.get("GITHUB_TOKEN")
if not token:
logger.critical("GITHUB_TOKEN required to download GitHub Actions artifacts")
exit(1)
headers = {"Authorization": f"token {token}"}
resp = requests.get(url, stream=True, allow_redirects=True, headers=headers)
resp.raise_for_status()
with open(to_path, "wb") as f:
for chunk in resp.iter_content(chunk_size=1024):
if chunk:
f.write(chunk)


def fetch_artifacts(pr, artifact_source):
def fetch_artifacts(pr, artifact_source, repo):
"""
Fetch artifacts from a PR.
Expand Down Expand Up @@ -155,6 +168,13 @@ def fetch_artifacts(pr, artifact_source):
# Circle CI builds
artifact_url = get_circleci_artifacts(check_run, platform)
yield from artifact_url
elif (
artifact_source == "github-actions" and
check_run.app.slug == "github-actions"
):
# GitHubActions builds
artifact_url = get_gha_artifacts(check_run, platform, repo)
yield from artifact_url


def get_azure_artifacts(check_run):
Expand Down Expand Up @@ -197,3 +217,18 @@ def get_circleci_artifacts(check_run, platform):
continue
else:
yield artifact_url

def parse_gha_build_id(url: str) -> str:
# Get workflow run id from URL
return re.search("runs/(\d+)/", url).group(1)

def get_gha_artifacts(check_run, platform, repo):
gha_workflow_id = parse_gha_build_id(check_run.details_url)
if (gha_workflow_id) :
# The workflow run is different from the check run
run = repo.get_workflow_run(int(gha_workflow_id))
artifacts = run.get_artifacts()
for artifact in artifacts:
# This URL is valid for 1 min and requires a token
artifact_url = artifact.archive_download_url
yield artifact_url
2 changes: 1 addition & 1 deletion bioconda_utils/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ def build(recipe_folder, config, packages="*", git_range=None, testonly=False,
@arg('--dryrun', action='store_true', help='''Do not actually upload anything.''')
@arg('--fallback', choices=['build', 'ignore'], default='build', help="What to do if no artifacts are found in the PR.")
@arg('--quay-upload-target', help="Provide a quay.io target to push docker images to.")
@arg('--artifact-source', choices=['azure', 'circleci'], default='azure', help="Application hosting build artifacts (e.g., Azure or Circle CI).")
@arg('--artifact-source', choices=['azure', 'circleci','github-actions'], default='azure', help="Application hosting build artifacts (e.g., Azure, Circle CI, or GitHub Actions).")
@enable_logging()
def handle_merged_pr(
recipe_folder,
Expand Down

0 comments on commit 858e1cf

Please sign in to comment.