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-atr-experiments.git
The following commit(s) were added to refs/heads/main by this push:
new b8b196a Improve the release candidate workflow
b8b196a is described below
commit b8b196a76bf66edab2d72eca1c6b306f950fa3e3
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Feb 19 19:24:54 2025 +0200
Improve the release candidate workflow
---
atr/config.py | 2 -
atr/db/__init__.py | 4 +-
atr/db/models.py | 15 +-
atr/routes.py | 143 +++++++-----
atr/static/css/atr.css | 6 +
.../{release-attach.html => candidate-attach.html} | 10 +-
.../{release-create.html => candidate-create.html} | 0
atr/templates/candidate-review.html | 163 ++++++++++++++
...verify.html => candidate-signature-verify.html} | 86 +++++---
atr/templates/includes/sidebar.html | 22 +-
atr/templates/index.html | 11 +-
.../{user-keys-add.html => keys-add.html} | 134 +-----------
atr/templates/keys-review.html | 141 ++++++++++++
atr/templates/pages.html | 240 ---------------------
atr/templates/user-uploads.html | 103 ---------
...al_schema.py => b561e6142755_initial_schema.py} | 6 +-
16 files changed, 503 insertions(+), 583 deletions(-)
diff --git a/atr/config.py b/atr/config.py
index 7a3c11d..8da79a7 100644
--- a/atr/config.py
+++ b/atr/config.py
@@ -23,14 +23,12 @@ from atr.db.models import __file__ as data_models_file
class AppConfig:
- # Get the project root directory (where alembic.ini is)
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STATE_DIR = os.path.join(PROJECT_ROOT, "state")
RELEASE_STORAGE_DIR = os.path.join(STATE_DIR, "releases")
DATA_MODELS_FILE = data_models_file
- # Use aiosqlite for async SQLite access
SQLITE_URL = config("SQLITE_URL", default="sqlite+aiosqlite:///./atr.db")
ADMIN_USERS = frozenset(
diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index 21e200e..003b436 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -17,7 +17,7 @@
import os
-from alembic import command
+# from alembic import command
from alembic.config import Config
from quart import current_app
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker,
create_async_engine
@@ -66,7 +66,7 @@ def create_database(app: QuartApp) -> None:
alembic_cfg.set_main_option("script_location",
os.path.join(project_root, "migrations"))
# Set the database URL in the config
alembic_cfg.set_main_option("sqlalchemy.url", sqlite_url)
- command.upgrade(alembic_cfg, "head")
+ # command.upgrade(alembic_cfg, "head")
# Create any tables that might be missing
async with engine.begin() as conn:
diff --git a/atr/db/models.py b/atr/db/models.py
index 6b27006..639f509 100644
--- a/atr/db/models.py
+++ b/atr/db/models.py
@@ -134,10 +134,16 @@ class DistributionChannel(SQLModel, table=True):
class Package(SQLModel, table=True):
- id: int | None = Field(default=None, primary_key=True)
- file: str
- signature: str
- checksum: str
+ # The SHA3-256 hash of the file, used as filename in storage
+ id_sha3: str = Field(primary_key=True)
+ # Original filename from uploader
+ filename: str
+ # SHA-512 hash of the file
+ sha512: str
+ # The signature file
+ signature_sha3: str
+ # Uploaded timestamp
+ uploaded: datetime.datetime
# Many-to-one: A package belongs to one release
release_key: str | None = Field(default=None,
foreign_key="release.storage_key")
@@ -179,6 +185,7 @@ class Release(SQLModel, table=True):
storage_key: str = Field(primary_key=True)
stage: ReleaseStage
phase: ReleasePhase
+ created: datetime.datetime
# Many-to-one: A release belongs to one PMC, a PMC can have multiple
releases
pmc_id: int | None = Field(default=None, foreign_key="pmc.id")
diff --git a/atr/routes.py b/atr/routes.py
index c37c9c3..bb4fc12 100644
--- a/atr/routes.py
+++ b/atr/routes.py
@@ -125,28 +125,44 @@ async def release_attach_post(session: ClientSession,
request: Request) -> Respo
# Save files using their hashes as filenames
uploads_path = Path(get_release_storage_dir())
- artifact_hash = await save_file_by_hash(uploads_path, artifact_file)
- # TODO: Do we need to do anything with the signature hash?
- # These should be identical, but path might be absolute?
- # TODO: Need to check, ideally. Could have a data browser
- signature_hash = await save_file_by_hash(uploads_path, signature_file)
+ artifact_sha3 = await save_file_by_hash(uploads_path, artifact_file)
+ signature_sha3 = await save_file_by_hash(uploads_path, signature_file)
- # Compute SHA-512 checksum of the artifact for the package record
- checksum_512 = compute_sha512(uploads_path / artifact_hash)
+ # Check if these files are already attached to this release
+ async with get_session() as db_session:
+ # Check for duplicate artifact or signature in a single query
+ statement = select(Package).where(
+ Package.release_key == release_key,
+ (Package.id_sha3 == artifact_sha3) | (Package.signature_sha3 ==
signature_sha3),
+ )
+ duplicate = (await db_session.execute(statement)).first()
+
+ if duplicate:
+ package = duplicate[0]
+ # TODO: Perhaps we should call the id_sha3 field artifact_sha3
instead
+ if package.id_sha3 == artifact_sha3:
+ raise ASFQuartException("This release artifact has already
been uploaded", errorcode=400)
+ else:
+ raise ASFQuartException("This signature file has already been
uploaded", errorcode=400)
+
+ # Compute SHA-512 of the artifact for the package record
+ sha512 = compute_sha512(uploads_path / artifact_sha3)
# Create the package record in the database
async with get_session() as db_session:
async with db_session.begin():
package = Package(
- file=artifact_hash,
- signature=signature_hash,
- checksum=checksum_512,
+ id_sha3=artifact_sha3,
+ filename=artifact_file.filename,
+ signature_sha3=signature_sha3,
+ sha512=sha512,
release_key=release_key,
+ uploaded=datetime.datetime.now(datetime.UTC),
)
db_session.add(package)
- # Redirect to the user's uploads page
- return redirect(url_for("root_user_uploads"))
+ # Redirect to the release candidate review page
+ return redirect(url_for("root_candidate_review"))
async def release_create_post(session: ClientSession, request: Request) ->
Response:
@@ -193,6 +209,7 @@ async def release_create_post(session: ClientSession,
request: Request) -> Respo
phase=ReleasePhase.RELEASE_CANDIDATE,
pmc_id=pmc.id,
version=version,
+ created=datetime.datetime.now(datetime.UTC),
)
db_session.add(release)
@@ -202,7 +219,7 @@ async def release_create_post(session: ClientSession,
request: Request) -> Respo
# Redirect to the attach artifacts page with the storage token
# We should possibly have a results, or list of releases, page instead
- return redirect(url_for("root_release_attach", storage_key=storage_token))
+ return redirect(url_for("root_candidate_attach",
storage_key=storage_token))
@APP.route("/")
@@ -211,15 +228,9 @@ async def root() -> str:
return await render_template("index.html")
[email protected]("/pages")
-async def root_pages() -> str:
- """List all pages on the website."""
- return await render_template("pages.html")
-
-
[email protected]("/release/attach", methods=["GET", "POST"])
[email protected]("/candidate/attach", methods=["GET", "POST"])
@require(Requirements.committer)
-async def root_release_attach() -> Response | str:
+async def root_candidate_attach() -> Response | str:
"""Attach package artifacts to an existing release."""
session = await session_read()
if session is None:
@@ -249,16 +260,16 @@ async def root_release_attach() -> Response | str:
# For GET requests, show the form
return await render_template(
- "release-attach.html",
+ "candidate-attach.html",
asf_id=session.uid,
releases=user_releases,
selected_release=storage_key,
)
[email protected]("/release/create", methods=["GET", "POST"])
[email protected]("/candidate/create", methods=["GET", "POST"])
@require(Requirements.committer)
-async def root_release_create() -> Response | str:
+async def root_candidate_create() -> Response | str:
"""Create a new release in the database."""
session = await session_read()
if session is None:
@@ -270,16 +281,16 @@ async def root_release_create() -> Response | str:
# For GET requests, show the form
return await render_template(
- "release-create.html",
+ "candidate-create.html",
asf_id=session.uid,
pmc_memberships=session.committees,
committer_projects=session.projects,
)
[email protected]("/release/signatures/verify/<release_key>")
[email protected]("/candidate/signatures/verify/<release_key>")
@require(Requirements.committer)
-async def root_release_signatures_verify(release_key: str) -> str:
+async def root_candidate_signatures_verify(release_key: str) -> str:
"""Verify the GPG signatures for all packages in a release candidate."""
session = await session_read()
if session is None:
@@ -314,10 +325,10 @@ async def root_release_signatures_verify(release_key:
str) -> str:
storage_dir = Path(get_release_storage_dir())
for package in release.packages:
- result = {"file": package.file}
+ result = {"file": package.id_sha3}
- artifact_path = storage_dir / package.file
- signature_path = storage_dir / package.signature
+ artifact_path = storage_dir / package.id_sha3
+ signature_path = storage_dir / package.signature_sha3
if not artifact_path.exists():
result["error"] = "Package artifact file not found"
@@ -326,12 +337,12 @@ async def root_release_signatures_verify(release_key:
str) -> str:
else:
# Verify the signature
result = await verify_gpg_signature(artifact_path,
signature_path, ascii_armored_keys)
- result["file"] = package.file
+ result["file"] = package.id_sha3
verification_results.append(result)
return await render_template(
- "release-signature-verify.html", release=release,
verification_results=verification_results
+ "candidate-signature-verify.html", release=release,
verification_results=verification_results
)
@@ -405,24 +416,39 @@ async def root_pmc_list() -> list[dict]:
]
[email protected]("/user/keys/add", methods=["GET", "POST"])
[email protected]("/keys/review")
@require(Requirements.committer)
-async def root_user_keys_add() -> str:
- """Add a new GPG key to the user's account."""
+async def root_keys_review() -> str:
+ """Show all GPG keys associated with the user's account."""
session = await session_read()
if session is None:
raise ASFQuartException("Not authenticated", errorcode=401)
- error = None
- key_info = None
- user_keys = []
-
# Get all existing keys for the user
async with get_session() as db_session:
pmcs_loader = selectinload(cast(InstrumentedAttribute[list[PMC]],
PublicSigningKey.pmcs))
statement =
select(PublicSigningKey).options(pmcs_loader).where(PublicSigningKey.apache_uid
== session.uid)
user_keys = (await db_session.execute(statement)).scalars().all()
+ return await render_template(
+ "keys-review.html",
+ asf_id=session.uid,
+ user_keys=user_keys,
+ algorithms=algorithms,
+ )
+
+
[email protected]("/keys/add", methods=["GET", "POST"])
+@require(Requirements.committer)
+async def root_keys_add() -> str:
+ """Add a new GPG key to the user's account."""
+ session = await session_read()
+ if session is None:
+ raise ASFQuartException("Not authenticated", errorcode=401)
+
+ error = None
+ key_info = None
+
if request.method == "POST":
form = await request.form
public_key = form.get("public_key")
@@ -434,26 +460,26 @@ async def root_user_keys_add() -> str:
selected_pmcs = form.getlist("selected_pmcs")
if not selected_pmcs:
return await render_template(
- "user-keys-add.html",
+ "keys-add.html",
asf_id=session.uid,
pmc_memberships=session.committees,
error="You must select at least one PMC",
key_info=None,
- user_keys=user_keys,
algorithms=algorithms,
committer_projects=session.projects,
)
# Ensure that the selected PMCs are ones of which the user is actually
a member
- invalid_pmcs = [pmc for pmc in selected_pmcs if pmc not in
session.committees]
+ invalid_pmcs = [
+ pmc for pmc in selected_pmcs if (pmc not in session.committees)
and (pmc not in session.projects)
+ ]
if invalid_pmcs:
return await render_template(
- "user-keys-add.html",
+ "keys-add.html",
asf_id=session.uid,
pmc_memberships=session.committees,
error=f"Invalid PMC selection: {', '.join(invalid_pmcs)}",
key_info=None,
- user_keys=user_keys,
algorithms=algorithms,
committer_projects=session.projects,
)
@@ -461,12 +487,11 @@ async def root_user_keys_add() -> str:
error, key_info = await user_keys_add(session, public_key,
selected_pmcs)
return await render_template(
- "user-keys-add.html",
+ "keys-add.html",
asf_id=session.uid,
pmc_memberships=session.committees,
error=error,
key_info=key_info,
- user_keys=user_keys,
algorithms=algorithms,
committer_projects=session.projects,
)
@@ -495,10 +520,10 @@ async def root_user_keys_delete() -> str:
return f"Deleted {count} keys"
[email protected]("/user/uploads")
[email protected]("/candidate/review")
@require(Requirements.committer)
-async def root_user_uploads() -> str:
- """Show all release candidates uploaded by the current user."""
+async def root_candidate_review() -> str:
+ """Show all release candidates to which the user has access."""
session = await session_read()
if session is None:
raise ASFQuartException("Not authenticated", errorcode=401)
@@ -525,7 +550,7 @@ async def root_user_uploads() -> str:
if session.uid in r.pmc.pmc_members:
user_releases.append(r)
- return await render_template("user-uploads.html",
releases=user_releases)
+ return await render_template("candidate-review.html",
releases=user_releases)
async def save_file_by_hash(base_dir: Path, file: FileStorage) -> str:
@@ -579,7 +604,9 @@ async def user_keys_add(session: ClientSession, public_key:
str, selected_pmcs:
if not import_result.fingerprints:
return ("Invalid public key format", None)
- fingerprint = import_result.fingerprints[0].lower()
+ fingerprint = import_result.fingerprints[0]
+ if fingerprint is not None:
+ fingerprint = fingerprint.lower()
# APP.logger.info("Import result: %s", vars(import_result))
# Get key details
# We could probably use import_result instead
@@ -589,7 +616,7 @@ async def user_keys_add(session: ClientSession, public_key:
str, selected_pmcs:
# Then we have the properties listed here:
# https://gnupg.readthedocs.io/en/latest/#listing-keys
# Note that "fingerprint" is not listed there, but we have it anyway...
- key = next((k for k in keys if k["fingerprint"].lower() == fingerprint),
None)
+ key = next((k for k in keys if (k["fingerprint"] is not None) and
(k["fingerprint"].lower() == fingerprint)), None)
if not key:
return ("Failed to import key", None)
if (key.get("algo") == "1") and (int(key.get("length", "0")) < 2048):
@@ -620,11 +647,15 @@ async def user_keys_add_session(
if not session.uid:
return ("You must be signed in to add a key", None)
+ fingerprint = key.get("fingerprint")
+ if not isinstance(fingerprint, str):
+ return ("Invalid key fingerprint", None)
+ fingerprint = fingerprint.lower()
uids = key.get("uids")
async with db_session.begin():
# Create new key record
key_record = PublicSigningKey(
- fingerprint=key["fingerprint"].lower(),
+ fingerprint=fingerprint,
algorithm=int(key["algo"]),
length=int(key.get("length", "0")),
created=datetime.datetime.fromtimestamp(int(key["date"])),
@@ -650,7 +681,7 @@ async def user_keys_add_session(
"",
{
"key_id": key["keyid"],
- "fingerprint": key["fingerprint"].lower(),
+ "fingerprint": key["fingerprint"].lower() if
key.get("fingerprint") else "Unknown",
"user_id": key["uids"][0] if key.get("uids") else "Unknown",
"creation_date": datetime.datetime.fromtimestamp(int(key["date"])),
"expiration_date":
datetime.datetime.fromtimestamp(int(key["expires"])) if key.get("expires") else
None,
@@ -694,8 +725,8 @@ async def verify_gpg_signature_file(
# Collect all available information for debugging
debug_info = {
"key_id": verified.key_id or "Not available",
- "fingerprint": verified.fingerprint.lower() or "Not available",
- "pubkey_fingerprint": verified.pubkey_fingerprint.lower() or "Not
available",
+ "fingerprint": verified.fingerprint.lower() if verified.fingerprint
else "Not available",
+ "pubkey_fingerprint": verified.pubkey_fingerprint.lower() if
verified.pubkey_fingerprint else "Not available",
"creation_date": verified.creation_date or "Not available",
"timestamp": verified.timestamp or "Not available",
"username": verified.username or "Not available",
diff --git a/atr/static/css/atr.css b/atr/static/css/atr.css
index 5aca184..6625b04 100644
--- a/atr/static/css/atr.css
+++ b/atr/static/css/atr.css
@@ -81,9 +81,15 @@ table td {
/* Not sure if we should keep it this way, but it seems pretty good */
font-family: ui-monospace, "SFMono-Regular", "Menlo", "Monaco",
"Consolas", monospace;
+ word-break: break-all;
font-size: 0.9em;
}
+table td.prose {
+ font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe
UI", "Oxygen", "Ubuntu", "Cantarell", "Open Sans", "Helvetica Neue", sans-serif;
+ font-size: 1em;
+}
+
table tr {
/* This doesn't always work; not clear why */
border-bottom: 1px solid #c1c2c3;
diff --git a/atr/templates/release-attach.html
b/atr/templates/candidate-attach.html
similarity index 93%
rename from atr/templates/release-attach.html
rename to atr/templates/candidate-attach.html
index 082a784..b304560 100644
--- a/atr/templates/release-attach.html
+++ b/atr/templates/candidate-attach.html
@@ -64,7 +64,8 @@
{% block content %}
<h1>Attach package artifacts</h1>
<p class="intro">
- Welcome, <strong>{{ asf_id }}</strong>! Use this form to attach package
artifacts to an existing release candidate.
+ Welcome, <strong>{{ asf_id }}</strong>! Use this form to attach package
artifacts
+ to an existing release candidate.
</p>
<form method="post" enctype="multipart/form-data" class="striking">
@@ -84,7 +85,12 @@
</option>
{% endfor %}
</select>
- {% if not releases %}<p class="error-message">No releases found
that you can attach artifacts to.</p>{% endif %}
+ {% if not releases %}
+ <p class="error-message">
+ No releases found that you can
+ attach artifacts to.
+ </p>
+ {% endif %}
</td>
</tr>
diff --git a/atr/templates/release-create.html
b/atr/templates/candidate-create.html
similarity index 100%
rename from atr/templates/release-create.html
rename to atr/templates/candidate-create.html
diff --git a/atr/templates/candidate-review.html
b/atr/templates/candidate-review.html
new file mode 100644
index 0000000..40f3e5a
--- /dev/null
+++ b/atr/templates/candidate-review.html
@@ -0,0 +1,163 @@
+{% extends "layouts/base.html" %}
+
+{% block title %}
+ Release candidates ~ ATR
+{% endblock title %}
+
+{% block description %}
+ Release candidates to which you have access.
+{% endblock description %}
+
+{% block stylesheets %}
+ {{ super() }}
+ <style>
+ .candidate-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 1rem 0;
+ }
+
+ .candidate-table th,
+ .candidate-table td {
+ padding: 0.75rem;
+ text-align: left;
+ border: 1px solid #ddd;
+ }
+
+ .candidate-table th {
+ background-color: #f5f5f5;
+ font-weight: 600;
+ width: 200px;
+ }
+
+ .candidate-table tr:hover {
+ background-color: #f8f8f8;
+ }
+
+ .candidate-meta {
+ color: #666;
+ font-size: 0.9em;
+ }
+
+ .no-releases {
+ color: #666;
+ font-style: italic;
+ }
+
+ .verify-link {
+ display: inline-block;
+ padding: 0.5rem 1rem;
+ background: #003366;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-weight: 500;
+ text-decoration: none;
+ }
+
+ .verify-link:hover {
+ background: #004477;
+ color: white;
+ }
+
+ .package-separator {
+ height: 2rem;
+ background-color: #f5f5f5;
+ }
+
+ .candidate-header {
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ padding: 1rem;
+ margin-bottom: 1rem;
+ background-color: #f8f8f8;
+ }
+
+ .candidate-header h3 {
+ margin: 0 0 0.5rem 0;
+ }
+
+ .candidate-meta {
+ color: #666;
+ font-size: 0.9em;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+ }
+
+ .candidate-meta-item::after {
+ content: "•";
+ margin-left: 1rem;
+ color: #ccc;
+ }
+
+ .candidate-meta-item:last-child::after {
+ content: none;
+ }
+ </style>
+{% endblock stylesheets %}
+
+{% block content %}
+ <h1>Release candidates</h1>
+ <p class="intro">Here are all the release candidates to which you have
access.</p>
+
+ {% if releases %}
+ {% for release in releases %}
+ <div class="candidate-header">
+ <h3>{{ release.pmc.project_name }}</h3>
+ <div class="candidate-meta">
+ <span class="candidate-meta-item">Version: {{ release.version
}}</span>
+ <span class="candidate-meta-item">Stage: {{ release.stage.value
}}</span>
+ <span class="candidate-meta-item">Phase: {{ release.phase.value
}}</span>
+ <span class="candidate-meta-item">Created: {{
release.created.strftime("%Y-%m-%d %H:%M UTC") }}</span>
+ </div>
+ </div>
+
+ <table class="candidate-table">
+ {% for package in release.packages %}
+ {% if not loop.first %}
+ <tr class="package-separator">
+ <td colspan="2"></td>
+ </tr>
+ {% endif %}
+ <tr>
+ <th>Original Filename</th>
+ <td>{{ package.filename }}</td>
+ </tr>
+ <tr>
+ <th>File Hash (SHA3)</th>
+ <td>{{ package.id_sha3 }}</td>
+ </tr>
+ <tr>
+ <th>Signature Hash (SHA3)</th>
+ <td>{{ package.signature_sha3 }}</td>
+ </tr>
+ <tr>
+ <th>SHA-512</th>
+ <td>{{ package.sha512 }}</td>
+ </tr>
+ <tr>
+ <th>Uploaded</th>
+ <td>{{ package.uploaded.strftime("%Y-%m-%d %H:%M UTC") }}</td>
+ </tr>
+ <tr>
+ <th>Actions</th>
+ <td class="prose">
+ <a class="verify-link"
+ href="{{ url_for('root_candidate_signatures_verify',
release_key=release.storage_key) }}">
+ Verify Signatures
+ </a>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endfor %}
+ {% else %}
+ <p class="no-releases">You haven't created any releases yet.</p>
+ {% endif %}
+
+ <p>
+ <a href="{{ url_for('root_candidate_create') }}">Create a release
candidate</a>
+ </p>
+{% endblock content %}
diff --git a/atr/templates/release-signature-verify.html
b/atr/templates/candidate-signature-verify.html
similarity index 67%
rename from atr/templates/release-signature-verify.html
rename to atr/templates/candidate-signature-verify.html
index 0d4c697..f9be33f 100644
--- a/atr/templates/release-signature-verify.html
+++ b/atr/templates/candidate-signature-verify.html
@@ -11,7 +11,7 @@
{% block stylesheets %}
{{ super() }}
<style>
- .release-info {
+ .candidate-info {
margin-bottom: 2rem;
}
@@ -37,6 +37,10 @@
background: #f5f5f5;
}
+ .verification-status .status {
+ font-weight: bold;
+ }
+
.navigation {
margin-top: 2rem;
}
@@ -51,7 +55,7 @@
}
.status.success {
- color: #28a745;
+ color: #219f3f;
}
.status.failure {
@@ -73,9 +77,20 @@
border: 1px solid #dee2e6;
}
- .debug-info h3 {
- margin-top: 0;
+ .debug-info summary {
color: #666;
+ font-weight: bold;
+ cursor: pointer;
+ padding-bottom: 0.5rem;
+ }
+
+ .debug-info summary:hover {
+ color: #333;
+ }
+
+ .debug-info[open] summary {
+ border-bottom: 1px solid #dee2e6;
+ margin-bottom: 1rem;
}
.debug-info dl {
@@ -95,9 +110,38 @@
word-break: break-all;
}
+ .candidate-header {
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ padding: 1rem;
+ margin-bottom: 1rem;
+ background-color: #f8f8f8;
+ }
+
+ .candidate-header h3 {
+ margin: 0 0 0.5rem 0;
+ }
+
+ .candidate-meta {
+ color: #666;
+ font-size: 0.9em;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+ }
+
+ .candidate-meta-item::after {
+ content: "•";
+ margin-left: 1rem;
+ color: #ccc;
+ }
+
+ .candidate-meta-item:last-child::after {
+ content: none;
+ }
+
pre.stderr {
background: #f8f9fa;
- padding: 0.5rem;
border-radius: 2px;
overflow-x: auto;
margin: 0.5rem 0;
@@ -109,13 +153,14 @@
{% block content %}
<h1>Verify release signatures</h1>
- <div class="release-info">
- <h2>{{ release.pmc.project_name }}</h2>
- <p>
- Stage: {{ release.stage.value }}
- •
- Phase: {{ release.phase.value }}
- </p>
+ <div class="candidate-header">
+ <h3>{{ release.pmc.project_name }}</h3>
+ <div class="candidate-meta">
+ <span class="candidate-meta-item">Version: {{ release.version }}</span>
+ <span class="candidate-meta-item">Stage: {{ release.stage.value }}</span>
+ <span class="candidate-meta-item">Phase: {{ release.phase.value }}</span>
+ <span class="candidate-meta-item">Created: {{
release.created.strftime("%Y-%m-%d %H:%M UTC") }}</span>
+ </div>
</div>
<div class="package-list">
@@ -140,8 +185,8 @@
{% endif %}
{% if result.debug_info %}
- <div class="debug-info">
- <h3>Debug Information</h3>
+ <details class="debug-info">
+ <summary>Debug Information</summary>
<dl>
{% for key, value in result.debug_info.items() %}
<dt>{{ key }}</dt>
@@ -154,21 +199,10 @@
</dd>
{% endfor %}
</dl>
- </div>
+ </details>
{% endif %}
</div>
</div>
{% endfor %}
</div>
-
- <h2>Navigation</h2>
-
- <div class="navigation">
- <p>
- <a href="{{ url_for('root_user_uploads') }}">Back to Your Uploads</a>
- </p>
- <p>
- <a href="{{ url_for('root_pages') }}">Return to Main Page</a>
- </p>
- </div>
{% endblock content %}
diff --git a/atr/templates/includes/sidebar.html
b/atr/templates/includes/sidebar.html
index 46c8ce9..65ad794 100644
--- a/atr/templates/includes/sidebar.html
+++ b/atr/templates/includes/sidebar.html
@@ -39,28 +39,32 @@
</ul>
{% if current_user %}
- <h3>Release management</h3>
+ <h3>Candidate management</h3>
<ul>
<li>
- <a href="{{ url_for('root_release_create') }}"
- {% if request.endpoint == 'root_release_create'
%}class="active"{% endif %}>Create release candidate</a>
+ <a href="{{ url_for('root_candidate_create') }}"
+ {% if request.endpoint == 'root_candidate_create'
%}class="active"{% endif %}>Create candidate</a>
</li>
<!-- TODO: Don't show this if the user doesn't have any release
candidates? -->
<li>
- <a href="{{ url_for('root_release_attach') }}"
- {% if request.endpoint == 'root_release_attach'
%}class="active"{% endif %}>Attach package artifacts</a>
+ <a href="{{ url_for('root_candidate_attach') }}"
+ {% if request.endpoint == 'root_candidate_attach'
%}class="active"{% endif %}>Attach artifacts</a>
</li>
<li>
- <a href="{{ url_for('root_user_uploads') }}"
- {% if request.endpoint == 'root_user_uploads' %}class="active"{%
endif %}>Your release candidates</a>
+ <a href="{{ url_for('root_candidate_review') }}"
+ {% if request.endpoint == 'root_candidate_review'
%}class="active"{% endif %}>Review candidates</a>
</li>
</ul>
<h3>User management</h3>
<ul>
<li>
- <a href="{{ url_for('root_user_keys_add') }}"
- {% if request.endpoint == 'root_user_keys_add' %}class="active"{%
endif %}>Add signing key</a>
+ <a href="{{ url_for('root_keys_review') }}"
+ {% if request.endpoint == 'root_keys_review' %}class="active"{%
endif %}>Your signing keys</a>
+ </li>
+ <li>
+ <a href="{{ url_for('root_keys_add') }}"
+ {% if request.endpoint == 'root_keys_add' %}class="active"{%
endif %}>Add signing key</a>
</li>
<li>
<a href="{{ url_for('root_user_keys_delete') }}"
diff --git a/atr/templates/index.html b/atr/templates/index.html
index 66bcd9e..06bf967 100644
--- a/atr/templates/index.html
+++ b/atr/templates/index.html
@@ -8,7 +8,9 @@
<h1>Apache Trusted Release</h1>
<p>
- ATR is a release management platform for <a
href="https://www.apache.org">Apache Software Foundation</a> projects. It
provides a standardized workflow for PMC members to submit, verify, and track
release candidates.
+ ATR is a release management platform for <a
href="https://www.apache.org">Apache Software
+ Foundation</a> projects. It provides a standardized workflow for PMC
members to submit,
+ verify, and track release candidates.
</p>
<h2>Key Features</h2>
@@ -21,20 +23,17 @@
<h2>Getting Started</h2>
<p>
- To submit a release candidate, you must be a PMC member of the target
project. First, <a href="{{ url_for('root_user_keys_add') }}">add your signing
key</a>, then <a href="{{ url_for('root_release_create') }}">create a release
candidate</a>.
+ To submit a release candidate, you must be a PMC member of the target
project. First, <a href="{{ url_for('root_keys_add') }}">add your signing
key</a>, then <a href="{{ url_for('root_candidate_create') }}">create a release
candidate</a>.
</p>
<h2>Documentation</h2>
<ul>
- <li>
- <a href="{{ url_for('root_pages') }}">Available endpoints and access
requirements</a>
- </li>
<li>
<a href="{{ url_for('root_pmc_directory') }}">PMC directory and release
manager assignments</a>
</li>
{% if current_user %}
<li>
- <a href="{{ url_for('root_user_uploads') }}">Track your uploaded
release candidates</a>
+ <a href="{{ url_for('root_candidate_review') }}">Track your release
candidates</a>
</li>
{% endif %}
</ul>
diff --git a/atr/templates/user-keys-add.html b/atr/templates/keys-add.html
similarity index 53%
rename from atr/templates/user-keys-add.html
rename to atr/templates/keys-add.html
index d5e7c13..274c840 100644
--- a/atr/templates/user-keys-add.html
+++ b/atr/templates/keys-add.html
@@ -5,7 +5,7 @@
{% endblock title %}
{% block description %}
- Add a GPG public key to your account.
+ Add a public signing key to your account.
{% endblock description %}
{% block stylesheets %}
@@ -15,7 +15,6 @@
margin-bottom: 1rem;
}
- /* TODO: Consider moving this to atr.css */
.form-group label {
display: inline-block;
margin-bottom: 1rem;
@@ -57,66 +56,6 @@
margin-top: 2rem;
}
- .success-message {
- color: #28a745;
- margin: 1rem 0;
- padding: 1rem;
- background: #d4edda;
- border-radius: 4px;
- }
-
- .existing-keys {
- margin-bottom: 2rem;
- padding: 1rem 2rem 2rem 2rem;
- background: #f8f9fa;
- border-radius: 4px;
- }
-
- .keys-grid {
- display: grid;
- /* This just messes up resizing */
- /* grid-template-columns: repeat(auto-fill, minmax(800px, 1fr)); */
- gap: 1.5rem;
- }
-
- .key-card {
- background: white;
- border: 1px solid #d1d2d3;
- border-radius: 4px;
- overflow: hidden;
- padding: 1rem;
- }
-
- .key-card table {
- margin: 0;
- }
-
- .key-card td {
- word-break: break-all;
- }
-
- .key-card h3 {
- margin-top: 0;
- margin-bottom: 1rem;
- }
-
- .delete-key-form {
- margin-top: 1rem;
- }
-
- .delete-button {
- background: #dc3545;
- color: white;
- border: none;
- padding: 0.5rem 1rem;
- border-radius: 4px;
- cursor: pointer;
- }
-
- .delete-button:hover {
- background: #c82333;
- }
-
.pmc-checkboxes {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
@@ -146,7 +85,7 @@
{% block content %}
<h1>Add signing key</h1>
- <p class="intro">Add your GPG public key to use for signing release
artifacts.</p>
+ <p class="intro">Add your public key to use for signing release
artifacts.</p>
<div class="user-info">
<p>
@@ -194,66 +133,13 @@
</div>
{% endif %}
- {% if success %}
- <div class="success-message">
- <h2>Success</h2>
- <p>{{ success }}</p>
- </div>
- {% endif %}
-
- {% if user_keys %}
- <h2>Your Existing Keys</h2>
- <div class="existing-keys">
- <div class="keys-grid">
- {% for key in user_keys %}
- <div class="key-card">
- <table>
- <tbody>
- <tr>
- <th>Fingerprint</th>
- <td>{{ key.fingerprint }}</td>
- </tr>
- <tr>
- <th>Key Type</th>
- <td>{{ algorithms[key.algorithm] }} ({{ key.length }}
bits)</td>
- </tr>
- <tr>
- <th>Created</th>
- <td>{{ key.created.strftime("%Y-%m-%d %H:%M:%S") }}</td>
- </tr>
- <tr>
- <th>Expires</th>
- <td>{{ key.expires.strftime("%Y-%m-%d %H:%M:%S") if
key.expires else 'Never' }}</td>
- </tr>
- <tr>
- <th>User ID</th>
- <td>{{ key.declared_uid or 'Not specified' }}</td>
- </tr>
- <tr>
- <th>Associated projects</th>
- <td>
- {% if key.pmcs %}
- {{ key.pmcs|map(attribute='project_name') |join(', ') }}
- {% else %}
- No projects associated
- {% endif %}
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- {% endfor %}
- </div>
- </div>
- {% endif %}
-
<form method="post" class="striking">
<div class="form-group">
<label for="public_key">Public Key:</label>
<textarea id="public_key"
name="public_key"
required
- placeholder="Paste your GPG public key here (in ASCII-armored
format)"
+ placeholder="Paste your public key here (in ASCII-armored
format)"
aria-describedby="key-help"></textarea>
<small id="key-help">
Your public key should be in ASCII-armored format, starting with
"-----BEGIN PGP PUBLIC KEY BLOCK-----"
@@ -269,8 +155,7 @@
<input type="checkbox"
id="pmc_{{ pmc }}"
name="selected_pmcs"
- value="{{ pmc }}"
- required />
+ value="{{ pmc }}" />
<label for="pmc_{{ pmc }}">{{ pmc }}</label>
</div>
{% endfor %}
@@ -285,15 +170,4 @@
<button type="submit">Add Key</button>
</form>
-
- <h2>Navigation</h2>
-
- <div class="navigation">
- <p>
- <a href="{{ url_for('root_user_uploads') }}">Back to Your Uploads</a>
- </p>
- <p>
- <a href="{{ url_for('root_pages') }}">Return to Main Page</a>
- </p>
- </div>
{% endblock content %}
diff --git a/atr/templates/keys-review.html b/atr/templates/keys-review.html
new file mode 100644
index 0000000..6455709
--- /dev/null
+++ b/atr/templates/keys-review.html
@@ -0,0 +1,141 @@
+{% extends "layouts/base.html" %}
+
+{% block title %}
+ Your signing keys ~ ATR
+{% endblock title %}
+
+{% block description %}
+ Review your signing keys.
+{% endblock description %}
+
+{% block stylesheets %}
+ {{ super() }}
+ <style>
+ .existing-keys {
+ margin-bottom: 2rem;
+ padding: 1rem 2rem 2rem 2rem;
+ background: #f8f9fa;
+ border-radius: 4px;
+ }
+
+ .keys-grid {
+ display: grid;
+ gap: 1.5rem;
+ }
+
+ .key-card {
+ background: white;
+ border: 1px solid #d1d2d3;
+ border-radius: 4px;
+ overflow: hidden;
+ padding: 1rem;
+ }
+
+ .key-card table {
+ margin: 0;
+ }
+
+ .key-card td {
+ word-break: break-all;
+ }
+
+ .key-card h3 {
+ margin-top: 0;
+ margin-bottom: 1rem;
+ }
+
+ .delete-key-form {
+ margin-top: 1rem;
+ }
+
+ .delete-button {
+ background: #dc3545;
+ color: white;
+ border: none;
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ cursor: pointer;
+ }
+
+ .delete-button:hover {
+ background: #c82333;
+ }
+
+ .navigation {
+ margin-top: 2rem;
+ }
+
+ .success-message {
+ color: #28a745;
+ margin: 1rem 0;
+ padding: 1rem;
+ background: #d4edda;
+ border-radius: 4px;
+ }
+ </style>
+{% endblock stylesheets %}
+
+{% block content %}
+ <h1>Your signing keys</h1>
+ <p class="intro">Review your public keys used for signing release
artifacts.</p>
+
+ <div class="user-info">
+ <p>
+ Welcome, <strong>{{ asf_id }}</strong>! You are authenticated as an ASF
committer.
+ </p>
+ </div>
+
+ {% if success %}
+ <div class="success-message">
+ <h2>Success</h2>
+ <p>{{ success }}</p>
+ </div>
+ {% endif %}
+
+ {% if user_keys %}
+ <div class="existing-keys">
+ <div class="keys-grid">
+ {% for key in user_keys %}
+ <div class="key-card">
+ <table>
+ <tbody>
+ <tr>
+ <th>Fingerprint</th>
+ <td>{{ key.fingerprint }}</td>
+ </tr>
+ <tr>
+ <th>Key Type</th>
+ <td>{{ algorithms[key.algorithm] }} ({{ key.length }}
bits)</td>
+ </tr>
+ <tr>
+ <th>Created</th>
+ <td>{{ key.created.strftime("%Y-%m-%d %H:%M:%S") }}</td>
+ </tr>
+ <tr>
+ <th>Expires</th>
+ <td>{{ key.expires.strftime("%Y-%m-%d %H:%M:%S") if
key.expires else 'Never' }}</td>
+ </tr>
+ <tr>
+ <th>User ID</th>
+ <td>{{ key.declared_uid or 'Not specified' }}</td>
+ </tr>
+ <tr>
+ <th>Associated projects</th>
+ <td>
+ {% if key.pmcs %}
+ {{ key.pmcs|map(attribute='project_name') |join(', ') }}
+ {% else %}
+ No projects associated
+ {% endif %}
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ {% endfor %}
+ </div>
+ </div>
+ {% else %}
+ <p>You haven't added any signing keys yet.</p>
+ {% endif %}
+{% endblock content %}
diff --git a/atr/templates/pages.html b/atr/templates/pages.html
deleted file mode 100644
index de9db30..0000000
--- a/atr/templates/pages.html
+++ /dev/null
@@ -1,240 +0,0 @@
-{% extends "layouts/base.html" %}
-
-{% block title %}
- Pages ~ ATR
-{% endblock title %}
-{% block description %}
- List of all pages and endpoints in ATR.
-{% endblock description %}
-
-{% block stylesheets %}
- {{ super() }}
- <style>
- .endpoint-list {
- margin: 2rem 0;
- }
-
- .endpoint-group {
- margin-bottom: 2rem;
- }
-
- .endpoint {
- border: 1px solid #ddd;
- padding: 1rem;
- margin-bottom: 1rem;
- border-radius: 4px;
- }
-
- .endpoint h3 {
- margin: 0 0 0.5rem 0;
- }
-
- .endpoint-meta {
- color: #666;
- font-size: 0.9em;
- margin-bottom: 0.5rem;
- }
-
- .endpoint-description {
- margin-bottom: 0.5rem;
- }
-
- .access-requirement {
- display: inline-block;
- padding: 0.25rem 0.5rem;
- border-radius: 2px;
- font-size: 0.8em;
- background: #f5f5f5;
- }
-
- .access-requirement.committer {
- background: #e6f3ff;
- border: 1px solid #cce5ff;
- }
-
- .access-requirement.admin {
- background: #ffeeba;
- border: 1px solid #f5d88c;
- }
-
- .access-requirement.public {
- background: #e6ffe6;
- border: 1px solid #ccebcc;
- }
-
- .access-requirement.warning {
- background: #ffe6e6;
- border: 1px solid #ffcccc;
- color: #cc0000;
- font-weight: bold;
- }
- </style>
-{% endblock stylesheets %}
-
-{% block content %}
- <h1>Pages</h1>
- <p class="intro">A complete list of all pages and endpoints available in
ATR.</p>
-
- <div class="endpoint-list">
- <div class="endpoint-group">
- <h2>Main Pages</h2>
-
- <div class="endpoint">
- <h3>
- <a href="{{ url_for('root') }}">/</a>
- </h3>
- <div class="endpoint-description">Main welcome page.</div>
- <div class="endpoint-meta">
- Access: <span class="access-requirement public">Public</span>
- </div>
- </div>
-
- <div class="endpoint">
- <h3>
- <a href="{{ url_for('root_pages') }}">/pages</a>
- </h3>
- <div class="endpoint-description">List of all pages on the website
(this page).</div>
- <div class="endpoint-meta">
- Access: <span class="access-requirement public">Public</span>
- </div>
- </div>
- </div>
-
- <div class="endpoint-group">
- <h2>PMC Management</h2>
-
- <div class="endpoint">
- <h3>
- <a href="{{ url_for('root_pmc_directory') }}">/pmc/directory</a>
- </h3>
- <div class="endpoint-description">Main PMC directory page with all
PMCs and their latest releases.</div>
- <div class="endpoint-meta">
- Access: <span class="access-requirement public">Public</span>
- </div>
- </div>
-
- <div class="endpoint">
- <h3>
- <a href="{{ url_for('root_pmc_list') }}">/pmc/list</a>
- </h3>
- <div class="endpoint-description">List all PMCs in the database (JSON
format).</div>
- <div class="endpoint-meta">
- Access: <span class="access-requirement public">Public</span>
- </div>
- </div>
-
- <div class="endpoint">
- <h3>/pmc/<project_name></h3>
- <div class="endpoint-description">Get details for a specific PMC (JSON
format).</div>
- <div class="endpoint-meta">
- Access: <span class="access-requirement public">Public</span>
- </div>
- </div>
- </div>
-
- <div class="endpoint-group">
- <h2>Release Management</h2>
-
- <div class="endpoint">
- <h3>
- <a href="{{ url_for('root_release_create') }}">/release/create</a>
- </h3>
- <div class="endpoint-description">Add a release candidate to the
database.</div>
- <div class="endpoint-meta">
- Access: <span class="access-requirement committer">Committer</span>
- <br />
- Additional requirement: Must be PMC member of the target project
- </div>
- </div>
-
- <div class="endpoint">
- <h3>
- <a href="{{ url_for('root_user_uploads') }}">/user/uploads</a>
- </h3>
- <div class="endpoint-description">Show all release candidates uploaded
by the current user.</div>
- <div class="endpoint-meta">
- Access: <span class="access-requirement committer">Committer</span>
- </div>
- </div>
-
- <div class="endpoint">
- <h3>/release/signatures/verify/<release_key></h3>
- <div class="endpoint-description">Verify GPG signatures for all
packages in a release candidate.</div>
- <div class="endpoint-meta">
- Access: <span class="access-requirement committer">Committer</span>
- </div>
- </div>
- </div>
-
- <div class="endpoint-group">
- <h2>User Management</h2>
-
- <div class="endpoint">
- <h3>
- <a href="{{ url_for('root_user_keys_add') }}">/user/keys/add</a>
- </h3>
- <div class="endpoint-description">Add a GPG public key to your account
for signing releases.</div>
- <div class="endpoint-meta">
- Access: <span class="access-requirement committer">Committer</span>
- </div>
- </div>
-
- <div class="endpoint">
- <h3>
- <a href="{{ url_for('root_user_keys_delete')
}}">/user/keys/delete</a>
- </h3>
- <div class="endpoint-description">Delete all GPG keys associated with
your account.</div>
- <div class="endpoint-meta">
- Access: <span class="access-requirement committer">Committer</span>
- <br />
- <span class="access-requirement warning">Warning: This will delete
all your keys without confirmation!</span>
- </div>
- </div>
- </div>
-
- <div class="endpoint-group">
- <h2>Administration</h2>
-
- <div class="endpoint">
- <h3>
- <a href="{{ url_for('secret_blueprint.secret_data')
}}">/secret/data</a>
- </h3>
- <div class="endpoint-description">Browse all records in the
database.</div>
- <div class="endpoint-meta">
- Access: <span class="access-requirement committer">Committer</span>
- <span class="access-requirement admin">Admin</span>
- <br />
- Additional requirement: Must be in ALLOWED_USERS list
- </div>
- </div>
-
- <div class="endpoint">
- <h3>
- <a href="{{ url_for('secret_blueprint.secret_pmcs_update')
}}">/secret/pmcs/update</a>
- </h3>
- <div class="endpoint-description">Update PMCs from remote,
authoritative committee-info.json.</div>
- <div class="endpoint-meta">
- Access: <span class="access-requirement committer">Committer</span>
- <span class="access-requirement admin">Admin</span>
- <br />
- Additional requirement: Must be in ALLOWED_USERS list
- </div>
- </div>
- </div>
-
- <div class="endpoint-group">
- <h2>API</h2>
-
- <div class="endpoint">
- <h3>
- <a href="{{ url_for('swagger_ui') }}">/api/docs</a>
- </h3>
- <div class="endpoint-description">Swagger UI.</div>
- <div class="endpoint-meta">
- Access: <span class="access-requirement public">Public</span>
- </div>
- </div>
- </div>
- </div>
-
-{% endblock content %}
diff --git a/atr/templates/user-uploads.html b/atr/templates/user-uploads.html
deleted file mode 100644
index 48df508..0000000
--- a/atr/templates/user-uploads.html
+++ /dev/null
@@ -1,103 +0,0 @@
-{% extends "layouts/base.html" %}
-
-{% block title %}
- Release candidates ~ ATR
-{% endblock title %}
-
-{% block description %}
- Release candidates to which you have access.
-{% endblock description %}
-
-{% block stylesheets %}
- {{ super() }}
- <style>
- .release-list {
- margin: 1rem 0;
- }
-
- div.release {
- border: 1px solid #ddd;
- padding: 1rem;
- margin-bottom: 1rem;
- border-radius: 4px;
- }
-
- .release-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 0.5rem;
- }
-
- .release-meta {
- color: #666;
- font-size: 0.9em;
- }
-
- .package-list {
- margin-top: 0.5rem;
- }
-
- .package {
- background: #f5f5f5;
- padding: 0.5rem;
- margin: 0.25rem 0;
- border-radius: 2px;
- }
-
- .package div {
- margin-bottom: 0.25rem;
- }
-
- .no-releases {
- color: #666;
- font-style: italic;
- }
- </style>
-{% endblock stylesheets %}
-
-{% block content %}
- <h1>Release candidates</h1>
- <p class="intro">Here are all the release candidates to which you have
access.</p>
-
- {% if releases %}
- <div class="release-list">
- {% for release in releases %}
- <div class="release">
- <div class="release-header">
- <h3>{{ release.pmc.project_name }}</h3>
- <span class="release-meta">
- Stage: {{ release.stage.value }}
- •
- Phase: {{ release.phase.value }}
- </span>
- </div>
- <div class="package-list">
- {% for package in release.packages %}
- <div class="package">
- <div>
- File: <span class="hex">{{ package.file }}</span>
- </div>
- <div>
- Signature: <span class="hex">{{ package.signature }}</span>
- </div>
- <div>
- Checksum (SHA-512): <span class="hex">{{ package.checksum
}}</span>
- </div>
- <p class="package-actions">
- <a href="{{ url_for('root_release_signatures_verify',
release_key=release.storage_key) }}">Verify Signatures</a>
- </p>
- </div>
- {% endfor %}
- </div>
- </div>
- {% endfor %}
- </div>
- {% else %}
- <p class="no-releases">You haven't created any releases yet.</p>
- {% endif %}
-
- <p>
- <a href="{{ url_for('root_release_create') }}">Create a release
candidate</a>
- </p>
-{% endblock content %}
diff --git a/migrations/versions/512e973a9ce4_initial_schema.py
b/migrations/versions/b561e6142755_initial_schema.py
similarity index 84%
rename from migrations/versions/512e973a9ce4_initial_schema.py
rename to migrations/versions/b561e6142755_initial_schema.py
index c9f6768..991d9e4 100644
--- a/migrations/versions/512e973a9ce4_initial_schema.py
+++ b/migrations/versions/b561e6142755_initial_schema.py
@@ -1,15 +1,15 @@
"""initial_schema
-Revision ID: 512e973a9ce4
+Revision ID: b561e6142755
Revises:
-Create Date: 2025-02-18 16:37:04.346002
+Create Date: 2025-02-19 18:52:21.878941
"""
from collections.abc import Sequence
# revision identifiers, used by Alembic.
-revision: str = "512e973a9ce4"
+revision: str = "b561e6142755"
down_revision: str | None = None
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]