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
1 change: 1 addition & 0 deletions .github/workflows/security-alert-burndown.lock.yml

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

1,284 changes: 1,284 additions & 0 deletions .github/workflows/test-project-url-default.lock.yml

Large diffs are not rendered by default.

56 changes: 56 additions & 0 deletions .github/workflows/test-project-url-default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
name: Test Project URL Default
engine: copilot
on:
workflow_dispatch:

project:
url: "https://github.com/orgs/githubnext/projects/1"

safe-outputs:
update-project:
max: 5
create-project-status-update:
max: 1
---

# Test Default Project URL

This workflow demonstrates the new `GH_AW_PROJECT_URL` environment variable feature.

When the `project` field is configured in the frontmatter, safe output entries like
`update-project` and `create-project-status-update` will automatically use this project
URL as a default when the message doesn't specify a project field.

## Test Cases

1. **Default project URL from frontmatter**: Safe output messages without a `project` field
will use the URL from the frontmatter configuration.

2. **Override with explicit project**: If a safe output message includes a `project` field,
it takes precedence over the frontmatter default.

## Example Safe Outputs

```json
{
"type": "update_project",
"content_type": "draft_issue",
"draft_title": "Test Issue Using Default Project URL",
"fields": {
"status": "Todo"
}
}
```

This will automatically use `https://github.com/orgs/githubnext/projects/1` from the frontmatter.

```json
{
"type": "create_project_status_update",
"body": "Project status update using default project URL",
"status": "ON_TRACK"
}
```

This will also use the default project URL from the frontmatter.
28 changes: 19 additions & 9 deletions actions/setup/js/create_project_status_update.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -298,13 +298,23 @@ async function main(config = {}) {

const output = message;

// Validate required fields
if (!output.project) {
core.error("Missing required field: project (GitHub project URL)");
return {
success: false,
error: "Missing required field: project",
};
// Get default project URL from environment if available
const defaultProjectUrl = process.env.GH_AW_PROJECT_URL || "";

// Validate project field - can use default from frontmatter if available
let effectiveProjectUrl = output.project;

if (!effectiveProjectUrl || typeof effectiveProjectUrl !== "string" || effectiveProjectUrl.trim() === "") {
if (defaultProjectUrl) {
core.info(`Using default project URL from frontmatter: ${defaultProjectUrl}`);
effectiveProjectUrl = defaultProjectUrl;
} else {
core.error("Missing required field: project (GitHub project URL)");
return {
success: false,
error: "Missing required field: project",
};
}
}

if (!output.body) {
Expand All @@ -316,10 +326,10 @@ async function main(config = {}) {
}

try {
core.info(`Creating status update for project: ${output.project}`);
core.info(`Creating status update for project: ${effectiveProjectUrl}`);

// Parse project URL and resolve project ID
const projectInfo = parseProjectUrl(output.project);
const projectInfo = parseProjectUrl(effectiveProjectUrl);
const projectNumberInt = parseInt(projectInfo.projectNumber, 10);

if (!Number.isFinite(projectNumberInt)) {
Expand Down
105 changes: 105 additions & 0 deletions actions/setup/js/create_project_status_update.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -479,4 +479,109 @@ describe("create_project_status_update", () => {
expect(mockGithub.graphql).toHaveBeenCalledTimes(3);
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("falling back to projectsV2 list search"));
});

it("should use default project URL from GH_AW_PROJECT_URL when message.project is missing", async () => {
// Set default project URL in environment
const defaultProjectUrl = "https://github.com/orgs/test-org/projects/42";
process.env.GH_AW_PROJECT_URL = defaultProjectUrl;

mockGithub.graphql
.mockResolvedValueOnce({
// First call: direct project query by number
organization: {
projectV2: {
id: "PVT_test123",
number: 42,
title: "Test Project",
url: defaultProjectUrl,
},
},
})
.mockResolvedValueOnce({
// Second call: create status update
createProjectV2StatusUpdate: {
statusUpdate: {
id: "PVTSU_test456",
body: "Default project status",
bodyHTML: "<p>Default project status</p>",
startDate: "2025-01-01",
targetDate: "2025-12-31",
status: "ON_TRACK",
createdAt: "2025-01-06T12:00:00Z",
},
},
});

const handler = await main({ max: 10 });

const messageWithoutProject = {
body: "Default project status",
status: "ON_TRACK",
start_date: "2025-01-01",
target_date: "2025-12-31",
};

const result = await handler(messageWithoutProject, {});

expect(result.success).toBe(true);
expect(result.status_update_id).toBe("PVTSU_test456");
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Using default project URL from frontmatter"));

// Cleanup
delete process.env.GH_AW_PROJECT_URL;
});

it("should prioritize message.project over GH_AW_PROJECT_URL when both are present", async () => {
// Set default project URL in environment (should be ignored)
process.env.GH_AW_PROJECT_URL = "https://github.com/orgs/test-org/projects/999";

const messageProjectUrl = "https://github.com/orgs/test-org/projects/42";

mockGithub.graphql
.mockResolvedValueOnce({
// First call: direct project query by number
organization: {
projectV2: {
id: "PVT_test789",
number: 42,
title: "Test Project",
url: messageProjectUrl,
},
},
})
.mockResolvedValueOnce({
// Second call: create status update
createProjectV2StatusUpdate: {
statusUpdate: {
id: "PVTSU_test789",
body: "Message project status",
bodyHTML: "<p>Message project status</p>",
startDate: "2025-01-01",
targetDate: "2025-12-31",
status: "ON_TRACK",
createdAt: "2025-01-06T12:00:00Z",
},
},
});

const handler = await main({ max: 10 });

const messageWithProject = {
project: messageProjectUrl,
body: "Message project status",
status: "ON_TRACK",
start_date: "2025-01-01",
target_date: "2025-12-31",
};

const result = await handler(messageWithProject, {});

expect(result.success).toBe(true);
expect(result.status_update_id).toBe("PVTSU_test789");
// Should not use default from environment
expect(mockCore.info).not.toHaveBeenCalledWith(expect.stringContaining("Using default project URL from frontmatter"));

// Cleanup
delete process.env.GH_AW_PROJECT_URL;
});
});
43 changes: 27 additions & 16 deletions actions/setup/js/update_project.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1017,29 +1017,40 @@ async function main(config = {}) {
}

try {
// Validate required project field
if (!message.project || typeof message.project !== "string" || message.project.trim() === "") {
const errorMsg = 'Missing required "project" field in update_project message. The "project" field must be a full GitHub project URL (e.g., "https://github.com/orgs/myorg/projects/42").';
core.error(errorMsg);

// Provide helpful context based on content_type
if (message.content_type === "draft_issue") {
core.error('For draft_issue content_type, you must include: {"project": "https://...", "content_type": "draft_issue", "draft_title": "...", "fields": {...}}');
} else if (message.content_type === "issue" || message.content_type === "pull_request") {
core.error(`For ${message.content_type} content_type, you must include: {"project": "https://...", "content_type": "${message.content_type}", "content_number": 123, "fields": {...}}`);
}
// Get default project URL from environment if available
const defaultProjectUrl = process.env.GH_AW_PROJECT_URL || "";

// Validate project field - can use default from frontmatter if available
let effectiveProjectUrl = message.project;

// If no project field in message, try to use default from frontmatter
if (!effectiveProjectUrl || typeof effectiveProjectUrl !== "string" || effectiveProjectUrl.trim() === "") {
if (defaultProjectUrl) {
core.info(`Using default project URL from frontmatter: ${defaultProjectUrl}`);
effectiveProjectUrl = defaultProjectUrl;
} else {
const errorMsg =
'Missing required "project" field in update_project message. The "project" field must be a full GitHub project URL (e.g., "https://github.com/orgs/myorg/projects/42"), or configure a default project URL in the workflow frontmatter.';
core.error(errorMsg);

// Provide helpful context based on content_type
if (message.content_type === "draft_issue") {
core.error('For draft_issue content_type, you must include: {"project": "https://...", "content_type": "draft_issue", "draft_title": "...", "fields": {...}}');
} else if (message.content_type === "issue" || message.content_type === "pull_request") {
core.error(`For ${message.content_type} content_type, you must include: {"project": "https://...", "content_type": "${message.content_type}", "content_number": 123, "fields": {...}}`);
}

return {
success: false,
error: errorMsg,
};
return {
success: false,
error: errorMsg,
};
}
}

// Validation passed - increment processed count
processedCount++;

// Resolve temporary project ID if present
let effectiveProjectUrl = message.project;

if (effectiveProjectUrl && typeof effectiveProjectUrl === "string") {
// Strip # prefix if present
Expand Down
54 changes: 54 additions & 0 deletions actions/setup/js/update_project.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1225,4 +1225,58 @@ describe("updateProject", () => {
expect(result.error).toContain('Missing required "project" field');
expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Missing required"));
});

it("should use default project URL from GH_AW_PROJECT_URL when message.project is missing", async () => {
// Set default project URL in environment
const defaultProjectUrl = "https://github.com/orgs/testowner/projects/60";
process.env.GH_AW_PROJECT_URL = defaultProjectUrl;

const messageHandler = await updateProjectHandlerFactory({});

const messageWithoutProject = {
type: "update_project",
content_type: "draft_issue",
draft_title: "Test Draft Issue",
draft_body: "This is a test",
};

queueResponses([repoResponse(), viewerResponse(), orgProjectV2Response(defaultProjectUrl, 60, "project-default"), addDraftIssueResponse("draft-item-default")]);

const result = await messageHandler(messageWithoutProject, new Map());

expect(result.success).toBe(true);
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Using default project URL from frontmatter"));
expect(getOutput("item-id")).toBe("draft-item-default");

// Cleanup
delete process.env.GH_AW_PROJECT_URL;
});

it("should prioritize message.project over GH_AW_PROJECT_URL when both are present", async () => {
// Set default project URL in environment
process.env.GH_AW_PROJECT_URL = "https://github.com/orgs/testowner/projects/999";

const messageHandler = await updateProjectHandlerFactory({});

const messageProjectUrl = "https://github.com/orgs/testowner/projects/60";
const messageWithProject = {
type: "update_project",
project: messageProjectUrl,
content_type: "draft_issue",
draft_title: "Test Draft Issue",
draft_body: "This is a test",
};

queueResponses([repoResponse(), viewerResponse(), orgProjectV2Response(messageProjectUrl, 60, "project-message"), addDraftIssueResponse("draft-item-message")]);

const result = await messageHandler(messageWithProject, new Map());

expect(result.success).toBe(true);
// Should not use default from environment
expect(mockCore.info).not.toHaveBeenCalledWith(expect.stringContaining("Using default project URL from frontmatter"));
expect(getOutput("item-id")).toBe("draft-item-message");

// Cleanup
delete process.env.GH_AW_PROJECT_URL;
});
});
1 change: 1 addition & 0 deletions docs/src/content/docs/agent-factory-status.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ These are experimental agentic workflows used by the GitHub Next team to learn,
| [Super Linter Report](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/super-linter.md) | copilot | [![Super Linter Report](https://github.com/githubnext/gh-aw/actions/workflows/super-linter.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/super-linter.lock.yml) | `0 14 * * 1-5` | - |
| [Terminal Stylist](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/terminal-stylist.md) | copilot | [![Terminal Stylist](https://github.com/githubnext/gh-aw/actions/workflows/terminal-stylist.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/terminal-stylist.lock.yml) | - | - |
| [Test Create PR Error Handling](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/test-create-pr-error-handling.md) | claude | [![Test Create PR Error Handling](https://github.com/githubnext/gh-aw/actions/workflows/test-create-pr-error-handling.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/test-create-pr-error-handling.lock.yml) | - | - |
| [Test Project URL Default](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/test-project-url-default.md) | copilot | [![Test Project URL Default](https://github.com/githubnext/gh-aw/actions/workflows/test-project-url-default.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/test-project-url-default.lock.yml) | - | - |
| [The Daily Repository Chronicle](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/daily-repo-chronicle.md) | copilot | [![The Daily Repository Chronicle](https://github.com/githubnext/gh-aw/actions/workflows/daily-repo-chronicle.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/daily-repo-chronicle.lock.yml) | `0 16 * * 1-5` | - |
| [The Great Escapi](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/firewall-escape.md) | copilot | [![The Great Escapi](https://github.com/githubnext/gh-aw/actions/workflows/firewall-escape.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/firewall-escape.lock.yml) | - | - |
| [Tidy](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/tidy.md) | copilot | [![Tidy](https://github.com/githubnext/gh-aw/actions/workflows/tidy.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/tidy.lock.yml) | `0 7 * * *` | - |
Expand Down
Loading
Loading