Skip to content

Commit e1344ab

Browse files
authored
Merge pull request #143 from lsst-sqre/tickets/DM-54492
DM-54492: Edition updates and tracking
2 parents 51d81f5 + 157a6a5 commit e1344ab

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+6893
-117
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Add edition_build_history table.
2+
3+
Logs every build that an edition has pointed to, enabling rollback
4+
and orphan detection.
5+
6+
Revision ID: g5h6i7j8k9l0
7+
Revises: f4a5b6c7d8e9
8+
Create Date: 2026-03-27 00:00:00.000000+00:00
9+
"""
10+
11+
import sqlalchemy as sa
12+
13+
from alembic import op
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = "g5h6i7j8k9l0"
17+
down_revision: str | None = "f4a5b6c7d8e9"
18+
branch_labels: str | None = None
19+
depends_on: str | None = None
20+
21+
22+
def upgrade() -> None:
23+
op.create_table(
24+
"edition_build_history",
25+
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
26+
sa.Column(
27+
"edition_id",
28+
sa.Integer,
29+
sa.ForeignKey("editions.id", ondelete="CASCADE"),
30+
nullable=False,
31+
),
32+
sa.Column(
33+
"build_id",
34+
sa.Integer,
35+
sa.ForeignKey("builds.id", ondelete="CASCADE"),
36+
nullable=False,
37+
),
38+
sa.Column("position", sa.Integer, nullable=False),
39+
sa.Column(
40+
"date_created",
41+
sa.DateTime(timezone=True),
42+
server_default=sa.func.now(),
43+
nullable=False,
44+
),
45+
)
46+
op.create_index(
47+
"idx_ebh_edition_id", "edition_build_history", ["edition_id"]
48+
)
49+
op.create_index(
50+
"idx_ebh_edition_position",
51+
"edition_build_history",
52+
["edition_id", "position"],
53+
)
54+
55+
56+
def downgrade() -> None:
57+
op.drop_table("edition_build_history")
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Add default_edition_config column to organizations.
2+
3+
Stores the organization-level default tracking configuration for
4+
the __main edition created on new projects.
5+
6+
Revision ID: h6i7j8k9l0m1
7+
Revises: g5h6i7j8k9l0
8+
Create Date: 2026-03-27 00:01:00.000000+00:00
9+
"""
10+
11+
import sqlalchemy as sa
12+
from sqlalchemy.dialects.postgresql import JSONB
13+
14+
from alembic import op
15+
16+
# revision identifiers, used by Alembic.
17+
revision: str = "h6i7j8k9l0m1"
18+
down_revision: str | None = "g5h6i7j8k9l0"
19+
branch_labels: str | None = None
20+
depends_on: str | None = None
21+
22+
23+
def upgrade() -> None:
24+
op.add_column(
25+
"organizations",
26+
sa.Column("default_edition_config", JSONB, nullable=True),
27+
)
28+
29+
30+
def downgrade() -> None:
31+
op.drop_column("organizations", "default_edition_config")
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Auto-detection of build provenance annotations from CI environments."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
7+
from .models.builds import BuildAnnotations
8+
9+
__all__ = [
10+
"detect_github_actions_annotations",
11+
"merge_annotations",
12+
]
13+
14+
15+
def detect_github_actions_annotations() -> BuildAnnotations | None:
16+
"""Detect build provenance from GitHub Actions environment variables.
17+
18+
Returns a populated ``BuildAnnotations`` if running inside GitHub
19+
Actions (``GITHUB_ACTIONS == "true"``), otherwise ``None``.
20+
"""
21+
if os.environ.get("GITHUB_ACTIONS") != "true":
22+
return None
23+
24+
server_url = os.environ.get("GITHUB_SERVER_URL", "https://github.com")
25+
repository = os.environ.get("GITHUB_REPOSITORY")
26+
run_id = os.environ.get("GITHUB_RUN_ID")
27+
28+
run_url: str | None = None
29+
if repository and run_id:
30+
run_url = f"{server_url}/{repository}/actions/runs/{run_id}"
31+
32+
return BuildAnnotations(
33+
commit_sha=os.environ.get("GITHUB_SHA"),
34+
github_repository=repository,
35+
github_run_id=run_id,
36+
github_run_url=run_url,
37+
github_run_attempt=os.environ.get("GITHUB_RUN_ATTEMPT"),
38+
github_workflow=os.environ.get("GITHUB_WORKFLOW"),
39+
github_actor=os.environ.get("GITHUB_ACTOR"),
40+
github_event_name=os.environ.get("GITHUB_EVENT_NAME"),
41+
ci_platform="github-actions",
42+
)
43+
44+
45+
def merge_annotations(
46+
auto: BuildAnnotations | None,
47+
manual: dict[str, str] | None,
48+
) -> BuildAnnotations | None:
49+
"""Merge auto-detected and manual annotations.
50+
51+
Manual entries take precedence over auto-detected values. Returns
52+
``None`` if both inputs are empty/None.
53+
"""
54+
result: dict[str, str] = {}
55+
if auto is not None:
56+
result.update(
57+
{k: v for k, v in auto.model_dump().items() if v is not None}
58+
)
59+
if manual:
60+
result.update(manual)
61+
if not result:
62+
return None
63+
return BuildAnnotations.model_validate(result)

client/src/docverse/client/_cli.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import click
1111

12+
from ._annotations import detect_github_actions_annotations, merge_annotations
1213
from ._client import DocverseClient
1314
from ._exceptions import BuildProcessingError, DocverseClientError
1415
from ._tar import create_tarball
@@ -76,6 +77,19 @@ def main() -> None:
7677
default=None,
7778
help="Alternate deployment name.",
7879
)
80+
@click.option(
81+
"--annotation",
82+
"-a",
83+
"annotations",
84+
multiple=True,
85+
help="Manual annotation in KEY=VALUE format. Can be repeated.",
86+
)
87+
@click.option(
88+
"--auto-annotations/--no-auto-annotations",
89+
default=True,
90+
show_default=True,
91+
help="Auto-detect annotations from CI environment variables.",
92+
)
7993
@click.option(
8094
"--no-wait",
8195
is_flag=True,
@@ -97,12 +111,26 @@ def upload( # noqa: PLR0913
97111
token: str,
98112
base_url: str,
99113
alternate_name: str | None,
114+
annotations: tuple[str, ...],
115+
auto_annotations: bool, # noqa: FBT001
100116
no_wait: bool, # noqa: FBT001
101117
verbose: bool, # noqa: FBT001
102118
) -> None:
103119
"""Upload a documentation build."""
104120
if git_ref is None:
105121
git_ref = _detect_git_ref()
122+
123+
# Parse manual annotations
124+
manual: dict[str, str] | None = None
125+
if annotations:
126+
manual = {}
127+
for item in annotations:
128+
if "=" not in item:
129+
msg = f"Invalid annotation format (expected KEY=VALUE): {item}"
130+
raise click.BadParameter(msg, param_hint="'--annotation'")
131+
key, value = item.split("=", 1)
132+
manual[key] = value
133+
106134
asyncio.run(
107135
_upload_async(
108136
org=org,
@@ -112,6 +140,8 @@ def upload( # noqa: PLR0913
112140
token=token,
113141
base_url=base_url,
114142
alternate_name=alternate_name,
143+
auto_annotations=auto_annotations,
144+
manual_annotations=manual,
115145
no_wait=no_wait,
116146
verbose=verbose,
117147
)
@@ -145,6 +175,8 @@ async def _upload_async( # noqa: PLR0913
145175
token: str,
146176
base_url: str,
147177
alternate_name: str | None,
178+
auto_annotations: bool,
179+
manual_annotations: dict[str, str] | None,
148180
no_wait: bool,
149181
verbose: bool,
150182
) -> None:
@@ -155,6 +187,12 @@ async def _upload_async( # noqa: PLR0913
155187
tarball_path, content_hash = create_tarball(source_dir)
156188
click.echo(f"Content hash: {content_hash}")
157189

190+
# Build annotations from auto-detection and manual entries
191+
auto = (
192+
detect_github_actions_annotations() if auto_annotations else None
193+
)
194+
merged_annotations = merge_annotations(auto, manual_annotations)
195+
158196
async with DocverseClient(base_url, token, verbose=verbose) as client:
159197
click.echo(f"Creating build for {org}/{project} @ {git_ref}")
160198
build = await client.create_build(
@@ -163,6 +201,7 @@ async def _upload_async( # noqa: PLR0913
163201
git_ref=git_ref,
164202
content_hash=content_hash,
165203
alternate_name=alternate_name,
204+
annotations=merged_annotations,
166205
)
167206
click.echo(f"Build created: {build.id}")
168207

client/src/docverse/client/_client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from ._exceptions import BuildProcessingError, DocverseClientError
1414
from .models import Build, BuildStatus, BuildUpdate, QueueJob
15+
from .models.builds import BuildAnnotations
1516
from .models.queue_enums import JobStatus
1617

1718
__all__ = ["DocverseClient"]
@@ -130,7 +131,7 @@ async def create_build( # noqa: PLR0913
130131
git_ref: str,
131132
content_hash: str,
132133
alternate_name: str | None = None,
133-
annotations: dict[str, Any] | None = None,
134+
annotations: BuildAnnotations | None = None,
134135
) -> Build:
135136
"""Create a new build.
136137
@@ -161,7 +162,7 @@ async def create_build( # noqa: PLR0913
161162
if alternate_name is not None:
162163
payload["alternate_name"] = alternate_name
163164
if annotations is not None:
164-
payload["annotations"] = annotations
165+
payload["annotations"] = annotations.model_dump(exclude_none=True)
165166

166167
url = f"/orgs/{org}/projects/{project}/builds"
167168
response = await self._client.post(url, json=payload)

client/src/docverse/client/models/__init__.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
"""Pydantic models for the Docverse API."""
22

3-
from .builds import Build, BuildCreate, BuildStatus, BuildUpdate
3+
from .builds import (
4+
Build,
5+
BuildAnnotations,
6+
BuildCreate,
7+
BuildStatus,
8+
BuildUpdate,
9+
)
410
from .credentials import (
511
AwsCredentials,
612
CloudflareCredentials,
@@ -12,9 +18,12 @@
1218
S3Credentials,
1319
)
1420
from .editions import (
21+
DefaultEditionConfig,
1522
Edition,
23+
EditionBuildHistoryEntry,
1624
EditionCreate,
1725
EditionKind,
26+
EditionRollback,
1827
EditionUpdate,
1928
TrackingMode,
2029
)
@@ -49,15 +58,19 @@
4958
__all__ = [
5059
"AwsCredentials",
5160
"Build",
61+
"BuildAnnotations",
5262
"BuildCreate",
5363
"BuildStatus",
5464
"BuildUpdate",
5565
"CloudflareCredentials",
5666
"CredentialPayload",
5767
"CredentialProvider",
68+
"DefaultEditionConfig",
5869
"Edition",
70+
"EditionBuildHistoryEntry",
5971
"EditionCreate",
6072
"EditionKind",
73+
"EditionRollback",
6174
"EditionUpdate",
6275
"FastlyCredentials",
6376
"GcpCredentials",

0 commit comments

Comments
 (0)