aboutsummaryrefslogtreecommitdiff
path: root/scripts/seed_pearl_workspace.py
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-06 18:27:47 -0500
committerCraig Jennings <c@cjennings.net>2026-06-06 18:27:47 -0500
commit7bd9f49ed7cb8482226d70ffaaa3d83fde194710 (patch)
treed3db8680e0e0091382fd0759ca5877c07c5fc45d /scripts/seed_pearl_workspace.py
parent4250bbb39ab1f01a09567ea8f202c2109a2dad30 (diff)
downloadpearl-7bd9f49ed7cb8482226d70ffaaa3d83fde194710.tar.gz
pearl-7bd9f49ed7cb8482226d70ffaaa3d83fde194710.zip
fix(scripts): make Triage the intake column and the default new-issue state
Two problems showed up dogfooding the seeded board. New issues landed in Backlog, because Linear's teamCreate sets the team's default new-issue state to its own Backlog default and the seed never changed it. And the intake column read "Triage Queue" when "Triage" is the name we want. The script now points the team's defaultIssueStateId at the intake column after the columns reconcile, so a fresh issue opens in Triage instead of Backlog. It skips the update when the default is already correct, so a re-run stays a no-op. The intake column is renamed Triage throughout: the target state, the Pearl Inbox view filter, and INTAKE_STATE. find_team reads defaultIssueState so the idempotence check has something to compare. Both mutations were schema-introspected first: TeamUpdateInput.defaultIssueStateId and CustomViewUpdateInput.filterData. The live board was fixed the same way (rename, default-state, Inbox filter) and the dry-run against it comes back all-update, no-create. 13 tests cover the rename, the default-state set on a fresh seed, and the skip when it's already Triage.
Diffstat (limited to 'scripts/seed_pearl_workspace.py')
-rw-r--r--scripts/seed_pearl_workspace.py37
1 files changed, 29 insertions, 8 deletions
diff --git a/scripts/seed_pearl_workspace.py b/scripts/seed_pearl_workspace.py
index 434a995..4e29b76 100644
--- a/scripts/seed_pearl_workspace.py
+++ b/scripts/seed_pearl_workspace.py
@@ -4,7 +4,7 @@
Brings a workspace to:
- a "Pearl" team whose columns are the six Agile states
- (Icebox, Triage Queue, Backlog, In Progress, Done, Canceled),
+ (Icebox, Triage, Backlog, In Progress, Done, Canceled),
- a "Pearl" project inside that team,
- three Custom Views: Pearl Open Issues, Pearl Icebox, Pearl Inbox.
@@ -15,7 +15,7 @@ live, not assumed): teamCreate seeds its own default columns, a team must keep
an unstarted state, and the Duplicate state is reserved. So this script never
archives. It renames Linear's defaults into the six Agile columns -- keeping
Backlog / In Progress / Done / Canceled, and repurposing the unstarted Todo as
-Triage Queue -- and creates only the genuinely-new Icebox. Linear's reserved
+Triage -- and creates only the genuinely-new Icebox. Linear's reserved
Duplicate state is left alone (it's hidden in the board UI); the script reports
any other leftover column for you to tidy.
@@ -23,7 +23,7 @@ Positions are set in a second pass with distinct nonzero values, because Linear
ignores a 0.0 position and appends a freshly-created state at a high one.
Two notes on the result:
- - Triage Queue is Linear's "unstarted" type, so pearl's grouped view (which
+ - Triage is Linear's "unstarted" type, so pearl's grouped view (which
orders by state type, then position) lists it after the backlog columns,
even though the Linear board itself keeps your column order.
- CustomViewCreateInput has no grouping field, so Pearl Open Issues is created
@@ -47,11 +47,11 @@ TEAM_KEY = "PEARL"
PROJECT_NAME = "Pearl"
# The six Agile columns. `sources` names the Linear default state a column is
-# reconciled from when it isn't present by its own name yet (Triage Queue takes
+# reconciled from when it isn't present by its own name yet (Triage takes
# over the unstarted Todo). Positions are distinct and nonzero.
TARGET_STATES = [
{"name": "Icebox", "type": "backlog", "color": "#bec2c8", "position": 1.0, "sources": []},
- {"name": "Triage Queue", "type": "unstarted", "color": "#e2e2e2", "position": 2.0,
+ {"name": "Triage", "type": "unstarted", "color": "#e2e2e2", "position": 2.0,
"sources": ["Todo"]},
{"name": "Backlog", "type": "backlog", "color": "#95a2b3", "position": 3.0, "sources": []},
{"name": "In Progress", "type": "started", "color": "#f2c94c", "position": 4.0, "sources": []},
@@ -60,6 +60,10 @@ TARGET_STATES = [
"sources": ["Cancelled"]},
]
+# New issues should land in the intake column, not Backlog. The team's
+# defaultIssueState is pointed here after the columns reconcile.
+INTAKE_STATE = "Triage"
+
VIEW_NAMES = ["Pearl Open Issues", "Pearl Icebox", "Pearl Inbox"]
@@ -72,7 +76,7 @@ def desired_views(project_id):
{"name": "Pearl Icebox",
"filterData": {**proj, "state": {"name": {"eq": "Icebox"}}}},
{"name": "Pearl Inbox",
- "filterData": {**proj, "state": {"name": {"eq": "Triage Queue"}}}},
+ "filterData": {**proj, "state": {"name": {"eq": "Triage"}}}},
]
@@ -141,7 +145,7 @@ class LinearClient:
def find_team(self):
data = self.execute(
- "query Teams { teams { nodes { id key name "
+ "query Teams { teams { nodes { id key name defaultIssueState { id } "
"states { nodes { id name type position } } } } }", {})
return next((t for t in data["teams"]["nodes"] if t["name"] == TEAM_NAME), None)
@@ -172,6 +176,12 @@ class LinearClient:
"{ workflowStateUpdate(id: $id, input: $input) { success } }",
{"id": state_id, "input": {"name": name, "position": position}})
+ def set_default_issue_state(self, team_id, state_id):
+ self.execute(
+ "mutation TeamUpdate($id: String!, $input: TeamUpdateInput!) "
+ "{ teamUpdate(id: $id, input: $input) { success } }",
+ {"id": team_id, "input": {"defaultIssueStateId": state_id}})
+
def find_project(self):
data = self.execute("query Projects { projects { nodes { id name } } }", {})
return next((p for p in data["projects"]["nodes"] if p["name"] == PROJECT_NAME), None)
@@ -204,11 +214,14 @@ def seed(client):
if team is None:
team = client.create_team()
states = client.team_states(team["id"]) # re-fetch: defaults aren't in the create reply
+ current_default = None
else:
states = team.get("states", {}).get("nodes", [])
+ current_default = (team.get("defaultIssueState") or {}).get("id")
team_id = team["id"]
states_created = []
+ resolved = {}
for entry in reconcile_state_plan(states):
target, match_id = entry["target"], entry["match_id"]
if match_id is None:
@@ -217,8 +230,15 @@ def seed(client):
# Set name + position in one update; a freshly-created state needs the
# position pass because Linear appends it at a high position otherwise.
client.update_state(match_id, target["name"], target["position"])
+ resolved[target["name"]] = match_id
leftover = leftover_state_names(states)
+ # New issues should default to the intake column, not Backlog.
+ intake_id = resolved.get(INTAKE_STATE)
+ default_set = bool(intake_id) and intake_id != current_default
+ if default_set:
+ client.set_default_issue_state(team_id, intake_id)
+
project = client.find_project() or client.create_project(team_id)
project_id = project["id"]
@@ -230,7 +250,8 @@ def seed(client):
views_created.append(spec["name"])
return {"team": team_id, "project": project_id, "states_created": states_created,
- "views_created": views_created, "leftover_states": leftover}
+ "views_created": views_created, "leftover_states": leftover,
+ "default_issue_state": INTAKE_STATE if default_set else "unchanged"}
def main(argv=None):