This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git
The following commit(s) were added to refs/heads/main by this push:
new 9106048 Use exclusion files in RAT checks
9106048 is described below
commit 91060481163985aee42b13aefca3f80726324c85
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Jun 24 17:08:33 2025 +0100
Use exclusion files in RAT checks
- Moves the .gitignore matcher to the util module
- Fixes some problems with extracting archives
- Adds path tracking in archives for efficient file search
- Searches for .rat-excludes or rat-excludes.txt in archives
- Ensures that RAT checks also fail on unrecognised licenses
- Improves the RAT check message
- Updates the test key for the browser tests
- Adds a script to automatically re-sign the browser test files
---
atr/archives.py | 77 +++++---
atr/db/interaction.py | 10 +-
atr/tasks/checks/__init__.py | 19 +-
atr/tasks/checks/license.py | 2 +-
atr/tasks/checks/paths.py | 4 +-
atr/tasks/checks/rat.py | 214 +++++++++++++--------
atr/tasks/sbom.py | 2 +-
atr/util.py | 17 +-
.../0924BA4B875A1A472D147C1EB11EB02801756B9E.asc | 42 ++++
.../E35604DD9E2892E5465B3D8A203F105A7B33A64F.asc | 64 ------
.../apache-tooling-test-example-0.2.tar.gz | Bin 4408 -> 4432 bytes
.../apache-tooling-test-example-0.2.tar.gz.asc | 18 +-
.../apache-tooling-test-example-0.2.tar.gz.sha512 | 2 +-
playwright/mk.sh | 43 +++++
playwright/test.py | 10 +-
15 files changed, 314 insertions(+), 210 deletions(-)
diff --git a/atr/archives.py b/atr/archives.py
index 0175fb7..1a4a490 100644
--- a/atr/archives.py
+++ b/atr/archives.py
@@ -36,37 +36,41 @@ def extract(
extract_dir: str,
max_size: int,
chunk_size: int,
-) -> int:
+ track_files: bool | set[str] = False,
+) -> tuple[int, list[str]]:
+ _LOGGER.info(f"Extracting {archive_path} to {extract_dir}")
+
total_extracted = 0
+ extracted_paths = []
try:
with tarzip.open_archive(archive_path) as archive:
match archive.specific():
case tarfile.TarFile() as tf:
for member in tf:
- keep_going, total_extracted = archive_extract_member(
- tf, member, extract_dir, total_extracted,
max_size, chunk_size
+ total_extracted, extracted_paths =
_archive_extract_member(
+ tf, member, extract_dir, total_extracted,
max_size, chunk_size, track_files, extracted_paths
)
- if not keep_going:
- break
case zipfile.ZipFile():
for member in archive:
if not isinstance(member, tarzip.ZipMember):
continue
- keep_going, total_extracted =
_zip_archive_extract_member(
- archive, member, extract_dir, total_extracted,
max_size, chunk_size
+ total_extracted, extracted_paths =
_zip_archive_extract_member(
+ archive,
+ member,
+ extract_dir,
+ total_extracted,
+ max_size,
+ chunk_size,
+ track_files,
+ extracted_paths,
)
- if not keep_going:
- break
-
- case _:
- raise ExtractionError("Unsupported archive type",
{"archive_path": archive_path})
except (tarfile.TarError, zipfile.BadZipFile, ValueError) as e:
raise ExtractionError(f"Failed to read archive: {e}", {"archive_path":
archive_path}) from e
- return total_extracted
+ return total_extracted, extracted_paths
def total_size(tgz_path: str, chunk_size: int = 4096) -> int:
@@ -125,16 +129,27 @@ def _archive_extract_safe_process_file(
return extracted_file_size
-def archive_extract_member(
- tf: tarfile.TarFile, member: tarfile.TarInfo, extract_dir: str,
total_extracted: int, max_size: int, chunk_size: int
-) -> tuple[bool, int]:
- if member.name and member.name.split("/")[-1].startswith("._"):
+def _archive_extract_member(
+ tf: tarfile.TarFile,
+ member: tarfile.TarInfo,
+ extract_dir: str,
+ total_extracted: int,
+ max_size: int,
+ chunk_size: int,
+ track_files: bool | set[str] = False,
+ extracted_paths: list[str] = [],
+) -> tuple[int, list[str]]:
+ member_basename = os.path.basename(member.name)
+ if member_basename.startswith("._"):
# Metadata convention
- return False, 0
+ return 0, extracted_paths
# Skip any character device, block device, or FIFO
if member.isdev():
- return False, 0
+ return 0, extracted_paths
+
+ if track_files and isinstance(track_files, set) and (member_basename in
track_files):
+ extracted_paths.append(member.name)
# Check whether extraction would exceed the size limit
if member.isreg() and ((total_extracted + member.size) > max_size):
@@ -149,7 +164,7 @@ def archive_extract_member(
target_path = os.path.join(extract_dir, member.name)
if not
os.path.abspath(target_path).startswith(os.path.abspath(extract_dir)):
_LOGGER.warning(f"Skipping potentially unsafe path: {member.name}")
- return False, 0
+ return 0, extracted_paths
tf.extract(member, extract_dir, numeric_owner=True)
elif member.isreg():
@@ -164,7 +179,7 @@ def archive_extract_member(
elif member.islnk():
_archive_extract_safe_process_hardlink(member, extract_dir)
- return True, total_extracted
+ return total_extracted, extracted_paths
def _archive_extract_safe_process_hardlink(member: tarfile.TarInfo,
extract_dir: str) -> None:
@@ -261,9 +276,15 @@ def _zip_archive_extract_member(
total_extracted: int,
max_size: int,
chunk_size: int,
-) -> tuple[bool, int]:
- if member.name.split("/")[-1].startswith("._"):
- return False, 0
+ track_files: bool | set[str] = False,
+ extracted_paths: list[str] = [],
+) -> tuple[int, list[str]]:
+ member_basename = os.path.basename(member.name)
+ if track_files and (isinstance(track_files, set) and (member_basename in
track_files)):
+ extracted_paths.append(member.name)
+
+ if member_basename.startswith("._"):
+ return 0, extracted_paths
if member.isfile() and (total_extracted + member.size) > max_size:
raise ExtractionError(
@@ -275,17 +296,17 @@ def _zip_archive_extract_member(
target_path = os.path.join(extract_dir, member.name)
if not
os.path.abspath(target_path).startswith(os.path.abspath(extract_dir)):
_LOGGER.warning("Skipping potentially unsafe path: %s",
member.name)
- return False, 0
+ return 0, extracted_paths
os.makedirs(target_path, exist_ok=True)
- return True, total_extracted
+ return total_extracted, extracted_paths
if member.isfile():
extracted_size = _zip_extract_safe_process_file(
archive, member, extract_dir, total_extracted, max_size, chunk_size
)
- return True, total_extracted + extracted_size
+ return total_extracted + extracted_size, extracted_paths
- return False, total_extracted
+ return total_extracted, extracted_paths
def _zip_extract_safe_process_file(
diff --git a/atr/db/interaction.py b/atr/db/interaction.py
index cff67ae..01d0abf 100644
--- a/atr/db/interaction.py
+++ b/atr/db/interaction.py
@@ -85,11 +85,15 @@ async def key_user_add(
added_keys = []
for key in keys:
- asf_uid = await util.asf_uid_from_uids(key.get("uids", []),
ldap_data=ldap_data)
- if (key.get("fingerprint") or "").upper() ==
"E35604DD9E2892E5465B3D8A203F105A7B33A64F":
+ uids = key.get("uids", [])
+ asf_uid = await util.asf_uid_from_uids(uids, ldap_data=ldap_data)
+ test_key_uids = ["Apache Tooling (For test use only)
<[email protected]>"]
+ is_admin = user.is_admin(session_asf_uid)
+ if (uids == test_key_uids) and is_admin:
# Allow the test key
# TODO: We should fix the test key, not add an exception for it
- pass
+ # But the admin check probably makes this safe enough
+ asf_uid = session_asf_uid
elif session_asf_uid and (asf_uid != session_asf_uid):
# TODO: Give a more detailed error message about why and what to do
raise InteractionError(f"Key {key.get('fingerprint', '').upper()}
is not associated with your ASF account")
diff --git a/atr/tasks/checks/__init__.py b/atr/tasks/checks/__init__.py
index 7e07d0c..f08e445 100644
--- a/atr/tasks/checks/__init__.py
+++ b/atr/tasks/checks/__init__.py
@@ -23,7 +23,6 @@ import functools
import pathlib
from typing import TYPE_CHECKING, Any
-import gitignore_parser
import sqlmodel
if TYPE_CHECKING:
@@ -163,7 +162,7 @@ class Recorder:
project = await self.project()
if not project.policy_binary_artifact_paths:
return False
- matches = _create_path_matcher(
+ matches = util.create_path_matcher(
project.policy_binary_artifact_paths, self.abs_path_base() /
".ignore", self.abs_path_base()
)
abs_path = await self.abs_path()
@@ -175,7 +174,7 @@ class Recorder:
project = await self.project()
if not project.policy_source_artifact_paths:
return False
- matches = _create_path_matcher(
+ matches = util.create_path_matcher(
project.policy_source_artifact_paths, self.abs_path_base() /
".ignore", self.abs_path_base()
)
abs_path = await self.abs_path()
@@ -254,17 +253,3 @@ def with_model(cls: type[schema.Strict]) ->
Callable[[Callable[..., Any]], Calla
return wrapper
return decorator
-
-
-def _create_path_matcher(lines: list[str], full_path: pathlib.Path, base_dir:
pathlib.Path) -> Callable[[str], bool]:
- rules = []
- negation = False
- for line_no, line in enumerate(lines, start=1):
- rule = gitignore_parser.rule_from_pattern(line.rstrip("\n"),
base_path=base_dir, source=(full_path, line_no))
- if rule:
- rules.append(rule)
- if rule.negation:
- negation = True
- if not negation:
- return lambda file_path: any(r.match(file_path) for r in rules)
- return lambda file_path: gitignore_parser.handle_negation(file_path, rules)
diff --git a/atr/tasks/checks/license.py b/atr/tasks/checks/license.py
index cf4c6fa..834c2ae 100644
--- a/atr/tasks/checks/license.py
+++ b/atr/tasks/checks/license.py
@@ -205,7 +205,7 @@ def _files_check_core_logic(artifact_path: str) ->
Iterator[Result]:
# Check for license files in the root directory
with tarzip.open_archive(artifact_path) as archive:
for member in archive:
- _LOGGER.warning(f"Checking member: {member.name}")
+ # _LOGGER.warning(f"Checking member: {member.name}")
if member.name and member.name.split("/")[-1].startswith("._"):
# Metadata convention
continue
diff --git a/atr/tasks/checks/paths.py b/atr/tasks/checks/paths.py
index daf5a9c..3d7e4f4 100644
--- a/atr/tasks/checks/paths.py
+++ b/atr/tasks/checks/paths.py
@@ -26,6 +26,7 @@ import atr.analysis as analysis
import atr.tasks.checks as checks
import atr.util as util
+_ALLOWED_TOP_LEVEL = {"CHANGES", "LICENSE", "NOTICE", "README"}
_LOGGER: Final = logging.getLogger(__name__)
@@ -179,8 +180,7 @@ async def _check_path_process_single(
await _check_metadata_rules(base_path, relative_path, relative_paths,
ext_metadata, errors, warnings)
else:
_LOGGER.info("Checking general rules for %s", full_path)
- allowed_top_level = {"CHANGES", "LICENSE", "NOTICE", "README"}
- if (relative_path.parent == pathlib.Path(".")) and (relative_path.name
not in allowed_top_level):
+ if (relative_path.parent == pathlib.Path(".")) and (relative_path.name
not in _ALLOWED_TOP_LEVEL):
warnings.append(f"Unknown top level file: {relative_path.name}")
for error in errors:
diff --git a/atr/tasks/checks/rat.py b/atr/tasks/checks/rat.py
index 716a9e1..4c30693 100644
--- a/atr/tasks/checks/rat.py
+++ b/atr/tasks/checks/rat.py
@@ -18,6 +18,7 @@
import asyncio
import logging
import os
+import pathlib
import subprocess
import tempfile
import xml.etree.ElementTree as ElementTree
@@ -26,6 +27,7 @@ from typing import Any, Final
import atr.archives as archives
import atr.config as config
import atr.tasks.checks as checks
+import atr.util as util
_CONFIG: Final = config.get()
_JAVA_MEMORY_ARGS: Final[list[str]] = []
@@ -39,6 +41,7 @@ _JAVA_MEMORY_ARGS: Final[list[str]] = []
# "-XX:CompressedClassSpaceSize=16m"
# ]
_LOGGER: Final = logging.getLogger(__name__)
+_RAT_EXCLUDES_FILENAMES: Final[set[str]] = {".rat-excludes",
"rat-excludes.txt"}
async def check(args: checks.FunctionArguments) -> str | None:
@@ -128,7 +131,13 @@ def _check_core_logic(
# Extract the archive to the temporary directory
_LOGGER.info(f"Extracting {artifact_path} to {temp_dir}")
- extracted_size = archives.extract(artifact_path, temp_dir,
max_size=max_extract_size, chunk_size=chunk_size)
+ extracted_size, extracted_paths = archives.extract(
+ artifact_path,
+ temp_dir,
+ max_size=max_extract_size,
+ chunk_size=chunk_size,
+ track_files=_RAT_EXCLUDES_FILENAMES,
+ )
_LOGGER.info(f"Extracted {extracted_size} bytes")
# Find the root directory
@@ -143,7 +152,9 @@ def _check_core_logic(
_LOGGER.info(f"Using root directory: {extract_dir}")
# Execute RAT and get results or error
- error_result, xml_output_path =
_check_core_logic_execute_rat(rat_jar_path, extract_dir, temp_dir)
+ error_result, xml_output_path = _check_core_logic_execute_rat(
+ rat_jar_path, extract_dir, temp_dir, extracted_paths
+ )
if error_result:
return error_result
@@ -175,7 +186,7 @@ def _check_core_logic(
def _check_core_logic_execute_rat(
- rat_jar_path: str, extract_dir: str, temp_dir: str
+ rat_jar_path: str, extract_dir: str, temp_dir: str, excluded_paths:
list[str]
) -> tuple[dict[str, Any] | None, str | None]:
"""Execute Apache RAT and process its output."""
# Define output file path
@@ -191,21 +202,20 @@ def _check_core_logic_execute_rat(
*_JAVA_MEMORY_ARGS,
"-jar",
rat_jar_path,
- "-d",
- extract_dir,
"-x",
"-o",
xml_output_path,
- "--exclude",
- "LICENSE",
- "--exclude",
- "NOTICE",
+ "-d",
+ "--",
+ ".",
]
+ if excluded_paths:
+ _rat_apply_exclusions(extract_dir, excluded_paths, temp_dir)
_LOGGER.info(f"Running Apache RAT: {' '.join(command)}")
- # Change working directory to temp_dir when running the process
+ # Change working directory to extract_dir when running the process
current_dir = os.getcwd()
- os.chdir(temp_dir)
+ os.chdir(extract_dir)
_LOGGER.info(f"Executing Apache RAT from directory: {os.getcwd()}")
@@ -283,10 +293,7 @@ def _check_core_logic_execute_rat(
os.chdir(current_dir)
# Check that the output file exists
- if os.path.exists(xml_output_path):
- _LOGGER.info(f"Found XML output at: {xml_output_path} (size:
{os.path.getsize(xml_output_path)} bytes)")
- return None, xml_output_path
- else:
+ if not os.path.exists(xml_output_path):
_LOGGER.error(f"XML output file not found at: {xml_output_path}")
# List files in the temporary directory
_LOGGER.info(f"Files in {temp_dir}: {os.listdir(temp_dir)}")
@@ -304,6 +311,10 @@ def _check_core_logic_execute_rat(
"errors": [f"Missing output file: {xml_output_path}"],
}, None
+ # The XML was found correctly
+ _LOGGER.info(f"Found XML output at: {xml_output_path} (size:
{os.path.getsize(xml_output_path)} bytes)")
+ return None, xml_output_path
+
def _check_core_logic_jar_exists(rat_jar_path: str) -> tuple[str, dict[str,
Any] | None]:
"""Verify that the Apache RAT JAR file exists and is accessible."""
@@ -356,68 +367,9 @@ def _check_core_logic_jar_exists(rat_jar_path: str) ->
tuple[str, dict[str, Any]
def _check_core_logic_parse_output(xml_file: str, base_dir: str) -> dict[str,
Any]:
- """Parse the XML output from Apache RAT."""
+ """Parse the XML output from Apache RAT safely."""
try:
- tree = ElementTree.parse(xml_file)
- root = tree.getroot()
-
- total_files = 0
- approved_licenses = 0
- unapproved_licenses = 0
- unknown_licenses = 0
-
- unapproved_files = []
- unknown_license_files = []
-
- # Process each resource
- for resource in root.findall(".//resource"):
- total_files += 1
-
- # Get the name attribute value
- name = resource.get("name", "")
-
- # Remove base_dir prefix for cleaner display
- if name.startswith(base_dir):
- name = name[len(base_dir) :].lstrip("/")
-
- # Get license information
- license_approval = resource.find("license-approval")
- license_family = resource.find("license-family")
-
- is_approved = license_approval is not None and
license_approval.get("name") == "true"
- license_name = license_family.get("name") if license_family is not
None else "Unknown"
-
- # Update counters and lists
- if is_approved:
- approved_licenses += 1
- elif license_name == "Unknown license":
- unknown_licenses += 1
- unknown_license_files.append({"name": name, "license":
license_name})
- else:
- unapproved_licenses += 1
- unapproved_files.append({"name": name, "license":
license_name})
-
- # Calculate overall validity
- valid = unapproved_licenses == 0
-
- # Prepare awkwardly long summary message
- message = f"""\
-Found {approved_licenses} files with approved licenses, {unapproved_licenses} \
-with unapproved licenses, and {unknown_licenses} with unknown licenses"""
-
- # We limit the number of files we report to 100
- return {
- "valid": valid,
- "message": message,
- "total_files": total_files,
- "approved_licenses": approved_licenses,
- "unapproved_licenses": unapproved_licenses,
- "unknown_licenses": unknown_licenses,
- "unapproved_files": unapproved_files[:100],
- "unknown_license_files": unknown_license_files[:100],
- "errors": [],
- }
-
+ return _check_core_logic_parse_output_core(xml_file, base_dir)
except Exception as e:
_LOGGER.error(f"Error parsing RAT output: {e}")
return {
@@ -431,6 +383,70 @@ with unapproved licenses, and {unknown_licenses} with
unknown licenses"""
}
+def _check_core_logic_parse_output_core(xml_file: str, base_dir: str) ->
dict[str, Any]:
+ """Parse the XML output from Apache RAT."""
+ tree = ElementTree.parse(xml_file)
+ root = tree.getroot()
+
+ total_files = 0
+ approved_licenses = 0
+ unapproved_licenses = 0
+ unknown_licenses = 0
+
+ unapproved_files = []
+ unknown_license_files = []
+
+ # Process each resource
+ for resource in root.findall(".//resource"):
+ total_files += 1
+
+ # Get the name attribute value
+ name = resource.get("name", "")
+
+ # Remove base_dir prefix for cleaner display
+ if name.startswith(base_dir):
+ name = name[len(base_dir) :].lstrip("/")
+
+ # Get license information
+ license_approval = resource.find("license-approval")
+ license_family = resource.find("license-family")
+
+ is_approved = True
+ if license_approval is not None:
+ if license_approval.get("name") == "false":
+ is_approved = False
+ license_name = license_family.get("name") if (license_family is not
None) else "Unknown"
+
+ # Update counters and lists
+ if is_approved:
+ approved_licenses += 1
+ elif license_name == "Unknown license":
+ unknown_licenses += 1
+ unknown_license_files.append({"name": name, "license":
license_name})
+ else:
+ unapproved_licenses += 1
+ unapproved_files.append({"name": name, "license": license_name})
+
+ # Calculate overall validity
+ valid = (unapproved_licenses == 0) and (unknown_licenses == 0)
+
+ # Prepare a summary message of just the right length
+ message = _summary_message(valid, unapproved_licenses, unknown_licenses)
+
+ # We limit the number of files we report to 100
+ return {
+ "valid": valid,
+ "message": message,
+ "total_files": total_files,
+ "approved_licenses": approved_licenses,
+ "unapproved_licenses": unapproved_licenses,
+ "unknown_licenses": unknown_licenses,
+ "unapproved_files": unapproved_files[:100],
+ "unknown_license_files": unknown_license_files[:100],
+ "errors": [],
+ }
+
+
def _check_java_installed() -> dict[str, Any] | None:
# Check that Java is installed
# TODO: Run this only once, when the server starts
@@ -478,6 +494,7 @@ def _check_java_installed() -> dict[str, Any] | None:
def _extracted_dir(temp_dir: str) -> str | None:
# Loop through all the dirs in temp_dir
extract_dir = None
+ _LOGGER.info(f"Checking directories in {temp_dir}: {os.listdir(temp_dir)}")
for dir_name in os.listdir(temp_dir):
if dir_name.startswith("."):
continue
@@ -489,3 +506,48 @@ def _extracted_dir(temp_dir: str) -> str | None:
else:
raise ValueError(f"Multiple root directories found: {extract_dir},
{dir_path}")
return extract_dir
+
+
+def _rat_apply_exclusions(extract_dir: str, excluded_paths: list[str],
temp_dir: str) -> None:
+ """Apply exclusions to the extracted directory."""
+ # Exclusions are difficult using the command line version of RAT
+ # Each line is interpreted as a literal AND a glob AND a regex
+ # Then, if ANY of those three match a filename, the file is excluded
+ # You cannot specify which syntax to use; all three are always tried
+ # You cannot specify that you want to match against the whole path
+ # Therefore, we take a different approach
+ # We interpret the exclusion file as a glob file in .gitignore format
+ # Then, we simply remove any files that match the glob
+ exclusion_lines = []
+ for excluded_path in excluded_paths:
+ abs_excluded_path = os.path.join(temp_dir, excluded_path)
+ if not os.path.exists(abs_excluded_path):
+ _LOGGER.error(f"Exclusion file not found: {abs_excluded_path}")
+ continue
+ if not os.path.isfile(abs_excluded_path):
+ _LOGGER.error(f"Exclusion file is not a file: {abs_excluded_path}")
+ continue
+ with open(abs_excluded_path, encoding="utf-8") as f:
+ exclusion_lines.extend(f.readlines())
+ matcher = util.create_path_matcher(
+ exclusion_lines, pathlib.Path(extract_dir) / ".ignore",
pathlib.Path(extract_dir)
+ )
+ for root, _dirs, files in os.walk(extract_dir):
+ for file in files:
+ abs_path = os.path.join(root, file)
+ if matcher(abs_path):
+ _LOGGER.info(f"Removing {abs_path} because it matches the
exclusion")
+ os.remove(abs_path)
+
+
+def _summary_message(valid: bool, unapproved_licenses: int, unknown_licenses:
int) -> str:
+ message = "All files have approved licenses"
+ if not valid:
+ message = "Found "
+ if unapproved_licenses > 0:
+ message += f"{unapproved_licenses} files with unapproved licenses"
+ if unknown_licenses > 0:
+ message += " and "
+ if unknown_licenses > 0:
+ message += f"{unknown_licenses} files with unknown licenses"
+ return message
diff --git a/atr/tasks/sbom.py b/atr/tasks/sbom.py
index 7992ac1..4f86c10 100644
--- a/atr/tasks/sbom.py
+++ b/atr/tasks/sbom.py
@@ -86,7 +86,7 @@ async def _generate_cyclonedx_core(artifact_path: str,
output_path: str) -> dict
# Extract the archive to the temporary directory
# TODO: Ideally we'd have task dependencies or archive caching
_LOGGER.info(f"Extracting {artifact_path} to {temp_dir}")
- extracted_size = await asyncio.to_thread(
+ extracted_size, _extracted_paths = await asyncio.to_thread(
archives.extract,
artifact_path,
str(temp_dir),
diff --git a/atr/util.py b/atr/util.py
index ad1878f..0aa3549 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -29,7 +29,7 @@ import tarfile
import tempfile
import uuid
import zipfile
-from collections.abc import AsyncGenerator, Callable, Sequence
+from collections.abc import AsyncGenerator, Callable, Iterable, Sequence
from typing import Any, Final, TypeVar
import aiofiles.os
@@ -37,6 +37,7 @@ import aioshutil
import asfquart
import asfquart.base as base
import asfquart.session as session
+import gitignore_parser
import httpx
import jinja2
import quart
@@ -275,6 +276,20 @@ async def create_hard_link_clone(
await _clone_recursive(source_dir, dest_dir)
+def create_path_matcher(lines: Iterable[str], full_path: pathlib.Path,
base_dir: pathlib.Path) -> Callable[[str], bool]:
+ rules = []
+ negation = False
+ for line_no, line in enumerate(lines, start=1):
+ rule = gitignore_parser.rule_from_pattern(line.rstrip("\n"),
base_path=base_dir, source=(full_path, line_no))
+ if rule:
+ rules.append(rule)
+ if rule.negation:
+ negation = True
+ if not negation:
+ return lambda file_path: any(r.match(file_path) for r in rules)
+ return lambda file_path: gitignore_parser.handle_negation(file_path, rules)
+
+
def email_from_uid(uid: str) -> str | None:
if m := re.search(r"<([^>]+)>", uid):
return m.group(1).lower()
diff --git a/playwright/0924BA4B875A1A472D147C1EB11EB02801756B9E.asc
b/playwright/0924BA4B875A1A472D147C1EB11EB02801756B9E.asc
new file mode 100644
index 0000000..1a0bd14
--- /dev/null
+++ b/playwright/0924BA4B875A1A472D147C1EB11EB02801756B9E.asc
@@ -0,0 +1,42 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Comment: 0924 BA4B 875A 1A47 2D14 7C1E B11E B028 0175 6B9E
+Comment: Apache Tooling (For test use only) <apache-tooling@exam
+
+xjMEaFrKuRYJKwYBBAHaRw8BAQdAf8ene7BE8ixAZMhJJWePxETQjKBHuJL3qAsJ
+c/HGTZ3CwBEEHxYKAIMFgmhayrkFiQWkj70DCwkHCRCxHrAoAXVrnkcUAAAAAAAe
+ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcjaDIP1oXI5qfuRF9d97kf
+3xx/CqciqcHFPU3ZVHpvcAMVCggCm4ECHgEWIQQJJLpLh1oaRy0UfB6xHrAoAXVr
+ngAAAa8A/jB81dDt+cyOj+VTQ3tbrgFU3TlYezjzs/JEqJrTfE9cAQCHb3jK+tZd
+K0C+5YQopDMo/MhkWPjGLvaauBwC8pbbCM1DQXBhY2hlIFRvb2xpbmcgKEZvciB0
+ZXN0IHVzZSBvbmx5KSA8YXBhY2hlLXRvb2xpbmdAZXhhbXBsZS5pbnZhbGlkPsLA
+EwQTFgoAhgWCaFrKuQWJBaSPvQMLCQcJELEesCgBdWueRxQAAAAAAB4AIHNhbHRA
+bm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ4BMML8pZTlkBSgj3d0nKZ068tOSAQ8E
+v5FIx+WvrCMCAxUKCAKZAQKbgQIeARYhBAkkukuHWhpHLRR8HrEesCgBdWueAACd
+fgD3YpYcOQAaku/qukVgiJ0n0eux0mMIHNMWLbz40X1ouAEAoylhe7ySr47009Oy
+BoDDDlbChPiPjVHkJvfBPumXhwvOMwRoWsq5FgkrBgEEAdpHDwEBB0AGXtYCNilT
+I1M3MUzF3eGK9M7Ax/mSJdjyRgiFy1+5w8LAxQQYFgoBNwWCaFrKuQWJBaSPvQkQ
+sR6wKAF1a55HFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn
+VBjBlT1Lse4x/eNxTYapJwSoYHM+L8dnRvMtP49ZYyICm6C+oAQZFgoAbwWCaFrK
+uQkQIUeYql9BqjZHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Au
+b3Jn5xIUllSf3Y6dQlEMi2JeTlTfa+SW+wQCnRwEAKi/yLoWIQTl6SWaidYoB0L8
+2tIhR5iqX0GqNgAATikA/3L0VT44NY7SN3GFtsmso6fFznA5PdesAKYhjtMhpEyV
+AP9hs0bzkb2xK+MxLViv6TqvF4HHqsFhOBFGd2f5Pj7KDhYhBAkkukuHWhpHLRR8
+HrEesCgBdWueAAANngD/Z9PeSYcwlQINT8qGo6r5LyYrr23M3bCTOHRH0viSSrEA
+/2kEz8NUAtCJ00s+j+tNZV1mTR4aX/xL3mNNN8sOLrQNzjMEaFrKuRYJKwYBBAHa
+Rw8BAQdAOIRVNk+dBcuy+ldmuWU4DcA3hQvyl4QAEYFpyqZXYYjCwMUEGBYKATcF
+gmhayrkFiQWkj70JELEesCgBdWueRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl
+cXVvaWEtcGdwLm9yZ7a10MOsaWwzj2i3UY5A2DdamQmIqlOs6rxXoEia4qnvApuC
+vqAEGRYKAG8FgmhayrkJEE9m+vmdYrj0RxQAAAAAAB4AIHNhbHRAbm90YXRpb25z
+LnNlcXVvaWEtcGdwLm9yZ53GaCuxxCI4KHoMH3+etvJ3bqxnjMJWTOBuJPejPA4J
+FiEEb6YmD+8n2tqPMh+RT2b6+Z1iuPQAAMhWAQDRuD8PGzzv/NZva5wpFfyxrasI
+saKIJ5Hzu+XVPTbV0AEAmgucr3c/NX5ekIN4G1euNHUW8DsDR+oTwJQyshV+Mw8W
+IQQJJLpLh1oaRy0UfB6xHrAoAXVrngAA0wkA/Rl6LcJoz87AvtaD2cTmO2xZwJR0
+jxvw5Rg+KhVBTLLgAP9U6VRz37AAi4i0K2WPRl7lRI0wkL5KDt+w6DihwCsxCs44
+BGhayrkSCisGAQQBl1UBBQEBB0AcxzOd403UVstZNkdaxK466P/GzuPLqwEdnYWI
+SmXuTQMBCAfCwAYEGBYKAHgFgmhayrkFiQWkj70JELEesCgBdWueRxQAAAAAAB4A
+IHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ3spMkAbPj9q1WRlb0Zql/eV
+FPcx6aLEuPfPBXETMvWbApuMFiEECSS6S4daGkctFHwesR6wKAF1a54AAO+SAP4z
+g/bCHhg+q2ZbZMFFupWcNL8ziFbI0PDSNuoaSzB4GwEA8zKo1Ft6uJ7cHvQzp+qX
+sm3HEBYzuBdcSKzUMFOgsQg=
+=1Ulz
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/playwright/E35604DD9E2892E5465B3D8A203F105A7B33A64F.asc
b/playwright/E35604DD9E2892E5465B3D8A203F105A7B33A64F.asc
deleted file mode 100644
index 97ab288..0000000
--- a/playwright/E35604DD9E2892E5465B3D8A203F105A7B33A64F.asc
+++ /dev/null
@@ -1,64 +0,0 @@
------BEGIN PGP PUBLIC KEY BLOCK-----
-
-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
-Qx20dp/Ekji4w6nAtopc4CTjL7YeRFdBKQ==
-=CTh1
------END PGP PUBLIC KEY BLOCK-----
diff --git
a/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz
b/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz
index 1450508..0512b3c 100644
Binary files
a/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz
and
b/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz
differ
diff --git
a/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz.asc
b/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz.asc
index 9dbc865..7d5b50b 100644
---
a/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz.asc
+++
b/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz.asc
@@ -1,16 +1,8 @@
-----BEGIN PGP SIGNATURE-----
-iQIzBAABCgAdFiEEL15oWrFvxNnxIOnMVhcfqY9WJTIFAmf+glMACgkQVhcfqY9W
-JTIDrQ//SHU6wH/l6iXg2ILaPdfDgM/dsX2o3OOGtoP2Xr8MoI7yt2cSDduV/xjE
-VX43/v8kIoFqZl/yhYyd3bO7oe3W22cApSwHb2csf9yH4/kx881iAP6JfcsOzKxP
-YQ4xuBrz6N3Vt6I2Fxtgic9FA2XkF86SiQS0++9/gQBOMksFXnAeivlly/81DjeK
-/SuKiL6hgxbKrausLum/e/hEbYy/Fom/demv8Psr9+3f3t1IHPi8iUAVcN1v7LFv
-nnkza7t1c4toNFHmIQYrbHccwjbjJ1uzTBt2LMFzq3QAuTehT7OGgSWM4Vsx/zzC
-UFg1g9ZXNcrv87JFJTQcopN8szY5YlTiRhjBVAGr8jU+3i2XH6EZZr5HlakKy4mG
-vftAekP209pnfK5WIYFbmuSlMsLYBdCj4kVcY0S6sjdEHKb8UEfmtn40vdvH5/QY
-loghG/t10FxPxaVIyaEHY2Z2QZCMEUrYVwLVlDFolSZb0y9ungnyRXzaWFAV0TGm
-SEYRjDxP+w6fxKYSaB37xbLcufO6BOK2UyDjGG1oLj/8gzDeLdYdqbs9YHV/e/xc
-GBDapYpb7BVN3WJBzmvMfRq1P0SrN3W/Mqo8OJjZ8PXyAOag+EcRHcjAlB7sujV6
-Sv2GFEo2Il67JtPYbFPXAg2V91SHDbMYib1lsc3kU6AyBPJN/Gg=
-=aros
+wr0EABYKAG8FgmhayrkJEE9m+vmdYrj0RxQAAAAAAB4AIHNhbHRAbm90YXRpb25z
+LnNlcXVvaWEtcGdwLm9yZ3vpwgIRTsF5adxcqdebGCCmSGKvgOzuVzTFzEdfkCIW
+FiEEb6YmD+8n2tqPMh+RT2b6+Z1iuPQAAPZAAQC2wpFu44Z10oaQHvXAoWdHWH1r
+rh0ypJtdf8m8Z3fU7gD9EbDlFqqr1g3VE+52ftONNAaOOCNFIRUAcajGjphXjws=
+=8tSd
-----END PGP SIGNATURE-----
diff --git
a/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz.sha512
b/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz.sha512
index d57f986..06bb2c0 100644
---
a/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz.sha512
+++
b/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz.sha512
@@ -1 +1 @@
-9973b48f7e2424621d865a2f9a139fd0da3b63cd2ec240a86e1ede914d524c4d5c95c79bc84c1da2b3d7e41463bb133188fe45b34fc095dd3e1c08a508ccd101
apache-tooling-test-example-0.2.tar.gz
+82709c7c20145bff4a62728a305d93c1a07f3082e209b147dd174dc31ddd151f0164a37a62dddb68c20eb8e21404199cd1863ea946438aecf509ced57d583a8c
apache-tooling-test-example-0.2.tar.gz
diff --git a/playwright/mk.sh b/playwright/mk.sh
new file mode 100755
index 0000000..99d7ff4
--- /dev/null
+++ b/playwright/mk.sh
@@ -0,0 +1,43 @@
+#!/bin/sh
+# This script requires Alpine Linux
+# Usage: sh mk.sh
+
+# Use strict mode in POSIX sh
+set -eux
+
+# Add Sequoia to system packages
+apk add cmd:sq
+
+# Set the UID for the key pair
+_uid='Apache Tooling (For test use only) <[email protected]>'
+
+# Generate the secret key
+sq key generate --userid "$_uid" --rev-cert rev-cert.tmp --shared-key \
+ --output tmp.secret.asc --without-password
+
+# Remove the rev cert
+rm rev-cert.tmp
+
+# Extract the fingerprint of the secret key
+_fp=$(sq inspect tmp.secret.asc | awk '/Fingerprint:/ {print $2}')
+
+# Move the secret key to a file name containing its fingerprint
+mv tmp.secret.asc "${_fp}.secret.asc"
+
+# Compute the public key, with a file name containing its fingerprint
+sq key delete --cert-file "${_fp}.secret.asc" --output "${_fp}.asc"
+
+# Enter the directory containing the artifact
+cd apache-tooling-test-example-0.2/
+
+# Generate the SHA-2-512 hash
+sha512sum apache-tooling-test-example-0.2.tar.gz > \
+ apache-tooling-test-example-0.2.tar.gz.sha512
+
+# Generate the signature
+sq sign --signer-file "../${_fp}.secret.asc" \
+ --signature-file apache-tooling-test-example-0.2.tar.gz > \
+ apache-tooling-test-example-0.2.tar.gz.asc
+
+# Remove the secret key
+rm "../${_fp}.secret.asc"
diff --git a/playwright/test.py b/playwright/test.py
index 371e269..8e111be 100644
--- a/playwright/test.py
+++ b/playwright/test.py
@@ -20,6 +20,7 @@
import argparse
import dataclasses
import getpass
+import glob
import logging
import os
import re
@@ -693,9 +694,12 @@ def test_checks_06_targz(page: sync_api.Page, credentials:
Credentials) -> None:
def test_openpgp_01_upload(page: sync_api.Page, credentials: Credentials) ->
None:
- key_fingerprint_lower = "e35604dd9e2892e5465b3d8a203f105a7b33a64f"
- key_fingerprint_upper = key_fingerprint_lower.upper()
- key_path = f"/run/tests/{key_fingerprint_lower.upper()}.asc"
+ for key_path in glob.glob("/run/tests/*.asc"):
+ key_fingerprint_lower =
os.path.basename(key_path).split(".")[0].lower()
+ key_fingerprint_upper = key_fingerprint_lower.upper()
+ break
+ else:
+ raise RuntimeError("No test key found")
logging.info("Starting OpenPGP key upload test")
go_to_path(page, "/keys")
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]