From ecb42a154fa5c46cd2315e4cc9e824aebb83f78f Mon Sep 17 00:00:00 2001 From: renaud gaudin Date: Tue, 22 Dec 2020 15:00:59 +0000 Subject: [PATCH] using regexp for pattern --- .github/workflows/ci.yml | 2 +- .gitignore | 4 +++ README.md | 65 +++++++++++++++++++++++++++++++++++++++- action.yml | 9 ++++-- compute_tags.py | 14 +++++++-- tests.py | 52 +++++++++++++++++++++++++++----- 6 files changed, 133 insertions(+), 13 deletions(-) create mode 100644 .gitignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51958fd..f133297 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: - name: Install dependencies run: python -m pip install -U pytest black - name: Black - run: black -c compute_tags.py + run: black -c compute_tags.py build_push.py - name: Tests run: pytest tests.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22e9769 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.pyc +__pycache__ +*.zim +*.egg-info/ diff --git a/README.md b/README.md index 01810ac..5aa1670 100644 --- a/README.md +++ b/README.md @@ -1 +1,64 @@ -# openzim-docker-action \ No newline at end of file +# docker-publish-action + +[![GitHub release](https://img.shields.io/github/release/openzim/docker-publish-action.svg)](https://github.com/openzim/docker-publish-action/releases/latest) +[![CI workflow](https://img.shields.io/github/workflow/status/openzim/docker-publish-action/CI?label=CI&logo=github)](https://github.com/openzim/docker-publish-action/actions?workflow=CI) + +A Github Action to automatically build and publish Openzim's images to **Both Docker Hub and Github Container Regisry**. + + +## Requirements + +On ghcr.io, as for Docker Hub, first part of image name is the *user* owning the image. The user or organization must have enabled *Improved container support* first. Users do that in Settings > Feature preview and Organizations in Settings > Packages. + +⚠️ this action is tailored for Openzim's workflow only. Use at your own risk. + +## Usage + + +```yaml +name: Docker + +on: + push: + branches: + - master + tags: + - v* + +jobs: + build-and-push: + name: Deploy Docker Image + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - name: Build and push + uses: openzim/docker-publish-action@v1 + with: + image-path: openzim/zimit + on-master: dev + tag-pattern: /^v*([0-9.]+)$/ + latest-on-tag: true + restrict-to: openzim/zimit + hub-username: ${{ secrets.DOCKERHUB_USERNAME }} + hub-password: ${{ secrets.DOCKERHUB_PASSWORD }} + ghcr-username: ${{ secrets.GHCR_USERNAME }} + ghcr-token: ${{ secrets.GHCR_TOKEN }} + +``` + +**Note**: th top-part `on` is just a filter on running that workflow. You can omit it but it's safer to not run it on refs that you know won't trigger anything. See [documentation](https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#on). + +| Input | Usage | +| :--- | :--- | +| `image-path` | **Name of your image on the registry** (without the version part).
Ex.: `openzim/zimit` would refer to [this image](https://hub.docker.com/r/openzim/zimit).
The same name is pushed on **both registries**. | +| `context` | **Path in the repository to use as build context**
Relative to repository root. Defaults to `.`. Ex: `dnscache` or `workers/slave`. | +| `dockerfile` | **Path to the Dockerfile recipe, relative to context**
Defaults to `Dockerfile`. Use `../` syntax if dockerfile is outside context. | +| `on-master` | **Tag to apply for every commit on default branch**.
Omit it if you don't want to push an image for non-tagged commits.
Only applies to commits on your default branch (`master` or `main`) | +| `tag-pattern` | **Regular expression to match tags with**.
Only git tags matching this regexp will trigger a build+push to the corresponding docker tag.
If not specifying a group, whole git tag is used as is on docker. | +| `latest-on-tag` | **Whether to push to docker tag `:latest` on every matched tag** (see `tag-pattern`)
Value must be `true` or `false`. Defaults to `false`. | +| `restrict-to` | **Don't push if action is run for a different repository**
Specify as `{owner}/{repository}`. | +| `hub-username` and `hub-password` | **Docker Hub user credentials to push images with** | +| `ghcr-username` and `ghcr-token` | **Github user credentials to push images with**
Token is a [PAT](https://github.com/settings/tokens) with `repo, workflow, write:packages` permissions.| + + +⚠️ After your initial run creating your image, you need to manually **make it public** via Github's UI (see packages) if you intend to pull images without authenticating. diff --git a/action.yml b/action.yml index 788a9c4..2e422e4 100644 --- a/action.yml +++ b/action.yml @@ -1,5 +1,10 @@ -name: Openzim Docker Action -description: Build and publish Docker Images +name: Openzim Docker Publish Action +description: Build and publish Docker Images to both Docker Hub and ghcr. +author: openzim +branding: + icon: package + color: green + inputs: image-path: description: target image path on both registries (ex. 'openzim/dnscache') diff --git a/compute_tags.py b/compute_tags.py index 8a677c5..6154316 100644 --- a/compute_tags.py +++ b/compute_tags.py @@ -4,6 +4,8 @@ import re import sys +perl_re = re.compile(r"^/(.+)/$") + if os.getenv("RESTRICT_TO") and os.getenv("GITHUB_REPOSITORY") != os.getenv( "RESTRICT_TO" ): @@ -22,10 +24,18 @@ ref = os.getenv("GITHUB_REF").split("/", 2)[-1] is_tag = os.getenv("GITHUB_REF").startswith("refs/tags/") if is_tag: - exp = os.getenv("TAG_PATTERN", "").replace("*", "(.+)") + exp = os.getenv("TAG_PATTERN", "") + # convert from perl syntax (/pattern/) to python one + if perl_re.match(exp): + exp = perl_re.match(exp).groups()[-1] res = re.match(exp, ref) if res: - version_tags.append(res.groups()[0]) + if res.groups(): + # we have a matching tag with a group, use the group part + version_tags.append(res.groups()[0]) + else: + # we have a matching tag without a group, use git tag + version_tags.append(ref) if os.getenv("LATEST_ON_TAG", "").lower() == "true": version_tags.append("latest") diff --git a/tests.py b/tests.py index 8cd3dbd..793494b 100644 --- a/tests.py +++ b/tests.py @@ -74,7 +74,7 @@ def test_dnscache_main(github_env, repo_name): repo_name="openzim/zimfarm", image_path="openzim/dnscache", on_master="latest", - tag_pattern="dnscache-v*", + tag_pattern="/^dnscache-v([0-9.]+)$/", restrict_to="openzim/zimfarm", is_on_main_branch=True, ) @@ -90,7 +90,7 @@ def test_dnscache_tag(github_env, repo_name): repo_name="openzim/zimfarm", image_path="openzim/dnscache", on_master="latest", - tag_pattern="dnscache-v*", + tag_pattern="/^dnscache-v([0-9.]+)$/", restrict_to="openzim/zimfarm", is_on_main_branch=True, is_tag="dnscache-v1.1", @@ -107,7 +107,7 @@ def test_dnscache_tag_and_latest(github_env, repo_name): repo_name="openzim/zimfarm", image_path="openzim/dnscache", on_master="latest", - tag_pattern="dnscache-v*", + tag_pattern="/^dnscache-v([0-9.]+)$/", restrict_to="openzim/zimfarm", is_on_main_branch=True, is_tag="dnscache-v1.1", @@ -126,7 +126,7 @@ def test_restrict_to(github_env): repo_name="rgaudin/test", image_path="openzim/dnscache", on_master="latest", - tag_pattern="dnscache-v*", + tag_pattern="/^dnscache-v([0-9.]+)$/", restrict_to="openzim/zimfarm", is_on_main_branch=True, ) @@ -141,7 +141,7 @@ def test_not_is_on_main_branch(github_env, repo_name): repo_name=repo_name, image_path="owner/image", on_master="latest", - tag_pattern="dnscache-v*", + tag_pattern="/^dnscache-v([0-9.]+)$/", is_on_main_branch=False, ) ) @@ -170,7 +170,7 @@ def test_zimit_main(github_env, repo_name): repo_name="openzim/zimit", image_path="openzim/zimit", on_master="dev", - tag_pattern="v*", + tag_pattern="v([0-9.]+)", restrict_to="openzim/zimit", is_on_main_branch=True, ) @@ -187,7 +187,7 @@ def test_zimit_tag(github_env, repo_name): repo_name="openzim/zimit", image_path="openzim/zimit", on_master="dev", - tag_pattern="v*", + tag_pattern="v([0-9.]+)", restrict_to="openzim/zimit", is_on_main_branch=True, is_tag="v1.1", @@ -212,3 +212,41 @@ def test_no_tag_on_master(github_env, repo_name): ) ) assert len(res) == 0 + + +@pytest.mark.parametrize( + "tag_pattern, tag, expected", + [ + # no group + ("v.+", "v1", "v1"), + ("v.+", "v1.1", "v1.1"), + # group + ("v([0-9.]+)", "v1", "1"), + ("v([0-9.]+)", "v1.1", "1.1"), + # caret for start + ("^v([0-9.]+)", "v1", "1"), + ("^v([0-9.]+)", "v1.1", "1.1"), + # dollar for end + ("^v([0-9.]+)$", "v1", "1"), + ("^v([0-9.]+)$", "v1.1", "1.1"), + # perl syntax + ("/v([0-9.]+)/", "v1", "1"), + ("/v([0-9.]+)/", "v1.1", "1.1"), + # perl with caret and dollar + ("/^v([0-9.]+)$/", "v1", "1"), + ("/^v([0-9.]+)$/", "v1.1", "1.1"), + ], + ) +def test_tag_patterns(github_env, repo_name, tag_pattern, tag, expected): + res = launch_and_retrieve( + **get_env( + github_env=github_env, + repo_name=repo_name, + image_path=repo_name, + tag_pattern=tag_pattern, + is_tag=tag, + ) + ) + assert len(res) == 2 + assert f"{repo_name}:{expected}" in res + assert f"ghcr.io/{repo_name}:{expected}" in res