用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/oyi77/1ai-skills --skill jira命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
基于 SOC 职业分类
正在显示 SKILL.md
| name | jira |
| description | Skill: jira. See SKILL.md body for details. Use when this domain is relevant. |
| domain | operations |
| author | oyi77 |
| license | Apache-2.0 |
| subdomain | business-operations |
| tags | ["business-ops","jira","management","operations"] |
| version | 1.0.0 |
Atlassian Jira is the industry-standard issue tracking and project management platform. This skill covers the Jira REST API, JQL (Jira Query Language), workflow transitions, sprint/scrum automation, webhook integration, and operational patterns for teams running Jira Cloud or Server/Data Center.
/rest/api/3/issue)/rest/api/3/search)/rest/agile/1.0/)Trigger phrases:
Use cases:
Generate at https://id.atlassian.com/manage-profile/security/api-tokens. Format: ATATT3xFfGF0... (64+ chars).
curl -s -H "Authorization: Bearer YOUR_JIRA_TOKEN" \
"https://your-domain.atlassian.net/rest/api/3/myself"
Never commit tokens to source code. Use environment variables.
curl -s -H "Authorization: Bearer $JIRA_PAT" \
"https://jira.company.com/rest/api/2/myself"
Create PATs in Jira Server under user profile → Personal Access Tokens. Scoped to the creating user's permissions.
For applications acting on behalf of users (not service accounts):
client_id and client_secret with scopes (read:jira-work, write:jira-work, manage:jira-project)GET https://auth.atlassian.com/authorize?audience=api.atlassian.com&client_id=...&scope=read:jira-work%20write:jira-work&redirect_uri=...&response_type=codecurl -s -X POST "https://auth.atlassian.com/oauth/token" \
-H "Content-Type: application/json" \
-d '{"grant_type": "authorization_code", "client_id": "...", "client_secret": "...", "code": "AUTH_CODE", "redirect_uri": "https://..."}'
Authorization: Bearer $ACCESS_TOKEN against https://api.atlassian.com/ex/jira/{cloudId}/rest/api/3/...Discover cloud ID via GET https://your-domain.atlassian.net/_edge/tenant_info.
Basic auth with email:password was deprecated on July 1, 2023 for Cloud. Use API tokens instead. Still works for Server/Data Center.
All examples use Cloud endpoints (/rest/api/3/). Server/Data Center uses /rest/api/2/ (equivalent but older). Agile endpoints use /rest/agile/1.0/.
| Operation | Method | Endpoint |
|---|---|---|
| Create issue | POST | /rest/api/3/issue |
| Get issue | GET | /rest/api/3/issue/{issueIdOrKey} |
| Update issue | PUT | /rest/api/3/issue/{issueIdOrKey} |
| Delete issue | DELETE | /rest/api/3/issue/{issueIdOrKey} |
Create issue with ADF description (ADF = Atlassian Document Format, required for Cloud):
curl -s -X POST "https://your-domain.atlassian.net/rest/api/3/issue" \
-H "Content-Type: application/json" \
-u "user@example.com:$JIRA_TOKEN" \
-d '{
"fields": {
"project": {"key": "PROJ"},
"issuetype": {"name": "Bug"},
"summary": "Login fails on Safari 17",
"description": {
"type": "doc", "version": 1,
"content": [{"type": "paragraph", "content": [{"type": "text", "text": "Steps to reproduce"}]}]
},
"priority": {"name": "High"},
"labels": ["frontend"],
"assignee": {"accountId": "712020:abc123"},
"duedate": "2026-08-15"
}
}'
Update issue (partial):
curl -s -X PUT "https://your-domain.atlassian.net/rest/api/3/issue/PROJ-123" \
-H "Content-Type: application/json" \
-u "user@example.com:$JIRA_TOKEN" \
-d '{"fields": {"summary": "Updated title", "priority": {"name": "Critical"}}}'
Transition IDs are numeric and instance-specific. Always fetch dynamically:
# Get available transitions
curl -s "https://your-domain.atlassian.net/rest/api/3/issue/PROJ-123/transitions" \
-u "user@example.com:$JIRA_TOKEN" \
| jq '.transitions[] | {id, name, toStatus: .to.name}'
# Transition to "Done" with resolution
curl -s -X POST "https://your-domain.atlassian.net/rest/api/3/issue/PROJ-123/transitions" \
-H "Content-Type: application/json" \
-u "user@example.com:$JIRA_TOKEN" \
-d '{"transition": {"id": "31"}, "fields": {"resolution": {"name": "Done"}}}'
curl -s -X POST "https://your-domain.atlassian.net/rest/api/3/search" \
-H "Content-Type: application/json" \
-u "user@example.com:$JIRA_TOKEN" \
-d '{
"jql": "project = PROJ AND status = \"In Progress\" AND assignee = currentUser()",
"startAt": 0, "maxResults": 50,
"fields": ["summary", "status", "assignee", "priority"]
}'
# List projects
curl -s "https://your-domain.atlassian.net/rest/api/3/project" -u "user@example.com:$JIRA_TOKEN"
# Get board configuration
curl -s "https://your-domain.atlassian.net/rest/agile/1.0/board/42/configuration" -u "user@example.com:$JIRA_TOKEN"
# List sprints for board
curl -s "https://your-domain.atlassian.net/rest/agile/1.0/board/42/sprint?state=active,future" \
-u "user@example.com:$JIRA_TOKEN"
# Create sprint
curl -s -X POST "https://your-domain.atlassian.net/rest/agile/1.0/sprint" \
-H "Content-Type: application/json" \
-u "user@example.com:$JIRA_TOKEN" \
-d '{"name": "Sprint 42", "originBoardId": 42, "goal": "Auth migration", "startDate": "2026-08-01T09:00:00.000Z", "endDate": "2026-08-14T17:00:00.000Z"}'
# Move issues to sprint
curl -s -X POST "https://your-domain.atlassian.net/rest/agile/1.0/sprint/123/issue" \
-H "Content-Type: application/json" \
-u "user@example.com:$JIRA_TOKEN" \
-d '{"issues": ["PROJ-123", "PROJ-124"]}'
Epics are issue type "Epic" linked via a custom field (customfield_*).
# Discover Epic Link field ID
curl -s "https://your-domain.atlassian.net/rest/api/3/field" \
-u "user@example.com:$JIRA_TOKEN" \
| jq '.[] | select(.name | test("Epic Link"; "i")) | .id'
# Link issue to epic
curl -s -X PUT "https://your-domain.atlassian.net/rest/api/3/issue/PROJ-124" \
-H "Content-Type: application/json" \
-u "user@example.com:$JIRA_TOKEN" \
-d '{"fields": {"customfield_10014": "EPIC-123"}}'
# Get issues in an epic
curl -s "https://your-domain.atlassian.net/rest/agile/1.0/board/42/epic/EPIC-123/issue" \
-u "user@example.com:$JIRA_TOKEN"
# Discover all custom fields
curl -s "https://your-domain.atlassian.net/rest/api/3/field" \
-u "user@example.com:$JIRA_TOKEN" \
| jq '.[] | select(.custom == true) | {id, name, schema_type: .schema.type}'
# Write custom fields (types vary)
curl -s -X PUT "https://your-domain.atlassian.net/rest/api/3/issue/PROJ-123" \
-H "Content-Type: application/json" \
-u "user@example.com:$JIRA_TOKEN" \
-d '{"fields": {
"customfield_10010": {"value": "Frontend"}, # select
"customfield_10020": "2026-08-01", # date
"customfield_10030": 42 # number
}}'
Custom field value formats: Select → {"value": "..."}, Multi-select → [{"value": "A"}, {"value": "B"}], User → {"accountId": "..."}, Labels → ["urgent"].
curl -s -X POST "https://your-domain.atlassian.net/rest/api/3/issueLink" \
-H "Content-Type: application/json" \
-u "user@example.com:$JIRA_TOKEN" \
-d '{"inwardIssue": {"key": "PROJ-123"}, "outwardIssue": {"key": "PROJ-456"}, "type": {"name": "Relates"}}'
Link types: Relates, Blocks/is blocked by, Clones/is cloned by, Duplicate/is duplicated by.
import os, requests
from requests.auth import HTTPBasicAuth
JIRA_URL = "https://your-domain.atlassian.net"
AUTH = HTTPBasicAuth(os.environ["JIRA_EMAIL"], os.environ["JIRA_TOKEN"])
HEADERS = {"Accept": "application/json", "Content-Type": "application/json"}
def create_issue(project, summary, issue_type="Task", priority=None):
fields = {"project": {"key": project}, "issuetype": {"name": issue_type}, "summary": summary}
if priority:
fields["priority"] = {"name": priority}
resp = requests.post(f"{JIRA_URL}/rest/api/3/issue", json={"fields": fields}, auth=AUTH, headers=HEADERS)
resp.raise_for_status()
return resp.json() # {"id": "10001", "key": "PROJ-124"}
def search_jql(jql, fields=None, max_results=50):
payload = {"jql": jql, "maxResults": max_results}
if fields:
payload["fields"] = fields
resp = requests.post(f"{JIRA_URL}/rest/api/3/search", json=payload, auth=AUTH, headers=HEADERS)
resp.raise_for_status()
return resp.json()["issues"]
def transition_issue(issue_key, transition_id, resolution=):
payload = {: {: (transition_id)}}
resolution:
payload[] = {: {: resolution}}
resp = requests.post(,
json=payload, auth=AUTH, headers=HEADERS)
resp.raise_for_status()
():
all_issues, start_at = [],
:
payload = {: jql, : start_at, : batch_size}
fields: payload[] = fields
data = requests.post(, json=payload, auth=AUTH, headers=HEADERS).json()
all_issues.extend(data[])
start_at + batch_size >= data[]:
start_at += batch_size
all_issues
issue = create_issue(, , , )
issues = search_jql()
transition_issue(, )
from jira import JIRA
jira = JIRA(server="https://your-domain.atlassian.net",
basic_auth=(os.environ["JIRA_EMAIL"], os.environ["JIRA_TOKEN"]))
issue = jira.create_issue(project="PROJ", summary="High CPU",
issuetype={"name": "Bug"}, priority={"name": "Critical"})
issues = jira.search_issues('project = PROJ AND status in ("To Do", "In Progress")', maxResults=50)
jira.transition_issue(issue, "21")
jira.add_comment(issue.key, "Deploying fix.")
const axios = require('axios');
const BASE = 'https://your-domain.atlassian.net';
const AUTH = Buffer.from(`${process.env.JIRA_EMAIL}:${process.env.JIRA_TOKEN}`).toString('base64');
const headers = {'Authorization': `Basic ${AUTH}`, 'Accept': 'application/json', 'Content-Type': 'application/json'};
// Create issue
const resp = await axios.post(`${BASE}/rest/api/3/issue`, {
fields: { project: { key: 'PROJ' }, issuetype: { name: 'Task' }, summary: 'Add rate limiting' }
}, { headers });
// Search
const search = await axios.post(`${BASE}/rest/api/3/search`, {
jql: 'project = PROJ AND created >= -7d ORDER BY priority DESC', maxResults: 25
}, { headers });
for (const issue of search.data.) .();
#!/usr/bin/env bash
set -euo pipefail
JIRA_URL="https://your-domain.atlassian.net"
AUTH_ARGS=(-u "$JIRA_EMAIL:$JIRA_TOKEN")
JSON_HEADER=(-H "Content-Type: application/json")
create_issue() {
curl -s -X POST "${JIRA_URL}/rest/api/3/issue" "${AUTH_ARGS[@]}" "${JSON_HEADER[@]}" \
-d "{\"fields\": {\"project\": {\"key\": \"$1\"}, \"issuetype\": {\"name\": \"$3\"}, \"summary\": \"$2\", \"priority\": {\"name\": \"$4\"}}}" | jq -r '.key'
}
search_jql() {
curl -s -X POST "${JIRA_URL}/rest/api/3/search" "${AUTH_ARGS[@]}" "${JSON_HEADER[@]}" \
-d "{\"jql\": \"$(echo "$1" | sed 's/"/\\"/g')\", \"maxResults\": ${2:-50}}" | jq -r '.issues[].key'
}
project = PROJ
issuetype = Bug
status = "In Progress"
priority in (Highest, High)
assignee = currentUser()
reporter = "john@example.com"
-- Relative
created >= -7d -- last 7 days
updated >= -24h -- last 24 hours
due < now() -- overdue
due = startOfDay() -- due today
duedate >= "2026-08-01" AND duedate <= "2026-08-31"
-- Date functions
created >= startOfWeek()
updated >= startOfMonth(-1)
resolutiondate >= "-30d"
status in ("To Do", "In Progress", "In Review")
status changed AFTER "2026-08-01"
status WAS "In Progress" BEFORE "2026-08-15"
resolution = Unresolved
resolution = Empty -- unresolved
status CHANGED FROM "In Progress" TO "Done" DURING (startOfDay(), now())
assignee = currentUser()
assignee in (membersOf("jira-software-users"))
assignee is EMPTY
assignee is not EMPTY
assignee != currentUser()
reporter = currentUser()
watcher = currentUser()
-- Epic Link (ID varies)
"Epic Link" = EPIC-123
cf[10010] = "Frontend"
"Story Points" = 5
"Story Points" > 3 AND "Story Points" <= 8
labels in ("frontend", "auth")
labels is EMPTY
summary ~ "login"
description ~ "crash"
votedIssues()
watchedIssues()
linkedIssues(PROJ-123)
issuesInEpics(EPIC-123)
-- Cloud only:
issueFunction in commented("by bob@example.com AFTER startOfWeek()")
issueFunction in issuesInEpics("project = EPIC AND status != Done")
-- Unresolved bugs older than 30 days
project = PROJ AND issuetype = Bug AND resolution = Unresolved AND created < -30d ORDER BY created ASC
-- My work in active sprint
assignee = currentUser() AND sprint in openSprints() ORDER BY priority DESC, duedate ASC
-- Sprint planning candidates
project = PROJ AND status = "To Do" AND "Story Points" is not EMPTY ORDER BY priority DESC
-- Stale issues (not updated in 2 weeks)
project = PROJ AND updated < -14d AND status != Done
-- Cross-project blocker bugs
resolution = Unresolved AND issuetype = Bug AND priority = Highest ORDER BY created ASC
-- Velocity helper
project = PROJ AND issuetype in (Story, Bug) AND status = Done AND resolutiondate >= startOfYear()
JQL results default to maxResults=50, maximum 100 per request. For large datasets:
# Traditional offset pagination
def paginate_jql(jql, batch_size=100):
all_issues, start_at = [], 0
while True:
data = requests.post(f"{JIRA_URL}/rest/api/3/search",
json={"jql": jql, "startAt": start_at, "maxResults": batch_size},
auth=AUTH, headers=HEADERS).json()
all_issues.extend(data["issues"])
if start_at + batch_size >= data["total"]:
break
start_at += batch_size
return all_issues
Jira Cloud also supports cursor-based pagination via /rest/api/3/search/jql with nextPageToken in the response. Prefer this for result sets over 1K issues.
curl -s -X POST "https://your-domain.atlassian.net/rest/api/3/webhook" \
-H "Content-Type: application/json" \
-u "user@example.com:$JIRA_TOKEN" \
-d '{
"url": "https://your-server.com/jira-webhook",
"events": ["jira:issue_created", "jira:issue_updated", "jira:issue_deleted"],
"filters": {"issue-related-events-section": "project = PROJ"}
}'
Events: jira:issue_created, jira:issue_updated, jira:issue_deleted, jira:worklog_updated, comment_created, comment_updated, sprint_*, board_*, project_*.
{
"timestamp": 1771234567890,
"webhookEvent": "jira:issue_updated",
"user": {"accountId": "712020:abc123", "displayName": "John Doe"},
"issue": {
"id": "10001", "key": "PROJ-123",
"fields": {"summary": "Fix login bug", "status": {"name": "In Progress"}}
},
"changelog": {
"items": [{"field": "status"
from flask import Flask, request
import hmac, hashlib, os
app = Flask(__name__)
SECRET = os.environ.get("JIRA_WEBHOOK_SECRET")
@app.route("/jira-webhook", methods=["POST"])
def handle_webhook():
if SECRET:
sig = request.headers.get("X-Hub-Signature", "")
expected = "sha256=" + hmac.new(SECRET.encode(), request.data, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected): return ("", 403)
event = request.json
event_type = event.get("webhookEvent")
issue = event.get("issue", {})
changelog = event.get("changelog", {})
if event_type == "jira:issue_created":
handle_created(issue["key"], issue["fields"])
elif event_type == "jira:issue_updated":
for item in changelog.get("items", []):
if item["field"] == "status":
handle_transition(issue["key"], item["fromString"], item["toString"])
return ("ok", 200)
@app.route("/jira-webhook", methods=["GET"])
def verify(): (, )
Jira Automation supports incoming webhooks that trigger rules. Create an Automation rule with trigger "Incoming webhook", then call the generated URL:
curl -s -X POST "https://automation.atlassian.com/pro/hooks/YOUR_WEBHOOK_ID" \
-H "Content-Type: application/json" \
-d '{"issueKey": "PROJ-123", "message": "Deploy completed"}'
import datetime
def create_sprint(board_id, name, start, end, goal=""):
return requests.post(f"{JIRA_URL}/rest/agile/1.0/sprint", auth=AUTH, headers=HEADERS, json={
"name": name, "originBoardId": board_id, "goal": goal,
"startDate": start.isoformat(), "endDate": end.isoformat(),
}).json()
def start_sprint(sprint_id):
requests.post(f"{JIRA_URL}/rest/agile/1.0/sprint/{sprint_id}", json={"state": "active"}, auth=AUTH, headers=HEADERS)
def close_sprint(sprint_id):
requests.post(f"{JIRA_URL}/rest/agile/1.0/sprint/{sprint_id}", json={"state": "closed"}, auth=AUTH, headers=HEADERS)
def get_active_sprint(board_id):
resp = requests.get(f"{JIRA_URL}/rest/agile/1.0/board/{board_id}/sprint?state=active", auth=AUTH, headers=HEADERS)
values = resp.json().get("values", [])
return values[0] if values else None
def move_to_sprint(sprint_id, issue_keys):
requests.post(f"{JIRA_URL}/rest/agile/1.0/sprint/{sprint_id}/issue",
json={: issue_keys}, auth=AUTH, headers=HEADERS)
board_id =
sprint_name =
get_active_sprint(board_id):
sprint = create_sprint(board_id, sprint_name, datetime.date.today(),
datetime.date.today() + datetime.timedelta(days=))
move_to_sprint(sprint[], [])
start_sprint(sprint[])
def sprint_burndown(sprint_id):
issues = []
for start in range(0, 99999, 100):
data = requests.get(f"{JIRA_URL}/rest/agile/1.0/sprint/{sprint_id}/issue",
params={"startAt": start, "maxResults": 100}, auth=AUTH, headers=HEADERS).json()
issues.extend(data["issues"])
if start + 100 >= data["total"]: break
total = sum(i["fields"].get("customfield_10016", 0) or 0 for i in issues)
done = sum(i["fields"].get("customfield_10016", 0) or 0 for i in issues if i["fields"]["status"]["name"] == "Done")
return {"total": total, "completed": done, "remaining": total - done}
def create_version(project_key, name, description, release_date):
return requests.post(f"{JIRA_URL}/rest/api/3/version", auth=AUTH, headers=HEADERS, json={
"project": project_key, "name": name, "description": description,
"releaseDate": release_date.isoformat(),
}).json()
def release_version(version_id):
requests.put(f"{JIRA_URL}/rest/api/3/version/{version_id}",
json={"released": True, "releaseDate": datetime.date.today().isoformat()},
auth=AUTH, headers=HEADERS)
def link_issues(inward, outward, link_type="Relates"):
requests.post(f"{JIRA_URL}/rest/api/3/issueLink", auth=AUTH, headers=HEADERS, json={
"inwardIssue": {"key": inward}, "outwardIssue": {"key": outward}, "type": {"name": link_type}})
TEMPO_URL = "https://api.tempo.io/4"
def log_work(issue_key, account_id, description, seconds, date=None):
requests.post(f"{TEMPO_URL}/worklogs", headers={"Authorization": f"Bearer {os.environ['TEMPO_TOKEN']}"},
json={"issueKey": issue_key, "authorAccountId": {"accountId": account_id},
"description": description, "timeSpentSeconds": seconds,
"startDate": date or datetime.date.today().isoformat(), "startTime": "09:00:00"})
| Issue | Impact | Mitigation |
|---|---|---|
| Rate limiting (Cloud) | 6-8 req/s per instance; 403 on burst | Exponential backoff, batch requests, use webhooks instead of polling |
| Rate limiting (Server) | Configurable low defaults | Check with admin; cache responses with ETag/If-None-Match |
| maxResults=100 ceiling | Full export of 5K issues needs 50 API calls | Use cursor pagination (/search/jql), iterate with startAt |
| JQL injection | Concatenated user input can bypass auth or delete data | Parameterize JQL; validate input; never trust user-provided JQL fragments |
| 2FA kills basic auth | Basic auth + 2FA = immediate 403 | Use API tokens (Cloud) or PATs (Server) — they bypass 2FA |
| Transition ID drift | IDs change after workflow edits | Always fetch /transitions dynamically; never hardcode |
| Custom field ID drift | IDs differ per instance | Discover via /rest/api/3/field; cache; use name aliases where possible |
| ADF requirement (Cloud) | Plain text descriptions rejected | Always use ADF for Cloud: {"type":"doc","version":1,"content":[...]} |
| Server vs Cloud drift | /api/3 vs /api/2 have subtle differences | Test against both deployment types; Server lacks many Cloud features |
| Large export timeout | 10K+ issues can timeout | Use /search/export (Data Center) or schedule CSV exports (Cloud) |
| Webhook delivery failures | Jira retries 24h, then drops | Monitor webhook health; implement idempotent handlers with dedup keys |
import time
def jira_request(method, url, **kwargs):
for attempt in range(5):
resp = requests.request(method, url, auth=AUTH, headers=HEADERS, **kwargs)
if resp.status_code == 429:
time.sleep(int(resp.headers.get("Retry-After", 2 ** attempt)))
continue
resp.raise_for_status()
return resp.json()
raise RuntimeError("Rate limit retries exhausted")
/transitions, not hardcoded/rest/api/3/fieldtotal > maxResults with proper iterationread:jira-work, write:jira-work)/api/3 vs /api/2)./project), custom field IDs (/field), transition IDs, board/sprint IDs. Cache these — they change less than auth but more than code.| Rationalization | Reality |
|---|---|
| "Hardcoding transition IDs is faster" | IDs change when workflows are edited. Fetch dynamically or automation breaks silently. |
| "Basic auth works fine for my script" | Cloud deprecated basic auth in 2023. Use API tokens now. |
| "My webhook handler returns 200 immediately" | You must process the event before returning 200. Jira retries for 24h on errors. |
| "JQL is like SQL — I can concatenate user input" | summary ~ " OR 1=1 --" bypasses access controls. Validate or parameterize everything. |
| "100 maxResults is enough" | 5K issues = 50 calls. Use cursor pagination for Cloud; offset pagination for Server. |
| "Jira can replace our entire workflow" | Jira excels at structured tracking but is poor at CI/CD, doc review, or HR. Use purpose-built tools and integrate. |
| "Marketplace add-ons are always stable" | Tempo, ScriptRunner, Structure have their own API versions, rate limits, and failure modes. |
| "Cloud and Server have the same API" | Endpoints, field formats, and features differ significantly. Test against your deployment type. |
| "Jira Automation handles everything" | Complex cross-project workflows still need custom scripts. Jira Automation has a 100-rule limit on Cloud. |