Skip to content

Commit 7c5dbae

Browse files
committed
Use list-based remote command builders
Refactor SSH command helpers to accept command argument lists and join them at the execution boundary with shlex.join(). Keep the two intentional wildcard operations as static sh -c snippets with dynamic paths passed as positional arguments, so glob expansion remains explicit without quoting values at each call site. Validation: tox -q -e py313 -- -k 'run_add_to_python_dot_org_quotes_remote_environment or upload_files_to_server_quotes_remote_cleanup_path or release_file_placement_quotes_remote_paths or remote_upload_commands_quote_url_derived_paths'; tox -q -e py313 -- tests/test_run_release.py tests/test_windows_merge_upload.py; tox -q -e mypy; tox -q -e lint.
1 parent 2753604 commit 7c5dbae

3 files changed

Lines changed: 144 additions & 68 deletions

File tree

run_release.py

Lines changed: 77 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@
4646
DOCS_SERVER = "docs.nyc1.psf.io"
4747

4848

49-
def quote_remote_shell(value: object) -> str:
50-
return shlex.quote(str(value))
49+
def join_remote_command(command: list[object]) -> str:
50+
return shlex.join(str(argument) for argument in command)
5151

5252

5353
WHATS_NEW_TEMPLATE = """
@@ -741,7 +741,7 @@ def upload_files_to_server(db: ReleaseShelf, server: str) -> None:
741741
ftp_client = MySFTPClient.from_transport(transport)
742742
assert ftp_client is not None, f"SFTP client to {server} is None"
743743

744-
client.exec_command(f"rm -rf {quote_remote_shell(destination)}")
744+
client.exec_command(join_remote_command(["rm", "-rf", destination]))
745745

746746
with contextlib.suppress(OSError):
747747
ftp_client.mkdir(str(destination))
@@ -789,37 +789,77 @@ def place_files_in_download_folder(db: ReleaseShelf) -> None:
789789
source = f"/home/psf-users/{db['ssh_user']}/{db['release']}"
790790
destination = f"/srv/www.python.org/ftp/python/{db['release'].normalized()}"
791791

792-
def execute_command(command: str) -> None:
792+
def execute_command(command: list[object]) -> None:
793793
channel = transport.open_session()
794-
channel.exec_command(command)
794+
channel.exec_command(join_remote_command(command))
795795
if channel.recv_exit_status() != 0:
796796
raise ReleaseException(channel.recv_stderr(1000))
797797

798-
def copy_and_set_permissions(source_glob: str, destination: str) -> None:
799-
quoted_destination = quote_remote_shell(destination)
800-
execute_command(f"mkdir -p {quoted_destination}")
801-
execute_command(f"cp {source_glob} {quoted_destination}")
798+
def copy_and_set_permissions(source: str, destination: str) -> None:
799+
execute_command(["mkdir", "-p", destination])
800+
execute_command(["sh", "-c", 'cp "$1"/* "$2"', "sh", source, destination])
802801
# Skip chgrp/chmod if already correct: another RM may have created
803802
# the directory, and only the owner can change group or permissions.
804803
execute_command(
805-
f"find {quoted_destination} -maxdepth 0 ! -group downloads "
806-
f"-exec chgrp downloads {{}} +"
804+
[
805+
"find",
806+
destination,
807+
"-maxdepth",
808+
"0",
809+
"!",
810+
"-group",
811+
"downloads",
812+
"-exec",
813+
"chgrp",
814+
"downloads",
815+
"{}",
816+
"+",
817+
]
807818
)
808819
execute_command(
809-
f"find {quoted_destination} -maxdepth 0 ! -perm 775 -exec chmod 775 {{}} +"
820+
[
821+
"find",
822+
destination,
823+
"-maxdepth",
824+
"0",
825+
"!",
826+
"-perm",
827+
"775",
828+
"-exec",
829+
"chmod",
830+
"775",
831+
"{}",
832+
"+",
833+
]
810834
)
811835
execute_command(
812-
f"find {quoted_destination} -maxdepth 1 -type f -user $USER "
813-
f"! -perm 664 -exec chmod 664 {{}} +"
836+
[
837+
"find",
838+
destination,
839+
"-maxdepth",
840+
"1",
841+
"-type",
842+
"f",
843+
"-user",
844+
db["ssh_user"],
845+
"!",
846+
"-perm",
847+
"664",
848+
"-exec",
849+
"chmod",
850+
"664",
851+
"{}",
852+
"+",
853+
]
814854
)
815855

816-
copy_and_set_permissions(f"{quote_remote_shell(source)}/downloads/*", destination)
856+
copy_and_set_permissions(f"{source}/downloads", destination)
817857

818858
# Docs
819859
release_tag = db["release"]
820860
if release_tag.is_final or release_tag.is_release_candidate:
821861
copy_and_set_permissions(
822-
f"{quote_remote_shell(source)}/docs/*",
862+
f"{source}/docs",
823863
f"/srv/www.python.org/ftp/python/doc/{release_tag}",
824864
)
825865

@@ -851,27 +891,31 @@ def unpack_docs_in_the_docs_server(db: ReleaseShelf) -> None:
851891
source = f"/home/psf-users/{db['ssh_user']}/{db['release']}"
852892
destination = f"/srv/docs.python.org/release/{release_tag}"
853893

854-
def execute_command(command: str) -> None:
894+
def execute_command(command: list[object]) -> None:
855895
channel = transport.open_session()
856-
channel.exec_command(command)
896+
channel.exec_command(join_remote_command(command))
857897
if channel.recv_exit_status() != 0:
858898
raise ReleaseException(channel.recv_stderr(1000))
859899

860900
docs_filename = f"python-{release_tag}-docs-html"
861-
quoted_destination = quote_remote_shell(destination)
862-
execute_command(f"mkdir -p {quoted_destination}")
901+
execute_command(["mkdir", "-p", destination])
902+
execute_command(["unzip", f"{source}/docs/{docs_filename}.zip", "-d", destination])
863903
execute_command(
864-
f"unzip {quote_remote_shell(f'{source}/docs/{docs_filename}.zip')} "
865-
f"-d {quoted_destination}"
904+
[
905+
"sh",
906+
"-c",
907+
'mv "$1"/* "$2"',
908+
"sh",
909+
f"/{destination}/{docs_filename}",
910+
destination,
911+
]
866912
)
913+
execute_command(["rm", "-rf", f"/{destination}/{docs_filename}"])
914+
execute_command(["chgrp", "-R", "docs", destination])
915+
execute_command(["chmod", "-R", "775", destination])
867916
execute_command(
868-
f"mv {quote_remote_shell(f'/{destination}/{docs_filename}')}/* "
869-
f"{quoted_destination}"
917+
["find", destination, "-type", "f", "-exec", "chmod", "664", "{}", ";"]
870918
)
871-
execute_command(f"rm -rf {quote_remote_shell(f'/{destination}/{docs_filename}')}")
872-
execute_command(f"chgrp -R docs {quoted_destination}")
873-
execute_command(f"chmod -R 775 {quoted_destination}")
874-
execute_command(f"find {quoted_destination} -type f -exec chmod 664 {{}} \\;")
875919

876920

877921
@functools.cache
@@ -1089,13 +1133,14 @@ def run_add_to_python_dot_org(db: ReleaseShelf) -> None:
10891133
identity_token = str(issuer.identity_token())
10901134

10911135
print("Adding files to python.org...")
1092-
command = " ".join(
1136+
command = join_remote_command(
10931137
[
1094-
f"AUTH_INFO={quote_remote_shell(auth_info)}",
1095-
f"SIGSTORE_IDENTITY_TOKEN={quote_remote_shell(identity_token)}",
1138+
"env",
1139+
f"AUTH_INFO={auth_info}",
1140+
f"SIGSTORE_IDENTITY_TOKEN={identity_token}",
10961141
"python3",
10971142
"add_to_pydotorg.py",
1098-
quote_remote_shell(db["release"]),
1143+
db["release"],
10991144
]
11001145
)
11011146
stdin, stdout, stderr = client.exec_command(command)

tests/test_run_release.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -302,8 +302,8 @@ def identity_token(self) -> str:
302302
run_release.run_add_to_python_dot_org(cast(ReleaseShelf, db))
303303

304304
assert commands == [
305-
"AUTH_INFO='user:key; echo pwned' "
306-
"SIGSTORE_IDENTITY_TOKEN='token; touch /tmp/pwned' "
305+
"env 'AUTH_INFO=user:key; echo pwned' "
306+
"'SIGSTORE_IDENTITY_TOKEN=token; touch /tmp/pwned' "
307307
"python3 add_to_pydotorg.py 3.15.0a1"
308308
]
309309

@@ -414,30 +414,34 @@ def get_transport(self) -> FakeTransport:
414414

415415
assert commands == [
416416
"mkdir -p /srv/www.python.org/ftp/python/3.15.0",
417-
"cp '/home/psf-users/release-manager; touch /tmp/pwned #/3.15.0rc1'/downloads/* "
417+
'sh -c \'cp "$1"/* "$2"\' sh '
418+
"'/home/psf-users/release-manager; touch /tmp/pwned #/3.15.0rc1/downloads' "
418419
"/srv/www.python.org/ftp/python/3.15.0",
419-
"find /srv/www.python.org/ftp/python/3.15.0 -maxdepth 0 ! -group downloads "
420-
"-exec chgrp downloads {} +",
421-
"find /srv/www.python.org/ftp/python/3.15.0 -maxdepth 0 ! -perm 775 "
422-
"-exec chmod 775 {} +",
423-
"find /srv/www.python.org/ftp/python/3.15.0 -type f ! -perm 664 "
424-
"-exec chmod 664 {} +",
420+
"find /srv/www.python.org/ftp/python/3.15.0 -maxdepth 0 '!' "
421+
"-group downloads -exec chgrp downloads '{}' +",
422+
"find /srv/www.python.org/ftp/python/3.15.0 -maxdepth 0 '!' -perm 775 "
423+
"-exec chmod 775 '{}' +",
424+
"find /srv/www.python.org/ftp/python/3.15.0 -maxdepth 1 -type f -user "
425+
"'release-manager; touch /tmp/pwned #' '!' -perm 664 -exec chmod 664 '{}' +",
425426
"mkdir -p /srv/www.python.org/ftp/python/doc/3.15.0rc1",
426-
"cp '/home/psf-users/release-manager; touch /tmp/pwned #/3.15.0rc1'/docs/* "
427+
'sh -c \'cp "$1"/* "$2"\' sh '
428+
"'/home/psf-users/release-manager; touch /tmp/pwned #/3.15.0rc1/docs' "
427429
"/srv/www.python.org/ftp/python/doc/3.15.0rc1",
428-
"find /srv/www.python.org/ftp/python/doc/3.15.0rc1 -maxdepth 0 ! -group downloads "
429-
"-exec chgrp downloads {} +",
430-
"find /srv/www.python.org/ftp/python/doc/3.15.0rc1 -maxdepth 0 ! -perm 775 "
431-
"-exec chmod 775 {} +",
432-
"find /srv/www.python.org/ftp/python/doc/3.15.0rc1 -type f ! -perm 664 "
433-
"-exec chmod 664 {} +",
430+
"find /srv/www.python.org/ftp/python/doc/3.15.0rc1 -maxdepth 0 '!' "
431+
"-group downloads -exec chgrp downloads '{}' +",
432+
"find /srv/www.python.org/ftp/python/doc/3.15.0rc1 -maxdepth 0 '!' "
433+
"-perm 775 -exec chmod 775 '{}' +",
434+
"find /srv/www.python.org/ftp/python/doc/3.15.0rc1 -maxdepth 1 -type f "
435+
"-user 'release-manager; touch /tmp/pwned #' '!' -perm 664 "
436+
"-exec chmod 664 '{}' +",
434437
"mkdir -p /srv/docs.python.org/release/3.15.0rc1",
435438
"unzip '/home/psf-users/release-manager; touch /tmp/pwned #/3.15.0rc1/docs/"
436439
"python-3.15.0rc1-docs-html.zip' -d /srv/docs.python.org/release/3.15.0rc1",
437-
"mv //srv/docs.python.org/release/3.15.0rc1/python-3.15.0rc1-docs-html/* "
440+
'sh -c \'mv "$1"/* "$2"\' sh '
441+
"//srv/docs.python.org/release/3.15.0rc1/python-3.15.0rc1-docs-html "
438442
"/srv/docs.python.org/release/3.15.0rc1",
439443
"rm -rf //srv/docs.python.org/release/3.15.0rc1/python-3.15.0rc1-docs-html",
440444
"chgrp -R docs /srv/docs.python.org/release/3.15.0rc1",
441445
"chmod -R 775 /srv/docs.python.org/release/3.15.0rc1",
442-
"find /srv/docs.python.org/release/3.15.0rc1 -type f -exec chmod 664 {} \\;",
446+
"find /srv/docs.python.org/release/3.15.0rc1 -type f -exec chmod 664 '{}' ';'",
443447
]

windows-release/merge-and-upload.py

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,16 @@ class RunError(Exception):
6868
pass
6969

7070

71-
def quote_remote_shell(value):
72-
return shlex.quote(str(value))
71+
def join_remote_command(command):
72+
return shlex.join(str(argument) for argument in command)
73+
74+
75+
def join_remote_commands(*commands):
76+
return " && ".join(join_remote_command(command) for command in commands)
77+
78+
79+
def remote_upload_path(path):
80+
return f"{UPLOAD_USER}@{UPLOAD_HOST}:{join_remote_command([path])}"
7381

7482

7583
def _run(*args, single_cmd=False):
@@ -90,12 +98,32 @@ def _run(*args, single_cmd=False):
9098
return out
9199

92100

93-
def call_ssh(*args, allow_fail=True):
101+
def call_ssh(command, allow_fail=True):
94102
if not UPLOAD_HOST or NO_UPLOAD or LOCAL_INDEX:
95-
print("Skipping", args, "because UPLOAD_HOST is missing")
103+
print("Skipping", command, "because UPLOAD_HOST is missing")
96104
return ""
97105
try:
98-
return _run(*_std_args(PLINK), f"{UPLOAD_USER}@{UPLOAD_HOST}", *args)
106+
return _run(
107+
*_std_args(PLINK),
108+
f"{UPLOAD_USER}@{UPLOAD_HOST}",
109+
join_remote_command(command),
110+
)
111+
except RunError as ex:
112+
if not allow_fail:
113+
raise
114+
return ex.args[1]
115+
116+
117+
def call_ssh_commands(*commands, allow_fail=True):
118+
if not UPLOAD_HOST or NO_UPLOAD or LOCAL_INDEX:
119+
print("Skipping", commands, "because UPLOAD_HOST is missing")
120+
return ""
121+
try:
122+
return _run(
123+
*_std_args(PLINK),
124+
f"{UPLOAD_USER}@{UPLOAD_HOST}",
125+
join_remote_commands(*commands),
126+
)
99127
except RunError as ex:
100128
if not allow_fail:
101129
raise
@@ -109,10 +137,12 @@ def upload_ssh(source, dest):
109137
_run(
110138
*_std_args(PSCP),
111139
source,
112-
f"{UPLOAD_USER}@{UPLOAD_HOST}:{quote_remote_shell(dest)}",
140+
remote_upload_path(dest),
141+
)
142+
call_ssh_commands(
143+
["chgrp", "downloads", dest],
144+
["chmod", "g-x,o+r", dest],
113145
)
114-
quoted_dest = quote_remote_shell(dest)
115-
call_ssh(f"chgrp downloads {quoted_dest} && chmod g-x,o+r {quoted_dest}")
116146

117147

118148
def download_ssh(source, dest):
@@ -122,18 +152,17 @@ def download_ssh(source, dest):
122152
Path(dest).parent.mkdir(exist_ok=True, parents=True)
123153
_run(
124154
*_std_args(PSCP),
125-
f"{UPLOAD_USER}@{UPLOAD_HOST}:{quote_remote_shell(source)}",
155+
remote_upload_path(source),
126156
dest,
127157
)
128158

129159

130160
def prepare_upload_dir(dest):
131161
destdir = dest.rpartition("/")[0]
132-
quoted_destdir = quote_remote_shell(destdir)
133-
call_ssh(
134-
f"mkdir {quoted_destdir} && "
135-
f"chgrp downloads {quoted_destdir} && "
136-
f"chmod a+rx {quoted_destdir}"
162+
call_ssh_commands(
163+
["mkdir", destdir],
164+
["chgrp", "downloads", destdir],
165+
["chmod", "a+rx", destdir],
137166
)
138167

139168

@@ -320,9 +349,7 @@ def find_missing_from_index(url, installs):
320349
INDEX_PATH = url2path(INDEX_URL)
321350

322351
try:
323-
INDEX_MTIME = int(
324-
call_ssh("stat", "-c", "%Y", quote_remote_shell(INDEX_PATH)) or "0"
325-
)
352+
INDEX_MTIME = int(call_ssh(["stat", "-c", "%Y", INDEX_PATH]) or "0")
326353
except ValueError:
327354
pass
328355

@@ -391,7 +418,7 @@ def find_missing_from_index(url, installs):
391418
# Check that nobody else has published while we were uploading
392419
if INDEX_FILE and INDEX_MTIME:
393420
try:
394-
mtime = int(call_ssh("stat", "-c", "%Y", quote_remote_shell(INDEX_PATH)) or "0")
421+
mtime = int(call_ssh(["stat", "-c", "%Y", INDEX_PATH]) or "0")
395422
except ValueError:
396423
mtime = 0
397424
if mtime > INDEX_MTIME:

0 commit comments

Comments
 (0)