jira-project-management

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Jira Project Management Skill

Jira项目管理技能

Purpose

用途

Comprehensive project administration including CRUD operations, components, versions, roles, permissions, and configuration.
全面的项目管理,包括CRUD操作、组件、版本、角色、权限和配置。

When to Use

适用场景

  • Creating/updating/deleting/archiving projects
  • Managing project components (modules, teams)
  • Managing versions/releases
  • Configuring project roles and permissions
  • Setting project properties and metadata
  • Validating project keys and names
  • 创建/更新/删除/归档项目
  • 管理项目组件(模块、团队)
  • 管理版本/发布
  • 配置项目角色和权限
  • 设置项目属性和元数据
  • 验证项目密钥和名称

Prerequisites

前置条件

  • Authenticated JiraClient (see jira-auth skill)
  • Jira admin or project admin permissions
  • Project key format: 2-10 uppercase letters
  • 已认证的JiraClient(参考jira-auth技能)
  • Jira管理员或项目管理员权限
  • 项目密钥格式:2-10个大写字母

Implementation Pattern

实现模式

Step 1: Define Types

Step 1: Define Types

typescript
interface Project {
  id: string;
  key: string;
  name: string;
  self: string;
  projectTypeKey: 'software' | 'service_desk' | 'business';
  simplified: boolean;
  style: 'classic' | 'next-gen';
  isPrivate: boolean;
  lead: {
    accountId: string;
    displayName: string;
  };
  description?: string;
  url?: string;
  avatarUrls: Record<string, string>;
  projectCategory?: {
    id: string;
    name: string;
  };
}

interface Component {
  id: string;
  name: string;
  description?: string;
  lead?: { accountId: string; displayName: string };
  assigneeType: 'PROJECT_DEFAULT' | 'COMPONENT_LEAD' | 'PROJECT_LEAD' | 'UNASSIGNED';
  project: string;
  projectId: number;
}

interface Version {
  id: string;
  name: string;
  description?: string;
  archived: boolean;
  released: boolean;
  startDate?: string;
  releaseDate?: string;
  projectId: number;
  overdue?: boolean;
}

interface ProjectRole {
  id: number;
  name: string;
  description: string;
  actors: Array<{
    id: number;
    displayName: string;
    type: 'atlassian-user-role-actor' | 'atlassian-group-role-actor';
    actorUser?: { accountId: string };
    actorGroup?: { name: string; displayName: string };
  }>;
}
typescript
interface Project {
  id: string;
  key: string;
  name: string;
  self: string;
  projectTypeKey: 'software' | 'service_desk' | 'business';
  simplified: boolean;
  style: 'classic' | 'next-gen';
  isPrivate: boolean;
  lead: {
    accountId: string;
    displayName: string;
  };
  description?: string;
  url?: string;
  avatarUrls: Record<string, string>;
  projectCategory?: {
    id: string;
    name: string;
  };
}

interface Component {
  id: string;
  name: string;
  description?: string;
  lead?: { accountId: string; displayName: string };
  assigneeType: 'PROJECT_DEFAULT' | 'COMPONENT_LEAD' | 'PROJECT_LEAD' | 'UNASSIGNED';
  project: string;
  projectId: number;
}

interface Version {
  id: string;
  name: string;
  description?: string;
  archived: boolean;
  released: boolean;
  startDate?: string;
  releaseDate?: string;
  projectId: number;
  overdue?: boolean;
}

interface ProjectRole {
  id: number;
  name: string;
  description: string;
  actors: Array<{
    id: number;
    displayName: string;
    type: 'atlassian-user-role-actor' | 'atlassian-group-role-actor';
    actorUser?: { accountId: string };
    actorGroup?: { name: string; displayName: string };
  }>;
}

Step 2: Project CRUD Operations

Step 2: Project CRUD Operations

typescript
// Create Project
interface CreateProjectInput {
  key: string;                    // 2-10 uppercase letters
  name: string;
  projectTypeKey: 'software' | 'service_desk' | 'business';
  leadAccountId: string;
  description?: string;
  assigneeType?: 'PROJECT_LEAD' | 'UNASSIGNED';
  categoryId?: number;
}

async function createProject(
  client: JiraClient,
  input: CreateProjectInput
): Promise<Project> {
  return client.request<Project>('/project', {
    method: 'POST',
    body: JSON.stringify({
      key: input.key,
      name: input.name,
      projectTypeKey: input.projectTypeKey,
      leadAccountId: input.leadAccountId,
      description: input.description,
      assigneeType: input.assigneeType || 'UNASSIGNED',
      categoryId: input.categoryId,
    }),
  });
}

// Update Project
async function updateProject(
  client: JiraClient,
  projectKeyOrId: string,
  updates: Partial<{
    key: string;
    name: string;
    description: string;
    leadAccountId: string;
    assigneeType: string;
    categoryId: number;
  }>
): Promise<Project> {
  return client.request<Project>(`/project/${projectKeyOrId}`, {
    method: 'PUT',
    body: JSON.stringify(updates),
  });
}

// Delete Project (moves to trash, recoverable for 60 days)
async function deleteProject(
  client: JiraClient,
  projectKeyOrId: string,
  enableUndo: boolean = true
): Promise<void> {
  await client.request(`/project/${projectKeyOrId}?enableUndo=${enableUndo}`, {
    method: 'DELETE',
  });
}

// Archive Project
async function archiveProject(
  client: JiraClient,
  projectKeyOrId: string
): Promise<void> {
  await client.request(`/project/${projectKeyOrId}/archive`, {
    method: 'POST',
  });
}

// Restore Project
async function restoreProject(
  client: JiraClient,
  projectKeyOrId: string
): Promise<Project> {
  return client.request<Project>(`/project/${projectKeyOrId}/restore`, {
    method: 'POST',
  });
}
typescript
// Create Project
interface CreateProjectInput {
  key: string;                    // 2-10 uppercase letters
  name: string;
  projectTypeKey: 'software' | 'service_desk' | 'business';
  leadAccountId: string;
  description?: string;
  assigneeType?: 'PROJECT_LEAD' | 'UNASSIGNED';
  categoryId?: number;
}

async function createProject(
  client: JiraClient,
  input: CreateProjectInput
): Promise<Project> {
  return client.request<Project>('/project', {
    method: 'POST',
    body: JSON.stringify({
      key: input.key,
      name: input.name,
      projectTypeKey: input.projectTypeKey,
      leadAccountId: input.leadAccountId,
      description: input.description,
      assigneeType: input.assigneeType || 'UNASSIGNED',
      categoryId: input.categoryId,
    }),
  });
}

// Update Project
async function updateProject(
  client: JiraClient,
  projectKeyOrId: string,
  updates: Partial<{
    key: string;
    name: string;
    description: string;
    leadAccountId: string;
    assigneeType: string;
    categoryId: number;
  }>
): Promise<Project> {
  return client.request<Project>(`/project/${projectKeyOrId}`, {
    method: 'PUT',
    body: JSON.stringify(updates),
  });
}

// Delete Project (moves to trash, recoverable for 60 days)
async function deleteProject(
  client: JiraClient,
  projectKeyOrId: string,
  enableUndo: boolean = true
): Promise<void> {
  await client.request(`/project/${projectKeyOrId}?enableUndo=${enableUndo}`, {
    method: 'DELETE',
  });
}

// Archive Project
async function archiveProject(
  client: JiraClient,
  projectKeyOrId: string
): Promise<void> {
  await client.request(`/project/${projectKeyOrId}/archive`, {
    method: 'POST',
  });
}

// Restore Project
async function restoreProject(
  client: JiraClient,
  projectKeyOrId: string
): Promise<Project> {
  return client.request<Project>(`/project/${projectKeyOrId}/restore`, {
    method: 'POST',
  });
}

Step 3: List and Search Projects

Step 3: List and Search Projects

typescript
interface ProjectSearchOptions {
  startAt?: number;
  maxResults?: number;
  orderBy?: 'category' | '-category' | 'key' | '-key' | 'name' | '-name' | 'owner' | '-owner';
  query?: string;           // Search in name/key
  typeKey?: string;         // software, service_desk, business
  categoryId?: number;
  expand?: ('description' | 'lead' | 'issueTypes' | 'url' | 'projectKeys' | 'permissions' | 'insight')[];
}

async function searchProjects(
  client: JiraClient,
  options: ProjectSearchOptions = {}
): Promise<{ values: Project[]; total: number; isLast: boolean }> {
  const params = new URLSearchParams();
  if (options.startAt) params.set('startAt', String(options.startAt));
  if (options.maxResults) params.set('maxResults', String(options.maxResults));
  if (options.orderBy) params.set('orderBy', options.orderBy);
  if (options.query) params.set('query', options.query);
  if (options.typeKey) params.set('typeKey', options.typeKey);
  if (options.categoryId) params.set('categoryId', String(options.categoryId));
  if (options.expand) params.set('expand', options.expand.join(','));

  return client.request(`/project/search?${params.toString()}`);
}

// Get recent projects
async function getRecentProjects(
  client: JiraClient,
  maxResults: number = 20
): Promise<Project[]> {
  const params = new URLSearchParams();
  params.set('maxResults', String(maxResults));
  params.set('expand', 'description,lead');
  return client.request(`/project/recent?${params.toString()}`);
}
typescript
interface ProjectSearchOptions {
  startAt?: number;
  maxResults?: number;
  orderBy?: 'category' | '-category' | 'key' | '-key' | 'name' | '-name' | 'owner' | '-owner';
  query?: string;           // Search in name/key
  typeKey?: string;         // software, service_desk, business
  categoryId?: number;
  expand?: ('description' | 'lead' | 'issueTypes' | 'url' | 'projectKeys' | 'permissions' | 'insight')[];
}

async function searchProjects(
  client: JiraClient,
  options: ProjectSearchOptions = {}
): Promise<{ values: Project[]; total: number; isLast: boolean }> {
  const params = new URLSearchParams();
  if (options.startAt) params.set('startAt', String(options.startAt));
  if (options.maxResults) params.set('maxResults', String(options.maxResults));
  if (options.orderBy) params.set('orderBy', options.orderBy);
  if (options.query) params.set('query', options.query);
  if (options.typeKey) params.set('typeKey', options.typeKey);
  if (options.categoryId) params.set('categoryId', String(options.categoryId));
  if (options.expand) params.set('expand', options.expand.join(','));

  return client.request(`/project/search?${params.toString()}`);
}

// Get recent projects
async function getRecentProjects(
  client: JiraClient,
  maxResults: number = 20
): Promise<Project[]> {
  const params = new URLSearchParams();
  params.set('maxResults', String(maxResults));
  params.set('expand', 'description,lead');
  return client.request(`/project/recent?${params.toString()}`);
}

Step 4: Component Management

Step 4: Component Management

typescript
// List Components
async function getProjectComponents(
  client: JiraClient,
  projectKeyOrId: string
): Promise<Component[]> {
  return client.request(`/project/${projectKeyOrId}/components`);
}

// Create Component
interface CreateComponentInput {
  project: string;        // Project key
  name: string;
  description?: string;
  leadAccountId?: string;
  assigneeType?: 'PROJECT_DEFAULT' | 'COMPONENT_LEAD' | 'PROJECT_LEAD' | 'UNASSIGNED';
}

async function createComponent(
  client: JiraClient,
  input: CreateComponentInput
): Promise<Component> {
  return client.request<Component>('/component', {
    method: 'POST',
    body: JSON.stringify({
      project: input.project,
      name: input.name,
      description: input.description,
      leadAccountId: input.leadAccountId,
      assigneeType: input.assigneeType || 'PROJECT_DEFAULT',
    }),
  });
}

// Update Component
async function updateComponent(
  client: JiraClient,
  componentId: string,
  updates: Partial<{
    name: string;
    description: string;
    leadAccountId: string;
    assigneeType: string;
  }>
): Promise<Component> {
  return client.request<Component>(`/component/${componentId}`, {
    method: 'PUT',
    body: JSON.stringify(updates),
  });
}

// Delete Component
async function deleteComponent(
  client: JiraClient,
  componentId: string,
  moveIssuesTo?: string  // Component ID to move issues to
): Promise<void> {
  const query = moveIssuesTo ? `?moveIssuesTo=${moveIssuesTo}` : '';
  await client.request(`/component/${componentId}${query}`, {
    method: 'DELETE',
  });
}

// Get Component Issue Counts
async function getComponentIssueCounts(
  client: JiraClient,
  componentId: string
): Promise<{ issueCount: number }> {
  return client.request(`/component/${componentId}/relatedIssueCounts`);
}
typescript
// List Components
async function getProjectComponents(
  client: JiraClient,
  projectKeyOrId: string
): Promise<Component[]> {
  return client.request(`/project/${projectKeyOrId}/components`);
}

// Create Component
interface CreateComponentInput {
  project: string;        // Project key
  name: string;
  description?: string;
  leadAccountId?: string;
  assigneeType?: 'PROJECT_DEFAULT' | 'COMPONENT_LEAD' | 'PROJECT_LEAD' | 'UNASSIGNED';
}

async function createComponent(
  client: JiraClient,
  input: CreateComponentInput
): Promise<Component> {
  return client.request<Component>('/component', {
    method: 'POST',
    body: JSON.stringify({
      project: input.project,
      name: input.name,
      description: input.description,
      leadAccountId: input.leadAccountId,
      assigneeType: input.assigneeType || 'PROJECT_DEFAULT',
    }),
  });
}

// Update Component
async function updateComponent(
  client: JiraClient,
  componentId: string,
  updates: Partial<{
    name: string;
    description: string;
    leadAccountId: string;
    assigneeType: string;
  }>
): Promise<Component> {
  return client.request<Component>(`/component/${componentId}`, {
    method: 'PUT',
    body: JSON.stringify(updates),
  });
}

// Delete Component
async function deleteComponent(
  client: JiraClient,
  componentId: string,
  moveIssuesTo?: string  // Component ID to move issues to
): Promise<void> {
  const query = moveIssuesTo ? `?moveIssuesTo=${moveIssuesTo}` : '';
  await client.request(`/component/${componentId}${query}`, {
    method: 'DELETE',
  });
}

// Get Component Issue Counts
async function getComponentIssueCounts(
  client: JiraClient,
  componentId: string
): Promise<{ issueCount: number }> {
  return client.request(`/component/${componentId}/relatedIssueCounts`);
}

Step 5: Version/Release Management

Step 5: Version/Release Management

typescript
// List Versions
async function getProjectVersions(
  client: JiraClient,
  projectKeyOrId: string,
  options: {
    startAt?: number;
    maxResults?: number;
    orderBy?: 'description' | '-description' | 'name' | '-name' | 'releaseDate' | '-releaseDate' | 'sequence' | '-sequence' | 'startDate' | '-startDate';
    status?: 'released' | 'unreleased' | 'archived';
    expand?: string;
  } = {}
): Promise<{ values: Version[]; total: number; isLast: boolean }> {
  const params = new URLSearchParams();
  if (options.startAt) params.set('startAt', String(options.startAt));
  if (options.maxResults) params.set('maxResults', String(options.maxResults));
  if (options.orderBy) params.set('orderBy', options.orderBy);
  if (options.status) params.set('status', options.status);
  if (options.expand) params.set('expand', options.expand);

  return client.request(`/project/${projectKeyOrId}/version?${params.toString()}`);
}

// Create Version
interface CreateVersionInput {
  projectId: number;
  name: string;
  description?: string;
  startDate?: string;     // YYYY-MM-DD
  releaseDate?: string;   // YYYY-MM-DD
  released?: boolean;
  archived?: boolean;
}

async function createVersion(
  client: JiraClient,
  input: CreateVersionInput
): Promise<Version> {
  return client.request<Version>('/version', {
    method: 'POST',
    body: JSON.stringify(input),
  });
}

// Update Version
async function updateVersion(
  client: JiraClient,
  versionId: string,
  updates: Partial<{
    name: string;
    description: string;
    startDate: string;
    releaseDate: string;
    released: boolean;
    archived: boolean;
    moveUnfixedIssuesTo: string;  // Version ID when releasing
  }>
): Promise<Version> {
  return client.request<Version>(`/version/${versionId}`, {
    method: 'PUT',
    body: JSON.stringify(updates),
  });
}

// Release Version (mark as released)
async function releaseVersion(
  client: JiraClient,
  versionId: string,
  moveUnfixedIssuesTo?: string
): Promise<Version> {
  return updateVersion(client, versionId, {
    released: true,
    releaseDate: new Date().toISOString().split('T')[0],
    moveUnfixedIssuesTo,
  });
}

// Delete Version
async function deleteVersion(
  client: JiraClient,
  versionId: string,
  options: {
    moveFixedIssuesTo?: string;
    moveAffectedIssuesTo?: string;
  } = {}
): Promise<void> {
  const params = new URLSearchParams();
  if (options.moveFixedIssuesTo) params.set('moveFixedIssuesTo', options.moveFixedIssuesTo);
  if (options.moveAffectedIssuesTo) params.set('moveAffectedIssuesTo', options.moveAffectedIssuesTo);

  const query = params.toString() ? `?${params.toString()}` : '';
  await client.request(`/version/${versionId}${query}`, {
    method: 'DELETE',
  });
}

// Get Version Issue Counts
async function getVersionIssueCounts(
  client: JiraClient,
  versionId: string
): Promise<{
  issuesFixedCount: number;
  issuesAffectedCount: number;
  issueCountWithCustomFieldsShowingVersion: number;
}> {
  return client.request(`/version/${versionId}/relatedIssueCounts`);
}

// Get Unresolved Issue Count
async function getUnresolvedIssueCount(
  client: JiraClient,
  versionId: string
): Promise<{ issuesUnresolvedCount: number; self: string }> {
  return client.request(`/version/${versionId}/unresolvedIssueCount`);
}
typescript
// List Versions
async function getProjectVersions(
  client: JiraClient,
  projectKeyOrId: string,
  options: {
    startAt?: number;
    maxResults?: number;
    orderBy?: 'description' | '-description' | 'name' | '-name' | 'releaseDate' | '-releaseDate' | 'sequence' | '-sequence' | 'startDate' | '-startDate';
    status?: 'released' | 'unreleased' | 'archived';
    expand?: string;
  } = {}
): Promise<{ values: Version[]; total: number; isLast: boolean }> {
  const params = new URLSearchParams();
  if (options.startAt) params.set('startAt', String(options.startAt));
  if (options.maxResults) params.set('maxResults', String(options.maxResults));
  if (options.orderBy) params.set('orderBy', options.orderBy);
  if (options.status) params.set('status', options.status);
  if (options.expand) params.set('expand', options.expand);

  return client.request(`/project/${projectKeyOrId}/version?${params.toString()}`);
}

// Create Version
interface CreateVersionInput {
  projectId: number;
  name: string;
  description?: string;
  startDate?: string;     // YYYY-MM-DD
  releaseDate?: string;   // YYYY-MM-DD
  released?: boolean;
  archived?: boolean;
}

async function createVersion(
  client: JiraClient,
  input: CreateVersionInput
): Promise<Version> {
  return client.request<Version>('/version', {
    method: 'POST',
    body: JSON.stringify(input),
  });
}

// Update Version
async function updateVersion(
  client: JiraClient,
  versionId: string,
  updates: Partial<{
    name: string;
    description: string;
    startDate: string;
    releaseDate: string;
    released: boolean;
    archived: boolean;
    moveUnfixedIssuesTo: string;  // Version ID when releasing
  }>
): Promise<Version> {
  return client.request<Version>(`/version/${versionId}`, {
    method: 'PUT',
    body: JSON.stringify(updates),
  });
}

// Release Version (mark as released)
async function releaseVersion(
  client: JiraClient,
  versionId: string,
  moveUnfixedIssuesTo?: string
): Promise<Version> {
  return updateVersion(client, versionId, {
    released: true,
    releaseDate: new Date().toISOString().split('T')[0],
    moveUnfixedIssuesTo,
  });
}

// Delete Version
async function deleteVersion(
  client: JiraClient,
  versionId: string,
  options: {
    moveFixedIssuesTo?: string;
    moveAffectedIssuesTo?: string;
  } = {}
): Promise<void> {
  const params = new URLSearchParams();
  if (options.moveFixedIssuesTo) params.set('moveFixedIssuesTo', options.moveFixedIssuesTo);
  if (options.moveAffectedIssuesTo) params.set('moveAffectedIssuesTo', options.moveAffectedIssuesTo);

  const query = params.toString() ? `?${params.toString()}` : '';
  await client.request(`/version/${versionId}${query}`, {
    method: 'DELETE',
  });
}

// Get Version Issue Counts
async function getVersionIssueCounts(
  client: JiraClient,
  versionId: string
): Promise<{
  issuesFixedCount: number;
  issuesAffectedCount: number;
  issueCountWithCustomFieldsShowingVersion: number;
}> {
  return client.request(`/version/${versionId}/relatedIssueCounts`);
}

// Get Unresolved Issue Count
async function getUnresolvedIssueCount(
  client: JiraClient,
  versionId: string
): Promise<{ issuesUnresolvedCount: number; self: string }> {
  return client.request(`/version/${versionId}/unresolvedIssueCount`);
}

Step 6: Project Roles

Step 6: Project Roles

typescript
// Get Project Roles
async function getProjectRoles(
  client: JiraClient,
  projectKeyOrId: string
): Promise<Record<string, string>> {
  // Returns map of role name -> role URL
  return client.request(`/project/${projectKeyOrId}/role`);
}

// Get Role Details
async function getProjectRole(
  client: JiraClient,
  projectKeyOrId: string,
  roleId: number
): Promise<ProjectRole> {
  return client.request(`/project/${projectKeyOrId}/role/${roleId}`);
}

// Add User to Role
async function addUserToRole(
  client: JiraClient,
  projectKeyOrId: string,
  roleId: number,
  accountId: string
): Promise<ProjectRole> {
  return client.request(`/project/${projectKeyOrId}/role/${roleId}`, {
    method: 'POST',
    body: JSON.stringify({
      user: [accountId],
    }),
  });
}

// Add Group to Role
async function addGroupToRole(
  client: JiraClient,
  projectKeyOrId: string,
  roleId: number,
  groupName: string
): Promise<ProjectRole> {
  return client.request(`/project/${projectKeyOrId}/role/${roleId}`, {
    method: 'POST',
    body: JSON.stringify({
      group: [groupName],
    }),
  });
}

// Remove Actor from Role
async function removeActorFromRole(
  client: JiraClient,
  projectKeyOrId: string,
  roleId: number,
  actorType: 'user' | 'group',
  actorValue: string  // accountId or groupName
): Promise<void> {
  const param = actorType === 'user' ? 'user' : 'group';
  await client.request(
    `/project/${projectKeyOrId}/role/${roleId}?${param}=${encodeURIComponent(actorValue)}`,
    { method: 'DELETE' }
  );
}
typescript
// Get Project Roles
async function getProjectRoles(
  client: JiraClient,
  projectKeyOrId: string
): Promise<Record<string, string>> {
  // Returns map of role name -> role URL
  return client.request(`/project/${projectKeyOrId}/role`);
}

// Get Role Details
async function getProjectRole(
  client: JiraClient,
  projectKeyOrId: string,
  roleId: number
): Promise<ProjectRole> {
  return client.request(`/project/${projectKeyOrId}/role/${roleId}`);
}

// Add User to Role
async function addUserToRole(
  client: JiraClient,
  projectKeyOrId: string,
  roleId: number,
  accountId: string
): Promise<ProjectRole> {
  return client.request(`/project/${projectKeyOrId}/role/${roleId}`, {
    method: 'POST',
    body: JSON.stringify({
      user: [accountId],
    }),
  });
}

// Add Group to Role
async function addGroupToRole(
  client: JiraClient,
  projectKeyOrId: string,
  roleId: number,
  groupName: string
): Promise<ProjectRole> {
  return client.request(`/project/${projectKeyOrId}/role/${roleId}`, {
    method: 'POST',
    body: JSON.stringify({
      group: [groupName],
    }),
  });
}

// Remove Actor from Role
async function removeActorFromRole(
  client: JiraClient,
  projectKeyOrId: string,
  roleId: number,
  actorType: 'user' | 'group',
  actorValue: string  // accountId or groupName
): Promise<void> {
  const param = actorType === 'user' ? 'user' : 'group';
  await client.request(
    `/project/${projectKeyOrId}/role/${roleId}?${param}=${encodeURIComponent(actorValue)}`,
    { method: 'DELETE' }
  );
}

Step 7: Project Properties

Step 7: Project Properties

typescript
// List Project Properties
async function getProjectProperties(
  client: JiraClient,
  projectKeyOrId: string
): Promise<{ keys: Array<{ key: string; self: string }> }> {
  return client.request(`/project/${projectKeyOrId}/properties`);
}

// Get Property
async function getProjectProperty(
  client: JiraClient,
  projectKeyOrId: string,
  propertyKey: string
): Promise<{ key: string; value: any }> {
  return client.request(`/project/${projectKeyOrId}/properties/${propertyKey}`);
}

// Set Property
async function setProjectProperty(
  client: JiraClient,
  projectKeyOrId: string,
  propertyKey: string,
  value: any
): Promise<void> {
  await client.request(`/project/${projectKeyOrId}/properties/${propertyKey}`, {
    method: 'PUT',
    body: JSON.stringify(value),
  });
}

// Delete Property
async function deleteProjectProperty(
  client: JiraClient,
  projectKeyOrId: string,
  propertyKey: string
): Promise<void> {
  await client.request(`/project/${projectKeyOrId}/properties/${propertyKey}`, {
    method: 'DELETE',
  });
}
typescript
// List Project Properties
async function getProjectProperties(
  client: JiraClient,
  projectKeyOrId: string
): Promise<{ keys: Array<{ key: string; self: string }> }> {
  return client.request(`/project/${projectKeyOrId}/properties`);
}

// Get Property
async function getProjectProperty(
  client: JiraClient,
  projectKeyOrId: string,
  propertyKey: string
): Promise<{ key: string; value: any }> {
  return client.request(`/project/${projectKeyOrId}/properties/${propertyKey}`);
}

// Set Property
async function setProjectProperty(
  client: JiraClient,
  projectKeyOrId: string,
  propertyKey: string,
  value: any
): Promise<void> {
  await client.request(`/project/${projectKeyOrId}/properties/${propertyKey}`, {
    method: 'PUT',
    body: JSON.stringify(value),
  });
}

// Delete Property
async function deleteProjectProperty(
  client: JiraClient,
  projectKeyOrId: string,
  propertyKey: string
): Promise<void> {
  await client.request(`/project/${projectKeyOrId}/properties/${propertyKey}`, {
    method: 'DELETE',
  });
}

Step 8: Project Validation

Step 8: Project Validation

typescript
// Validate Project Key
async function validateProjectKey(
  client: JiraClient,
  key: string
): Promise<{ errorMessages: string[]; errors: Record<string, string> }> {
  return client.request(`/projectvalidate/key?key=${encodeURIComponent(key)}`);
}

// Get Valid Project Key Suggestion
async function getValidProjectKey(
  client: JiraClient,
  key: string
): Promise<string> {
  return client.request(`/projectvalidate/validProjectKey?key=${encodeURIComponent(key)}`);
}

// Get Valid Project Name
async function getValidProjectName(
  client: JiraClient,
  name: string
): Promise<string> {
  return client.request(`/projectvalidate/validProjectName?name=${encodeURIComponent(name)}`);
}

// Get Project Types
async function getProjectTypes(
  client: JiraClient
): Promise<Array<{
  key: string;
  formattedKey: string;
  descriptionI18nKey: string;
  icon: string;
  color: string;
}>> {
  return client.request('/project/type');
}
typescript
// Validate Project Key
async function validateProjectKey(
  client: JiraClient,
  key: string
): Promise<{ errorMessages: string[]; errors: Record<string, string> }> {
  return client.request(`/projectvalidate/key?key=${encodeURIComponent(key)}`);
}

// Get Valid Project Key Suggestion
async function getValidProjectKey(
  client: JiraClient,
  key: string
): Promise<string> {
  return client.request(`/projectvalidate/validProjectKey?key=${encodeURIComponent(key)}`);
}

// Get Valid Project Name
async function getValidProjectName(
  client: JiraClient,
  name: string
): Promise<string> {
  return client.request(`/projectvalidate/validProjectName?name=${encodeURIComponent(name)}`);
}

// Get Project Types
async function getProjectTypes(
  client: JiraClient
): Promise<Array<{
  key: string;
  formattedKey: string;
  descriptionI18nKey: string;
  icon: string;
  color: string;
}>> {
  return client.request('/project/type');
}

Step 9: High-Level Helpers

Step 9: High-Level Helpers

typescript
// Full project setup with components and version
async function setupProject(
  client: JiraClient,
  config: {
    key: string;
    name: string;
    leadAccountId: string;
    description?: string;
    components?: string[];
    initialVersion?: string;
  }
): Promise<{
  project: Project;
  components: Component[];
  version?: Version;
}> {
  // Create project
  const project = await createProject(client, {
    key: config.key,
    name: config.name,
    projectTypeKey: 'software',
    leadAccountId: config.leadAccountId,
    description: config.description,
  });

  // Create components
  const components: Component[] = [];
  for (const compName of config.components || []) {
    const comp = await createComponent(client, {
      project: project.key,
      name: compName,
    });
    components.push(comp);
  }

  // Create initial version
  let version: Version | undefined;
  if (config.initialVersion) {
    version = await createVersion(client, {
      projectId: parseInt(project.id),
      name: config.initialVersion,
    });
  }

  return { project, components, version };
}

// Clone project structure (components + unreleased versions)
async function cloneProjectStructure(
  client: JiraClient,
  sourceProjectKey: string,
  targetProjectKey: string
): Promise<{
  componentsCloned: number;
  versionsCloned: number;
}> {
  // Get source components
  const sourceComponents = await getProjectComponents(client, sourceProjectKey);

  // Get source versions (unreleased only)
  const sourceVersions = await getProjectVersions(client, sourceProjectKey, {
    status: 'unreleased',
  });

  // Get target project
  const targetProject = await client.request<Project>(`/project/${targetProjectKey}`);

  // Clone components
  for (const comp of sourceComponents) {
    await createComponent(client, {
      project: targetProjectKey,
      name: comp.name,
      description: comp.description,
    });
  }

  // Clone versions
  for (const ver of sourceVersions.values) {
    await createVersion(client, {
      projectId: parseInt(targetProject.id),
      name: ver.name,
      description: ver.description,
      startDate: ver.startDate,
      releaseDate: ver.releaseDate,
    });
  }

  return {
    componentsCloned: sourceComponents.length,
    versionsCloned: sourceVersions.values.length,
  };
}
typescript
// Full project setup with components and version
async function setupProject(
  client: JiraClient,
  config: {
    key: string;
    name: string;
    leadAccountId: string;
    description?: string;
    components?: string[];
    initialVersion?: string;
  }
): Promise<{
  project: Project;
  components: Component[];
  version?: Version;
}> {
  // Create project
  const project = await createProject(client, {
    key: config.key,
    name: config.name,
    projectTypeKey: 'software',
    leadAccountId: config.leadAccountId,
    description: config.description,
  });

  // Create components
  const components: Component[] = [];
  for (const compName of config.components || []) {
    const comp = await createComponent(client, {
      project: project.key,
      name: compName,
    });
    components.push(comp);
  }

  // Create initial version
  let version: Version | undefined;
  if (config.initialVersion) {
    version = await createVersion(client, {
      projectId: parseInt(project.id),
      name: config.initialVersion,
    });
  }

  return { project, components, version };
}

// Clone project structure (components + unreleased versions)
async function cloneProjectStructure(
  client: JiraClient,
  sourceProjectKey: string,
  targetProjectKey: string
): Promise<{
  componentsCloned: number;
  versionsCloned: number;
}> {
  // Get source components
  const sourceComponents = await getProjectComponents(client, sourceProjectKey);

  // Get source versions (unreleased only)
  const sourceVersions = await getProjectVersions(client, sourceProjectKey, {
    status: 'unreleased',
  });

  // Get target project
  const targetProject = await client.request<Project>(`/project/${targetProjectKey}`);

  // Clone components
  for (const comp of sourceComponents) {
    await createComponent(client, {
      project: targetProjectKey,
      name: comp.name,
      description: comp.description,
    });
  }

  // Clone versions
  for (const ver of sourceVersions.values) {
    await createVersion(client, {
      projectId: parseInt(targetProject.id),
      name: ver.name,
      description: ver.description,
      startDate: ver.startDate,
      releaseDate: ver.releaseDate,
    });
  }

  return {
    componentsCloned: sourceComponents.length,
    versionsCloned: sourceVersions.values.length,
  };
}

curl Examples

curl示例

Create Project

Create Project

bash
curl -X POST "$JIRA_BASE_URL/rest/api/3/project" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "NEWPROJ",
    "name": "New Project",
    "projectTypeKey": "software",
    "leadAccountId": "5b10a2844c20165700ede21g",
    "description": "Project description"
  }'
bash
curl -X POST "$JIRA_BASE_URL/rest/api/3/project" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "NEWPROJ",
    "name": "New Project",
    "projectTypeKey": "software",
    "leadAccountId": "5b10a2844c20165700ede21g",
    "description": "Project description"
  }'

Update Project

Update Project

bash
curl -X PUT "$JIRA_BASE_URL/rest/api/3/project/SCRUM" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Updated Project Name",
    "description": "Updated description"
  }'
bash
curl -X PUT "$JIRA_BASE_URL/rest/api/3/project/SCRUM" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Updated Project Name",
    "description": "Updated description"
  }'

Delete Project

Delete Project

bash
curl -X DELETE "$JIRA_BASE_URL/rest/api/3/project/SCRUM?enableUndo=true" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)"
bash
curl -X DELETE "$JIRA_BASE_URL/rest/api/3/project/SCRUM?enableUndo=true" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)"

Search Projects

Search Projects

bash
curl -X GET "$JIRA_BASE_URL/rest/api/3/project/search?query=scrum&expand=description,lead" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Accept: application/json"
bash
curl -X GET "$JIRA_BASE_URL/rest/api/3/project/search?query=scrum&expand=description,lead" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Accept: application/json"

Create Component

Create Component

bash
curl -X POST "$JIRA_BASE_URL/rest/api/3/component" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Content-Type: application/json" \
  -d '{
    "project": "SCRUM",
    "name": "Backend",
    "description": "Backend services"
  }'
bash
curl -X POST "$JIRA_BASE_URL/rest/api/3/component" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Content-Type: application/json" \
  -d '{
    "project": "SCRUM",
    "name": "Backend",
    "description": "Backend services"
  }'

Create Version

Create Version

bash
curl -X POST "$JIRA_BASE_URL/rest/api/3/version" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": 10000,
    "name": "v1.0.0",
    "description": "First release",
    "releaseDate": "2025-01-15"
  }'
bash
curl -X POST "$JIRA_BASE_URL/rest/api/3/version" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": 10000,
    "name": "v1.0.0",
    "description": "First release",
    "releaseDate": "2025-01-15"
  }'

Release Version

Release Version

bash
curl -X PUT "$JIRA_BASE_URL/rest/api/3/version/10001" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Content-Type: application/json" \
  -d '{
    "released": true,
    "releaseDate": "2025-12-10"
  }'
bash
curl -X PUT "$JIRA_BASE_URL/rest/api/3/version/10001" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Content-Type: application/json" \
  -d '{
    "released": true,
    "releaseDate": "2025-12-10"
  }'

Get Project Roles

Get Project Roles

bash
curl -X GET "$JIRA_BASE_URL/rest/api/3/project/SCRUM/role" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Accept: application/json"
bash
curl -X GET "$JIRA_BASE_URL/rest/api/3/project/SCRUM/role" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Accept: application/json"

Add User to Role

Add User to Role

bash
curl -X POST "$JIRA_BASE_URL/rest/api/3/project/SCRUM/role/10002" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Content-Type: application/json" \
  -d '{
    "user": ["5b10a2844c20165700ede21g"]
  }'
bash
curl -X POST "$JIRA_BASE_URL/rest/api/3/project/SCRUM/role/10002" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Content-Type: application/json" \
  -d '{
    "user": ["5b10a2844c20165700ede21g"]
  }'

Validate Project Key

Validate Project Key

bash
curl -X GET "$JIRA_BASE_URL/rest/api/3/projectvalidate/key?key=NEWPROJ" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Accept: application/json"
bash
curl -X GET "$JIRA_BASE_URL/rest/api/3/projectvalidate/key?key=NEWPROJ" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Accept: application/json"

Set Project Property

Set Project Property

bash
curl -X PUT "$JIRA_BASE_URL/rest/api/3/project/SCRUM/properties/custom-config" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Content-Type: application/json" \
  -d '{"setting1": "value1", "setting2": true}'
bash
curl -X PUT "$JIRA_BASE_URL/rest/api/3/project/SCRUM/properties/custom-config" \
  -H "Authorization: Basic $(echo -n 'email:token' | base64)" \
  -H "Content-Type: application/json" \
  -d '{"setting1": "value1", "setting2": true}'

API Endpoints Summary

API端点汇总

OperationMethodPath
Create projectPOST
/project
Get projectGET
/project/{projectIdOrKey}
Update projectPUT
/project/{projectIdOrKey}
Delete projectDELETE
/project/{projectIdOrKey}
Archive projectPOST
/project/{projectIdOrKey}/archive
Restore projectPOST
/project/{projectIdOrKey}/restore
Search projectsGET
/project/search
Recent projectsGET
/project/recent
List componentsGET
/project/{projectIdOrKey}/components
Create componentPOST
/component
Update componentPUT
/component/{id}
Delete componentDELETE
/component/{id}
List versionsGET
/project/{projectIdOrKey}/version
Create versionPOST
/version
Update versionPUT
/version/{id}
Delete versionDELETE
/version/{id}
Get rolesGET
/project/{projectIdOrKey}/role
Get roleGET
/project/{projectIdOrKey}/role/{roleId}
Add to rolePOST
/project/{projectIdOrKey}/role/{roleId}
Remove from roleDELETE
/project/{projectIdOrKey}/role/{roleId}
List propertiesGET
/project/{projectIdOrKey}/properties
Get propertyGET
/project/{projectIdOrKey}/properties/{key}
Set propertyPUT
/project/{projectIdOrKey}/properties/{key}
Delete propertyDELETE
/project/{projectIdOrKey}/properties/{key}
Validate keyGET
/projectvalidate/key
Valid keyGET
/projectvalidate/validProjectKey
Project typesGET
/project/type
操作请求方法路径
创建项目POST
/project
获取项目GET
/project/{projectIdOrKey}
更新项目PUT
/project/{projectIdOrKey}
删除项目DELETE
/project/{projectIdOrKey}
归档项目POST
/project/{projectIdOrKey}/archive
恢复项目POST
/project/{projectIdOrKey}/restore
搜索项目GET
/project/search
最近项目GET
/project/recent
列出组件GET
/project/{projectIdOrKey}/components
创建组件POST
/component
更新组件PUT
/component/{id}
删除组件DELETE
/component/{id}
列出版本GET
/project/{projectIdOrKey}/version
创建版本POST
/version
更新版本PUT
/version/{id}
删除版本DELETE
/version/{id}
获取角色GET
/project/{projectIdOrKey}/role
获取角色详情GET
/project/{projectIdOrKey}/role/{roleId}
添加角色成员POST
/project/{projectIdOrKey}/role/{roleId}
移除角色成员DELETE
/project/{projectIdOrKey}/role/{roleId}
列出属性GET
/project/{projectIdOrKey}/properties
获取属性GET
/project/{projectIdOrKey}/properties/{key}
设置属性PUT
/project/{projectIdOrKey}/properties/{key}
删除属性DELETE
/project/{projectIdOrKey}/properties/{key}
验证密钥GET
/projectvalidate/key
获取有效密钥GET
/projectvalidate/validProjectKey
项目类型GET
/project/type

Common Patterns

通用模式

Project Key Rules

项目密钥规则

  • 2-10 uppercase letters only
  • Must be unique across instance
  • Cannot be reused for 60 days after deletion
  • 仅允许2-10个大写字母
  • 需在实例中唯一
  • 删除后60天内无法复用

Permission Requirements

权限要求

OperationRequired Permission
Create projectJira admin
Update projectProject admin
Delete projectJira admin
Manage componentsProject admin
Manage versionsProject admin
Manage rolesProject admin
操作所需权限
创建项目Jira管理员
更新项目项目管理员
删除项目Jira管理员
管理组件项目管理员
管理版本项目管理员
管理角色项目管理员

Project Types

项目类型

TypeUse Case
software
Scrum/Kanban dev projects
service_desk
Customer support projects
business
Simple task tracking
类型适用场景
software
Scrum/Kanban开发项目
service_desk
客户支持项目
business
简单任务追踪

Common Mistakes

常见误区

  • Using lowercase in project keys
  • Forgetting to get projectId (numeric) for version creation
  • Not handling 404 for deleted/archived projects
  • Assuming role IDs are consistent (query first)
  • Not using enableUndo=true for safe deletion
  • 项目密钥使用小写字母
  • 创建版本时忘记获取项目ID(数字格式)
  • 未处理已删除/归档项目的404错误
  • 假设角色ID是固定的(需先查询)
  • 删除项目时未启用enableUndo=true以确保可恢复

References

参考链接

Version History

版本历史

  • 2025-12-10: Created comprehensive project management skill
  • 2025-12-10:创建全面的项目管理技能文档