From d09ce1530f15993e93bebc85a268e6a12dde118b Mon Sep 17 00:00:00 2001 From: Neil Carvalho Date: Fri, 15 May 2026 15:41:51 -0300 Subject: [PATCH] Ensure labels exist before creating PRs `gh` does not create labels automatically when you create a PR referencing one. If the label does not exist, the PR creation fails. This commit adds an `ensure_labels` step before creating the PRs that lists and creates the labels that will be used in the following steps. --- lib/executor.rb | 1 + lib/gh_client.rb | 30 +++++++++++++++++++++++++++++ test/executor_test.rb | 3 +++ test/gh_client_test.rb | 43 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+) diff --git a/lib/executor.rb b/lib/executor.rb index e58d2da..d00aaee 100644 --- a/lib/executor.rb +++ b/lib/executor.rb @@ -111,6 +111,7 @@ def handle_open(action) ) end + @gh.ensure_labels(@labels) @git.checkout_fresh_branch(branch: spec.branch, base: @base) committed = pin_packages_and_commit(spec) if !committed diff --git a/lib/gh_client.rb b/lib/gh_client.rb index f8cdb8b..02dff18 100644 --- a/lib/gh_client.rb +++ b/lib/gh_client.rb @@ -65,6 +65,23 @@ def create_pr(branch:, base:, title:, body:, labels: []) result.stdout.strip[%r{/pull/(\d+)}, 1]&.to_i end + # Ensures every label in +labels+ exists in the repo, creating any that + # are missing. Called once before opening PRs so that `create_pr` never + # fails with "label does not exist". Missing labels are created with a + # neutral default color; existing labels are left untouched. + def ensure_labels(labels) + return if labels.empty? + existing = list_label_names + labels.each do |label| + next if existing.include?(label) + @runner.run( + "gh", "label", "create", label, + "--repo", @repo, + "--color", "0075ca" + ) + end + end + # Edits an existing PR's title and body. Used after force-pushing a # changed branch — the body must be re-rendered so the metadata # block reflects the new package set. @@ -98,6 +115,19 @@ def close_pr(number:, comment: nil) private + def list_label_names + result = @runner.run( + "gh", "label", "list", + "--repo", @repo, + "--json", "name", + "--limit", "200" + ) + return [] unless result.success? + JSON.parse(result.stdout.force_encoding("UTF-8")).map { |l| l["name"] } + rescue JSON::ParserError + [] + end + def parse_pr_list(stdout, branch_prefix) parsed = JSON.parse(stdout.force_encoding("UTF-8")) # `head:foo/` is a *search* term, not a strict filter — GitHub's diff --git a/test/executor_test.rb b/test/executor_test.rb index 2cf4ffb..bcc7d0a 100644 --- a/test/executor_test.rb +++ b/test/executor_test.rb @@ -26,6 +26,9 @@ def initialize @next_pr_number = 1000 end + def ensure_labels(labels) + end + def create_pr(branch:, base:, title:, body:, labels: []) @created << {branch: branch, base: base, title: title, body: body, labels: labels} n = @next_pr_number diff --git a/test/gh_client_test.rb b/test/gh_client_test.rb index cabe086..1f45351 100644 --- a/test/gh_client_test.rb +++ b/test/gh_client_test.rb @@ -115,6 +115,49 @@ def test_list_open_prs_propagates_gh_failure assert_includes err.message, "auth required" end + # ---- ensure_labels ---- + + def test_ensure_labels_is_a_no_op_when_labels_is_empty + @client.ensure_labels([]) + assert_equal 0, @runner.calls.size + end + + def test_ensure_labels_creates_missing_labels + @runner.add( + pattern: ["gh", "label", "list", "--repo", REPO, "--json", "name", "--limit", "200"], + stdout: +%([{"name":"dependencies"}]) + ) + @runner.add( + pattern: ["gh", "label", "create", "javascript", "--repo", REPO, "--color", "0075ca"], + stdout: +"" + ) + @client.ensure_labels(%w[dependencies javascript]) + assert_equal 2, @runner.calls.size + end + + def test_ensure_labels_skips_labels_that_already_exist + @runner.add( + pattern: ["gh", "label", "list", "--repo", REPO, "--json", "name", "--limit", "200"], + stdout: +%([{"name":"dependencies"},{"name":"javascript"}]) + ) + @client.ensure_labels(%w[dependencies javascript]) + assert_equal 1, @runner.calls.size + end + + def test_ensure_labels_tolerates_gh_label_list_failure + @runner.add( + pattern: ["gh", "label", "list", "--repo", REPO, "--json", "name", "--limit", "200"], + stderr: "not found", + exit_code: 1 + ) + @runner.add( + pattern: ["gh", "label", "create", "dependencies", "--repo", REPO, "--color", "0075ca"], + stdout: +"" + ) + @client.ensure_labels(%w[dependencies]) + assert_equal 2, @runner.calls.size + end + # ---- create_pr ---- def test_create_pr_invokes_gh_with_title_body_branch_and_base