Jira
通过我们的 Jira 集成,您可以根据您的映射和定义,将 Jira 云账户中的 "问题 "和 "项目 "导入 Port。
常见被用于情况
- 映射 Jira 组织环境中的问题和项目。
- 实时关注对象变更(创建/更新/删除),并自动将变更应用到 Port 中的实体。
- 使用自助操作创建/删除 Jira 对象。
先决条件
To install the integration, you need a Kubernetes cluster that the integration's container chart will be deployed to.
Please make sure that you have kubectl
and helm
installed on your machine, and that your kubectl
CLI is connected to the Kubernetes cluster where you plan to install the integration.
安装
从以下安装方法中选择一种:
- Real Time & Always On
- Scheduled
使用该安装选项意味着集成将能使用 webhook 实时更新 Port。
本表总结了安装时可用的参数,请在下面的脚本中按自己的需要进行设置,然后复制并在终端运行:
Parameter | Description | Example | Required |
---|---|---|---|
port.clientId | Your port client id | ✅ | |
port.clientSecret | Your port client secret | ✅ | |
integration.secrets.atlassianUserEmail | The email of the user used to query Jira | [email protected] | ✅ |
integration.secrets.atlassianUserToken | Jira API token generated by the user | ✅ | |
integration.config.jiraHost | The URL of your Jira | https://example.atlassian.net | ✅ |
integration.config.appHost | The host of the Port Ocean app. Used to set up the integration endpoint as the target for webhooks created in Jira | https://my-ocean-integration.com | ❌ |
Advanced configuration
Parameter | Description |
---|---|
integration.eventListener.type | The event listener type. Read more about event listeners |
integration.type | The integration to be installed |
scheduledResyncInterval | The number of minutes between each resync. When not set the integration will resync for each event listener resync event. Read more about scheduledResyncInterval |
initializePortResources | Default true, When set to true the integration will create default blueprints and the port App config Mapping. Read more about initializePortResources |
- Helm
- ArgoCD
To install the integration using Helm, run the following command:
helm repo add --force-update port-labs https://port-labs.github.io/helm-charts
helm upgrade --install my-jira-integration port-labs/port-ocean \
--set port.clientId="PORT_CLIENT_ID" \
--set port.clientSecret="PORT_CLIENT_SECRET" \
--set port.baseUrl="https://api.getport.io" \
--set initializePortResources=true \
--set scheduledResyncInterval=120 \
--set integration.identifier="my-jira-integration" \
--set integration.type="jira" \
--set integration.eventListener.type="POLLING" \
--set integration.config.jiraHost="string" \
--set integration.secrets.atlassianUserEmail="string" \
--set integration.secrets.atlassianUserToken="string"
To install the integration using ArgoCD, follow these steps:
- 在你的 git 仓库的
argocd/my-ocean-jira-integration
中创建一个内容为values.yaml
的文件:
ATLASSIAN_JIRA_HOST``ATLASSIAN_USER_EMAIL
和 ATLASSIAN_USER_TOKEN
的占位符。initializePortResources: true
scheduledResyncInterval: 120
integration:
identifier: my-ocean-jira-integration
type: jira
eventListener:
type: POLLING
config:
jiraHost: ATLASSIAN_JIRA_HOST
secrets:
atlassianUserEmail: ATLASSIAN_USER_EMAIL
atlassianUserToken: ATLASSIAN_USER_TOKEN
2.创建以下 "my-ocean-jira-integration.yaml "配置清单,安装 "my-ocean-jira-integration "ArgoCD应用程序:
YOUR_PORT_CLIENT_ID``YOUR_PORT_CLIENT_SECRET
和 YOUR_GIT_REPO_URL
的占位符。多种来源的 ArgoCD 文档可在here 上找到。
ArgoCD Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-ocean-jira-integration
namespace: argocd
spec:
destination:
namespace: mmy-ocean-jira-integration
server: https://kubernetes.default.svc
project: default
sources:
- repoURL: 'https://port-labs.github.io/helm-charts/'
chart: port-ocean
targetRevision: 0.1.14
helm:
valueFiles:
- $values/argocd/my-ocean-jira-integration/values.yaml
parameters:
- name: port.clientId
value: YOUR_PORT_CLIENT_ID
- name: port.clientSecret
value: YOUR_PORT_CLIENT_SECRET
- repoURL: YOUR_GIT_REPO_URL
targetRevision: main
ref: values
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
3.使用 kubectl
配置应用程序清单:
kubectl apply -f my-ocean-jira-integration.yaml
- GitHub
- Jenkins
- Azure Devops
This workflow will run the Jira integration once and then exit, this is useful for scheduled ingestion of data.
确保配置以下Github Secrets :
Parameter | Description | Example | Required |
---|---|---|---|
OCEAN__INTEGRATION__CONFIG__JIRA_HOST | The URL of your Jira | https://example.atlassian.net | ✅ |
OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_EMAIL | The email of the user used to query Jira | [email protected] | ✅ |
OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_TOKEN | Jira API token generated by the user | ✅ | |
OCEAN__PORT__CLIENT_ID | Your Port client (How to get the credentials) id | ✅ | |
OCEAN__PORT__CLIENT_SECRET | Your Port client (How to get the credentials) secret | ✅ | |
OCEAN__INITIALIZE_PORT_RESOURCES | Default true, When set to true the integration will create default blueprints and the port App config Mapping. Read more about initializePortResources | ❌ | |
OCEAN__INTEGRATION__IDENTIFIER | The identifier of the integration that will be installed | ❌ |
下面是 jira-integration.yml
工作流程文件的示例:
name: Jira Exporter Workflow
# This workflow responsible for running Jira exporter.
on:
workflow_dispatch:
jobs:
run-integration:
runs-on: ubuntu-latest
steps:
- name: Run Jira Integration
run: |
# Set Docker image and run the container
integration_type="jira"
version="latest"
image_name="ghcr.io/port-labs/port-ocean-$integration_type:$version"
docker run -i --rm --platform=linux/amd64 \
-e OCEAN__EVENT_LISTENER='{"type":"ONCE"}' \
-e OCEAN__INITIALIZE_PORT_RESOURCES=true \
-e OCEAN__INTEGRATION__CONFIG__JIRA_HOST=${{ secrets.OCEAN__INTEGRATION__CONFIG__JIRA_HOST }} \
-e OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_EMAIL=${{ secrets.OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_EMAIL }} \
-e OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_TOKEN=${{ secrets.OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_TOKEN }} \
-e OCEAN__PORT__CLIENT_ID=${{ secrets.OCEAN__PORT__CLIENT_ID }} \
-e OCEAN__PORT__CLIENT_SECRET=${{ secrets.OCEAN__PORT__CLIENT_SECRET }} \
$image_name
exit $?
This pipeline will run the Jira integration once and then exit, this is useful for scheduled ingestion of data.
请确保配置以下Jenkins Credentials 的 "Secret Text "类型:
Parameter | Description | Example | Required |
---|---|---|---|
OCEAN__INTEGRATION__CONFIG__JIRA_HOST | The URL of your Jira | https://example.atlassian.net | ✅ |
OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_EMAIL | The email of the user used to query Jira | [email protected] | ✅ |
OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_TOKEN | Jira API token generated by the user | ✅ | |
OCEAN__PORT__CLIENT_ID | Your Port client (How to get the credentials) id | ✅ | |
OCEAN__PORT__CLIENT_SECRET | Your Port client (How to get the credentials) secret | ✅ | |
OCEAN__INITIALIZE_PORT_RESOURCES | Default true, When set to true the integration will create default blueprints and the port App config Mapping. Read more about initializePortResources | ❌ | |
OCEAN__INTEGRATION__IDENTIFIER | The identifier of the integration that will be installed | ❌ |
下面是 Jenkinsfile
groovy Pipelines 文件的示例:
pipeline {
agent any
stages {
stage('Run Jira Integration') {
steps {
script {
withCredentials([
string(credentialsId: 'OCEAN__INTEGRATION__CONFIG__JIRA_HOST', variable: 'OCEAN__INTEGRATION__CONFIG__JIRA_HOST'),
string(credentialsId: 'OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_EMAIL', variable: 'OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_EMAIL'),
string(credentialsId: 'OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_TOKEN', variable: 'OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_TOKEN'),
string(credentialsId: 'OCEAN__PORT__CLIENT_ID', variable: 'OCEAN__PORT__CLIENT_ID'),
string(credentialsId: 'OCEAN__PORT__CLIENT_SECRET', variable: 'OCEAN__PORT__CLIENT_SECRET'),
]) {
sh('''
#Set Docker image and run the container
integration_type="jira"
version="latest"
image_name="ghcr.io/port-labs/port-ocean-${integration_type}:${version}"
docker run -i --rm --platform=linux/amd64 \
-e OCEAN__EVENT_LISTENER='{"type":"ONCE"}' \
-e OCEAN__INITIALIZE_PORT_RESOURCES=true \
-e OCEAN__INTEGRATION__CONFIG__JIRA_HOST=$OCEAN__INTEGRATION__CONFIG__JIRA_HOST \
-e OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_EMAIL=$OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_EMAIL \
-e OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_TOKEN=$OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_TOKEN \
-e OCEAN__PORT__CLIENT_ID=$OCEAN__PORT__CLIENT_ID \
-e OCEAN__PORT__CLIENT_SECRET=$OCEAN__PORT__CLIENT_SECRET \
$image_name
exit $?
''')
}
}
}
}
}
}
This pipeline will run the Jira integration once and then exit, this is useful for scheduled ingestion of data.
Your Azure Devops agent should be able to run docker commands. Learn more about agents here.
If you want the integration to update Port in real time using webhooks you should use the Real Time & Always On installation option.
Make sure to configure the following variables using Azure Devops variable groups. Add them into in a variable group named port-ocean-credentials
:
Parameter | Description | Example | Required |
---|---|---|---|
OCEAN__INTEGRATION__CONFIG__JIRA_HOST | The URL of your Jira | https://example.atlassian.net | ✅ |
OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_EMAIL | The email of the user used to query Jira | [email protected] | ✅ |
OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_TOKEN | Jira API token generated by the user | ✅ | |
OCEAN__PORT__CLIENT_ID | Your Port client (How to get the credentials) id | ✅ | |
OCEAN__PORT__CLIENT_SECRET | Your Port client (How to get the credentials) secret | ✅ | |
OCEAN__INITIALIZE_PORT_RESOURCES | Default true, When set to true the integration will create default blueprints and the port App config Mapping. Read more about initializePortResources | ❌ | |
OCEAN__INTEGRATION__IDENTIFIER | The identifier of the integration that will be installed | ❌ |
下面是 jira-integration.yml
Pipelines 文件的示例:
trigger:
- main
pool:
vmImage: "ubuntu-latest"
variables:
- group: port-ocean-credentials
steps:
- script: |
# Set Docker image and run the container
integration_type="jira"
version="latest"
image_name="ghcr.io/port-labs/port-ocean-$integration_type:$version"
docker run -i --rm \
-e OCEAN__EVENT_LISTENER='{"type":"ONCE"}' \
-e OCEAN__INITIALIZE_PORT_RESOURCES=true \
-e OCEAN__INTEGRATION__CONFIG__JIRA_HOST=${OCEAN__INTEGRATION__CONFIG__JIRA_HOST} \
-e OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_EMAIL=${OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_EMAIL} \
-e OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_TOKEN=${OCEAN__INTEGRATION__CONFIG__ATLASSIAN_USER_TOKEN} \
-e OCEAN__PORT__CLIENT_ID=${OCEAN__PORT__CLIENT_ID} \
-e OCEAN__PORT__CLIENT_SECRET=${OCEAN__PORT__CLIENT_SECRET} \
$image_name
exit $?
displayName: 'Ingest Data into Port'
有关代理或自签名证书等高级配置,click here 。
接收 Jira 对象
Jira 集成使用 YAML 配置来描述将数据加载到开发人员门户的过程。
下面是配置中的一个示例片段,演示了从 Jira 获取 "项目 "数据的过程:
createMissingRelatedEntities: true
deleteDependentEntities: true
resources:
- kind: project
selector:
query: "true"
port:
entity:
mappings:
identifier: .key
title: .name
blueprint: '"jiraProject"'
properties:
url: (.self | split("/") | .[:3] | join("/")) + "/projects/" + .key
该集成利用JQ JSON processor 对来自 Jira API 事件的现有字段和 Values 进行选择、修改、连接、转换和其他操作。
createMissingRelatedEntities
(创建缺失的相关实体)- 用于启用在 Port 中创建缺失的相关实体。 当您想在一次调用中创建一个实体及其相关实体,或者想创建一个其相关实体尚不存在的实体时,这个参数非常有用。deleteDependentEntities
- 用于启用从属 Port 实体的删除。 当您有两个具有必填关系的蓝图,且关系中的目标实体应被删除时,此参数非常有用。 在这种情况下,如果此参数设置为 false
,删除操作将失败。 如果设置为 true
,源实体也将被删除。
配置结构
集成配置决定了将从 Jira 查询哪些资源,以及将在 Port 中创建哪些实体和属性。
-
集成配置的根密钥是 "资源 "密钥:
resources:
- kind: project
selector:
... -
类型 "键是 Jira 对象的指定符:
resources:
- kind: project
selector:
... -
通过 "选择器 "和 "查询 "键,您可以过滤哪些指定 "类型 "的对象将被录入软件目录:
resources:
- kind: project
selector:
query: "true" # JQ boolean expression. If evaluated to false - this object will be skipped.
port:
要使用 JQL 过滤,请在 selector
对象中添加一个 jql
键,并将所需的 JQL 查询作为值。 例如:
resources:
- kind: issue # JQL filtering can only be used with the "issue" kind
selector:
query: "true" # JQ boolean expression. If evaluated to false - this object will be skipped.
jql: "status != Done" # JQL query, will only ingest issues whose status is not "Done"
port:
-
Port"、"实体 "和 "映射 "键被用来将 Jira 对象字段映射到Port实体。要创建多个同类映射,可在
resources
数组中添加另一项;resources:
- kind: project
selector:
query: "true"
port:
entity:
mappings: # Mappings between one Jira object to a Port entity. Each value is a JQ query.
identifier: .key
title: .name
blueprint: '"jiraProject"'
properties:
url: (.self | split("/") | .[:3] | join("/")) + "/projects/" + .key
- kind: project # In this instance project is mapped again with a different filter
selector:
query: '.name == "MyProjectName"'
port:
entity:
mappings: ...
blueprint
键的值 - 如果要使用硬编码字符串,则需要用 2 组引号封装,例如使用一对单引号 ('
),然后再使用一对双引号 ("
)。将数据输入Port
要使用integration configuration 引用 Jira 对象,可以按照以下步骤操作:
- 转到 DevPortal Builder 页面。
- 选择要被 Jira 引用的蓝图。
- 从菜单中选择采集数据选项。
- 在项目管理 Provider 类别下选择 Jira。
- 根据需要修改configuration 。
- 单击
Resync
。
示例
蓝图和相关集成配置示例:
问题
Issue blueprint
{
"identifier": "jiraIssue",
"title": "Jira Issue",
"icon": "Jira",
"schema": {
"properties": {
"url": {
"title": "Issue URL",
"type": "string",
"format": "url",
"description": "URL to the issue in Jira"
},
"status": {
"title": "Status",
"type": "string",
"description": "The status of the issue"
},
"issueType": {
"title": "Type",
"type": "string",
"description": "The type of the issue"
},
"components": {
"title": "Components",
"type": "array",
"description": "The components related to this issue"
},
"assignee": {
"title": "Assignee",
"type": "string",
"format": "user",
"description": "The user assigned to the issue"
},
"reporter": {
"title": "Reporter",
"type": "string",
"description": "The user that reported to the issue",
"format": "user"
},
"creator": {
"title": "Creator",
"type": "string",
"description": "The user that created to the issue",
"format": "user"
},
"priority": {
"title": "Priority",
"type": "string",
"description": "The priority of the issue"
},
"created": {
"title": "Created At",
"type": "string",
"description": "The created datetime of the issue",
"format": "date-time"
},
"updated": {
"title": "Updated At",
"type": "string",
"description": "The updated datetime of the issue",
"format": "date-time"
}
}
},
"mirrorProperties": {},
"calculationProperties": {},
"aggregationProperties": {},
"relations": {
"project": {
"target": "jiraProject",
"title": "Project",
"description": "The Jira project that contains this issue",
"required": false,
"many": false
},
"parentIssue": {
"target": "jiraIssue",
"title": "Parent Issue",
"required": false,
"many": false
},
"subtasks": {
"target": "jiraIssue",
"title": "Subtasks",
"required": false,
"many": true
}
}
}
Integration configuration
createMissingRelatedEntities: true
deleteDependentEntities: true
resources:
- kind: issue
selector:
query: "true"
port:
entity:
mappings:
identifier: .key
title: .fields.summary
blueprint: '"jiraIssue"'
properties:
url: (.self | split("/") | .[:3] | join("/")) + "/browse/" + .key
status: .fields.status.name
issueType: .fields.issuetype.name
components: .fields.components
assignee: .fields.assignee.displayName
reporter: .fields.reporter.displayName
creator: .fields.creator.displayName
priority: .fields.priority.id
created: .fields.created
updated: .fields.updated
relations:
project: .fields.project.key
parentIssue: .fields.parent.key
subtasks: .fields.subtasks | map(.key)
项目
Project blueprint
{
"identifier": "jiraProject",
"title": "Jira Project",
"icon": "Jira",
"description": "A Jira project",
"schema": {
"properties": {
"url": {
"title": "Project URL",
"type": "string",
"format": "url",
"description": "URL to the project in Jira"
}
}
},
"mirrorProperties": {},
"calculationProperties": {},
"aggregationProperties": {},
"relations": {}
}
Integration configuration
createMissingRelatedEntities: true
deleteDependentEntities: true
resources:
- kind: project
selector:
query: "true"
port:
entity:
mappings:
identifier: .key
title: .name
blueprint: '"jiraProject"'
properties:
url: (.self | split("/") | .[:3] | join("/")) + "/projects/" + .key
让我们来测试一下
本节包括来自 Jira 的响应数据示例。 此外,还包括根据上一节提供的 Ocean 配置从重新同步事件中创建的实体。
有效载荷
下面是 Jira 提供的有效载荷结构示例:
Project response data
{
"expand": "description,lead,issueTypes,url,projectKeys,permissions,insight",
"self": "https://myaccount.atlassian.net/rest/api/3/project/10000",
"id": "10000",
"key": "PA",
"name": "Port-AI",
"avatarUrls": {
"48x48": "https://myaccount.atlassian.net/rest/api/3/universal_avatar/view/type/project/avatar/10413",
"24x24": "https://myaccount.atlassian.net/rest/api/3/universal_avatar/view/type/project/avatar/10413?size=small",
"16x16": "https://myaccount.atlassian.net/rest/api/3/universal_avatar/view/type/project/avatar/10413?size=xsmall",
"32x32": "https://myaccount.atlassian.net/rest/api/3/universal_avatar/view/type/project/avatar/10413?size=medium"
},
"projectTypeKey": "software",
"simplified": true,
"style": "next-gen",
"isPrivate": false,
"properties": {},
"entityId": "7f4f8d6f-705b-4074-84be-46f0d012cd8e",
"uuid": "7f4f8d6f-705b-4074-84be-46f0d012cd8e"
}
Issue response data
{
"expand": "operations,versionedRepresentations,editmeta,changelog,customfield_10010.requestTypePractice,renderedFields",
"id": "10000",
"self": "https://myaccount.atlassian.net/rest/api/3/issue/10000",
"key": "PA-1",
"fields": {
"statuscategorychangedate": "2023-11-06T11:02:59.341+0000",
"issuetype": {
"self": "https://myaccount.atlassian.net/rest/api/3/issuetype/10001",
"id": "10001",
"description": "Tasks track small, distinct pieces of work.",
"iconUrl": "https://myaccount.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium",
"name": "Task",
"subtask": false,
"avatarId": 10318,
"entityId": "a7309bf9-70c5-4237-bdaf-0261037b6ecc",
"hierarchyLevel": 0
},
"timespent": "None",
"customfield_10030": "None",
"project": {
"self": "https://myaccount.atlassian.net/rest/api/3/project/10000",
"id": "10000",
"key": "PA",
"name": "Port-AI",
"projectTypeKey": "software",
"simplified": true,
"avatarUrls": {
"48x48": "https://myaccount.atlassian.net/rest/api/3/universal_avatar/view/type/project/avatar/10413",
"24x24": "https://myaccount.atlassian.net/rest/api/3/universal_avatar/view/type/project/avatar/10413?size=small",
"16x16": "https://myaccount.atlassian.net/rest/api/3/universal_avatar/view/type/project/avatar/10413?size=xsmall",
"32x32": "https://myaccount.atlassian.net/rest/api/3/universal_avatar/view/type/project/avatar/10413?size=medium"
}
},
"customfield_10031": "None",
"customfield_10032": "None",
"fixVersions": [],
"aggregatetimespent": "None",
"resolution": "None",
"customfield_10027": "None",
"customfield_10028": "None",
"customfield_10029": "None",
"resolutiondate": "None",
"workratio": -1,
"watches": {
"self": "https://myaccount.atlassian.net/rest/api/3/issue/PA-1/watchers",
"watchCount": 1,
"isWatching": true
},
"lastViewed": "None",
"created": "2023-11-06T11:02:59.000+0000",
"customfield_10020": "None",
"customfield_10021": "None",
"customfield_10022": "None",
"priority": {
"self": "https://myaccount.atlassian.net/rest/api/3/priority/3",
"iconUrl": "https://myaccount.atlassian.net/images/icons/priorities/medium.svg",
"name": "Medium",
"id": "3"
},
"customfield_10023": "None",
"customfield_10024": "None",
"customfield_10025": "None",
"labels": ["infra"],
"customfield_10026": "None",
"customfield_10016": "None",
"customfield_10017": "None",
"customfield_10018": {
"hasEpicLinkFieldDependency": false,
"showField": false,
"nonEditableReason": {
"reason": "PLUGIN_LICENSE_ERROR",
"message": "The Parent Link is only available to Jira Premium users."
}
},
"customfield_10019": "0|hzzzzz:",
"timeestimate": "None",
"aggregatetimeoriginalestimate": "None",
"versions": [],
"issuelinks": [],
"assignee": {
"self": "https://myaccount.atlassian.net/rest/api/3/user?accountId=712020%3A05acda87-42da-44d8-b21e-f71a508e5d11",
"accountId": "712020:05acda87-42da-44d8-b21e-f71a508e5d11",
"emailAddress": "[email protected]",
"avatarUrls": {
"48x48": "https://secure.gravatar.com/avatar/0d5d34ceb820d324d69046a1b2f51dc0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIC-3.png",
"24x24": "https://secure.gravatar.com/avatar/0d5d34ceb820d324d69046a1b2f51dc0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIC-3.png",
"16x16": "https://secure.gravatar.com/avatar/0d5d34ceb820d324d69046a1b2f51dc0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIC-3.png",
"32x32": "https://secure.gravatar.com/avatar/0d5d34ceb820d324d69046a1b2f51dc0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIC-3.png"
},
"displayName": "User Name",
"active": true,
"timeZone": "UTC",
"accountType": "atlassian"
},
"updated": "2023-11-06T11:03:18.244+0000",
"status": {
"self": "https://myaccount.atlassian.net/rest/api/3/status/10000",
"description": "",
"iconUrl": "https://myaccount.atlassian.net/",
"name": "To Do",
"id": "10000",
"statusCategory": {
"self": "https://myaccount.atlassian.net/rest/api/3/statuscategory/2",
"id": 2,
"key": "new",
"colorName": "blue-gray",
"name": "To Do"
}
},
"components": [],
"timeoriginalestimate": "None",
"description": "None",
"customfield_10010": "None",
"customfield_10014": "None",
"customfield_10015": "None",
"customfield_10005": "None",
"customfield_10006": "None",
"security": "None",
"customfield_10007": "None",
"customfield_10008": "None",
"aggregatetimeestimate": "None",
"customfield_10009": "None",
"summary": "Setup infra",
"creator": {
"self": "https://myaccount.atlassian.net/rest/api/3/user?accountId=712020%3A05acda87-42da-44d8-b21e-f71a508e5d11",
"accountId": "712020:05acda87-42da-44d8-b21e-f71a508e5d11",
"emailAddress": "[email protected]",
"avatarUrls": {
"48x48": "https://secure.gravatar.com/avatar/0d5d34ceb820d324d69046a1b2f51dc0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIC-3.png",
"24x24": "https://secure.gravatar.com/avatar/0d5d34ceb820d324d69046a1b2f51dc0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIC-3.png",
"16x16": "https://secure.gravatar.com/avatar/0d5d34ceb820d324d69046a1b2f51dc0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIC-3.png",
"32x32": "https://secure.gravatar.com/avatar/0d5d34ceb820d324d69046a1b2f51dc0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIC-3.png"
},
"displayName": "User Name",
"active": true,
"timeZone": "UTC",
"accountType": "atlassian"
},
"subtasks": [],
"reporter": {
"self": "https://myaccount.atlassian.net/rest/api/3/user?accountId=712020%3A05acda87-42da-44d8-b21e-f71a508e5d11",
"accountId": "712020:05acda87-42da-44d8-b21e-f71a508e5d11",
"emailAddress": "[email protected]",
"avatarUrls": {
"48x48": "https://secure.gravatar.com/avatar/0d5d34ceb820d324d69046a1b2f51dc0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIC-3.png",
"24x24": "https://secure.gravatar.com/avatar/0d5d34ceb820d324d69046a1b2f51dc0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIC-3.png",
"16x16": "https://secure.gravatar.com/avatar/0d5d34ceb820d324d69046a1b2f51dc0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIC-3.png",
"32x32": "https://secure.gravatar.com/avatar/0d5d34ceb820d324d69046a1b2f51dc0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIC-3.png"
},
"displayName": "User Name",
"active": true,
"timeZone": "UTC",
"accountType": "atlassian"
},
"aggregateprogress": {
"progress": 0,
"total": 0
},
"customfield_10001": "None",
"customfield_10002": "None",
"customfield_10003": "None",
"customfield_10004": "None",
"environment": "None",
"duedate": "None",
"progress": {
"progress": 0,
"total": 0
},
"votes": {
"self": "https://myaccount.atlassian.net/rest/api/3/issue/PA-1/votes",
"votes": 0,
"hasVoted": false
}
}
}
映射结果
结合样本有效载荷和 Ocean 配置,可生成以下 Port 实体:
Project entity in Port
{
"identifier": "PA",
"title": "Port-AI",
"icon": null,
"blueprint": "jiraProject",
"team": [],
"properties": {
"url": "https://myaccount.atlassian.net/projects/PA"
},
"relations": {},
"createdAt": "2023-11-06T11:22:05.433Z",
"createdBy": "hBx3VFZjqgLPEoQLp7POx5XaoB0cgsxW",
"updatedAt": "2023-11-06T11:22:05.433Z",
"updatedBy": "hBx3VFZjqgLPEoQLp7POx5XaoB0cgsxW"
}
Issue entity in Port
{
"identifier": "PA-1",
"title": "Setup infra",
"icon": null,
"blueprint": "jiraIssue",
"team": [],
"properties": {
"url": "https://myaccount.atlassian.net/browse/PA-1",
"status": "To Do",
"issueType": "Task",
"components": [],
"assignee": "User Name",
"reporter": "User Name",
"creator": "User Name",
"priority": "3",
"created": "2023-11-06T11:02:59.000+0000",
"updated": "2023-11-06T11:03:18.244+0000"
},
"relations": {
"parentIssue": null,
"project": "PA",
"subtasks": []
},
"createdAt": "2023-11-06T11:22:07.550Z",
"createdBy": "hBx3VFZjqgLPEoQLp7POx5XaoB0cgsxW",
"updatedAt": "2023-11-06T11:22:07.550Z",
"updatedBy": "hBx3VFZjqgLPEoQLp7POx5XaoB0cgsxW"
}
通过 webhook 进行替代安装
虽然上述 Ocean 集成是推荐的安装方法,但您可能更喜欢使用 webhook 从 Jira 引用数据。 如果是这样,请使用以下说明:
Webhook installation (click to expand)
在本示例中,您将在Jira 和 Port 之间创建一个 webhook 集成,用于接收 Jira 问题实体。
Port configuration
创建以下蓝图定义:
Jira issue blueprint
{
"identifier": "jiraIssue",
"description": "This blueprint represents issues from Jira",
"title": "Jira Issue",
"icon": "Jira",
"schema": {
"properties": {
"summary": {
"type": "string",
"title": "Summary"
},
"description": {
"type": "string",
"title": "Description"
},
"lastChangeType": {
"type": "string",
"title": "Last change type",
"description": "The type of the latest change made to this issue"
},
"changingUser": {
"type": "string",
"title": "Changing user",
"description": "The user that made the latest change"
},
"issueUrl": {
"type": "string",
"title": "Issue URL",
"format": "url",
"description": "URL to the issue"
},
"issueType": {
"type": "string",
"title": "Type",
"description": "The type of the issue"
},
"status": {
"type": "string",
"title": "Status",
"description": "The status of the issue"
}
},
"required": []
},
"mirrorProperties": {},
"calculationProperties": {},
"relations": {}
}
创建以下 webhook 配置using Port's UI
Jira issue webhook configuration
- 基本信息 选项卡 - 填写以下详细信息:
1.title:
Jira mapper
; 2.标识符 :jira_mapper
; 3.Description :将 Jira 问题映射到 Port
的 webhook 配置; 4.图标 :Jira
; - 集成配置选项卡 - 填写以下 JQ 映射:
[
{
"blueprint": "jiraIssue",
"entity": {
"identifier": ".body.issue.key",
"title": ".body.issue.key + \" - \" + .body.issue.fields.summary",
"properties": {
"summary": ".body.issue.fields.summary",
"description": ".body.issue.fields.description",
"status": ".body.issue.fields.status.name",
"lastChangeType": ".body.issue_event_type_name",
"changingUser": ".body.user.displayName",
"issueUrl": "(.body.issue.self | split(\"rest\") | first) + \"browse/\" + .body.issue.key",
"issueType": ".body.issue.fields.issuetype.name"
}
}
}
]
3.单击页面底部的保存。
Create a webhook in Jira
- 以具有管理 Jira 全局权限的用户身份登录 Jira;
- 单击右上角的齿轮图标;
- 选择 系统;
- 在左侧边栏底部的高级下,选择Webhooks;
- 点击创建 Webhook
- 输入以下详细信息:
- 名称"- 被用于一个有意义的名称,如 Port Webhook;
- 状态"--请确保网络钩子已启用;
- Webhook URL
- 输入创建 Webhook 配置后收到的
url` 键的值; Description
- 输入 webhook 的描述; 5.问题相关事件"- 在此部分输入 JQL 查询,以筛选发送到 webhook 的问题(如果此字段为空,则所有问题都将触发 webhook 事件); 6.问题 "下 - 标记创建、更新和删除; 7.单击页面底部的创建。
完成!您对问题所做的任何更改(打开、关闭、编辑等)都会触发 webhook 事件,Jira 会将该事件发送到 Port 提供的 webhook URL。 Port 会根据映射解析事件,并相应地更新目录实体。
Let's Test It
本节包括创建或更新问题时从 Jira 发送的 webhook 事件示例。 此外,还包括根据上一节提供的 webhook 配置从事件中创建的实体。
Payload
下面是创建 Jira 问题时发送到 webhook URL 的有效载荷结构示例:
Webhook event payload
{
"timestamp": 1686916266116,
"webhookEvent": "jira:issue_created",
"issue_event_type_name": "issue_created",
"user": {
"self": "https://account.atlassian.net/rest/api/2/user?accountId=557058%3A69f39959-769f-4dac-8a7a-46eb55b03723",
"accountId": "557058%3A69f39959-769f-4dac-8a7a-46eb55b03723",
"avatarUrls": {
"48x48": "https://secure.gravatar.com/avatar/9df2ac1caa70b0a67ff0561f7d0363e5?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIC-1.png"
},
"displayName": "Your Name",
"active": true,
"timeZone": "Europe/London",
"accountType": "atlassian"
},
"issue": {
"id": "10000",
"self": "https://account.atlassian.net/rest/api/2/10000",
"key": "PI-1",
"fields": {
"statuscategorychangedate": "2023-06-16T11:51:06.277+0000",
"issuetype": {
"self": "https://account.atlassian.net/rest/api/2/issuetype/10002",
"id": "10002",
"description": "Epics track collections of related bugs, stories, and tasks.",
"iconUrl": "https://account.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10307?size=medium",
"name": "Epic",
"subtask": false,
"avatarId": 10307,
"entityId": "66c6d416-6eb4-4b38-92fa-9a7d68c64165",
"hierarchyLevel": 1
},
"timespent": "None",
"project": {
"self": "https://account.atlassian.net/rest/api/2/project/10000",
"id": "10000",
"key": "PI",
"name": "Port Integration",
"projectTypeKey": "software",
"simplified": true,
"avatarUrls": {
"48x48": "https://account.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10413"
}
},
"fixVersions": [],
"aggregatetimespent": "None",
"resolution": "None",
"resolutiondate": "None",
"workratio": -1,
"watches": {
"self": "https://account.atlassian.net/rest/api/2/issue/PI-1/watchers",
"watchCount": 0,
"isWatching": false
},
"issuerestriction": {
"issuerestrictions": {},
"shouldDisplay": true
},
"lastViewed": "None",
"created": "2023-06-16T11:51:05.291+0000",
"priority": {
"self": "https://account.atlassian.net/rest/api/2/priority/3",
"iconUrl": "https://account.atlassian.net/images/icons/priorities/medium.svg",
"name": "Medium",
"id": "3"
},
"labels": ["cloud", "infra"],
"issuelinks": [],
"assignee": {
"self": "https://account.atlassian.net/rest/api/2/user?accountId=557058%3A69f39947-769f-4dac-8a7a-46eb55b03705",
"accountId": "557058:69f39947-769f-4dac-8a7a-46eb55b03705",
"avatarUrls": {
"48x48": "https://secure.gravatar.com/avatar/9df2ac1caa70b0a67ff0561f7d0363e5?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIC-1.png"
},
"displayName": "Your Name",
"active": true,
"timeZone": "Europe/London",
"accountType": "atlassian"
},
"updated": "2023-06-16T11:51:05.291+0000",
"status": {
"self": "https://account.atlassian.net/rest/api/2/status/10000",
"description": "",
"iconUrl": "https://account.atlassian.net/",
"name": "To Do",
"id": "10000",
"statusCategory": {
"self": "https://account.atlassian.net/rest/api/2/statuscategory/2",
"id": 2,
"key": "new",
"colorName": "blue-gray",
"name": "New"
}
},
"components": [],
"timeoriginalestimate": "None",
"description": "We need to migrate our current infrastructure from in-house to the cloud",
"attachment": [],
"summary": "Migrate Infra to Cloud",
"creator": {
"self": "https://account.atlassian.net/rest/api/2/user?accountId=557058%3A69f39947-769f-4dac-8a7a-46eb55b03705",
"accountId": "557058:69f39947-769f-4dac-8a7a-46eb55b03705",
"avatarUrls": {
"48x48": "https://secure.gravatar.com/avatar/9df2ac1caa70b0a67ff0561f7d0363e5?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIC-1.png"
},
"displayName": "Your Name",
"active": true,
"timeZone": "Europe/London",
"accountType": "atlassian"
},
"subtasks": [],
"reporter": {
"self": "https://account.atlassian.net/rest/api/2/user?accountId=557058%3A69f39947-769f-4dac-8a7a-46eb55b03705",
"accountId": "557058:69f39947-769f-4dac-8a7a-46eb55b03705",
"avatarUrls": {
"48x48": "https://secure.gravatar.com/avatar/9df2ac1caa70b0a67ff0561f7d0363e5?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIC-1.png"
},
"displayName": "Your Name",
"active": true,
"timeZone": "Europe/London",
"accountType": "atlassian"
},
"aggregateprogress": {
"progress": 0,
"total": 0
},
"environment": "None",
"duedate": "2023-06-19",
"progress": {
"progress": 0,
"total": 0
},
"votes": {
"self": "https://account.atlassian.net/rest/api/2/issue/PI-1/votes",
"votes": 0,
"hasVoted": false
}
}
},
"changelog": {
"id": "10001",
"items": [
{
"field": "status",
"fieldtype": "jira",
"fieldId": "status",
"from": "10000",
"fromString": "To Do",
"to": "10001",
"toString": "In Progress"
}
]
}
}
Mapping Result
结合示例有效载荷和 webhook 配置可生成以下 Port 实体:
{
"identifier": "PI-1",
"title": "PI-1 - Migrate Infra to Cloud",
"blueprint": "jiraIssue",
"properties": {
"summary": "Migrate Infra to Cloud",
"description": "We need to migrate our current infrastructure from in-house to the cloud",
"status": "To Do",
"lastChangeType": "issue_created",
"changingUser": "Your Name",
"issueUrl": "https://account.atlassian.net/browse/PI-1",
"issueType": "Epic"
},
"relations": {}
}
Import Jira Historical Issues
在本示例中,您将使用 Provider 提供的 Python 脚本从 Jira API 获取数据并将其引用到 Port。
Prerequisites
本示例使用的是上一节中的blueprint and webhook 定义。
此外,它还需要一个 Jira API 标记,作为参数提供给 Python 脚本
Create the Jira API token
- 登录Jira account ;
- 单击创建 API 标记;
- 在出现的对话框中,为您的令牌输入一个简洁易记的标签,然后单击 创建;
- 单击复制将令牌复制到剪贴板,离开此页面后将不会再有机会查看令牌值;
使用以下 Python 脚本将历史 Jira 问题引用到 Port:
Jira Python script for historical issues
# Dependencies to Install
# pip install jira
# pip install requests
from jira import JIRA
import requests
import json
import os
def authenticate_with_jira(server, username, api_token):
# This function authenticates with JIRA and returns a JIRA object
return JIRA(server=server, basic_auth=(username, api_token))
def send_to_port(data, port_url):
# This function sends issue data to Port
headers = {'Content-Type': 'application/json'}
response = requests.post(port_url, headers=headers, data=json.dumps(data))
# Check the response from Port
if response.status_code != 202:
print(f"Error sending issue {data['issue']['key']} to Port: {response.status_code}")
else:
print(f"Issue {data['issue']['key']} successfully sent to Port.")
return response
def process_issues(issues):
# This function processes the issues from JIRA
total_issues_processed = 0
for issue in issues:
try:
description = issue.fields.description if issue.fields.description else None
data_to_send = {
"issue": {
"key": issue.key,
"fields": {
"summary": issue.fields.summary,
"description": description,
"status": {"name": issue.fields.status.name},
"issuetype": {"name": issue.fields.issuetype.name},
"self": issue.self,
},
"lastChangeType": "",
"self": issue.self,
},
"user": {"displayName": issue.fields.creator.displayName},
}
response = send_to_port(data_to_send, os.getenv('PORT_URL')) ## Your WEBHOOK_URL after creating the Port Webhook
if response.status_code == 202:
total_issues_processed += 1
except Exception as e:
print(f"Error processing issue {issue.key}: {str(e)}")
return total_issues_processed
def main():
jira_server = os.getenv('JIRA_SERVER') ## This is your Jira Domain https://{your_domain}.atlassian.net
username = os.getenv('USERNAMES')
api_token = os.getenv('API_TOKEN')
status_list = ["In Progress", "To Do"]
status = ','.join(f'"{s}"' for s in status_list)
jira = authenticate_with_jira(jira_server, username, api_token)
start_at = 0
max_results = 20
total_issues_processed = 0
while True:
issues = jira.search_issues(f'status in ({status})', startAt=start_at, maxResults=max_results)
if not issues:
break
total_issues_processed += process_issues(issues)
start_at += max_results
print(f"Total issues processed: {total_issues_processed}")
if __name__ == "__main__":
main()
脚本需要以下环境变量
PORT_URL
- 创建 webhook 配置后由 Port 生成的 webhook URL;JIRA_SERVER
- 您的 Jira 域名,例如https://{YOUR_DOMAIN}.atlassian.net
;USERNAMES
- 您的 Jira 用户名;API_TOKEN
- 您的 Jira API 令牌(在上一步中创建)。
完成!现在您可以将历史问题从 Jira 导入 Port。 Port 将根据映射解析问题,并相应地更新目录实体。