spack reproduce-build: accept URLs from web interface (#42261)

Sometimes the logs are too long and the copy & paste command is not
shown. In that case I'd like to just copy the failing GitLab job URL in
my browser to `spack reproduce-build <url>`.
This commit is contained in:
Harmen Stoppels 2024-01-31 15:58:51 +01:00 committed by GitHub
parent 5c49bb45c7
commit f27bff81ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 83 additions and 7 deletions

View File

@ -6,6 +6,7 @@
import json import json
import os import os
import shutil import shutil
from urllib.parse import urlparse, urlunparse
import llnl.util.filesystem as fs import llnl.util.filesystem as fs
import llnl.util.tty as tty import llnl.util.tty as tty
@ -157,7 +158,9 @@ def setup_parser(subparser):
description=deindent(ci_reproduce.__doc__), description=deindent(ci_reproduce.__doc__),
help=spack.cmd.first_line(ci_reproduce.__doc__), help=spack.cmd.first_line(ci_reproduce.__doc__),
) )
reproduce.add_argument("job_url", help="URL of job artifacts bundle") reproduce.add_argument(
"job_url", help="URL of GitLab job web page or artifact", type=_gitlab_artifacts_url
)
reproduce.add_argument( reproduce.add_argument(
"--runtime", "--runtime",
help="Container runtime to use.", help="Container runtime to use.",
@ -792,11 +795,6 @@ def ci_reproduce(args):
artifacts of the provided gitlab pipeline rebuild job's URL will be used to derive artifacts of the provided gitlab pipeline rebuild job's URL will be used to derive
instructions for reproducing the build locally instructions for reproducing the build locally
""" """
job_url = args.job_url
work_dir = args.working_dir
autostart = args.autostart
runtime = args.runtime
# Allow passing GPG key for reprocuding protected CI jobs # Allow passing GPG key for reprocuding protected CI jobs
if args.gpg_file: if args.gpg_file:
gpg_key_url = url_util.path_to_file_url(args.gpg_file) gpg_key_url = url_util.path_to_file_url(args.gpg_file)
@ -805,7 +803,47 @@ def ci_reproduce(args):
else: else:
gpg_key_url = None gpg_key_url = None
return spack_ci.reproduce_ci_job(job_url, work_dir, autostart, gpg_key_url, runtime) return spack_ci.reproduce_ci_job(
args.job_url, args.working_dir, args.autostart, gpg_key_url, args.runtime
)
def _gitlab_artifacts_url(url: str) -> str:
"""Take a URL either to the URL of the job in the GitLab UI, or to the artifacts zip file,
and output the URL to the artifacts zip file."""
parsed = urlparse(url)
if not parsed.scheme or not parsed.netloc:
raise ValueError(url)
parts = parsed.path.split("/")
if len(parts) < 2:
raise ValueError(url)
# Just use API endpoints verbatim, they're probably generated by Spack.
if parts[1] == "api":
return url
# If it's a URL to the job in the Gitlab UI, we may need to append the artifacts path.
minus_idx = parts.index("-")
# Remove repeated slashes in the remainder
rest = [p for p in parts[minus_idx + 1 :] if p]
# Now the format is jobs/X or jobs/X/artifacts/download
if len(rest) < 2 or rest[0] != "jobs":
raise ValueError(url)
if len(rest) == 2:
# replace jobs/X with jobs/X/artifacts/download
rest.extend(("artifacts", "download"))
# Replace the parts and unparse.
parts[minus_idx + 1 :] = rest
# Don't allow fragments / queries
return urlunparse(parsed._replace(path="/".join(parts), fragment="", query=""))
def ci(parser, args): def ci(parser, args):

View File

@ -16,6 +16,7 @@
import spack import spack
import spack.binary_distribution import spack.binary_distribution
import spack.ci as ci import spack.ci as ci
import spack.cmd.ci
import spack.config import spack.config
import spack.environment as ev import spack.environment as ev
import spack.hash_types as ht import spack.hash_types as ht
@ -2028,6 +2029,43 @@ def fake_download_and_extract_artifacts(url, work_dir):
assert expect_out in rep_out assert expect_out in rep_out
@pytest.mark.parametrize(
"url_in,url_out",
[
(
"https://example.com/api/v4/projects/1/jobs/2/artifacts",
"https://example.com/api/v4/projects/1/jobs/2/artifacts",
),
(
"https://example.com/spack/spack/-/jobs/123456/artifacts/download",
"https://example.com/spack/spack/-/jobs/123456/artifacts/download",
),
(
"https://example.com/spack/spack/-/jobs/123456",
"https://example.com/spack/spack/-/jobs/123456/artifacts/download",
),
(
"https://example.com/spack/spack/-/jobs/////123456////?x=y#z",
"https://example.com/spack/spack/-/jobs/123456/artifacts/download",
),
],
)
def test_reproduce_build_url_validation(url_in, url_out):
assert spack.cmd.ci._gitlab_artifacts_url(url_in) == url_out
def test_reproduce_build_url_validation_fails():
"""Wrong URLs should cause an exception"""
with pytest.raises(SystemExit):
ci_cmd("reproduce-build", "example.com/spack/spack/-/jobs/123456/artifacts/download")
with pytest.raises(SystemExit):
ci_cmd("reproduce-build", "https://example.com/spack/spack/-/issues")
with pytest.raises(SystemExit):
ci_cmd("reproduce-build", "https://example.com/spack/spack/-")
@pytest.mark.parametrize( @pytest.mark.parametrize(
"subcmd", [(""), ("generate"), ("rebuild-index"), ("rebuild"), ("reproduce-build")] "subcmd", [(""), ("generate"), ("rebuild-index"), ("rebuild"), ("reproduce-build")]
) )