feat: auto-generate side-repo maintenance workflow for SideRepoOps pattern#26382
feat: auto-generate side-repo maintenance workflow for SideRepoOps pattern#26382
Conversation
…attern - Detect SideRepoOps patterns from checkout configs with current: true - Generate agentics-maintenance-<slug>.yml for each static target repo - Add GH_AW_TARGET_REPO_SLUG support to close_expired_* and create_labels scripts - Include close-expired-entities, apply_safe_outputs, create_labels, validate jobs - Use custom token from checkout config for target repo authentication - Add unit tests for collectSideRepoTargets, sanitizeRepoForFilename, and workflow generation Agent-Logs-Url: https://github.com/github/gh-aw/sessions/dc073fff-9a15-4d12-93e4-a16cdc65fdc9 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…n JS scripts Agent-Logs-Url: https://github.com/github/gh-aw/sessions/dc073fff-9a15-4d12-93e4-a16cdc65fdc9 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
… in JS scripts Agent-Logs-Url: https://github.com/github/gh-aw/sessions/dc073fff-9a15-4d12-93e4-a16cdc65fdc9 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…-independent test comparison Agent-Logs-Url: https://github.com/github/gh-aw/sessions/dc073fff-9a15-4d12-93e4-a16cdc65fdc9 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
🎉 Great work on this PR! The implementation of auto-generating side-repo maintenance workflows for the SideRepoOps pattern is well-structured and cohesive — covering the Go generation logic (
|
🧪 Test Quality Sentinel ReportTest Quality Score: 100/100✅ Excellent test quality
Test Classification Details
Flagged Tests — Requires ReviewNone — all new tests passed all quality checks. 🎉 Language SupportTests analyzed:
Verdict
📖 Understanding Test ClassificationsDesign Tests (High Value) verify what the system does:
Implementation Tests (Low Value) verify how the system does it:
Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators. References: §24455822796
|
|
Commit pushed:
|
🏗️ Design Decision Gate — ADR RequiredThis PR makes significant changes to core business logic (>100 new lines in AI has analyzed the PR diff and generated a draft ADR to help you get started: 📄 Draft ADR: What to do next
Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision. Why ADRs Matter
ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you. 📋 Michael Nygard ADR Format ReferenceAn ADR must contain these four sections to be considered complete:
All ADRs are stored in
References: §24455822765 Note 🔒 Integrity filter blocked 1 itemThe following item were blocked because they don't meet the GitHub integrity level.
To allow these resources, lower tools:
github:
min-integrity: approved # merged | approved | unapproved | none
|
There was a problem hiding this comment.
Pull request overview
Adds first-class SideRepoOps support by generating per-target maintenance workflows and enabling existing JS maintenance scripts to operate against a target repo (via GH_AW_TARGET_REPO_SLUG) instead of always using context.repo.
Changes:
- Generate
agentics-maintenance-<owner-repo>.ymlside-repo maintenance workflows based oncheckoutconfigs withcurrent: true. - Add cross-repo targeting support to maintenance JS scripts (
create_labels, close-expired-*). - Add Go tests for side-repo target collection, filename sanitization, and side-repo workflow generation.
Show a summary per file
| File | Description |
|---|---|
pkg/workflow/maintenance_workflow.go |
Adds side-repo target detection and emits per-target maintenance workflows; calls side-repo generation even when expires isn’t used. |
pkg/workflow/maintenance_workflow_test.go |
Adds tests covering side-repo target detection and side-repo workflow generation behavior. |
actions/setup/js/create_labels.cjs |
Uses GH_AW_TARGET_REPO_SLUG (when valid) to create labels in a target repo. |
actions/setup/js/close_expired_discussions.cjs |
Resolves owner/repo from GH_AW_TARGET_REPO_SLUG to close expired discussions cross-repo. |
actions/setup/js/close_expired_issues.cjs |
Resolves owner/repo from GH_AW_TARGET_REPO_SLUG to close expired issues cross-repo. |
actions/setup/js/close_expired_pull_requests.cjs |
Resolves owner/repo from GH_AW_TARGET_REPO_SLUG to close expired PRs cross-repo. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 7/7 changed files
- Comments generated: 9
| This workflow is generated for the SideRepoOps target repository "` + repoSlug + `". | ||
| It is invoked via workflow_call from the hosting repository's agentics-maintenance.yml | ||
| and runs maintenance operations against the target repository.` |
There was a problem hiding this comment.
The side-repo workflow header/instructions state it "is invoked via workflow_call from the hosting repository's agentics-maintenance.yml", but this generator does not add any caller job/step to agentics-maintenance.yml (and there are no other references to agentics-maintenance-.yml in this file). Either update the hosting maintenance workflow generation to actually call these reusable workflows, or adjust the generated instructions so they don’t claim an invocation path that doesn’t exist.
| // Fetch all existing labels from the repository. | ||
| // When GH_AW_TARGET_REPO_SLUG is set (SideRepoOps pattern), create labels in that | ||
| // repository instead of the execution context repository. | ||
| // The slug must be in exact "owner/repo" format (one slash, non-empty on both sides). | ||
| const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG; | ||
| const isCrossRepo = Boolean(targetRepoSlug && /^[^/]+\/[^/]+$/.test(targetRepoSlug)); | ||
| const [owner, repo] = isCrossRepo ? targetRepoSlug.split("/", 2) : [context.repo.owner, context.repo.repo]; | ||
| if (isCrossRepo) { | ||
| core.info(`Using target repository from GH_AW_TARGET_REPO_SLUG: ${owner}/${repo}`); | ||
| } |
There was a problem hiding this comment.
This code treats GH_AW_TARGET_REPO_SLUG as optional and silently falls back to context.repo when it’s set but invalid. That contradicts the preceding comment that the slug "must" be in exact owner/repo format and can lead to label creation happening in the wrong repo without any signal. Consider reusing the existing repo parsing/validation helpers in actions/setup/js/repo_helpers.cjs (e.g., parseRepoSlug) and calling core.setFailed (or at least core.warning) when GH_AW_TARGET_REPO_SLUG is present but malformed.
| const isCrossRepo = Boolean(targetRepoSlug && /^[^/]+\/[^/]+$/.test(targetRepoSlug)); | ||
| const [owner, repo] = isCrossRepo ? targetRepoSlug.split("/", 2) : [context.repo.owner, context.repo.repo]; | ||
| if (isCrossRepo) { |
There was a problem hiding this comment.
This script silently ignores GH_AW_TARGET_REPO_SLUG when it’s set but malformed, despite the comment stating it must be in exact owner/repo format. That can cause the cleanup to run against context.repo unexpectedly. Consider centralizing slug parsing via actions/setup/js/repo_helpers.cjs (parseRepoSlug / resolveAndValidateRepo) and failing (or at least warning) when GH_AW_TARGET_REPO_SLUG is present but invalid.
| const isCrossRepo = Boolean(targetRepoSlug && /^[^/]+\/[^/]+$/.test(targetRepoSlug)); | |
| const [owner, repo] = isCrossRepo ? targetRepoSlug.split("/", 2) : [context.repo.owner, context.repo.repo]; | |
| if (isCrossRepo) { | |
| const hasTargetRepoSlug = typeof targetRepoSlug === "string" && targetRepoSlug.length > 0; | |
| const isValidTargetRepoSlug = hasTargetRepoSlug && /^[^/]+\/[^/]+$/.test(targetRepoSlug); | |
| if (hasTargetRepoSlug && !isValidTargetRepoSlug) { | |
| throw new Error( | |
| `Invalid GH_AW_TARGET_REPO_SLUG: "${targetRepoSlug}". Expected exact "owner/repo" format.`, | |
| ); | |
| } | |
| const [owner, repo] = isValidTargetRepoSlug | |
| ? targetRepoSlug.split("/", 2) | |
| : [context.repo.owner, context.repo.repo]; | |
| if (isValidTargetRepoSlug) { |
| } | ||
| fmt.Fprintf(os.Stderr, " Generated side-repo maintenance workflow: %s\n", filename) | ||
| } | ||
|
|
There was a problem hiding this comment.
generateAllSideRepoMaintenanceWorkflows builds a generatedFiles map (and the comment says it will be used to clean up stale files), but the map is never read and no cleanup actually happens. As-is, stale agentics-maintenance-.yml files will accumulate when targets are removed/renamed. Either implement the cleanup (e.g., scan workflowDir for agentics-maintenance-.yml not in generatedFiles and remove them) or drop the unused tracking/comment to avoid misleading future readers.
| entries, err := os.ReadDir(workflowDir) | |
| if err != nil { | |
| return fmt.Errorf("failed to read workflow directory %s for stale side-repo maintenance workflow cleanup: %w", workflowDir, err) | |
| } | |
| for _, entry := range entries { | |
| if entry.IsDir() { | |
| continue | |
| } | |
| name := entry.Name() | |
| if !strings.HasPrefix(name, "agentics-maintenance-") || !strings.HasSuffix(name, ".yml") { | |
| continue | |
| } | |
| if generatedFiles[name] { | |
| continue | |
| } | |
| stalePath := filepath.Join(workflowDir, name) | |
| maintenanceLog.Printf("Removing stale side-repo maintenance workflow: %s", name) | |
| if err := os.Remove(stalePath); err != nil { | |
| return fmt.Errorf("failed to remove stale side-repo maintenance workflow %s: %w", stalePath, err) | |
| } | |
| } |
|
@copilot review all comments |
…s for cross-repo parsing Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b6e9b78c-2911-4790-afd2-19c256e942ed Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
…ow generation Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8c645f08-4a39-40f2-af2e-bf9e0385b09a Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8c645f08-4a39-40f2-af2e-bf9e0385b09a Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Addressed all reviewer comments across two commits (6bc1ab3 and 0a3a930):
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot use fuzzy scheduling to scatter cron |
…flows Agent-Logs-Url: https://github.com/github/gh-aw/sessions/bd7ad416-3b66-44fe-a087-aeef004f7cfc Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Done in 7ad9368. Added The frequency tier (every 2h / 6h / 12h / daily) is still determined by Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot add integration tests |
…tion Agent-Logs-Url: https://github.com/github/gh-aw/sessions/5287e268-8f33-4e18-8a6e-8d4de8729ed8 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Done in 007a21a. Added
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
In a SideRepoOps setup,
agentics-maintenance.ymlis only generated for the hosting repo. The target repo's maintenance workflow (safe-outputs replay, label creation, expired-entity cleanup) had to be hand-rolled and manually synced on everygh aw upgradecycle.Changes
Go — workflow generation (
pkg/workflow/maintenance_workflow.go)collectSideRepoTargets()— scans compiledWorkflowDatafor checkout configs withcurrent: trueand a static (non-expression)repositoryfield; deduplicates by slug, preserving first-seen order. When the same repo appears multiple times with different tokens, the non-empty token is preferred.sanitizeRepoForFilename()— convertsowner/repo→owner-repofor use in file namessideRepoCronSeed()/generateSideRepoMaintenanceCron()— derives a deterministic minute and hour offset from the FNV-1a hash of the repository slug, scattering each side-repo workflow's cron across the clock face so multiple targets don't all fire at the same time. Frequency tier (every 2h / 6h / 12h / daily) is still governed byminExpiresDays.generateSideRepoMaintenanceWorkflow()— emitsagentics-maintenance-<slug>.ymlwith:workflow_call+workflow_dispatchtriggersschedule:cron trigger with fuzzy offset (included when any workflow usesexpires, matching theclose-expired-entitiesjob condition)apply_safe_outputsjob: uses custom token from the checkout config +GH_AW_TARGET_REPO_SLUG; includesactions/checkoutsparse-checkout step in dev/script modescreate_labelsjob: creates labels in the target repovalidate_workflowsjob: same as hosting-repo variantclose-expired-entitiesjob: included only when any workflow usesexpiresgenerateAllSideRepoMaintenanceWorkflows()— removes staleagentics-maintenance-*.ymlfiles no longer referenced by current targets on recompileGenerateMaintenanceWorkflow()modified to invoke side-repo generation even when noexpiresis set (socreate_labels/apply_safe_outputsare always available)When no custom token is in the checkout config, the generated workflow falls back to
${{ secrets.GH_AW_GITHUB_TOKEN }}.Example: a workflow with
will produce
.github/workflows/agentics-maintenance-my-org-target-repo.ymlpre-wired with the custom token andGH_AW_TARGET_REPO_SLUG: "my-org/target-repo"on every cross-repo job. Expression-based repositories (e.g.${{ inputs.target_repo }}) are skipped since no static filename can be derived.JavaScript — cross-repo support (
actions/setup/js/)close_expired_discussions.cjs,close_expired_issues.cjs,close_expired_pull_requests.cjs, andcreate_labels.cjsnow resolveowner/repovia a sharedresolveExecutionOwnerRepo()helper added torepo_helpers.cjs. The helper readsGH_AW_TARGET_REPO_SLUGand validates it withparseRepoSlug(); if the variable is set but malformed it throws (instead of silently falling back tocontext.repo) to prevent wrong-repo operations.Tests
TestCollectSideRepoTargets— includes cases for token preference and first-seen order preservationTestSanitizeRepoForFilenameTestGenerateSideRepoMaintenanceCron— verifies determinism, distinct offsets per repo, valid minute range, and correct frequency-tier selectionTestGenerateSideRepoMaintenanceWorkflow— includes cases for schedule trigger presence and correct fuzzy cron value, and stale file cleanuprepo_helpers.test.cjs— coversresolveExecutionOwnerRepo()including the throw-on-malformed-slug behaviourside_repo_maintenance_integration_test.go— end-to-end integration tests usingcompiler.ParseWorkflowFile→GenerateMaintenanceWorkflow:GH_AW_TARGET_REPO_SLUG, custom token)schedule:trigger andclose-expired-entities:job whenexpiresis set, with correct fuzzy cronGH_AW_GITHUB_TOKENfallback when no custom token is configured