-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Add Zexbox.JiraClient #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
7b6713d
feat: Add Zexbox.OpenTelemetry URL helpers with tests
Jez-A 4bdc42c
feat: Remove Jaeger fallback from generate_trace_url
Jez-A 61b7154
feat: Add JiraClient and OpenTelemetry URL helpers
Jez-A 5113876
style: Run mix format
Jez-A d474fe2
fix: Use POST /search/jql for search and add Accept header to all req…
Jez-A a488bbc
refactor: Convert JiraClient public API to explicit positional arguments
Jez-A 0fe9c12
fix: Name bare _ wildcards to satisfy Credo consistency check
Jez-A 34f4acb
Cleanup after moving the Telemetry to another PR
Jez-A 4ed4e32
Cleanup mix too
Jez-A 95a2974
not needed
Jez-A 8d572e6
Corrected fields type and cleanup
Jez-A 8d02399
Admin
Jez-A 74c51f4
Clearer separation between public and private methods
Jez-A 24b1313
refactor: Simplify and improve JiraClient readability
Jez-A File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| defmodule Zexbox.JiraClient do | ||
| @moduledoc """ | ||
| HTTP client for the Jira Cloud REST API v3. | ||
|
|
||
| Mirrors `Opsbox::JiraClient`. Authenticates with Basic auth using | ||
| `JIRA_USER_EMAIL_ADDRESS` and `JIRA_API_TOKEN` environment variables | ||
| (or `:jira_email` / `:jira_api_token` application config). | ||
|
|
||
| ## Configuration | ||
|
|
||
| ```elixir | ||
| config :zexbox, | ||
| jira_base_url: "https://your-org.atlassian.net", | ||
| jira_email: System.get_env("JIRA_USER_EMAIL_ADDRESS"), | ||
| jira_api_token: System.get_env("JIRA_API_TOKEN") | ||
| ``` | ||
|
|
||
| All public functions return `{:ok, result}` or `{:error, reason}`. | ||
| """ | ||
|
|
||
| @bug_fingerprint_field %{id: "customfield_13442", name: "Bug Fingerprint[Short text]"} | ||
| @zigl_team_field %{id: "customfield_10101", name: "ZIGL Team[Dropdown]"} | ||
|
|
||
| @doc "Returns the bug fingerprint custom field metadata." | ||
| @spec bug_fingerprint_field() :: %{id: String.t(), name: String.t()} | ||
| def bug_fingerprint_field, do: @bug_fingerprint_field | ||
|
|
||
| @doc "Returns the ZIGL team custom field metadata." | ||
| @spec zigl_team_field() :: %{id: String.t(), name: String.t()} | ||
| def zigl_team_field, do: @zigl_team_field | ||
|
|
||
| @doc """ | ||
| Search for the latest issues matching a JQL query (max 50 results). | ||
|
|
||
| - `jql` – JQL query string. | ||
| - `project_key` – optional; prepends `project = KEY AND` to the JQL. | ||
|
|
||
| Returns `{:ok, [issue_map]}` where each map includes a `"url"` browse key, | ||
| or `{:error, reason}` on failure. | ||
| """ | ||
| @spec search_latest_issues(String.t(), String.t() | nil) :: {:ok, [map()]} | {:error, term()} | ||
| def search_latest_issues(jql, project_key \\ nil) do | ||
| fetch_issues(build_query(jql, project_key)) | ||
| end | ||
|
|
||
| @doc """ | ||
| Create a new Jira issue. | ||
|
|
||
| - `project_key` – Jira project key (e.g. `"SS"`). | ||
| - `summary` – issue summary string. | ||
| - `description` – ADF map (already built; not converted). | ||
| - `issuetype` – issue type name (e.g. `"Bug"`). | ||
| - `priority` – priority name (e.g. `"High"`). | ||
| - `custom_fields` – optional map of custom field ID → value (string keys). | ||
|
|
||
| Returns `{:ok, issue_map}` with a `"url"` browse key added, or `{:error, reason}`. | ||
| """ | ||
| @spec create_issue(String.t(), String.t(), map(), String.t(), String.t(), map()) :: | ||
| {:ok, map()} | {:error, term()} | ||
| def create_issue(project_key, summary, description, issuetype, priority, custom_fields \\ %{}) do | ||
| fields = | ||
| Map.merge( | ||
| %{ | ||
| "project" => %{"key" => project_key}, | ||
| "summary" => summary, | ||
| "description" => description, | ||
| "issuetype" => %{"name" => issuetype}, | ||
| "priority" => %{"name" => priority} | ||
| }, | ||
| custom_fields | ||
| ) | ||
|
|
||
| post_issue(fields) | ||
| end | ||
|
|
||
| @doc """ | ||
| Transition a Jira issue to a new status by name (case-insensitive match). | ||
|
|
||
| - `issue_key` – issue key (e.g. `"SS-42"`). | ||
| - `status_name` – target status name (e.g. `"To do"`). | ||
|
|
||
| Returns `{:ok, %{success: true, status: name}}` or `{:error, reason}`. | ||
| """ | ||
| @spec transition_issue(String.t(), String.t()) :: {:ok, map()} | {:error, term()} | ||
| def transition_issue(issue_key, status_name) do | ||
| client = build_client() | ||
|
|
||
| with {:ok, data} <- jira_get(client, "/rest/api/3/issue/#{issue_key}/transitions"), | ||
| transitions = Map.get(data, "transitions", []), | ||
| {:ok, target} <- find_transition(transitions, status_name), | ||
| {:ok, _resp} <- | ||
| jira_post(client, "/rest/api/3/issue/#{issue_key}/transitions", %{ | ||
| "transition" => %{"id" => target["id"]} | ||
| }) do | ||
| {:ok, %{success: true, status: get_in(target, ["to", "name"])}} | ||
| end | ||
| end | ||
|
|
||
| @doc """ | ||
| Add a comment to an existing Jira issue. | ||
|
|
||
| - `issue_key` – issue key (e.g. `"SS-42"`). | ||
| - `comment` – ADF map for the comment body (already built; not converted). | ||
|
|
||
| Returns `{:ok, comment_map}` or `{:error, reason}`. | ||
| """ | ||
| @spec add_comment(String.t(), map()) :: {:ok, map()} | {:error, term()} | ||
| def add_comment(issue_key, comment), do: post_comment(issue_key, comment) | ||
|
|
||
| # --- Private --- | ||
|
|
||
| defp build_query(jql, nil), do: jql | ||
| defp build_query(jql, project_key), do: "project = #{project_key} AND #{jql}" | ||
|
|
||
| defp fetch_issues(query) do | ||
| build_client() | ||
| |> jira_get("/rest/api/3/issue/search", | ||
| jql: query, | ||
| maxResults: 50, | ||
| fields: ["key", "id", "self", "status", "summary"] | ||
| ) | ||
| |> attach_issue_urls() | ||
| end | ||
|
|
||
| defp post_issue(fields) do | ||
| build_client() | ||
| |> jira_post("/rest/api/3/issue", %{"fields" => fields}) | ||
| |> attach_issue_url() | ||
| end | ||
|
|
||
| defp post_comment(issue_key, comment) do | ||
| build_client() | ||
| |> jira_post("/rest/api/3/issue/#{issue_key}/comment", %{"body" => comment}) | ||
| end | ||
|
|
||
| defp attach_issue_urls({:ok, body}) do | ||
| issues = Map.get(body, "issues", []) | ||
| {:ok, Enum.map(issues, &Map.put(&1, "url", browse_url(&1["key"])))} | ||
| end | ||
|
|
||
| defp attach_issue_urls({:error, _reason} = err), do: err | ||
|
|
||
| defp attach_issue_url({:ok, result}), | ||
| do: {:ok, Map.put(result, "url", browse_url(result["key"]))} | ||
|
|
||
| defp attach_issue_url({:error, _reason} = err), do: err | ||
|
|
||
| defp browse_url(key), do: "#{config(:jira_base_url, nil)}/browse/#{key}" | ||
|
|
||
| defp find_transition(transitions, status_name) do | ||
| case Enum.find(transitions, &matches_status?(&1, status_name)) do | ||
| nil -> {:error, "Cannot transition to '#{status_name}'"} | ||
| target -> {:ok, target} | ||
| end | ||
| end | ||
|
|
||
| defp matches_status?(transition, status_name) do | ||
| to_name = get_in(transition, ["to", "name"]) |> to_string() | ||
| String.downcase(to_name) == String.downcase(status_name) | ||
| end | ||
|
|
||
| defp jira_get(client, path, params \\ []) do | ||
| Req.get(client, url: path, params: params) | ||
| |> handle_response() | ||
| end | ||
|
|
||
| defp jira_post(client, path, body) do | ||
| Req.post(client, url: path, json: body) | ||
| |> handle_response() | ||
| end | ||
|
|
||
| defp handle_response({:ok, %{status: status, body: body}}) when status in 200..299, | ||
| do: {:ok, body || %{}} | ||
|
|
||
| defp handle_response({:ok, %{status: status, body: body}}), | ||
| do: {:error, "HTTP #{status}: #{inspect(body)}"} | ||
|
|
||
| defp handle_response({:error, reason}), | ||
| do: {:error, inspect(reason)} | ||
|
|
||
| defp build_client do | ||
| email = config(:jira_email, System.get_env("JIRA_USER_EMAIL_ADDRESS", "")) | ||
| token = config(:jira_api_token, System.get_env("JIRA_API_TOKEN", "")) | ||
|
|
||
| Req.new( | ||
| base_url: config(:jira_base_url, nil), | ||
| auth: {:basic, "#{email}:#{token}"}, | ||
| headers: [{"accept", "application/json"}] | ||
| ) | ||
| end | ||
|
|
||
| defp config(key, default), do: Application.get_env(:zexbox, key, default) | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.