-
Notifications
You must be signed in to change notification settings - Fork 295
Explore CentralRepoOps #16950
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Explore CentralRepoOps #16950
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,198 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: Reusable Central Deterministic Gate | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| on: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workflow_call: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| inputs: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rollout_profile: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: Rollout profile (pilot, standard, broad) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| required: false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: standard | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| targets_pilot_json: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: JSON array of owner/repo targets for pilot profile | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| required: false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: "[]" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| targets_standard_json: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: JSON array of owner/repo targets for standard profile | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| required: false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: "[]" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| targets_broad_json: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: JSON array of owner/repo targets for broad profile | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| required: false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: "[]" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| target_ref: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: Default target git ref for deterministic checkout/build | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| required: false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: main | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| shard_count: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: Number of rollout shards | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| required: false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: "1" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| shard_index: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: Zero-based shard index to execute | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| required: false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: "0" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| outputs: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| primary_target_repo: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: Primary repository used by the agentic wrapper checkout step | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: ${{ jobs.select_shard.outputs.primary_target_repo }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| target_ref: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: Target git ref applied to deterministic checkout/build | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: ${{ jobs.resolve_policy.outputs.target_ref }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: ${{ jobs.resolve_policy.outputs.target_ref }} | |
| value: ${{ jobs.resolve_policy.outputs.target_ref }} | |
| selected_targets_json: | |
| description: JSON array of selected owner/repo targets for this shard | |
| value: ${{ jobs.select_shard.outputs.selected_targets_json }} |
Copilot
AI
Feb 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Target repo validation is very loose (repo.includes('/')), which allows values that aren't valid owner/repo (extra slashes, whitespace, etc.) to pass the gate and then fail later during checkout. Tighten validation by trimming and requiring exactly one slash and non-empty owner/repo parts (or use a stricter regex) so invalid targets fail with a clear policy error.
| try { | |
| const parsed = JSON.parse(targetsByProfile[profile]); | |
| if (!Array.isArray(parsed) || !parsed.every((repo) => typeof repo === 'string' && repo.includes('/'))) { | |
| core.setFailed(`targets_${profile}_json must be a JSON array of owner/repo strings`); | |
| return; | |
| } | |
| targets = parsed; | |
| const isValidRepo = (value) => { | |
| if (typeof value !== 'string') return false; | |
| const trimmed = value.trim(); | |
| if (!trimmed) return false; | |
| const parts = trimmed.split('/'); | |
| if (parts.length !== 2) return false; | |
| const [owner, repo] = parts; | |
| if (!owner || !repo) return false; | |
| // Disallow whitespace within owner or repo segments | |
| if (/\s/.test(owner) || /\s/.test(repo)) return false; | |
| return true; | |
| }; | |
| try { | |
| const parsed = JSON.parse(targetsByProfile[profile]); | |
| if (!Array.isArray(parsed) || !parsed.every(isValidRepo)) { | |
| core.setFailed(`targets_${profile}_json must be a JSON array of owner/repo strings`); | |
| return; | |
| } | |
| targets = parsed.map((repo) => repo.trim()); |
Copilot
AI
Feb 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The deterministic Node build path runs npm ci whenever package.json exists. npm ci fails if no lockfile is present (common in repos using yarn/pnpm or no lock committed), which can cause false-negative gate failures. Consider checking for package-lock.json/npm-shrinkwrap.json before npm ci, and falling back to npm install or skipping JS build when no lockfile is available.
| npm ci | |
| npm run --if-present build | |
| if [[ -f package-lock.json || -f npm-shrinkwrap.json ]]; then | |
| npm ci | |
| npm run --if-present build | |
| else | |
| echo "package.json found, but no npm lockfile (package-lock.json or npm-shrinkwrap.json) present; skipping Node deterministic build." | |
| fi |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,105 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| name: Reusable Central Replay Failed Shards | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| on: | ||||||||||||||||||||||||||||||||||||||||||||||
| workflow_call: | ||||||||||||||||||||||||||||||||||||||||||||||
| inputs: | ||||||||||||||||||||||||||||||||||||||||||||||
| failed_repos_json: | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+3
to
+6
|
||||||||||||||||||||||||||||||||||||||||||||||
| description: JSON array of owner/repo values that failed in a previous run | ||||||||||||||||||||||||||||||||||||||||||||||
| required: false | ||||||||||||||||||||||||||||||||||||||||||||||
| default: "[]" | ||||||||||||||||||||||||||||||||||||||||||||||
| type: string | ||||||||||||||||||||||||||||||||||||||||||||||
| target_ref: | ||||||||||||||||||||||||||||||||||||||||||||||
| description: Git ref used for replay checkout/build validation | ||||||||||||||||||||||||||||||||||||||||||||||
| required: false | ||||||||||||||||||||||||||||||||||||||||||||||
| default: main | ||||||||||||||||||||||||||||||||||||||||||||||
| type: string | ||||||||||||||||||||||||||||||||||||||||||||||
| max_replays: | ||||||||||||||||||||||||||||||||||||||||||||||
| description: Maximum number of failed repositories to replay in one run | ||||||||||||||||||||||||||||||||||||||||||||||
| required: false | ||||||||||||||||||||||||||||||||||||||||||||||
| default: "10" | ||||||||||||||||||||||||||||||||||||||||||||||
| type: string | ||||||||||||||||||||||||||||||||||||||||||||||
| outputs: | ||||||||||||||||||||||||||||||||||||||||||||||
| replay_targets_json: | ||||||||||||||||||||||||||||||||||||||||||||||
| description: JSON array of replay targets selected for this run | ||||||||||||||||||||||||||||||||||||||||||||||
| value: ${{ jobs.select_replays.outputs.replay_targets_json }} | ||||||||||||||||||||||||||||||||||||||||||||||
| replay_count: | ||||||||||||||||||||||||||||||||||||||||||||||
| description: Number of replay targets selected | ||||||||||||||||||||||||||||||||||||||||||||||
| value: ${{ jobs.select_replays.outputs.replay_count }} | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| permissions: | ||||||||||||||||||||||||||||||||||||||||||||||
| contents: read | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| jobs: | ||||||||||||||||||||||||||||||||||||||||||||||
| select_replays: | ||||||||||||||||||||||||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||||||||||||||||||||||||
| outputs: | ||||||||||||||||||||||||||||||||||||||||||||||
| replay_targets_json: ${{ steps.select.outputs.replay_targets_json }} | ||||||||||||||||||||||||||||||||||||||||||||||
| replay_count: ${{ steps.select.outputs.replay_count }} | ||||||||||||||||||||||||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||||||||||||||||||||||||
| - id: select | ||||||||||||||||||||||||||||||||||||||||||||||
| uses: actions/github-script@v7 | ||||||||||||||||||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||||||||||||||||||
| FAILED_REPOS_JSON: ${{ inputs.failed_repos_json }} | ||||||||||||||||||||||||||||||||||||||||||||||
| MAX_REPLAYS: ${{ inputs.max_replays }} | ||||||||||||||||||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||||||||||||||||||
| script: | | ||||||||||||||||||||||||||||||||||||||||||||||
| const raw = process.env.FAILED_REPOS_JSON || '[]'; | ||||||||||||||||||||||||||||||||||||||||||||||
| const maxReplaysRaw = String(process.env.MAX_REPLAYS || '10').trim(); | ||||||||||||||||||||||||||||||||||||||||||||||
| const maxReplays = Number.parseInt(maxReplaysRaw, 10); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (!Number.isInteger(maxReplays) || maxReplays <= 0) { | ||||||||||||||||||||||||||||||||||||||||||||||
| core.setFailed(`max_replays must be a positive integer, got '${maxReplaysRaw}'`); | ||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| let repos; | ||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| repos = JSON.parse(raw); | ||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||
| core.setFailed(`failed_repos_json is not valid JSON: ${error.message}`); | ||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (!Array.isArray(repos)) { | ||||||||||||||||||||||||||||||||||||||||||||||
| core.setFailed('failed_repos_json must be a JSON array'); | ||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const cleaned = [...new Set(repos.filter((repo) => typeof repo === 'string' && repo.includes('/')))]; | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
| const cleaned = [...new Set(repos.filter((repo) => typeof repo === 'string' && repo.includes('/')))]; | |
| const normalizeRepo = (value) => { | |
| if (typeof value !== 'string') return null; | |
| const trimmed = value.trim(); | |
| if (!trimmed) return null; | |
| const parts = trimmed.split('/'); | |
| if (parts.length !== 2) return null; | |
| const [owner, repo] = parts; | |
| if (!owner || !repo) return null; | |
| return `${owner}/${repo}`; | |
| }; | |
| const cleaned = [ | |
| ...new Set( | |
| repos | |
| .map(normalizeRepo) | |
| .filter((repo) => repo !== null) | |
| ), | |
| ]; |
Copilot
AI
Feb 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The replay validation runs npm ci based solely on package.json. npm ci requires a lockfile and will fail for repos without package-lock.json/npm-shrinkwrap.json (or using pnpm/yarn), which can make replays fail even when the repo is healthy. Consider conditioning npm ci on a lockfile and/or providing a safer fallback.
| npm ci | |
| if [[ -f package-lock.json || -f npm-shrinkwrap.json ]]; then | |
| npm ci | |
| else | |
| echo "No npm lockfile detected; using 'npm install' instead of 'npm ci'." | |
| npm install | |
| fi |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This reusable workflow uses
secrets.REPO_TOKENfor cross-repo checkouts, but the secret is not declared underon.workflow_call.secrets. Declare the expected secret(s) (e.g.,REPO_TOKEN) so callers can pass them (including viasecrets: inherit) and the workflow fails fast with a clear contract.