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
50 changes: 25 additions & 25 deletions .github/workflows/jsweep.lock.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .github/workflows/jsweep.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ safe-outputs:
create-pull-request:
expires: 2d
title-prefix: "[jsweep] "
branch-prefix: "signed/"
labels: [unbloat, automation]
draft: true
if-no-changes: "ignore"
Expand Down
16 changes: 16 additions & 0 deletions actions/setup/js/create_pull_request.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,16 @@ async function handleRemoteBranchCollision(branchName, preserveBranchName, optio
*/
async function main(config = {}) {
// Extract configuration
const rawBranchPrefix = config.branch_prefix || "";
const normalizedBranchPrefix = normalizeBranchName(rawBranchPrefix);
if (rawBranchPrefix && normalizedBranchPrefix !== rawBranchPrefix) {
core.warning(
`Branch prefix "${rawBranchPrefix}" contains characters that are invalid in a git ref. ` +
`Using normalized prefix: "${normalizedBranchPrefix}". ` +
`Update branch-prefix in the workflow configuration to avoid this warning.`
);
}
const branchPrefix = normalizedBranchPrefix;
const titlePrefix = config.title_prefix || "";
const envLabels = parseStringListConfig(config.labels);
const configFallbackLabels = parseStringListConfig(config.fallback_labels);
Expand Down Expand Up @@ -1313,6 +1323,12 @@ async function main(config = {}) {
branchName = `${workflowId}-${randomHex}`;
}

// Apply the configured branch prefix (e.g. "signed/") if it hasn't already been applied.
if (branchPrefix && !branchName.startsWith(branchPrefix)) {
branchName = `${branchPrefix}${branchName}`;
core.info(`Applied branch prefix: ${branchName}`);
}
Comment on lines +1326 to +1330

core.info(`Generated branch name: ${branchName}`);
core.info(`Base branch: ${baseBranch}`);

Expand Down
111 changes: 111 additions & 0 deletions actions/setup/js/create_pull_request.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2596,3 +2596,114 @@ describe("create_pull_request - rate-limit retry", () => {
expect(secondCall.body).toContain("could not be set");
});
});

describe("create_pull_request - branch-prefix config", () => {
let originalEnv;
let tempDir;

beforeEach(() => {
originalEnv = { ...process.env };
process.env.GH_AW_WORKFLOW_ID = "jsweep";
process.env.GITHUB_REPOSITORY = "test-owner/test-repo";
process.env.GITHUB_BASE_REF = "main";
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-branch-prefix-test-"));

global.core = {
info: vi.fn(),
warning: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
setFailed: vi.fn(),
setOutput: vi.fn(),
startGroup: vi.fn(),
endGroup: vi.fn(),
summary: { addRaw: vi.fn().mockReturnThis(), write: vi.fn().mockResolvedValue(undefined) },
};
global.github = {
rest: {
pulls: { create: vi.fn().mockResolvedValue({ data: { number: 1, html_url: "https://github.com/test/pull/1" } }) },
repos: { get: vi.fn().mockResolvedValue({ data: { default_branch: "main" } }) },
issues: { addLabels: vi.fn().mockResolvedValue({}) },
},
graphql: vi.fn(),
};
global.context = {
eventName: "workflow_dispatch",
repo: { owner: "test-owner", repo: "test-repo" },
payload: {},
};
global.exec = {
exec: vi.fn().mockResolvedValue(0),
getExecOutput: vi.fn().mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }),
};

delete require.cache[require.resolve("./create_pull_request.cjs")];
});

afterEach(() => {
for (const key of Object.keys(process.env)) {
if (!(key in originalEnv)) delete process.env[key];
}
Object.assign(process.env, originalEnv);
if (tempDir && fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
delete global.core;
delete global.github;
delete global.context;
delete global.exec;
vi.clearAllMocks();
});

it("should prepend branch-prefix to auto-generated branch name when agent provides no branch", async () => {
const { main } = require("./create_pull_request.cjs");
const handler = await main({ branch_prefix: "signed/", allow_empty: true });

await handler({ title: "Test PR", body: "body" }, {});

const branchArg = global.github.rest.pulls.create.mock.calls[0][0].head;
expect(branchArg).toMatch(/^signed\/jsweep-/);
});

it("should prepend branch-prefix to agent-specified branch name", async () => {
const { main } = require("./create_pull_request.cjs");
const handler = await main({ branch_prefix: "signed/", allow_empty: true });

await handler({ title: "Test PR", body: "body", branch: "my-feature" }, {});

const branchArg = global.github.rest.pulls.create.mock.calls[0][0].head;
expect(branchArg).toMatch(/^signed\/my-feature/);
});

it("should not double-apply branch-prefix when agent branch already starts with the prefix", async () => {
const { main } = require("./create_pull_request.cjs");
const handler = await main({ branch_prefix: "signed/", allow_empty: true });

await handler({ title: "Test PR", body: "body", branch: "signed/already-prefixed" }, {});

const branchArg = global.github.rest.pulls.create.mock.calls[0][0].head;
expect(branchArg).toMatch(/^signed\/already-prefixed/);
expect(branchArg).not.toMatch(/^signed\/signed\//);
});

it("should not add any prefix when branch-prefix is not configured", async () => {
const { main } = require("./create_pull_request.cjs");
const handler = await main({ allow_empty: true });

await handler({ title: "Test PR", body: "body" }, {});

const branchArg = global.github.rest.pulls.create.mock.calls[0][0].head;
expect(branchArg).toMatch(/^jsweep-/);
expect(branchArg).not.toContain("signed/");
});

it("should normalize an invalid branch-prefix and emit a warning", async () => {
const { main } = require("./create_pull_request.cjs");
const handler = await main({ branch_prefix: "bad prefix: ", allow_empty: true });

await handler({ title: "Test PR", body: "body" }, {});

expect(global.core.warning).toHaveBeenCalledWith(expect.stringMatching(/branch prefix.*characters that are invalid/i));
const branchArg = global.github.rest.pulls.create.mock.calls[0][0].head;
// normalized prefix "bad-prefix" should be applied
expect(branchArg).toMatch(/^bad-prefix/);
});
});
6 changes: 6 additions & 0 deletions docs/src/content/docs/reference/frontmatter-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -4165,6 +4165,12 @@ safe-outputs:
# Format 2: GitHub Actions expression that resolves to an integer at runtime
max: "example-value"

# Optional prefix to prepend to the pull request branch name (e.g. "signed/").
# Applied before the agent-specified or auto-generated branch name. When set, any
# branch name that does not already start with this prefix will have it prepended.
# (optional)
branch-prefix: "example-value"

# Optional prefix for the pull request title
# (optional)
title-prefix: "example-value"
Expand Down
4 changes: 4 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -6102,6 +6102,10 @@
}
]
},
"branch-prefix": {
"type": "string",
"description": "Optional prefix to prepend to the pull request branch name (e.g. \"signed/\"). Applied before the agent-specified or auto-generated branch name."
},
"title-prefix": {
"type": "string",
"description": "Optional prefix for the pull request title"
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/compiler_safe_outputs_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ var handlerRegistry = map[string]handlerBuilder{
}
builder := newHandlerConfigBuilder().
AddTemplatableInt("max", c.Max).
AddIfNotEmpty("branch_prefix", c.BranchPrefix).
AddIfNotEmpty("title_prefix", c.TitlePrefix).
AddTemplatableStringSlice("labels", c.Labels).
AddStringSlice("fallback_labels", c.FallbackLabels).
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/create_pull_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ func getFallbackAsIssue(config *CreatePullRequestsConfig) bool {
// CreatePullRequestsConfig holds configuration for creating GitHub pull requests from agent output
type CreatePullRequestsConfig struct {
BaseSafeOutputConfig `yaml:",inline"`
BranchPrefix string `yaml:"branch-prefix,omitempty"` // Optional prefix for the pull request branch name (e.g. "signed/"). Applied before the agent-specified or auto-generated branch name.
TitlePrefix string `yaml:"title-prefix,omitempty"`
Labels []string `yaml:"labels,omitempty"`
AllowedLabels []string `yaml:"allowed-labels,omitempty"` // Optional list of allowed labels. If omitted, any labels are allowed (including creating new ones).
Expand Down
7 changes: 7 additions & 0 deletions pkg/workflow/jsweep_workflow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,13 @@ func TestJSweepWorkflowConfiguration(t *testing.T) {
t.Error("jsweep workflow should batch validation commands into a single chained command")
}
})

// Test 17: Verify the branch prefix is set to "signed/"
t.Run("BranchPrefixSignedSlash", func(t *testing.T) {
if !strings.Contains(mdContent, `branch-prefix: "signed/"`) {
t.Error(`jsweep workflow should set branch-prefix to "signed/" in create-pull-request safe-outputs`)
}
})
}

// TestJSweepWorkflowLockFile validates that the compiled jsweep.lock.yml file
Expand Down
3 changes: 3 additions & 0 deletions pkg/workflow/tool_description_enhancer.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ func enhanceToolDescription(toolName, baseDescription string, safeOutputs *SafeO
if templatableIntValue(config.Max) > 0 {
constraints = append(constraints, fmt.Sprintf("Maximum %d pull request(s) can be created.", templatableIntValue(config.Max)))
}
if config.BranchPrefix != "" {
constraints = append(constraints, fmt.Sprintf("Branch name will be prefixed with %q.", config.BranchPrefix))
}
if config.TitlePrefix != "" {
constraints = append(constraints, fmt.Sprintf("Title will be prefixed with %q.", config.TitlePrefix))
}
Expand Down