Published: Dec 28, 2023 by Isaac Johnson
Let’s build on what we did a couple days ago and look into Plane.so Github Syncing, JIRA Syncing and the REST API.
We’re going to try and get issues to sync out of a private Github repo I use today to hold Blog issues, a similar Atlassian hosted JIRA instance and lastly, we’ll setup the API in a Github workflow to show REST-based issue creation.
Github Sync
Perhaps the most exciting feature I wanted to see was a Github sync. Today, I can easily mirror my repo contents, and that pulls the code into Forgejo or Gitea. However, my issues are always a sticking point.
To get started, we can go to either “Organization/Settings” or if just using a free account, “Workspaces/Settings”
Under Integrations, we’ll see (presently) two options - Slack and Github. Click “Install” on Github
We can now pick all or some repos to allow Plane.so to query and update Issues, PRs and Repo Hooks
After I confirm my identity with Github, we return to Plane.so to see it’s now installed
The first project I went to, the Blog Posts one I created last time, I could not see any Repositories listed under Integrations/Github
My next attempt was to create a new project for the purpose of syncing
However, it too was devoid of Repos
I thought, perhaps I should break from their steps and try the “Import” back in workspace settings
This started a wizard
I next picked my new Project and flipped the toggle for “Sync Issues”
However, the “next” was not clickable and I got a clear “you can’t go on” mouse icon
I’m a bit stumped as even the current plan (Plane Free) shows All Integrations listed
Uninstall also seems to fail
Update on SaaS Github
I reached out to the Developers via Discord and evidently, they are working on some updates. This will likely be fixed by the time you read this.
JIRA
Let’s see if we have any better luck with JIRA.
I’ll try Import from JIRA next
Here we can see I have some issues, mostly as imported via the Feedback form at the top
I can now use those settings in the Import section of Plane
It clearly sees the issues and types. I’ll review and click “Next”
In my case, I’ll need to map JIRA Users to Plane.so users which I handle in the next screen
My last step is to confirm and import
Here we now see a processing notification show up in the import area
Which quickly changes to completed
This is awesome. I can clearly see all the issues synced over!
If I pick on issue to checkout, we can see a User ask that was filed on March 5th:
Turned into BLOGP-27 with a referential link back to JIRA in the “Links” section
REST API
We can check out the API Documentation here to get started.
Before I do that, however, I need to get an API token I can use with it. We can find that under Workspace/settings in API Tokens.
I’ll click “Add API token” and a dialogue pops up lower in the screen to set things like title and date to expire
It auto-downloads a CSV and shows it once
I can now see it in the Tokens page
If you try and use the self-hosted URLs, such as
builder@DESKTOP-QADGF36:~$ curl -X GET -H 'X-API-Key: plane_api_xxxxxxxxxxxxxxxxxxxxx' -H 'Content-Type: application/json' https://app.plane.so/api/v1/workspaces/tpk/project/9ca799e6-52c4-4a9e-8b40-461eef4f57e9/issues/
Redirecting...
builder@DESKTOP-QADGF36:~$ curl -X GET -H 'X-API-Key: plane_api_xxxxxxxxxxxxxxxxxxxxx' -H 'Content-Type: application/json' https://app.plane.so/tpk/api/v1/workspaces/tpk/project/9ca799e6-52c4-4a9e-8b40-461eef4f57e9/issues/
Redirecting...
builder@DESKTOP-QADGF36:~$ curl -X GET -H 'X-API-Key: plane_api_xxxxxxxxxxxxxxxxxxxxx' -H 'Content-Type: application/json' https://plane.so/api/v1/workspaces/tpk/project/9ca799e6-52c4-4a9e-8b40-461eef4f57e9/issuesa/
Redirecting...
you will likely see “Redirecting…” which goes to
For the SaaS option (plane.so) we use https://api.plane.so
.
E.g.
$ curl builder@DESKTOP-QADGF3builder@DESbuilder@DESbuilder@DESbuilder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ curl -X GET -H 'X-API-Key: plane_api_xxxxxxxxxxxxxx' -H 'Content-Type: application/json' https://api.plane.so/api/v1/workspaces/tpk/projects/ | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2164 0 2164 0 0 7141 0 --:--:-- --:--:-- --:--:-- 7118
{
"next_cursor": "100:1:0",
"prev_cursor": "100:-1:1",
"next_page_results": false,
"prev_page_results": false,
"count": 2,
"total_pages": 1,
"extra_stats": null,
"results": [
{
"id": "1ded0aff-c785-4e58-954d-e899dda20129",
"total_members": 1,
"total_cycles": 0,
"total_modules": 0,
"is_member": true,
"sort_order": 55535,
"member_role": 20,
"is_deployed": false,
"created_at": "2023-12-20T06:07:35.024774-06:00",
"updated_at": "2023-12-20T06:07:35.024793-06:00",
"name": "Jekyll-Blog",
"description": "A Sync with my Jekyll-Blog Github Repo",
"description_text": null,
"description_html": null,
"network": 0,
"identifier": "JEKYL",
"emoji": "128076",
"icon_prop": null,
"module_view": true,
"cycle_view": true,
"issue_views_view": true,
"page_view": true,
"inbox_view": false,
"cover_image": "https://images.unsplash.com/photo-1464925257126-6450e871c667?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80",
"archive_in": 0,
"close_in": 0,
"created_by": "23ab4dc2-0c58-4183-a63a-1e8eab4c3e8b",
"updated_by": "23ab4dc2-0c58-4183-a63a-1e8eab4c3e8b",
"workspace": "4dab1b78-d62a-481b-8c9e-f2af8648e1cd",
"default_assignee": null,
"project_lead": null,
"estimate": null,
"default_state": null
},
{
"id": "9ca799e6-52c4-4a9e-8b40-461eef4f57e9",
"total_members": 3,
"total_cycles": 0,
"total_modules": 0,
"is_member": true,
"sort_order": 65535,
"member_role": 20,
"is_deployed": false,
"created_at": "2023-12-15T05:47:09.997318-06:00",
"updated_at": "2023-12-15T05:47:09.997344-06:00",
"name": "Blog Posts",
"description": "Blog Posts on Freshbrewed.science",
"description_text": null,
"description_html": null,
"network": 2,
"identifier": "BLOGP",
"emoji": "9935",
"icon_prop": null,
"module_view": true,
"cycle_view": true,
"issue_views_view": true,
"page_view": true,
"inbox_view": false,
"cover_image": "https://images.unsplash.com/photo-1531045535792-b515d59c3d1f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80",
"archive_in": 0,
"close_in": 0,
"created_by": "23ab4dc2-0c58-4183-a63a-1e8eab4c3e8b",
"updated_by": "23ab4dc2-0c58-4183-a63a-1e8eab4c3e8b",
"workspace": "4dab1b78-d62a-481b-8c9e-f2af8648e1cd",
"default_assignee": null,
"project_lead": null,
"estimate": null,
"default_state": null
}
]
}
Which means we can use the Project ID to then fetch the issues in our project
$ curl -X GET -H 'X-API-Key: plane_api_xxxxxxxxxxxxxxxxxx' -H 'Content-Type: application/json' https://api.plane.so/api/v1/workspaces/tpk/projects/9ca799e6-52c4-4a9e-8b40-461eef4f57e9/issues/ | jq '.results[] | .name'
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 25172 0 25172 0 0 82802 0 --:--:-- --:--:-- --:--:-- 82802
"A Quick Story"
"MyDockerIssue"
"One Last Test"
"this is the summary"
"One Last Test"
"This is a final test"
"Your blog is great but no rss feed or media :("
"This is a Test Issue"
"This is a NEW test"
"This is a test"
"PST: STP: This is a server to ping test"
"Some Non Server"
"STP: ping a host"
"STP: ping a host"
"STP: ping a host"
"STP: ping a host"
"STP: Extreme Home Health"
"STP: Ping a Host Again"
"STP: ping a host 3"
"STP: Test again"
"STP: Last test"
"STP: Test again 5"
"STP: Test again 8"
"STP: Yet Another Test"
"STP: One Last Test"
"Awesome"
"test"
"qsq"
"Write about Plane.so"
My next goal is to actually create an Issue with the REST API.
From the Issues API docs it should just be a POST to /api/v1/workspaces/:slug/projects/:project_id/issues/
Most of the fields are optional. Here we can see a basic Issue object
$ cat issueObj.json
{
"estimate_point": null,
"name": "My REST Issue",
"description_html": "<p>This is just some issue from a curl command</p>",
"description_stripped": "This is just some issue from a curl command",
"priority": "none",
"is_draft": false,
"labels": []
}
The we can POST it
$ curl -X POST -H 'X-API-Key: plane_api_xxxxxxxxxxxxxxx' -H 'Content-Type: application/json' https://api.plane.so/api/v1/workspaces/tpk/projects/9ca799e6-52c4-4a9e-8b40-461eef4f57e9/issues/ --data @issueObj.json
{"id":"9ddc84a8-cdd0-4370-9b5b-c81b80dee7b5","created_at":"2023-12-21T14:19:02.066920-06:00","updated_at":"2023-12-21T14:19:02.066937-06:00","estimate_point":null,"name":"My REST Issue","description_html":"<p>This is just some issue from a curl command</p>","priority":"none","start_date":null,"target_date":null,"sequence_id":30,"sort_order":355535.0,"completed_at":null,"archived_at":null,"is_draft":false,"created_by":"23ab4dc2-0c58-4183-a63a-1e8eab4c3e8b","updated_by":"23ab4dc2-0c58-4183-a63a-1e8eab4c3e8b","project":"9ca799e6-52c4-4a9e-8b40-461eef4f57e9","workspace":"4dab1b78-d62a-481b-8c9e-f2af8648e1cd","parent":null,"state":"6ad12d87-1cff-4544-9c58-cc0ca8489571","assignees":[],"labels":[]}
and then the resulting issue via the Web UI
I’m interested in the way labels work.
I updated the label to “UserAsk”
Then I fetched the issue to see how it changed
$ curl -X GET -H 'X-API-Key: plane_api_xxxxxxxxxxxxxxxxxxxx' -H 'Content-Type: application/json' https://api.plane.so/api/v1/workspaces/tpk/projects/9ca799e6-52c4-4a9e-8b40-461eef4f57e9/issues/9ddc84a8-cdd0-4370-9b5b-c81b80dee7b5/ | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 738 0 738 0 0 2975 0 --:--:-- --:--:-- --:--:-- 2963
{
"id": "9ddc84a8-cdd0-4370-9b5b-c81b80dee7b5",
"created_at": "2023-12-21T14:19:02.066920-06:00",
"updated_at": "2023-12-22T06:02:08.669038-06:00",
"estimate_point": null,
"name": "My REST Issue",
"description_html": "<p>This is just some issue from a curl command</p>",
"priority": "none",
"start_date": null,
"target_date": null,
"sequence_id": 30,
"sort_order": 355535,
"completed_at": null,
"archived_at": null,
"is_draft": false,
"created_by": "23ab4dc2-0c58-4183-a63a-1e8eab4c3e8b",
"updated_by": "23ab4dc2-0c58-4183-a63a-1e8eab4c3e8b",
"project": "9ca799e6-52c4-4a9e-8b40-461eef4f57e9",
"workspace": "4dab1b78-d62a-481b-8c9e-f2af8648e1cd",
"parent": null,
"state": "6ad12d87-1cff-4544-9c58-cc0ca8489571",
"assignees": [],
"labels": [
"93fcb710-94bf-4e8c-b419-9e6dfbee660f"
]
}
Integration with Github
Since I presently load GH Issues and JIRA with new feedback form data, why not Plane.so?
My first step is to put the Key in AKV for storage and usage later:
$ az keyvault secret set --vault-name wldemokv --name planesotoken --value plane_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
{
"attributes": {
"created": "2023-12-21T20:27:12+00:00",
"enabled": true,
"expires": null,
"notBefore": null,
"recoverableDays": 90,
"recoveryLevel": "Recoverable+Purgeable",
"updated": "2023-12-21T20:27:12+00:00"
},
"contentType": null,
"id": "https://wldemokv.vault.azure.net/secrets/planesotoken/5c35f83bf5894464a8d47a3af7dc6ba4",
"kid": null,
"managed": null,
"name": "planesotoken",
"tags": {
"file-encoding": "utf-8"
},
"value": "plane_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
We’ll then look at our Github Workflow in the workflowTriggerTest repo.
We’ll add block for Plane.so. If you don’t want labels added as I do, you can leave that set to "labels": []
- name: 'Plane.so Create'
run: |
az keyvault secret show --vault-name wldemokv --name planesotoken -o json | jq -r .value > planesotoken
# Create in Hosted Atlassian
cat >planeJSON <<EOT
{
"estimate_point": null,
"name": "$",
"description_html": "<p>$ :: Requested by $</p>",
"description_stripped": "$ :: Requested by $",
"priority": "none",
"is_draft": false,
"labels": [
"93fcb710-94bf-4e8c-b419-9e6dfbee660f"
]
}
EOT
curl --request POST --url "https://api.plane.so/api/v1/workspaces/tpk/projects/9ca799e6-52c4-4a9e-8b40-461eef4f57e9/issues/" -H "X-API-Key: `cat planesotoken | tr -d '\n'`" -H 'Content-Type: application/json' --header 'Accept: application/json' -d @planeJSON
I pushed right to main, so this should be live. We can test by requesting a feature.
While the user just sees a little Success page response
I end up seeing a build light come on when the feedback form action is running
The workflow looks good
Let’s check our Plane.so project. I can see the new issue with requestor details!
Since this flow populates Github and JIRA as well, we can see the Github Issue
and JIRA ticket
One of the struggles I have, however, is how to show them publicly. They are all in, ultimately, private places:
I feel like we could go a bit farther, but we’ll save that for next time
Summary
Today we continued from our first post and tried, though did not succeed in getting Github Issue syncing to work. We did, however, get JIRA to sync without troubles. We then moved on to the REST API and used it to update a Github Workflow triggered by a static form.
If you want to read more on the Feedback forms, I have a few articles written about how that was setup:
- Mar 7, 2023: Feedback Forms: JIRA, Discord, Keybase, Teams and Datadog
- Mar 3, 2023: Feedback Forms: Moving to triggered GH Actions
- Feb 28, 2023: Feedback Forms: Azure Work Items and Github Issues
- June 24, 2021: Feedback Forms to AzDO: Adding Email
- June 23, 2021: Feedback Forms to AzDO Work Items
In the next two weeks I have planned two more blog posts where we’re going to dig into the REST API with a containerized python based report service, so stay tuned!