Skip to content

Commit a8e4912

Browse files
authored
Report nightly releases in Asana (#6261)
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1210591276276376?focus=true ### Description Report the nightly releases in Asana with associated changelog ### Steps to test this PR _Test_ Run the following command ```bash python3 scripts/release/create-asana-release.py \ --tag "5.238.0.6-nightly" \ --android-repo-path "." \ --trigger-phrase "Task/Issue URL:" \ --asana-project-id "1184843898389381" \ --asana-section-id "1210591276276370" \ --asana-workspace-id "137249556945" \ --asana-api-key-env-var ASANA_TOKEN ``` `ASANA_TOKEN` is an env variable in your computer with the Asana API token. That should create the proper task in the proper project.
1 parent 464fe69 commit a8e4912

3 files changed

Lines changed: 234 additions & 1 deletion

File tree

.github/workflows/release_nightly.yml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,25 @@ jobs:
144144
- name: Set successful summary
145145
if: steps.check_for_changes.outputs.has_changes == 'true'
146146
run: |
147-
echo "### Nightly release completed! :rocket:" >> $GITHUB_STEP_SUMMARY
147+
echo "### Nightly release completed! :rocket:" >> $GITHUB_STEP_SUMMARY
148+
149+
- name: Create Asana nightly release task
150+
env:
151+
ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }}
152+
shell: bash
153+
working-directory: Android
154+
run: |
155+
RELEASE_TASK_URL=$(python ./scripts/release/create-asana-release.py \
156+
--android-repo-path . \
157+
--windows-browser-repo-path ../windows-browser \
158+
--tag ${{ steps.generate_version_name.outputs.version }} \
159+
--version-string ${{ env.VERSION_STRING }}-${{ github.run_number }} \
160+
--trigger-phrase 'Task/Issue URL:' \
161+
--asana-project-id ${{ vars.GH_ANDROID_APP_RELEASES_PROJECT_ID }} \
162+
--asana-section-id ${{ vars.GH_ANDROID_APP_RELEASES_NIGHTLY_SECTION_ID }} \
163+
--asana-workspace-id ${{ secrets.GH_ASANA_WORKSPACE_ID }} \
164+
--asana-api-key-env-var ASANA_ACCESS_TOKEN)
165+
echo "RELEASE_TASK_URL=$RELEASE_TASK_URL" >> $GITHUB_ENV
148166
149167
- name: Create Asana task when workflow failed
150168
if: ${{ failure() }}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import re
5+
import argparse
6+
from typing import List
7+
import git
8+
from git import Repo
9+
import asana
10+
from dataclasses import dataclass
11+
import sys
12+
import subprocess
13+
14+
@dataclass
15+
class AsanaTaskLink:
16+
url: str | None
17+
commit_hash: str
18+
19+
def log(message: str) -> None:
20+
print(message, file=sys.stderr)
21+
22+
def get_commits_between(repo_path: str, start_commit: str, end_commit: str) -> List[git.Commit]:
23+
"""
24+
Get a list of commits between two commit hashes in a git repository.
25+
"""
26+
repo = Repo(repo_path)
27+
28+
# Get all commits between start (exclusive) and end (inclusive)
29+
commits = list(repo.iter_commits(f"{start_commit}..{end_commit}"))
30+
31+
return commits
32+
33+
def extract_asana_task_links(commits: List[git.Commit], url_prefix: str) -> List[AsanaTaskLink]:
34+
"""
35+
Extract Asana task links from commit messages.
36+
"""
37+
task_links = []
38+
url_pattern = re.compile(rf"{re.escape(url_prefix)}\s*(https://app\.asana\.com/\S*)")
39+
40+
for commit in commits:
41+
message = commit.message
42+
match = url_pattern.search(message)
43+
full_url = None
44+
if match:
45+
full_url = match.group(1)
46+
47+
task_links.append(AsanaTaskLink(
48+
url=full_url,
49+
commit_hash=commit.hexsha,
50+
))
51+
52+
return task_links
53+
54+
def extract_task_id_from_url(url: str) -> str:
55+
"""
56+
Extract task ID from various Asana URL formats.
57+
Examples:
58+
- https://app.asana.com/1/137249556945/project/1209107918776641/task/1210066941136479?focus=true
59+
- https://app.asana.com/1/137249556945/project/1209077031784564/task/1210067100873189
60+
- https://app.asana.com/0/1208400340757517/1208902659709099
61+
- https://app.asana.com/0/1208400340757517/1208902659709099/f
62+
"""
63+
# Remove any query parameters
64+
url = url.split('?')[0]
65+
66+
# Split by slashes and get the last non-empty part
67+
parts = [p for p in url.split('/') if p]
68+
69+
# The task ID is either:
70+
# - The last part before any query parameters (for /task/ format)
71+
# - The last part (for short format)
72+
task_id = parts[-1]
73+
74+
# If the last part is 'f', get the previous part
75+
if task_id == 'f':
76+
task_id = parts[-2]
77+
78+
return task_id
79+
80+
def create_asana_task(client: asana.ApiClient,
81+
workspace_id: str,
82+
release_tag: str,
83+
task_links: List[AsanaTaskLink],
84+
section_id: str) -> str:
85+
"""
86+
Create a new Asana task with the list of task links in its description.
87+
"""
88+
# Format the description with all task links
89+
description = "<body>"
90+
description += "<h2>Included Tasks</h2>"
91+
for link in task_links:
92+
if link.url:
93+
task_link = f"<a data-asana-gid=\"{extract_task_id_from_url(link.url)}\"/>"
94+
else:
95+
task_link = "no task"
96+
commit_url = f"https://github.com/duckduckgo/Android/commit/{link.commit_hash}"
97+
description += f"- {task_link} - <a href=\"{commit_url}\">{link.commit_hash[:9]}</a>\n"
98+
99+
description += "</body>"
100+
101+
tasks_api = asana.TasksApi(client)
102+
section_api = asana.SectionsApi(client)
103+
104+
# Create the task
105+
task = tasks_api.create_task(
106+
{
107+
"data": {
108+
"name": f"Android Nightly Release {release_tag}",
109+
"html_notes": description,
110+
"workspace": workspace_id
111+
}
112+
},
113+
{}
114+
)
115+
116+
# Add the task to the project and optionally to a section
117+
section_api.add_task_for_section(
118+
section_id,
119+
{
120+
"body": {
121+
"data": {
122+
"task": task['gid'],
123+
}
124+
}
125+
}
126+
)
127+
128+
return task['gid']
129+
130+
def get_latest_nightly_tag_before_commit(repo_path: str, current_tag: str) -> str | None:
131+
"""
132+
Return the previous *nightly* tag before `current_tag`, sorted by creation date (not version number).
133+
"""
134+
nightly_pattern = re.compile(r'^\d+\.\d+\.\d+(?:\.\d+)?-nightly$')
135+
136+
try:
137+
result = subprocess.run(
138+
["git", "-C", repo_path, "tag", "--sort=creatordate"],
139+
capture_output=True,
140+
text=True,
141+
check=True
142+
)
143+
tags = result.stdout.strip().splitlines()
144+
145+
# Filter to only nightly tags
146+
nightly_tags = [tag for tag in tags if nightly_pattern.match(tag)]
147+
148+
if current_tag not in nightly_tags:
149+
return None
150+
151+
idx = nightly_tags.index(current_tag)
152+
return nightly_tags[idx - 1] if idx > 0 else None
153+
except subprocess.CalledProcessError:
154+
return None
155+
156+
def main():
157+
parser = argparse.ArgumentParser(description='Create an Asana task with links to tasks from git commits')
158+
parser.add_argument('--tag', required=True, help='Tag to use as end commit') # Example: v0.44.0
159+
parser.add_argument('--android-repo-path', default='.', help='Path to Android git repository (default: current directory)')
160+
parser.add_argument('--trigger-phrase', required=True, help='Prefix for Asana task URLs in commit messages')
161+
parser.add_argument('--asana-project-id', required=True, help='Asana project ID')
162+
parser.add_argument('--asana-section-id', required=True, help='Asana section ID to place the task in')
163+
parser.add_argument('--asana-workspace-id', required=True, help='Asana workspace ID')
164+
parser.add_argument('--asana-api-key-env-var', required=True, help='Environment variable name containing the API key')
165+
166+
args = parser.parse_args()
167+
168+
try:
169+
# Get environment variables
170+
asana_api_key = os.getenv(args.asana_api_key_env_var)
171+
172+
# Validate environment variables
173+
if not asana_api_key:
174+
log("Error: Missing required environment variable")
175+
log(f"Please set {args.asana_api_key_env_var}")
176+
return 1
177+
178+
configuration = asana.Configuration()
179+
configuration.access_token = asana_api_key
180+
client = asana.ApiClient(configuration)
181+
182+
# Get the start tag (latest tag before the specified tag)
183+
# start_tag = get_latest_tag_before_commit(args.android_repo_path, args.tag)
184+
start_tag = get_latest_nightly_tag_before_commit(args.android_repo_path, args.tag)
185+
if not start_tag:
186+
log(f"Error: No previous version tag found before {args.tag}")
187+
return 1
188+
189+
log(f"Using tag {start_tag} as start commit")
190+
log(f"Using tag {args.tag} as end commit")
191+
192+
# Get commits between the tags
193+
commits = get_commits_between(args.android_repo_path, start_tag, args.tag)
194+
195+
log(f"Extracting task links from {len(commits)} commits")
196+
# Extract Asana task links from commit messages
197+
task_links = extract_asana_task_links(commits, args.trigger_phrase)
198+
199+
# Create the Asana task with the tag name
200+
task_id = create_asana_task(client, args.asana_workspace_id, args.tag, task_links, args.asana_section_id)
201+
task_url = f"https://app.asana.com/1/{args.asana_workspace_id}/project/{args.asana_project_id}/task/{task_id}"
202+
print(task_url) # Only the URL is ever printed to stdout
203+
204+
return 0
205+
206+
except Exception as e:
207+
import traceback
208+
log(f"Unexpected error: {e}")
209+
log(f"Stack trace:\n{traceback.format_exc()}")
210+
return 1
211+
212+
if __name__ == "__main__":
213+
exit(main())

scripts/release/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
GitPython>=3.1.40
2+
asana>=3.2.2

0 commit comments

Comments
 (0)