Skip to content

Commit 4764ec1

Browse files
Copilotpelikhan
andauthored
Fix Linear and Jira smoke issue creation (#58632)
* Initial plan * Fix external issue smoke outputs Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Apply remaining changes Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Default Jira base URL in compiler Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Keep Jira default test in sync Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Co-authored-by: Peli de Halleux <pelikhan@users.noreply.github.com>
1 parent 233a703 commit 4764ec1

6 files changed

Lines changed: 46 additions & 10 deletions

File tree

.github/aw/safe-outputs-content.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ description: Safe-output reference for issue, discussion, comment, and pull requ
2525
| `jira-add-comment` | `jira_add_comment` | `issue_key`, `body` |
2626
| `jira-add-label` | `jira_add_label` | `issue_key`, `label` |
2727

28-
Use the Jira-prefixed tool whenever the target is Jira. Unprefixed issue, comment, and label tools target GitHub. The compiler supplies `JIRA_BASE_URL` from `vars.JIRA_BASE_URL` and `JIRA_USER_EMAIL` and `JIRA_API_TOKEN` from same-named secrets; `safe-outputs.env` may override them. Description and comment strings are converted to ADF internally. Label addition is additive and preserves existing labels. Each Jira output supports `max` and `staged`; staged mode sends no HTTP request and does not require credentials.
28+
Use the Jira-prefixed tool whenever the target is Jira. Unprefixed issue, comment, and label tools target GitHub. The compiler supplies `JIRA_BASE_URL` and supplies `JIRA_USER_EMAIL` and `JIRA_API_TOKEN` from same-named secrets; `safe-outputs.env` may override them. Description and comment strings are converted to ADF internally. Label addition is additive and preserves existing labels. Each Jira output supports `max` and `staged`; staged mode sends no HTTP request and does not require credentials.
2929

3030
Jira update, comment, and label operations require a known issue key. Same-run references to an issue created by `jira_create_issue` are not supported. The initial integration does not provide transitions, assignments, custom fields, label removal, JQL, bulk operations, or arbitrary REST calls.
3131

.github/workflows/smoke-issues.lock.yml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

actions/setup/js/linear_create_issue.cjs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ const LINEAR_CREATE_ISSUE = `mutation LinearCreateIssue($input: IssueCreateInput
2222
}
2323
}`;
2424

25+
const LINEAR_RESOLVE_PROJECT = `query ResolveLinearProject($slugId: String!) {
26+
projects(filter: { slugId: { eq: $slugId } }, first: 1) {
27+
nodes {
28+
id
29+
}
30+
}
31+
}`;
32+
2533
async function main(config = {}) {
2634
const teamId = config.team_id;
2735
if (typeof teamId !== "string" || !LINEAR_UUID_PATTERN.test(teamId)) {
@@ -56,7 +64,16 @@ async function main(config = {}) {
5664

5765
const input = { teamId, title, description };
5866
if (projectId) {
59-
input.projectId = projectId;
67+
if (LINEAR_UUID_PATTERN.test(projectId)) {
68+
input.projectId = projectId;
69+
} else {
70+
const projectData = await linearGraphQL(LINEAR_RESOLVE_PROJECT, { slugId: projectId });
71+
const resolvedProjectId = projectData?.projects?.nodes?.[0]?.id;
72+
if (typeof resolvedProjectId !== "string" || !LINEAR_UUID_PATTERN.test(resolvedProjectId)) {
73+
throw new Error(`${ERR_CONFIG}: linear_create_issue could not resolve the configured project ID`);
74+
}
75+
input.projectId = resolvedProjectId;
76+
}
6077
}
6178
const data = await linearGraphQL(LINEAR_CREATE_ISSUE, { input });
6279
const payload = data?.issueCreate;
@@ -72,4 +89,4 @@ async function main(config = {}) {
7289
};
7390
}
7491

75-
module.exports = { LINEAR_CREATE_ISSUE, main };
92+
module.exports = { LINEAR_CREATE_ISSUE, LINEAR_RESOLVE_PROJECT, main };

actions/setup/js/linear_safe_outputs.test.cjs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createRequire } from "module";
33

44
const require = createRequire(import.meta.url);
55
const { LINEAR_GRAPHQL_ENDPOINT, linearGraphQL } = require("./linear_graphql.cjs");
6-
const { LINEAR_CREATE_ISSUE, main: createIssue } = require("./linear_create_issue.cjs");
6+
const { LINEAR_CREATE_ISSUE, LINEAR_RESOLVE_PROJECT, main: createIssue } = require("./linear_create_issue.cjs");
77
const { LINEAR_COMMENT_CREATE, main: addComment } = require("./linear_add_comment.cjs");
88
const { LINEAR_UPDATE_ISSUE, main: updateIssue } = require("./linear_update_issue.cjs");
99

@@ -30,23 +30,28 @@ describe("Linear safe outputs", () => {
3030
});
3131

3232
it("posts fixed GraphQL documents with variables and raw API-key authorization", async () => {
33-
fetch.mockResolvedValue(response({ data: { issueCreate: { success: true, issue: { id: "id", identifier: "ENG-1", title: "Safe title" } } } }));
33+
fetch
34+
.mockResolvedValueOnce(response({ data: { projects: { nodes: [{ id: "a3f91a0b-6d71-4c58-a4bb-72b925bbebc8" }] } } }))
35+
.mockResolvedValueOnce(response({ data: { issueCreate: { success: true, issue: { id: "id", identifier: "ENG-1", title: "Safe title" } } } }));
3436
const handler = await createIssue({ team_id: "9cfb482a-81e3-4154-b5b9-2c805e70a02d", project_id: "810f57a7e383" });
3537
await handler({ title: "Safe title", body: "Detailed hello to @user" });
3638

37-
expect(fetch).toHaveBeenCalledWith(
39+
expect(fetch).toHaveBeenNthCalledWith(
40+
1,
3841
LINEAR_GRAPHQL_ENDPOINT,
3942
expect.objectContaining({
4043
method: "POST",
4144
headers: { "Content-Type": "application/json", Authorization: "linear-secret" },
4245
})
4346
);
44-
const request = JSON.parse(fetch.mock.calls[0][1].body);
47+
const projectRequest = JSON.parse(fetch.mock.calls[0][1].body);
48+
expect(projectRequest).toEqual({ query: LINEAR_RESOLVE_PROJECT, variables: { slugId: "810f57a7e383" } });
49+
const request = JSON.parse(fetch.mock.calls[1][1].body);
4550
expect(request.query).toBe(LINEAR_CREATE_ISSUE);
4651
expect(request.query).not.toContain("Safe title");
4752
expect(request.variables.input).toEqual({
4853
teamId: "9cfb482a-81e3-4154-b5b9-2c805e70a02d",
49-
projectId: "810f57a7e383",
54+
projectId: "a3f91a0b-6d71-4c58-a4bb-72b925bbebc8",
5055
title: "Safe title",
5156
description: "Detailed hello to `@user`",
5257
});

pkg/constants/tool_constants.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package constants
33
const (
44
LinearMCPReadOnlyURL = "https://mcp.linear.app/mcp/readonly"
55
LinearMCPDefaultTokenExpr = "${{ secrets.LINEAR_API_KEY }}"
6-
JiraBaseURLExpr = "${{ vars.JIRA_BASE_URL }}"
6+
JiraBaseURLExpr = "https://pelidehalleux.atlassian.net"
77
JiraUserEmailExpr = "${{ secrets.JIRA_USER_EMAIL }}"
88
JiraAPITokenExpr = "${{ secrets.JIRA_API_TOKEN }}"
99
)

pkg/workflow/jira_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"strings"
55
"testing"
66

7+
"github.com/github/gh-aw/pkg/constants"
78
"github.com/stretchr/testify/assert"
89
"github.com/stretchr/testify/require"
910
)
@@ -71,6 +72,7 @@ func TestJiraCredentialsAreAddedOnlyToProcessorStep(t *testing.T) {
7172
"OTHER": "value",
7273
},
7374
}
75+
7476
steps := make([]string, 3, 8)
7577
copy(steps, []string{
7678
" - name: Process Safe Outputs\n",
@@ -89,3 +91,15 @@ func TestJiraCredentialsAreAddedOnlyToProcessorStep(t *testing.T) {
8991
NewCompiler().addCustomSafeOutputEnvVars(&customSteps, &WorkflowData{SafeOutputs: config})
9092
assert.Equal(t, " OTHER: value\n", strings.Join(customSteps, ""))
9193
}
94+
95+
func TestJiraCredentialsUseDefaultBaseURL(t *testing.T) {
96+
config := &SafeOutputsConfig{JiraCreateIssue: &JiraSafeOutputConfig{}}
97+
steps := []string{
98+
" - name: Process Safe Outputs\n",
99+
" env:\n",
100+
" with:\n",
101+
}
102+
103+
rendered := strings.Join(injectJiraCredentialsIntoProcessorStep(steps, config), "")
104+
assert.Contains(t, rendered, "JIRA_BASE_URL: "+constants.JiraBaseURLExpr)
105+
}

0 commit comments

Comments
 (0)