151 lines
5.8 KiB
Ruby
151 lines
5.8 KiB
Ruby
require_relative "board_spec"
|
|
|
|
module Backlog
|
|
# Idempotently creates/repairs a uniform Planka board: the project it
|
|
# lives under, its lists, and its label set — plus the Planka 2.1.1
|
|
# quirks the planka-api gem does not handle for us:
|
|
#
|
|
# 1. undocumented `type` fields on create (project: "shared" so a
|
|
# manager can be added later; list: "active"; the gem already
|
|
# requires callers to pass these — it does not default them)
|
|
# 2. bot-created projects are invisible to the human: a `"private"`
|
|
# project auto-assigns its creator as sole ownerProjectManagerId and
|
|
# rejects additional managers (422). We create projects as
|
|
# `type: "shared"` instead, then explicitly add the human as a
|
|
# project manager and null out ownerProjectManagerId.
|
|
# 3. label colors must come from Planka's (undocumented) color-name
|
|
# whitelist — we try an ordered candidate list per label, same
|
|
# pattern the planka-api gem uses for its own priority labels.
|
|
#
|
|
# This class never archives a board — archiving only happens on explicit
|
|
# human request (see BoardSpec / the resolver). It does provide the raw
|
|
# rename primitives (+activate+ / +archive+) that a caller may invoke.
|
|
class BoardEnsurer
|
|
Result = Struct.new(:board, :created, :project, :lists_created, :labels_created, keyword_init: true)
|
|
|
|
# @param client [Planka::Client] an already-authenticated client
|
|
# @param human_user_id [String, Integer, nil] Planka user id of the
|
|
# human who must be able to see bot-created projects. Falls back to
|
|
# +ENV["PLANKA_HUMAN_USER_ID"]+. Only needed when a project doesn't
|
|
# already exist.
|
|
def initialize(client:, human_user_id: nil)
|
|
@client = client
|
|
@human_user_id = human_user_id || ENV["PLANKA_HUMAN_USER_ID"]
|
|
end
|
|
|
|
# @param name [String] board name (never an archived---prefixed name;
|
|
# this method only ever targets the active board)
|
|
# @param project_name [String] "Dev" or "Clients" (or any project name)
|
|
# @return [Result]
|
|
def ensure(name, project_name:)
|
|
raise ArgumentError, "board name must not use the archived-- prefix" if BoardSpec.archived?(name)
|
|
|
|
project = find_or_create_project(project_name)
|
|
board, created = find_or_create_board(project, name)
|
|
|
|
lists_created = ensure_lists(board)
|
|
labels_created = ensure_labels(board)
|
|
|
|
Result.new(board: board, created: created, project: project,
|
|
lists_created: lists_created, labels_created: labels_created)
|
|
end
|
|
|
|
# Rename an archived board back to its active name. Raises if no
|
|
# archived board by that name exists.
|
|
#
|
|
# @param name [String] the active (non-prefixed) name to activate
|
|
# @return [Types::Board] the updated board
|
|
def activate(name)
|
|
board = find_board_by_name(BoardSpec.archived_name(name))
|
|
raise "no archived board named #{BoardSpec.archived_name(name).inspect} found" unless board
|
|
|
|
@client.boards.update(board.id, name: name)
|
|
end
|
|
|
|
# Rename an active board to its archived form. Only ever called on
|
|
# explicit human request — never invoked by +ensure+.
|
|
#
|
|
# @param name [String] the active (non-prefixed) name to archive
|
|
# @return [Types::Board] the updated board
|
|
def archive(name)
|
|
board = find_board_by_name(name)
|
|
raise "no active board named #{name.inspect} found" unless board
|
|
|
|
@client.boards.update(board.id, name: BoardSpec.archived_name(name))
|
|
end
|
|
|
|
private
|
|
|
|
def find_or_create_project(project_name)
|
|
existing = @client.projects.list.find { |p| p.name == project_name }
|
|
return existing if existing
|
|
|
|
project = @client.projects.create(name: project_name, type: "shared")
|
|
fix_visibility(project)
|
|
project
|
|
end
|
|
|
|
# Quirk 2: null the bot's sole ownerProjectManagerId and add the human
|
|
# as a project manager, so the project shows up for them.
|
|
def fix_visibility(project)
|
|
return unless @human_user_id
|
|
|
|
@client.projects.add_manager(project.id, @human_user_id)
|
|
@client.projects.update(project.id, ownerProjectManagerId: nil)
|
|
end
|
|
|
|
def find_or_create_board(project, name)
|
|
board = find_board_by_name(name, project: project)
|
|
return [board, false] if board
|
|
|
|
created = @client.boards.create(project.id, name: name, position: 65_536)
|
|
[created, true]
|
|
end
|
|
|
|
def find_board_by_name(name, project: nil)
|
|
projects = project ? [project] : @client.projects.list
|
|
projects.each do |p|
|
|
board = @client.boards.list(p.id).find { |b| b.name == name }
|
|
return board if board
|
|
end
|
|
nil
|
|
end
|
|
|
|
def ensure_lists(board)
|
|
existing = @client.lists.list(board.id).filter_map(&:name)
|
|
created = []
|
|
BoardSpec::LISTS.each_with_index do |list_name, index|
|
|
next if existing.include?(list_name)
|
|
|
|
@client.lists.create(board.id, name: list_name, type: "active", position: (index + 1) * 65_536)
|
|
created << list_name
|
|
end
|
|
created
|
|
end
|
|
|
|
def ensure_labels(board)
|
|
existing = @client.labels.list(board.id).filter_map(&:name)
|
|
created = []
|
|
BoardSpec::LABEL_NAMES.each do |label_name|
|
|
next if existing.include?(label_name)
|
|
|
|
create_label_with_fallback_color(board, label_name)
|
|
created << label_name
|
|
end
|
|
created
|
|
end
|
|
|
|
def create_label_with_fallback_color(board, label_name)
|
|
candidates = BoardSpec::LABEL_COLOR_CANDIDATES.fetch(label_name)
|
|
last_error = nil
|
|
candidates.each do |color|
|
|
return @client.labels.create(board.id, name: label_name, color: color, position: 65_536)
|
|
rescue Planka::ValidationError, Planka::BadRequestError => e
|
|
last_error = e
|
|
next
|
|
end
|
|
raise last_error || RuntimeError, "could not create label #{label_name.inspect}: no candidate color accepted"
|
|
end
|
|
end
|
|
end
|