os-backlog: cache resolved Planka board id in .cc-os/config (issue #22)
- Backlog::BoardResolver with injected config read/write; Cards does no filesystem I/O - board_id honored only for planka: trackers; id hit validated against returned board name; 404/mismatch/cache failure degrades to name lookup and rewrites the cache - comment-preserving Config.set mirrors os-status's writer (Config.merge not used) - FakePlankaClient boards.get 404 support; 14 new tests; suite 90 runs / 0 failures Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CLeMz48rvG3s9XpAsDxeho
This commit is contained in:
parent
90db409721
commit
46b0584643
|
|
@ -47,6 +47,21 @@ rescue StandardError => e
|
||||||
fail_soft("could not authenticate to Planka: #{e.message}")
|
fail_soft("could not authenticate to Planka: #{e.message}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Cards wired to the cwd repo's .cc-os/config board_id cache (issue #22):
|
||||||
|
# the BoardResolver gets injected read/write lambdas; any cache failure
|
||||||
|
# degrades to the plain name lookup inside the resolver itself.
|
||||||
|
def build_cards(client)
|
||||||
|
config_path = File.join(Dir.pwd, ".cc-os", "config")
|
||||||
|
read_config = -> { File.exist?(config_path) ? File.read(config_path) : nil }
|
||||||
|
write_config = lambda do |contents|
|
||||||
|
FileUtils.mkdir_p(File.dirname(config_path))
|
||||||
|
File.write(config_path, contents)
|
||||||
|
end
|
||||||
|
resolver = Backlog::BoardResolver.new(client: client, read_config: read_config,
|
||||||
|
write_config: write_config)
|
||||||
|
Backlog::Cards.new(client: client, board_resolver: resolver)
|
||||||
|
end
|
||||||
|
|
||||||
def parse_project_flag(args)
|
def parse_project_flag(args)
|
||||||
idx = args.index("--project")
|
idx = args.index("--project")
|
||||||
return "Dev" unless idx
|
return "Dev" unless idx
|
||||||
|
|
@ -157,19 +172,19 @@ when "card-add"
|
||||||
title = require_flag(rest, "--title", "card-add")
|
title = require_flag(rest, "--title", "card-add")
|
||||||
description = parse_flag(rest, "--description")
|
description = parse_flag(rest, "--description")
|
||||||
client = build_client
|
client = build_client
|
||||||
card = Backlog::Cards.new(client: client).add(board_name: board, title: title, description: description)
|
card = build_cards(client).add(board_name: board, title: title, description: description)
|
||||||
puts "created card ##{card.id} at Backlog on #{board}: #{card.name}"
|
puts "created card ##{card.id} at Backlog on #{board}: #{card.name}"
|
||||||
when "card-move"
|
when "card-move"
|
||||||
board = require_flag(rest, "--board", "card-move")
|
board = require_flag(rest, "--board", "card-move")
|
||||||
card_id = require_flag(rest, "--card", "card-move")
|
card_id = require_flag(rest, "--card", "card-move")
|
||||||
to = require_flag(rest, "--to", "card-move")
|
to = require_flag(rest, "--to", "card-move")
|
||||||
client = build_client
|
client = build_client
|
||||||
card = Backlog::Cards.new(client: client).move(board_name: board, card_id: card_id, to: to)
|
card = build_cards(client).move(board_name: board, card_id: card_id, to: to)
|
||||||
puts "moved card ##{card.id} to #{to} on #{board}"
|
puts "moved card ##{card.id} to #{to} on #{board}"
|
||||||
when "cards"
|
when "cards"
|
||||||
board = require_flag(rest, "--board", "cards")
|
board = require_flag(rest, "--board", "cards")
|
||||||
client = build_client
|
client = build_client
|
||||||
snapshot = Backlog::Cards.new(client: client).snapshot(board_name: board)
|
snapshot = build_cards(client).snapshot(board_name: board)
|
||||||
snapshot["lists"].each do |list|
|
snapshot["lists"].each do |list|
|
||||||
puts "#{list['name']} (#{list['cards'].size})"
|
puts "#{list['name']} (#{list['cards'].size})"
|
||||||
list["cards"].each do |card|
|
list["cards"].each do |card|
|
||||||
|
|
@ -180,19 +195,19 @@ when "cards"
|
||||||
when "snapshot"
|
when "snapshot"
|
||||||
board = require_flag(rest, "--board", "snapshot")
|
board = require_flag(rest, "--board", "snapshot")
|
||||||
client = build_client
|
client = build_client
|
||||||
puts JSON.pretty_generate(Backlog::Cards.new(client: client).snapshot(board_name: board))
|
puts JSON.pretty_generate(build_cards(client).snapshot(board_name: board))
|
||||||
when "card-label"
|
when "card-label"
|
||||||
board = require_flag(rest, "--board", "card-label")
|
board = require_flag(rest, "--board", "card-label")
|
||||||
card_id = require_flag(rest, "--card", "card-label")
|
card_id = require_flag(rest, "--card", "card-label")
|
||||||
label = require_flag(rest, "--label", "card-label")
|
label = require_flag(rest, "--label", "card-label")
|
||||||
client = build_client
|
client = build_client
|
||||||
Backlog::Cards.new(client: client).attach_label(board_name: board, card_id: card_id, label: label)
|
build_cards(client).attach_label(board_name: board, card_id: card_id, label: label)
|
||||||
puts "labeled card ##{card_id} with #{label}"
|
puts "labeled card ##{card_id} with #{label}"
|
||||||
when "card-comment"
|
when "card-comment"
|
||||||
card_id = require_flag(rest, "--card", "card-comment")
|
card_id = require_flag(rest, "--card", "card-comment")
|
||||||
text = require_flag(rest, "--text", "card-comment")
|
text = require_flag(rest, "--text", "card-comment")
|
||||||
client = build_client
|
client = build_client
|
||||||
comment = Backlog::Cards.new(client: client).comment(card_id: card_id, text: text)
|
comment = build_cards(client).comment(card_id: card_id, text: text)
|
||||||
puts "commented on card ##{card_id} (comment ##{comment.id})"
|
puts "commented on card ##{card_id} (comment ##{comment.id})"
|
||||||
when "inspect"
|
when "inspect"
|
||||||
repo_path = Dir.pwd
|
repo_path = Dir.pwd
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
require_relative "backlog/board_spec"
|
require_relative "backlog/board_spec"
|
||||||
require_relative "backlog/board_ensurer"
|
require_relative "backlog/board_ensurer"
|
||||||
|
require_relative "backlog/board_resolver"
|
||||||
require_relative "backlog/cards"
|
require_relative "backlog/cards"
|
||||||
require_relative "backlog/config"
|
require_relative "backlog/config"
|
||||||
require_relative "backlog/inspector"
|
require_relative "backlog/inspector"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
require_relative "config"
|
||||||
|
|
||||||
|
module Backlog
|
||||||
|
# Resolves a board name to its Planka board record, caching the resolved
|
||||||
|
# id as `board_id=<id>` in the repo's `.cc-os/config` (issue #22).
|
||||||
|
#
|
||||||
|
# The tracker's board *name* stays the source of truth; the id is only a
|
||||||
|
# cache. The cached id is honored only when the config's tracker is
|
||||||
|
# exactly `planka:<board_name>`, and a cached `boards.get(id)` hit counts
|
||||||
|
# only if the returned board's name still matches — a 404 or a rename
|
||||||
|
# falls back to the projects+boards name lookup and rewrites the cache.
|
||||||
|
# Any cache failure (unreadable config, unwritable config, stale id)
|
||||||
|
# degrades to the name lookup; it never fails the command.
|
||||||
|
#
|
||||||
|
# Filesystem access is injected: `read_config` returns the raw
|
||||||
|
# `.cc-os/config` contents (or nil), `write_config` persists updated
|
||||||
|
# contents. Cache writes go through Config.set, the comment-preserving
|
||||||
|
# writer — never Config.merge.
|
||||||
|
class BoardResolver
|
||||||
|
# @param client [Planka::Client] an already-authenticated client
|
||||||
|
# @param read_config [#call] -> String, nil — raw .cc-os/config contents
|
||||||
|
# @param write_config [#call] (String) -> void — persist updated contents
|
||||||
|
def initialize(client:, read_config:, write_config:)
|
||||||
|
@client = client
|
||||||
|
@read_config = read_config
|
||||||
|
@write_config = write_config
|
||||||
|
end
|
||||||
|
|
||||||
|
# A resolver with no config access: always name-lookup, never caches.
|
||||||
|
def self.null(client:)
|
||||||
|
new(client: client, read_config: -> {}, write_config: ->(_) {})
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param name [String] the board name (source of truth)
|
||||||
|
# @return [Planka::Types::Board]
|
||||||
|
# @raise [RuntimeError] if no board with that name exists
|
||||||
|
def board_for(name)
|
||||||
|
cached = cached_board(name)
|
||||||
|
return cached if cached
|
||||||
|
|
||||||
|
board = lookup_by_name!(name)
|
||||||
|
cache_id(name, board.id)
|
||||||
|
board
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def cached_board(name)
|
||||||
|
config = safe_config
|
||||||
|
return nil unless config && config.tracker == "planka:#{name}"
|
||||||
|
|
||||||
|
id = config.board_id
|
||||||
|
return nil unless id
|
||||||
|
|
||||||
|
board = @client.boards.get(id).board
|
||||||
|
board && board.name == name ? board : nil
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def lookup_by_name!(name)
|
||||||
|
@client.projects.list.each do |project|
|
||||||
|
board = @client.boards.list(project.id).find { |b| b.name == name }
|
||||||
|
return board if board
|
||||||
|
end
|
||||||
|
raise "no board named #{name.inspect} found (run board-ensure first)"
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_id(name, id)
|
||||||
|
contents = @read_config.call
|
||||||
|
config = Config.new(contents)
|
||||||
|
return unless config.tracker == "planka:#{name}"
|
||||||
|
return if config.board_id == id.to_s
|
||||||
|
|
||||||
|
@write_config.call(Config.set(contents, "board_id", id))
|
||||||
|
rescue StandardError
|
||||||
|
nil # cache write failure never fails the command
|
||||||
|
end
|
||||||
|
|
||||||
|
def safe_config
|
||||||
|
Config.new(@read_config.call)
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
require_relative "board_spec"
|
require_relative "board_spec"
|
||||||
|
require_relative "board_resolver"
|
||||||
|
|
||||||
module Backlog
|
module Backlog
|
||||||
# Card-level operations over an authenticated Planka client: create a
|
# Card-level operations over an authenticated Planka client: create a
|
||||||
|
|
@ -12,8 +13,13 @@ module Backlog
|
||||||
# Never deletes anything.
|
# Never deletes anything.
|
||||||
class Cards
|
class Cards
|
||||||
# @param client [Planka::Client] an already-authenticated client
|
# @param client [Planka::Client] an already-authenticated client
|
||||||
def initialize(client:)
|
# @param board_resolver [BoardResolver, nil] resolves board names to
|
||||||
|
# board records (with the .cc-os/config board_id cache — issue #22);
|
||||||
|
# defaults to a cache-less name-only resolver. Cards never touches
|
||||||
|
# the filesystem itself — all config I/O lives in the resolver.
|
||||||
|
def initialize(client:, board_resolver: nil)
|
||||||
@client = client
|
@client = client
|
||||||
|
@board_resolver = board_resolver || BoardResolver.null(client: client)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Create a card at the board's Backlog list. Raises if the board or
|
# Create a card at the board's Backlog list. Raises if the board or
|
||||||
|
|
@ -163,11 +169,7 @@ module Backlog
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_board!(name)
|
def find_board!(name)
|
||||||
@client.projects.list.each do |project|
|
@board_resolver.board_for(name)
|
||||||
board = @client.boards.list(project.id).find { |b| b.name == name }
|
|
||||||
return board if board
|
|
||||||
end
|
|
||||||
raise "no board named #{name.inspect} found (run board-ensure first)"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_list!(board, list_name)
|
def find_list!(board, list_name)
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,13 @@ module Backlog
|
||||||
@data["tracker"]
|
@data["tracker"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [String, nil] the cached Planka board id, if present. Only a
|
||||||
|
# cache — the tracker's board *name* stays the source of truth
|
||||||
|
# (issue #22); BoardResolver decides whether to honor it.
|
||||||
|
def board_id
|
||||||
|
@data["board_id"]
|
||||||
|
end
|
||||||
|
|
||||||
def configured? = !board.nil?
|
def configured? = !board.nil?
|
||||||
def tracker_configured? = !tracker.nil?
|
def tracker_configured? = !tracker.nil?
|
||||||
|
|
||||||
|
|
@ -55,5 +62,35 @@ module Backlog
|
||||||
data[key.to_s] = value.to_s
|
data[key.to_s] = value.to_s
|
||||||
data.map { |k, v| "#{k}=#{v}\n" }.join
|
data.map { |k, v| "#{k}=#{v}\n" }.join
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Like .merge but comment/format-preserving: every non-key line
|
||||||
|
# (comments, blanks) is left byte-for-byte untouched; existing
|
||||||
|
# key=value lines are re-emitted in the normalized no-spaces form; the
|
||||||
|
# key is replaced in place or appended if absent. Mirrors os-status's
|
||||||
|
# write_config_value (plugins/os-status/hooks/state.py) — the shared
|
||||||
|
# .cc-os/config contract owner. Pure — no filesystem.
|
||||||
|
#
|
||||||
|
# @param contents [String, nil] existing raw `.cc-os/config` contents
|
||||||
|
# @param key [String, Symbol] key to set
|
||||||
|
# @param value [Object] value to set it to
|
||||||
|
# @return [String] the updated contents
|
||||||
|
def self.set(contents, key, value)
|
||||||
|
key = key.to_s
|
||||||
|
found = false
|
||||||
|
out = contents.to_s.split("\n").map do |line|
|
||||||
|
stripped = line.strip
|
||||||
|
next line if stripped.empty? || stripped.start_with?("#") || !stripped.include?("=")
|
||||||
|
|
||||||
|
k, v = stripped.split("=", 2)
|
||||||
|
if k.strip == key
|
||||||
|
found = true
|
||||||
|
"#{key}=#{value}"
|
||||||
|
else
|
||||||
|
"#{k.strip}=#{v.strip}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
out << "#{key}=#{value}" unless found
|
||||||
|
"#{out.join("\n")}\n"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
require_relative "test_helper"
|
||||||
|
|
||||||
|
class BoardResolverTest < Minitest::Test
|
||||||
|
include BacklogTestHelpers
|
||||||
|
|
||||||
|
def setup
|
||||||
|
@client = FakePlankaClient.new
|
||||||
|
ensurer = Backlog::BoardEnsurer.new(client: @client, human_user_id: "human-1")
|
||||||
|
@board = ensurer.ensure("llf-schema", project_name: "Dev").board
|
||||||
|
@writes = []
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolver_with(config)
|
||||||
|
@config = config
|
||||||
|
Backlog::BoardResolver.new(
|
||||||
|
client: @client,
|
||||||
|
read_config: -> { @config },
|
||||||
|
write_config: ->(contents) { @writes << contents }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def config_with_cache(id = @board.id)
|
||||||
|
"# repo config\ntracker=planka:llf-schema\nboard_id=#{id}\nversion=3\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_cache_hit_uses_boards_get_and_skips_name_listing
|
||||||
|
@client.projects.define_singleton_method(:list) { raise "projects.list must not be called on a cache hit" }
|
||||||
|
resolver = resolver_with(config_with_cache)
|
||||||
|
|
||||||
|
board = resolver.board_for("llf-schema")
|
||||||
|
|
||||||
|
assert_equal @board.id, board.id
|
||||||
|
assert_empty @writes
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_cache_miss_resolves_by_name_and_writes_board_id_preserving_comments
|
||||||
|
resolver = resolver_with("# repo config\ntracker=planka:llf-schema\nversion=3\n")
|
||||||
|
|
||||||
|
board = resolver.board_for("llf-schema")
|
||||||
|
|
||||||
|
assert_equal @board.id, board.id
|
||||||
|
assert_equal 1, @writes.size
|
||||||
|
assert_equal "# repo config\ntracker=planka:llf-schema\nversion=3\nboard_id=#{@board.id}\n",
|
||||||
|
@writes.last
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_stale_id_404_falls_back_to_name_lookup_and_rewrites_cache
|
||||||
|
resolver = resolver_with(config_with_cache("no-such-id"))
|
||||||
|
|
||||||
|
board = resolver.board_for("llf-schema")
|
||||||
|
|
||||||
|
assert_equal @board.id, board.id
|
||||||
|
assert_includes @writes.last, "board_id=#{@board.id}\n"
|
||||||
|
assert_includes @writes.last, "# repo config"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_renamed_board_name_mismatch_falls_back_and_rewrites_cache
|
||||||
|
stale = @board
|
||||||
|
@client.boards.update(stale.id, name: "something-else")
|
||||||
|
current = @client.boards.create(stale.project_id, name: "llf-schema", position: 2)
|
||||||
|
resolver = resolver_with(config_with_cache(stale.id))
|
||||||
|
|
||||||
|
board = resolver.board_for("llf-schema")
|
||||||
|
|
||||||
|
assert_equal current.id, board.id
|
||||||
|
assert_includes @writes.last, "board_id=#{current.id}\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_non_planka_tracker_ignores_board_id_and_never_writes
|
||||||
|
resolver = resolver_with("tracker=forgejo:jared/cc-os\nboard_id=bogus-id\n")
|
||||||
|
|
||||||
|
board = resolver.board_for("llf-schema")
|
||||||
|
|
||||||
|
assert_equal @board.id, board.id
|
||||||
|
assert_empty @writes
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_board_id_for_different_planka_board_name_is_ignored
|
||||||
|
resolver = resolver_with("tracker=planka:other-board\nboard_id=bogus-id\n")
|
||||||
|
|
||||||
|
board = resolver.board_for("llf-schema")
|
||||||
|
|
||||||
|
assert_equal @board.id, board.id
|
||||||
|
assert_empty @writes # tracker doesn't match this board name; no cache write either
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_unreadable_config_degrades_to_name_lookup
|
||||||
|
resolver = Backlog::BoardResolver.new(
|
||||||
|
client: @client,
|
||||||
|
read_config: -> { raise Errno::EACCES, "config" },
|
||||||
|
write_config: ->(contents) { @writes << contents }
|
||||||
|
)
|
||||||
|
|
||||||
|
board = resolver.board_for("llf-schema")
|
||||||
|
|
||||||
|
assert_equal @board.id, board.id
|
||||||
|
assert_empty @writes
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_unwritable_config_never_fails_the_command
|
||||||
|
resolver = Backlog::BoardResolver.new(
|
||||||
|
client: @client,
|
||||||
|
read_config: -> { "tracker=planka:llf-schema\n" },
|
||||||
|
write_config: ->(_) { raise Errno::EACCES, "config" }
|
||||||
|
)
|
||||||
|
|
||||||
|
board = resolver.board_for("llf-schema")
|
||||||
|
|
||||||
|
assert_equal @board.id, board.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_matching_cached_id_is_not_rewritten
|
||||||
|
resolver = resolver_with(config_with_cache)
|
||||||
|
resolver.board_for("llf-schema")
|
||||||
|
|
||||||
|
assert_empty @writes
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_unknown_board_still_raises
|
||||||
|
resolver = resolver_with(nil)
|
||||||
|
error = assert_raises(RuntimeError) { resolver.board_for("nope") }
|
||||||
|
assert_match(/no board named "nope"/, error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_cards_add_through_resolver_writes_cache_once
|
||||||
|
resolver = resolver_with("tracker=planka:llf-schema\n")
|
||||||
|
cards = Backlog::Cards.new(client: @client, board_resolver: resolver)
|
||||||
|
|
||||||
|
card = cards.add(board_name: "llf-schema", title: "Cache me")
|
||||||
|
|
||||||
|
assert_equal "Cache me", card.name
|
||||||
|
assert_equal 1, @writes.size
|
||||||
|
assert_includes @writes.last, "board_id=#{@board.id}\n"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -93,4 +93,33 @@ class ConfigTest < Minitest::Test
|
||||||
assert_equal "my-hub", updated.to_h["hub"]
|
assert_equal "my-hub", updated.to_h["hub"]
|
||||||
assert_equal "planka:board", updated.tracker
|
assert_equal "planka:board", updated.tracker
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_board_id_reader
|
||||||
|
config = Backlog::Config.new("tracker=planka:board\nboard_id=42\n")
|
||||||
|
assert_equal "42", config.board_id
|
||||||
|
assert_nil Backlog::Config.new(nil).board_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_set_preserves_comments_and_blank_lines
|
||||||
|
existing = "# managed by cc-os\ntracker=planka:board\n\n# stamped by os-status\nversion=3\n"
|
||||||
|
updated = Backlog::Config.set(existing, "board_id", "42")
|
||||||
|
assert_equal "# managed by cc-os\ntracker=planka:board\n\n# stamped by os-status\nversion=3\nboard_id=42\n",
|
||||||
|
updated
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_set_replaces_existing_key_in_place
|
||||||
|
existing = "# top comment\nboard_id=old\ntracker=planka:board\n"
|
||||||
|
updated = Backlog::Config.set(existing, "board_id", "new")
|
||||||
|
assert_equal "# top comment\nboard_id=new\ntracker=planka:board\n", updated
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_set_normalizes_spaced_key_value_lines_only
|
||||||
|
existing = "# note\nversion = 1\n"
|
||||||
|
updated = Backlog::Config.set(existing, "board_id", "42")
|
||||||
|
assert_equal "# note\nversion=1\nboard_id=42\n", updated
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_set_on_nil_contents
|
||||||
|
assert_equal "board_id=42\n", Backlog::Config.set(nil, "board_id", "42")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,11 @@ module BacklogTestHelpers
|
||||||
|
|
||||||
def get(id)
|
def get(id)
|
||||||
board = @client.boards_store.find { |b| b.id == id }
|
board = @client.boards_store.find { |b| b.id == id }
|
||||||
|
unless board
|
||||||
|
raise Planka::NotFoundError.new(status: 404,
|
||||||
|
body: { "message" => "board #{id} not found" })
|
||||||
|
end
|
||||||
|
|
||||||
Planka::Types::BoardDetail.new(
|
Planka::Types::BoardDetail.new(
|
||||||
board: board,
|
board: board,
|
||||||
lists: @client.lists.list(id),
|
lists: @client.lists.list(id),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue