diff --git a/actions/setup/js/push_repo_memory.cjs b/actions/setup/js/push_repo_memory.cjs index 4757a5d3c7..23318e27fd 100644 --- a/actions/setup/js/push_repo_memory.cjs +++ b/actions/setup/js/push_repo_memory.cjs @@ -49,6 +49,17 @@ async function main() { } } + // ============================================================================ + // CAMPAIGN-SPECIFIC VALIDATION FUNCTIONS + // ============================================================================ + // The following functions implement validation for the campaign convention: + // When memoryId is "campaigns" and file-glob matches "/**", + // enforce specific JSON schemas for cursor.json and metrics/*.json files. + // + // This is a domain-specific convention used by Campaign Workflows to maintain + // durable state in repo-memory. See docs/guides/campaigns/ for details. + // ============================================================================ + /** @param {any} obj @param {string} campaignId @param {string} relPath */ function validateCampaignCursor(obj, campaignId, relPath) { if (!isPlainObject(obj)) { @@ -128,9 +139,20 @@ async function main() { // The artifactDir IS the memory directory (no nested structure needed) const sourceMemoryPath = artifactDir; - // Campaign mode enforcement (agentic campaigns): - // We treat repo-memory ID "campaigns" with a single file-glob like "/**" as a strong contract. - // In this mode, cursor.json and at least one metrics snapshot are required. + // ============================================================================ + // CAMPAIGN MODE DETECTION + // ============================================================================ + // Campaign Workflows use a convention-based pattern in repo-memory: + // - memoryId: "campaigns" + // - file-glob: "/**" + // + // When this pattern is detected, we enforce campaign-specific validation: + // 1. cursor.json must exist and follow the cursor schema + // 2. At least one metrics/*.json file must exist and follow the metrics schema + // + // This ensures campaigns maintain durable state consistency across workflow runs. + // Non-campaign repo-memory configurations bypass this validation entirely. + // ============================================================================ const singlePattern = fileGlobFilter.trim().split(/\s+/).filter(Boolean); const campaignPattern = singlePattern.length === 1 ? singlePattern[0] : ""; const campaignMatch = memoryId === "campaigns" ? /^([^*?]+)\/\*\*$/.exec(campaignPattern) : null; @@ -242,7 +264,8 @@ async function main() { throw new Error("File size validation failed"); } - // Campaign JSON contract checks (only for the campaign subtree). + // Campaign-specific JSON validation (only when campaign mode is active) + // This enforces the campaign state file schemas for cursor and metrics if (isCampaignMode && relativeFilePath.startsWith(`${campaignId}/`)) { if (relativeFilePath === `${campaignId}/cursor.json`) { const obj = tryParseJSONFile(fullPath); @@ -271,7 +294,8 @@ async function main() { return; } - // Campaign mode validation: ensure required files were found + // Campaign mode validation: ensure required state files were found + // This enforcement is only active when campaign mode is detected if (isCampaignMode) { if (!campaignCursorFound) { core.error(`Missing required campaign cursor file: ${campaignId}/cursor.json`); diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 651c4912cc..e5137778f0 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -136,6 +136,9 @@ func (c *Compiler) CompileWorkflowData(workflowData *WorkflowData, markdownPath } // Emit experimental warning for campaigns feature + // Campaign workflows (.campaign.md) are compiled by the campaign system in pkg/campaign/ + // This warning is part of the general workflow compilation pipeline and simply + // detects campaign files to inform users about the experimental status. if strings.HasSuffix(markdownPath, ".campaign.md") { fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Using experimental feature: campaigns - This is a preview feature for multi-workflow orchestration. The campaign spec format, CLI commands, and repo-memory conventions may change in future releases. Workflows may break or require migration when the feature stabilizes.")) c.IncrementWarningCount() diff --git a/pkg/workflow/safe_output_validation_config.go b/pkg/workflow/safe_output_validation_config.go index 65bfb7d62c..58727acfcb 100644 --- a/pkg/workflow/safe_output_validation_config.go +++ b/pkg/workflow/safe_output_validation_config.go @@ -232,7 +232,10 @@ var ValidationConfig = map[string]TypeValidationConfig{ "update_project": { DefaultMax: 10, Fields: map[string]FieldValidation{ - "project": {Required: true, Type: "string", Sanitize: true, MaxLength: 512, Pattern: "^https://github\\.com/(orgs|users)/[^/]+/projects/\\d+", PatternError: "must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/42)"}, + "project": {Required: true, Type: "string", Sanitize: true, MaxLength: 512, Pattern: "^https://github\\.com/(orgs|users)/[^/]+/projects/\\d+", PatternError: "must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/42)"}, + // campaign_id is an optional field used by Campaign Workflows to tag project items. + // When provided, the update-project safe output applies a "campaign:" label. + // This is part of the campaign tracking convention but not required for general use. "campaign_id": {Type: "string", Sanitize: true, MaxLength: 128}, "content_type": {Type: "string", Enum: []string{"issue", "pull_request"}}, "content_number": {OptionalPositiveInteger: true}, diff --git a/pkg/workflow/tools.go b/pkg/workflow/tools.go index 545cb60179..146836f0f8 100644 --- a/pkg/workflow/tools.go +++ b/pkg/workflow/tools.go @@ -151,10 +151,25 @@ func (c *Compiler) applyDefaults(data *WorkflowData, markdownPath string) { data.ParsedTools = NewTools(data.Tools) if data.Permissions == "" { - // Default behavior: keep existing workflows stable with read-all. - // Campaign orchestrators intentionally omit permissions from the generated - // .campaign.g.md frontmatter, so we compute explicit, minimal read permissions - // for the agent job at compile time. + // ============================================================================ + // PERMISSIONS DEFAULTS + // ============================================================================ + // Default behavior: keep existing workflows stable with read-all permissions. + // + // CAMPAIGN-SPECIFIC HANDLING: + // Campaign orchestrator workflows (.campaign.g.md files) are auto-generated + // by the BuildOrchestrator function in pkg/campaign/orchestrator.go. + // These generated workflows intentionally omit explicit permissions in their + // frontmatter, so we compute minimal read permissions here at compile time. + // + // This is part of the campaign orchestrator generation pattern where: + // 1. Campaign specs (.campaign.md) define high-level campaign configuration + // 2. BuildOrchestrator generates orchestrator workflows (.campaign.g.md) + // 3. The compiler applies default permissions to the generated workflow + // + // This separation allows campaign configuration to remain declarative while + // ensuring generated orchestrators have appropriate permissions. + // ============================================================================ if strings.HasSuffix(markdownPath, ".campaign.g.md") { perms := NewPermissions() // Campaign orchestrators always need to read repository contents and tracker issues.