diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 00000000..0367b5c3 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,55 @@ +name: Docker + +on: + workflow_dispatch: + inputs: + tagInput: + description: 'Tag' + required: true + + release: + types: [created] + tags: + - 'v*' + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - + name: Login to Docker Hub + uses: docker/login-action@v2 + with: + registry: docker.io + username: ${{ secrets.DOCKER_HUB_USER }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + - name: Determine version tag + id: version-tag + run: | + INPUT_VALUE="${{ github.event.inputs.tagInput }}" + if [ -z "$INPUT_VALUE" ]; then + INPUT_VALUE="${{ github.ref_name }}" + fi + echo "::set-output name=value::$INPUT_VALUE" + - + name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: | + tighten/takeout:latest + tighten/takeout:${{ steps.version-tag.outputs.value }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..258204bd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM php:8.1-cli-alpine + +ENV TAKEOUT_CONTAINER=1 + +COPY --from=docker/buildx-bin /buildx /usr/libexec/docker/cli-plugins/docker-buildx + +# Install the PHP extensions & Docker +RUN apk add --no-cache --update docker openrc ncurses \ + && docker-php-ext-configure pcntl --enable-pcntl \ + && docker-php-ext-install -j$(nproc) pcntl \ + && rc-update add docker boot + +WORKDIR /takeout + +COPY builds/takeout /usr/local/bin/takeout + +ENTRYPOINT ["takeout"] + diff --git a/README.md b/README.md index 6efff50e..96fbeb14 100644 --- a/README.md +++ b/README.md @@ -18,18 +18,17 @@ But you can also easily enable ElasticSearch, PostgreSQL, MSSQL, Mongo, Redis, a ## Requirements - macOS, Linux, Windows 10 or WSL2 -- [Composer](https://getcomposer.org/) installed - Docker installed (macOS: [Docker for Mac](https://docs.docker.com/docker-for-mac/), Windows: [Docker for Windows](https://docs.docker.com/docker-for-windows/)) ## Installation -Install Takeout with Composer by running: +To install Takeout locally, add this alias to your `~/.bashrc` (or similar): ```bash -composer global require "tightenco/takeout:~2.7" +alias takeout="docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -it tighten/takeout:latest" ``` -Make sure the `~/.composer/vendor/bin` directory is in your system's "PATH". +_Note: Previous versions of Takeout required installing it via `composer global require`. That's discouraged now and the Docker image is the preferred way._ ## Usage @@ -243,7 +242,6 @@ The best way to see our future plans is to check out the [Projects Board](https: If you're working with us and are assigned to push a release, here's the easiest process: - 1. Visit the [Takeout Releases page](https://github.com/tighten/takeout/releases); figure out what your next tag will be (increase the third number if it's a patch or fix; increase the second number if it's adding features) 2. On your local machine, pull down the latest version of `main` (`git checkout main && git pull`) 3. Build for the version you're targeting (`php ./takeout app:build`) @@ -252,4 +250,31 @@ If you're working with us and are assigned to push a release, here's the easiest 6. [Draft a new release](https://github.com/tighten/takeout/releases/new) with both the tag version and release title of your tag (e.g. `v1.5.1`) 7. Use the "Generate release notes" button to generate release notes from the merged PRs. 8. Hit `Publish release` -9. Profit 😆 +9. The new tag and release will trigger the [`docker-publish.yml`](.github/workflows/docker-publish.yml) workflow, which should take care of building and pushing the new image of the Docker container (see the "Building The Docker Image Manually" section below) +10. Profit 😆 + +## Building The Docker Image Manually + +The important thing is to remember to build both `linux/amd64` and `linux/arm64` images. We rely on Docker's `buildx` command, which uses Docker's [BuildKit](https://github.com/moby/buildkit) behind the scenes, which allows us to build for multiple platforms, independently of the platform of the machine building the image. + +You may build and publish a new version of the docker image using the following command: + +```bash +docker buildx build --platform=linux/amd64,linux/arm64 -t tighten/takeout:latest --push . +``` + +If it's the first time you're building the image, you may get the following error: + +``` +ERROR: Multiple platforms feature is currently not supported for docker driver. Please switch to a different driver (eg. "docker buildx create --use") +``` + +This means that you first need to create a builder container, which you maydo like so: + +```bash +docker buildx create --use +``` + +After that, retrying the `buildx` command should work. + +Please, note that building the container will simply copy the current version of the Takeout `phar` file at [builds/takeout](./builds/takeout) to inside the container and publish that, so make sure you have to most recent version built locally. If you don't, follow the release process to build the new version before building the Docker image. diff --git a/app/Shell/DockerTags.php b/app/Shell/DockerTags.php index 0c8bc015..c4dfa45d 100644 --- a/app/Shell/DockerTags.php +++ b/app/Shell/DockerTags.php @@ -44,22 +44,18 @@ public function getLatestTag(): string public function getTags(): Collection { $response = json_decode($this->getTagsResponse()->getContents(), true); + $platform = $this->platform(); return collect($response['results']) - ->when(in_array($platform, $this->armArchitectures, true), $this->armSupportedImagesOnlyFilter()) - ->when(! in_array($platform, $this->armArchitectures, true), $this->nonArmOnlySupportImagesFilter()) + ->when(in_array($platform, $this->armArchitectures, true), $this->onlyArmImagesFilter()) + ->when(! in_array($platform, $this->armArchitectures, true), $this->onlyNonArmImagesFilter()) ->pluck('name') ->sort(new VersionComparator) ->values(); } - /** - * Return a function intended to filter tags, ensuring images that do not support arm architecture are filtered out. - * - * @return callable - */ - protected function armSupportedImagesOnlyFilter() + protected function onlyArmImagesFilter() { return function ($tags) { return $tags->filter(function ($tag) { @@ -76,12 +72,7 @@ protected function armSupportedImagesOnlyFilter() }; } - /** - * Return a function intended to filter tags, that ensures are arm-only images are filtered out. - * - * @return callable - */ - protected function nonArmOnlySupportImagesFilter() + protected function onlyNonArmImagesFilter() { return function ($tags) { return $tags->filter(function ($tag) { diff --git a/tests/Feature/DockerTagsTest.php b/tests/Feature/DockerTagsTest.php index 64c54a0c..5efe064d 100644 --- a/tests/Feature/DockerTagsTest.php +++ b/tests/Feature/DockerTagsTest.php @@ -10,12 +10,19 @@ use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Response; use Mockery as M; -use Tests\Support\IntelDockerTags; -use Tests\Support\M1DockerTags; +use Tests\Support\FakePlatformDockerTags; use Tests\TestCase; class DockerTagsTest extends TestCase { + public static function armPlatforms(): array + { + return [ + [FakePlatformDockerTags::M1_ARM_PLATFORM], + [FakePlatformDockerTags::LINUX_ARM_PLATFORM], + ]; + } + /** @test */ function it_gets_the_latest_tag_not_named_latest() { @@ -45,14 +52,17 @@ function it_sorts_the_versions_naturally() $this->assertEquals('16.2', $tags->shift()); } - /** @test */ - function it_detects_arm64_based_images_when_running_on_arm64_based_host() + /** + * @test + * + * @dataProvider armPlatforms + */ + function it_detects_arm_based_images_when_running_on_arm64_based_host($platform) { $handlerStack = HandlerStack::create($this->mockImagesResponseHandler()); $client = new Client(['handler' => $handlerStack]); - /** @var DockerTags $dockerTags */ - $dockerTags = M::mock(M1DockerTags::class, [$client, app(MySql::class)])->makePartial(); + $dockerTags = (new FakePlatformDockerTags($client, app(MySql::class)))->withFakePlatform($platform); $this->assertEquals('1.0.0-arm64', $dockerTags->getLatestTag()); } @@ -63,8 +73,7 @@ function it_gets_latest_tag_on_intel_platform() $handlerStack = HandlerStack::create($this->mockImagesResponseHandler()); $client = new Client(['handler' => $handlerStack]); - /** @var DockerTags $dockerTags */ - $dockerTags = M::mock(IntelDockerTags::class, [$client, app(MySql::class)])->makePartial(); + $dockerTags = (new FakePlatformDockerTags($client, app(MySql::class)))->withFakePlatform(FakePlatformDockerTags::INTEL_ARM_PLATFORM); $this->assertEquals('1.0.0', $dockerTags->getLatestTag()); } @@ -77,13 +86,14 @@ private function mockImagesResponseHandler() [ 'name' => 'latest', 'images' => [ + ['architecture' => 'x86_64'], ['architecture' => 'amd64'], - ['architecture' => 'arm64'], ], ], [ 'name' => '1.0.0', 'images' => [ + ['architecture' => 'x86_64'], ['architecture' => 'amd64'], ], ], @@ -91,6 +101,7 @@ private function mockImagesResponseHandler() 'name' => '1.0.0-arm64', 'images' => [ ['architecture' => 'arm64'], + ['architecture' => 'aarch64'], ], ], ], diff --git a/tests/Support/FakePlatformDockerTags.php b/tests/Support/FakePlatformDockerTags.php new file mode 100644 index 00000000..dc4b7dc4 --- /dev/null +++ b/tests/Support/FakePlatformDockerTags.php @@ -0,0 +1,26 @@ +fakePlatform = $platform; + + return $this; + } + + protected function platform(): string + { + return $this->fakePlatform; + } +} diff --git a/tests/Support/IntelDockerTags.php b/tests/Support/IntelDockerTags.php deleted file mode 100644 index 423c0003..00000000 --- a/tests/Support/IntelDockerTags.php +++ /dev/null @@ -1,13 +0,0 @@ -