diff --git a/README.md b/README.md index af81d5e..738ef03 100644 --- a/README.md +++ b/README.md @@ -48,22 +48,23 @@ jobs: automation: runs-on: ubuntu-latest steps: - - name: Add Jira info - uses: contractify/add-jira-info@v1 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - jira-base-url: ${{ secrets.JIRA_BASE_URL }} - jira-username: ${{ secrets.JIRA_USERNAME }} - jira-token: ${{ secrets.JIRA_TOKEN }} - jira-project-key: PRJ - add-label-with-issue-type: true - issue-type-label-color: FBCA04 - issue-type-label-description: 'Jira Issue Type' - add-jira-key-to-title: true - add-jira-key-to-body: true + - name: Add Jira info + uses: contractify/add-jira-info@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + jira-base-url: ${{ secrets.JIRA_BASE_URL }} + jira-username: ${{ secrets.JIRA_USERNAME }} + jira-token: ${{ secrets.JIRA_TOKEN }} + jira-project-key: PRJ + add-label-with-issue-type: true + issue-type-label-color: FBCA04 + issue-type-label-description: "Jira Issue Type" + add-jira-key-to-title: true + add-jira-key-to-body: true + add-jira-fix-versions-to-body: true ``` -The `on:` section defines when the workflow needs to run. We ussually run them +The `on:` section defines when the workflow needs to run. We usually run them on everything that has to do with a pull request. We also use `workflow_dispatch` to allow us to manually trigger the workflow. @@ -76,18 +77,19 @@ We strongly suggest to store the sensitive configuration parameters as Various inputs are defined in [`action.yml`](action.yml) to let you configure the action: -| Name | Description | Required | Default | -| - | - | - | - | -| `github-token` | Token to use to authorize label changes. Typically the GITHUB_TOKEN secret, with `contents:read` and `pull-requests:write` access | N/A | -| `jira-base-url` | The subdomain of JIRA cloud that you use to access it. Ex: "https://your-domain.atlassian.net". | `true` | `null` | -| `jira-username` | Username used to fetch Jira Issue information. Check [below](#how-to-get-the-jira-token-and-jira-username) for more details on how to generate the token. | `true` | `null` | -| `jira-token` | Token used to fetch Jira Issue information. Check [below](#how-to-get-the-jira-token-and-jira-username) for more details on how to generate the token. | `true` | `null` | -| `jira-project-key` | Key of project in jira. First part of issue key | `true` | `null` | -| `add-label-with-issue-type` | If set to `true`, a label with the issue type from Jira will be added to the pull request | `false` | `true` | -| `issue-type-label-color` | The hex color to use for the issue type label | `false` | `FBCA04` | -| `issue-type-label-description` | The description to use for the issue type label | `false` | `Jira Issue Type` | -| `add-jira-key-to-title` | If set to `true`, the title of the pull request will be prefixed with the Jira issue key | `false` | `true` | -| `add-jira-key-to-body` | If set to `true`, the body of the pull request will be suffix with a link to the Jira issue | `false` | `true` | +| Name | Description | Required | Default | +| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------- | +| `github-token` | Token to use to authorize label changes. Typically the GITHUB_TOKEN secret, with `contents:read` and `pull-requests:write` access | N/A | +| `jira-base-url` | The subdomain of JIRA cloud that you use to access it. Ex: "https://your-domain.atlassian.net". | `true` | `null` | +| `jira-username` | Username used to fetch Jira Issue information. Check [below](#how-to-get-the-jira-token-and-jira-username) for more details on how to generate the token. | `true` | `null` | +| `jira-token` | Token used to fetch Jira Issue information. Check [below](#how-to-get-the-jira-token-and-jira-username) for more details on how to generate the token. | `true` | `null` | +| `jira-project-key` | Key of project in jira. First part of issue key | `true` | `null` | +| `add-label-with-issue-type` | If set to `true`, a label with the issue type from Jira will be added to the pull request | `false` | `true` | +| `issue-type-label-color` | The hex color to use for the issue type label | `false` | `FBCA04` | +| `issue-type-label-description` | The description to use for the issue type label | `false` | `Jira Issue Type` | +| `add-jira-key-to-title` | If set to `true`, the title of the pull request will be prefixed with the Jira issue key | `false` | `true` | +| `add-jira-key-to-body` | If set to `true`, the body of the pull request will be suffix with a link to the Jira issue | `false` | `true` | +| `add-jira-fix-versions-to-body` | If set to `true`, the body of the pull request will be suffix with the `fixVersions` from to the Jira issue | `false` | `true` | Tokens are private, so it's suggested adding them as [GitHub secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets). @@ -155,6 +157,7 @@ It will automatically be assigned to the pull request. ## How to get the `jira-token` and `jira-username` The Jira token is used to fetch issue information via the Jira REST API. To get the token: + 1. Generate an [API token via JIRA](https://confluence.atlassian.com/cloud/api-tokens-938839638.html) 2. Add the Jira username to the `JIRA_USERNAME` secret in your project 3. Add the Jira API token to the `JIRA_TOKEN` secret in your project @@ -166,6 +169,7 @@ Note: The user should have the [required permissions (mentioned under GET Issue) Contractify is a blooming Belgian SaaS scale-up offering contract management software and services. We help business leaders, legal & finance teams to + - 🗄️ centralize contracts & responsibilities, even in a decentralized organization. - 📝 keep track of all contracts & related mails or documents in 1 tool - 🔔 automate & collaborate on contract follow-up tasks @@ -173,6 +177,7 @@ We help business leaders, legal & finance teams to - 📊 report on custom contract data The cloud platform is easily supplemented with full contract management support, including: + - ✔️ registration and follow up of your existing & new contracts - ✔️ expert advice on contract management - ✔️ periodic reporting & status updates diff --git a/__tests__/jira_client.test.ts b/__tests__/jira_client.test.ts index dcd5d6a..e5b17e3 100644 --- a/__tests__/jira_client.test.ts +++ b/__tests__/jira_client.test.ts @@ -26,7 +26,7 @@ describe("get jira issue type", () => { }; nock("https://base-url") - .get("/rest/api/3/issue/PRJ-123?fields=issuetype,summary") + .get("/rest/api/3/issue/PRJ-123?fields=issuetype,summary,fixVersions") .reply(200, () => response); const issue = await client.getIssue(new JiraKey("PRJ", "123")); @@ -34,6 +34,42 @@ describe("get jira issue type", () => { }); }); +describe("get jira issue fixVersions", () => { + let client: JiraClient; + + beforeEach(() => { + client = new JiraClient("https://base-url", "username", "token", "PRJ"); + }); + + it("gets the fixVersions property of a jira issue", async () => { + const response = { + fields: { + issuetype: { + name: "Story", + }, + summary: "My Issue", + fixVersions: [ + { + description: "", + name: "v1.0.0", + archived: false, + released: false, + releaseDate: "2023-10-31", + }, + ], + }, + }; + + nock("https://base-url") + .get("/rest/api/3/issue/PRJ-123?fields=issuetype,summary,fixVersions") + .reply(200, () => response); + + const issue = await client.getIssue(new JiraKey("PRJ", "123")); + expect(issue?.type).toBe("story"); + expect(issue?.fixVersions![0]).toBe("v1.0.0"); + }); +}); + describe("extract jira key", () => { let client: JiraClient; @@ -43,35 +79,35 @@ describe("extract jira key", () => { it("extracts the jira key if present", () => { const jiraKey = client.extractJiraKey( - "PRJ-3721_actions-workflow-improvements" + "PRJ-3721_actions-workflow-improvements", ); expect(jiraKey?.toString()).toBe("PRJ-3721"); }); it("extracts the jira key if present without underscore", () => { const jiraKey = client.extractJiraKey( - "PRJ-3721-actions-workflow-improvements" + "PRJ-3721-actions-workflow-improvements", ); expect(jiraKey?.toString()).toBe("PRJ-3721"); }); it("extracts the jira key from a feature branch if present", () => { const jiraKey = client.extractJiraKey( - "feature/PRJ-3721_actions-workflow-improvements" + "feature/PRJ-3721_actions-workflow-improvements", ); expect(jiraKey?.toString()).toBe("PRJ-3721"); }); it("extracts the jira key case insensitive", () => { const jiraKey = client.extractJiraKey( - "PRJ-3721_actions-workflow-improvements" + "PRJ-3721_actions-workflow-improvements", ); expect(jiraKey?.toString()).toBe("PRJ-3721"); }); it("returns undefined if not present", () => { const jiraKey = client.extractJiraKey( - "prj3721_actions-workflow-improvements" + "prj3721_actions-workflow-improvements", ); expect(jiraKey).toBeUndefined(); }); diff --git a/__tests__/updater.test.ts b/__tests__/updater.test.ts index c25adde..b364cc5 100644 --- a/__tests__/updater.test.ts +++ b/__tests__/updater.test.ts @@ -72,7 +72,9 @@ describe("body", () => { beforeEach(() => { const jiraKey = new JiraKey("PRJ", "1234"); - const jiraIssue = new JiraIssue(jiraKey, "http://jira", "title", "story"); + const jiraIssue = new JiraIssue(jiraKey, "http://jira", "title", "story", [ + "v1.0.0", + ]); updater = new Updater(jiraIssue); }); @@ -124,4 +126,41 @@ describe("body", () => { const actual = updater.body(body); expect(actual).toBe("PRJ-1234\n\ntest"); }); + + it("adds the fixVersions to an undefined body", () => { + const body = undefined; + + const actual = updater.addFixVersionsToBody(body); + expect(actual).toBe("**Fix versions**: v1.0.0"); + }); + + it("adds the fixVersions to an empty body", () => { + const body = ""; + + const actual = updater.addFixVersionsToBody(body); + expect(actual).toBe("**Fix versions**: v1.0.0"); + }); + + it("adds the fixVersions to an existing body", () => { + const body = "test"; + + const actual = updater.addFixVersionsToBody(body); + expect(actual).toBe("test\n\n**Fix versions**: v1.0.0"); + }); + + it("adds the fixVersions to an existing body with reference to ticket", () => { + const body = "test\n\nReferences PRJ-1234"; + + const actual = updater.addFixVersionsToBody(body); + expect(actual).toBe( + "test\n\nReferences PRJ-1234\n\n**Fix versions**: v1.0.0", + ); + }); + + it("update the fixVersions if the body contains the fixVersions already", () => { + const body = "**Fix versions**: v0.9.9"; + + const actual = updater.addFixVersionsToBody(body); + expect(actual).toBe("**Fix versions**: v1.0.0"); + }); }); diff --git a/action.yml b/action.yml index 1d2403f..c76f551 100644 --- a/action.yml +++ b/action.yml @@ -1,39 +1,43 @@ -name: 'Add Jira info to pull request' -description: 'Automatically add Jira info to a pull request' -author: 'Contractify' +name: "Add Jira info to pull request" +description: "Automatically add Jira info to a pull request" +author: "Contractify" inputs: github-token: - description: 'The GITHUB_TOKEN secret' + description: "The GITHUB_TOKEN secret" jira-username: - description: 'Username used to access the Jira REST API. Must have read access to your Jira Projects & Issues.' + description: "Username used to access the Jira REST API. Must have read access to your Jira Projects & Issues." jira-token: - description: 'API Token used to access the Jira REST API. Must have read access to your Jira Projects & Issues.' + description: "API Token used to access the Jira REST API. Must have read access to your Jira Projects & Issues." required: true jira-base-url: description: 'The subdomain of JIRA cloud that you use to access it. Ex: "https://your-domain.atlassian.net"' required: true jira-project-key: - description: 'Key of project in jira. First part of issue key' + description: "Key of project in jira. First part of issue key" required: true - default: '' + default: "" add-label-with-issue-type: - description: 'If set to true, a label with the issue type from Jira will be added to the pull request' + description: "If set to true, a label with the issue type from Jira will be added to the pull request" default: "true" required: false issue-type-label-color: - description: 'The hex color of the label to use for the issue type' - default: 'FBCA04' + description: "The hex color of the label to use for the issue type" + default: "FBCA04" required: false issue-type-label-description: - description: 'The description of the label to use for the issue type' - default: 'Jira Issue Type' + description: "The description of the label to use for the issue type" + default: "Jira Issue Type" required: false add-jira-key-to-title: - description: 'If set to true, the title of the pull request will be prefixed with the Jira issue key' + description: "If set to true, the title of the pull request will be prefixed with the Jira issue key" default: "true" required: false add-jira-key-to-body: - description: 'If set to true, the body of the pull request will be suffix with a link to the Jira issue' + description: "If set to true, the body of the pull request will be suffix with a link to the Jira issue" + default: "true" + required: false + add-jira-fix-versions-to-body: + description: "If set to `true`, the body of the pull request will be suffix with the `fixVersions` from to the Jira issue" default: "true" required: false @@ -42,5 +46,5 @@ branding: color: green runs: - using: 'node16' - main: 'dist/index.js' + using: "node16" + main: "dist/index.js" diff --git a/src/common/jira_client.ts b/src/common/jira_client.ts index 5111746..a0ebec0 100644 --- a/src/common/jira_client.ts +++ b/src/common/jira_client.ts @@ -2,7 +2,10 @@ import { HttpClient } from "@actions/http-client"; import { BasicCredentialHandler } from "@actions/http-client/lib/auth"; export class JiraKey { - constructor(public project: string, public number: string) {} + constructor( + public project: string, + public number: string, + ) {} toString(): string { return `${this.project}-${this.number}`; @@ -14,7 +17,8 @@ export class JiraIssue { public key: JiraKey, public link: string, public title: string | undefined, - public type: string | undefined + public type: string | undefined, + public fixVersions?: string[], ) {} toString(): string { @@ -29,7 +33,7 @@ export class JiraClient { private baseUrl: string, private username: string, private token: string, - private projectKey: string + private projectKey: string, ) { const credentials = new BasicCredentialHandler(this.username, this.token); @@ -52,18 +56,23 @@ export class JiraClient { async getIssue(key: JiraKey): Promise { try { const res = await this.client.get( - this.getRestApiUrl(`issue/${key}?fields=issuetype,summary`) + this.getRestApiUrl(`issue/${key}?fields=issuetype,summary,fixVersions`), ); const body: string = await res.readBody(); const obj = JSON.parse(body); var issuetype: string | undefined = undefined; var title: string | undefined = undefined; + var fixVersions: string[] | undefined = undefined; for (let field in obj.fields) { if (field === "issuetype") { issuetype = obj.fields[field].name?.toLowerCase(); } else if (field === "summary") { title = obj.fields[field]; + } else if (field === "fixVersions") { + fixVersions = obj.fields[field] + .map(({ name }) => name) + .filter(Boolean); } } @@ -71,7 +80,8 @@ export class JiraClient { key, `${this.baseUrl}/browse/${key}`, title, - issuetype + issuetype, + fixVersions, ); } catch (error: any) { if (error.response) { diff --git a/src/common/updater.ts b/src/common/updater.ts index 04e64ea..d478960 100644 --- a/src/common/updater.ts +++ b/src/common/updater.ts @@ -50,4 +50,27 @@ export class Updater { return `${body}\n\n[**${this.jiraIssue.key}** | ${this.jiraIssue.title}](${this.jiraIssue.link})`.trim(); } + + addFixVersionsToBody(body: string | undefined): string | undefined { + const { fixVersions } = this.jiraIssue; + + if (!fixVersions?.length) { + return body; + } + + if (!body) { + body = ""; + } + + if (body.includes("**Fix versions**:")) { + body = body.replace( + /\*\*Fix versions\*\*:.*$/, + `**Fix versions**: ${fixVersions.join(",")}`, + ); + } else { + body = `${body}\n\n**Fix versions**: ${fixVersions.join(",")}`.trim(); + } + + return body; + } } diff --git a/src/main.ts b/src/main.ts index 99f9070..ea5968f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,7 +18,7 @@ export async function run() { const jiraProjectKey = core.getInput("jira-project-key", { required: true }); const addLabelWithIssueType = core.getBooleanInput( - "add-label-with-issue-type" + "add-label-with-issue-type", ); const issueTypeLabelColor = core.getInput("issue-type-label-color") || "FBCA04"; @@ -26,6 +26,9 @@ export async function run() { core.getInput("issue-type-label-description") || "Jira Issue Type"; const addJiraKeyToTitle = core.getBooleanInput("add-jira-key-to-title"); const addJiraKeyToBody = core.getBooleanInput("add-jira-key-to-body"); + const addJiraFixVersionsToBody = core.getBooleanInput( + "add-jira-fix-versions-to-body", + ); const githubClient = new GithubClient(githubToken); @@ -33,7 +36,7 @@ export async function run() { jiraBaseUrl, jiraUsername, jiraToken, - jiraProjectKey + jiraProjectKey, ); const pullRequest = await githubClient.getPullRequest(); @@ -78,7 +81,7 @@ export async function run() { await githubClient.createLabel( jiraIssue.type, issueTypeLabelDescription, - issueTypeLabelColor + issueTypeLabelColor, ); } @@ -102,6 +105,10 @@ export async function run() { pullRequest.body = updater.body(pullRequest.body); } + if (addJiraFixVersionsToBody) { + pullRequest.body = updater.addFixVersionsToBody(pullRequest.body); + } + core.info(` Updating pull request`); await githubClient.updatePullRequest(pullRequest); }