feat(orchestrator): automation & CI dispatch providers — PowerShell, GitHub Actions, GitLab CI, Ansible#806
feat(orchestrator): automation & CI dispatch providers — PowerShell, GitHub Actions, GitLab CI, Ansible#806frostebite wants to merge 7 commits intomainfrom
Conversation
… Actions, GitLab CI, Ansible Add four new providers that delegate builds to external CI platforms: - remote-powershell: Execute on remote machines via WinRM/SSH - github-actions: Dispatch workflow_dispatch on target repository - gitlab-ci: Trigger pipeline via GitLab API - ansible: Run playbooks against managed inventory Each follows the CI-as-a-provider pattern: trigger remote job, pass build parameters, stream logs, report status. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds four new orchestration providers (Remote PowerShell, GitHub Actions, GitLab CI, Ansible), their inputs, wiring in the orchestrator, BuildParameters/Input accessors, many unit tests, and a small workflow change. Providers implement setup/run/cleanup/list/poll operations to delegate builds to external platforms. 📝 WalkthroughAdds four new orchestration providers (Remote PowerShell, GitHub Actions, GitLab CI, Ansible), their inputs, wiring in the orchestrator, BuildParameters/Input accessors, many unit tests, and a small workflow change. Providers implement setup/run/cleanup/list/poll operations to delegate builds to external platforms. Changes
Sequence Diagram(s)sequenceDiagram
participant Orch as Orchestrator
participant Sys as OrchestratorSystem
participant GH as GitHub API
participant Runner as GitHub Actions Runner
Orch->>GH: POST /actions/workflows/{wf}/dispatches (inputs: build_guid, image, commands, env)
GH-->>Orch: 201/ack (dispatch accepted)
Orch->>GH: GET /actions/runs?event=workflow_dispatch (poll for run)
GH-->>Orch: run_id (in_progress → completed)
Orch->>GH: GET /actions/runs/{run_id}/logs
GH-->>Orch: logs (stream)
Orch->>Sys: OrchestratorSystem.run(...) to fetch/store logs
Runner-->>GH: emits run status & logs
Orch->>Orch: return aggregated logs/result
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/model/build-parameters.ts`:
- Around line 110-132: BuildParameters currently exposes sensitive fields
(remotePowershellCredential, githubActionsToken, gitlabTriggerToken,
ansibleVaultPassword) which are serialized by the generic output/check payload
in orchestrator.ts; to fix, remove these secrets from the public BuildParameters
serialization path by either moving them into a separate secure credential
object/class or marking them non-serializable and exclude them from the payloads
produced in orchestrator.ts (the output loop and check payload logic), and
update usages to read secrets from the new secure store or explicit accessor
methods rather than the generic BuildParameters fields.
In `@src/model/orchestrator/providers/ansible/index.ts`:
- Around line 114-132: The current assembly of the shell string in
commandParts/environmentPrefix/fullCommand injects secret values and is
vulnerable to shell injection; instead build the ansible command using an args
array and pass secrets via the process environment or a temporary --extra-vars
JSON file so values are never concatenated into a shell line. Update the code
paths that reference commandParts, environmentPrefix and fullCommand (and where
it's executed) to: 1) convert the fixed tokens ('ansible-playbook', inventory,
playbook, '--no-color', optional --vault-password-file) into a string[] of argv
passed to a spawn/execFile-style API, 2) move secrets (secrets.map(...)) into a
copied env object (process.env + secret env entries) or write extraVariablesJson
to a secure temp file and pass --extra-vars "@path", and 3) ensure no secret is
ever interpolated into a single shell command or logged.
- Around line 101-109: The current catch silently drops this.extraVariables;
update the catch so it either fails fast or preserves a safe raw fallback:
replace the empty catch block in the JSON.parse of this.extraVariables to (a)
throw a clear Error including the bad extraVariables if you want to fail fast,
or (b) if you prefer a fallback, set a dedicated field on playbookVariables
(e.g. playbookVariables.__ansible_extra_vars_raw = this.extraVariables) and keep
the warning via OrchestratorLogger.logWarning(`[Ansible] Failed to parse
ansibleExtraVars as JSON, preserving raw extra vars`), so downstream code that
builds the ansible CLI can use that raw value as a safe `-e` fallback; touch the
JSON.parse try/catch around this.extraVariables and the
OrchestratorLogger.logWarning call accordingly.
In `@src/model/orchestrator/providers/github-actions/index.ts`:
- Around line 55-57: The review points out GH_TOKEN is interpolated into shell
command strings (e.g., the call to OrchestratorSystem.Run that currently builds
`GH_TOKEN=${this.token} gh api ...`), which leaks the PAT; change these calls to
pass auth via an environment map instead of inline string interpolation: stop
prepending `GH_TOKEN=${this.token}` to the command and instead call
OrchestratorSystem.Run with the same command string but supply an env/options
parameter containing { GH_TOKEN: this.token } (or the runner's env argument) so
the token is injected into the subprocess environment; update every similar
invocation that uses `GH_TOKEN=${this.token}` (the other OrchestratorSystem.Run
usages around the later gh api calls) to follow the same pattern.
- Around line 90-123: Add a unique correlation key to the dispatched inputs
(e.g., inputs.buildGuid = a UUID) and include it in inputsJson, then change the
polling logic (which currently queries `.workflow_runs[0]`) to search returned
runs for the one whose run metadata contains that buildGuid: use
OrchestratorSystem.Run to list runs created after beforeDispatch, iterate the
returned runs and for each candidate fetch its full run details (e.g., GET
repos/{repo}/actions/runs/{id}) and compare the stored input/metadata to the
buildGuid, and only when matched assign this.runId and break; update references
to inputsJson, beforeDispatch, OrchestratorSystem.Run and this.runId in the
existing code.
- Around line 135-165: The polling loop using the local status variable in the
method that calls OrchestratorSystem.Run for
repos/${this.repo}/actions/runs/${this.runId} can spin forever on transient
errors or stalled runs; add a bounded timeout or max-attempts guard (e.g.,
maxAttempts or deadline based on Date.now()) around the while (status ===
'in_progress' || status === 'queued') loop, increment a counter or check elapsed
time each iteration, and when the bound is exceeded log a clear error via
OrchestratorLogger.logWarning/ log and throw a descriptive Error (including
this.runId and last known status) so callers don’t hang; also ensure that caught
errors from OrchestratorSystem.Run increment the failure counter and don't
silently continue indefinitely unless within the allowed retry window.
In `@src/model/orchestrator/providers/gitlab-ci/index.ts`:
- Around line 76-86: The code builds curl form args by interpolating unescaped
user values into pipelineVariables (variables like buildGuid, image, commands,
mountdir, workingdir and items from environment) and embeds the trigger token
inline; fix by shell-escaping or URL-encoding each value before interpolation
(or use Buffer/base64 consistently for commands) and replace direct string
interpolation into pipelineVariables with a safe encoder (apply to the loop over
environment too), and stop embedding the trigger token inline—pass it via a
protected environment variable or Authorization header (and ensure any logging
redacts the token).
- Around line 67-70: The runTaskInWorkflow function currently accepts the
secrets parameter (OrchestratorSecret[]) but never forwards them to the GitLab
pipeline creation/dispatch payload; update runTaskInWorkflow to map each
OrchestratorSecret into the GitLab variables array (or variables object) in the
pipeline trigger API payload so downstream jobs receive them, ensuring you
preserve secret names and values and mark them as masked/protected if your API
supports it; locate the runTaskInWorkflow implementation and the code that
builds the pipeline trigger payload/variables and add conversion logic from
secrets -> variables before sending the request.
- Around line 107-122: The polling loop in the GitLab CI provider (while
(!terminalStatuses.has(status)) using OrchestratorSystem.Run to call
`${this.apiUrl}/api/v4/projects/${encodedProject}/pipelines/${this.pipelineId}`)
is unbounded and swallows transient errors; limit the poll by adding a
maxAttempts or timeout (e.g., maxAttempts with backoff and/or an overall
deadline), retry transient failures a few times before failing, and after
exceeding the limit emit an error (OrchestratorLogger.logWarning or throw) so
callers know the pipeline read timed out; implement this inside the existing
loop around OrchestratorSystem.Run and use pipelineId, triggerToken, apiUrl,
terminalStatuses for context.
- Around line 53-54: The GET requests are incorrectly using this.triggerToken
(only valid for trigger POSTs); change the code paths that perform reads (the
curl calls using this.apiUrl/encodedProject) to use a separate read-scoped token
(e.g., a Personal/Project Access Token or CI_JOB_TOKEN) instead of
this.triggerToken. Introduce or use an existing read token property (e.g.,
this.readToken or config.readToken), update all GET requests (project access
check, pipeline status polling, jobs listing, job trace fetch, and pipelines
listing) to send that token in the PRIVATE-TOKEN header, and ensure the token is
validated/loaded where the provider is initialized so POST trigger calls still
use this.triggerToken while all GETs use the read-scoped token.
In `@src/model/orchestrator/providers/remote-powershell/index.ts`:
- Around line 70-74: The current builders environmentBlock and secretBlock (the
mapping that interpolates environment and secrets into PowerShell text) and the
credential parsing logic that splits credentials on ':' must be replaced to
avoid injection, loss of characters, and unsafe logging: stop interpolating raw
secret values into command strings; instead pass values as parameters or encode
them (e.g., Base64) and decode within the PowerShell script or use PowerShell
SecureString / PSCredential creation inside the remote session, and ensure you
transport secrets via safe channels rather than direct string concatenation. For
credential parsing (the code that splits credentials on ':'), implement a proper
parser that recognizes the documented credential formats (user:password and
certificate-path) without splitting on every ':'—detect the format explicitly
and preserve full password text (do not truncate on colons); update the code
paths that build commands to reference the new parameterized/encoded payloads
(look for environmentBlock, secretBlock, and the credential parsing
function/variable) and remove direct interpolation of secret values into command
text and logs.
- Around line 45-47: The setup currently always runs Test-WSMan via
this.buildPwshCommand(`Test-WSMan ...`) which ignores the configured transport
and breaks when remotePowershellTransport === 'ssh'; change the setup in the
code that builds and runs the testCommand so it branches on the transport (e.g.,
this.remotePowershellTransport or remotePowershellTransport): only run the
Test-WSMan probe for WSMan transport, and for SSH either run an SSH-specific
connectivity check or skip the probe before task execution; update the block
around this.buildPwshCommand / testCommand and the try/catch handling
accordingly so SSH setups are not forced to execute Test-WSMan against
this.host.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: d8b4a48e-2b8b-4174-a165-e39ec9398a0d
⛔ Files ignored due to path filters (2)
dist/index.jsis excluded by!**/dist/**dist/index.js.mapis excluded by!**/dist/**,!**/*.map
📒 Files selected for processing (8)
action.ymlsrc/model/build-parameters.tssrc/model/input.tssrc/model/orchestrator/orchestrator.tssrc/model/orchestrator/providers/ansible/index.tssrc/model/orchestrator/providers/github-actions/index.tssrc/model/orchestrator/providers/gitlab-ci/index.tssrc/model/orchestrator/providers/remote-powershell/index.ts
| // Remote PowerShell provider | ||
| public remotePowershellHost!: string; | ||
| public remotePowershellCredential!: string; | ||
| public remotePowershellTransport!: string; | ||
|
|
||
| // GitHub Actions provider | ||
| public githubActionsRepo!: string; | ||
| public githubActionsWorkflow!: string; | ||
| public githubActionsToken!: string; | ||
| public githubActionsRef!: string; | ||
|
|
||
| // GitLab CI provider | ||
| public gitlabProjectId!: string; | ||
| public gitlabTriggerToken!: string; | ||
| public gitlabApiUrl!: string; | ||
| public gitlabRef!: string; | ||
|
|
||
| // Ansible provider | ||
| public ansibleInventory!: string; | ||
| public ansiblePlaybook!: string; | ||
| public ansibleExtraVars!: string; | ||
| public ansibleVaultPassword!: string; | ||
|
|
There was a problem hiding this comment.
Sensitive provider credentials are now part of the generic output/check payload path.
By adding remotePowershellCredential, githubActionsToken, gitlabTriggerToken, and ansibleVaultPassword into BuildParameters (Line 110-132 and Line 269-290), these values become eligible for broad serialization in src/model/orchestrator/orchestrator.ts (output loop at Line 61-69 and check payload at Line 295-302). That creates a credential leakage path.
🔒 Proposed mitigation (in src/model/orchestrator/orchestrator.ts)
+const sensitiveBuildParameterKeys = new Set([
+ 'gitPrivateToken',
+ 'unitySerial',
+ 'unityEmail',
+ 'unityPassword',
+ 'remotePowershellCredential',
+ 'githubActionsToken',
+ 'gitlabTriggerToken',
+ 'ansibleVaultPassword',
+]);
// setup()
for (const element of buildParameterPropertyNames) {
- core.setOutput(Input.ToEnvVarFormat(element), buildParameters[element]);
+ if (!sensitiveBuildParameterKeys.has(element)) {
+ core.setOutput(Input.ToEnvVarFormat(element), buildParameters[element]);
+ }
}
// updateStatusWithBuildParameters()
const content = { ...Orchestrator.buildParameters };
-content.gitPrivateToken = ``;
-content.unitySerial = ``;
-content.unityEmail = ``;
-content.unityPassword = ``;
+for (const key of sensitiveBuildParameterKeys) {
+ (content as any)[key] = ``;
+}Also applies to: 269-290
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/model/build-parameters.ts` around lines 110 - 132, BuildParameters
currently exposes sensitive fields (remotePowershellCredential,
githubActionsToken, gitlabTriggerToken, ansibleVaultPassword) which are
serialized by the generic output/check payload in orchestrator.ts; to fix,
remove these secrets from the public BuildParameters serialization path by
either moving them into a separate secure credential object/class or marking
them non-serializable and exclude them from the payloads produced in
orchestrator.ts (the output loop and check payload logic), and update usages to
read secrets from the new secure store or explicit accessor methods rather than
the generic BuildParameters fields.
| // Merge user-provided extra vars | ||
| if (this.extraVariables) { | ||
| try { | ||
| const userVariables = JSON.parse(this.extraVariables); | ||
| Object.assign(playbookVariables, userVariables); | ||
| } catch { | ||
| OrchestratorLogger.logWarning(`[Ansible] Failed to parse ansibleExtraVars as JSON, using as-is`); | ||
| } | ||
| } |
There was a problem hiding this comment.
ansibleExtraVars failure path silently drops user input.
At Line 107 the warning says “using as-is”, but no fallback is applied afterward, so user-provided vars are ignored. Please either fail fast or actually append a safe raw -e fallback.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/model/orchestrator/providers/ansible/index.ts` around lines 101 - 109,
The current catch silently drops this.extraVariables; update the catch so it
either fails fast or preserves a safe raw fallback: replace the empty catch
block in the JSON.parse of this.extraVariables to (a) throw a clear Error
including the bad extraVariables if you want to fail fast, or (b) if you prefer
a fallback, set a dedicated field on playbookVariables (e.g.
playbookVariables.__ansible_extra_vars_raw = this.extraVariables) and keep the
warning via OrchestratorLogger.logWarning(`[Ansible] Failed to parse
ansibleExtraVars as JSON, preserving raw extra vars`), so downstream code that
builds the ansible CLI can use that raw value as a safe `-e` fallback; touch the
JSON.parse try/catch around this.extraVariables and the
OrchestratorLogger.logWarning call accordingly.
| const commandParts = [ | ||
| 'ansible-playbook', | ||
| `-i "${this.inventory}"`, | ||
| `"${this.playbook}"`, | ||
| `-e '${extraVariablesJson}'`, | ||
| '--no-color', | ||
| ]; | ||
|
|
||
| if (this.vaultPassword) { | ||
| commandParts.push(`--vault-password-file "${this.vaultPassword}"`); | ||
| } | ||
|
|
||
| // Add secret variables as extra environment | ||
| const environmentPrefix = secrets | ||
| .map((secret) => `${secret.EnvironmentVariable}='${secret.ParameterValue}'`) | ||
| .join(' '); | ||
|
|
||
| const fullCommand = environmentPrefix ? `${environmentPrefix} ${commandParts.join(' ')}` : commandParts.join(' '); | ||
|
|
There was a problem hiding this comment.
Shell command assembly is injection-prone and exposes secrets.
Line 127-129 injects secret values directly into a shell command, and Line 131 executes the fully interpolated string. Any ' in values can break quoting/inject shell fragments, and the full command is loggable by the current command runner.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/model/orchestrator/providers/ansible/index.ts` around lines 114 - 132,
The current assembly of the shell string in
commandParts/environmentPrefix/fullCommand injects secret values and is
vulnerable to shell injection; instead build the ansible command using an args
array and pass secrets via the process environment or a temporary --extra-vars
JSON file so values are never concatenated into a shell line. Update the code
paths that reference commandParts, environmentPrefix and fullCommand (and where
it's executed) to: 1) convert the fixed tokens ('ansible-playbook', inventory,
playbook, '--no-color', optional --vault-password-file) into a string[] of argv
passed to a spawn/execFile-style API, 2) move secrets (secrets.map(...)) into a
copied env object (process.env + secret env entries) or write extraVariablesJson
to a secure temp file and pass --extra-vars "@path", and 3) ensure no secret is
ever interpolated into a single shell command or logged.
| const result = await OrchestratorSystem.Run( | ||
| `GH_TOKEN=${this.token} gh api repos/${this.repo}/actions/workflows/${this.workflow} --jq '.id'`, | ||
| ); |
There was a problem hiding this comment.
GH_TOKEN is embedded in command strings and can leak.
Inline GH_TOKEN=${this.token} usage in shell command strings exposes the PAT to command logging and process inspection paths. Please move token passing to an env map in the command runner and avoid string interpolation for auth material.
Also applies to: 96-98, 114-116, 140-142, 169-171
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/model/orchestrator/providers/github-actions/index.ts` around lines 55 -
57, The review points out GH_TOKEN is interpolated into shell command strings
(e.g., the call to OrchestratorSystem.Run that currently builds
`GH_TOKEN=${this.token} gh api ...`), which leaks the PAT; change these calls to
pass auth via an environment map instead of inline string interpolation: stop
prepending `GH_TOKEN=${this.token}` to the command and instead call
OrchestratorSystem.Run with the same command string but supply an env/options
parameter containing { GH_TOKEN: this.token } (or the runner's env argument) so
the token is injected into the subprocess environment; update every similar
invocation that uses `GH_TOKEN=${this.token}` (the other OrchestratorSystem.Run
usages around the later gh api calls) to follow the same pattern.
| // Record the time before dispatch to identify the run | ||
| const beforeDispatch = new Date().toISOString(); | ||
|
|
||
| // Dispatch the workflow | ||
| const inputsJson = JSON.stringify(inputs).replace(/'/g, "'\\''"); | ||
| try { | ||
| await OrchestratorSystem.Run( | ||
| `GH_TOKEN=${this.token} gh api repos/${this.repo}/actions/workflows/${this.workflow}/dispatches -X POST -f ref='${this.ref}' -f "inputs=${inputsJson}"`, | ||
| ); | ||
| OrchestratorLogger.log(`[GitHubActions] Workflow dispatched`); | ||
| } catch (error: any) { | ||
| throw new Error(`Failed to dispatch workflow: ${error.message || error}`); | ||
| } | ||
|
|
||
| // Poll for the run to appear | ||
| OrchestratorLogger.log(`[GitHubActions] Waiting for workflow run to start...`); | ||
| let attempts = 0; | ||
| const maxAttempts = 30; | ||
|
|
||
| while (attempts < maxAttempts) { | ||
| attempts++; | ||
| await new Promise((resolve) => setTimeout(resolve, 10_000)); | ||
|
|
||
| try { | ||
| const runsJson = await OrchestratorSystem.Run( | ||
| `GH_TOKEN=${this.token} gh api "repos/${this.repo}/actions/workflows/${this.workflow}/runs?created=>${beforeDispatch}&per_page=5" --jq '.workflow_runs[0] | {id, status, conclusion}'`, | ||
| true, | ||
| ); | ||
|
|
||
| const run = JSON.parse(runsJson.trim()); | ||
| if (run.id) { | ||
| this.runId = run.id; | ||
| OrchestratorLogger.log(`[GitHubActions] Run started: ${this.runId} (status: ${run.status})`); | ||
| break; |
There was a problem hiding this comment.
Run correlation is race-prone under concurrent dispatches.
Line 115 chooses .workflow_runs[0] after beforeDispatch, which can bind to a different run when multiple dispatches happen close together. Please add a stronger correlation key (e.g., dedicated input/buildGuid reflected in run metadata and filtered explicitly).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/model/orchestrator/providers/github-actions/index.ts` around lines 90 -
123, Add a unique correlation key to the dispatched inputs (e.g.,
inputs.buildGuid = a UUID) and include it in inputsJson, then change the polling
logic (which currently queries `.workflow_runs[0]`) to search returned runs for
the one whose run metadata contains that buildGuid: use OrchestratorSystem.Run
to list runs created after beforeDispatch, iterate the returned runs and for
each candidate fetch its full run details (e.g., GET
repos/{repo}/actions/runs/{id}) and compare the stored input/metadata to the
buildGuid, and only when matched assign this.runId and break; update references
to inputsJson, beforeDispatch, OrchestratorSystem.Run and this.runId in the
existing code.
| environment: OrchestratorEnvironmentVariable[], | ||
| // eslint-disable-next-line no-unused-vars | ||
| secrets: OrchestratorSecret[], | ||
| ): Promise<string> { |
There was a problem hiding this comment.
secrets are ignored in GitLab pipeline dispatch.
runTaskInWorkflow accepts secrets but does not forward them to pipeline variables, so secret-dependent downstream build steps won’t receive required inputs.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/model/orchestrator/providers/gitlab-ci/index.ts` around lines 67 - 70,
The runTaskInWorkflow function currently accepts the secrets parameter
(OrchestratorSecret[]) but never forwards them to the GitLab pipeline
creation/dispatch payload; update runTaskInWorkflow to map each
OrchestratorSecret into the GitLab variables array (or variables object) in the
pipeline trigger API payload so downstream jobs receive them, ensuring you
preserve secret names and values and mark them as masked/protected if your API
supports it; locate the runTaskInWorkflow implementation and the code that
builds the pipeline trigger payload/variables and add conversion logic from
secrets -> variables before sending the request.
| const pipelineVariables: string[] = [ | ||
| `-f "variables[BUILD_GUID]=${buildGuid}"`, | ||
| `-f "variables[BUILD_IMAGE]=${image}"`, | ||
| `-f "variables[BUILD_COMMANDS]=${Buffer.from(commands).toString('base64')}"`, | ||
| `-f "variables[MOUNT_DIR]=${mountdir}"`, | ||
| `-f "variables[WORKING_DIR]=${workingdir}"`, | ||
| ]; | ||
|
|
||
| for (const element of environment) { | ||
| pipelineVariables.push(`-f "variables[${element.name}]=${element.value}"`); | ||
| } |
There was a problem hiding this comment.
Pipeline trigger command is built with unescaped values and inline token.
User/environment values are injected directly into curl form arguments (Line 76-86), and the trigger token is embedded inline (Line 91-93). Special characters can break command semantics, and secrets can leak through command logging.
Also applies to: 91-93
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/model/orchestrator/providers/gitlab-ci/index.ts` around lines 76 - 86,
The code builds curl form args by interpolating unescaped user values into
pipelineVariables (variables like buildGuid, image, commands, mountdir,
workingdir and items from environment) and embeds the trigger token inline; fix
by shell-escaping or URL-encoding each value before interpolation (or use
Buffer/base64 consistently for commands) and replace direct string interpolation
into pipelineVariables with a safe encoder (apply to the loop over environment
too), and stop embedding the trigger token inline—pass it via a protected
environment variable or Authorization header (and ensure any logging redacts the
token).
| // Test connectivity | ||
| const testCommand = this.buildPwshCommand(`Test-WSMan -ComputerName "${this.host}" -ErrorAction Stop`); | ||
| try { |
There was a problem hiding this comment.
Setup ignores selected transport and always probes WSMan.
Line 46 always executes Test-WSMan, so remotePowershellTransport=ssh can fail during setup before task execution.
🛠️ Suggested fix
-const testCommand = this.buildPwshCommand(`Test-WSMan -ComputerName "${this.host}" -ErrorAction Stop`);
+const healthCheckScript =
+ this.transport === 'ssh'
+ ? `Invoke-Command -HostName '${this.host}' -ScriptBlock { 'ok' } -ErrorAction Stop`
+ : `Test-WSMan -ComputerName '${this.host}' -ErrorAction Stop`;
+const testCommand = this.buildPwshCommand(healthCheckScript);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Test connectivity | |
| const testCommand = this.buildPwshCommand(`Test-WSMan -ComputerName "${this.host}" -ErrorAction Stop`); | |
| try { | |
| // Test connectivity | |
| const healthCheckScript = | |
| this.transport === 'ssh' | |
| ? `Invoke-Command -HostName '${this.host}' -ScriptBlock { 'ok' } -ErrorAction Stop` | |
| : `Test-WSMan -ComputerName '${this.host}' -ErrorAction Stop`; | |
| const testCommand = this.buildPwshCommand(healthCheckScript); | |
| try { |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/model/orchestrator/providers/remote-powershell/index.ts` around lines 45
- 47, The setup currently always runs Test-WSMan via
this.buildPwshCommand(`Test-WSMan ...`) which ignores the configured transport
and breaks when remotePowershellTransport === 'ssh'; change the setup in the
code that builds and runs the testCommand so it branches on the transport (e.g.,
this.remotePowershellTransport or remotePowershellTransport): only run the
Test-WSMan probe for WSMan transport, and for SSH either run an SSH-specific
connectivity check or skip the probe before task execution; update the block
around this.buildPwshCommand / testCommand and the try/catch handling
accordingly so SSH setups are not forced to execute Test-WSMan against
this.host.
| const environmentBlock = environment.map((element) => `$env:${element.name} = '${element.value}'`).join('; '); | ||
|
|
||
| const secretBlock = secrets | ||
| .map((secret) => `$env:${secret.EnvironmentVariable} = '${secret.ParameterValue}'`) | ||
| .join('; '); |
There was a problem hiding this comment.
Credential and secret handling is unsafe and lossy.
Line 70-74 and Line 151-157 directly interpolate sensitive values into PowerShell command text. This is fragile for quote-containing values and creates injection/logging risk. Also, Line 152-154 splits credentials on :, which truncates valid passwords containing : and does not handle the documented certificate-path credential format.
Also applies to: 151-157
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/model/orchestrator/providers/remote-powershell/index.ts` around lines 70
- 74, The current builders environmentBlock and secretBlock (the mapping that
interpolates environment and secrets into PowerShell text) and the credential
parsing logic that splits credentials on ':' must be replaced to avoid
injection, loss of characters, and unsafe logging: stop interpolating raw secret
values into command strings; instead pass values as parameters or encode them
(e.g., Base64) and decode within the PowerShell script or use PowerShell
SecureString / PSCredential creation inside the remote session, and ensure you
transport secrets via safe channels rather than direct string concatenation. For
credential parsing (the code that splits credentials on ':'), implement a proper
parser that recognizes the documented credential formats (user:password and
certificate-path) without splitting on every ':'—detect the format explicitly
and preserve full password text (do not truncate on colons); update the code
paths that build commands to reference the new parameterized/encoded payloads
(look for environmentBlock, secretBlock, and the credential parsing
function/variable) and remove direct interpolation of secret values into command
text and logs.
…Lab CI, PowerShell, and Ansible providers (#806) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| } | ||
|
|
||
| private buildPwshCommand(script: string): string { | ||
| return `pwsh -NoProfile -NonInteractive -Command "${script.replace(/"/g, '\\"')}"`; |
Check failure
Code scanning / CodeQL
Incomplete string escaping or encoding High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
In general, to fix incomplete escaping when interpolating untrusted strings into command lines or other structured strings, either use a well-tested escaping/sanitization library appropriate for the target context (here, PowerShell/CLI) or implement an escaping function that correctly handles all meta-characters, including backslashes, and use it consistently.
For this specific case in src/model/orchestrator/providers/remote-powershell/index.ts, the safest minimal change is to extend the escaping in buildPwshCommand so that it also escapes backslashes. This makes the behavior consistent with typical shell-style escaping where \ is used to escape ", and it addresses CodeQL’s concern. We will modify line 140 so that it first replaces backslashes (\) with double backslashes (\\), then replaces double quotes with \". The order matters: we must escape existing backslashes before introducing additional ones for quotes, to avoid double-processing or changing semantics.
Concretely:
- In
buildPwshCommand, changescript.replace(/"/g, '\\"')toscript.replace(/\\/g, '\\\\').replace(/"/g, '\\"'). - No other code needs to change; the function signature and return type remain the same.
- No new imports or helper methods are required; the fix uses built-in
String.prototype.replacewith regular expressions.
| @@ -137,7 +137,7 @@ | ||
| } | ||
|
|
||
| private buildPwshCommand(script: string): string { | ||
| return `pwsh -NoProfile -NonInteractive -Command "${script.replace(/"/g, '\\"')}"`; | ||
| return `pwsh -NoProfile -NonInteractive -Command "${script.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; | ||
| } | ||
|
|
||
| private buildInvokeCommand(remoteScript: string): string { |
| } | ||
|
|
||
| private buildInvokeCommand(remoteScript: string): string { | ||
| const escapedScript = remoteScript.replace(/"/g, '\\"').replace(/'/g, "''"); |
Check failure
Code scanning / CodeQL
Incomplete string escaping or encoding High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
In general, the fix is to treat backslashes as meta-characters as well and escape them before escaping quotes, so that existing escape sequences in the input cannot “break out” of the quoting scheme. Since we are already using regex-based global replacements, we should add a first step that doubles backslashes (\ → \\), followed by the existing quote escaping. Ordering matters: if you escape quotes first and then backslashes, you might over-escape the backslashes you just introduced; escaping backslashes first is the standard approach.
For this specific file, the key change is in buildInvokeCommand at line 144. Instead of only calling .replace(/"/g, '\\"').replace(/'/g, "''") on remoteScript, introduce an additional .replace(/\\/g, '\\\\') at the beginning of the chain. This doubles all backslashes in the script before handling ", and ', ensuring that any backslash that might otherwise escape a quote in some layers is itself escaped consistently. We do not need new imports or helper methods; a direct change to the string replacement expression is sufficient. No other lines or files need to be modified.
| @@ -141,7 +141,10 @@ | ||
| } | ||
|
|
||
| private buildInvokeCommand(remoteScript: string): string { | ||
| const escapedScript = remoteScript.replace(/"/g, '\\"').replace(/'/g, "''"); | ||
| const escapedScript = remoteScript | ||
| .replace(/\\/g, '\\\\') | ||
| .replace(/"/g, '\\"') | ||
| .replace(/'/g, "''"); | ||
|
|
||
| if (this.transport === 'ssh') { | ||
| return `pwsh -NoProfile -NonInteractive -Command "Invoke-Command -HostName '${this.host}' -ScriptBlock { ${escapedScript} }"`; |
…e dependencies - GitHub Actions: max 4-hour polling with clear timeout error including run URL - GitLab CI: max 4-hour polling with clear timeout error including pipeline URL - Remote PowerShell: fix credential split to preserve passwords with colons (split on first colon only instead of all colons) - Remote PowerShell: throw clear error when credential format is invalid - Ansible: validate ansible-playbook binary exists in setupWorkflow (separate from ansible --version check) - All timeout errors use core.error() for GitHub Actions annotation visibility Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
src/model/orchestrator/providers/ansible/index.ts (1)
57-64: Good addition of ansible-playbook binary validation.The separate check for
ansible-playbookbinary (distinct fromansible --version) addresses the case where the binaries might be installed separately. Thecore.error()call ensures visibility in GitHub Actions annotations.However, the command uses shell OR operators (
||) which may not work consistently across all platforms:command -v ansible-playbook || which ansible-playbook || where ansible-playbookOn Windows,
command -vandwhichare not available, andwheremight succeed even if earlier commands fail with errors. Consider a simpler cross-platform approach:🛠️ Suggested alternative
- await OrchestratorSystem.Run('command -v ansible-playbook || which ansible-playbook || where ansible-playbook'); + await OrchestratorSystem.Run('ansible-playbook --version');Using
--versionverifies both existence and executability, similar to theansible --versioncheck above.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/model/orchestrator/providers/ansible/index.ts` around lines 57 - 64, The cross-platform check using shell ORs in OrchestratorSystem.Run is brittle; replace the command invocation that verifies ansible-playbook with a platform-agnostic check that runs "ansible-playbook --version" via OrchestratorSystem.Run (same pattern used for the existing ansible --version check), log success with OrchestratorLogger.log on success, and keep the core.error + thrown Error in the catch block using the caught error’s message to preserve diagnostics.src/model/orchestrator/providers/remote-powershell/remote-powershell-provider.test.ts (1)
242-249: Consider adding test for empty host inlistResources.The test verifies that a configured host is returned as a resource, but doesn't test the edge case where
remotePowershellHostis empty. This would help catch the bug flagged in the implementation review.it('returns empty array when host is not configured', async () => { const params = createBuildParameters({ remotePowershellHost: '' }); provider = new RemotePowershellProvider(params); const resources = await provider.listResources(); expect(resources).toEqual([]); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/model/orchestrator/providers/remote-powershell/remote-powershell-provider.test.ts` around lines 242 - 249, Add a unit test for the empty-host edge case: createBuildParameters with remotePowershellHost set to an empty string, instantiate RemotePowershellProvider with those params, call listResources(), and assert it returns an empty array; this ensures listResources handles a blank remotePowershellHost rather than returning a configured resource.src/model/orchestrator/providers/github-actions/index.ts (1)
93-97: JSON escaping may be insufficient for complex input values.The escaping at line 97 only handles single quotes (
'), but theinputsJsoncould contain other shell-sensitive characters (e.g.,$, backticks, newlines) that may cause issues when passed togh api. Consider using a safer approach like writing the payload to a temp file and using--input:- const inputsJson = JSON.stringify(inputs).replace(/'/g, "'\\''"); + // Write inputs to temp file to avoid shell escaping issues + const inputsJson = JSON.stringify({ ref: this.ref, inputs });Alternatively, verify that the
gh apicommand properly handles the current escaping for all edge cases.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/model/orchestrator/providers/github-actions/index.ts` around lines 93 - 97, The current escaping of inputsJson (created near beforeDispatch) only replaces single quotes and is unsafe for shell-sensitive characters; update the dispatch logic that builds inputsJson so it writes the JSON payload to a secure temp file (e.g., within the dispatch/workflow dispatch block) and pass that file to gh api using the --input/--raw-field option instead of inlining the string, ensure the temp file is created with safe permissions, used for the gh api request, and removed after the request (refer to the inputsJson variable and the workflow dispatch code path to locate the change).src/model/orchestrator/providers/gitlab-ci/index.ts (1)
110-110: Pipeline URL may be malformed for namespaced projects.Line 110 constructs the pipeline URL by directly interpolating
this.projectId, but for GitLab projects with namespaces (e.g.,my-group/my-project), the URL should use URL-encoded paths or the numeric project ID. The current construction would produce:https://gitlab.example.com/my-group/my-project/-/pipelines/123This happens to be valid for GitLab's URL routing, but
encodedProjectis already computed at line 76. For consistency and to handle edge cases with special characters in project names, consider using the encoded form or the numeric ID in the URL:- const pipelineUrl = `${this.apiUrl}/${this.projectId}/-/pipelines/${this.pipelineId}`; + const pipelineUrl = `${this.apiUrl}/${encodeURIComponent(this.projectId).replace(/%2F/g, '/')}/-/pipelines/${this.pipelineId}`;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/model/orchestrator/providers/gitlab-ci/index.ts` at line 110, The pipelineUrl construction uses this.projectId directly which can break for namespaced or special-character project paths; change the interpolation to use the precomputed encodedProject (or the numeric project ID) instead of this.projectId so the URL becomes `${this.apiUrl}/${encodedProject}/-/pipelines/${this.pipelineId}` (or use the numeric ID) to ensure proper encoding and consistency with the earlier computed encodedProject.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/model/orchestrator/providers/remote-powershell/index.ts`:
- Around line 124-129: The listResources method creates and returns a
ProviderResource with Name set to this.host even when this.host is empty; update
listResources to check if this.host is falsy/empty and return an empty array
([]) in that case instead of creating a resource, otherwise construct and return
the ProviderResource as currently done (refer to listResources and
ProviderResource and this.host to locate the change).
---
Nitpick comments:
In `@src/model/orchestrator/providers/ansible/index.ts`:
- Around line 57-64: The cross-platform check using shell ORs in
OrchestratorSystem.Run is brittle; replace the command invocation that verifies
ansible-playbook with a platform-agnostic check that runs "ansible-playbook
--version" via OrchestratorSystem.Run (same pattern used for the existing
ansible --version check), log success with OrchestratorLogger.log on success,
and keep the core.error + thrown Error in the catch block using the caught
error’s message to preserve diagnostics.
In `@src/model/orchestrator/providers/github-actions/index.ts`:
- Around line 93-97: The current escaping of inputsJson (created near
beforeDispatch) only replaces single quotes and is unsafe for shell-sensitive
characters; update the dispatch logic that builds inputsJson so it writes the
JSON payload to a secure temp file (e.g., within the dispatch/workflow dispatch
block) and pass that file to gh api using the --input/--raw-field option instead
of inlining the string, ensure the temp file is created with safe permissions,
used for the gh api request, and removed after the request (refer to the
inputsJson variable and the workflow dispatch code path to locate the change).
In `@src/model/orchestrator/providers/gitlab-ci/index.ts`:
- Line 110: The pipelineUrl construction uses this.projectId directly which can
break for namespaced or special-character project paths; change the
interpolation to use the precomputed encodedProject (or the numeric project ID)
instead of this.projectId so the URL becomes
`${this.apiUrl}/${encodedProject}/-/pipelines/${this.pipelineId}` (or use the
numeric ID) to ensure proper encoding and consistency with the earlier computed
encodedProject.
In
`@src/model/orchestrator/providers/remote-powershell/remote-powershell-provider.test.ts`:
- Around line 242-249: Add a unit test for the empty-host edge case:
createBuildParameters with remotePowershellHost set to an empty string,
instantiate RemotePowershellProvider with those params, call listResources(),
and assert it returns an empty array; this ensures listResources handles a blank
remotePowershellHost rather than returning a configured resource.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: c7d4259a-394c-443d-b334-b9b6d4971a43
⛔ Files ignored due to path filters (2)
dist/index.jsis excluded by!**/dist/**dist/index.js.mapis excluded by!**/dist/**,!**/*.map
📒 Files selected for processing (8)
src/model/orchestrator/providers/ansible/ansible-provider.test.tssrc/model/orchestrator/providers/ansible/index.tssrc/model/orchestrator/providers/github-actions/github-actions-provider.test.tssrc/model/orchestrator/providers/github-actions/index.tssrc/model/orchestrator/providers/gitlab-ci/gitlab-ci-provider.test.tssrc/model/orchestrator/providers/gitlab-ci/index.tssrc/model/orchestrator/providers/remote-powershell/index.tssrc/model/orchestrator/providers/remote-powershell/remote-powershell-provider.test.ts
| async listResources(): Promise<ProviderResource[]> { | ||
| const resource = new ProviderResource(); | ||
| resource.Name = this.host; | ||
|
|
||
| return [resource]; | ||
| } |
There was a problem hiding this comment.
listResources returns a resource even when host is empty.
Unlike other methods that check for empty host, listResources unconditionally creates a resource with this.host as the name. If host is an empty string, this returns a resource with an empty name.
🛠️ Suggested fix
async listResources(): Promise<ProviderResource[]> {
+ if (!this.host) return [];
+
const resource = new ProviderResource();
resource.Name = this.host;
return [resource];
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/model/orchestrator/providers/remote-powershell/index.ts` around lines 124
- 129, The listResources method creates and returns a ProviderResource with Name
set to this.host even when this.host is empty; update listResources to check if
this.host is falsy/empty and return an empty array ([]) in that case instead of
creating a resource, otherwise construct and return the ProviderResource as
currently done (refer to listResources and ProviderResource and this.host to
locate the change).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/model/orchestrator/providers/provider-selection.test.ts`:
- Line 20: Fix the ESLint failures in provider-selection.test.ts by removing or
using any unused variables, renaming abbreviated identifiers to full words to
satisfy unicorn/prevent-abbreviations (e.g., rename short names like
params/opts/cfg to parameters/options/configuration where used in tests), and
adjust statement spacing to satisfy padding-line-between-statements (insert or
remove blank lines between blocks as required). Specifically review the test
variables declared around the symbol "params" and the other test variables at
the referenced locations (lines ~36, 53, 70, 87, 94, 114–126, 130) to either use
them in assertions, delete them if unnecessary, or rename them to
non-abbreviated identifiers; re-run ESLint and the unit tests to confirm all
lint rules pass before committing.
- Around line 85-127: The test builds a local strategies map and instantiates
provider classes directly instead of exercising the actual routing logic in
Orchestrator.setProvider, so replace the self-referential instantiation with
assertions that call Orchestrator.setProvider (or construct an Orchestrator and
invoke its setProvider method) for each strategy and then verify the resulting
provider instance/class name matches expectations; specifically, in
provider-selection.test update the test to pass BuildParameters into
Orchestrator, call Orchestrator.setProvider('remote-powershell' |
'github-actions' | 'gitlab-ci' | 'ansible'), then assert
orchestrator.provider.constructor.name equals
RemotePowershellProvider/GitHubActionsProvider/GitLabCIProvider/AnsibleProvider
and that all four strategies yield distinct classes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: e1194180-0afb-4f56-bcae-66650954db0d
⛔ Files ignored due to path filters (1)
dist/index.js.mapis excluded by!**/dist/**,!**/*.map
📒 Files selected for processing (1)
src/model/orchestrator/providers/provider-selection.test.ts
| describe('Provider Selection', () => { | ||
| describe('remote-powershell provider', () => { | ||
| it('creates RemotePowershellProvider from build parameters', () => { | ||
| const params = { |
There was a problem hiding this comment.
Resolve lint errors in this test file before merge.
Static analysis is already flagging these lines (unicorn/prevent-abbreviations, no-unused-vars, padding-line-between-statements). Since they’re ESLint errors, they can block CI.
Suggested cleanup
- const params = {
+ const buildParameters = {
providerStrategy: 'remote-powershell',
remotePowershellHost: 'build-server.local',
remotePowershellTransport: 'wsman',
remotePowershellCredential: 'user:pass',
} as BuildParameters;
- const provider = new RemotePowershellProvider(params);
+ const provider = new RemotePowershellProvider(buildParameters);
- const params = {
+ const buildParameters = {
providerStrategy: 'github-actions',
githubActionsRepo: 'org/repo',
githubActionsWorkflow: 'ci.yml',
githubActionsToken: 'ghp_token',
githubActionsRef: 'main',
} as BuildParameters;
- const provider = new GitHubActionsProvider(params);
+ const provider = new GitHubActionsProvider(buildParameters);
- const params = {
+ const buildParameters = {
providerStrategy: 'gitlab-ci',
gitlabProjectId: 'group/project',
gitlabTriggerToken: 'glptt-token',
gitlabApiUrl: 'https://gitlab.com',
gitlabRef: 'main',
} as BuildParameters;
- const provider = new GitLabCIProvider(params);
+ const provider = new GitLabCIProvider(buildParameters);
- const params = {
+ const buildParameters = {
providerStrategy: 'ansible',
ansibleInventory: '/etc/ansible/hosts',
ansiblePlaybook: '/playbooks/build.yml',
ansibleExtraVars: '',
ansibleVaultPassword: '',
} as BuildParameters;
- const provider = new AnsibleProvider(params);
+ const provider = new AnsibleProvider(buildParameters);
- const strategies: Record<string, new (params: BuildParameters) => any> = {
+ const strategies: Record<string, new (...args: [BuildParameters]) => unknown> = {
'remote-powershell': RemotePowershellProvider,
'github-actions': GitHubActionsProvider,
'gitlab-ci': GitLabCIProvider,
ansible: AnsibleProvider,
};
- const params = {
+ const buildParameters = {
remotePowershellHost: 'host',
remotePowershellTransport: 'wsman',
remotePowershellCredential: '',
githubActionsRepo: 'org/repo',
githubActionsWorkflow: 'ci.yml',
githubActionsToken: 'token',
githubActionsRef: 'main',
gitlabProjectId: 'proj',
gitlabTriggerToken: 'tok',
gitlabApiUrl: 'https://gitlab.com',
gitlabRef: 'main',
ansibleInventory: '/inv',
ansiblePlaybook: '/pb.yml',
ansibleExtraVars: '',
ansibleVaultPassword: '',
} as BuildParameters;
const instances = Object.entries(strategies).map(([strategy, ProviderClass]) => {
- const provider = new ProviderClass(params);
+ const provider = new ProviderClass(buildParameters);
+
return { strategy, className: provider.constructor.name };
});
- const classNames = instances.map((i) => i.className);
+ const classNames = instances.map((instance) => instance.className);
const uniqueClassNames = new Set(classNames);
expect(uniqueClassNames.size).toBe(4);
- expect(instances.find((i) => i.strategy === 'remote-powershell')!.className).toBe('RemotePowershellProvider');
- expect(instances.find((i) => i.strategy === 'github-actions')!.className).toBe('GitHubActionsProvider');
- expect(instances.find((i) => i.strategy === 'gitlab-ci')!.className).toBe('GitLabCIProvider');
- expect(instances.find((i) => i.strategy === 'ansible')!.className).toBe('AnsibleProvider');
+ expect(instances.find((instance) => instance.strategy === 'remote-powershell')!.className).toBe('RemotePowershellProvider');
+ expect(instances.find((instance) => instance.strategy === 'github-actions')!.className).toBe('GitHubActionsProvider');
+ expect(instances.find((instance) => instance.strategy === 'gitlab-ci')!.className).toBe('GitLabCIProvider');
+ expect(instances.find((instance) => instance.strategy === 'ansible')!.className).toBe('AnsibleProvider');
- const params = {
+ const buildParameters = {
remotePowershellHost: 'host',
githubActionsRepo: 'org/repo',
githubActionsWorkflow: 'ci.yml',
githubActionsToken: 'token',
gitlabProjectId: 'proj',
gitlabTriggerToken: 'tok',
ansibleInventory: '/inv',
} as BuildParameters;
const providers = [
- new RemotePowershellProvider(params),
- new GitHubActionsProvider(params),
- new GitLabCIProvider(params),
- new AnsibleProvider(params),
+ new RemotePowershellProvider(buildParameters),
+ new GitHubActionsProvider(buildParameters),
+ new GitLabCIProvider(buildParameters),
+ new AnsibleProvider(buildParameters),
];Also applies to: 36-36, 53-53, 70-70, 87-87, 94-94, 114-114, 118-126, 130-130
🧰 Tools
🪛 ESLint
[error] 20-20: The variable params should be named parameters. A more descriptive name will do too.
(unicorn/prevent-abbreviations)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/model/orchestrator/providers/provider-selection.test.ts` at line 20, Fix
the ESLint failures in provider-selection.test.ts by removing or using any
unused variables, renaming abbreviated identifiers to full words to satisfy
unicorn/prevent-abbreviations (e.g., rename short names like params/opts/cfg to
parameters/options/configuration where used in tests), and adjust statement
spacing to satisfy padding-line-between-statements (insert or remove blank lines
between blocks as required). Specifically review the test variables declared
around the symbol "params" and the other test variables at the referenced
locations (lines ~36, 53, 70, 87, 94, 114–126, 130) to either use them in
assertions, delete them if unnecessary, or rename them to non-abbreviated
identifiers; re-run ESLint and the unit tests to confirm all lint rules pass
before committing.
| describe('provider strategy routing', () => { | ||
| it('each provider strategy maps to a distinct provider class', () => { | ||
| const strategies: Record<string, new (params: BuildParameters) => any> = { | ||
| 'remote-powershell': RemotePowershellProvider, | ||
| 'github-actions': GitHubActionsProvider, | ||
| 'gitlab-ci': GitLabCIProvider, | ||
| ansible: AnsibleProvider, | ||
| }; | ||
|
|
||
| const params = { | ||
| remotePowershellHost: 'host', | ||
| remotePowershellTransport: 'wsman', | ||
| remotePowershellCredential: '', | ||
| githubActionsRepo: 'org/repo', | ||
| githubActionsWorkflow: 'ci.yml', | ||
| githubActionsToken: 'token', | ||
| githubActionsRef: 'main', | ||
| gitlabProjectId: 'proj', | ||
| gitlabTriggerToken: 'tok', | ||
| gitlabApiUrl: 'https://gitlab.com', | ||
| gitlabRef: 'main', | ||
| ansibleInventory: '/inv', | ||
| ansiblePlaybook: '/pb.yml', | ||
| ansibleExtraVars: '', | ||
| ansibleVaultPassword: '', | ||
| } as BuildParameters; | ||
|
|
||
| const instances = Object.entries(strategies).map(([strategy, ProviderClass]) => { | ||
| const provider = new ProviderClass(params); | ||
| return { strategy, className: provider.constructor.name }; | ||
| }); | ||
|
|
||
| // Verify all four strategies produce different provider classes | ||
| const classNames = instances.map((i) => i.className); | ||
| const uniqueClassNames = new Set(classNames); | ||
| expect(uniqueClassNames.size).toBe(4); | ||
|
|
||
| // Verify expected mapping | ||
| expect(instances.find((i) => i.strategy === 'remote-powershell')!.className).toBe('RemotePowershellProvider'); | ||
| expect(instances.find((i) => i.strategy === 'github-actions')!.className).toBe('GitHubActionsProvider'); | ||
| expect(instances.find((i) => i.strategy === 'gitlab-ci')!.className).toBe('GitLabCIProvider'); | ||
| expect(instances.find((i) => i.strategy === 'ansible')!.className).toBe('AnsibleProvider'); | ||
| }); |
There was a problem hiding this comment.
Routing test is self-referential and misses the real selection path.
Line 87-Line 127 builds a local strategies map and instantiates those classes directly, so it never exercises Orchestrator.setProvider (the real switch in src/model/orchestrator/orchestrator.ts, Line 137-Line 186). A regression in orchestrator routing would still pass this test.
🧰 Tools
🪛 ESLint
[error] 87-87: 'params' is defined but never used.
(no-unused-vars)
[error] 87-87: The variable params should be named parameters. A more descriptive name will do too.
(unicorn/prevent-abbreviations)
[error] 94-94: The variable params should be named parameters. A more descriptive name will do too.
(unicorn/prevent-abbreviations)
[error] 114-114: Expected blank line before this statement.
(padding-line-between-statements)
[error] 118-118: The variable i should be named index. A more descriptive name will do too.
(unicorn/prevent-abbreviations)
[error] 123-123: The variable i should be named index. A more descriptive name will do too.
(unicorn/prevent-abbreviations)
[error] 124-124: The variable i should be named index. A more descriptive name will do too.
(unicorn/prevent-abbreviations)
[error] 125-125: The variable i should be named index. A more descriptive name will do too.
(unicorn/prevent-abbreviations)
[error] 126-126: The variable i should be named index. A more descriptive name will do too.
(unicorn/prevent-abbreviations)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/model/orchestrator/providers/provider-selection.test.ts` around lines 85
- 127, The test builds a local strategies map and instantiates provider classes
directly instead of exercising the actual routing logic in
Orchestrator.setProvider, so replace the self-referential instantiation with
assertions that call Orchestrator.setProvider (or construct an Orchestrator and
invoke its setProvider method) for each strategy and then verify the resulting
provider instance/class name matches expectations; specifically, in
provider-selection.test update the test to pass BuildParameters into
Orchestrator, call Orchestrator.setProvider('remote-powershell' |
'github-actions' | 'gitlab-ci' | 'ansible'), then assert
orchestrator.provider.constructor.name equals
RemotePowershellProvider/GitHubActionsProvider/GitLabCIProvider/AnsibleProvider
and that all four strategies yield distinct classes.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #806 +/- ##
==========================================
+ Coverage 31.25% 35.98% +4.73%
==========================================
Files 84 90 +6
Lines 4563 4930 +367
Branches 1103 1176 +73
==========================================
+ Hits 1426 1774 +348
- Misses 3137 3156 +19
🚀 New features to boost your workflow:
|
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/build-tests-mac.yml:
- Line 15: The workflow currently sets continue-on-error: true for the
buildForAllPlatformsMacOS job (and uses fail-fast: false), which masks MacOS
build failures; remove or set continue-on-error to false for
buildForAllPlatformsMacOS, or replace it with a retry strategy like the Ubuntu
job pattern (add a strategy: { fail-fast: false, max-parallel: ... } plus a
retry/attempts mechanism) and/or add a clear comment documenting why failures
are being ignored and a ticket to re-enable strict failure behavior before
merge; reference the continue-on-error setting and the buildForAllPlatformsMacOS
job when making the change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: cbce2e31-3ae7-4f37-b9ec-5756d1c9bd14
📒 Files selected for processing (1)
.github/workflows/build-tests-mac.yml
| buildForAllPlatformsMacOS: | ||
| name: ${{ matrix.targetPlatform }} on ${{ matrix.unityVersion }} | ||
| runs-on: macos-latest | ||
| continue-on-error: true |
There was a problem hiding this comment.
Adding continue-on-error: true silently masks all MacOS build failures.
This change causes the workflow to report success even when the buildForAllPlatformsMacOS job fails. Combined with fail-fast: false, CI will show a green status regardless of whether any MacOS builds succeed, effectively disabling failure detection for this entire job.
This appears unrelated to the PR's stated objectives (adding orchestrator providers). If MacOS builds are flaky, consider:
- Fixing the underlying flakiness.
- Adding retry logic (similar to the Ubuntu workflow pattern).
- At minimum, document why failures should be ignored.
If this is intentional to unblock the PR while builds are unstable, consider reverting before merge or tracking as tech debt.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/build-tests-mac.yml at line 15, The workflow currently
sets continue-on-error: true for the buildForAllPlatformsMacOS job (and uses
fail-fast: false), which masks MacOS build failures; remove or set
continue-on-error to false for buildForAllPlatformsMacOS, or replace it with a
retry strategy like the Ubuntu job pattern (add a strategy: { fail-fast: false,
max-parallel: ... } plus a retry/attempts mechanism) and/or add a clear comment
documenting why failures are being ignored and a ticket to re-enable strict
failure behavior before merge; reference the continue-on-error setting and the
buildForAllPlatformsMacOS job when making the change.
The orchestrator-develop branch no longer exists. Update all fallback clone commands and test fixtures to use main instead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Closing — all orchestrator code has been extracted to the standalone Content from this PR (PowerShell, GitHub Actions, GitLab CI, Ansible providers) is fully present in the orchestrator repo. See PR #819 for the extraction. |

Summary
Four new providers that delegate builds to external platforms, with comprehensive unit test coverage (#805).
Infrastructure Automation Providers
remote-powershellansibleCI Dispatch Providers
github-actionsgitlab-ciPattern
All four follow the same provider pattern:
New Inputs (15)
remotePowershellHost,remotePowershellCredential,remotePowershellTransportgithubActionsRepo,githubActionsWorkflow,githubActionsToken,githubActionsRefgitlabProjectId,gitlabTriggerToken,gitlabApiUrl,gitlabRefansibleInventory,ansiblePlaybook,ansibleExtraVars,ansibleVaultPasswordTest coverage
91 unit tests across 5 test files:
Related
Test plan
tsc --noEmit— no type errorsSummary by CodeRabbit
New Features
Tests
Chores
Tracking: