git: 674c4d4fee24 - main - misc/auto-tempo: Manage Jira Tempo Timesheets worklogs from a text file

From: Eugene Grosbein <eugen_at_FreeBSD.org>
Date: Fri, 03 Jul 2026 04:26:08 UTC
The branch main has been updated by eugen:

URL: https://cgit.FreeBSD.org/ports/commit/?id=674c4d4fee24a7dbaaa18142874a3e1d76c40949

commit 674c4d4fee24a7dbaaa18142874a3e1d76c40949
Author:     Vyacheslav Olkhovchenkov <slw@zxy.spb.ru>
AuthorDate: 2026-07-03 04:21:13 +0000
Commit:     Eugene Grosbein <eugen@FreeBSD.org>
CommitDate: 2026-07-03 04:25:29 +0000

    misc/auto-tempo: Manage Jira Tempo Timesheets worklogs from a text file
    
    AutoTempo manages Jira worklogs through the Tempo Timesheets REST API
    using a simple, version-controllable text file.
    It can generate a monthly worklog template pre-populated
    with working days, expand keyword shortcuts and recurring entries
    defined in config.toml, validate daily totals, and apply worklogs
    idempotently to Jira. A helper can draft worklogs from local
    git commit history.
    
    Targets Tempo Timesheets on Jira Server/Data Center;
    authentication uses a Jira personal access token.
    The tool is run from a directory containing a config.toml file.
    
    PR:             296427
---
 misc/Makefile                            |   1 +
 misc/auto-tempo/Makefile                 |  35 +++++++++
 misc/auto-tempo/distinfo                 |   3 +
 misc/auto-tempo/files/patch-autotempo.py | 128 +++++++++++++++++++++++++++++++
 misc/auto-tempo/pkg-descr                |  10 +++
 5 files changed, 177 insertions(+)

diff --git a/misc/Makefile b/misc/Makefile
index 3f4d323dab1c..d28378ad36ba 100644
--- a/misc/Makefile
+++ b/misc/Makefile
@@ -21,6 +21,7 @@
     SUBDIR += asbutton
     SUBDIR += asr-manpages
     SUBDIR += astc-encoder
+    SUBDIR += auto-tempo
     SUBDIR += bb
     SUBDIR += bbjd
     SUBDIR += bdelta
diff --git a/misc/auto-tempo/Makefile b/misc/auto-tempo/Makefile
new file mode 100644
index 000000000000..aa94087889ad
--- /dev/null
+++ b/misc/auto-tempo/Makefile
@@ -0,0 +1,35 @@
+PORTNAME=	auto-tempo
+DISTVERSION=	g20250902
+CATEGORIES=	misc python
+PKGNAMEPREFIX=	${PYTHON_PKGNAMEPREFIX}
+
+MAINTAINER=	slw@zxy.spb.ru
+COMMENT=	Manage Jira Tempo Timesheets worklogs from a text file
+WWW=		https://github.com/atteo/Auto-Tempo
+
+LICENSE=	APACHE20
+
+RUN_DEPENDS=	${PYTHON_PKGNAMEPREFIX}requests>0:www/py-requests@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}toml>0:textproc/py-toml@${PY_FLAVOR} \
+		git:devel/git
+
+USES=		python shebangfix
+SHEBANG_FILES=	autotempo.py
+
+USE_PYTHON=	flavors
+USE_GITHUB=	yes
+
+GH_ACCOUNT=	atteo
+GH_PROJECT=	Auto-Tempo
+GH_TAGNAME=	0eb4159
+
+NO_ARCH=	yes
+NO_BUILD=	yes
+
+PLIST_FILES=	bin/autotempo
+
+do-install:
+		${INSTALL_SCRIPT} ${WRKSRC}/autotempo.py \
+			${STAGEDIR}${PREFIX}/bin/autotempo
+
+.include <bsd.port.mk>
diff --git a/misc/auto-tempo/distinfo b/misc/auto-tempo/distinfo
new file mode 100644
index 000000000000..365b90ef9492
--- /dev/null
+++ b/misc/auto-tempo/distinfo
@@ -0,0 +1,3 @@
+TIMESTAMP = 1782911686
+SHA256 (atteo-Auto-Tempo-g20250902-0eb4159_GH0.tar.gz) = afa9c97cb130a299fd485b164d74ed7643b206c8f79307d1f3ea9b9f697818ea
+SIZE (atteo-Auto-Tempo-g20250902-0eb4159_GH0.tar.gz) = 9111
diff --git a/misc/auto-tempo/files/patch-autotempo.py b/misc/auto-tempo/files/patch-autotempo.py
new file mode 100644
index 000000000000..3407d1037d8a
--- /dev/null
+++ b/misc/auto-tempo/files/patch-autotempo.py
@@ -0,0 +1,128 @@
+Local extensions plus a crash fix, carried as a port patch.
+
+Bug fix (upstreamable): query the days/search endpoint for the authenticated
+user instead of a user key hardcoded in the source.  Without it the response
+is empty and the tool aborts with "list index out of range".
+
+Local feature: derive each working day's required hours from a configurable
+contract value (daily_hours) instead of a hardcoded 8, and shorten a working
+day by preholiday_reduction when the next calendar day is a Tempo HOLIDAY.
+Tempo is used only as the holiday calendar; the work scheme is not required.
+
+--- autotempo.py.orig
++++ autotempo.py
+@@ -25,6 +25,8 @@
+     API_TOKEN = config["JIRA"]["API_TOKEN"]
+     keywords = config["keyword"]
+     EMAIL = config["user"]["email"]
++    DAILY_HOURS = config.get("user", {}).get("daily_hours", 8.0)
++    PREHOLIDAY_REDUCTION = config.get("user", {}).get("preholiday_reduction", 1.0)
+ except KeyError as e:
+     print(f"Missing configuration key: {e}")
+     exit(1)
+@@ -36,6 +38,8 @@
+ 
+ [user]
+ email = "your-email@example.com"                      # Your email address (used for git inspect)
++daily_hours = 8.0                                     # Contract daily working hours
++preholiday_reduction = 1.0                            # Hours subtracted from a pre-holiday day
+ 
+ # Define keywords for common tasks (optional)
+ [keyword.meeting]
+@@ -111,25 +115,41 @@
+     data = {
+         "from": start_date,
+         "to": end_date,
+-        "userKeys": ["JIRAUSER55710"]
++        "userKeys": [WORKER]
+     }
+-    
++
+     try:
+         datetime.datetime.strptime(start_date, "%Y-%m-%d")
+         datetime.datetime.strptime(end_date, "%Y-%m-%d")
+     except ValueError as e:
+         print(f"Invalid date format: {e}")
+-        return set()
++        return {}
+ 
+     response = requests.post(url, headers=headers, data=json.dumps(data))
+ 
+-    if response.status_code == 200:
+-        days_info = response.json()
+-        working_days = {day['date'] for day in days_info[0]['days'] if day['type'] == "WORKING_DAY"}
+-        return working_days
+-    else:
++    if response.status_code != 200:
+         raise FatalError(f"Failed to retrieve working days: {response.status_code} {response.text}", response)
+ 
++    days_info = response.json()
++    if not days_info:
++        raise FatalError(f"Tempo returned no day data for {WORKER}")
++
++    days = days_info[0]['days']
++    holidays = {d['date'] for d in days if d['type'] == "HOLIDAY"}
++
++    required = {}
++    for d in days:
++        if d['type'] != "WORKING_DAY":
++            continue
++        date = d['date']
++        hours = DAILY_HOURS
++        next_day = (datetime.datetime.strptime(date, "%Y-%m-%d")
++                    + datetime.timedelta(days=1)).strftime("%Y-%m-%d")
++        if next_day in holidays:
++            hours -= PREHOLIDAY_REDUCTION
++        required[date] = round(hours, 2)
++    return required
++
+ def get_existing_worklogs_for_date(date):
+     url = f"{JIRA_URL}/rest/tempo-timesheets/4/worklogs/search"
+     headers = {
+@@ -261,6 +281,7 @@
+     # Generate template
+     template_lines = []
+     for day in sorted(working_days):
++        base = working_days[day]
+         day_dt = datetime.datetime.strptime(day, "%Y-%m-%d")
+         day_of_week_name = day_dt.strftime('%A')
+         
+@@ -284,14 +305,14 @@
+                         print(f"Warning: could not parse hours from automatic worklog: '{worklog_line}'. Skipping for hour calculation.")
+         
+         if day_lines:
+-            remaining_hours = 8.0 - total_auto_hours
++            remaining_hours = base - total_auto_hours
+             if remaining_hours > 0:
+                 day_lines.append(f"{day} {remaining_hours:.1f} jira-ticket \"comment\"")
+             elif remaining_hours < 0:
+-                print(f"Warning: total hours for non-overtime automatic worklogs on {day} ({day_of_week_name}) exceeds 8 hours.")
++                print(f"Warning: non-overtime automatic worklogs on {day} ({day_of_week_name}) exceed {base} hours.")
+             template_lines.extend(day_lines)
+         else:
+-            template_lines.append(f"{day} 8.0 jira-ticket \"comment\"")
++            template_lines.append(f"{day} {base} jira-ticket \"comment\"")
+ 
+     # Output template
+     template_content = (
+@@ -362,8 +383,9 @@
+     for date in sorted(all_dates):
+         total_hours = daily_hours.get(date, 0)
+         if date in working_days:
+-            if total_hours != 8:
+-                raise ValueError(f"Total non-overtime logged hours for {date} is {total_hours}, which is not equal to 8. Stopping worklog application.")
++            expected = working_days[date]
++            if total_hours != expected:
++                raise ValueError(f"Total non-overtime logged hours for {date} is {total_hours}, expected {expected}. Stopping worklog application.")
+             valid_dates.append(date)
+         else:
+             if total_hours != 0:
+@@ -518,7 +540,7 @@
+     worklog_lines = []
+     for commit in commits:
+         commit_hash, date, message = commit.split(" ", 2)
+-        worklog_lines.append(f"{date} 8.0 {commit_hash} \"{message}\"")
++        worklog_lines.append(f"{date} {DAILY_HOURS} {commit_hash} \"{message}\"")
+ 
+     # Output worklog
+     worklog_content = "\n".join(worklog_lines)
diff --git a/misc/auto-tempo/pkg-descr b/misc/auto-tempo/pkg-descr
new file mode 100644
index 000000000000..daf30f098003
--- /dev/null
+++ b/misc/auto-tempo/pkg-descr
@@ -0,0 +1,10 @@
+AutoTempo manages Jira worklogs through the Tempo Timesheets REST API using a
+simple, version-controllable text file.  It can generate a monthly worklog
+template pre-populated with working days, expand keyword shortcuts and
+recurring entries defined in config.toml, validate daily totals, and apply
+worklogs idempotently to Jira.  A helper can draft worklogs from local git
+commit history.
+
+Targets Tempo Timesheets on Jira Server/Data Center; authentication uses a
+Jira personal access token.  The tool is run from a directory containing a
+config.toml file.