diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 247175d..27d5c73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,21 +13,13 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -env: - PUBLISH: ${{ github.event_name == 'push' - && (startsWith(github.ref, 'refs/tags/0') - || startsWith(github.ref, 'refs/tags/1') - || startsWith(github.ref, 'refs/tags/2') - || startsWith(github.ref, 'refs/tags/3') - || startsWith(github.ref, 'refs/tags/4') - || startsWith(github.ref, 'refs/tags/5') - || startsWith(github.ref, 'refs/tags/6') - || startsWith(github.ref, 'refs/tags/7') - || startsWith(github.ref, 'refs/tags/8') - || startsWith(github.ref, 'refs/tags/9')) }} - jobs: - docker: + + ############ + # Building # + ############ + + build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -35,79 +27,154 @@ jobs: fetch-depth: 0 - uses: docker/setup-buildx-action@v2 - - uses: satackey/action-docker-layer-caching@v0.0.11 - continue-on-error: true - if: ${{ env.PUBLISH != 'true' && github.ref != 'refs/heads/main' }} - - run: make docker.image no-cache=no tag=build-${{ github.run_number }} - if: ${{ env.PUBLISH != 'true' && github.ref != 'refs/heads/main' }} + - run: make docker.image no-cache=yes + tag=build-${{ github.run_number }} + + - run: make docker.tar to-file=image.tar + tags=build-${{ github.run_number }} + - uses: actions/upload-artifact@v3 + with: + name: image-${{ github.run_number }} + path: image.tar + retention-days: 1 + - - run: make docker.image no-cache=yes tag=build-${{ github.run_number }} - if: ${{ env.PUBLISH == 'true' || github.ref == 'refs/heads/main' }} + + ########### + # Testing # + ########### + + test: + needs: ["build"] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 - run: make npm.install - - run: make test.docker tag=build-${{ github.run_number }} - - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + - uses: actions/download-artifact@v3 with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - if: ${{ env.PUBLISH == 'true' }} - - name: Login to Quay.io - uses: docker/login-action@v2 + name: image-${{ github.run_number }} + - run: make docker.untar from-file=image.tar + + - run: make test.docker + tag=build-${{ github.run_number }} + + + + + ############# + # Releasing # + ############# + + release-docker: + name: Release Docker image + needs: ["build", "test"] + if: ${{ github.event_name == 'push' + && startsWith(github.ref, 'refs/tags/') }} + strategy: + fail-fast: false + matrix: + registry: ["docker.io", "ghcr.io", "quay.io"] + runs-on: ubuntu-latest + steps: + # Skip if this is fork and no credentials are provided. + - id: skip + run: echo ::set-output name=no::${{ !( + github.repository_owner != 'instrumentisto' + && ((matrix.registry == 'quay.io' + && secrets.QUAYIO_ROBOT_USER == '') + || (matrix.registry == 'docker.io' + && secrets.DOCKERHUB_BOT_USER == '')) + ) }} + + - uses: actions/checkout@v3 + if: ${{ steps.skip.outputs.no == 'true' }} + + - name: Parse Docker image name from Git repository name + id: image + uses: actions-ecosystem/action-regex-match@v2 with: - registry: quay.io - username: instrumentisto+bot - password: ${{ secrets.QUAYIO_ROBOT_TOKEN }} - if: ${{ env.PUBLISH == 'true' }} - - name: Login to Docker Hub + text: ${{ github.repository }} + regex: '^${{ github.repository_owner }}/(.+)-docker-image$' + + - uses: actions/download-artifact@v3 + with: + name: image-${{ github.run_number }} + if: ${{ steps.skip.outputs.no == 'true' }} + - run: make docker.untar from-file=image.tar + if: ${{ steps.skip.outputs.no == 'true' }} + + - name: Login to ${{ matrix.registry }} container registry uses: docker/login-action@v2 with: - username: instrumentistobot - password: ${{ secrets.DOCKERHUB_BOT_PASS }} - if: ${{ env.PUBLISH == 'true' }} + registry: ${{ matrix.registry }} + username: ${{ (matrix.registry == 'docker.io' + && secrets.DOCKERHUB_BOT_USER) + || (matrix.registry == 'quay.io' + && secrets.QUAYIO_ROBOT_USER) + || github.repository_owner }} + password: ${{ (matrix.registry == 'docker.io' + && secrets.DOCKERHUB_BOT_PASS) + || (matrix.registry == 'quay.io' + && secrets.QUAYIO_ROBOT_TOKEN) + || secrets.GITHUB_TOKEN }} + if: ${{ steps.skip.outputs.no == 'true' }} - - run: make docker.tags of=build-${{ github.run_number }} - if: ${{ env.PUBLISH == 'true' }} + - run: make docker.tags + registries=${{ matrix.registry }} + of=build-${{ github.run_number }} + if: ${{ steps.skip.outputs.no == 'true' }} - run: make docker.push - if: ${{ env.PUBLISH == 'true' }} + registries=${{ matrix.registry }} + if: ${{ steps.skip.outputs.no == 'true' }} # On GitHub Container Registry README is automatically updated on pushes. - - name: Update README on Quay.io + - name: Update README on Docker Hub uses: christian-korneck/update-container-description-action@v1 - env: - DOCKER_APIKEY: ${{ secrets.QUAYIO_API_TOKEN }} with: - provider: quay - destination_container_repo: quay.io/instrumentisto/pure-ftpd + provider: dockerhub + destination_container_repo: ${{ github.repository_owner }}/${{ steps.image.outputs.group1 }} readme_file: README.md - if: ${{ env.PUBLISH == 'true' }} - - name: Update README on Docker Hub - uses: christian-korneck/update-container-description-action@v1 env: - DOCKER_USER: instrumentistobot + DOCKER_USER: ${{ secrets.DOCKERHUB_BOT_USER }} DOCKER_PASS: ${{ secrets.DOCKERHUB_BOT_PASS }} + if: ${{ steps.skip.outputs.no == 'true' + && matrix.registry == 'docker.io' }} + - name: Update README on Quay.io + uses: christian-korneck/update-container-description-action@v1 with: - provider: dockerhub - destination_container_repo: instrumentisto/pure-ftpd + provider: quay + destination_container_repo: ${{ matrix.registry }}/${{ github.repository_owner }}/${{ steps.image.outputs.group1 }} readme_file: README.md - if: ${{ env.PUBLISH == 'true' }} + env: + DOCKER_APIKEY: ${{ secrets.QUAYIO_API_TOKEN }} + if: ${{ steps.skip.outputs.no == 'true' + && matrix.registry == 'quay.io' }} + + release-github: + name: Release on GitHub + needs: ["release-docker"] + if: ${{ github.event_name == 'push' + && startsWith(github.ref, 'refs/tags/') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 - - name: Parse release version from Git tag - id: release - run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/} - if: ${{ env.PUBLISH == 'true' }} + - name: Parse semver versions from Git tag + id: semver + uses: actions-ecosystem/action-regex-match@v2 + with: + text: ${{ github.ref }} + regex: '^refs/tags/((([0-9]+)\.[0-9]+)\.[0-9]+-(.+))$' - name: Parse CHANGELOG link id: changelog - run: echo ::set-output name=LINK::https://github.com/${{ github.repository }}/blob/${{ steps.release.outputs.VERSION }}/CHANGELOG.md#$(sed -n '/^## \[${{ steps.release.outputs.VERSION }}\]/{s/^## \[\(.*\)\][^0-9]*\([0-9].*\)/\1--\2/;s/[^0-9a-z-]*//g;p;}' CHANGELOG.md) - if: ${{ env.PUBLISH == 'true' }} - - name: Release on GitHub + run: echo ::set-output + name=LINK::https://github.com/${{ github.repository }}/blob/${{ steps.semver.outputs.group1 }}/CHANGELOG.md#$(sed -n '/^## \[${{ steps.semver.outputs.group1 }}\]/{s/^## \[\(.*\)\][^0-9]*\([0-9].*\)/\1--\2/;s/[^0-9a-z-]*//g;p;}' CHANGELOG.md) + + - name: Create GitHub release uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - name: ${{ steps.release.outputs.VERSION }} + name: ${{ steps.semver.outputs.group1 }} body: | [Changelog](${{ steps.changelog.outputs.LINK }}) - if: ${{ env.PUBLISH == 'true' }} diff --git a/.gitignore b/.gitignore index 3f4c570..117b113 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ /.idea/ +/.vscode/ /*.iml .DS_Store +/.cache/ /node_modules/ /package-lock.json /yarn.lock diff --git a/Makefile b/Makefile index 5074eb0..f722458 100644 --- a/Makefile +++ b/Makefile @@ -24,9 +24,9 @@ BUILD_REV ?= $(strip \ NAME := pure-ftpd OWNER := $(or $(GITHUB_REPOSITORY_OWNER),instrumentisto) -NAMESPACES := $(OWNER) \ - ghcr.io/$(OWNER) \ - quay.io/$(OWNER) +REGISTRIES := $(strip $(subst $(comma), ,\ + $(shell grep -m1 'registry: \["' .github/workflows/ci.yml \ + | cut -d':' -f2 | tr -d '"]['))) TAGS ?= $(PURE_FTPD_VER)-r$(BUILD_REV) \ $(PURE_FTPD_VER) \ $(strip $(shell echo $(PURE_FTPD_VER) | cut -d '.' -f1,2)) \ @@ -58,8 +58,8 @@ test: test.docker # Docker commands # ################### -docker-namespaces = $(strip $(if $(call eq,$(namespaces),),\ - $(NAMESPACES),$(subst $(comma), ,$(namespaces)))) +docker-registries = $(strip $(if $(call eq,$(registries),),\ + $(REGISTRIES),$(subst $(comma), ,$(registries)))) docker-tags = $(strip $(if $(call eq,$(tags),),\ $(TAGS),$(subst $(comma), ,$(tags)))) @@ -93,16 +93,16 @@ docker.image: # # Usage: # make docker.push [tags=($(TAGS)|[,...])] -# [namespaces=($(NAMESPACES)|[,...])] +# [registries=($(REGISTRIES)|[,...])] docker.push: $(foreach tag,$(subst $(comma), ,$(docker-tags)),\ - $(foreach namespace,$(subst $(comma), ,$(docker-namespaces)),\ - $(call docker.push.do,$(namespace),$(tag)))) + $(foreach registry,$(subst $(comma), ,$(docker-registries)),\ + $(call docker.push.do,$(registry),$(tag)))) define docker.push.do $(eval repo := $(strip $(1))) $(eval tag := $(strip $(2))) - docker push $(repo)/$(NAME):$(tag) + docker push $(repo)/$(OWNER)/$(NAME):$(tag) endef @@ -111,23 +111,47 @@ endef # Usage: # make docker.tags [of=($(VERSION)|)] # [tags=($(TAGS)|[,...])] -# [namespaces=($(NAMESPACES)|[,...])] +# [registries=($(REGISTRIES)|[,...])] docker.tags: $(foreach tag,$(subst $(comma), ,$(docker-tags)),\ - $(foreach namespace,$(subst $(comma), ,$(docker-namespaces)),\ - $(call docker.tags.do,$(or $(of),$(VERSION)),$(namespace),$(tag)))) + $(foreach registry,$(subst $(comma), ,$(docker-registries)),\ + $(call docker.tags.do,$(or $(of),$(VERSION)),$(registry),$(tag)))) define docker.tags.do $(eval from := $(strip $(1))) $(eval repo := $(strip $(2))) $(eval to := $(strip $(3))) - docker tag $(OWNER)/$(NAME):$(from) $(repo)/$(NAME):$(to) + docker tag $(OWNER)/$(NAME):$(from) $(repo)/$(OWNER)/$(NAME):$(to) endef +# Save Docker images to a tarball file. +# +# Usage: +# make docker.tar [to-file=(.cache/image.tar|)] +# [tags=($(VERSION)|[,...])] + +docker-tar-file = $(or $(to-file),.cache/image.tar) + +docker.tar: + @mkdir -p $(dir $(docker-tar-file)) + docker save -o $(docker-tar-file) \ + $(foreach tag,$(subst $(comma), ,$(or $(tags),$(VERSION))),\ + $(OWNER)/$(NAME):$(tag)) + + docker.test: test.docker +# Load Docker images from a tarball file. +# +# Usage: +# make docker.untar [from-file=(.cache/image.tar|)] + +docker.untar: + docker load -i $(or $(from-file),.cache/image.tar) + + #################### @@ -140,7 +164,7 @@ docker.test: test.docker # https://github.com/bats-core/bats-core # # Usage: -# make test.docker [tag=($(VERSION)|)] +# make test.docker [tag=($(VERSION)|)] test.docker: ifeq ($(wildcard node_modules/.bin/bats),) @@ -201,7 +225,8 @@ endif ################## .PHONY: image push release tags test \ - docker.image docker.push docker.tags docker.test \ + docker.image docker.push docker.tags docker.tar docker.test \ + docker.untar \ git.release \ npm.install \ test.docker