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 0a85dd2  Add a page to resolve a vote for a specific release
0a85dd2 is described below

commit 0a85dd24682c6faf77f678542460b652f0f785ab
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Apr 22 19:02:57 2025 +0100

    Add a page to resolve a vote for a specific release
---
 atr/routes/candidate.py                      |  63 ++++++++---
 atr/static/css/atr.css                       |   2 +-
 atr/templates/candidate-resolve-release.html | 154 +++++++++++++++++++++++++++
 atr/templates/index-committer.html           |   2 +-
 playwright/test.py                           |  83 ++++++---------
 5 files changed, 239 insertions(+), 65 deletions(-)

diff --git a/atr/routes/candidate.py b/atr/routes/candidate.py
index ff34561..0a915df 100644
--- a/atr/routes/candidate.py
+++ b/atr/routes/candidate.py
@@ -33,6 +33,7 @@ import atr.db.models as models
 import atr.revision as revision
 import atr.routes as routes
 import atr.routes.preview as preview
+import atr.routes.root as root
 import atr.util as util
 
 if asfquart.APP is ...:
@@ -69,6 +70,49 @@ async def resolve(session: routes.CommitterSession) -> 
response.Response | str:
     return await _resolve_post(session)
 
 
[email protected]("/resolve/<project_name>/<version_name>")
+async def resolve_release(
+    session: routes.CommitterSession, project_name: str, version_name: str
+) -> response.Response | str:
+    """Resolve the vote on a release candidate."""
+    if not any((p.name == project_name) for p in (await 
session.user_projects)):
+        return await session.redirect(root.index, error="You do not have 
access to this project")
+
+    async with db.session() as data:
+        release_name = models.release_name(project_name, version_name)
+        release = await data.release(name=release_name, _project=True, 
_committee=True, _tasks=True).demand(
+            base.ASFQuartException("Release does not exist", errorcode=404)
+        )
+
+    form = await ResolveForm.create_form()
+
+    # Find the most recent VOTE_INITIATE task for this release
+    # TODO: Make this a proper query
+    latest_vote_task = None
+    task_mid = None
+    archive_url = None
+    for task in sorted(release.tasks, key=lambda t: t.added, reverse=True):
+        if task.task_type == models.TaskType.VOTE_INITIATE:
+            latest_vote_task = task
+            break
+
+    logging.warning(f"Latest vote task 2: {latest_vote_task}")
+    if latest_vote_task and (latest_vote_task.status == 
models.TaskStatus.COMPLETED) and latest_vote_task.result:
+        task_mid = _task_mid(latest_vote_task)
+        archive_url = await _task_archive_url(task_mid)
+
+    return await quart.render_template(
+        "candidate-resolve-release.html",
+        release=release,
+        format_artifact_name=_format_artifact_name,
+        form=form,
+        format_datetime=routes.format_datetime,
+        vote_task=latest_vote_task,
+        task_mid=task_mid,
+        archive_url=archive_url,
+    )
+
+
 @routes.committer("/candidate/view/<project_name>/<version_name>")
 async def view(session: routes.CommitterSession, project_name: str, 
version_name: str) -> response.Response | str:
     """View all the files in the rsync upload directory for a release."""
@@ -176,6 +220,7 @@ async def _resolve_get(session: routes.CommitterSession) -> 
str:
                 latest_vote_task = task
                 break
 
+        logging.warning(f"Latest vote task: {latest_vote_task}")
         if latest_vote_task and (latest_vote_task.status == 
models.TaskStatus.COMPLETED) and latest_vote_task.result:
             task_mid = _task_mid(latest_vote_task)
             archive_url = await _task_archive_url(task_mid)
@@ -234,22 +279,10 @@ async def _resolve_post(session: routes.CommitterSession) 
-> response.Response:
                 release.phase = models.ReleasePhase.RELEASE_CANDIDATE_DRAFT
                 success_message = "Vote marked as failed"
 
-            # # Create a task for vote resolution notification
-            # task = models.Task(
-            #     status=models.TaskStatus.QUEUED,
-            #     task_type="vote_resolve",
-            #     task_args=[
-            #         candidate_name,
-            #         vote_result,
-            #         session.uid,
-            #     ],
-            # )
-            # data.add(task)
-
-            await data.commit()
-
     await _resolve_post_files(project_name, release, vote_result, session.uid)
-    return await session.redirect(preview.previews, success=success_message)
+    return await session.redirect(
+        preview.announce_release, success=success_message, 
project_name=project_name, version_name=release.version
+    )
 
 
 async def _resolve_post_files(project_name: str, release: models.Release, 
vote_result: str, asf_uid: str) -> None:
diff --git a/atr/static/css/atr.css b/atr/static/css/atr.css
index 68588e6..789fc1d 100644
--- a/atr/static/css/atr.css
+++ b/atr/static/css/atr.css
@@ -408,7 +408,7 @@ img {
 }
 
 h1 strong {
-    font-weight: 550;
+    font-weight: 525;
 }
 
 h1 em {
diff --git a/atr/templates/candidate-resolve-release.html 
b/atr/templates/candidate-resolve-release.html
new file mode 100644
index 0000000..c133fa4
--- /dev/null
+++ b/atr/templates/candidate-resolve-release.html
@@ -0,0 +1,154 @@
+{% extends "layouts/base.html" %}
+
+{% block title %}
+  Resolve vote for {{ release.short_display_name }} ~ ATR
+{% endblock title %}
+
+{% block description %}
+  Resolve the vote for the {{ release.project.display_name }} {{ 
release.version }} release candidate.
+{% endblock description %}
+
+{% block stylesheets %}
+  {{ super() }}
+  <style>
+      .atr-candidate-meta-item::after {
+          content: "•";
+          margin-left: 1rem;
+          color: #ccc;
+      }
+
+      .atr-candidate-meta-item:last-child::after {
+          content: none;
+      }
+  </style>
+{% endblock stylesheets %}
+
+{% block content %}
+  <p>
+    <a href="{{ as_url(routes.root.index) }}" class="back-link">← Back to 
Select a release</a>
+  </p>
+  <h1>
+    Resolve vote for <strong>{{ release.project.short_display_name }}</strong> 
<em>{{ release.version }}</em>
+  </h1>
+  <p class="intro">
+    Resolve the vote for the <strong>release candidate</strong> for {{ 
release.project.display_name }} {{ release.version }}. If you resolve a vote as 
passed, the release candidate will be promoted to the next stage, making it a 
release preview. If you resolve a vote as failed, the release candidate will be 
returned to the draft stage.
+  </p>
+
+  <div class="card mb-4">
+    <div class="card-header d-flex justify-content-between align-items-center">
+      <h5 class="mb-0">Release information</h5>
+    </div>
+    <div class="card-body">
+      <div class="row">
+        <div class="col-md-6">
+          <p>
+            <strong>Project:</strong>
+            <a href="{{ as_url(routes.projects.view, 
name=release.project.name) }}">{{ release.project.display_name }}</a>
+          </p>
+          <p>
+            <strong>Version:</strong> {{ release.version }}
+          </p>
+          <p>
+            <strong>Label:</strong> {{ release.name }}
+          </p>
+        </div>
+        <div class="col-md-6">
+          <p>
+            <strong>Phase:</strong> <span>{{ release.phase.value.upper() 
}}</span>
+          </p>
+          <p>
+            <strong>Created:</strong> {{ format_datetime(release.created) }}
+          </p>
+          {% if release.vote_started %}
+            <p>
+              <strong>Vote Started:</strong> {{ 
format_datetime(release.vote_started) }}
+            </p>
+          {% endif %}
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <div class="card mb-4">
+    <div class="card-header">
+      <h5 class="mb-0">Vote email details</h5>
+    </div>
+    <div class="card-body">
+      <div class="p-3 border rounded bg-white mb-3">
+        {% if vote_task %}
+          {% if vote_task.status.value == "completed" %}
+            <p class="mb-0 text-success fw-semibold">
+              <i class="bi bi-check-circle-fill me-1"></i> Vote email sent: {{ 
format_datetime(vote_task.completed) }}
+            </p>
+            {% if task_mid %}
+              <p class="mt-2 mb-0 text-muted ps-4">
+                Message-ID: <code class="user-select-all">{{ task_mid }}</code>
+              </p>
+            {% endif %}
+          {% elif vote_task.status.value == "failed" %}
+            <p class="mb-1 text-danger fw-semibold">
+              <i class="bi bi-x-octagon-fill me-1"></i> Vote email failed: {{ 
format_datetime(vote_task.completed) }}
+            </p>
+            <div class="alert alert-danger mt-2 mb-0 p-2" role="alert">
+              <p class="mb-0 p-2 text-danger">{{ vote_task.error }}</p>
+            </div>
+          {% else %}
+            <p class="mb-0 text-warning fw-semibold">
+              <i class="bi bi-hourglass-split me-1"></i> Vote email status: {{ 
vote_task.status.value.upper() }}
+              {% if vote_task.started %}
+                (Started: {{ format_datetime(vote_task.started) }})
+              {% else %}
+                (Added: {{ format_datetime(vote_task.added) }})
+              {% endif %}
+            </p>
+          {% endif %}
+          {% if archive_url %}
+            <p class="mt-2 mb-0 text-muted ps-4">
+              <a href="{{ archive_url }}" target="_blank" rel="noopener 
noreferrer">View vote email in the archive <i class="bi bi-box-arrow-up-right 
ms-1"></i></a>
+            </p>
+          {% elif task_mid %}
+            <p class="mt-2 mb-0 text-muted ps-4">Could not retrieve archive 
URL for this message.</p>
+          {% endif %}
+        {% else %}
+          <p class="mb-0 text-muted">
+            <i class="bi bi-question-circle me-1"></i> Vote email: No vote 
initiation task found for this release.
+          </p>
+        {% endif %}
+      </div>
+    </div>
+  </div>
+
+  <div class="card mb-4">
+    <div class="card-header">
+      <h5 class="mb-0">Resolve vote</h5>
+    </div>
+    <div class="card-body">
+      <form method="post"
+            action="{{ as_url(routes.candidate.resolve) }}"
+            class="atr-canary py-4 px-5 needs-validation"
+            novalidate>
+        <input type="hidden" name="candidate_name" value="{{ release.name }}" 
/>
+        {{ form.csrf_token }}
+
+        <div class="mb-3 pb-3 row border-bottom">
+          <label class="col-sm-3 col-form-label text-sm-end fw-semibold">{{ 
form.vote_result.label.text }}:</label>
+          <div class="col-sm-9 pt-2">
+            {% for subfield in form.vote_result %}
+              <div class="form-check form-check-inline">
+                {{ subfield(class="form-check-input" + (" is-invalid" if 
form.vote_result.errors else "") , id=subfield.id ~ "_" ~ loop.index) }}
+                <label class="form-check-label" for="{{ subfield.id }}_{{ 
loop.index }}">{{ subfield.label.text }}</label>
+              </div>
+            {% endfor %}
+            {% if form.vote_result.errors %}
+              <div class="invalid-feedback d-block">{{ 
form.vote_result.errors[0] }}</div>
+            {% endif %}
+          </div>
+        </div>
+
+        <div class="row">
+          <div class="col-sm-9 offset-sm-3">{{ form.submit(class_="btn 
btn-primary mt-3") }}</div>
+        </div>
+      </form>
+    </div>
+  </div>
+{% endblock content %}
diff --git a/atr/templates/index-committer.html 
b/atr/templates/index-committer.html
index c80a689..a361faa 100644
--- a/atr/templates/index-committer.html
+++ b/atr/templates/index-committer.html
@@ -122,7 +122,7 @@
             {% if release.phase.value == "release_candidate_draft" %}
               {% set release_link = as_url(routes.draft.compose, 
project_name=release.project.name, version_name=release.version) %}
             {% elif release.phase.value == "release_candidate" %}
-              {% set release_link = as_url(routes.candidate.view, 
project_name=release.project.name, version_name=release.version) %}
+              {% set release_link = as_url(routes.candidate.resolve_release, 
project_name=release.project.name, version_name=release.version) %}
             {% elif release.phase.value == "release_preview" %}
               {% set release_link = as_url(routes.preview.announce_release, 
project_name=release.project.name, version_name=release.version) %}
             {% endif %}
diff --git a/playwright/test.py b/playwright/test.py
index 057a5f0..df6ccc5 100644
--- a/playwright/test.py
+++ b/playwright/test.py
@@ -118,8 +118,8 @@ def lifecycle_01_add_draft(page: sync_api.Page, 
credentials: Credentials, versio
     sync_api.expect(submit_button_locator).to_be_enabled()
     submit_button_locator.click()
 
-    logging.info(f"Waiting for navigation to 
/draft/content/tooling-test-example/{version_name} after adding draft")
-    wait_for_path(page, f"/draft/content/tooling-test-example/{version_name}")
+    logging.info(f"Waiting for navigation to 
/compose/tooling-test-example/{version_name} after adding draft")
+    wait_for_path(page, f"/compose/tooling-test-example/{version_name}")
     logging.info("Add draft actions completed successfully")
 
 
@@ -132,9 +132,9 @@ def lifecycle_02_check_draft_added(page: sync_api.Page, 
credentials: Credentials
 
 
 def lifecycle_03_add_file(page: sync_api.Page, credentials: Credentials, 
version_name: str) -> None:
-    logging.info(f"Navigating to the add file page for tooling-test-example 
{version_name}")
-    go_to_path(page, f"/draft/add/tooling-test-example/{version_name}")
-    logging.info("Add file page loaded")
+    logging.info(f"Navigating to the upload file page for tooling-test-example 
{version_name}")
+    go_to_path(page, f"/upload/tooling-test-example/{version_name}")
+    logging.info("Upload file page loaded")
 
     logging.info("Locating the file input")
     file_input_locator = page.locator('input[name="file_data"]')
@@ -208,25 +208,16 @@ def lifecycle_05_resolve_vote(page: sync_api.Page, 
credentials: Credentials, ver
     sync_api.expect(submit_button_locator).to_be_enabled()
     submit_button_locator.click()
 
-    logging.info("Waiting for navigation to /previews after resolving the 
vote")
-    wait_for_path(page, "/previews")
+    logging.info(
+        f"Waiting for navigation to 
/preview/announce/tooling-test-example/{version_name} after resolving the vote"
+    )
+    wait_for_path(page, 
f"/preview/announce/tooling-test-example/{version_name}")
     logging.info("Vote resolution actions completed successfully")
 
 
 def lifecycle_06_announce_preview(page: sync_api.Page, credentials: 
Credentials, version_name: str) -> None:
-    logging.info(f"Locating the link to announce the preview for 
tooling-test-example {version_name}")
-    announce_link_locator = page.locator(f'a[title="Announce Apache Tooling 
Test Example {version_name}"]')
-    sync_api.expect(announce_link_locator).to_be_visible()
-
-    logging.info("Following the link to announce the preview")
-    announce_link_locator.click()
-
-    logging.info("Waiting for navigation to /preview/announce")
-    wait_for_path(page, "/preview/announce")
-    logging.info("Announce preview navigation completed successfully")
-
     logging.info(f"Locating the announcement form for tooling-test-example 
{version_name}")
-    form_locator = page.locator(f'#tooling-test-example-{esc_id(version_name)} 
form[action="/preview/announce"]')
+    form_locator = page.locator('form[action="/preview/announce"]')
     sync_api.expect(form_locator).to_be_visible()
 
     logging.info("Locating the confirmation checkbox within the form")
@@ -509,7 +500,7 @@ def test_checks_01_hashing_sha512(page: sync_api.Page, 
credentials: Credentials)
     version_name = "0.2"
     filename_sha512 = f"apache-{project_name}-{version_name}.tar.gz.sha512"
     evaluate_page_path = f"/draft/evaluate/{project_name}/{version_name}"
-    evaluate_file_path = f"{evaluate_page_path}/{filename_sha512}"
+    report_file_path = 
f"/report/{project_name}/{version_name}/{filename_sha512}"
 
     logging.info(f"Starting hashing check test for {filename_sha512}")
 
@@ -525,9 +516,9 @@ def test_checks_01_hashing_sha512(page: sync_api.Page, 
credentials: Credentials)
     logging.info(f"Clicking 'Evaluate file' link for {filename_sha512}")
     evaluate_link_locator.click()
 
-    logging.info(f"Waiting for navigation to {evaluate_file_path}")
-    wait_for_path(page, evaluate_file_path)
-    logging.info(f"Successfully navigated to {evaluate_file_path}")
+    logging.info(f"Waiting for navigation to {report_file_path}")
+    wait_for_path(page, report_file_path)
+    logging.info(f"Successfully navigated to {report_file_path}")
 
     logging.info("Verifying Hashing Check status")
     hashing_check_div_locator = 
page.locator("div.border:has(span.fw-bold:text-is('Hashing Check'))")
@@ -544,7 +535,7 @@ def test_checks_02_license_files(page: sync_api.Page, 
credentials: Credentials)
     version_name = "0.2"
     filename_targz = f"apache-{project_name}-{version_name}.tar.gz"
     evaluate_page_path = f"/draft/evaluate/{project_name}/{version_name}"
-    evaluate_file_path = f"{evaluate_page_path}/{filename_targz}"
+    report_file_path = 
f"/report/{project_name}/{version_name}/{filename_targz}"
 
     logging.info(f"Starting License Files check test for {filename_targz}")
 
@@ -560,9 +551,9 @@ def test_checks_02_license_files(page: sync_api.Page, 
credentials: Credentials)
     logging.info(f"Clicking 'Evaluate file' link for {filename_targz}")
     evaluate_link_locator.click()
 
-    logging.info(f"Waiting for navigation to {evaluate_file_path}")
-    wait_for_path(page, evaluate_file_path)
-    logging.info(f"Successfully navigated to {evaluate_file_path}")
+    logging.info(f"Waiting for navigation to {report_file_path}")
+    wait_for_path(page, report_file_path)
+    logging.info(f"Successfully navigated to {report_file_path}")
 
     logging.info("Verifying License Files check status")
     license_check_div_locator = 
page.locator("div.border:has(span.fw-bold:text-is('License Files'))")
@@ -578,15 +569,14 @@ def test_checks_03_license_headers(page: sync_api.Page, 
credentials: Credentials
     project_name = "tooling-test-example"
     version_name = "0.2"
     filename_targz = f"apache-{project_name}-{version_name}.tar.gz"
-    evaluate_page_path = f"/draft/evaluate/{project_name}/{version_name}"
-    evaluate_file_path = f"{evaluate_page_path}/{filename_targz}"
+    report_file_path = 
f"/report/{project_name}/{version_name}/{filename_targz}"
 
     logging.info(f"Starting License Headers check test for {filename_targz}")
 
     # Don't repeat the link test, just go straight there
-    logging.info(f"Navigating to evaluate file page {evaluate_file_path}")
-    go_to_path(page, evaluate_file_path)
-    logging.info(f"Successfully navigated to {evaluate_file_path}")
+    logging.info(f"Navigating to report page {report_file_path}")
+    go_to_path(page, report_file_path)
+    logging.info(f"Successfully navigated to {report_file_path}")
 
     logging.info("Verifying License Headers check status")
     header_check_div_locator = 
page.locator("div.border:has(span.fw-bold:text-is('License Headers'))")
@@ -602,14 +592,13 @@ def test_checks_04_paths(page: sync_api.Page, 
credentials: Credentials) -> None:
     project_name = "tooling-test-example"
     version_name = "0.2"
     filename_sha512 = f"apache-{project_name}-{version_name}.tar.gz.sha512"
-    evaluate_page_path = f"/draft/evaluate/{project_name}/{version_name}"
-    evaluate_file_path = f"{evaluate_page_path}/{filename_sha512}"
+    report_file_path = 
f"/report/{project_name}/{version_name}/{filename_sha512}"
 
     logging.info(f"Starting Paths check test for {filename_sha512}")
 
-    logging.info(f"Navigating to evaluate file page {evaluate_file_path}")
-    go_to_path(page, evaluate_file_path)
-    logging.info(f"Successfully navigated to {evaluate_file_path}")
+    logging.info(f"Navigating to report page {report_file_path}")
+    go_to_path(page, report_file_path)
+    logging.info(f"Successfully navigated to {report_file_path}")
 
     # TODO: It's a bit strange to have the status in the check name
     # But we have to do this because we need separate Recorder objects
@@ -627,14 +616,13 @@ def test_checks_05_signature(page: sync_api.Page, 
credentials: Credentials) -> N
     project_name = "tooling-test-example"
     version_name = "0.2"
     filename_asc = f"apache-{project_name}-{version_name}.tar.gz.asc"
-    evaluate_page_path = f"/draft/evaluate/{project_name}/{version_name}"
-    evaluate_file_path = f"{evaluate_page_path}/{filename_asc}"
+    report_file_path = f"/report/{project_name}/{version_name}/{filename_asc}"
 
     logging.info(f"Starting Signature check test for {filename_asc}")
 
-    logging.info(f"Navigating to evaluate file page {evaluate_file_path}")
-    go_to_path(page, evaluate_file_path)
-    logging.info(f"Successfully navigated to {evaluate_file_path}")
+    logging.info(f"Navigating to report page {report_file_path}")
+    go_to_path(page, report_file_path)
+    logging.info(f"Successfully navigated to {report_file_path}")
 
     logging.info("Verifying Signature Check status")
     signature_check_div_locator = 
page.locator("div.border:has(span.fw-bold:text-is('Signature Check'))")
@@ -650,14 +638,13 @@ def test_checks_06_targz(page: sync_api.Page, 
credentials: Credentials) -> None:
     project_name = "tooling-test-example"
     version_name = "0.2"
     filename_targz = f"apache-{project_name}-{version_name}.tar.gz"
-    evaluate_page_path = f"/draft/evaluate/{project_name}/{version_name}"
-    evaluate_file_path = f"{evaluate_page_path}/{filename_targz}"
+    report_file_path = 
f"/report/{project_name}/{version_name}/{filename_targz}"
 
     logging.info(f"Starting Targz checks for {filename_targz}")
 
-    logging.info(f"Navigating to evaluate file page {evaluate_file_path}")
-    go_to_path(page, evaluate_file_path)
-    logging.info(f"Successfully navigated to {evaluate_file_path}")
+    logging.info(f"Navigating to report page {report_file_path}")
+    go_to_path(page, report_file_path)
+    logging.info(f"Successfully navigated to {report_file_path}")
 
     logging.info("Verifying Targz Integrity status")
     integrity_div_locator = 
page.locator("div.border:has(span.fw-bold:text-is('Targz Integrity'))")
@@ -995,7 +982,7 @@ def test_ssh_02_rsync_upload(page: sync_api.Page, 
credentials: Credentials) -> N
     logging.info("rsync upload test completed successfully")
 
     logging.info(f"Extracting latest revision from {evaluate_path}")
-    revision_link_locator = 
page.locator(f'a[href^="/draft/revisions/{project_name}/{version_name}#"]')
+    revision_link_locator = 
page.locator(f'a[href^="/revisions/{project_name}/{version_name}#"]')
     sync_api.expect(revision_link_locator).to_be_visible()
     revision_href = revision_link_locator.get_attribute("href")
     if not revision_href:


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to