Plane.so, Open-Source Pjm: Part 2: Integrations and APIs

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”

/content/images/2023/12/planeso2-01.png

Under Integrations, we’ll see (presently) two options - Slack and Github. Click “Install” on Github

/content/images/2023/12/planeso2-02.png

We can now pick all or some repos to allow Plane.so to query and update Issues, PRs and Repo Hooks

/content/images/2023/12/planeso2-03.png

After I confirm my identity with Github, we return to Plane.so to see it’s now installed

/content/images/2023/12/planeso2-04.png

The first project I went to, the Blog Posts one I created last time, I could not see any Repositories listed under Integrations/Github

/content/images/2023/12/planeso2-05.png

My next attempt was to create a new project for the purpose of syncing

/content/images/2023/12/planeso2-06.png

However, it too was devoid of Repos

/content/images/2023/12/planeso2-07.png

I thought, perhaps I should break from their steps and try the “Import” back in workspace settings

/content/images/2023/12/planeso2-08.png

This started a wizard

/content/images/2023/12/planeso2-09.png

I next picked my new Project and flipped the toggle for “Sync Issues”

/content/images/2023/12/planeso2-10.png

However, the “next” was not clickable and I got a clear “you can’t go on” mouse icon

/content/images/2023/12/planeso2-11.png

I’m a bit stumped as even the current plan (Plane Free) shows All Integrations listed

/content/images/2023/12/planeso2-12.png

Uninstall also seems to fail

/content/images/2023/12/planeso2-13.png

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.

/content/images/2023/12/planeso2-39.png

JIRA

Let’s see if we have any better luck with JIRA.

I’ll try Import from JIRA next

/content/images/2023/12/planeso2-14.png

Here we can see I have some issues, mostly as imported via the Feedback form at the top

/content/images/2023/12/planeso2-15.png

I can now use those settings in the Import section of Plane

/content/images/2023/12/planeso2-16.png

It clearly sees the issues and types. I’ll review and click “Next”

/content/images/2023/12/planeso2-17.png

In my case, I’ll need to map JIRA Users to Plane.so users which I handle in the next screen

/content/images/2023/12/planeso2-18.png

My last step is to confirm and import

/content/images/2023/12/planeso2-19.png

Here we now see a processing notification show up in the import area

/content/images/2023/12/planeso2-20.png

Which quickly changes to completed

/content/images/2023/12/planeso2-21.png

This is awesome. I can clearly see all the issues synced over!

/content/images/2023/12/planeso2-22.png

If I pick on issue to checkout, we can see a User ask that was filed on March 5th:

/content/images/2023/12/planeso2-23.png

Turned into BLOGP-27 with a referential link back to JIRA in the “Links” section

/content/images/2023/12/planeso2-24.png

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.

/content/images/2023/12/planeso2-25.png

I’ll click “Add API token” and a dialogue pops up lower in the screen to set things like title and date to expire

/content/images/2023/12/planeso2-26.png

It auto-downloads a CSV and shows it once

/content/images/2023/12/planeso2-27.png

I can now see it in the Tokens page

/content/images/2023/12/planeso2-28.png

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

/content/images/2023/12/planeso2-29.png

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

/content/images/2023/12/planeso2-30.png

I’m interested in the way labels work.

I updated the label to “UserAsk”

/content/images/2023/12/planeso2-31.png

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.

/content/images/2023/12/planeso2-32.png

While the user just sees a little Success page response

/content/images/2023/12/planeso2-33.png

I end up seeing a build light come on when the feedback form action is running

/content/images/2023/12/138731ab-bbdf-4f6d-b101-16a9186289a9.jpg

The workflow looks good

/content/images/2023/12/planeso2-34.png

Let’s check our Plane.so project. I can see the new issue with requestor details!

/content/images/2023/12/planeso2-35.png

Since this flow populates Github and JIRA as well, we can see the Github Issue

/content/images/2023/12/planeso2-36.png

and JIRA ticket

/content/images/2023/12/planeso2-37.png

One of the struggles I have, however, is how to show them publicly. They are all in, ultimately, private places:

/content/images/2023/12/planeso2-38.png

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:

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!

Agile OpenSource Plane

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes