You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by ar...@apache.org on 2026/08/13 09:56:14 UTC
(tooling-trusted-releases) branch embargoed_release_storage updated (a0a35f9b -> 68132580)
This is an automated email from the ASF dual-hosted git repository.
alitheg pushed a change to branch embargoed_release_storage
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
discard a0a35f9b #1506 - separate unfinished dir for embargoed releases
new 68132580 #1506 - separate unfinished dir for embargoed releases
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (a0a35f9b)
\
N -- N -- N refs/heads/embargoed_release_storage (68132580)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
tests/unit/test_admin_consistency.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org
(tooling-trusted-releases) 01/01: #1506 - separate unfinished dir for embargoed releases
Posted by ar...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
alitheg pushed a commit to branch embargoed_release_storage
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
commit 68132580a10e04884e279b8007eaf2dd6a79452e
Author: Alastair McFarlane <ar...@apache.org>
AuthorDate: Tue Aug 11 17:48:31 2026 +0100
#1506 - separate unfinished dir for embargoed releases
---
atr/admin/__init__.py | 14 +++++
atr/config.py | 3 +
atr/get/file.py | 4 +-
atr/paths.py | 105 ++++++++++++++++++-----------------
atr/server.py | 15 +++--
atr/tasks/__init__.py | 10 ++--
atr/tasks/checks/__init__.py | 38 +++++++++++--
atr/tasks/sbom.py | 33 +++++++++--
atr/worker.py | 7 +++
tests/unit/test_admin_consistency.py | 4 +-
tests/unit/test_paths.py | 88 +++++++++++++++++++++++++++++
tests/unit/test_sbom_osv.py | 3 +-
12 files changed, 251 insertions(+), 73 deletions(-)
diff --git a/atr/admin/__init__.py b/atr/admin/__init__.py
index 74ffd8e9..13d307b8 100644
--- a/atr/admin/__init__.py
+++ b/atr/admin/__init__.py
@@ -2700,10 +2700,24 @@ async def _consistency_database_dirs(releases: Sequence[sql.Release]) -> list[st
async def _get_filesystem_dirs() -> list[str]:
filesystem_dirs = []
await _get_filesystem_dirs_finished(filesystem_dirs)
+ await _get_filesystem_dirs_embargoed(filesystem_dirs)
await _get_filesystem_dirs_unfinished(filesystem_dirs)
return filesystem_dirs
+async def _get_filesystem_dirs_embargoed(filesystem_dirs: list[str]) -> None:
+ embargoed_dir = paths.get_embargoed_dir()
+ embargoed_dir_contents = await aiofiles.os.listdir(embargoed_dir)
+ for project_dir in embargoed_dir_contents:
+ project_dir_path = os.path.join(embargoed_dir, project_dir)
+ if await aiofiles.os.path.isdir(project_dir_path):
+ for version_dir in await aiofiles.os.listdir(project_dir_path):
+ if await aiofiles.os.path.isdir(os.path.join(project_dir_path, version_dir)):
+ version_dir_path = os.path.join(project_dir_path, version_dir)
+ if await aiofiles.os.path.isdir(version_dir_path):
+ filesystem_dirs.append(version_dir_path)
+
+
async def _get_filesystem_dirs_finished(filesystem_dirs: list[str]) -> None:
finished_dir = paths.get_finished_dir()
finished_dir_contents = await aiofiles.os.listdir(finished_dir)
diff --git a/atr/config.py b/atr/config.py
index cfe6c1a5..5d74bdcf 100644
--- a/atr/config.py
+++ b/atr/config.py
@@ -150,6 +150,8 @@ class AppConfig:
JWT_SECRET_KEY = _config_secrets("JWT_SECRET_KEY", STATE_DIR, default=None, cast=str)
FINISHED_STORAGE_DIR = os.path.join(STATE_DIR, "finished")
UNFINISHED_STORAGE_DIR = os.path.join(STATE_DIR, "unfinished")
+ # Embargoed (expedited, pre-release) files live apart from the shared unfinished tree
+ EMBARGOED_STORAGE_DIR = os.path.join(STATE_DIR, "embargoed")
# TODO: By convention this is at /x1/, but we can symlink it here perhaps?
# TODO: We need to get Puppet to check SVN out initially, or do it manually
SVN_STORAGE_DIR = os.path.join(STATE_DIR, "subversion")
@@ -320,6 +322,7 @@ def validate() -> None:
(conf.STATE_DIR, "STATE_DIR"),
(conf.FINISHED_STORAGE_DIR, "FINISHED_STORAGE_DIR"),
(conf.UNFINISHED_STORAGE_DIR, "UNFINISHED_STORAGE_DIR"),
+ (conf.EMBARGOED_STORAGE_DIR, "EMBARGOED_STORAGE_DIR"),
(conf.SVN_STORAGE_DIR, "SVN_STORAGE_DIR"),
(conf.ARCHIVES_STORAGE_DIR, "ARCHIVES_STORAGE_DIR"),
(conf.ATTESTABLE_STORAGE_DIR, "ATTESTABLE_STORAGE_DIR"),
diff --git a/atr/get/file.py b/atr/get/file.py
index 3074ae5a..7a65dee2 100644
--- a/atr/get/file.py
+++ b/atr/get/file.py
@@ -318,7 +318,9 @@ async def _release_file_stats(
version_key: safe.VersionKey,
) -> list[util.FileStat]:
revision_number = release.safe_latest_revision_number
- gen = util.content_list(paths.get_unfinished_dir(), project_key, version_key, revision_number)
+ # Embargoed releases keep their files in a separate root, so list from wherever this one lives.
+ root = paths.get_embargoed_dir() if release.is_embargoed else paths.get_unfinished_dir()
+ gen = util.content_list(root, project_key, version_key, revision_number)
file_stats = [stat async for stat in gen]
file_stats.sort(key=lambda fs: fs.path)
return file_stats
diff --git a/atr/paths.py b/atr/paths.py
index 2423deee..394da95f 100644
--- a/atr/paths.py
+++ b/atr/paths.py
@@ -35,9 +35,14 @@ def audit_release_log_file(project_key: safe.ProjectKey, version_key: safe.Versi
def base_path_for_revision(
- project_key: safe.ProjectKey, version_key: safe.VersionKey, revision: safe.RevisionNumber
+ project_key: safe.ProjectKey,
+ version_key: safe.VersionKey,
+ revision: safe.RevisionNumber,
+ embargoed: bool = False,
) -> safe.StatePath:
- return get_unfinished_dir() / project_key / version_key / revision
+ # Callers without a Release object pass embargoed explicitly; it's always the release's
+ # is_embargoed, so the root choice matches _release_root.
+ return _unfinished_root(embargoed) / project_key / version_key / revision
def closer_download_url(path: str | safe.RelPath) -> str:
@@ -85,6 +90,10 @@ def get_catalog_site_dir() -> safe.StatePath:
return safe.StatePath(pathlib.Path(config.get().CATALOG_SITE_DIR))
+def get_embargoed_dir() -> safe.StatePath:
+ return safe.StatePath(pathlib.Path(config.get().EMBARGOED_STORAGE_DIR))
+
+
def get_finished_dir() -> safe.StatePath:
return safe.StatePath(pathlib.Path(config.get().FINISHED_STORAGE_DIR))
@@ -111,9 +120,12 @@ def get_unfinished_dir() -> safe.StatePath:
def get_unfinished_dir_for(
- project_key: safe.ProjectKey, version_key: safe.VersionKey, revision: safe.RevisionNumber
+ project_key: safe.ProjectKey,
+ version_key: safe.VersionKey,
+ revision: safe.RevisionNumber,
+ embargoed: bool = False,
) -> safe.StatePath:
- return get_unfinished_dir() / project_key / version_key / revision
+ return base_path_for_revision(project_key, version_key, revision, embargoed=embargoed)
def get_unfinished_tombstone_for(project_key: safe.ProjectKey, version_key: safe.VersionKey) -> safe.StatePath:
@@ -136,62 +148,55 @@ def release_directory(release: sql.Release) -> safe.StatePath:
def release_directory_base(release: sql.Release) -> safe.StatePath:
- """Determine the filesystem directory for a given release based on its phase."""
- phase = release.phase
- project_key = release.project_key
- version_key = release.version
-
- base_dir: safe.StatePath | None = None
- match phase:
- case sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT:
- base_dir = get_unfinished_dir()
- case sql.ReleasePhase.RELEASE_CANDIDATE:
- base_dir = get_unfinished_dir()
- case sql.ReleasePhase.RELEASE_PREVIEW:
- base_dir = get_unfinished_dir()
- case sql.ReleasePhase.RELEASE:
- base_dir = get_finished_dir()
- # Do not add "case _" here
- return base_dir / project_key / version_key
+ """Determine the filesystem directory for a given release based on its phase and embargo status."""
+ return _release_root(release) / release.project_key / release.version
def release_directory_revision(release: sql.Release) -> safe.StatePath | None:
"""Return the path to the directory containing the active files for a given release phase."""
- path_project = release.project_key
- path_version = release.version
- match release.phase:
- case (
- sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT
- | sql.ReleasePhase.RELEASE_CANDIDATE
- | sql.ReleasePhase.RELEASE_PREVIEW
- ):
- if (path_revision := release.latest_revision_number) is None:
- return None
- path = get_unfinished_dir() / path_project / path_version / path_revision
- case sql.ReleasePhase.RELEASE:
- path = get_finished_dir() / path_project / path_version
- # Do not add "case _" here
- return path
+ base = _release_root(release) / release.project_key / release.version
+ # A released release has no revision subdirectory; everything earlier is revision-scoped.
+ if release.phase == sql.ReleasePhase.RELEASE:
+ return base
+ if (path_revision := release.latest_revision_number) is None:
+ return None
+ return base / path_revision
def release_directory_version(release: sql.Release) -> safe.StatePath:
"""Return the path to the directory containing the active files for a given release phase."""
- path_project = release.project_key
- path_version = release.version
+ return _release_root(release) / release.project_key / release.version
+
+
+def revision_path_for_file(
+ project_key: safe.ProjectKey,
+ version_key: safe.VersionKey,
+ revision: safe.RevisionNumber,
+ file_name: str,
+ embargoed: bool = False,
+) -> safe.StatePath:
+ return base_path_for_revision(project_key, version_key, revision, embargoed=embargoed) / file_name
+
+
+def _release_root(release: sql.Release) -> safe.StatePath:
+ # The phase decides the tree: a release's files sit in the unfinished area until it's
+ # published, and in the finished area thereafter. Embargo only shifts the pre-publication
+ # root, so it applies within the unfinished cases and never to a published release.
+ base_dir: safe.StatePath | None = None
match release.phase:
- case (
- sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT
- | sql.ReleasePhase.RELEASE_CANDIDATE
- | sql.ReleasePhase.RELEASE_PREVIEW
- ):
- path = get_unfinished_dir() / path_project / path_version
+ case sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT:
+ base_dir = _unfinished_root(release.is_embargoed)
+ case sql.ReleasePhase.RELEASE_CANDIDATE:
+ base_dir = _unfinished_root(release.is_embargoed)
+ case sql.ReleasePhase.RELEASE_PREVIEW:
+ base_dir = _unfinished_root(release.is_embargoed)
case sql.ReleasePhase.RELEASE:
- path = get_finished_dir() / path_project / path_version
+ base_dir = get_finished_dir()
# Do not add "case _" here
- return path
+ return base_dir
-def revision_path_for_file(
- project_key: safe.ProjectKey, version_key: safe.VersionKey, revision: safe.RevisionNumber, file_name: str
-) -> safe.StatePath:
- return base_path_for_revision(project_key, version_key, revision) / file_name
+def _unfinished_root(embargoed: bool) -> safe.StatePath:
+ # Before publication a release's files live in the unfinished tree, unless it's embargoed,
+ # in which case they're kept apart in the embargoed tree.
+ return get_embargoed_dir() if embargoed else get_unfinished_dir()
diff --git a/atr/server.py b/atr/server.py
index f6fb1cff..05452f65 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -190,6 +190,7 @@ def _app_dirs_setup(state_dir_str: str) -> None:
pathlib.Path(state_dir_str) / "secrets" / "generated",
pathlib.Path(paths.get_archives_dir()),
pathlib.Path(paths.get_catalog_site_dir()),
+ pathlib.Path(paths.get_embargoed_dir()),
pathlib.Path(paths.get_finished_dir()),
pathlib.Path(paths.get_quarantined_dir()),
pathlib.Path(paths.get_runtime_dir()),
@@ -198,6 +199,7 @@ def _app_dirs_setup(state_dir_str: str) -> None:
]
archives_dir = pathlib.Path(paths.get_archives_dir())
unfinished_dir = pathlib.Path(paths.get_unfinished_dir())
+ embargoed_dir = pathlib.Path(paths.get_embargoed_dir())
enforce_permissions = not config.is_dev_environment()
for directory in directories_to_ensure:
directory.mkdir(parents=True, exist_ok=True)
@@ -206,8 +208,9 @@ def _app_dirs_setup(state_dir_str: str) -> None:
# Some directories need custom permissions
if directory == archives_dir:
_enforce_archives_permissions(archives_dir)
- elif directory == unfinished_dir:
- _enforce_unfinished_permissions(unfinished_dir)
+ elif directory in (unfinished_dir, embargoed_dir):
+ # The embargoed tree holds the same immutable revision layout as unfinished
+ _enforce_unfinished_permissions(directory)
else:
util.chmod_directories(directory, permissions=0o755)
@@ -1214,9 +1217,13 @@ async def _reset_request_log_context():
def _set_file_permissions_to_read_only() -> None:
- """Set permissions of all files in the unfinished and finished directories to read only."""
+ """Set permissions of all files in the unfinished, embargoed and finished directories to read only."""
# TODO: After a migration period, incorrect permissions should be an error
- directories = [pathlib.Path(paths.get_unfinished_dir()), pathlib.Path(paths.get_finished_dir())]
+ directories = [
+ pathlib.Path(paths.get_unfinished_dir()),
+ pathlib.Path(paths.get_embargoed_dir()),
+ pathlib.Path(paths.get_finished_dir()),
+ ]
fixed_count = 0
for directory in directories:
if not directory.exists():
diff --git a/atr/tasks/__init__.py b/atr/tasks/__init__.py
index eef8ea4e..dca9b4f5 100644
--- a/atr/tasks/__init__.py
+++ b/atr/tasks/__init__.py
@@ -207,11 +207,6 @@ async def draft_checks(
suffix_filter: list[str] | None = None,
) -> int:
"""Core logic to analyse a draft revision and queue checks."""
- # Construct path to the specific revision
- # We don't have the release object here, so we can't use util.release_directory
- revision_path = file_paths.get_unfinished_dir() / str(project_key) / str(release_version) / str(revision_number)
- relative_paths = [path async for path in util.paths_recursive(revision_path)]
-
async with db.ensure_session(caller_data) as data:
release = await data.release(
project_key=str(project_key),
@@ -220,6 +215,11 @@ async def draft_checks(
_release_policy=True,
_project_release_policy=True,
).demand(RuntimeError("Release not found"))
+ # Read the revision files from the tree that matches the release's embargo status.
+ revision_path = file_paths.base_path_for_revision(
+ project_key, release_version, revision_number, embargoed=release.is_embargoed
+ )
+ relative_paths = [path async for path in util.paths_recursive(revision_path)]
other_releases = (
await data.release(project_key=str(project_key), phase=sql.ReleasePhase.RELEASE)
.order_by(sql.Release.released)
diff --git a/atr/tasks/checks/__init__.py b/atr/tasks/checks/__init__.py
index c0365005..bed0abc4 100644
--- a/atr/tasks/checks/__init__.py
+++ b/atr/tasks/checks/__init__.py
@@ -67,6 +67,7 @@ class Recorder:
member_rel_path: str | None
revision_number: safe.RevisionNumber
afresh: bool
+ embargoed: bool
__cached: bool
__input_hash: str | None
@@ -81,6 +82,7 @@ class Recorder:
primary_rel_path: safe.RelPath | None = None,
member_rel_path: str | None = None,
afresh: bool = True,
+ embargoed: bool = False,
) -> None:
self.checker = function_key(checker)
self.checker_version = checker_version
@@ -89,6 +91,7 @@ class Recorder:
self.primary_rel_path = primary_rel_path
self.member_rel_path = member_rel_path
self.afresh = afresh
+ self.embargoed = embargoed
self.constructed = False
self.member_problems: dict[sql.CheckResultStatus, int] = {}
self.__cached = False
@@ -109,6 +112,7 @@ class Recorder:
primary_rel_path: safe.RelPath | None = None,
member_rel_path: str | None = None,
afresh: bool = True,
+ embargoed: bool = False,
) -> Recorder:
recorder = cls(
checker,
@@ -120,6 +124,7 @@ class Recorder:
primary_rel_path,
member_rel_path,
afresh,
+ embargoed,
)
if afresh is True:
# Clear outer path whether it's specified or not
@@ -191,7 +196,9 @@ class Recorder:
return self.abs_path_base() / rel_path_part
def abs_path_base(self) -> safe.StatePath:
- return file_paths.base_path_for_revision(self.project_key, self.version_key, self.revision_number)
+ return file_paths.base_path_for_revision(
+ self.project_key, self.version_key, self.revision_number, embargoed=self.embargoed
+ )
async def project(self) -> sql.Project:
# TODO: Cache project
@@ -352,7 +359,11 @@ async def resolve_archive_dir(args: FunctionArguments) -> safe.StatePath | None:
content_hash = await data.release_file_hash_at(release.key, str(args.primary_rel_path), revision_seq)
if content_hash is None:
abs_path = file_paths.revision_path_for_file(
- args.project_key, args.version_key, args.revision_number, str(args.primary_rel_path)
+ args.project_key,
+ args.version_key,
+ args.revision_number,
+ str(args.primary_rel_path),
+ embargoed=release.is_embargoed,
)
if await aiofiles.os.path.isfile(abs_path):
content_hash = await hashes.compute_file_hash(abs_path)
@@ -397,7 +408,11 @@ async def resolve_cache_key( # noqa: C901
if file_hash is None:
if path is None:
path = file_paths.revision_path_for_file(
- release.safe_project_key, release.safe_version_key, revision, file or ""
+ release.safe_project_key,
+ release.safe_version_key,
+ revision,
+ file or "",
+ embargoed=release.is_embargoed,
)
file_hash = await hashes.compute_file_hash(path)
if file_hash:
@@ -456,7 +471,10 @@ async def _resolve_all_files(release: sql.Release, rel_path: str | None = None)
return []
if not (
base_path := file_paths.base_path_for_revision(
- release.safe_project_key, release.safe_version_key, release.safe_latest_revision_number
+ release.safe_project_key,
+ release.safe_version_key,
+ release.safe_latest_revision_number,
+ embargoed=release.is_embargoed,
)
):
return []
@@ -539,7 +557,11 @@ async def _resolve_suffixed_file_existence(release: sql.Release, rel_path: str |
entries = []
for candidate in analysis.sbom_candidates(rel_path, analysis.SBOM_SUFFIXES):
abs_path = file_paths.revision_path_for_file(
- release.safe_project_key, release.safe_version_key, release.safe_latest_revision_number, candidate
+ release.safe_project_key,
+ release.safe_version_key,
+ release.safe_latest_revision_number,
+ candidate,
+ embargoed=release.is_embargoed,
)
entries.append(f"{candidate}={await aiofiles.os.path.exists(abs_path.path)}")
return sorted(entries)
@@ -549,7 +571,11 @@ async def _resolve_unsuffixed_file_hash(release: sql.Release, rel_path: str | No
if (not rel_path) or (not release.latest_revision_number):
return ""
abs_path = file_paths.revision_path_for_file(
- release.safe_project_key, release.safe_version_key, release.safe_latest_revision_number, rel_path
+ release.safe_project_key,
+ release.safe_version_key,
+ release.safe_latest_revision_number,
+ rel_path,
+ embargoed=release.is_embargoed,
)
plain_path = abs_path.path.with_suffix("")
if await aiofiles.os.path.isfile(plain_path):
diff --git a/atr/tasks/sbom.py b/atr/tasks/sbom.py
index 2d71daa5..3bf1bd7c 100644
--- a/atr/tasks/sbom.py
+++ b/atr/tasks/sbom.py
@@ -86,7 +86,10 @@ async def augment(args: args.FileArgs) -> results.Results | None:
revision_str = str(args.revision_number)
path_str = str(args.file_path)
- base_dir = paths.get_unfinished_dir_for(args.project_key, args.version_key, args.revision_number)
+ embargoed = await _release_is_embargoed(args.project_key, args.version_key)
+ base_dir = paths.get_unfinished_dir_for(
+ args.project_key, args.version_key, args.revision_number, embargoed=embargoed
+ )
if not await aiofiles.os.path.isdir(base_dir):
raise SBOMScoringError("Revision directory does not exist", {"base_dir": str(base_dir)})
full_path = base_dir / path_str
@@ -155,7 +158,10 @@ async def generate(args: args.FileArgs) -> results.Results | None:
revision_str = str(args.revision_number)
path_str = str(args.file_path)
- base_dir = paths.get_unfinished_dir_for(args.project_key, args.version_key, args.revision_number)
+ embargoed = await _release_is_embargoed(args.project_key, args.version_key)
+ base_dir = paths.get_unfinished_dir_for(
+ args.project_key, args.version_key, args.revision_number, embargoed=embargoed
+ )
if not await aiofiles.os.path.isdir(base_dir):
raise SBOMGenerationError("Revision directory does not exist", {"base_dir": str(base_dir)})
if not await aiofiles.os.path.isfile(base_dir / path_str):
@@ -238,7 +244,10 @@ async def osv_scan(args: args.FileArgs) -> results.Results | None:
revision_str = str(args.revision_number)
path_str = str(args.file_path)
- base_dir = paths.get_unfinished_dir_for(args.project_key, args.version_key, args.revision_number)
+ embargoed = await _release_is_embargoed(args.project_key, args.version_key)
+ base_dir = paths.get_unfinished_dir_for(
+ args.project_key, args.version_key, args.revision_number, embargoed=embargoed
+ )
if not await aiofiles.os.path.isdir(base_dir):
raise SBOMScanningError("Revision directory does not exist", {"base_dir": str(base_dir)})
full_path = base_dir / path_str
@@ -311,7 +320,10 @@ async def osv_scan(args: args.FileArgs) -> results.Results | None:
async def score_qs(args: args.FileArgs) -> results.Results | None:
path_str = str(args.file_path)
- base_dir = paths.get_unfinished_dir_for(args.project_key, args.version_key, args.revision_number)
+ embargoed = await _release_is_embargoed(args.project_key, args.version_key)
+ base_dir = paths.get_unfinished_dir_for(
+ args.project_key, args.version_key, args.revision_number, embargoed=embargoed
+ )
if not await aiofiles.os.path.isdir(base_dir):
raise SBOMScoringError("Revision directory does not exist", {"base_dir": str(base_dir)})
full_path = base_dir / path_str
@@ -350,7 +362,10 @@ async def score_qs(args: args.FileArgs) -> results.Results | None:
async def score_tool(args: args.ScoreArgs) -> results.Results | None:
path_str = str(args.file_path)
- base_dir = paths.get_unfinished_dir_for(args.project_key, args.version_key, args.revision_number)
+ embargoed = await _release_is_embargoed(args.project_key, args.version_key)
+ base_dir = paths.get_unfinished_dir_for(
+ args.project_key, args.version_key, args.revision_number, embargoed=embargoed
+ )
if not await aiofiles.os.path.isdir(base_dir):
raise SBOMScoringError("Revision directory does not exist", {"base_dir": str(base_dir)})
full_path = base_dir / path_str
@@ -662,6 +677,14 @@ def _promote_primary_component(doc: dict[str, Any]) -> None:
doc["components"] = [c for c in doc.get("components", []) if c is not root_component]
+async def _release_is_embargoed(project_key: safe.ProjectKey, version_key: safe.VersionKey) -> bool:
+ # Embargoed releases keep their files in a separate root, so the source revision has to be read
+ # from there. A missing release falls back to the shared unfinished tree.
+ async with db.session() as data:
+ release = await data.release(project_key=str(project_key), version=str(version_key)).get()
+ return bool(release and release.is_embargoed)
+
+
async def _sbom_describes_source(args: args.ScoreArgs, sbom_rel_path: str) -> bool:
# Find the classification of the artifact this SBOM pairs with
artifact_rel = None
diff --git a/atr/worker.py b/atr/worker.py
index 6acaea3c..117f8761 100644
--- a/atr/worker.py
+++ b/atr/worker.py
@@ -130,6 +130,12 @@ async def _execute_check_task(
version_key = safe.VersionKey(task_obj.version_key)
revision_number = safe.RevisionNumber(task_obj.revision_number)
+ # An embargoed release keeps its files in a separate root, so the recorder needs to know
+ # which tree to read from. A missing release falls back to the shared unfinished tree.
+ async with db.session() as data:
+ release = await data.release(project_key=str(project_key), version=str(version_key)).get()
+ embargoed = bool(release and release.is_embargoed)
+
async def recorder_factory(checker_version: str | None = None) -> checks.Recorder:
return await checks.Recorder.create(
checker=handler,
@@ -139,6 +145,7 @@ async def _execute_check_task(
version_key=version_key,
revision_number=revision_number,
primary_rel_path=task_obj.safe_primary_rel_path,
+ embargoed=embargoed,
)
function_arguments = checks.FunctionArguments(
diff --git a/tests/unit/test_admin_consistency.py b/tests/unit/test_admin_consistency.py
index d49fbccf..e9cf9209 100644
--- a/tests/unit/test_admin_consistency.py
+++ b/tests/unit/test_admin_consistency.py
@@ -25,11 +25,13 @@ import atr.config as config
import atr.models.sql as sql
-def release(phase: sql.ReleasePhase, project_key: str, version: str) -> mock.MagicMock:
+def release(phase: sql.ReleasePhase, project_key: str, version: str, expedited: bool = False) -> mock.MagicMock:
result = mock.MagicMock()
result.phase = phase
result.project_key = project_key
result.version = version
+ # Mirror the real is_embargoed property, so path resolution picks the right storage root.
+ result.is_embargoed = expedited and (phase != sql.ReleasePhase.RELEASE)
return result
diff --git a/tests/unit/test_paths.py b/tests/unit/test_paths.py
index c076854a..b52ba0c8 100644
--- a/tests/unit/test_paths.py
+++ b/tests/unit/test_paths.py
@@ -21,9 +21,30 @@ import types
import pytest
import atr.models.safe as safe
+import atr.models.sql as sql
import atr.paths as paths
+def _storage_config(tmp_path: pathlib.Path) -> types.SimpleNamespace:
+ return types.SimpleNamespace(
+ UNFINISHED_STORAGE_DIR=str(tmp_path / "unfinished"),
+ EMBARGOED_STORAGE_DIR=str(tmp_path / "embargoed"),
+ FINISHED_STORAGE_DIR=str(tmp_path / "finished"),
+ )
+
+
+def _release(phase: sql.ReleasePhase, embargoed: bool, revision: str | None = "00001") -> types.SimpleNamespace:
+ # is_embargoed is a property on the real model; here we set it directly so each branch is
+ # exercised in isolation, independent of the expedited/phase combination that produces it.
+ return types.SimpleNamespace(
+ project_key="example",
+ version="1.2.3",
+ phase=phase,
+ is_embargoed=embargoed,
+ latest_revision_number=revision,
+ )
+
+
def test_committee_dist_relpath_for_podling() -> None:
committee = types.SimpleNamespace(key="myproject", is_podling=True)
@@ -88,3 +109,70 @@ def test_quarantine_directory_rejects_non_alnum_token():
quarantined = types.SimpleNamespace(token="../escape")
with pytest.raises(ValueError, match="Invalid quarantine token"):
paths.quarantine_directory(quarantined)
+
+
+def test_get_embargoed_dir_uses_embargoed_storage_dir(monkeypatch, tmp_path: pathlib.Path):
+ monkeypatch.setattr("atr.config.get", lambda: _storage_config(tmp_path))
+ assert paths.get_embargoed_dir().path == tmp_path / "embargoed"
+
+
+def test_release_directory_base_routes_embargoed_release_to_embargoed_root(monkeypatch, tmp_path: pathlib.Path):
+ monkeypatch.setattr("atr.config.get", lambda: _storage_config(tmp_path))
+ release = _release(sql.ReleasePhase.RELEASE_CANDIDATE, embargoed=True)
+ assert paths.release_directory_base(release).path == tmp_path / "embargoed" / "example" / "1.2.3"
+
+
+def test_release_directory_base_routes_unembargoed_draft_to_unfinished_root(monkeypatch, tmp_path: pathlib.Path):
+ monkeypatch.setattr("atr.config.get", lambda: _storage_config(tmp_path))
+ release = _release(sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT, embargoed=False)
+ assert paths.release_directory_base(release).path == tmp_path / "unfinished" / "example" / "1.2.3"
+
+
+def test_release_directory_base_routes_released_to_finished_root(monkeypatch, tmp_path: pathlib.Path):
+ monkeypatch.setattr("atr.config.get", lambda: _storage_config(tmp_path))
+ release = _release(sql.ReleasePhase.RELEASE, embargoed=False)
+ assert paths.release_directory_base(release).path == tmp_path / "finished" / "example" / "1.2.3"
+
+
+def test_release_directory_includes_revision_under_embargoed_root(monkeypatch, tmp_path: pathlib.Path):
+ monkeypatch.setattr("atr.config.get", lambda: _storage_config(tmp_path))
+ release = _release(sql.ReleasePhase.RELEASE_PREVIEW, embargoed=True, revision="00007")
+ assert paths.release_directory(release).path == tmp_path / "embargoed" / "example" / "1.2.3" / "00007"
+
+
+def test_release_directory_revision_uses_embargoed_root(monkeypatch, tmp_path: pathlib.Path):
+ monkeypatch.setattr("atr.config.get", lambda: _storage_config(tmp_path))
+ release = _release(sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT, embargoed=True, revision="00003")
+ revision_dir = paths.release_directory_revision(release)
+ assert revision_dir is not None
+ assert revision_dir.path == tmp_path / "embargoed" / "example" / "1.2.3" / "00003"
+
+
+def test_release_directory_version_uses_embargoed_root(monkeypatch, tmp_path: pathlib.Path):
+ monkeypatch.setattr("atr.config.get", lambda: _storage_config(tmp_path))
+ release = _release(sql.ReleasePhase.RELEASE_CANDIDATE, embargoed=True)
+ assert paths.release_directory_version(release).path == tmp_path / "embargoed" / "example" / "1.2.3"
+
+
+def test_base_path_for_revision_defaults_to_unfinished_root(monkeypatch, tmp_path: pathlib.Path):
+ monkeypatch.setattr("atr.config.get", lambda: _storage_config(tmp_path))
+ result = paths.base_path_for_revision("example", "1.2.3", "00001")
+ assert result.path == tmp_path / "unfinished" / "example" / "1.2.3" / "00001"
+
+
+def test_base_path_for_revision_uses_embargoed_root_when_flagged(monkeypatch, tmp_path: pathlib.Path):
+ monkeypatch.setattr("atr.config.get", lambda: _storage_config(tmp_path))
+ result = paths.base_path_for_revision("example", "1.2.3", "00001", embargoed=True)
+ assert result.path == tmp_path / "embargoed" / "example" / "1.2.3" / "00001"
+
+
+def test_get_unfinished_dir_for_uses_embargoed_root_when_flagged(monkeypatch, tmp_path: pathlib.Path):
+ monkeypatch.setattr("atr.config.get", lambda: _storage_config(tmp_path))
+ result = paths.get_unfinished_dir_for("example", "1.2.3", "00001", embargoed=True)
+ assert result.path == tmp_path / "embargoed" / "example" / "1.2.3" / "00001"
+
+
+def test_revision_path_for_file_uses_embargoed_root_when_flagged(monkeypatch, tmp_path: pathlib.Path):
+ monkeypatch.setattr("atr.config.get", lambda: _storage_config(tmp_path))
+ result = paths.revision_path_for_file("example", "1.2.3", "00001", "file.tar.gz", embargoed=True)
+ assert result.path == tmp_path / "embargoed" / "example" / "1.2.3" / "00001" / "file.tar.gz"
diff --git a/tests/unit/test_sbom_osv.py b/tests/unit/test_sbom_osv.py
index 7e1220b5..9e2db956 100644
--- a/tests/unit/test_sbom_osv.py
+++ b/tests/unit/test_sbom_osv.py
@@ -75,7 +75,8 @@ def sleeps(monkeypatch):
async def test_osv_scan_reports_truncated_response_as_unavailable(tmp_path, monkeypatch):
rel_path = safe.RelPath("artifact.cdx.json")
(tmp_path / str(rel_path)).write_text("{}", encoding="utf-8")
- monkeypatch.setattr(tasks_sbom.paths, "get_unfinished_dir_for", lambda *_args: safe.StatePath(tmp_path))
+ monkeypatch.setattr(tasks_sbom, "_release_is_embargoed", mock.AsyncMock(return_value=False))
+ monkeypatch.setattr(tasks_sbom.paths, "get_unfinished_dir_for", lambda *_args, **_kwargs: safe.StatePath(tmp_path))
monkeypatch.setattr(tasks_sbom.sbom.utilities, "path_to_bundle", lambda _path: mock.sentinel.bundle)
monkeypatch.setattr(
tasks_sbom.sbom.osv,
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org