Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/aw/safe-outputs-content.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ description: Safe-output reference for issue, discussion, comment, and pull requ
| `jira-add-comment` | `jira_add_comment` | `issue_key`, `body` |
| `jira-add-label` | `jira_add_label` | `issue_key`, `label` |

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.
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.

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.

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/smoke-issues.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 19 additions & 2 deletions actions/setup/js/linear_create_issue.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ const LINEAR_CREATE_ISSUE = `mutation LinearCreateIssue($input: IssueCreateInput
}
}`;

const LINEAR_RESOLVE_PROJECT = `query ResolveLinearProject($slugId: String!) {
projects(filter: { slugId: { eq: $slugId } }, first: 1) {
nodes {
id
}
}
}`;

async function main(config = {}) {
const teamId = config.team_id;
if (typeof teamId !== "string" || !LINEAR_UUID_PATTERN.test(teamId)) {
Expand Down Expand Up @@ -56,7 +64,16 @@ async function main(config = {}) {

const input = { teamId, title, description };
if (projectId) {
input.projectId = projectId;
if (LINEAR_UUID_PATTERN.test(projectId)) {
input.projectId = projectId;
} else {
const projectData = await linearGraphQL(LINEAR_RESOLVE_PROJECT, { slugId: projectId });
const resolvedProjectId = projectData?.projects?.nodes?.[0]?.id;
if (typeof resolvedProjectId !== "string" || !LINEAR_UUID_PATTERN.test(resolvedProjectId)) {
throw new Error(`${ERR_CONFIG}: linear_create_issue could not resolve the configured project ID`);
}
input.projectId = resolvedProjectId;
}
}
const data = await linearGraphQL(LINEAR_CREATE_ISSUE, { input });
const payload = data?.issueCreate;
Expand All @@ -72,4 +89,4 @@ async function main(config = {}) {
};
}

module.exports = { LINEAR_CREATE_ISSUE, main };
module.exports = { LINEAR_CREATE_ISSUE, LINEAR_RESOLVE_PROJECT, main };
15 changes: 10 additions & 5 deletions actions/setup/js/linear_safe_outputs.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createRequire } from "module";

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

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

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

expect(fetch).toHaveBeenCalledWith(
expect(fetch).toHaveBeenNthCalledWith(
1,
LINEAR_GRAPHQL_ENDPOINT,
expect.objectContaining({
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "linear-secret" },
})
);
const request = JSON.parse(fetch.mock.calls[0][1].body);
const projectRequest = JSON.parse(fetch.mock.calls[0][1].body);
expect(projectRequest).toEqual({ query: LINEAR_RESOLVE_PROJECT, variables: { slugId: "810f57a7e383" } });
const request = JSON.parse(fetch.mock.calls[1][1].body);
expect(request.query).toBe(LINEAR_CREATE_ISSUE);
expect(request.query).not.toContain("Safe title");
expect(request.variables.input).toEqual({
teamId: "9cfb482a-81e3-4154-b5b9-2c805e70a02d",
projectId: "810f57a7e383",
projectId: "a3f91a0b-6d71-4c58-a4bb-72b925bbebc8",
title: "Safe title",
description: "Detailed hello to `@user`",
});
Expand Down
2 changes: 1 addition & 1 deletion pkg/constants/tool_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package constants
const (
LinearMCPReadOnlyURL = "https://mcp.linear.app/mcp/readonly"
LinearMCPDefaultTokenExpr = "${{ secrets.LINEAR_API_KEY }}"
JiraBaseURLExpr = "${{ vars.JIRA_BASE_URL }}"
JiraBaseURLExpr = "https://pelidehalleux.atlassian.net"
JiraUserEmailExpr = "${{ secrets.JIRA_USER_EMAIL }}"
JiraAPITokenExpr = "${{ secrets.JIRA_API_TOKEN }}"
)
Expand Down
14 changes: 14 additions & 0 deletions pkg/workflow/jira_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"strings"
"testing"

"github.com/github/gh-aw/pkg/constants"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -71,6 +72,7 @@ func TestJiraCredentialsAreAddedOnlyToProcessorStep(t *testing.T) {
"OTHER": "value",
},
}

steps := make([]string, 3, 8)
copy(steps, []string{
" - name: Process Safe Outputs\n",
Expand All @@ -89,3 +91,15 @@ func TestJiraCredentialsAreAddedOnlyToProcessorStep(t *testing.T) {
NewCompiler().addCustomSafeOutputEnvVars(&customSteps, &WorkflowData{SafeOutputs: config})
assert.Equal(t, " OTHER: value\n", strings.Join(customSteps, ""))
}

func TestJiraCredentialsUseDefaultBaseURL(t *testing.T) {
config := &SafeOutputsConfig{JiraCreateIssue: &JiraSafeOutputConfig{}}
steps := []string{
" - name: Process Safe Outputs\n",
" env:\n",
" with:\n",
}

rendered := strings.Join(injectJiraCredentialsIntoProcessorStep(steps, config), "")
assert.Contains(t, rendered, "JIRA_BASE_URL: "+constants.JiraBaseURLExpr)
}
Loading