Skip to content

Commit 2f9f37c

Browse files
anirudh5harmaclaude
andcommitted
feat(tracker/jira): REST adapter for issue intake
Implements ports.Tracker against Jira Cloud REST. Scope is the project key plus the tenant site base URL (carried per-call via TrackerRepo.BaseURL so one adapter instance can serve multiple Jira tenants under one account). List builds a JQL query (project + statusCategory + labels + assignee ORDER BY created DESC) and calls /rest/api/3/search. Quoted scalars are escaped so a `"` in a label can't break out of the JQL string. Status-category mapping: - new / unknown -> open - indeterminate -> in_progress - done -> done, or cancelled when the status name is "Cancelled" / "Won't Do" Description bodies use Jira's Atlassian Document Format; the adapter flattens text leaves into plaintext so the intake prompt stays readable without a heavyweight ADF dependency. Credentials come from AO_JIRA_EMAIL + AO_JIRA_TOKEN as Basic auth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent ca5c493 commit 2f9f37c

3 files changed

Lines changed: 664 additions & 0 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package jira
2+
3+
import (
4+
"context"
5+
"errors"
6+
"os"
7+
"strings"
8+
)
9+
10+
// CredentialSource yields Jira Cloud Basic-auth credentials (email + API
11+
// token). Jira's REST API does not accept bearer tokens for cloud accounts;
12+
// every request is `Authorization: Basic base64(email:token)`.
13+
type CredentialSource interface {
14+
Credentials(ctx context.Context) (email, token string, err error)
15+
}
16+
17+
// ErrNoCredentials is returned when no email/token pair can be obtained.
18+
var ErrNoCredentials = errors.New("jira tracker: no credentials configured")
19+
20+
// StaticCredentials are literal values, typically used in tests.
21+
type StaticCredentials struct {
22+
Email string
23+
Token string
24+
}
25+
26+
// Credentials returns the literal pair, or ErrNoCredentials if either is blank.
27+
func (s StaticCredentials) Credentials(context.Context) (string, string, error) {
28+
email := strings.TrimSpace(s.Email)
29+
token := strings.TrimSpace(s.Token)
30+
if email == "" || token == "" {
31+
return "", "", ErrNoCredentials
32+
}
33+
return email, token, nil
34+
}
35+
36+
// EnvCredentials reads Jira email/token pairs from env vars. Email and Token
37+
// each accept a list of var names to support overlay scopes (project-specific
38+
// vars beat the global default).
39+
type EnvCredentials struct {
40+
EmailVars []string
41+
TokenVars []string
42+
}
43+
44+
// Credentials returns the first non-empty value from each var list, with
45+
// AO_JIRA_EMAIL / AO_JIRA_TOKEN as defaults.
46+
func (s EnvCredentials) Credentials(context.Context) (string, string, error) {
47+
email := firstNonEmpty(s.EmailVars, "AO_JIRA_EMAIL")
48+
token := firstNonEmpty(s.TokenVars, "AO_JIRA_TOKEN")
49+
if email == "" || token == "" {
50+
return "", "", ErrNoCredentials
51+
}
52+
return email, token, nil
53+
}
54+
55+
func firstNonEmpty(names []string, fallback string) string {
56+
for _, name := range names {
57+
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
58+
return v
59+
}
60+
}
61+
return strings.TrimSpace(os.Getenv(fallback))
62+
}

0 commit comments

Comments
 (0)