fix(providers): add polling timeouts, fix credential parsing, validate 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>
This commit is contained in:
frostebite
2026-03-05 13:04:15 +00:00
parent ff56194b30
commit 120c3c5b24
8 changed files with 203 additions and 40 deletions
@@ -148,11 +148,17 @@ class RemotePowershellProvider implements ProviderInterface {
}
// WinRM (default)
const credentialPart = this.credential
? `-Credential (New-Object PSCredential('${this.credential.split(':')[0]}', (ConvertTo-SecureString '${
this.credential.split(':')[1]
}' -AsPlainText -Force)))`
: '';
// Split on the FIRST colon only — passwords may contain colons
let credentialPart = '';
if (this.credential) {
const colonIndex = this.credential.indexOf(':');
if (colonIndex === -1) {
throw new Error('remotePowershellCredential must be in "username:password" format (no colon found)');
}
const user = this.credential.substring(0, colonIndex);
const pass = this.credential.substring(colonIndex + 1);
credentialPart = `-Credential (New-Object PSCredential('${user}', (ConvertTo-SecureString '${pass}' -AsPlainText -Force)))`;
}
return `pwsh -NoProfile -NonInteractive -Command "Invoke-Command -ComputerName '${this.host}' ${credentialPart} -ScriptBlock { ${escapedScript} }"`;
}
@@ -140,9 +140,7 @@ describe('RemotePowershellProvider', () => {
it('includes secrets in the remote script block', async () => {
mockRun.mockResolvedValueOnce('output');
const secrets = [
{ ParameterKey: 'key1', EnvironmentVariable: 'SECRET_KEY', ParameterValue: 'secret-val-123' },
];
const secrets = [{ ParameterKey: 'key1', EnvironmentVariable: 'SECRET_KEY', ParameterValue: 'secret-val-123' }];
await provider.runTaskInWorkflow('guid-sec', 'img', 'build-cmd', '/m', '/w', [], secrets as any);
@@ -178,13 +176,38 @@ describe('RemotePowershellProvider', () => {
const execError = new Error('Remote execution failed: access denied');
mockRun.mockRejectedValueOnce(execError);
await expect(
provider.runTaskInWorkflow('guid-fail', 'img', 'cmd', '/m', '/w', [], []),
).rejects.toThrow('Remote execution failed');
await expect(provider.runTaskInWorkflow('guid-fail', 'img', 'cmd', '/m', '/w', [], [])).rejects.toThrow(
'Remote execution failed',
);
expect(mockLogWarning).toHaveBeenCalledWith(expect.stringContaining('Task failed'));
});
it('preserves passwords containing colons when splitting credentials', async () => {
const params = createBuildParameters({
remotePowershellCredential: 'admin:P@ss:w0rd:with:colons!',
});
provider = new RemotePowershellProvider(params);
mockRun.mockResolvedValueOnce('output');
await provider.runTaskInWorkflow('guid-colon', 'img', 'cmd', '/m', '/w', [], []);
const command = mockRun.mock.calls[0][0];
expect(command).toContain("PSCredential('admin'");
expect(command).toContain("ConvertTo-SecureString 'P@ss:w0rd:with:colons!'");
});
it('throws when credential has no colon separator', async () => {
const params = createBuildParameters({
remotePowershellCredential: 'nocolonhere',
});
provider = new RemotePowershellProvider(params);
await expect(provider.runTaskInWorkflow('guid-badcred', 'img', 'cmd', '/m', '/w', [], [])).rejects.toThrow(
'username:password',
);
});
it('sets working directory in the remote script', async () => {
mockRun.mockResolvedValueOnce('output');