diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml
index 5f2506a..c6865b9 100644
--- a/.github/workflows/static-analysis.yml
+++ b/.github/workflows/static-analysis.yml
@@ -6,6 +6,7 @@ on:
branches:
- main
- develop
+ - feature/*
jobs:
build:
runs-on: ubuntu-latest
@@ -13,14 +14,14 @@ jobs:
strategy:
matrix:
include:
- - php: "8.1"
- phpunit: "10"
- phpunit-config: "phpunit-10.xml.dist"
- php: "8.2"
phpunit: "11"
- phpunit-config: "phpunit.xml.dist"
+ phpunit-config: "phpunit-11.xml.dist"
- php: "8.3"
- phpunit: "11"
+ phpunit: "12"
+ phpunit-config: "phpunit-12.xml.dist"
+ - php: "8.4"
+ phpunit: "13"
phpunit-config: "phpunit.xml.dist"
steps:
diff --git a/.gitignore b/.gitignore
index dd969ed..72a28b5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,5 @@ Tests/data/**
!Tests/data/temp/.empty
composer.lock
.phpunit.cache
+GEMINI.md
+AGENTS.md
diff --git a/Asm/Ansible/Ansible.php b/Asm/Ansible/Ansible.php
index 49ec365..c7c38d0 100644
--- a/Asm/Ansible/Ansible.php
+++ b/Asm/Ansible/Ansible.php
@@ -6,6 +6,8 @@
use Asm\Ansible\Command\AnsibleGalaxy;
use Asm\Ansible\Command\AnsibleGalaxyInterface;
+use Asm\Ansible\Command\AnsibleGalaxyCollection;
+use Asm\Ansible\Command\AnsibleGalaxyCollectionInterface;
use Asm\Ansible\Command\AnsiblePlaybook;
use Asm\Ansible\Command\AnsiblePlaybookInterface;
use Asm\Ansible\Exception\CommandException;
@@ -44,8 +46,11 @@ final class Ansible implements LoggerAwareInterface
* @param string $playbookCommand path to playbook executable, default ansible-playbook
* @param string $galaxyCommand path to galaxy executable, default ansible-galaxy
*/
- public function __construct(string $ansibleBaseDir, string $playbookCommand = '', string $galaxyCommand = '')
- {
+ public function __construct(
+ string $ansibleBaseDir,
+ string $playbookCommand = '',
+ string $galaxyCommand = ''
+ ) {
$this->ansibleBaseDir = $this->checkDir($ansibleBaseDir);
$this->playbookCommand = $this->checkCommand($playbookCommand, 'ansible-playbook');
$this->galaxyCommand = $this->checkCommand($galaxyCommand, 'ansible-galaxy');
@@ -80,6 +85,19 @@ public function galaxy(): AnsibleGalaxyInterface
);
}
+ /**
+ * AnsibleGalaxyCollection instance creator
+ *
+ * @return AnsibleGalaxyCollectionInterface
+ */
+ public function galaxyCollection(): AnsibleGalaxyCollectionInterface
+ {
+ return new AnsibleGalaxyCollection(
+ $this->createProcess($this->galaxyCommand),
+ $this->logger
+ );
+ }
+
/**
* Set process timeout in seconds.
*
diff --git a/Asm/Ansible/Collection/AnsibleCollection.php b/Asm/Ansible/Collection/AnsibleCollection.php
new file mode 100644
index 0000000..8b71403
--- /dev/null
+++ b/Asm/Ansible/Collection/AnsibleCollection.php
@@ -0,0 +1,61 @@
+processRunner->run($command);
+
+ if (!$result->isSuccessful()) {
+ throw new CollectionException(
+ "Failed to install collection: $collection",
+ $result->getExitCode(),
+ null,
+ implode(' ', $command),
+ $result->getOutput()
+ );
+ }
+
+ return $result;
+ }
+
+ public function list(): ProcessResult
+ {
+ return $this->processRunner->run(['ansible-galaxy', 'collection', 'list']);
+ }
+
+ public function uninstall(string $collection): ProcessResult
+ {
+ $command = ['ansible-galaxy', 'collection', 'remove', $collection];
+
+ $result = $this->processRunner->run($command);
+
+ if (!$result->isSuccessful()) {
+ throw new CollectionException(
+ "Failed to uninstall collection: $collection",
+ $result->getExitCode(),
+ null,
+ implode(' ', $command),
+ $result->getOutput()
+ );
+ }
+
+ return $result;
+ }
+}
diff --git a/Asm/Ansible/Command/AbstractAnsibleCommand.php b/Asm/Ansible/Command/AbstractAnsibleCommand.php
index 41cbefa..eea3887 100644
--- a/Asm/Ansible/Command/AbstractAnsibleCommand.php
+++ b/Asm/Ansible/Command/AbstractAnsibleCommand.php
@@ -40,7 +40,7 @@ abstract class AbstractAnsibleCommand
* @param ProcessBuilderInterface $processBuilder
* @param LoggerInterface|null $logger
*/
- public function __construct(ProcessBuilderInterface $processBuilder, LoggerInterface $logger = null)
+ public function __construct(ProcessBuilderInterface $processBuilder, ?LoggerInterface $logger = null)
{
$this->processBuilder = $processBuilder;
$this->options = [];
@@ -59,7 +59,7 @@ public function __construct(ProcessBuilderInterface $processBuilder, LoggerInter
protected function prepareArguments(bool $asArray = true): string|array
{
$arguments = array_merge(
- [$this->getBaseOptions()],
+ $this->getBaseOptionsAsArray(),
$this->getOptions(),
$this->getParameters()
);
@@ -142,6 +142,16 @@ protected function getBaseOptions(): string
return implode(' ', $this->baseOptions);
}
+ /**
+ * Get base options as array.
+ *
+ * @return array
+ */
+ protected function getBaseOptionsAsArray(): array
+ {
+ return $this->baseOptions;
+ }
+
/**
* Check if param is array or string and implode with glue if necessary.
*
diff --git a/Asm/Ansible/Command/AnsibleGalaxy.php b/Asm/Ansible/Command/AnsibleGalaxy.php
index 9c0157d..e39407d 100644
--- a/Asm/Ansible/Command/AnsibleGalaxy.php
+++ b/Asm/Ansible/Command/AnsibleGalaxy.php
@@ -33,6 +33,7 @@ public function execute(?callable $callback = null): int|string
public function init(string $roleName): AnsibleGalaxyInterface
{
$this
+ ->addBaseOption('role')
->addBaseOption('init')
->addBaseOption($roleName);
@@ -51,6 +52,7 @@ public function info(string $role, string $version = ''): AnsibleGalaxyInterface
}
$this
+ ->addBaseOption('role')
->addBaseOption('info')
->addBaseOption($role);
@@ -70,10 +72,16 @@ public function install(string|array $roles = ''): AnsibleGalaxyInterface
{
$roles = $this->checkParam($roles, ' ');
+ $this->addBaseOption('role');
$this->addBaseOption('install');
if ('' !== $roles) {
- $this->addBaseOption($roles);
+ $rolesArray = explode(' ', $roles);
+ foreach ($rolesArray as $role) {
+ if ($role !== '') {
+ $this->addBaseOption($role);
+ }
+ }
}
return $this;
@@ -87,6 +95,7 @@ public function install(string|array $roles = ''): AnsibleGalaxyInterface
*/
public function modulelist(string $roleName = ''): AnsibleGalaxyInterface
{
+ $this->addBaseOption('role');
$this->addBaseOption('list');
if ('' !== $roleName) {
@@ -106,9 +115,17 @@ public function remove(string|array $roles = ''): AnsibleGalaxyInterface
{
$roles = $this->checkParam($roles, ' ');
- $this
- ->addBaseOption('remove')
- ->addBaseOption($roles);
+ $this->addBaseOption('role');
+ $this->addBaseOption('remove');
+
+ if ('' !== $roles) {
+ $rolesArray = explode(' ', $roles);
+ foreach ($rolesArray as $role) {
+ if ($role !== '') {
+ $this->addBaseOption($role);
+ }
+ }
+ }
return $this;
}
diff --git a/Asm/Ansible/Command/AnsibleGalaxyCollection.php b/Asm/Ansible/Command/AnsibleGalaxyCollection.php
new file mode 100644
index 0000000..eec8cf5
--- /dev/null
+++ b/Asm/Ansible/Command/AnsibleGalaxyCollection.php
@@ -0,0 +1,149 @@
+runProcess($callback);
+ }
+
+ /**
+ * Initialize a new collection with base structure.
+ *
+ * @param string $collectionName
+ * @return AnsibleGalaxyCollectionInterface
+ */
+ public function init(string $collectionName): AnsibleGalaxyCollectionInterface
+ {
+ $this
+ ->addBaseOption('collection')
+ ->addBaseOption('init')
+ ->addBaseOption($collectionName);
+
+ return $this;
+ }
+
+ /**
+ * Build an Ansible collection artifact that can be published to Ansible Galaxy.
+ *
+ * @param string $collectionPath
+ * @return AnsibleGalaxyCollectionInterface
+ */
+ public function build(string $collectionPath = ''): AnsibleGalaxyCollectionInterface
+ {
+ $this->addBaseOption('collection');
+ $this->addBaseOption('build');
+
+ if ('' !== $collectionPath) {
+ $this->addBaseOption($collectionPath);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Publish a collection artifact to Ansible Galaxy.
+ *
+ * @param string $artifactPath
+ * @return AnsibleGalaxyCollectionInterface
+ */
+ public function publish(string $artifactPath): AnsibleGalaxyCollectionInterface
+ {
+ $this->addBaseOption('collection');
+ $this->addBaseOption('publish');
+ $this->addBaseOption($artifactPath);
+
+ return $this;
+ }
+
+ /**
+ * Install collection(s).
+ *
+ * @param string|array $collections collection_name(s) | path/to/collection.tar.gz
+ * @return AnsibleGalaxyCollectionInterface
+ */
+ public function install(string|array $collections = ''): AnsibleGalaxyCollectionInterface
+ {
+ $collections = $this->checkParam($collections, ' ');
+
+ $this->addBaseOption('collection');
+ $this->addBaseOption('install');
+
+ if ('' !== $collections) {
+ $collectionsArray = explode(' ', $collections);
+ foreach ($collectionsArray as $collection) {
+ if ($collection !== '') {
+ $this->addBaseOption($collection);
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * The path in which the skeleton collection will be created.
+ * The default is the current working directory.
+ *
+ * @param string $path
+ * @return AnsibleGalaxyCollectionInterface
+ */
+ public function initPath(string $path = ''): AnsibleGalaxyCollectionInterface
+ {
+ $this->addOption('--init-path', $path);
+
+ return $this;
+ }
+
+ /**
+ * Force overwriting an existing collection.
+ *
+ * @return AnsibleGalaxyCollectionInterface
+ */
+ public function force(): AnsibleGalaxyCollectionInterface
+ {
+ $this->addParameter('--force');
+
+ return $this;
+ }
+
+ /**
+ * The path to the directory containing your collections.
+ *
+ * @param string $collectionsPath
+ * @return AnsibleGalaxyCollectionInterface
+ */
+ public function collectionsPath(string $collectionsPath): AnsibleGalaxyCollectionInterface
+ {
+ $this->addOption('--collections-path', $collectionsPath);
+
+ return $this;
+ }
+
+ /**
+ * Get parameter string which will be used to call ansible.
+ *
+ * @param bool $asArray
+ * @return string|array
+ */
+ public function getCommandlineArguments(bool $asArray = true): string|array
+ {
+ return $this->prepareArguments($asArray);
+ }
+}
diff --git a/Asm/Ansible/Command/AnsibleGalaxyCollectionInterface.php b/Asm/Ansible/Command/AnsibleGalaxyCollectionInterface.php
new file mode 100644
index 0000000..7111c8a
--- /dev/null
+++ b/Asm/Ansible/Command/AnsibleGalaxyCollectionInterface.php
@@ -0,0 +1,68 @@
+addParameter('--ask-su-pass');
-
- return $this;
- }
-
/**
* Ask for sudo password.
*
@@ -118,6 +106,45 @@ public function becomeUser(string $user = 'root'): AnsiblePlaybookInterface
return $this;
}
+ /**
+ * privilege escalation method to use (default=sudo)
+ *
+ * @param string $method
+ * @return AnsiblePlaybookInterface
+ */
+ public function becomeMethod(string $method = 'sudo'): AnsiblePlaybookInterface
+ {
+ $this->addOption('--become-method', $method);
+
+ return $this;
+ }
+
+ /**
+ * Become password file
+ *
+ * @param string $file
+ * @return AnsiblePlaybookInterface
+ */
+ public function becomePasswordFile(string $file): AnsiblePlaybookInterface
+ {
+ $this->addOption('--become-password-file', $file);
+
+ return $this;
+ }
+
+ /**
+ * Connection password file
+ *
+ * @param string $file
+ * @return AnsiblePlaybookInterface
+ */
+ public function connectionPasswordFile(string $file): AnsiblePlaybookInterface
+ {
+ $this->addOption('--connection-password-file', $file);
+
+ return $this;
+ }
+
/**
* Don't make any changes; instead, try to predict some of the changes that may occur.
*
@@ -361,6 +388,18 @@ public function listTasks(): AnsiblePlaybookInterface
return $this;
}
+ /**
+ * List all available tags.
+ *
+ * @return AnsiblePlaybookInterface
+ */
+ public function listTags(): AnsiblePlaybookInterface
+ {
+ $this->addParameter('--list-tags');
+
+ return $this;
+ }
+
/**
* Specify path(s) to module library (default=/usr/share/ansible/).
*
@@ -465,31 +504,6 @@ public function step(): AnsiblePlaybookInterface
return $this;
}
- /**
- * Run operations with su.
- *
- * @return AnsiblePlaybookInterface
- */
- public function su(): AnsiblePlaybookInterface
- {
- $this->addParameter('--su');
-
- return $this;
- }
-
- /**
- * Run operations with su as this user (default=root).
- *
- * @param string $user
- * @return AnsiblePlaybookInterface
- */
- public function suUser(string $user = 'root'): AnsiblePlaybookInterface
- {
- $this->addOption('--su-user', $user);
-
- return $this;
- }
-
/**
* Perform a syntax check on the playbook, but do not execute it.
*
@@ -592,32 +606,6 @@ public function flushCache(): AnsiblePlaybookInterface
return $this;
}
- /**
- * the new vault identity to use for rekey
- *
- * @param string $vaultId
- * @return AnsiblePlaybookInterface
- */
- public function newVaultId(string $vaultId): AnsiblePlaybookInterface
- {
- $this->addOption('--new-vault-id', $vaultId);
-
- return $this;
- }
-
- /**
- * new vault password file for rekey
- *
- * @param string $passwordFile
- * @return AnsiblePlaybookInterface
- */
- public function newVaultPasswordFile(string $passwordFile): AnsiblePlaybookInterface
- {
- $this->addOption('--new-vault-password-file', $passwordFile);
-
- return $this;
- }
-
/**
* specify extra arguments to pass to scp only (e.g. -l)
*
@@ -751,8 +739,12 @@ public function sshPipelining(bool $enable = false): AnsiblePlaybookInterface
private function checkInventory(): void
{
if (!$this->hasInventory) {
- $inventory = str_replace('.yml', '', $this->getBaseOptions());
- $this->inventoryFile($inventory);
+ $baseOptions = $this->getBaseOptionsAsArray();
+ $playbook = isset($baseOptions[0]) ? $baseOptions[0] : '';
+ $inventory = str_replace('.yml', '', $playbook);
+ if ($inventory !== '') {
+ $this->inventoryFile($inventory);
+ }
}
}
}
diff --git a/Asm/Ansible/Command/AnsiblePlaybookInterface.php b/Asm/Ansible/Command/AnsiblePlaybookInterface.php
index 6412ecf..299d716 100644
--- a/Asm/Ansible/Command/AnsiblePlaybookInterface.php
+++ b/Asm/Ansible/Command/AnsiblePlaybookInterface.php
@@ -27,13 +27,6 @@ public function play(string $playbook): AnsiblePlaybookInterface;
*/
public function askPass(): AnsiblePlaybookInterface;
- /**
- * Ask for su password.
- *
- * @return AnsiblePlaybookInterface
- */
- public function askSuPass(): AnsiblePlaybookInterface;
-
/**
* Ask for sudo password.
*
@@ -64,6 +57,30 @@ public function become(): AnsiblePlaybookInterface;
*/
public function becomeUser(string $user = 'root'): AnsiblePlaybookInterface;
+ /**
+ * privilege escalation method to use (default=sudo)
+ *
+ * @param string $method
+ * @return AnsiblePlaybookInterface
+ */
+ public function becomeMethod(string $method = 'sudo'): AnsiblePlaybookInterface;
+
+ /**
+ * Become password file
+ *
+ * @param string $file
+ * @return AnsiblePlaybookInterface
+ */
+ public function becomePasswordFile(string $file): AnsiblePlaybookInterface;
+
+ /**
+ * Connection password file
+ *
+ * @param string $file
+ * @return AnsiblePlaybookInterface
+ */
+ public function connectionPasswordFile(string $file): AnsiblePlaybookInterface;
+
/**
* Don't make any changes; instead, try to predict some of the changes that may occur.
*
@@ -191,28 +208,19 @@ public function listHosts(): AnsiblePlaybookInterface;
public function listTasks(): AnsiblePlaybookInterface;
/**
- * Specify path(s) to module library (default=/usr/share/ansible/).
+ * List all available tags.
*
- * @param array $path list of paths for modules
* @return AnsiblePlaybookInterface
*/
- public function modulePath(array $path = ['/usr/share/ansible/']): AnsiblePlaybookInterface;
+ public function listTags(): AnsiblePlaybookInterface;
/**
- * the new vault identity to use for rekey
- *
- * @param string $vaultId
- * @return AnsiblePlaybookInterface
- */
- public function newVaultId(string $vaultId): AnsiblePlaybookInterface;
-
- /**
- * new vault password file for rekey
+ * Specify path(s) to module library (default=/usr/share/ansible/).
*
- * @param string $passwordFile
+ * @param array $path list of paths for modules
* @return AnsiblePlaybookInterface
*/
- public function newVaultPasswordFile(string $passwordFile): AnsiblePlaybookInterface;
+ public function modulePath(array $path = ['/usr/share/ansible/']): AnsiblePlaybookInterface;
/**
* Disable cowsay
@@ -267,21 +275,6 @@ public function startAtTask(string $task): AnsiblePlaybookInterface;
*/
public function step(): AnsiblePlaybookInterface;
- /**
- * Run operations with su.
- *
- * @return AnsiblePlaybookInterface
- */
- public function su(): AnsiblePlaybookInterface;
-
- /**
- * Run operations with su as this user (default=root).
- *
- * @param string $user
- * @return AnsiblePlaybookInterface
- */
- public function suUser(string $user = 'root'): AnsiblePlaybookInterface;
-
/**
* specify extra arguments to pass to scp only (e.g. -l)
*
diff --git a/Asm/Ansible/Exception/AnsibleException.php b/Asm/Ansible/Exception/AnsibleException.php
new file mode 100644
index 0000000..0240dcb
--- /dev/null
+++ b/Asm/Ansible/Exception/AnsibleException.php
@@ -0,0 +1,28 @@
+command;
+ }
+
+ public function getOutput(): ?string
+ {
+ return $this->output;
+ }
+}
diff --git a/Asm/Ansible/Exception/CollectionException.php b/Asm/Ansible/Exception/CollectionException.php
new file mode 100644
index 0000000..8ba63ac
--- /dev/null
+++ b/Asm/Ansible/Exception/CollectionException.php
@@ -0,0 +1,9 @@
+hosts[$host] = $variables;
+ return $this;
+ }
+
+ public function addGroup(string $group, array $hosts = []): self
+ {
+ $this->groups[$group] = $hosts;
+ return $this;
+ }
+
+ public function setGroupVariables(string $group, array $variables): self
+ {
+ $this->variables[$group] = $variables;
+ return $this;
+ }
+
+ public function toYaml(): string
+ {
+ $inventory = ['all' => ['hosts' => $this->hosts]];
+
+ foreach ($this->groups as $group => $hosts) {
+ $inventory['all']['children'][$group] = ['hosts' => []];
+ foreach ($hosts as $host) {
+ $inventory['all']['children'][$group]['hosts'][$host] = null;
+ }
+ }
+
+ foreach ($this->variables as $group => $vars) {
+ if (isset($inventory['all']['children'][$group])) {
+ $inventory['all']['children'][$group]['vars'] = $vars;
+ }
+ }
+
+ return Yaml::dump($inventory);
+ }
+
+ public function saveToFile(string $filename): void
+ {
+ $yaml = $this->toYaml();
+ if (file_put_contents($filename, $yaml) === false) {
+ throw new InventoryException("Failed to save inventory to file: $filename");
+ }
+ }
+}
diff --git a/Asm/Ansible/Process/ProcessBuilder.php b/Asm/Ansible/Process/ProcessBuilder.php
index b1002fe..062fd93 100644
--- a/Asm/Ansible/Process/ProcessBuilder.php
+++ b/Asm/Ansible/Process/ProcessBuilder.php
@@ -64,6 +64,17 @@ public function setArguments(array $arguments): ProcessBuilderInterface
return $this;
}
+ /**
+ * @param string $path
+ * @return ProcessBuilderInterface
+ */
+ public function setWorkingDirectory(string $path): ProcessBuilderInterface
+ {
+ $this->path = $path;
+
+ return $this;
+ }
+
/**
* @param int $timeout
* @return ProcessBuilderInterface
diff --git a/Asm/Ansible/Process/ProcessBuilderInterface.php b/Asm/Ansible/Process/ProcessBuilderInterface.php
index 12bb73f..5d68e56 100644
--- a/Asm/Ansible/Process/ProcessBuilderInterface.php
+++ b/Asm/Ansible/Process/ProcessBuilderInterface.php
@@ -18,6 +18,12 @@ interface ProcessBuilderInterface
*/
public function setArguments(array $arguments): ProcessBuilderInterface;
+ /**
+ * @param string $path
+ * @return ProcessBuilderInterface
+ */
+ public function setWorkingDirectory(string $path): ProcessBuilderInterface;
+
/**
* @param int $timeout
* @return ProcessBuilderInterface
diff --git a/Asm/Ansible/Process/ProcessResult.php b/Asm/Ansible/Process/ProcessResult.php
new file mode 100644
index 0000000..b1d8227
--- /dev/null
+++ b/Asm/Ansible/Process/ProcessResult.php
@@ -0,0 +1,38 @@
+exitCode === 0;
+ }
+
+ public function getExitCode(): int
+ {
+ return $this->exitCode;
+ }
+
+ public function getOutput(): string
+ {
+ return $this->output;
+ }
+
+ public function getErrorOutput(): string
+ {
+ return $this->errorOutput;
+ }
+}
diff --git a/Asm/Ansible/Process/ProcessRunner.php b/Asm/Ansible/Process/ProcessRunner.php
new file mode 100644
index 0000000..0bd32ec
--- /dev/null
+++ b/Asm/Ansible/Process/ProcessRunner.php
@@ -0,0 +1,55 @@
+logger = $logger ?? new NullLogger();
+ }
+
+ public function run(array $command, ?string $workingDirectory = null, ?int $timeout = null): ProcessResult
+ {
+ $this->logger->info('Executing command: ' . implode(' ', $command));
+
+ $process = $this->processBuilder->setArguments($command);
+
+ if ($workingDirectory !== null) {
+ $process->setWorkingDirectory($workingDirectory);
+ }
+
+ if ($timeout !== null) {
+ $process->setTimeout($timeout);
+ }
+
+ $result = $process->getProcess()->run();
+
+ $processResult = new ProcessResult(
+ $result,
+ $process->getProcess()->getOutput(),
+ $process->getProcess()->getErrorOutput()
+ );
+
+ if (!$processResult->isSuccessful()) {
+ $this->logger->error('Command failed', [
+ 'command' => implode(' ', $command),
+ 'exit_code' => $processResult->getExitCode(),
+ 'output' => $processResult->getOutput(),
+ 'error' => $processResult->getErrorOutput()
+ ]);
+ }
+
+ return $processResult;
+ }
+}
diff --git a/Asm/Ansible/Validation/PlaybookValidator.php b/Asm/Ansible/Validation/PlaybookValidator.php
new file mode 100644
index 0000000..daba1df
--- /dev/null
+++ b/Asm/Ansible/Validation/PlaybookValidator.php
@@ -0,0 +1,43 @@
+processRunner->run([
+ 'ansible-playbook', '--syntax-check', $playbookPath
+ ]);
+
+ $isValid = $result->isSuccessful();
+ $errors = [];
+ $warnings = [];
+
+ if (!$isValid) {
+ $errors = $this->parseErrors($result->getOutput());
+ }
+
+ return new ValidationResult($isValid, $errors, $warnings);
+ }
+
+ private function parseErrors(array $output): array
+ {
+ $errors = [];
+ foreach ($output as $line) {
+ if (str_contains($line, 'ERROR!')) {
+ $errors[] = trim(str_replace('ERROR!', '', $line));
+ }
+ }
+ return $errors;
+ }
+}
diff --git a/Asm/Ansible/Validation/ValidationResult.php b/Asm/Ansible/Validation/ValidationResult.php
new file mode 100644
index 0000000..2660a90
--- /dev/null
+++ b/Asm/Ansible/Validation/ValidationResult.php
@@ -0,0 +1,30 @@
+isValid;
+ }
+
+ public function getErrors(): array
+ {
+ return $this->errors;
+ }
+
+ public function getWarnings(): array
+ {
+ return $this->warnings;
+ }
+}
diff --git a/Asm/Ansible/Vault/VaultManager.php b/Asm/Ansible/Vault/VaultManager.php
new file mode 100644
index 0000000..a30daf8
--- /dev/null
+++ b/Asm/Ansible/Vault/VaultManager.php
@@ -0,0 +1,69 @@
+processRunner->run($command);
+
+ if (!$result->isSuccessful()) {
+ throw new VaultException(
+ "Failed to encrypt file: $file",
+ $result->getExitCode(),
+ null,
+ implode(' ', $command),
+ $result->getOutput()
+ );
+ }
+
+ return $result;
+ }
+
+ public function decrypt(string $file, string $vaultPasswordFile): ProcessResult
+ {
+ $command = [
+ 'ansible-vault', 'decrypt', $file,
+ '--vault-password-file', $vaultPasswordFile
+ ];
+
+ $result = $this->processRunner->run($command);
+
+ if (!$result->isSuccessful()) {
+ throw new VaultException(
+ "Failed to decrypt file: $file",
+ $result->getExitCode(),
+ null,
+ implode(' ', $command),
+ $result->getOutput()
+ );
+ }
+
+ return $result;
+ }
+
+ public function view(string $file, string $vaultPasswordFile): ProcessResult
+ {
+ return $this->processRunner->run([
+ 'ansible-vault', 'view', $file,
+ '--vault-password-file', $vaultPasswordFile
+ ]);
+ }
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 905149b..6defae2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,27 @@
# Changelog
-## v4.0
+## Unreleased
+ * Updated API compatibility to modern Ansible (e.g., explicitly prepending 'role' to ansible-galaxy commands).
+ * Introduced `AnsibleGalaxyCollectionInterface` for full `ansible-galaxy collection` support (init, build, publish, install).
+ * Resolved `Symfony\Process` argument passing issues to ensure reliable escaping of options.
+ * Modernized `ansible-playbook` wrapper by dropping deprecated `--su` parameters and adding `--become-method`, `--become-password-file`, `--connection-password-file`, and `--list-tags`.
+ * Refactored codebase for PHP 8.4 compatibility (e.g., explicitly nullable `LoggerInterface` parameter).
+ * Fixed GitHub Actions workflow dependency resolution for PHP 8.2 and 8.3 by broadening `phpunit/phpunit` version constraints.
+ * Upgraded Docker development environment to simultaneously support PHP 8.2, 8.3, and 8.4 via `compose.yaml` profiles/services.
+ * Created robust DTOs (e.g., `ProcessResult`) for process output and exit code management.
+ * Resolved over 30 static analysis and code style errors (`phpstan`, `phpcs`).
+ * General library, static analysis tooling (PHPUnit 13), and test suite upgrades.
+
+## v5.0.0
+ * Wrapping each host into quotes instead of having double quotes around all hosts.
+ * Fixed error output parsing (removed extra escaping).
+ * Permitted JSON format strings for the `--extra-vars` parameter.
+
+## v4.1.0
+ * Allowed all psr/log versions to be used.
+ * Feature/build and version cleanup.
+
+## v4.0.0
* drop php7.x compat, 8.0+ only
* removed travis, scrutinizer, replace with github actions
* add symfony process v6.x, drop process <5.x
diff --git a/Dockerfile b/Dockerfile
index b6aff1b..5ce12e8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,23 +1,19 @@
-FROM php:8.3-cli
+ARG PHP_VERSION=8.4
+FROM php:${PHP_VERSION}-cli
WORKDIR /app
ENV ANSIBLE_VERSION 2.9.17
# composer
-RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
- && php -r "if (hash_file('sha384', 'composer-setup.php') === 'dac665fdc30fdd8ec78b38b9800061b4150413ff2e3b6f88543c636f7cd84f6db9189d43a81e5503cda447da73c7e5b6') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" \
- && php composer-setup.php \
- && php -r "unlink('composer-setup.php');" \
- && mv composer.phar /usr/bin/composer \
- && chmod +x /usr/bin/composer
+COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# python, pipx & ansible
RUN apt-get update \
&& apt-get install -y gcc python3 git zip 7zip unzip pipx \
&& apt-get clean all; \
pipx install --upgrade pip; \
- pipx install "ansible==${ANSIBLE_VERSION}"; \
+ pipx install ansible-core; \
pipx install ansible;
# keep container running
diff --git a/Makefile b/Makefile
index ad88589..e176032 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,9 @@
# Executables (local)
DOCKER_COMP = docker compose
+PHP_VERSION ?= 8.4
# Docker containers
-PHP_CONT = $(DOCKER_COMP) exec php-ansible
+PHP_CONT = $(DOCKER_COMP) exec php-ansible-$(PHP_VERSION)
# Executables
PHP = $(PHP_CONT) php
@@ -30,10 +31,10 @@ down: ## Stop the docker hub
logs: ## Show live logs
@$(DOCKER_COMP) logs --tail=0 --follow
-sh: ## Connect to the php-fpm container
+sh: ## Connect to the php container (use PHP_VERSION=8.2|8.3|8.4 to specify)
@$(PHP_CONT) sh
-bash: ## Connect to the php-fpm container via bash so up and down arrows go to previous commands
+bash: ## Connect to the php container via bash (use PHP_VERSION=8.2|8.3|8.4 to specify)
@$(PHP_CONT) bash
test: ## Start tests with phpunit, pass the parameter "c=" to add options to phpunit, example: make test c="--group e2e --stop-on-failure"
@@ -42,15 +43,15 @@ test: ## Start tests with phpunit, pass the parameter "c=" to add options to php
analyze: ## Start analysis with phpstan, pass the parameter "c=" to add options to phpstan. Default config ist always used, example: make analyze c="--group e2e"
@$(eval c ?=)
- @$(PHP_CONT) vendor/bin/phpstan --configuration=phpstan.neon $(c)
+ @$(PHP_CONT) vendor/bin/phpstan --configuration=phpstan.neon --memory-limit=256M $(c)
codestyle: ## Start codestyle analysis with phpcs, pass the parameter "c=" to add options to phpcs. Default config ist always used. Example: make codestyle c="--parallel=2"
@$(eval c ?=)
- @$(PHP_CONT) vendor/bin/phpcs --standard=phpcs.xml.dist $(c)
+ @$(PHP_CONT) vendor/bin/phpcs --standard=phpcs.xml $(c)
codestyle-fix: ## Start codestyle analysis with phpcbf, pass the parameter "c=" to add options to phpcbf. Default config ist always used. Example: make codestyle c="--parallel=2"
@$(eval c ?=)
- @$(PHP_CONT) vendor/bin/phpcbf --standard=phpcs.xml.dist $(c)
+ @$(PHP_CONT) vendor/bin/phpcbf --standard=phpcs.xml $(c)
psalm: ## Start code analysis with psalm, pass the parameter "c=" to add options to psalm. Default config ist always used. Example: make codestyle c="--level=2"
@$(eval c ?=)
@@ -63,4 +64,4 @@ composer: ## Run composer, pass the parameter "c=" to run a given command, examp
vendor: ## Install vendors according to the current composer.lock file
vendor: c=install --prefer-dist --no-dev --no-progress --no-scripts --no-interaction
-vendor: composer
\ No newline at end of file
+vendor: composer
diff --git a/README.md b/README.md
index 44d619d..84ed7b4 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
# php-ansible library
-
+
This library is a OOP-wrapper for the ansible provisioning tool.
I intend to use this library for a symfony2 bundle and also a deployment GUI, based on php/symfony2.
@@ -111,8 +111,10 @@ $ansible
### Galaxy
-The syntax follows ansible's syntax with one deviation: list is a reserved keyword in php (array context) and
-therefore I had to rename it to "modulelist()".
+The `ansible-galaxy` commands support both the `role` and `collection` functionality.
+
+#### Role Commands
+The syntax follows ansible's syntax with one deviation: `list` is a reserved keyword in php (array context) and therefore I had to rename it to "modulelist()".
```php
$ansible
@@ -124,10 +126,10 @@ $ansible
would generate:
```bash
-$ ansible-galaxy init my_role --init-path=/tmp/my_path
+$ ansible-galaxy role init my_role --init-path=/tmp/my_path
```
-You can access all galaxy commands:
+You can access all galaxy role commands:
* `init()`
* `info()`
@@ -136,6 +138,28 @@ You can access all galaxy commands:
* `modulelist()`
* `remove()`
+#### Collection Commands
+
+```php
+$ansible
+ ->galaxyCollection()
+ ->install('my_namespace.my_collection')
+ ->collectionsPath('/tmp/my_path')
+ ->execute();
+```
+would generate:
+
+```bash
+$ ansible-galaxy collection install my_namespace.my_collection --collections-path=/tmp/my_path
+```
+
+You can access all galaxy collection commands:
+
+ * `init()`
+ * `build()`
+ * `publish()`
+ * `install()`
+
You can combine the calls with their possible arguments, though I don't have any logic preventing e.g. ```--force``` from being applied to e.g. info().
Possible arguments/options:
@@ -164,10 +188,11 @@ $ansible
## Development
-You can use the provided docker image with ```make build``` which uses a default php-cli docker image and ansible 2.x. See the ```Dockerfile``` for more info.
-Start the container with ```make up```.
-Composer install: ```make vendor```
-You can run code or the tests within the container: ```make test c="--testdox"```
+You can use the provided docker setup with ```make build``` which creates PHP 8.2, 8.3, and 8.4 containers with modern ansible versions. See the ```Dockerfile``` and ```compose.yaml``` for more info.
+Start the containers with ```make up```.
+Composer install: ```make vendor``` (defaults to PHP 8.4, use `make vendor PHP_VERSION=8.2` etc.)
+You can run code or the tests within the container: ```make test```
+To run tools on a specific PHP version, specify `PHP_VERSION`: ```make test PHP_VERSION=8.3```
## Thank you for your contributions!
@@ -181,8 +206,6 @@ thank you for reviewing, bug reporting, suggestions and PRs :-)
The Next steps for implementation are:
- improve type handling and structure, due to overall complexity of the playbook at the moment
-- scalar typehints all over the place
-- provide docker support for development
- wrapping the library into a bundle -> maybe
- provide commandline-capabilities -> maybe
diff --git a/Tests/Asm/Ansible/Command/AnsibleGalaxyCollectionTest.php b/Tests/Asm/Ansible/Command/AnsibleGalaxyCollectionTest.php
new file mode 100644
index 0000000..920c4b8
--- /dev/null
+++ b/Tests/Asm/Ansible/Command/AnsibleGalaxyCollectionTest.php
@@ -0,0 +1,137 @@
+getGalaxyUri(), $this->getProjectUri());
+ $ansibleGalaxyCollection = new AnsibleGalaxyCollection($process);
+
+ $this->assertInstanceOf(AnsibleGalaxyCollection::class, $ansibleGalaxyCollection);
+
+ return $ansibleGalaxyCollection;
+ }
+
+ public function testExecute(): void
+ {
+ // Skipped on Windows
+ if (Env::isWindows()) {
+ $this->assertTrue(true);
+ return;
+ }
+
+ $command = $this->testCreateInstance();
+ $command->execute();
+
+ // if command executes without exception
+ $this->assertTrue(true);
+ }
+
+ public function testInit(): void
+ {
+ $command = $this->testCreateInstance();
+
+ $command
+ ->init('test_collection')
+ ->initPath('/tmp/php-ansible')
+ ->force();
+
+ $arguments = array_flip($command->getCommandlineArguments());
+
+ $this->assertArrayHasKey('collection', $arguments);
+ $this->assertArrayHasKey('init', $arguments);
+ $this->assertArrayHasKey('test_collection', $arguments);
+ $this->assertArrayHasKey('--init-path=/tmp/php-ansible', $arguments);
+ $this->assertArrayHasKey('--force', $arguments);
+ }
+
+ public function testBuild(): void
+ {
+ $command = $this->testCreateInstance();
+ $command->build();
+ $arguments = array_flip($command->getCommandlineArguments());
+
+ $this->assertArrayHasKey('collection', $arguments);
+ $this->assertArrayHasKey('build', $arguments);
+ }
+
+ public function testBuildWithPath(): void
+ {
+ $command = $this->testCreateInstance();
+ $command->build('/path/to/collection');
+ $arguments = array_flip($command->getCommandlineArguments());
+
+ $this->assertArrayHasKey('collection', $arguments);
+ $this->assertArrayHasKey('build', $arguments);
+ $this->assertArrayHasKey('/path/to/collection', $arguments);
+ }
+
+ public function testPublish(): void
+ {
+ $command = $this->testCreateInstance();
+ $command->publish('namespace-collection-1.0.0.tar.gz');
+ $arguments = array_flip($command->getCommandlineArguments());
+
+ $this->assertArrayHasKey('collection', $arguments);
+ $this->assertArrayHasKey('publish', $arguments);
+ $this->assertArrayHasKey('namespace-collection-1.0.0.tar.gz', $arguments);
+ }
+
+ public function testInstall(): void
+ {
+ $command = $this->testCreateInstance();
+ $command->install('test_collection');
+ $arguments = array_flip($command->getCommandlineArguments());
+
+ $this->assertArrayHasKey('collection', $arguments);
+ $this->assertArrayHasKey('install', $arguments);
+ $this->assertArrayHasKey('test_collection', $arguments);
+ }
+
+ public function testInstallWithCollections(): void
+ {
+ $command = $this->testCreateInstance();
+ $command->install(
+ [
+ 'test_collection',
+ 'another_collection',
+ 'yet_another_collection'
+ ]
+ );
+ $arguments = array_flip($command->getCommandlineArguments());
+
+ $this->assertArrayHasKey('collection', $arguments);
+ $this->assertArrayHasKey('install', $arguments);
+ $this->assertArrayHasKey('test_collection', $arguments);
+ $this->assertArrayHasKey('another_collection', $arguments);
+ $this->assertArrayHasKey('yet_another_collection', $arguments);
+ }
+
+ public function testInstallWithOptions(): void
+ {
+ $command = $this->testCreateInstance();
+
+ $command
+ ->install()
+ ->collectionsPath('/tmp/collections')
+ ->force();
+
+ $arguments = array_flip($command->getCommandlineArguments());
+
+ $this->assertArrayHasKey('collection', $arguments);
+ $this->assertArrayHasKey('install', $arguments);
+ $this->assertArrayHasKey('--collections-path=/tmp/collections', $arguments);
+ $this->assertArrayHasKey('--force', $arguments);
+ }
+}
diff --git a/Tests/Asm/Ansible/Command/AnsibleGalaxyTest.php b/Tests/Asm/Ansible/Command/AnsibleGalaxyTest.php
index 98d1821..1258452 100644
--- a/Tests/Asm/Ansible/Command/AnsibleGalaxyTest.php
+++ b/Tests/Asm/Ansible/Command/AnsibleGalaxyTest.php
@@ -51,7 +51,9 @@ public function testInit(): void
$arguments = array_flip($command->getCommandlineArguments());
- $this->assertArrayHasKey('init test_role', $arguments);
+ $this->assertArrayHasKey('role', $arguments);
+ $this->assertArrayHasKey('init', $arguments);
+ $this->assertArrayHasKey('test_role', $arguments);
$this->assertArrayHasKey('--init-path=/tmp/php-ansible', $arguments);
$this->assertArrayHasKey('--force', $arguments);
$this->assertArrayHasKey('--offline', $arguments);
@@ -64,7 +66,9 @@ public function testInfo(): void
$command->info('test_role');
$arguments = array_flip($command->getCommandlineArguments());
- $this->assertArrayHasKey('info test_role', $arguments);
+ $this->assertArrayHasKey('role', $arguments);
+ $this->assertArrayHasKey('info', $arguments);
+ $this->assertArrayHasKey('test_role', $arguments);
}
public function testInfoWithVersion(): void
@@ -73,7 +77,9 @@ public function testInfoWithVersion(): void
$command->info('test_role', '1.0');
$arguments = array_flip($command->getCommandlineArguments());
- $this->assertArrayHasKey('info test_role,1.0', $arguments);
+ $this->assertArrayHasKey('role', $arguments);
+ $this->assertArrayHasKey('info', $arguments);
+ $this->assertArrayHasKey('test_role,1.0', $arguments);
}
public function testInstall(): void
@@ -82,7 +88,9 @@ public function testInstall(): void
$command->install('test_role');
$arguments = array_flip($command->getCommandlineArguments());
- $this->assertArrayHasKey('install test_role', $arguments);
+ $this->assertArrayHasKey('role', $arguments);
+ $this->assertArrayHasKey('install', $arguments);
+ $this->assertArrayHasKey('test_role', $arguments);
}
public function testInstallWithRoles(): void
@@ -97,7 +105,11 @@ public function testInstallWithRoles(): void
);
$arguments = array_flip($command->getCommandlineArguments());
- $this->assertArrayHasKey('install test_role another_role yet_another_role', $arguments);
+ $this->assertArrayHasKey('role', $arguments);
+ $this->assertArrayHasKey('install', $arguments);
+ $this->assertArrayHasKey('test_role', $arguments);
+ $this->assertArrayHasKey('another_role', $arguments);
+ $this->assertArrayHasKey('yet_another_role', $arguments);
}
public function testInstallWithOptions(): void
@@ -116,6 +128,7 @@ public function testInstallWithOptions(): void
$arguments = array_flip($command->getCommandlineArguments());
+ $this->assertArrayHasKey('role', $arguments);
$this->assertArrayHasKey('install', $arguments);
$this->assertArrayHasKey('--role-file=test_role', $arguments);
$this->assertArrayHasKey('--roles-path=/tmp/roles', $arguments);
@@ -130,7 +143,7 @@ public function testList(): void
$command = $this->testCreateInstance();
$command->modulelist();
- $this->assertEquals('list', $command->getCommandlineArguments(false));
+ $this->assertEquals('role list', $command->getCommandlineArguments(false));
}
public function testListWithRole(): void
@@ -138,7 +151,7 @@ public function testListWithRole(): void
$command = $this->testCreateInstance();
$command->modulelist('test_role');
- $this->assertEquals('list test_role', $command->getCommandlineArguments(false));
+ $this->assertEquals('role list test_role', $command->getCommandlineArguments(false));
}
public function testListWithHelp(): void
@@ -148,7 +161,7 @@ public function testListWithHelp(): void
->modulelist()
->help();
- $this->assertEquals('list --help', $command->getCommandlineArguments(false));
+ $this->assertEquals('role list --help', $command->getCommandlineArguments(false));
}
public function testRemove(): void
@@ -156,7 +169,7 @@ public function testRemove(): void
$command = $this->testCreateInstance();
$command->remove('test_role');
- $this->assertEquals('remove test_role', $command->getCommandlineArguments(false));
+ $this->assertEquals('role remove test_role', $command->getCommandlineArguments(false));
}
public function testRemoveRoles(): void
@@ -169,6 +182,6 @@ public function testRemoveRoles(): void
]
);
- $this->assertEquals('remove test_role another_role', $command->getCommandlineArguments(false));
+ $this->assertEquals('role remove test_role another_role', $command->getCommandlineArguments(false));
}
}
diff --git a/Tests/Asm/Ansible/Command/AnsiblePlaybookTest.php b/Tests/Asm/Ansible/Command/AnsiblePlaybookTest.php
index 32cc499..b9b33f8 100644
--- a/Tests/Asm/Ansible/Command/AnsiblePlaybookTest.php
+++ b/Tests/Asm/Ansible/Command/AnsiblePlaybookTest.php
@@ -61,18 +61,6 @@ public function testAskPassArgumentPresent()
$this->assertArrayHasKey('--ask-pass', $arguments);
}
- public function testAskSuPassArgumentPresent()
- {
- $command = $this->testCreateInstance();
- $command
- ->play($this->getPlayUri())
- ->askSuPass();
-
- $arguments = array_flip($command->getCommandlineArguments());
- $this->assertArrayHasKey('--ask-su-pass', $arguments);
- }
-
-
public function testAskBecomePassArgumentPresent()
{
$command = $this->testCreateInstance();
@@ -217,6 +205,18 @@ public function testListTasksArgumentPresent()
}
+ public function testListTagsArgumentPresent()
+ {
+ $command = $this->testCreateInstance();
+ $command
+ ->play($this->getPlayUri())
+ ->listTags();
+
+ $arguments = array_flip($command->getCommandlineArguments());
+ $this->assertArrayHasKey('--list-tags', $arguments);
+ }
+
+
public function testModulePathArgumentPresent()
{
$command = $this->testCreateInstance();
@@ -295,63 +295,75 @@ public function testStepArgumentPresent()
}
- public function testSuArgumentPresent()
+ public function testBecomeArgumentPresent()
{
$command = $this->testCreateInstance();
$command
->play($this->getPlayUri())
- ->su();
+ ->become();
$arguments = array_flip($command->getCommandlineArguments());
- $this->assertArrayHasKey('--su', $arguments);
+ $this->assertArrayHasKey('--become', $arguments);
}
- public function testSuUserArgumentPresent()
+ public function testBecomeUserArgumentPresent()
{
$command = $this->testCreateInstance();
$command
->play($this->getPlayUri())
- ->suUser();
+ ->becomeUser();
$arguments = array_flip($command->getCommandlineArguments());
- $this->assertArrayHasKey('--su-user=root', $arguments);
+ $this->assertArrayHasKey('--become-user=root', $arguments);
$command
- ->suUser('maschmann');
+ ->becomeUser('maschmann');
$arguments = array_flip($command->getCommandlineArguments());
- $this->assertArrayHasKey('--su-user=maschmann', $arguments);
+ $this->assertArrayHasKey('--become-user=maschmann', $arguments);
}
- public function testBecomeArgumentPresent()
+ public function testBecomeMethodArgumentPresent()
{
$command = $this->testCreateInstance();
$command
->play($this->getPlayUri())
- ->become();
+ ->becomeMethod();
$arguments = array_flip($command->getCommandlineArguments());
- $this->assertArrayHasKey('--become', $arguments);
+ $this->assertArrayHasKey('--become-method=sudo', $arguments);
+
+ $command
+ ->becomeMethod('su');
+
+ $arguments = array_flip($command->getCommandlineArguments());
+ $this->assertArrayHasKey('--become-method=su', $arguments);
}
- public function testBecomeUserArgumentPresent()
+ public function testBecomePasswordFileArgumentPresent()
{
$command = $this->testCreateInstance();
$command
->play($this->getPlayUri())
- ->becomeUser();
+ ->becomePasswordFile('/path/to/become_pass');
$arguments = array_flip($command->getCommandlineArguments());
- $this->assertArrayHasKey('--become-user=root', $arguments);
+ $this->assertArrayHasKey('--become-password-file=/path/to/become_pass', $arguments);
+ }
+
+ public function testConnectionPasswordFileArgumentPresent()
+ {
+ $command = $this->testCreateInstance();
$command
- ->becomeUser('maschmann');
+ ->play($this->getPlayUri())
+ ->connectionPasswordFile('/path/to/conn_pass');
$arguments = array_flip($command->getCommandlineArguments());
- $this->assertArrayHasKey('--become-user=maschmann', $arguments);
+ $this->assertArrayHasKey('--connection-password-file=/path/to/conn_pass', $arguments);
}
@@ -475,30 +487,6 @@ public function testFlushCacheParameterPresent()
}
- public function testNewVaultIdArgumentPresent()
- {
- $command = $this->testCreateInstance();
- $command
- ->play($this->getPlayUri())
- ->newVaultId('someId');
-
- $arguments = array_flip($command->getCommandlineArguments());
- $this->assertArrayHasKey('--new-vault-id=someId', $arguments);
- }
-
-
- public function testNewVaultPasswordFileArgumentPresent()
- {
- $command = $this->testCreateInstance();
- $command
- ->play($this->getPlayUri())
- ->newVaultPasswordFile('/path/to/vault');
-
- $arguments = array_flip($command->getCommandlineArguments());
- $this->assertArrayHasKey('--new-vault-password-file=/path/to/vault', $arguments);
- }
-
-
public function testScpExtraArgsArgumentPresent()
{
$command = $this->testCreateInstance();
diff --git a/compose.yaml b/compose.yaml
index 2f0ea52..c4cbe7f 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -1,6 +1,27 @@
services:
- php-ansible:
- build: .
- container_name: php-ansible
+ php-ansible-8.4:
+ build:
+ context: .
+ args:
+ PHP_VERSION: "8.4"
+ container_name: php-ansible-8.4
volumes:
- - ./:/app
\ No newline at end of file
+ - ./:/app
+
+ php-ansible-8.3:
+ build:
+ context: .
+ args:
+ PHP_VERSION: "8.3"
+ container_name: php-ansible-8.3
+ volumes:
+ - ./:/app
+
+ php-ansible-8.2:
+ build:
+ context: .
+ args:
+ PHP_VERSION: "8.2"
+ container_name: php-ansible-8.2
+ volumes:
+ - ./:/app
diff --git a/composer.json b/composer.json
index af82267..ac127d0 100644
--- a/composer.json
+++ b/composer.json
@@ -15,14 +15,16 @@
}
],
"require": {
- "php": "^8.1.0|^8.2.0|^8.3.0",
- "psr/log": "^2.0|^3.0",
- "symfony/process": "^5.3|^6.0|^7.0"
+ "php": "^8.2.0|^8.3.0|^8.4.0",
+ "psr/log": "^3.0",
+ "symfony/process": "^6.0|^7.0",
+ "symfony/yaml": "^6.0|^7.0"
},
"require-dev": {
- "phpunit/phpunit": "^10.0|^11.0",
+ "phpunit/phpunit": "^11.0 || ^12.0 || ^13.0",
"mikey179/vfsstream": "^1.6",
- "phpstan/phpstan": "^1.12"
+ "phpstan/phpstan": "^2.1",
+ "squizlabs/php_codesniffer": "^4.0"
},
"autoload": {
"psr-4": {
diff --git a/phpunit-10.xml.dist b/phpunit-11.xml.dist
similarity index 86%
rename from phpunit-10.xml.dist
rename to phpunit-11.xml.dist
index 189c807..bc9cb59 100644
--- a/phpunit-10.xml.dist
+++ b/phpunit-11.xml.dist
@@ -1,6 +1,11 @@
-
-
+
+
+
+ ./Tests
+
+
+
.
@@ -8,10 +13,5 @@
./Tests
./vendor
-
-
-
- ./Tests
-
-
+
diff --git a/phpunit-12.xml.dist b/phpunit-12.xml.dist
new file mode 100644
index 0000000..455f80d
--- /dev/null
+++ b/phpunit-12.xml.dist
@@ -0,0 +1,17 @@
+
+
+
+
+ ./Tests
+
+
+
+
+ .
+
+
+ ./Tests
+ ./vendor
+
+
+
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index bc9cb59..79fcadc 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,5 +1,5 @@
-
+
./Tests