You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@arrow.apache.org by ra...@apache.org on 2023/01/18 08:16:41 UTC
[arrow] branch master updated: GH-14997: [Release] Ensure archery release tasks works with both new style GitHub issues and old style JIRA issues (#33615)
This is an automated email from the ASF dual-hosted git repository.
raulcd pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/master by this push:
new 4e439f6a59 GH-14997: [Release] Ensure archery release tasks works with both new style GitHub issues and old style JIRA issues (#33615)
4e439f6a59 is described below
commit 4e439f6a597180c5fc8ff1552c860cecd33736c5
Author: Raúl Cumplido <ra...@gmail.com>
AuthorDate: Wed Jan 18 09:16:31 2023 +0100
GH-14997: [Release] Ensure archery release tasks works with both new style GitHub issues and old style JIRA issues (#33615)
I've decided to do all the archery release tasks on a single PR:
* Closes: #14997
* Closes: #14999
* Closes: #15002
Authored-by: Raúl Cumplido <ra...@gmail.com>
Signed-off-by: Raúl Cumplido <ra...@gmail.com>
---
dev/archery/archery/release/cli.py | 41 ++--
dev/archery/archery/release/core.py | 258 ++++++++++++++-------
dev/archery/archery/release/reports.py | 7 +-
dev/archery/archery/release/tests/test_release.py | 91 +++++---
.../archery/templates/release_changelog.md.j2 | 4 +
.../archery/templates/release_curation.txt.j2 | 20 +-
dev/archery/setup.py | 2 +-
7 files changed, 278 insertions(+), 145 deletions(-)
diff --git a/dev/archery/archery/release/cli.py b/dev/archery/archery/release/cli.py
index 4fbf93861e..ed15dcb1ed 100644
--- a/dev/archery/archery/release/cli.py
+++ b/dev/archery/archery/release/cli.py
@@ -20,34 +20,33 @@ import pathlib
import click
from ..utils.cli import validate_arrow_sources
-from .core import Jira, CachedJira, Release
+from .core import IssueTracker, Release
@click.group('release')
@click.option("--src", metavar="<arrow_src>", default=None,
callback=validate_arrow_sources,
help="Specify Arrow source directory.")
-@click.option("--jira-cache", type=click.Path(), default=None,
- help="File path to cache queried JIRA issues per version.")
+@click.option('--github-token', '-t', default=None,
+ envvar="CROSSBOW_GITHUB_TOKEN",
+ help='OAuth token for GitHub authentication')
@click.pass_obj
-def release(obj, src, jira_cache):
+def release(obj, src, github_token):
"""Release releated commands."""
- jira = Jira()
- if jira_cache is not None:
- jira = CachedJira(jira_cache, jira=jira)
- obj['jira'] = jira
+ obj['issue_tracker'] = IssueTracker(github_token=github_token)
obj['repo'] = src.path
-@release.command('curate', help="Lists release related Jira issues.")
+@release.command('curate', help="Lists release related issues.")
@click.argument('version')
@click.option('--minimal/--full', '-m/-f',
- help="Only show actionable Jira issues.", default=False)
+ help="Only show actionable issues.", default=False)
@click.pass_obj
def release_curate(obj, version, minimal):
"""Release curation."""
- release = Release.from_jira(version, jira=obj['jira'], repo=obj['repo'])
+ release = Release(version, repo=obj['repo'],
+ issue_tracker=obj['issue_tracker'])
curation = release.curate(minimal)
click.echo(curation.render('console'))
@@ -64,10 +63,10 @@ def release_changelog():
@click.pass_obj
def release_changelog_add(obj, version):
"""Prepend the changelog with the current release"""
- jira, repo = obj['jira'], obj['repo']
+ repo, issue_tracker = obj['repo'], obj['issue_tracker']
# just handle the current version
- release = Release.from_jira(version, jira=jira, repo=repo)
+ release = Release(version, repo=repo, issue_tracker=issue_tracker)
if release.is_released:
raise ValueError('This version has been already released!')
@@ -87,10 +86,10 @@ def release_changelog_add(obj, version):
@click.pass_obj
def release_changelog_generate(obj, version, output):
"""Generate the changelog of a specific release."""
- jira, repo = obj['jira'], obj['repo']
+ repo, issue_tracker = obj['repo'], obj['issue_tracker']
# just handle the current version
- release = Release.from_jira(version, jira=jira, repo=repo)
+ release = Release(version, repo=repo, issue_tracker=issue_tracker)
changelog = release.changelog()
output.write(changelog.render('markdown'))
@@ -100,13 +99,15 @@ def release_changelog_generate(obj, version, output):
@click.pass_obj
def release_changelog_regenerate(obj):
"""Regeneretate the whole CHANGELOG.md file"""
- jira, repo = obj['jira'], obj['repo']
+ issue_tracker, repo = obj['issue_tracker'], obj['repo']
changelogs = []
+ issue_tracker = IssueTracker(issue_tracker=issue_tracker)
- for version in jira.project_versions('ARROW'):
+ for version in issue_tracker.project_versions():
if not version.released:
continue
- release = Release.from_jira(version, jira=jira, repo=repo)
+ release = Release(version, repo=repo,
+ issue_tracker=issue_tracker)
click.echo('Querying changelog for version: {}'.format(version))
changelogs.append(release.changelog())
@@ -129,7 +130,9 @@ def release_cherry_pick(obj, version, dry_run, recreate):
"""
Cherry pick commits.
"""
- release = Release.from_jira(version, jira=obj['jira'], repo=obj['repo'])
+ issue_tracker = obj['issue_tracker']
+ release = Release(version,
+ repo=obj['repo'], issue_tracker=issue_tracker)
if not dry_run:
release.cherry_pick_commits(recreate_branch=recreate)
diff --git a/dev/archery/archery/release/core.py b/dev/archery/archery/release/core.py
index 03eceb80a1..822d408f88 100644
--- a/dev/archery/archery/release/core.py
+++ b/dev/archery/archery/release/core.py
@@ -21,16 +21,16 @@ import functools
import os
import pathlib
import re
-import shelve
import warnings
from git import Repo
+from github import Github
from jira import JIRA
from semver import VersionInfo as SemVer
from ..utils.source import ArrowSources
from ..utils.logger import logger
-from .reports import ReleaseCuration, JiraChangelog
+from .reports import ReleaseCuration, ReleaseChangelog
def cached_property(fn):
@@ -58,13 +58,29 @@ class Version(SemVer):
release_date=getattr(jira_version, 'releaseDate', None)
)
+ @classmethod
+ def from_milestone(cls, milestone):
+ return cls.parse(
+ milestone.title,
+ released=milestone.state == "closed",
+ release_date=milestone.due_on
+ )
+
+
+ORIGINAL_ARROW_REGEX = re.compile(
+ r"\*This issue was originally created as " +
+ r"\[(?P<issue>ARROW\-(?P<issue_id>(\d+)))\]"
+)
+
class Issue:
- def __init__(self, key, type, summary):
+ def __init__(self, key, type, summary, github_issue=None):
self.key = key
self.type = type
self.summary = summary
+ self.github_issue_id = getattr(github_issue, "number", None)
+ self._github_issue = github_issue
@classmethod
def from_jira(cls, jira_issue):
@@ -74,13 +90,49 @@ class Issue:
summary=jira_issue.fields.summary
)
+ @classmethod
+ def from_github(cls, github_issue):
+ original_jira = cls.original_jira_id(github_issue)
+ key = original_jira or github_issue.number
+ return cls(
+ key=key,
+ type=next(
+ iter(
+ [
+ label.name for label in github_issue.labels
+ if label.name.startswith("Type:")
+ ]
+ ), None),
+ summary=github_issue.title,
+ github_issue=github_issue
+ )
+
@property
def project(self):
+ if isinstance(self.key, int):
+ return 'GH'
return self.key.split('-')[0]
@property
def number(self):
- return int(self.key.split('-')[1])
+ if isinstance(self.key, str):
+ return int(self.key.split('-')[1])
+ else:
+ return self.key
+
+ @cached_property
+ def is_pr(self):
+ return bool(self._github_issue and self._github_issue.pull_request)
+
+ @classmethod
+ def original_jira_id(cls, github_issue):
+ # All migrated issues contain body
+ if not github_issue.body:
+ return None
+ matches = ORIGINAL_ARROW_REGEX.search(github_issue.body)
+ if matches:
+ values = matches.groupdict()
+ return values['issue']
class Jira(JIRA):
@@ -88,54 +140,54 @@ class Jira(JIRA):
def __init__(self, url='https://issues.apache.org/jira'):
super().__init__(url)
- def project_version(self, version_string, project='ARROW'):
- # query version from jira to populated with additional metadata
- versions = {str(v): v for v in self.project_versions(project)}
- return versions[version_string]
+ def issue(self, key):
+ return Issue.from_jira(super().issue(key))
+
+
+class IssueTracker:
+
+ def __init__(self, github_token=None):
+ github = Github(github_token)
+ self.github_repo = github.get_repo('apache/arrow')
- def project_versions(self, project):
+ def project_version(self, version_string):
+ for milestone in self.project_versions():
+ if milestone == version_string:
+ return milestone
+
+ def project_versions(self):
versions = []
- for v in super().project_versions(project):
+ milestones = self.github_repo.get_milestones(state="all")
+ for milestone in milestones:
try:
- versions.append(Version.from_jira(v))
+ versions.append(Version.from_milestone(milestone))
except ValueError:
# ignore invalid semantic versions like JS-0.4.0
continue
return sorted(versions, reverse=True)
- def issue(self, key):
- return Issue.from_jira(super().issue(key))
-
- def project_issues(self, version, project='ARROW'):
- query = "project={} AND fixVersion={}".format(project, version)
- issues = super().search_issues(query, maxResults=False)
- return list(map(Issue.from_jira, issues))
-
-
-class CachedJira:
-
- def __init__(self, cache_path, jira=None):
- self.jira = jira or Jira()
- self.cache_path = cache_path
+ def _milestone_from_semver(self, semver):
+ milestones = self.github_repo.get_milestones(state="all")
+ for milestone in milestones:
+ try:
+ if milestone.title == semver:
+ return milestone
+ except ValueError:
+ # ignore invalid semantic versions like JS-0.3.0
+ continue
- def __getattr__(self, name):
- attr = getattr(self.jira, name)
- return self._cached(name, attr) if callable(attr) else attr
+ def project_issues(self, version):
+ issues = self.github_repo.get_issues(
+ milestone=self._milestone_from_semver(version),
+ state="all")
+ return list(map(Issue.from_github, issues))
- def _cached(self, name, method):
- def wrapper(*args, **kwargs):
- key = str((name, args, kwargs))
- with shelve.open(self.cache_path) as cache:
- try:
- result = cache[key]
- except KeyError:
- cache[key] = result = method(*args, **kwargs)
- return result
- return wrapper
+ def issue(self, key):
+ return Issue.from_github(self.github_repo.get_issue(key))
_TITLE_REGEX = re.compile(
- r"(?P<issue>(?P<project>(ARROW|PARQUET))\-\d+)?\s*:?\s*"
+ r"(?P<issue>(?P<project>(ARROW|PARQUET|GH))\-(?P<issue_id>(\d+)))?\s*:?\s*"
r"(?P<minor>(MINOR))?\s*:?\s*"
r"(?P<components>\[.*\])?\s*(?P<summary>.*)"
)
@@ -145,9 +197,10 @@ _COMPONENT_REGEX = re.compile(r"\[([^\[\]]+)\]")
class CommitTitle:
def __init__(self, summary, project=None, issue=None, minor=None,
- components=None):
+ components=None, issue_id=None):
self.project = project
self.issue = issue
+ self.issue_id = issue_id
self.components = components or []
self.summary = summary
self.minor = bool(minor)
@@ -186,6 +239,7 @@ class CommitTitle:
values['summary'],
project=values.get('project'),
issue=values.get('issue'),
+ issue_id=values.get('issue_id'),
minor=values.get('minor'),
components=components
)
@@ -230,7 +284,8 @@ class Commit:
class Release:
- def __new__(self, version, jira=None, repo=None):
+ def __new__(self, version, repo=None, github_token=None,
+ issue_tracker=None):
if isinstance(version, str):
version = Version.parse(version)
elif not isinstance(version, Version):
@@ -250,15 +305,7 @@ class Release:
return super().__new__(klass)
- def __init__(self, version, jira, repo):
- if jira is None:
- jira = Jira()
- elif isinstance(jira, str):
- jira = Jira(jira)
- elif not isinstance(jira, (Jira, CachedJira)):
- raise TypeError("`jira` argument must be a server url or a valid "
- "Jira instance")
-
+ def __init__(self, version, repo, issue_tracker):
if repo is None:
arrow = ArrowSources.find()
repo = Repo(arrow.path)
@@ -269,13 +316,14 @@ class Release:
"instance")
if isinstance(version, str):
- version = jira.project_version(version, project='ARROW')
+ version = issue_tracker.project_version(version)
+
elif not isinstance(version, Version):
raise TypeError(version)
self.version = version
- self.jira = jira
self.repo = repo
+ self.issue_tracker = issue_tracker
def __repr__(self):
if self.version.released:
@@ -284,10 +332,6 @@ class Release:
status = "pending"
return f"<{self.__class__.__name__} {self.version!r} {status}>"
- @staticmethod
- def from_jira(version, jira=None, repo=None):
- return Release(version, jira, repo)
-
@property
def is_released(self):
return self.version.released
@@ -322,7 +366,8 @@ class Release:
# first release doesn't have a previous one
return None
else:
- return Release.from_jira(previous, jira=self.jira, repo=self.repo)
+ return Release(previous, repo=self.repo,
+ issue_tracker=self.issue_tracker)
@cached_property
def next(self):
@@ -332,13 +377,21 @@ class Release:
raise ValueError("There is no upcoming release set in JIRA after "
f"version {self.version}")
upcoming = self.siblings[position - 1]
- return Release.from_jira(upcoming, jira=self.jira, repo=self.repo)
+ return Release(upcoming, repo=self.repo,
+ issue_tracker=self.issue_tracker)
@cached_property
def issues(self):
- issues = self.jira.project_issues(self.version, project='ARROW')
+ issues = self.issue_tracker.project_issues(
+ self.version
+ )
return {i.key: i for i in issues}
+ @cached_property
+ def github_issue_ids(self):
+ return {v.github_issue_id for v in self.issues.values()
+ if v.github_issue_id}
+
@cached_property
def commits(self):
"""
@@ -351,7 +404,11 @@ class Release:
lower = self.repo.tags[self.previous.tag]
if self.version.released:
- upper = self.repo.tags[self.tag]
+ try:
+ upper = self.repo.tags[self.tag]
+ except IndexError:
+ warnings.warn(f"Release tag `{self.tag}` doesn't exist.")
+ return []
else:
try:
upper = self.repo.branches[self.branch]
@@ -362,6 +419,10 @@ class Release:
commit_range = f"{lower}..{upper}"
return list(map(Commit, self.repo.iter_commits(commit_range)))
+ @cached_property
+ def jira_instance(self):
+ return Jira()
+
@cached_property
def default_branch(self):
default_branch_name = os.getenv("ARCHERY_DEFAULT_BRANCH")
@@ -388,7 +449,7 @@ class Release:
# The last token is the default branch name
default_branch_name = origin_head_name_tokenized[-1]
- except KeyError:
+ except (KeyError, IndexError):
# Use a hard-coded default value to set default_branch_name
# TODO: ARROW-18011 to track changing the hard coded default
# value from "master" to "main".
@@ -403,29 +464,43 @@ class Release:
return default_branch_name
def curate(self, minimal=False):
- # handle commits with parquet issue key specially and query them from
- # jira and add it to the issues
+ # handle commits with parquet issue key specially
release_issues = self.issues
-
- within, outside, nojira, parquet = [], [], [], []
+ within, outside, noissue, parquet, minor = [], [], [], [], []
for c in self.commits:
if c.issue is None:
- nojira.append(c)
- elif c.issue in release_issues:
- within.append((release_issues[c.issue], c))
+ if c.title.minor:
+ minor.append(c)
+ else:
+ noissue.append(c)
+ elif c.project == 'GH':
+ if int(c.issue_id) in release_issues:
+ within.append((release_issues[int(c.issue_id)], c))
+ else:
+ outside.append(
+ (self.issue_tracker.issue(int(c.issue_id)), c))
+ elif c.project == 'ARROW':
+ if c.issue in release_issues:
+ within.append((release_issues[c.issue], c))
+ else:
+ outside.append((self.jira_instance.issue(c.issue), c))
elif c.project == 'PARQUET':
- parquet.append((self.jira.issue(c.issue), c))
+ parquet.append((self.jira_instance.issue(c.issue), c))
else:
- outside.append((self.jira.issue(c.issue), c))
+ warnings.warn(
+ f'Issue {c.issue} is not MINOR nor pertains to GH' +
+ ', ARROW or PARQUET')
+ outside.append((c.issue, c))
# remaining jira tickets
within_keys = {i.key for i, c in within}
+ # Take into account that some issues milestoned are prs
nopatch = [issue for key, issue in release_issues.items()
- if key not in within_keys]
+ if key not in within_keys and issue.is_pr is False]
return ReleaseCuration(release=self, within=within, outside=outside,
- nojira=nojira, parquet=parquet, nopatch=nopatch,
- minimal=minimal)
+ noissue=noissue, parquet=parquet,
+ nopatch=nopatch, minimal=minimal, minor=minor)
def changelog(self):
issue_commit_pairs = []
@@ -451,16 +526,26 @@ class Release:
'Task': 'New Features and Improvements',
'Test': 'Bug Fixes',
'Wish': 'New Features and Improvements',
+ 'Type: bug': 'Bug Fixes',
+ 'Type: enhancement': 'New Features and Improvements',
+ 'Type: task': 'New Features and Improvements',
+ 'Type: test': 'Bug Fixes',
+ 'Type: usage': 'New Features and Improvements',
}
categories = defaultdict(list)
for issue, commit in issue_commit_pairs:
- categories[issue_types[issue.type]].append((issue, commit))
+ try:
+ categories[issue_types[issue.type]].append((issue, commit))
+ except KeyError:
+ # If issue or pr don't have a type assume task.
+ # Currently the label for type is not mandatory on GitHub.
+ categories[issue_types['Type: task']].append((issue, commit))
# sort issues by the issue key in ascending order
for issues in categories.values():
issues.sort(key=lambda pair: (pair[0].project, pair[0].number))
- return JiraChangelog(release=self, categories=categories)
+ return ReleaseChangelog(release=self, categories=categories)
def commits_to_pick(self, exclude_already_applied=True):
# collect commits applied on the default branch since the root of the
@@ -481,10 +566,18 @@ class Release:
# iterate over the commits applied on the main branch and filter out
# the ones that are included in the jira release
- patches_to_pick = [c for c in commits if
- c.issue in self.issues and
- c.title not in already_applied]
-
+ patches_to_pick = []
+ for c in commits:
+ key = c.issue
+ # For the release we assume all issues that have to be
+ # cherry-picked are merged with the GH issue id instead of the
+ # JIRA ARROW one. That's why we use github_issues along with
+ # issues. This is only to correct the mapping for migrated issues.
+ if c.issue and c.issue.startswith("GH-"):
+ key = int(c.issue_id)
+ if ((key in self.github_issue_ids or key in self.issues) and
+ c.title not in already_applied):
+ patches_to_pick.append(c)
return reversed(patches_to_pick)
def cherry_pick_commits(self, recreate_branch=True):
@@ -525,7 +618,7 @@ class MajorRelease(Release):
Filter only the major releases.
"""
# handle minor releases before 1.0 as major releases
- return [v for v in self.jira.project_versions('ARROW')
+ return [v for v in self.issue_tracker.project_versions()
if v.patch == 0 and (v.major == 0 or v.minor == 0)]
@@ -544,7 +637,8 @@ class MinorRelease(Release):
"""
Filter the major and minor releases.
"""
- return [v for v in self.jira.project_versions('ARROW') if v.patch == 0]
+ return [v for v in self.issue_tracker.project_versions()
+ if v.patch == 0]
class PatchRelease(Release):
@@ -562,4 +656,4 @@ class PatchRelease(Release):
"""
No filtering, consider all releases.
"""
- return self.jira.project_versions('ARROW')
+ return self.issue_tracker.project_versions()
diff --git a/dev/archery/archery/release/reports.py b/dev/archery/archery/release/reports.py
index 43093487c0..4299eaa7ed 100644
--- a/dev/archery/archery/release/reports.py
+++ b/dev/archery/archery/release/reports.py
@@ -27,14 +27,15 @@ class ReleaseCuration(JinjaReport):
'release',
'within',
'outside',
- 'nojira',
+ 'noissue',
'parquet',
'nopatch',
- 'minimal'
+ 'minimal',
+ 'minor'
]
-class JiraChangelog(JinjaReport):
+class ReleaseChangelog(JinjaReport):
templates = {
'markdown': 'release_changelog.md.j2',
'html': 'release_changelog.html.j2'
diff --git a/dev/archery/archery/release/tests/test_release.py b/dev/archery/archery/release/tests/test_release.py
index 1283b4bcb4..22b43c7cb3 100644
--- a/dev/archery/archery/release/tests/test_release.py
+++ b/dev/archery/archery/release/tests/test_release.py
@@ -19,13 +19,29 @@ import pytest
from archery.release.core import (
Release, MajorRelease, MinorRelease, PatchRelease,
- Jira, Version, Issue, CommitTitle, Commit
+ IssueTracker, Version, Issue, CommitTitle, Commit
)
from archery.testing import DotDict
# subset of issues per revision
_issues = {
+ "3.0.0": [
+ Issue("GH-9784", type="Bug", summary="[C++] Title"),
+ Issue("GH-9767", type="New Feature", summary="[Crossbow] Title"),
+ Issue("GH-1231", type="Bug", summary="[Java] Title"),
+ Issue("GH-1244", type="Bug", summary="[C++] Title"),
+ Issue("GH-1301", type="Bug", summary="[Python][Archery] Title")
+ ],
+ "2.0.0": [
+ Issue("ARROW-9784", type="Bug", summary="[Java] Title"),
+ Issue("ARROW-9767", type="New Feature", summary="[Crossbow] Title"),
+ Issue("GH-1230", type="Bug", summary="[Dev] Title"),
+ Issue("ARROW-9694", type="Bug", summary="[Release] Title"),
+ Issue("ARROW-5643", type="Bug", summary="[Go] Title"),
+ Issue("GH-1243", type="Bug", summary="[Python] Title"),
+ Issue("GH-1300", type="Bug", summary="[CI][Archery] Title")
+ ],
"1.0.1": [
Issue("ARROW-9684", type="Bug", summary="[C++] Title"),
Issue("ARROW-9667", type="New Feature", summary="[Crossbow] Title"),
@@ -62,13 +78,14 @@ _issues = {
}
-class FakeJira(Jira):
+class FakeIssueTracker(IssueTracker):
def __init__(self):
pass
- def project_versions(self, project='ARROW'):
+ def project_versions(self):
return [
+ Version.parse("4.0.0", released=False),
Version.parse("3.0.0", released=False),
Version.parse("2.0.0", released=False),
Version.parse("1.1.0", released=False),
@@ -82,16 +99,16 @@ class FakeJira(Jira):
Version.parse("0.15.0", released=True),
]
- def project_issues(self, version, project='ARROW'):
+ def project_issues(self, version):
return _issues[str(version)]
@pytest.fixture
-def fake_jira():
- return FakeJira()
+def fake_issue_tracker():
+ return FakeIssueTracker()
-def test_version(fake_jira):
+def test_version(fake_issue_tracker):
v = Version.parse("1.2.5")
assert str(v) == "1.2.5"
assert v.major == 1
@@ -109,7 +126,7 @@ def test_version(fake_jira):
assert v.release_date == "2020-01-01"
-def test_issue(fake_jira):
+def test_issue(fake_issue_tracker):
i = Issue("ARROW-1234", type='Bug', summary="title")
assert i.key == "ARROW-1234"
assert i.type == "Bug"
@@ -212,78 +229,78 @@ def test_commit_title():
assert t.minor is False
-def test_release_basics(fake_jira):
- r = Release.from_jira("1.0.0", jira=fake_jira)
+def test_release_basics(fake_issue_tracker):
+ r = Release("1.0.0", repo=None, issue_tracker=fake_issue_tracker)
assert isinstance(r, MajorRelease)
assert r.is_released is True
assert r.branch == 'maint-1.0.0'
assert r.tag == 'apache-arrow-1.0.0'
- r = Release.from_jira("1.1.0", jira=fake_jira)
+ r = Release("1.1.0", repo=None, issue_tracker=fake_issue_tracker)
assert isinstance(r, MinorRelease)
assert r.is_released is False
assert r.branch == 'maint-1.x.x'
assert r.tag == 'apache-arrow-1.1.0'
# minor releases before 1.0 are treated as major releases
- r = Release.from_jira("0.17.0", jira=fake_jira)
+ r = Release("0.17.0", repo=None, issue_tracker=fake_issue_tracker)
assert isinstance(r, MajorRelease)
assert r.is_released is True
assert r.branch == 'maint-0.17.0'
assert r.tag == 'apache-arrow-0.17.0'
- r = Release.from_jira("0.17.1", jira=fake_jira)
+ r = Release("0.17.1", repo=None, issue_tracker=fake_issue_tracker)
assert isinstance(r, PatchRelease)
assert r.is_released is True
assert r.branch == 'maint-0.17.x'
assert r.tag == 'apache-arrow-0.17.1'
-def test_previous_and_next_release(fake_jira):
- r = Release.from_jira("3.0.0", jira=fake_jira)
+def test_previous_and_next_release(fake_issue_tracker):
+ r = Release("4.0.0", repo=None, issue_tracker=fake_issue_tracker)
assert isinstance(r.previous, MajorRelease)
- assert r.previous.version == Version.parse("2.0.0")
+ assert r.previous.version == Version.parse("3.0.0")
with pytest.raises(ValueError, match="There is no upcoming release set"):
assert r.next
- r = Release.from_jira("2.0.0", jira=fake_jira)
+ r = Release("3.0.0", repo=None, issue_tracker=fake_issue_tracker)
assert isinstance(r.previous, MajorRelease)
assert isinstance(r.next, MajorRelease)
- assert r.previous.version == Version.parse("1.0.0")
- assert r.next.version == Version.parse("3.0.0")
+ assert r.previous.version == Version.parse("2.0.0")
+ assert r.next.version == Version.parse("4.0.0")
- r = Release.from_jira("1.1.0", jira=fake_jira)
+ r = Release("1.1.0", repo=None, issue_tracker=fake_issue_tracker)
assert isinstance(r.previous, MajorRelease)
assert isinstance(r.next, MajorRelease)
assert r.previous.version == Version.parse("1.0.0")
assert r.next.version == Version.parse("2.0.0")
- r = Release.from_jira("1.0.0", jira=fake_jira)
+ r = Release("1.0.0", repo=None, issue_tracker=fake_issue_tracker)
assert isinstance(r.next, MajorRelease)
assert isinstance(r.previous, MajorRelease)
assert r.previous.version == Version.parse("0.17.0")
assert r.next.version == Version.parse("2.0.0")
- r = Release.from_jira("0.17.0", jira=fake_jira)
+ r = Release("0.17.0", repo=None, issue_tracker=fake_issue_tracker)
assert isinstance(r.previous, MajorRelease)
assert r.previous.version == Version.parse("0.16.0")
- r = Release.from_jira("0.15.2", jira=fake_jira)
+ r = Release("0.15.2", repo=None, issue_tracker=fake_issue_tracker)
assert isinstance(r.previous, PatchRelease)
assert isinstance(r.next, MajorRelease)
assert r.previous.version == Version.parse("0.15.1")
assert r.next.version == Version.parse("0.16.0")
- r = Release.from_jira("0.15.1", jira=fake_jira)
+ r = Release("0.15.1", repo=None, issue_tracker=fake_issue_tracker)
assert isinstance(r.previous, MajorRelease)
assert isinstance(r.next, PatchRelease)
assert r.previous.version == Version.parse("0.15.0")
assert r.next.version == Version.parse("0.15.2")
-def test_release_issues(fake_jira):
+def test_release_issues(fake_issue_tracker):
# major release issues
- r = Release.from_jira("1.0.0", jira=fake_jira)
+ r = Release("1.0.0", repo=None, issue_tracker=fake_issue_tracker)
assert r.issues.keys() == set([
"ARROW-300",
"ARROW-4427",
@@ -295,7 +312,7 @@ def test_release_issues(fake_jira):
"ARROW-8973"
])
# minor release issues
- r = Release.from_jira("0.17.0", jira=fake_jira)
+ r = Release("0.17.0", repo=None, issue_tracker=fake_issue_tracker)
assert r.issues.keys() == set([
"ARROW-2882",
"ARROW-2587",
@@ -305,7 +322,7 @@ def test_release_issues(fake_jira):
"ARROW-1636",
])
# patch release issues
- r = Release.from_jira("1.0.1", jira=fake_jira)
+ r = Release("1.0.1", repo=None, issue_tracker=fake_issue_tracker)
assert r.issues.keys() == set([
"ARROW-9684",
"ARROW-9667",
@@ -315,6 +332,16 @@ def test_release_issues(fake_jira):
"ARROW-9609",
"ARROW-9606"
])
+ r = Release("2.0.0", repo=None, issue_tracker=fake_issue_tracker)
+ assert r.issues.keys() == set([
+ "ARROW-9784",
+ "ARROW-9767",
+ "GH-1230",
+ "ARROW-9694",
+ "ARROW-5643",
+ "GH-1243",
+ "GH-1300"
+ ])
@pytest.mark.parametrize(('version', 'ncommits'), [
@@ -323,8 +350,8 @@ def test_release_issues(fake_jira):
("0.17.0", 569),
("0.15.1", 41)
])
-def test_release_commits(fake_jira, version, ncommits):
- r = Release.from_jira(version, jira=fake_jira)
+def test_release_commits(fake_issue_tracker, version, ncommits):
+ r = Release(version, repo=None, issue_tracker=fake_issue_tracker)
assert len(r.commits) == ncommits
for c in r.commits:
assert isinstance(c, Commit)
@@ -332,8 +359,8 @@ def test_release_commits(fake_jira, version, ncommits):
assert c.url.endswith(c.hexsha)
-def test_maintenance_patch_selection(fake_jira):
- r = Release.from_jira("0.17.1", jira=fake_jira)
+def test_maintenance_patch_selection(fake_issue_tracker):
+ r = Release("0.17.1", repo=None, issue_tracker=fake_issue_tracker)
shas_to_pick = [
c.hexsha for c in r.commits_to_pick(exclude_already_applied=False)
diff --git a/dev/archery/archery/templates/release_changelog.md.j2 b/dev/archery/archery/templates/release_changelog.md.j2
index 0c9efbc42f..0eedb217a8 100644
--- a/dev/archery/archery/templates/release_changelog.md.j2
+++ b/dev/archery/archery/templates/release_changelog.md.j2
@@ -23,7 +23,11 @@
## {{ category }}
{% for issue, commit in issue_commit_pairs -%}
+{% if issue.project in ('ARROW', 'PARQUET') -%}
* [{{ issue.key }}](https://issues.apache.org/jira/browse/{{ issue.key }}) - {{ commit.title.to_string(with_issue=False) if commit else issue.summary | md }}
+{% else -%}
+* [GH-{{ issue.key }}](https://github.com/apache/arrow/issues/{{ issue.key }}) - {{ commit.title.to_string(with_issue=False) if commit else issue.summary | md }}
+{% endif -%}
{% endfor %}
{% endfor %}
diff --git a/dev/archery/archery/templates/release_curation.txt.j2 b/dev/archery/archery/templates/release_curation.txt.j2
index 4f524d001c..0796f45162 100644
--- a/dev/archery/archery/templates/release_curation.txt.j2
+++ b/dev/archery/archery/templates/release_curation.txt.j2
@@ -17,26 +17,30 @@
# under the License.
#}
{%- if not minimal -%}
-Total number of JIRA tickets assigned to version {{ release.version }}: {{ release.issues|length }}
+### Total number of GitHub tickets assigned to version {{ release.version }}: {{ release.issues|length }}
-Total number of applied patches since version {{ release.previous.version }}: {{ release.commits|length }}
+### Total number of applied patches since version {{ release.previous.version }}: {{ release.commits|length }}
-Patches with assigned issue in version {{ release.version }}:
+### Patches with assigned issue in version {{ release.version }}: {{ within|length }}
{% for issue, commit in within -%}
- {{ commit.url }} {{ commit.title }}
{% endfor %}
{% endif -%}
-Patches with assigned issue outside of version {{ release.version }}:
+### Patches with assigned issue outside of version {{ release.version }}: {{ outside|length }}
{% for issue, commit in outside -%}
- {{ commit.url }} {{ commit.title }}
{% endfor %}
{% if not minimal -%}
-Patches in version {{ release.version }} without a linked issue:
-{% for commit in nojira -%}
+### Minor patches in version {{ release.version }}: {{ minor|length }}
+{% for commit in minor -%}
- {{ commit.url }} {{ commit.title }}
{% endfor %}
-JIRA issues in version {{ release.version }} without a linked patch:
+### Patches in version {{ release.version }} without a linked issue:
+{% for commit in noissue -%}
+ - {{ commit.url }} {{ commit.title }}
+{% endfor %}
+### JIRA issues in version {{ release.version }} without a linked patch: {{ nopatch|length }}
{% for issue in nopatch -%}
- - https://issues.apache.org/jira/browse/{{ issue.key }}
+ - https://github.com/apache/arrow/issues/{{ issue.key }}
{% endfor %}
{%- endif -%}
\ No newline at end of file
diff --git a/dev/archery/setup.py b/dev/archery/setup.py
index 4b13608cf8..51f066c9ed 100755
--- a/dev/archery/setup.py
+++ b/dev/archery/setup.py
@@ -31,7 +31,7 @@ extras = {
'lint': ['numpydoc==1.1.0', 'autopep8', 'flake8', 'cmake_format==0.6.13'],
'benchmark': ['pandas'],
'docker': ['ruamel.yaml', 'python-dotenv'],
- 'release': [jinja_req, 'jira', 'semver', 'gitpython'],
+ 'release': ['pygithub', jinja_req, 'jira', 'semver', 'gitpython'],
'crossbow': ['github3.py', jinja_req, 'pygit2>=1.6.0', 'requests',
'ruamel.yaml', 'setuptools_scm'],
'crossbow-upload': ['github3.py', jinja_req, 'ruamel.yaml',