Repository: deployphp/deployer Branch: master Commit: 6e15e217ae5f Files: 338 Total size: 1014.5 KB Directory structure: gitextract_pd_hfplz/ ├── .gitattributes ├── .github/ │ ├── DISCUSSION_TEMPLATE/ │ │ └── bugs.yml │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ └── config.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── labeler.yml │ └── workflows/ │ ├── check.yml │ ├── docker.yml │ ├── docs-sync.yml │ ├── docs.yml │ ├── labeler.yml │ ├── lint.yml │ ├── release.yml │ ├── stale.yml │ └── test.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── bin/ │ ├── build │ ├── dep │ └── docgen ├── composer.json ├── contrib/ │ ├── bugsnag.php │ ├── cachetool.php │ ├── chatwork.php │ ├── cimonitor.php │ ├── cloudflare.php │ ├── cpanel.php │ ├── crontab.php │ ├── directadmin.php │ ├── discord.php │ ├── grafana.php │ ├── hangouts.php │ ├── hipchat.php │ ├── ispmanager.php │ ├── mattermost.php │ ├── ms-teams.php │ ├── newrelic.php │ ├── npm.php │ ├── ntfy.php │ ├── phinx.php │ ├── php-fpm.php │ ├── rabbit.php │ ├── raygun.php │ ├── rocketchat.php │ ├── rollbar.php │ ├── rsync.php │ ├── sentry.php │ ├── slack.php │ ├── supervisord-monitor.php │ ├── telegram.php │ ├── webpack_encore.php │ ├── workplace.php │ ├── yammer.php │ └── yarn.php ├── docs/ │ ├── KNOWN_BUGS.md │ ├── UPGRADE.md │ ├── api.md │ ├── avoid-php-fpm-reloading.md │ ├── basics.md │ ├── ci-cd.md │ ├── cli.md │ ├── contrib/ │ │ ├── README.md │ │ ├── bugsnag.md │ │ ├── cachetool.md │ │ ├── chatwork.md │ │ ├── cimonitor.md │ │ ├── cloudflare.md │ │ ├── cpanel.md │ │ ├── crontab.md │ │ ├── directadmin.md │ │ ├── discord.md │ │ ├── grafana.md │ │ ├── hangouts.md │ │ ├── hipchat.md │ │ ├── ispmanager.md │ │ ├── mattermost.md │ │ ├── ms-teams.md │ │ ├── newrelic.md │ │ ├── npm.md │ │ ├── ntfy.md │ │ ├── phinx.md │ │ ├── php-fpm.md │ │ ├── rabbit.md │ │ ├── raygun.md │ │ ├── rocketchat.md │ │ ├── rollbar.md │ │ ├── rsync.md │ │ ├── sentry.md │ │ ├── slack.md │ │ ├── supervisord-monitor.md │ │ ├── telegram.md │ │ ├── webpack_encore.md │ │ ├── workplace.md │ │ ├── yammer.md │ │ └── yarn.md │ ├── getting-started.md │ ├── hosts.md │ ├── installation.md │ ├── recipe/ │ │ ├── README.md │ │ ├── cakephp.md │ │ ├── codeigniter.md │ │ ├── codeigniter4.md │ │ ├── common.md │ │ ├── composer.md │ │ ├── contao.md │ │ ├── craftcms.md │ │ ├── deploy/ │ │ │ ├── check_remote.md │ │ │ ├── cleanup.md │ │ │ ├── clear_paths.md │ │ │ ├── copy_dirs.md │ │ │ ├── env.md │ │ │ ├── info.md │ │ │ ├── lock.md │ │ │ ├── push.md │ │ │ ├── release.md │ │ │ ├── rollback.md │ │ │ ├── setup.md │ │ │ ├── shared.md │ │ │ ├── symlink.md │ │ │ ├── update_code.md │ │ │ ├── vendors.md │ │ │ └── writable.md │ │ ├── drupal7.md │ │ ├── drupal8.md │ │ ├── flow_framework.md │ │ ├── fuelphp.md │ │ ├── joomla.md │ │ ├── laravel.md │ │ ├── magento.md │ │ ├── magento2.md │ │ ├── pimcore.md │ │ ├── prestashop.md │ │ ├── provision/ │ │ │ ├── databases.md │ │ │ ├── nodejs.md │ │ │ ├── php.md │ │ │ ├── user.md │ │ │ └── website.md │ │ ├── provision.md │ │ ├── shopware.md │ │ ├── silverstripe.md │ │ ├── spiral.md │ │ ├── statamic.md │ │ ├── sulu.md │ │ ├── symfony.md │ │ ├── typo3.md │ │ ├── wordpress.md │ │ ├── yii.md │ │ └── zend_framework.md │ ├── selector.md │ ├── sidebar.js │ ├── tasks.md │ └── yaml.md ├── phpstan.neon ├── phpunit.xml ├── recipe/ │ ├── cakephp.php │ ├── codeigniter.php │ ├── codeigniter4.php │ ├── common.php │ ├── composer.php │ ├── contao.php │ ├── craftcms.php │ ├── deploy/ │ │ ├── check_remote.php │ │ ├── cleanup.php │ │ ├── clear_paths.php │ │ ├── copy_dirs.php │ │ ├── env.php │ │ ├── info.php │ │ ├── lock.php │ │ ├── push.php │ │ ├── release.php │ │ ├── rollback.php │ │ ├── setup.php │ │ ├── shared.php │ │ ├── symlink.php │ │ ├── update_code.php │ │ ├── vendors.php │ │ └── writable.php │ ├── drupal7.php │ ├── drupal8.php │ ├── flow_framework.php │ ├── fuelphp.php │ ├── joomla.php │ ├── laravel.php │ ├── magento.php │ ├── magento2.php │ ├── pimcore.php │ ├── prestashop.php │ ├── provision/ │ │ ├── 404.html │ │ ├── Caddyfile │ │ ├── databases.php │ │ ├── nodejs.php │ │ ├── php.php │ │ ├── user.php │ │ └── website.php │ ├── provision.php │ ├── shopware.php │ ├── silverstripe.php │ ├── spiral.php │ ├── statamic.php │ ├── sulu.php │ ├── symfony.php │ ├── typo3.php │ ├── wordpress.php │ ├── yii.php │ └── zend_framework.php ├── src/ │ ├── Collection/ │ │ └── Collection.php │ ├── Command/ │ │ ├── BlackjackCommand.php │ │ ├── CommandCommon.php │ │ ├── ConfigCommand.php │ │ ├── CustomOption.php │ │ ├── InitCommand.php │ │ ├── MainCommand.php │ │ ├── RunCommand.php │ │ ├── SelectCommand.php │ │ ├── SshCommand.php │ │ ├── TreeCommand.php │ │ └── WorkerCommand.php │ ├── Component/ │ │ ├── PharUpdate/ │ │ │ ├── Console/ │ │ │ │ ├── Command.php │ │ │ │ └── Helper.php │ │ │ ├── Exception/ │ │ │ │ ├── Exception.php │ │ │ │ ├── ExceptionInterface.php │ │ │ │ ├── FileException.php │ │ │ │ ├── InvalidArgumentException.php │ │ │ │ └── LogicException.php │ │ │ ├── Manager.php │ │ │ ├── Manifest.php │ │ │ ├── Update.php │ │ │ └── Version/ │ │ │ ├── Builder.php │ │ │ ├── Comparator.php │ │ │ ├── Dumper.php │ │ │ ├── Exception/ │ │ │ │ ├── InvalidIdentifierException.php │ │ │ │ ├── InvalidNumberException.php │ │ │ │ ├── InvalidStringRepresentationException.php │ │ │ │ └── VersionException.php │ │ │ ├── Parser.php │ │ │ ├── Validator.php │ │ │ └── Version.php │ │ └── Pimple/ │ │ ├── Container.php │ │ └── Exception/ │ │ ├── ExpectedInvokableException.php │ │ ├── FrozenServiceException.php │ │ ├── InvalidServiceIdentifierException.php │ │ └── UnknownIdentifierException.php │ ├── Configuration.php │ ├── Deployer.php │ ├── Documentation/ │ │ ├── ApiGen.php │ │ ├── DocConfig.php │ │ ├── DocGen.php │ │ ├── DocRecipe.php │ │ └── DocTask.php │ ├── Exception/ │ │ ├── ConfigurationException.php │ │ ├── Exception.php │ │ ├── GracefulShutdownException.php │ │ ├── HttpieException.php │ │ ├── RunException.php │ │ ├── TimeoutException.php │ │ └── WillAskUser.php │ ├── Executor/ │ │ ├── Master.php │ │ ├── Messenger.php │ │ ├── Planner.php │ │ ├── Response.php │ │ ├── Server.php │ │ └── Worker.php │ ├── Host/ │ │ ├── Host.php │ │ ├── HostCollection.php │ │ ├── Localhost.php │ │ └── Range.php │ ├── Importer/ │ │ └── Importer.php │ ├── Logger/ │ │ ├── Handler/ │ │ │ ├── FileHandler.php │ │ │ ├── HandlerInterface.php │ │ │ └── NullHandler.php │ │ └── Logger.php │ ├── ProcessRunner/ │ │ ├── Printer.php │ │ └── ProcessRunner.php │ ├── Selector/ │ │ └── Selector.php │ ├── Ssh/ │ │ ├── IOArguments.php │ │ ├── RunParams.php │ │ └── SshClient.php │ ├── Support/ │ │ ├── ObjectProxy.php │ │ ├── Reporter.php │ │ └── helpers.php │ ├── Task/ │ │ ├── Context.php │ │ ├── GroupTask.php │ │ ├── ScriptManager.php │ │ ├── Task.php │ │ └── TaskCollection.php │ ├── Utility/ │ │ ├── Httpie.php │ │ └── Rsync.php │ ├── functions.php │ └── schema.json └── tests/ ├── bootstrap.php ├── fixtures/ │ ├── project/ │ │ └── uploaded.html │ └── repository/ │ ├── README.md │ ├── composer.json │ └── uploads/ │ └── poem.txt ├── joy/ │ ├── HostDefaultConfigTest.php │ ├── JoyTest.php │ └── OnFuncTest.php ├── legacy/ │ ├── AbstractTest.php │ ├── CurrentPathTest.php │ ├── DeployTest.php │ ├── EnvTest.php │ ├── NamedArgumentsTest.php │ ├── OncePerNodeTest.php │ ├── OnceTest.php │ ├── ParallelTest.php │ ├── SelectTest.php │ ├── UpdateCodeTest.php │ ├── YamlTest.php │ └── recipe/ │ ├── deploy.php │ ├── deploy.yaml │ ├── env.php │ ├── once.php │ ├── once_per_node.php │ ├── parallel.php │ ├── select.php │ └── update_code.php ├── phpstan-baseline.neon └── src/ ├── Collection/ │ └── CollectionTest.php ├── Command/ │ └── BlackjackCommandTest.php ├── Component/ │ └── Pimple/ │ └── PimpleTest.php ├── Configuration/ │ └── ConfigurationTest.php ├── DeployerTest.php ├── FunctionsTest.php ├── Host/ │ ├── ConfigurationTest.php │ ├── HostTest.php │ └── RangeTest.php ├── Importer/ │ └── ImporterTest.php ├── Selector/ │ └── SelectorTest.php ├── Ssh/ │ └── IOArgumentsTest.php ├── Support/ │ ├── HelpersTest.php │ └── ObjectProxyTest.php └── Task/ ├── ContextTest.php ├── ScriptManagerTest.php └── TaskTest.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ /.gitattributes export-ignore /.github/ export-ignore /.gitignore export-ignore /docs/ export-ignore /phpcs.xml export-ignore /phpstan.neon export-ignore /phpunit.xml export-ignore /tests/ export-ignore ================================================ FILE: .github/DISCUSSION_TEMPLATE/bugs.yml ================================================ body: - type: markdown attributes: value: | **Before opening a bug report, please search the existing discussions.** - type: input id: deployer-version attributes: label: Deployer Version description: Which version of Deployer are you using? Please provide the full version. placeholder: v8.0.0 validations: required: true - type: input id: target-os attributes: label: Target OS description: Which operating system are you using? Please provide the full version. placeholder: Ubuntu 24.04 validations: required: true - type: dropdown id: php-version attributes: label: Which PHP version are you using? options: - PHP 8.5 - PHP 8.4 - PHP 8.3 - PHP 8.2 - PHP 8.1 - PHP 8.0 - PHP 7.4 - PHP 7.3 - PHP 7.2 - PHP 7.1 - PHP 7.0 - PHP 5.6 - PHP 5.5 - PHP 5.4 - PHP 5.3 validations: required: true - type: textarea id: deploy-src attributes: label: Content of deploy.php or deploy.yaml description: Please, provide a minimal reproducible example of deploy.php or deploy.yaml file. validations: required: false - type: textarea attributes: label: Steps to reproduce description: Please provide the steps to reproduce the bug. validations: required: true ================================================ FILE: .github/FUNDING.yml ================================================ github: antonmedv ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Bug Report url: https://github.com/deployphp/deployer/discussions/new?category=bugs about: Submit a bug or an issue - name: Feature request url: https://github.com/deployphp/deployer/discussions/new?category=features about: For ideas or feature requests - name: Support questions & other url: https://github.com/deployphp/deployer/discussions/new?category=help-needed about: If you have a question or need help using the library - name: General discussion url: https://github.com/deployphp/deployer/discussions/new?category=general about: Start a new discussion ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ - [ ] Bug fix #…? - [ ] New feature? - [ ] BC breaks? - [ ] Tests added? - [ ] Docs added? Please, regenerate docs by running next command: $ php bin/docgen ================================================ FILE: .github/labeler.yml ================================================ v7: - base-branch: "7.x" ================================================ FILE: .github/workflows/check.yml ================================================ name: check on: push: branches: [ master ] pull_request: branches: [ master ] jobs: phpstan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '8.2' - name: Validate composer.json and composer.lock run: composer validate - name: Get Composer Cache Directory id: composer-cache run: | echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: | ${{ runner.os }}-composer- - name: Install dependencies if: steps.composer-cache.outputs.cache-hit != 'true' run: composer install --prefer-dist --no-progress - name: Run test suite run: composer phpstan code-style: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '8.2' - name: Validate composer.json and composer.lock run: composer validate - name: Get Composer Cache Directory id: composer-cache run: | echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: | ${{ runner.os }}-composer- - name: Install dependencies if: steps.composer-cache.outputs.cache-hit != 'true' run: composer install --prefer-dist --no-progress - name: Run php-cs-fixer run: composer check ================================================ FILE: .github/workflows/docker.yml ================================================ name: docker on: release: types: [ published ] workflow_dispatch: inputs: version: description: 'Version' required: true permissions: id-token: write attestations: write jobs: build-and-push: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '8.2' - name: Get version run: | echo "RELEASE_VERSION=${GITHUB_REF#refs/*/v}" >> $GITHUB_ENV if [ -n "$VERSION" ]; then echo "RELEASE_VERSION=${{ inputs.version }}" >> $GITHUB_ENV fi env: VERSION: ${{ inputs.version }} - name: Build phar run: php -d phar.readonly=0 bin/build -v"$RELEASE_VERSION" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: deployphp/deployer tags: | type=raw,value=latest,enable={{is_default_branch}} type=sha,format=long type=sha type=semver,pattern=v{{major}}.{{minor}}.{{patch}} type=semver,pattern=v{{major}}.{{minor}} type=semver,pattern=v{{major}} type=ref,event=tag - name: Login to Docker Hub uses: docker/login-action@v3 with: username: deployphp password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm64 push: true provenance: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} ================================================ FILE: .github/workflows/docs-sync.yml ================================================ name: doc-sync on: push: branches: [ master ] permissions: contents: write jobs: docgen-and-commit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '8.2' - name: Get Composer Cache Directory id: composer-cache run: | echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: | ${{ runner.os }}-composer- - name: Install dependencies if: steps.composer-cache.outputs.cache-hit != 'true' run: composer install --prefer-dist --no-progress - name: Run docgen run: php bin/docgen - name: Add & Commit uses: EndBug/add-and-commit@v9 with: default_author: github_actions add: 'docs' message: '[automatic] Update docs with bin/docgen' ================================================ FILE: .github/workflows/docs.yml ================================================ name: doc on: push: branches: [ master ] pull_request: branches: [ master ] jobs: docgen: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '8.2' - name: Get Composer Cache Directory id: composer-cache run: | echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: | ${{ runner.os }}-composer- - name: Install dependencies if: steps.composer-cache.outputs.cache-hit != 'true' run: composer install --prefer-dist --no-progress - name: Run docgen run: php bin/docgen - name: Check for uncommitted changes run: | status=$(git status --porcelain docs/); [ -z "$status" ] || { echo "Please, run bin/docgen and commit next files:"; echo $status; exit 1; } ================================================ FILE: .github/workflows/labeler.yml ================================================ name: labeler on: - pull_request_target jobs: labeler: permissions: contents: read pull-requests: write runs-on: ubuntu-latest steps: - uses: actions/labeler@v6 ================================================ FILE: .github/workflows/lint.yml ================================================ name: lint on: push: branches: [ master ] pull_request: types: [opened, synchronize, reopened, ready_for_review] jobs: lint: runs-on: ubuntu-latest strategy: matrix: php-versions: [ '8.2' ] steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} tools: cs2pr, parallel-lint - name: Lint sources run: composer exec --no-interaction -- parallel-lint bin/ contrib/ recipe/ src/ tests/ --checkstyle | cs2pr ================================================ FILE: .github/workflows/release.yml ================================================ name: release on: release: types: [published] workflow_dispatch: inputs: version: description: 'Version' required: true permissions: contents: write jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Get version run: | echo "RELEASE_VERSION=${GITHUB_REF#refs/*/v}" >> $GITHUB_ENV if [ -n "$VERSION" ]; then echo "RELEASE_VERSION=${{ inputs.version }}" >> $GITHUB_ENV fi env: VERSION: ${{ inputs.version }} - name: Build phar run: php -d phar.readonly=0 bin/build -v"$RELEASE_VERSION" - name: Sign phar run: | mkdir -p ~/.gnupg/ chmod 0700 ~/.gnupg/ echo "$GPG_SIGNING_KEY" > ~/.gnupg/private.key gpg --import --no-tty --batch --yes ~/.gnupg/private.key gpg -u anton@deployer.org --batch --pinentry-mode loopback --passphrase "${GPG_PASSPHRASE}" --detach-sign --output deployer.phar.asc deployer.phar env: GPG_SIGNING_KEY: | ${{ secrets.GPG_SIGNING_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - name: Upload phar run: gh release upload v"${RELEASE_VERSION}" deployer.phar env: GH_TOKEN: ${{ github.token }} - name: Upload signature run: gh release upload v"${RELEASE_VERSION}" deployer.phar.asc env: GH_TOKEN: ${{ github.token }} ================================================ FILE: .github/workflows/stale.yml ================================================ name: stale on: schedule: - cron: "* * * * *" workflow_dispatch: jobs: close-issues: runs-on: ubuntu-latest permissions: issues: write steps: - uses: actions/stale@v9 with: days-before-issue-stale: 0 days-before-issue-close: 0 ignore-updates: true close-issue-message: | This issue has been automatically closed. Please, open a discussion for bug reports and feature requests. Read more: https://github.com/deployphp/deployer/discussions/3888 days-before-pr-stale: -1 days-before-pr-close: -1 operations-per-run: 1440 ================================================ FILE: .github/workflows/test.yml ================================================ name: test on: push: branches: [ master ] pull_request: branches: [ master ] jobs: unit: runs-on: ubuntu-latest strategy: matrix: php-versions: [ '8.2', '8.3' ] steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} extensions: mbstring, intl coverage: xdebug - name: Validate composer.json and composer.lock run: composer validate - name: Get Composer Cache Directory id: composer-cache run: | echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: | ${{ runner.os }}-composer- - name: Install dependencies if: steps.composer-cache.outputs.cache-hit != 'true' run: composer install --prefer-dist --no-progress - name: Run test suite run: composer test ================================================ FILE: .gitignore ================================================ /vendor/ *.phar .phpunit.result.cache docker-compose.override.yml .php-cs-fixer.cache .idea/ ================================================ FILE: .php-cs-fixer.dist.php ================================================ in(__DIR__ . '/src') ->in(__DIR__ . '/recipe') ->in(__DIR__ . '/contrib') ->in(__DIR__ . '/tests'); return (new PhpCsFixer\Config()) ->setRules([ '@PER-CS' => true, // Due to historical reasons we have to keep this. // Docs parser expects comment right after php tag. 'blank_line_after_opening_tag' => false, // For PHP 7.4 compatibility. 'trailing_comma_in_multiline' => [ 'elements' => ['arguments', 'array_destructuring', 'arrays'] ], ]) ->setFinder($finder); ================================================ FILE: Dockerfile ================================================ FROM php:8.4-cli-alpine RUN apk add --no-cache bash git openssh-client rsync zip unzip libzip-dev \ RUN docker-php-ext-install mbstring mcrypt pcntl sockets curl zip COPY --chmod=755 deployer.phar /bin/dep WORKDIR /app ENTRYPOINT ["/bin/dep"] ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright © 2013 Anton Medvedev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

Deployer Logo Deployer

The PHP deployment tool with support for popular frameworks out of the box.



Deployer Screenshot


---

Browser testing via
Testmu AI

--- Build Status Latest Stable Version License See [deployer.org](https://deployer.org) for more information and documentation. ## Features - Automatic server **provisioning**. - **Zero downtime** deployments. - Ready to use recipes for **most frameworks**. ## Additional resources * [GitHub Action for Deployer](https://github.com/deployphp/action) * [Deployer Docker Image](https://hub.docker.com/r/deployphp/deployer) ## License [MIT](https://github.com/deployphp/deployer/blob/master/LICENSE)

Join Crow Watch

================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions Deployer is generally backwards compatible with very few exceptions, so we recommend users to always use the latest version to experience stability, performance and security. We generally backport security issues to a single previous major version, unless this is not possible or feasible with a reasonable effort. | Version | Supported | |---------|--------------------| | 8 | :white_check_mark: | | 7 | :white_check_mark: | | < 7 | :x: | ## Reporting a Vulnerability If you believe you've discovered a serious vulnerability, please contact the Expr core team at anton+security@medv.io. We will evaluate your report and if necessary issue a fix and an advisory. If the issue was previously undisclosed, we'll also mention your name in the credits. ================================================ FILE: bin/build ================================================ #!/usr/bin/env php * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (ini_get('phar.readonly') === '1') { throw new \Exception('Writing to phar files is disabled. Change your `php.ini` or append `-d phar.readonly=false` to the shebang, if supported by your `env` executable.'); } define('__ROOT__', realpath(__DIR__ . '/..')); chdir(__ROOT__); $opt = getopt('v:', ['nozip']); $version = $opt['v'] ?? null; if (empty($version)) { echo "Please, specify version as \"-v8.0.0\".\n"; exit(1); } if (!preg_match('/^\d+\.\d+\.\d+(\-\w+(\.\d+)?)?$/', $version)) { echo "Version must be \"7.0.0-beta.42\". Got \"$version\".\n"; exit(1); } echo `set -x; composer install --no-dev --prefer-dist --optimize-autoloader`; $pharName = "deployer.phar"; $pharFile = __ROOT__ . '/' . $pharName; if (file_exists($pharFile)) { unlink($pharFile); } $ignore = [ '.anton', '.git', 'Tests', 'tests', 'deploy.php', '.php-cs-fixer.dist.php', ]; $phar = new \Phar($pharFile, 0, $pharName); $phar->setSignatureAlgorithm(\Phar::SHA1); $phar->startBuffering(); $iterator = new RecursiveDirectoryIterator(__ROOT__, FilesystemIterator::SKIP_DOTS); $iterator = new RecursiveCallbackFilterIterator($iterator, function (SplFileInfo $fileInfo) use ($ignore) { return !in_array($fileInfo->getBasename(), $ignore, true); }); $iterator = new RecursiveIteratorIterator($iterator); $iterator = new CallbackFilterIterator($iterator, function (SplFileInfo $fileInfo) { //'bash', 'fish', 'zsh' is a completion templates return in_array($fileInfo->getExtension(), ['php', 'exe', 'bash', 'fish', 'zsh'], true); }); foreach ($iterator as $fileInfo) { $file = str_replace(__ROOT__, '', $fileInfo->getRealPath()); echo "+ " . $file . "\n"; $phar->addFile($fileInfo->getRealPath(), $file); if (!array_key_exists('nozip', $opt)) { $phar[$file]->compress(Phar::GZ); if (!$phar[$file]->isCompressed()) { echo "Could not compress File: $file\n"; } } } // Add Caddyfile echo "+ /recipe/provision/Caddyfile\n"; $phar->addFile(realpath(__DIR__ . '/../recipe/provision/Caddyfile'), '/recipe/provision/Caddyfile'); // Add 404.html echo "+ /recipe/provision/404.html\n"; $phar->addFile(realpath(__DIR__ . '/../recipe/provision/404.html'), '/recipe/provision/404.html'); // Add bin/dep file echo "+ /bin/dep\n"; $depContent = file_get_contents(__ROOT__ . '/bin/dep'); $depContent = str_replace("#!/usr/bin/env php\n", '', $depContent); $depContent = str_replace('__FILE__', 'str_replace("phar://", "", Phar::running())', $depContent); $depContent = preg_replace("/run\('.+?'/", "run('$version'", $depContent); $phar->addFromString('bin/dep', $depContent); $phar->setStub( <<stopBuffering(); unset($phar); echo "$pharName was created successfully.\n"; ================================================ FILE: bin/dep ================================================ #!/usr/bin/env php * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ // Check PHP version if (PHP_VERSION_ID < 80200) { fwrite(STDERR, "PHP 8.2 or higher is required.\n"); exit(1); } // Detect deploy.php location $deployFile = null; foreach ($argv as $i => $arg) { if (preg_match('/^(-f|--file)$/', $arg, $match) && $i + 1 < count($argv)) { $deployFile = $argv[$i + 1]; break; } if (preg_match('/^--file=(?.+)$/', $arg, $match)) { $deployFile = $match['file']; break; } if (preg_match('/^-f=?(?.+)$/', $arg, $match)) { $deployFile = $match['file']; break; } } if (!empty($deployFile)) { $deployFile = realpath($deployFile); } $lookUp = function (string $name): ?string { $dir = getcwd(); for ($i = 0; $i < 10; $i++) { $path = "$dir/$name"; if (is_readable($path)) { return $path; } $dir = dirname($dir); } return ''; }; if (empty($deployFile)) { $deployFile = $lookUp('deploy.php'); } if (empty($deployFile)) { $deployFile = $lookUp('deploy.yaml'); } if (empty($deployFile)) { $deployFile = $lookUp('deploy.yml'); } // Detect autoload location $autoload = [ __DIR__ . '/../vendor/autoload.php', // The dep located at "deployer.phar/bin" or in development. __DIR__ . '/../../../autoload.php', // The dep located at "vendor/deployer/deployer/bin". __DIR__ . '/../autoload.php', // The dep located at "vendor/bin". ]; $includes = [ __DIR__ . '/..', __DIR__ . '/../../../deployer/deployer', __DIR__ . '/../deployer/deployer', ]; $includePath = false; for ($i = 0; $i < count($autoload); $i++) { if (file_exists($autoload[$i]) && is_dir($includes[$i])) { require $autoload[$i]; $includePath = $includes[$i]; break; } } if (empty($includePath)) { fwrite(STDERR, "Error: The `autoload.php` file not found in:\n"); for ($i = 0; $i < count($autoload); $i++) { $a = file_exists($autoload[$i]) ? 'true' : 'false'; $b = is_dir($includes[$i]) ? 'true' : 'false'; fwrite(STDERR, " - file_exists($autoload[$i]) = $a\n"); fwrite(STDERR, " is_dir($includes[$i]) = $b\n"); } exit(1); } // Errors to exception set_error_handler(function ($severity, $message, $filename, $lineno) { if (error_reporting() == 0) { return; } if (error_reporting() & $severity) { throw new ErrorException($message, 0, $severity, $filename, $lineno); } }); // Enable recipe loading set_include_path($includePath . PATH_SEPARATOR . get_include_path()); // Deployer constants define('DEPLOYER', true); define('DEPLOYER_BIN', __FILE__); define('DEPLOYER_DEPLOY_FILE', $deployFile); Deployer\Deployer::run('master', $deployFile); ================================================ FILE: bin/docgen ================================================ #!/usr/bin/env php * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer; use Deployer\Documentation\ApiGen; use Deployer\Documentation\DocGen; use Symfony\Component\Console\Application; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Output\ConsoleOutput; require __DIR__ . '/../vendor/autoload.php'; chdir(realpath(__DIR__ . '/..')); $input = new ArgvInput(); $output = new ConsoleOutput(); $app = new Application('DocGen', '1.0.0'); $app->setDefaultCommand('all'); $api = function () use ($output) { $parser = new ApiGen(); $parser->parse(file_get_contents(__DIR__ . '/../src/functions.php')); $md = $parser->markdown(); file_put_contents(__DIR__ . '/../docs/api.md', $md); $output->writeln('API Reference documentation updated.'); }; $recipes = function () use ($input, $output) { $docgen = new DocGen(__DIR__ . '/..'); $docgen->parse(__DIR__ . '/../recipe'); $docgen->parse(__DIR__ . '/../contrib'); if ($input->getOption('json')) { echo json_encode($docgen->recipes, JSON_PRETTY_PRINT); return; } $docgen->gen(__DIR__ . '/../docs'); $output->writeln('Recipes documentation updated.'); }; $app->register('api')->setCode($api); $app->register('recipes')->setCode($recipes)->addOption('json'); $app->register('all')->setCode(function () use ($recipes, $api) { $api(); $recipes(); echo `git status`; })->addOption('json'); $app->run($input, $output); ================================================ FILE: composer.json ================================================ { "name": "deployer/deployer", "description": "Deployment Tool", "license": "MIT", "homepage": "https://deployer.org", "support": { "docs": "https://deployer.org/docs", "source": "https://github.com/deployphp/deployer", "issues": "https://github.com/deployphp/deployer/issues" }, "authors": [ { "name": "Anton Medvedev", "email": "anton@medv.io" } ], "funding": [ { "type": "github", "url": "https://github.com/sponsors/antonmedv" } ], "autoload": { "psr-4": { "Deployer\\": "src/" }, "files": [ "src/functions.php", "src/Support/helpers.php" ] }, "scripts": { "test": "pest", "test:e2e": "pest --config tests/e2e/phpunit-e2e.xml", "check": "php-cs-fixer check", "fix": "php-cs-fixer fix", "phpstan": "phpstan analyse -c phpstan.neon --memory-limit 1G", "phpstan:baseline": "@phpstan --generate-baseline tests/phpstan-baseline.neon" }, "bin": [ "bin/dep" ], "require": { "php": ">=8.2", "symfony/console": "^7.2", "symfony/process": "^7.2", "symfony/yaml": "^7.2" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.68", "pestphp/pest": "^3.3", "phpstan/phpstan": "^1.4", "phpunit/php-code-coverage": "^11.0", "phpunit/phpunit": "^11.4" }, "config": { "sort-packages": true, "process-timeout": 0, "allow-plugins": { "pestphp/pest-plugin": true, "dealerdirect/phpcodesniffer-composer-installer": true } } } ================================================ FILE: contrib/bugsnag.php ================================================ get('bugsnag_api_key'), 'releaseStage' => get('target'), 'repository' => get('repository'), 'provider' => get('bugsnag_provider', ''), 'branch' => get('branch'), 'revision' => runLocally('git log -n 1 --format="%h"'), 'appVersion' => get('bugsnag_app_version', ''), ]; Httpie::post('https://notify.bugsnag.com/deploy') ->jsonBody($data) ->send(); }); ================================================ FILE: contrib/cachetool.php ================================================ set('cachetool', '127.0.0.1:9000'); host('production') ->set('cachetool', '/var/run/php-fpm.sock'); ``` By default, if no `cachetool` parameter is provided, this recipe will fallback to the global setting. If your deployment user does not have permission to access the php-fpm.sock, you can alternatively use the web adapter that creates a temporary php file and makes a web request to it with a configuration like ```php set('cachetool_args', '--web --web-path=./public --web-url=https://{{hostname}}'); ``` ## Usage Since APCu and OPcache deal with compiling and caching files, they should be executed right after the symlink is created for the new release: ```php after('deploy:symlink', 'cachetool:clear:opcache'); // or after('deploy:symlink', 'cachetool:clear:apcu'); ``` ## Read more Read more information about cachetool on the website: http://gordalina.github.io/cachetool/ */ namespace Deployer; set('cachetool', ''); /** * URL to download cachetool from if it is not available * * CacheTool 9.x works with PHP >=8.1 * CacheTool 8.x works with PHP >=8.0 * CacheTool 7.x works with PHP >=7.3 */ set('cachetool_url', 'https://github.com/gordalina/cachetool/releases/download/9.1.0/cachetool.phar'); set('cachetool_args', ''); set('bin/cachetool', function () { if (!test('[ -f {{release_or_current_path}}/cachetool.phar ]')) { run("cd {{release_or_current_path}} && curl -sLO {{cachetool_url}}"); } return '{{release_or_current_path}}/cachetool.phar'; }); set('cachetool_options', function () { $options = (array) get('cachetool'); $fullOptions = (string) get('cachetool_args'); $return = []; if ($fullOptions !== '') { $return = [$fullOptions]; } elseif (count($options) > 0) { foreach ($options as $option) { if (is_string($option) && $option !== '') { $return[] = "--fcgi={$option}"; } } } return $return ?: ['']; }); /** * Clear opcache cache */ desc('Clears OPcode cache'); task('cachetool:clear:opcache', function () { $options = get('cachetool_options'); foreach ($options as $option) { run("cd {{release_or_current_path}} && {{bin/php}} {{bin/cachetool}} opcache:reset $option"); } }); /** * Clear APCu cache */ desc('Clears APCu system cache'); task('cachetool:clear:apcu', function () { $options = get('cachetool_options'); foreach ($options as $option) { run("cd {{release_or_current_path}} && {{bin/php}} {{bin/cachetool}} apcu:cache:clear $option"); } }); /** * Clear file status cache, including the realpath cache */ desc('Clears file status and realpath caches'); task('cachetool:clear:stat', function () { $options = get('cachetool_options'); foreach ($options as $option) { run("cd {{release_or_current_path}} && {{bin/php}} {{bin/cachetool}} stat:clear $option"); } }); ================================================ FILE: contrib/chatwork.php ================================================ formBody(['body' => get('chatwork_message')]) ->header("X-ChatWorkToken", get('chatwork_token')) ->send(); }); // Tasks desc('Tests messages'); task('chatwork:test', function () { set('chatwork_message', get('chatwork_notify_text')); invoke('chatwork_send_message'); set('chatwork_message', get('chatwork_success_text')); invoke('chatwork_send_message'); set('chatwork_message', get('chatwork_failure_text')); invoke('chatwork_send_message'); }) ->once(); desc('Notifies Chatwork'); task('chatwork:notify', function () { if (!get('chatwork_token', false)) { return; } if (!get('chatwork_room_id', false)) { return; } set('chatwork_message', get('chatwork_notify_text')); invoke('chatwork_send_message'); }) ->once() ->hidden(); desc('Notifies Chatwork about deploy finish'); task('chatwork:notify:success', function () { if (!get('chatwork_token', false)) { return; } if (!get('chatwork_room_id', false)) { return; } set('chatwork_message', get('chatwork_success_text')); invoke('chatwork_send_message'); }) ->once() ->hidden(); desc('Notifies Chatwork about deploy failure'); task('chatwork:notify:failure', function () { if (!get('chatwork_token', false)) { return; } if (!get('chatwork_room_id', false)) { return; } set('chatwork_message', get('chatwork_failure_text')); invoke('chatwork_send_message'); }) ->once() ->hidden(); ================================================ FILE: contrib/cimonitor.php ================================================ 'John Doe', 'email' => 'john@enrise.com', ]; }); ``` Various cimonitor statusses are set, in case you want to change these yourselves. See the [CIMonitor documentation](https://cimonitor.readthedocs.io/en/latest/) for the usages of different states. ## Usage If you want to notify only about beginning of deployment add this line only: ```php before('deploy', 'cimonitor:notify'); ``` If you want to notify about successful end of deployment add this too: ```php after('deploy:success', 'cimonitor:notify:success'); ``` If you want to notify about failed deployment add this too: ```php after('deploy:failed', 'cimonitor:notify:failure'); ``` */ namespace Deployer; use Deployer\Utility\Httpie; // Title of project based on git repo set('cimonitor_title', function () { $repo = get('repository'); $pattern = '/\w+\/\w+/'; return preg_match($pattern, $repo, $titles) ? $titles[0] : $repo; }); set('cimonitor_user', function () { return [ 'name' => runLocally('git config --get user.name'), 'email' => runLocally('git config --get user.email'), ]; }); // CI monitor status states and job states set('cimonitor_status_info', 'info'); set('cimonitor_status_warning', 'warning'); set('cimonitor_status_error', 'error'); set('cimonitor_status_success', 'success'); set('cimonitor_job_state_info', get('cimonitor_status_info')); set('cimonitor_job_state_pending', 'pending'); set('cimonitor_job_state_running', 'running'); set('cimonitor_job_state_warning', get('cimonitor_status_warning')); set('cimonitor_job_state_error', get('cimonitor_status_error')); set('cimonitor_job_state_success', get('cimonitor_status_success')); desc('Notifies CIMonitor'); task('cimonitor:notify', function () { if (!get('cimonitor_webhook', false)) { return; } $body = [ 'state' => get('cimonitor_status_warning'), 'branch' => get('branch'), 'title' => get('cimonitor_title'), 'user' => get('cimonitor_user'), 'stages' => [get('stage', '')], 'jobs' => [ [ 'name' => 'Deploying...', 'stage' => '', 'state' => get('cimonitor_job_state_running'), ], ], ]; Httpie::post(get('cimonitor_webhook'))->jsonBody($body)->send(); }) ->once() ->hidden(); desc('Notifies CIMonitor about deploy finish'); task('cimonitor:notify:success', function () { if (!get('cimonitor_webhook', false)) { return; } $depstage = 'Deployed to ' . get('stage', ''); $body = [ 'state' => get('cimonitor_status_success'), 'branch' => get('branch'), 'title' => get('cimonitor_title'), 'user' => get('cimonitor_user'), 'stages' => [$depstage], 'jobs' => [ [ 'name' => 'Deploy', 'stage' => $depstage, 'state' => get('cimonitor_job_state_success'), ], ], ]; Httpie::post(get('cimonitor_webhook'))->jsonBody($body)->send(); }) ->once() ->hidden(); desc('Notifies CIMonitor about deploy failure'); task('cimonitor:notify:failure', function () { if (!get('cimonitor_webhook', false)) { return; } $body = [ 'state' => get('cimonitor_status_error'), 'branch' => get('branch'), 'title' => get('cimonitor_title'), 'user' => get('cimonitor_user'), 'stages' => [get('stage', '')], 'jobs' => [ [ 'name' => 'Deploy', 'stage' => '', 'state' => get('cimonitor_job_state_error'), ], ], ]; Httpie::post(get('cimonitor_webhook'))->jsonBody($body)->send(); }) ->once() ->hidden(); ================================================ FILE: contrib/cloudflare.php ================================================ $config['service_key'], ]; } elseif (!empty($config['email']) && !empty($config['api_key'])) { $headers = [ 'X-Auth-Key' => $config['api_key'], 'X-Auth-Email' => $config['email'], ]; } elseif (!empty($config['api_token'])) { $headers = [ 'Authorization' => 'Bearer ' . $config['api_token'], ]; } else { throw new \RuntimeException("Set a service key or email / api key"); } $headers['Content-Type'] = 'application/json'; $makeRequest = function ($url, $opts = []) use ($headers) { $ch = curl_init("https://api.cloudflare.com/client/v4/$url"); $parsedHeaders = []; foreach ($headers as $key => $value) { $parsedHeaders[] = "$key: $value"; } curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => $parsedHeaders, CURLOPT_RETURNTRANSFER => true, ]); curl_setopt_array($ch, $opts); $res = curl_exec($ch); if (curl_errno($ch)) { throw new \RuntimeException("Error making curl request (result: $res)"); } if (PHP_MAJOR_VERSION < 8) { curl_close($ch); } return $res; }; $zoneId = $config['zone_id']; if (empty($zoneId)) { if (empty($config['domain'])) { throw new \RuntimeException("Set a domain"); } // get the mysterious zone id from Cloud Flare $zones = json_decode($makeRequest( "zones?name={$config['domain']}", ), true); if (!empty($zones['errors'])) { throw new \RuntimeException('Problem with zone data'); } else { $zoneId = current($zones['result'])['id']; } } // make purge request $makeRequest( "zones/$zoneId/purge_cache", [ CURLOPT_CUSTOMREQUEST => 'DELETE', CURLOPT_POSTFIELDS => json_encode( [ 'purge_everything' => true, ], ), ], ); }); ================================================ FILE: contrib/cpanel.php ================================================ getenv('CPANEL_HOST'), 'port' => getenv('CPANEL_PORT'), 'username' => getenv('CPANEL_USERNAME'), 'auth_type' => getenv('CPANEL_AUTH_TYPE'), 'token' => getenv('CPANEL_TOKEN'), 'user' => getenv('CPANEL_USER'), 'db_user' => getenv('CPANEL_DB_USER'), 'db_user_privileges' => getenv('CPANEL_DB_PRIVILEGES'), 'timeout' => 500, 'allowInStage' => ['staging', 'beta', 'alpha'], 'create_domain_format' => '%s-%s-%s', 'create_domain_values' => ['staging', 'master', get('application')], 'subdomain_prefix' => substr(md5(get('application')), 0,4) . '-', 'subdomain_suffix' => getenv('SUDOMAIN_SUFFIX'), 'create_db_format' => '%s_%s-%s-%s', 'create_db_values' => ['apps', 'staging','master', get('application')], ]); ``` - `cpanel` – array with configuration for cPanel - `username` – WHM account - `user` – cPanel account that you want in charge of the domain - `token` – WHM API token - `create_domain_format` – Format for name creation of domain - `create_domain_values` – The actual value reference for naming - `subdomain_prefix` – cPanel has a weird way of dealing with addons and subdomains, you cannot create 2 addons with the same subdomain, so you need to change it in some way, example uses first 4 chars of md5(app_name) - `subdomain_suffix` – cPanel has a weird way of dealing with addons and subdomains, so the suffix needs to be your main domain for that account for deletion purposes - `addondir` – addon dir is different from the deploy path because cPanel "injects" /home/user/ into the path, so tilde cannot be used - `allowInStage` – Define the stages that cPanel recipe actions are allowed in #### .env file example ``` CPANEL_HOST=xxx.xxx.xxx.xxx CPANEL_PORT=2087 CPANEL_USERNAME=root CPANEL_TOKEN=xxxx CPANEL_USER=xxx CPANEL_AUTH_TYPE=hash CPANEL_DB_USER=db_user CPANEL_DB_PRIVILEGES="ALL PRIVILEGES" SUDOMAIN_SUFFIX=.mymaindomain.com ``` ### Tasks - `cpanel:createaddondomain` Creates an addon domain - `cpanel:deleteaddondomain` Removes an addon domain - `cpanel:createdb` Creates a new database ### Usage A complete example with configs, staging and deployment ``` load(); // this is used just so an .env file can be used for credentials require 'cpanel.php'; // Project name set('application', 'myproject.com'); // Project repository set('repository', 'git@github.com:myorg/myproject.com'); set('cpanel', [ 'host' => getenv('CPANEL_HOST'), 'port' => getenv('CPANEL_PORT'), 'username' => getenv('CPANEL_USERNAME'), 'auth_type' => getenv('CPANEL_AUTH_TYPE'), 'token' => getenv('CPANEL_TOKEN'), 'user' => getenv('CPANEL_USER'), 'db_user' => getenv('CPANEL_DB_USER'), 'db_user_privileges' => getenv('CPANEL_DB_PRIVILEGES'), 'timeout' => 500, 'allowInStage' => ['staging', 'beta', 'alpha'], 'create_domain_format' => '%s-%s-%s', 'create_domain_values' => ['staging', 'master', get('application')], 'subdomain_prefix' => substr(md5(get('application')), 0,4) . '-', 'subdomain_suffix' => getenv('SUDOMAIN_SUFFIX'), 'create_db_format' => '%s_%s-%s-%s', 'create_db_values' => ['apps', 'staging','master', get('application')], ]); host('myproject.com') ->stage('staging') ->set('cpanel_createdb', vsprintf(get('cpanel')['create_db_format'], get('cpanel')['create_db_values'])) ->set('branch', 'dev-branch') ->set('deploy_path', '~/staging/' . vsprintf(get('cpanel')['create_domain_format'], get('cpanel')['create_domain_values'])) ->set('addondir', 'staging/' . vsprintf(get('cpanel')['create_domain_format'], get('cpanel')['create_domain_values'])); // Tasks task('build', function () { run('cd {{release_path}} && build'); }); after('deploy:prepare', 'cpanel:createaddondomain'); after('deploy:prepare', 'cpanel:createdb'); ``` */ namespace Deployer; use Deployer\Task\Context; use Gufy\CpanelPhp\Cpanel; /** * @return Cpanel * @throws Exception\Exception */ function getCpanel() { $config = get('cpanel', []); $allowInStage = $config['allowInStage']; $stage = input()->getArgument('stage'); if (!class_exists('\Gufy\CpanelPhp\Cpanel')) { throw new \RuntimeException("Please install php package gufy/cpanel-php to use CPanel API"); } if (!in_array($stage, $allowInStage)) { throw new \RuntimeException(sprintf("Since it creates addon domains and databases, CPanel recipe is available only in the %s environments", implode($allowInStage))); } if (!is_array($config) || !isset($config['host']) || !isset($config['port']) || !isset($config['username']) || !isset($config['token']) || !isset($config['user'])) { throw new \RuntimeException("Please configure CPanel config: set('cpanel', array('host' => 'xxx.xxx.xxx.xxx:', 'port' => 2087 , 'username' => 'root', 'token' => 'asdfasdf', 'cpaneluser' => 'guy'));"); } $cpanel = new Cpanel([ 'host' => 'https://' . $config['host'] . ':' . $config['port'], 'username' => $config['username'], 'auth_type' => $config['auth_type'], 'password' => $config['token'], ]); $cpanel->setTimeout($config['timeout']); return $cpanel; } function getDomainInfo() { $domain = vsprintf(get('cpanel')['create_domain_format'], get('cpanel')['create_domain_values']); $cleanDomain = str_replace(['.', ',', ' ', '/', '-'], '', $domain); $subDomain = get('cpanel')['subdomain_prefix'] . $cleanDomain; return [ 'domain' => $domain, 'subDomain' => $subDomain, 'subDomainWithSuffix' => $subDomain . get('cpanel')['subdomain_suffix'], ]; } desc('Creates database though CPanel API'); task('cpanel:createdb', function () { $cpanel = getCPanel(); $config = get('cpanel', []); if (!askConfirmation(sprintf('This will try to create the database %s on the host though CPanel API, ok?', get('cpanel_createdb')), true)) { return; } $createDbDataResult = $cpanel->cpanel('MysqlFE', 'createdb', $config['user'], ['db' => get('cpanel_createdb')]); $addPrivilegesDataResult = $cpanel->cpanel('MysqlFE', 'setdbuserprivileges', $config['user'], ['privileges' => $config['db_user_privileges'], 'db' => get('cpanel_createdb'), 'dbuser' => $config['db_user']]); $createDbData = json_decode($createDbDataResult, true); $addPrivilegesData = json_decode($addPrivilegesDataResult, true); if (isset($createDbData['cpanelresult']['error'])) { writeln($createDbData['cpanelresult']['error']); } else { writeln('Successfully created database!'); } if (isset($addPrivilegesData['cpanelresult']['error'])) { writeln($addPrivilegesData['cpanelresult']['error']); } else { writeln('Successfully added privileges to database!'); } }); desc('Creates addon domain though CPanel API'); task('cpanel:createaddondomain', function () { $cpanel = getCPanel(); $config = get('cpanel', []); $domain = getDomainInfo()['domain']; $subDomain = getDomainInfo()['subDomain']; if (!askConfirmation(sprintf('This will try to create the addon domain %s and point it to %s and subdomain %s, ok?', $domain, get('addondir'), $subDomain), true)) { return; } writeln(sprintf('Creates addon domain %s and pointing it to %s', $domain, get('addondir'))); $addAddonDomainResult = $cpanel->cpanel('AddonDomain', 'addaddondomain', $config['user'], ['dir' => get('addondir'), 'newdomain' => $domain, 'subdomain' => $subDomain]); $addAddonDomainData = json_decode($addAddonDomainResult, true); if (isset($addAddonDomainResult['cpanelresult']['error'])) { writeln($addAddonDomainData['cpanelresult']['error']); } else { writeln('Successfully created addon domain!'); writeln($addAddonDomainData['cpanelresult']['data'][0]['reason']); } }); desc('Deletes addon domain though CPanel API'); task('cpanel:deleteaddondomain', function () { $cpanel = getCPanel(); $config = get('cpanel', []); $domain = getDomainInfo()['domain']; $subDomain = getDomainInfo()['subDomain']; $subDomainWithSuffix = getDomainInfo()['subDomainWithSuffix']; if (!askConfirmation(sprintf('This will delete the addon domain %s with corresponding subdomain %s, ok?', $domain, $subDomain), true)) { return; } writeln(sprintf('Deleting addon domain %s', $domain)); $delAddonDomainResult = $cpanel->cpanel('AddonDomain', 'deladdondomain', $config['user'], ['domain' => $domain, 'subdomain' => $subDomainWithSuffix]); $delAddonDomainResult = json_decode($delAddonDomainResult, true); if (isset($delAddonDomainResult['cpanelresult']['error'])) { writeln($delAddonDomainResult['cpanelresult']['error']); } else { writeln('Successfully deleted addon domain!'); writeln($delAddonDomainResult['cpanelresult']['data'][0]['reason']); } }); ================================================ FILE: contrib/crontab.php ================================================ > /dev/null 2>&1', ]); ``` */ namespace Deployer; use function Deployer\Support\escape_shell_argument; // Get path to bin set('bin/crontab', function () { return which('crontab'); }); // Set the identifier used in the crontab, application name by default set('crontab:identifier', function () { return get('application', 'application'); }); // Use sudo to run crontab. When running crontab with sudo, you can use the `-u` parameter to change a crontab for a different user. set('crontab:use_sudo', false); desc('Sync crontab jobs'); task('crontab:sync', function () { $cronJobsLocal = array_map( fn($job) => parse($job), get('crontab:jobs', []), ); if (count($cronJobsLocal) == 0) { writeln("Nothing to sync - configure crontab:jobs"); return; } $cronJobs = getRemoteCrontab(); $identifier = get('crontab:identifier'); $sectionStart = "###< $identifier"; $sectionEnd = "###> $identifier"; // find our cronjob section $start = array_search($sectionStart, $cronJobs); $end = array_search($sectionEnd, $cronJobs); if ($start === false || $end === false) { // Move the duplicates over when first generating the section foreach ($cronJobs as $index => $cronJob) { if (in_array($cronJob, $cronJobsLocal)) { unset($cronJobs[$index]); writeln("Crontab: Found existing job in crontab, moving it to the section"); } } // Create the section $cronJobs[] = $sectionStart; $cronJobs = [...$cronJobs, ...$cronJobsLocal]; $cronJobs[] = $sectionEnd; writeln("Crontab: Found no section, created the section with configured jobs"); } else { // Replace the existing section array_splice($cronJobs, $start + 1, $end - $start - 1, $cronJobsLocal); writeln("Crontab: Found existing section, replaced with configured jobs"); } setRemoteCrontab($cronJobs); }); desc('Remove crontab jobs'); task('crontab:remove', function () { $cronJobsLocal = array_map( fn($job) => parse($job), get('crontab:jobs', []), ); $cronJobs = getRemoteCrontab(); $identifier = get('crontab:identifier'); $sectionStart = "###< $identifier"; $sectionEnd = "###> $identifier"; // Find our cronjob section $start = array_search($sectionStart, $cronJobs); $end = array_search($sectionEnd, $cronJobs); if ($start && $end) { // Remove the existing section array_splice($cronJobs, $start + 1, $end - $start - 1); writeln("Crontab: Found existing section, removed jobs"); } elseif (count($cronJobsLocal) > 0) { $foundJobs = false; // Remove individual jobs if no section is present foreach ($cronJobs as $index => $cronJob) { if (in_array($cronJob, $cronJobsLocal)) { unset($cronJobs[$index]); $foundJobs = true; } } if ($foundJobs) { writeln("Crontab: Found existing jobs in crontab, removed jobs"); } else { writeln("Crontab: No existing jobs in crontab, skipping"); return; } } else { writeln("Crontab: Found no section and crontab:jobs is not configured, skipping"); return; } setRemoteCrontab($cronJobs); }); function setRemoteCrontab(array $lines): void { $sudo = get('crontab:use_sudo') ? 'sudo' : ''; $tmpCrontabPath = sprintf('/tmp/%s', uniqid('crontab_save_')); if (test("[ -f '$tmpCrontabPath' ]")) { run("unlink '$tmpCrontabPath'"); } foreach ($lines as $line) { run("echo " . escape_shell_argument($line) . " >> $tmpCrontabPath"); } run("$sudo {{bin/crontab}} " . $tmpCrontabPath); run('unlink ' . $tmpCrontabPath); } function getRemoteCrontab(): array { $sudo = get('crontab:use_sudo') ? 'sudo' : ''; if (!test("$sudo {{bin/crontab}} -l >> /dev/null 2>&1")) { return []; } return explode(PHP_EOL, run("$sudo {{bin/crontab}} -l")); } ================================================ FILE: contrib/directadmin.php ================================================ '127.0.0.1', 'port' => 2222, 'username' => 'admin', 'password' => 'password']);"); } return $config; } /** * DirectAdmin * * @param string $action * @param array $data * * @return void */ function DirectAdmin(string $action, array $data = []) { $config = getDirectAdminConfig(); $scheme = $config['scheme'] ?? 'http'; $port = $config['port'] ?? 2222; $result = Httpie::post(sprintf('%s://%s:%s/%s', $scheme, $config['host'], $port, $action)) ->formBody($data) ->setopt(CURLOPT_USERPWD, $config['username'] . ':' . $config['password']) ->send(); parse_str($result, $resultData); if ($resultData['error'] === '1') { $resultData['details'] = trim($resultData['details']); $resultData['details'] = str_replace(['\\n', '\\r'], '', $resultData['details']); $resultData['details'] = strip_tags($resultData['details']); writeln('DirectAdmin message: ' . $resultData['details'] . ''); } } desc('Creates a database on DirectAdmin'); task('directadmin:createdb', function () { $config = getDirectAdminConfig(); if (!is_array($config) || !isset($config['db_name']) || !isset($config['db_user']) || !isset($config['db_password'])) { throw new \RuntimeException("Please add the following DirectAdmin config:" . PHP_EOL . "add('directadmin', ['db_name' => 'test', 'db_user' => 'test', 'db_password' => '123456']);"); } DirectAdmin('CMD_API_DATABASES', [ 'action' => 'create', 'name' => $config['db_name'], 'user' => $config['db_user'], 'passwd' => $config['db_password'], 'passwd2' => $config['db_password'], ]); }); desc('Deletes a database on DirectAdmin'); task('directadmin:deletedb', function () { $config = getDirectAdminConfig(); if (!is_array($config) || !isset($config['db_user'])) { throw new \RuntimeException("Please add the following DirectAdmin config:" . PHP_EOL . "add('directadmin', ['db_user' => 'test_database']);"); } DirectAdmin('CMD_API_DATABASES', [ 'action' => 'delete', 'select0' => $config['username'] . '_' . $config['db_user'], ]); }); desc('Creates a domain on DirectAdmin'); task('directadmin:createdomain', function () { $config = getDirectAdminConfig(); if (!is_array($config) || !isset($config['domain_name'])) { throw new \RuntimeException("Please add the following DirectAdmin config:" . PHP_EOL . "add('directadmin', ['domain_name' => 'test.example.com']);"); } DirectAdmin('CMD_API_DOMAIN', [ 'action' => 'create', 'domain' => $config['domain_name'], 'ssl' => $config['domain_ssl'] ?? 'On', 'cgi' => $config['domain_cgi'] ?? 'ON', 'php' => $config['domain_php'] ?? 'ON', ]); }); desc('Deletes a domain on DirectAdmin'); task('directadmin:deletedomain', function () { $config = getDirectAdminConfig(); if (!is_array($config) || !isset($config['domain_name'])) { throw new \RuntimeException("Please add the following DirectAdmin config:" . PHP_EOL . "add('directadmin', ['domain_name' => 'test.example.com']);"); } DirectAdmin('CMD_API_DOMAIN', [ 'delete' => 'anything', 'confirmed' => 'anything', 'select0' => $config['domain_name'], ]); }); desc('Symlink your private_html to public_html'); task('directadmin:symlink-private-html', function () { $config = getDirectAdminConfig(); if (!is_array($config) || !isset($config['domain_name'])) { throw new \RuntimeException("Please add the following DirectAdmin config:" . PHP_EOL . "add('directadmin', ['domain_name' => 'test.example.com']);"); } DirectAdmin('CMD_API_DOMAIN', [ 'action' => 'private_html', 'domain' => $config['domain_name'], 'val' => 'symlink', ]); }); desc('Changes the PHP version from a domain'); task('directadmin:php-version', function () { $config = getDirectAdminConfig(); if (!is_array($config) || !isset($config['domain_name']) || !isset($config['domain_php_version'])) { throw new \RuntimeException("Please add the following DirectAdmin config:" . PHP_EOL . "add('directadmin', ['domain_name' => 'test.example.com', 'domain_php_version' => 1]);"); } DirectAdmin('CMD_API_DOMAIN', [ 'action' => 'php_selector', 'domain' => $config['domain_name'], 'php1_select' => $config['domain_php_version'], ]); }); ================================================ FILE: contrib/discord.php ================================================ parse(':information_source: **{{user}}** is deploying branch `{{what}}` to _{{where}}_'), ]; }); set('discord_success_text', function () { return [ 'text' => parse(':white_check_mark: Branch `{{what}}` deployed to _{{where}}_ successfully'), ]; }); set('discord_failure_text', function () { return [ 'text' => parse(':no_entry_sign: Branch `{{what}}` has failed to deploy to _{{where}}_'), ]; }); // The message set('discord_message', 'discord_notify_text'); // Helpers task('discord_send_message', function () { $message = get(get('discord_message')); Httpie::post(get('discord_webhook'))->jsonBody($message)->send(); }); // Tasks desc('Tests messages'); task('discord:test', function () { set('discord_message', 'discord_notify_text'); invoke('discord_send_message'); set('discord_message', 'discord_success_text'); invoke('discord_send_message'); set('discord_message', 'discord_failure_text'); invoke('discord_send_message'); }) ->once(); desc('Notifies Discord'); task('discord:notify', function () { set('discord_message', 'discord_notify_text'); invoke('discord_send_message'); }) ->once() ->isHidden(); desc('Notifies Discord about deploy finish'); task('discord:notify:success', function () { set('discord_message', 'discord_success_text'); invoke('discord_send_message'); }) ->once() ->isHidden(); desc('Notifies Discord about deploy failure'); task('discord:notify:failure', function () { set('discord_message', 'discord_failure_text'); invoke('discord_send_message'); }) ->once() ->isHidden(); ================================================ FILE: contrib/grafana.php ================================================ 'eyJrIj...', 'url' => 'http://grafana/api/annotations', 'tags' => ['deploy', 'production'], ]); ``` ## Usage If you want to create annotation about successful end of deployment. ```php after('deploy:success', 'grafana:annotation'); ``` */ namespace Deployer; use Deployer\Utility\Httpie; desc('Creates Grafana annotation of deployment'); task('grafana:annotation', function () { $defaultConfig = [ 'url' => null, 'token' => null, 'time' => round(microtime(true) * 1000), 'tags' => [], 'text' => null, ]; $config = array_merge($defaultConfig, (array) get('grafana')); if (!is_array($config) || !isset($config['url']) || !isset($config['token'])) { throw new \RuntimeException("Please configure Grafana: set('grafana', ['url' => 'https://localhost/api/annotations', token' => 'eyJrIjo...']);"); } $params = [ 'time' => $config['time'], 'isRegion' => false, 'tags' => $config['tags'], 'text' => $config['text'], ]; if (!isset($params['text'])) { $params['text'] = 'Deployed ' . trim(runLocally('git log -n 1 --format="%h"')); } Httpie::post($config['url']) ->header('Authorization', 'Bearer ' . $config['token']) ->jsonBody($params) ->send(); }); ================================================ FILE: contrib/hangouts.php ================================================ [ 'title' => get('chat_title'), 'subtitle' => get('chat_subtitle'), 'imageUrl' => get('favicon'), 'imageStyle' => 'IMAGE', ], 'sections' => [ 'widgets' => [ 'keyValue' => [ 'topLabel' => get('chat_line1'), 'content' => get('chat_line2'), 'contentMultiline' => false, 'bottomLabel' => 'started', // Use 'iconUrl' to set a custom icon URL (png) 'icon' => 'CLOCK', 'button' => [ 'textButton' => [ 'text' => 'Visit site', 'onClick' => [ 'openLink' => [ 'url' => get('hostname'), ], ], ], ], ], ], ], ]; Httpie::post(get('chat_webhook'))->jsonBody(['cards' => $card])->send(); }) ->once() ->hidden(); desc('Notifies Google Hangouts Chat about deploy finish'); task('chat:notify:success', function () { if (!get('chat_webhook', false)) { return; } $card = [ 'header' => [ 'title' => get('chat_title'), 'subtitle' => get('chat_subtitle'), 'imageUrl' => get('favicon'), 'imageStyle' => 'IMAGE', ], 'sections' => [ 'widgets' => [ 'keyValue' => [ 'topLabel' => get('chat_line1'), 'content' => get('chat_line2'), 'contentMultiline' => false, 'bottomLabel' => 'succeeded', // Use 'iconUrl' to set a custom icon URL (png) 'icon' => 'STAR', 'button' => [ 'textButton' => [ 'text' => 'Visit site', 'onClick' => [ 'openLink' => [ 'url' => get('hostname'), ], ], ], ], ], ], ], ]; Httpie::post(get('chat_webhook'))->jsonBody(['cards' => $card])->send(); }) ->once() ->hidden(); desc('Notifies Google Hangouts Chat about deploy failure'); task('chat:notify:failure', function () { if (!get('chat_webhook', false)) { return; } $card = [ 'header' => [ 'title' => get('chat_title'), 'subtitle' => get('chat_subtitle'), 'imageUrl' => get('favicon'), 'imageStyle' => 'IMAGE', ], 'sections' => [ 'widgets' => [ 'keyValue' => [ 'topLabel' => get('chat_line1'), 'content' => get('chat_line2'), 'contentMultiline' => false, 'bottomLabel' => 'failed', // Use 'iconUrl' to set a custom icon URL (png) // or use 'icon' and pick from this list: // https://developers.google.com/hangouts/chat/reference/message-formats/cards#customicons 'button' => [ 'textButton' => [ 'text' => 'Visit site', 'onClick' => [ 'openLink' => [ 'url' => get('hostname'), ], ], ], ], ], ], ], ]; Httpie::post(get('chat_webhook'))->jsonBody(['cards' => $card])->send(); }) ->once() ->hidden(); ================================================ FILE: contrib/hipchat.php ================================================ get('hipchat_room_id'), 'from' => get('target'), 'message' => get('hipchat_message'), 'color' => get('hipchat_color'), 'auth_token' => get('hipchat_token'), 'notify' => 0, 'format' => 'json', ]; Httpie::get(get('hipchat_url')) ->query($params) ->send(); }); ================================================ FILE: contrib/ispmanager.php ================================================ [ 'dsn' => 'https://root:password@localhost:1500/ispmgr', 'secure' => true, ], 'createDomain' => null, 'updateDomain' => null, 'deleteDomain' => null, 'createDatabase' => null, 'deleteDatabase' => null, 'phpSelect' => null, 'createAlias' => null, 'deleteAlias' => null, ]); // Vhost default configuration set('vhost', [ 'name' => '{{domain}}', 'php_enable' => 'on', 'aliases' => 'www.{{domain}}', 'home' => 'www/{{domain}}', 'owner' => get('ispmanager_owner'), 'email' => 'webmaster@{{domain}}', 'charset' => 'off', 'dirindex' => 'index.php uploaded.html', 'ssi' => 'on', 'php' => 'on', 'php_mode' => 'php_mode_mod', 'basedir' => 'on', 'php_apache_version' => 'native', 'cgi' => 'off', 'log_access' => 'on', 'log_error' => 'on', ]); // Storage set('ispmanager_session', ''); set('ispmanager_databases', [ 'servers' => [], 'hosts' => [], 'dblist' => [], ]); set('ispmanager_domains', []); set('ispmanager_phplist', []); set('ispmanager_aliaslist', []); desc('Installs ispmanager'); task('ispmanager:init', function () { $config = get('ispmanager'); if (!is_null($config['createDatabase']) || !is_null($config['deleteDatabase'])) { invoke('ispmanager:db-server-list'); invoke('ispmanager:db-list'); } if (!is_null($config['createDomain']) || !is_null($config['deleteDomain'])) { invoke('ispmanager:domain-list'); } if (!is_null($config['phpSelect'])) { invoke('ispmanager:domain-list'); invoke('ispmanager:get-php-list'); } if (!is_null($config['createAlias']) || !is_null($config['deleteAlias'])) { invoke('ispmanager:domain-list'); } }); desc('Takes database servers list'); task('ispmanager:db-server-list', function () { $response = ispmanagerRequest('get', [ 'func' => 'db.server', ]); $hostList = []; $serverList = []; if (isset($response['doc']['elem']) && count($response['doc']['elem']) > 0) { foreach ($response['doc']['elem'] as $dbServer) { $serverList[$dbServer['name']['$']] = [ 'host' => $dbServer['host']['$'], 'name' => $dbServer['name']['$'], 'version' => $dbServer['savedver']['$'], ]; if (!strpos($dbServer['host']['$'], ':')) { $dbHost = $dbServer['host']['$'] . ':3306'; } else { $dbHost = $dbServer['host']['$']; } $hostList[$dbHost] = [ 'host' => $dbHost, 'name' => $dbServer['name']['$'], 'version' => $dbServer['savedver']['$'], ]; } } add('ispmanager_databases', [ 'servers' => $serverList, 'hosts' => $hostList, ]); }); desc('Takes databases list'); task('ispmanager:db-list', function () { $response = ispmanagerRequest('get', [ 'func' => 'db', ]); $dbList = []; if (isset($response['doc']['elem']) && count($response['doc']['elem']) > 0) { foreach ($response['doc']['elem'] as $db) { $dbList[$db['pair']['$']] = [ 'name' => $db['name']['$'], 'server' => $db['server']['$'], 'location' => $db['pair']['$'], ]; } } add('ispmanager_databases', [ 'dblist' => $dbList, ]); }); desc('Takes domain list'); task('ispmanager:domain-list', function () { $response = ispmanagerRequest('get', [ 'func' => 'webdomain', ]); $domainList = []; if (isset($response['doc']['elem']) && count($response['doc']['elem']) > 0) { foreach ($response['doc']['elem'] as $domain) { $domainList[] = $domain['name']['$']; } } add('ispmanager_domains', $domainList); }); desc('Creates new database'); task('ispmanager:db-create', function () { $config = get('ispmanager'); if (is_null($config['createDatabase'])) { warning('Action for database create is not active'); return; } $dsnData = parse_url($config['createDatabase']['dsn']); $dbInfo = get('ispmanager_databases'); $hostInfo = null; foreach ($dbInfo['hosts'] as $hostData) { if ($hostData['host'] == $dsnData['host'] . ':' . $dsnData['port']) { $hostInfo = $hostData; break; } } if (is_null($hostInfo)) { throw new Exception('Incorrect DB host'); } $dbName = substr($dsnData['path'], 1); $dbLocation = $dbName . '->' . $hostInfo['name']; if (isset($dbInfo['dblist'][$dbLocation])) { if (!isset($config['createDatabase']['skipIfExist']) || !$config['createDatabase']['skipIfExist']) { throw new Exception('Database already exists!'); } else { warning('Database already exists - skipping'); return; } } $dbCreateRequest = [ 'func' => 'db.edit', 'name' => $dbName, 'owner' => get('ispmanager_owner'), 'server' => $hostInfo['name'], 'charset' => $config['createDatabase']['charset'], 'sok' => 'ok', ]; if ($dsnData['user'] == '*') { $dbCreateRequest['user'] = '*'; $dbCreateRequest['username'] = $dbName; if ($dsnData['pass'] == '*') { $dbCreateRequest['password'] = generatePassword(8); } else { $dbCreateRequest['password'] = $dsnData['pass']; } } else { $dbCreateRequest['user'] = $dsnData['user']; } $response = ispmanagerRequest('post', $dbCreateRequest); if (isset($response['doc']['error']['msg']['$'])) { throw new Exception($response['doc']['error']['msg']['$']); } else { info('Database successfully created'); } }); desc('Deletes database'); task('ispmanager:db-delete', function () { $config = get('ispmanager'); if (is_null($config['deleteDatabase'])) { warning('Action for database delete is not active'); return; } $dbInfo = get('ispmanager_databases'); $dsnData = parse_url($config['deleteDatabase']['dsn']); $hostInfo = null; foreach ($dbInfo['hosts'] as $hostData) { if ($hostData['host'] == $dsnData['host'] . ':' . $dsnData['port']) { $hostInfo = $hostData; break; } } if (is_null($hostInfo)) { throw new Exception('Incorrect DB host'); } $dbName = substr($dsnData['path'], 1); $dbLocation = $dbName . '->' . $hostInfo['name']; if (!isset($dbInfo['dblist'][$dbLocation])) { if (!isset($config['deleteDatabase']['skipIfNotExist']) || !$config['deleteDatabase']['skipIfNotExist']) { throw new Exception('Database not exist!'); } else { warning('Database not exist - skipping'); return; } } $dbDeleteRequest = [ 'func' => 'db.delete', 'elid' => $dbLocation, ]; $response = ispmanagerRequest('post', $dbDeleteRequest); if (isset($response['doc']['error']['msg']['$'])) { throw new Exception($response['doc']['error']['msg']['$']); } else { info('Database successfully deleted'); } }); desc('Creates new domain'); task('ispmanager:domain-create', function () { $config = get('ispmanager'); if (is_null($config['createDomain'])) { warning('Action for domain create is not active'); return; } if (!isset($config['createDomain']['name']) || $config['createDomain']['name'] == '') { throw new Exception('Invalid domain name!'); } // Check domain exists $existDomains = get('ispmanager_domains'); if (in_array($config['createDomain']['name'], $existDomains)) { if (!isset($config['createDomain']['skipIfExist']) || !$config['createDomain']['skipIfExist']) { throw new Exception('Domain already exists!'); } else { warning('Domain already exists - skipping'); return; } } // Build vhost create request $vhostTemplate = get('vhost'); $domainCreateRequest = [ 'func' => 'webdomain.edit', 'sok' => 'ok', ]; foreach ($vhostTemplate as $key => $value) { $domainCreateRequest[$key] = str_replace('{{domain}}', $config['createDomain']['name'], $vhostTemplate[$key]); } $response = ispmanagerRequest('post', $domainCreateRequest); if (isset($response['doc']['error']['msg']['$'])) { throw new Exception($response['doc']['error']['msg']['$']); } else { info('Domain successfully created'); } }); desc('Gets allowed PHP modes and versions'); task('ispmanager:get-php-list', function () { // Get www-root settings for fpm version $response = ispmanagerRequest('get', [ 'func' => 'user.edit', 'elid' => get('ispmanager_owner'), 'elname' => get('ispmanager_owner'), ]); $userFPMVersion = $response['doc']['limit_php_fpm_version']['$'] ?? null; $response = ispmanagerRequest('get', [ 'func' => 'phpversions', ]); $versions = []; foreach ($response['doc']['elem'] as $phpVersion) { $versions[$phpVersion['key']['$']] = [ 'name' => $phpVersion['name']['$'], 'php_mode_mod' => false, 'php_mode_cgi' => false, 'php_mode_fcgi_apache' => false, 'php_mode_fcgi_nginxfpm' => false, ]; if (isset($phpVersion['default_apache']) && $phpVersion['default_apache']['$'] == 'on') { $versions[$phpVersion['key']['$']]['php_mode_mod'] = true; } if (isset($phpVersion['cgi']) && $phpVersion['cgi']['$'] == 'on') { $versions[$phpVersion['key']['$']]['php_mode_cgi'] = true; } if (isset($phpVersion['apache']) && $phpVersion['apache']['$'] == 'on') { $versions[$phpVersion['key']['$']]['php_mode_fcgi_apache'] = true; } if (isset($phpVersion['fpm']) && $phpVersion['fpm']['$'] == 'on' && $phpVersion['key']['$'] == $userFPMVersion) { $versions[$phpVersion['key']['$']]['php_mode_fcgi_nginxfpm'] = true; } } add('ispmanager_phplist', $versions); }); desc('Prints allowed PHP modes and versions'); task('ispmanager:print-php-list', function () { invoke('ispmanager:get-php-list'); $versions = get('ispmanager_phplist'); writeln("PHP versions: "); writeln(str_repeat('*', 32)); foreach ($versions as $versionKey => $versionData) { writeln('PHP ' . $versionData['name'] . ' (ID: ' . $versionKey . ')'); writeln(str_repeat('*', 32)); if (!$versionData['php_mode_mod']) { writeln('Apache module support (php_mode_mod) - NO'); } else { writeln('Apache module support (php_mode_mod) - YES'); } if (!$versionData['php_mode_cgi']) { writeln('CGI support (php_mode_cgi) - NO'); } else { writeln('CGI support (php_mode_cgi) - YES'); } if (!$versionData['php_mode_fcgi_apache']) { writeln('Apache fast-cgi support (php_mode_fcgi_apache) - NO'); } else { writeln('Apache fast-cgi support (php_mode_fcgi_apache) - YES'); } if (!$versionData['php_mode_fcgi_nginxfpm']) { writeln('nginx fast-cgi support (php_mode_fcgi_nginxfpm) - NO'); } else { writeln('nginx fast-cgi support (php_mode_fcgi_nginxfpm) - YES'); } writeln(str_repeat('*', 32)); } }); desc('Switches PHP version for domain'); task('ispmanager:domain-php-select', function () { $config = get('ispmanager'); if (is_null($config['phpSelect'])) { warning('Action for domain update is not active'); return; } if (!isset($config['phpSelect']['name']) || $config['phpSelect']['name'] == '') { throw new Exception('Invalid domain name!'); } $existDomains = get('ispmanager_domains'); if (!in_array($config['phpSelect']['name'], $existDomains)) { throw new Exception('Domain not exist!'); } if (!isset($config['phpSelect']['mode']) || !isset($config['phpSelect']['version'])) { throw new Exception('Incorrect settings for select php version'); } $phpVersions = get('ispmanager_phplist'); $newVersion = $config['phpSelect']['version']; $newMode = $config['phpSelect']['mode']; if (!isset($phpVersions[$newVersion])) { throw new Exception('Incorrect php version'); } $versionData = $phpVersions[$newVersion]; if (!isset($versionData[$newMode]) || !$versionData[$newMode]) { throw new Exception('Incorrect php mode'); } $domainUpdateRequest = [ 'func' => 'webdomain.edit', 'elid' => $config['phpSelect']['name'], 'name' => $config['phpSelect']['name'], 'php_mode' => $newMode, 'sok' => 'ok', ]; if ($newMode == 'php_mode_mod') { $domainUpdateRequest['php_apache_version'] = $newVersion; } elseif ($newMode == 'php_mode_cgi') { $domainUpdateRequest['php_cgi_version'] = $newVersion; } elseif ($newMode == 'php_mode_fcgi_apache') { $domainUpdateRequest['php_cgi_version'] = $newVersion; $domainUpdateRequest['php_apache_version'] = $newVersion; } elseif ($newMode == 'php_mode_fcgi_nginxfpm') { $domainUpdateRequest['php_cgi_version'] = $newVersion; $domainUpdateRequest['php_fpm_version'] = $newVersion; } else { throw new Exception('Unknown PHP mode!'); } $response = ispmanagerRequest('post', $domainUpdateRequest); if (isset($response['doc']['error']['msg']['$'])) { throw new Exception($response['doc']['error']['msg']['$']); } else { info('PHP successfully selected'); } }); desc('Creates new domain alias'); task('ispmanager:domain-alias-create', function () { $config = get('ispmanager'); if (is_null($config['createAlias'])) { warning('Action for alias create is not active'); return; } if (!isset($config['createAlias']['name']) || $config['createAlias']['name'] == '') { throw new Exception('Invalid domain name!'); } $existDomains = get('ispmanager_domains'); if (!in_array($config['createAlias']['name'], $existDomains)) { throw new Exception('Domain not exist!'); } if (!isset($config['createAlias']['alias']) || $config['createAlias']['alias'] == '') { throw new Exception('Invalid alias name!'); } // Get current domain data $response = ispmanagerRequest('get', [ 'func' => 'webdomain.edit', 'elid' => $config['createAlias']['name'], 'elname' => $config['createAlias']['name'], ]); $existAliases = []; if (isset($response['doc']['aliases']['$'])) { $existAliases = explode(' ', $response['doc']['aliases']['$']); } $newAliasList = []; $createAliasList = explode(' ', $config['createAlias']['alias']); foreach ($createAliasList as $createAlias) { if (in_array($createAlias, $existAliases)) { if (!isset($config['createAlias']['skipIfExist']) || !$config['createAlias']['skipIfExist']) { throw new Exception('Alias already exists!'); } else { warning('Alias ' . $createAlias . ' already exists - skipping'); continue; } } $newAliasList[] = $createAlias; } $saveAliases = array_merge($existAliases, $newAliasList); $domainUpdateRequest = [ 'func' => 'webdomain.edit', 'elid' => $config['createAlias']['name'], 'name' => $config['createAlias']['name'], 'aliases' => implode(' ', $saveAliases), 'sok' => 'ok', ]; $response = ispmanagerRequest('post', $domainUpdateRequest); if (isset($response['doc']['error']['msg']['$'])) { throw new Exception($response['doc']['error']['msg']['$']); } else { info('Alias successfully created'); } }); desc('Deletes domain alias'); task('ispmanager:domain-alias-delete', function () { $config = get('ispmanager'); if (is_null($config['deleteAlias'])) { warning('Action for alias create is not active'); return; } if (!isset($config['deleteAlias']['name']) || $config['deleteAlias']['name'] == '') { throw new Exception('Invalid domain name!'); } $existDomains = get('ispmanager_domains'); if (!in_array($config['deleteAlias']['name'], $existDomains)) { throw new Exception('Domain not exist!'); } if (!isset($config['deleteAlias']['alias']) || $config['deleteAlias']['alias'] == '') { throw new Exception('Invalid alias name!'); } // Get current domain data $response = ispmanagerRequest('get', [ 'func' => 'webdomain.edit', 'elid' => $config['createAlias']['name'], 'elname' => $config['createAlias']['name'], ]); $existAliases = []; if (isset($response['doc']['aliases']['$'])) { $existAliases = explode(' ', $response['doc']['aliases']['$']); } $deleteAliasList = explode(' ', $config['deleteAlias']['alias']); foreach ($deleteAliasList as $deleteAlias) { if (!in_array($deleteAlias, $existAliases)) { if (!isset($config['deleteAlias']['skipIfNotExist']) || !$config['deleteAlias']['skipIfNotExist']) { throw new Exception('Alias not exist!'); } else { warning('Alias ' . $deleteAlias . ' not exist - skipping'); continue; } } if (($index = array_search($deleteAlias, $existAliases)) !== false) { unset($existAliases[$index]); } } $domainUpdateRequest = [ 'func' => 'webdomain.edit', 'elid' => $config['deleteAlias']['name'], 'name' => $config['deleteAlias']['name'], 'aliases' => implode(' ', $existAliases), 'sok' => 'ok', ]; $response = ispmanagerRequest('post', $domainUpdateRequest); if (isset($response['doc']['error']['msg']['$'])) { throw new Exception($response['doc']['error']['msg']['$']); } else { info('Alias successfully deleted'); } }); desc('Deletes domain'); task('ispmanager:domain-delete', function () { $config = get('ispmanager'); if (is_null($config['deleteDomain'])) { warning('Action for domain delete is not active'); return; } if (!isset($config['deleteDomain']['name']) || $config['deleteDomain']['name'] == '') { throw new Exception('Invalid domain name!'); } // Check domain exists $existDomains = get('ispmanager_domains'); if (!in_array($config['deleteDomain']['name'], $existDomains)) { if (!isset($config['deleteDomain']['skipIfNotExist']) || !$config['deleteDomain']['skipIfNotExist']) { throw new Exception('Domain not exist!'); } else { warning('Domain not exist - skipping'); return; } } // Build request $domainDeleteRequest = [ 'func' => 'webdomain.delete.confirm', 'elid' => $config['deleteDomain']['name'], 'sok' => 'ok', ]; if (!isset($config['deleteDomain']['removeDir']) || !$config['deleteDomain']['removeDir']) { $domainDeleteRequest['remove_directory'] = 'off'; } else { $domainDeleteRequest['remove_directory'] = 'on'; } $response = ispmanagerRequest('post', $domainDeleteRequest); if (isset($response['doc']['error']['msg']['$'])) { throw new Exception($response['doc']['error']['msg']['$']); } else { info('Domain successfully deleted'); } }); desc('Auto task processing'); task('ispmanager:process', function () { $config = get('ispmanager'); if (!is_null($config['createDatabase'])) { invoke('ispmanager:db-create'); } if (!is_null($config['deleteDatabase'])) { invoke('ispmanager:db-delete'); } if (!is_null($config['createDomain'])) { invoke('ispmanager:domain-create'); } if (!is_null($config['deleteDomain'])) { invoke('ispmanager:domain-delete'); } if (!is_null($config['phpSelect'])) { invoke('ispmanager:domain-php-select'); } if (!is_null($config['createAlias'])) { invoke('ispmanager:domain-alias-create'); } if (!is_null($config['deleteAlias'])) { invoke('ispmanager:domain-alias-delete'); } }); function ispmanagerRequest($method, $requestData) { $config = get('ispmanager'); $dsnData = parse_url($config['api']['dsn']); $requestUrl = $dsnData['scheme'] . '://' . $dsnData['host'] . ':' . $dsnData['port'] . $dsnData['path']; if ($config['api']['secure'] && get('ispmanager_session') == '') { ispmanagerAuthRequest($requestUrl, $dsnData['user'], $dsnData['pass']); } if ($method == 'post') { return Httpie::post($requestUrl) ->formBody(prepareRequest($requestData)) ->setopt(CURLOPT_SSL_VERIFYPEER, false) ->setopt(CURLOPT_SSL_VERIFYHOST, false) ->getJson(); } elseif ($method == 'get') { return Httpie::get($requestUrl) ->query(prepareRequest($requestData)) ->setopt(CURLOPT_SSL_VERIFYPEER, false) ->setopt(CURLOPT_SSL_VERIFYHOST, false) ->getJson(); } else { throw new Exception('Unknown request method'); } } function ispmanagerAuthRequest($url, $login, $pass) { $authRequestData = [ 'func' => 'auth', 'username' => $login, 'password' => $pass, ]; $responseData = Httpie::post($url) ->setopt(CURLOPT_SSL_VERIFYPEER, false) ->setopt(CURLOPT_SSL_VERIFYHOST, false) ->formBody(prepareRequest($authRequestData)) ->getJson(); if (isset($responseData['doc']['auth']['$id'])) { set('ispmanager_session', $responseData['doc']['auth']['$id']); } else { throw new Exception('Error while create auth session'); } } function prepareRequest($requestData) { $config = get('ispmanager'); $dsnData = parse_url($config['api']['dsn']); if (!isset($requestData['out'])) { $requestData['out'] = 'json'; } if (!$config['api']['secure']) { $requestData['authinfo'] = $dsnData['user'] . ':' . $dsnData['pass']; } else { if (get('ispmanager_session') != '') { $requestData['auth'] = get('ispmanager_session'); } } return $requestData; } function generatePassword($lenght) { return substr(md5(uniqid()), 0, $lenght); } // Callbacks before actions under domains before('ispmanager:domain-alias-create', 'ispmanager:init'); before('ispmanager:domain-alias-delete', 'ispmanager:init'); before('ispmanager:domain-create', 'ispmanager:init'); before('ispmanager:domain-delete', 'ispmanager:init'); before('ispmanager:domain-php-select', 'ispmanager:init'); // Callbacks before actions under databases before('ispmanager:db-create', 'ispmanager:init'); before('ispmanager:db-delete', 'ispmanager:init'); ================================================ FILE: contrib/mattermost.php ================================================ get('mattermost_text'), 'username' => get('mattermost_username'), ]; if (get('mattermost_channel')) { $body['channel'] = get('mattermost_channel'); } if (get('mattermost_icon_url')) { $body['icon_url'] = get('mattermost_icon_url'); } Httpie::post(get('mattermost_webhook'))->jsonBody($body)->send(); }); desc('Notifies mattermost about deploy finish'); task('mattermost:notify:success', function () { if (null === get('mattermost_webhook')) { return; } $body = [ 'text' => get('mattermost_success_text'), 'username' => get('mattermost_username'), ]; if (get('mattermost_channel')) { $body['channel'] = get('mattermost_channel'); } if (get('mattermost_icon_url')) { $body['icon_url'] = get('mattermost_icon_url'); } Httpie::post(get('mattermost_webhook'))->jsonBody($body)->send(); }); desc('Notifies mattermost about deploy failure'); task('mattermost:notify:failure', function () { if (null === get('mattermost_webhook')) { return; } $body = [ 'text' => get('mattermost_failure_text'), 'username' => get('mattermost_username'), ]; if (get('mattermost_channel')) { $body['channel'] = get('mattermost_channel'); } if (get('mattermost_icon_url')) { $body['icon_url'] = get('mattermost_icon_url'); } Httpie::post(get('mattermost_webhook'))->jsonBody($body)->send(); }); ================================================ FILE: contrib/ms-teams.php ================================================ jsonBody([ "themeColor" => get('teams_color'), 'text' => get('teams_text'), ])->send(); } catch (\Exception $e) { if (get('teams_failure_continue', false)) { warning('Error sending Teams Notification: ' . $e->getMessage()); } else { throw $e; } } }) ->once() ->hidden(); desc('Notifies Teams about deploy finish'); task('teams:notify:success', function () { if (!get('teams_webhook', false)) { warning('No MS Teams webhook configured'); return; } try { Httpie::post(get('teams_webhook'))->jsonBody([ "themeColor" => get('teams_success_color'), 'text' => get('teams_success_text'), ])->send(); } catch (\Exception $e) { if (get('teams_failure_continue', false)) { warning('Error sending Teams Notification: ' . $e->getMessage()); } else { throw $e; } } }) ->once() ->hidden(); desc('Notifies Teams about deploy failure'); task('teams:notify:failure', function () { if (!get('teams_webhook', false)) { warning('No MS Teams webhook configured'); return; } try { Httpie::post(get('teams_webhook'))->jsonBody([ "themeColor" => get('teams_failure_color'), 'text' => get('teams_failure_text'), ])->send(); } catch (\Exception $e) { if (get('teams_failure_continue', false)) { warning('Error sending Teams Notification: ' . $e->getMessage()); } else { throw $e; } } }) ->once() ->hidden(); ================================================ FILE: contrib/newrelic.php ================================================ get('user'), 'revision' => get('newrelic_revision'), 'description' => get('newrelic_description'), ]; Httpie::post("https://$endpoint/v2/applications/$appId/deployments.json") ->header("X-Api-Key", $apiKey) ->query(['deployment' => $data]) ->send(); } }) ->once() ->hidden(); ================================================ FILE: contrib/npm.php ================================================ jsonBody([ "topic" => get('ntfy_topic'), "title" => get('ntfy_title'), "message" => get('ntfy_text'), "tags" => explode(",", get('ntfy_tags')), "priority" => get('ntfy_priority'), ])->send(); }) ->once() ->hidden(); desc('Notifies ntfy server about deploy finish'); task('ntfy:notify:success', function () { if (!get('ntfy_topic', false)) { warning('No ntfy topic configured'); return; } Httpie::post(get('ntfy_server'))->jsonBody([ "topic" => get('ntfy_topic'), "title" => get('ntfy_title'), "message" => get('ntfy_success_text'), "tags" => explode(",", get('ntfy_success_tags')), "priority" => get('ntfy_success_priority'), ])->send(); }) ->once() ->hidden(); desc('Notifies ntfy server about deploy failure'); task('ntfy:notify:failure', function () { if (!get('ntfy_topic', false)) { warning('No ntfy topic configured'); return; } Httpie::post(get('ntfy_server'))->jsonBody([ "topic" => get('ntfy_topic'), "title" => get('ntfy_title'), "message" => get('ntfy_failure_text'), "tags" => explode(",", get('ntfy_failure_tags')), "priority" => get('ntfy_failure_priority'), ])->send(); }) ->once() ->hidden(); ================================================ FILE: contrib/phinx.php ================================================ 'development', 'configuration' => './migration/.phinx.yml', 'target' => '20120103083322', 'remove-all' => '', ]; set('phinx_path', '/usr/local/phinx/bin/phinx'); set('phinx', $phinx_env_vars); after('cleanup', 'phinx:migrate'); // or set it for a specific server host('dev') ->user('user') ->set('deploy_path', '/var/www') ->set('phinx', $phinx_env_vars) ->set('phinx_path', ''); ``` ## Suggested Usage You can run all tasks before or after any tasks (but you need to specify external configs for phinx). If you use internal configs (which are in your project) you need to run it after the `deploy:update_code` task is completed. ## Read more For further reading see [phinx.org](https://phinx.org). Complete descriptions of all possible options can be found on the [commands page](http://docs.phinx.org/en/latest/commands.html). */ namespace Deployer; use Deployer\Exception\RunException; /* * Phinx recipe for Deployer * * @author Alexey Boyko * @contributor Security-Database * @copyright 2016 Alexey Boyko * @license MIT https://github.com/deployphp/recipes/blob/master/LICENSE * * @link https://github.com/deployphp/recipes * * @see http://deployer.org * @see https://phinx.org */ /** * Path to Phinx */ set('bin/phinx', function () { try { $phinxPath = which('phinx'); } catch (RunException $e) { $phinxPath = null; } if ($phinxPath !== null) { return "phinx"; } elseif (test('[ -f {{release_path}}/vendor/bin/phinx ]')) { return "{{release_path}}/vendor/bin/phinx"; } elseif (test('[ -f ~/.composer/vendor/bin/phinx ]')) { return '~/.composer/vendor/bin/phinx'; } else { throw new \RuntimeException('Cannot find phinx. Please specify path to phinx manually'); } }); /** * Make Phinx command * * @param string $cmdName Name of command * @param array $conf Command options(config) * * @return string Phinx command to execute */ function phinx_get_cmd($cmdName, $conf) { $phinx = get('phinx_path') ?: get('bin/phinx'); $phinxCmd = "$phinx $cmdName"; $options = ''; foreach ($conf as $name => $value) { $options .= " --$name $value"; } $phinxCmd .= $options; return $phinxCmd; } /** * Returns options array that allowed for command * * @param array $allowedOptions List of allowed options * * @return array Array of options */ function phinx_get_allowed_config($allowedOptions) { $opts = []; try { foreach (get('phinx') as $key => $val) { if (in_array($key, $allowedOptions)) { $opts[$key] = $val; } } } catch (\RuntimeException $e) { } return $opts; } desc('Migrats database with phinx'); task('phinx:migrate', function () { $ALLOWED_OPTIONS = [ 'configuration', 'date', 'environment', 'target', 'parser', ]; $conf = phinx_get_allowed_config($ALLOWED_OPTIONS); cd('{{release_path}}'); $phinxCmd = phinx_get_cmd('migrate', $conf); run($phinxCmd); cd('{{deploy_path}}'); }); desc('Rollbacks database migrations with phinx'); task('phinx:rollback', function () { $ALLOWED_OPTIONS = [ 'configuration', 'date', 'environment', 'target', 'parser', ]; $conf = phinx_get_allowed_config($ALLOWED_OPTIONS); cd('{{release_path}}'); $phinxCmd = phinx_get_cmd('rollback', $conf); run($phinxCmd); cd('{{deploy_path}}'); }); desc('Seeds database with phinx'); task('phinx:seed', function () { $ALLOWED_OPTIONS = [ 'configuration', 'environment', 'parser', 'seed', ]; $conf = phinx_get_allowed_config($ALLOWED_OPTIONS); cd('{{release_path}}'); $phinxCmd = phinx_get_cmd('seed:run', $conf); run($phinxCmd); cd('{{deploy_path}}'); }); desc('Sets a migrations breakpoint with phinx'); task('phinx:breakpoint', function () { $ALLOWED_OPTIONS = [ 'configuration', 'environment', 'remove-all', 'target', ]; $conf = phinx_get_allowed_config($ALLOWED_OPTIONS); cd('{{release_path}}'); $phinxCmd = phinx_get_cmd('breakpoint', $conf); run($phinxCmd); cd('{{deploy_path}}'); }); ================================================ FILE: contrib/php-fpm.php ================================================ 'localhost', 'port' => '5672', 'username' => 'guest', 'password' => 'guest', 'channel' => 'notify-channel', 'vhost' => '/my-app' ]); ``` ### Suggested Usage Since you should only notify RabbitMQ channel of a successful deployment, the `deploy:rabbit` task should be executed right at the end. ```php // deploy.php before('deploy:end', 'deploy:rabbit'); ``` */ namespace Deployer; use Deployer\Task\Context; use PhpAmqpLib\Connection\AMQPConnection; use PhpAmqpLib\Message\AMQPMessage; desc('Notifies RabbitMQ channel about deployment'); task('deploy:rabbit', function () { if (!class_exists('PhpAmqpLib\Connection\AMQPConnection')) { throw new \RuntimeException("Please install php package videlalvaro/php-amqplib to use rabbitmq"); } $config = get('rabbit', []); if (!isset($config['message'])) { $releasePath = get('release_path'); $host = Context::get()->getHost(); $stage = get('stage', false); $stageInfo = ($stage) ? sprintf(' on *%s*', $stage) : ''; $message = "Deployment to '%s'%s was successful\n(%s)"; $config['message'] = sprintf( $message, $host->getHostname(), $stageInfo, $releasePath, ); } $defaultConfig = [ 'host' => 'localhost', 'port' => 5672, 'username' => 'guest', 'password' => 'guest', 'vhost' => '/', ]; $config = array_merge($defaultConfig, $config); if (!is_array($config) || !isset($config['channel']) || !isset($config['host']) || !isset($config['port']) || !isset($config['username']) || !isset($config['password']) || !isset($config['vhost'])) { throw new \RuntimeException("Please configure rabbit config: set('rabbit', array('channel' => 'channel', 'host' => 'host', 'port' => 'port', 'username' => 'username', 'password' => 'password'));"); } $connection = new AMQPConnection($config['host'], $config['port'], $config['username'], $config['password'], $config['vhost']); $channel = $connection->channel(); $msg = new AMQPMessage($config['message']); $channel->basic_publish($msg, $config['channel'], $config['channel']); $channel->close(); $connection->close(); }); ================================================ FILE: contrib/raygun.php ================================================ get('raygun_api_key'), 'version' => get('raygun_version'), 'ownerName' => get('raygun_owner_name'), 'emailAddress' => get('raygun_email'), 'comment' => get('raygun_comment'), 'scmIdentifier' => get('raygun_scm_identifier'), 'scmType' => get('raygun_scm_type'), ]; Httpie::post('https://app.raygun.io/deployments') ->jsonBody($data) ->send(); }); ================================================ FILE: contrib/rocketchat.php ================================================ get('rockchat_title'), 'username' => get('rocketchat_username'), 'attachments' => [[ 'text' => get('rocketchat_text'), 'color' => get('rocketchat_color'), ]], ]; if (get('rocketchat_channel')) { $body['channel'] = get('rocketchat_channel'); } if (get('rocketchat_room_id')) { $body['roomId'] = get('rocketchat_room_id'); } if (get('rocketchat_icon_url')) { $body['avatar'] = get('rocketchat_icon_url'); } elseif (get('rocketchat_icon_emoji')) { $body['emoji'] = get('rocketchat_icon_emoji'); } Httpie::post(get('rocketchat_webhook'))->jsonBody($body)->send(); }); desc('Notifies RocketChat about deploy finish'); task('rocketchat:notify:success', function () { if (null === get('rocketchat_webhook')) { return; } $body = [ 'text' => get('rockchat_title'), 'username' => get('rocketchat_username'), 'attachments' => [[ 'text' => get('rocketchat_success_text'), 'color' => get('rocketchat_success_color'), ]], ]; if (get('rocketchat_channel')) { $body['channel'] = get('rocketchat_channel'); } if (get('rocketchat_room_id')) { $body['roomId'] = get('rocketchat_room_id'); } if (get('rocketchat_icon_url')) { $body['avatar'] = get('rocketchat_icon_url'); } elseif (get('rocketchat_icon_emoji')) { $body['emoji'] = get('rocketchat_icon_emoji'); } Httpie::post(get('rocketchat_webhook'))->jsonBody($body)->send(); }); desc('Notifies RocketChat about deploy failure'); task('rocketchat:notify:failure', function () { if (null === get('rocketchat_webhook')) { return; } $body = [ 'text' => get('rockchat_title'), 'username' => get('rocketchat_username'), 'attachments' => [[ 'color' => get('rocketchat_failure_color'), 'text' => get('rocketchat_failure_text'), ]], ]; if (get('rocketchat_channel')) { $body['channel'] = get('rocketchat_channel'); } if (get('rocketchat_room_id')) { $body['roomId'] = get('rocketchat_room_id'); } if (get('rocketchat_icon_url')) { $body['avatar'] = get('rocketchat_icon_url'); } elseif (get('rocketchat_icon_emoji')) { $body['emoji'] = get('rocketchat_icon_emoji'); } Httpie::post(get('rocketchat_webhook'))->jsonBody($body)->send(); }); ================================================ FILE: contrib/rollbar.php ================================================ get('rollbar_token'), 'environment' => get('where'), 'revision' => runLocally('git log -n 1 --format="%h"'), 'local_username' => get('user'), 'rollbar_username' => get('rollbar_username'), 'comment' => get('rollbar_comment'), ]; Httpie::post('https://api.rollbar.com/api/1/deploy/') ->formBody($params) ->send(); }) ->once(); ================================================ FILE: contrib/rsync.php ================================================ [ '.git', 'deploy.php', ], 'exclude-file' => false, 'include' => [], 'include-file' => false, 'filter' => [], 'filter-file' => false, 'filter-perdir'=> false, 'flags' => 'rz', // Recursive, with compress 'options' => ['delete'], 'timeout' => 60, ]); ``` If You have multiple excludes, You can put them in file and reference that instead. If You use `deploy:rsync_warmup` You could set additional options that could speed-up and/or affect way things are working. For example: ```php // deploy.php set('rsync',[ 'exclude' => ['excludes_file'], 'exclude-file' => '/tmp/localdeploys/excludes_file', //Use absolute path to avoid possible rsync problems 'include' => [], 'include-file' => false, 'filter' => [], 'filter-file' => false, 'filter-perdir' => false, 'flags' => 'rzcE', // Recursive, with compress, check based on checksum rather than time/size, preserve Executable flag 'options' => ['delete', 'delete-after', 'force'], //Delete after successful transfer, delete even if deleted dir is not empty 'timeout' => 3600, //for those huge repos or crappy connection ]); ``` ### Parameter - **rsync_src**: per-host rsync source. This can be server, stage or whatever-dependent. By default it's set to current directory - **rsync_dest**: per-host rsync destination. This can be server, stage or whatever-dependent. by default it's equivalent to release deploy destination. ### Sample configurations: This is default configuration: ```php set('rsync_src', __DIR__); set('rsync_dest','{{release_path}}'); ``` If You use local deploy recipe You can set src to local release: ```php host('hostname') ->hostname('10.10.10.10') ->port(22) ->set('deploy_path','/your/remote/path/app') ->set('rsync_src', '/your/local/path/app') ->set('rsync_dest','{{release_path}}'); ``` ## Usage - `rsync` task Set `rsync_src` to locally cloned repository and rsync to `rsync_dest`. Then set this task instead of `deploy:update_code` in Your `deploy` task if Your hosting provider does not allow git. - `rsync:warmup` task If Your deploy task looks like: ```php task('deploy', [ 'deploy:prepare', 'deploy:release', 'rsync', 'deploy:vendors', 'deploy:symlink', ])->desc('Deploy your project'); ``` And Your `rsync_dest` is set to `{{release_path}}` then You could add this task to run before `rsync` task or after `deploy:release`, whatever is more convenient. */ namespace Deployer; use Deployer\Host\Localhost; use Deployer\Task\Context; set('rsync', [ 'exclude' => [ '.git', 'deploy.php', ], 'exclude-file' => false, 'include' => [], 'include-file' => false, 'filter' => [], 'filter-file' => false, 'filter-perdir' => false, 'flags' => 'rz', 'options' => ['delete'], 'timeout' => 300, ]); set('rsync_src', __DIR__); set('rsync_dest', '{{release_path}}'); set('rsync_excludes', function () { $config = get('rsync'); $excludes = $config['exclude']; $excludeFile = $config['exclude-file']; $excludesRsync = ''; foreach ($excludes as $exclude) { $excludesRsync .= ' --exclude=' . escapeshellarg($exclude); } if (!empty($excludeFile) && file_exists($excludeFile) && is_file($excludeFile) && is_readable($excludeFile)) { $excludesRsync .= ' --exclude-from=' . escapeshellarg($excludeFile); } return $excludesRsync; }); set('rsync_includes', function () { $config = get('rsync'); $includes = $config['include']; $includeFile = $config['include-file']; $includesRsync = ''; foreach ($includes as $include) { $includesRsync .= ' --include=' . escapeshellarg($include); } if (!empty($includeFile) && file_exists($includeFile) && is_file($includeFile) && is_readable($includeFile)) { $includesRsync .= ' --include-from=' . escapeshellarg($includeFile); } return $includesRsync; }); set('rsync_filter', function () { $config = get('rsync'); $filters = $config['filter']; $filterFile = $config['filter-file']; $filterPerDir = $config['filter-perdir']; $filtersRsync = ''; foreach ($filters as $filter) { $filtersRsync .= " --filter='$filter'"; } if (!empty($filterFile)) { $filtersRsync .= " --filter='merge $filterFile'"; } if (!empty($filterPerDir)) { $filtersRsync .= " --filter='dir-merge $filterPerDir'"; } return $filtersRsync; }); set('rsync_options', function () { $config = get('rsync'); $options = $config['options']; $optionsRsync = []; foreach ($options as $option) { $optionsRsync[] = "--$option"; } return implode(' ', $optionsRsync); }); desc('Warmups remote Rsync target'); task('rsync:warmup', function () { $config = get('rsync'); $source = "{{current_path}}"; $destination = "{{deploy_path}}/release"; if (test("[ -d $(echo $source) ]")) { run("rsync -{$config['flags']} {{rsync_options}}{{rsync_excludes}}{{rsync_includes}}{{rsync_filter}} $source/ $destination/"); } else { writeln("No way to warmup rsync."); } }); desc('Rsync local->remote'); task('rsync', function () { $config = get('rsync'); $src = get('rsync_src'); while (is_callable($src)) { $src = $src(); } if (!trim($src)) { // if $src is not set here rsync is going to do a directory listing // exiting with code 0, since only doing a directory listing clearly // is not what we want to achieve we need to throw an exception throw new \RuntimeException('You need to specify a source path.'); } $dst = get('rsync_dest'); while (is_callable($dst)) { $dst = $dst(); } if (!trim($dst)) { // if $dst is not set here we are going to sync to root // and even worse - depending on rsync flags and permission - // might end up deleting everything we have write permission to throw new \RuntimeException('You need to specify a destination path.'); } $rsyncFlags = (is_string($config['flags']) && trim($config['flags']) !== '') ? "-{$config['flags']}" : ''; $host = Context::get()->getHost(); if ($host instanceof Localhost) { runLocally("rsync {$rsyncFlags} {{rsync_options}}{{rsync_includes}}{{rsync_excludes}}{{rsync_filter}} '$src/' '$dst/'", timeout: $config['timeout']); return; } $sshArguments = $host->connectionOptionsString(); runLocally("rsync {$rsyncFlags} -e 'ssh $sshArguments' {{rsync_options}}{{rsync_includes}}{{rsync_excludes}}{{rsync_filter}} '$src/' '{$host->connectionString()}:$dst/'", timeout: $config['timeout']); }); ================================================ FILE: contrib/sentry.php ================================================ 'exampleorg', 'projects' => [ 'exampleproj' ], 'token' => 'd47828...', 'version' => '0.0.1', ]); ``` ### Suggested Usage Since you should only notify Sentry of a successful deployment, the deploy:sentry task should be executed right at the end. ```php // deploy.php after('deploy', 'deploy:sentry'); ``` */ namespace Deployer; use Closure; use DateTime; use Deployer\Exception\ConfigurationException; use Deployer\Utility\Httpie; desc('Notifies Sentry of deployment'); task( 'deploy:sentry', static function () { $now = date('c'); $defaultConfig = [ 'version' => getReleaseGitRef(), 'version_prefix' => null, 'refs' => [], 'ref' => null, 'commits' => getGitCommitsRefs(), 'url' => null, 'date_released' => $now, 'date_deploy_started' => $now, 'date_deploy_finished' => $now, 'sentry_server' => 'https://sentry.io', 'previous_commit' => null, 'environment' => get('symfony_env', 'prod'), 'deploy_name' => null, ]; $config = array_merge($defaultConfig, (array) get('sentry')); array_walk( $config, static function (&$value) use ($config) { if (is_callable($value)) { $value = $value($config); } }, ); if ( !isset($config['organization'], $config['token'], $config['version']) || (empty($config['projects']) || !is_array($config['projects'])) ) { throw new \RuntimeException( << 'exampleorg', 'projects' => [ 'exampleproj', 'exampleproje2' ], 'token' => 'd47828...', ] );" EXAMPLE, ); } $releaseData = array_filter( [ 'version' => ($config['version_prefix'] ?? '') . $config['version'], 'refs' => $config['refs'], 'ref' => $config['ref'], 'url' => $config['url'], 'commits' => array_slice($config['commits'] ?? [], 0), // reset keys to serialize as array in json 'dateReleased' => $config['date_released'], 'projects' => $config['projects'], 'previousCommit' => $config['previous_commit'], ], ); $releasesApiUrl = $config['sentry_server'] . '/api/0/organizations/' . $config['organization'] . '/releases/'; $response = Httpie::post( $releasesApiUrl, ) ->setopt(CURLOPT_TIMEOUT, 10) ->header('Authorization', sprintf('Bearer %s', $config['token'])) ->jsonBody($releaseData) ->getJson(); if (!isset($response['version'], $response['projects'])) { throw new \RuntimeException(sprintf('Unable to create a release: %s', print_r($response, true))); } writeln( sprintf( 'Sentry: Release of version %s ' . 'for projects: %s created successfully.', $response['version'], implode(', ', array_column($response['projects'], 'slug')), ), ); $deployData = array_filter( [ 'environment' => $config['environment'], 'name' => $config['deploy_name'], 'url' => $config['url'], 'dateStarted' => $config['date_deploy_started'], 'dateFinished' => $config['date_deploy_finished'], ], ); $response = Httpie::post( $releasesApiUrl . $response['version'] . '/deploys/', ) ->setopt(CURLOPT_TIMEOUT, 10) ->header('Authorization', sprintf('Bearer %s', $config['token'])) ->jsonBody($deployData) ->getJson(); if (!isset($response['id'], $response['environment'])) { throw new \RuntimeException(sprintf('Unable to create a deployment: %s', print_r($response, true))); } writeln( sprintf( 'Sentry: Deployment %s ' . 'for environment %s created successfully', $response['id'], $response['environment'], ), ); }, ); function getPreviousReleaseRevision() { switch (get('update_code_strategy')) { case 'local_archive': case 'archive': if (has('previous_release')) { return run('cat {{previous_release}}/REVISION'); } return null; case 'clone': if (has('previous_release')) { cd('{{previous_release}}'); return trim(run('git rev-parse HEAD')); } return null; default: throw new ConfigurationException(parse("Unknown `update_code_strategy` option: {{update_code_strategy}}.")); } } function getCurrentReleaseRevision() { switch (get('update_code_strategy')) { case 'local_archive': case 'archive': return run('cat {{release_path}}/REVISION'); case 'clone': cd('{{release_path}}'); return trim(run('git rev-parse HEAD')); default: throw new ConfigurationException(parse("Unknown `update_code_strategy` option: {{update_code_strategy}}.")); } } function getReleaseGitRef(): Closure { return static function ($config = []): string { $strategy = get('update_code_strategy'); if ($strategy === 'archive') { cd('{{deploy_path}}/.dep/repo'); } else { cd('{{release_path}}'); } if (isset($config['git_version_command'])) { return trim(run($config['git_version_command'])); } if ($strategy !== 'clone') { return run('cat {{current_path}}/REVISION'); } return trim(run('git log -n 1 --format="%h"')); }; } function getGitCommitsRefs(): Closure { return static function ($config = []): array { $previousReleaseRevision = getPreviousReleaseRevision(); $currentReleaseRevision = getCurrentReleaseRevision() ?: 'HEAD'; if ($previousReleaseRevision === null) { $commitRange = $currentReleaseRevision; } else { $commitRange = $previousReleaseRevision . '..' . $currentReleaseRevision; } try { if (get('update_code_strategy') === 'archive') { cd('{{deploy_path}}/.dep/repo'); } else { cd('{{release_path}}'); } $result = run(sprintf('git rev-list --pretty="%s" %s', 'format:%H#%an#%ae#%at#%s', $commitRange)); $lines = array_filter( // limit number of commits for first release with many commits array_map('trim', array_slice(explode("\n", $result), 0, 200)), static function (string $line): bool { return !empty($line) && strpos($line, 'commit') !== 0; }, ); return array_map( static function (string $line): array { [$ref, $authorName, $authorEmail, $timestamp, $message] = explode('#', $line, 5); return [ 'id' => $ref, 'author_name' => $authorName, 'author_email' => $authorEmail, 'message' => $message, 'timestamp' => date(\DateTime::ATOM, (int) $timestamp), ]; }, $lines, ); } catch (\Deployer\Exception\RunException $e) { writeln($e->getMessage()); return []; } }; } ================================================ FILE: contrib/slack.php ================================================ Add to Slack Add hook on deploy: ```php before('deploy', 'slack:notify'); ``` ## Configuration - `slack_webhook` – slack incoming webhook url, **required** ``` set('slack_webhook', 'https://hooks.slack.com/...'); ``` - `slack_channel` - channel to send notification to. The default is the channel configured in the webhook - `slack_title` – the title of application, default `{{application}}` - `slack_text` – notification message template, markdown supported ``` set('slack_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*'); ``` - `slack_success_text` – success template, default: ``` set('slack_success_text', 'Deploy to *{{where}}* successful'); ``` - `slack_failure_text` – failure template, default: ``` set('slack_failure_text', 'Deploy to *{{where}}* failed'); ``` - `slack_color` – color's attachment - `slack_success_color` – success color's attachment - `slack_failure_color` – failure color's attachment - `slack_fields` - set attachments fields for pretty output in Slack, default: ``` set('slack_fields', []); ``` ## Usage If you want to notify only about beginning of deployment add this line only: ```php before('deploy', 'slack:notify'); ``` If you want to notify about successful end of deployment add this too: ```php after('deploy:success', 'slack:notify:success'); ``` If you want to notify about failed deployment add this too: ```php after('deploy:failed', 'slack:notify:failure'); ``` */ namespace Deployer; use Deployer\Utility\Httpie; // Channel to publish to, when false the default channel the webhook will be used set('slack_channel', false); // Title of project set('slack_title', function () { return get('application', 'Project'); }); // Deploy message set('slack_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*'); set('slack_success_text', 'Deploy to *{{where}}* successful'); set('slack_failure_text', 'Deploy to *{{where}}* failed'); set('slack_rollback_text', '_{{user}}_ rolled back changes on *{{where}}*'); set('slack_fields', []); // Color of attachment set('slack_color', '#4d91f7'); set('slack_success_color', '#00c100'); set('slack_failure_color', '#ff0909'); set('slack_rollback_color', '#eba211'); function checkSlackAnswer($result) { if ('invalid_token' === $result) { warning('Invalid Slack token'); return false; } return true; } desc('Notifies Slack'); task('slack:notify', function () { if (!get('slack_webhook', false)) { warning('No Slack webhook configured'); return; } $attachment = [ 'title' => get('slack_title'), 'text' => get('slack_text'), 'color' => get('slack_color'), 'mrkdwn_in' => ['text'], ]; $result = Httpie::post(get('slack_webhook'))->jsonBody(['channel' => get('slack_channel'), 'attachments' => [$attachment]])->send(); checkSlackAnswer($result); }) ->once() ->hidden(); desc('Notifies Slack about deploy finish'); task('slack:notify:success', function () { if (!get('slack_webhook', false)) { warning('No Slack webhook configured'); return; } $attachment = [ 'title' => get('slack_title'), 'text' => get('slack_success_text'), 'color' => get('slack_success_color'), 'fields' => get('slack_fields'), 'mrkdwn_in' => ['text'], ]; $result = Httpie::post(get('slack_webhook'))->jsonBody(['channel' => get('slack_channel'), 'attachments' => [$attachment]])->send(); checkSlackAnswer($result); }) ->once() ->hidden(); desc('Notifies Slack about deploy failure'); task('slack:notify:failure', function () { if (!get('slack_webhook', false)) { warning('No Slack webhook configured'); return; } $attachment = [ 'title' => get('slack_title'), 'text' => get('slack_failure_text'), 'color' => get('slack_failure_color'), 'mrkdwn_in' => ['text'], ]; $result = Httpie::post(get('slack_webhook'))->jsonBody(['channel' => get('slack_channel'), 'attachments' => [$attachment]])->send(); checkSlackAnswer($result); }) ->once() ->hidden(); desc('Notifies Slack about rollback'); task('slack:notify:rollback', function () { if (!get('slack_webhook', false)) { warning('No Slack webhook configured'); return; } $attachment = [ 'title' => get('slack_title'), 'text' => get('slack_rollback_text'), 'color' => get('slack_rollback_color'), 'mrkdwn_in' => ['text'], ]; $result = Httpie::post(get('slack_webhook'))->jsonBody(['channel' => get('slack_channel'), 'attachments' => [$attachment]])->send(); checkSlackAnswer($result); }) ->once() ->hidden(); ================================================ FILE: contrib/supervisord-monitor.php ================================================ 'https://youruri.xyz/supervisor', 'basic_auth_user' => 'username', 'basic_auth_password' => 'password', 'process_name' => 'process01', ]); ``` or ``` set('supervisord_uri', 'https://youruri.xyz/supervisor'); set('supervisord_basic_auth_user', 'username'); set('supervisord_basic_auth_password', 'password'); set('supervisord_process_name', 'process01'); ``` - `supervisord` – array with configuration for Supervisord - `uri` – URI to the Supervisord monitor page - `basic_auth_user` – Basic auth username to access the URI - `basic_auth_password` – Basic auth password to access the URI - `process_name` – the process name, as visible in the Supervisord monitor page. Multiple processes can be listed here, comma separated ### Task - `supervisord-monitor:restart` Restarts given processes - `supervisord-monitor:stop` Stops given processes - `supervisord-monitor:start` Starts given processes ### Usage A complete example with configs, staging and deployment ``` 'https://youruri.xyz/supervisor', 'basic_auth_user' => 'username', 'basic_auth_password' => 'password', 'process_name' => 'process01', ]); host('staging.myproject.com') ->set('branch', 'develop') ->set('labels', ['stage' => 'staging']); host('myproject.com') ->set('branch', 'main') ->set('labels', ['stage' => 'production']); // Tasks task('build', function () { run('cd {{release_path}} && build'); }); task('deploy', [ 'build', 'supervisord', ]); task('supervisord', ['supervisord-monitor:restart']) ->select('stage=production'); ``` */ namespace Deployer; use Deployer\Utility\Httpie; function supervisordCheckConfig() { $config = get('supervisord', []); foreach ($config as $key => $value) { if ($value) { set('supervisord_' . $key, $value); } } if (!get('supervisord_uri') || !get('supervisord_basic_auth_user') || !get('supervisord_basic_auth_password') || !get('supervisord_process_name')) { throw new \RuntimeException("Please configure Supervisord config: set('supervisord', array('uri' => 'yourdomain.xyz/supervisor', 'basic_auth_user' => 'abc' , 'basic_auth_password' => 'xyz', 'process_name' => 'process01,process02')); or set('supervisord_uri', 'yourdomain.xyz/supervisor'); set('supervisord_basic_auth_user', 'abc'); etc"); } } function supervisordGetBasicAuthToken() { return 'Basic ' . base64_encode(get('supervisord_basic_auth_user') . ':' . get('supervisord_basic_auth_password')); } function supervisordIsAuthenticated() { supervisordCheckConfig(); $authResponseInfo = []; Httpie::post(get('supervisord_uri'))->header('Authorization', supervisordGetBasicAuthToken())->send($authResponseInfo); return $authResponseInfo['http_code'] === 200; } function supervisordControlAction($name, $action = 'stop') { $stopResponseInfo = []; Httpie::post(get('supervisord_uri') . '/control/' . $action . '/localhost/' . $name)->header('Authorization', supervisordGetBasicAuthToken())->send($stopResponseInfo); return $stopResponseInfo['http_code'] === 200; } task('supervisord-monitor:restart', function () { if (supervisordIsAuthenticated()) { $names = explode(',', get('supervisord_process_name')); foreach ($names as $name) { $name = trim($name); if (supervisordControlAction($name, 'stop')) { writeln('Daemon [' . $name . '] stopped'); if (supervisordControlAction($name, 'start')) { writeln('Daemon [' . $name . '] started'); } } } } else { writeln('Authentication failed'); } }); task('supervisord-monitor:stop', function () { if (supervisordIsAuthenticated()) { $names = explode(',', get('supervisord_process_name')); foreach ($names as $name) { $name = trim($name); if (supervisordControlAction($name, 'stop')) { writeln('Daemon [' . $name . '] stopped'); } } } else { writeln('Authentication failed'); } }); task('supervisord-monitor:start', function () { if (supervisordIsAuthenticated()) { $names = explode(',', get('supervisord_process_name')); foreach ($names as $name) { $name = trim($name); if (supervisordControlAction($name, 'start')) { writeln('Daemon [' . $name . '] started'); } } } else { writeln('Authentication failed'); } }); ================================================ FILE: contrib/telegram.php ================================================ get('telegram_chat_id'), 'text' => get('telegram_text'), 'parse_mode' => 'Markdown', ], ); $httpie = Httpie::get($telegramUrl); if (get('telegram_proxy', '') !== '') { $httpie = $httpie->setopt(CURLOPT_PROXY, get('telegram_proxy')); } $httpie->send(); }) ->once() ->hidden(); desc('Notifies Telegram about deploy finish'); task('telegram:notify:success', function () { if (!get('telegram_token', false)) { warning('No Telegram token configured'); return; } if (!get('telegram_chat_id', false)) { warning('No Telegram chat id configured'); return; } $telegramUrl = get('telegram_url') . '?' . http_build_query( [ 'chat_id' => get('telegram_chat_id'), 'text' => get('telegram_success_text'), 'parse_mode' => 'Markdown', ], ); $httpie = Httpie::get($telegramUrl); if (get('telegram_proxy', '') !== '') { $httpie = $httpie->setopt(CURLOPT_PROXY, get('telegram_proxy')); } $httpie->send(); }) ->once() ->hidden(); desc('Notifies Telegram about deploy failure'); task('telegram:notify:failure', function () { if (!get('telegram_token', false)) { warning('No Telegram token configured'); return; } if (!get('telegram_chat_id', false)) { warning('No Telegram chat id configured'); return; } $telegramUrl = get('telegram_url') . '?' . http_build_query( [ 'chat_id' => get('telegram_chat_id'), 'text' => get('telegram_failure_text'), 'parse_mode' => 'Markdown', ], ); $httpie = Httpie::get($telegramUrl); if (get('telegram_proxy', '') !== '') { $httpie = $httpie->setopt(CURLOPT_PROXY, get('telegram_proxy')); } $httpie->send(); }) ->once() ->hidden(); ================================================ FILE: contrib/webpack_encore.php ================================================ /feed?access_token='); // With publishing bot set('workplace_webhook', 'https://graph.facebook.com/v3.0/group/feed?access_token='); // Use markdown on message set('workplace_webhook', 'https://graph.facebook.com//feed?access_token=&formatting=MARKDOWN'); ``` - `workplace_text` - notification message ``` set('workplace_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*'); ``` - `workplace_success_text` – success template, default: ``` set('workplace_success_text', 'Deploy to *{{where}}* successful'); ``` - `workplace_failure_text` – failure template, default: ``` set('workplace_failure_text', 'Deploy to *{{where}}* failed'); ``` - `workplace_edit_post` – whether to create a new post for deploy result, or edit the first one created, default creates a new post: ``` set('workplace_edit_post', false); ``` ## Usage If you want to notify only about beginning of deployment add this line only: ```php before('deploy', 'workplace:notify'); ``` If you want to notify about successful end of deployment add this too: ```php after('deploy:success', 'workplace:notify:success'); ``` If you want to notify about failed deployment add this too: ```php after('deploy:failed', 'workplace:notify:failure'); ``` */ namespace Deployer; use Deployer\Utility\Httpie; // Deploy message set('workplace_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*'); set('workplace_success_text', 'Deploy to *{{where}}* successful'); set('workplace_failure_text', 'Deploy to *{{where}}* failed'); // By default, create a new post for every message set('workplace_edit_post', false); desc('Notifies Workplace'); task('workplace:notify', function () { if (!get('workplace_webhook', false)) { return; } $url = get('workplace_webhook') . '&message=' . urlencode(get('workplace_text')); $response = Httpie::post($url)->getJson(); if (get('workplace_edit_post', false)) { // Endpoint will be something like: https//graph.facebook.com/? $url = sprintf( '%s://%s/%s?%s', parse_url(get('workplace_webhook'), PHP_URL_SCHEME), parse_url(get('workplace_webhook'), PHP_URL_HOST), $response['id'], parse_url(get('workplace_webhook'), PHP_URL_QUERY), ); // Replace the webhook with a url that points to the created post set('workplace_webhook', $url); } }) ->once() ->hidden(); desc('Notifies Workplace about deploy finish'); task('workplace:notify:success', function () { if (!get('workplace_webhook', false)) { return; } $url = get('workplace_webhook') . '&message=' . urlencode(get('workplace_success_text')); Httpie::post($url)->send(); }) ->once() ->hidden(); desc('Notifies Workplace about deploy failure'); task('workplace:notify:failure', function () { if (!get('workplace_webhook', false)) { return; } $url = get('workplace_webhook') . '&message=' . urlencode(get('workplace_failure_text')); Httpie::post($url)->send(); }) ->once() ->hidden(); ================================================ FILE: contrib/yammer.php ================================================ {{user}} deploying {{what}} to {{where}} ``` - `yammer_success_body` – success template, default: ``` Deploy to {{where}} successful ``` - `yammer_failure_body` – failure template, default: ``` Deploy to {{where}} failed ``` ## Usage If you want to notify only about beginning of deployment add this line only: ```php before('deploy', 'yammer:notify'); ``` If you want to notify about successful end of deployment add this too: ```php after('deploy:success', 'yammer:notify:success'); ``` If you want to notify about failed deployment add this too: ```php after('deploy:failed', 'yammer:notify:failure'); ``` */ namespace Deployer; use Deployer\Utility\Httpie; set('yammer_url', 'https://www.yammer.com/api/v1/messages.json'); // Title of project set('yammer_title', function () { return get('application', 'Project'); }); // Deploy message set('yammer_body', '{{user}} deploying {{what}} to {{where}}'); set('yammer_success_body', 'Deploy to {{where}} successful'); set('yammer_failure_body', 'Deploy to {{where}} failed'); desc('Notifies Yammer'); task('yammer:notify', function () { $params = [ 'is_rich_text' => 'true', 'message_type' => 'announcement', 'group_id' => get('yammer_group_id'), 'title' => get('yammer_title'), 'body' => get('yammer_body'), ]; Httpie::post(get('yammer_url')) ->header('Authorization', 'Bearer ' . get('yammer_token')) ->header('Content-type', 'application/json') ->jsonBody($params) ->send(); }) ->once() ->hidden(); desc('Notifies Yammer about deploy finish'); task('yammer:notify:success', function () { $params = [ 'is_rich_text' => 'true', 'message_type' => 'announcement', 'group_id' => get('yammer_group_id'), 'title' => get('yammer_title'), 'body' => get('yammer_success_body'), ]; Httpie::post(get('yammer_url')) ->header('Authorization', 'Bearer ' . get('yammer_token')) ->header('Content-type', 'application/json') ->jsonBody($params) ->send(); }) ->once() ->hidden(); desc('Notifies Yammer about deploy failure'); task('yammer:notify:failure', function () { $params = [ 'is_rich_text' => 'true', 'message_type' => 'announcement', 'group_id' => get('yammer_group_id'), 'title' => get('yammer_title'), 'body' => get('yammer_failure_body'), ]; Httpie::post(get('yammer_url')) ->header('Authorization', 'Bearer ' . get('yammer_token')) ->header('Content-type', 'application/json') ->jsonBody($params) ->send(); }) ->once() ->hidden(); ================================================ FILE: contrib/yarn.php ================================================ ['--bwlimit=4096']]); }); ``` The issue was also described in the [Github Action](https://github.com/deployphp/action/issues/35). ================================================ FILE: docs/UPGRADE.md ================================================ # Upgrade a major version ## Upgrade from 7.x to 8.x - `run()` and `runLocally()` doesn't accept `options` parameter anymore. Use named arguments instead. - `no_throw` is now `nothrow`. - `real_time_output` is now `forceOutput`. - `idle_timeout` is now `idleTimeout`. ## Upgrade from 6.x to 7.x ### Step 1: Update deploy.php 1. Change config `hostname` to `alias`. 2. Change config `real_hostname` to `hostname`. 3. Change config `user` to `remote_user`. 4. Update `host()` definitions: 1. Add `set` prefix to all setters: `identityFile` -> `setIdentityFile` or `set('identity_file')` 2. Update `host(...)->addSshOption('UserKnownHostsFile', '/dev/null')` to `host(...)->setSshArguments(['-o UserKnownHostsFile=/dev/null']);` 3. Replace _stage_ with labels, i.e. ```php host('deployer.org') ->set('labels', ['stage' => 'prod']); ``` When deploying instead of `dep deploy prod` use `dep deploy stage=prod`. 4. `alias()` is deleted, `host()` itself sets alias and hostname, to override hostname use `setHostname()`. 5. Update `task()` definitions. 1. Replace `onRoles()` with `select()`: ```php task(...) ->select('stage=prod'); ``` 2. Don't use string-based task definition, it's not available anymore. Don't forget to set correct working directory. ```php # from task('deploy:npm-install', 'npm clean-install'); # to task('deploy:npm-install', function() { cd('{{release_path}}'); run('npm clean-install'); }); ``` 3. Remove `shallow()` tasks options. 6. Third party recipes now live inside main Deployer repo in _contrib_: ```php require 'contrib/rsync.php'; ``` 7. Replace `inventory()` with `import()`. It now can import hosts, configs, tasks: ```yaml import: recipe/common.php config: application: deployer shared_dirs: - uploads - storage/logs/ - storage/db shared_files: - .env - config/test.yaml keep_releases: 3 http_user: false hosts: prod: local: true tasks: deploy: - deploy:prepare - deploy:vendors - deploy:publish deploy:vendors: - run: "cd {{release_path}} && echo {{bin/composer}} {{composer_options}} 2>&1" ``` 8. Rename task `success` to `deploy:success` and `cleanup` to `deploy:cleanup`. 9. Verbosity functions (`isDebug()`, etc) got deleted. Use `output()->isDebug()` instead. 10. `runLocally()` commands are executed relative to the recipe file directory. This behaviour can be overridden via an environment variable: ``` DEPLOYER_ROOT=. vendor/bin/dep taskname ``` 11. Replace `local()` tasks with combination of `once()` and `runLocally()` func. 12. Replace `locateBinaryPath()` with `which()` func. 13. Replace `default_stage` with `default_selector`, and adjust the value accordingly (for example: "prod" to "stage=prod"). 14. Replace `onHosts()` and `onStage()` with [labels & selectors](selector.md). 15. Replace `setPrivate()` with [`hidden()`](tasks.md#hidden). 16. Configuration property `writable_recursive` defaults to `false`. This behaviour can be overridden with: ```php set('writable_recursive', true); ``` 17. `.git` directory is not present in release directory anymore. The previous behavior can be restored with: ```php set('update_code_strategy', 'clone'); ``` ### Step 2: Deploy Since the release history numbering is not compatible between v6 and v7, you need to specify the `release_name` manually for the first time. Otherwise you start with release 1. 1. Find out next release name (ssh to the host, `ls` releases dir, find the biggest number). Example: `42`. 2. Deploy with release_name: ``` dep deploy -o release_name=43 ``` :::note In case a rollback is needed, manually change the `current` symlink: ``` ln -nfs releases/42 current ``` ::: :::note In case there are multiple hosts with different release names, you should create a `{{deploy_path}}/.dep/latest_release` file in each host with the current release number of that particular host. ::: ## Upgrade from 5.x to 6.x 1. Changed branch option priority If you have host definition with `branch(...)` parameter, adding `--branch` option will not override it any more. If no `branch(...)` parameter persists, branch will be fetched from current local git branch. ```php host('prod') ->set('branch', 'production') ``` In order to return to old behavior add checking of `--branch` option. ```php host('prod') ->set('branch', function () { return input()->getOption('branch') ?: 'production'; }) ``` 2. Add `deploy:info` task to the beginning to `deploy` task. 3. `run` returns string instead of `Deployer\Type\Result` Now `run` and `runLocally` returns `string` instead of `Deployer\Type\Result`. Replace method calls as: - `run('command')->toString()` → `run('command')` - `run('if command; then echo "true"; fi;')->toBool()` → `test('command')` 4. `env_vars` renamed to `env` - `set('env_vars', 'FOO=bar');` → `set('env', ['FOO' => 'bar']);` If your are using Symfony recipe, then you need to change `env` setting: - `set('env', 'prod');` → `set('symfony_env', 'prod');` ## Upgrade from 4.x to 5.x 1. Servers to Hosts - `server($hostname)` to `host($hostname)`, and `server($name, $hostname)` to `host($name)->hostname($hostname)` - `localServer($name)` to `localhost()` - `cluster($name, $nodes, $port)` to `hosts(...$hodes)` - `serverList($file)` to `inventory($file)` If you need to deploy to same server use [host aliases](https://deployer.org/docs/hosts#host-aliases): ```php host('domain.com/green', 'domain.com/blue') ->set('deploy_path', '~/{{hostname}}') ... ``` Or you can define different hosts with same hostname: ```php host('production') ->hostname('domain.com') ->set('deploy_path', '~/production') ... host('beta') ->hostname('domain.com') ->set('deploy_path', '~/beta') ... ``` 2. Configuration options - Rename `{{server.name}}` to `{{hostname}}` 3. DotArray syntax In v5 access to nested arrays in config via dot notation was removed. If you was using it, consider to move to plain config options. Refactor this: ```php set('a', ['b' => 1]); // ... get('a.b'); ``` To: ```php set('a_b', 1); // ... get('a_b'); ``` 4. Credentials Best practice in new v5 is to omit credentials for connection in `deploy.php` and write them in `~/.ssh/config` instead. - `identityFile($publicKeyFile,, $privateKeyFile, $passPhrase)` to `identityFile($privateKeyFile)` - `pemFile($pemFile)` to `identityFile($pemFile)` - `forwardAgent()` to `forwardAgent(true)` 5. Tasks constraints - `onlyOn` to `onHosts` - `onlyOnStage` to `onStage` ## Upgrade from 3.x to 4.x 1. Namespace for functions Add to beginning of _deploy.php_ next line: ```php use function Deployer\{server, task, run, set, get, add, before, after}; ``` If you are using PHP version less than 5.6, you can use this: ```php namespace Deployer; ``` 2. `env()` to `set()`/`get()` Rename all calls `env($name, $value)` to `set($name, $value)`. Rename all rvalue `env($name)` to `get($name)`. Rename all `server(...)->env(...)` to `server(...)->set(...)`. 3. Moved _NonFatalException_ Rename `Deployer\Task\NonFatalException` to `Deployer\Exception\NonFatalException`. 4. Prior release cleanup Due to changes in release management, the new cleanup task will ignore any prior releases deployed with 3.x. These will need to be manually removed after migrating to and successfully releasing via 4.x. ## Upgrade from 2.x to 3.x 1. ### `->path('...')` Replace your server paths configuration: ```php server(...) ->path(...); ``` to: ```php server(...) ->env('deploy_path', '...'); ``` ================================================ FILE: docs/api.md ================================================ # API Reference ## host() ```php host(string ...$hostname): Host|ObjectProxy ``` Defines a host or hosts. ```php host('example.org'); host('prod.example.org', 'staging.example.org'); ``` Inside task can be used to get `Host` instance of an alias. ```php task('test', function () { $port = host('example.org')->get('port'); }); ``` ## localhost() ```php localhost(string ...$hostnames): Localhost|ObjectProxy ``` Define a local host. Deployer will not connect to this host, but will execute commands locally instead. ```php localhost('ci'); // Alias and hostname will be "ci". ``` ## currentHost() ```php currentHost(): Host ``` Returns current host. ## select() ```php select(string $selector): array ``` Returns hosts based on provided selector. ```php on(select('stage=prod, role=db'), function (Host $host) { ... }); ``` ## selectedHosts() ```php selectedHosts(): array ``` Returns array of hosts selected by user via CLI. ## import() ```php import(string $file): void ``` Import other php or yaml recipes. ```php import('recipe/common.php'); ``` ```php import(__DIR__ . '/config/hosts.yaml'); ``` ## desc() ```php desc(?string $title = null): ?string ``` Set task description. ## task() ```php task(string $name, callable|array|null $body = null): Task ``` Define a new task and save to tasks list. Alternatively get a defined task. | Argument | Type | Comment | |---|---|---| | `$name` | `string` | Name of current task. | | `$body` | `callable` or `array` or `null` | Callable task, array of other tasks names or nothing to get a defined tasks | ## before() ```php before(string $task, string|callable $do): ?Task ``` Call that task before specified task runs. | Argument | Type | Comment | |---|---|---| | `$task` | `string` | The task before $that should be run. | | `$do` | `string` or `callable` | The task to be run. | ## after() ```php after(string $task, string|callable $do): ?Task ``` Call that task after specified task runs. | Argument | Type | Comment | |---|---|---| | `$task` | `string` | The task after $that should be run. | | `$do` | `string` or `callable` | The task to be run. | ## fail() ```php fail(string $task, string|callable $do): ?Task ``` Setup which task run on failure of $task. When called multiple times for a task, previous fail() definitions will be overridden. | Argument | Type | Comment | |---|---|---| | `$task` | `string` | The task which need to fail so $that should be run. | | `$do` | `string` or `callable` | The task to be run. | ## option() ```php option(string $name, $shortcut = null, ?int $mode = null, string $description = '', $default = null): void ``` Add users options. | Argument | Type | Comment | |---|---|---| | `$name` | `string` | The option name | | `$shortcut` | `string` or `array` or `null` | The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts | | `$mode` | `int` or `null` | The option mode: One of the VALUE_* constants | | `$description` | `string` | A description text | | `$default` | `string` or `string[]` or `int` or `bool` or `null` | The default value (must be null for self::VALUE_NONE) | ## cd() ```php cd(string $path): void ``` Change the current working directory. ```php cd('~/myapp'); run('ls'); // Will run `ls` in ~/myapp. ``` ## become() ```php become(string $user): \Closure ``` Change the current user. Usage: ```php $restore = become('deployer'); // do something $restore(); // revert back to the previous user ``` ## within() ```php within(string $path, callable $callback): mixed ``` Execute a callback within a specific directory and revert back to the initial working directory. ## run() ```php run( string $command, ?string $cwd = null, ?array $env = null, #[\SensitiveParameter] ?string $secret = null, ?bool $nothrow = false, ?bool $forceOutput = false, ?int $timeout = null, ?int $idleTimeout = null, ): string ``` Executes given command on remote host. Examples: ```php run('echo hello world'); run('cd {{deploy_path}} && git status'); run('password %secret%', secret: getenv('CI_SECRET')); run('curl medv.io', timeout: 5); ``` ```php $path = run('readlink {{deploy_path}}/current'); run("echo $path"); ``` | Argument | Type | Comment | |---|---|---| | `$command` | `string` | Command to run on remote host. | | `$cwd` | `string` or `null` | Sets the process working directory. If not set {{working_path}} will be used. | | `$timeout` | `int` or `null` | Sets the process timeout (max. runtime). The timeout in seconds (default: 300 sec; see {{default_timeout}}, `null` to disable). | | `$idleTimeout` | `int` or `null` | Sets the process idle timeout (max. time since last output) in seconds. | | `$secret` | `string` or `null` | Placeholder `%secret%` can be used in command. Placeholder will be replaced with this value and will not appear in any logs. | | `$env` | `array` or `null` | Array of environment variables: `run('echo $KEY', env: ['key' => 'value']);` | | `$forceOutput` | `bool` or `null` | Print command output in real-time. | | `$nothrow` | `bool` or `null` | Don't throw an exception of non-zero exit code. | ## runLocally() ```php runLocally( string $command, ?string $cwd = null, ?int $timeout = null, ?int $idleTimeout = null, #[\SensitiveParameter] ?string $secret = null, ?array $env = null, ?bool $forceOutput = false, ?bool $nothrow = false, ?string $shell = null, ): string ``` Execute commands on a local machine. Examples: ```php $user = runLocally('git config user.name'); runLocally("echo $user"); ``` | Argument | Type | Comment | |---|---|---| | `$command` | `string` | Command to run on localhost. | | `$cwd` | `string` or `null` | Sets the process working directory. If not set {{working_path}} will be used. | | `$timeout` | `int` or `null` | Sets the process timeout (max. runtime). The timeout in seconds (default: 300 sec, `null` to disable). | | `$idleTimeout` | `int` or `null` | Sets the process idle timeout (max. time since last output) in seconds. | | `$secret` | `string` or `null` | Placeholder `%secret%` can be used in command. Placeholder will be replaced with this value and will not appear in any logs. | | `$env` | `array` or `null` | Array of environment variables: `runLocally('echo $KEY', env: ['key' => 'value']);` | | `$forceOutput` | `bool` or `null` | Print command output in real-time. | | `$nothrow` | `bool` or `null` | Don't throw an exception of non-zero exit code. | | `$shell` | `string` or `null` | Shell to run in. Default is `bash -s`. | ## test() ```php test(string $command): bool ``` Run test command. Example: ```php if (test('[ -d {{release_path}} ]')) { ... } ``` ## testLocally() ```php testLocally(string $command): bool ``` Run test command locally. Example: testLocally('[ -d {{local_release_path}} ]') ## on() ```php on($hosts, callable $callback): void ``` Iterate other hosts, allowing to call run a func in callback. ```php on(select('stage=prod, role=db'), function ($host) { ... }); ``` ```php on(host('example.org'), function ($host) { ... }); ``` ```php on(Deployer::get()->hosts, function ($host) { ... }); ``` ## invoke() ```php invoke(string $taskName): void ``` Runs a task. ```php invoke('deploy:symlink'); ``` ## upload() ```php upload($source, string $destination, array $config = []): void ``` Upload files or directories to host. > To upload the _contents_ of a directory, include a trailing slash (eg `upload('build/', '{{release_path}}/public');`). > Without the trailing slash, the build directory itself will be uploaded (resulting in `{{release_path}}/public/build`). The `$config` array supports the following keys: - `flags` for overriding the default `-azP` passed to the `rsync` command - `options` with additional flags passed directly to the `rsync` command - `timeout` for `Process::fromShellCommandline()` (`null` by default) - `progress_bar` to display upload/download progress - `display_stats` to display rsync set of statistics Note: due to the way php escapes command line arguments, list-notation for the rsync `--exclude={'file','anotherfile'}` option will not work. A workaround is to add a separate `--exclude=file` argument for each exclude to `options` (also, _do not_ wrap the filename/filter in quotes). An alternative might be to write the excludes to a temporary file (one per line) and use `--exclude-from=temporary_file` argument instead. ## download() ```php download(string $source, string $destination, array $config = []): void ``` Download file or directory from host ## info() ```php info(string $message): void ``` Writes an info message. ## warning() ```php warning(string $message): void ``` Writes an warning message. ## writeln() ```php writeln(string $message, int $options = 0): void ``` Writes a message to the output and adds a newline at the end. ## parse() ```php parse(string $value): string ``` Parse set values. ## set() ```php set(string $name, $value): void ``` Setup configuration option. ## add() ```php add(string $name, array $array): void ``` Merge new config params to existing config array. ## get() ```php get(string $name, $default = null) ``` Get configuration value. ## has() ```php has(string $name): bool ``` Check if there is such configuration option. ## ask() ```php ask(string $message, ?string $default = null, ?array $autocomplete = null): ?string ``` ## askChoice() ```php askChoice(string $message, array $availableChoices, $default = null, bool $multiselect = false) ``` ## askConfirmation() ```php askConfirmation(string $message, bool $default = false): bool ``` ## askHiddenResponse() ```php askHiddenResponse(string $message): string ``` ## input() ```php input(): InputInterface ``` ## output() ```php output(): OutputInterface ``` ## commandExist() ```php commandExist(string $command): bool ``` Check if command exists ## commandSupportsOption() ```php commandSupportsOption(string $command, string $option): bool ``` ## which() ```php which(string $name): string ``` ## remoteEnv() ```php remoteEnv(): array ``` Returns remote environments variables as an array. ```php $remotePath = remoteEnv()['PATH']; run('echo $PATH', env: ['PATH' => "/home/user/bin:$remotePath"]); ``` ## error() ```php error(string $message): Exception ``` Creates a new exception. ## timestamp() ```php timestamp(): string ``` Returns current timestamp in UTC timezone in ISO8601 format. ## fetch() ```php fetch(string $url, string $method = 'get', array $headers = [], ?string $body = null, ?array &$info = null, bool $nothrow = false): string ``` Example usage: ```php $result = fetch('{{domain}}', info: $info); var_dump($info['http_code'], $result); ``` ================================================ FILE: docs/avoid-php-fpm-reloading.md ================================================ # Avoid PHP-FPM Reloading Deployer symlinks _current_ to latest release dir. ``` current -> releases/3/ releases/ 1/ 2/ 3/ ``` ## The problem PHP Opcodes get cached. And if `SCRIPT_FILENAME` contains _current_ symlink, on new deploy nothing updates. Usually, a solution is simple to reload **php-fpm** after deploy, but such reload can lead to **dropped** or **failed** requests. The correct fix is to configure your server set `SCRIPT_FILENAME` to a resolved path. You can check your server configuration by printing `SCRIPT_FILENAME`. ```php echo $_SERVER['SCRIPT_FILENAME']; ``` If it prints something like `/home/deployer/example.com/current/index.php` with _current_ in the path, your server configured incorrectly. ## Fix for Nginx Nginx has special variable `$realpath_root`, use it to set up `SCRIPT_FILENAME` and `DOCUMENT_ROOT`: ```diff location ~ \.php$ { include fastcgi_params; fastcgi_pass unix:/var/run/php/php-fpm.sock; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; - fastcgi_param DOCUMENT_ROOT $document_root; + fastcgi_param DOCUMENT_ROOT $realpath_root; } ``` ## Fix for Caddy :::tip If you're already using servers provisioned by Deployer, you don't need to fix anything, as everything is already configured properly. ::: Use `resolve_root_symlink`: ``` php_fastcgi * unix//run/php/php-fpm.sock { resolve_root_symlink } ``` ## Fix for Apache Enable `revalidate_path` in `php.ini`: ```ini opcache.revalidate_path=1 ``` ================================================ FILE: docs/basics.md ================================================ # Basics Deployer operates around two main concepts: [**hosts**](hosts.md) and [**tasks**](tasks.md). These are defined within a **recipe**, which is simply a file containing **hosts** and **tasks** definitions. The Deployer CLI requires two arguments: 1. A **task** to execute. 2. A **selector** to determine the hosts the task will run on. Here's an example: ```sh $ dep deploy deployer.org ------ ------------ task selector ``` Deployer uses the [selector](selector.md) to choose which hosts to execute the task on. After selecting hosts, it prepares the environment (details later) and runs the task. ### Host Selection - If no selector is specified, Deployer prompts you to choose a host. - If your recipe has only one host, it is automatically selected. - To run a task on all hosts, use the `all` selector. By default, the `dep` CLI looks for a `deploy.php` or `deploy.yaml` file in the current directory. Alternatively, you can specify a recipe file explicitly using the `-f` or `--file` option: ```sh $ dep --file=deploy.php deploy deployer.org ``` --- ## Writing Your First Recipe Here's an example of a simple recipe: ```php namespace Deployer; host('deployer.org'); task('my_task', function () { run('whoami'); }); ``` To execute this task on `deployer.org`: ```sh $ dep my_task task my_task ``` ### Increasing Verbosity By default, Deployer only shows task names. To see detailed output (e.g., the result of the `whoami` command), use the `-v` option: ```sh $ dep my_task -v task my_task [deployer.org] run whoami [deployer.org] deployer ``` --- ## Working with Multiple Hosts You can define multiple hosts in your recipe: ```php host('deployer.org'); host('medv.io'); ``` Deployer connects to hosts using the same `~/.ssh/config` file as the `ssh` command. Alternatively, you can specify [connection options](hosts.md) directly in the recipe. Run a task on both hosts: ```sh $ dep my_task -v all task my_task [deployer.org] run whoami [medv.io] run whoami [deployer.org] deployer [medv.io] anton ``` ### Controlling Parallelism By default, tasks run in parallel on all selected hosts, which may mix the output. To limit execution to one host at a time: ```sh $ dep my_task -v all --limit 1 task my_task [deployer.org] run whoami [deployer.org] deployer [medv.io] run whoami [medv.io] deployer ``` You can also specify a [limit level](tasks.md#limit) for individual tasks to control parallelism. --- ## Configuring Hosts Each host can have a set of key-value configuration options. Here's an example: ```php host('deployer.org')->set('my_config', 'foo'); host('medv.io')->set('my_config', 'bar'); ``` Access these options in a task using the [currentHost](api.md#currenthost) function: ```php task('my_task', function () { $myConfig = currentHost()->get('my_config'); writeln("my_config: " . $myConfig); }); ``` Or more concisely with the [get](api.md#get) function: ```php task('my_task', function () { $myConfig = get('my_config'); writeln("my_config: " . $myConfig); }); ``` Or using brackets syntax `{{` and `}}`: ```php task('my_task', function () { writeln("my_config: {{my_config}}"); }); ``` --- ## Global Configurations Host configurations inherit global options. Here's how to set a global configuration: ```php set('my_config', 'global'); host('deployer.org'); host('medv.io'); ``` Both hosts will inherit `my_config` with the value `global`. You can override these values for individual hosts as needed. ```php set('my_config', 'global'); host('deployer.org'); host('medv.io')->set('my_config', 'bar'); ``` --- ## Dynamic Configurations You can define dynamic configuration values using callbacks. These are evaluated the first time they are accessed, and the result is stored for subsequent use: ```php set('whoami', function () { return run('whoami'); }); task('my_task', function () { writeln('Who am I? {{whoami}}'); }); ``` When executed: ```sh $ dep my_task all task my_task [deployer.org] Who am I? deployer [medv.io] Who am I? anton ``` --- Dynamic configurations are cached after the first use: ```php set('current_date', function () { return run('date'); }); task('my_task', function () { writeln('What time is it? {{current_date}}'); run('sleep 5'); writeln('What time is it? {{current_date}}'); }); ``` Running this task: ```sh $ dep my_task deployer.org -v task my_task [deployer.org] run date [deployer.org] Wed 03 Nov 2021 01:16:53 PM UTC [deployer.org] What time is it? Wed 03 Nov 2021 01:16:53 PM UTC [deployer.org] run sleep 5 [deployer.org] What time is it? Wed 03 Nov 2021 01:16:53 PM UTC ``` --- ## Overriding Configurations via CLI You can override configuration values using the `-o` option: ```sh $ dep my_task deployer.org -v -o current_date="I don't know" task my_task [deployer.org] What time is it? I don't know [deployer.org] run sleep 5 [deployer.org] What time is it? I don't know ``` Since `current_date` is overridden, the callback is never executed. :::note If you need to create a new configuration option based on the overridden one, use dynamic configuration syntax: ```php set('dir_name', 'test'); // calling get during recipe initialization will give you the original value defined above set('uses_original_dir_name', '/path/to/' . get('dir_name')); // use dynamic configuration syntax if you need to get a value passed via -o option set('uses_overridden_dir_name', function () { return '/path/to/' . get('dir_name'); }); task('my_task', function () { writeln('Path: {{uses_original_dir_name}}'); writeln('Path: {{uses_overridden_dir_name}}'); }); ``` ```sh $ dep my_task deployer.org -v -o dir_name="prod" task my_task [deployer.org] Path: /path/to/test [deployer.org] Path: /path/to/prod ``` ::: --- By now, you should have a solid understanding of Deployer’s basics, from defining tasks and hosts to working with configurations and dynamic values. Happy deploying! ================================================ FILE: docs/ci-cd.md ================================================ # CI/CD ## GitHub Actions Use official [GitHub Action for Deployer](https://github.com/deployphp/action). Create `.github/workflows/deploy.yml` file with following content: ```yaml name: deploy on: push: branches: [master] concurrency: production_environment jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: "8.1" - name: Install dependencies run: composer install - name: Deploy uses: deployphp/action@v1 with: private-key: ${{ secrets.PRIVATE_KEY }} dep: deploy ``` :::warning The `concurrency: production_environment` is important as it prevents concurrent deploys. ::: ## GitLab CI/CD Set the following variables in your GitLab project: - `SSH_KNOWN_HOSTS`: Content of `~/.ssh/known_hosts` file. The public SSH keys for a host may be obtained using the utility `ssh-keyscan`. For example: `ssh-keyscan deployer.org`. - `SSH_PRIVATE_KEY`: Private key for connecting to remote hosts. To generate a private key: `ssh-keygen -t ed25519 -C 'gitlab@deployer.org'`. Create a .gitlab-ci.yml file with the following content: ```yml stages: - deploy deploy: stage: deploy image: name: deployphp/deployer:v7 entrypoint: [""] before_script: - mkdir -p ~/.ssh - eval $(ssh-agent -s) - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts - chmod 644 ~/.ssh/known_hosts - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null script: - dep deploy -vvv resource_group: production only: - master ``` ## Bitbucket Pipelines Firstly, [generate a new SSH key and add it to your workspace for the server](https://support.atlassian.com/bitbucket-cloud/docs/configure-ssh-and-two-step-verification/). There are instructions on the SSH Keys page that can help you add this key to your server. You may also need to [define your environment variables](https://support.atlassian.com/bitbucket-cloud/docs/set-up-and-monitor-deployments/#Step-1--Define-your-environments) that you need to use in your deploy commands. Create a bitbucket-pipelines.yml file with the following content: ```yml pipelines: branches: develop: - stage: # this is the target deployment name and it will inherit the environment from it deployment: staging name: Deploy Staging steps: - step: name: Composer Install image: composer/composer:2.2 caches: - composer script: - composer install --quiet artifacts: # we need to save all these files so that they can be picked up in the actual deployment - vendor/** - step: name: NPM Install image: node:22-bullseye-slim caches: - node script: - npm install --silent artifacts: # we need to save all these files so that they can be picked up in the actual deployment - public/build/** - step: name: Deployer Deploy timeout: 6m # if it takes longer than this, error out # @see https://hub.docker.com/r/deployphp/deployer/tags?name=v7.5 image: deployphp/deployer:v7.5.8 script: # pass $DEVELOP and $STAGING variables from the "staging" deployment environment - php /bin/deployer.phar deploy --branch=$DEVELOP stage=$STAGING ``` ### Deployment concurrency Only one deployment job runs at a time with the [`resource_group` keyword](https://docs.gitlab.com/ee/ci/yaml/index.html#resource_group) in .gitlab-ci.yml. In addition, you can ensure that older deployment jobs are cancelled automatically when a newer deployment runs by enabling the [skip outdated deployment jobs](https://docs.gitlab.com/ee/ci/pipelines/settings.html#prevent-outdated-deployment-jobs) feature (enabled by default). ### Deploy secrets It is not recommended to commit secrets to the repository, you could use a GitLab variable to store them instead. Many frameworks use dotenv to store secrets, let's create a GitLab file variable named `DOTENV`, so it can be deployed along with the code. Set up a deployer task to copy secrets to the server: ```php task('deploy:secrets', function () { upload(getenv('DOTENV'), '{{deploy_path}}/shared/.env'); }); ``` Run the task immediately after updating the code. ================================================ FILE: docs/cli.md ================================================ # CLI Usage We recommend adding the following alias to your .bashrc file: ```bash alias dep='vendor/bin/dep' ``` It is also recommended to install the completion script for Deployer. Completion supports: - tasks, - options, - host names, - and configs. For example, on macOS run the following commands: ```bash brew install bash-completion dep completion bash > /usr/local/etc/bash_completion.d/deployer ``` ## Overriding configuration options For example, if your _deploy.php_ file contains this configuration: ```php set('ssh_multiplexing', false); ``` And you want to enable [ssh multiplexing](https://en.wikibooks.org/wiki/OpenSSH/Cookbook/Multiplexing) without modifying the recipe, you can pass the `-o` option to the `dep` command: ``` dep deploy -o ssh_multiplexing=true ``` To override multiple config options, you can pass multiple `-o` args: ``` dep deploy -o ssh_multiplexing=true -o branch=master ``` ## Running arbitrary commands Run any command on one or more hosts: ``` dep run 'uptime -p' ``` ## Tree command Deployer supports [task grouping](tasks.md#task-grouping) and [before/after hooks](tasks.md#addbefore). To visualize the task hierarchy, use the **dep tree** command. ``` $ dep tree deploy The task-tree for deploy: └── deploy ├── deploy:prepare │ ├── deploy:info │ ├── deploy:setup │ ├── deploy:lock │ ├── deploy:release │ ├── deploy:update_code │ ├── build // after deploy:update_code │ ├── deploy:shared │ └── deploy:writable ├── deploy:vendors ├── artisan:storage:link ├── artisan:config:cache ├── artisan:route:cache ├── artisan:view:cache ├── artisan:migrate └── deploy:publish ├── deploy:symlink ├── deploy:unlock ├── deploy:cleanup └── deploy:success ``` ## Execution plan Before executing tasks, Deployer needs to flatten the task tree and decide in which order it will be executing tasks on which hosts. Use the `--plan` option to output a table with tasks/hosts: ``` $ dep deploy --plan all ┌──────────────────────┬──────────────────────┬──────────────────────┬──────────────────────┐ │ prod01 │ prod02 │ prod03 │ prod04 │ ├──────────────────────┼──────────────────────┼──────────────────────┼──────────────────────┤ │ deploy:info │ deploy:info │ deploy:info │ deploy:info │ │ deploy:setup │ deploy:setup │ deploy:setup │ deploy:setup │ │ deploy:lock │ deploy:lock │ deploy:lock │ deploy:lock │ │ deploy:release │ deploy:release │ deploy:release │ deploy:release │ │ deploy:update_code │ deploy:update_code │ deploy:update_code │ deploy:update_code │ │ build │ build │ build │ build │ │ deploy:shared │ deploy:shared │ deploy:shared │ deploy:shared │ │ deploy:writable │ deploy:writable │ deploy:writable │ deploy:writable │ │ deploy:vendors │ deploy:vendors │ deploy:vendors │ deploy:vendors │ │ artisan:storage:link │ artisan:storage:link │ artisan:storage:link │ artisan:storage:link │ │ artisan:config:cache │ artisan:config:cache │ artisan:config:cache │ artisan:config:cache │ │ artisan:route:cache │ artisan:route:cache │ artisan:route:cache │ artisan:route:cache │ │ artisan:view:cache │ artisan:view:cache │ artisan:view:cache │ artisan:view:cache │ │ artisan:migrate │ artisan:migrate │ artisan:migrate │ artisan:migrate │ │ deploy:symlink │ - │ - │ - │ │ - │ deploy:symlink │ - │ - │ │ - │ - │ deploy:symlink │ - │ │ - │ - │ - │ deploy:symlink │ │ deploy:unlock │ deploy:unlock │ deploy:unlock │ deploy:unlock │ │ deploy:cleanup │ deploy:cleanup │ deploy:cleanup │ deploy:cleanup │ │ deploy:success │ deploy:success │ deploy:success │ deploy:success │ └──────────────────────┴──────────────────────┴──────────────────────┴──────────────────────┘ ``` The **deploy.php**: ```php host('prod[01:04]'); task('deploy:symlink')->limit(1); ``` ## The `runLocally` working dir By default, `runLocally()` commands are executed relative to the recipe file directory. This can be overridden globally by setting an environment variable: ``` DEPLOYER_ROOT=. dep taskname` ``` Alternatively, the root directory can be overridden per command via the cwd configuration. ```php runLocally('ls', ['cwd' => '/root/directory']); ``` ## Play blackjack > Yeah, well. I'm gonna go build my own theme park... with blackjack and hookers! > > In fact, forget the park! > > — Bender ``` dep blackjack ``` ================================================ FILE: docs/contrib/README.md ================================================ # All Contrib Recipes * [Bugsnag Recipe](/docs/contrib/bugsnag.md) * [Cachetool Recipe](/docs/contrib/cachetool.md) * [Chatwork Recipe](/docs/contrib/chatwork.md) * [Cimonitor Recipe](/docs/contrib/cimonitor.md) * [Cloudflare Recipe](/docs/contrib/cloudflare.md) * [Cpanel Recipe](/docs/contrib/cpanel.md) * [Crontab Recipe](/docs/contrib/crontab.md) * [Directadmin Recipe](/docs/contrib/directadmin.md) * [Discord Recipe](/docs/contrib/discord.md) * [Grafana Recipe](/docs/contrib/grafana.md) * [Hangouts Recipe](/docs/contrib/hangouts.md) * [Hipchat Recipe](/docs/contrib/hipchat.md) * [Ispmanager Recipe](/docs/contrib/ispmanager.md) * [Mattermost Recipe](/docs/contrib/mattermost.md) * [Ms-teams Recipe](/docs/contrib/ms-teams.md) * [Newrelic Recipe](/docs/contrib/newrelic.md) * [Npm Recipe](/docs/contrib/npm.md) * [Ntfy Recipe](/docs/contrib/ntfy.md) * [Phinx Recipe](/docs/contrib/phinx.md) * [Php-fpm Recipe](/docs/contrib/php-fpm.md) * [Rabbit Recipe](/docs/contrib/rabbit.md) * [Raygun Recipe](/docs/contrib/raygun.md) * [Rocketchat Recipe](/docs/contrib/rocketchat.md) * [Rollbar Recipe](/docs/contrib/rollbar.md) * [Rsync Recipe](/docs/contrib/rsync.md) * [Sentry Recipe](/docs/contrib/sentry.md) * [Slack Recipe](/docs/contrib/slack.md) * [Supervisord-monitor Recipe](/docs/contrib/supervisord-monitor.md) * [Telegram Recipe](/docs/contrib/telegram.md) * [Webpack_encore Recipe](/docs/contrib/webpack_encore.md) * [Workplace Recipe](/docs/contrib/workplace.md) * [Yammer Recipe](/docs/contrib/yammer.md) * [Yarn Recipe](/docs/contrib/yarn.md) ================================================ FILE: docs/contrib/bugsnag.md ================================================ # Bugsnag Recipe ```php require 'contrib/bugsnag.php'; ``` [Source](/contrib/bugsnag.php) ## Configuration - *bugsnag_api_key* – the API Key associated with the project. Informs Bugsnag which project has been deployed. This is the only required field. - *bugsnag_provider* – the name of your source control provider. Required when repository is supplied and only for on-premise services. - *bugsnag_app_version* – the app version of the code you are currently deploying. Only set this if you tag your releases with semantic version numbers and deploy infrequently. (Optional.) ## Usage Since you should only notify Bugsnag of a successful deployment, the `bugsnag:notify` task should be executed right at the end. ```php after('deploy', 'bugsnag:notify'); ``` ## Tasks ### bugsnag\:notify {#bugsnag-notify} [Source](https://github.com/deployphp/deployer/blob/master/contrib/bugsnag.php#L24) Notifies Bugsnag of deployment. ================================================ FILE: docs/contrib/cachetool.md ================================================ # Cachetool Recipe ```php require 'contrib/cachetool.php'; ``` [Source](/contrib/cachetool.php) ## Configuration - **cachetool** *(optional)*: accepts a *string* or an *array* of strings with the unix socket or ip address to php-fpm. If `cachetool` is not given, then the application will look for a configuration file. The file must be named .cachetool.yml or .cachetool.yaml. CacheTool will look for this file on the current directory and in any parent directory until it finds one. If the paths above fail it will try to load /etc/cachetool.yml or /etc/cachetool.yaml configuration file. ```php set('cachetool', '/var/run/php-fpm.sock'); // or set('cachetool', '127.0.0.1:9000'); // or set('cachetool', ['/var/run/php-fpm.sock', '/var/run/php-fpm-other.sock']); ``` You can also specify different cachetool settings for each host: ```php host('staging') ->set('cachetool', '127.0.0.1:9000'); host('production') ->set('cachetool', '/var/run/php-fpm.sock'); ``` By default, if no `cachetool` parameter is provided, this recipe will fallback to the global setting. If your deployment user does not have permission to access the php-fpm.sock, you can alternatively use the web adapter that creates a temporary php file and makes a web request to it with a configuration like ```php set('cachetool_args', '--web --web-path=./public --web-url=https://{{hostname}}'); ``` ## Usage Since APCu and OPcache deal with compiling and caching files, they should be executed right after the symlink is created for the new release: ```php after('deploy:symlink', 'cachetool:clear:opcache'); or after('deploy:symlink', 'cachetool:clear:apcu'); ``` ## Read more Read more information about cachetool on the website: http://gordalina.github.io/cachetool/ ## Configuration ### cachetool [Source](https://github.com/deployphp/deployer/blob/master/contrib/cachetool.php#L51) ## Configuration - **cachetool** *(optional)*: accepts a *string* or an *array* of strings with the unix socket or ip address to php-fpm. If `cachetool` is not given, then the application will look for a configuration file. The file must be named .cachetool.yml or .cachetool.yaml. CacheTool will look for this file on the current directory and in any parent directory until it finds one. If the paths above fail it will try to load /etc/cachetool.yml or /etc/cachetool.yaml configuration file. ```php set('cachetool', '/var/run/php-fpm.sock'); // or set('cachetool', '127.0.0.1:9000'); // or set('cachetool', ['/var/run/php-fpm.sock', '/var/run/php-fpm-other.sock']); ``` You can also specify different cachetool settings for each host: ```php host('staging') ->set('cachetool', '127.0.0.1:9000'); host('production') ->set('cachetool', '/var/run/php-fpm.sock'); ``` By default, if no `cachetool` parameter is provided, this recipe will fallback to the global setting. If your deployment user does not have permission to access the php-fpm.sock, you can alternatively use the web adapter that creates a temporary php file and makes a web request to it with a configuration like ```php set('cachetool_args', '--web --web-path=./public --web-url=https://{{hostname}}'); ``` ## Usage Since APCu and OPcache deal with compiling and caching files, they should be executed right after the symlink is created for the new release: ```php after('deploy:symlink', 'cachetool:clear:opcache'); or after('deploy:symlink', 'cachetool:clear:apcu'); ``` ## Read more Read more information about cachetool on the website: http://gordalina.github.io/cachetool/ ### cachetool_url [Source](https://github.com/deployphp/deployer/blob/master/contrib/cachetool.php#L59) URL to download cachetool from if it is not available CacheTool 9.x works with PHP >=8.1 CacheTool 8.x works with PHP >=8.0 CacheTool 7.x works with PHP >=7.3 ```php title="Default value" 'https://github.com/gordalina/cachetool/releases/download/9.1.0/cachetool.phar' ``` ### cachetool_args [Source](https://github.com/deployphp/deployer/blob/master/contrib/cachetool.php#L60) ### bin/cachetool [Source](https://github.com/deployphp/deployer/blob/master/contrib/cachetool.php#L61) ```php title="Default value" if (!test('[ -f {{release_or_current_path}}/cachetool.phar ]')) { run("cd {{release_or_current_path}} && curl -sLO {{cachetool_url}}"); } return '{{release_or_current_path}}/cachetool.phar'; ``` ### cachetool_options [Source](https://github.com/deployphp/deployer/blob/master/contrib/cachetool.php#L67) :::info Autogenerated The value of this configuration is autogenerated on access. ::: ## Tasks ### cachetool\:clear\:opcache {#cachetool-clear-opcache} [Source](https://github.com/deployphp/deployer/blob/master/contrib/cachetool.php#L89) Clears OPcode cache. Clear opcache cache ### cachetool\:clear\:apcu {#cachetool-clear-apcu} [Source](https://github.com/deployphp/deployer/blob/master/contrib/cachetool.php#L100) Clears APCu system cache. Clear APCu cache ### cachetool\:clear\:stat {#cachetool-clear-stat} [Source](https://github.com/deployphp/deployer/blob/master/contrib/cachetool.php#L111) Clears file status and realpath caches. Clear file status cache, including the realpath cache ================================================ FILE: docs/contrib/chatwork.md ================================================ # Chatwork Recipe ```php require 'contrib/chatwork.php'; ``` [Source](/contrib/chatwork.php) # Chatwork Recipe ## Installing 1. Create chatwork account by any manual in the internet 2. Take chatwork token (Like: b29a700e2d15bef3f26ae6a5c142d1ea) and set `chatwork_token` parameter 3. Take chatwork room id from url after clicked on the room, and set `chatwork_room_id` parameter 4. If you want, you can edit `chatwork_notify_text`, `chatwork_success_text` or `chatwork_failure_text` 5. Require chatwork recipe in your `deploy.php` file ```php # https://deployer.org/recipes.html require 'recipe/chatwork.php'; ``` Add hook on deploy: ```php before('deploy', 'chatwork:notify'); ``` ## Configuration - `chatwork_token` – chatwork bot token, **required** - `chatwork_room_id` — chatwork room to push messages to **required** - `chatwork_notify_text` – notification message template ``` [info] [title](*) Deployment Status: Deploying[/title] Repo: {{repository}} Branch: {{branch}} Server: {{hostname}} Release Path: {{release_path}} Current Path: {{current_path}} [/info] ``` - `chatwork_success_text` – success template, default: ``` [info] [title](*) Deployment Status: Successfully[/title] Repo: {{repository}} Branch: {{branch}} Server: {{hostname}} Release Path: {{release_path}} Current Path: {{current_path}} [/info]" ``` - `chatwork_failure_text` – failure template, default: ``` [info] [title](*) Deployment Status: Failed[/title] Repo: {{repository}} Branch: {{branch}} Server: {{hostname}} Release Path: {{release_path}} Current Path: {{current_path}} [/info]" ``` ## Tasks - `chatwork:notify` – send message to chatwork - `chatwork:notify:success` – send success message to chatwork - `chatwork:notify:failure` – send failure message to chatwork ## Usage If you want to notify only about beginning of deployment add this line only: ```php before('deploy', 'chatwork:notify'); ``` If you want to notify about successful end of deployment add this too: ```php after('deploy:success', 'chatwork:notify:success'); ``` If you want to notify about failed deployment add this too: ```php after('deploy:failed', 'chatwork:notify:failure'); ``` ## Configuration ### chatwork_token [Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L93) Chatwork settings :::info Required Throws exception if not set. ::: ### chatwork_room_id [Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L96) :::info Required Throws exception if not set. ::: ### chatwork_api [Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L99) ```php title="Default value" return 'https://api.chatwork.com/v2/rooms/' . get('chatwork_room_id') . '/messages'; ``` ### chatwork_notify_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L104) The Messages ```php title="Default value" "[info]\n[title](*) Deployment Status: Deploying[/title]\nRepo: {{repository}}\nBranch: {{branch}}\nServer: {{hostname}}\nRelease Path: {{release_path}}\nCurrent Path: {{current_path}}\n[/info]" ``` ### chatwork_success_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L105) ```php title="Default value" "[info]\n[title](*) Deployment Status: Successfully[/title]\nRepo: {{repository}}\nBranch: {{branch}}\nServer: {{hostname}}\nRelease Path: {{release_path}}\nCurrent Path: {{current_path}}\n[/info]" ``` ### chatwork_failure_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L106) ```php title="Default value" "[info]\n[title](*) Deployment Status: Failed[/title]\nRepo: {{repository}}\nBranch: {{branch}}\nServer: {{hostname}}\nRelease Path: {{release_path}}\nCurrent Path: {{current_path}}\n[/info]" ``` ## Tasks ### chatwork_send_message {#chatwork_send_message} [Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L109) Helpers ### chatwork\:test {#chatwork-test} [Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L118) Tests messages. Tasks ### chatwork\:notify {#chatwork-notify} [Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L129) Notifies Chatwork. ### chatwork\:notify\:success {#chatwork-notify-success} [Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L144) Notifies Chatwork about deploy finish. ### chatwork\:notify\:failure {#chatwork-notify-failure} [Source](https://github.com/deployphp/deployer/blob/master/contrib/chatwork.php#L160) Notifies Chatwork about deploy failure. ================================================ FILE: docs/contrib/cimonitor.md ================================================ # Cimonitor Recipe ```php require 'contrib/cimonitor.php'; ``` [Source](/contrib/cimonitor.php) Monitor your deployments on [CIMonitor](https://github.com/CIMonitor/CIMonitor). ![CIMonitorGif](https://www.steefmin.xyz/deployer-example.gif) Add tasks on deploy: ```php before('deploy', 'cimonitor:notify'); after('deploy:success', 'cimonitor:notify:success'); after('deploy:failed', 'cimonitor:notify:failure'); ``` ## Configuration - `cimonitor_webhook` – CIMonitor server webhook url, **required** ``` set('cimonitor_webhook', 'https://cimonitor.enrise.com/webhook/deployer'); ``` - `cimonitor_title` – the title of application, default the username\reponame combination from `{{repository}}` ``` set('cimonitor_title', ''); ``` - `cimonitor_user` – User object with name and email, default gets information from `git config` ``` set('cimonitor_user', function () { return [ 'name' => 'John Doe', 'email' => 'john@enrise.com', ]; }); ``` Various cimonitor statusses are set, in case you want to change these yourselves. See the [CIMonitor documentation](https://cimonitor.readthedocs.io/en/latest/) for the usages of different states. ## Usage If you want to notify only about beginning of deployment add this line only: ```php before('deploy', 'cimonitor:notify'); ``` If you want to notify about successful end of deployment add this too: ```php after('deploy:success', 'cimonitor:notify:success'); ``` If you want to notify about failed deployment add this too: ```php after('deploy:failed', 'cimonitor:notify:failure'); ``` ## Configuration ### cimonitor_title [Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L64) Title of project based on git repo ```php title="Default value" $repo = get('repository'); $pattern = '/\w+\/\w+/'; return preg_match($pattern, $repo, $titles) ? $titles[0] : $repo; ``` ### cimonitor_user [Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L69) ```php title="Default value" return [ 'name' => runLocally('git config --get user.name'), 'email' => runLocally('git config --get user.email'), ]; ``` ### cimonitor_status_info [Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L77) CI monitor status states and job states ```php title="Default value" 'info' ``` ### cimonitor_status_warning [Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L78) ```php title="Default value" 'warning' ``` ### cimonitor_status_error [Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L79) ```php title="Default value" 'error' ``` ### cimonitor_status_success [Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L80) ```php title="Default value" 'success' ``` ### cimonitor_job_state_info [Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L81) ```php title="Default value" get('cimonitor_status_info') ``` ### cimonitor_job_state_pending [Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L82) ```php title="Default value" 'pending' ``` ### cimonitor_job_state_running [Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L83) ```php title="Default value" 'running' ``` ### cimonitor_job_state_warning [Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L84) ```php title="Default value" get('cimonitor_status_warning') ``` ### cimonitor_job_state_error [Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L85) ```php title="Default value" get('cimonitor_status_error') ``` ### cimonitor_job_state_success [Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L86) ```php title="Default value" get('cimonitor_status_success') ``` ## Tasks ### cimonitor\:notify {#cimonitor-notify} [Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L89) Notifies CIMonitor. ### cimonitor\:notify\:success {#cimonitor-notify-success} [Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L115) Notifies CIMonitor about deploy finish. ### cimonitor\:notify\:failure {#cimonitor-notify-failure} [Source](https://github.com/deployphp/deployer/blob/master/contrib/cimonitor.php#L143) Notifies CIMonitor about deploy failure. ================================================ FILE: docs/contrib/cloudflare.md ================================================ # Cloudflare Recipe ```php require 'contrib/cloudflare.php'; ``` [Source](/contrib/cloudflare.php) ### Configuration - `cloudflare` – array with configuration for cloudflare - `service_key` – Cloudflare Service Key. If this is not provided, use api_key and email. - `api_key` – Cloudflare API key generated on the "My Account" page. - `email` – Cloudflare Email address associated with your account. - `api_token` – Cloudflare API Token generated on the "My Account" page. - `domain` – The domain you want to clear (optional if zone_id is provided). - `zone_id` – Cloudflare Zone ID (optional). ### Usage Since the website should be built and some load is likely about to be applied to your server, this should be one of, if not the, last tasks before cleanup ## Tasks ### deploy\:cloudflare {#deploy-cloudflare} [Source](https://github.com/deployphp/deployer/blob/master/contrib/cloudflare.php#L24) Clears Cloudflare Cache. ### Configuration - `cloudflare` – array with configuration for cloudflare - `service_key` – Cloudflare Service Key. If this is not provided, use api_key and email. - `api_key` – Cloudflare API key generated on the "My Account" page. - `email` – Cloudflare Email address associated with your account. - `api_token` – Cloudflare API Token generated on the "My Account" page. - `domain` – The domain you want to clear (optional if zone_id is provided). - `zone_id` – Cloudflare Zone ID (optional). ### Usage Since the website should be built and some load is likely about to be applied to your server, this should be one of, if not the, last tasks before cleanup ================================================ FILE: docs/contrib/cpanel.md ================================================ # Cpanel Recipe ```php require 'contrib/cpanel.php'; ``` [Source](/contrib/cpanel.php) ### Description This is a recipe that uses the [cPanel 2 API](https://documentation.cPanel.net/display/DD/Guide+to+cPanel+API+2). Unfortunately the [UAPI](https://documentation.cPanel.net/display/DD/Guide+to+UAPI) that is recommended does not have support for creating addon domains. The main idea behind is for staging purposes but I guess you can use it for other interesting concepts. The idea is, every branch possibly has its own staging domain/subdomain (staging-neat-feature.project.com) and database db_neat-feature_project so it can be tested. This recipe can make the domain/subdomain and database creation part of the deployment process so you don't have to manually create them through an interface. ### Configuration The example uses a .env file and Dotenv for configuration, but you can set the parameters as you wish ``` set('cpanel', [ 'host' => getenv('CPANEL_HOST'), 'port' => getenv('CPANEL_PORT'), 'username' => getenv('CPANEL_USERNAME'), 'auth_type' => getenv('CPANEL_AUTH_TYPE'), 'token' => getenv('CPANEL_TOKEN'), 'user' => getenv('CPANEL_USER'), 'db_user' => getenv('CPANEL_DB_USER'), 'db_user_privileges' => getenv('CPANEL_DB_PRIVILEGES'), 'timeout' => 500, 'allowInStage' => ['staging', 'beta', 'alpha'], 'create_domain_format' => '%s-%s-%s', 'create_domain_values' => ['staging', 'master', get('application')], 'subdomain_prefix' => substr(md5(get('application')), 0,4) . '-', 'subdomain_suffix' => getenv('SUDOMAIN_SUFFIX'), 'create_db_format' => '%s_%s-%s-%s', 'create_db_values' => ['apps', 'staging','master', get('application')], ]); ``` - `cpanel` – array with configuration for cPanel - `username` – WHM account - `user` – cPanel account that you want in charge of the domain - `token` – WHM API token - `create_domain_format` – Format for name creation of domain - `create_domain_values` – The actual value reference for naming - `subdomain_prefix` – cPanel has a weird way of dealing with addons and subdomains, you cannot create 2 addons with the same subdomain, so you need to change it in some way, example uses first 4 chars of md5(app_name) - `subdomain_suffix` – cPanel has a weird way of dealing with addons and subdomains, so the suffix needs to be your main domain for that account for deletion purposes - `addondir` – addon dir is different from the deploy path because cPanel "injects" /home/user/ into the path, so tilde cannot be used - `allowInStage` – Define the stages that cPanel recipe actions are allowed in #### .env file example ``` CPANEL_HOST=xxx.xxx.xxx.xxx CPANEL_PORT=2087 CPANEL_USERNAME=root CPANEL_TOKEN=xxxx CPANEL_USER=xxx CPANEL_AUTH_TYPE=hash CPANEL_DB_USER=db_user CPANEL_DB_PRIVILEGES="ALL PRIVILEGES" SUDOMAIN_SUFFIX=.mymaindomain.com ``` ### Tasks - `cpanel:createaddondomain` Creates an addon domain - `cpanel:deleteaddondomain` Removes an addon domain - `cpanel:createdb` Creates a new database ### Usage A complete example with configs, staging and deployment ``` load(); // this is used just so an .env file can be used for credentials require 'cpanel.php'; Project name set('application', 'myproject.com'); Project repository set('repository', 'git@github.com:myorg/myproject.com'); set('cpanel', [ 'host' => getenv('CPANEL_HOST'), 'port' => getenv('CPANEL_PORT'), 'username' => getenv('CPANEL_USERNAME'), 'auth_type' => getenv('CPANEL_AUTH_TYPE'), 'token' => getenv('CPANEL_TOKEN'), 'user' => getenv('CPANEL_USER'), 'db_user' => getenv('CPANEL_DB_USER'), 'db_user_privileges' => getenv('CPANEL_DB_PRIVILEGES'), 'timeout' => 500, 'allowInStage' => ['staging', 'beta', 'alpha'], 'create_domain_format' => '%s-%s-%s', 'create_domain_values' => ['staging', 'master', get('application')], 'subdomain_prefix' => substr(md5(get('application')), 0,4) . '-', 'subdomain_suffix' => getenv('SUDOMAIN_SUFFIX'), 'create_db_format' => '%s_%s-%s-%s', 'create_db_values' => ['apps', 'staging','master', get('application')], ]); host('myproject.com') ->stage('staging') ->set('cpanel_createdb', vsprintf(get('cpanel')['create_db_format'], get('cpanel')['create_db_values'])) ->set('branch', 'dev-branch') ->set('deploy_path', '~/staging/' . vsprintf(get('cpanel')['create_domain_format'], get('cpanel')['create_domain_values'])) ->set('addondir', 'staging/' . vsprintf(get('cpanel')['create_domain_format'], get('cpanel')['create_domain_values'])); Tasks task('build', function () { run('cd {{release_path}} && build'); }); after('deploy:prepare', 'cpanel:createaddondomain'); after('deploy:prepare', 'cpanel:createdb'); ``` ## Tasks ### cpanel\:createdb {#cpanel-createdb} [Source](https://github.com/deployphp/deployer/blob/master/contrib/cpanel.php#L196) Creates database though CPanel API. ### cpanel\:createaddondomain {#cpanel-createaddondomain} [Source](https://github.com/deployphp/deployer/blob/master/contrib/cpanel.php#L224) Creates addon domain though CPanel API. ### cpanel\:deleteaddondomain {#cpanel-deleteaddondomain} [Source](https://github.com/deployphp/deployer/blob/master/contrib/cpanel.php#L247) Deletes addon domain though CPanel API. ================================================ FILE: docs/contrib/crontab.md ================================================ # Crontab Recipe ```php require 'contrib/crontab.php'; ``` [Source](/contrib/crontab.php) Recipe for adding crontab jobs. This recipe creates a new section in the crontab file with the configured jobs. The section is identified by the *crontab:identifier* variable, by default the application name. ## Configuration - *crontab:jobs* - An array of strings with crontab lines. ## Usage ```php require 'contrib/crontab.php'; after('deploy:success', 'crontab:sync'); add('crontab:jobs', [ '* * * * * cd {{current_path}} && {{bin/php}} artisan schedule:run >> /dev/null 2>&1', ]); ``` ## Configuration ### bin/crontab [Source](https://github.com/deployphp/deployer/blob/master/contrib/crontab.php#L30) Get path to bin ```php title="Default value" return which('crontab'); ``` ### crontab:identifier [Source](https://github.com/deployphp/deployer/blob/master/contrib/crontab.php#L35) Set the identifier used in the crontab, application name by default ```php title="Default value" return get('application', 'application'); ``` ### crontab:use_sudo [Source](https://github.com/deployphp/deployer/blob/master/contrib/crontab.php#L40) Use sudo to run crontab. When running crontab with sudo, you can use the `-u` parameter to change a crontab for a different user. ```php title="Default value" false ``` ## Tasks ### crontab\:sync {#crontab-sync} [Source](https://github.com/deployphp/deployer/blob/master/contrib/crontab.php#L43) Sync crontab jobs. ### crontab\:remove {#crontab-remove} [Source](https://github.com/deployphp/deployer/blob/master/contrib/crontab.php#L87) Remove crontab jobs. ================================================ FILE: docs/contrib/directadmin.md ================================================ # Directadmin Recipe ```php require 'contrib/directadmin.php'; ``` [Source](/contrib/directadmin.php) ### Configuration - `directadmin` – array with configuration for DirectAdmin - `host` – DirectAdmin host - `port` – DirectAdmin port (default: 2222, not required) - `scheme` – DirectAdmin scheme (default: http, not required) - `username` – DirectAdmin username - `password` – DirectAdmin password (it is recommended to use login keys!) - `db_user` – Database username (required when using directadmin:createdb or directadmin:deletedb) - `db_name` – Database namse (required when using directadmin:createdb) - `db_password` – Database password (required when using directadmin:createdb) - `domain_name` – Domain to create, delete or edit (required when using directadmin:createdomain, directadmin:deletedomain, directadmin:symlink-private-html or directadmin:php-version) - `domain_ssl` – Enable SSL, options: ON/OFF, default: ON (optional when using directadmin:createdb) - `domain_cgi` – Enable CGI, options: ON/OFF, default: ON (optional when using directadmin:createdb) - `domain_php` – Enable PHP, options: ON/OFF, default: ON (optional when using directadmin:createdb) - `domain_php_version` – Domain PHP Version, default: 1 (required when using directadmin:php-version) ## Tasks ### directadmin\:createdb {#directadmin-createdb} [Source](https://github.com/deployphp/deployer/blob/master/contrib/directadmin.php#L76) Creates a database on DirectAdmin. ### directadmin\:deletedb {#directadmin-deletedb} [Source](https://github.com/deployphp/deployer/blob/master/contrib/directadmin.php#L96) Deletes a database on DirectAdmin. ### directadmin\:createdomain {#directadmin-createdomain} [Source](https://github.com/deployphp/deployer/blob/master/contrib/directadmin.php#L111) Creates a domain on DirectAdmin. ### directadmin\:deletedomain {#directadmin-deletedomain} [Source](https://github.com/deployphp/deployer/blob/master/contrib/directadmin.php#L129) Deletes a domain on DirectAdmin. ### directadmin\:symlink-private-html {#directadmin-symlink-private-html} [Source](https://github.com/deployphp/deployer/blob/master/contrib/directadmin.php#L145) Symlink your private_html to public_html. ### directadmin\:php-version {#directadmin-php-version} [Source](https://github.com/deployphp/deployer/blob/master/contrib/directadmin.php#L161) Changes the PHP version from a domain. ================================================ FILE: docs/contrib/discord.md ================================================ # Discord Recipe ```php require 'contrib/discord.php'; ``` [Source](/contrib/discord.php) ## Installing Add hook on deploy: ```php before('deploy', 'discord:notify'); ``` ## Configuration - `discord_channel` – Discord channel ID, **required** - `discord_token` – Discord channel token, **required** - `discord_notify_text` – notification message template, markdown supported, default: ```markdown :​information_source: **{{user}}** is deploying branch `{{branch}}` to _{{where}}_ ``` - `discord_success_text` – success template, default: ```markdown :​white_check_mark: Branch `{{branch}}` deployed to _{{where}}_ successfully ``` - `discord_failure_text` – failure template, default: ```markdown :​no_entry_sign: Branch `{{branch}}` has failed to deploy to _{{where}}_ ## Usage If you want to notify only about beginning of deployment add this line only: ```php before('deploy', 'discord:notify'); ``` If you want to notify about successful end of deployment add this too: ```php after('deploy:success', 'discord:notify:success'); ``` If you want to notify about failed deployment add this too: ```php after('deploy:failed', 'discord:notify:failure'); ``` ## Configuration ### discord_webhook [Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L54) ```php title="Default value" return 'https://discordapp.com/api/webhooks/{{discord_channel}}/{{discord_token}}/slack'; ``` ### discord_notify_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L59) Deploy messages ```php title="Default value" return [ 'text' => parse(':​information_source: **{{user}}** is deploying branch `{{what}}` to _{{where}}_'), ]; ``` ### discord_success_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L64) ```php title="Default value" return [ 'text' => parse(':​white_check_mark: Branch `{{what}}` deployed to _{{where}}_ successfully'), ]; ``` ### discord_failure_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L69) ```php title="Default value" return [ 'text' => parse(':​no_entry_sign: Branch `{{what}}` has failed to deploy to _{{where}}_'), ]; ``` ### discord_message [Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L76) The message ```php title="Default value" 'discord_notify_text' ``` ## Tasks ### discord_send_message {#discord_send_message} [Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L79) Helpers ### discord\:test {#discord-test} [Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L87) Tests messages. Tasks ### discord\:notify {#discord-notify} [Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L98) Notifies Discord. ### discord\:notify\:success {#discord-notify-success} [Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L106) Notifies Discord about deploy finish. ### discord\:notify\:failure {#discord-notify-failure} [Source](https://github.com/deployphp/deployer/blob/master/contrib/discord.php#L114) Notifies Discord about deploy failure. ================================================ FILE: docs/contrib/grafana.md ================================================ # Grafana Recipe ```php require 'contrib/grafana.php'; ``` [Source](/contrib/grafana.php) ## Configuration options - **url** *(required)*: the URL to the creates annotation api endpoint. - **token** *(required)*: authentication token. Can be created at Grafana Console. - **time** *(optional)* – set deploy time of annotation. specify epoch milliseconds. (Defaults is set to the current time in epoch milliseconds.) - **tags** *(optional)* – set tag of annotation. - **text** *(optional)* – set text of annotation. (Defaults is set to "Deployed " + git log -n 1 --format="%h") ```php deploy.php set('grafana', [ 'token' => 'eyJrIj...', 'url' => 'http://grafana/api/annotations', 'tags' => ['deploy', 'production'], ]); ``` ## Usage If you want to create annotation about successful end of deployment. ```php after('deploy:success', 'grafana:annotation'); ``` ## Tasks ### grafana\:annotation {#grafana-annotation} [Source](https://github.com/deployphp/deployer/blob/master/contrib/grafana.php#L38) Creates Grafana annotation of deployment. ================================================ FILE: docs/contrib/hangouts.md ================================================ # Hangouts Recipe ```php require 'contrib/hangouts.php'; ``` [Source](/contrib/hangouts.php) Add hook on deploy: ```php before('deploy', 'chat:notify'); ``` ## Configuration - `chat_webhook` – chat incoming webhook url, **required** - `chat_title` – the title of your notification card, default `{{application}}` - `chat_subtitle` – the subtitle of your card, default `{{hostname}}` - `chat_favicon` – an image for the header of your card, default `http://{{hostname}}/favicon.png` - `chat_line1` – first line of the text in your card, default: `{{branch}}` - `chat_line2` – second line of the text in your card, default: `{{stage}}` ## Usage If you want to notify only about beginning of deployment add this line only: ```php before('deploy', 'chat:notify'); ``` If you want to notify about successful end of deployment add this too: ```php after('deploy:success', 'chat:notify:success'); ``` If you want to notify about failed deployment add this too: ```php after('deploy:failed', 'chat:notify:failure'); ``` ## Configuration ### chat_title [Source](https://github.com/deployphp/deployer/blob/master/contrib/hangouts.php#L46) Title of project ```php title="Default value" return get('application', 'Project'); ``` ### chat_subtitle [Source](https://github.com/deployphp/deployer/blob/master/contrib/hangouts.php#L50) ```php title="Default value" get('hostname') ``` ### favicon [Source](https://github.com/deployphp/deployer/blob/master/contrib/hangouts.php#L53) If 'favicon' is set Google Hangouts Chat will decorate your card with an image. ```php title="Default value" 'http://{{hostname}}/favicon.png' ``` ### chat_line1 [Source](https://github.com/deployphp/deployer/blob/master/contrib/hangouts.php#L56) Deploy messages ```php title="Default value" '{{branch}}' ``` ### chat_line2 [Source](https://github.com/deployphp/deployer/blob/master/contrib/hangouts.php#L57) ```php title="Default value" '{{stage}}' ``` ## Tasks ### chat\:notify {#chat-notify} [Source](https://github.com/deployphp/deployer/blob/master/contrib/hangouts.php#L60) Notifies Google Hangouts Chat. ### chat\:notify\:success {#chat-notify-success} [Source](https://github.com/deployphp/deployer/blob/master/contrib/hangouts.php#L102) Notifies Google Hangouts Chat about deploy finish. ### chat\:notify\:failure {#chat-notify-failure} [Source](https://github.com/deployphp/deployer/blob/master/contrib/hangouts.php#L144) Notifies Google Hangouts Chat about deploy failure. ================================================ FILE: docs/contrib/hipchat.md ================================================ # Hipchat Recipe ```php require 'contrib/hipchat.php'; ``` [Source](/contrib/hipchat.php) ## Configuration - `hipchat_token` – Hipchat V1 auth token - `hipchat_room_id` – Room ID or name - `hipchat_message` – Deploy message, default is `_{{user}}_ deploying `{{what}}` to *{{where}}*` - `hipchat_from` – Default to target - `hipchat_color` – Message color, default is **green** - `hipchat_url` – The URL to the message endpoint, default is https://api.hipchat.com/v1/rooms/message ## Usage Since you should only notify Hipchat room of a successful deployment, the `hipchat:notify` task should be executed right at the end. ```php after('deploy', 'hipchat:notify'); ``` ## Configuration ### hipchat_color [Source](https://github.com/deployphp/deployer/blob/master/contrib/hipchat.php#L26) ```php title="Default value" 'green' ``` ### hipchat_from [Source](https://github.com/deployphp/deployer/blob/master/contrib/hipchat.php#L27) ```php title="Default value" '{{where}}' ``` ### hipchat_message [Source](https://github.com/deployphp/deployer/blob/master/contrib/hipchat.php#L28) ```php title="Default value" '_{{user}}_ deploying `{{what}}` to *{{where}}*' ``` ### hipchat_url [Source](https://github.com/deployphp/deployer/blob/master/contrib/hipchat.php#L29) ```php title="Default value" 'https://api.hipchat.com/v1/rooms/message' ``` ## Tasks ### hipchat\:notify {#hipchat-notify} [Source](https://github.com/deployphp/deployer/blob/master/contrib/hipchat.php#L32) Notifies Hipchat channel of deployment. ================================================ FILE: docs/contrib/ispmanager.md ================================================ # Ispmanager Recipe ```php require 'contrib/ispmanager.php'; ``` [Source](/contrib/ispmanager.php) This recipe for work with ISPManager Lite panel by API. ## Configuration ### ispmanager_owner [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L11) ```php title="Default value" 'www-root' ``` ### ispmanager_doc_root [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L12) ```php title="Default value" '/var/www/' . get('ispmanager_owner') . '/data/' ``` ### ispmanager [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L15) ISPManager default configuration ```php title="Default value" [ 'api' => [ 'dsn' => 'https://root:password@localhost:1500/ispmgr', 'secure' => true, ], 'createDomain' => null, 'updateDomain' => null, 'deleteDomain' => null, 'createDatabase' => null, 'deleteDatabase' => null, 'phpSelect' => null, 'createAlias' => null, 'deleteAlias' => null, ] ``` ### vhost [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L31) Vhost default configuration ```php title="Default value" [ 'name' => '{{domain}}', 'php_enable' => 'on', 'aliases' => 'www.{{domain}}', 'home' => 'www/{{domain}}', 'owner' => get('ispmanager_owner'), 'email' => 'webmaster@{{domain}}', 'charset' => 'off', 'dirindex' => 'index.php uploaded.html', 'ssi' => 'on', 'php' => 'on', 'php_mode' => 'php_mode_mod', 'basedir' => 'on', 'php_apache_version' => 'native', 'cgi' => 'off', 'log_access' => 'on', 'log_error' => 'on', ] ``` ### ispmanager_session [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L51) Storage ### ispmanager_databases [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L52) ```php title="Default value" [ 'servers' => [], 'hosts' => [], 'dblist' => [], ] ``` ### ispmanager_domains [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L58) ### ispmanager_phplist [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L59) ### ispmanager_aliaslist [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L60) ## Tasks ### ispmanager\:init {#ispmanager-init} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L63) Installs ispmanager. ### ispmanager\:db-server-list {#ispmanager-db-server-list} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L86) Takes database servers list. ### ispmanager\:db-list {#ispmanager-db-list} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L123) Takes databases list. ### ispmanager\:domain-list {#ispmanager-domain-list} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L145) Takes domain list. ### ispmanager\:db-create {#ispmanager-db-create} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L161) Creates new database. ### ispmanager\:db-delete {#ispmanager-db-delete} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L231) Deletes database. ### ispmanager\:domain-create {#ispmanager-domain-create} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L282) Creates new domain. ### ispmanager\:get-php-list {#ispmanager-get-php-list} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L328) Gets allowed PHP modes and versions. ### ispmanager\:print-php-list {#ispmanager-print-php-list} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L374) Prints allowed PHP modes and versions. ### ispmanager\:domain-php-select {#ispmanager-domain-php-select} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L412) Switches PHP version for domain. ### ispmanager\:domain-alias-create {#ispmanager-domain-alias-create} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L481) Creates new domain alias. ### ispmanager\:domain-alias-delete {#ispmanager-domain-alias-delete} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L550) Deletes domain alias. ### ispmanager\:domain-delete {#ispmanager-domain-delete} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L618) Deletes domain. ### ispmanager\:process {#ispmanager-process} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ispmanager.php#L665) Auto task processing. ================================================ FILE: docs/contrib/mattermost.md ================================================ # Mattermost Recipe ```php require 'contrib/mattermost.php'; ``` [Source](/contrib/mattermost.php) ## Installing Create a Mattermost incoming webhook, through the administration panel. Add hook on deploy: ``` before('deploy', 'mattermost:notify'); ``` ## Configuration - `mattermost_webhook` - incoming mattermost webook **required** ``` set('mattermost_webook', 'https://{your-mattermost-site}/hooks/xxx-generatedkey-xxx'); ``` - `mattermost_channel` - overrides the channel the message posts in ``` set('mattermost_channel', 'town-square'); ``` - `mattermost_username` - overrides the username the message posts as ``` set('mattermost_username', 'deployer'); ``` - `mattermost_icon_url` - overrides the profile picture the message posts with ``` set('mattermost_icon_url', 'https://domain.com/your-icon.png'); ``` - `mattermost_text` - notification message ``` set('mattermost_text', '_{{user}}_ deploying `{{what}}` to **{{where}}**'); ``` - `mattermost_success_text` – success template, default: ``` set('mattermost_success_text', 'Deploy to **{{where}}** successful {{mattermost_success_emoji}}'); ``` - `mattermost_failure_text` – failure template, default: ``` set('mattermost_failure_text', 'Deploy to **{{where}}** failed {{mattermost_failure_emoji}}'); ``` - `mattermost_success_emoji` – emoji added at the end of success text - `mattermost_failure_emoji` – emoji added at the end of failure text For detailed information about Mattermost hooks see: https://developers.mattermost.com/integrate/incoming-webhooks/ ## Usage If you want to notify only about beginning of deployment add this line only: ```php before('deploy', 'mattermost:notify'); ``` If you want to notify about successful end of deployment add this too: ```php after('deploy:success', 'mattermost:notify:success'); ``` If you want to notify about failed deployment add this too: ```php after('deploy:failed', 'mattermost:notify:failure'); ``` ## Configuration ### mattermost_webhook [Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L81) ```php title="Default value" null ``` ### mattermost_channel [Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L82) ```php title="Default value" null ``` ### mattermost_username [Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L83) ```php title="Default value" 'deployer' ``` ### mattermost_icon_url [Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L84) ```php title="Default value" null ``` ### mattermost_success_emoji [Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L86) ```php title="Default value" ':​white_check_mark:' ``` ### mattermost_failure_emoji [Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L87) ```php title="Default value" ':​x:' ``` ### mattermost_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L89) ```php title="Default value" '_{{user}}_ deploying `{{what}}` to **{{where}}**' ``` ### mattermost_success_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L90) ```php title="Default value" 'Deploy to **{{where}}** successful {{mattermost_success_emoji}}' ``` ### mattermost_failure_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L91) ```php title="Default value" 'Deploy to **{{where}}** failed {{mattermost_failure_emoji}}' ``` ## Tasks ### mattermost\:notify {#mattermost-notify} [Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L94) Notifies mattermost. ### mattermost\:notify\:success {#mattermost-notify-success} [Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L115) Notifies mattermost about deploy finish. ### mattermost\:notify\:failure {#mattermost-notify-failure} [Source](https://github.com/deployphp/deployer/blob/master/contrib/mattermost.php#L136) Notifies mattermost about deploy failure. ================================================ FILE: docs/contrib/ms-teams.md ================================================ # Ms-teams Recipe ```php require 'contrib/ms-teams.php'; ``` [Source](/contrib/ms-teams.php) ## Installing Require ms-teams recipe in your `deploy.php` file: Setup: 1. Open MS Teams 2. Navigate to Teams section 3. Select existing or create new team 4. Select existing or create new channel 5. Hover over channel to get three dots, click, in menu select "Connectors" 6. Search for and configure "Incoming Webhook" 7. Confirm/create and copy your Webhook URL 8. Setup deploy.php Add in header: ```php require 'contrib/ms-teams.php'; set('teams_webhook', 'https://outlook.office.com/webhook/...'); ``` Add in content: ```php before('deploy', 'teams:notify'); after('deploy:success', 'teams:notify:success'); after('deploy:failed', 'teams:notify:failure'); ``` 9.) Sip your coffee ## Configuration - `teams_webhook` – teams incoming webhook url, **required** ``` set('teams_webhook', 'https://outlook.office.com/webhook/...'); ``` - `teams_title` – the title of application, default `{{application}}` - `teams_text` – notification message template, markdown supported ``` set('teams_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*'); ``` - `teams_success_text` – success template, default: ``` set('teams_success_text', 'Deploy to *{{where}}* successful'); ``` - `teams_failure_text` – failure template, default: ``` set('teams_failure_text', 'Deploy to *{{where}}* failed'); ``` - `teams_color` – color's attachment - `teams_success_color` – success color's attachment - `teams_failure_color` – failure color's attachment ## Usage If you want to notify only about beginning of deployment add this line only: ```php before('deploy', 'teams:notify'); ``` If you want to notify about successful end of deployment add this too: ```php after('deploy:success', 'teams:notify:success'); ``` If you want to notify about failed deployment add this too: ```php after('deploy:failed', 'teams:notify:failure'); ``` ## Configuration ### teams_title [Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L79) Title of project ```php title="Default value" return get('application', 'Project'); ``` ### teams_failure_continue [Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L84) Allow Continue on Failure ```php title="Default value" false ``` ### teams_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L87) Deploy message ```php title="Default value" '_{{user}}_ deploying `{{what}}` to *{{where}}*' ``` ### teams_success_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L88) ```php title="Default value" 'Deploy to *{{where}}* successful' ``` ### teams_failure_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L89) ```php title="Default value" 'Deploy to *{{where}}* failed' ``` ### teams_color [Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L92) Color of attachment ```php title="Default value" '#4d91f7' ``` ### teams_success_color [Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L93) ```php title="Default value" '#00c100' ``` ### teams_failure_color [Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L94) ```php title="Default value" '#ff0909' ``` ## Tasks ### teams\:notify {#teams-notify} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L97) Notifies Teams. ### teams\:notify\:success {#teams-notify-success} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L121) Notifies Teams about deploy finish. ### teams\:notify\:failure {#teams-notify-failure} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ms-teams.php#L144) Notifies Teams about deploy failure. ================================================ FILE: docs/contrib/newrelic.md ================================================ # Newrelic Recipe ```php require 'contrib/newrelic.php'; ``` [Source](/contrib/newrelic.php) ## Configuration - `newrelic_app_id` – newrelic's app id - `newrelic_api_key` – newrelic's api key - `newrelic_description` – message to send - `newrelic_endpoint` – newrelic's REST API endpoint ## Usage Since you should only notify New Relic of a successful deployment, the `newrelic:notify` task should be executed right at the end. ```php after('deploy', 'newrelic:notify'); ``` ## Configuration ### newrelic_app_id [Source](https://github.com/deployphp/deployer/blob/master/contrib/newrelic.php#L24) :::info Required Throws exception if not set. ::: ### newrelic_description [Source](https://github.com/deployphp/deployer/blob/master/contrib/newrelic.php#L28) ```php title="Default value" return runLocally('git log -n 1 --format="%an: %s" | tr \'"\' "\'"'); ``` ### newrelic_revision [Source](https://github.com/deployphp/deployer/blob/master/contrib/newrelic.php#L32) ```php title="Default value" return runLocally('git log -n 1 --format="%h"'); ``` ### newrelic_endpoint [Source](https://github.com/deployphp/deployer/blob/master/contrib/newrelic.php#L36) ```php title="Default value" 'api.newrelic.com' ``` ## Tasks ### newrelic\:notify {#newrelic-notify} [Source](https://github.com/deployphp/deployer/blob/master/contrib/newrelic.php#L39) Notifies New Relic of deployment. ================================================ FILE: docs/contrib/npm.md ================================================ # Npm Recipe ```php require 'contrib/npm.php'; ``` [Source](/contrib/npm.php) ## Configuration - `bin/npm` *(optional)*: set npm binary, automatically detected otherwise. ## Usage ```php after('deploy:update_code', 'npm:install'); ``` ## Configuration ### bin/npm [Source](https://github.com/deployphp/deployer/blob/master/contrib/npm.php#L17) ## Configuration - `bin/npm` *(optional)*: set npm binary, automatically detected otherwise. ## Usage ```php after('deploy:update_code', 'npm:install'); ``` ```php title="Default value" return which('npm'); ``` ## Tasks ### npm\:install {#npm-install} [Source](https://github.com/deployphp/deployer/blob/master/contrib/npm.php#L27) Installs npm packages. Uses `npm ci` command. This command is similar to npm install, except it's meant to be used in automated environments such as test platforms, continuous integration, and deployment -- or any situation where you want to make sure you're doing a clean install of your dependencies. ================================================ FILE: docs/contrib/ntfy.md ================================================ # Ntfy Recipe ```php require 'contrib/ntfy.php'; ``` [Source](/contrib/ntfy.php) ## Installing Require ntfy.sh recipe in your `deploy.php` file: Setup: 1. Setup deploy.php Add in header: ```php require 'contrib/ntfy.php'; set('ntfy_topic', 'ntfy.sh/mytopic'); ``` Add in content: ```php before('deploy', 'ntfy:notify'); after('deploy:success', 'ntfy:notify:success'); after('deploy:failed', 'ntfy:notify:failure'); ``` 9.) Sip your coffee ## Configuration - `ntfy_server` – ntfy server url, default `ntfy.sh` ``` set('ntfy_server', 'ntfy.sh'); ``` - `ntfy_topic` – ntfy topic, **required** ``` set('ntfy_topic', 'mysecrettopic'); ``` - `ntfy_title` – the title of the message, default `{{application}}` - `ntfy_text` – notification message template ``` set('ntfy_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*'); ``` - `ntfy_tags` – notification message tags / emojis (comma separated) ``` set('ntfy_tags', `information_source`); ``` - `ntfy_priority` – notification message priority (integer) ``` set('ntfy_priority', 5); ``` - `ntfy_success_text` – success template, default: ``` set('ntfy_success_text', 'Deploy to *{{where}}* successful'); ``` - `ntfy_success_tags` – success tags / emojis (comma separated) ``` set('ntfy_success_tags', `white_check_mark,champagne`); ``` - `ntfy_success_priority` – success notification message priority - `ntfy_failure_text` – failure template, default: ``` set('ntfy_failure_text', 'Deploy to *{{where}}* failed'); ``` - `ntfy_failure_tags` – failure tags / emojis (comma separated) ``` set('ntfy_failure_tags', `warning,skull`); ``` - `ntfy_failure_priority` – failure notification message priority ## Usage If you want to notify only about beginning of deployment add this line only: ```php before('deploy', 'ntfy:notify'); ``` If you want to notify about successful end of deployment add this too: ```php after('deploy:success', 'ntfy:notify:success'); ``` If you want to notify about failed deployment add this too: ```php after('deploy:failed', 'ntfy:notify:failure'); ``` ## Configuration ### ntfy_server [Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L90) ```php title="Default value" 'ntfy.sh' ``` ### ntfy_title [Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L93) Title of project ```php title="Default value" return get('application', 'Project'); ``` ### ntfy_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L98) Deploy message ```php title="Default value" '_{{user}}_ deploying `{{what}}` to *{{where}}*' ``` ### ntfy_success_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L99) ```php title="Default value" 'Deploy to *{{where}}* successful' ``` ### ntfy_failure_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L100) ```php title="Default value" 'Deploy to *{{where}}* failed' ``` ### ntfy_tags [Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L103) Message tags ### ntfy_success_tags [Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L104) ### ntfy_failure_tags [Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L105) ## Tasks ### ntfy\:notify {#ntfy-notify} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L108) Notifies ntfy server. ### ntfy\:notify\:success {#ntfy-notify-success} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L126) Notifies ntfy server about deploy finish. ### ntfy\:notify\:failure {#ntfy-notify-failure} [Source](https://github.com/deployphp/deployer/blob/master/contrib/ntfy.php#L144) Notifies ntfy server about deploy failure. ================================================ FILE: docs/contrib/phinx.md ================================================ # Phinx Recipe ```php require 'contrib/phinx.php'; ``` [Source](/contrib/phinx.php) ## Configuration options All options are in the config parameter `phinx` specified as an array (instead of the `phinx_path` variable). All parameters are *optional*, but you can specify them with a dictionary (to change all parameters) or by deployer dot notation (to change one option). ### Phinx params - `phinx.environment` - `phinx.date` - `phinx.configuration` N.B. current directory is the project directory - `phinx.target` - `phinx.seed` - `phinx.parser` - `phinx.remove-all` (pass empty string as value) ### Phinx path params - `phinx_path` Specify phinx path (by default phinx is searched for in $PATH, ./vendor/bin and ~/.composer/vendor/bin) ### Example of usage ```php $phinx_env_vars = [ 'environment' => 'development', 'configuration' => './migration/.phinx.yml', 'target' => '20120103083322', 'remove-all' => '', ]; set('phinx_path', '/usr/local/phinx/bin/phinx'); set('phinx', $phinx_env_vars); after('cleanup', 'phinx:migrate'); or set it for a specific server host('dev') ->user('user') ->set('deploy_path', '/var/www') ->set('phinx', $phinx_env_vars) ->set('phinx_path', ''); ``` ## Suggested Usage You can run all tasks before or after any tasks (but you need to specify external configs for phinx). If you use internal configs (which are in your project) you need to run it after the `deploy:update_code` task is completed. ## Read more For further reading see [phinx.org](https://phinx.org). Complete descriptions of all possible options can be found on the [commands page](http://docs.phinx.org/en/latest/commands.html). ## Configuration ### bin/phinx [Source](https://github.com/deployphp/deployer/blob/master/contrib/phinx.php#L81) Phinx recipe for Deployer @author Alexey Boyko @contributor Security-Database @copyright 2016 Alexey Boyko @license MIT https://github.com/deployphp/recipes/blob/master/LICENSE @link https://github.com/deployphp/recipes @see http://deployer.org @see https://phinx.org Path to Phinx :::info Autogenerated The value of this configuration is autogenerated on access. ::: ## Tasks ### phinx\:migrate {#phinx-migrate} [Source](https://github.com/deployphp/deployer/blob/master/contrib/phinx.php#L149) Migrats database with phinx. ### phinx\:rollback {#phinx-rollback} [Source](https://github.com/deployphp/deployer/blob/master/contrib/phinx.php#L170) Rollbacks database migrations with phinx. ### phinx\:seed {#phinx-seed} [Source](https://github.com/deployphp/deployer/blob/master/contrib/phinx.php#L191) Seeds database with phinx. ### phinx\:breakpoint {#phinx-breakpoint} [Source](https://github.com/deployphp/deployer/blob/master/contrib/phinx.php#L211) Sets a migrations breakpoint with phinx. ================================================ FILE: docs/contrib/php-fpm.md ================================================ # Php-fpm Recipe ```php require 'contrib/php-fpm.php'; ``` [Source](/contrib/php-fpm.php) :::caution Do **not** reload php-fpm. Some user requests could fail or not complete in the process of reloading. Instead, configure your server [properly](avoid-php-fpm-reloading). If you're using Deployer's provision recipe, it's already configured the right way and no php-fpm reload is needed. ::: ## Configuration - `php_fpm_version` – The PHP-fpm version. For example: `8.0`. - `php_fpm_service` – The full name of the PHP-fpm service. Defaults to `php{{php_fpm_version}}-fpm`. - `php_fpm_command` – The command to run to reload PHP-fpm. Defaults to `sudo systemctl reload {{php_fpm_service}}`. ## Usage Start by explicitely providing the current version of PHP-version using the `php_fpm_version`. Alternatively, you may use any of the options above to configure how PHP-fpm should reload. Then, add the `php-fpm:reload` task at the end of your deployments by using the `after` method like so. ```php set('php_fpm_version', '8.0'); after('deploy', 'php-fpm:reload'); ``` ## Configuration ### php_fpm_version [Source](https://github.com/deployphp/deployer/blob/master/contrib/php-fpm.php#L35) :::caution Do **not** reload php-fpm. Some user requests could fail or not complete in the process of reloading. Instead, configure your server [properly](avoid-php-fpm-reloading). If you're using Deployer's provision recipe, it's already configured the right way and no php-fpm reload is needed. ::: ## Configuration - `php_fpm_version` – The PHP-fpm version. For example: `8.0`. - `php_fpm_service` – The full name of the PHP-fpm service. Defaults to `php[php_fpm_version](/docs/contrib/php-fpm.md#php_fpm_version)-fpm`. - `php_fpm_command` – The command to run to reload PHP-fpm. Defaults to `sudo systemctl reload [php_fpm_service](/docs/contrib/php-fpm.md#php_fpm_service)`. ## Usage Start by explicitely providing the current version of PHP-version using the `php_fpm_version`. Alternatively, you may use any of the options above to configure how PHP-fpm should reload. Then, add the `php-fpm:reload` task at the end of your deployments by using the `after` method like so. ```php set('php_fpm_version', '8.0'); after('deploy', 'php-fpm:reload'); ``` Automatically detects by using [bin/php](/docs/recipe/common.md#bin/php). ```php title="Default value" return run('{{bin/php}} -r "printf(\'%d.%d\', PHP_MAJOR_VERSION, PHP_MINOR_VERSION);"'); ``` ### php_fpm_service [Source](https://github.com/deployphp/deployer/blob/master/contrib/php-fpm.php#L39) ```php title="Default value" 'php{{php_fpm_version}}-fpm' ``` ## Tasks ### php-fpm\:reload {#php-fpm-reload} [Source](https://github.com/deployphp/deployer/blob/master/contrib/php-fpm.php#L42) Reloads the php-fpm service. ================================================ FILE: docs/contrib/rabbit.md ================================================ # Rabbit Recipe ```php require 'contrib/rabbit.php'; ``` [Source](/contrib/rabbit.php) ### Installing ```php deploy.php require 'recipe/rabbit.php'; ``` ### Configuration options - **rabbit** *(required)*: accepts an *array* with the connection information to [rabbitmq](http://www.rabbitmq.com) server token and team name. You can provide also other configuration options: - *host* - default is localhost - *port* - default is 5672 - *username* - default is *guest* - *password* - default is *guest* - *channel* - no default value, need to be specified via config - *message* - default is **Deployment to '$host' on *$prod* was successful\n$releasePath** - *vhost* - default is ```php deploy.php set('rabbit', [ 'host' => 'localhost', 'port' => '5672', 'username' => 'guest', 'password' => 'guest', 'channel' => 'notify-channel', 'vhost' => '/my-app' ]); ``` ### Suggested Usage Since you should only notify RabbitMQ channel of a successful deployment, the `deploy:rabbit` task should be executed right at the end. ```php deploy.php before('deploy:end', 'deploy:rabbit'); ``` ## Tasks ### deploy\:rabbit {#deploy-rabbit} [Source](https://github.com/deployphp/deployer/blob/master/contrib/rabbit.php#L58) Notifies RabbitMQ channel about deployment. ================================================ FILE: docs/contrib/raygun.md ================================================ # Raygun Recipe ```php require 'contrib/raygun.php'; ``` [Source](/contrib/raygun.php) ## Configuration - `raygun_api_key` – the API key of your Raygun application - `raygun_version` – the version of your application that this deployment is releasing - `raygun_owner_name` – the name of the person creating this deployment - `raygun_email` – the email of the person creating this deployment - `raygun_comment` – the deployment notes - `raygun_scm_identifier` – the commit that this deployment was built off - `raygun_scm_type` - the source control system you use ## Usage To notify Raygun of a successful deployment, you can use the 'raygun:notify' task after a deployment. ```php after('deploy', 'raygun:notify'); ``` ## Tasks ### raygun\:notify {#raygun-notify} [Source](https://github.com/deployphp/deployer/blob/master/contrib/raygun.php#L28) Notifies Raygun of deployment. ================================================ FILE: docs/contrib/rocketchat.md ================================================ # Rocketchat Recipe ```php require 'contrib/rocketchat.php'; ``` [Source](/contrib/rocketchat.php) ## Installing Create a RocketChat incoming webhook, through the administration panel. Add hook on deploy: ``` before('deploy', 'rocketchat:notify'); ``` ## Configuration - `rocketchat_webhook` - incoming rocketchat webook **required** ``` set('rocketchat_webhook', 'https://rocketchat.yourcompany.com/hooks/XXXXX'); ``` - `rocketchat_title` - the title of the application, defaults to `{{application}}` - `rocketchat_text` - notification message ``` set('rocketchat_text', '_{{user}}_ deploying {{what}} to {{where}}'); ``` - `rocketchat_success_text` – success template, default: ``` set('rocketchat_success_text', 'Deploy to *{{where}}* successful'); ``` - `rocketchat_failure_text` – failure template, default: ``` set('rocketchat_failure_text', 'Deploy to *{{where}}* failed'); ``` - `rocketchat_color` – color's attachment - `rocketchat_success_color` – success color's attachment - `rocketchat_failure_color` – failure color's attachment ## Usage If you want to notify only about beginning of deployment add this line only: ```php before('deploy', 'rocketchat:notify'); ``` If you want to notify about successful end of deployment add this too: ```php after('deploy:success', 'rocketchat:notify:success'); ``` If you want to notify about failed deployment add this too: ```php after('deploy:failed', 'rocketchat:notify:failure'); ``` ## Configuration ### rockchat_title [Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L65) ```php title="Default value" return get('application', 'Project'); ``` ### rocketchat_icon_emoji [Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L69) ```php title="Default value" ':robot:' ``` ### rocketchat_icon_url [Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L70) ```php title="Default value" null ``` ### rocketchat_channel [Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L72) ```php title="Default value" null ``` ### rocketchat_room_id [Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L73) ```php title="Default value" null ``` ### rocketchat_username [Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L74) ```php title="Default value" null ``` ### rocketchat_webhook [Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L75) ```php title="Default value" null ``` ### rocketchat_color [Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L77) ```php title="Default value" '#000000' ``` ### rocketchat_success_color [Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L78) ```php title="Default value" '#00c100' ``` ### rocketchat_failure_color [Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L79) ```php title="Default value" '#ff0909' ``` ### rocketchat_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L81) ```php title="Default value" '_{{user}}_ deploying `{{what}}` to *{{where}}*' ``` ### rocketchat_success_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L82) ```php title="Default value" 'Deploy to *{{where}}* successful' ``` ### rocketchat_failure_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L83) ```php title="Default value" 'Deploy to *{{where}}* failed' ``` ## Tasks ### rocketchat\:notify {#rocketchat-notify} [Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L86) Notifies RocketChat. ### rocketchat\:notify\:success {#rocketchat-notify-success} [Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L116) Notifies RocketChat about deploy finish. ### rocketchat\:notify\:failure {#rocketchat-notify-failure} [Source](https://github.com/deployphp/deployer/blob/master/contrib/rocketchat.php#L146) Notifies RocketChat about deploy failure. ================================================ FILE: docs/contrib/rollbar.md ================================================ # Rollbar Recipe ```php require 'contrib/rollbar.php'; ``` [Source](/contrib/rollbar.php) ## Configuration - `rollbar_token` – access token to rollbar api - `rollbar_comment` – comment about deploy, default to ```php set('rollbar_comment', '_{{user}}_ deploying `{{what}}` to *{{where}}*'); ``` - `rollbar_username` – rollbar user name ## Usage Since you should only notify Rollbar channel of a successful deployment, the `rollbar:notify` task should be executed right at the end. ```php after('deploy', 'rollbar:notify'); ``` ## Configuration ### rollbar_comment [Source](https://github.com/deployphp/deployer/blob/master/contrib/rollbar.php#L27) ```php title="Default value" '_{{user}}_ deploying `{{what}}` to *{{where}}*' ``` ## Tasks ### rollbar\:notify {#rollbar-notify} [Source](https://github.com/deployphp/deployer/blob/master/contrib/rollbar.php#L30) Notifies Rollbar of deployment. ================================================ FILE: docs/contrib/rsync.md ================================================ # Rsync Recipe ```php require 'contrib/rsync.php'; ``` [Source](/contrib/rsync.php) :::warning This must not be confused with `/src/Utility/Rsync.php`, deployer's built-in rsync. Their configuration options are also very different, read carefully below. ::: ## Configuration options - **rsync**: Accepts an array with following rsync options (all are optional and defaults are ok): - *exclude*: accepts an *array* with patterns to be excluded from sending to server - *exclude-file*: accepts a *string* containing absolute path to file, which contains exclude patterns - *include*: accepts an *array* with patterns to be included in sending to server - *include-file*: accepts a *string* containing absolute path to file, which contains include patterns - *filter*: accepts an *array* of rsync filter rules - *filter-file*: accepts a *string* containing merge-file filename. - *filter-perdir*: accepts a *string* containing merge-file filename to be scanned and merger per each directory in rsync list on files to send - *flags*: accepts a *string* of flags to set when calling rsync command. Please **avoid** flags that accept params, and use *options* instead. - *options*: accepts an *array* of options to set when calling rsync command. **DO NOT** prefix options with `--` as it's automatically added. - *timeout*: accepts an *int* defining timeout for rsync command to run locally. ### Sample Configuration: Following is default configuration. By default rsync ignores only git dir and `deploy.php` file. ```php deploy.php set('rsync',[ 'exclude' => [ '.git', 'deploy.php', ], 'exclude-file' => false, 'include' => [], 'include-file' => false, 'filter' => [], 'filter-file' => false, 'filter-perdir'=> false, 'flags' => 'rz', // Recursive, with compress 'options' => ['delete'], 'timeout' => 60, ]); ``` If You have multiple excludes, You can put them in file and reference that instead. If You use `deploy:rsync_warmup` You could set additional options that could speed-up and/or affect way things are working. For example: ```php deploy.php set('rsync',[ 'exclude' => ['excludes_file'], 'exclude-file' => '/tmp/localdeploys/excludes_file', //Use absolute path to avoid possible rsync problems 'include' => [], 'include-file' => false, 'filter' => [], 'filter-file' => false, 'filter-perdir' => false, 'flags' => 'rzcE', // Recursive, with compress, check based on checksum rather than time/size, preserve Executable flag 'options' => ['delete', 'delete-after', 'force'], //Delete after successful transfer, delete even if deleted dir is not empty 'timeout' => 3600, //for those huge repos or crappy connection ]); ``` ### Parameter - **rsync_src**: per-host rsync source. This can be server, stage or whatever-dependent. By default it's set to current directory - **rsync_dest**: per-host rsync destination. This can be server, stage or whatever-dependent. by default it's equivalent to release deploy destination. ### Sample configurations: This is default configuration: ```php set('rsync_src', __DIR__); set('rsync_dest','{{release_path}}'); ``` If You use local deploy recipe You can set src to local release: ```php host('hostname') ->hostname('10.10.10.10') ->port(22) ->set('deploy_path','/your/remote/path/app') ->set('rsync_src', '/your/local/path/app') ->set('rsync_dest','{{release_path}}'); ``` ## Usage - `rsync` task Set `rsync_src` to locally cloned repository and rsync to `rsync_dest`. Then set this task instead of `deploy:update_code` in Your `deploy` task if Your hosting provider does not allow git. - `rsync:warmup` task If Your deploy task looks like: ```php task('deploy', [ 'deploy:prepare', 'deploy:release', 'rsync', 'deploy:vendors', 'deploy:symlink', ])->desc('Deploy your project'); ``` And Your `rsync_dest` is set to `{{release_path}}` then You could add this task to run before `rsync` task or after `deploy:release`, whatever is more convenient. ## Configuration ### rsync [Source](https://github.com/deployphp/deployer/blob/master/contrib/rsync.php#L119) ```php title="Default value" [ 'exclude' => [ '.git', 'deploy.php', ], 'exclude-file' => false, 'include' => [], 'include-file' => false, 'filter' => [], 'filter-file' => false, 'filter-perdir' => false, 'flags' => 'rz', 'options' => ['delete'], 'timeout' => 300, ] ``` ### rsync_src [Source](https://github.com/deployphp/deployer/blob/master/contrib/rsync.php#L135) ```php title="Default value" __DIR__ ``` ### rsync_dest [Source](https://github.com/deployphp/deployer/blob/master/contrib/rsync.php#L136) ```php title="Default value" '{{release_path}}' ``` ### rsync_excludes [Source](https://github.com/deployphp/deployer/blob/master/contrib/rsync.php#L138) :::info Autogenerated The value of this configuration is autogenerated on access. ::: ### rsync_includes [Source](https://github.com/deployphp/deployer/blob/master/contrib/rsync.php#L153) :::info Autogenerated The value of this configuration is autogenerated on access. ::: ### rsync_filter [Source](https://github.com/deployphp/deployer/blob/master/contrib/rsync.php#L168) :::info Autogenerated The value of this configuration is autogenerated on access. ::: ### rsync_options [Source](https://github.com/deployphp/deployer/blob/master/contrib/rsync.php#L186) :::info Autogenerated The value of this configuration is autogenerated on access. ::: ## Tasks ### rsync\:warmup {#rsync-warmup} [Source](https://github.com/deployphp/deployer/blob/master/contrib/rsync.php#L198) Warmups remote Rsync target. ### rsync {#rsync} [Source](https://github.com/deployphp/deployer/blob/master/contrib/rsync.php#L213) Rsync local->remote. ================================================ FILE: docs/contrib/sentry.md ================================================ # Sentry Recipe ```php require 'contrib/sentry.php'; ``` [Source](/contrib/sentry.php) ### Configuration options - **organization** *(required)*: the slug of the organization the release belongs to. - **projects** *(required)*: array of slugs of the projects to create a release for. - **token** *(required)*: authentication token. Can be created at [https://sentry.io/settings/account/api/auth-tokens/] - **version** *(required)* – a version identifier for this release. Can be a version number, a commit hash etc. (Defaults is set to git log -n 1 --format="%h".) - **version_prefix** *(optional)* - a string prefixed to version. Releases are global per organization so indipentent projects needs to prefix version number with unique string to avoid conflicts - **environment** *(optional)* - the environment you’re deploying to. By default framework's environment is used. For example for symfony, *symfony_env* configuration is read otherwise defaults to 'prod'. - **ref** *(optional)* – an optional commit reference. This is useful if a tagged version has been provided. - **refs** *(optional)* - array to indicate the start and end commits for each repository included in a release. Head commits must include parameters *repository* and *commit*) (the HEAD sha). They can optionally include *previousCommit* (the sha of the HEAD of the previous release), which should be specified if this is the first time you’ve sent commit data. - **commits** *(optional)* - array commits data to be associated with the release. Commits must include parameters *id* (the sha of the commit), and can optionally include *repository*, *message*, *author_name*, *author_email* and *timestamp*. By default will send all new commits, unless it's a first release, then only first 200 will be sent. - **url** *(optional)* – a URL that points to the release. This can be the path to an online interface to the sourcecode for instance. - **date_released** *(optional)* – date that indicates when the release went live. If not provided the current time is assumed. - **sentry_server** *(optional)* – sentry server (if you host it yourself). defaults to hosted sentry service. - **date_deploy_started** *(optional)* - date that indicates when the deploy started. Defaults to current time. - **date_deploy_finished** *(optional)* - date that indicates when the deploy ended. If not provided, the current time is used. - **deploy_name** *(optional)* - name of the deploy - **git_version_command** *(optional)* - the command that retrieves the git version information (Defaults is set to git log -n 1 --format="%h", other options are git describe --tags --abbrev=0) ```php deploy.php set('sentry', [ 'organization' => 'exampleorg', 'projects' => [ 'exampleproj' ], 'token' => 'd47828...', 'version' => '0.0.1', ]); ``` ### Suggested Usage Since you should only notify Sentry of a successful deployment, the deploy:sentry task should be executed right at the end. ```php deploy.php after('deploy', 'deploy:sentry'); ``` ================================================ FILE: docs/contrib/slack.md ================================================ # Slack Recipe ```php require 'contrib/slack.php'; ``` [Source](/contrib/slack.php) ## Installing Add to Slack Add hook on deploy: ```php before('deploy', 'slack:notify'); ``` ## Configuration - `slack_webhook` – slack incoming webhook url, **required** ``` set('slack_webhook', 'https://hooks.slack.com/...'); ``` - `slack_channel` - channel to send notification to. The default is the channel configured in the webhook - `slack_title` – the title of application, default `{{application}}` - `slack_text` – notification message template, markdown supported ``` set('slack_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*'); ``` - `slack_success_text` – success template, default: ``` set('slack_success_text', 'Deploy to *{{where}}* successful'); ``` - `slack_failure_text` – failure template, default: ``` set('slack_failure_text', 'Deploy to *{{where}}* failed'); ``` - `slack_color` – color's attachment - `slack_success_color` – success color's attachment - `slack_failure_color` – failure color's attachment - `slack_fields` - set attachments fields for pretty output in Slack, default: ``` set('slack_fields', []); ``` ## Usage If you want to notify only about beginning of deployment add this line only: ```php before('deploy', 'slack:notify'); ``` If you want to notify about successful end of deployment add this too: ```php after('deploy:success', 'slack:notify:success'); ``` If you want to notify about failed deployment add this too: ```php after('deploy:failed', 'slack:notify:failure'); ``` ## Configuration ### slack_channel [Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L70) Channel to publish to, when false the default channel the webhook will be used ```php title="Default value" false ``` ### slack_title [Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L73) Title of project ```php title="Default value" return get('application', 'Project'); ``` ### slack_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L78) Deploy message ```php title="Default value" '_{{user}}_ deploying `{{what}}` to *{{where}}*' ``` ### slack_success_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L79) ```php title="Default value" 'Deploy to *{{where}}* successful' ``` ### slack_failure_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L80) ```php title="Default value" 'Deploy to *{{where}}* failed' ``` ### slack_rollback_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L81) ```php title="Default value" '_{{user}}_ rolled back changes on *{{where}}*' ``` ### slack_fields [Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L82) ### slack_color [Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L85) Color of attachment ```php title="Default value" '#4d91f7' ``` ### slack_success_color [Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L86) ```php title="Default value" '#00c100' ``` ### slack_failure_color [Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L87) ```php title="Default value" '#ff0909' ``` ### slack_rollback_color [Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L88) ```php title="Default value" '#eba211' ``` ## Tasks ### slack\:notify {#slack-notify} [Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L100) Notifies Slack. ### slack\:notify\:success {#slack-notify-success} [Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L120) Notifies Slack about deploy finish. ### slack\:notify\:failure {#slack-notify-failure} [Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L141) Notifies Slack about deploy failure. ### slack\:notify\:rollback {#slack-notify-rollback} [Source](https://github.com/deployphp/deployer/blob/master/contrib/slack.php#L161) Notifies Slack about rollback. ================================================ FILE: docs/contrib/supervisord-monitor.md ================================================ # Supervisord-monitor Recipe ```php require 'contrib/supervisord-monitor.php'; ``` [Source](/contrib/supervisord-monitor.php) ### Description This is a recipe that uses the [Supervisord server monitoring project](https://github.com/mlazarov/supervisord-monitor). With this recipe the possibility is created to restart a supervisord process through the Supervisor Monitor webtool, by using cURL. This workaround is particular usefull when the deployment user has unsuficient rights to restart a daemon process from the cli. ### Configuration ``` set('supervisord', [ 'uri' => 'https://youruri.xyz/supervisor', 'basic_auth_user' => 'username', 'basic_auth_password' => 'password', 'process_name' => 'process01', ]); ``` or ``` set('supervisord_uri', 'https://youruri.xyz/supervisor'); set('supervisord_basic_auth_user', 'username'); set('supervisord_basic_auth_password', 'password'); set('supervisord_process_name', 'process01'); ``` - `supervisord` – array with configuration for Supervisord - `uri` – URI to the Supervisord monitor page - `basic_auth_user` – Basic auth username to access the URI - `basic_auth_password` – Basic auth password to access the URI - `process_name` – the process name, as visible in the Supervisord monitor page. Multiple processes can be listed here, comma separated ### Task - `supervisord-monitor:restart` Restarts given processes - `supervisord-monitor:stop` Stops given processes - `supervisord-monitor:start` Starts given processes ### Usage A complete example with configs, staging and deployment ``` 'https://youruri.xyz/supervisor', 'basic_auth_user' => 'username', 'basic_auth_password' => 'password', 'process_name' => 'process01', ]); host('staging.myproject.com') ->set('branch', 'develop') ->set('labels', ['stage' => 'staging']); host('myproject.com') ->set('branch', 'main') ->set('labels', ['stage' => 'production']); Tasks task('build', function () { run('cd {{release_path}} && build'); }); task('deploy', [ 'build', 'supervisord', ]); task('supervisord', ['supervisord-monitor:restart']) ->select('stage=production'); ``` ## Tasks ### supervisord-monitor\:restart {#supervisord-monitor-restart} [Source](https://github.com/deployphp/deployer/blob/master/contrib/supervisord-monitor.php#L134) ### supervisord-monitor\:stop {#supervisord-monitor-stop} [Source](https://github.com/deployphp/deployer/blob/master/contrib/supervisord-monitor.php#L151) ### supervisord-monitor\:start {#supervisord-monitor-start} [Source](https://github.com/deployphp/deployer/blob/master/contrib/supervisord-monitor.php#L165) ================================================ FILE: docs/contrib/telegram.md ================================================ # Telegram Recipe ```php require 'contrib/telegram.php'; ``` [Source](/contrib/telegram.php) ## Installing 1. Create telegram bot with [BotFather](https://t.me/BotFather) and grab the token provided 2. Send `/start` to your bot and open https://api.telegram.org/bot{$TELEGRAM_TOKEN_HERE}/getUpdates 3. Take chat_id from response Add hook on deploy: ```php before('deploy', 'telegram:notify'); ``` ## Configuration - `telegram_token` – telegram bot token, **required** - `telegram_chat_id` — chat ID to push messages to - `telegram_proxy` - proxy connection string in [CURLOPT_PROXY](https://curl.haxx.se/libcurl/c/CURLOPT_PROXY.html) form like: ``` http://proxy:80 socks5://user:password@host:3128 ``` - `telegram_title` – the title of application, default `{{application}}` - `telegram_text` – notification message template ``` _{{user}}_ deploying `{{what}}` to *{{where}}* ``` - `telegram_success_text` – success template, default: ``` Deploy to *{{where}}* successful ``` - `telegram_failure_text` – failure template, default: ``` Deploy to *{{where}}* failed ``` ## Usage If you want to notify only about beginning of deployment add this line only: ```php before('deploy', 'telegram:notify'); ``` If you want to notify about successful end of deployment add this too: ```php after('deploy:success', 'telegram:notify:success'); ``` If you want to notify about failed deployment add this too: ```php after('deploy:failed', 'telegram:notify:failure'); ## Configuration ### telegram_title [Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L65) Title of project ```php title="Default value" return get('application', 'Project'); ``` ### telegram_token [Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L70) Telegram settings :::info Required Throws exception if not set. ::: ### telegram_chat_id [Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L73) :::info Required Throws exception if not set. ::: ### telegram_url [Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L76) ```php title="Default value" return 'https://api.telegram.org/bot' . get('telegram_token') . '/sendmessage'; ``` ### telegram_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L81) Deploy message ```php title="Default value" '_{{user}}_ deploying `{{what}}` to *{{where}}*' ``` ### telegram_success_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L82) ```php title="Default value" 'Deploy to *{{where}}* successful' ``` ### telegram_failure_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L83) ```php title="Default value" 'Deploy to *{{where}}* failed' ``` ## Tasks ### telegram\:notify {#telegram-notify} [Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L87) Notifies Telegram. ### telegram\:notify\:success {#telegram-notify-success} [Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L118) Notifies Telegram about deploy finish. ### telegram\:notify\:failure {#telegram-notify-failure} [Source](https://github.com/deployphp/deployer/blob/master/contrib/telegram.php#L149) Notifies Telegram about deploy failure. ================================================ FILE: docs/contrib/webpack_encore.md ================================================ # Webpack Encore Recipe ```php require 'contrib/webpack_encore.php'; ``` [Source](/contrib/webpack_encore.php) * Requires * [npm](/docs/contrib/npm.md) * [yarn](/docs/contrib/yarn.md) ## Configuration - **webpack_encore/package_manager** *(optional)*: set yarn or npm. We try to find if yarn or npm is available and used. ## Usage ```php For Yarn after('deploy:update_code', 'yarn:install'); For npm after('deploy:update_code', 'npm:install'); after('deploy:update_code', 'webpack_encore:build'); ``` ## Configuration ### webpack_encore/package_manager [Source](https://github.com/deployphp/deployer/blob/master/contrib/webpack_encore.php#L25) ## Configuration - **webpack_encore/package_manager** *(optional)*: set yarn or npm. We try to find if yarn or npm is available and used. ## Usage ```php For Yarn after('deploy:update_code', 'yarn:install'); For npm after('deploy:update_code', 'npm:install'); after('deploy:update_code', 'webpack_encore:build'); ``` :::info Autogenerated The value of this configuration is autogenerated on access. ::: ### webpack_encore/env [Source](https://github.com/deployphp/deployer/blob/master/contrib/webpack_encore.php#L33) ```php title="Default value" 'production' ``` ## Tasks ### webpack_encore\:build {#webpack_encore-build} [Source](https://github.com/deployphp/deployer/blob/master/contrib/webpack_encore.php#L36) Runs webpack encore build. ================================================ FILE: docs/contrib/workplace.md ================================================ # Workplace Recipe ```php require 'contrib/workplace.php'; ``` [Source](/contrib/workplace.php) This recipes works with Custom Integrations and Publishing Bots. Add hook on deploy: ``` before('deploy', 'workplace:notify'); ``` ## Configuration - `workplace_webhook` - incoming workplace webhook **required** ``` // With custom integration set('workplace_webhook', 'https://graph.facebook.com//feed?access_token='); // With publishing bot set('workplace_webhook', 'https://graph.facebook.com/v3.0/group/feed?access_token='); // Use markdown on message set('workplace_webhook', 'https://graph.facebook.com//feed?access_token=&formatting=MARKDOWN'); ``` - `workplace_text` - notification message ``` set('workplace_text', '_{{user}}_ deploying `{{what}}` to *{{where}}*'); ``` - `workplace_success_text` – success template, default: ``` set('workplace_success_text', 'Deploy to *{{where}}* successful'); ``` - `workplace_failure_text` – failure template, default: ``` set('workplace_failure_text', 'Deploy to *{{where}}* failed'); ``` - `workplace_edit_post` – whether to create a new post for deploy result, or edit the first one created, default creates a new post: ``` set('workplace_edit_post', false); ``` ## Usage If you want to notify only about beginning of deployment add this line only: ```php before('deploy', 'workplace:notify'); ``` If you want to notify about successful end of deployment add this too: ```php after('deploy:success', 'workplace:notify:success'); ``` If you want to notify about failed deployment add this too: ```php after('deploy:failed', 'workplace:notify:failure'); ``` ## Configuration ### workplace_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/workplace.php#L71) Deploy message ```php title="Default value" '_{{user}}_ deploying `{{what}}` to *{{where}}*' ``` ### workplace_success_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/workplace.php#L72) ```php title="Default value" 'Deploy to *{{where}}* successful' ``` ### workplace_failure_text [Source](https://github.com/deployphp/deployer/blob/master/contrib/workplace.php#L73) ```php title="Default value" 'Deploy to *{{where}}* failed' ``` ### workplace_edit_post [Source](https://github.com/deployphp/deployer/blob/master/contrib/workplace.php#L76) By default, create a new post for every message ```php title="Default value" false ``` ## Tasks ### workplace\:notify {#workplace-notify} [Source](https://github.com/deployphp/deployer/blob/master/contrib/workplace.php#L79) Notifies Workplace. ### workplace\:notify\:success {#workplace-notify-success} [Source](https://github.com/deployphp/deployer/blob/master/contrib/workplace.php#L103) Notifies Workplace about deploy finish. ### workplace\:notify\:failure {#workplace-notify-failure} [Source](https://github.com/deployphp/deployer/blob/master/contrib/workplace.php#L114) Notifies Workplace about deploy failure. ================================================ FILE: docs/contrib/yammer.md ================================================ # Yammer Recipe ```php require 'contrib/yammer.php'; ``` [Source](/contrib/yammer.php) Add hook on deploy: ```php before('deploy', 'yammer:notify'); ``` ## Configuration - `yammer_url` – The URL to the message endpoint, default is https://www.yammer.com/api/v1/messages.json - `yammer_token` *(required)* – Yammer auth token - `yammer_group_id` *(required)* - Group ID - `yammer_title` – the title of application, default `{{application}}` - `yammer_body` – notification message template, default: ``` {{user}} deploying {{what}} to {{where}} ``` - `yammer_success_body` – success template, default: ``` Deploy to {{where}} successful ``` - `yammer_failure_body` – failure template, default: ``` Deploy to {{where}} failed ``` ## Usage If you want to notify only about beginning of deployment add this line only: ```php before('deploy', 'yammer:notify'); ``` If you want to notify about successful end of deployment add this too: ```php after('deploy:success', 'yammer:notify:success'); ``` If you want to notify about failed deployment add this too: ```php after('deploy:failed', 'yammer:notify:failure'); ``` ## Configuration ### yammer_url [Source](https://github.com/deployphp/deployer/blob/master/contrib/yammer.php#L55) ```php title="Default value" 'https://www.yammer.com/api/v1/messages.json' ``` ### yammer_title [Source](https://github.com/deployphp/deployer/blob/master/contrib/yammer.php#L58) Title of project ```php title="Default value" return get('application', 'Project'); ``` ### yammer_body [Source](https://github.com/deployphp/deployer/blob/master/contrib/yammer.php#L63) Deploy message ```php title="Default value" '{{user}} deploying {{what}} to {{where}}' ``` ### yammer_success_body [Source](https://github.com/deployphp/deployer/blob/master/contrib/yammer.php#L64) ```php title="Default value" 'Deploy to {{where}} successful' ``` ### yammer_failure_body [Source](https://github.com/deployphp/deployer/blob/master/contrib/yammer.php#L65) ```php title="Default value" 'Deploy to {{where}} failed' ``` ## Tasks ### yammer\:notify {#yammer-notify} [Source](https://github.com/deployphp/deployer/blob/master/contrib/yammer.php#L68) Notifies Yammer. ### yammer\:notify\:success {#yammer-notify-success} [Source](https://github.com/deployphp/deployer/blob/master/contrib/yammer.php#L87) Notifies Yammer about deploy finish. ### yammer\:notify\:failure {#yammer-notify-failure} [Source](https://github.com/deployphp/deployer/blob/master/contrib/yammer.php#L106) Notifies Yammer about deploy failure. ================================================ FILE: docs/contrib/yarn.md ================================================ # Yarn Recipe ```php require 'contrib/yarn.php'; ``` [Source](/contrib/yarn.php) ## Configuration - **bin/yarn** *(optional)*: set Yarn binary, automatically detected otherwise. ## Usage ```php after('deploy:update_code', 'yarn:install'); ``` ## Configuration ### bin/yarn [Source](https://github.com/deployphp/deployer/blob/master/contrib/yarn.php#L16) ## Configuration - **bin/yarn** *(optional)*: set Yarn binary, automatically detected otherwise. ## Usage ```php after('deploy:update_code', 'yarn:install'); ``` ```php title="Default value" return which('yarn'); ``` ## Tasks ### yarn\:install {#yarn-install} [Source](https://github.com/deployphp/deployer/blob/master/contrib/yarn.php#L22) Installs Yarn packages. In there is a {{previous_release}}, node_modules will be copied from it before installing deps with yarn. ================================================ FILE: docs/getting-started.md ================================================ # Getting Started This tutorial will guide you through: - Setting up a new host with the [provision](recipe/provision.md) recipe. - Configuring a deployment and performing your first deploy. ## Step 1: Install Deployer {#install} First, [install Deployer](installation.md). Once installed, navigate to your project directory and run: ```sh dep init ``` Deployer will prompt you with a series of questions. After completing them, you'll have a **deploy.php** or **deploy.yaml** file—your deployment recipe. This file defines hosts, tasks, and dependencies on other recipes. Framework-specific recipes provided by Deployer are based on the [common](recipe/common.md) recipe. --- ## Step 2: Provision a New Server {#provision} :::note If you already have a configured web server, skip to [deployment](#deploy). ::: ### Setting Up Your VPS Create a new VPS with a provider like Linode, DigitalOcean, Vultr, AWS, or GCP. Use an **Ubuntu** image, as it's supported by Deployer's [provision](recipe/provision.md) recipe. To utilize Deployer for server provisioning, you must initially configure your server to permit key-based authentication for the root user (which is disabled by default for recent Ubuntu images). Once provisioning is complete, this root access via SSH can be disabled. :::tip Set up a DNS record pointing your domain to your server's IP address. This allows you to SSH into the server using your domain name instead of its IP. ::: ### Configuring `deploy.php` Your **deploy.php** recipe should define your host with key parameters: - **`remote_user`**: The SSH username. - **`deploy_path`**: The file path where your project will be deployed. Example: ```php host('example.org') ->set('remote_user', 'deployer') ->set('deploy_path', '~/example'); ``` If your server only has a `root` user, the `provision` recipe will create and configure a `deployer` user for you. ### Adding an Identity Key To connect to your server, use an identity key or private key. Instead of defining it directly in your host configuration, add it to your **~/.ssh/config** file: ``` Host * IdentityFile ~/.ssh/id_rsa ``` ### Provisioning the Server Run the following command to provision your server: ```sh dep provision ``` :::tip - To change the default `root` user, use: ```sh dep provision -o provision_user=your-user ``` - If your remote user can `sudo` to become root, use: ```sh dep provision -o become=root ``` ::: During provisioning, Deployer will ask about PHP versions, database preferences, and more. It takes about **5 minutes** and installs everything required to run a website. The deployment path is configured as [deploy_path](recipe/common.md#deploy_path). --- ## Step 3: Deploy Your Project {#deploy} Deploy your project with: ```sh dep deploy ``` If the deployment fails, Deployer will display the error and the failed command. You may need to configure your `.env` file or similar credentials. To edit files directly on the server: ```sh dep ssh ``` If needed, resume deployment from the last step: ```sh dep deploy --start-from deploy:migrate ``` --- ## Step 4: Post-Deployment Configuration After the first successful deployment, the server directory structure looks like this: ``` ~/example // deploy_path |- current -> releases/1 // Symlink to current release |- releases // Directory for all releases |- 1 // Latest release |- ... |- .env -> shared/.env // Symlink to shared .env file |- shared // Shared files between releases |- ... |- .env // Shared .env file |- .dep // Deployer configuration files ``` ### Web Server Setup Configure your web server to serve from the `current` directory. Example for Nginx: ```nginx root /home/deployer/example/current/public; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } ``` For those using the [provision recipe](recipe/provision.md), Deployer will automatically configure the Caddy web server to serve from the [public_path](recipe/provision/website.md#public_path). --- ## Step 5: Adding a Build Step To automate build steps, add a task in your **deploy.php**: ```php task('build', function () { cd('{{release_path}}'); run('npm install'); run('npm run prod'); }); after('deploy:update_code', 'build'); ``` --- ## Examining Deployments Use the `releases` task to view deployment details: ```sh dep releases ``` Example output: ``` +---------------------+--------- deployer.org -------+--------+-----------+ | Date (UTC) | Release | Author | Target | Commit | +---------------------+-------------+----------------+--------+-----------+ | 2021-11-05 14:00:22 | 1 (current) | Anton Medvedev | HEAD | 943ded2be | +---------------------+-------------+----------------+--------+-----------+ ``` :::tip During development, the [dep push](recipe/deploy/push.md) task maybe useful to create a patch of local changes and push them to the host. ::: --- With Deployer, you're now ready to efficiently set up, provision, and manage deployments for your projects! ================================================ FILE: docs/hosts.md ================================================ # Hosts In Deployer, you define hosts using the [host()](api.md#host) function. ### Defining a Host ```php host('example.org'); ``` Each host is associated with configuration key-value pairs. When you define a host, two key configurations are set: - **`hostname`**: Used for connecting to the remote host. - **`alias`**: A unique identifier for the host in recipe. ### Example: Using Host Configurations You can access host configurations within tasks with the [currentHost()](api.md#currenthost) function: ```php task('test', function () { $hostname = currentHost()->get('hostname'); $alias = currentHost()->get('alias'); writeln("The $alias is $hostname"); }); ``` Or using brackets syntax: ```php task('test', function () { writeln('The {{alias}} is {{hostname}}'); }); ``` Running the task: ```sh $ dep test [example.org] The example.org is example.org ``` ### Overriding Hostname You can override the default hostname with the `set()` method: ```php host('example.org') ->set('hostname', 'example.cloud.google.com'); ``` Now the `hostname` is used for SSH connections, but the `alias` remains unchanged: ```sh $ dep test [example.org] The example.org is example.cloud.google.com ``` ### Configuring Remote User Specify the `remote_user` to define which user to connect as: ```php host('example.org') ->set('hostname', 'example.cloud.google.com') ->set('remote_user', 'deployer'); ``` Deployer will now connect using `ssh deployer@example.cloud.google.com`. Alternatively, you can use special setter methods for better IDE autocompletion: ```php host('example.org') ->setHostname('example.cloud.google.com') ->setRemoteUser('deployer'); ``` --- ## Host Labels Labels allow you to group and identify hosts for specific deployments. Labels are defined as key-value pairs: ```php host('example.org')->setLabels(['stage' => 'prod']); host('staging.example.org')->setLabels(['stage' => 'staging']); ``` Labels become powerful in multi-server setups: ```php host('admin.example.org')->setLabels(['stage' => 'prod', 'role' => 'web']); host('web[1:5].example.org')->setLabels(['stage' => 'prod', 'role' => 'web']); host('db[1:2].example.org')->setLabels(['stage' => 'prod', 'role' => 'db']); host('test.example.org')->setLabels(['stage' => 'test', 'role' => 'web']); host('special.example.org')->setLabels(['role' => 'special']); ``` ### Filtering Hosts by Labels When deploying, you can filter hosts using label selectors: ```sh $ dep deploy stage=prod&role=web,role=special ``` - Use `&` to specify multiple labels that must match on the same host. - Use `,` to separate multiple selections. Set a default selection string for convenience: ```php set('default_selector', "stage=prod&role=web,role=special"); ``` --- ## Host Configurations ### Key Host Configurations | Config Key | Description | |------------------------|------------------------------------------------------------------------------------------------| | **`alias`** | Identifier for the host (e.g., `prod`, `staging`). | | **`hostname`** | Actual hostname or IP address used for SSH connections. | | **`remote_user`** | SSH username. Defaults to the current OS user or `~/.ssh/config`. | | **`port`** | SSH port. Default is `22`. | | **`config_file`** | SSH config file location. Default is `~/.ssh/config`. | | **`identity_file`** | SSH private key file. E.g., `~/.ssh/id_rsa`. | | **`forward_agent`** | Enable SSH agent forwarding. Default is `true`. | | **`ssh_multiplexing`** | Enable SSH multiplexing for performance. Default is `true`. | | **`shell`** | Shell to use. Default is `bash -ls`. | | **`deploy_path`** | Directory for deployments. E.g., `~/myapp`. | | **`labels`** | Key-value pairs for host selection. | | **`ssh_arguments`** | Additional SSH options. E.g., `['-o UserKnownHostsFile=/dev/null']`. | | **`ssh_control_path`** | Control path for SSH multiplexing. Default is `~/.ssh/%C` or `/dev/shm/%C` in CI environments. | ### Best Practices Avoid storing sensitive SSH connection parameters in `deploy.php`. Instead, configure them in `~/.ssh/config`: ``` Host * IdentityFile ~/.ssh/id_rsa ``` --- ## Advanced Host Definitions ### Multiple Hosts Define multiple hosts in one call: ```php host('example.org', 'deployer.org', 'another.org')->setRemoteUser('anton'); ``` ### Host Ranges For patterns with many hosts, use ranges: ```php host('www[01:50].example.org'); // Will define hosts "www01.example.org", "www02.example.org", etc. host('db[a:f].example.org'); // Will define hosts "dba.example.org", "dbb.example.org", etc. ``` - Numeric ranges can include leading zeros. - Alphabetic ranges are also supported. ### Localhost Use the [localhost()](api.md#localhost) function for local execution: ```php localhost(); // Alias and hostname are "localhost". localhost('ci'); // Alias is "ci", hostname is "localhost". ``` Now [run()](api.md#run) will execute on command locally. Alternatively, you can use [runLocally()](api.md#runlocally) function. ### YAML Inventory Separate host definitions into an external file using the [import()](api.md#import) function: ```php title="deploy.php" import('inventory.yaml'); ``` ```yaml title="inventory.yaml" hosts: example.org: remote_user: deployer deployer.org: remote_user: deployer ``` --- With these tools and configurations, you can manage and deploy to hosts effectively, whether it's a single server or a complex multi-host setup. Happy deploying! ================================================ FILE: docs/installation.md ================================================ # Installation There are two ways to install Deployer: globally or locally. Global installation is recommended for most users, as it allows you to use Deployer from any directory. Local (or project) installation is preferred for CI/CD pipelines, as it allows you to use the same version of Deployer across all environments. ## Global Installation To install Deployer globally, use one of the following commands in your project directory: ```sh composer global require deployer/deployer ``` Or: ```sh phive install deployer ``` :::tip Path to Executable Make sure that Composer's global bin directory is in your `PATH`. Typically, you can add the following line to your shell configuration file (e.g., `.bashrc`, `.zshrc`): ```sh export PATH="$HOME/.composer/vendor/bin:$PATH" ``` After adding this line, reload your shell configuration: ```sh source ~/.bashrc ``` or, for Zsh: ```sh source ~/.zshrc ``` ::: To set up Deployer in your project and create the `deploy.php` configuration file, run: ```sh dep init ``` ### Autocomplete Support Deployer includes support for autocompletion, helping you quickly find task names, options, and hosts. To enable autocomplete for various shells, use the following commands: - **Bash**: ```sh dep completion bash > /etc/bash_completion.d/deployer ``` Make sure your `.bashrc` file sources the generated file so that bash completion works. - **Zsh**: ```sh dep completion zsh > ~/.zsh/completion/_deployer ``` Ensure that your `.zshrc` file includes the directory where `_deployer` is located in the `fpath`. - **Fish**: ```sh dep completion fish > ~/.config/fish/completions/deployer.fish ``` The generated file will be automatically loaded by Fish. ## Project Installation The project installation method is recommended for CI/CD pipelines, as it allows you to use the same version of Deployer across all environments. To install Deployer in your project, run the following command: ```sh composer require --dev deployer/deployer ``` :::tip Configuring Shell Alias To make using Deployer more convenient, you can set up a shell alias. This will allow you to run Deployer commands more easily. Add the following line to your shell configuration file (e.g., `.bashrc`, `.zshrc`): ```sh alias dep='vendor/bin/dep' ``` This alias lets you use `dep` instead of typing the full path each time. ::: Then, to initialize Deployer in your project, use: ```sh vendor/bin/dep init ``` ## Downloading the Phar File Another option for installing Deployer is to download the Phar file. You can find the latest version on the [download page](/download). Adding `deployer.phar` to your project repository is recommended to ensure everyone, including your CI pipeline, uses the same version of Deployer. This helps maintain consistency across all environments. Once downloaded, run it in your project directory: ```sh php deployer.phar init ``` This method provides a simple way to use Deployer without needing Composer. ================================================ FILE: docs/recipe/README.md ================================================ # All Recipes * [Cakephp Recipe](/docs/recipe/cakephp.md) * [Codeigniter 4 Recipe](/docs/recipe/codeigniter4.md) * [Codeigniter Recipe](/docs/recipe/codeigniter.md) * [Common Recipe](/docs/recipe/common.md) * [Composer Recipe](/docs/recipe/composer.md) * [Contao Recipe](/docs/recipe/contao.md) * [Craftcms Recipe](/docs/recipe/craftcms.md) * [Drupal 7 Recipe](/docs/recipe/drupal7.md) * [Drupal 8 Recipe](/docs/recipe/drupal8.md) * [Flow Framework Recipe](/docs/recipe/flow_framework.md) * [Fuelphp Recipe](/docs/recipe/fuelphp.md) * [Joomla Recipe](/docs/recipe/joomla.md) * [Laravel Recipe](/docs/recipe/laravel.md) * [Magento 2 Recipe](/docs/recipe/magento2.md) * [Magento Recipe](/docs/recipe/magento.md) * [Pimcore Recipe](/docs/recipe/pimcore.md) * [Prestashop Recipe](/docs/recipe/prestashop.md) * [Provision Recipe](/docs/recipe/provision.md) * [Shopware Recipe](/docs/recipe/shopware.md) * [Silverstripe Recipe](/docs/recipe/silverstripe.md) * [Spiral Recipe](/docs/recipe/spiral.md) * [Statamic Recipe](/docs/recipe/statamic.md) * [Sulu Recipe](/docs/recipe/sulu.md) * [Symfony Recipe](/docs/recipe/symfony.md) * [TYPO3 Recipe](/docs/recipe/typo3.md) * [WordPress Recipe](/docs/recipe/wordpress.md) * [Yii2 Recipe](/docs/recipe/yii.md) * [Zend Framework Recipe](/docs/recipe/zend_framework.md) ================================================ FILE: docs/recipe/cakephp.md ================================================ # How to Deploy a Cakephp Project ```php require 'recipe/cakephp.php'; ``` [Source](/recipe/cakephp.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Cakephp application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Cakephp** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors * [deploy:init](/docs/recipe/cakephp.md#deploy-init) – * [deploy:run_migrations](/docs/recipe/cakephp.md#deploy-run_migrations) – * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The cakephp recipe is based on the [common](/docs/recipe/common.md) recipe. ## Configuration ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/cakephp.php#L14) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. CakePHP 4 Project Template configuration CakePHP 4 Project Template shared dirs ```php title="Default value" [ 'logs', 'tmp', ] ``` ### shared_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/cakephp.php#L20) Overrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`. CakePHP 4 Project Template shared files ```php title="Default value" [ 'config/.env', 'config/app.php', ] ``` ## Tasks ### deploy\:init {#deploy-init} [Source](https://github.com/deployphp/deployer/blob/master/recipe/cakephp.php#L28) Create plugins' symlinks ### deploy\:run_migrations {#deploy-run_migrations} [Source](https://github.com/deployphp/deployer/blob/master/recipe/cakephp.php#L35) Run migrations ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/cakephp.php#L43) Main task This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) * [deploy:init](/docs/recipe/cakephp.md#deploy-init) * [deploy:run_migrations](/docs/recipe/cakephp.md#deploy-run_migrations) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ================================================ FILE: docs/recipe/codeigniter.md ================================================ # How to Deploy a Codeigniter Project ```php require 'recipe/codeigniter.php'; ``` [Source](/recipe/codeigniter.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Codeigniter application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Codeigniter** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The codeigniter recipe is based on the [common](/docs/recipe/common.md) recipe. ## Configuration ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter.php#L10) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. CodeIgniter shared dirs ```php title="Default value" ['application/cache', 'application/logs'] ``` ### writable_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter.php#L13) Overrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`. CodeIgniter writable dirs ```php title="Default value" ['application/cache', 'application/logs'] ``` ## Tasks ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter.php#L19) Deploys your project. Main task This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ================================================ FILE: docs/recipe/codeigniter4.md ================================================ # How to Deploy a Codeigniter 4 Project ```php require 'recipe/codeigniter4.php'; ``` [Source](/recipe/codeigniter4.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Codeigniter 4 application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Codeigniter 4** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors * [spark:optimize](/docs/recipe/codeigniter4.md#spark-optimize) – Optimize for production. * [spark:migrate](/docs/recipe/codeigniter4.md#spark-migrate) – Locates and runs all new migrations against the database. * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The codeigniter4 recipe is based on the [common](/docs/recipe/common.md) recipe. ## Configuration ### public_path [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L10) Overrides [public_path](/docs/recipe/provision/website.md#public_path) from `recipe/provision/website.php`. Default Configurations ```php title="Default value" 'public' ``` ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L12) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. ```php title="Default value" ['writable'] ``` ### shared_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L14) Overrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`. ```php title="Default value" ['.env'] ``` ### writable_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L16) Overrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`. ```php title="Default value" [ 'writable/cache', 'writable/debugbar', 'writable/logs', 'writable/session', 'writable/uploads', ] ``` ### log_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L24) ```php title="Default value" 'writable/logs/*.log' ``` ### codeigniter4_version [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L26) ```php title="Default value" $result = run('{{bin/php}} {{release_or_current_path}}/spark'); preg_match_all('/(\d+\.?)+/', $result, $matches); return $matches[0][0] ?? 5.5; ``` ## Tasks ### spark\:cache\:info {#spark-cache-info} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L94) Shows file cache information in the current system. Discover & Checks ### spark\:config\:check {#spark-config-check} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L97) Check your Config values. ### spark\:env {#spark-env} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L100) Retrieves the current environment, or set a new one. ### spark\:filter\:check {#spark-filter-check} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L103) Check filters for a route. ### spark\:lang\:find {#spark-lang-find} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L106) Find and save available phrases to translate. ### spark\:namespaces {#spark-namespaces} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L109) Verifies your namespaces are setup correctly. ### spark\:phpini\:check {#spark-phpini-check} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L112) Check your php.ini values. ### spark\:routes {#spark-routes} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L115) Displays all routes. ### spark\:key\:generate {#spark-key-generate} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L123) Generates a new encryption key and writes it in an `.env` file. Actions ### spark\:optimize {#spark-optimize} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L126) Optimize for production. ### spark\:publish {#spark-publish} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L129) Discovers and executes all predefined Publisher classes. ### spark\:db\:create {#spark-db-create} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L137) Create a new database schema. Database and migrations. ### spark\:db\:seed {#spark-db-seed} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L140) Runs the specified seeder to populate known data into the database. ### spark\:db\:table {#spark-db-table} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L143) Retrieves information on the selected table. ### spark\:migrate {#spark-migrate} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L146) Locates and runs all new migrations against the database. ### spark\:migrate\:refresh {#spark-migrate-refresh} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L149) Does a rollback followed by a latest to refresh the current state of the database. ### spark\:migrate\:rollback {#spark-migrate-rollback} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L152) Runs the "down" method for all migrations in the last batch. ### spark\:migrate\:status {#spark-migrate-status} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L155) Displays a list of all migrations and whether they\'ve been run or not. ### spark\:cache\:clear {#spark-cache-clear} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L163) Clears the current system caches. Housekeeping ### spark\:debugbar\:clear {#spark-debugbar-clear} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L166) Clears all debugbar JSON files. ### spark\:logs\:clear {#spark-logs-clear} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L169) Clears all log files. ### spark\:custom {#spark-custom} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L176) Run a custom spark command. Custom Spark Command for shield or setting packages ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/codeigniter4.php#L184) Deploys your project. Main deploy task. This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) * [spark:optimize](/docs/recipe/codeigniter4.md#spark-optimize) * [spark:migrate](/docs/recipe/codeigniter4.md#spark-migrate) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ================================================ FILE: docs/recipe/common.md ================================================ # Common Recipe ```php require 'recipe/common.php'; ``` [Source](/recipe/common.php) * Requires * [provision](/docs/recipe/provision.md) * [check_remote](/docs/recipe/deploy/check_remote.md) * [cleanup](/docs/recipe/deploy/cleanup.md) * [clear_paths](/docs/recipe/deploy/clear_paths.md) * [copy_dirs](/docs/recipe/deploy/copy_dirs.md) * [env](/docs/recipe/deploy/env.md) * [info](/docs/recipe/deploy/info.md) * [lock](/docs/recipe/deploy/lock.md) * [push](/docs/recipe/deploy/push.md) * [release](/docs/recipe/deploy/release.md) * [rollback](/docs/recipe/deploy/rollback.md) * [setup](/docs/recipe/deploy/setup.md) * [shared](/docs/recipe/deploy/shared.md) * [symlink](/docs/recipe/deploy/symlink.md) * [update_code](/docs/recipe/deploy/update_code.md) * [vendors](/docs/recipe/deploy/vendors.md) * [writable](/docs/recipe/deploy/writable.md) ## Configuration ### user [Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L31) Name of current user who is running deploy. If not set will try automatically get git user name, otherwise output of `whoami` command. :::info Autogenerated The value of this configuration is autogenerated on access. ::: ### keep_releases [Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L54) Number of releases to preserve in releases folder. ```php title="Default value" 10 ``` ### repository [Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L57) Repository to deploy. ### default_timeout [Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L62) Default timeout for `run()` and `runLocally()` functions. Set to `null` to disable timeout. ```php title="Default value" 300 ``` ### env [Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L78) Remote environment variables. ```php set('env', [ 'KEY' => 'something', ]); ``` It is possible to override it per `run()` call. ```php run('echo $KEY', env: ['KEY' => 'over']); ``` ### dotenv [Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L87) Path to `.env` file which will be used as environment variables for each command per `run()`. ```php set('dotenv', '{{release_or_current_path}}/.env'); ``` ```php title="Default value" false ``` ### deploy_path [Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L97) The deploy path. For example can be set for a bunch of host once as: ```php set('deploy_path', '~/{{alias}}'); ``` :::info Required Throws exception if not set. ::: ### current_path [Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L107) Return current release path. Default to [deploy_path](/docs/recipe/common.md#deploy_path)/`current`. ```php set('current_path', '/var/public_html'); ``` ```php title="Default value" '{{deploy_path}}/current' ``` ### bin/php [Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L110) Path to the `php` bin. ```php title="Default value" if (currentHost()->hasOwn('php_version')) { return '/usr/bin/php{{php_version}}'; } return which('php'); ``` ### bin/git [Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L118) Path to the `git` bin. ```php title="Default value" return which('git'); ``` ### use_relative_symlink [Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L124) Should [bin/symlink](/docs/recipe/common.md#bin/symlink) use `--relative` option or not. Will detect automatically. ```php title="Default value" return commandSupportsOption('ln', '--relative'); ``` ### bin/symlink [Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L129) Path to the `ln` bin. With predefined options `-nfs`. ```php title="Default value" return get('use_relative_symlink') ? 'ln -nfs --relative' : 'ln -nfs'; ``` ### sudo_askpass [Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L136) Path to a file which will store temp script with sudo password. Defaults to `.dep/sudo_pass`. This script is only temporary and will be deleted after sudo command executed. :::info Autogenerated The value of this configuration is autogenerated on access. ::: ## Tasks ### deploy\:prepare {#deploy-prepare} [Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L145) Prepares a new release. This task is group task which contains next tasks: * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) ### deploy\:publish {#deploy-publish} [Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L157) Publishes the release. This task is group task which contains next tasks: * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) * [deploy:success](/docs/recipe/common.md#deploy-success) ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L165) Deploys your project. This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ### deploy\:success {#deploy-success} [Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L174) Deploys your project. Prints success message ### deploy\:failed {#deploy-failed} [Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L183) Hook on deploy failure. ### logs\:app {#logs-app} [Source](https://github.com/deployphp/deployer/blob/master/recipe/common.php#L192) Shows application logs. Follows latest application logs. ================================================ FILE: docs/recipe/composer.md ================================================ # Composer Recipe ```php require 'recipe/composer.php'; ``` [Source](/recipe/composer.php) * Requires * [common](/docs/recipe/common.md) ## Tasks ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/composer.php#L10) Deploys your project. This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ================================================ FILE: docs/recipe/contao.md ================================================ # How to Deploy a Contao Project ```php require 'recipe/contao.php'; ``` [Source](/recipe/contao.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Contao application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Contao** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors * [contao:maintenance:enable](/docs/recipe/contao.md#contao-maintenance-enable) – Enable maintenance mode * [contao:migrate](/docs/recipe/contao.md#contao-migrate) – Run Contao migrations * [contao:maintenance:disable](/docs/recipe/contao.md#contao-maintenance-disable) – Disable maintenance mode * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The contao recipe is based on the [symfony](/docs/recipe/symfony.md) recipe. ## Configuration ### public_path [Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L12) Overrides [public_path](/docs/recipe/provision/website.md#public_path) from `recipe/provision/website.php`. The public path is the path to be set as DocumentRoot and is defined in the `composer.json` of the project but defaults to `public` from Contao 5.0 on. This path is relative from the [current_path](/docs/recipe/common.md#current_path), see [`recipe/provision/website.php`](/docs/recipe/provision/website.php#public_path). ```php title="Default value" $composerConfig = json_decode(file_get_contents('./composer.json'), true, 512, JSON_THROW_ON_ERROR); return $composerConfig['extra']['public-dir'] ?? 'public'; ``` ### bin/console [Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L30) Overrides [bin/console](/docs/recipe/symfony.md#bin/console) from `recipe/symfony.php`. ```php title="Default value" return '{{bin/php}} {{release_or_current_path}}/vendor/bin/contao-console'; ``` ### contao_version [Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L34) ```php title="Default value" $result = run('{{bin/console}} --version'); preg_match_all('/(\d+\.?)+/', $result, $matches); return $matches[0][0] ?? 'n/a'; ``` ### symfony_version [Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L40) Overrides [symfony_version](/docs/recipe/symfony.md#symfony_version) from `recipe/symfony.php`. ```php title="Default value" $result = run('{{bin/console}} about'); preg_match_all('/(\d+\.?)+/', $result, $matches); return $matches[0][0] ?? 5.0; ``` ## Tasks ### contao\:migrate {#contao-migrate} [Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L56) Run Contao migrations. This task updates the database. A database backup is saved automatically as a default. To automatically drop the obsolete database structures, you can override the task as follows: ```php task('contao:migrate', function () { run('{{bin/php}} {{bin/console}} contao:migrate --with-deletes {{console_options}}'); }); ``` ### contao\:manager\:download {#contao-manager-download} [Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L62) Download the Contao Manager. Downloads the `contao-manager.phar.php` into the public path. ### contao\:install\:lock {#contao-install-lock} [Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L68) Lock the Contao Install Tool. Locks the Contao install tool which is useful if you don't use it. ### contao\:manager\:lock {#contao-manager-lock} [Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L74) Lock the Contao Manager. Locks the Contao Manager which is useful if you only need the API of the Manager rather than the UI. ### contao\:maintenance\:enable {#contao-maintenance-enable} [Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L80) Enable maintenance mode. ### contao\:maintenance\:disable {#contao-maintenance-disable} [Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L95) Disable maintenance mode. ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/contao.php#L107) Deploy the project. This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) * [contao:maintenance:enable](/docs/recipe/contao.md#contao-maintenance-enable) * [contao:migrate](/docs/recipe/contao.md#contao-migrate) * [contao:maintenance:disable](/docs/recipe/contao.md#contao-maintenance-disable) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ================================================ FILE: docs/recipe/craftcms.md ================================================ # How to Deploy a Craftcms Project ```php require 'recipe/craftcms.php'; ``` [Source](/recipe/craftcms.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Craftcms application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Craftcms** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors The craftcms recipe is based on the [common](/docs/recipe/common.md) recipe. ## Configuration ### log_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/craftcms.php#L9) ```php title="Default value" 'storage/logs/*.log' ``` ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/craftcms.php#L11) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. ```php title="Default value" [ 'storage', 'web/assets', ] ``` ### shared_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/craftcms.php#L16) Overrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`. ```php title="Default value" ['.env'] ``` ### writable_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/craftcms.php#L18) Overrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`. ```php title="Default value" [ 'config/project', 'storage', 'web/assets', 'web/cpresources', ] ``` ## Tasks ### craft\:gc {#craft-gc} [Source](https://github.com/deployphp/deployer/blob/master/recipe/craftcms.php#L120) Runs garbage collection. Garbage collection ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/craftcms.php#L127) Deploys Craft CMS. Main deploy This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) ================================================ FILE: docs/recipe/deploy/check_remote.md ================================================ # Check Remote Recipe ```php require 'recipe/deploy/check_remote.php'; ``` [Source](/recipe/deploy/check_remote.php) ## Tasks ### deploy\:check_remote {#deploy-check_remote} [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/check_remote.php#L11) Checks remote head. Cancel deployment if there would be no change to the codebase. This avoids unnecessary releases if the latest commit has already been deployed. ================================================ FILE: docs/recipe/deploy/cleanup.md ================================================ # Cleanup Recipe ```php require 'recipe/deploy/cleanup.php'; ``` [Source](/recipe/deploy/cleanup.php) ## Configuration ### cleanup_use_sudo [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/cleanup.php#L6) Use sudo in deploy:cleanup task for rm command. ```php title="Default value" false ``` ## Tasks ### deploy\:cleanup {#deploy-cleanup} [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/cleanup.php#L9) Cleanup old releases. ================================================ FILE: docs/recipe/deploy/clear_paths.md ================================================ # Clear Paths Recipe ```php require 'recipe/deploy/clear_paths.php'; ``` [Source](/recipe/deploy/clear_paths.php) ## Configuration ### clear_paths [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/clear_paths.php#L6) List of paths to remove from [release_path](/docs/recipe/deploy/release.md#release_path). ### clear_use_sudo [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/clear_paths.php#L9) Use sudo for deploy:clear_path task? ```php title="Default value" false ``` ## Tasks ### deploy\:clear_paths {#deploy-clear_paths} [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/clear_paths.php#L12) Cleanup files and/or directories. ================================================ FILE: docs/recipe/deploy/copy_dirs.md ================================================ # Copy Dirs Recipe ```php require 'recipe/deploy/copy_dirs.php'; ``` [Source](/recipe/deploy/copy_dirs.php) ## Configuration ### copy_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/copy_dirs.php#L7) List of dirs to copy between releases. For example you can copy `node_modules` to speedup npm install. ## Tasks ### deploy\:copy_dirs {#deploy-copy_dirs} [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/copy_dirs.php#L10) Copies directories. ================================================ FILE: docs/recipe/deploy/env.md ================================================ # Env Recipe ```php require 'recipe/deploy/env.php'; ``` [Source](/recipe/deploy/env.php) ## Configuration ### dotenv_example [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/env.php#L5) ```php title="Default value" '.env.example' ``` ## Tasks ### deploy\:env {#deploy-env} [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/env.php#L8) Configure .env file. ================================================ FILE: docs/recipe/deploy/info.md ================================================ # Info Recipe ```php require 'recipe/deploy/info.php'; ``` [Source](/recipe/deploy/info.php) ## Configuration ### what [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/info.php#L9) Defines "what" text for the 'deploy:info' task. Uses one of the following sources: 1. Repository name 2. Application name :::info Autogenerated The value of this configuration is autogenerated on access. ::: ### where [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/info.php#L25) Defines "where" text for the 'deploy:info' task. Uses one of the following sources: 1. Host's stage label 2. Host's alias :::info Autogenerated The value of this configuration is autogenerated on access. ::: ## Tasks ### deploy\:info {#deploy-info} [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/info.php#L34) Displays info about deployment. ================================================ FILE: docs/recipe/deploy/lock.md ================================================ # Lock Recipe ```php require 'recipe/deploy/lock.php'; ``` [Source](/recipe/deploy/lock.php) ## Tasks ### deploy\:lock {#deploy-lock} [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/lock.php#L8) Locks deploy. ### deploy\:unlock {#deploy-unlock} [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/lock.php#L21) Unlocks deploy. ### deploy\:is_locked {#deploy-is_locked} [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/lock.php#L26) Checks if deploy is locked. ================================================ FILE: docs/recipe/deploy/push.md ================================================ # Push Recipe ```php require 'recipe/deploy/push.php'; ``` [Source](/recipe/deploy/push.php) ## Tasks ### push {#push} [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/push.php#L9) Pushes local changes to remote host. Creates patch of local changes and pushes them on host. And applies to current_path. Push can be done many times. The task purpose to be used only for development. ================================================ FILE: docs/recipe/deploy/release.md ================================================ # Release Recipe ```php require 'recipe/deploy/release.php'; ``` [Source](/recipe/deploy/release.php) ## Configuration ### release_name [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/release.php#L11) The name of the release. ```php title="Default value" return within('{{deploy_path}}', function () { $latest = run('cat .dep/latest_release || echo 0'); return strval(intval($latest) + 1); }); ``` ### releases_log [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/release.php#L19) Holds releases log from `.dep/releases_log` file. :::info Autogenerated The value of this configuration is autogenerated on access. ::: ### releases_list [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/release.php#L34) Return list of release names on host. :::info Autogenerated The value of this configuration is autogenerated on access. ::: ### release_path [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/release.php#L61) Return release path. :::info Autogenerated The value of this configuration is autogenerated on access. ::: ### release_revision [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/release.php#L72) Current release revision. Usually a git hash. ```php title="Default value" return run('cat {{release_path}}/REVISION'); ``` ### release_or_current_path [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/release.php#L78) Return the release path during a deployment but fallback to the current path otherwise. ```php title="Default value" $releaseExists = test('[ -h {{deploy_path}}/release ]'); return $releaseExists ? get('release_path') : get('current_path'); ``` ## Tasks ### deploy\:release {#deploy-release} [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/release.php#L85) Prepares release. Clean up unfinished releases and prepare next release ### releases {#releases} [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/release.php#L160) Shows releases list. Example output: ``` +---------------------+------example.org ------------+--------+-----------+ | Date (UTC) | Release | Author | Target | Commit | +---------------------+-------------+----------------+--------+-----------+ | 2021-11-06 20:51:45 | 1 | Anton Medvedev | HEAD | 34d24192e | | 2021-11-06 21:00:50 | 2 (bad) | Anton Medvedev | HEAD | 392948a40 | | 2021-11-06 23:19:20 | 3 | Anton Medvedev | HEAD | a4057a36c | | 2021-11-06 23:24:30 | 4 (current) | Anton Medvedev | HEAD | s3wa45ca6 | +---------------------+-------------+----------------+--------+-----------+ ``` ================================================ FILE: docs/recipe/deploy/rollback.md ================================================ # Rollback Recipe ```php require 'recipe/deploy/rollback.php'; ``` [Source](/recipe/deploy/rollback.php) ## Configuration ### rollback_candidate [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/rollback.php#L20) Rollback candidate will be automatically chosen by looking at output of `ls` command and content of `.dep/releases_log`. If rollback candidate is marked as **BAD_RELEASE**, it will be skipped. :::tip You can override rollback candidate via: ``` dep rollback -o rollback_candidate=123 ``` ::: :::info Autogenerated The value of this configuration is autogenerated on access. ::: ## Tasks ### rollback {#rollback} [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/rollback.php#L63) Rollbacks to the previous release. Uses [rollback_candidate](/docs/recipe/deploy/rollback.md#rollback_candidate) for symlinking. Current release will be marked as bad by creating file **BAD_RELEASE** with timestamp and [user](/docs/recipe/common.md#user). :::warning You can always manually symlink [current_path](/docs/recipe/common.md#current_path) to proper release. ``` dep run '{{bin/symlink}} releases/123 {{current_path}}' ``` ::: ================================================ FILE: docs/recipe/deploy/setup.md ================================================ # Setup Recipe ```php require 'recipe/deploy/setup.php'; ``` [Source](/recipe/deploy/setup.php) ## Tasks ### deploy\:setup {#deploy-setup} [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/setup.php#L6) Prepares host for deploy. ================================================ FILE: docs/recipe/deploy/shared.md ================================================ # Shared Recipe ```php require 'recipe/deploy/shared.php'; ``` [Source](/recipe/deploy/shared.php) ## Configuration ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/shared.php#L13) List of dirs what will be shared between releases. Each release will have symlink to those dirs stored in [deploy_path](/docs/recipe/common.md#deploy_path)/shared dir. ```php set('shared_dirs', ['storage']); ``` ### shared_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/shared.php#L20) List of files what will be shared between releases. Each release will have symlink to those files stored in [deploy_path](/docs/recipe/common.md#deploy_path)/shared dir. ```php set('shared_files', ['.env']); ``` ## Tasks ### deploy\:shared {#deploy-shared} [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/shared.php#L23) Creates symlinks for shared files and dirs. ================================================ FILE: docs/recipe/deploy/symlink.md ================================================ # Symlink Recipe ```php require 'recipe/deploy/symlink.php'; ``` [Source](/recipe/deploy/symlink.php) ## Configuration ### use_atomic_symlink [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/symlink.php#L6) Use mv -T if available. Will check automatically. ```php title="Default value" return commandSupportsOption('mv', '--no-target-directory'); ``` ## Tasks ### deploy\:symlink {#deploy-symlink} [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/symlink.php#L11) Creates symlink to release. ================================================ FILE: docs/recipe/deploy/update_code.md ================================================ # Update Code Recipe ```php require 'recipe/deploy/update_code.php'; ``` [Source](/recipe/deploy/update_code.php) ## Configuration ### branch [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/update_code.php#L12) Determines which branch to deploy. Can be overridden with CLI option `--branch`. If not specified, will get current git HEAD branch as default branch to deploy. ```php title="Default value" 'HEAD' ``` ### target [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/update_code.php#L19) The deploy target: a branch, a tag or a revision. :::info Autogenerated The value of this configuration is autogenerated on access. ::: ### update_code_strategy [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/update_code.php#L49) Sets deploy:update_code strategy. Can be one of: - local_archive (copies the repository from local machine) - archive (default, fetches the code from the remote repository) - clone (if you need the origin repository `.git` dir in your [release_path](/docs/recipe/deploy/release.md#release_path), clones from remote repository) ```php title="Default value" 'archive' ``` ### git_ssh_command [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/update_code.php#L55) Sets environment variable _GIT_SSH_COMMAND_ for `git clone` command. If `StrictHostKeyChecking` flag is set to `accept-new` then ssh will automatically add new host keys to the user known hosts files, but will not permit connections to hosts with changed host keys. ```php title="Default value" 'ssh -o StrictHostKeyChecking=accept-new' ``` ### sub_directory [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/update_code.php#L67) Specifies a sub directory within the repository to deploy. Works only when [`update_code_strategy`](#update_code_strategy) is set to `archive` (default) or `local_archive`. Example: - set value to `src` if you want to deploy the folder that lives at `/src`. - set value to `src/api` if you want to deploy the folder that lives at `/src/api`. Note: do not use a leading `/`! ```php title="Default value" false ``` ## Tasks ### deploy\:update_code {#deploy-update_code} [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/update_code.php#L73) Updates code. Update code at [release_path](/docs/recipe/deploy/release.md#release_path) on host. ================================================ FILE: docs/recipe/deploy/vendors.md ================================================ # Vendors Recipe ```php require 'recipe/deploy/vendors.php'; ``` [Source](/recipe/deploy/vendors.php) ## Configuration ### composer_action [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/vendors.php#L5) ```php title="Default value" 'install' ``` ### composer_options [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/vendors.php#L6) ```php title="Default value" '--verbose --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader' ``` ### composer_version [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/vendors.php#L7) ```php title="Default value" null ``` ### bin/composer [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/vendors.php#L10) Returns Composer binary path if found. Otherwise, tries to install composer to `.dep/composer.phar`. :::info Autogenerated The value of this configuration is autogenerated on access. ::: ## Tasks ### deploy\:vendors {#deploy-vendors} [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/vendors.php#L32) Installs vendors. ================================================ FILE: docs/recipe/deploy/writable.md ================================================ # Writable Recipe ```php require 'recipe/deploy/writable.php'; ``` [Source](/recipe/deploy/writable.php) ## Configuration ### http_user [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L9) Used to make a writable directory by a server. Used in `chown` and `acl` modes of [writable_mode](/docs/recipe/deploy/writable.md#writable_mode). Attempts automatically to detect http user in process list. :::info Autogenerated The value of this configuration is autogenerated on access. ::: ### http_group [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L26) Used to make a writable directory by a server. Used in `chgrp` mode of [writable_mode](/docs/recipe/deploy/writable.md#writable_mode) only. Attempts automatically to detect http user in process list. :::info Autogenerated The value of this configuration is autogenerated on access. ::: ### writable_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L41) List of writable dirs. ### writable_mode [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L50) One of: - chown - chgrp - chmod - acl - sticky - skip ```php title="Default value" 'acl' ``` ### writable_use_sudo [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L53) Using sudo in writable commands? ```php title="Default value" false ``` ### writable_recursive [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L56) Use recursive mode (-R)? ```php title="Default value" false ``` ### writable_chmod_mode [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L59) The chmod mode. ```php title="Default value" '0755' ``` ### writable_acl_groups [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L62) List of additional groups to give write permission to. ### writable_acl_force [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L65) Force ACLs to be reapplied even if they already exist. Useful when recursive ACLs need to reach new nested paths but sudo isn't available. Slower, so enable only to fix writable dir permissions. ```php title="Default value" false ``` ## Tasks ### deploy\:writable {#deploy-writable} [Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/writable.php#L68) Makes writable dirs. ================================================ FILE: docs/recipe/drupal7.md ================================================ # How to Deploy a Drupal 7 Project ```php require 'recipe/drupal7.php'; ``` [Source](/recipe/drupal7.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Drupal 7 application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Drupal 7** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The drupal7 recipe is based on the [common](/docs/recipe/common.md) recipe. ## Configuration ### drupal_site [Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal7.php#L15) Set Drupal 7 site. Change if you use different site ```php title="Default value" 'default' ``` ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal7.php#L18) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. Drupal 7 shared dirs ```php title="Default value" [ 'sites/{{drupal_site}}/files', ] ``` ### shared_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal7.php#L23) Overrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`. Drupal 7 shared files ```php title="Default value" [ 'sites/{{drupal_site}}/settings.php', ] ``` ### writable_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal7.php#L28) Overrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`. Drupal 7 writable dirs ```php title="Default value" [ 'sites/{{drupal_site}}/files', ] ``` ## Tasks ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal7.php#L9) This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ### drupal\:settings {#drupal-settings} [Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal7.php#L34) Create and upload Drupal 7 settings.php using values from secrets ### drupal\:upload_files {#drupal-upload_files} [Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal7.php#L76) Upload Drupal 7 files folder ================================================ FILE: docs/recipe/drupal8.md ================================================ # How to Deploy a Drupal 8 Project ```php require 'recipe/drupal8.php'; ``` [Source](/recipe/drupal8.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Drupal 8 application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Drupal 8** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The drupal8 recipe is based on the [common](/docs/recipe/common.md) recipe. ## Configuration ### drupal_site [Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal8.php#L15) Set drupal site. Change if you use different site ```php title="Default value" 'default' ``` ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal8.php#L19) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. Drupal 8 shared dirs ```php title="Default value" [ 'sites/{{drupal_site}}/files', ] ``` ### shared_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal8.php#L24) Overrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`. Drupal 8 shared files ```php title="Default value" [ 'sites/{{drupal_site}}/settings.php', 'sites/{{drupal_site}}/services.yml', ] ``` ### writable_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal8.php#L30) Overrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`. Drupal 8 Writable dirs ```php title="Default value" [ 'sites/{{drupal_site}}/files', ] ``` ## Tasks ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/drupal8.php#L9) This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ================================================ FILE: docs/recipe/flow_framework.md ================================================ # How to Deploy a Flow Framework Project ```php require 'recipe/flow_framework.php'; ``` [Source](/recipe/flow_framework.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Flow Framework application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Flow Framework** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors * [deploy:run_migrations](/docs/recipe/flow_framework.md#deploy-run_migrations) – Applies database migrations * [deploy:publish_resources](/docs/recipe/flow_framework.md#deploy-publish_resources) – Publishes resources * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The flow_framework recipe is based on the [common](/docs/recipe/common.md) recipe. ## Configuration ### flow_context [Source](https://github.com/deployphp/deployer/blob/master/recipe/flow_framework.php#L10) Flow-Framework application-context ```php title="Default value" 'Production' ``` ### flow_command [Source](https://github.com/deployphp/deployer/blob/master/recipe/flow_framework.php#L13) Flow-Framework cli-command ```php title="Default value" 'flow' ``` ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/flow_framework.php#L16) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. Flow-Framework shared directories ```php title="Default value" [ 'Data/Persistent', 'Data/Logs', 'Configuration/{{flow_context}}', ] ``` ## Tasks ### deploy\:run_migrations {#deploy-run_migrations} [Source](https://github.com/deployphp/deployer/blob/master/recipe/flow_framework.php#L26) Applies database migrations. Apply database migrations ### deploy\:publish_resources {#deploy-publish_resources} [Source](https://github.com/deployphp/deployer/blob/master/recipe/flow_framework.php#L34) Publishes resources. Publish resources ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/flow_framework.php#L42) Deploys your project. Main task This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) * [deploy:run_migrations](/docs/recipe/flow_framework.md#deploy-run_migrations) * [deploy:publish_resources](/docs/recipe/flow_framework.md#deploy-publish_resources) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ================================================ FILE: docs/recipe/fuelphp.md ================================================ # How to Deploy a Fuelphp Project ```php require 'recipe/fuelphp.php'; ``` [Source](/recipe/fuelphp.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Fuelphp application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Fuelphp** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The fuelphp recipe is based on the [common](/docs/recipe/common.md) recipe. ## Configuration ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/fuelphp.php#L10) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. FuelPHP 1.x shared dirs ```php title="Default value" [ 'fuel/app/cache', 'fuel/app/logs', ] ``` ## Tasks ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/fuelphp.php#L18) Deploys your project. Main task This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ================================================ FILE: docs/recipe/joomla.md ================================================ # How to Deploy a Joomla Project ```php require 'recipe/joomla.php'; ``` [Source](/recipe/joomla.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Joomla application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Joomla** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The joomla recipe is based on the [common](/docs/recipe/common.md) recipe. ## Configuration ### shared_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/joomla.php#L9) Overrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`. ```php title="Default value" ['configuration.php'] ``` ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/joomla.php#L10) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. ```php title="Default value" ['images'] ``` ### writable_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/joomla.php#L11) Overrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`. ```php title="Default value" ['images'] ``` ## Tasks ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/joomla.php#L14) Deploys your project. This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ================================================ FILE: docs/recipe/laravel.md ================================================ # How to Deploy a Laravel Project ```php require 'recipe/laravel.php'; ``` [Source](/recipe/laravel.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Laravel application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Laravel** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors * [artisan:storage:link](/docs/recipe/laravel.md#artisan-storage-link) – Creates the symbolic links configured for the application * [artisan:optimize](/docs/recipe/laravel.md#artisan-optimize) – Cache the framework bootstrap files * [artisan:migrate](/docs/recipe/laravel.md#artisan-migrate) – Runs the database migrations * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project * [artisan:reload](/docs/recipe/laravel.md#artisan-reload) – Reload running services The laravel recipe is based on the [common](/docs/recipe/common.md) recipe. ## Configuration ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L9) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. ```php title="Default value" ['storage'] ``` ### shared_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L10) Overrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`. ```php title="Default value" ['.env'] ``` ### writable_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L11) Overrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`. ```php title="Default value" [ 'bootstrap/cache', 'storage', ] ``` ### writable_recursive [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L15) Overrides [writable_recursive](/docs/recipe/deploy/writable.md#writable_recursive) from `recipe/deploy/writable.php`. ```php title="Default value" true ``` ### log_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L16) ```php title="Default value" 'storage/logs/*.log' ``` ### bin/artisan [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L17) ```php title="Default value" '{{release_or_current_path}}/artisan' ``` ### laravel_version [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L18) ```php title="Default value" $result = run("{{bin/php}} {{bin/artisan}} --version"); preg_match_all('/(\d+\.?)+/', $result, $matches); return $matches[0][0] ?? 5.5; ``` ### public_path [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L23) Overrides [public_path](/docs/recipe/provision/website.md#public_path) from `recipe/provision/website.php`. ```php title="Default value" 'public' ``` ## Tasks ### artisan\:down {#artisan-down} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L87) Puts the application into maintenance / demo mode. Maintenance mode. ### artisan\:up {#artisan-up} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L90) Brings the application out of maintenance mode. ### artisan\:key\:generate {#artisan-key-generate} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L97) Sets the application key. Generate keys. ### artisan\:passport\:keys {#artisan-passport-keys} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L100) Creates the encryption keys for API authentication. ### artisan\:db\:seed {#artisan-db-seed} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L107) Seeds the database with records. Database and migrations. ### artisan\:migrate {#artisan-migrate} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L110) Runs the database migrations. ### artisan\:migrate\:fresh {#artisan-migrate-fresh} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L113) Drops all tables and re-run all migrations. ### artisan\:migrate\:rollback {#artisan-migrate-rollback} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L116) Rollbacks the last database migration. ### artisan\:migrate\:status {#artisan-migrate-status} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L119) Shows the status of each migration. ### artisan\:cache\:clear {#artisan-cache-clear} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L126) Flushes the application cache. Cache and optimizations. ### artisan\:config\:cache {#artisan-config-cache} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L129) Creates a cache file for faster configuration loading. ### artisan\:config\:clear {#artisan-config-clear} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L132) Removes the configuration cache file. ### artisan\:event\:cache {#artisan-event-cache} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L135) Discovers and cache the application\'s events and listeners. ### artisan\:event\:clear {#artisan-event-clear} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L138) Clears all cached events and listeners. ### artisan\:event\:list {#artisan-event-list} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L141) Lists the application\'s events and listeners. ### artisan\:optimize {#artisan-optimize} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L144) Cache the framework bootstrap files. ### artisan\:optimize\:clear {#artisan-optimize-clear} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L147) Removes the cached bootstrap files. ### artisan\:reload {#artisan-reload} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L150) Reload running services. ### artisan\:route\:cache {#artisan-route-cache} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L153) Creates a route cache file for faster route registration. ### artisan\:route\:clear {#artisan-route-clear} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L156) Removes the route cache file. ### artisan\:route\:list {#artisan-route-list} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L159) Lists all registered routes. ### artisan\:storage\:link {#artisan-storage-link} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L162) Creates the symbolic links configured for the application. ### artisan\:view\:cache {#artisan-view-cache} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L165) Compiles all of the application\'s Blade templates. ### artisan\:view\:clear {#artisan-view-clear} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L168) Clears all compiled view files. ### artisan\:queue\:failed {#artisan-queue-failed} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L175) Lists all of the failed queue jobs. Queue and Horizon. ### artisan\:queue\:flush {#artisan-queue-flush} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L178) Flushes all of the failed queue jobs. ### artisan\:queue\:restart {#artisan-queue-restart} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L181) Restarts queue worker daemons after their current job. ### artisan\:horizon {#artisan-horizon} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L184) Starts a master supervisor in the foreground. ### artisan\:horizon\:clear {#artisan-horizon-clear} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L187) Deletes all of the jobs from the specified queue. ### artisan\:horizon\:continue {#artisan-horizon-continue} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L190) Instructs the master supervisor to continue processing jobs. ### artisan\:horizon\:list {#artisan-horizon-list} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L193) Lists all of the deployed machines. ### artisan\:horizon\:pause {#artisan-horizon-pause} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L196) Pauses the master supervisor. ### artisan\:horizon\:purge {#artisan-horizon-purge} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L199) Terminates any rogue Horizon processes. ### artisan\:horizon\:status {#artisan-horizon-status} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L202) Gets the current status of Horizon. ### artisan\:horizon\:terminate {#artisan-horizon-terminate} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L205) Terminates the master supervisor so it can be restarted. ### artisan\:horizon\:publish {#artisan-horizon-publish} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L208) Publish all of the Horizon resources. ### artisan\:horizon\:supervisors {#artisan-horizon-supervisors} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L211) Lists all of the supervisors. ### artisan\:horizon\:clear-metrics {#artisan-horizon-clear-metrics} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L214) Deletes metrics for all jobs and queues. ### artisan\:horizon\:snapshot {#artisan-horizon-snapshot} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L217) Stores a snapshot of the queue metrics. ### artisan\:schedule\:interrupt {#artisan-schedule-interrupt} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L224) Interrupt in-progress schedule:run invocations. Scheduler. ### artisan\:telescope\:clear {#artisan-telescope-clear} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L231) Clears all entries from Telescope. Telescope. ### artisan\:telescope\:prune {#artisan-telescope-prune} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L234) Prunes stale entries from the Telescope database. ### artisan\:octane {#artisan-octane} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L241) Starts the octane server. Octane. ### artisan\:octane\:reload {#artisan-octane-reload} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L244) Reloads the octane server. ### artisan\:octane\:stop {#artisan-octane-stop} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L247) Stops the octane server. ### artisan\:octane\:status {#artisan-octane-status} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L250) Check the status of the octane server. ### artisan\:nova\:publish {#artisan-nova-publish} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L257) Publish all of the Laravel Nova resources. Nova. ### artisan\:reverb\:start {#artisan-reverb-start} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L264) Starts the Reverb server. Reverb. ### artisan\:reverb\:restart {#artisan-reverb-restart} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L267) Restarts the Reverb server. ### artisan\:pulse\:check {#artisan-pulse-check} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L274) Starts the Pulse server. Pulse. ### artisan\:pulse\:restart {#artisan-pulse-restart} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L277) Restarts the Pulse server. ### artisan\:pulse\:purge {#artisan-pulse-purge} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L280) Purges all Pulse data from storage. ### artisan\:pulse\:work {#artisan-pulse-work} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L283) Process incoming Pulse data from the ingest stream. ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/laravel.php#L289) Deploys your project. Main deploy task. This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) * [artisan:storage:link](/docs/recipe/laravel.md#artisan-storage-link) * [artisan:optimize](/docs/recipe/laravel.md#artisan-optimize) * [artisan:migrate](/docs/recipe/laravel.md#artisan-migrate) * [deploy:publish](/docs/recipe/common.md#deploy-publish) * [artisan:reload](/docs/recipe/laravel.md#artisan-reload) ================================================ FILE: docs/recipe/magento.md ================================================ # How to Deploy a Magento Project ```php require 'recipe/magento.php'; ``` [Source](/recipe/magento.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Magento application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Magento** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:cache:clear](/docs/recipe/magento.md#deploy-cache-clear) – Clears cache * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The magento recipe is based on the [common](/docs/recipe/common.md) recipe. ## Configuration ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento.php#L14) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. Magento Configuration Magento shared dirs ```php title="Default value" ['var', 'media'] ``` ### shared_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento.php#L17) Overrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`. Magento shared files ```php title="Default value" ['app/etc/local.xml'] ``` ### writable_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento.php#L20) Overrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`. Magento writable dirs ```php title="Default value" ['var', 'media'] ``` ## Tasks ### deploy\:cache\:clear {#deploy-cache-clear} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento.php#L26) Clears cache. Clear cache ### deploy\:clear_version {#deploy-clear_version} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento.php#L33) Remove files that can be used to compromise Magento ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento.php#L47) Deploys your project. Main task This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:cache:clear](/docs/recipe/magento.md#deploy-cache-clear) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ================================================ FILE: docs/recipe/magento2.md ================================================ # How to Deploy a Magento 2 Project ```php require 'recipe/magento2.php'; ``` [Source](/recipe/magento2.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Magento 2 application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Magento 2** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors * [deploy:clear_paths](/docs/recipe/deploy/clear_paths.md#deploy-clear_paths) – Cleanup files and/or directories * [deploy:magento](/docs/recipe/magento2.md#deploy-magento) – Magento2 deployment operations * [magento:build](/docs/recipe/magento2.md#magento-build) – Magento2 build operations * [magento:compile](/docs/recipe/magento2.md#magento-compile) – Compiles magento di * [magento:deploy:assets](/docs/recipe/magento2.md#magento-deploy-assets) – Deploys assets * [magento:maintenance:enable-if-needed](/docs/recipe/magento2.md#magento-maintenance-enable-if-needed) – Set maintenance mode if needed * [magento:config:import](/docs/recipe/magento2.md#magento-config-import) – Config Import * [magento:upgrade](/docs/recipe/magento2.md#magento-upgrade) – Run upgrades if needed * [magento:maintenance:disable](/docs/recipe/magento2.md#magento-maintenance-disable) – Disables maintenance mode * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project In addition the **Magento 2** recipe contains an artifact deployment. This is a two step process where you first execute ```php bin/dep artifact:build [options] [localhost] ``` to build an artifact, which then is deployed on a server with ```php bin/dep artifact:deploy [host] ``` The `localhost` to build the artifact on has to be declared local, so either add ```php localhost() ->set('local', true); ``` to your deploy.php or ```yaml hosts: localhost: local: true ``` to your deploy yaml. The [artifact:build](#artifact:build) command of **Magento 2** consists of: * [build:prepare](/docs/recipe/magento2.md#build-prepare) – Prepare local artifact build * [build:remove-generated](/docs/recipe/magento2.md#build-remove-generated) – Clears generated files prior to building. * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors * [magento:compile](/docs/recipe/magento2.md#magento-compile) – Compiles magento di * [magento:deploy:assets](/docs/recipe/magento2.md#magento-deploy-assets) – Deploys assets * [artifact:package](/docs/recipe/magento2.md#artifact-package) – Packages all relevant files in an artifact. The [artifact:deploy](#artifact:deploy) command of **Magento 2** consists of: * [artifact:prepare](/docs/recipe/magento2.md#artifact-prepare) – Prepares an artifact on the target server * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [artifact:upload](/docs/recipe/magento2.md#artifact-upload) – Uploads artifact in release folder for extraction. * [artifact:extract](/docs/recipe/magento2.md#artifact-extract) – Extracts artifact in release path. * [deploy:additional-shared](/docs/recipe/magento2.md#deploy-additional-shared) – Adds additional files and dirs to the list of shared files and dirs * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [magento:maintenance:enable-if-needed](/docs/recipe/magento2.md#magento-maintenance-enable-if-needed) – Set maintenance mode if needed * [magento:config:import](/docs/recipe/magento2.md#magento-config-import) – Config Import * [magento:upgrade](/docs/recipe/magento2.md#magento-upgrade) – Run upgrades if needed * [magento:maintenance:disable](/docs/recipe/magento2.md#magento-maintenance-disable) – Disables maintenance mode * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [artifact:finish](/docs/recipe/magento2.md#artifact-finish) – Executes the tasks after artifact is released * [magento:cache:flush](/docs/recipe/magento2.md#magento-cache-flush) – Flushes Magento Cache * [cachetool:clear:opcache](/docs/contrib/cachetool.md#cachetool-clear-opcache) – Clears OPcode cache * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The magento2 recipe is based on the [common](/docs/recipe/common.md) recipe. ## Configuration ### static_content_locales [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L26) Configuration By default setup:static-content:deploy uses `en_US`. To change that, simply put `set('static_content_locales', 'en_US de_DE');` in you deployer script. ```php title="Default value" 'en_US' ``` ### magento_themes [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L43) Configuration You can also set the themes to run against. By default it'll deploy all themes - `add('magento_themes', ['Magento/luma', 'Magento/backend']);` If the themes are set as a simple list of strings, then all languages defined in [static_content_locales](/docs/recipe/magento2.md#static_content_locales) are compiled for the given themes. Alternatively The themes can be defined as an associative array, where the key represents the theme name and the key contains the languages for the compilation (for this specific theme) Example: set('magento_themes', ['Magento/luma']); - Will compile this theme with every language from [static_content_locales](/docs/recipe/magento2.md#static_content_locales) set('magento_themes', [ 'Magento/luma' => null, - Will compile all languages from [static_content_locales](/docs/recipe/magento2.md#static_content_locales) for Magento/luma 'Custom/theme' => 'en_US fr_FR' - Will compile only en_US and fr_FR for Custom/theme 'Custom/another' => '[static_content_locales](/docs/recipe/magento2.md#static_content_locales) it_IT' - Will compile all languages from [static_content_locales](/docs/recipe/magento2.md#static_content_locales) + it_IT for Custom/another ]); - Will compile this theme with every language ```php title="Default value" [ ] ``` ### static_deploy_options [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L48) Static content deployment options, e.g. '--no-parent' ### split_static_deployment [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L51) Deploy frontend and adminhtml together as default ```php title="Default value" false ``` ### static_content_locales_backend [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L54) Use the default languages for the backend as default ```php title="Default value" '{{static_content_locales}}' ``` ### magento_themes_backend [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L58) backend themes to deploy. Only used if split_static_deployment=true This setting supports the same options/structure as [magento_themes](/docs/recipe/magento2.md#magento_themes) ```php title="Default value" ['Magento/backend' => null] ``` ### static_content_jobs [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L64) Configuration Also set the number of concurrent jobs to run. The default is 1 Update using: `set('static_content_jobs', '1');` ```php title="Default value" '1' ``` ### content_version [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L66) ```php title="Default value" return time(); ``` ### magento_dir [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L71) Magento directory relative to repository root. Use "." (default) if it is not located in a subdirectory ```php title="Default value" '.' ``` ### shared_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L74) Overrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`. ```php title="Default value" [ '{{magento_dir}}/app/etc/env.php', '{{magento_dir}}/var/.maintenance.ip', ] ``` ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L78) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. ```php title="Default value" [ '{{magento_dir}}/var/composer_home', '{{magento_dir}}/var/log', '{{magento_dir}}/var/export', '{{magento_dir}}/var/report', '{{magento_dir}}/var/import', '{{magento_dir}}/var/import_history', '{{magento_dir}}/var/session', '{{magento_dir}}/var/importexport', '{{magento_dir}}/var/backups', '{{magento_dir}}/var/tmp', '{{magento_dir}}/pub/sitemap', '{{magento_dir}}/pub/media', '{{magento_dir}}/pub/static/_cache', ] ``` ### writable_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L93) Overrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`. ```php title="Default value" [ '{{magento_dir}}/var', '{{magento_dir}}/pub/static', '{{magento_dir}}/pub/media', '{{magento_dir}}/generated', '{{magento_dir}}/var/page_cache', ] ``` ### clear_paths [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L100) Overrides [clear_paths](/docs/recipe/deploy/clear_paths.md#clear_paths) from `recipe/deploy/clear_paths.php`. ```php title="Default value" [ '{{magento_dir}}/generated/*', '{{magento_dir}}/pub/static/_cache/*', '{{magento_dir}}/var/generation/*', '{{magento_dir}}/var/cache/*', '{{magento_dir}}/var/page_cache/*', '{{magento_dir}}/var/view_preprocessed/*', ] ``` ### bin/magento [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L109) ```php title="Default value" '{{release_or_current_path}}/{{magento_dir}}/bin/magento' ``` ### magento_version [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L111) ```php title="Default value" // detect version $versionOutput = run('{{bin/php}} {{bin/magento}} --version'); preg_match('/(\d+\.?)+(-p\d+)?$/', $versionOutput, $matches); return $matches[0] ?? '2.0'; ``` ### config_import_needed [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L118) :::info Autogenerated The value of this configuration is autogenerated on access. ::: ### database_upgrade_needed [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L132) :::info Autogenerated The value of this configuration is autogenerated on access. ::: ### full_upgrade_needed [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L147) ```php title="Default value" //Some conditions, such as new RabittMQ services require a full upgrade and are not detecet by setup:db:status //TODO: Add checks, once implemented, for detecting necessary full upgrade process. See future RabbitMQ Check: https://github.com/magento/magento2/pull/39698 return false; ``` ### upgrade_needed [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L153) :::info Autogenerated The value of this configuration is autogenerated on access. ::: ### enable_zerodowntime [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L163) Deploy without setting maintenance mode if possible ```php title="Default value" true ``` ### artifact_file [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L378) Artifact deployment section The file the artifact is saved to ```php title="Default value" 'artifact.tar.gz' ``` ### artifact_dir [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L381) The directory the artifact is saved in ```php title="Default value" 'artifacts' ``` ### artifact_excludes_file [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L385) Points to a file with a list of files to exclude from packaging. The format is as with the `tar --exclude-from=[file]` option ```php title="Default value" 'artifacts/excludes' ``` ### build_from_repo [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L388) If set to true, the artifact is built from a clean copy of the project repository instead of the current working directory ```php title="Default value" false ``` ### repository [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L391) Overrides [repository](/docs/recipe/common.md#repository) from `recipe/common.php`. Set this value if "build_from_repo" is set to true. The target to deploy must also be set with "--branch", "--tag" or "--revision" ```php title="Default value" null ``` ### artifact_path [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L394) The relative path to the artifact file. If the directory does not exist, it will be created ```php title="Default value" if (!testLocally('[ -d {{artifact_dir}} ]')) { runLocally('mkdir -p {{artifact_dir}}'); } return get('artifact_dir') . '/' . get('artifact_file'); ``` ### bin/tar [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L402) The location of the tar command. On MacOS you should have installed gtar, as it supports the required settings :::info Autogenerated The value of this configuration is autogenerated on access. ::: ### additional_shared_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L474) Array of shared files that will be added to the default shared_files without overriding ### additional_shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L476) Array of shared directories that will be added to the default shared_dirs without overriding ## Tasks ### magento\:compile {#magento-compile} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L173) Compiles magento di. Tasks To work correctly with artifact deployment, it is necessary to set the MAGE_MODE correctly in `app/etc/config.php` e.g. ```php 'MAGE_MODE' => 'production' ``` ### magento\:deploy\:assets {#magento-deploy-assets} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L199) Deploys assets. To work correctly with artifact deployment it is necessary to set `system/dev/js` , `system/dev/css` and `system/dev/template` in `app/etc/config.php`, e.g.: ```php 'system' => [ 'default' => [ 'dev' => [ 'js' => [ 'merge_files' => '1', 'minify_files' => '1' ], 'css' => [ 'merge_files' => '1', 'minify_files' => '1' ], 'template' => [ 'minify_html' => '1' ] ] ] ``` ### magento\:deploy\:assets\:adminhtml {#magento-deploy-assets-adminhtml} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L216) Deploys assets for backend only. ### magento\:deploy\:assets\:frontend {#magento-deploy-assets-frontend} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L221) Deploys assets for frontend only. ### magento\:sync\:content_version {#magento-sync-content_version} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L284) Syncs content version. ### magento\:maintenance\:enable {#magento-maintenance-enable} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L294) Enables maintenance mode. ### magento\:maintenance\:disable {#magento-maintenance-disable} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L300) Disables maintenance mode. ### magento\:maintenance\:enable-if-needed {#magento-maintenance-enable-if-needed} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L306) Set maintenance mode if needed. ### magento\:config\:import {#magento-config-import} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L313) Config Import. ### magento\:upgrade\:db {#magento-upgrade-db} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L322) Upgrades magento database. ### magento\:upgrade {#magento-upgrade} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L334) Run upgrades if needed. ### magento\:cache\:flush {#magento-cache-flush} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L343) Flushes Magento Cache. ### deploy\:magento {#deploy-magento} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L348) Magento2 deployment operations. This task is group task which contains next tasks: * [magento:build](/docs/recipe/magento2.md#magento-build) * [magento:maintenance:enable-if-needed](/docs/recipe/magento2.md#magento-maintenance-enable-if-needed) * [magento:config:import](/docs/recipe/magento2.md#magento-config-import) * [magento:upgrade](/docs/recipe/magento2.md#magento-upgrade) * [magento:maintenance:disable](/docs/recipe/magento2.md#magento-maintenance-disable) ### magento\:build {#magento-build} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L357) Magento2 build operations. This task is group task which contains next tasks: * [magento:compile](/docs/recipe/magento2.md#magento-compile) * [magento:deploy:assets](/docs/recipe/magento2.md#magento-deploy-assets) ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L363) Deploys your project. This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) * [deploy:clear_paths](/docs/recipe/deploy/clear_paths.md#deploy-clear_paths) * [deploy:magento](/docs/recipe/magento2.md#deploy-magento) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ### artifact\:package {#artifact-package} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L413) Packages all relevant files in an artifact. tasks section ### artifact\:upload {#artifact-upload} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L423) Uploads artifact in release folder for extraction. ### artifact\:extract {#artifact-extract} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L428) Extracts artifact in release path. ### build\:remove-generated {#build-remove-generated} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L434) Clears generated files prior to building. ### build\:prepare {#build-prepare} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L439) Prepare local artifact build. ### artifact\:build {#artifact-build} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L464) Builds an artifact. This task is group task which contains next tasks: * [build:prepare](/docs/recipe/magento2.md#build-prepare) * [build:remove-generated](/docs/recipe/magento2.md#build-remove-generated) * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) * [magento:compile](/docs/recipe/magento2.md#magento-compile) * [magento:deploy:assets](/docs/recipe/magento2.md#magento-deploy-assets) * [artifact:package](/docs/recipe/magento2.md#artifact-package) ### deploy\:additional-shared {#deploy-additional-shared} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L480) Adds additional files and dirs to the list of shared files and dirs. ### magento\:set_cache_prefix {#magento-set_cache_prefix} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L495) Update cache id_prefix. Update cache id_prefix on deploy so that you are compiling against a fresh cache Reference Issue: https://github.com/davidalger/capistrano-magento2/issues/151 To use this feature, add the following to your deployer scripts: ```php after('deploy:shared', 'magento:set_cache_prefix'); after('deploy:magento', 'magento:cleanup_cache_prefix'); ``` ### magento\:cleanup_cache_prefix {#magento-cleanup_cache_prefix} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L535) Cleanup cache id_prefix env files. After successful deployment, move the tmp_env.php file to env.php ready for next deployment ### magento\:cron\:stop {#magento-cron-stop} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L551) Remove cron from crontab and kill running cron jobs. Remove cron from crontab and kill running cron jobs To use this feature, add the following to your deployer scripts: ```php after('magento:maintenance:enable-if-needed', 'magento:cron:stop'); ``` ### magento\:cron\:install {#magento-cron-install} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L567) Install cron in crontab. Install cron in crontab To use this feature, add the following to your deployer scripts: ```php after('magento:upgrade', 'magento:cron:install'); ``` ### artifact\:prepare {#artifact-prepare} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L573) Prepares an artifact on the target server. This task is group task which contains next tasks: * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) * [artifact:upload](/docs/recipe/magento2.md#artifact-upload) * [artifact:extract](/docs/recipe/magento2.md#artifact-extract) * [deploy:additional-shared](/docs/recipe/magento2.md#deploy-additional-shared) * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) ### artifact\:finish {#artifact-finish} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L586) Executes the tasks after artifact is released. This task is group task which contains next tasks: * [magento:cache:flush](/docs/recipe/magento2.md#magento-cache-flush) * [cachetool:clear:opcache](/docs/contrib/cachetool.md#cachetool-clear-opcache) * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) * [deploy:success](/docs/recipe/common.md#deploy-success) ### artifact\:deploy {#artifact-deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L595) Actually releases the artifact deployment. This task is group task which contains next tasks: * [artifact:prepare](/docs/recipe/magento2.md#artifact-prepare) * [magento:maintenance:enable-if-needed](/docs/recipe/magento2.md#magento-maintenance-enable-if-needed) * [magento:config:import](/docs/recipe/magento2.md#magento-config-import) * [magento:upgrade](/docs/recipe/magento2.md#magento-upgrade) * [magento:maintenance:disable](/docs/recipe/magento2.md#magento-maintenance-disable) * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) * [artifact:finish](/docs/recipe/magento2.md#artifact-finish) ================================================ FILE: docs/recipe/pimcore.md ================================================ # How to Deploy a Pimcore Project ```php require 'recipe/pimcore.php'; ``` [Source](/recipe/pimcore.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Pimcore application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Pimcore** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors * [deploy:cache:clear](/docs/recipe/symfony.md#deploy-cache-clear) – Clears cache * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The pimcore recipe is based on the [symfony](/docs/recipe/symfony.md) recipe. ## Tasks ### pimcore\:rebuild-classes {#pimcore-rebuild-classes} [Source](https://github.com/deployphp/deployer/blob/master/recipe/pimcore.php#L16) Rebuilds Pimcore Classes. ### pimcore\:cache_clear {#pimcore-cache_clear} [Source](https://github.com/deployphp/deployer/blob/master/recipe/pimcore.php#L22) Removes cache. ### pimcore\:deploy {#pimcore-deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/pimcore.php#L26) This task is group task which contains next tasks: * [pimcore:rebuild-classes](/docs/recipe/pimcore.md#pimcore-rebuild-classes) * [pimcore:cache_clear](/docs/recipe/pimcore.md#pimcore-cache_clear) ================================================ FILE: docs/recipe/prestashop.md ================================================ # How to Deploy a Prestashop Project ```php require 'recipe/prestashop.php'; ``` [Source](/recipe/prestashop.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Prestashop application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Prestashop** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The prestashop recipe is based on the [common](/docs/recipe/common.md) recipe. ## Configuration ### shared_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/prestashop.php#L9) Overrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`. ```php title="Default value" [ 'config/settings.inc.php', '.htaccess', ] ``` ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/prestashop.php#L13) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. ```php title="Default value" [ 'img', 'log', 'download', 'upload', 'translations', 'mails', 'themes/default-bootstrap/lang', 'themes/default-bootstrap/mails', 'themes/default-bootstrap/pdf/lang', ] ``` ### writable_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/prestashop.php#L24) Overrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`. ```php title="Default value" [ 'img', 'log', 'cache', 'download', 'upload', 'translations', 'mails', 'themes/default-bootstrap/lang', 'themes/default-bootstrap/mails', 'themes/default-bootstrap/pdf/lang', 'themes/default-bootstrap/cache', ] ``` ================================================ FILE: docs/recipe/provision/databases.md ================================================ # Databases Recipe ```php require 'recipe/provision/databases.php'; ``` [Source](/recipe/provision/databases.php) ## Configuration ### db_type [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/databases.php#L5) :::info Autogenerated The value of this configuration is autogenerated on access. ::: ### db_name [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/databases.php#L15) ```php title="Default value" return ask(' DB name: ', 'prod'); ``` ### db_user [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/databases.php#L19) ```php title="Default value" return ask(' DB user: ', 'deployer'); ``` ### db_password [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/databases.php#L23) ```php title="Default value" return askHiddenResponse(' DB password: '); ``` ## Tasks ### provision\:databases {#provision-databases} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/databases.php#L28) Provision databases. ### provision\:mysql {#provision-mysql} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/databases.php#L40) Provision MySQL. ### provision\:mariadb {#provision-mariadb} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/databases.php#L51) Provision MariaDB. ### provision\:postgresql {#provision-postgresql} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/databases.php#L62) Provision PostgreSQL. ================================================ FILE: docs/recipe/provision/nodejs.md ================================================ # Nodejs Recipe ```php require 'recipe/provision/nodejs.php'; ``` [Source](/recipe/provision/nodejs.php) ## Configuration ### node_version [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/nodejs.php#L7) ```php title="Default value" '--lts' ``` ## Tasks ### provision\:node {#provision-node} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/nodejs.php#L10) Installs npm packages. ================================================ FILE: docs/recipe/provision/php.md ================================================ # Php Recipe ```php require 'recipe/provision/php.php'; ``` [Source](/recipe/provision/php.php) ## Configuration ### php_version [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/php.php#L5) :::info Autogenerated The value of this configuration is autogenerated on access. ::: ## Tasks ### provision\:php {#provision-php} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/php.php#L18) Installs PHP packages. ### logs\:php-fpm {#logs-php-fpm} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/php.php#L73) Shows php-fpm logs. ### provision\:composer {#provision-composer} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/php.php#L82) Installs Composer. ================================================ FILE: docs/recipe/provision/user.md ================================================ # User Recipe ```php require 'recipe/provision/user.php'; ``` [Source](/recipe/provision/user.php) ## Configuration ### sudo_password [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/user.php#L7) ```php title="Default value" return askHiddenResponse(' Password for sudo: '); ``` ## Tasks ### provision\:user {#provision-user} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/user.php#L13) Setups a deployer user. ### provision\:ssh_copy_id {#provision-ssh_copy_id} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/user.php#L61) Copy public key to remote server. ================================================ FILE: docs/recipe/provision/website.md ================================================ # Website Recipe ```php require 'recipe/provision/website.php'; ``` [Source](/recipe/provision/website.php) ## Configuration ### domain [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/website.php#L7) ```php title="Default value" return ask(' Domain: ', get('hostname')); ``` ### public_path [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/website.php#L11) ```php title="Default value" return ask(' Public path: ', 'public'); ``` ## Tasks ### provision\:server {#provision-server} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/website.php#L16) Configures a server. ### provision\:website {#provision-website} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/website.php#L25) Provision website. ### logs\:access {#logs-access} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/website.php#L69) Shows access logs. ### logs\:caddy {#logs-caddy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision/website.php#L74) Shows caddy syslog. ================================================ FILE: docs/recipe/provision.md ================================================ # Provision Recipe ```php require 'recipe/provision.php'; ``` [Source](/recipe/provision.php) * Requires * [databases](/docs/recipe/provision/databases.md) * [nodejs](/docs/recipe/provision/nodejs.md) * [php](/docs/recipe/provision/php.md) * [user](/docs/recipe/provision/user.md) * [website](/docs/recipe/provision/website.md) ## Configuration ### lsb_release [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L19) Name of lsb_release like: focal, bionic, etc. As only Ubuntu 20.04 LTS is supported for provision should be the `focal`. ```php title="Default value" return run("lsb_release -s -c"); ``` ### provision_user [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L43) Default user to use for provisioning. ```php title="Default value" 'root' ``` ## Tasks ### provision {#provision} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L24) Provision the server. This task is group task which contains next tasks: * [provision:check](/docs/recipe/provision.md#provision-check) * [provision:configure](/docs/recipe/provision.md#provision-configure) * [provision:update](/docs/recipe/provision.md#provision-update) * [provision:upgrade](/docs/recipe/provision.md#provision-upgrade) * [provision:install](/docs/recipe/provision.md#provision-install) * [provision:ssh](/docs/recipe/provision.md#provision-ssh) * [provision:firewall](/docs/recipe/provision.md#provision-firewall) * [provision:user](/docs/recipe/provision/user.md#provision-user) * [provision:php](/docs/recipe/provision/php.md#provision-php) * [provision:node](/docs/recipe/provision/nodejs.md#provision-node) * [provision:databases](/docs/recipe/provision/databases.md#provision-databases) * [provision:composer](/docs/recipe/provision/php.md#provision-composer) * [provision:server](/docs/recipe/provision/website.md#provision-server) * [provision:website](/docs/recipe/provision/website.md#provision-website) * [provision:verify](/docs/recipe/provision.md#provision-verify) ### provision\:check {#provision-check} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L46) Checks pre-required state. ### provision\:configure {#provision-configure} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L71) Collects required params. ### provision\:update {#provision-update} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L123) Adds repositories and update. ### provision\:upgrade {#provision-upgrade} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L149) Upgrades all packages. ### provision\:install {#provision-install} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L157) Installs packages. ### provision\:ssh {#provision-ssh} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L192) Configures the ssh. ### provision\:firewall {#provision-firewall} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L204) Setups a firewall. ### provision\:verify {#provision-verify} [Source](https://github.com/deployphp/deployer/blob/master/recipe/provision.php#L213) Verifies what provision was successful. ================================================ FILE: docs/recipe/shopware.md ================================================ # How to Deploy a Shopware Project ```php require 'recipe/shopware.php'; ``` [Source](/recipe/shopware.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Shopware application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Shopware** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [sw:writable:jwt](/docs/recipe/shopware.md#sw-writable-jwt) – * [sw:deploy](/docs/recipe/shopware.md#sw-deploy) – * [sw:database:migrate](/docs/recipe/shopware.md#sw-database-migrate) – * [sw:plugin:refresh](/docs/recipe/shopware.md#sw-plugin-refresh) – * [sw:theme:refresh](/docs/recipe/shopware.md#sw-theme-refresh) – * [sw:scheduled-task:register](/docs/recipe/shopware.md#sw-scheduled-task-register) – * [sw:cache:clear](/docs/recipe/shopware.md#sw-cache-clear) – * [sw:plugin:update:all](/docs/recipe/shopware.md#sw-plugin-update-all) – * [sw:cache:clear](/docs/recipe/shopware.md#sw-cache-clear) – * [deploy:clear_paths](/docs/recipe/deploy/clear_paths.md#deploy-clear_paths) – Cleanup files and/or directories * [sw:cache:warmup](/docs/recipe/shopware.md#sw-cache-warmup) – * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The shopware recipe is based on the [common](/docs/recipe/common.md) recipe. ## Usage Add `repository` to your _deploy.php_ file: ```php set('repository', 'git@github.com:shopware/production.git'); ``` configure host: ```php host('SSH-HOSTNAME') ->set('remote_user', 'SSH-USER') ->set('deploy_path', '/var/www/shopware') // This is the path where deployer will create its directory structure ->set('http_user', 'www-data') // Not needed, if the `user` is the same, the web server is running with ->set('http_group', 'www-data') ->set('writable_mode', 'chmod') ->set('writable_recursive', true) ->set('become', 'www-data'); // You might want to change user to execute remote tasks because of access rights of created cache files ``` :::note Please remember that the installation must be modified so that it can be [build without database](https://developer.shopware.com/docs/guides/hosting/installation-updates/deployments/build-w-o-db#compiling-the-storefront-without-database). ::: ## Configuration ### bin/console [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L35) ```php title="Default value" '{{bin/php}} {{release_or_current_path}}/bin/console' ``` ### default_timeout [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L37) Overrides [default_timeout](/docs/recipe/common.md#default_timeout) from `recipe/common.php`. ### shared_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L40) Overrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`. These files are shared among all releases. ```php title="Default value" [ '.env.local', 'install.lock', 'public/.htaccess', 'public/.user.ini', ] ``` ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L48) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. These directories are shared among all releases. ```php title="Default value" [ 'config/jwt', 'files', 'var/log', 'public/media', 'public/plugins', 'public/thumbnail', 'public/sitemap', ] ``` ### writable_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L60) Overrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`. These directories are made writable (the definition of "writable" requires attention). Please note that the files in `config/jwt/*` receive special attention in the `sw:writable:jwt` task. ```php title="Default value" [ 'config/jwt', 'custom/plugins', 'files', 'public/bundles', 'public/css', 'public/fonts', 'public/js', 'public/media', 'public/plugins', 'public/sitemap', 'public/theme', 'public/thumbnail', 'var', ] ``` ### shopware_version [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L77) This sets the Shopware version to the version of the Shopware console command. ```php title="Default value" $versionOutput = run('cd {{release_path}} && {{bin/console}} -V'); preg_match('/(\d+\.\d+\.\d+\.\d+)/', $versionOutput, $matches); return $matches[0] ?? '6.6.0'; ``` ## Tasks ### sw\:cache\:clear {#sw-cache-clear} [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L84) This task remotely executes the `cache:clear` console command on the target server. ### sw\:cache\:warmup {#sw-cache-warmup} [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L90) This task remotely executes the cache warmup console commands on the target server, so that the first user, who visits the website, doesn't have to wait for the cache to be built up. ### sw\:database\:migrate {#sw-database-migrate} [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L100) This task remotely executes the `database:migrate` console command on the target server. ### sw\:plugin\:refresh {#sw-plugin-refresh} [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L104) ### sw\:scheduled-task\:register {#sw-scheduled-task-register} [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L108) ### sw\:theme\:refresh {#sw-theme-refresh} [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L112) ### sw\:theme\:compile {#sw-theme-compile} [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L118) This task is not used by default, but can be used, e.g. in combination with `SHOPWARE_SKIP_THEME_COMPILE=1`, to build the theme remotely instead of locally. ### sw\:plugin\:update\:all {#sw-plugin-update-all} [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L130) ### sw\:writable\:jwt {#sw-writable-jwt} [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L140) ### sw\:deploy {#sw-deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L150) Grouped SW deploy tasks. This task is group task which contains next tasks: * [sw:database:migrate](/docs/recipe/shopware.md#sw-database-migrate) * [sw:plugin:refresh](/docs/recipe/shopware.md#sw-plugin-refresh) * [sw:theme:refresh](/docs/recipe/shopware.md#sw-theme-refresh) * [sw:scheduled-task:register](/docs/recipe/shopware.md#sw-scheduled-task-register) * [sw:cache:clear](/docs/recipe/shopware.md#sw-cache-clear) * [sw:plugin:update:all](/docs/recipe/shopware.md#sw-plugin-update-all) * [sw:cache:clear](/docs/recipe/shopware.md#sw-cache-clear) ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L161) Deploys your project. This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [sw:writable:jwt](/docs/recipe/shopware.md#sw-writable-jwt) * [sw:deploy](/docs/recipe/shopware.md#sw-deploy) * [deploy:clear_paths](/docs/recipe/deploy/clear_paths.md#deploy-clear_paths) * [sw:cache:warmup](/docs/recipe/shopware.md#sw-cache-warmup) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ### sw-build-without-db\:get-remote-config {#sw-build-without-db-get-remote-config} [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L180) ### sw-build-without-db\:build {#sw-build-without-db-build} [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L193) ### sw-build-without-db {#sw-build-without-db} [Source](https://github.com/deployphp/deployer/blob/master/recipe/shopware.php#L197) This task is group task which contains next tasks: * [sw-build-without-db:get-remote-config](/docs/recipe/shopware.md#sw-build-without-db-get-remote-config) * [sw-build-without-db:build](/docs/recipe/shopware.md#sw-build-without-db-build) ================================================ FILE: docs/recipe/silverstripe.md ================================================ # How to Deploy a Silverstripe Project ```php require 'recipe/silverstripe.php'; ``` [Source](/recipe/silverstripe.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Silverstripe application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Silverstripe** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors * [silverstripe:buildflush](/docs/recipe/silverstripe.md#silverstripe-buildflush) – Runs /dev/build?flush=all * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The silverstripe recipe is based on the [common](/docs/recipe/common.md) recipe. ## Configuration ### shared_assets [Source](https://github.com/deployphp/deployer/blob/master/recipe/silverstripe.php#L13) Silverstripe configuration ```php title="Default value" if (test('[ -d {{release_or_current_path}}/public ]') || test('[ -d {{deploy_path}}/shared/public ]')) { return 'public/assets'; } return 'assets'; ``` ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/silverstripe.php#L22) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. Silverstripe shared dirs ```php title="Default value" [ '{{shared_assets}}', ] ``` ### writable_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/silverstripe.php#L27) Overrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`. Silverstripe writable dirs ```php title="Default value" [ '{{shared_assets}}', ] ``` ### silverstripe_cli_script [Source](https://github.com/deployphp/deployer/blob/master/recipe/silverstripe.php#L32) Silverstripe cli script :::info Autogenerated The value of this configuration is autogenerated on access. ::: ## Tasks ### silverstripe\:build {#silverstripe-build} [Source](https://github.com/deployphp/deployer/blob/master/recipe/silverstripe.php#L48) Runs /dev/build. Helper tasks ### silverstripe\:buildflush {#silverstripe-buildflush} [Source](https://github.com/deployphp/deployer/blob/master/recipe/silverstripe.php#L53) Runs /dev/build?flush=all. ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/silverstripe.php#L61) Deploys your project. Main task This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) * [silverstripe:buildflush](/docs/recipe/silverstripe.md#silverstripe-buildflush) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ================================================ FILE: docs/recipe/spiral.md ================================================ # How to Deploy a Spiral Project ```php require 'recipe/spiral.php'; ``` [Source](/recipe/spiral.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Spiral application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Spiral** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors * [spiral:encrypt-key](/docs/recipe/spiral.md#spiral-encrypt-key) – Generate new encryption key, if it doesn\'t exist * [spiral:configure](/docs/recipe/spiral.md#spiral-configure) – Configure project * [deploy:download-rr](/docs/recipe/spiral.md#deploy-download-rr) – Download RoadRunner * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project * [deploy:restart-rr](/docs/recipe/spiral.md#deploy-restart-rr) – Restart RoadRunner The spiral recipe is based on the [common](/docs/recipe/common.md) recipe. ## Configuration ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L10) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. Spiral shared dirs ```php title="Default value" ['runtime'] ``` ### writable_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L13) Overrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`. Spiral writable dirs ```php title="Default value" ['runtime', 'public'] ``` ### roadrunner_path [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L16) Path to the RoadRunner server ```php title="Default value" '{{release_or_current_path}}' ``` ### dotenv_example [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L18) Overrides [dotenv_example](/docs/recipe/deploy/env.md#dotenv_example) from `recipe/deploy/env.php`. ```php title="Default value" '.env.sample' ``` ## Tasks ### spiral\:configure {#spiral-configure} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L58) Configure project. Spiral Framework console commands ### spiral\:cycle {#spiral-cycle} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L61) Update (init) cycle schema from database and annotated classes. ### spiral\:migrate {#spiral-migrate} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L64) Perform all outstanding migrations. ### spiral\:update {#spiral-update} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L67) Update project state. ### spiral\:cache\:clean {#spiral-cache-clean} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L70) Clean application runtime cache. ### spiral\:i18n\:reset {#spiral-i18n-reset} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L73) Reset translation cache. ### spiral\:encrypt-key {#spiral-encrypt-key} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L76) Generate new encryption key, if it doesn\'t exist. ### spiral\:views\:compile {#spiral-views-compile} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L79) Warm-up view cache. ### spiral\:views\:reset {#spiral-views-reset} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L82) Clear view cache. ### cycle\:migrate {#cycle-migrate} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L88) Generate ORM schema migrations. Cycle ORM and migrations console commands ### cycle\:render {#cycle-render} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L91) Render available CycleORM schemas. ### cycle\:sync {#cycle-sync} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L94) Sync Cycle ORM schema with database without intermediate migration (risk operation). ### migrate\:init {#migrate-init} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L97) Init migrations component (create migrations table). ### migrate\:replay {#migrate-replay} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L100) Replay (down, up) one or multiple migrations. ### migrate\:rollback {#migrate-rollback} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L103) Rollback one (default) or multiple migrations. ### migrate\:status {#migrate-status} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L106) Get list of all available migrations and their statuses. ### roadrunner\:serve {#roadrunner-serve} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L112) Start RoadRunner server. RoadRunner console commands ### roadrunner\:stop {#roadrunner-stop} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L117) Stop RoadRunner server. ### roadrunner\:reset {#roadrunner-reset} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L120) Reset workers of all services. ### deploy\:download-rr {#deploy-download-rr} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L126) Download RoadRunner. Download and restart RoadRunner ### deploy\:restart-rr {#deploy-restart-rr} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L132) Restart RoadRunner. ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/spiral.php#L146) Deploys your project. Main task This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) * [spiral:encrypt-key](/docs/recipe/spiral.md#spiral-encrypt-key) * [spiral:configure](/docs/recipe/spiral.md#spiral-configure) * [deploy:download-rr](/docs/recipe/spiral.md#deploy-download-rr) * [deploy:publish](/docs/recipe/common.md#deploy-publish) * [deploy:restart-rr](/docs/recipe/spiral.md#deploy-restart-rr) ================================================ FILE: docs/recipe/statamic.md ================================================ # How to Deploy a Statamic Project ```php require 'recipe/statamic.php'; ``` [Source](/recipe/statamic.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Statamic application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Statamic** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors * [artisan:storage:link](/docs/recipe/laravel.md#artisan-storage-link) – Creates the symbolic links configured for the application * [artisan:cache:clear](/docs/recipe/laravel.md#artisan-cache-clear) – Flushes the application cache * [statamic:stache:clear](/docs/recipe/statamic.md#statamic-stache-clear) – Clears the "Stache" cache * [statamic:stache:warm](/docs/recipe/statamic.md#statamic-stache-warm) – Builds the "Stache" cache * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The statamic recipe is based on the [laravel](/docs/recipe/laravel.md) recipe. ## Configuration ### statamic_version [Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L16) ```php title="Default value" $result = run('{{bin/php}} {{release_or_current_path}}/please --version'); preg_match_all('/(\d+\.?)+/', $result, $matches); return $matches[0][0] ?? 'unknown'; ``` ## Tasks ### statamic\:addons\:discover {#statamic-addons-discover} [Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L27) Rebuilds the cached addon package manifest. Addons ### statamic\:assets\:generate-presets {#statamic-assets-generate-presets} [Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L34) Generates asset preset manipulations. Assets ### statamic\:assets\:meta {#statamic-assets-meta} [Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L37) Generates asset metadata files. ### statamic\:git\:commit {#statamic-git-commit} [Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L44) Git add and commit tracked content. Git ### statamic\:glide\:clear {#statamic-glide-clear} [Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L51) Clears the Glide image cache. Glide ### statamic\:responsive\:generate {#statamic-responsive-generate} [Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L58) Generates responsive images. Responsive Images (not in the core) ### statamic\:responsive\:regenerate {#statamic-responsive-regenerate} [Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L61) Regenerate responsive images. ### statamic\:search\:insert {#statamic-search-insert} [Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L68) Inserts an item into its search indexes. Search ### statamic\:search\:update {#statamic-search-update} [Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L71) Update a search index. ### statamic\:stache\:clear {#statamic-stache-clear} [Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L78) Clears the "Stache" cache. Stache ### statamic\:stache\:doctor {#statamic-stache-doctor} [Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L81) Diagnose any problems with the Stache. ### statamic\:stache\:refresh {#statamic-stache-refresh} [Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L84) Clears and rebuild the "Stache" cache. ### statamic\:stache\:warm {#statamic-stache-warm} [Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L87) Builds the "Stache" cache. ### statamic\:static\:clear {#statamic-static-clear} [Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L94) Clears the static page cache. Static ### statamic\:static\:warm {#statamic-static-warm} [Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L97) Warms the static cache by visiting all URLs. ### statamic\:support\:details {#statamic-support-details} [Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L104) Outputs details helpful for support requests. Support ### statamic\:updates\:run {#statamic-updates-run} [Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L111) Runs update scripts from specific version. Updated ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/statamic.php#L119) Deploys your project. Main Deploy Script for Statamic, which will overwrite the Laravel default. This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) * [artisan:storage:link](/docs/recipe/laravel.md#artisan-storage-link) * [artisan:cache:clear](/docs/recipe/laravel.md#artisan-cache-clear) * [statamic:stache:clear](/docs/recipe/statamic.md#statamic-stache-clear) * [statamic:stache:warm](/docs/recipe/statamic.md#statamic-stache-warm) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ================================================ FILE: docs/recipe/sulu.md ================================================ # How to Deploy a Sulu Project ```php require 'recipe/sulu.php'; ``` [Source](/recipe/sulu.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Sulu application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Sulu** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors * [deploy:cache:clear](/docs/recipe/symfony.md#deploy-cache-clear) – Clears cache * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The sulu recipe is based on the [symfony](/docs/recipe/symfony.md) recipe. ## Configuration ### bin/websiteconsole [Source](https://github.com/deployphp/deployer/blob/master/recipe/sulu.php#L13) ```php title="Default value" return parse('{{bin/php}} {{release_or_current_path}}/bin/websiteconsole --no-interaction'); ``` ## Tasks ### phpcr\:migrate {#phpcr-migrate} [Source](https://github.com/deployphp/deployer/blob/master/recipe/sulu.php#L18) Migrates PHPCR. ### deploy\:website\:cache\:clear {#deploy-website-cache-clear} [Source](https://github.com/deployphp/deployer/blob/master/recipe/sulu.php#L23) Clears cache. ### deploy\:website\:cache\:warmup {#deploy-website-cache-warmup} [Source](https://github.com/deployphp/deployer/blob/master/recipe/sulu.php#L28) Warmups cache. ================================================ FILE: docs/recipe/symfony.md ================================================ # How to Deploy a Symfony Application ```php require 'recipe/symfony.php'; ``` [Source](/recipe/symfony.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Symfony application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Symfony** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors * [deploy:cache:clear](/docs/recipe/symfony.md#deploy-cache-clear) – Clears cache * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The symfony recipe is based on the [common](/docs/recipe/common.md) recipe. ## Configuration ### symfony_version [Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L9) ```php title="Default value" $result = run('{{bin/console}} --version'); preg_match_all('/(\d+\.?)+/', $result, $matches); return $matches[0][0] ?? 5.0; ``` ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L15) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. ```php title="Default value" [ 'var/log', ] ``` ### shared_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L19) Overrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`. ```php title="Default value" [ '.env.local', ] ``` ### writable_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L23) Overrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`. ```php title="Default value" [ 'var', 'var/cache', 'var/log', 'var/sessions', ] ``` ### log_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L30) ```php title="Default value" 'var/log/*.log' ``` ### migrations_config [Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L32) ### doctrine_schema_validate_config [Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L34) ### bin/console [Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L36) ```php title="Default value" '{{bin/php}} {{release_or_current_path}}/bin/console' ``` ### console_options [Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L38) ```php title="Default value" return '--no-interaction'; ``` ## Tasks ### database\:migrate {#database-migrate} [Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L43) Migrates database. ### doctrine\:schema\:validate {#doctrine-schema-validate} [Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L53) Validate the Doctrine mapping files. ### deploy\:cache\:clear {#deploy-cache-clear} [Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L58) Clears cache. ### deploy\:dump-env {#deploy-dump-env} [Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L67) Optimize environment variables. ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/symfony.php#L74) Deploys project. This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) * [deploy:cache:clear](/docs/recipe/symfony.md#deploy-cache-clear) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ================================================ FILE: docs/recipe/typo3.md ================================================ # How to Deploy a TYPO3 Project ```php require 'recipe/typo3.php'; ``` [Source](/recipe/typo3.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your TYPO3 application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **TYPO3** consists of: * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [typo3:update_code](/docs/recipe/typo3.md#typo3-update_code) – * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors * [typo3:install:fixfolderstructure](/docs/recipe/typo3.md#typo3-install-fixfolderstructure) – TYPO3 - Fix folder structure * [typo3:extension:setup](/docs/recipe/typo3.md#typo3-extension-setup) – TYPO3 - Set up all extensions * [typo3:language:update](/docs/recipe/typo3.md#typo3-language-update) – TYPO3 - Update the language files of all activated extensions * [typo3:cache:flush](/docs/recipe/typo3.md#typo3-cache-flush) – TYPO3 - Clear all caches * [typo3:cache:warmup](/docs/recipe/typo3.md#typo3-cache-warmup) – TYPO3 - Cache warmup for system caches * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The typo3 recipe is based on the [common](/docs/recipe/common.md) recipe. TYPO3 Deployer Recipe Usage Examples: Deploy to production (using Git as source): vendor/bin/dep deploy production Deploy to staging using rsync: # In deploy.php or servers config, enable rsync set('use_rsync', true); vendor/bin/dep deploy staging Common TYPO3 commands: vendor/bin/dep typo3:cache:flush # Clear all TYPO3 caches vendor/bin/dep typo3:cache:warmup # Warmup system caches vendor/bin/dep typo3:language:update # Update extension language files vendor/bin/dep typo3:extension:setup # Set up all extensions vendor/bin/dep typo3:install:fixfolderstructure # Automatically create required files and folders for TYPO3 ## Configuration ### composer_config [Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L35) Parse composer.json and return its contents as an array. Used for auto-detecting TYPO3 settings like public_dir and bin_dir. ```php title="Default value" return json_decode(file_get_contents('./composer.json'), true, 512, JSON_THROW_ON_ERROR); ``` ### typo3/public_dir [Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L44) TYPO3 public (web) directory. Automatically determined from composer.json. Defaults to "public". :::info Autogenerated The value of this configuration is autogenerated on access. ::: ### bin/typo3 [Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L58) Path to the TYPO3 CLI binary. Determined from composer.json "config.bin-dir" or defaults to "vendor/bin/typo3". :::info Autogenerated The value of this configuration is autogenerated on access. ::: ### log_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L71) Log files to display when running `./vendor/bin/dep logs:app` ```php title="Default value" 'var/log/typo3_*.log' ``` ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L77) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. Directories that persist between releases. Shared via symlinks from the shared/ directory. ```php title="Default value" [ '{{typo3/public_dir}}/fileadmin', '{{typo3/public_dir}}/typo3temp/assets', 'var/lock', 'var/log', 'var/session', 'var/spool', ] ``` ### writable_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L99) Overrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`. Writeable directories ```php title="Default value" [ '{{typo3/public_dir}}/fileadmin', '{{typo3/public_dir}}/typo3temp/assets', 'var/cache', 'var/lock', 'var/log', ] ``` ### composer_options [Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L110) Overrides [composer_options](/docs/recipe/deploy/vendors.md#composer_options) from `recipe/deploy/vendors.php`. Composer install options for production. ```php title="Default value" ' --no-dev --verbose --prefer-dist --no-progress --no-interaction --optimize-autoloader' ``` ### use_rsync [Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L116) If set in the config this recipe uses rsync. Default setting: false (uses the Git repository) ```php title="Default value" false ``` ### update_code_task [Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L118) ```php title="Default value" return get('use_rsync') ? 'rsync' : 'deploy:update_code'; ``` ### rsync [Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L145) ```php title="Default value" [ 'exclude' => array_merge(get('shared_dirs'), get('shared_files'), $exclude), 'exclude-file' => false, 'include' => ['vendor'], 'include-file' => false, 'filter' => ['dir-merge,-n /.gitignore'], 'filter-file' => false, 'filter-perdir' => false, 'flags' => 'avz', 'options' => ['delete', 'keep-dirlinks', 'links'], 'timeout' => 600, ] ``` ### typo3_updateschema_types [Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L162) List of schema update types. `safe` includes all necessary operations, to add or change fields or tables. ```php title="Default value" 'safe' ``` ## Tasks ### typo3\:update_code {#typo3-update_code} [Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L122) ### typo3\:cache\:flush {#typo3-cache-flush} [Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L171) TYPO3 - Clear all caches. TYPO3 Commands All run via [bin/php](/docs/recipe/common.md#bin/php) [release_path](/docs/recipe/deploy/release.md#release_path)/[bin/typo3](/docs/recipe/typo3.md#bin/typo3) ### typo3\:cache\:warmup {#typo3-cache-warmup} [Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L176) TYPO3 - Cache warmup for system caches. ### typo3\:language\:update {#typo3-language-update} [Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L181) TYPO3 - Update the language files of all activated extensions. ### typo3\:extension\:setup {#typo3-extension-setup} [Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L186) TYPO3 - Set up all extensions. ### typo3\:install\:fixfolderstructure {#typo3-install-fixfolderstructure} [Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L191) TYPO3 - Fix folder structure. ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/typo3.php#L212) Deploys a TYPO3 project. Main deploy task for TYPO3. 1. Lock deploy to avoid concurrent runs 2. Create release directory 3. Update code (Git or rsync) 4. Symlink shared dirs/files 5. Fix TYPO3 folder structure 6. Ensure writable dirs 7. Run extension setup & perform schema updates 8. Update language files 9. Install composer vendors 10. Flush caches 11. Warm up TYPO3 caches 12. Unlock and clean up This task is group task which contains next tasks: * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) * [typo3:update_code](/docs/recipe/typo3.md#typo3-update_code) * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) * [typo3:install:fixfolderstructure](/docs/recipe/typo3.md#typo3-install-fixfolderstructure) * [typo3:extension:setup](/docs/recipe/typo3.md#typo3-extension-setup) * [typo3:language:update](/docs/recipe/typo3.md#typo3-language-update) * [typo3:cache:flush](/docs/recipe/typo3.md#typo3-cache-flush) * [typo3:cache:warmup](/docs/recipe/typo3.md#typo3-cache-warmup) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ================================================ FILE: docs/recipe/wordpress.md ================================================ # How to Deploy a WordPress Project ```php require 'recipe/wordpress.php'; ``` [Source](/recipe/wordpress.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your WordPress application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **WordPress** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The wordpress recipe is based on the [common](/docs/recipe/common.md) recipe. ## Configuration ### shared_files [Source](https://github.com/deployphp/deployer/blob/master/recipe/wordpress.php#L9) Overrides [shared_files](/docs/recipe/deploy/shared.md#shared_files) from `recipe/deploy/shared.php`. ```php title="Default value" ['wp-config.php'] ``` ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/wordpress.php#L10) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. ```php title="Default value" ['wp-content/uploads'] ``` ### writable_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/wordpress.php#L11) Overrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`. ```php title="Default value" ['wp-content/uploads'] ``` ## Tasks ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/wordpress.php#L14) Deploys your project. This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ================================================ FILE: docs/recipe/yii.md ================================================ # How to Deploy a Yii2 Project ```php require 'recipe/yii.php'; ``` [Source](/recipe/yii.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Yii2 application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Yii2** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors * [deploy:migrate](/docs/recipe/yii.md#deploy-migrate) – Runs Yii2 migrations for your project * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The yii recipe is based on the [common](/docs/recipe/common.md) recipe. ## Configuration ### shared_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/yii.php#L10) Overrides [shared_dirs](/docs/recipe/deploy/shared.md#shared_dirs) from `recipe/deploy/shared.php`. Yii shared dirs ```php title="Default value" ['runtime'] ``` ### writable_dirs [Source](https://github.com/deployphp/deployer/blob/master/recipe/yii.php#L13) Overrides [writable_dirs](/docs/recipe/deploy/writable.md#writable_dirs) from `recipe/deploy/writable.php`. Yii writable dirs ```php title="Default value" ['runtime'] ``` ## Tasks ### deploy\:migrate {#deploy-migrate} [Source](https://github.com/deployphp/deployer/blob/master/recipe/yii.php#L16) Runs Yii2 migrations for your project. ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/yii.php#L24) Deploys your project. Main task This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) * [deploy:migrate](/docs/recipe/yii.md#deploy-migrate) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ================================================ FILE: docs/recipe/zend_framework.md ================================================ # How to Deploy a Zend Framework Project ```php require 'recipe/zend_framework.php'; ``` [Source](/recipe/zend_framework.php) Deployer is a free and open source deployment tool written in PHP. It helps you to deploy your Zend Framework application to a server. It is very easy to use and has a lot of features. Three main features of Deployer are: - **Provisioning** - provision your server for you. - **Zero downtime deployment** - deploy your application without a downtime. - **Rollbacks** - rollback your application to a previous version, if something goes wrong. Additionally, Deployer has a lot of other features, like: - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax. - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application. - **Secure** - Deployer uses SSH to connect to your server. - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks. You can read more about Deployer in [Getting Started](/docs/getting-started.md). The [deploy](#deploy) task of **Zend Framework** consists of: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) – Prepares a new release * [deploy:info](/docs/recipe/deploy/info.md#deploy-info) – Displays info about deployment * [deploy:setup](/docs/recipe/deploy/setup.md#deploy-setup) – Prepares host for deploy * [deploy:lock](/docs/recipe/deploy/lock.md#deploy-lock) – Locks deploy * [deploy:release](/docs/recipe/deploy/release.md#deploy-release) – Prepares release * [deploy:update_code](/docs/recipe/deploy/update_code.md#deploy-update_code) – Updates code * [deploy:env](/docs/recipe/deploy/env.md#deploy-env) – Configure .env file * [deploy:shared](/docs/recipe/deploy/shared.md#deploy-shared) – Creates symlinks for shared files and dirs * [deploy:writable](/docs/recipe/deploy/writable.md#deploy-writable) – Makes writable dirs * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) – Installs vendors * [deploy:publish](/docs/recipe/common.md#deploy-publish) – Publishes the release * [deploy:symlink](/docs/recipe/deploy/symlink.md#deploy-symlink) – Creates symlink to release * [deploy:unlock](/docs/recipe/deploy/lock.md#deploy-unlock) – Unlocks deploy * [deploy:cleanup](/docs/recipe/deploy/cleanup.md#deploy-cleanup) – Cleanup old releases * [deploy:success](/docs/recipe/common.md#deploy-success) – Deploys your project The zend_framework recipe is based on the [common](/docs/recipe/common.md) recipe. ## Tasks ### deploy {#deploy} [Source](https://github.com/deployphp/deployer/blob/master/recipe/zend_framework.php#L13) Deploys your project. Main task This task is group task which contains next tasks: * [deploy:prepare](/docs/recipe/common.md#deploy-prepare) * [deploy:vendors](/docs/recipe/deploy/vendors.md#deploy-vendors) * [deploy:publish](/docs/recipe/common.md#deploy-publish) ================================================ FILE: docs/selector.md ================================================ # Selector Deployer uses the selector to choose hosts. Each host can have a set of labels. Labels are key-value pairs. For example, `stage: production` or `role: web`. You can use labels to select hosts. For example, `dep deploy stage=production` will deploy to all hosts with `stage: production` label. Let's define two labels, **type** and **env**, to our hosts: ```php host('web.example.com') ->setLabels([ 'type' => 'web', 'env' => 'prod', ]); host('db.example.com') ->setLabels([ 'type' => 'db', 'env' => 'prod', ]); ``` or use `->addLabels()` method to add labels to the existing host. Now let's define a task to check labels: ```php task('info', function () { writeln('type:' . get('labels')['type'] . ' env:' . get('labels')['env']); }); ``` Now we can run this task with a selector: ```bash $ dep info env=prod task info [web.example.com] type:web env:prod [db.example.com] type:db env:prod ``` As you can see, Deployer will run this task on all hosts with the `env: prod` label. And if we define only the `type` label, Deployer will run this task on the specified host. ```bash dep info type=web task info [web.example.com] type:web env:prod ``` ## Selector syntax Selector syntax consists of a list of conditions, separated by `,` or `&`. There comma means **OR** and `&` means **AND**. For example, `type=web,env=prod` is a selector of: `type=web` **OR** `env=prod`. ```bash $ dep info 'type=web,env=prod' task info [web.example.com] type:web env:prod [db.example.com] type:db env:prod ``` As you can see, both hosts are selected (as both of them have the `env: prod` label). We can use `&` to define **AND**. For example, `type=web & env=prod` is a selector for hosts with `type: web` **AND** `env: prod` labels. ```bash $ dep info 'type=web & env=prod' task info [web.example.com] type:web env:prod ``` We can use `|` to define **OR** in a subquery. For example, `type=web|db & env=prod` is a selector for hosts with (`type: web` **OR** `type: db`) **AND** `env: prod` labels. ```bash $ dep info 'type=web|db & env=prod' task info [web.example.com] type:web env:prod [db.example.com] type:db env:prod ``` We can also use `!=` to negate a label. For example, `type!=web` is a selector for all hosts which do not have a `type: web` label. ```bash $ dep info 'type!=web' task info [db.example.com] type:db env:prod ``` :::note Deployer CLI can take a few selectors as arguments. For example, `dep info type=web env=prod` is the same as `dep info 'type=web,env=prod'`. You can install bash autocompletion for Deployer CLI, which will help you to write selectors. See [installation](installation.md) for more. ::: Deployer also has a few special selectors: - `all` - select all hosts - `alias=...` - select host by alias If a selector does not contain an `=` sign, Deployer will assume that it is an alias. For example `dep info web.example.com` is the same as `dep info alias=web.example.com`. ```bash $ dep info web.example.com task info [web.example.com] type:web env:prod ``` ```bash $ dep info 'web.example.com' 'db.example.com' $ # Same as: $ dep info 'alias=web.example.com,alias=db.example.com' ```` ## Using the select() function You can use the [select()](api.md#select) function to select hosts by selector in your PHP code. ```php task('info', function () { $hosts = select('type=web|db,env=prod'); foreach ($hosts as $host) { writeln('type:' . $host->get('labels')['type'] . ' env:' . $host->get('labels')['env']); } }); ``` Or you can use the [on()](api.md#on) function to run a task on selected hosts. ```php task('info', function () { on(select('all'), function () { writeln('type:' . get('labels')['type'] . ' env:' . get('labels')['env']); }); }); ``` ## Task selectors To restrict a task to run only on selected hosts, you can use the [select()](tasks.md#select) method. ```php task('info', function () { // ... })->select('type=web|db,env=prod'); ``` ## Labels in YAML You can also define labels in a YAML recipe. For example: ```yaml hosts: web.example.com: remote_user: deployer env: environment: production labels: env: prod ``` But make sure to distinguish between the `env` and `labels.env` keys. `env` is a configuration key, and `labels.env` is a label. ```php task('info', function () { writeln('env:' . get('env')['environment'] . ' labels.env:' . get('labels')['env']); }); ``` Will print: ```bash $ dep info env=prod task info [web.example.com] env:production labels.env:prod ``` ================================================ FILE: docs/sidebar.js ================================================ module.exports = [ "installation", "getting-started", "basics", { type: "category", label: "Main Concepts", items: ["hosts", "tasks", "selector"], }, "ci-cd", "yaml", "cli", "api", { type: "category", label: "Other", items: ["avoid-php-fpm-reloading", "UPGRADE", "KNOWN_BUGS"], }, ]; ================================================ FILE: docs/tasks.md ================================================ # Tasks Define a task by using the [task](api.md#task) function. Also, you can give a description for a task with the [desc](api.md#desc) function called before _task_: ```php desc('My task'); task('my_task', function () { .... }); ``` To get the task or override task config, call the _task_ function without the second argument: ```php task('my_task')->disable(); ``` ## Task config ### desc() Sets task's description. ```php task('deploy', function () { // ... })->desc('Task description'); ``` Same as using [desc()](api.md#desc) function helper: ```php desc('Task description'); task('deploy', function () { // ... }); ``` ### once() Sets the task to run only on one of the selected hosts. ### oncePerNode() Sets the task to run only on **one node** of the selected hosts. The node is identified by its [hostname](hosts.md#hostname). For instance, multiple hosts might deploy to a single physical machine (with a unique hostname). ```php host('foo')->setHostname('example.com'); host('bar')->setHostname('example.com'); host('pro')->setHostname('another.com'); task('apt:update', function () { // This task will be executed twice, only on "foo" and "pro" hosts. run('apt-get update'); })->oncePerNode(); ``` ### hidden() Hides the task from CLI usage page. ### addBefore() Adds a before hook to the task. ### addAfter() Adds an after hook to the task. ### limit() Limits the number of hosts the task will be executed on in parallel. Default is unlimited (runs the task on all hosts in parallel). ### select() Sets the task's host selector. ### addSelector() Adds the task's selector. ### verbose() Makes the task always verbose, as if the `-v` option is persistently enabled. ### disable() Disables the task. the task will not be executed. ### enable() Enables the task. ## Task grouping You can combine tasks in groups: ```php task('deploy', [ 'deploy:prepare', 'deploy:update_code', 'deploy:vendors', 'deploy:symlink', 'cleanup' ]); ``` ## Task hooks You can define tasks to be run before or after specific tasks. ```php task('deploy:done', function () { writeln('Deploy done!'); }); after('deploy', 'deploy:done'); ``` After the `deploy` task executed, `deploy:done` will be triggered. :::note You can see which hooks are enabled via the **dep tree** command. ``` dep tree deploy ``` ::: ================================================ FILE: docs/yaml.md ================================================ # YAML Deployer supports recipes written in YAML. For validating the structure, Deployer uses the JSON Schema declared in [schema.json](https://github.com/deployphp/deployer/blob/master/src/schema.json). Here is an example of a YAML recipe: ```yaml import: - recipe/laravel.php config: repository: "git@github.com:example/example.com.git" remote_user: deployer hosts: example.com: deploy_path: "~/example" tasks: build: - cd: "{{release_path}}" - run: "npm run build" after: deploy:failed: deploy:unlock ``` YAML recipes can include recipes written in PHP. For example, some tasks maybe written in PHP and imported into YAML. Conversely, it's also possible to import a YAML recipe from PHP using the [import()](api.md#import) function. ================================================ FILE: phpstan.neon ================================================ includes: - tests/phpstan-baseline.neon parameters: level: 5 paths: - src - recipe - contrib ignoreErrors: - "#^Constant DEPLOYER_VERSION not found\\.$#" - "#^Constant DEPLOYER_BIN not found\\.$#" - "#^Constant MASTER_ENDPOINT not found\\.$#" - "#CpanelPhp#" - "#AMQPMessage#" excludePaths: - src/Component/PharUpdate/* ================================================ FILE: phpunit.xml ================================================ src/ recipe/ vendor/ bin/ tests/src/ tests/legacy/ tests/joy/ ================================================ FILE: recipe/cakephp.php ================================================ desc('Initialization'); /** * Run migrations */ task('deploy:run_migrations', function () { run('{{bin/php}} {{release_or_current_path}}/bin/cake.php migrations migrate --no-lock'); run('{{bin/php}} {{release_or_current_path}}/bin/cake.php schema_cache build'); })->desc('Run migrations'); /** * Main task */ task('deploy', [ 'deploy:prepare', 'deploy:vendors', 'deploy:init', 'deploy:run_migrations', 'deploy:publish', ])->desc('Deploy your project'); ================================================ FILE: recipe/codeigniter.php ================================================ #.#: The minimum Codeigniter4 version required (included). * - 'max' => #.#: The maximum Codeigniter4 version required (included). * - 'skipIfNoEnv': Skip and warn the user if `.env` file is inexistent or empty. * - 'failIfNoEnv': Fail the command if `.env` file is inexistent or empty. * - 'showOutput': Show the output of the command if given. * * @param string $command The spark command (with cli options if any). * @param array $options The options that define the behavior of the command. * @return callable A function that can be used as a task. */ function spark($command, $options = []) { return function () use ($command, $options) { // Ensure the spark command is available on the current version. $versionTooEarly = array_key_exists('min', $options) && codeigniter4_version_compare($options['min'], '<'); $versionTooLate = array_key_exists('max', $options) && codeigniter4_version_compare($options['max'], '>'); if ($versionTooEarly || $versionTooLate) { return; } // Ensure we warn or fail when a command relies on the ".env" file. if (in_array('failIfNoEnv', $options) && !test('[ -s {{release_or_current_path}}/.env ]')) { throw new \Exception('Your .env file is empty! Cannot proceed.'); } if (in_array('skipIfNoEnv', $options) && !test('[ -s {{release_or_current_path}}/.env ]')) { warning("Your .env file is empty! Skipping..."); return; } $spark = '{{release_or_current_path}}/spark'; // Run the spark command. $output = run("{{bin/php}} $spark $command"); // Output the results when appropriate. if (in_array('showOutput', $options)) { writeln("$output"); } }; } function codeigniter4_version_compare($version, $comparator) { return version_compare(get('codeigniter4_version'), $version, $comparator); } /** * Discover & Checks */ desc('Shows file cache information in the current system.'); task('spark:cache:info', spark('cache:info', ['showOutput'])); desc('Check your Config values.'); task('spark:config:check', spark('config:check', ['skipIfNoEnv', 'showOutput', 'min' => '4.5.0'])); desc('Retrieves the current environment, or set a new one.'); task('spark:env', spark('env', ['skipIfNoEnv', 'showOutput'])); desc('Check filters for a route.'); task('spark:filter:check', spark('filter:check', ['showOutput', 'min' => '4.3.0'])); desc('Find and save available phrases to translate.'); task('spark:lang:find', spark('lang:find', ['showOutput', 'min' => '4.5.0'])); desc('Verifies your namespaces are setup correctly.'); task('spark:namespaces', spark('namespaces', ['showOutput'])); desc('Check your php.ini values.'); task('spark:phpini:check', spark('phpini:check', ['showOutput', 'min' => '4.5.0'])); desc('Displays all routes.'); task('spark:routes', spark('routes', ['showOutput', 'min' => '4.3.0'])); /** * Actions */ desc('Generates a new encryption key and writes it in an `.env` file.'); task('spark:key:generate', spark('key:generate', ['skipIfNoEnv'])); desc('Optimize for production.'); task('spark:optimize', spark('optimize', ['min' => '4.5.0'])); desc('Discovers and executes all predefined Publisher classes.'); task('spark:publish', spark('publish', ['skipIfNoEnv', 'showOutput'])); /* * Database and migrations. */ desc('Create a new database schema.'); task('spark:db:create', spark('db:create', ['showOutput'])); desc('Runs the specified seeder to populate known data into the database.'); task('spark:db:seed', spark('db:seed', ['skipIfNoEnv'])); desc('Retrieves information on the selected table.'); task('spark:db:table', spark('db:table', ['skipIfNoEnv', 'showOutput', 'min' => '4.5.0'])); desc('Locates and runs all new migrations against the database.'); task('spark:migrate', spark('migrate --all', ['skipIfNoEnv'])); desc('Does a rollback followed by a latest to refresh the current state of the database.'); task('spark:migrate:refresh', spark('migrate:refresh -f --all', ['skipIfNoEnv'])); desc('Runs the "down" method for all migrations in the last batch.'); task('spark:migrate:rollback', spark('migrate:rollback -f', ['skipIfNoEnv', 'showOutput'])); desc('Displays a list of all migrations and whether they\'ve been run or not.'); task('spark:migrate:status', spark('migrate:status', ['skipIfNoEnv', 'showOutput'])); /** * Housekeeping */ desc('Clears the current system caches.'); task('spark:cache:clear', spark('cache:clear')); desc('Clears all debugbar JSON files.'); task('spark:debugbar:clear', spark('debugbar:clear')); desc('Clears all log files.'); task('spark:logs:clear', spark('logs:clear')); /** * Custom Spark Command for shield or setting packages */ desc('Run a custom spark command.'); task('spark:custom', spark('', ['showOutput'])); /** * Main deploy task. */ desc('Deploys your project'); task('deploy', [ 'deploy:prepare', 'deploy:vendors', 'spark:optimize', 'spark:migrate', 'deploy:publish', ]); ================================================ FILE: recipe/common.php ================================================ 'something', * ]); * ``` * * It is possible to override it per `run()` call. * * ```php * run('echo $KEY', env: ['KEY' => 'over']); * ``` */ set('env', []); /** * Path to `.env` file which will be used as environment variables for each command per `run()`. * * ```php * set('dotenv', '{{release_or_current_path}}/.env'); * ``` */ set('dotenv', false); /** * The deploy path. * * For example can be set for a bunch of host once as: * ```php * set('deploy_path', '~/{{alias}}'); * ``` */ set('deploy_path', function () { throw new ConfigurationException('Please, specify `deploy_path`.'); }); /** * Return current release path. Default to {{deploy_path}}/`current`. * ```php * set('current_path', '/var/public_html'); * ``` */ set('current_path', '{{deploy_path}}/current'); // Path to the `php` bin. set('bin/php', function () { if (currentHost()->hasOwn('php_version')) { return '/usr/bin/php{{php_version}}'; } return which('php'); }); // Path to the `git` bin. set('bin/git', function () { return which('git'); }); // Should {{bin/symlink}} use `--relative` option or not. Will detect // automatically. set('use_relative_symlink', function () { return commandSupportsOption('ln', '--relative'); }); // Path to the `ln` bin. With predefined options `-nfs`. set('bin/symlink', function () { return get('use_relative_symlink') ? 'ln -nfs --relative' : 'ln -nfs'; }); // Path to a file which will store temp script with sudo password. // Defaults to `.dep/sudo_pass`. This script is only temporary and will be deleted after // sudo command executed. set('sudo_askpass', function () { if (test('[ -d {{deploy_path}}/.dep ]')) { return '{{deploy_path}}/.dep/sudo_pass'; } else { return '/tmp/dep_sudo_pass'; } }); desc('Prepares a new release'); task('deploy:prepare', [ 'deploy:info', 'deploy:setup', 'deploy:lock', 'deploy:release', 'deploy:update_code', 'deploy:env', 'deploy:shared', 'deploy:writable', ]); desc('Publishes the release'); task('deploy:publish', [ 'deploy:symlink', 'deploy:unlock', 'deploy:cleanup', 'deploy:success', ]); desc('Deploys your project'); task('deploy', [ 'deploy:prepare', 'deploy:publish', ]); /** * Prints success message */ task('deploy:success', function () { info('successfully deployed!'); }) ->hidden(); /** * Hook on deploy failure. */ task('deploy:failed', function () {}) ->hidden(); fail('deploy', 'deploy:failed'); /** * Follows latest application logs. */ desc('Shows application logs'); task('logs:app', function () { if (!has('log_files')) { warning("Please, specify \"log_files\" option."); return; } cd('{{current_path}}'); run('tail -f {{log_files}}'); })->verbose(); ================================================ FILE: recipe/composer.php ================================================ contao-manager/login.lock'); }); desc('Enable maintenance mode'); task('contao:maintenance:enable', function () { // Enable maintenance mode in both the current and release build, so that the maintenance mode will be enabled // for the current installation before the symlink changes and the new installation after the symlink changed. foreach (array_unique([parse('{{current_path}}'), parse('{{release_or_current_path}}')]) as $path) { // The current path might not be present during first deploy. if (!test("[ -d $path ]")) { continue; } cd($path); run('{{bin/console}} contao:maintenance-mode enable {{console_options}}'); } }); desc('Disable maintenance mode'); task('contao:maintenance:disable', function () { foreach (array_unique([parse('{{current_path}}'), parse('{{release_or_current_path}}')]) as $path) { if (!test("[ -d $path ]")) { continue; } cd($path); run('{{bin/console}} contao:maintenance-mode disable {{console_options}}'); } }); desc('Deploy the project'); task('deploy', [ 'deploy:prepare', 'deploy:vendors', 'contao:maintenance:enable', 'contao:migrate', 'contao:maintenance:disable', 'deploy:publish', ]); ================================================ FILE: recipe/craftcms.php ================================================ $output"); } }; } /* * Migrations */ desc('Runs all pending Craft, plugin, and content migrations'); task('craft:migrate/all', craft('migrate/all')); desc('Upgrades Craft by applying new migrations'); task('craft:migrate/up', craft('migrate/up')); /* * Generate keys */ desc('Generates a new application ID and saves it in the `.env` file'); task('craft:setup/app-id', craft('setup/app-id')); desc('Generates a new security key and saves it in the `.env` file'); task('craft:setup/security-key', craft('setup/security-key')); /* * Project config */ desc('Applies project config file changes.'); task('craft:project-config/apply', craft('project-config/apply')); /* * Caches */ desc('Flushes all caches registered in the system'); task('craft:cache/flush-all', craft('cache/flush-all')); desc('Clear all caches'); task('craft:clear-caches/all', craft('clear-caches/all')); desc('Clear all Asset caches'); task('craft:clear-caches/asset', craft('clear-caches/asset')); desc('Clear all Asset indexing data'); task('craft:clear-caches/asset-indexing-data', craft('clear-caches/asset-indexing-data')); desc('Clear all compiled classes'); task('craft:clear-caches/compiled-classes', craft('clear-caches/compiled-classes')); desc('Clear all compiled templates'); task('craft:clear-caches/compiled-templates', craft('clear-caches/compiled-templates')); desc('Clear all control panel resources'); task('craft:clear-caches/cp-resources', craft('clear-caches/cp-resources')); desc('Clear all data caches'); task('craft:clear-caches/data', craft('clear-caches/data')); desc('Clear all temp files'); task('craft:clear-caches/temp-files', craft('clear-caches/temp-files')); /* * Garbage collection */ desc('Runs garbage collection'); task('craft:gc', craft('gc --delete-all-trashed=1 --silent-exit-on-exception=1', ['showOutput'])); /* * Main deploy */ desc('Deploys Craft CMS'); task('deploy', [ 'deploy:prepare', 'deploy:vendors', 'craft:clear-caches/compiled-classes', 'craft:migrate/all', 'craft:project-config/apply', 'craft:gc', 'craft:clear-caches/all', 'deploy:publish', ]); ================================================ FILE: recipe/deploy/check_remote.php ================================================ getOption('revision'); if (!$targetRevision) { $ref = 'HEAD'; $opt = ''; if ($tag = input()->getOption('tag')) { $ref = $tag; $opt = '--tags'; } elseif ($branch = get('branch')) { $ref = $branch; $opt = '--heads'; } $remoteLs = runLocally("git ls-remote $opt $repository $ref"); if (strstr($remoteLs, "\n")) { throw new Exception("Could not determine target revision. '$ref' matched multiple commits."); } if (!$remoteLs) { throw new Exception("Could not resolve a revision from '$ref'."); } $targetRevision = substr($remoteLs, 0, strpos($remoteLs, "\t")); } // Compare commit hashes. We use strpos to support short versions. $targetRevision = trim($targetRevision); $lastDeployedRevision = run('cat {{current_path}}/REVISION'); if ($targetRevision && strpos($lastDeployedRevision, $targetRevision) === 0) { throw new GracefulShutdownException("Already up-to-date."); } info("deployed different version"); }); ================================================ FILE: recipe/deploy/cleanup.php ================================================ 0) { foreach (array_slice($releases, $keep) as $release) { run("$sudo rm -rf {{deploy_path}}/releases/$release"); } } }); ================================================ FILE: recipe/deploy/clear_paths.php ================================================ getAlias(); }); desc('Displays info about deployment'); task('deploy:info', function () { $releaseName = test('[ -d {{deploy_path}}/.dep ]') ? get('release_name') : 1; info("deploying {{what}} to {{where}} (release {$releaseName})"); }); ================================================ FILE: recipe/deploy/lock.php ================================================ {{deploy_path}}/.dep/deploy.lock"); if ($locked === '+locked') { $lockedUser = run("cat {{deploy_path}}/.dep/deploy.lock"); throw new GracefulShutdownException( "Deploy locked by $lockedUser.\n" . "Execute \"deploy:unlock\" task to unlock.", ); } }); desc('Unlocks deploy'); task('deploy:unlock', function () { run("rm -f {{deploy_path}}/.dep/deploy.lock");//always success }); desc('Checks if deploy is locked'); task('deploy:is_locked', function () { $locked = test("[ -f {{deploy_path}}/.dep/deploy.lock ]"); if ($locked) { $lockedUser = run("cat {{deploy_path}}/.dep/deploy.lock"); throw new GracefulShutdownException("Deploy is locked by $lockedUser."); } info('Deploy is unlocked.'); }); ================================================ FILE: recipe/deploy/push.php ================================================ false, 'options' => ['--relative']], ); // Mark this release as dirty. run("echo '{{user}}' > {{current_path}}/DIRTY_RELEASE"); }); ================================================ FILE: recipe/deploy/release.php ================================================ .dep/latest_release"); } // Metainfo. $timestamp = timestamp(); $metainfo = [ 'created_at' => $timestamp, 'release_name' => $releaseName, 'user' => get('user'), 'target' => get('target'), ]; // Save metainfo about release. $json = escape_shell_argument(json_encode($metainfo)); run("echo $json >> .dep/releases_log"); // Make new release. run("mkdir -p $releasePath"); run("{{bin/symlink}} $releasePath {{deploy_path}}/release"); // Add to releases list. array_unshift($releasesList, $releaseName); set('releases_list', $releasesList); // Set previous_release. if (isset($releasesList[1])) { set('previous_release', "{{deploy_path}}/releases/{$releasesList[1]}"); } }); desc('Shows releases list'); /* * Example output: * ``` * +---------------------+------example.org ------------+--------+-----------+ * | Date (UTC) | Release | Author | Target | Commit | * +---------------------+-------------+----------------+--------+-----------+ * | 2021-11-06 20:51:45 | 1 | Anton Medvedev | HEAD | 34d24192e | * | 2021-11-06 21:00:50 | 2 (bad) | Anton Medvedev | HEAD | 392948a40 | * | 2021-11-06 23:19:20 | 3 | Anton Medvedev | HEAD | a4057a36c | * | 2021-11-06 23:24:30 | 4 (current) | Anton Medvedev | HEAD | s3wa45ca6 | * +---------------------+-------------+----------------+--------+-----------+ * ``` */ task('releases', function () { cd('{{deploy_path}}'); $releasesLog = get('releases_log'); $currentRelease = basename(run('readlink {{current_path}}')); $releasesList = get('releases_list'); $table = []; $tz = !empty(getenv('TIMEZONE')) ? getenv('TIMEZONE') : date_default_timezone_get(); foreach ($releasesLog as &$metainfo) { $date = \DateTime::createFromFormat(\DateTimeInterface::ISO8601, $metainfo['created_at']); $date->setTimezone(new \DateTimeZone($tz)); $status = $release = $metainfo['release_name']; if (in_array($release, $releasesList, true)) { if (test("[ -f releases/$release/BAD_RELEASE ]")) { $status = "$release (bad)"; } elseif (test("[ -f releases/$release/DIRTY_RELEASE ]")) { $status = "$release (dirty)"; } else { $status = "$release"; } try { $revision = run("cat releases/$release/REVISION"); } catch (\Throwable $e) { $revision = 'unknown'; } } else { $revision = 'unknown'; } if ($release === $currentRelease) { $status .= ' (current)'; } $table[] = [ $date->format("Y-m-d H:i:s"), $status, $metainfo['user'], $metainfo['target'], $revision, ]; } (new Table(output())) ->setHeaderTitle(currentHost()->getAlias()) ->setHeaders(["Date ($tz)", 'Release', 'Author', 'Target', 'Commit']) ->setRows($table) ->render(); }); ================================================ FILE: recipe/deploy/rollback.php ================================================ $currentRelease."); if (!test("[ -d releases/$candidate ]")) { throw new \RuntimeException(parse("Release \"$candidate\" not found in \"{{deploy_path}}/releases\".")); } if (test("[ -f releases/$candidate/BAD_RELEASE ]")) { writeln("Candidate $candidate marked as bad release."); if (!askConfirmation("Continue rollback to $candidate?")) { writeln('Rollback aborted.'); return; } } writeln("Rolling back to $candidate release."); // Symlink to old release. run("{{bin/symlink}} releases/$candidate {{current_path}}"); // Mark release as bad. $timestamp = timestamp(); run("echo '$timestamp,{{user}}' > releases/$currentRelease/BAD_RELEASE"); writeln("rollback to release $candidate was successful"); }); ================================================ FILE: recipe/deploy/setup.php ================================================ getVerbosity() === OutputInterface::VERBOSITY_DEBUG ? 'v' : ''; foreach (get('shared_dirs') as $dir) { // Make sure all path without tailing slash. $dir = trim($dir, '/'); // Check if shared dir does not exist. if (!test("[ -d $sharedPath/$dir ]")) { // Create shared dir if it does not exist. run("mkdir -p $sharedPath/$dir"); // If release contains shared dir, copy that dir from release to shared. if (test("[ -d $(echo {{release_path}}/$dir) ]")) { run("cp -r$copyVerbosity {{release_path}}/$dir $sharedPath/" . dirname($dir)); } } // Remove from source. run("rm -rf {{release_path}}/$dir"); // Create path to shared dir in release dir if it does not exist. // Symlink will not create the path and will fail otherwise. run("mkdir -p `dirname {{release_path}}/$dir`"); // Symlink shared dir to release dir run("{{bin/symlink}} $sharedPath/$dir {{release_path}}/$dir"); } foreach (get('shared_files') as $file) { $dirname = dirname(parse($file)); // Create dir of shared file if not existing if (!test("[ -d $sharedPath/$dirname ]")) { run("mkdir -p $sharedPath/$dirname"); } // Check if shared file does not exist in shared. // and file exist in release if (!test("[ -f $sharedPath/$file ]") && test("[ -f {{release_path}}/$file ]")) { // Copy file in shared dir if not present run("cp -r$copyVerbosity {{release_path}}/$file $sharedPath/$file"); } // Remove from source. run("if [ -f $(echo {{release_path}}/$file) ]; then rm -rf {{release_path}}/$file; fi"); // Ensure dir is available in release run("if [ ! -d $(echo {{release_path}}/$dirname) ]; then mkdir -p {{release_path}}/$dirname;fi"); // Touch shared run("[ -f $sharedPath/$file ] || touch $sharedPath/$file"); // Symlink shared dir to release dir run("{{bin/symlink}} $sharedPath/$file {{release_path}}/$file"); } }); ================================================ FILE: recipe/deploy/symlink.php ================================================ hasOption('branch') && !empty(input()->getOption('branch'))) { $target = input()->getOption('branch'); } if (input()->hasOption('tag') && !empty(input()->getOption('tag'))) { $target = input()->getOption('tag'); } if (input()->hasOption('revision') && !empty(input()->getOption('revision'))) { $target = input()->getOption('revision'); } if (empty($target)) { $target = "HEAD"; } return $target; }); // Sets deploy:update_code strategy. // Can be one of: // - local_archive (copies the repository from local machine) // - archive (default, fetches the code from the remote repository) // - clone (if you need the origin repository `.git` dir in your {{release_path}}, clones from remote repository) set('update_code_strategy', 'archive'); // Sets environment variable _GIT_SSH_COMMAND_ for `git clone` command. // If `StrictHostKeyChecking` flag is set to `accept-new` then ssh will // automatically add new host keys to the user known hosts files, but // will not permit connections to hosts with changed host keys. set('git_ssh_command', 'ssh -o StrictHostKeyChecking=accept-new'); /** * Specifies a sub directory within the repository to deploy. * Works only when [`update_code_strategy`](#update_code_strategy) is set to `archive` (default) or `local_archive`. * * Example: * - set value to `src` if you want to deploy the folder that lives at `/src`. * - set value to `src/api` if you want to deploy the folder that lives at `/src/api`. * * Note: do not use a leading `/`! */ set('sub_directory', false); /** * Update code at {{release_path}} on host. */ desc('Updates code'); task('deploy:update_code', function () { $strategy = get('update_code_strategy'); $target = get('target'); $git = get('bin/git'); $targetWithDir = $target; if (!empty(get('sub_directory'))) { $targetWithDir .= ':{{sub_directory}}'; } if ($strategy === 'local_archive') { $gitRoot = runLocally("$git rev-parse --show-toplevel"); runLocally("$git -C " . escapeshellarg($gitRoot) . " archive $targetWithDir -o archive.tar"); upload("$gitRoot/archive.tar", '{{release_path}}/archive.tar'); run("tar -xf {{release_path}}/archive.tar -C {{release_path}}"); run("rm {{release_path}}/archive.tar"); unlink("$gitRoot/archive.tar"); $rev = escapeshellarg(runLocally("git rev-list $target -1")); } else { $repository = get('repository'); if (empty($repository)) { throw new ConfigurationException("Missing 'repository' configuration."); } $bare = parse('{{deploy_path}}/.dep/repo'); $env = [ 'GIT_TERMINAL_PROMPT' => '0', 'GIT_SSH_COMMAND' => get('git_ssh_command'), ]; start: // Clone the repository to a bare repo. run("[ -d $bare ] || mkdir -p $bare"); run("[ -f $bare/HEAD ] || $git clone --mirror $repository $bare 2>&1", env: $env); cd($bare); // If remote url changed, drop `.dep/repo` and reinstall. if (run("$git config --get remote.origin.url") !== $repository) { cd('{{deploy_path}}'); run("rm -rf $bare"); goto start; } run("$git remote update 2>&1", env: $env); // Copy to release_path. if ($strategy === 'archive') { run("$git archive $targetWithDir | tar -x -f - -C {{release_path}} 2>&1"); } elseif ($strategy === 'clone') { cd('{{release_path}}'); run("$git clone -l $bare ."); run("$git remote set-url origin $repository", env: $env); run("$git checkout --force $target"); } else { throw new ConfigurationException(parse("Unknown `update_code_strategy` option: {{update_code_strategy}}.")); } $rev = escapeshellarg(run("$git rev-list $target -1")); } // Save git revision in REVISION file. run("echo $rev > {{release_path}}/REVISION"); }); ================================================ FILE: recipe/deploy/vendors.php ================================================ &1'); }); ================================================ FILE: recipe/deploy/writable.php ================================================ $group) { if ($index > 0) { $setFaclGroups .= " "; } $setFaclGroups .= "-m g:\"$group\":rwX"; } // Check if remote user exists, before adding it to setfacl $remoteUserExists = test("id -u $remoteUser &>/dev/null 2>&1 || exit 0"); if ($remoteUserExists === true) { $setFaclUsers .= " -m u:$remoteUser:rwX"; } if (empty($sudo)) { // When running without sudo, exception may be thrown // if executing setfacl on files created by http user (in directory that has been setfacl before). // These directories/files should be skipped unless forcing ACL reset. // Now, we will check each directory for ACL and only setfacl for which has not been set before, // unless writable_acl_force is enabled. $writeableDirs = get('writable_dirs'); $forceAcl = get('writable_acl_force'); foreach ($writeableDirs as $dir) { // Check if ACL has been set or not $hasfacl = run("getfacl -p $dir | grep \"^user:$httpUser:.*w\" | wc -l"); // Set ACL for directory if it has not been set before or if forcing ACL reset if ($forceAcl || !$hasfacl) { run("setfacl -L $recursive $setFaclUsers $setFaclGroups $dir"); run("setfacl -dL $recursive $setFaclUsers $setFaclGroups $dir"); } } } else { run("$sudo setfacl -L $recursive $setFaclUsers $setFaclGroups $dirs"); run("$sudo setfacl -dL $recursive $setFaclUsers $setFaclGroups $dirs"); } } else { $alias = currentHost()->getAlias(); throw new \RuntimeException("Can't set writable dirs with ACL.\nInstall ACL with next command:\ndep run 'sudo apt-get install acl' -- $alias"); } } elseif ($mode === 'sticky') { // Changes the group of the files, sets sticky bit to the directories // and add the writable bit for all files run("for dir in $dirs;" . 'do ' . 'chgrp -L -R {{http_group}} ${dir}; ' . 'find ${dir} -type d -exec chmod g+rwxs \{\} \;;' . 'find ${dir} -type f -exec chmod g+rw \{\} \;;' . 'done'); } elseif ($mode === 'skip') { // Does nothing, saves time if no changes are required for some environments return; } else { throw new \RuntimeException("Unknown writable_mode `$mode`."); } }); ================================================ FILE: recipe/drupal7.php ================================================ $value) { $keys = []; for ($i = $iterator->getDepth(); $i > 0; $i--) { $keys[] = $iterator->getSubIterator($i - 1)->key(); } $keys[] = $key; $replacements['{{' . implode('.', $keys) . '}}'] = $value; } //Create settings from template $settings = file_get_contents($template); $settings = strtr($settings, $replacements); writeln('settings.php created successfully'); $tmpFilename = tempnam(sys_get_temp_dir(), 'tmp_settings_'); file_put_contents($tmpFilename, $settings); upload($tmpFilename, '{{deploy_path}}/shared/sites/{{drupal_site}}/settings.php'); unlink($tmpFilename); } }); //Upload Drupal 7 files folder task('drupal:upload_files', function () { if (askConfirmation('Are you sure?')) { upload('sites/{{drupal_site}}/files', '{{deploy_path}}/shared/sites/{{drupal_site}}/files'); } }); ================================================ FILE: recipe/drupal8.php ================================================ #.#: The minimum Laravel version required (included). * - 'max' => #.#: The maximum Laravel version required (included). * - 'skipIfNoEnv': Skip and warn the user if `.env` file is inexistant or empty. * - 'failIfNoEnv': Fail the command if `.env` file is inexistant or empty. * - 'showOutput': Show the output of the command if given. * * @param string $command The artisan command (with cli options if any). * @param array $options The options that define the behaviour of the command. * @return callable A function that can be used as a task. */ function artisan($command, $options = []) { return function () use ($command, $options) { // Ensure the artisan command is available on the current version. $versionTooEarly = array_key_exists('min', $options) && laravel_version_compare($options['min'], '<'); $versionTooLate = array_key_exists('max', $options) && laravel_version_compare($options['max'], '>'); if ($versionTooEarly || $versionTooLate) { return; } // Get the dotenv path or use default. $dotenv = get('dotenv', '{{release_or_current_path}}/.env'); // Ensure we warn or fail when a command relies on the ".env" file. if (in_array('failIfNoEnv', $options) && !test("[ -s $dotenv ]")) { throw new \Exception('Your .env file is empty! Cannot proceed.'); } if (in_array('skipIfNoEnv', $options) && !test("[ -s $dotenv ]")) { warning("Your .env file is empty! Skipping..."); return; } // Run the artisan command. $output = run("{{bin/php}} {{bin/artisan}} $command"); // Output the results when appropriate. if (in_array('showOutput', $options)) { writeln("$output"); } }; } function laravel_version_compare($version, $comparator) { return version_compare(get('laravel_version'), $version, $comparator); } /* * Maintenance mode. */ desc('Puts the application into maintenance / demo mode'); task('artisan:down', artisan('down', ['showOutput'])); desc('Brings the application out of maintenance mode'); task('artisan:up', artisan('up', ['showOutput'])); /* * Generate keys. */ desc('Sets the application key'); task('artisan:key:generate', artisan('key:generate')); desc('Creates the encryption keys for API authentication'); task('artisan:passport:keys', artisan('passport:keys')); /* * Database and migrations. */ desc('Seeds the database with records'); task('artisan:db:seed', artisan('db:seed --force', ['skipIfNoEnv', 'showOutput'])); desc('Runs the database migrations'); task('artisan:migrate', artisan('migrate --force', ['skipIfNoEnv'])); desc('Drops all tables and re-run all migrations'); task('artisan:migrate:fresh', artisan('migrate:fresh --force', ['skipIfNoEnv'])); desc('Rollbacks the last database migration'); task('artisan:migrate:rollback', artisan('migrate:rollback --force', ['skipIfNoEnv', 'showOutput'])); desc('Shows the status of each migration'); task('artisan:migrate:status', artisan('migrate:status', ['skipIfNoEnv', 'showOutput'])); /* * Cache and optimizations. */ desc('Flushes the application cache'); task('artisan:cache:clear', artisan('cache:clear')); desc('Creates a cache file for faster configuration loading'); task('artisan:config:cache', artisan('config:cache')); desc('Removes the configuration cache file'); task('artisan:config:clear', artisan('config:clear')); desc('Discovers and cache the application\'s events and listeners'); task('artisan:event:cache', artisan('event:cache', ['min' => '5.8.9'])); desc('Clears all cached events and listeners'); task('artisan:event:clear', artisan('event:clear', ['min' => '5.8.9'])); desc('Lists the application\'s events and listeners'); task('artisan:event:list', artisan('event:list', ['showOutput', 'min' => '5.8.9'])); desc('Cache the framework bootstrap files'); task('artisan:optimize', artisan('optimize')); desc('Removes the cached bootstrap files'); task('artisan:optimize:clear', artisan('optimize:clear')); desc('Reload running services'); task('artisan:reload', artisan('reload', ['min' => '12.41'])); desc('Creates a route cache file for faster route registration'); task('artisan:route:cache', artisan('route:cache')); desc('Removes the route cache file'); task('artisan:route:clear', artisan('route:clear')); desc('Lists all registered routes'); task('artisan:route:list', artisan('route:list', ['showOutput'])); desc('Creates the symbolic links configured for the application'); task('artisan:storage:link', artisan('storage:link', ['min' => 5.3])); desc('Compiles all of the application\'s Blade templates'); task('artisan:view:cache', artisan('view:cache', ['min' => 5.6])); desc('Clears all compiled view files'); task('artisan:view:clear', artisan('view:clear')); /** * Queue and Horizon. */ desc('Lists all of the failed queue jobs'); task('artisan:queue:failed', artisan('queue:failed', ['showOutput'])); desc('Flushes all of the failed queue jobs'); task('artisan:queue:flush', artisan('queue:flush')); desc('Restarts queue worker daemons after their current job'); task('artisan:queue:restart', artisan('queue:restart')); desc('Starts a master supervisor in the foreground'); task('artisan:horizon', artisan('horizon')); desc('Deletes all of the jobs from the specified queue'); task('artisan:horizon:clear', artisan('horizon:clear --force')); desc('Instructs the master supervisor to continue processing jobs'); task('artisan:horizon:continue', artisan('horizon:continue')); desc('Lists all of the deployed machines'); task('artisan:horizon:list', artisan('horizon:list', ['showOutput'])); desc('Pauses the master supervisor'); task('artisan:horizon:pause', artisan('horizon:pause')); desc('Terminates any rogue Horizon processes'); task('artisan:horizon:purge', artisan('horizon:purge')); desc('Gets the current status of Horizon'); task('artisan:horizon:status', artisan('horizon:status', ['showOutput'])); desc('Terminates the master supervisor so it can be restarted'); task('artisan:horizon:terminate', artisan('horizon:terminate')); desc('Publish all of the Horizon resources'); task('artisan:horizon:publish', artisan('horizon:publish')); desc('Lists all of the supervisors'); task('artisan:horizon:supervisors', artisan('horizon:supervisors', ['showOutput'])); desc('Deletes metrics for all jobs and queues'); task('artisan:horizon:clear-metrics', artisan('horizon:clear-metrics')); desc('Stores a snapshot of the queue metrics'); task('artisan:horizon:snapshot', artisan('horizon:snapshot')); /* * Scheduler. */ desc('Interrupt in-progress schedule:run invocations'); task('artisan:schedule:interrupt', artisan('schedule:interrupt')); /* * Telescope. */ desc('Clears all entries from Telescope'); task('artisan:telescope:clear', artisan('telescope:clear')); desc('Prunes stale entries from the Telescope database'); task('artisan:telescope:prune', artisan('telescope:prune')); /* * Octane. */ desc('Starts the octane server'); task('artisan:octane', artisan('octane:start')); desc('Reloads the octane server'); task('artisan:octane:reload', artisan('octane:reload')); desc('Stops the octane server'); task('artisan:octane:stop', artisan('octane:stop')); desc('Check the status of the octane server'); task('artisan:octane:status', artisan('octane:status')); /* * Nova. */ desc('Publish all of the Laravel Nova resources'); task('artisan:nova:publish', artisan('nova:publish')); /* * Reverb. */ desc('Starts the Reverb server'); task('artisan:reverb:start', artisan('reverb:start')); desc('Restarts the Reverb server'); task('artisan:reverb:restart', artisan('reverb:restart')); /* * Pulse. */ desc('Starts the Pulse server'); task('artisan:pulse:check', artisan('pulse:check')); desc('Restarts the Pulse server'); task('artisan:pulse:restart', artisan('pulse:restart')); desc('Purges all Pulse data from storage'); task('artisan:pulse:purge', artisan('pulse:purge')); desc('Process incoming Pulse data from the ingest stream'); task('artisan:pulse:work', artisan('pulse:work')); /** * Main deploy task. */ desc('Deploys your project'); task('deploy', [ 'deploy:prepare', 'deploy:vendors', 'artisan:storage:link', 'artisan:optimize', 'artisan:migrate', 'deploy:publish', 'artisan:reload', ]); ================================================ FILE: recipe/magento.php ================================================ cleanCache();\""); }); /** * Remove files that can be used to compromise Magento */ task('deploy:clear_version', function () { run("rm -f {{release_or_current_path}}/LICENSE.html"); run("rm -f {{release_or_current_path}}/LICENSE.txt"); run("rm -f {{release_or_current_path}}/LICENSE_AFL.txt"); run("rm -f {{release_or_current_path}}/RELEASE_NOTES.txt"); })->hidden(); after('deploy:update_code', 'deploy:clear_version'); /** * Main task */ desc('Deploys your project'); task('deploy', [ 'deploy:prepare', 'deploy:cache:clear', 'deploy:publish', ]); ================================================ FILE: recipe/magento2.php ================================================ null, - Will compile all languages from {{static_content_locales}} for Magento/luma // 'Custom/theme' => 'en_US fr_FR' - Will compile only en_US and fr_FR for Custom/theme // 'Custom/another' => '{{static_content_locales}} it_IT' - Will compile all languages from {{static_content_locales}} + it_IT for Custom/another // ]); - Will compile this theme with every language set('magento_themes', [ ]); // Static content deployment options, e.g. '--no-parent' set('static_deploy_options', ''); // Deploy frontend and adminhtml together as default set('split_static_deployment', false); // Use the default languages for the backend as default set('static_content_locales_backend', '{{static_content_locales}}'); // backend themes to deploy. Only used if split_static_deployment=true // This setting supports the same options/structure as {{magento_themes}} set('magento_themes_backend', ['Magento/backend' => null]); // Configuration // Also set the number of concurrent jobs to run. The default is 1 // Update using: `set('static_content_jobs', '1');` set('static_content_jobs', '1'); set('content_version', function () { return time(); }); // Magento directory relative to repository root. Use "." (default) if it is not located in a subdirectory set('magento_dir', '.'); set('shared_files', [ '{{magento_dir}}/app/etc/env.php', '{{magento_dir}}/var/.maintenance.ip', ]); set('shared_dirs', [ '{{magento_dir}}/var/composer_home', '{{magento_dir}}/var/log', '{{magento_dir}}/var/export', '{{magento_dir}}/var/report', '{{magento_dir}}/var/import', '{{magento_dir}}/var/import_history', '{{magento_dir}}/var/session', '{{magento_dir}}/var/importexport', '{{magento_dir}}/var/backups', '{{magento_dir}}/var/tmp', '{{magento_dir}}/pub/sitemap', '{{magento_dir}}/pub/media', '{{magento_dir}}/pub/static/_cache', ]); set('writable_dirs', [ '{{magento_dir}}/var', '{{magento_dir}}/pub/static', '{{magento_dir}}/pub/media', '{{magento_dir}}/generated', '{{magento_dir}}/var/page_cache', ]); set('clear_paths', [ '{{magento_dir}}/generated/*', '{{magento_dir}}/pub/static/_cache/*', '{{magento_dir}}/var/generation/*', '{{magento_dir}}/var/cache/*', '{{magento_dir}}/var/page_cache/*', '{{magento_dir}}/var/view_preprocessed/*', ]); set('bin/magento', '{{release_or_current_path}}/{{magento_dir}}/bin/magento'); set('magento_version', function () { // detect version $versionOutput = run('{{bin/php}} {{bin/magento}} --version'); preg_match('/(\d+\.?)+(-p\d+)?$/', $versionOutput, $matches); return $matches[0] ?? '2.0'; }); set('config_import_needed', function () { // detect if app:config:import is needed try { run('{{bin/php}} {{bin/magento}} app:config:status'); } catch (RunException $e) { if ($e->getExitCode() == CONFIG_IMPORT_NEEDED_EXIT_CODE) { return true; } throw $e; } return false; }); set('database_upgrade_needed', function () { // detect if db upgrade is needed try { run('{{bin/php}} {{bin/magento}} setup:db:status'); } catch (RunException $e) { if ($e->getExitCode() == DB_UPDATE_NEEDED_EXIT_CODE) { return true; } throw $e; } return false; }); set('full_upgrade_needed', function () { //Some conditions, such as new RabittMQ services require a full upgrade and are not detecet by setup:db:status //TODO: Add checks, once implemented, for detecting necessary full upgrade process. See future RabbitMQ Check: https://github.com/magento/magento2/pull/39698 return false; }); set('upgrade_needed', function () { // Detect necessary upgrade, partial db or full upgrade try { return get('database_upgrade_needed') || get('full_upgrade_needed'); } catch (RunException $e) { throw $e; } }); // Deploy without setting maintenance mode if possible set('enable_zerodowntime', true); // Tasks // To work correctly with artifact deployment, it is necessary to set the MAGE_MODE correctly in `app/etc/config.php` // e.g. // ```php // 'MAGE_MODE' => 'production' // ``` desc('Compiles magento di'); task('magento:compile', function () { run("{{bin/php}} {{bin/magento}} setup:di:compile"); run('cd {{release_or_current_path}}/{{magento_dir}} && {{bin/composer}} dump-autoload -o'); }); // To work correctly with artifact deployment it is necessary to set `system/dev/js` , `system/dev/css` and `system/dev/template` // in `app/etc/config.php`, e.g.: // ```php // 'system' => [ // 'default' => [ // 'dev' => [ // 'js' => [ // 'merge_files' => '1', // 'minify_files' => '1' // ], // 'css' => [ // 'merge_files' => '1', // 'minify_files' => '1' // ], // 'template' => [ // 'minify_html' => '1' // ] // ] // ] // ``` desc('Deploys assets'); task('magento:deploy:assets', function () { $themesToCompile = ''; if (get('split_static_deployment')) { invoke('magento:deploy:assets:adminhtml'); invoke('magento:deploy:assets:frontend'); } else { if (count(get('magento_themes')) > 0) { $themes = array_is_list(get('magento_themes')) ? get('magento_themes') : array_keys(get('magento_themes')); foreach ($themes as $theme) { $themesToCompile .= ' -t ' . $theme; } } run("{{bin/php}} {{bin/magento}} setup:static-content:deploy -f --content-version={{content_version}} {{static_deploy_options}} {{static_content_locales}} $themesToCompile -j {{static_content_jobs}}"); } }); desc('Deploys assets for backend only'); task('magento:deploy:assets:adminhtml', function () { magentoDeployAssetsSplit('backend'); }); desc('Deploys assets for frontend only'); task('magento:deploy:assets:frontend', function () { magentoDeployAssetsSplit('frontend'); }); /** * @phpstan-param 'frontend'|'backend' $area * * @throws ConfigurationException */ function magentoDeployAssetsSplit(string $area) { if (!in_array($area, ['frontend', 'backend'], true)) { throw new ConfigurationException("\$area must be either 'frontend' or 'backend', '$area' given"); } $maxProcesses = get('static_content_jobs'); $isFrontend = $area === 'frontend'; $suffix = $isFrontend ? '' : '_backend'; $themesConfig = get("magento_themes$suffix"); $defaultLanguages = get("static_content_locales$suffix"); $useDefaultLanguages = array_is_list($themesConfig); /** @var list $themes */ $themes = $useDefaultLanguages ? array_values($themesConfig) : array_keys($themesConfig); $staticContentArea = $isFrontend ? 'frontend' : 'adminhtml'; // group themes by their languages to minimize number of static content deploy commands, and allow parallel jobs $themeGroups = []; if ($useDefaultLanguages) { $themeGroups[$defaultLanguages] = $themes; } else { foreach ($themes as $theme) { $locales = array_unique(array_filter(explode(' ', parse($themesConfig[$theme] ?? $defaultLanguages)))); sort($locales); // sort locales to ensure consistent grouping $localesSorted = implode(' ', $locales); $themeGroups[$localesSorted][] = $theme; } } foreach ($themeGroups as $locales => $themes) { $localeCount = substr_count($locales, ' ') + 1; // how many themes can we process in parallel? $maxConcurrentThemes = max(1, floor($maxProcesses / $localeCount)); // WARNING: when static_content_jobs>1, and it's doing more than 1 theme-locale per process, it can get stuck - so we do 1batch at a time do { $chunk = array_splice($themes, 0, $maxConcurrentThemes); $themeArgs = '-t ' . implode(' -t ', $chunk); run("{{bin/php}} {{bin/magento}} setup:static-content:deploy -f --area=$staticContentArea --content-version={{content_version}} {{static_deploy_options}} $locales $themeArgs -j {{static_content_jobs}}"); } while (!empty($themes)); } } desc('Syncs content version'); task('magento:sync:content_version', function () { $timestamp = time(); on(select('all'), function (Host $host) use ($timestamp) { $host->set('content_version', $timestamp); }); })->once(); before('magento:deploy:assets', 'magento:sync:content_version'); desc('Enables maintenance mode'); task('magento:maintenance:enable', function () { // do not use {{bin/magento}} because it would be in "release" but the maintenance mode must be set in "current" run("if [ -d $(echo {{current_path}}) ]; then {{bin/php}} {{current_path}}/{{magento_dir}}/bin/magento maintenance:enable; fi"); }); desc('Disables maintenance mode'); task('magento:maintenance:disable', function () { // do not use {{bin/magento}} because it would be in "release" but the maintenance mode must be set in "current" run("if [ -d $(echo {{current_path}}) ]; then {{bin/php}} {{current_path}}/{{magento_dir}}/bin/magento maintenance:disable; fi"); }); desc('Set maintenance mode if needed'); task('magento:maintenance:enable-if-needed', function () { ! get('enable_zerodowntime') || get('upgrade_needed') || get('config_import_needed') ? invoke('magento:maintenance:enable') : writeln('Config and database up to date => no maintenance mode'); }); desc('Config Import'); task('magento:config:import', function () { if (get('config_import_needed')) { run('{{bin/php}} {{bin/magento}} app:config:import --no-interaction'); } else { writeln('App config is up to date => import skipped'); } }); desc('Upgrades magento database'); task('magento:upgrade:db', function () { if (get('database_upgrade_needed')) { // clear config cache, so there is no error when a new MQ topic is introduced run("{{bin/php}} {{bin/magento}} cache:clean config"); run("{{bin/php}} {{bin/magento}} setup:db-schema:upgrade --no-interaction"); run("{{bin/php}} {{bin/magento}} setup:db-data:upgrade --no-interaction"); } else { writeln('Database schema is up to date => upgrade skipped'); } }); desc('Run upgrades if needed'); task('magento:upgrade', function () { if (get('full_upgrade_needed')) { run("{{bin/php}} {{bin/magento}} setup:upgrade --keep-generated"); } elseif (get('database_upgrade_needed')) { invoke('magento:upgrade:db'); } })->once(); desc('Flushes Magento Cache'); task('magento:cache:flush', function () { run("{{bin/php}} {{bin/magento}} cache:flush"); }); desc('Magento2 deployment operations'); task('deploy:magento', [ 'magento:build', 'magento:maintenance:enable-if-needed', 'magento:config:import', 'magento:upgrade', 'magento:maintenance:disable', ]); desc('Magento2 build operations'); task('magento:build', [ 'magento:compile', 'magento:deploy:assets', ]); desc('Deploys your project'); task('deploy', [ 'deploy:prepare', 'deploy:vendors', 'deploy:clear_paths', 'deploy:magento', 'deploy:publish', ]); after('deploy:symlink', 'magento:cache:flush'); after('deploy:failed', 'magento:maintenance:disable'); // Artifact deployment section // The file the artifact is saved to set('artifact_file', 'artifact.tar.gz'); // The directory the artifact is saved in set('artifact_dir', 'artifacts'); // Points to a file with a list of files to exclude from packaging. // The format is as with the `tar --exclude-from=[file]` option set('artifact_excludes_file', 'artifacts/excludes'); // If set to true, the artifact is built from a clean copy of the project repository instead of the current working directory set('build_from_repo', false); // Set this value if "build_from_repo" is set to true. The target to deploy must also be set with "--branch", "--tag" or "--revision" set('repository', null); // The relative path to the artifact file. If the directory does not exist, it will be created set('artifact_path', function () { if (!testLocally('[ -d {{artifact_dir}} ]')) { runLocally('mkdir -p {{artifact_dir}}'); } return get('artifact_dir') . '/' . get('artifact_file'); }); // The location of the tar command. On MacOS you should have installed gtar, as it supports the required settings set('bin/tar', function () { if (commandExist('gtar')) { return which('gtar'); } else { return which('tar'); } }); // tasks section desc('Packages all relevant files in an artifact.'); task('artifact:package', function () { if (!test('[ -f {{artifact_excludes_file}} ]')) { throw new GracefulShutdownException( "No artifact excludes file provided, provide one at artifacts/excludes or change location", ); } run('{{bin/tar}} --exclude-from={{artifact_excludes_file}} -czf {{artifact_path}} -C {{release_or_current_path}} .'); }); desc('Uploads artifact in release folder for extraction.'); task('artifact:upload', function () { upload(get('artifact_path'), '{{release_path}}'); }); desc('Extracts artifact in release path.'); task('artifact:extract', function () { run('{{bin/tar}} -xzpf {{release_path}}/{{artifact_file}} -C {{release_path}}'); run('rm -rf {{release_path}}/{{artifact_file}}'); }); desc('Clears generated files prior to building.'); task('build:remove-generated', function () { run('rm -rf generated/*'); }); desc('Prepare local artifact build'); task('build:prepare', function () { if (!currentHost()->get('local')) { throw new GracefulShutdownException('Artifact can only be built locally, you provided a non local host'); } $buildDir = get('build_from_repo') ? get('artifact_dir') . '/repo' : '.'; set('deploy_path', $buildDir); set('release_path', $buildDir); set('current_path', $buildDir); if (!get('build_from_repo')) { return; } $repository = (string) get('repository'); if ($repository === '') { throw new GracefulShutdownException('You must specify the "repository" option.'); } run('rm -rf {{release_or_current_path}}'); run('git clone {{repository}} {{release_or_current_path}}'); run('git -C {{release_or_current_path}} checkout --force {{target}}'); }); desc('Builds an artifact.'); task('artifact:build', [ 'build:prepare', 'build:remove-generated', 'deploy:vendors', 'magento:compile', 'magento:deploy:assets', 'artifact:package', ]); // Array of shared files that will be added to the default shared_files without overriding set('additional_shared_files', []); // Array of shared directories that will be added to the default shared_dirs without overriding set('additional_shared_dirs', []); desc('Adds additional files and dirs to the list of shared files and dirs'); task('deploy:additional-shared', function () { add('shared_files', get('additional_shared_files')); add('shared_dirs', get('additional_shared_dirs')); }); /** * Update cache id_prefix on deploy so that you are compiling against a fresh cache * Reference Issue: https://github.com/davidalger/capistrano-magento2/issues/151 * To use this feature, add the following to your deployer scripts: * ```php * after('deploy:shared', 'magento:set_cache_prefix'); * after('deploy:magento', 'magento:cleanup_cache_prefix'); * ``` **/ desc('Update cache id_prefix'); task('magento:set_cache_prefix', function () { //download current env config $tmpConfigFile = tempnam(sys_get_temp_dir(), 'deployer_config'); download('{{deploy_path}}/shared/' . ENV_CONFIG_FILE_PATH, $tmpConfigFile); $envConfigArray = include($tmpConfigFile); //set prefix to `alias_releasename_` $prefixUpdate = get('alias') . '_' . get('release_name') . '_'; //check for preload keys and update if (isset($envConfigArray['cache']['frontend']['default']['backend_options']['preload_keys'])) { $oldPrefix = $envConfigArray['cache']['frontend']['default']['id_prefix']; $preloadKeys = $envConfigArray['cache']['frontend']['default']['backend_options']['preload_keys']; $newPreloadKeys = []; foreach ($preloadKeys as $preloadKey) { $newPreloadKeys[] = preg_replace('/^' . $oldPrefix . '/', $prefixUpdate, $preloadKey); } $envConfigArray['cache']['frontend']['default']['backend_options']['preload_keys'] = $newPreloadKeys; } //update id_prefix to include release name $envConfigArray['cache']['frontend']['default']['id_prefix'] = $prefixUpdate; $envConfigArray['cache']['frontend']['page_cache']['id_prefix'] = $prefixUpdate; //Generate configuration array as string $envConfigStr = ' 404 Not Found

Not Found

The requested URL was not found on this server.

================================================ FILE: recipe/provision/Caddyfile ================================================ {{domain}} { root * {{deploy_path}}/current/{{public_path}} encode zstd gzip file_server php_fastcgi * unix//run/php/php{{php_version}}-fpm.sock { resolve_root_symlink } log { output file {{deploy_path}}/log/access.log { mode 0644 } } handle_errors { @404 { expression {http.error.status_code} == 404 } rewrite @404 /404.html encode zstd gzip file_server { root /var/deployer } } } ================================================ FILE: recipe/provision/databases.php ================================================ limit(1); desc('Provision MySQL'); task('provision:mysql', function () { run('apt-get install -y mysql-server', env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900); run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'0.0.0.0' IDENTIFIED BY '%secret%';\"", secret: get('db_password')); run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'%' IDENTIFIED BY '%secret%';\"", secret: get('db_password')); run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'0.0.0.0' WITH GRANT OPTION;\""); run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'%' WITH GRANT OPTION;\""); run("mysql --user=\"root\" -e \"FLUSH PRIVILEGES;\""); run("mysql --user=\"root\" -e \"CREATE DATABASE IF NOT EXISTS {{db_name}} character set UTF8mb4 collate utf8mb4_bin;\""); }); desc('Provision MariaDB'); task('provision:mariadb', function () { run('apt-get install -y mariadb-server', env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900); run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'0.0.0.0' IDENTIFIED BY '%secret%';\"", secret: get('db_password')); run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'%' IDENTIFIED BY '%secret%';\"", secret: get('db_password')); run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'0.0.0.0' WITH GRANT OPTION;\""); run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'%' WITH GRANT OPTION;\""); run("mysql --user=\"root\" -e \"FLUSH PRIVILEGES;\""); run("mysql --user=\"root\" -e \"CREATE DATABASE IF NOT EXISTS {{db_name}} character set UTF8mb4 collate utf8mb4_bin;\""); }); desc('Provision PostgreSQL'); task('provision:postgresql', function () { run('apt-get install -y postgresql postgresql-contrib', env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900); run("sudo -u postgres psql <<< $'CREATE DATABASE {{db_name}};'"); run("sudo -u postgres psql <<< $'CREATE USER {{db_user}} WITH ENCRYPTED PASSWORD \'%secret%\';'", secret: get('db_password')); run("sudo -u postgres psql <<< $'GRANT ALL PRIVILEGES ON DATABASE {{db_name}} TO {{db_user}};'"); }); ================================================ FILE: recipe/provision/nodejs.php ================================================ > /etc/profile.d/fnm.sh"); }) ->oncePerNode(); ================================================ FILE: recipe/provision/php.php ================================================ 2) { $defaultPhpVersion = "$parts[0].$parts[1]"; } return ask(' What PHP version to install? ', $defaultPhpVersion, ['5.6', '7.4', '8.0', '8.1', '8.2', '8.3']); }); desc('Installs PHP packages'); task('provision:php', function () { set('remote_user', get('provision_user')); $version = get('php_version'); info("Installing PHP $version"); $packages = [ "php$version-bcmath", "php$version-cli", "php$version-curl", "php$version-dev", "php$version-fpm", "php$version-gd", "php$version-imap", "php$version-intl", "php$version-mbstring", "php$version-mysql", "php$version-pgsql", "php$version-readline", "php$version-soap", "php$version-sqlite3", "php$version-xml", "php$version-zip", ]; run('apt-get install -y ' . implode(' ', $packages), env: ['DEBIAN_FRONTEND' => 'noninteractive']); // Configure PHP-CLI run("sed -i 's/error_reporting = .*/error_reporting = E_ALL/' /etc/php/$version/cli/php.ini"); run("sed -i 's/display_errors = .*/display_errors = On/' /etc/php/$version/cli/php.ini"); run("sed -i 's/memory_limit = .*/memory_limit = 512M/' /etc/php/$version/cli/php.ini"); run("sed -i 's/upload_max_filesize = .*/upload_max_filesize = 128M/' /etc/php/$version/cli/php.ini"); run("sed -i 's/;date.timezone.*/date.timezone = UTC/' /etc/php/$version/cli/php.ini"); // Configure PHP-FPM run("sed -i 's/error_reporting = .*/error_reporting = E_ALL/' /etc/php/$version/fpm/php.ini"); run("sed -i 's/display_errors = .*/display_errors = On/' /etc/php/$version/fpm/php.ini"); run("sed -i 's/memory_limit = .*/memory_limit = 512M/' /etc/php/$version/fpm/php.ini"); run("sed -i 's/upload_max_filesize = .*/upload_max_filesize = 128M/' /etc/php/$version/fpm/php.ini"); run("sed -i 's/;date.timezone.*/date.timezone = UTC/' /etc/php/$version/fpm/php.ini"); run("sed -i 's/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/' /etc/php/$version/fpm/php.ini"); // Configure FPM Pool run("sed -i 's/;request_terminate_timeout = .*/request_terminate_timeout = 60/' /etc/php/$version/fpm/pool.d/www.conf"); run("sed -i 's/;catch_workers_output = .*/catch_workers_output = yes/' /etc/php/$version/fpm/pool.d/www.conf"); run("sed -i 's/;php_flag\[display_errors\] = .*/php_flag[display_errors] = yes/' /etc/php/$version/fpm/pool.d/www.conf"); run("sed -i 's/;php_admin_value\[error_log\] = .*/php_admin_value[error_log] = \/var\/log\/fpm-php.www.log/' /etc/php/$version/fpm/pool.d/www.conf"); run("sed -i 's/;php_admin_flag\[log_errors\] = .*/php_admin_flag[log_errors] = on/' /etc/php/$version/fpm/pool.d/www.conf"); // Configure PHP sessions directory run('chmod 733 /var/lib/php/sessions'); run('chmod +t /var/lib/php/sessions'); }) ->verbose() ->limit(1); desc('Shows php-fpm logs'); task('logs:php-fpm', function () { $fpmLogs = run("ls -1 /var/log | grep fpm"); if (empty($fpmLogs)) { throw new \RuntimeException('No PHP-FPM logs found.'); } run("sudo tail -f /var/log/$fpmLogs"); })->verbose(); desc('Installs Composer'); task('provision:composer', function () { run('curl -sS https://getcomposer.org/installer | php'); run('mv composer.phar /usr/local/bin/composer'); })->oncePerNode(); ================================================ FILE: recipe/provision/user.php ================================================ /dev/null 2>&1')) { // TODO: Check what created deployer user configured correctly. // TODO: Update sudo_password of deployer user. // TODO: Copy ssh_copy_id to deployer ssh dir. info('deployer user already exist'); } else { run('useradd deployer'); run('mkdir -p /home/deployer/.ssh'); run('mkdir -p /home/deployer/.deployer'); run('adduser deployer sudo'); run('chsh -s /bin/bash deployer'); run('cp /root/.profile /home/deployer/.profile'); run('cp /root/.bashrc /home/deployer/.bashrc'); run('touch /home/deployer/.sudo_as_admin_successful'); // Make color prompt. run("sed -i 's/#force_color_prompt=yes/force_color_prompt=yes/' /home/deployer/.bashrc"); $password = run("mkpasswd -m sha-512 '%secret%'", secret: get('sudo_password')); run("usermod --password '%secret%' deployer", secret: $password); // Copy root public key to deployer user so user can login without password. run('cp /root/.ssh/authorized_keys /home/deployer/.ssh/authorized_keys'); // Create ssh key if not already exists. run('ssh-keygen -f /home/deployer/.ssh/id_ed25519 -t ed25519 -N ""'); try { run('chown -R deployer:deployer /home/deployer'); run('chmod -R 755 /home/deployer'); run('chmod 700 /home/deployer/.ssh'); run('chmod 600 /home/deployer/.ssh/id_ed25519'); run('chmod 600 /home/deployer/.ssh/authorized_keys'); } catch (\Throwable $e) { warning($e->getMessage()); } run('usermod -a -G www-data deployer'); run('usermod -a -G caddy deployer'); } })->oncePerNode(); desc('Copy public key to remote server'); task('provision:ssh_copy_id', function () { $defaultKeys = [ '~/.ssh/id_rsa.pub', '~/.ssh/id_ed25519.pub', '~/.ssh/id_ecdsa.pub', '~/.ssh/id_dsa.pub', ]; $publicKeyContent = false; foreach ($defaultKeys as $key) { $file = parse_home_dir($key); if (file_exists($file)) { $publicKeyContent = file_get_contents($file); break; } } if (!$publicKeyContent) { $publicKeyContent = ask(' Public key: ', ''); } if (empty($publicKeyContent)) { info('Skipping public key copy as no public key was found or provided.'); return; } run('echo "$PUBLIC_KEY" >> /home/deployer/.ssh/authorized_keys', env: ['PUBLIC_KEY' => $publicKeyContent]); }); ================================================ FILE: recipe/provision/website.php ================================================ /var/deployer/404.html"); })->oncePerNode(); desc('Provision website'); task('provision:website', function () { $restoreBecome = become('deployer'); run("[ -d {{deploy_path}} ] || mkdir -p {{deploy_path}}"); run("chown -R deployer:deployer {{deploy_path}}"); set('deploy_path', run("realpath {{deploy_path}}")); cd('{{deploy_path}}'); run("[ -d log ] || mkdir log"); run("chgrp caddy log"); $caddyfile = parse(file_get_contents(__DIR__ . '/Caddyfile')); if (test('[ -f Caddyfile ]')) { run("echo $'$caddyfile' > Caddyfile.new"); $diff = run('diff -U5 --color=always Caddyfile Caddyfile.new', nothrow: true); if (empty($diff)) { run('rm Caddyfile.new'); } else { info('Found Caddyfile changes'); writeln("\n" . $diff); $answer = askChoice(' Which Caddyfile to save? ', ['old', 'new'], 0); if ($answer === 'old') { run('rm Caddyfile.new'); } else { run('mv Caddyfile.new Caddyfile'); } } } else { run("echo $'$caddyfile' > Caddyfile"); } $restoreBecome(); if (!test("grep -q 'import {{deploy_path}}/Caddyfile' /etc/caddy/Caddyfile")) { run("echo 'import {{deploy_path}}/Caddyfile' >> /etc/caddy/Caddyfile"); } run('service caddy reload'); info("Website {{domain}} configured!"); })->limit(1); desc('Shows access logs'); task('logs:access', function () { run('tail -f {{deploy_path}}/log/access.log'); })->verbose(); desc('Shows caddy syslog'); task('logs:caddy', function () { run('sudo journalctl -u caddy -f'); })->verbose(); ================================================ FILE: recipe/provision.php ================================================ $name, 'VERSION_ID' => $version] = parse_ini_string($release); if ($name !== 'Ubuntu') { warning('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); warning('!! !!'); warning('!! Only Ubuntu is supported! !!'); warning('!! !!'); warning('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); if (!askConfirmation(' Do you want to continue? (Not recommended)', false)) { throw new \RuntimeException('Provision aborted due to incompatible OS.'); } } // Also only version 20 and older are supported. if (version_compare($version, '20', '<')) { warning("Ubuntu $version is not supported. Use Ubuntu 20 or newer."); if (!askConfirmation(' Do you want to continue? (Not recommended)', false)) { throw new \RuntimeException('Provision aborted due to incompatible OS.'); } } })->oncePerNode(); desc('Collects required params'); task('provision:configure', function () { set('remote_user', get('provision_user')); $params = [ 'sudo_password', 'domain', 'public_path', 'php_version', 'db_type', ]; $dbparams = [ 'db_user', 'db_name', 'db_password', ]; $showCode = false; foreach ($params as $name) { if (!Context::get()->getConfig()->hasOwn($name)) { $showCode = true; } get($name); } if (get('db_type') !== 'none') { foreach ($dbparams as $name) { if (!Context::get()->getConfig()->hasOwn($name)) { $showCode = true; } get($name); } } if ($showCode) { $code = "\n\n====== Configuration Start ======"; $code .= "\nhost('{{alias}}')"; $codeParams = $params; if (get('db_type') !== 'none') { $codeParams = array_merge($codeParams, $dbparams); } foreach ($codeParams as $name) { $code .= "\n ->set('$name', '" . get($name) . "')"; } $code .= ";\n"; $code .= "====== Configuration End ======\n\n"; writeln($code); } }); desc('Adds repositories and update'); task('provision:update', function () { set('remote_user', get('provision_user')); // Update before installing anything run('apt-get update', env: ['DEBIAN_FRONTEND' => 'noninteractive']); // Pre-requisites run('apt install -y curl gpg software-properties-common', env: ['DEBIAN_FRONTEND' => 'noninteractive']); // PHP run('apt-add-repository ppa:ondrej/php -y', env: [ 'DEBIAN_FRONTEND' => 'noninteractive', 'LC_ALL' => 'C.UTF-8', ]); // Caddy run("curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor --yes -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg"); run("curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' > /etc/apt/sources.list.d/caddy-stable.list"); // Update run('apt-get update', env: ['DEBIAN_FRONTEND' => 'noninteractive']); }) ->oncePerNode() ->verbose(); desc('Upgrades all packages'); task('provision:upgrade', function () { set('remote_user', get('provision_user')); run('apt-get upgrade -y', env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900); }) ->oncePerNode() ->verbose(); desc('Installs packages'); task('provision:install', function () { set('remote_user', get('provision_user')); $packages = [ 'acl', 'apt-transport-https', 'build-essential', 'caddy', 'curl', 'debian-archive-keyring', 'debian-keyring', 'fail2ban', 'gcc', 'git', 'libmcrypt4', 'libpcre3-dev', 'libsqlite3-dev', 'make', 'ncdu', 'nodejs', 'pkg-config', 'python-is-python3', 'redis', 'sendmail', 'sqlite3', 'ufw', 'unzip', 'uuid-runtime', 'whois', ]; run('apt-get install -y ' . implode(' ', $packages), env: ['DEBIAN_FRONTEND' => 'noninteractive'], timeout: 900); }) ->verbose() ->oncePerNode(); desc('Configures the ssh'); task('provision:ssh', function () { set('remote_user', get('provision_user')); run("sed -i 's/PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config"); run('ssh-keygen -A'); run('service ssh restart'); if (test('[ ! -d /root/.ssh ]')) { run('mkdir -p /root/.ssh'); run('touch /root/.ssh/authorized_keys'); } })->oncePerNode(); desc('Setups a firewall'); task('provision:firewall', function () { set('remote_user', get('provision_user')); run('ufw allow 22'); run('ufw allow 80'); run('ufw allow 443'); run('ufw --force enable'); })->oncePerNode(); desc('Verifies what provision was successful'); task('provision:verify', function () { fetch('{{domain}}', 'get', [], null, $info, true); if ($info['http_code'] === 404) { info("provisioned successfully!"); } }); ================================================ FILE: recipe/shopware.php ================================================ set('remote_user', 'SSH-USER') * ->set('deploy_path', '/var/www/shopware') // This is the path where deployer will create its directory structure * ->set('http_user', 'www-data') // Not needed, if the `user` is the same, the web server is running with * ->set('http_group', 'www-data') * ->set('writable_mode', 'chmod') * ->set('writable_recursive', true) * ->set('become', 'www-data'); // You might want to change user to execute remote tasks because of access rights of created cache files * ``` * * :::note * Please remember that the installation must be modified so that it can be * [build without database](https://developer.shopware.com/docs/guides/hosting/installation-updates/deployments/build-w-o-db#compiling-the-storefront-without-database). * ::: */ namespace Deployer; require_once __DIR__ . '/common.php'; add('recipes', ['shopware']); set('bin/console', '{{bin/php}} {{release_or_current_path}}/bin/console'); set('default_timeout', 3600); // Increase when tasks take longer than that. // These files are shared among all releases. set('shared_files', [ '.env.local', 'install.lock', 'public/.htaccess', 'public/.user.ini', ]); // These directories are shared among all releases. set('shared_dirs', [ 'config/jwt', 'files', 'var/log', 'public/media', 'public/plugins', 'public/thumbnail', 'public/sitemap', ]); // These directories are made writable (the definition of "writable" requires attention). // Please note that the files in `config/jwt/*` receive special attention in the `sw:writable:jwt` task. set('writable_dirs', [ 'config/jwt', 'custom/plugins', 'files', 'public/bundles', 'public/css', 'public/fonts', 'public/js', 'public/media', 'public/plugins', 'public/sitemap', 'public/theme', 'public/thumbnail', 'var', ]); // This sets the Shopware version to the version of the Shopware console command. set('shopware_version', function () { $versionOutput = run('cd {{release_path}} && {{bin/console}} -V'); preg_match('/(\d+\.\d+\.\d+\.\d+)/', $versionOutput, $matches); return $matches[0] ?? '6.6.0'; }); // This task remotely executes the `cache:clear` console command on the target server. task('sw:cache:clear', static function () { run('cd {{release_path}} && {{bin/console}} cache:clear --no-warmup'); }); // This task remotely executes the cache warmup console commands on the target server, so that the first user, who // visits the website, doesn't have to wait for the cache to be built up. task('sw:cache:warmup', static function () { run('cd {{release_path}} && {{bin/console}} cache:warmup'); // Shopware 6.6+ dropped support for the http:cache:warmup command, so only execute it if the version is less than 6.6 if (version_compare(get('shopware_version'), '6.6.0') < 0) { run('cd {{release_path}} && {{bin/console}} http:cache:warm:up'); } }); // This task remotely executes the `database:migrate` console command on the target server. task('sw:database:migrate', static function () { run('cd {{release_path}} && {{bin/console}} database:migrate --all'); }); task('sw:plugin:refresh', function () { run('cd {{release_path}} && {{bin/console}} plugin:refresh'); }); task('sw:scheduled-task:register', function () { run('cd {{release_path}} && {{bin/console}} scheduled-task:register'); }); task('sw:theme:refresh', function () { run('cd {{release_path}} && {{bin/console}} theme:refresh'); }); // This task is not used by default, but can be used, e.g. in combination with `SHOPWARE_SKIP_THEME_COMPILE=1`, // to build the theme remotely instead of locally. task('sw:theme:compile', function () { run('cd {{release_path}} && {{bin/console}} theme:compile'); }); function getPlugins(): array { $output = run('cd {{release_path}} && {{bin/console}} plugin:list --json'); $plugins = json_decode($output); return $plugins; } task('sw:plugin:update:all', static function () { $plugins = getPlugins(); foreach ($plugins as $plugin) { if ($plugin->installedAt && $plugin->upgradeVersion) { writeln("Running plugin update for " . $plugin->name . "\n"); run("cd {{release_path}} && {{bin/console}} plugin:update " . $plugin->name); } } }); task('sw:writable:jwt', static function () { if (!test('[ -d {{deploy_path}}/config/jwt/ ]')) { return; } run('cd {{release_path}} && find config/jwt/ -type f -exec chmod -R 660 {} +'); }); /** * Grouped SW deploy tasks. */ task('sw:deploy', [ 'sw:database:migrate', 'sw:plugin:refresh', 'sw:theme:refresh', 'sw:scheduled-task:register', 'sw:cache:clear', 'sw:plugin:update:all', 'sw:cache:clear', ]); desc('Deploys your project'); task('deploy', [ 'deploy:prepare', 'sw:writable:jwt', 'sw:deploy', 'deploy:clear_paths', 'sw:cache:warmup', 'deploy:publish', ]); task('deploy:update_code')->setCallback(static function () { upload('.', '{{release_path}}', [ 'options' => [ '--exclude=.git', '--exclude=deploy.php', '--exclude=node_modules', ], ]); }); task('sw-build-without-db:get-remote-config', static function () { if (!test('[ -d {{current_path}} ]')) { return; } within('{{current_path}}', function () { run('{{bin/php}} ./bin/console bundle:dump'); download('{{current_path}}/var/plugins.json', './var/'); run('{{bin/php}} ./bin/console theme:dump -n'); download('{{current_path}}/files/theme-config', './files/'); }); }); task('sw-build-without-db:build', static function () { runLocally('CI=1 SHOPWARE_SKIP_BUNDLE_DUMP=1 ./bin/build-js.sh'); }); task('sw-build-without-db', [ 'sw-build-without-db:get-remote-config', 'sw-build-without-db:build', ]); before('deploy:update_code', 'sw-build-without-db'); ================================================ FILE: recipe/silverstripe.php ================================================ $output"); } }; } /** * Run a RoadRunner command. * * Supported options: * - 'showOutput': Show the output of the command if given. */ function rr(string $command, array $options = []): \Closure { return function () use ($command, $options): void { $output = run("cd {{roadrunner_path}} && ./rr $command"); if (\in_array('showOutput', $options, true)) { writeln("$output"); } }; } /** * Spiral Framework console commands */ desc('Configure project'); task('spiral:configure', command('configure', ['showOutput'])); desc('Update (init) cycle schema from database and annotated classes'); task('spiral:cycle', command('cycle', ['showOutput'])); desc('Perform all outstanding migrations'); task('spiral:migrate', command('migrate', ['showOutput'])); desc('Update project state'); task('spiral:update', command('update', ['showOutput'])); desc('Clean application runtime cache'); task('spiral:cache:clean', command('cache:clean', ['showOutput'])); desc('Reset translation cache'); task('spiral:i18n:reset', command('i18n:reset', ['showOutput'])); desc('Generate new encryption key, if it doesn\'t exist'); task('spiral:encrypt-key', command('encrypt:key -m .env -p', ['showOutput'])); desc('Warm-up view cache'); task('spiral:views:compile', command('views:compile', ['showOutput'])); desc('Clear view cache'); task('spiral:views:reset', command('views:reset', ['showOutput'])); /** * Cycle ORM and migrations console commands */ desc('Generate ORM schema migrations'); task('cycle:migrate', command('cycle:migrate', ['showOutput'])); desc('Render available CycleORM schemas'); task('cycle:render', command('cycle:render', ['showOutput'])); desc('Sync Cycle ORM schema with database without intermediate migration (risk operation)'); task('cycle:sync', command('cycle:sync', ['showOutput'])); desc('Init migrations component (create migrations table)'); task('migrate:init', command('migrate:init', ['showOutput'])); desc('Replay (down, up) one or multiple migrations'); task('migrate:replay', command('migrate:replay', ['showOutput'])); desc('Rollback one (default) or multiple migrations'); task('migrate:rollback', command('migrate:rollback', ['showOutput'])); desc('Get list of all available migrations and their statuses'); task('migrate:status', command('migrate:status', ['showOutput'])); /** * RoadRunner console commands */ desc('Start RoadRunner server'); task('roadrunner:serve', function (): void { exec(parse('cd {{roadrunner_path}} && ./rr serve -p > /dev/null 2>&1 &')); }); desc('Stop RoadRunner server'); task('roadrunner:stop', rr('stop', ['showOutput'])); desc('Reset workers of all services'); task('roadrunner:reset', rr('reset', ['showOutput'])); /** * Download and restart RoadRunner */ desc('Download RoadRunner'); task('deploy:download-rr', function (): void { $output = run("cd {{release_or_current_path}} && {{bin/php}} ./vendor/bin/rr get-binary -l {{roadrunner_path}}"); writeln("$output"); }); desc('Restart RoadRunner'); task('deploy:restart-rr', function (): void { try { invoke('roadrunner:reset'); writeln("Roadrunner successfully restarted."); } catch (\Throwable $e) { invoke('roadrunner:serve'); writeln("Roadrunner successfully started."); } }); /** * Main task */ desc('Deploys your project'); task('deploy', [ 'deploy:prepare', 'deploy:vendors', 'spiral:encrypt-key', 'spiral:configure', 'deploy:download-rr', 'deploy:publish', 'deploy:restart-rr', ]); ================================================ FILE: recipe/statamic.php ================================================ array_merge(get('shared_dirs'), get('shared_files'), $exclude), 'exclude-file' => false, 'include' => ['vendor'], 'include-file' => false, 'filter' => ['dir-merge,-n /.gitignore'], 'filter-file' => false, 'filter-perdir' => false, 'flags' => 'avz', 'options' => ['delete', 'keep-dirlinks', 'links'], 'timeout' => 600, ]); /** * List of schema update types. * `safe` includes all necessary operations, to add or change fields or tables. */ set('typo3_updateschema_types', 'safe'); /** * TYPO3 Commands * All run via {{bin/php}} {{release_path}}/{{bin/typo3}} */ desc('TYPO3 - Clear all caches'); task('typo3:cache:flush', function () { run('{{bin/php}} {{release_path}}/{{bin/typo3}} cache:flush'); }); desc('TYPO3 - Cache warmup for system caches'); task('typo3:cache:warmup', function () { run('{{bin/php}} {{release_path}}/{{bin/typo3}} cache:warmup --group system'); }); desc('TYPO3 - Update the language files of all activated extensions'); task('typo3:language:update', function () { run('{{bin/php}} {{release_path}}/{{bin/typo3}} language:update'); }); desc('TYPO3 - Set up all extensions'); task('typo3:extension:setup', function () { run('{{bin/php}} {{release_path}}/{{bin/typo3}} extension:setup'); }); desc('TYPO3 - Fix folder structure'); task('typo3:install:fixfolderstructure', function () { run('{{bin/php}} {{release_path}}/{{bin/typo3}} install:fixfolderstructure'); }); /** * Main deploy task for TYPO3. * * 1. Lock deploy to avoid concurrent runs * 2. Create release directory * 3. Update code (Git or rsync) * 4. Symlink shared dirs/files * 5. Fix TYPO3 folder structure * 6. Ensure writable dirs * 7. Run extension setup & perform schema updates * 8. Update language files * 9. Install composer vendors * 10. Flush caches * 11. Warm up TYPO3 caches * 12. Unlock and clean up */ desc('Deploys a TYPO3 project'); task('deploy', [ 'deploy:info', 'deploy:setup', 'deploy:lock', 'deploy:release', 'typo3:update_code', 'deploy:shared', 'deploy:writable', 'deploy:vendors', 'typo3:install:fixfolderstructure', 'typo3:extension:setup', 'typo3:language:update', 'typo3:cache:flush', 'typo3:cache:warmup', 'deploy:publish', ]); after('deploy:failed', 'deploy:unlock'); ================================================ FILE: recipe/wordpress.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Collection; use Countable; use IteratorAggregate; class Collection implements Countable, IteratorAggregate { protected array $values = []; public function all(): array { return $this->values; } public function get(string $name): mixed { if ($this->has($name)) { return $this->values[$name]; } throw $this->notFound($name); } public function has(string $name): bool { return array_key_exists($name, $this->values); } public function set(string $name, mixed $object) { $this->values[$name] = $object; } public function remove(string $name): void { if ($this->has($name)) { unset($this->values[$name]); } throw $this->notFound($name); } public function count(): int { return count($this->values); } public function select(callable $callback): array { $values = []; foreach ($this->values as $key => $value) { if ($callback($value, $key)) { $values[$key] = $value; } } return $values; } /** * @return \ArrayIterator|\Traversable */ #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->values); } protected function notFound(string $name): \InvalidArgumentException { return new \InvalidArgumentException("Element \"$name\" not found in collection."); } } ================================================ FILE: src/Command/BlackjackCommand.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Command; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface as Input; use Symfony\Component\Console\Output\OutputInterface as Output; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Style\SymfonyStyle; use function Deployer\Support\array_flatten; class BlackjackCommand extends Command { use CommandCommon; /** * @var Input */ private $input; /** * @var Output */ private $output; public function __construct() { parent::__construct('blackjack'); $this->setDescription('Play blackjack'); } protected function execute(Input $input, Output $output): int { $this->input = $input; $this->output = $output; $this->telemetry(); $io = new SymfonyStyle($this->input, $this->output); if (getenv('COLORTERM') === 'truecolor') { $this->print("\x1b[38;2;255;95;109m╭\x1b[39m\x1b[38;2;255;95;107m─\x1b[39m\x1b[38;2;255;96;106m─\x1b[39m\x1b[38;2;255;96;104m─\x1b[39m\x1b[38;2;255;96;103m─\x1b[39m\x1b[38;2;255;97;101m─\x1b[39m\x1b[38;2;255;97;100m─\x1b[39m\x1b[38;2;255;97;99m─\x1b[39m\x1b[38;2;255;98;97m─\x1b[39m\x1b[38;2;255;100;98m─\x1b[39m\x1b[38;2;255;102;98m─\x1b[39m\x1b[38;2;255;104;98m─\x1b[39m\x1b[38;2;255;106;99m─\x1b[39m\x1b[38;2;255;108;99m─\x1b[39m\x1b[38;2;255;110;99m─\x1b[39m\x1b[38;2;255;112;100m─\x1b[39m\x1b[38;2;255;114;100m─\x1b[39m\x1b[38;2;255;116;100m─\x1b[39m\x1b[38;2;255;118;100m─\x1b[39m\x1b[38;2;255;120;101m─\x1b[39m\x1b[38;2;255;122;101m─\x1b[39m\x1b[38;2;255;124;101m─\x1b[39m\x1b[38;2;255;126;102m╮\x1b[39m"); $this->print("\x1b[38;2;255;128;102m│\x1b[39m \x1b[38;2;255;130;102m│\x1b[39m"); $this->print("\x1b[38;2;255;132;103m│\x1b[39m \x1b[38;2;255;134;103mW\x1b[39m\x1b[38;2;255;136;103me\x1b[39m\x1b[38;2;255;138;104ml\x1b[39m\x1b[38;2;255;140;104mc\x1b[39m\x1b[38;2;255;142;104mo\x1b[39m\x1b[38;2;255;144;104mm\x1b[39m\x1b[38;2;255;146;105me\x1b[39m\x1b[38;2;255;148;105m!\x1b[39m \x1b[38;2;255;150;105m│\x1b[39m"); $this->print("\x1b[38;2;255;152;106m│\x1b[39m \x1b[38;2;255;153;106m│\x1b[39m"); $this->print("\x1b[38;2;255;155;106m╰\x1b[39m\x1b[38;2;255;157;107m─\x1b[39m\x1b[38;2;255;159;107m─\x1b[39m\x1b[38;2;255;161;107m─\x1b[39m\x1b[38;2;255;163;108m─\x1b[39m\x1b[38;2;255;165;108m─\x1b[39m\x1b[38;2;255;166;108m─\x1b[39m\x1b[38;2;255;168;108m─\x1b[39m\x1b[38;2;255;170;109m─\x1b[39m\x1b[38;2;255;172;109m─\x1b[39m\x1b[38;2;255;174;109m─\x1b[39m\x1b[38;2;255;176;110m─\x1b[39m\x1b[38;2;255;177;110m─\x1b[39m\x1b[38;2;255;179;110m─\x1b[39m\x1b[38;2;255;181;111m─\x1b[39m\x1b[38;2;255;183;111m─\x1b[39m\x1b[38;2;255;185;111m─\x1b[39m\x1b[38;2;255;186;111m─\x1b[39m\x1b[38;2;255;188;112m─\x1b[39m\x1b[38;2;255;190;112m─\x1b[39m\x1b[38;2;255;192;112m─\x1b[39m\x1b[38;2;255;193;113m─\x1b[39m\x1b[38;2;255;195;113m╯\x1b[0m"); } else { $this->print("╭─────────────────────╮"); $this->print("│ │"); $this->print("│ Welcome! │"); $this->print("│ │"); $this->print("╰─────────────────────╯"); } $money = 100; if (md5(strval(getenv('MONEY'))) === '5a7c2f336d0cc43b68951e75cdffe333') { $money += 25; $this->print('You got an extra $25.'); } elseif (md5(strval(getenv('MONEY'))) === '530029252abcbda4a2a2069036ccc7fc') { $money += 100; $this->print('You got an extra $100.'); } elseif (md5(strval(getenv('MONEY'))) === '1aa827a06ecbfa5d6fa7c62ad245f3a3') { $money = 100000; } $hasWatch = true; $orderWhiskey = false; $whiskeyLevel = 0; $deck = $this->newDeck(); $graveyard = []; $dealersHand = []; $playersHand = []; shuffle($deck); $deal = function () use (&$deck, &$graveyard) { if (count($deck) == 0) { shuffle($graveyard); $deck = $graveyard; $graveyard = []; } return array_pop($deck); }; start: $this->print("You have $$money."); if ($money > 0) { $bet = (int) $io->ask('Your bet', '5'); if ($bet <= 0) { goto start; } if ($bet > $money) { goto start; } } elseif ($hasWatch) { // @phpstan-ignore-line $answer = $io->askQuestion(new ChoiceQuestion('?', ['leave', '- Here, take my watch! [$25]'], 0)); if ($answer == 'leave') { goto leave; } else { $hasWatch = false; $money = 25; $bet = 25; } } else { goto leave; } $graveyard = array_merge($graveyard, $dealersHand); $dealersHand = []; $dealersHand[] = $deal(); $this->print("Dealers hand:"); $this->printHand($dealersHand); $graveyard = array_merge($graveyard, $playersHand); $playersHand = []; $playersHand[] = $deal(); $playersHand[] = $deal(); $this->print("Your hand:"); $this->printHand($playersHand, 2); while (true) { $question = new ChoiceQuestion('Your turn', ['hit', 'stand'], 0); $answer = $io->askQuestion($question); if ($answer === 'hit') { $playersHand[] = $deal(); usleep(200000); } if ($answer === 'stand') { break; } $this->printHand($playersHand); $handValue = self::handValue($playersHand); if ($handValue > 21) { $this->print("You got $handValue."); $this->print("Bust!"); $this->print("-$$bet"); $money -= $bet; goto nextRound; } } $this->printHand($dealersHand); $this->print("Dealer: " . self::handValue($dealersHand)); sleep(1); while (self::handValue($dealersHand) <= 17) { $dealersHand[] = $deal(); $this->printHand($dealersHand); $this->print("Dealer: " . self::handValue($dealersHand)); sleep(1); } $d = self::handValue($dealersHand); $p = self::handValue($playersHand); $this->print("You got $p and dealer $d."); if ($d > 21 || $p > $d) { $this->print("You won!"); $this->print("+$$bet"); $money += $bet; } elseif ($p < $d) { $this->print("You lose!"); $this->print("-$$bet"); $money -= $bet; } else { $this->print("Push!"); } nextRound: $choices = ['continue', 'leave']; if ($orderWhiskey) { $orderWhiskey = false; $whiskeyLevel = 4; $this->print(); $this->print('The waitress brought whiskey and says:'); $this->print(' - Your whiskey, sir.'); if ($money >= 5) { array_push($choices, 'tip the waitress [$5]'); } } elseif ($money >= 5) { array_push($choices, 'order whiskey [$5]'); } if ($whiskeyLevel > 0) { $this->printWhiskey($whiskeyLevel); $whiskeyLevel--; } $answer = $io->askQuestion(new ChoiceQuestion('?', $choices, 0)); if ($answer == 'leave') { goto leave; } elseif ($money >= 5 && $answer == 'order whiskey [$5]') { $orderWhiskey = true; $this->print('You say:'); $this->print(' - Whiskey, please.'); $money -= 5; } elseif ($money >= 5 && $answer == 'tip the waitress [$5]') { $this->print('The waitress says:'); $this->print(' - Thank you, sir!'); $money -= 5; } $this->print(); $this->print("=====> Next round <====="); goto start; leave: if ($money >= 5) { $answer = $io->ask('Leave a $5 tip to the dealer?', 'yes'); if ($answer === 'yes') { $this->print("You can leave a tip here:"); $this->print(); $this->print("- https://github.com/sponsors/antonmedv"); $this->print("- https://paypal.me/antonmedv"); $this->print(); } } $this->print('Thanks for playing, Come again!'); return 0; } private function newDeck(): array { $deck = []; foreach (['♠', '♣', '♥', '♦'] as $suit) { for ($i = 2; $i <= 10; $i++) { $deck[] = [strval($i), $suit]; } $deck[] = ['J', $suit]; $deck[] = ['Q', $suit]; $deck[] = ['K', $suit]; $deck[] = ['A', $suit]; } return $deck; } public static function handValue(array $hand): int { $aces = 0; $value = 0; foreach ($hand as [$rank]) { switch ($rank) { case '2': $value += 2; break; case '3': $value += 3; break; case '4': $value += 4; break; case '5': $value += 5; break; case '6': $value += 6; break; case '7': $value += 7; break; case '8': $value += 8; break; case '9': $value += 9; break; case '10': case 'J': case 'Q': case 'K': $value += 10; break; case 'A': $aces++; break; } } $variants = [$value]; while ($aces-- > 0) { $variants = array_flatten(array_map(function ($v) { return [$v + 1, $v + 11]; }, $variants)); } $sum = $variants[0]; for ($i = 1; $i < count($variants); $i++) { if ($variants[$i] <= 21) { $sum = $variants[$i]; } else { break; } } return $sum; } private function print(string $text = "") { $this->output->writeln(" $text"); } private function printHand(array $hand, int $offset = 1) { $cards = []; for ($i = 0; $i < count($hand) - $offset; $i++) { [$rank] = $hand[$i]; $cards[] = [ "┌───", "│" . str_pad($rank, 3), "│ ", "│ ", "│ ", "│ ", "└───", ]; } for (; $i < count($hand); $i++) { [$rank, $suit] = $hand[$i]; $cards[] = [ "┌───────┐", "│" . str_pad($rank, 7) . "│", "│ │", "│ " . $suit . " │", "│ │", "│" . str_pad($rank, 7, " ", STR_PAD_LEFT) . "│", "└───────┘", ]; } for ($i = 0; $i < 7; $i++) { $this->output->write(" "); foreach ($cards as $lines) { $this->output->write($lines[$i]); } $this->output->write("\n"); } } private function printWhiskey(int $whiskeyLevel) { if ($whiskeyLevel == 4) { echo << * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Command; use Deployer\Deployer; use Deployer\Support\Reporter; trait CommandCommon { /** * Collecting anonymous stat helps Deployer team improve developer experience. * If you are not comfortable with this, you will always be able to disable this * by setting DO_NOT_TRACK environment variable to `1`. * @codeCoverageIgnore */ protected function telemetry(array $data = []): void { if (getenv('DO_NOT_TRACK') === 'true') { return; } try { Reporter::report(array_merge([ 'command_name' => $this->getName(), 'deployer_version' => DEPLOYER_VERSION, 'deployer_phar' => Deployer::isPharArchive(), 'php_version' => phpversion(), 'os' => defined('PHP_OS_FAMILY') ? PHP_OS_FAMILY : (stristr(PHP_OS, 'DAR') ? 'OSX' : (stristr(PHP_OS, 'WIN') ? 'WIN' : (stristr(PHP_OS, 'LINUX') ? 'LINUX' : PHP_OS))), ], $data)); } catch (\Throwable $e) { return; } } } ================================================ FILE: src/Command/ConfigCommand.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Command; use Deployer\Deployer; use Deployer\Exception\WillAskUser; use Deployer\Task\Context; use Symfony\Component\Console\Input\InputInterface as Input; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface as Output; use Symfony\Component\Yaml\Yaml; class ConfigCommand extends SelectCommand { public function __construct(Deployer $deployer) { parent::__construct('config', $deployer); $this->setDescription('Get all configuration options for hosts'); } protected function configure() { parent::configure(); $this->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (json, yaml)', 'yaml'); $this->getDefinition()->getArgument('selector')->setDefault(['all']); } protected function execute(Input $input, Output $output): int { $this->deployer->input = $input; $this->deployer->output = new NullOutput(); $hosts = $this->selectHosts($input, $output); $data = []; $keys = $this->deployer->config->keys(); define('DEPLOYER_NO_ASK', true); foreach ($hosts as $host) { Context::push(new Context($host)); $values = []; foreach ($keys as $key) { try { $values[$key] = $host->get($key); } catch (WillAskUser $exception) { $values[$key] = ['ask' => $exception->getMessage()]; } catch (\Throwable $exception) { $values[$key] = ['error' => $exception->getMessage()]; } } foreach ($host->config()->persist() as $k => $v) { $values[$k] = $v; } ksort($values); $data[$host->getAlias()] = $values; Context::pop(); } $format = $input->getOption('format'); switch ($format) { case 'json': $output->writeln(json_encode($data, JSON_PRETTY_PRINT)); break; case 'yaml': $output->write(Yaml::dump($data)); break; default: throw new \Exception("Unknown format: $format."); } return 0; } } ================================================ FILE: src/Command/CustomOption.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Command; use Deployer\Host\Host; trait CustomOption { /** * @param Host[] $hosts * @param string[] $options */ protected function applyOverrides(array $hosts, array $options) { $override = []; foreach ($options as $option) { [$name, $value] = explode('=', $option); $value = $this->castValueToPhpType(trim($value)); $override[trim($name)] = $value; } foreach ($hosts as $host) { foreach ($override as $key => $value) { $host->set($key, $value); } } } /** * @param mixed $value * @return bool|mixed */ protected function castValueToPhpType($value) { switch ($value) { case 'true': return true; case 'false': return false; default: return $value; } } } ================================================ FILE: src/Command/InitCommand.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Command; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Process\Exception\RuntimeException; use Symfony\Component\Process\PhpProcess; use Symfony\Component\Process\Process; class InitCommand extends Command { use CommandCommon; protected function configure() { $this ->setName('init') ->setDescription('Initialize deployer in your project') ->addOption('path', 'p', InputOption::VALUE_REQUIRED, 'Recipe path'); } protected function execute(InputInterface $input, OutputInterface $output): int { if (getenv('COLORTERM') === 'truecolor') { $output->write( <<write( <<getOption('path'); $language = $io->choice('Select recipe language', ['php', 'yaml'], 'php'); if (empty($recipePath)) { $recipePath = "deploy.$language"; } // Avoid accidentally override of existing file. if (file_exists($recipePath)) { $io->warning("$recipePath already exists"); if (!$io->confirm("Do you want to override the existing file?", false)) { $io->block('👍🏻'); exit(1); } } // Template $template = $io->choice('Select project template', $this->recipes(), 'common'); // Repo $default = ''; try { $process = Process::fromShellCommandline('git remote get-url origin'); $default = $process->mustRun()->getOutput(); $default = trim($default); } catch (RuntimeException $e) { } $repository = $io->ask('Repository', $default); // Guess host if (preg_match('/github.com:(?[A-Za-z0-9_.\-]+)\//', $repository, $m)) { $org = $m['org']; $tempHostFile = tempnam(sys_get_temp_dir(), 'temp-host-file'); $php = new PhpProcess( <<blog, PHP_URL_HOST); file_put_contents('$tempHostFile', \$host); EOF, ); $php->start(); } // Project $default = ''; try { $process = Process::fromShellCommandline('basename "$PWD"'); $default = $process->mustRun()->getOutput(); $default = trim($default); } catch (RuntimeException $e) { } $project = $io->ask('Project name', $default); // Hosts $host = null; if (isset($tempHostFile)) { $host = file_get_contents($tempHostFile); } $hostsString = $io->ask('Hosts (comma separated)', $host); if ($hostsString !== null) { $hosts = explode(',', $hostsString); } else { $hosts = []; } file_put_contents($recipePath, $this->$language($template, $project, $repository, $hosts)); $this->telemetry(); $output->writeln(sprintf( 'Successfully created %s', $recipePath, )); return 0; } private function php(string $template, string $project, string $repository, array $hosts): string { $h = ""; foreach ($hosts as $host) { $h .= "host('{$host}')\n" . " ->set('remote_user', 'deployer')\n" . " ->set('deploy_path', '~/{$project}');\n"; } return <<getAdditionalConfigs($template); return <<isDot()) { continue; } if ($fileinfo->isDir()) { continue; } $recipe = pathinfo($fileinfo->getFilename(), PATHINFO_FILENAME); if ($recipe === 'README') { continue; } $recipes[] = $recipe; } sort($recipes); return $recipes; } } ================================================ FILE: src/Command/MainCommand.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Command; use Deployer\Deployer; use Deployer\Exception\Exception; use Deployer\Exception\GracefulShutdownException; use Deployer\Executor\Planner; use Deployer\Utility\Httpie; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface as Input; use Symfony\Component\Console\Input\InputOption as Option; use Symfony\Component\Console\Output\OutputInterface as Output; class MainCommand extends SelectCommand { use CustomOption; use CommandCommon; public function __construct(string $name, ?string $description, Deployer $deployer) { parent::__construct($name, $deployer); if ($description) { $this->setDescription($description); } } protected function configure() { parent::configure(); // Add global options defined with `option()` func. $this->getDefinition()->addOptions($this->deployer->inputDefinition->getOptions()); $this->addOption( 'option', 'o', Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, 'Set configuration option', ); $this->addOption( 'limit', 'l', Option::VALUE_REQUIRED, 'How many tasks to run in parallel?', ); $this->addOption( 'no-hooks', null, Option::VALUE_NONE, 'Run tasks without after/before hooks', ); $this->addOption( 'plan', null, Option::VALUE_NONE, 'Show execution plan', ); $this->addOption( 'start-from', null, Option::VALUE_REQUIRED, 'Start execution from this task', ); $this->addOption( 'log', null, Option::VALUE_REQUIRED, 'Write log to a file', ); $this->addOption( 'profile', null, Option::VALUE_REQUIRED, 'Write profile to a file', ); } protected function execute(Input $input, Output $output): int { $this->deployer->input = $input; $this->deployer->output = $output; $this->deployer['log'] = $input->getOption('log'); $this->telemetry([ 'project_hash' => empty($this->deployer->config['repository']) ? null : sha1($this->deployer->config['repository']), 'hosts_count' => $this->deployer->hosts->count(), 'recipes' => $this->deployer->config->get('recipes', []), ]); $hosts = $this->selectHosts($input, $output); $this->applyOverrides($hosts, $input->getOption('option')); // Save selected_hosts for selectedHosts() func. $hostsAliases = []; foreach ($hosts as $host) { $hostsAliases[] = $host->getAlias(); } // Save selected_hosts per each host, and not globally. Otherwise it will // not be accessible for workers. foreach ($hosts as $host) { $host->set('selected_hosts', $hostsAliases); } $plan = $input->getOption('plan') ? new Planner($output, $hosts) : null; $this->deployer->scriptManager->setHooksEnabled(!$input->getOption('no-hooks')); $startFrom = $input->getOption('start-from'); if ($startFrom && !$this->deployer->tasks->has($startFrom)) { throw new Exception("Task $startFrom does not exist."); } $skippedTasks = []; $tasks = $this->deployer->scriptManager->getTasks($this->getName(), $startFrom, $skippedTasks); if (empty($tasks)) { throw new Exception('No task will be executed, because the selected hosts do not meet the conditions of the tasks'); } if (!$plan) { $this->checkUpdates(); if (!empty($skippedTasks)) { foreach ($skippedTasks as $taskName) { $output->writeln("skip $taskName"); } } } $exitCode = $this->deployer->master->run($tasks, $hosts, $plan); if ($plan) { $plan->render(); return 0; } if ($exitCode === 0) { $this->showBanner(); return 0; } if ($exitCode === GracefulShutdownException::EXIT_CODE) { return 1; } // Check if we have tasks to execute on failure. if ($this->deployer['fail']->has($this->getName())) { $taskName = $this->deployer['fail']->get($this->getName()); $tasks = $this->deployer->scriptManager->getTasks($taskName); $this->deployer->master->run($tasks, $hosts); } return $exitCode; } private function checkUpdates() { try { fwrite(STDERR, Httpie::get('https://deployer.org/check-updates/' . DEPLOYER_VERSION)->send()); } catch (\Throwable $e) { // Meh } } private function showBanner() { if (getenv('DO_NOT_SHOW_BANNER') === 'true') { return; } try { $withColors = ''; if (function_exists('posix_isatty') && posix_isatty(STDOUT)) { $withColors = '_with_colors'; } fwrite(STDERR, Httpie::get("https://deployer.medv.io/banners/" . $this->getName() . $withColors)->send()); } catch (\Throwable $e) { // Meh } } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { parent::complete($input, $suggestions); if ($input->mustSuggestOptionValuesFor('start-from')) { $taskNames = []; foreach ($this->deployer->scriptManager->getTasks($this->getName()) as $task) { $taskNames[] = $task->getName(); } $suggestions->suggestValues($taskNames); } } } ================================================ FILE: src/Command/RunCommand.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Command; use Deployer\Deployer; use Deployer\Task\Context; use Deployer\Task\Task; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface as Input; use Symfony\Component\Console\Input\InputOption as Option; use Symfony\Component\Console\Output\OutputInterface as Output; use function Deployer\cd; use function Deployer\get; use function Deployer\has; use function Deployer\run; use function Deployer\test; class RunCommand extends SelectCommand { use CustomOption; public function __construct(Deployer $deployer) { parent::__construct('run', $deployer); $this->setDescription('Run any arbitrary command on hosts'); } protected function configure() { $this->addArgument( 'command-to-run', InputArgument::REQUIRED, 'Command to run on a remote host', ); parent::configure(); $this->addOption( 'option', 'o', Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, 'Set configuration option', ); $this->addOption( 'timeout', 't', Option::VALUE_REQUIRED, 'Command timeout in seconds', ); } protected function execute(Input $input, Output $output): int { $this->deployer->input = $input; $this->deployer->output = $output; $command = $input->getArgument('command-to-run') ?? ''; $hosts = $this->selectHosts($input, $output); $this->applyOverrides($hosts, $input->getOption('option')); $task = new Task($command, function () use ($input, $command) { if (has('current_path')) { $path = get('current_path'); if (test("[ -d $path ]")) { cd($path); } } run( $command, timeout: intval($input->getOption('timeout')), forceOutput: true, ); }); foreach ($hosts as $host) { try { $task->run(new Context($host)); } catch (\Throwable $exception) { $this->deployer->messenger->renderException($exception, $host); } } return 0; } } ================================================ FILE: src/Command/SelectCommand.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Command; use Deployer\Deployer; use Deployer\Exception\ConfigurationException; use Deployer\Exception\Exception; use Deployer\Host\Host; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface as Input; use Symfony\Component\Console\Output\OutputInterface as Output; use Symfony\Component\Console\Question\ChoiceQuestion; abstract class SelectCommand extends Command { /** * @var Deployer */ protected $deployer; public function __construct(string $name, Deployer $deployer) { $this->deployer = $deployer; parent::__construct($name); } protected function configure() { $this->addArgument('selector', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Host selector'); } /** * @return Host[] */ protected function selectHosts(Input $input, Output $output): array { $output->getFormatter()->setStyle('success', new OutputFormatterStyle('green')); if (!$output->isDecorated() && !defined('NO_ANSI')) { define('NO_ANSI', 'true'); } $selector = $input->getArgument('selector'); $selector = empty($selector) ? Deployer::get()->config->get('default_selector', '') : $selector; $selectExpression = is_array($selector) ? implode(',', $selector) : $selector; if (empty($selectExpression)) { if (count($this->deployer->hosts) === 0) { throw new ConfigurationException("No host configured.\nSpecify at least one host: `localhost();`."); } elseif (count($this->deployer->hosts) === 1) { $hosts = $this->deployer->hosts->all(); } elseif ($input->isInteractive()) { $hostsAliases = []; foreach ($this->deployer->hosts as $host) { $hostsAliases[] = $host->getAlias(); } /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); $question = new ChoiceQuestion( 'Select hosts: (comma separated)', $hostsAliases, ); $question->setMultiselect(true); $question->setErrorMessage('There is no "%s" host.'); $answer = $helper->ask($input, $output, $question); $answer = array_unique($answer); $hosts = $this->deployer->hosts->select(function (Host $host) use ($answer) { return in_array($host->getAlias(), $answer, true); }); } } else { $hosts = $this->deployer->selector->select($selectExpression); } if (empty($hosts)) { $message = 'No host selected.'; if (!empty($selectExpression)) { $message .= " Please, check your selector:\n\n $selectExpression"; } throw new Exception($message); } return $hosts; } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { parent::complete($input, $suggestions); if ($input->mustSuggestArgumentValuesFor('selector')) { $selectors = ['all']; $configs = []; foreach ($this->deployer->hosts as $host) { $configs[$host->getAlias()] = $host->config()->persist(); } foreach ($configs as $alias => $c) { $selectors[] = $alias; foreach ($c['labels'] ?? [] as $label => $value) { $selectors[] = "$label=$value"; } } $selectors = array_unique($selectors); $suggestions->suggestValues($selectors); } if ($input->mustSuggestOptionValuesFor('option')) { $values = []; foreach ($this->deployer->config->keys() as $key) { $values[] = $key . '='; } $suggestions->suggestValues($values); } } } ================================================ FILE: src/Command/SshCommand.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Command; use Deployer\Deployer; use Deployer\Host\Localhost; use Deployer\Task\Context; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; /** * @codeCoverageIgnore */ class SshCommand extends Command { use CommandCommon; /** * @var Deployer */ private $deployer; public function __construct(Deployer $deployer) { parent::__construct('ssh'); $this->setDescription('Connect to host through ssh'); $this->deployer = $deployer; } protected function configure() { $this->addArgument( 'hostname', InputArgument::OPTIONAL, 'Hostname', ); } protected function execute(InputInterface $input, OutputInterface $output): int { $this->telemetry(); $hostname = $input->getArgument('hostname'); if (!empty($hostname)) { $host = $this->deployer->hosts->get($hostname); } else { $hostsAliases = []; foreach ($this->deployer->hosts as $host) { if ($host instanceof Localhost) { continue; } $hostsAliases[] = $host->getAlias(); } if (count($hostsAliases) === 0) { $output->writeln('No remote hosts.'); return 2; // Because there are no hosts. } if (count($hostsAliases) === 1) { $host = $this->deployer->hosts->get($hostsAliases[0]); } else { /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); $question = new ChoiceQuestion( 'Select host:', $hostsAliases, ); $question->setErrorMessage('There is no "%s" host.'); $hostname = $helper->ask($input, $output, $question); $host = $this->deployer->hosts->get($hostname); } } $shell_path = 'exec $SHELL -l'; if ($host->has('shell_path')) { $shell_path = 'exec ' . $host->get('shell_path') . ' -l'; } Context::push(new Context($host)); $host->setSshMultiplexing(false); $options = $host->connectionOptionsString(); $deployPath = $host->get('deploy_path', '~'); if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { passthru("ssh -t $options {$host->connectionString()} \"cd $deployPath/current 2>/dev/null || cd $deployPath; $shell_path\""); } else { passthru("ssh -t $options {$host->connectionString()} 'cd $deployPath/current 2>/dev/null || cd $deployPath; $shell_path'"); } return 0; } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { parent::complete($input, $suggestions); if ($input->mustSuggestArgumentValuesFor('hostname')) { $suggestions->suggestValues(array_keys($this->deployer->hosts->all())); } } } ================================================ FILE: src/Command/TreeCommand.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Command; use Deployer\Deployer; use Deployer\Task\GroupTask; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface as Input; use Symfony\Component\Console\Output\OutputInterface as Output; class TreeCommand extends Command { /** * @var Output */ protected $output; /** * @var Deployer */ private $deployer; /** * @var array */ private $tree; /** * @var int */ private $depth = 0; /** * @var array */ private $openGroupDepths = []; public function __construct(Deployer $deployer) { parent::__construct('tree'); $this->setDescription('Display the task-tree for a given task'); $this->deployer = $deployer; $this->tree = []; } protected function configure() { $this->addArgument( 'task', InputArgument::REQUIRED, 'Task to display the tree for', ); } protected function execute(Input $input, Output $output): int { $this->output = $output; $rootTaskName = $input->getArgument('task'); $this->buildTree($rootTaskName); $this->outputTree($rootTaskName); return 0; } private function buildTree(string $taskName) { $this->createTreeFromTaskName($taskName, '', true); } private function createTreeFromTaskName(string $taskName, string $postfix = '', bool $isLast = false) { $task = $this->deployer->tasks->get($taskName); if (!$task->isEnabled()) { if (empty($postfix)) { $postfix = ' // disabled'; } else { $postfix .= '; disabled'; } } if ($task->getBefore()) { $beforePostfix = sprintf(" // before %s", $task->getName()); foreach ($task->getBefore() as $beforeTask) { $this->createTreeFromTaskName($beforeTask, $beforePostfix); } } if ($task instanceof GroupTask) { $isLast = $isLast && empty($task->getAfter()); $this->addTaskToTree($task->getName() . $postfix, $isLast); if (!$isLast) { $this->openGroupDepths[] = $this->depth; } $this->depth++; $taskGroup = $task->getGroup(); foreach ($taskGroup as $subtask) { $isLastSubtask = $subtask === end($taskGroup); $this->createTreeFromTaskName($subtask, '', $isLastSubtask); } if (!$isLast) { array_pop($this->openGroupDepths); } $this->depth--; } else { $this->addTaskToTree($task->getName() . $postfix, $isLast); } if ($task->getAfter()) { $afterPostfix = sprintf(" // after %s", $task->getName()); foreach ($task->getAfter() as $afterTask) { $this->createTreeFromTaskName($afterTask, $afterPostfix); } } } private function addTaskToTree(string $taskName, bool $isLast = false) { $this->tree[] = [ 'taskName' => $taskName, 'depth' => $this->depth, 'isLast' => $isLast, 'openDepths' => $this->openGroupDepths, ]; } private function outputTree(string $taskName) { $this->output->writeln("The task-tree for $taskName:"); /** * @var int number of spaces for each depth increase */ $REPEAT_COUNT = 4; foreach ($this->tree as $treeItem) { $depth = $treeItem['depth']; $startSymbol = $treeItem['isLast'] || $treeItem === end($this->tree) ? '└' : '├'; $prefix = ''; for ($i = 0; $i < $depth; $i++) { if (in_array($i, $treeItem['openDepths'])) { $prefix .= '│' . str_repeat(' ', $REPEAT_COUNT - 1); } else { $prefix .= str_repeat(' ', $REPEAT_COUNT); } } $prefix .= $startSymbol . '──'; $this->output->writeln(sprintf('%s %s', $prefix, $treeItem['taskName'])); } } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { parent::complete($input, $suggestions); if ($input->mustSuggestArgumentValuesFor('task')) { $suggestions->suggestValues(array_keys($this->deployer->tasks->all())); } } } ================================================ FILE: src/Command/WorkerCommand.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Command; use Deployer\Deployer; use Deployer\Executor\Worker; use Deployer\Host\Localhost; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption as Option; use Symfony\Component\Console\Output\OutputInterface; use function Deployer\localhost; class WorkerCommand extends MainCommand { public function __construct(Deployer $deployer) { parent::__construct('worker', null, $deployer); $this->setHidden(true); } protected function configure() { parent::configure(); $this->addOption('task', null, Option::VALUE_REQUIRED); $this->addOption('host', null, Option::VALUE_REQUIRED); $this->addOption('port', null, Option::VALUE_REQUIRED); $this->addOption('decorated', null, Option::VALUE_NONE); } protected function execute(InputInterface $input, OutputInterface $output): int { $this->deployer->input = $input; $this->deployer->output = $output; $this->deployer['log'] = $input->getOption('log'); $output->setDecorated($input->getOption('decorated')); if (!$output->isDecorated() && !defined('NO_ANSI')) { define('NO_ANSI', 'true'); } define('MASTER_ENDPOINT', 'http://localhost:' . $input->getOption('port')); $task = $this->deployer->tasks->get($input->getOption('task')); $host = $this->deployer->hosts->get($input->getOption('host')); $host->config()->load(); $worker = new Worker($this->deployer); $exitCode = $worker->execute($task, $host); $host->config()->save(); return $exitCode; } } ================================================ FILE: src/Component/PharUpdate/Console/Command.php ================================================ */ class Command extends Base { /** * Disable the ability to upgrade? * * @var boolean */ private $disableUpgrade = false; /** * The manifest file URI. * * @var string */ private $manifestUri; /** * The running file (the Phar that will be updated). * * @var string */ private $runningFile; /** * @param string $name The command name. * @param boolean $disable Disable upgrading? */ public function __construct(string $name, bool $disable = false) { $this->disableUpgrade = $disable; parent::__construct($name); } /** * Sets the manifest URI. * * @param string $uri The URI. */ public function setManifestUri(string $uri) { $this->manifestUri = $uri; } /** * Sets the running file (the Phar that will be updated). * * @param string $file The file name or path. */ public function setRunningFile(string $file): void { $this->runningFile = $file; } /** * {@inheritdoc} */ protected function configure() { $this->setDescription('Updates the application.'); $this->addOption( 'pre', 'p', InputOption::VALUE_NONE, 'Allow pre-release updates.', ); $this->addOption( 'redo', 'r', InputOption::VALUE_NONE, 'Redownload update if already using current version.', ); if (false === $this->disableUpgrade) { $this->addOption( 'upgrade', 'u', InputOption::VALUE_NONE, 'Upgrade to next major release, if available.', ); } } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output): int { if (null === $this->manifestUri) { throw new LogicException( 'No manifest URI has been configured.', ); } $output->writeln('Looking for updates...'); /** @var Helper */ $pharUpdate = $this->getHelper('phar-update'); /** @var Manager $manager */ $manager = $pharUpdate->getManager($this->manifestUri); $manager->setRunningFile($this->runningFile); if ($manager->update( $this->getApplication()->getVersion(), $this->disableUpgrade ?: (false === $input->getOption('upgrade')), $input->getOption('pre'), )) { $output->writeln('Update successful!'); } else { $output->writeln('Already up-to-date.'); } return self::SUCCESS; } } ================================================ FILE: src/Component/PharUpdate/Console/Helper.php ================================================ */ class Helper extends Base { /** * Returns the update manager. * * @param string $uri The manifest file URI. * * @return Manager The update manager. */ public function getManager(string $uri): Manager { return new Manager(Manifest::loadFile($uri)); } public function getName(): string { return 'phar-update'; } } ================================================ FILE: src/Component/PharUpdate/Exception/Exception.php ================================================ */ class Exception extends \Exception implements ExceptionInterface { /** * Creates a new exception using a format and values. * * @param mixed $value,... The value(s). */ public static function create(string $format, $value = null): self { if (0 < func_num_args()) { $format = vsprintf($format, array_slice(func_get_args(), 1)); } return new static($format); } /** * Creates an exception for the last error message. */ public static function lastError(): self { $error = error_get_last(); return new static($error['message']); } } ================================================ FILE: src/Component/PharUpdate/Exception/ExceptionInterface.php ================================================ */ interface ExceptionInterface {} ================================================ FILE: src/Component/PharUpdate/Exception/FileException.php ================================================ */ class FileException extends Exception {} ================================================ FILE: src/Component/PharUpdate/Exception/InvalidArgumentException.php ================================================ */ class InvalidArgumentException extends Exception {} ================================================ FILE: src/Component/PharUpdate/Exception/LogicException.php ================================================ */ class LogicException extends Exception {} ================================================ FILE: src/Component/PharUpdate/Manager.php ================================================ */ class Manager { /** * The update manifest. * * @var Manifest */ private $manifest; /** * The running file (the Phar that will be updated). * * @var string */ private $runningFile; /** * Sets the update manifest. * * @param Manifest $manifest The manifest. */ public function __construct(Manifest $manifest) { $this->manifest = $manifest; } /** * Returns the manifest. * * @return Manifest The manifest. */ public function getManifest(): Manifest { return $this->manifest; } /** * Returns the running file (the Phar that will be updated). * * @return string The file. */ public function getRunningFile(): string { if (null === $this->runningFile) { $this->runningFile = realpath($_SERVER['argv'][0]); } return $this->runningFile; } /** * Sets the running file (the Phar that will be updated). * * @param string $file The file name or path. * * @throws Exception\Exception * @throws InvalidArgumentException If the file path is invalid. */ public function setRunningFile(string $file): void { if (false === is_file($file)) { throw InvalidArgumentException::create( 'The file "%s" is not a file or it does not exist.', $file, ); } $this->runningFile = $file; } /** * Updates the running Phar if any is available. * * @param string|Version $version The current version. * @param boolean $major Lock to current major version? * @param boolean $pre Allow pre-releases? * * @return boolean TRUE if an update was performed, FALSE if none available. */ public function update($version, bool $major = false, bool $pre = false): bool { if (false === ($version instanceof Version)) { $version = Parser::toVersion($version); } if (null !== ($update = $this->manifest->findRecent( $version, $major, $pre, ))) { $update->getFile(); $update->copyTo($this->getRunningFile()); return true; } return false; } } ================================================ FILE: src/Component/PharUpdate/Manifest.php ================================================ */ class Manifest { /** * The list of updates in the manifest. * * @var Update[] */ private $updates; /** * Sets the list of updates from the manifest. * * @param Update[] $updates The updates. */ public function __construct(array $updates = []) { $this->updates = $updates; } /** * Finds the most recent update and returns it. * * @param Version $version The current version. * @param boolean $major Lock to major version? * @param boolean $pre Allow pre-releases? */ public function findRecent(Version $version, bool $major = false, bool $pre = false): ?Update { /** @var Update|null */ $current = null; $major = $major ? $version->getMajor() : null; foreach ($this->updates as $update) { if ($major && ($major !== $update->getVersion()->getMajor())) { continue; } if ((false === $pre) && !$update->getVersion()->isStable()) { continue; } $test = $current ? $current->getVersion() : $version; if (false === $update->isNewer($test)) { continue; } $current = $update; } return $current; } /** * Returns the list of updates in the manifest. * * @return Update[] The updates. */ public function getUpdates(): array { return $this->updates; } /** * Loads the manifest from a JSON encoded string. * * @param string $json The JSON encoded string. */ public static function load(string $json): self { return self::create(json_decode($json)); } /** * Loads the manifest from a JSON encoded file. * * @param string $file The JSON encoded file. */ public static function loadFile(string $file): self { return self::create(json_decode(file_get_contents($file))); } /** * Validates the data, processes it, and returns a new instance of Manifest. * * @param array $decoded The decoded JSON data. * * @return static The new instance. */ private static function create(array $decoded): self { $updates = []; foreach ($decoded as $update) { $updates[] = new Update( $update->name, $update->sha1, $update->url, Parser::toVersion($update->version), $update->publicKey ?? null, ); } usort( $updates, function (Update $a, Update $b) { return Comparator::isGreaterThan( $a->getVersion(), $b->getVersion(), ) ? 1 : 0; }, ); return new static($updates); } } ================================================ FILE: src/Component/PharUpdate/Update.php ================================================ */ class Update { /** * The temporary file path. * * @var string|null */ private $file; /** * The name of the update file. * * @var string */ private $name; /** * The URL where the public key can be downloaded from. * * @var string */ private $publicKey; /** * The SHA1 file checksum. * * @var string */ private $sha1; /** * The URL where the update can be downloaded from. * * @var string */ private $url; /** * The version of the update. * * @var Version */ private $version; /** * Sets the update information. * * @param string $name The name of the update file. * @param string $sha1 The SHA1 file checksum. * @param string $url The URL where the update can be downloaded from. * @param Version $version The version of the update. * @param string $key The URL where the public key can be downloaded * from. */ public function __construct( string $name, string $sha1, string $url, Version $version, string $key = null, ) { $this->name = $name; $this->publicKey = $key; $this->sha1 = $sha1; $this->url = $url; $this->version = $version; } /** * Copies the update file to the destination. * * @param string $file The target file. * * @throws Exception\Exception * @throws FileException If the file could not be replaced. */ public function copyTo(string $file): void { if (null === $this->file) { throw LogicException::create( 'The update file has not been downloaded.', ); } $mode = 0o755; if (file_exists($file)) { $mode = fileperms($file) & 511; } if (false === @copy($this->file, $file)) { throw FileException::lastError(); } if (false === @chmod($file, $mode)) { throw FileException::lastError(); } $key = $file . '.pubkey'; if (file_exists($this->file . '.pubkey')) { if (false === @copy($this->file . '.pubkey', $key)) { throw FileException::lastError(); } } elseif (file_exists($key)) { if (false === @unlink($key)) { throw FileException::lastError(); } } } /** * Cleans up by deleting the temporary update file. * * @throws FileException If the file could not be deleted. */ public function deleteFile(): void { if ($this->file) { if (file_exists($this->file)) { if (false === @unlink($this->file)) { throw FileException::lastError(); } } if (file_exists($this->file . '.pubkey')) { if (false === @unlink($this->file . '.pubkey')) { throw FileException::lastError(); } } $dir = dirname($this->file); if (file_exists($dir)) { if (false === @rmdir($dir)) { throw FileException::lastError(); } } $this->file = null; } } /** * Downloads the update file to a temporary location. * * @return string The temporary file path. * * @throws Exception\Exception * @throws FileException If the SHA1 checksum differs. * @throws UnexpectedValueException If the Phar is corrupt. */ public function getFile(): ?string { if (null === $this->file) { unlink($this->file = tempnam(sys_get_temp_dir(), 'upd')); mkdir($this->file); $this->file .= DIRECTORY_SEPARATOR . $this->name; $in = new SplFileObject($this->url, 'rb', false); $out = new SplFileObject($this->file, 'wb', false); while (false === $in->eof()) { $out->fwrite($in->fgets()); } unset($in, $out); if ($this->publicKey) { $in = new SplFileObject($this->publicKey, 'r', false); $out = new SplFileObject($this->file . '.pubkey', 'w', false); while (false === $in->eof()) { $out->fwrite($in->fgets()); } unset($in, $out); } if ($this->sha1 !== ($sha1 = sha1_file($this->file))) { $this->deleteFile(); throw FileException::create( 'Mismatch of the SHA1 checksum (%s) of the downloaded file (%s).', $this->sha1, $sha1, ); } // double check try { new Phar($this->file); } catch (UnexpectedValueException $exception) { $this->deleteFile(); throw $exception; } } return $this->file; } /** * Returns name of the update file. */ public function getName(): string { return $this->name; } /** * Returns the URL where the public key can be downloaded from. */ public function getPublicKey(): string { return $this->publicKey; } /** * Returns the SHA1 file checksum. */ public function getSha1(): string { return $this->sha1; } /** * Returns the URL where the update can be downloaded from. */ public function getUrl(): string { return $this->url; } /** * Returns the version of the update. */ public function getVersion(): Version { return $this->version; } /** * Checks if this update is newer than the version given. * * @param Version $version The current version. * * @return boolean TRUE if the update is newer, FALSE if not. */ public function isNewer(Version $version): bool { return Comparator::isGreaterThan($this->version, $version); } } ================================================ FILE: src/Component/PharUpdate/Version/Builder.php ================================================ */ class Builder extends Version { /** * Removes the build metadata identifiers. */ public function clearBuild(): void { $this->build = []; } /** * Removes the pre-release version identifiers. */ public function clearPreRelease(): void { $this->preRelease = []; } /** * Creates a new Version builder. * * @return Builder The Version builder. */ public static function create(): Builder { return new Builder(); } /** * Returns a readonly Version instance. * * @return Version The readonly Version instance. */ public function getVersion(): Version { return new Version( $this->major, $this->minor, $this->patch, $this->preRelease, $this->build, ); } /** * Imports the version components. * * @param array $components The components. * * @return Builder The Version builder. */ public function importComponents(array $components): self { if (isset($components[Parser::BUILD])) { $this->build = $components[Parser::BUILD]; } else { $this->build = []; } if (isset($components[Parser::MAJOR])) { $this->major = $components[Parser::MAJOR]; } else { $this->major = 0; } if (isset($components[Parser::MINOR])) { $this->minor = $components[Parser::MINOR]; } else { $this->minor = 0; } if (isset($components[Parser::PATCH])) { $this->patch = $components[Parser::PATCH]; } else { $this->patch = 0; } if (isset($components[Parser::PRE_RELEASE])) { $this->preRelease = $components[Parser::PRE_RELEASE]; } else { $this->preRelease = []; } return $this; } /** * Imports the version string representation. * * @param string $version The string representation. * * @return Builder The Version builder. */ public function importString(string $version): self { return $this->importComponents(Parser::toComponents($version)); } /** * Imports an existing Version instance. * * @param Version $version A Version instance. * * @return Builder The Version builder. */ public function importVersion(Version $version): self { return $this ->setMajor($version->getMajor()) ->setMinor($version->getMinor()) ->setPatch($version->getPatch()) ->setPreRelease($version->getPreRelease()) ->setBuild($version->getBuild()); } /** * Increments the major version number and resets the minor and patch * version numbers to zero. * * @param int $amount Increment by what amount? * * @return Builder The Version builder. */ public function incrementMajor(int $amount = 1): self { $this->major += $amount; $this->minor = 0; $this->patch = 0; return $this; } /** * Increments the minor version number and resets the patch version number * to zero. * * @param int $amount Increment by what amount? * * @return Builder The Version builder. */ public function incrementMinor(int $amount = 1): self { $this->minor += $amount; $this->patch = 0; return $this; } /** * Increments the patch version number. * * @param int $amount Increment by what amount? * * @return Builder The Version builder. */ public function incrementPatch(int $amount = 1): self { $this->patch += $amount; return $this; } /** * Sets the build metadata identifiers. * * @param array $identifiers The build metadata identifiers. * * @return Builder The Version builder. * * @throws InvalidIdentifierException If an identifier is invalid. */ public function setBuild(array $identifiers): self { foreach ($identifiers as $identifier) { if (!Validator::isIdentifier($identifier)) { throw new InvalidIdentifierException($identifier); } } $this->build = $identifiers; return $this; } /** * Sets the major version number. * * @param int $number The major version number. * * @return Builder The Version builder. * * @throws InvalidNumberException If the number is invalid. */ public function setMajor(int $number): self { if (!Validator::isNumber($number)) { throw new InvalidNumberException($number); } $this->major = intval($number); return $this; } /** * Sets the minor version number. * * @param int $number The minor version number. * * @return Builder The Version builder. * * @throws InvalidNumberException If the number is invalid. */ public function setMinor(int $number): self { if (!Validator::isNumber($number)) { throw new InvalidNumberException($number); } $this->minor = intval($number); return $this; } /** * Sets the patch version number. * * @param int $number The patch version number. * * @return Builder The Version builder. * * @throws InvalidNumberException If the number is invalid. */ public function setPatch(int $number): self { if (!Validator::isNumber($number)) { throw new InvalidNumberException($number); } $this->patch = intval($number); return $this; } /** * Sets the pre-release version identifiers. * * @param array $identifiers The pre-release version identifiers. * * @return Builder The Version builder. * * @throws InvalidIdentifierException If an identifier is invalid. */ public function setPreRelease(array $identifiers): self { foreach ($identifiers as $identifier) { if (!Validator::isIdentifier($identifier)) { throw new InvalidIdentifierException($identifier); } } $this->preRelease = $identifiers; return $this; } } ================================================ FILE: src/Component/PharUpdate/Version/Comparator.php ================================================ */ class Comparator { /** * The version is equal to another. */ public const EQUAL_TO = 0; /** * The version is greater than another. */ public const GREATER_THAN = 1; /** * The version is less than another. */ public const LESS_THAN = -1; /** * Compares one version with another. * * @param Version $left The left version to compare. * @param Version $right The right version to compare. * * @return integer Returns Comparator::EQUAL_TO if the two versions are * equal. If the left version is less than the right * version, Comparator::LESS_THAN is returned. If the left * version is greater than the right version, * Comparator::GREATER_THAN is returned. */ public static function compareTo(Version $left, Version $right) { switch (true) { case ($left->getMajor() < $right->getMajor()): return self::LESS_THAN; case ($left->getMajor() > $right->getMajor()): return self::GREATER_THAN; case ($left->getMinor() > $right->getMinor()): return self::GREATER_THAN; case ($left->getMinor() < $right->getMinor()): return self::LESS_THAN; case ($left->getPatch() > $right->getPatch()): return self::GREATER_THAN; case ($left->getPatch() < $right->getPatch()): return self::LESS_THAN; // @codeCoverageIgnoreStart } // @codeCoverageIgnoreEnd return self::compareIdentifiers( $left->getPreRelease(), $right->getPreRelease(), ); } /** * Checks if the left version is equal to the right. * * @param Version $left The left version to compare. * @param Version $right The right version to compare. * * @return boolean TRUE if the left version is equal to the right, FALSE * if not. */ public static function isEqualTo(Version $left, Version $right) { return (self::EQUAL_TO === self::compareTo($left, $right)); } /** * Checks if the left version is greater than the right. * * @param Version $left The left version to compare. * @param Version $right The right version to compare. * * @return boolean TRUE if the left version is greater than the right, * FALSE if not. */ public static function isGreaterThan(Version $left, Version $right) { return (self::GREATER_THAN === self::compareTo($left, $right)); } /** * Checks if the left version is less than the right. * * @param Version $left The left version to compare. * @param Version $right The right version to compare. * * @return boolean TRUE if the left version is less than the right, * FALSE if not. */ public static function isLessThan(Version $left, Version $right) { return (self::LESS_THAN === self::compareTo($left, $right)); } /** * Compares the identifier components of the left and right versions. * * @param array $left The left identifiers. * @param array $right The right identifiers. * * @return integer Returns Comparator::EQUAL_TO if the two identifiers are * equal. If the left identifiers is less than the right * identifiers, Comparator::LESS_THAN is returned. If the * left identifiers is greater than the right identifiers, * Comparator::GREATER_THAN is returned. */ public static function compareIdentifiers(array $left, array $right) { if ($left && empty($right)) { return self::LESS_THAN; } elseif (empty($left) && $right) { return self::GREATER_THAN; } $l = $left; $r = $right; $x = self::GREATER_THAN; $y = self::LESS_THAN; if (count($l) < count($r)) { $l = $right; $r = $left; $x = self::LESS_THAN; $y = self::GREATER_THAN; } foreach (array_keys($l) as $i) { if (!isset($r[$i])) { return $x; } if ($l[$i] === $r[$i]) { continue; } if (true === ($li = (false != preg_match('/^\d+$/', $l[$i])))) { $l[$i] = intval($l[$i]); } if (true === ($ri = (false != preg_match('/^\d+$/', $r[$i])))) { $r[$i] = intval($r[$i]); } if ($li && $ri) { return ($l[$i] > $r[$i]) ? $x : $y; } elseif (!$li && $ri) { return $x; } elseif ($li && !$ri) { return $y; } $result = strcmp($l[$i], $r[$i]); if ($result > 0) { return $x; } elseif ($result < 0) { return $y; } } return self::EQUAL_TO; } } ================================================ FILE: src/Component/PharUpdate/Version/Dumper.php ================================================ */ class Dumper { /** * Returns the components of a Version instance. * * @param Version $version A version. * * @return array The components. */ public static function toComponents(Version $version) { return [ Parser::MAJOR => $version->getMajor(), Parser::MINOR => $version->getMinor(), Parser::PATCH => $version->getPatch(), Parser::PRE_RELEASE => $version->getPreRelease(), Parser::BUILD => $version->getBuild(), ]; } /** * Returns the string representation of a Version instance. * * @param Version $version A version. * * @return string The string representation. */ public static function toString(Version $version) { return sprintf( '%d.%d.%d%s%s', $version->getMajor(), $version->getMinor(), $version->getPatch(), $version->getPreRelease() ? '-' . join('.', $version->getPreRelease()) : '', $version->getBuild() ? '+' . join('.', $version->getBuild()) : '', ); } } ================================================ FILE: src/Component/PharUpdate/Version/Exception/InvalidIdentifierException.php ================================================ */ class InvalidIdentifierException extends VersionException { /** * The invalid identifier. * * @var string */ private $identifier; /** * Sets the invalid identifier. * * @param string $identifier The invalid identifier. */ public function __construct(string $identifier) { parent::__construct( sprintf( 'The identifier "%s" is invalid.', $identifier, ), ); $this->identifier = $identifier; } /** * Returns the invalid identifier. * * @return string The invalid identifier. */ public function getIdentifier(): string { return $this->identifier; } } ================================================ FILE: src/Component/PharUpdate/Version/Exception/InvalidNumberException.php ================================================ */ class InvalidNumberException extends VersionException { /** * The invalid version number. * * @var mixed */ private $number; /** * Sets the invalid version number. * * @param mixed $number The invalid version number. */ public function __construct($number) { parent::__construct( sprintf( 'The version number "%s" is invalid.', $number, ), ); $this->number = $number; } /** * Returns the invalid version number. * * @return mixed The invalid version number. */ public function getNumber() { return $this->number; } } ================================================ FILE: src/Component/PharUpdate/Version/Exception/InvalidStringRepresentationException.php ================================================ */ class InvalidStringRepresentationException extends VersionException { /** * The invalid string representation. * * @var string */ private $version; /** * Sets the invalid string representation. * * @param string $version The string representation. */ public function __construct(string $version) { parent::__construct( sprintf( 'The version string representation "%s" is invalid.', $version, ), ); $this->version = $version; } /** * Returns the invalid string representation. * * @return string The invalid string representation. */ public function getVersion(): string { return $this->version; } } ================================================ FILE: src/Component/PharUpdate/Version/Exception/VersionException.php ================================================ */ class VersionException extends Exception {} ================================================ FILE: src/Component/PharUpdate/Version/Parser.php ================================================ */ class Parser { /** * The build metadata component. */ public const BUILD = 'build'; /** * The major version number component. */ public const MAJOR = 'major'; /** * The minor version number component. */ public const MINOR = 'minor'; /** * The patch version number component. */ public const PATCH = 'patch'; /** * The pre-release version number component. */ public const PRE_RELEASE = 'pre'; /** * Returns a Version builder for the string representation. * * @param string $version The string representation. * * @return Builder A Version builder. */ public static function toBuilder(string $version): Builder { return Builder::create()->importComponents( self::toComponents($version), ); } /** * Returns the components of the string representation. * * @param string $version The string representation. * * @return array The components of the version. * * @throws InvalidStringRepresentationException If the string representation * is invalid. */ public static function toComponents(string $version): array { if (!Validator::isVersion($version)) { throw new InvalidStringRepresentationException($version); } if (false !== strpos($version, '+')) { [$version, $build] = explode('+', $version); $build = explode('.', $build); } if (false !== strpos($version, '-')) { [$version, $pre] = explode('-', $version); $pre = explode('.', $pre); } [ $major, $minor, $patch, ] = explode('.', $version); return [ self::MAJOR => intval($major), self::MINOR => intval($minor), self::PATCH => intval($patch), self::PRE_RELEASE => $pre ?? [], self::BUILD => $build ?? [], ]; } /** * Returns a Version instance for the string representation. * * @param string $version The string representation. * * @return Version A Version instance. */ public static function toVersion(string $version): Version { $components = self::toComponents($version); return new Version( $components['major'], $components['minor'], $components['patch'], $components['pre'], $components['build'], ); } } ================================================ FILE: src/Component/PharUpdate/Version/Validator.php ================================================ */ class Validator { /** * The regular expression for a valid identifier. */ public const IDENTIFIER_REGEX = '/^[0-9A-Za-z\-]+$/'; /** * The regular expression for a valid semantic version number. */ public const VERSION_REGEX = '/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/'; /** * Checks if a identifier is valid. * * @param string $identifier A identifier. * * @return boolean TRUE if the identifier is valid, FALSE If not. */ public static function isIdentifier(string $identifier): bool { return (true == preg_match(self::IDENTIFIER_REGEX, $identifier)); } /** * Checks if a number is a valid version number. * * @param integer $number A number. * * @return boolean TRUE if the number is valid, FALSE If not. */ public static function isNumber(int $number): bool { return (true == preg_match('/^(0|[1-9]\d*)$/', (string) $number)); } /** * Checks if the string representation of a version number is valid. * * @param string $version The string representation. * * @return boolean TRUE if the string representation is valid, FALSE if not. */ public static function isVersion(string $version): bool { return (true == preg_match(self::VERSION_REGEX, $version)); } } ================================================ FILE: src/Component/PharUpdate/Version/Version.php ================================================ */ class Version { /** * The build metadata identifiers. * * @var array */ protected $build; /** * The major version number. * * @var integer */ protected $major; /** * The minor version number. * * @var integer */ protected $minor; /** * The patch version number. * * @var integer */ protected $patch; /** * The pre-release version identifiers. * * @var array */ protected $preRelease; /** * Sets the version information. * * @param int $major The major version number. * @param int $minor The minor version number. * @param int $patch The patch version number. * @param array $pre The pre-release version identifiers. * @param array $build The build metadata identifiers. */ public function __construct( int $major = 0, int $minor = 0, int $patch = 0, array $pre = [], array $build = [], ) { $this->build = $build; $this->major = $major; $this->minor = $minor; $this->patch = $patch; $this->preRelease = $pre; } /** * Returns the build metadata identifiers. * * @return array The build metadata identifiers. */ public function getBuild(): array { return $this->build; } /** * Returns the major version number. * * @return int The major version number. */ public function getMajor(): int { return $this->major; } /** * Returns the minor version number. * * @return int The minor version number. */ public function getMinor(): int { return $this->minor; } /** * Returns the patch version number. * * @return int The patch version number. */ public function getPatch(): int { return $this->patch; } /** * Returns the pre-release version identifiers. * * @return array The pre-release version identifiers. */ public function getPreRelease(): array { return $this->preRelease; } /** * Checks if the version number is stable. * * @return boolean TRUE if it is stable, FALSE if not. */ public function isStable(): bool { return empty($this->preRelease) && $this->major !== 0; } /** * Returns string representation. * * @return string The string representation. */ public function __toString(): string { return Dumper::toString($this); } } ================================================ FILE: src/Component/Pimple/Container.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Component\Pimple; use Deployer\Component\Pimple\Exception\ExpectedInvokableException; use Deployer\Component\Pimple\Exception\FrozenServiceException; use Deployer\Component\Pimple\Exception\InvalidServiceIdentifierException; use Deployer\Component\Pimple\Exception\UnknownIdentifierException; /** * Container main class. * * @author Fabien Potencier */ class Container implements \ArrayAccess { /** * @var array */ private $values = []; /** * @var \SplObjectStorage */ private $factories; /** * @var \SplObjectStorage */ private $protected; /** * @var array */ private $frozen = []; /** * @var array */ private $raw = []; /** * @var array */ private $keys = []; /** * Instantiates the container. * * Objects and parameters can be passed as argument to the constructor. * * @param array $values The parameters or objects */ public function __construct(array $values = []) { $this->factories = new \SplObjectStorage(); $this->protected = new \SplObjectStorage(); foreach ($values as $key => $value) { $this->offsetSet($key, $value); } } /** * Sets a parameter or an object. * * Objects must be defined as Closures. * * Allowing any PHP callable leads to difficult to debug problems * as function names (strings) are callable (creating a function with * the same name as an existing parameter would break your container). * * @param string $id The unique identifier for the parameter or object * @param mixed $value The value of the parameter or a closure to define an object * * @throws FrozenServiceException Prevent override of a frozen service * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint */ #[\ReturnTypeWillChange] public function offsetSet($id, $value) { if (isset($this->frozen[$id])) { throw new FrozenServiceException($id); } $this->values[$id] = $value; $this->keys[$id] = true; } /** * Gets a parameter or an object. * * @param string $id The unique identifier for the parameter or object * * @return mixed The value of the parameter or an object * * @throws UnknownIdentifierException If the identifier is not defined * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint */ #[\ReturnTypeWillChange] public function offsetGet($id) { if (!isset($this->keys[$id])) { throw new UnknownIdentifierException($id); } if ( isset($this->raw[$id]) || !\is_object($this->values[$id]) || isset($this->protected[$this->values[$id]]) || !\method_exists($this->values[$id], '__invoke') ) { return $this->values[$id]; } if (isset($this->factories[$this->values[$id]])) { return $this->values[$id]($this); } $raw = $this->values[$id]; $val = $this->values[$id] = $raw($this); $this->raw[$id] = $raw; $this->frozen[$id] = true; return $val; } /** * Checks if a parameter or an object is set. * * @param string $id The unique identifier for the parameter or object * * @return bool * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint */ #[\ReturnTypeWillChange] public function offsetExists($id) { return isset($this->keys[$id]); } /** * Unsets a parameter or an object. * * @param string $id The unique identifier for the parameter or object * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint */ #[\ReturnTypeWillChange] public function offsetUnset($id) { if (isset($this->keys[$id])) { if (\is_object($this->values[$id])) { unset($this->factories[$this->values[$id]], $this->protected[$this->values[$id]]); } unset($this->values[$id], $this->frozen[$id], $this->raw[$id], $this->keys[$id]); } } /** * Marks a callable as being a factory service. * * @param callable $callable A service definition to be used as a factory * * @return callable The passed callable * * @throws ExpectedInvokableException Service definition has to be a closure or an invokable object */ public function factory(callable $callable) { if (!\method_exists($callable, '__invoke')) { throw new ExpectedInvokableException('Service definition is not a Closure or invokable object.'); } $this->factories->attach($callable); return $callable; } /** * Protects a callable from being interpreted as a service. * * This is useful when you want to store a callable as a parameter. * * @param callable $callable A callable to protect from being evaluated * * @return callable The passed callable * * @throws ExpectedInvokableException Service definition has to be a closure or an invokable object */ public function protect(callable $callable) { if (!\method_exists($callable, '__invoke')) { throw new ExpectedInvokableException('Callable is not a Closure or invokable object.'); } $this->protected->attach($callable); return $callable; } /** * Gets a parameter or the closure defining an object. * * @param string $id The unique identifier for the parameter or object * * @return mixed The value of the parameter or the closure defining an object * * @throws UnknownIdentifierException If the identifier is not defined */ public function raw(string $id) { if (!isset($this->keys[$id])) { throw new UnknownIdentifierException($id); } if (isset($this->raw[$id])) { return $this->raw[$id]; } return $this->values[$id]; } /** * Extends an object definition. * * Useful when you want to extend an existing object definition, * without necessarily loading that object. * * @param string $id The unique identifier for the object * @param callable $callable A service definition to extend the original * * @return callable The wrapped callable * * @throws UnknownIdentifierException If the identifier is not defined * @throws FrozenServiceException If the service is frozen * @throws InvalidServiceIdentifierException If the identifier belongs to a parameter * @throws ExpectedInvokableException If the extension callable is not a closure or an invokable object */ public function extend(string $id, callable $callable) { if (!isset($this->keys[$id])) { throw new UnknownIdentifierException($id); } if (isset($this->frozen[$id])) { throw new FrozenServiceException($id); } if (!\is_object($this->values[$id]) || !\method_exists($this->values[$id], '__invoke')) { throw new InvalidServiceIdentifierException($id); } if (isset($this->protected[$this->values[$id]])) { @\trigger_error(\sprintf('How Pimple behaves when extending protected closures will be fixed in Pimple 4. Are you sure "%s" should be protected?', $id), E_USER_DEPRECATED); } if (!\is_object($callable) || !\method_exists($callable, '__invoke')) { throw new ExpectedInvokableException('Extension service definition is not a Closure or invokable object.'); } $factory = $this->values[$id]; $extended = function ($c) use ($callable, $factory) { return $callable($factory($c), $c); }; if (isset($this->factories[$factory])) { $this->factories->detach($factory); $this->factories->attach($extended); } return $this[$id] = $extended; } /** * Returns all defined value names. * * @return array An array of value names */ public function keys() { return \array_keys($this->values); } } ================================================ FILE: src/Component/Pimple/Exception/ExpectedInvokableException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Component\Pimple\Exception; use Psr\Container\ContainerExceptionInterface; /** * A closure or invokable object was expected. * * @author Pascal Luna */ class ExpectedInvokableException extends \InvalidArgumentException implements ContainerExceptionInterface {} ================================================ FILE: src/Component/Pimple/Exception/FrozenServiceException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Component\Pimple\Exception; use Psr\Container\ContainerExceptionInterface; /** * An attempt to modify a frozen service was made. * * @author Pascal Luna */ class FrozenServiceException extends \RuntimeException implements ContainerExceptionInterface { /** * @param string $id Identifier of the frozen service */ public function __construct(string $id) { parent::__construct(\sprintf('Cannot override frozen service "%s".', $id)); } } ================================================ FILE: src/Component/Pimple/Exception/InvalidServiceIdentifierException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Component\Pimple\Exception; use Psr\Container\NotFoundExceptionInterface; /** * An attempt to perform an operation that requires a service identifier was made. * * @author Pascal Luna */ class InvalidServiceIdentifierException extends \InvalidArgumentException implements NotFoundExceptionInterface { /** * @param string $id The invalid identifier */ public function __construct(string $id) { parent::__construct(\sprintf('Identifier "%s" does not contain an object definition.', $id)); } } ================================================ FILE: src/Component/Pimple/Exception/UnknownIdentifierException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Component\Pimple\Exception; use Psr\Container\NotFoundExceptionInterface; /** * The identifier of a valid service or parameter was expected. * * @author Pascal Luna */ class UnknownIdentifierException extends \InvalidArgumentException implements NotFoundExceptionInterface { /** * @param string $id The unknown identifier */ public function __construct(string $id) { parent::__construct(\sprintf('Identifier "%s" is not defined.', $id)); } } ================================================ FILE: src/Configuration.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer; use Deployer\Exception\ConfigurationException; use Deployer\Utility\Httpie; use function Deployer\Support\array_merge_alternate; use function Deployer\Support\is_closure; use function Deployer\Support\normalize_line_endings; class Configuration implements \ArrayAccess { private ?Configuration $parent; private array $values = []; public function __construct(?Configuration $parent = null) { $this->parent = $parent; } public function update(array $values): void { $this->values = array_merge($this->values, $values); } public function bind(Configuration $parent): void { $this->parent = $parent; } public function set(string $name, mixed $value): void { $this->values[$name] = $value; } public function has(string $name): bool { $ok = array_key_exists($name, $this->values); if ($ok) { return true; } if ($this->parent) { return $this->parent->has($name); } return false; } public function hasOwn(string $name): bool { return array_key_exists($name, $this->values); } public function add(string $name, array $array): void { if ($this->has($name)) { $config = $this->get($name); if (!is_array($config)) { throw new ConfigurationException("Config option \"$name\" isn't array."); } $this->set($name, array_merge_alternate($config, $array)); } else { $this->set($name, $array); } } public function get(string $name, mixed $default = null): mixed { if (array_key_exists($name, $this->values)) { if (is_closure($this->values[$name])) { return $this->values[$name] = $this->parse(call_user_func($this->values[$name])); } else { return $this->parse($this->values[$name]); } } if ($this->parent) { $rawValue = $this->parent->fetch($name); if ($rawValue !== null) { if (is_closure($rawValue)) { return $this->values[$name] = $this->parse(call_user_func($rawValue)); } else { return $this->values[$name] = $this->parse($rawValue); } } } if (func_num_args() >= 2) { return $this->parse($default); } throw new ConfigurationException("Config option \"$name\" does not exist."); } protected function fetch(string $name): mixed { if (array_key_exists($name, $this->values)) { return $this->values[$name]; } if ($this->parent) { return $this->parent->fetch($name); } return null; } public function parse(mixed $value): mixed { if (is_string($value)) { $normalizedValue = normalize_line_endings($value); return preg_replace_callback('/\{\{\s*([\w\.\/-]+)\s*\}\}/', function (array $matches) { return $this->get($matches[1]); }, $normalizedValue); } return $value; } public function keys(): array { return array_keys($this->values); } /** * @param string $offset * @return bool */ #[\ReturnTypeWillChange] public function offsetExists($offset) { return $this->has($offset); } /** * @param string $offset * @return mixed */ #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->get($offset); } /** * @param string $offset * @param mixed $value */ #[\ReturnTypeWillChange] public function offsetSet($offset, $value): void { $this->set($offset, $value); } /** * @param mixed $offset */ #[\ReturnTypeWillChange] public function offsetUnset($offset): void { unset($this->values[$offset]); } public function load(): void { if (!Deployer::isWorker()) { return; } $values = Httpie::get(MASTER_ENDPOINT . '/load') ->setopt(CURLOPT_CONNECTTIMEOUT, 0) ->setopt(CURLOPT_TIMEOUT, 0) ->jsonBody([ 'host' => $this->get('alias'), ]) ->getJson(); $this->update($values); } public function save(): void { if (!Deployer::isWorker()) { return; } Httpie::get(MASTER_ENDPOINT . '/save') ->setopt(CURLOPT_CONNECTTIMEOUT, 0) ->setopt(CURLOPT_TIMEOUT, 0) ->jsonBody([ 'host' => $this->get('alias'), 'config' => $this->persist(), ]) ->getJson(); } public function persist(): array { $values = []; foreach ($this->values as $key => $value) { if (is_closure($value)) { continue; } $values[$key] = $value; } return $values; } } ================================================ FILE: src/Deployer.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer; use Deployer\Collection\Collection; use Deployer\Command\BlackjackCommand; use Deployer\Command\ConfigCommand; use Deployer\Command\InitCommand; use Deployer\Command\MainCommand; use Deployer\Command\RunCommand; use Deployer\Command\SshCommand; use Deployer\Command\TreeCommand; use Deployer\Command\WorkerCommand; use Deployer\Component\PharUpdate\Console\Command as PharUpdateCommand; use Deployer\Component\PharUpdate\Console\Helper as PharUpdateHelper; use Deployer\Component\Pimple\Container; use Deployer\ProcessRunner\Printer; use Deployer\ProcessRunner\ProcessRunner; use Deployer\Ssh\SshClient; use Deployer\Configuration; use Deployer\Executor\Master; use Deployer\Executor\Messenger; use Deployer\Host\Host; use Deployer\Host\HostCollection; use Deployer\Host\Localhost; use Deployer\Importer\Importer; use Deployer\Logger\Handler\FileHandler; use Deployer\Logger\Handler\NullHandler; use Deployer\Logger\Logger; use Deployer\Selector\Selector; use Deployer\Task\ScriptManager; use Deployer\Task\TaskCollection; use Deployer\Utility\Httpie; use Deployer\Utility\Rsync; use Symfony\Component\Console; use Symfony\Component\Console\Application; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\OutputInterface; use Throwable; /** * @property Application $console * @property InputInterface $input * @property OutputInterface $output * @property Task\TaskCollection|Task\Task[] $tasks * @property HostCollection|Host[] $hosts * @property Configuration $config * @property Rsync $rsync * @property SshClient $sshClient * @property ProcessRunner $processRunner * @property Task\ScriptManager $scriptManager * @property Selector $selector * @property Master $master * @property Messenger $messenger * @property Messenger $logger * @property Printer $pop * @property Collection $fail * @property InputDefinition $inputDefinition * @property Importer $importer */ class Deployer extends Container { private static Deployer $instance; public function __construct(Application $console) { parent::__construct(); /****************************** * Console * ******************************/ $console->getDefinition()->addOption( new InputOption('file', 'f', InputOption::VALUE_REQUIRED, 'Recipe file path'), ); $this['console'] = function () use ($console) { return $console; }; $this['input'] = function () { throw new \RuntimeException('Uninitialized "input" in Deployer container.'); }; $this['output'] = function () { throw new \RuntimeException('Uninitialized "output" in Deployer container.'); }; $this['inputDefinition'] = function () { return new InputDefinition(); }; $this['questionHelper'] = function () { return $this->getHelper('question'); }; /****************************** * Config * ******************************/ $this['config'] = function () { return new Configuration(); }; // -l act as if it had been invoked as a login shell (i.e. source ~/.profile file) // -s commands are read from the standard input (no arguments should remain after this option) $this->config['shell'] = function () { if (currentHost() instanceof Localhost) { return 'bash -s'; // Non-login shell for localhost. } return 'bash -ls'; }; $this->config['forward_agent'] = true; $this->config['ssh_multiplexing'] = true; /****************************** * Core * ******************************/ $this['pop'] = function ($c) { return new Printer($c['output']); }; $this['sshClient'] = function ($c) { return new SshClient($c['output'], $c['pop'], $c['logger']); }; $this['rsync'] = function ($c) { return new Rsync($c['pop'], $c['output']); }; $this['processRunner'] = function ($c) { return new ProcessRunner($c['pop'], $c['logger']); }; $this['tasks'] = function () { return new TaskCollection(); }; $this['hosts'] = function () { return new HostCollection(); }; $this['scriptManager'] = function ($c) { return new ScriptManager($c['tasks']); }; $this['selector'] = function ($c) { return new Selector($c['hosts']); }; $this['fail'] = function () { return new Collection(); }; $this['messenger'] = function ($c) { return new Messenger($c['input'], $c['output'], $c['logger']); }; $this['master'] = function ($c) { return new Master( $c['hosts'], $c['input'], $c['output'], $c['messenger'], ); }; $this['importer'] = function () { return new Importer(); }; /****************************** * Logger * ******************************/ $this['log_handler'] = function () { return !empty($this['log']) ? new FileHandler($this['log']) : new NullHandler(); }; $this['logger'] = function () { return new Logger($this['log_handler']); }; self::$instance = $this; } public static function get(): self { return self::$instance; } public function init(): void { $this->addTaskCommands(); $this->getConsole()->add(new BlackjackCommand()); $this->getConsole()->add(new ConfigCommand($this)); $this->getConsole()->add(new WorkerCommand($this)); $this->getConsole()->add(new InitCommand()); $this->getConsole()->add(new TreeCommand($this)); $this->getConsole()->add(new SshCommand($this)); $this->getConsole()->add(new RunCommand($this)); if (self::isPharArchive()) { $selfUpdate = new PharUpdateCommand('self-update'); $selfUpdate->setDescription('Updates deployer.phar to the latest version'); $selfUpdate->setManifestUri('https://deployer.org/manifest.json'); $selfUpdate->setRunningFile(DEPLOYER_BIN); $this->getConsole()->add($selfUpdate); $this->getConsole()->getHelperSet()->set(new PharUpdateHelper()); } } /** * Transform tasks to console commands. */ public function addTaskCommands(): void { foreach ($this->tasks as $name => $task) { $command = new MainCommand($name, $task->getDescription(), $this); $command->setHidden($task->isHidden()); $this->getConsole()->add($command); } } public function __get(string $name): mixed { if (isset($this[$name])) { return $this[$name]; } else { throw new \InvalidArgumentException("Property \"$name\" does not exist."); } } public function __set(string $name, mixed $value): void { $this[$name] = $value; } public function getConsole(): Application { return $this['console']; } public function getHelper(string $name): Console\Helper\HelperInterface { return $this->getConsole()->getHelperSet()->get($name); } public static function run(string $version, ?string $deployFile): void { if (str_contains($version, 'master')) { // Get version from composer.lock $lockFile = __DIR__ . '/../../../../composer.lock'; if (file_exists($lockFile)) { $content = file_get_contents($lockFile); $json = json_decode($content); foreach ($json->packages as $package) { if ($package->name === 'deployer/deployer') { $version = $package->version; } } } } // Version must be without "v" prefix. // Incorrect: v7.0.0 // Correct: 7.0.0 // But deployphp/deployer uses tags with "v", and it gets passed to // the composer.json file. Let's manually remove it from the version. if (preg_match("/^v/", $version)) { $version = substr($version, 1); } if (!defined('DEPLOYER_VERSION')) { define('DEPLOYER_VERSION', $version); } $input = new ArgvInput(); $output = new ConsoleOutput(); try { $console = new Application('Deployer', $version); $deployer = new self($console); // Import recipe file if (is_readable($deployFile ?? '')) { $deployer->importer->import($deployFile); } $deployer->init(); $console->run($input, $output); } catch (Throwable $exception) { if (str_contains("$input", "-vvv")) { $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); } self::printException($output, $exception); exit(1); } } public static function printException(OutputInterface $output, Throwable $exception): void { $class = get_class($exception); $file = basename($exception->getFile()); $output->writeln([ " {$class} in {$file} on line {$exception->getLine()}:", "", implode("\n", array_map(function ($line) { return " " . $line; }, explode("\n", $exception->getMessage()))), "", ]); if ($output->isDebug()) { $output->writeln($exception->getTraceAsString()); } if ($exception->getPrevious()) { self::printException($output, $exception->getPrevious()); } } public static function isWorker(): bool { return defined('MASTER_ENDPOINT'); } /** * @return array|bool|string */ public static function masterCall(Host $host, string $func, mixed ...$arguments): mixed { // As request to master will stop master permanently, wait a little bit // in order for ticker gather worker outputs and print it to user. usleep(100_000); // Sleep 100ms. return Httpie::get(MASTER_ENDPOINT . '/proxy') ->setopt(CURLOPT_CONNECTTIMEOUT, 0) // no timeout ->setopt(CURLOPT_TIMEOUT, 0) // no timeout ->jsonBody([ 'host' => $host->getAlias(), 'func' => $func, 'arguments' => $arguments, ]) ->getJson(); } public static function isPharArchive(): bool { return str_starts_with(__FILE__, 'phar:'); } } ================================================ FILE: src/Documentation/ApiGen.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Documentation; class ApiGen { /** * @var array */ private $fns = []; public function parse(string $source): void { $comment = ''; $params = ''; $signature = ''; $source = str_replace("\r\n", "\n", $source); $state = 'root'; foreach (explode("\n", $source) as $lineNumber => $line) { switch ($state) { case 'root': if (str_starts_with($line, '/**')) { $state = 'comment'; break; } if (str_starts_with($line, 'function')) { $signature = preg_replace('/^function\s+/', '', $line); $funcName = preg_replace('/\(.*$/', '', $signature); $this->fns[] = [ 'comment' => $comment, 'params' => $params, 'funcName' => $funcName, 'signature' => $signature, ]; $comment = ''; $params = ''; if (str_ends_with($signature, '(')) { $state = 'params'; } else { $signature = ''; } } break; case 'comment': if (str_ends_with($line, '*/')) { $state = 'root'; break; } if (preg_match('/^\s\*\s@param\s(?.+?)\$(?.+?)\s(?.+)$/', $line, $matches)) { if (empty($params)) { $params = "| Argument | Type | Comment |\n|---|---|---|\n"; } $type = implode(' or ', array_map(function ($t) { $t = trim($t, ' '); return "`$t`"; }, explode('|', $matches['type']))); $params .= "| `\${$matches['name']}` | $type | {$matches['comment']} |\n"; break; } if (str_starts_with($line, ' * @')) { break; } $comment .= preg_replace('/^\s\*\s?/', '', $line) . "\n"; break; case 'params': if (preg_match('/^\).+\{$/', $line, $matches)) { $signature .= "\n" . preg_replace('/\{$/', '', $line); $this->fns[count($this->fns) - 1]['signature'] = $signature; $state = 'root'; } else { $signature .= "\n" . $line; } break; } } } public function markdown(): string { $output = << # API Reference MD; foreach ($this->fns as $fn) { [ 'comment' => $comment, 'params' => $params, 'funcName' => $funcName, 'signature' => $signature, ] = $fn; if (!empty($params)) { $params = "\n$params"; } $output .= << * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Documentation; class DocConfig { /** * @var string */ public $name; /** * @var string */ public $defaultValue; /** * @var string */ public $comment; /** * @var string */ public $recipePath; /** * @var int */ public $lineNumber; } ================================================ FILE: src/Documentation/DocGen.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Documentation; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use RecursiveRegexIterator; use RegexIterator; class DocGen { /** * @var string */ public $root; /** * @var DocRecipe[] */ public $recipes = []; public function __construct(string $root) { $this->root = str_replace(DIRECTORY_SEPARATOR, '/', realpath($root)); } public function parse(string $source): void { $directory = new RecursiveDirectoryIterator($source); $iterator = new RegexIterator(new RecursiveIteratorIterator($directory), '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH); foreach ($iterator as [$path]) { $realPath = str_replace(DIRECTORY_SEPARATOR, '/', realpath($path)); $recipePath = str_replace($this->root . '/', '', $realPath); $recipeName = preg_replace('/\.php$/i', '', basename($recipePath)); $recipe = new DocRecipe($recipeName, $recipePath); $recipe->parse(file_get_contents($path)); $this->recipes[$recipePath] = $recipe; } } public function gen(string $destination): ?string { foreach ($this->recipes as $recipe) { // $find will try to return DocConfig for a given config $name. $findConfig = function (string $name) use ($recipe): ?DocConfig { if (array_key_exists($name, $recipe->config)) { return $recipe->config[$name]; } foreach ($recipe->require as $r) { if (array_key_exists($r, $this->recipes)) { if (array_key_exists($name, $this->recipes[$r]->config)) { return $this->recipes[$r]->config[$name]; } } } foreach ($this->recipes as $r) { if (array_key_exists($name, $r->config)) { return $r->config[$name]; } } return null; }; $findConfigOverride = function (DocRecipe $recipe, string $name) use (&$findConfigOverride): ?DocConfig { foreach ($recipe->require as $r) { if (array_key_exists($r, $this->recipes)) { if (array_key_exists($name, $this->recipes[$r]->config)) { return $this->recipes[$r]->config[$name]; } } } foreach ($recipe->require as $r) { if (array_key_exists($r, $this->recipes)) { return $findConfigOverride($this->recipes[$r], $name); } } return null; }; // Replace all {{name}} with link to correct config declaration. $replaceLinks = function (string $comment) use ($findConfig): string { $output = ''; $code = false; foreach (explode("\n", $comment) as $i => $line) { if (str_starts_with($line, '```') || str_starts_with($line, '~~~')) { $code = !$code; } if ($code) { $output .= $line; $output .= "\n"; continue; } $output .= preg_replace_callback('#(\{\{(?[\w_:\-/]+)\}\})#', function ($m) use ($findConfig) { $name = $m['name']; $config = $findConfig($name); if ($config !== null) { $md = php_to_md($config->recipePath); $anchor = anchor($name); return "[$name](/docs/$md#$anchor)"; } return "{{" . $name . "}}"; }, $line); $output .= "\n"; } return $output; }; $findTask = function (string $name, bool $searchOtherRecipes = true) use ($recipe): ?DocTask { if (array_key_exists($name, $recipe->tasks)) { return $recipe->tasks[$name]; } foreach ($recipe->require as $r) { if (array_key_exists($r, $this->recipes)) { if (array_key_exists($name, $this->recipes[$r]->tasks)) { return $this->recipes[$r]->tasks[$name]; } } } if ($searchOtherRecipes) { foreach ($this->recipes as $r) { if (array_key_exists($name, $r->tasks)) { return $r->tasks[$name]; } } } return null; }; $title = join(' ', array_map('ucfirst', explode('_', $recipe->recipeName))) . ' Recipe'; $config = ''; $tasks = ''; $intro = <<recipePath'; ``` [Source](/$recipe->recipePath) MD; if (is_framework_recipe($recipe)) { $brandName = framework_brand_name($recipe->recipeName); $typeOfProject = preg_match('/^symfony/i', $recipe->recipeName) ? 'Application' : 'Project'; $title = "How to Deploy a $brandName $typeOfProject"; $intro .= <<group as $taskName) { $t = $findTask($taskName); if ($t !== null) { $intro .= "$ident* {$t->mdLink()} – $t->desc\n"; if ($t->group !== null) { $map($t, $ident . ' '); } } } }; $deployTask = $findTask('deploy'); if ($deployTask !== null) { $intro .= "The [deploy](#deploy) task of **$brandName** consists of:\n"; $map($deployTask); } $intro .= "\n\n"; $artifactBuildTask = $findTask('artifact:build', false); $artifactDeployTask = $findTask('artifact:deploy', false); if ($artifactDeployTask !== null && $artifactBuildTask !== null) { $intro .= "In addition the **$brandName** recipe contains an artifact deployment.\n"; $intro .= <<set('local', true); ``` to your deploy.php or ```yaml hosts: localhost: local: true ``` to your deploy yaml. The [artifact:build](#artifact:build) command of **$brandName** consists of: MD; $map($artifactBuildTask); $intro .= "\n\n The [artifact:deploy](#artifact:deploy) command of **$brandName** consists of:\n"; $map($artifactDeployTask); $intro .= "\n\n"; } } if (count($recipe->require) > 0) { if (is_framework_recipe($recipe)) { $link = recipe_to_md_link($recipe->require[0]); $intro .= "The $recipe->recipeName recipe is based on the $link recipe.\n"; } else { $intro .= "* Requires\n"; foreach ($recipe->require as $r) { $link = recipe_to_md_link($r); $intro .= " * {$link}\n"; } } } if (!empty($recipe->comment)) { $intro .= "\n$recipe->comment\n"; } if (count($recipe->config) > 0) { $config .= "## Configuration\n"; foreach ($recipe->config as $c) { $config .= "### {$c->name}\n"; $config .= "[Source](https://github.com/deployphp/deployer/blob/master/{$c->recipePath}#L{$c->lineNumber})\n\n"; $o = $findConfigOverride($recipe, $c->name); if ($o !== null) { $md = php_to_md($o->recipePath); $anchor = anchor($c->name); $config .= "Overrides [{$c->name}](/docs/$md#$anchor) from `$o->recipePath`.\n\n"; } $config .= $replaceLinks($c->comment); $config .= "\n"; if ( !empty($c->defaultValue) && $c->defaultValue !== "''" && $c->defaultValue !== '[]' ) { $config .= "```php title=\"Default value\"\n"; $config .= $c->defaultValue; $config .= "\n"; $config .= "```\n"; } $config .= "\n\n"; } } if (count($recipe->tasks) > 0) { $tasks .= "## Tasks\n\n"; foreach ($recipe->tasks as $t) { $anchorTag = '{#' . anchor($t->name) . '}'; $name = title($t->name); $tasks .= "### $name $anchorTag\n"; $tasks .= "[Source](https://github.com/deployphp/deployer/blob/master/{$t->recipePath}#L{$t->lineNumber})\n\n"; $tasks .= add_tailing_dot($t->desc) . "\n\n"; $tasks .= $replaceLinks($t->comment); if (is_array($t->group)) { $tasks .= "\n\n"; $tasks .= "This task is group task which contains next tasks:\n"; foreach ($t->group as $taskName) { $t = $findTask($taskName); if ($t !== null) { $tasks .= "* {$t->mdLink()}\n"; } else { $tasks .= "* `$taskName`\n"; } } } $tasks .= "\n\n"; } } $output = << # $title $intro $config $tasks MD; $filePath = "$destination/" . php_to_md($recipe->recipePath); if (!file_exists(dirname($filePath))) { mkdir(dirname($filePath), 0o755, true); } $output = remove_text_emoji($output); file_put_contents($filePath, $output); } $this->generateRecipesIndex($destination); $this->generateContribIndex($destination); return null; } public function generateRecipesIndex(string $destination) { $index = "# All Recipes\n\n"; $list = []; foreach ($this->recipes as $recipe) { if (preg_match('/^recipe\/[^\/]+\.php$/', $recipe->recipePath)) { $name = framework_brand_name($recipe->recipeName); $list[] = "* [$name Recipe](/docs/recipe/{$recipe->recipeName}.md)"; } } sort($list); $index .= implode("\n", $list); file_put_contents("$destination/recipe/README.md", $index); } public function generateContribIndex(string $destination) { $index = "# All Contrib Recipes\n\n"; $list = []; foreach ($this->recipes as $recipe) { if (preg_match('/^contrib\/[^\/]+\.php$/', $recipe->recipePath)) { $name = ucfirst($recipe->recipeName); $list[] = "* [$name Recipe](/docs/contrib/$recipe->recipeName.md)"; } } sort($list); $index .= implode("\n", $list); file_put_contents("$destination/contrib/README.md", $index); } } function trim_comment(string $line): string { return preg_replace('#^(/\*\*?\s?|\s\*\s?|//\s?)#', '', $line); } function indent(string $text): string { return implode("\n", array_map(function ($line) { return " " . $line; }, explode("\n", $text))); } function php_to_md(string $file): string { return preg_replace('#\.php$#', '.md', $file); } function title(string $s): string { return str_replace(':', '\\:', $s); } function anchor(string $s): string { return strtolower(str_replace(':', '-', $s)); } function remove_text_emoji(string $text): string { return preg_replace('/:(bowtie|smile|laughing|blush|smiley|relaxed|smirk|heart_eyes|kissing_heart|kissing_closed_eyes|flushed|relieved|satisfied|grin|wink|stuck_out_tongue_winking_eye|stuck_out_tongue_closed_eyes|grinning|kissing|kissing_smiling_eyes|stuck_out_tongue|sleeping|worried|frowning|anguished|open_mouth|grimacing|confused|hushed|expressionless|unamused|sweat_smile|sweat|disappointed_relieved|weary|pensive|disappointed|confounded|fearful|cold_sweat|persevere|cry|sob|joy|astonished|scream|neckbeard|tired_face|angry|rage|triumph|sleepy|yum|mask|sunglasses|dizzy_face|imp|smiling_imp|neutral_face|no_mouth|innocent|alien|yellow_heart|blue_heart|purple_heart|heart|green_heart|broken_heart|heartbeat|heartpulse|two_hearts|revolving_hearts|cupid|sparkling_heart|sparkles|star|star2|dizzy|boom|collision|anger|exclamation|question|grey_exclamation|grey_question|zzz|dash|sweat_drops|notes|musical_note|fire|hankey|poop|shit|\+1|thumbsup|\-1|thumbsdown|ok_hand|punch|facepunch|fist|v|wave|hand|raised_hand|open_hands|point_up|point_down|point_left|point_right|raised_hands|pray|point_up_2|clap|muscle|metal|fu|walking|runner|running|couple|family|two_men_holding_hands|two_women_holding_hands|dancer|dancers|ok_woman|no_good|information_desk_person|raising_hand|bride_with_veil|person_with_pouting_face|person_frowning|bow|couplekiss|couple_with_heart|massage|haircut|nail_care|boy|girl|woman|man|baby|older_woman|older_man|person_with_blond_hair|man_with_gua_pi_mao|man_with_turban|construction_worker|cop|angel|princess|smiley_cat|smile_cat|heart_eyes_cat|kissing_cat|smirk_cat|scream_cat|crying_cat_face|joy_cat|pouting_cat|japanese_ogre|japanese_goblin|see_no_evil|hear_no_evil|speak_no_evil|guardsman|skull|feet|lips|kiss|droplet|ear|eyes|nose|tongue|love_letter|bust_in_silhouette|busts_in_silhouette|speech_balloon|thought_balloon|feelsgood|finnadie|goberserk|godmode|hurtrealbad|rage1|rage2|rage3|rage4|suspect|trollface|sunny|umbrella|cloud|snowflake|snowman|zap|cyclone|foggy|ocean|cat|dog|mouse|hamster|rabbit|wolf|frog|tiger|koala|bear|pig|pig_nose|cow|boar|monkey_face|monkey|horse|racehorse|camel|sheep|elephant|panda_face|snake|bird|baby_chick|hatched_chick|hatching_chick|chicken|penguin|turtle|bug|honeybee|ant|beetle|snail|octopus|tropical_fish|fish|whale|whale2|dolphin|cow2|ram|rat|water_buffalo|tiger2|rabbit2|dragon|goat|rooster|dog2|pig2|mouse2|ox|dragon_face|blowfish|crocodile|dromedary_camel|leopard|cat2|poodle|paw_prints|bouquet|cherry_blossom|tulip|four_leaf_clover|rose|sunflower|hibiscus|maple_leaf|leaves|fallen_leaf|herb|mushroom|cactus|palm_tree|evergreen_tree|deciduous_tree|chestnut|seedling|blossom|ear_of_rice|shell|globe_with_meridians|sun_with_face|full_moon_with_face|new_moon_with_face|new_moon|waxing_crescent_moon|first_quarter_moon|waxing_gibbous_moon|full_moon|waning_gibbous_moon|last_quarter_moon|waning_crescent_moon|last_quarter_moon_with_face|first_quarter_moon_with_face|moon|earth_africa|earth_americas|earth_asia|volcano|milky_way|partly_sunny|octocat|squirrel|bamboo|gift_heart|dolls|school_satchel|mortar_board|flags|fireworks|sparkler|wind_chime|rice_scene|jack_o_lantern|ghost|santa|christmas_tree|gift|bell|no_bell|tanabata_tree|tada|confetti_ball|balloon|crystal_ball|cd|dvd|floppy_disk|camera|video_camera|movie_camera|computer|tv|iphone|phone|telephone|telephone_receiver|pager|fax|minidisc|vhs|sound|speaker|mute|loudspeaker|mega|hourglass|hourglass_flowing_sand|alarm_clock|watch|radio|satellite|loop|mag|mag_right|unlock|lock|lock_with_ink_pen|closed_lock_with_key|key|bulb|flashlight|high_brightness|low_brightness|electric_plug|battery|calling|email|mailbox|postbox|bath|bathtub|shower|toilet|wrench|nut_and_bolt|hammer|seat|moneybag|yen|dollar|pound|euro|credit_card|money_with_wings|e-mail|inbox_tray|outbox_tray|envelope|incoming_envelope|postal_horn|mailbox_closed|mailbox_with_mail|mailbox_with_no_mail|door|smoking|bomb|gun|hocho|pill|syringe|page_facing_up|page_with_curl|bookmark_tabs|bar_chart|chart_with_upwards_trend|chart_with_downwards_trend|scroll|clipboard|calendar|date|card_index|file_folder|open_file_folder|scissors|pushpin|paperclip|black_nib|pencil2|straight_ruler|triangular_ruler|closed_book|green_book|blue_book|orange_book|notebook|notebook_with_decorative_cover|ledger|books|bookmark|name_badge|microscope|telescope|newspaper|football|basketball|soccer|baseball|tennis|8ball|rugby_football|bowling|golf|mountain_bicyclist|bicyclist|horse_racing|snowboarder|swimmer|surfer|ski|spades|hearts|clubs|diamonds|gem|ring|trophy|musical_score|musical_keyboard|violin|space_invader|video_game|black_joker|flower_playing_cards|game_die|dart|mahjong|clapper|memo|pencil|book|art|microphone|headphones|trumpet|saxophone|guitar|shoe|sandal|high_heel|lipstick|boot|shirt|tshirt|necktie|womans_clothes|dress|running_shirt_with_sash|jeans|kimono|bikini|ribbon|tophat|crown|womans_hat|mans_shoe|closed_umbrella|briefcase|handbag|pouch|purse|eyeglasses|fishing_pole_and_fish|coffee|tea|sake|baby_bottle|beer|beers|cocktail|tropical_drink|wine_glass|fork_and_knife|pizza|hamburger|fries|poultry_leg|meat_on_bone|spaghetti|curry|fried_shrimp|bento|sushi|fish_cake|rice_ball|rice_cracker|rice|ramen|stew|oden|dango|egg|bread|doughnut|custard|icecream|ice_cream|shaved_ice|birthday|cake|cookie|chocolate_bar|candy|lollipop|honey_pot|apple|green_apple|tangerine|lemon|cherries|grapes|watermelon|strawberry|peach|melon|banana|pear|pineapple|sweet_potato|eggplant|tomato|corn|house|house_with_garden|school|office|post_office|hospital|bank|convenience_store|love_hotel|hotel|wedding|church|department_store|european_post_office|city_sunrise|city_sunset|japanese_castle|european_castle|tent|factory|tokyo_tower|japan|mount_fuji|sunrise_over_mountains|sunrise|stars|statue_of_liberty|bridge_at_night|carousel_horse|rainbow|ferris_wheel|fountain|roller_coaster|ship|speedboat|boat|sailboat|rowboat|anchor|rocket|airplane|helicopter|steam_locomotive|tram|mountain_railway|bike|aerial_tramway|suspension_railway|mountain_cableway|tractor|blue_car|oncoming_automobile|car|red_car|taxi|oncoming_taxi|articulated_lorry|bus|oncoming_bus|rotating_light|police_car|oncoming_police_car|fire_engine|ambulance|minibus|truck|train|station|train2|bullettrain_front|bullettrain_side|light_rail|monorail|railway_car|trolleybus|ticket|fuelpump|vertical_traffic_light|traffic_light|warning|construction|beginner|atm|slot_machine|busstop|barber|hotsprings|checkered_flag|crossed_flags|izakaya_lantern|moyai|circus_tent|performing_arts|round_pushpin|triangular_flag_on_post|jp|kr|cn|us|fr|es|it|ru|gb|uk|de|one|two|three|four|five|six|seven|eight|nine|keycap_ten|1234|zero|hash|symbols|arrow_backward|arrow_down|arrow_forward|arrow_left|capital_abcd|abcd|abc|arrow_lower_left|arrow_lower_right|arrow_right|arrow_up|arrow_upper_left|arrow_upper_right|arrow_double_down|arrow_double_up|arrow_down_small|arrow_heading_down|arrow_heading_up|leftwards_arrow_with_hook|arrow_right_hook|left_right_arrow|arrow_up_down|arrow_up_small|arrows_clockwise|arrows_counterclockwise|rewind|fast_forward|information_source|ok|twisted_rightwards_arrows|repeat|repeat_one|new|top|up|cool|free|ng|cinema|koko|signal_strength|u5272|u5408|u55b6|u6307|u6708|u6709|u6e80|u7121|u7533|u7a7a|u7981|sa|restroom|mens|womens|baby_symbol|no_smoking|parking|wheelchair|metro|baggage_claim|accept|wc|potable_water|put_litter_in_its_place|secret|congratulations|m|passport_control|left_luggage|customs|ideograph_advantage|cl|sos|id|no_entry_sign|underage|no_mobile_phones|do_not_litter|non-potable_water|no_bicycles|no_pedestrians|children_crossing|no_entry|eight_spoked_asterisk|eight_pointed_black_star|heart_decoration|vs|vibration_mode|mobile_phone_off|chart|currency_exchange|aries|taurus|gemini|cancer|leo|virgo|libra|scorpius|sagittarius|capricorn|aquarius|pisces|ophiuchus|six_pointed_star|negative_squared_cross_mark|a|b|ab|o2|diamond_shape_with_a_dot_inside|recycle|end|on|soon|clock1|clock130|clock10|clock1030|clock11|clock1130|clock12|clock1230|clock2|clock230|clock3|clock330|clock4|clock430|clock5|clock530|clock6|clock630|clock7|clock730|clock8|clock830|clock9|clock930|heavy_dollar_sign|copyright|registered|tm|x|heavy_exclamation_mark|bangbang|interrobang|o|heavy_multiplication_x|heavy_plus_sign|heavy_minus_sign|heavy_division_sign|white_flower|100|heavy_check_mark|ballot_box_with_check|radio_button|link|curly_loop|wavy_dash|part_alternation_mark|trident|black_square|white_square|white_check_mark|black_square_button|white_square_button|black_circle|white_circle|red_circle|large_blue_circle|large_blue_diamond|large_orange_diamond|small_blue_diamond|small_orange_diamond|small_red_triangle|small_red_triangle_down|shipit):/i', ':​\1:', $text); } function add_tailing_dot(string $sentence): string { if (empty($sentence)) { return $sentence; } if (str_ends_with($sentence, '.')) { return $sentence; } return $sentence . '.'; } function recipe_to_md_link(string $recipe): string { $md = php_to_md($recipe); $basename = basename($recipe, '.php'); return "[$basename](/docs/$md)"; } function is_framework_recipe(DocRecipe $recipe): bool { return preg_match('/recipe\/[\w_\d]+\.php$/', $recipe->recipePath) && !in_array($recipe->recipeName, ['common', 'composer', 'provision'], true); } function framework_brand_name(string $brandName): string { $brandName = preg_replace('/(\w+)(\d)/', '$1 $2', $brandName); $brandName = preg_replace('/typo 3/', 'TYPO3', $brandName); $brandName = preg_replace('/yii/', 'Yii2', $brandName); $brandName = preg_replace('/wordpress/', 'WordPress', $brandName); $brandName = preg_replace('/_/', ' ', $brandName); $brandName = preg_replace('/framework/', 'Framework', $brandName); return ucfirst($brandName); } ================================================ FILE: src/Documentation/DocRecipe.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Documentation; class DocRecipe { /** * @var string */ public $recipeName; /** * @var string */ public $recipePath; /** * @var string */ public $comment; /** * @var string[] */ public $require = []; /** * @var DocConfig[] */ public $config = []; /** * @var DocTask[] */ public $tasks = []; public function __construct(string $recipeName, string $recipePath) { $this->recipeName = $recipeName; $this->recipePath = $recipePath; } /** * @return bool|int */ public function parse(string $content) { $comment = ''; $desc = ''; $currentTask = null; $content = str_replace("\r\n", "\n", $content); $state = 'root'; $lines = explode("\n", $content); for ($i = 0; $i < count($lines); $i++) { $line = $lines[$i]; if (empty($line)) { continue; // Skip empty lines } $m = []; $match = function ($regexp) use ($line, &$m) { return preg_match("#$regexp#", $line, $m); }; switch ($state) { case 'root': if ($match('^/\*\*?')) { $state = 'comment'; $comment .= trim_comment($line) . "\n"; break; } if ($match('^//')) { $comment .= trim_comment($line) . "\n"; break; } if ($match('^require.+?[\'"](?.+?)[\'"]')) { $this->require[] = dirname($this->recipePath) . $m['recipe']; break; } if ($match('^set\([\'"](?[\w_:\-/]+?)[\'"]')) { $set = new DocConfig(); $set->name = $m['config_name']; $set->comment = trim($comment); $comment = ''; $set->recipePath = $this->recipePath; $set->lineNumber = $i + 1; if (preg_match('#^set\(.+?,\s(?.+?)\);$#', $line, $m)) { $set->defaultValue = $m['value']; } if (preg_match('#^set\(.+?,\s\[$#', $line, $m)) { $multiLineArray = "[\n"; $line = $lines[++$i]; while (!preg_match('/^]/', $line)) { $multiLineArray .= $line . "\n"; $line = $lines[++$i]; } $multiLineArray .= "]"; $set->defaultValue = $multiLineArray; } if (preg_match('/^set\(.+?, function/', $line, $m)) { $body = []; $line = $lines[++$i]; while (!preg_match('/^}\);$/', $line)) { $body[] = trim($line); $line = $lines[++$i]; } if (count($body) === 1 && preg_match('/throw new/', $body[0])) { $set->comment .= "\n:::info Required\nThrows exception if not set.\n:::\n"; } elseif (count($body) <= 4) { $set->defaultValue = implode("\n", $body); } else { $set->comment .= "\n:::info Autogenerated\nThe value of this configuration is autogenerated on access.\n:::\n"; } } $this->config[$set->name] = $set; break; } if ($match('^desc\([\'"](?.+?)[\'"]\);$')) { $desc = $m['desc']; break; } if ($match('^task\([\'"](?[\w_:-]+?)[\'"],\s\[$')) { $task = new DocTask(); $task->name = $m['task_name']; $task->desc = $desc; $task->comment = trim($comment); $comment = ''; $task->group = []; $task->recipePath = $this->recipePath; $task->lineNumber = $i + 1; $this->tasks[$task->name] = $task; $state = 'group_task'; $currentTask = $task; break; } if ($match('^task\([\'"](?[\w_:-]+?)[\'"],')) { $task = new DocTask(); $task->name = $m['task_name']; $task->desc = $desc; $task->comment = trim($comment); $comment = ''; $task->recipePath = $this->recipePath; $task->lineNumber = $i + 1; $this->tasks[$task->name] = $task; break; } if ($match('^<\?php')) { break; } if ($match('^namespace Deployer;$')) { $this->comment = $comment; break; } $desc = ''; $comment = ''; break; case 'comment': if ($match('\*/\s*$')) { $state = 'root'; break; } $comment .= trim_comment($line) . "\n"; break; case 'group_task': if ($match('^\s+\'(?[\w_:-]+?)\',$')) { $currentTask->group[] = $m['task_name']; break; } $state = 'root'; break; } } return false; } } ================================================ FILE: src/Documentation/DocTask.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Documentation; class DocTask { /** * @var string */ public $name; /** * @var string */ public $desc; /** * @var string */ public $comment; /** * @var array */ public $group; /** * @var string */ public $recipePath; /** * @var int */ public $lineNumber; public function mdLink(): string { $md = php_to_md($this->recipePath); $anchor = anchor($this->name); return "[$this->name](/docs/$md#$anchor)"; } } ================================================ FILE: src/Exception/ConfigurationException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Exception; class ConfigurationException extends \RuntimeException {} ================================================ FILE: src/Exception/Exception.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Exception; use Throwable; class Exception extends \Exception { /** * @var string */ private static $taskSourceLocation = ''; /** * @var string */ private $taskFilename = ''; /** * @var int|mixed */ private $taskLineNumber = 0; public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null) { if (function_exists('debug_backtrace')) { $trace = debug_backtrace(); foreach ($trace as $t) { if (!empty($t['file']) && $t['file'] === self::$taskSourceLocation) { $this->taskFilename = basename($t['file']); $this->taskLineNumber = $t['line']; break; } } } parent::__construct($message, $code, $previous); } public static function setTaskSourceLocation(string $filepath): void { self::$taskSourceLocation = $filepath; } public function getTaskFilename(): string { return $this->taskFilename; } public function getTaskLineNumber(): int { return $this->taskLineNumber; } public function setTaskFilename(string $taskFilename): void { $this->taskFilename = $taskFilename; } public function setTaskLineNumber(int $taskLineNumber): void { $this->taskLineNumber = $taskLineNumber; } } ================================================ FILE: src/Exception/GracefulShutdownException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Exception; /** * Then this exception thrown, it will not trigger "fail" callback. * * fail('deploy', 'deploy:failed'); * * task('deploy', function () { * throw new GracefulShutdownException(...); * }); * * In example above task `deploy:failed` will not be called. */ class GracefulShutdownException extends Exception { public const EXIT_CODE = 42; } ================================================ FILE: src/Exception/HttpieException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Exception; class HttpieException extends \RuntimeException {} ================================================ FILE: src/Exception/RunException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Exception; use Deployer\Host\Host; use Symfony\Component\Process\Process; class RunException extends Exception { /** * @var Host */ private $host; /** * @var string */ private $command; /** * @var int */ private $exitCode; /** * @var string */ private $output; /** * @var string */ private $errorOutput; public function __construct( Host $host, string $command, int $exitCode, string $output, string $errorOutput, ) { $this->host = $host; $this->command = $command; $this->exitCode = $exitCode; $this->output = $output; $this->errorOutput = $errorOutput; $message = sprintf('The command "%s" failed.', $command); parent::__construct($message, $exitCode); } public function getHost(): Host { return $this->host; } public function getCommand(): string { return $this->command; } public function getExitCode(): int { return $this->exitCode; } public function getExitCodeText(): string { return Process::$exitCodes[$this->exitCode] ?? 'Unknown error'; } public function getOutput(): string { return $this->output; } public function getErrorOutput(): string { return $this->errorOutput; } } ================================================ FILE: src/Exception/TimeoutException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Exception; class TimeoutException extends Exception { public function __construct( string $command, ?float $timeout, ) { $message = sprintf('The command "%s" exceeded the timeout of %s seconds.', $command, $timeout); parent::__construct($message, 1); } } ================================================ FILE: src/Exception/WillAskUser.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Exception; class WillAskUser extends Exception { public function __construct(string $message) { parent::__construct($message); } } ================================================ FILE: src/Executor/Master.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Executor; use Deployer\Deployer; use Deployer\Host\Host; use Deployer\Host\HostCollection; use Deployer\Selector\Selector; use Deployer\Ssh\IOArguments; use Deployer\Task\Context; use Deployer\Task\Task; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; function spinner(string $message = ''): string { $frame = FRAMES[(int) ((int) (new \DateTime())->format('u') / 1e5) % count(FRAMES)]; return " $frame $message\r"; } class Master { private HostCollection $hosts; private InputInterface $input; private OutputInterface $output; private Messenger $messenger; private string|false $phpBin; public function __construct( HostCollection $hosts, InputInterface $input, OutputInterface $output, Messenger $messenger, ) { $this->hosts = $hosts; $this->input = $input; $this->output = $output; $this->messenger = $messenger; $this->phpBin = (new PhpExecutableFinder())->find(); } /** * @param Task[] $tasks * @param Host[] $hosts */ public function run(array $tasks, array $hosts, ?Planner $plan = null): int { $globalLimit = (int) $this->input->getOption('limit') ?: count($hosts); foreach ($tasks as $task) { if (!$plan) { $this->messenger->startTask($task); } $plannedHosts = $hosts; $limit = min($globalLimit, $task->getLimit() ?? $globalLimit); if ($task->isOnce()) { $plannedHosts = []; foreach ($hosts as $currentHost) { if (Selector::apply($task->getSelector(), $currentHost)) { $plannedHosts[] = $currentHost; break; } } } elseif ($task->isOncePerNode()) { $plannedHosts = []; foreach ($hosts as $currentHost) { if (Selector::apply($task->getSelector(), $currentHost)) { $nodeLabel = $currentHost->getHostname(); $labels = $currentHost->config()->get('labels', []); if (is_array($labels) && array_key_exists('node', $labels)) { $nodeLabel = $labels['node']; } if (array_key_exists($nodeLabel, $plannedHosts)) { continue; } $plannedHosts[$nodeLabel] = $currentHost; } } } if ($limit === 1 || count($plannedHosts) === 1) { foreach ($plannedHosts as $currentHost) { if (!Selector::apply($task->getSelector(), $currentHost)) { if ($plan) { $plan->commit([], $task); } continue; } if ($plan) { $plan->commit([$currentHost], $task); continue; } $exitCode = $this->runTask($task, [$currentHost]); if ($exitCode !== 0) { return $exitCode; } } } else { foreach (array_chunk($plannedHosts, $limit) as $chunk) { $selectedHosts = []; foreach ($chunk as $currentHost) { if (Selector::apply($task->getSelector(), $currentHost)) { $selectedHosts[] = $currentHost; } } if ($plan) { $plan->commit($selectedHosts, $task); continue; } $exitCode = $this->runTask($task, $selectedHosts); if ($exitCode !== 0) { return $exitCode; } } } if (!$plan) { $this->messenger->endTask($task); } } return 0; } /** * @param Host[] $hosts */ private function runTask(Task $task, array $hosts): int { if (getenv('DEPLOYER_LOCAL_WORKER') === 'true') { // This allows to code coverage all recipe, // as well as speedup tests by not spawning // lots of processes. Also there is a few tests // what runs with workers for tests subprocess // communications. foreach ($hosts as $host) { $worker = new Worker(Deployer::get()); $exitCode = $worker->execute($task, $host); if ($exitCode !== 0) { $this->messenger->endTask($task, true); return $exitCode; } } return 0; } $server = new Server('127.0.0.1', 0, $this->output); /** @var Process[] $processes */ $processes = []; $server->afterRun(function (int $port) use (&$processes, $hosts, $task) { foreach ($hosts as $host) { $processes[] = $this->createProcess($host, $task, $port); } foreach ($processes as $process) { $process->start(); } }); $echoCallback = function (string $output) { $output = preg_replace('/\n$/', '', $output); if (strlen($output) !== 0) { $this->output->writeln($output); } }; $server->ticker(function () use (&$processes, $server, $echoCallback) { $this->gatherOutput($processes, $echoCallback); if ($this->output->isDecorated() && !getenv('CI')) { $this->output->write(spinner()); } if ($this->allFinished($processes)) { $server->stop(); } }); $server->router(function (string $path, array $payload) { switch ($path) { case '/load': ['host' => $host] = $payload; $host = $this->hosts->get($host); $config = $host->config()->persist(); return new Response(200, $config); case '/save': ['host' => $host, 'config' => $config] = $payload; $host = $this->hosts->get($host); $host->config()->update($config); return new Response(200, true); case '/proxy': ['host' => $host, 'func' => $func, 'arguments' => $arguments] = $payload; Context::push(new Context($this->hosts->get($host))); $answer = call_user_func($func, ...$arguments); Context::pop(); return new Response(200, $answer); default: return new Response(404, null); } }); $server->run(); if ($this->output->isDecorated() && !getenv('CI')) { $this->output->write(" \r"); // clear spinner } $this->gatherOutput($processes, $echoCallback); if ($this->cumulativeExitCode($processes) !== 0) { $this->messenger->endTask($task, true); } return $this->cumulativeExitCode($processes); } protected function createProcess(Host $host, Task $task, int $port): Process { $command = [ $this->phpBin, DEPLOYER_BIN, 'worker', '--port', $port, '--task', $task, '--host', $host->getAlias(), ]; $command = array_merge($command, IOArguments::collect($this->input, $this->output)); if ($task->isVerbose() && $this->output->getVerbosity() === OutputInterface::VERBOSITY_NORMAL) { $command[] = '-v'; } if ($this->output->isDebug()) { $this->output->writeln("[$host] " . join(' ', $command)); } return new Process($command); } /** * @param Process[] $processes */ protected function allFinished(array $processes): bool { foreach ($processes as $process) { if (!$process->isTerminated()) { return false; } } return true; } /** * @param Process[] $processes */ protected function gatherOutput(array $processes, callable $callback): void { foreach ($processes as $process) { $output = $process->getIncrementalOutput(); if (strlen($output) !== 0) { $callback($output); } $errorOutput = $process->getIncrementalErrorOutput(); if (strlen($errorOutput) !== 0) { $callback($errorOutput); } } } /** * @param Process[] $processes */ protected function cumulativeExitCode(array $processes): int { foreach ($processes as $process) { if ($process->getExitCode() > 0) { return $process->getExitCode(); } } return 0; } } ================================================ FILE: src/Executor/Messenger.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Executor; use Deployer\Exception\Exception; use Deployer\Exception\RunException; use Deployer\Host\Host; use Deployer\Logger\Logger; use Deployer\Task\Task; use Symfony\Component\Console\Input\Input; use Symfony\Component\Console\Output\Output; use Throwable; class Messenger { /** * @var Input */ private $input; /** * @var Output */ private $output; /** * @var Logger */ private $logger; /** * @var int|double */ private $startTime; public function __construct(Input $input, Output $output, Logger $logger) { $this->input = $input; $this->output = $output; $this->logger = $logger; } public function startTask(Task $task): void { $this->startTime = round(microtime(true) * 1000); if (getenv('GITHUB_WORKFLOW')) { $this->output->writeln("::group::task {$task->getName()}"); } elseif (getenv('GITLAB_CI')) { $sectionId = md5($task->getName()); $start = round($this->startTime / 1000); $this->output->writeln("\e[0Ksection_start:{$start}:{$sectionId}\r\e[0K{$task->getName()}"); } else { $this->output->writeln("task {$task->getName()}"); } $this->logger->log("task {$task->getName()}"); } /* * Print task was ok. */ public function endTask(Task $task, bool $error = false): void { if (empty($this->startTime)) { $this->startTime = round(microtime(true) * 1000); } $endTime = round(microtime(true) * 1000); $millis = $endTime - $this->startTime; $seconds = floor($millis / 1000); $millis = $millis - $seconds * 1000; $taskTime = ($seconds > 0 ? "{$seconds}s " : "") . "{$millis}ms"; if (getenv('GITHUB_WORKFLOW')) { $this->output->writeln("::endgroup::"); } elseif (getenv('GITLAB_CI')) { $sectionId = md5($task->getName()); $endTime = round($endTime / 1000); $this->output->writeln("\e[0Ksection_end:{$endTime}:{$sectionId}\r\e[0K"); } elseif ($this->output->isVeryVerbose()) { $this->output->writeln("done {$task->getName()} $taskTime"); } if ($error) { $this->output->writeln("\e[0K\e[31;1mERROR: Task {$task->getName()} failed!\e[0;m"); return; } $this->logger->log("done {$task->getName()} $taskTime"); if (!empty($this->input->getOption('profile'))) { $line = sprintf("%s\t%s\n", $task->getName(), $taskTime); file_put_contents($this->input->getOption('profile'), $line, FILE_APPEND); } } public function endOnHost(Host $host): void { if ($this->output->isVeryVerbose()) { $this->output->writeln("done on $host"); } } public function renderException(Throwable $exception, Host $host): void { if ($exception instanceof RunException) { $message = ""; $message .= "[$host] error in {$exception->getTaskFilename()} on line {$exception->getTaskLineNumber()}:\n"; if ($this->output->getVerbosity() === Output::VERBOSITY_NORMAL) { $message .= "[$host] run {$exception->getCommand()}\n"; foreach (explode("\n", $exception->getErrorOutput()) as $line) { $line = trim($line); if ($line !== "") { $message .= "[$host] err $line\n"; } } foreach (explode("\n", $exception->getOutput()) as $line) { $line = trim($line); if ($line !== "") { $message .= "[$host] $line\n"; } } } $message .= "[$host] exit code {$exception->getExitCode()} ({$exception->getExitCodeText()})\n"; $this->output->write($message); } else { $message = ""; $class = get_class($exception); $file = basename($exception->getFile()); $line = $exception->getLine(); if ($exception instanceof Exception) { $file = $exception->getTaskFilename(); $line = $exception->getTaskLineNumber(); } $message .= "[$host] $class in $file on line $line:\n"; $message .= "[$host]\n"; foreach (explode("\n", $exception->getMessage()) as $line) { $line = trim($line); if ($line !== "") { $message .= "[$host] $line\n"; } } $message .= "[$host]\n"; if ($this->output->isDebug()) { foreach (explode("\n", $exception->getTraceAsString()) as $line) { $line = trim($line); if ($line !== "") { $message .= "[$host] $line\n"; } } } $this->output->write($message); } $this->logger->log($exception->__toString()); if ($exception->getPrevious()) { $this->renderException($exception->getPrevious(), $host); } } } ================================================ FILE: src/Executor/Planner.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Executor; use Deployer\Host\Host; use Deployer\Task\Task; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Output\OutputInterface; class Planner { /** * @var Table */ private $table; /** * @var array */ private $template; /** * Planner constructor. * * @param Host[] $hosts */ public function __construct(OutputInterface $output, array $hosts) { $headers = []; $this->template = []; foreach ($hosts as $host) { $headers[] = $host->getTag(); $this->template[] = $host->getAlias(); } $this->table = new Table($output); $this->table->setHeaders($headers); $this->table->setStyle('box'); } /** * @param Host[] $hosts */ public function commit(array $hosts, Task $task): void { $row = []; foreach ($this->template as $alias) { $on = "-"; foreach ($hosts as $host) { if ($alias === $host->getAlias()) { $on = $task->getName(); break; } } $row[] = $on; } $this->table->addRow($row); } public function render() { $this->table->render(); } } ================================================ FILE: src/Executor/Response.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Executor; class Response { private int $status; private mixed $body; public function __construct(int $status, mixed $body) { $this->status = $status; $this->body = $body; } public function getStatus(): int { return $this->status; } public function getBody(): mixed { return $this->body; } } ================================================ FILE: src/Executor/Server.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Executor; use Closure; use Deployer\Exception\Exception; use Symfony\Component\Console\Output\OutputInterface; class Server { private string $host; private int $port; private OutputInterface $output; private bool $stop = false; /** * @var ?resource */ private $socket; /** * @var resource[] */ private array $clientSockets = []; private Closure $afterCallback; private Closure $tickerCallback; private Closure $routerCallback; public function __construct($host, $port, OutputInterface $output) { self::checkRequiredExtensionsExists(); $this->host = $host; $this->port = $port; $this->output = $output; } public static function checkRequiredExtensionsExists(): void { if (!function_exists('socket_import_stream')) { throw new Exception('Required PHP extension "sockets" is not loaded'); } if (!function_exists('stream_set_blocking')) { throw new Exception('Required PHP extension "stream" is not loaded'); } } public function run(): void { try { $this->socket = $this->createServerSocket(); $this->updatePort(); if ($this->output->isDebug()) { $this->output->writeln("[master] Starting server at http://{$this->host}:{$this->port}"); } ($this->afterCallback)($this->port); while (true) { $this->acceptNewConnections(); $this->handleClientRequests(); // Prevent CPU exhaustion and 60fps ticker. usleep(16_000); // 16ms ($this->tickerCallback)(); if ($this->stop) { break; } } if ($this->output->isDebug()) { $this->output->writeln("[master] Stopping server at http://{$this->host}:{$this->port}"); } } finally { if (isset($this->socket)) { fclose($this->socket); } } } /** * @return resource * @throws Exception */ private function createServerSocket() { $server = stream_socket_server("tcp://{$this->host}:{$this->port}", $errno, $errstr); if (!$server) { throw new Exception("Socket creation failed: $errstr ($errno)"); } if (!stream_set_blocking($server, false)) { throw new Exception("Failed to set server socket to non-blocking mode"); } return $server; } private function updatePort(): void { $name = stream_socket_get_name($this->socket, false); if ($name) { list(, $port) = explode(':', $name); $this->port = (int) $port; } else { throw new Exception("Failed to get the assigned port"); } } private function acceptNewConnections(): void { $newClientSocket = @stream_socket_accept($this->socket, 0); if ($newClientSocket) { if (!stream_set_blocking($newClientSocket, false)) { throw new Exception("Failed to set client socket to non-blocking mode"); } $this->clientSockets[] = $newClientSocket; } } private function handleClientRequests(): void { foreach ($this->clientSockets as $key => $clientSocket) { if (feof($clientSocket)) { $this->closeClientSocket($clientSocket, $key); continue; } $request = $this->readClientRequest($clientSocket); list($path, $payload) = $this->parseRequest($request); $response = ($this->routerCallback)($path, $payload); $this->sendResponse($clientSocket, $response); $this->closeClientSocket($clientSocket, $key); } } private function readClientRequest($clientSocket) { $request = stream_get_contents($clientSocket); if ($request === false) { throw new Exception('Socket read failed'); } return $request; } private function parseRequest($request) { $lines = explode("\r\n", $request); $requestLine = $lines[0]; $parts = explode(' ', $requestLine); if (count($parts) !== 3) { throw new Exception("Malformed request line: $requestLine"); } $path = $parts[1]; $headers = []; for ($i = 1; $i < count($lines); $i++) { $line = $lines[$i]; if (empty($line)) { break; } [$key, $value] = explode(':', $line, 2); $headers[$key] = trim($value); } if (empty($headers['Content-Type']) || $headers['Content-Type'] !== 'application/json') { throw new Exception("Malformed request: invalid Content-Type"); } $payload = json_decode(implode("\n", array_slice($lines, $i + 1)), true, flags: JSON_THROW_ON_ERROR); return [$path, $payload]; } private function sendResponse($clientSocket, Response $response) { $code = $response->getStatus(); $content = json_encode($response->getBody(), flags: JSON_PRETTY_PRINT); $headers = "HTTP/1.1 $code OK\r\n" . "Content-Type: application/json\r\n" . "Content-Length: " . strlen($content) . "\r\n" . "Connection: close\r\n\r\n"; fwrite($clientSocket, $headers . $content); } private function closeClientSocket($clientSocket, $key): void { fclose($clientSocket); unset($this->clientSockets[$key]); } public function afterRun(Closure $param): void { $this->afterCallback = $param; } public function ticker(Closure $param): void { $this->tickerCallback = $param; } public function router(Closure $param) { $this->routerCallback = $param; } public function stop(): void { $this->stop = true; } } ================================================ FILE: src/Executor/Worker.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Executor; use Deployer\Deployer; use Deployer\Exception\Exception; use Deployer\Exception\GracefulShutdownException; use Deployer\Exception\RunException; use Deployer\Host\Host; use Deployer\Task\Context; use Deployer\Task\Task; use Throwable; class Worker { private Deployer $deployer; public function __construct(Deployer $deployer) { $this->deployer = $deployer; } public function execute(Task $task, Host $host): int { try { Exception::setTaskSourceLocation($task->getSourceLocation()); $context = new Context($host); $task->run($context); if ($task->getName() !== 'connect') { $this->deployer->messenger->endOnHost($host); } return 0; } catch (Throwable $e) { $this->deployer->messenger->renderException($e, $host); if ($e instanceof GracefulShutdownException) { return GracefulShutdownException::EXIT_CODE; } if ($e instanceof RunException) { return $e->getExitCode(); } return 255; } } } ================================================ FILE: src/Host/Host.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Host; use Deployer\Configuration; use Deployer\Deployer; use Deployer\Exception\ConfigurationException; use Deployer\Exception\Exception; use Deployer\Task\Context; use function Deployer\Support\colorize_host; use function Deployer\Support\parse_home_dir; class Host { /** * @var Configuration $config */ private $config; public function __construct(string $hostname) { $parent = null; if (Deployer::get()) { $parent = Deployer::get()->config; } $this->config = new Configuration($parent); $this->set('#alias', $hostname); $this->set('hostname', preg_replace('/\/.+$/', '', $hostname)); } public function __toString(): string { return $this->getTag(); } public function config(): Configuration { return $this->config; } /** * @param mixed $value */ public function set(string $name, $value): self { if ($name === 'alias') { throw new ConfigurationException("Can not update alias of the host.\nThis will change only host own alias,\nbut not the key it is stored in HostCollection."); } if ($name === '#alias') { $name = 'alias'; } $this->config->set($name, $value); return $this; } public function add(string $name, array $value): self { $this->config->add($name, $value); return $this; } public function has(string $name): bool { return $this->config->has($name); } public function hasOwn(string $name): bool { return $this->config->hasOwn($name); } /** * @param mixed|null $default * @return mixed|null */ public function get(string $name, $default = null) { return $this->config->get($name, $default); } public function getAlias(): ?string { return $this->config->get('alias', null); } public function setTag(string $tag): self { $this->config->set('tag', $tag); return $this; } public function getTag(): ?string { return $this->config->get('tag', colorize_host($this->getAlias())); } public function setHostname(string $hostname): self { $this->config->set('hostname', $hostname); return $this; } public function getHostname(): ?string { return $this->config->get('hostname', null); } public function setRemoteUser(string $user): self { $this->config->set('remote_user', $user); return $this; } public function getRemoteUser(): ?string { return $this->config->get('remote_user', null); } /** * @param string|int|null $port * @return $this */ public function setPort($port): self { $this->config->set('port', $port); return $this; } /** * @return string|int|null */ public function getPort() { return $this->config->get('port', null); } public function setConfigFile(string $file): self { $this->config->set('config_file', $file); return $this; } public function getConfigFile(): ?string { return $this->config->get('config_file', null); } public function setIdentityFile(string $file): self { $this->config->set('identity_file', $file); return $this; } public function getIdentityFile(): ?string { return $this->config->get('identity_file', null); } public function setForwardAgent(bool $on): self { $this->config->set('forward_agent', $on); return $this; } public function getForwardAgent(): ?bool { return $this->config->get('forward_agent', null); } public function setSshMultiplexing(bool $on): self { $this->config->set('ssh_multiplexing', $on); return $this; } public function getSshMultiplexing(): ?bool { return $this->config->get('ssh_multiplexing', null); } public function setShell(string $command): self { $this->config->set('shell', $command); return $this; } public function getShell(): ?string { return $this->config->get('shell', null); } public function setShellPath(string $path): self { $this->config->set('shell_path', $path); return $this; } public function getShellPath(): ?string { return $this->config->get('shell_path', null); } public function setDeployPath(string $path): self { $this->config->set('deploy_path', $path); return $this; } public function getDeployPath(): ?string { return $this->config->get('deploy_path', null); } public function setLabels(array $labels): self { $this->config->set('labels', $labels); return $this; } public function addLabels(array $labels): self { $existingLabels = $this->getLabels() ?? []; $this->setLabels(array_replace_recursive($existingLabels, $labels)); return $this; } public function getLabels(): ?array { return $this->config->get('labels', null); } public function setSshArguments(array $args): self { $this->config->set('ssh_arguments', $args); return $this; } public function getSshArguments(): ?array { return $this->config->get('ssh_arguments', null); } public function setSshControlPath(string $path): self { $this->config->set('ssh_control_path', $path); return $this; } public function getSshControlPath(): string { return $this->config->get('ssh_control_path', $this->generateControlPath()); } private function generateControlPath(): string { $C = $this->getHostname(); if ($this->has('remote_user')) { $C = $this->getRemoteUser() . '@' . $C; } if ($this->has('port')) { $C .= ':' . $this->getPort(); } // In case of CI environment, lets use shared memory. if (getenv('CI') && is_writable('/dev/shm')) { return "/dev/shm/$C"; } return "~/.ssh/$C"; } public function connectionString(): string { if ($this->get('remote_user', '') !== '') { return $this->get('remote_user') . '@' . $this->get('hostname'); } return $this->get('hostname'); } public function connectionOptionsString(): string { return implode(' ', array_map('escapeshellarg', $this->connectionOptionsArray())); } /** * @return string[] */ public function connectionOptionsArray(): array { $options = []; if ($this->has('ssh_arguments')) { foreach ($this->getSshArguments() as $arg) { $options = array_merge($options, explode(' ', $arg)); } } if ($this->has('port')) { $options = array_merge($options, ['-p', $this->getPort()]); } if ($this->has('config_file')) { $options = array_merge($options, ['-F', parse_home_dir($this->getConfigFile())]); } if ($this->has('identity_file')) { $options = array_merge($options, ['-i', parse_home_dir($this->getIdentityFile())]); } if ($this->has('forward_agent') && $this->getForwardAgent()) { $options = array_merge($options, ['-A']); } if ($this->has('ssh_multiplexing') && $this->getSshMultiplexing()) { $options = array_merge($options, [ '-o', 'ControlMaster=auto', '-o', 'ControlPersist=60', '-o', 'ControlPath=' . $this->getSshControlPath(), ]); } return $options; } } ================================================ FILE: src/Host/HostCollection.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Host; use Deployer\Collection\Collection; /** * @method Host get($name) * @method Host[] getIterator() */ class HostCollection extends Collection { protected function notFound(string $name): \InvalidArgumentException { return new \InvalidArgumentException("Host \"$name\" not found."); } } ================================================ FILE: src/Host/Localhost.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Host; class Localhost extends Host { public function __construct(string $hostname = 'localhost') { parent::__construct($hostname); } } ================================================ FILE: src/Host/Range.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Host; class Range { public const PATTERN = '/\[(.+?)\]/'; public static function expand(array $hostnames): array { $expanded = []; foreach ($hostnames as $hostname) { if (preg_match(self::PATTERN, $hostname, $matches)) { [$start, $end] = explode(':', $matches[1]); $zeroBased = (bool) preg_match('/^0[1-9]/', $start); foreach (range($start, $end) as $i) { $expanded[] = preg_replace(self::PATTERN, self::format((string) $i, $zeroBased), $hostname); } } else { $expanded[] = $hostname; } } return $expanded; } private static function format(string $i, bool $zeroBased): string { if ($zeroBased) { return strlen($i) === 1 ? "0$i" : $i; } else { return $i; } } } ================================================ FILE: src/Importer/Importer.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Importer; use Deployer\Exception\ConfigurationException; use Deployer\Exception\Exception; use Symfony\Component\Yaml\Yaml; use function array_filter; use function array_keys; use function Deployer\after; use function Deployer\before; use function Deployer\cd; use function Deployer\download; use function Deployer\host; use function Deployer\localhost; use function Deployer\run; use function Deployer\runLocally; use function Deployer\set; use function Deployer\Support\find_line_number; use function Deployer\task; use function Deployer\upload; use const ARRAY_FILTER_USE_KEY; class Importer { /** * @var string */ private static $recipeFilename; /** * @var string */ private static $recipeSource; /** * @param string|string[] $paths */ public static function import($paths) { if (!is_array($paths)) { $paths = [$paths]; } foreach ($paths as $path) { if (preg_match('/\.php$/i', $path)) { // Prevent variable leak into deploy.php file call_user_func(function () use ($path) { // Reorder autoload stack $originStack = spl_autoload_functions(); require $path; $newStack = spl_autoload_functions(); if ($originStack[0] !== $newStack[0]) { foreach (array_reverse($originStack) as $loader) { spl_autoload_unregister($loader); spl_autoload_register($loader, true, true); } } }); } elseif (preg_match('/\.ya?ml$/i', $path)) { self::$recipeFilename = basename($path); self::$recipeSource = file_get_contents($path, true); $root = array_filter(Yaml::parse(self::$recipeSource), static function (string $key) { return !str_starts_with($key, '.'); }, ARRAY_FILTER_USE_KEY); foreach (array_keys($root) as $key) { static::$key($root[$key]); } } else { throw new Exception("Unknown file format: $path\nOnly .php and .yaml supported."); } } } protected static function hosts(array $hosts) { foreach ($hosts as $alias => $config) { if ($config['local'] ?? false) { $host = localhost($alias); } else { $host = host($alias); } if (is_array($config)) { foreach ($config as $key => $value) { $host->set($key, $value); } } } } protected static function config(array $config) { foreach ($config as $key => $value) { set($key, $value); } } protected static function tasks(array $tasks) { $buildTask = function ($name, $steps) { $body = function () {}; $task = task($name, $body); foreach ($steps as $step) { $buildStep = function ($step) use (&$body, $task) { extract($step); if (isset($cd)) { $prev = $body; $body = function () use ($cd, $prev) { $prev(); cd($cd); }; } if (isset($run)) { $has = 'run'; $prev = $body; $body = function () use ($run, $prev) { $prev(); try { run($run); } catch (Exception $e) { $e->setTaskFilename(self::$recipeFilename); $e->setTaskLineNumber(find_line_number(self::$recipeSource, $run)); throw $e; } }; } if (isset($run_locally)) { if (isset($has)) { throw new ConfigurationException("Task step can not have both $has and run_locally."); } $has = 'run_locally'; $prev = $body; $body = function () use ($run_locally, $prev) { $prev(); try { runLocally($run_locally); } catch (Exception $e) { $e->setTaskFilename(self::$recipeFilename); $e->setTaskLineNumber(find_line_number(self::$recipeSource, $run_locally)); throw $e; } }; } if (isset($upload)) { if (isset($has)) { throw new ConfigurationException("Task step can not have both $has and upload."); } $has = 'upload'; $prev = $body; $body = function () use ($upload, $prev) { $prev(); upload($upload['src'], $upload['dest']); }; } if (isset($download)) { if (isset($has)) { throw new ConfigurationException("Task step can not have both $has and download."); } $has = 'download'; $prev = $body; $body = function () use ($download, $prev) { $prev(); download($download['src'], $download['dest']); }; } $methods = [ 'desc', 'once', 'hidden', 'limit', 'select', ]; foreach ($methods as $method) { if (isset($$method)) { $task->$method($$method); } } }; $buildStep($step); $task->setCallback($body); } }; foreach ($tasks as $name => $config) { foreach ($config as $key => $value) { if (!is_int($key) || !is_string($value)) { goto not_a_group_task; } } // Create a group task. task($name, $config); continue; not_a_group_task: $buildTask($name, $config); } } protected static function after(array $after) { foreach ($after as $key => $value) { if (is_array($value)) { foreach (array_reverse($value) as $v) { after($key, $v); } } else { after($key, $value); } } } protected static function before(array $before) { foreach ($before as $key => $value) { if (is_array($value)) { foreach (array_reverse($value) as $v) { before($key, $v); } } else { before($key, $value); } } } } ================================================ FILE: src/Logger/Handler/FileHandler.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Logger\Handler; class FileHandler implements HandlerInterface { /** * @var string */ private $filePath; public function __construct(string $filePath) { $this->filePath = $filePath; } public function log(string $message): void { file_put_contents($this->filePath, $message, FILE_APPEND); } } ================================================ FILE: src/Logger/Handler/HandlerInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Logger\Handler; interface HandlerInterface { public function log(string $message): void; } ================================================ FILE: src/Logger/Handler/NullHandler.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Logger\Handler; class NullHandler implements HandlerInterface { public function log(string $message): void {} } ================================================ FILE: src/Logger/Logger.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Logger; use Deployer\ProcessRunner\Printer; use Deployer\Host\Host; use Deployer\Logger\Handler\HandlerInterface; class Logger { /** * @var HandlerInterface */ private $handler; public function __construct(HandlerInterface $handler) { $this->handler = $handler; } public function log(string $message): void { $this->handler->log("$message\n"); } public function callback(Host $host): \Closure { return function ($type, $buffer) use ($host) { $this->printBuffer($host, $type, $buffer); }; } public function printBuffer(Host $host, string $type, string $buffer): void { foreach (explode("\n", rtrim($buffer)) as $line) { $this->writeln($host, $type, $line); } } public function writeln(Host $host, string $type, string $line): void { // Omit empty lines if (empty($line)) { return; } $this->log("[{$host->getAlias()}] $line"); } } ================================================ FILE: src/ProcessRunner/Printer.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\ProcessRunner; use Deployer\Host\Host; use Symfony\Component\Console\Output\OutputInterface; class Printer { private OutputInterface $output; public function __construct(OutputInterface $output) { $this->output = $output; } public function command(Host $host, string $type, string $command): void { // -v for run command if ($this->output->isVerbose()) { $this->output->writeln("[$host] $type $command"); } } /** * Returns a callable for use with the symfony Process->run($callable) method. * * @return callable A function expecting a int $type (e.g. Process::OUT or Process::ERR) and string $buffer parameters. */ public function callback(Host $host, bool $forceOutput): callable { return function ($type, $buffer) use ($forceOutput, $host) { if ($this->output->isVerbose() || $forceOutput) { $this->printBuffer($type, $host, $buffer); } }; } /** * @param string $type Process::OUT or Process::ERR */ public function printBuffer(string $type, Host $host, string $buffer): void { foreach (explode("\n", rtrim($buffer)) as $line) { $this->writeln($type, $host, $line); } } public function writeln(string $type, Host $host, string $line): void { // Omit empty lines if (empty($line)) { return; } $this->output->writeln("[$host] $line"); } } ================================================ FILE: src/ProcessRunner/ProcessRunner.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\ProcessRunner; use Deployer\Exception\RunException; use Deployer\Exception\TimeoutException; use Deployer\Host\Host; use Deployer\Logger\Logger; use Deployer\Ssh\RunParams; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Process; use function Deployer\Support\deployer_root; use function Deployer\Support\env_stringify; class ProcessRunner { private Printer $pop; private Logger $logger; public function __construct(Printer $pop, Logger $logger) { $this->pop = $pop; $this->logger = $logger; } public function run(Host $host, string $command, RunParams $params): string { $this->pop->command($host, 'run', $command); $terminalOutput = $this->pop->callback($host, $params->forceOutput); $callback = function ($type, $buffer) use ($host, $terminalOutput) { $this->logger->printBuffer($host, $type, $buffer); $terminalOutput($type, $buffer); }; if (!empty($params->secrets)) { foreach ($params->secrets as $key => $value) { $command = str_replace('%' . $key . '%', $value, $command); } } if (!empty($params->env)) { $env = env_stringify($params->env); $command = "export $env; $command"; } if (!empty($params->dotenv)) { $command = "source $params->dotenv; $command"; } $process = Process::fromShellCommandline($params->shell) ->setInput($command) ->setTimeout($params->timeout) ->setIdleTimeout($params->idleTimeout) ->setWorkingDirectory($params->cwd ?? deployer_root()); try { $process->mustRun($callback); return $process->getOutput(); } catch (ProcessFailedException) { if ($params->nothrow) { return ''; } throw new RunException( $host, $command, $process->getExitCode(), $process->getOutput(), $process->getErrorOutput(), ); } catch (ProcessTimedOutException $exception) { // @phpstan-ignore-line PHPStan doesn't know about ProcessTimedOutException for some reason. throw new TimeoutException( $command, $exception->getExceededTimeout(), ); } } } ================================================ FILE: src/Selector/Selector.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Selector; use Deployer\Host\Host; use Deployer\Host\HostCollection; use function Deployer\Support\array_all; class Selector { /** * @var HostCollection */ private $hosts; public function __construct(HostCollection $hosts) { $this->hosts = $hosts; } /** * @return Host[] */ public function select(string $selectExpression) { $conditions = self::parse($selectExpression); $hosts = []; foreach ($this->hosts as $host) { if (self::apply($conditions, $host)) { $hosts[] = $host; } } return $hosts; } public static function apply(?array $conditions, Host $host): bool { if (empty($conditions)) { return true; } $labels = $host->get('labels', []); $labels['alias'] = $host->getAlias(); $labels['true'] = 'true'; $isTrue = function ($value) { return $value; }; foreach ($conditions as $hmm) { $ok = []; foreach ($hmm as [$op, $var, $value]) { if (is_array($value)) { $orOk = []; foreach ($value as $val) { $orOk[] = self::compare($op, $labels[$var] ?? null, $val); } $ok[] = count(array_filter($orOk, $isTrue)) > 0; } else { $ok[] = self::compare($op, $labels[$var] ?? null, $value); } } if (count($ok) > 0 && array_all($ok, $isTrue)) { return true; } } return false; } /** * @param string|string[] $a */ private static function compare(string $op, $a, ?string $b): bool { $matchFunction = function ($a, ?string $b) { foreach ((array) $a as $item) { if ($item === $b) { return true; } } return false; }; if ($op === '=') { return $matchFunction($a, $b); } if ($op === '!=') { return !$matchFunction($a, $b); } return false; } public static function parse(string $expression): array { $all = []; foreach (explode(',', $expression) as $sub) { $conditions = []; foreach (explode('&', $sub) as $part) { $part = trim($part); if ($part === 'all') { $conditions[] = ['=', 'true', 'true']; continue; } if (preg_match('/(?.+?)(?!?=)(?.+)/', $part, $match)) { $values = array_map('trim', explode('|', trim($match['value']))); $conditions[] = [$match['op'], trim($match['var']), $values]; } else { $conditions[] = ['=', 'alias', trim($part)]; } } $all[] = $conditions; } return $all; } } ================================================ FILE: src/Ssh/IOArguments.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Ssh; use Deployer\Exception\Exception; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class IOArguments { public static function collect(InputInterface $input, OutputInterface $output): array { $arguments = []; foreach ($input->getOptions() as $name => $value) { if (!$input->getOption($name)) { continue; } if ($name === 'file') { $arguments[] = "--file"; $arguments[] = ltrim($value, '='); continue; } if (in_array($name, ['verbose'], true)) { continue; } if (!is_array($value)) { $value = [$value]; } foreach ($value as $v) { if (is_bool($v)) { $arguments[] = "--$name"; continue; } $arguments[] = "--$name"; $arguments[] = $v; } } if ($output->isDecorated()) { $arguments[] = '--decorated'; } $verbosity = self::verbosity($output->getVerbosity()); if (!empty($verbosity)) { $arguments[] = $verbosity; } return $arguments; } private static function verbosity(int $verbosity): string { switch ($verbosity) { case OutputInterface::VERBOSITY_QUIET: return '-q'; case OutputInterface::VERBOSITY_NORMAL: return ''; case OutputInterface::VERBOSITY_VERBOSE: return '-v'; case OutputInterface::VERBOSITY_VERY_VERBOSE: return '-vv'; case OutputInterface::VERBOSITY_DEBUG: return '-vvv'; default: throw new Exception('Unknown verbosity level: ' . $verbosity); } } } ================================================ FILE: src/Ssh/RunParams.php ================================================ secrets = array_merge($params->secrets ?? [], $secrets ?? []); $params->timeout = $timeout ?? $params->timeout; return $params; } } ================================================ FILE: src/Ssh/SshClient.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Ssh; use Deployer\ProcessRunner\Printer; use Deployer\Exception\RunException; use Deployer\Exception\TimeoutException; use Deployer\Host\Host; use Deployer\Logger\Logger; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Process; use function Deployer\Support\env_stringify; class SshClient { private OutputInterface $output; private Printer $pop; private Logger $logger; public function __construct(OutputInterface $output, Printer $pop, Logger $logger) { $this->output = $output; $this->pop = $pop; $this->logger = $logger; } public function run(Host $host, string $command, RunParams $params): string { $shellId = 'id$' . bin2hex(random_bytes(10)); $shellCommand = $host->getShell(); if ($host->has('become') && !empty($host->get('become'))) { $shellCommand = "sudo -H -u {$host->get('become')} " . $shellCommand; } $ssh = array_merge(['ssh'], $host->connectionOptionsArray(), [$host->connectionString(), ": $shellId; $shellCommand"]); // -vvv for ssh command if ($this->output->isDebug()) { $sshString = $ssh[0]; for ($i = 1; $i < count($ssh); $i++) { $sshString .= ' ' . escapeshellarg((string) $ssh[$i]); } $this->output->writeln("[$host] $sshString"); } if (!empty($params->cwd)) { $command = "cd $params->cwd && ($command)"; } if (!empty($params->env)) { $env = env_stringify($params->env); $command = "export $env; $command"; } if (!empty($params->secrets)) { foreach ($params->secrets as $key => $value) { $command = str_replace('%' . $key . '%', strval($value), $command); } } $this->pop->command($host, 'run', $command); $this->logger->log("[{$host->getAlias()}] run $command"); $process = new Process($ssh); $process ->setInput($command) ->setTimeout($params->timeout) ->setIdleTimeout($params->idleTimeout); $callback = function ($type, $buffer) use ($params, $host) { $this->logger->printBuffer($host, $type, $buffer); $this->pop->callback($host, $params->forceOutput)($type, $buffer); }; try { $process->run($callback); } catch (ProcessTimedOutException $exception) { // Let's try to kill all processes started by this command. $pid = $this->run($host, "ps x | grep $shellId | grep -v grep | awk '{print \$1}'", $params->with(timeout: 10)); // Minus before pid means all processes in this group. $this->run($host, "kill -9 -$pid", $params->with(timeout: 20)); throw new TimeoutException( $command, $exception->getExceededTimeout(), ); } $output = $process->getOutput(); $exitCode = $process->getExitCode(); if ($exitCode !== 0 && !$params->nothrow) { throw new RunException( $host, $command, $exitCode, $output, $process->getErrorOutput(), ); } return $output; } } ================================================ FILE: src/Support/ObjectProxy.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Support; class ObjectProxy { /** * @var array */ private $objects; public function __construct(array $objects) { $this->objects = $objects; } public function __call(string $name, array $arguments): self { foreach ($this->objects as $object) { $object->$name(...$arguments); } return $this; } } ================================================ FILE: src/Support/Reporter.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Support; use Deployer\Utility\Httpie; use Symfony\Component\Process\PhpProcess; /** * @codeCoverageIgnore */ class Reporter { public static function report(array $stats): void { $version = DEPLOYER_VERSION; $body = json_encode($stats); $length = strlen($body); $php = new PhpProcess(<<start(); } } ================================================ FILE: src/Support/helpers.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Support; function array_flatten(array $array): array { $flatten = []; array_walk_recursive($array, function ($value) use (&$flatten) { $flatten[] = $value; }); return $flatten; } /** * Recursively merge two config arrays with a specific behavior: * * 1. scalar values are overridden * 2. array values are extended uniquely if all keys are numeric * 3. all other array values are merged */ function array_merge_alternate(array $original, array $override): array { foreach ($override as $key => $value) { if (isset($original[$key])) { if (!is_array($original[$key])) { if (is_numeric($key)) { // Append scalar value $original[] = $value; } else { // Override scalar value $original[$key] = $value; } } elseif (array_keys($original[$key]) === range(0, count($original[$key]) - 1)) { // Uniquely append to array with numeric keys $original[$key] = array_unique(array_merge($original[$key], $value)); } else { // Merge all other arrays $original[$key] = array_merge_alternate($original[$key], $value); } } else { // Simply add new key/value $original[$key] = $value; } } return $original; } function env_stringify(array $array): string { return implode(' ', array_map( function ($key, $value) { return sprintf("%s=%s", $key, escapeshellarg((string) $value)); }, array_keys($array), $array, )); } function is_closure(mixed $var): bool { return is_object($var) && ($var instanceof \Closure); } /** * Check if all elements satisfy predicate. */ function array_all(array $array, callable $predicate): bool { foreach ($array as $key => $value) { if (!$predicate($value, $key)) { return false; } } return true; } /** * Cleanup CRLF new line endings. */ function normalize_line_endings(string $string): string { return str_replace(["\r\n", "\r"], "\n", $string); } /** * Expand leading tilde (~) symbol in given path. */ function parse_home_dir(string $path): string { if ('~' === $path || str_starts_with($path, '~/')) { if (isset($_SERVER['HOME'])) { $home = $_SERVER['HOME']; } elseif (isset($_SERVER['HOMEDRIVE'], $_SERVER['HOMEPATH'])) { $home = $_SERVER['HOMEDRIVE'] . $_SERVER['HOMEPATH']; } else { return $path; } return $home . substr($path, 1); } return $path; } function find_line_number(string $source, string $string): int { $string = explode(PHP_EOL, $string)[0]; $before = strstr($source, $string, true); if (false !== $before) { return count(explode(PHP_EOL, $before)); } return 1; } function colorize_host(string $alias): string { if (defined('NO_ANSI')) { return $alias; } if (in_array($alias, ['localhost', 'local'], true)) { return $alias; } if (getenv('COLORTERM') === 'truecolor') { $hsv = function ($h, $s, $v) { $r = $g = $b = $i = $f = $p = $q = $t = 0; $i = floor($h * 6); $f = $h * 6 - $i; $p = $v * (1 - $s); $q = $v * (1 - $f * $s); $t = $v * (1 - (1 - $f) * $s); switch ($i % 6) { case 0: $r = $v; $g = $t; $b = $p; break; case 1: $r = $q; $g = $v; $b = $p; break; case 2: $r = $p; $g = $v; $b = $t; break; case 3: $r = $p; $g = $q; $b = $v; break; case 4: $r = $t; $g = $p; $b = $v; break; case 5: $r = $v; $g = $p; $b = $q; break; } $r = round($r * 255); $g = round($g * 255); $b = round($b * 255); return "\x1b[38;2;{$r};{$g};{$b}m"; }; $total = 100; $colors = []; for ($i = 0; $i < $total; $i++) { $colors[] = $hsv($i / $total, .5, .9); } if ($alias === 'prod' || $alias === 'production') { return "$colors[99]$alias\x1b[0m"; } if ($alias === 'beta') { return "$colors[14]$alias\x1b[0m"; } $tag = $colors[abs(crc32($alias)) % count($colors)]; return "$tag$alias\x1b[0m"; } $colors = [ 'fg=cyan;options=bold', 'fg=green;options=bold', 'fg=yellow;options=bold', 'fg=cyan', 'fg=blue', 'fg=yellow', 'fg=magenta', 'fg=blue;options=bold', 'fg=green', 'fg=magenta;options=bold', 'fg=red;options=bold', ]; $tag = $colors[abs(crc32($alias)) % count($colors)]; return "<$tag>$alias"; } function escape_shell_argument(string $argument): string { return "'" . str_replace("'", "'\\''", $argument) . "'"; } function deployer_root(): string { if (getenv('DEPLOYER_ROOT') !== false) { return getenv('DEPLOYER_ROOT'); } else { if (defined('DEPLOYER_DEPLOY_FILE')) { return dirname(DEPLOYER_DEPLOY_FILE); } else { return getcwd(); } } } ================================================ FILE: src/Task/Context.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Task; use Deployer\Configuration; use Deployer\Exception\Exception; use Deployer\Host\Host; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class Context { private Host $host; /** * @var Context[] */ private static array $contexts = []; public function __construct(Host $host) { $this->host = $host; } public static function push(Context $context): void { self::$contexts[] = $context; } public static function has(): bool { return !empty(self::$contexts); } public static function get(): Context { if (empty(self::$contexts)) { throw new Exception("Context was requested but was not available."); } return end(self::$contexts); } public static function pop(): ?Context { return array_pop(self::$contexts); } /** * Throws a Exception when not called within a task-context and therefore no Context is available. * * This method provides a useful error to the end-user to make him/her aware * to use a function in the required task-context. * * @throws Exception */ public static function required(string $callerName): void { if (empty(self::$contexts)) { throw new Exception("'$callerName' can only be used within a task."); } } public function getConfig(): Configuration { return $this->host->config(); } public function getHost(): Host { return $this->host; } } ================================================ FILE: src/Task/GroupTask.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Task; use function Deployer\invoke; class GroupTask extends Task { /** * List of tasks. * * @var string[] */ private $group; /** * @param string[] $group */ public function __construct(string $name, array $group) { parent::__construct($name); $this->group = $group; } public function run(Context $context): void { foreach ($this->group as $item) { invoke($item); } } /** * List of dependent tasks names * * @return string[] */ public function getGroup(): array { return $this->group; } public function setGroup(array $group): void { $this->group = $group; } } ================================================ FILE: src/Task/ScriptManager.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Task; use Deployer\Exception\Exception; use function Deployer\Support\array_flatten; class ScriptManager { /** * @var TaskCollection */ private $tasks; /** * @var bool */ private $hooksEnabled = true; /** * @var array */ private $visitedTasks = []; public function __construct(TaskCollection $tasks) { $this->tasks = $tasks; } /** * Return tasks to run. * * @return Task[] */ public function getTasks(string $name, ?string $startFrom = null, array &$skipped = []): array { $tasks = []; $this->visitedTasks = []; $allTasks = $this->doGetTasks($name); if ($startFrom === null) { $tasks = $allTasks; } else { $skip = true; foreach ($allTasks as $task) { if ($skip) { if ($task->getName() === $startFrom) { $skip = false; } else { $skipped[] = $task->getName(); continue; } } $tasks[] = $task; } if (count($tasks) === 0) { throw new Exception('All tasks skipped via --start-from option. Nothing to run.'); } } $enabledTasks = []; foreach ($tasks as $task) { if ($task->isEnabled()) { $enabledTasks[] = $task; } } return $enabledTasks; } /** * @return Task[] */ public function doGetTasks(string $name): array { if (array_key_exists($name, $this->visitedTasks)) { if ($this->visitedTasks[$name] >= 100) { throw new Exception("Looks like a circular dependency with \"$name\" task."); } $this->visitedTasks[$name]++; } else { $this->visitedTasks[$name] = 1; } $tasks = []; $task = $this->tasks->get($name); if ($this->hooksEnabled) { $tasks = array_merge(array_map([$this, 'doGetTasks'], $task->getBefore()), $tasks); } if ($task instanceof GroupTask) { foreach ($task->getGroup() as $taskName) { $subTasks = $this->doGetTasks($taskName); foreach ($subTasks as $subTask) { $subTask->addSelector($task->getSelector()); if ($task->isOnce()) { $subTask->once(); } $tasks[] = $subTask; } } } else { $tasks[] = $task; } if ($this->hooksEnabled) { $tasks = array_merge($tasks, array_map([$this, 'doGetTasks'], $task->getAfter())); } return array_flatten($tasks); } public function getHooksEnabled(): bool { return $this->hooksEnabled; } public function setHooksEnabled(bool $hooksEnabled): void { $this->hooksEnabled = $hooksEnabled; } } ================================================ FILE: src/Task/Task.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Task; use Deployer\Selector\Selector; class Task { /** * @var string */ private $name; /** * @var callable|null */ private $callback; /** * @var string */ private $description; /** * @var string */ private $sourceLocation = ''; /** * @var array */ private $before = []; /** * @var array */ private $after = []; /** * @var bool */ private $hidden = false; /** * @var bool */ private $once = false; /** * @var bool */ private $oncePerNode = false; /** * @var int|null */ private $limit = null; /** * @var array|null */ private $selector = null; /** * @var bool */ private $verbose = false; /** * @var bool */ private $enabled = true; /** * @param callable():void $callback */ public function __construct(string $name, ?callable $callback = null) { $this->name = $name; $this->callback = $callback; } /** * @param callable():void $callback */ public function setCallback(callable $callback): void { $this->callback = $callback; } public function run(Context $context): void { Context::push($context); try { call_user_func($this->callback); // call task } finally { if ($context->getConfig() !== null) { $context->getConfig()->set('working_path', null); } Context::pop(); } } public function getName(): string { return $this->name; } public function __toString(): string { return $this->getName(); } public function getDescription(): ?string { return $this->description; } public function desc(string $description): self { $this->description = $description; return $this; } public function getSourceLocation(): string { return $this->sourceLocation; } public function setSourceLocation(string $path): void { $this->sourceLocation = $path; } public function saveSourceLocation(): void { if (function_exists('debug_backtrace')) { $trace = debug_backtrace(); $this->sourceLocation = $trace[1]['file']; } } /** * Mark this task to run only once on one of hosts. */ public function once(bool $once = true): self { $this->once = $once; return $this; } public function isOnce(): bool { return $this->once; } /** * Mark task to only run once per node. * Node is a group of hosts with same hostname or with same node label. */ public function oncePerNode(bool $once = true): self { $this->oncePerNode = $once; return $this; } public function isOncePerNode(): bool { return $this->oncePerNode; } /** * Mark task as hidden and not accessible from CLI. */ public function hidden(bool $hidden = true): self { $this->hidden = $hidden; return $this; } public function isHidden(): bool { return $this->hidden; } /** * Make $task being run before this task. */ public function addBefore(string $task): self { array_unshift($this->before, $task); return $this; } /** * Make $task being run after this task */ public function addAfter(string $task): self { array_push($this->after, $task); return $this; } public function getBefore(): array { return $this->before; } public function getAfter(): array { return $this->after; } public function getLimit(): ?int { return $this->limit; } public function limit(?int $limit): self { $this->limit = $limit; return $this; } public function select(string $selector): self { $this->selector = Selector::parse($selector); return $this; } /** * @return array */ public function getSelector(): ?array { return $this->selector; } public function addSelector(?array $newSelector): void { if ($newSelector !== null) { if ($this->selector === null) { $this->selector = $newSelector; } else { $this->selector = array_merge($this->selector, $newSelector); } } } public function isVerbose(): bool { return $this->verbose; } public function verbose(bool $verbose = true): self { $this->verbose = $verbose; return $this; } public function isEnabled(): bool { return $this->enabled; } public function disable(): self { $this->enabled = false; return $this; } public function enable(): self { $this->enabled = true; return $this; } } ================================================ FILE: src/Task/TaskCollection.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Task; use Deployer\Collection\Collection; /** * @method Task get($name) * @method Task[] getIterator() */ class TaskCollection extends Collection { protected function notFound(string $name): \InvalidArgumentException { return new \InvalidArgumentException("Task `$name` not found."); } public function add(Task $task): void { $this->set($task->getName(), $task); } } ================================================ FILE: src/Utility/Httpie.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Utility; use Deployer\Exception\HttpieException; class Httpie { private string $method = 'GET'; private string $url = ''; private array $headers = []; private string $body = ''; private array $curlopts = []; private bool $nothrow = false; public function __construct() { if (!extension_loaded('curl')) { throw new \Exception( "Please, install curl extension.\n" . "https://php.net/curl.installation", ); } } public static function get(string $url): Httpie { $http = new self(); $http->method = 'GET'; $http->url = $url; return $http; } public static function post(string $url): Httpie { $http = new self(); $http->method = 'POST'; $http->url = $url; return $http; } public static function patch(string $url): Httpie { $http = new self(); $http->method = 'PATCH'; $http->url = $url; return $http; } public static function put(string $url): Httpie { $http = new self(); $http->method = 'PUT'; $http->url = $url; return $http; } public static function delete(string $url): Httpie { $http = new self(); $http->method = 'DELETE'; $http->url = $url; return $http; } public function query(array $params): self { $this->url .= '?' . http_build_query($params); return $this; } public function header(string $header, string $value): self { $this->headers[$header] = $value; return $this; } public function body(string $body): self { $this->body = $body; $this->headers = array_merge($this->headers, [ 'Content-Length' => strlen($this->body), ]); return $this; } public function jsonBody(array $data): self { $this->body = json_encode($data, JSON_PRETTY_PRINT); $this->headers = array_merge($this->headers, [ 'Content-Type' => 'application/json', 'Content-Length' => strlen($this->body), ]); return $this; } public function formBody(array $data): self { $this->body = http_build_query($data); $this->headers = array_merge($this->headers, [ 'Content-type' => 'application/x-www-form-urlencoded', 'Content-Length' => strlen($this->body), ]); return $this; } /** * @param mixed $value */ public function setopt(int $key, $value): self { $this->curlopts[$key] = $value; return $this; } public function nothrow(bool $on = true): self { $this->nothrow = $on; return $this; } public function send(?array &$info = null): string { if ($this->url === '') { throw new \RuntimeException('URL must not be empty to Httpie::send()'); } $ch = curl_init($this->url); curl_setopt($ch, CURLOPT_USERAGENT, 'Deployer ' . DEPLOYER_VERSION); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method); $headers = []; foreach ($this->headers as $key => $value) { $headers[] = "$key: $value"; } curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_POSTFIELDS, $this->body); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_MAXREDIRS, 10); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); curl_setopt($ch, CURLOPT_TIMEOUT, 5); foreach ($this->curlopts as $key => $value) { curl_setopt($ch, $key, $value); } $result = curl_exec($ch); $info = curl_getinfo($ch); if ($result === false) { if ($this->nothrow) { $result = ''; } else { $error = curl_error($ch); $errno = curl_errno($ch); if (PHP_MAJOR_VERSION < 8) { curl_close($ch); } throw new HttpieException($error, $errno); } } if (PHP_MAJOR_VERSION < 8) { curl_close($ch); } return $result; } public function getJson(): mixed { $result = $this->send(); $response = json_decode($result, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new HttpieException( 'JSON Error: ' . json_last_error_msg() . '\n' . 'Response: ' . $result, ); } return $response; } } ================================================ FILE: src/Utility/Rsync.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Utility; use Deployer\ProcessRunner\Printer; use Deployer\Exception\RunException; use Deployer\Host\Host; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; use function Deployer\writeln; class Rsync { /** * @var Printer */ private $pop; /** * @var OutputInterface */ private $output; public function __construct(Printer $pop, OutputInterface $output) { $this->pop = $pop; $this->output = $output; } /** * Start rsync process. * * @param string|string[] $source * @phpstan-param array{flags?: string, options?: array, timeout?: int|null, progress_bar?: bool, display_stats?: bool} $config * @throws RunException */ public function call(Host $host, $source, string $destination, array $config = []): void { $defaults = [ 'timeout' => null, 'options' => [], 'flags' => '-azP', 'progress_bar' => true, 'display_stats' => false, ]; $config = array_merge($defaults, $config); $options = $config['options']; $flags = $config['flags']; $displayStats = $config['display_stats'] || in_array('--stats', $options, true); if ($displayStats && !in_array('--stats', $options, true)) { $options[] = '--stats'; } $connectionOptions = $host->connectionOptionsString(); if ($connectionOptions !== '') { $options = array_merge($options, ['-e', "ssh $connectionOptions"]); } if ($host->has('become') && !empty($host->get('become'))) { $options = array_merge($options, ['--rsync-path', "sudo -H -u {$host->get('become')} rsync"]); } if (!is_array($source)) { $source = [$source]; } $command = array_values(array_filter( array_merge(['rsync', $flags], $options, $source, [$destination]), function (string $value) { return $value !== ''; }, )); $commandString = $command[0]; for ($i = 1; $i < count($command); $i++) { $commandString .= ' ' . escapeshellarg($command[$i]); } if ($this->output->isVerbose()) { $this->output->writeln("[$host] $commandString"); } $progressBar = null; if ($this->output->getVerbosity() === OutputInterface::VERBOSITY_NORMAL && $config['progress_bar']) { $progressBar = new ProgressBar($this->output); $progressBar->setBarCharacter(''); $progressBar->setProgressCharacter('>'); $progressBar->setEmptyBarCharacter('-'); } $fullOutput = ''; $callback = function ($type, $buffer) use ($host, $progressBar, &$fullOutput) { $fullOutput .= $buffer; if ($progressBar) { foreach (explode("\n", $buffer) as $line) { if (preg_match('/(to-chk|to-check)=(\d+?)\/(\d+)/', $line, $match)) { $max = intval($match[3]); $step = $max - intval($match[2]); $progressBar->setMaxSteps($max); $progressBar->setFormat("[$host] %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%"); $progressBar->setProgress($step); } } return; } if ($this->output->isVerbose()) { $this->pop->printBuffer($type, $host, $buffer); } }; $process = new Process($command); $process->setTimeout($config['timeout']); try { $process->mustRun($callback); if ($displayStats) { $stats = []; $statsStarted = false; foreach (explode("\n", $fullOutput) as $line) { if (strpos($line, 'Number of files') === 0) { $statsStarted = true; } if ($statsStarted) { if (empty($line)) { break; } $stats[] = $line; } } writeln("Rsync operation stats\n" . '' . implode("\n", $stats) . ''); } } catch (ProcessFailedException $exception) { throw new RunException( $host, $commandString, $process->getExitCode(), $process->getOutput(), $process->getErrorOutput(), ); } finally { if ($progressBar) { $progressBar->clear(); } } } } ================================================ FILE: src/functions.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer; use Deployer\Exception\Exception; use Deployer\Exception\GracefulShutdownException; use Deployer\Exception\RunException; use Deployer\Exception\TimeoutException; use Deployer\Exception\WillAskUser; use Deployer\Host\Host; use Deployer\Host\Localhost; use Deployer\Host\Range; use Deployer\Importer\Importer; use Deployer\Ssh\RunParams; use Deployer\Support\ObjectProxy; use Deployer\Task\Context; use Deployer\Task\GroupTask; use Deployer\Task\Task; use Deployer\Utility\Httpie; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; use function Deployer\Support\array_merge_alternate; use function Deployer\Support\is_closure; /** * Defines a host or hosts. * ```php * host('example.org'); * host('prod.example.org', 'staging.example.org'); * ``` * * Inside task can be used to get `Host` instance of an alias. * ```php * task('test', function () { * $port = host('example.org')->get('port'); * }); * ``` */ function host(string ...$hostname): Host|ObjectProxy { $deployer = Deployer::get(); if (count($hostname) === 1 && $deployer->hosts->has($hostname[0])) { return $deployer->hosts->get($hostname[0]); } $aliases = Range::expand($hostname); foreach ($aliases as $alias) { if ($deployer->hosts->has($alias)) { $host = $deployer->hosts->get($alias); throw new \InvalidArgumentException("Host \"$host\" already exists."); } } if (count($aliases) === 1) { $host = new Host($aliases[0]); $deployer->hosts->set($aliases[0], $host); return $host; } else { $hosts = array_map(function ($hostname) use ($deployer): Host { $host = new Host($hostname); $deployer->hosts->set($hostname, $host); return $host; }, $aliases); return new ObjectProxy($hosts); } } /** * Define a local host. * Deployer will not connect to this host, but will execute commands locally instead. * * ```php * localhost('ci'); // Alias and hostname will be "ci". * ``` */ function localhost(string ...$hostnames): Localhost|ObjectProxy { $deployer = Deployer::get(); $hostnames = Range::expand($hostnames); if (count($hostnames) <= 1) { $host = count($hostnames) === 1 ? new Localhost($hostnames[0]) : new Localhost(); $deployer->hosts->set($host->getAlias(), $host); return $host; } else { $hosts = array_map(function ($hostname) use ($deployer): Localhost { $host = new Localhost($hostname); $deployer->hosts->set($host->getAlias(), $host); return $host; }, $hostnames); return new ObjectProxy($hosts); } } /** * Returns current host. */ function currentHost(): Host { return Context::get()->getHost(); } /** * Returns hosts based on provided selector. * * ```php * on(select('stage=prod, role=db'), function (Host $host) { * ... * }); * ``` * * @return Host[] */ function select(string $selector): array { return Deployer::get()->selector->select($selector); } /** * Returns array of hosts selected by user via CLI. * * @return Host[] */ function selectedHosts(): array { $hosts = []; foreach (get('selected_hosts', []) as $alias) { $hosts[] = Deployer::get()->hosts->get($alias); } return $hosts; } /** * Import other php or yaml recipes. * * ```php * import('recipe/common.php'); * ``` * * ```php * import(__DIR__ . '/config/hosts.yaml'); * ``` */ function import(string $file): void { Importer::import($file); } /** * Set task description. */ function desc(?string $title = null): ?string { static $store = null; if ($title === null) { return $store; } else { return $store = $title; } } /** * Define a new task and save to tasks list. * * Alternatively get a defined task. * * @param string $name Name of current task. * @param callable|array|null $body Callable task, array of other tasks names or nothing to get a defined tasks * @return Task */ function task(string $name, callable|array|null $body = null): Task { $deployer = Deployer::get(); if (empty($body)) { return $deployer->tasks->get($name); } if (is_callable($body)) { $task = new Task($name, $body); } elseif (is_array($body)) { $task = new GroupTask($name, $body); } else { throw new \InvalidArgumentException('Task body should be a function or an array.'); } if ($deployer->tasks->has($name)) { // If task already exists, try to replace. $existingTask = $deployer->tasks->get($name); if (get_class($existingTask) !== get_class($task)) { // There is no "up" or "down"casting in PHP. throw new \Exception('Tried to replace Task \'' . $name . '\' with a GroupTask or vice-versa. This is not supported. If you are sure you want to do that, remove the old task `Deployer::get()->tasks->remove()` and then re-add the task.'); } if ($existingTask instanceof GroupTask) { $existingTask->setGroup($body); } elseif ($existingTask instanceof Task) { $existingTask->setCallback($body); } $task = $existingTask; } else { // If task does not exist, add it to the Collection. $deployer->tasks->set($name, $task); } $task->saveSourceLocation(); if (!empty(desc())) { $task->desc(desc()); desc(''); // Clear title. } return $task; } /** * Call that task before specified task runs. * * @param string $task The task before $that should be run. * @param string|callable $do The task to be run. * * @return ?Task */ function before(string $task, string|callable $do): ?Task { if (is_closure($do)) { $newTask = task("before:$task", $do); before($task, "before:$task"); return $newTask; } task($task)->addBefore($do); return null; } /** * Call that task after specified task runs. * * @param string $task The task after $that should be run. * @param string|callable $do The task to be run. * * @return ?Task */ function after(string $task, string|callable $do): ?Task { if (is_closure($do)) { $newTask = task("after:$task", $do); after($task, "after:$task"); return $newTask; } task($task)->addAfter($do); return null; } /** * Setup which task run on failure of $task. * When called multiple times for a task, previous fail() definitions will be overridden. * * @param string $task The task which need to fail so $that should be run. * @param string|callable $do The task to be run. * * @return ?Task */ function fail(string $task, string|callable $do): ?Task { if (is_callable($do)) { $newTask = task("fail:$task", $do); fail($task, "fail:$task"); return $newTask; } $deployer = Deployer::get(); $deployer->fail->set($task, $do); return null; } /** * Add users options. * * @param string $name The option name * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts * @param int|null $mode The option mode: One of the VALUE_* constants * @param string $description A description text * @param string|string[]|int|bool|null $default The default value (must be null for self::VALUE_NONE) */ function option(string $name, $shortcut = null, ?int $mode = null, string $description = '', $default = null): void { Deployer::get()->inputDefinition->addOption( new InputOption($name, $shortcut, $mode, $description, $default), ); } /** * Change the current working directory. * * ```php * cd('~/myapp'); * run('ls'); // Will run `ls` in ~/myapp. * ``` */ function cd(string $path): void { set('working_path', parse($path)); } /** * Change the current user. * * Usage: * ```php * $restore = become('deployer'); * * // do something * * $restore(); // revert back to the previous user * ``` * * @param string $user * @return \Closure */ function become(string $user): \Closure { $currentBecome = get('become'); set('become', $user); return function () use ($currentBecome) { set('become', $currentBecome); }; } /** * Execute a callback within a specific directory and revert back to the initial working directory. * * @return mixed Return value of the $callback function or null if callback doesn't return anything * @throws Exception */ function within(string $path, callable $callback): mixed { $lastWorkingPath = get('working_path', ''); try { set('working_path', parse($path)); return $callback(); } finally { set('working_path', $lastWorkingPath); } } /** * Executes given command on remote host. * * Examples: * * ```php * run('echo hello world'); * run('cd {{deploy_path}} && git status'); * run('password %secret%', secret: getenv('CI_SECRET')); * run('curl medv.io', timeout: 5); * ``` * * ```php * $path = run('readlink {{deploy_path}}/current'); * run("echo $path"); * ``` * * @param string $command Command to run on remote host. * @param string|null $cwd Sets the process working directory. If not set {{working_path}} will be used. * @param int|null $timeout Sets the process timeout (max. runtime). The timeout in seconds (default: 300 sec; see {{default_timeout}}, `null` to disable). * @param int|null $idleTimeout Sets the process idle timeout (max. time since last output) in seconds. * @param string|null $secret Placeholder `%secret%` can be used in command. Placeholder will be replaced with this value and will not appear in any logs. * @param array|null $env Array of environment variables: `run('echo $KEY', env: ['key' => 'value']);` * @param bool|null $forceOutput Print command output in real-time. * @param bool|null $nothrow Don't throw an exception of non-zero exit code. * @return string * @throws RunException * @throws TimeoutException * @throws WillAskUser */ function run( string $command, ?string $cwd = null, ?array $env = null, #[\SensitiveParameter] ?string $secret = null, ?bool $nothrow = false, ?bool $forceOutput = false, ?int $timeout = null, ?int $idleTimeout = null, ): string { $runParams = new RunParams( shell: currentHost()->getShell(), cwd: $cwd ?? has('working_path') ? get('working_path') : null, env: array_merge_alternate(get('env', []), $env ?? []), nothrow: $nothrow, timeout: $timeout ?? get('default_timeout', 300), idleTimeout: $idleTimeout, forceOutput: $forceOutput, secrets: empty($secret) ? null : ['secret' => $secret], ); $dotenv = get('dotenv', false); if (!empty($dotenv)) { $runParams->dotenv = $dotenv; } $run = function (string $command, ?RunParams $params = null) use ($runParams): string { $params = $params ?? $runParams; $host = currentHost(); $command = parse($command); if ($host instanceof Localhost) { $process = Deployer::get()->processRunner; $output = $process->run($host, $command, $params); } else { $client = Deployer::get()->sshClient; $output = $client->run($host, $command, $params); } return rtrim($output); }; if (preg_match('/^sudo\b/', $command)) { try { return $run($command); } catch (RunException) { $askpass = get('sudo_askpass', '/tmp/dep_sudo_pass'); $password = get('sudo_pass', false); if ($password === false) { writeln("run $command"); $password = askHiddenResponse(" [sudo] password for {{remote_user}}: "); } $run("echo -e '#!/bin/sh\necho \"\$PASSWORD\"' > $askpass"); $run("chmod a+x $askpass"); $command = preg_replace('/^sudo\b/', 'sudo -A', $command); $output = $run(" SUDO_ASKPASS=$askpass PASSWORD=%sudo_pass% $command", $runParams->with( secrets: ['sudo_pass' => escapeshellarg($password)], )); $run("rm $askpass"); return $output; } } else { return $run($command); } } /** * Execute commands on a local machine. * * Examples: * * ```php * $user = runLocally('git config user.name'); * runLocally("echo $user"); * ``` * * @param string $command Command to run on localhost. * @param string|null $cwd Sets the process working directory. If not set {{working_path}} will be used. * @param int|null $timeout Sets the process timeout (max. runtime). The timeout in seconds (default: 300 sec, `null` to disable). * @param int|null $idleTimeout Sets the process idle timeout (max. time since last output) in seconds. * @param string|null $secret Placeholder `%secret%` can be used in command. Placeholder will be replaced with this value and will not appear in any logs. * @param array|null $env Array of environment variables: `runLocally('echo $KEY', env: ['key' => 'value']);` * @param bool|null $forceOutput Print command output in real-time. * @param bool|null $nothrow Don't throw an exception of non-zero exit code. * @param string|null $shell Shell to run in. Default is `bash -s`. * * @return string * @throws RunException * @throws TimeoutException */ function runLocally( string $command, ?string $cwd = null, ?int $timeout = null, ?int $idleTimeout = null, #[\SensitiveParameter] ?string $secret = null, ?array $env = null, ?bool $forceOutput = false, ?bool $nothrow = false, ?string $shell = null, ): string { $runParams = new RunParams( shell: $shell ?? 'bash -s', cwd: $cwd, env: $env, nothrow: $nothrow, timeout: $timeout, idleTimeout: $idleTimeout, forceOutput: $forceOutput, secrets: empty($secret) ? null : ['secret' => $secret], ); $process = Deployer::get()->processRunner; $command = parse($command); $output = $process->run(new Localhost(), $command, $runParams); return rtrim($output); } /** * Run test command. * Example: * * ```php * if (test('[ -d {{release_path}} ]')) { * ... * } * ``` * */ function test(string $command): bool { $true = '+' . array_rand(array_flip(['accurate', 'appropriate', 'correct', 'legitimate', 'precise', 'right', 'true', 'yes', 'indeed'])); return trim(run("if $command; then echo $true; fi")) === $true; } /** * Run test command locally. * Example: * * testLocally('[ -d {{local_release_path}} ]') * */ function testLocally(string $command): bool { return runLocally("if $command; then echo +true; fi") === '+true'; } /** * Iterate other hosts, allowing to call run a func in callback. * * ```php * on(select('stage=prod, role=db'), function ($host) { * ... * }); * ``` * * ```php * on(host('example.org'), function ($host) { * ... * }); * ``` * * ```php * on(Deployer::get()->hosts, function ($host) { * ... * }); * ``` * * @param Host|Host[] $hosts */ function on($hosts, callable $callback): void { if (!is_array($hosts) && !($hosts instanceof \Traversable)) { $hosts = [$hosts]; } foreach ($hosts as $host) { if ($host instanceof Host) { $host->config()->load(); Context::push(new Context($host)); try { $callback($host); $host->config()->save(); } catch (GracefulShutdownException $e) { Deployer::get()->messenger->renderException($e, $host); } finally { Context::pop(); } } else { throw new \InvalidArgumentException("Function on can iterate only on Host instances."); } } } /** * Runs a task. * ```php * invoke('deploy:symlink'); * ``` * * @throws Exception */ function invoke(string $taskName): void { $task = Deployer::get()->tasks->get($taskName); Deployer::get()->messenger->startTask($task); $task->run(Context::get()); Deployer::get()->messenger->endTask($task); } /** * Upload files or directories to host. * * > To upload the _contents_ of a directory, include a trailing slash (eg `upload('build/', '{{release_path}}/public');`). * > Without the trailing slash, the build directory itself will be uploaded (resulting in `{{release_path}}/public/build`). * * The `$config` array supports the following keys: * * - `flags` for overriding the default `-azP` passed to the `rsync` command * - `options` with additional flags passed directly to the `rsync` command * - `timeout` for `Process::fromShellCommandline()` (`null` by default) * - `progress_bar` to display upload/download progress * - `display_stats` to display rsync set of statistics * * Note: due to the way php escapes command line arguments, list-notation for the rsync `--exclude={'file','anotherfile'}` option will not work. * A workaround is to add a separate `--exclude=file` argument for each exclude to `options` (also, _do not_ wrap the filename/filter in quotes). * An alternative might be to write the excludes to a temporary file (one per line) and use `--exclude-from=temporary_file` argument instead. * * @param string|string[] $source * @param array $config * @phpstan-param array{flags?: string, options?: array, timeout?: int|null, progress_bar?: bool, display_stats?: bool} $config * * @throws RunException */ function upload($source, string $destination, array $config = []): void { $rsync = Deployer::get()->rsync; $host = currentHost(); $source = is_array($source) ? array_map('Deployer\parse', $source) : parse($source); $destination = parse($destination); if ($host instanceof Localhost) { $rsync->call($host, $source, $destination, $config); } else { $rsync->call($host, $source, "{$host->connectionString()}:$destination", $config); } } /** * Download file or directory from host * * @param array $config * * @throws RunException */ function download(string $source, string $destination, array $config = []): void { $rsync = Deployer::get()->rsync; $host = currentHost(); $source = parse($source); $destination = parse($destination); if ($host instanceof Localhost) { $rsync->call($host, $source, $destination, $config); } else { $rsync->call($host, "{$host->connectionString()}:$source", $destination, $config); } } /** * Writes an info message. */ function info(string $message): void { writeln("info " . parse($message)); } /** * Writes an warning message. */ function warning(string $message): void { $message = "warning $message"; if (Context::has()) { writeln($message); } else { Deployer::get()->output->writeln($message); } } /** * Writes a message to the output and adds a newline at the end. */ function writeln(string $message, int $options = 0): void { $host = currentHost(); output()->writeln("[$host] " . parse($message), $options); } /** * Parse set values. */ function parse(string $value): string { return Context::get()->getConfig()->parse($value); } /** * Setup configuration option. * @param mixed $value * @throws Exception */ function set(string $name, $value): void { if (!Context::has()) { Deployer::get()->config->set($name, $value); } else { Context::get()->getConfig()->set($name, $value); } } /** * Merge new config params to existing config array. * * @param array $array */ function add(string $name, array $array): void { if (!Context::has()) { Deployer::get()->config->add($name, $array); } else { Context::get()->getConfig()->add($name, $array); } } /** * Get configuration value. * * @param mixed|null $default * * @return mixed */ function get(string $name, $default = null) { if (!Context::has()) { return Deployer::get()->config->get($name, $default); } else { return Context::get()->getConfig()->get($name, $default); } } /** * Check if there is such configuration option. */ function has(string $name): bool { if (!Context::has()) { return Deployer::get()->config->has($name); } else { return Context::get()->getConfig()->has($name); } } function ask(string $message, ?string $default = null, ?array $autocomplete = null): ?string { if (defined('DEPLOYER_NO_ASK')) { throw new WillAskUser($message); } Context::required(__FUNCTION__); if (output()->isQuiet()) { return $default; } if (Deployer::isWorker()) { return Deployer::masterCall(currentHost(), __FUNCTION__, ...func_get_args()); } /** @var QuestionHelper */ $helper = Deployer::get()->getHelper('question'); $tag = currentHost()->getTag(); $message = parse($message); $message = "[$tag] $message " . (($default === null) ? "" : "(default: $default) "); $question = new Question($message, $default); if (!empty($autocomplete)) { $question->setAutocompleterValues($autocomplete); } return $helper->ask(input(), output(), $question); } /** * @param mixed $default * @return mixed * @throws Exception */ function askChoice(string $message, array $availableChoices, $default = null, bool $multiselect = false) { if (defined('DEPLOYER_NO_ASK')) { throw new WillAskUser($message); } Context::required(__FUNCTION__); if (empty($availableChoices)) { throw new \InvalidArgumentException('Available choices should not be empty'); } if ($default !== null && !array_key_exists($default, $availableChoices)) { throw new \InvalidArgumentException('Default choice is not available'); } if (output()->isQuiet()) { if ($default === null) { $default = key($availableChoices); } return [$default => $availableChoices[$default]]; } if (Deployer::isWorker()) { return Deployer::masterCall(currentHost(), __FUNCTION__, ...func_get_args()); } /** @var QuestionHelper */ $helper = Deployer::get()->getHelper('question'); $tag = currentHost()->getTag(); $message = parse($message); $message = "[$tag] $message " . (($default === null) ? "" : "(default: $default) "); $question = new ChoiceQuestion($message, $availableChoices, $default); $question->setMultiselect($multiselect); return $helper->ask(input(), output(), $question); } function askConfirmation(string $message, bool $default = false): bool { if (defined('DEPLOYER_NO_ASK')) { throw new WillAskUser($message); } Context::required(__FUNCTION__); if (output()->isQuiet()) { return $default; } if (Deployer::isWorker()) { return Deployer::masterCall(currentHost(), __FUNCTION__, ...func_get_args()); } /** @var QuestionHelper */ $helper = Deployer::get()->getHelper('question'); $yesOrNo = $default ? 'Y/n' : 'y/N'; $tag = currentHost()->getTag(); $message = parse($message); $message = "[$tag] $message [$yesOrNo] "; $question = new ConfirmationQuestion($message, $default); return $helper->ask(input(), output(), $question); } function askHiddenResponse(string $message): string { if (defined('DEPLOYER_NO_ASK')) { throw new WillAskUser($message); } Context::required(__FUNCTION__); if (output()->isQuiet()) { return ''; } if (Deployer::isWorker()) { return (string) Deployer::masterCall(currentHost(), __FUNCTION__, ...func_get_args()); } /** @var QuestionHelper */ $helper = Deployer::get()->getHelper('question'); $tag = currentHost()->getTag(); $message = parse($message); $message = "[$tag] $message "; $question = new Question($message); $question->setHidden(true); $question->setHiddenFallback(false); return (string) $helper->ask(input(), output(), $question); } function input(): InputInterface { return Deployer::get()->input; } function output(): OutputInterface { return Deployer::get()->output; } /** * Check if command exists * * @throws RunException */ function commandExist(string $command): bool { return test("hash $command 2>/dev/null"); } /** * @throws RunException */ function commandSupportsOption(string $command, string $option): bool { $man = run("(man $command 2>&1 || $command -h 2>&1 || $command --help 2>&1) | grep -- $option || true"); if (empty($man)) { return false; } return str_contains($man, $option); } /** * @throws RunException */ function which(string $name): string { $nameEscaped = escapeshellarg($name); // Try `command`, should cover all Bourne-like shells // Try `which`, should cover most other cases // Fallback to `type` command, if the rest fails $path = run("command -v $nameEscaped || which $nameEscaped || type -p $nameEscaped"); if (empty($path)) { throw new \RuntimeException("Can't locate [$nameEscaped] - neither of [command|which|type] commands are available"); } // Deal with issue when `type -p` outputs something like `type -ap` in some implementations return trim(str_replace("$name is", "", $path)); } /** * Returns remote environments variables as an array. * ```php * $remotePath = remoteEnv()['PATH']; * run('echo $PATH', env: ['PATH' => "/home/user/bin:$remotePath"]); * ``` */ function remoteEnv(): array { $vars = []; $data = run('env'); foreach (explode("\n", $data) as $line) { [$name, $value] = explode('=', $line, 2); $vars[$name] = $value; } return $vars; } /** * Creates a new exception. */ function error(string $message): Exception { return new Exception(parse($message)); } /** * Returns current timestamp in UTC timezone in ISO8601 format. */ function timestamp(): string { return (new \DateTime('now', new \DateTimeZone('UTC')))->format(\DateTime::ISO8601); } /** * Example usage: * ```php * $result = fetch('{{domain}}', info: $info); * var_dump($info['http_code'], $result); * ``` */ function fetch(string $url, string $method = 'get', array $headers = [], ?string $body = null, ?array &$info = null, bool $nothrow = false): string { $url = parse($url); if (strtolower($method) === 'get') { $http = Httpie::get($url); } elseif (strtolower($method) === 'post') { $http = Httpie::post($url); } else { throw new \InvalidArgumentException("Unknown method \"$method\"."); } $http = $http->nothrow($nothrow); foreach ($headers as $key => $value) { $http = $http->header($key, $value); } if ($body !== null) { $http = $http->body($body); } return $http->send($info); } ================================================ FILE: src/schema.json ================================================ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "http://deployer.org/schema.json#", "type": "object", "additionalProperties": false, "properties": { "version": { "type": "string" }, "import": { "oneOf": [ { "type": "string" }, { "type": "array", "items": { "type": "string" } } ] }, "config": { "type": "object" }, "hosts": { "type": "object", "patternProperties": { "^": { "oneOf": [ { "type": "object", "properties": { "local": { "type": "boolean" } } }, { "type": "null" } ] } } }, "tasks": { "type": "object", "patternProperties": { "^": { "oneOf": [ { "type": "array", "items": { "type": "object", "properties": { "cd": { "type": "string" }, "run": { "type": "string" }, "run_locally": { "type": "string" }, "upload": { "type": "object", "required": [ "src", "dest" ], "properties": { "src": { "oneOf": [ { "type": "string" }, { "type": "array", "items": { "type": "string" } } ] }, "dest": { "type": "string" } } }, "download": { "type": "object", "required": [ "src", "dest" ], "properties": { "src": { "type": "string" }, "dest": { "type": "string" } } }, "desc": { "type": "string" }, "once": { "type": "boolean" }, "hidden": { "type": "boolean" }, "limit": { "type": "number" }, "select": { "type": "string" } } } }, { "type": "array", "items": { "type": "string" } } ] } } }, "before": { "type": "object" }, "after": { "type": "object" } } } ================================================ FILE: tests/bootstrap.php ================================================ &1`; `cd $repository && git add .`; `cd $repository && git config user.name 'Anton Medvedev'`; `cd $repository && git config user.email 'anton.medv@example.com'`; `cd $repository && git commit -m 'first commit'`; ================================================ FILE: tests/fixtures/project/uploaded.html ================================================ ================================================ FILE: tests/fixtures/repository/README.md ================================================ # Example repository ================================================ FILE: tests/fixtures/repository/composer.json ================================================ { "name": "ಠ_ಠ", "require": { "php": "^7.3" } } ================================================ FILE: tests/fixtures/repository/uploads/poem.txt ================================================ Night, street, lamp, drugstore, A dull and meaningless light. Go on and live another quarter century - Nothing will change. There's no way out. You'll die, then start from the beginning, It will repeat, just like before: Night, icy ripples on a canal, Drugstore, street, lamp. A. A. Blok 10 October 1912 ================================================ FILE: tests/joy/HostDefaultConfigTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace joy; class HostDefaultConfigTest extends JoyTest { protected function recipe(): string { return <<<'PHP' getPort(); writeln(empty($port) ? 'empty' : "port:$port"); }); PHP; } public function testOnFunc() { $this->dep('test'); $display = $this->tester->getDisplay(); self::assertEquals(0, $this->tester->getStatusCode(), $display); self::assertStringContainsString('empty', $display); } } ================================================ FILE: tests/joy/JoyTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace joy; use Deployer\Deployer; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Tester\ApplicationTester; use const __TEMP_DIR__; abstract class JoyTest extends TestCase { /** * @var ApplicationTester */ protected $tester; /** * @var Deployer */ protected $deployer; public static function setUpBeforeClass(): void { self::cleanUp(); mkdir(__TEMP_DIR__); } public static function tearDownAfterClass(): void { self::cleanUp(); } protected static function cleanUp() { if (is_dir(__TEMP_DIR__)) { exec('rm -rf ' . __TEMP_DIR__); } } protected function init(string $recipe) { $console = new Application(); $console->setAutoExit(false); $this->tester = new ApplicationTester($console); $this->deployer = new Deployer($console); $this->deployer->importer->import($recipe); $this->deployer->init(); $this->deployer->config->set('deploy_path', __TEMP_DIR__ . '/{{hostname}}'); } protected function dep(string $task, array $args = []): int { $recipe = __TEMP_DIR__ . '/' . get_called_class() . '.php'; file_put_contents($recipe, $this->recipe()); $this->init($recipe); return $this->tester->run(array_merge([ $task, 'selector' => 'all', '--file' => $recipe, '--limit' => 1, ], $args), [ 'verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'interactive' => false, ]); } abstract protected function recipe(): string; } ================================================ FILE: tests/joy/OnFuncTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace joy; class OnFuncTest extends JoyTest { protected function recipe(): string { return <<<'PHP' once(); PHP; } public function testOnFunc() { putenv('DEPLOYER_LOCAL_WORKER=false'); $this->dep('test'); putenv('DEPLOYER_LOCAL_WORKER=true'); $display = $this->tester->getDisplay(); self::assertEquals(0, $this->tester->getStatusCode(), $display); self::assertStringContainsString('[prod] foo = prod', $display); self::assertStringContainsString('[beta] foo = beta', $display); } } ================================================ FILE: tests/legacy/AbstractTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Output\Output; use Symfony\Component\Console\Tester\ApplicationTester; /** * @deprecated Use JoyTest instead. */ abstract class AbstractTest extends TestCase { /** * @var ApplicationTester */ protected $tester; /** * @var Deployer */ protected $deployer; public static function setUpBeforeClass(): void { self::cleanUp(); mkdir(__TEMP_DIR__); } public static function tearDownAfterClass(): void { self::cleanUp(); } protected static function cleanUp() { if (is_dir(__TEMP_DIR__)) { exec('rm -rf ' . __TEMP_DIR__); } } protected function init(string $recipe) { $console = new Application(); $console->setAutoExit(false); $this->tester = new ApplicationTester($console); $this->deployer = new Deployer($console); $this->deployer->importer->import($recipe); $this->deployer->init(); $this->deployer->config->set('deploy_path', __TEMP_DIR__ . '/{{hostname}}'); } protected function dep(string $recipe, string $task) { $this->init($recipe); $this->tester->run([ $task, 'selector' => 'all', '-f' => $recipe, '-l' => 1, ], [ 'verbosity' => Output::VERBOSITY_VERBOSE, 'interactive' => false, ]); } } ================================================ FILE: tests/legacy/CurrentPathTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer; use Symfony\Component\Console\Output\Output; class CurrentPathTest extends AbstractTest { public const RECIPE = __DIR__ . '/recipe/deploy.php'; public function testDeployWithDifferentCurrentPath() { $currentPath = __TEMP_DIR__ . '/prod/public_html'; $this->init(self::RECIPE); $this->tester->run([ 'deploy', 'selector' => 'prod', '-f' => self::RECIPE, '-o' => ['current_path=' . $currentPath], ], [ 'verbosity' => Output::VERBOSITY_VERBOSE, ]); $display = $this->tester->getDisplay(); self::assertEquals(0, $this->tester->getStatusCode(), $display); self::assertFileExists($currentPath . '/README.md'); self::assertFileExists($currentPath . '/config/test.yaml'); } } ================================================ FILE: tests/legacy/DeployTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer; use Symfony\Component\Console\Output\Output; class DeployTest extends AbstractTest { public const RECIPE = __DIR__ . '/recipe/deploy.php'; public function testDeploy() { $display = $this->dep(self::RECIPE, 'deploy'); $display = $this->tester->getDisplay(); self::assertEquals(0, $this->tester->getStatusCode(), $display); foreach ($this->deployer->hosts as $host) { $deployPath = $host->get('deploy_path'); self::assertDirectoryExists($deployPath . '/.dep'); self::assertDirectoryExists($deployPath . '/releases'); self::assertDirectoryExists($deployPath . '/shared'); self::assertDirectoryExists($deployPath . '/current'); self::assertDirectoryExists($deployPath . '/current/'); self::assertFileExists($deployPath . '/current/README.md'); self::assertDirectoryExists($deployPath . '/current/storage/logs'); self::assertDirectoryExists($deployPath . '/current/storage/db'); self::assertDirectoryExists($deployPath . '/shared/storage/logs'); self::assertDirectoryExists($deployPath . '/shared/storage/db'); self::assertFileExists($deployPath . '/shared/uploads/poem.txt'); self::assertFileExists($deployPath . '/shared/.env'); self::assertFileExists($deployPath . '/current/config/test.yaml'); self::assertFileExists($deployPath . '/shared/config/test.yaml'); self::assertEquals(1, intval(exec("cd $deployPath && ls -1 releases | wc -l"))); } } public function testDeploySelectHosts() { $this->init(self::RECIPE); $this->tester->setInputs(['0,1']); $this->tester->run(['deploy', '-f' => self::RECIPE, '-l' => 1], [ 'verbosity' => Output::VERBOSITY_NORMAL, 'interactive' => true, ]); self::assertEquals(0, $this->tester->getStatusCode(), $this->tester->getDisplay()); } public function testKeepReleases() { for ($i = 0; $i < 3; $i++) { $this->dep(self::RECIPE, 'deploy'); self::assertEquals(0, $this->tester->getStatusCode(), $this->tester->getDisplay()); } for ($i = 0; $i < 6; $i++) { $this->dep(self::RECIPE, 'deploy:fail'); self::assertEquals(1, $this->tester->getStatusCode(), $this->tester->getDisplay()); } for ($i = 0; $i < 3; $i++) { $this->dep(self::RECIPE, 'deploy'); self::assertEquals(0, $this->tester->getStatusCode(), $this->tester->getDisplay()); } foreach ($this->deployer->hosts as $host) { $deployPath = $host->get('deploy_path'); self::assertEquals(3, intval(exec("cd $deployPath && ls -1 releases | wc -l"))); } } /** * @depends testKeepReleases */ public function testRollback() { $this->dep(self::RECIPE, 'rollback'); self::assertEquals(0, $this->tester->getStatusCode(), $this->tester->getDisplay()); foreach ($this->deployer->hosts as $host) { $deployPath = $host->get('deploy_path'); self::assertEquals(3, intval(exec("cd $deployPath && ls -1 releases | wc -l"))); } } public function testFail() { $this->dep(self::RECIPE, 'deploy:fail'); $display = $this->tester->getDisplay(); self::assertEquals(1, $this->tester->getStatusCode(), $display); foreach ($this->deployer->hosts as $host) { $deployPath = $host->get('deploy_path'); self::assertEquals('ok', exec("cd $deployPath && [ -f .dep/deploy.lock ] || echo ok"), 'fail hooks deploy:unlock did not run'); } } /** * @depends testFail */ public function testCleanup() { $this->dep(self::RECIPE, 'deploy:cleanup'); self::assertEquals(0, $this->tester->getStatusCode(), $this->tester->getDisplay()); foreach ($this->deployer->hosts as $host) { $deployPath = $host->get('deploy_path'); self::assertFileDoesNotExist($deployPath . '/release'); } } public function testIsUnlockedExitsWithOneWhenDeployIsLocked() { $this->dep(self::RECIPE, 'deploy:lock'); $this->dep(self::RECIPE, 'deploy:is_locked'); $display = $this->tester->getDisplay(); self::assertStringContainsString('Deploy is locked by ', $display); self::assertSame(1, $this->tester->getStatusCode()); } public function testIsUnlockedExitsWithZeroWhenDeployIsNotLocked() { $this->dep(self::RECIPE, 'deploy:unlock'); $this->dep(self::RECIPE, 'deploy:is_locked'); $display = $this->tester->getDisplay(); self::assertStringContainsString('Deploy is unlocked.', $display); self::assertSame(0, $this->tester->getStatusCode()); } } ================================================ FILE: tests/legacy/EnvTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer; class EnvTest extends AbstractTest { public const RECIPE = __DIR__ . '/recipe/env.php'; public function testOnce() { $this->dep(self::RECIPE, 'test'); $display = $this->tester->getDisplay(); self::assertEquals(0, $this->tester->getStatusCode(), $display); self::assertStringContainsString('global=global', $display); self::assertStringContainsString('local=local', $display); self::assertStringContainsString('dotenv=Hello, world!', $display); self::assertStringContainsString('dotenv=local', $display); } } ================================================ FILE: tests/legacy/NamedArgumentsTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer; use Symfony\Component\Console\Output\Output; // TODO: Wait until Deployer 7.1 with only php8 supports. //class NamedArgumentsTest extends AbstractTest //{ // const RECIPE = __DIR__ . '/recipe/named_arguments.php'; // // public function testRunWithNamedArguments() // { // $this->init(self::RECIPE); // $this->tester->run(['named_arguments', '-f' => self::RECIPE], ['verbosity' => Output::VERBOSITY_VERBOSE]); // // $display = $this->tester->getDisplay(); // self::assertEquals(0, $this->tester->getStatusCode(), $display); // self::assertStringContainsString('Hello, world!', $display); // } // // public function testRunWithOptions() // { // $this->init(self::RECIPE); // $this->tester->run(['options', '-f' => self::RECIPE], ['verbosity' => Output::VERBOSITY_VERBOSE]); // // $display = $this->tester->getDisplay(); // self::assertEquals(0, $this->tester->getStatusCode(), $display); // self::assertStringContainsString('Hello, Anton!', $display); // } // // public function testRunWithOptionsWithNamedArguments() // { // $this->init(self::RECIPE); // $this->tester->run(['options_with_named_arguments', '-f' => self::RECIPE], ['verbosity' => Output::VERBOSITY_VERBOSE]); // // $display = $this->tester->getDisplay(); // self::assertEquals(0, $this->tester->getStatusCode(), $display); // self::assertStringContainsString('Hello, override!', $display); // } // // public function testRunLocallyWithNamedArguments() // { // $this->init(self::RECIPE); // $this->tester->run(['run_locally_named_arguments', '-f' => self::RECIPE], ['verbosity' => Output::VERBOSITY_VERBOSE]); // // $display = $this->tester->getDisplay(); // self::assertEquals(0, $this->tester->getStatusCode(), $display); // self::assertStringContainsString('Hello, world!', $display); // } //} ================================================ FILE: tests/legacy/OncePerNodeTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer; class OncePerNodeTest extends AbstractTest { public const RECIPE = __DIR__ . '/recipe/once_per_node.php'; public function testOnce() { $this->dep(self::RECIPE, 'test_once_per_node'); $display = $this->tester->getDisplay(); self::assertEquals(0, $this->tester->getStatusCode(), $display); self::assertStringContainsString('alias: group_a_1 hostname: localhost', $display); self::assertStringNotContainsString('alias: group_a_2 hostname: localhost', $display); self::assertStringContainsString('alias: group_b_1 hostname: group_b_1', $display); self::assertStringNotContainsString('alias: group_b_2 hostname: group_b_2', $display); } } ================================================ FILE: tests/legacy/OnceTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer; class OnceTest extends AbstractTest { public const RECIPE = __DIR__ . '/recipe/once.php'; public function testOnce() { $this->dep(self::RECIPE, 'test_once'); $display = $this->tester->getDisplay(); self::assertEquals(0, $this->tester->getStatusCode(), $display); self::assertTrue(substr_count($display, 'SHOULD BE ONCE') == 1, $display); } } ================================================ FILE: tests/legacy/ParallelTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer; use Symfony\Component\Console\Output\Output; class ParallelTest extends AbstractTest { public const RECIPE = __DIR__ . '/recipe/parallel.php'; public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); putenv('DEPLOYER_LOCAL_WORKER=false'); // Allow to start workers. Don't forget to disable it later. } public static function tearDownAfterClass(): void { putenv('DEPLOYER_LOCAL_WORKER=true'); parent::tearDownAfterClass(); } public function testWorker() { $this->init(self::RECIPE); $this->tester->run([ 'echo', '-f' => self::RECIPE, 'selector' => 'all', ], [ 'verbosity' => Output::VERBOSITY_NORMAL, ]); self::assertEquals(0, $this->tester->getStatusCode(), $this->tester->getDisplay()); } public function testServer() { $this->init(self::RECIPE); $this->tester->setInputs(['prod', 'Black bear']); $this->tester->run([ 'ask', '-f' => self::RECIPE, ], [ 'verbosity' => Output::VERBOSITY_NORMAL, 'interactive' => true, ]); $display = $this->tester->getDisplay(); self::assertEquals(0, $this->tester->getStatusCode(), $display); self::assertStringContainsString('[prod] Question: What kind of bear is best?', $display); self::assertStringContainsString('[prod] Black bear', $display); } public function testOption() { $this->init(self::RECIPE); $this->tester->run( [ 'echo', 'selector' => 'all', '-o' => ['greet=Hello'], '-f' => self::RECIPE, //'-l' => 1, ], [ 'verbosity' => Output::VERBOSITY_DEBUG, 'interactive' => false, ], ); $display = $this->tester->getDisplay(); self::assertEquals(0, $this->tester->getStatusCode(), $display); self::assertStringContainsString('[prod] Hello, prod!', $display); self::assertStringContainsString('[beta] Hello, beta!', $display); } public function testCachedHostConfig() { $this->init(self::RECIPE); $this->tester->run([ 'cache_config_test', '-f' => self::RECIPE, 'selector' => 'all', ], [ 'verbosity' => Output::VERBOSITY_NORMAL, ]); $display = $this->tester->getDisplay(); self::assertEquals(0, $this->tester->getStatusCode(), $display); self::assertTrue(substr_count($display, 'worker on prod') == 1, $display); self::assertTrue(substr_count($display, 'worker on beta') == 1, $display); } public function testHostConfigFromCallback() { $this->init(self::RECIPE); $this->tester->run([ 'host_config_from_callback', '-f' => self::RECIPE, 'selector' => 'all', ], [ 'verbosity' => Output::VERBOSITY_NORMAL, ]); $display = $this->tester->getDisplay(); self::assertEquals(0, $this->tester->getStatusCode(), $display); self::assertTrue(substr_count($display, '[prod] config value is from global') == 1, $display); self::assertTrue(substr_count($display, '[beta] config value is from callback') == 1, $display); } } ================================================ FILE: tests/legacy/SelectTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer; use Symfony\Component\Console\Output\Output; class SelectTest extends AbstractTest { public const RECIPE = __DIR__ . '/recipe/select.php'; public function testSelect() { $this->init(self::RECIPE); $this->tester->run([ 'test', '-f' => self::RECIPE, 'selector' => 'prod', ], [ 'verbosity' => Output::VERBOSITY_DEBUG, ]); $display = $this->tester->getDisplay(); self::assertEquals(0, $this->tester->getStatusCode(), $display); self::assertStringNotContainsString('executing on prod', $display); self::assertStringContainsString('executing on beta', $display); self::assertStringContainsString('executing on dev', $display); } } ================================================ FILE: tests/legacy/UpdateCodeTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer; use Symfony\Component\Console\Output\Output; class UpdateCodeTest extends AbstractTest { public const RECIPE = __DIR__ . '/recipe/update_code.php'; public function testDeployWithDifferentUpdateCodeTask() { $this->init(self::RECIPE); $this->tester->run([ 'deploy', 'selector' => 'prod', '-f' => self::RECIPE, ], [ 'verbosity' => Output::VERBOSITY_VERBOSE, ]); $display = $this->tester->getDisplay(); $deployPath = $this->deployer->hosts->get('prod')->getDeployPath(); self::assertEquals(0, $this->tester->getStatusCode(), $display); self::assertFileExists($deployPath . '/current/uploaded.html'); } } ================================================ FILE: tests/legacy/YamlTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer; use Symfony\Component\Console\Output\Output; class YamlTest extends AbstractTest { public const RECIPE = __DIR__ . '/recipe/deploy.yaml'; public function testDeploy() { $this->init(self::RECIPE); $this->deployer->config->set('repository', __REPOSITORY__); $this->tester->run([ 'deploy', 'selector' => 'all', '-f' => self::RECIPE, ], [ 'verbosity' => Output::VERBOSITY_VERBOSE, 'interactive' => false, ]); $display = $this->tester->getDisplay(); self::assertEquals(0, $this->tester->getStatusCode(), $display); foreach ($this->deployer->hosts as $host) { $deployPath = $host->get('deploy_path'); self::assertDirectoryExists($deployPath . '/.dep'); self::assertDirectoryExists($deployPath . '/releases'); self::assertDirectoryExists($deployPath . '/shared'); self::assertDirectoryExists($deployPath . '/current'); self::assertDirectoryExists($deployPath . '/current/'); self::assertFileExists($deployPath . '/current/README.md'); self::assertDirectoryExists($deployPath . '/current/storage/logs'); self::assertDirectoryExists($deployPath . '/current/storage/db'); self::assertDirectoryExists($deployPath . '/shared/storage/logs'); self::assertDirectoryExists($deployPath . '/shared/storage/db'); self::assertFileExists($deployPath . '/shared/uploads/poem.txt'); self::assertFileExists($deployPath . '/shared/.env'); self::assertFileExists($deployPath . '/current/config/test.yaml'); self::assertFileExists($deployPath . '/shared/config/test.yaml'); self::assertEquals(1, intval(`cd $deployPath && ls -1 releases | wc -l`)); } } } ================================================ FILE: tests/legacy/recipe/deploy.php ================================================ &1'); }); task('deploy:fail', [ 'deploy:prepare', 'fail', 'deploy:publish', ]); task('fail', function () { run('false'); }); fail('deploy:fail', 'deploy:unlock'); ================================================ FILE: tests/legacy/recipe/deploy.yaml ================================================ import: recipe/common.php config: application: deployer shared_dirs: - uploads - storage/logs/ - storage/db shared_files: - .env - config/test.yaml keep_releases: 3 http_user: false hosts: prod: local: true tasks: deploy: - deploy:prepare - deploy:vendors - deploy:publish deploy:vendors: - cd: '{{release_path}}' - run: echo {{bin/composer}} {{composer_options}} 2>&1 ================================================ FILE: tests/legacy/recipe/env.php ================================================ 'global', ]); task('test', function () { info('global=' . run('echo $VAR')); info('local=' . run('echo $VAR', env: ['VAR' => 'local'])); info('dotenv=' . run('echo $KEY')); info('dotenv=' . run('echo $KEY', env: ['KEY' => 'local'])); }); before('test', function () { run('mkdir -p {{deploy_path}}'); run('echo KEY="\'Hello, world!\'" > {{deploy_path}}/.env'); set('dotenv', '{{deploy_path}}/.env'); }); ================================================ FILE: tests/legacy/recipe/once.php ================================================ once(); ================================================ FILE: tests/legacy/recipe/once_per_node.php ================================================ setHostname('localhost'); localhost('group_a_2') ->setHostname('localhost'); localhost('group_b_1') ->setLabels(['node' => 'anna']); localhost('group_b_2') ->setLabels(['node' => 'anna']); task('test_once_per_node', function () { writeln('alias: {{alias}} hostname: {{hostname}}'); })->oncePerNode(); ================================================ FILE: tests/legacy/recipe/parallel.php ================================================ set('host_level_callback_config', function () { return 'from callback'; }); // testServer: task('ask', function () { $answer = ask('Question: What kind of bear is best?'); writeln($answer); }); // testWorker, testOption: set('greet', '_'); task('echo', function () { $alias = currentHost()->getAlias(); run("echo {{greet}}, $alias!"); }); // testCachedHostConfig: set('upper_host', function () { writeln('running ' . (Deployer::isWorker() ? 'worker' : 'master') . ' on ' . currentHost()->getAlias()); return strtoupper(currentHost()->getAlias()); }); task('cache_config_test', function () { writeln('echo 1: {{upper_host}}'); }); after('cache_config_test', function () { writeln('echo 2: {{upper_host}}'); }); // testHostConfigFromCallback: set('host_level_callback_config', 'from global'); task('host_config_from_callback', function () { writeln('config value is {{host_level_callback_config}}'); }); ================================================ FILE: tests/legacy/recipe/select.php ================================================ setLabels(['env' => 'prod']); localhost('beta')->setLabels(['env' => 'dev']); localhost('dev')->setLabels(['env' => 'dev']); task('test', function () { on(select('env=dev'), function () { info('executing on {{alias}}'); }); }); ================================================ FILE: tests/legacy/recipe/update_code.php ================================================ \" between 100|125|200|100000 and 0 is always true\\.$#" count: 1 path: ../src/Command/BlackjackCommand.php - message: "#^Else branch is unreachable because previous condition is always true\\.$#" count: 1 path: ../src/Command/BlackjackCommand.php - message: "#^If condition is always false\\.$#" count: 1 path: ../src/Command/BlackjackCommand.php - message: "#^Comparison operation \"\\>\" between 0 and 0 is always false\\.$#" count: 1 path: ../src/Command/BlackjackCommand.php - message: "#^Unsafe usage of new static\\(\\)\\.$#" count: 2 path: ../src/Component/PharUpdate/Exception/Exception.php - message: "#^If condition is always true\\.$#" count: 1 path: ../src/Host/Host.php - message: "#^Unreachable statement \\- code above always terminates\\.$#" count: 1 path: ../src/Importer/Importer.php ================================================ FILE: tests/src/Collection/CollectionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Collection; use Deployer\Host\HostCollection; use Deployer\Task\TaskCollection; use PHPUnit\Framework\TestCase; class CollectionTest extends TestCase { public static function collections() { return [ [new Collection()], [new TaskCollection()], [new HostCollection()], ]; } /** * @dataProvider collections */ public function testCollection($collection) { $this->assertInstanceOf(Collection::class, $collection); $object = new \stdClass(); $collection->set('object', $object); $this->assertTrue($collection->has('object')); $this->assertEquals($object, $collection->get('object')); $this->assertEquals(['object' => $object], $collection->select(function ($value, $key) use ($object) { return $value === $object && $key === 'object'; })); } /** * @dataProvider collections * @depends testCollection */ public function testException($collection) { $this->expectException(\InvalidArgumentException::class); $collection->get('unexpected'); } } ================================================ FILE: tests/src/Command/BlackjackCommandTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Component\Pimple; use Deployer\Component\Pimple\Exception\FrozenServiceException; use Deployer\Component\Pimple\Exception\InvalidServiceIdentifierException; use Deployer\Component\Pimple\Exception\UnknownIdentifierException; use InvalidArgumentException; use PHPUnit\Framework\TestCase; use ReflectionProperty; use RuntimeException; use function extension_loaded; class PimpleTest extends TestCase { public function testWithString() { $pimple = new Container(); $pimple['param'] = 'value'; $this->assertEquals('value', $pimple['param']); } public function testWithClosure() { $pimple = new Container(); $pimple['service'] = function () { return new Service(); }; $this->assertInstanceOf(Service::class, $pimple['service']); } public function testServicesShouldBeDifferent() { $pimple = new Container(); $pimple['service'] = $pimple->factory(function () { return new Service(); }); $serviceOne = $pimple['service']; $this->assertInstanceOf(Service::class, $serviceOne); $serviceTwo = $pimple['service']; $this->assertInstanceOf(Service::class, $serviceTwo); $this->assertNotSame($serviceOne, $serviceTwo); } public function testShouldPassContainerAsParameter() { $pimple = new Container(); $pimple['service'] = function () { return new Service(); }; $pimple['container'] = function ($container) { return $container; }; $this->assertNotSame($pimple, $pimple['service']); $this->assertSame($pimple, $pimple['container']); } public function testIsset() { $pimple = new Container(); $pimple['param'] = 'value'; $pimple['service'] = function () { return new Service(); }; $pimple['null'] = null; $this->assertTrue(isset($pimple['param'])); $this->assertTrue(isset($pimple['service'])); $this->assertTrue(isset($pimple['null'])); $this->assertFalse(isset($pimple['non_existent'])); } public function testConstructorInjection() { $params = ['param' => 'value']; $pimple = new Container($params); $this->assertSame($params['param'], $pimple['param']); } public function testOffsetGetValidatesKeyIsPresent() { $this->expectException(UnknownIdentifierException::class); $this->expectExceptionMessage('Identifier "foo" is not defined.'); $pimple = new Container(); echo $pimple['foo']; } /** * @group legacy */ public function testLegacyOffsetGetValidatesKeyIsPresent() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Identifier "foo" is not defined.'); $pimple = new Container(); echo $pimple['foo']; } public function testOffsetGetHonorsNullValues() { $pimple = new Container(); $pimple['foo'] = null; $this->assertNull($pimple['foo']); } public function testUnset() { $pimple = new Container(); $pimple['param'] = 'value'; $pimple['service'] = function () { return new Service(); }; unset($pimple['param'], $pimple['service']); $this->assertFalse(isset($pimple['param'])); $this->assertFalse(isset($pimple['service'])); } /** * @dataProvider serviceDefinitionProvider */ public function testShare($service) { $pimple = new Container(); $pimple['shared_service'] = $service; $serviceOne = $pimple['shared_service']; $this->assertInstanceOf(Service::class, $serviceOne); $serviceTwo = $pimple['shared_service']; $this->assertInstanceOf(Service::class, $serviceTwo); $this->assertSame($serviceOne, $serviceTwo); } /** * @dataProvider serviceDefinitionProvider */ public function testProtect($service) { $pimple = new Container(); $pimple['protected'] = $pimple->protect($service); $this->assertSame($service, $pimple['protected']); } public function testGlobalFunctionNameAsParameterValue() { $pimple = new Container(); $pimple['global_function'] = 'strlen'; $this->assertSame('strlen', $pimple['global_function']); } public function testRaw() { $pimple = new Container(); $pimple['service'] = $definition = $pimple->factory(function () { return 'foo'; }); $this->assertSame($definition, $pimple->raw('service')); } public function testRawHonorsNullValues() { $pimple = new Container(); $pimple['foo'] = null; $this->assertNull($pimple->raw('foo')); } public function testRawValidatesKeyIsPresent() { $this->expectException(UnknownIdentifierException::class); $this->expectExceptionMessage('Identifier "foo" is not defined.'); $pimple = new Container(); $pimple->raw('foo'); } /** * @group legacy */ public function testLegacyRawValidatesKeyIsPresent() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Identifier "foo" is not defined.'); $pimple = new Container(); $pimple->raw('foo'); } /** * @dataProvider serviceDefinitionProvider */ public function testExtend($service) { $pimple = new Container(); $pimple['shared_service'] = function () { return new Service(); }; $pimple['factory_service'] = $pimple->factory(function () { return new Service(); }); $pimple->extend('shared_service', $service); $serviceOne = $pimple['shared_service']; $this->assertInstanceOf(Service::class, $serviceOne); $serviceTwo = $pimple['shared_service']; $this->assertInstanceOf(Service::class, $serviceTwo); $this->assertSame($serviceOne, $serviceTwo); $this->assertSame($serviceOne->value, $serviceTwo->value); $pimple->extend('factory_service', $service); $serviceOne = $pimple['factory_service']; $this->assertInstanceOf(Service::class, $serviceOne); $serviceTwo = $pimple['factory_service']; $this->assertInstanceOf(Service::class, $serviceTwo); $this->assertNotSame($serviceOne, $serviceTwo); $this->assertNotSame($serviceOne->value, $serviceTwo->value); } public function testExtendDoesNotLeakWithFactories() { if (extension_loaded('pimple')) { $this->markTestSkipped('Pimple extension does not support this test'); } $pimple = new Container(); $pimple['foo'] = $pimple->factory(function () { return; }); $pimple['foo'] = $pimple->extend('foo', function ($foo, $pimple) { return; }); unset($pimple['foo']); $p = new ReflectionProperty($pimple, 'values'); $p->setAccessible(true); $this->assertEmpty($p->getValue($pimple)); $p = new ReflectionProperty($pimple, 'factories'); $p->setAccessible(true); $this->assertCount(0, $p->getValue($pimple)); } public function testExtendValidatesKeyIsPresent() { $this->expectException(UnknownIdentifierException::class); $this->expectExceptionMessage('Identifier "foo" is not defined.'); $pimple = new Container(); $pimple->extend('foo', function () {}); } /** * @group legacy */ public function testLegacyExtendValidatesKeyIsPresent() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Identifier "foo" is not defined.'); $pimple = new Container(); $pimple->extend('foo', function () {}); } public function testKeys() { $pimple = new Container(); $pimple['foo'] = 123; $pimple['bar'] = 123; $this->assertEquals(['foo', 'bar'], $pimple->keys()); } /** @test */ public function settingAnInvokableObjectShouldTreatItAsFactory() { $pimple = new Container(); $pimple['invokable'] = new Invokable(); $this->assertInstanceOf(Service::class, $pimple['invokable']); } /** @test */ public function settingNonInvokableObjectShouldTreatItAsParameter() { $pimple = new Container(); $pimple['non_invokable'] = new NonInvokable(); $this->assertInstanceOf(NonInvokable::class, $pimple['non_invokable']); } /** * @dataProvider badServiceDefinitionProvider */ public function testFactoryFailsForInvalidServiceDefinitions($service) { $this->expectException(\TypeError::class); $pimple = new Container(); $pimple->factory($service); } /** * @group legacy * @dataProvider badServiceDefinitionProvider */ public function testLegacyFactoryFailsForInvalidServiceDefinitions($service) { $this->expectException(\TypeError::class); $pimple = new Container(); $pimple->factory($service); } /** * @dataProvider badServiceDefinitionProvider */ public function testProtectFailsForInvalidServiceDefinitions($service) { $this->expectException(\TypeError::class); $pimple = new Container(); $pimple->protect($service); } /** * @group legacy * @dataProvider badServiceDefinitionProvider */ public function testLegacyProtectFailsForInvalidServiceDefinitions($service) { $this->expectException(\TypeError::class); $pimple = new Container(); $pimple->protect($service); } /** * @dataProvider badServiceDefinitionProvider */ public function testExtendFailsForKeysNotContainingServiceDefinitions($service) { $this->expectException(InvalidServiceIdentifierException::class); $this->expectExceptionMessage('Identifier "foo" does not contain an object definition.'); $pimple = new Container(); $pimple['foo'] = $service; $pimple->extend('foo', function () {}); } /** * @group legacy * @dataProvider badServiceDefinitionProvider */ public function testLegacyExtendFailsForKeysNotContainingServiceDefinitions($service) { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Identifier "foo" does not contain an object definition.'); $pimple = new Container(); $pimple['foo'] = $service; $pimple->extend('foo', function () {}); } /** * @group legacy * @expectedDeprecation How Pimple behaves when extending protected closures will be fixed in Pimple 4. Are you sure "foo" should be protected? */ public function testExtendingProtectedClosureDeprecation() { $pimple = new Container(); $pimple['foo'] = $pimple->protect(function () { return 'bar'; }); $pimple->extend('foo', function ($value) { return $value . '-baz'; }); $this->assertSame('bar-baz', $pimple['foo']); } /** * @dataProvider badServiceDefinitionProvider */ public function testExtendFailsForInvalidServiceDefinitions($service) { $this->expectException(\TypeError::class); $pimple = new Container(); $pimple['foo'] = function () {}; $pimple->extend('foo', $service); } /** * @group legacy * @dataProvider badServiceDefinitionProvider */ public function testLegacyExtendFailsForInvalidServiceDefinitions($service) { $this->expectException(\TypeError::class); $pimple = new Container(); $pimple['foo'] = function () {}; $pimple->extend('foo', $service); } public function testExtendFailsIfFrozenServiceIsNonInvokable() { $this->expectException(FrozenServiceException::class); $this->expectExceptionMessage('Cannot override frozen service "foo".'); $pimple = new Container(); $pimple['foo'] = function () { return new NonInvokable(); }; $foo = $pimple['foo']; $pimple->extend('foo', function () {}); } public function testExtendFailsIfFrozenServiceIsInvokable() { $this->expectException(FrozenServiceException::class); $this->expectExceptionMessage('Cannot override frozen service "foo".'); $pimple = new Container(); $pimple['foo'] = function () { return new Invokable(); }; $foo = $pimple['foo']; $pimple->extend('foo', function () {}); } /** * Provider for invalid service definitions. */ public static function badServiceDefinitionProvider() { return [ [123], [new NonInvokable()], ]; } /** * Provider for service definitions. */ public static function serviceDefinitionProvider() { return [ [function ($value) { $service = new Service(); $service->value = $value; return $service; }], [new Invokable()], ]; } public function testDefiningNewServiceAfterFreeze() { $pimple = new Container(); $pimple['foo'] = function () { return 'foo'; }; $foo = $pimple['foo']; $pimple['bar'] = function () { return 'bar'; }; $this->assertSame('bar', $pimple['bar']); } public function testOverridingServiceAfterFreeze() { $this->expectException(FrozenServiceException::class); $this->expectExceptionMessage('Cannot override frozen service "foo".'); $pimple = new Container(); $pimple['foo'] = function () { return 'foo'; }; $foo = $pimple['foo']; $pimple['foo'] = function () { return 'bar'; }; } /** * @group legacy */ public function testLegacyOverridingServiceAfterFreeze() { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Cannot override frozen service "foo".'); $pimple = new Container(); $pimple['foo'] = function () { return 'foo'; }; $foo = $pimple['foo']; $pimple['foo'] = function () { return 'bar'; }; } public function testRemovingServiceAfterFreeze() { $pimple = new Container(); $pimple['foo'] = function () { return 'foo'; }; $foo = $pimple['foo']; unset($pimple['foo']); $pimple['foo'] = function () { return 'bar'; }; $this->assertSame('bar', $pimple['foo']); } public function testExtendingService() { $pimple = new Container(); $pimple['foo'] = function () { return 'foo'; }; $pimple['foo'] = $pimple->extend('foo', function ($foo, $app) { return "$foo.bar"; }); $pimple['foo'] = $pimple->extend('foo', function ($foo, $app) { return "$foo.baz"; }); $this->assertSame('foo.bar.baz', $pimple['foo']); } public function testExtendingServiceAfterOtherServiceFreeze() { $pimple = new Container(); $pimple['foo'] = function () { return 'foo'; }; $pimple['bar'] = function () { return 'bar'; }; $foo = $pimple['foo']; $pimple['bar'] = $pimple->extend('bar', function ($bar, $app) { return "$bar.baz"; }); $this->assertSame('bar.baz', $pimple['bar']); } } class Invokable { public function __invoke($value = null) { $service = new Service(); $service->value = $value; return $service; } } class NonInvokable { public function __call($a, $b) {} } class Service { public $value; } ================================================ FILE: tests/src/Configuration/ConfigurationTest.php ================================================ set('foo', 'a'); $config['bar'] = 'b'; self::assertEquals('a b', $config->parse('{{foo}} {{bar}}')); } public function testUnset() { $config = new Configuration(); $config->set('opt', true); unset($config['opt']); self::assertFalse(isset($config['opt'])); } public function testGet() { $config = new Configuration(); $config->set('opt', true); $config->set('fn', function () { return 'func'; }); self::assertTrue(isset($config['opt'])); self::assertEquals(true, $config['opt']); self::assertEquals('func', $config['fn']); } public function testGetDefault() { $config = new Configuration(); $config->set('name', 'alpha'); self::assertEquals('/alpha', $config->get('path', '/{{name}}')); } public function testGetException() { $this->expectException(ConfigurationException::class); $config = new Configuration(); $config->set('name', 'alpha'); self::assertEquals('/alpha', $config->get('path')); } public function testGetParent() { $parent = new Configuration(); $config = new Configuration($parent); $parent->set('opt', 'value'); self::assertEquals('value', $parent['opt']); self::assertEquals('value', $config['opt']); $parent->set('opt', 'newValue'); self::assertEquals('newValue', $parent['opt']); self::assertEquals('value', $config['opt']); $config->set('opt', 'hostValue'); self::assertEquals('newValue', $parent['opt']); self::assertEquals('hostValue', $config['opt']); self::assertEquals('okay', $config->get('miss', 'okay')); } public function testGetParentParent() { $global = new Configuration(); $parent = new Configuration($global); $config = new Configuration($parent); $global->set('global', 'value from {{path}}'); $parent->set('path', 'parent'); self::assertEquals('value from parent', $config->get('global')); } public function testGetParentWhatDependsOnChild() { $parent = new Configuration(); $alpha = new Configuration($parent); $beta = new Configuration($parent); $parent->set('deploy_path', 'path/{{name}}'); $alpha->set('name', 'alpha'); $beta->set('name', 'beta'); self::assertEquals('path/alpha', $alpha->get('deploy_path')); self::assertEquals('path/beta', $beta->get('deploy_path')); } public function testGetFromCallback() { $config = new Configuration(); $config->set('func', function () { return 'param'; }); self::assertEquals('param', $config['func']); } public function testAdd() { $config = new Configuration(); $config->set('opt', ['foo', 'bar']); $config->add('opt', ['baz']); self::assertEquals(['foo', 'bar', 'baz'], $config['opt']); } public function testAddEmpty() { $config = new Configuration(); $config->add('opt', ['baz']); self::assertEquals(['baz'], $config['opt']); } public function testAddDefaultToNotArray() { $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Config option "config" isn\'t array.'); $config = new Configuration(); $config->set('config', 'option'); $config->add('config', ['three']); } public function testAddToParent() { $parent = new Configuration(); $alpha = new Configuration($parent); $parent->set('files', ['a', 'b']); $alpha->add('files', ['c']); self::assertEquals(['a', 'b', 'c'], $alpha->get('files')); } public function testAddToParentCallback() { $parent = new Configuration(); $alpha = new Configuration($parent); $parent->set('files', function () { return ['a', 'b']; }); $alpha->add('files', ['c']); self::assertEquals(['a', 'b', 'c'], $alpha->get('files')); } public function testPersist() { $parent = new Configuration(); $alpha = new Configuration($parent); $parent->set('global', 'do not include'); $alpha->set('whoami', function () { $this->fail('should not be called'); }); $alpha->set('name', 'alpha'); self::assertEquals(['name' => 'alpha'], $alpha->persist()); } } ================================================ FILE: tests/src/DeployerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class DeployerTest extends TestCase { private $deployer; protected function setUp(): void { $console = new Application(); $input = $this->createMock(InputInterface::class); $output = $this->createMock(OutputInterface::class); $this->deployer = new Deployer($console, $input, $output); } protected function tearDown(): void { unset($this->deployer); } public function testInstance() { $this->assertEquals($this->deployer, Deployer::get()); } } ================================================ FILE: tests/src/FunctionsTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer; use Deployer\Configuration; use Deployer\Host\Host; use Deployer\Host\Localhost; use Deployer\Task\Context; use Deployer\Task\GroupTask; use Deployer\Task\Task; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Input\Input; use Symfony\Component\Console\Output\Output; use function Deployer\localhost; class FunctionsTest extends TestCase { /** * @var Deployer */ private $deployer; protected function setUp(): void { $console = new Application(); $input = $this->createMock(Input::class); $output = $this->createMock(Output::class); $host = new Localhost(); $this->deployer = new Deployer($console); $this->deployer['input'] = $input; $this->deployer['output'] = $output; Context::push(new Context($host)); } protected function tearDown(): void { Context::pop(); unset($this->deployer); $this->deployer = null; } public function testHost() { host('domain.com'); self::assertInstanceOf(Host::class, $this->deployer->hosts->get('domain.com')); host('a1.domain.com', 'a2.domain.com')->set('roles', 'app'); self::assertInstanceOf(Host::class, $this->deployer->hosts->get('a1.domain.com')); self::assertInstanceOf(Host::class, $this->deployer->hosts->get('a2.domain.com')); host('db[1:2].domain.com')->set('roles', 'db'); self::assertInstanceOf(Host::class, $this->deployer->hosts->get('db1.domain.com')); self::assertInstanceOf(Host::class, $this->deployer->hosts->get('db2.domain.com')); } public function testLocalhost() { localhost('domain.com'); self::assertInstanceOf(Localhost::class, $this->deployer->hosts->get('domain.com')); } public function testTask() { task('task', function () {}); $task = $this->deployer->tasks->get('task'); self::assertInstanceOf(Task::class, $task); $task = task('task'); self::assertInstanceOf(Task::class, $task); task('group', ['task']); $task = $this->deployer->tasks->get('group'); self::assertInstanceOf(GroupTask::class, $task); } public function testBefore() { task('main', function () {}); task('before', function () {}); before('main', 'before'); before('before', function () {}); $names = $this->taskToNames($this->deployer->scriptManager->getTasks('main')); self::assertEquals(['before:before', 'before', 'main'], $names); } public function testAfter() { task('main', function () {}); task('after', function () {}); after('main', 'after'); after('after', function () {}); $names = $this->taskToNames($this->deployer->scriptManager->getTasks('main')); self::assertEquals(['main', 'after', 'after:after'], $names); } public function testRunLocally() { $output = runLocally('echo "hello"'); self::assertEquals('hello', $output); } public function testWithinSetsWorkingPaths() { Context::get()->getConfig()->set('working_path', '/foo'); within('/bar', function () { $withinWorkingPath = Context::get()->getConfig()->get('working_path'); self::assertEquals('/bar', $withinWorkingPath); }); $originalWorkingPath = Context::get()->getConfig()->get('working_path'); self::assertEquals('/foo', $originalWorkingPath); } public function testWithinRestoresWorkingPathInCaseOfException() { Context::get()->getConfig()->set('working_path', '/foo'); try { within('/bar', function () { throw new \Exception('Dummy exception'); }); } catch (\Exception $exception) { // noop } $originalWorkingPath = Context::get()->getConfig()->get('working_path'); self::assertEquals('/foo', $originalWorkingPath); } public function testWithinReturningValue() { $output = within('/foo', function () { return 'bar'; }); self::assertEquals('bar', $output); } public function testWithinWithVoidFunction() { $output = within('/foo', function () { // noop }); self::assertNull($output); } private function taskToNames($tasks) { return array_map(function (Task $task) { return $task->getName(); }, $tasks); } } ================================================ FILE: tests/src/Host/ConfigurationTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Host; use Deployer\Configuration; use Deployer\Exception\ConfigurationException; use PHPUnit\Framework\TestCase; class ConfigurationTest extends TestCase { public function testConfiguration() { $config = new Configuration(); $config->set('int', 42); $config->set('string', 'value'); $config->set('array', [1, 'two']); $config->set('hyphen-ated', 'hyphen'); $config->set('parse', 'is {{int}}'); $config->set('parse-hyphen', 'has {{hyphen-ated}}'); $config->set('callback', function () { return 'callback'; }); $this->assertEquals(42, $config->get('int')); $this->assertEquals('value', $config->get('string')); $this->assertEquals([1, 'two'], $config->get('array')); $this->assertEquals('default', $config->get('no', 'default')); $this->assertEquals(null, $config->get('no', null)); $this->assertEquals('callback', $config->get('callback')); $this->assertEquals('is 42', $config->get('parse')); $this->assertEquals('has hyphen', $config->get('parse-hyphen')); $config->set('int', 11); $this->assertEquals('is 11', $config->get('parse')); $this->expectException('RuntimeException'); $config->get('so'); } public function testAddParams() { $config = new Configuration(); $config->set('config', [ 'one', 'two' => 2, 'nested' => [], ]); $config->add('config', [ 'two' => 20, 'nested' => [ 'first', ], ]); $config->add('config', [ 'nested' => [ 'second', ], ]); $config->add('config', [ 'extra', ]); $expected = [ 'one', 'two' => 20, 'nested' => [ 'first', 'second', ], 'extra', ]; $this->assertEquals($expected, $config->get('config')); } public function testAddParamsToNotArray() { $this->expectException(ConfigurationException::class); $config = new Configuration(); $config->set('config', 'option'); $config->add('config', ['three']); } } ================================================ FILE: tests/src/Host/HostTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Host; use Deployer\Configuration; use PHPUnit\Framework\TestCase; class HostTest extends TestCase { public function testHost() { $host = new Host('host'); $host ->setHostname('hostname') ->setRemoteUser('remote_user') ->setPort(22) ->setConfigFile('~/.ssh/config') ->setIdentityFile('~/.ssh/id_rsa') ->setForwardAgent(true) ->setSshMultiplexing(true); self::assertEquals('host', $host->getAlias()); self::assertStringContainsString('host', $host->getTag()); self::assertEquals('hostname', $host->getHostname()); self::assertEquals('remote_user', $host->getRemoteUser()); self::assertEquals(22, $host->getPort()); self::assertEquals('~/.ssh/config', $host->getConfigFile()); self::assertEquals('~/.ssh/id_rsa', $host->getIdentityFile()); self::assertEquals(true, $host->getForwardAgent()); self::assertEquals(true, $host->getSshMultiplexing()); } public function testConfigurationAccessor() { $host = new Host('host'); $host ->set('roles', ['db', 'app']) ->set('key', 'value') ->set('array', [1]) ->add('array', [2]); self::assertEquals(['db', 'app'], $host->get('roles')); self::assertEquals('value', $host->get('key')); self::assertEquals([1, 2], $host->get('array')); } public function testHostAlias() { $host = new Host('host/alias'); self::assertEquals('host/alias', $host->getAlias()); self::assertEquals('host', $host->getHostname()); } public function testHostWithParams() { $host = new Host('host'); $value = 'new_value'; $host ->set('env', $value) ->set('identity_file', '{{env}}'); self::assertEquals($value, $host->getIdentityFile()); } public function testHostWithUserFromConfig() { $parent = new Configuration(); $parent->set("deploy_user", function () { return "test_user"; }); $host = new Host('host'); $host->config()->bind($parent); $host ->setHostname('host') ->setRemoteUser('{{deploy_user}}') ->setPort(22); self::assertEquals('test_user@host', $host->connectionString()); } } ================================================ FILE: tests/src/Host/RangeTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Host; use PHPUnit\Framework\TestCase; class RangeTest extends TestCase { public function testExpand() { self::assertEquals(['h1', 'h2', 'h3'], Range::expand(['h[1:3]'])); self::assertEquals(['h1', 'h2', 'ha'], Range::expand(['h[1:2]', 'ha'])); self::assertEquals(['h0', 'h1'], Range::expand(['h[0:1]'])); self::assertEquals(['h1'], Range::expand(['h[1:1]'])); self::assertEquals(['ha', 'hb', 'hc', 'hd'], Range::expand(['h[a:d]'])); $hostnames = Range::expand(['h[01:20]']); self::assertContains('h01', $hostnames); self::assertContains('h10', $hostnames); self::assertContains('h20', $hostnames); self::assertCount(100, Range::expand(['h[1:100]'])); self::assertCount(26, Range::expand(['h[a:z]'])); } } ================================================ FILE: tests/src/Importer/ImporterTest.php ================================================ previousInput = $deployer->input; $this->previousOutput = $deployer->output; } public function tearDown(): void { Deployer::get()->input = $this->previousInput; Deployer::get()->output = $this->previousOutput; } public function testCanOneOverrideStaticMethod(): void { $extendedImporter = new class extends Importer { public static $config = []; protected static function config(array $config) { static::$config = $config; } }; $data = << 'bar'], $extendedImporter::$config); } public function testImporterIgnoresYamlHiddenKeys(): void { $data = <<hosts->has('production')); self::assertTrue(Deployer::get()->hosts->has('acceptance')); self::assertTrue(Deployer::get()->hosts->has('production.beta')); self::assertEquals('acceptance', Deployer::get()->hosts->get('acceptance')->getLabels()['stage']); self::assertEquals('production', Deployer::get()->hosts->get('production')->getLabels()['stage']); self::assertEquals('foo', Deployer::get()->hosts->get('acceptance')->getRemoteUser()); self::assertEquals('bar', Deployer::get()->hosts->get('production')->getRemoteUser()); } } ================================================ FILE: tests/src/Selector/SelectorTest.php ================================================ set('labels', ['stage' => 'prod']); $front = (new Host('prod.domain.com/front'))->set('labels', ['stage' => 'prod', 'tier' => 'frontend']); $beta = (new Host('beta.domain.com'))->set('labels', ['stage' => 'beta']); $dev = (new Host('dev'))->set('labels', ['stage' => 'dev']); $multi = (new Host('multi'))->set('labels', ['stage' => ['prod', 'beta']]); $allHosts = [$prod, $front, $beta, $dev, $multi]; $hosts = new HostCollection(); foreach ($allHosts as $host) { $hosts->set($host->getAlias(), $host); } $selector = new Selector($hosts); self::assertEquals($allHosts, $selector->select('all')); self::assertEquals([$prod, $front, $multi], $selector->select('stage=prod')); self::assertEquals([$front], $selector->select('stage=prod & tier=frontend')); self::assertEquals([$front, $beta, $multi], $selector->select('prod.domain.com/front, stage=beta')); self::assertEquals([$prod, $beta, $dev, $multi], $selector->select('all & tier != frontend')); self::assertEquals([$prod, $front, $dev], $selector->select('stage != beta')); } } ================================================ FILE: tests/src/Ssh/IOArgumentsTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Support; use PHPUnit\Framework\TestCase; class HelpersTest extends TestCase { public function testArrayFlatten() { self::assertEquals(['a', 'b', 'c'], array_flatten(['a', ['b', 'key' => ['c']]])); } public function testArrayMergeAlternate() { $config = [ 'one', 'two' => 2, 'nested' => [], ]; $config = array_merge_alternate($config, [ 'two' => 20, 'nested' => [ 'first', ], ]); $config = array_merge_alternate($config, [ 'nested' => [ 'second', ], ]); $config = array_merge_alternate($config, [ 'extra', ]); self::assertEquals([ 'one', 'two' => 20, 'nested' => [ 'first', 'second', ], 'extra', ], $config); } public function testParseHomeDir() { $this->assertStringStartsWith('/', parse_home_dir('~/path')); $this->assertStringStartsWith('/', parse_home_dir('~')); $this->assertStringStartsWith('~', parse_home_dir('~path')); $this->assertStringEndsWith('~', parse_home_dir('path~')); } public function testEscapeShellArgument() { $this->assertEquals('\'{"foobar":"Lorem ipsum\'\\\'\'s dolor"}\'', escape_shell_argument(json_encode(['foobar' => 'Lorem ipsum\'s dolor']))); } } ================================================ FILE: tests/src/Support/ObjectProxyTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Support; use PHPUnit\Framework\TestCase; class ObjectProxyTest extends TestCase { public function testObjectProxy() { $mock = self::getMockBuilder('stdClass') ->addMethods(['foo']) ->getMock(); $mock ->expects(self::once()) ->method('foo') ->with('a', 'b'); $proxy = new ObjectProxy([$mock]); $proxy->foo('a', 'b'); } } ================================================ FILE: tests/src/Task/ContextTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Task; use Deployer\Configuration; use Deployer\Host\Host; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class ContextTest extends TestCase { public function testContext() { $host = $this->getMockBuilder(Host::class)->disableOriginalConstructor()->getMock(); $host ->expects($this->once()) ->method('config') ->willReturn($this->createMock(Configuration::class)); $context = new Context($host); $this->assertInstanceOf(Host::class, $context->getHost()); $this->assertInstanceOf(Configuration::class, $context->getConfig()); Context::push($context); $this->assertEquals($context, Context::get()); $this->assertEquals($context, Context::pop()); } } ================================================ FILE: tests/src/Task/ScriptManagerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Task; use PHPUnit\Framework\TestCase; class ScriptManagerTest extends TestCase { public function testGetTasks() { $notify = new Task('notify'); $info = new GroupTask('info', ['notify']); $deploy = new GroupTask('deploy', ['deploy:setup', 'deploy:release']); $deploy->addBefore($info); $setup = new Task('deploy:setup'); $release = new Task('deploy:release'); $taskCollection = new TaskCollection(); $taskCollection->set($notify->getName(), $notify); $taskCollection->set($info->getName(), $info); $taskCollection->set($deploy->getName(), $deploy); $taskCollection->set($setup->getName(), $setup); $taskCollection->set($release->getName(), $release); $scriptManager = new ScriptManager($taskCollection); self::assertEquals([$notify, $setup, $release], $scriptManager->getTasks('deploy')); } public function testOnce() { $a = new Task('a'); $b = new Task('b'); $b->once(); $group = new GroupTask('group', ['a', 'b']); $taskCollection = new TaskCollection(); $taskCollection->add($a); $taskCollection->add($b); $taskCollection->add($group); $scriptManager = new ScriptManager($taskCollection); self::assertEquals([$a, $b], $scriptManager->getTasks('group')); self::assertFalse($a->isOnce()); self::assertTrue($b->isOnce()); $group->once(); self::assertEquals([$a, $b], $scriptManager->getTasks('group')); self::assertTrue($a->isOnce()); self::assertTrue($b->isOnce()); } public function testSelectsCombine() { $a = new Task('a'); $b = new Task('b'); $c = new Task('c'); $b->select('stage=beta'); $c->select('stage=alpha|beta & role=db'); $group = new GroupTask('group', ['a', 'b', 'c']); $taskCollection = new TaskCollection(); $taskCollection->add($a); $taskCollection->add($b); $taskCollection->add($c); $taskCollection->add($group); $scriptManager = new ScriptManager($taskCollection); self::assertEquals([$a, $b, $c], $scriptManager->getTasks('group')); self::assertNull($a->getSelector()); self::assertEquals([[['=', 'stage', ['beta']]]], $b->getSelector()); self::assertEquals([[['=', 'stage', ['alpha', 'beta']],['=', 'role', ['db']]]], $c->getSelector()); $group->select('role=prod'); self::assertEquals([$a, $b, $c], $scriptManager->getTasks('group')); self::assertEquals([[['=', 'role', ['prod']]]], $a->getSelector()); self::assertEquals([[['=', 'stage', ['beta']]],[['=', 'role', ['prod']]]], $b->getSelector()); self::assertEquals([[['=', 'stage', ['alpha', 'beta']],['=', 'role', ['db']]],[['=', 'role', ['prod']]]], $c->getSelector()); } public function testThrowsExceptionIfTaskCollectionEmpty() { self::expectException(\InvalidArgumentException::class); $scriptManager = new ScriptManager(new TaskCollection()); $scriptManager->getTasks(''); } public function testThrowsExceptionIfTaskDontExists() { self::expectException(\InvalidArgumentException::class); $taskCollection = new TaskCollection(); $taskCollection->set('testTask', new Task('testTask')); $scriptManager = new ScriptManager($taskCollection); $scriptManager->getTasks('testTask2'); } } ================================================ FILE: tests/src/Task/TaskTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Deployer\Task; use Deployer\Host\Host; use PHPUnit\Framework\TestCase; use function Deployer\invoke; use function Deployer\task; class TaskTest extends TestCase { protected function tearDown(): void { StubTask::$runned = 0; } public function testTask() { $mock = self::getMockBuilder('stdClass') ->addMethods(['callback']) ->getMock(); $mock ->expects(self::exactly(1)) ->method('callback'); $task = new Task('task_name', function () use ($mock) { $mock->callback(); }); $context = self::getMockBuilder(Context::class) ->disableOriginalConstructor() ->getMock(); $task->run($context); self::assertEquals('task_name', $task->getName()); $task->desc('Task description.'); self::assertEquals('Task description.', $task->getDescription()); $task->hidden(); self::assertTrue($task->isHidden()); $task->once(); self::assertTrue($task->isOnce()); $task->oncePerNode(); self::assertTrue($task->isOncePerNode()); } public function testInit() { $context = self::getMockBuilder(Context::class)->disableOriginalConstructor()->getMock(); // Test create task with [$object, 'method'] $mock1 = self::getMockBuilder('stdClass') ->addMethods(['callback']) ->getMock(); $mock1 ->expects(self::once()) ->method('callback'); $task1 = new Task('task1', [$mock1, 'callback']); $task1->run($context); // Test create task with anonymous functions $mock2 = self::getMockBuilder('stdClass') ->addMethods(['callback']) ->getMock(); $mock2 ->expects(self::once()) ->method('callback'); $task2 = new Task('task2', function () use ($mock2) { $mock2->callback(); }); $task2->run($context); self::assertEquals(0, StubTask::$runned); $task3 = new Task('task3', new StubTask()); $task3->run($context); self::assertEquals(1, StubTask::$runned); } public function testGroupInvoke(): void { $spy = new StubTask(); task('foo', $spy); task('bar', $spy); task('group', ['foo', 'bar']); (new Task('group:invoke', function () { invoke('group'); }))->run(new Context(new Host('localhost'))); $this->assertSame(2, StubTask::$runned); } } /** * Stub class for task callable by __invoke() */ class StubTask { public static $runned = 0; public function __invoke() { self::$runned++; } }