Skip to content

Commit

Permalink
Merge pull request #55 from useshortcut/daniel/sc-264420/import-revie…
Browse files Browse the repository at this point in the history
…wers-add-comment

[pivotal] Import story review information
  • Loading branch information
semperos authored Apr 10, 2024
2 parents 1cf85e4 + 443c4aa commit a07600e
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 29 deletions.
10 changes: 7 additions & 3 deletions pivotal-import/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ In order to run this, you will require a Pivotal account and the ability to sign
- Follow instructions printed to the console to ensure the mapping of Pivotal and Shortcut data is complete and correct.
- Refer to `data/priorities.csv`, `data/states.csv`, and `data/users.csv` to review these mappings.
1. If the dry-run output looks correct, you can apply the import to your Shortcut workspace by running `make import-apply`
1. Refer to `data/shortcut_imported_entities.csv` to review all epics, stories, etc. imported successfully into Shortcut.
1. If you find that you need to adjust your configuration or your Pivotal data and try again, you can run `make delete` to review a dry-run and `make delete-apply` to actually delete the imported Shortcut epics and stories listed in that CSV file.
- The console should print a link to an import-specific Shortcut label page that you can review to find all imported Stories and Epics.
- If you run the importer multiple times, you can review all imported Stories and Epics by visiting Settings > Labels and then searching for the `pivotal->shortcut` label and clicking on it.
1. If you find that you need to adjust your configuration or your Pivotal data and try again, you can run `make delete` to review a dry-run and `make delete-apply` to actually delete the imported Shortcut epics and stories listed in `data/shortcut_imported_entities.csv`. You can also archive or delete content in the Shortcut application if needed.

# Operation

Expand All @@ -31,7 +32,10 @@ If `pivotal_import.py` completes without errors, you can run the script with the

The following are known limitations:

- **No story reviewers:** Pivotal story reviewers are not imported.
- **Limited story reviews:** Shortcut does not have a feature equivalent to Pivotal story reviews, so they are imported as follows:
- Pivotal story reviewers are imported as Shortcut story followers on the stories they were assigned for review. Shortcut story followers receive updates in their Shortcut Activity Feed for all story updates.
- Imported stories that had Pivotal reviews have an additional comment with a table that lists all of the story reviews from Pivotal (reviewer, review type, and review status).
- Imported stories that had Pivotal reviews have a label in Shortcut of `pivotal-had-review`.
- **No story blockers:** Pivotal story blockers (the relationships between stories) are not imported.
- **No iterations:** Pivotal iterations are not imported.
- **Epics are imported as unstarted:** Imported epics are set to an unstarted "Todo" state.
Expand Down
50 changes: 44 additions & 6 deletions pivotal-import/pivotal_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"""The label associated with all chore stories created from release types in Pivotal."""
PIVOTAL_RELEASE_TYPE_LABEL = "pivotal-release"

"""The label indicating a story had reviews in Pivotal."""
PIVOTAL_HAD_REVIEW_LABEL = "pivotal-had-review"


def sc_creator(items):
"""Create Shortcut entities utilizing bulk APIs whenever possible.
Expand All @@ -45,7 +48,6 @@ def sc_creator(items):
"""
batch_stories = []
ids = []

def create_stories(stories):
entities = [s["entity"] for s in stories]
Expand Down Expand Up @@ -114,6 +116,8 @@ def parse_priority(priority):
"comment": ("comments", parse_comment),
"owned by": "owners",
"reviewer": "reviewers",
"review type": "review_types",
"review status": "review_states",
"task status": "task_states",
"task": "task_titles",
}
Expand Down Expand Up @@ -148,6 +152,17 @@ def parse_priority(priority):
],
}

review_as_comment_text_prefix = """\\[Pivotal Importer\\] Reviewers have been added as followers on this Shortcut Story.
The following table describes the state of their reviews when they were imported into Shortcut from Pivotal Tracker:
| Reviewer | Review Type | Review Status |
|---|---|---|"""


def escape_md_table_syntax(s):
return s.replace("|", "\\|")


def parse_row(row, headers):
d = dict()
Expand Down Expand Up @@ -204,10 +219,8 @@ def build_entity(ctx, d):
if author_id:
new_comment["author_id"] = author_id
comments.append(new_comment)
if comments:
d["comments"] = comments
elif "comments" in d:
del d["comments"]
# other things we process are reified as comments,
# so we'll add comments to the d later in processing

# releases become Shortcut Stories of type "chore"
if d["story_type"] == "release":
Expand Down Expand Up @@ -256,6 +269,23 @@ def build_entity(ctx, d):
for reviewer in reviewers
if reviewer in user_to_sc_id
]
d.setdefault("labels", []).append({"name": PIVOTAL_HAD_REVIEW_LABEL})

# format table of all reviewers, types, and statuses as a comment on the imported story
if reviewers:
comment_text = review_as_comment_text_prefix
for reviewer, review_type, review_status in zip(
d.get("reviewers", []),
d.get("review_types", []),
d.get("review_states", []),
):
reviewer = escape_md_table_syntax(reviewer)
review_type = escape_md_table_syntax(review_type)
review_status = escape_md_table_syntax(review_status)
comment_text += f"\n|{reviewer}|{review_type}|{review_status}|"
comments.append(
{"author_id": d.get("requested_by_id", None), "text": comment_text}
)

# Custom Fields
custom_fields = []
Expand All @@ -272,6 +302,14 @@ def build_entity(ctx, d):
if custom_fields:
d["custom_fields"] = custom_fields

# as a last step, ensure comments (both those that were comments
# in Pivotal, and those we add during import to fill feature gaps)
# are all added to the d dict
if comments:
d["comments"] = comments
elif "comments" in d:
del d["comments"]

elif type == "epic":
pass

Expand Down Expand Up @@ -324,7 +362,7 @@ def _get_next_id():
return id

def mock_emitter(items):
for ix, item in enumerate(items):
for item in items:
entity_id = _get_next_id()
created_entity = item["entity"].copy()
created_entity["id"] = entity_id
Expand Down
112 changes: 92 additions & 20 deletions pivotal-import/pivotal_import_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def create_test_ctx():
"Daniel McFadden": "daniel_member_id",
"Emmanuelle Charpentier": "emmanuelle_member_id",
"Giorgio Parisi": "giorgio_member_id",
"Piper | Barnes": "piper_member_id",
},
"workflow_config": {"unstarted": 400001, "started": 400002, "done": 400003},
}
Expand Down Expand Up @@ -68,14 +69,16 @@ def test_parse_reviewers():
"Amy Williams",
"Giorgio Parisi",
"Emmanuelle Charpentier",
"Piper | Barnes",
]
} == parse_row(
[
"Amy Williams",
"Giorgio Parisi",
"Emmanuelle Charpentier",
"Piper | Barnes",
],
["reviewer", "reviewer", "reviewer"],
["reviewer", "reviewer", "reviewer", "reviewer"],
)


Expand Down Expand Up @@ -113,6 +116,94 @@ def test_build_story_with_comments():
} == build_entity(ctx, d)


def test_build_story_with_reviews():
ctx = create_test_ctx()
rows = [
# just reviews, no Pivotal comments
{
"story_type": "feature",
"reviewers": ["Emmanuelle Charpentier", "Giorgio Parisi"],
"review_types": ["code", "security"],
"review_states": ["unstarted", "in_review"],
},
# both Pivotal comments and reviews, which we add as Shortcut comments
{
"story_type": "bug",
"requester": "Daniel McFadden",
"reviewers": ["Emmanuelle Charpentier", "Giorgio Parisi", "Piper | Barnes"],
"review_types": ["code", "security", "custom qa"],
"review_states": ["unstarted", "in_review", "passed"],
"comments": [
{"text": "Comment 1"},
{"text": "Comment 2"},
{"text": "Comment 3"},
],
},
]

assert (
[
{
"type": "story",
"entity": {
"story_type": "feature",
"comments": [
{
"author_id": None,
"text": review_as_comment_text_prefix
+ """
|Emmanuelle Charpentier|code|unstarted|
|Giorgio Parisi|security|in_review|""",
},
],
"follower_ids": [
"emmanuelle_member_id",
"giorgio_member_id",
],
"labels": [
{"name": PIVOTAL_TO_SHORTCUT_LABEL},
{"name": PIVOTAL_TO_SHORTCUT_RUN_LABEL},
{"name": PIVOTAL_HAD_REVIEW_LABEL},
],
},
"parsed_row": rows[0],
},
{
"type": "story",
"entity": {
"story_type": "bug",
"requested_by_id": "daniel_member_id",
"comments": [
{"text": "Comment 1"},
{"text": "Comment 2"},
{"text": "Comment 3"},
{
"author_id": "daniel_member_id",
"text": review_as_comment_text_prefix
+ """
|Emmanuelle Charpentier|code|unstarted|
|Giorgio Parisi|security|in_review|
|Piper \\| Barnes|custom qa|passed|""",
},
],
"follower_ids": [
"emmanuelle_member_id",
"giorgio_member_id",
"piper_member_id",
],
"labels": [
{"name": PIVOTAL_TO_SHORTCUT_LABEL},
{"name": PIVOTAL_TO_SHORTCUT_RUN_LABEL},
{"name": PIVOTAL_HAD_REVIEW_LABEL},
],
},
"parsed_row": rows[1],
},
]
== [build_entity(ctx, d) for d in rows]
)


def test_build_story_priority_mapping():
ctx = create_test_ctx()
rows = [
Expand Down Expand Up @@ -210,10 +301,6 @@ def test_build_story_user_mapping():
"story_type": "bug",
"owners": ["Amy Williams", "Daniel McFadden"],
},
{
"story_type": "chore",
"reviewers": ["Giorgio Parisi", "Emmanuelle Charpentier"],
},
]

assert [
Expand Down Expand Up @@ -244,21 +331,6 @@ def test_build_story_user_mapping():
},
"parsed_row": rows[1],
},
{
"type": "story",
"entity": {
"story_type": "chore",
"follower_ids": [
ctx["user_config"]["Giorgio Parisi"],
ctx["user_config"]["Emmanuelle Charpentier"],
],
"labels": [
{"name": PIVOTAL_TO_SHORTCUT_LABEL},
{"name": PIVOTAL_TO_SHORTCUT_RUN_LABEL},
],
},
"parsed_row": rows[2],
},
] == [build_entity(ctx, d) for d in rows]


Expand Down

0 comments on commit a07600e

Please sign in to comment.