This is an automated email from the ASF dual-hosted git repository.
kou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/main by this push:
new 288cd43bdc GH-48623: [CI][Archery][Dev] Add missing headers to email
reports (#48624)
288cd43bdc is described below
commit 288cd43bdc2a8a38df414832f4aa985c203a0ffc
Author: Sutou Kouhei <[email protected]>
AuthorDate: Tue Jan 13 12:08:03 2026 +0900
GH-48623: [CI][Archery][Dev] Add missing headers to email reports (#48624)
### Rationale for this change
Our email reports miss the following headers:
* `MIME-Version: 1.0`
* `Content-Type: text/plain; charset="utf-8"`
* `Message-Id: ${AUTO_GENERATED_MESSAGE_ID}`
* `Date: ${DATE_IN_RFC_2822}`
### What changes are included in this PR?
Add these headers.
### Are these changes tested?
Yes.
### Are there any user-facing changes?
No.
* GitHub Issue: #48623
Authored-by: Sutou Kouhei <[email protected]>
Signed-off-by: Sutou Kouhei <[email protected]>
---
.github/workflows/cpp_extra.yml | 2 +
.github/workflows/package_linux.yml | 2 +
.github/workflows/r_extra.yml | 38 ++++++++-------
dev/archery/archery/ci/cli.py | 28 ++++++++---
dev/archery/archery/crossbow/cli.py | 54 ++++++++++++++-------
dev/archery/archery/crossbow/reports.py | 43 ++++++++++++++---
.../{email-report.txt => nightly-email-report.txt} | 5 ++
.../fixtures/token-expiration-email-report.txt | 14 ++++++
dev/archery/archery/crossbow/tests/test_reports.py | 55 ++++++++++++++++++----
.../archery/templates/email_nightly_report.txt.j2 | 10 +---
.../templates/email_token_expiration.txt.j2 | 11 ++---
.../archery/templates/email_workflow_report.txt.j2 | 10 +---
12 files changed, 193 insertions(+), 79 deletions(-)
diff --git a/.github/workflows/cpp_extra.yml b/.github/workflows/cpp_extra.yml
index 614649660d..f353fe8634 100644
--- a/.github/workflows/cpp_extra.yml
+++ b/.github/workflows/cpp_extra.yml
@@ -39,6 +39,7 @@ on:
- 'ci/scripts/util_*'
- 'cpp/**'
- 'compose.yaml'
+ - 'dev/archery/archery/**'
- 'format/Flight.proto'
- 'testing'
tags:
@@ -61,6 +62,7 @@ on:
- 'ci/scripts/util_*'
- 'cpp/**'
- 'compose.yaml'
+ - 'dev/archery/archery/**'
- 'format/Flight.proto'
- 'testing'
types:
diff --git a/.github/workflows/package_linux.yml
b/.github/workflows/package_linux.yml
index 3e4b759215..1d2ae61f1e 100644
--- a/.github/workflows/package_linux.yml
+++ b/.github/workflows/package_linux.yml
@@ -29,6 +29,7 @@ on:
- '.github/workflows/report_ci.yml'
- 'cpp/**'
- 'c_glib/**'
+ - 'dev/archery/archery/**'
- 'dev/release/binary-task.rb'
- 'dev/release/verify-apt.sh'
- 'dev/release/verify-yum.sh'
@@ -43,6 +44,7 @@ on:
- '.github/workflows/report_ci.yml'
- 'cpp/**'
- 'c_glib/**'
+ - 'dev/archery/archery/**'
- 'dev/release/binary-task.rb'
- 'dev/release/verify-apt.sh'
- 'dev/release/verify-yum.sh'
diff --git a/.github/workflows/r_extra.yml b/.github/workflows/r_extra.yml
index 5b11aa12ee..7482725059 100644
--- a/.github/workflows/r_extra.yml
+++ b/.github/workflows/r_extra.yml
@@ -27,15 +27,16 @@ on:
- '.github/workflows/check_labels.yml'
- '.github/workflows/r_extra.yml'
- '.github/workflows/report_ci.yml'
- - "ci/docker/**"
- - "ci/etc/rprofile"
- - "ci/scripts/PKGBUILD"
- - "ci/scripts/cpp_*.sh"
- - "ci/scripts/install_minio.sh"
- - "ci/scripts/r_*.sh"
- - "cpp/**"
- - "compose.yaml"
- - "r/**"
+ - 'ci/docker/**'
+ - 'ci/etc/rprofile'
+ - 'ci/scripts/PKGBUILD'
+ - 'ci/scripts/cpp_*.sh'
+ - 'ci/scripts/install_minio.sh'
+ - 'ci/scripts/r_*.sh'
+ - 'cpp/**'
+ - 'compose.yaml'
+ - 'dev/archery/archery/**'
+ - 'r/**'
tags:
- '**'
pull_request:
@@ -44,15 +45,16 @@ on:
- '.github/workflows/check_labels.yml'
- '.github/workflows/r_extra.yml'
- '.github/workflows/report_ci.yml'
- - "ci/docker/**"
- - "ci/etc/rprofile"
- - "ci/scripts/PKGBUILD"
- - "ci/scripts/cpp_*.sh"
- - "ci/scripts/install_minio.sh"
- - "ci/scripts/r_*.sh"
- - "cpp/**"
- - "compose.yaml"
- - "r/**"
+ - 'ci/docker/**'
+ - 'ci/etc/rprofile'
+ - 'ci/scripts/PKGBUILD'
+ - 'ci/scripts/cpp_*.sh'
+ - 'ci/scripts/install_minio.sh'
+ - 'ci/scripts/r_*.sh'
+ - 'cpp/**'
+ - 'compose.yaml'
+ - 'dev/archery/archery/**'
+ - 'r/**'
types:
- labeled
- opened
diff --git a/dev/archery/archery/ci/cli.py b/dev/archery/archery/ci/cli.py
index bf7b68d532..5597dff733 100644
--- a/dev/archery/archery/ci/cli.py
+++ b/dev/archery/archery/ci/cli.py
@@ -73,6 +73,22 @@ def report_chat(obj, workflow_id, send, repository, ignore,
webhook,
output.write(report_chat.render("workflow_report"))
+class WorkflowEmailReport(EmailReport):
+ def __init__(self, **kwargs):
+ super().__init__('workflow_report', **kwargs)
+
+ def date(self):
+ return self.report.datetime
+
+ def subject(self):
+ workflow = self.report
+ date = self.date().strftime('%Y-%m-%d')
+ return (
+ f'[{date}] Arrow Build Report for Job {workflow.name}: '
+ f'{len(workflow.failed_jobs())} failed'
+ )
+
+
@ci.command()
@click.argument('workflow_id', required=True)
@click.option('--sender-name', '-n',
@@ -105,9 +121,10 @@ def report_email(obj, workflow_id, sender_name,
sender_email, recipient_email,
"""
output = obj['output']
- email_report = EmailReport(
- report=Workflow(workflow_id, repository,
- ignore_job=ignore, gh_token=obj['github_token']),
+ workflow = Workflow(workflow_id, repository,
+ ignore_job=ignore, gh_token=obj['github_token'])
+ email_report = WorkflowEmailReport(
+ report=workflow,
sender_name=sender_name,
sender_email=sender_email,
recipient_email=recipient_email
@@ -119,8 +136,7 @@ def report_email(obj, workflow_id, sender_name,
sender_email, recipient_email,
smtp_password=smtp_password,
smtp_server=smtp_server,
smtp_port=smtp_port,
- recipient_email=recipient_email,
- message=email_report.render("workflow_report")
+ report=email_report
)
else:
- output.write(email_report.render("workflow_report"))
+ output.write(str(email_report.render()))
diff --git a/dev/archery/archery/crossbow/cli.py
b/dev/archery/archery/crossbow/cli.py
index c73c4d1ff7..10aa3dedf4 100644
--- a/dev/archery/archery/crossbow/cli.py
+++ b/dev/archery/archery/crossbow/cli.py
@@ -343,6 +343,22 @@ def latest_prefix(obj, prefix, fetch):
click.echo(latest.branch)
+class NightlyEmailReport(EmailReport):
+ def __init__(self, **kwargs):
+ super().__init__('nightly_report', **kwargs)
+
+ def subject(self):
+ report = self.report
+ n_errors = len(report.tasks_by_state['error'])
+ n_failures = len(report.tasks_by_state['failure'])
+ n_pendings = len(report.tasks_by_state['pending'])
+ return (
+ f'[NIGHTLY] Arrow Build Report for Job {report.job.branch}: '
+ f'{n_errors + n_failures} failed, '
+ f'{n_pendings} pending'
+ )
+
+
@crossbow.command()
@click.argument('job-name', required=True)
@click.option('--sender-name', '-n',
@@ -382,8 +398,9 @@ def report(obj, job_name, sender_name, sender_email,
recipient_email,
queue.fetch()
job = queue.get(job_name)
- email_report = EmailReport(
- report=Report(job),
+ report = Report(job)
+ email_report = NightlyEmailReport(
+ report=report,
sender_name=sender_name,
sender_email=sender_email,
recipient_email=recipient_email
@@ -401,11 +418,10 @@ def report(obj, job_name, sender_name, sender_email,
recipient_email,
smtp_password=smtp_password,
smtp_server=smtp_server,
smtp_port=smtp_port,
- recipient_email=recipient_email,
- message=email_report.render("nightly_report")
+ report=email_report
)
else:
- output.write(email_report.render("nightly_report"))
+ output.write(str(email_report.render()))
@crossbow.command()
@@ -601,6 +617,17 @@ def delete_old_branches(obj, dry_run, days, maximum):
print(batch)
+class TokenExpirationEmailReport(EmailReport):
+ def __init__(self, **kwargs):
+ super().__init__('token_expiration', **kwargs)
+
+ def subject(self):
+ token_expiration_date = self.report.token_expiration_date
+ return (
+ f'[CI] Arrow Crossbow Token Expiration in {token_expiration_date}'
+ )
+
+
@crossbow.command()
@click.option('--days', default=30,
help='Notification will be sent if expiration date is '
@@ -645,23 +672,18 @@ def notify_token_expiration(obj, days, sender_name,
sender_email,
self.token_expiration_date = token_expiration_date
self.days_left = days_left
- email_report = EmailReport(
- report=TokenExpirationReport(
- token_expiration_date or "ALREADY_EXPIRED", days_left),
- sender_name=sender_name,
- sender_email=sender_email,
- recipient_email=recipient_email
- )
+ if not token_expiration_date:
+ token_expiration_date = 'ALREADY_EXPIRED'
+ report = TokenExpirationReport(token_expiration_date, days_left)
+ email_report = TokenExpirationEmailReport(report)
- message = email_report.render("token_expiration").strip()
if send:
ReportUtils.send_email(
smtp_user=smtp_user,
smtp_password=smtp_password,
smtp_server=smtp_server,
smtp_port=smtp_port,
- recipient_email=recipient_email,
- message=message
+ report=email_report
)
else:
- output.write(message)
+ output.write(str(email_report.render()))
diff --git a/dev/archery/archery/crossbow/reports.py
b/dev/archery/archery/crossbow/reports.py
index 32962410d6..a2c0487a2b 100644
--- a/dev/archery/archery/crossbow/reports.py
+++ b/dev/archery/archery/crossbow/reports.py
@@ -17,6 +17,10 @@
import collections
import csv
+import datetime
+import email.headerregistry
+import email.message
+import email.utils
import operator
import fnmatch
import functools
@@ -246,7 +250,7 @@ class ReportUtils:
@classmethod
def send_email(cls, smtp_user, smtp_password, smtp_server, smtp_port,
- recipient_email, message):
+ report):
from smtplib import SMTP, SMTP_SSL
if smtp_port == 465:
@@ -259,7 +263,8 @@ class ReportUtils:
else:
smtp.starttls()
smtp.login(smtp_user, smtp_password)
- smtp.sendmail(smtp_user, recipient_email, message)
+ message = report.render()
+ smtp.send_message(smtp_user, report.recipient_email, message)
@classmethod
def write_csv(cls, report, add_headers=True):
@@ -271,11 +276,6 @@ class ReportUtils:
class EmailReport(JinjaReport):
- templates = {
- 'nightly_report': 'email_nightly_report.txt.j2',
- 'token_expiration': 'email_token_expiration.txt.j2',
- 'workflow_report': 'email_workflow_report.txt.j2',
- }
fields = [
'report',
'sender_name',
@@ -283,6 +283,35 @@ class EmailReport(JinjaReport):
'recipient_email',
]
+ def __init__(self, template_name, **kwargs):
+ self._template_name = template_name
+ super().__init__(**kwargs)
+
+ @property
+ def templates(self):
+ return {
+ self._template_name: f'email_{self._template_name}.txt.j2',
+ }
+
+ def date(self):
+ return None
+
+ def render(self):
+ message = email.message.EmailMessage()
+ message.set_charset('utf-8')
+ message['Message-Id'] = email.utils.make_msgid()
+ date = self.date()
+ if isinstance(date, datetime.datetime):
+ message['Date'] = date
+ else:
+ message['Date'] = email.utils.formatdate(date)
+ message['From'] = email.headerregistry.Address(
+ self.sender_name, addr_spec=self.sender_email)
+ message['To'] =
email.headerregistry.Address(addr_spec=self.recipient_email)
+ message['Subject'] = self.subject()
+ message.set_content(super().render(self._template_name))
+ return message
+
class CommentReport(Report):
diff --git a/dev/archery/archery/crossbow/tests/fixtures/email-report.txt
b/dev/archery/archery/crossbow/tests/fixtures/nightly-email-report.txt
similarity index 83%
rename from dev/archery/archery/crossbow/tests/fixtures/email-report.txt
rename to dev/archery/archery/crossbow/tests/fixtures/nightly-email-report.txt
index c29cafd393..5e7b8e9c67 100644
--- a/dev/archery/archery/crossbow/tests/fixtures/email-report.txt
+++ b/dev/archery/archery/crossbow/tests/fixtures/nightly-email-report.txt
@@ -1,6 +1,11 @@
+MIME-Version: 1.0
+Message-Id: <message-id>
+Date: date
From: Sender Reporter <[email protected]>
To: [email protected]
Subject: [NIGHTLY] Arrow Build Report for Job ursabot-1: 2 failed, 1 pending
+Content-Type: text/plain; charset="utf-8"
+Content-Transfer-Encoding: 7bit
Arrow Build Report for Job ursabot-1
diff --git
a/dev/archery/archery/crossbow/tests/fixtures/token-expiration-email-report.txt
b/dev/archery/archery/crossbow/tests/fixtures/token-expiration-email-report.txt
new file mode 100644
index 0000000000..1f8ccbf30c
--- /dev/null
+++
b/dev/archery/archery/crossbow/tests/fixtures/token-expiration-email-report.txt
@@ -0,0 +1,14 @@
+MIME-Version: 1.0
+Message-Id: <message-id>
+Date: date
+From: Sender Reporter <[email protected]>
+To: [email protected]
+Subject: [CI] Arrow Crossbow Token Expiration in 2026-01-17
+Content-Type: text/plain; charset="utf-8"
+Content-Transfer-Encoding: 7bit
+
+The Arrow Crossbow Token will expire in 7 days.
+
+Please generate a new Token. Send it to Apache INFRA to update the
+CROSSBOW_GITHUB_TOKEN. Update it on the crossbow repository and in
+the Azure pipelines.
diff --git a/dev/archery/archery/crossbow/tests/test_reports.py
b/dev/archery/archery/crossbow/tests/test_reports.py
index 620b4c78bb..02012d2f1b 100644
--- a/dev/archery/archery/crossbow/tests/test_reports.py
+++ b/dev/archery/archery/crossbow/tests/test_reports.py
@@ -15,11 +15,12 @@
# specific language governing permissions and limitations
# under the License.
+import re
import textwrap
+from archery.crossbow.cli import (NightlyEmailReport,
TokenExpirationEmailReport)
from archery.crossbow.core import yaml
-from archery.crossbow.reports import (ChatReport, CommentReport, EmailReport,
- Report)
+from archery.crossbow.reports import (ChatReport, CommentReport, Report)
def test_crossbow_comment_formatter(load_fixture):
@@ -71,19 +72,55 @@ def
test_crossbow_chat_report_extra_message_success(load_fixture):
assert report_chat.render("text") == textwrap.dedent(expected_msg)
-def test_crossbow_email_report(load_fixture):
- expected_msg = load_fixture('email-report.txt')
+def test_crossbow_nightly_email_report(load_fixture):
+ expected_msg = load_fixture('nightly-email-report.txt')
job = load_fixture('crossbow-job.yaml', decoder=yaml.load)
report = Report(job)
assert report.tasks_by_state is not None
- email_report = EmailReport(report=report, sender_name="Sender Reporter",
- sender_email="[email protected]",
- recipient_email="[email protected]")
+ email_report = NightlyEmailReport(
+ report=report,
+ sender_name='Sender Reporter',
+ sender_email='[email protected]',
+ recipient_email='[email protected]'
+ )
- assert (
- email_report.render("nightly_report") == textwrap.dedent(expected_msg)
+ actual = str(email_report.render())
+ # Normalize dynamic headers
+ actual = re.sub(r'(?m)^Message-Id: <.+?>',
+ 'Message-Id: <message-id>',
+ actual)
+ actual = re.sub(r'(?m)^Date: [^\n]+ -0000$',
+ 'Date: date',
+ actual)
+ assert actual == textwrap.dedent(expected_msg)
+
+
+def test_crossbow_token_expiration_email_report(load_fixture):
+ expected_msg = load_fixture('token-expiration-email-report.txt')
+
+ class TokenExpirationReport:
+ def __init__(self, token_expiration_date, days_left):
+ self.token_expiration_date = token_expiration_date
+ self.days_left = days_left
+
+ report = TokenExpirationReport('2026-01-17', 7)
+ email_report = TokenExpirationEmailReport(
+ report=report,
+ sender_name='Sender Reporter',
+ sender_email='[email protected]',
+ recipient_email='[email protected]'
)
+ actual = str(email_report.render())
+ # Normalize dynamic headers
+ actual = re.sub(r'(?m)^Message-Id: <.+?>',
+ 'Message-Id: <message-id>',
+ actual)
+ actual = re.sub(r'(?m)^Date: [^\n]+ -0000$',
+ 'Date: date',
+ actual)
+ assert actual == textwrap.dedent(expected_msg)
+
def test_crossbow_export_report(load_fixture):
job = load_fixture('crossbow-job.yaml', decoder=yaml.load)
diff --git a/dev/archery/archery/templates/email_nightly_report.txt.j2
b/dev/archery/archery/templates/email_nightly_report.txt.j2
index bc040734b0..7b43d7c867 100644
--- a/dev/archery/archery/templates/email_nightly_report.txt.j2
+++ b/dev/archery/archery/templates/email_nightly_report.txt.j2
@@ -15,13 +15,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-#}
-{%- if True -%}
-{%- endif -%}
-From: {{ sender_name }} <{{ sender_email }}>
-To: {{ recipient_email }}
-Subject: [NIGHTLY] Arrow Build Report for Job {{report.job.branch}}: {{
(report.tasks_by_state["error"] | length) + (report.tasks_by_state["failure"]
| length) }} failed, {{ report.tasks_by_state["pending"] | length }} pending
-
+-#}
Arrow Build Report for Job {{ report.job.branch }}
See https://s3.amazonaws.com/arrow-data/index.html for more information.
@@ -58,4 +52,4 @@ Succeeded Tasks:
- {{ task_name }}
{{ report.task_url(task) }}
{% endfor %}
-{%- endif -%}
\ No newline at end of file
+{%- endif -%}
diff --git a/dev/archery/archery/templates/email_token_expiration.txt.j2
b/dev/archery/archery/templates/email_token_expiration.txt.j2
index 54c2005e57..340cb4a535 100644
--- a/dev/archery/archery/templates/email_token_expiration.txt.j2
+++ b/dev/archery/archery/templates/email_token_expiration.txt.j2
@@ -15,12 +15,9 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-#}
-From: {{ sender_name }} <{{ sender_email }}>
-To: {{ recipient_email }}
-Subject: [CI] Arrow Crossbow Token Expiration in {{
report.token_expiration_date }}
-
+-#}
The Arrow Crossbow Token will expire in {{ report.days_left }} days.
-Please generate a new Token. Send it to Apache INFRA to update the
CROSSBOW_GITHUB_TOKEN.
-Update it on the crossbow repository and in the Azure pipelines.
+Please generate a new Token. Send it to Apache INFRA to update the
+CROSSBOW_GITHUB_TOKEN. Update it on the crossbow repository and in
+the Azure pipelines.
diff --git a/dev/archery/archery/templates/email_workflow_report.txt.j2
b/dev/archery/archery/templates/email_workflow_report.txt.j2
index 193856c180..6668d6c67e 100644
--- a/dev/archery/archery/templates/email_workflow_report.txt.j2
+++ b/dev/archery/archery/templates/email_workflow_report.txt.j2
@@ -15,13 +15,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-#}
-{%- if True -%}
-{%- endif -%}
-From: {{ sender_name }} <{{ sender_email }}>
-To: {{ recipient_email }}
-Subject: [{{ report.datetime.strftime('%Y-%m-%d') }}] Arrow Build Report for
{{ report.name }}: {{ report.failed_jobs() | length }} failed
-
+-#}
Arrow Build Report for {{ report.name }}
Workflow URL: {{ report.url }}
@@ -42,4 +36,4 @@ Succeeded Jobs:
- {{ job.name }}
{{ job.url }}
{% endfor %}
-{%- endif -%}
\ No newline at end of file
+{%- endif -%}