From 2f83320d17853b6fc5380472eb7daed634f90198 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 29 Mar 2021 13:04:53 +0200 Subject: [PATCH] v2 (#50) Co-authored-by: CrazyMax --- .github/workflows/ci.yml | 103 +++-- .github/workflows/test.yml | 11 +- .github/workflows/validate.yml | 25 -- README.md | 474 ++++++++++++++------ UPGRADE.md | 295 +++++++++++++ __tests__/flavor.test.ts | 115 +++++ __tests__/meta.test.ts | 773 ++++++++++++++++++++++++++++----- __tests__/tag.test.ts | 471 ++++++++++++++++++++ action.yml | 44 +- dist/index.js | 605 ++++++++++++++++++++++---- src/context.ts | 28 +- src/flavor.ts | 42 ++ src/main.ts | 28 +- src/meta.ts | 350 ++++++++++++--- src/tag.ts | 180 ++++++++ 15 files changed, 3016 insertions(+), 528 deletions(-) delete mode 100644 .github/workflows/validate.yml create mode 100644 UPGRADE.md create mode 100644 __tests__/flavor.test.ts create mode 100644 __tests__/tag.test.ts create mode 100644 src/flavor.ts create mode 100644 src/tag.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26e5b38..b5a8193 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,10 +5,14 @@ on: - cron: '0 */4 * * *' # every 4 hours push: branches: - - '**' + - 'master' + - 'releases/v*' tags: - 'v*.*.*' pull_request: + branches: + - 'master' + - 'releases/v*' env: DOCKER_IMAGE: localhost:5000/name/app @@ -27,7 +31,12 @@ jobs: images: | ${{ env.DOCKER_IMAGE }} ghcr.io/name/app - tag-sha: true + tags: | + type=schedule + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=sha tag-schedule: runs-on: ubuntu-latest @@ -50,8 +59,12 @@ jobs: images: | ${{ env.DOCKER_IMAGE }} ghcr.io/name/app - tag-sha: true - tag-schedule: ${{ matrix.tag-schedule }} + tags: | + type=schedule,pattern=${{ matrix.tag-schedule }} + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=sha tag-match: runs-on: ubuntu-latest @@ -76,18 +89,23 @@ jobs: images: | ${{ env.DOCKER_IMAGE }} ghcr.io/name/app - tag-sha: true - tag-match: ${{ matrix.tag-match }} - tag-match-group: ${{ matrix.tag-match-group }} + tags: | + type=schedule + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=match,"pattern=${{ matrix.tag-match }}",group=${{ matrix.tag-match-group }} + type=sha tag-semver: runs-on: ubuntu-latest strategy: fail-fast: false matrix: - tag-latest: - - 'true' - - 'false' + flavor-latest: + - "auto" + - "true" + - "false" steps: - name: Checkout @@ -99,13 +117,19 @@ jobs: images: | ${{ env.DOCKER_IMAGE }} ghcr.io/name/app - tag-semver: | - {{raw}} - {{version}} - {{major}}.{{minor}}.{{patch}} - tag-latest: ${{ matrix.tag-latest }} + tags: | + type=schedule + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=semver,pattern={{raw}} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}}.{{patch}} + type=sha + flavor: | + latest=${{ matrix.flavor-latest }} - label-custom: + flavor: runs-on: ubuntu-latest steps: - @@ -118,7 +142,24 @@ jobs: images: | ${{ env.DOCKER_IMAGE }} ghcr.io/name/app - label-custom: | + flavor: | + prefix=foo- + suffix=-bar + + labels: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v2 + - + name: Docker meta + uses: ./ + with: + images: | + ${{ env.DOCKER_IMAGE }} + ghcr.io/name/app + labels: | maintainer=CrazyMax org.opencontainers.image.title=MyCustomTitle org.opencontainers.image.description=Another description @@ -141,11 +182,15 @@ jobs: uses: ./ with: images: ${{ env.DOCKER_IMAGE }} - tag-sha: true - tag-semver: | - v{{version}} - v{{major}}.{{minor}} - v{{major}} + tags: | + type=schedule + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=semver,pattern=v{{version}} + type=semver,pattern=v{{major}}.{{minor}} + type=semver,pattern=v{{major}} + type=sha - name: Set up QEMU uses: docker/setup-qemu-action@v1 @@ -192,11 +237,15 @@ jobs: images: | ${{ env.DOCKER_IMAGE }} ghcr.io/name/app - tag-sha: true - tag-semver: | - {{version}} - {{major}}.{{minor}} - {{major}} + tags: | + type=schedule + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha - name: Set up QEMU uses: docker/setup-qemu-action@v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6c8bd39..3bac495 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,7 @@ on: pull_request: branches: - 'master' + - 'releases/v*' paths-ignore: - '**.md' @@ -20,12 +21,18 @@ jobs: - name: Checkout uses: actions/checkout@v2 + - + name: Validate + uses: docker/bake-action@v1 + with: + targets: validate - name: Test - run: docker buildx bake test + uses: docker/bake-action@v1 + with: + targets: test - name: Upload coverage uses: codecov/codecov-action@v1 with: - token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage/clover.xml diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml deleted file mode 100644 index 0134e68..0000000 --- a/.github/workflows/validate.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: validate - -on: - push: - branches: - - 'master' - - 'releases/v*' - paths-ignore: - - '**.md' - pull_request: - branches: - - 'master' - paths-ignore: - - '**.md' - -jobs: - validate: - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v2 - - - name: Validate - run: docker buildx bake validate diff --git a/README.md b/README.md index 2e52e0f..6b86be8 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,10 @@ [![Become a sponsor](https://img.shields.io/badge/sponsor-crazy--max-181717.svg?logo=github&style=flat-square)](https://github.com/sponsors/crazy-max) [![Paypal Donate](https://img.shields.io/badge/donate-paypal-00457c.svg?logo=paypal&style=flat-square)](https://www.paypal.me/crazyws) +## Upgrade from v1 + +`v2` of this action includes significant changes. Please read the [upgrade notes](UPGRADE.md) for a smooth migration. + ## About GitHub Action to extract metadata (tags, labels) for Docker. This action is particularly useful if used with @@ -23,11 +27,17 @@ ___ * [Customizing](#customizing) * [inputs](#inputs) * [outputs](#outputs) +* [`flavor` input](#flavor-input) +* [`tags` input](#tags-input) + * [`type=schedule`](#typeschedule) + * [`type=semver`](#typesemver) + * [`type=match`](#typematch) + * [`type=edge`](#typeedge) + * [`type=ref`](#typeref) + * [`type=raw`](#typeraw) + * [`type=sha`](#typesha) * [Notes](#notes) * [Latest tag](#latest-tag) - * [Handle semver tag](#handle-semver-tag) - * [`tag-match` examples](#tag-match-examples) - * [Schedule tag](#schedule-tag) * [Overwrite labels](#overwrite-labels) * [Keep up-to-date with GitHub Dependabot](#keep-up-to-date-with-github-dependabot) * [Contributing](#contributing) @@ -37,24 +47,18 @@ ___ ### Basic -| Event | Ref | Commit SHA | Docker Tags | -|-----------------|-------------------------------|------------|-------------------------------------| -| `pull_request` | `refs/pull/2/merge` | `a123b57` | `pr-2` | -| `push` | `refs/heads/master` | `cf20257` | `master` | -| `push` | `refs/heads/my/branch` | `a5df687` | `my-branch` | -| `push tag` | `refs/tags/v1.2.3` | `ad132f5` | `v1.2.3`, `latest` | -| `push tag` | `refs/tags/v2.0.8-beta.67` | `fc89efd` | `v2.0.8-beta.67`, `latest` | - ```yaml name: ci on: push: branches: - - '**' + - 'master' tags: - 'v*' pull_request: + branches: + - 'master' jobs: docker: @@ -65,16 +69,10 @@ jobs: uses: actions/checkout@v2 - name: Docker meta - id: docker_meta - uses: crazy-max/ghaction-docker-meta@v1 + id: meta + uses: crazy-max/ghaction-docker-meta@v2 with: images: name/app - - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - name: Login to DockerHub if: github.event_name != 'pull_request' @@ -87,33 +85,33 @@ jobs: uses: docker/build-push-action@v2 with: context: . - file: ./Dockerfile - platforms: linux/amd64,linux/arm64,linux/386 push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.docker_meta.outputs.tags }} - labels: ${{ steps.docker_meta.outputs.labels }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} ``` +| Event | Ref | Docker Tags | +|-----------------|-------------------------------|-------------------------------------| +| `pull_request` | `refs/pull/2/merge` | `pr-2` | +| `push` | `refs/heads/master` | `master` | +| `push` | `refs/heads/releases/v1` | `releases-v1` | +| `push tag` | `refs/tags/v1.2.3` | `v1.2.3`, `latest` | +| `push tag` | `refs/tags/v2.0.8-beta.67` | `v2.0.8-beta.67`, `latest` | + ### Semver -| Event | Ref | Commit SHA | Docker Tags | -|-----------------|-------------------------------|------------|-------------------------------------| -| `pull_request` | `refs/pull/2/merge` | `a123b57` | `pr-2` | -| `push` | `refs/heads/master` | `cf20257` | `master` | -| `push` | `refs/heads/my/branch` | `a5df687` | `my-branch` | -| `push tag` | `refs/tags/v1.2.3` | `ad132f5` | `1.2.3`, `1.2`, `latest` | -| `push tag` | `refs/tags/v2.0.8-beta.67` | `fc89efd` | `2.0.8-beta.67` | - ```yaml name: ci on: push: branches: - - '**' + - 'master' tags: - 'v*' pull_request: + branches: + - 'master' jobs: docker: @@ -124,19 +122,16 @@ jobs: uses: actions/checkout@v2 - name: Docker meta - id: docker_meta - uses: crazy-max/ghaction-docker-meta@v1 + id: meta + uses: crazy-max/ghaction-docker-meta@v2 with: images: name/app - tag-semver: | - {{version}} - {{major}}.{{minor}} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + tags: | + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} - name: Login to DockerHub if: github.event_name != 'pull_request' @@ -149,13 +144,19 @@ jobs: uses: docker/build-push-action@v2 with: context: . - file: ./Dockerfile - platforms: linux/amd64,linux/arm64,linux/386 push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.docker_meta.outputs.tags }} - labels: ${{ steps.docker_meta.outputs.labels }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} ``` +| Event | Ref | Docker Tags | +|-----------------|-------------------------------|-------------------------------------| +| `pull_request` | `refs/pull/2/merge` | `pr-2` | +| `push` | `refs/heads/master` | `master` | +| `push` | `refs/heads/releases/v1` | `releases-v1` | +| `push tag` | `refs/tags/v1.2.3` | `1.2.3`, `1.2`, `latest` | +| `push tag` | `refs/tags/v2.0.8-beta.67` | `2.0.8-beta.67` | + ### Bake definition This action also handles a bake definition file that can be used with the @@ -164,7 +165,6 @@ This action also handles a bake definition file that can be used with the ```hcl // docker-bake.hcl - target "ghaction-docker-meta" {} target "build" { @@ -181,10 +181,9 @@ name: ci on: push: branches: - - '**' + - 'master' tags: - 'v*' - pull_request: jobs: docker: @@ -195,40 +194,37 @@ jobs: uses: actions/checkout@v2 - name: Docker meta - id: docker_meta - uses: crazy-max/ghaction-docker-meta@v1 + id: meta + uses: crazy-max/ghaction-docker-meta@v2 with: images: name/app - tag-sha: true - tag-semver: | - {{version}} - {{major}}.{{minor}} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + tags: | + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha - name: Build uses: docker/bake-action@v1 with: files: | ./docker-bake.hcl - ${{ steps.docker_meta.outputs.bake-file }} - targets: | - build + ${{ steps.meta.outputs.bake-file }} + targets: build ``` -Content of `${{ steps.docker_meta.outputs.bake-file }}` file will look like this: +Content of `${{ steps.meta.outputs.bake-file }}` file will look like this with `refs/tags/v1.2.3` ref: ```json { "target": { "ghaction-docker-meta": { "tags": [ - "name/app:1.1.1", - "name/app:1.1", + "name/app:1.2.3", + "name/app:1.2", + "name/app:sha-90dd603", "name/app:latest" ], "labels": { @@ -236,14 +232,14 @@ Content of `${{ steps.docker_meta.outputs.bake-file }}` file will look like this "org.opencontainers.image.description": "This your first repo!", "org.opencontainers.image.url": "https://github.com/octocat/Hello-World", "org.opencontainers.image.source": "https://github.com/octocat/Hello-World", - "org.opencontainers.image.version": "1.1.1", + "org.opencontainers.image.version": "1.2.3", "org.opencontainers.image.created": "2020-01-10T00:30:00.000Z", "org.opencontainers.image.revision": "90dd6032fac8bda1b6c4436a2e65de27961ed071", "org.opencontainers.image.licenses": "MIT" }, "args": { "DOCKER_META_IMAGES": "name/app", - "DOCKER_META_VERSION": "1.1.1" + "DOCKER_META_VERSION": "1.2.3" } } } @@ -272,22 +268,12 @@ Following inputs can be used as `step.with` keys | Name | Type | Description | |---------------------|----------|------------------------------------| | `images` | List/CSV | List of Docker images to use as base name for tags | -| `tag-sha` | Bool | Add git short commit as Docker tag (default `false`) | -| `tag-edge` | Bool | Enable edge branch tagging (default `false`) | -| `tag-edge-branch` | String | Branch that will be tagged as edge (default `repo.default_branch`) | -| `tag-semver` | List/CSV | Handle Git tag as semver [template](#handle-semver-tag) if possible | -| `tag-match` | String | RegExp to match against a Git tag and use first match as Docker tag | -| `tag-match-group` | Number | Group to get if `tag-match` matches (default `0`) | -| `tag-latest` | Bool | Set `latest` Docker tag if `tag-semver`, `tag-match` or Git tag event occurs (default `true`) | -| `tag-schedule` | String | [Template](#schedule-tag) to apply to schedule tag (default `nightly`) | -| `tag-custom` | List/CSV | List of custom tags | -| `tag-custom-only` | Bool | Only use `tag-custom` as Docker tags | -| `label-custom` | List | List of custom labels | +| `tags` | List | List of [tags](#tags-input) as key-value pair attributes | +| `flavor` | List | [Flavor](#flavor-input) to apply | +| `labels` | List | List of custom labels | | `sep-tags` | String | Separator to use for tags output (default `\n`) | | `sep-labels` | String | Separator to use for labels output (default `\n`) | -> `tag-semver` and `tag-match` are mutually exclusive - ### outputs Following outputs are available @@ -299,59 +285,277 @@ Following outputs are available | `labels` | String | Docker labels | | `bake-file` | File | [Bake definition file](https://github.com/docker/buildx#file-definition) path | +## `flavor` input + +`flavor` defines a global behavior for [`tags`](#tags-input): + +```yaml +flavor: | + latest=auto + prefix= + suffix= +``` + +* `latest=`: Handle [latest tag](#latest-tag) (default `auto`) +* `prefix=`: A global prefix for each generated tag +* `suffix=`: A global suffix for each generated tag + +## `tags` input + +`tags` is the core input of this action as everything related to it will reflect the output metadata. This one is in +the form of a key-value pair list in CSV format to remove limitations intrinsically linked to GitHub Actions +(only string format is handled in the input fields). Here is an example: + +```yaml +tags: | + type=schedule + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha +``` + +Each entry is defined by a `type`, which are: + +* [`type=schedule`](#typeschedule) +* [`type=semver`](#typesemver) +* [`type=match`](#typematch) +* [`type=edge`](#typeedge) +* [`type=ref`](#typeref) +* [`type=raw`](#typeraw) +* [`type=sha`](#typesha) + +And global attributes: + +* `enable=` enable this entry (default `true`) +* `priority=` priority to manage the order of tags +* `prefix=` add prefix +* `suffix=` add suffix + +Default entries if `tags` input is empty: + +```yaml +tags: | + type=schedule + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr +``` + +### `type=schedule` + +```yaml +tags: | + # minimal + type=schedule + # default + type=schedule,pattern=nightly + # handlebars + type=schedule,pattern={{date 'YYYYMMDD'}} +``` + +Will be used on [schedule event](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#schedule). + +`pattern` is a specially crafted attribute to support [Handlebars template](https://handlebarsjs.com/guide/) with +the following expressions: +* `date 'format'` ; render date by its [moment format](https://momentjs.com/docs/#/displaying/format/) + +| Pattern | Output | +|--------------------------|----------------------| +| `nightly` | `nightly` | +| `{{date 'YYYYMMDD'}}` | `20210326` | + +Extended attributes and default values: + +```yaml +tags: | + type=schedule,enable=true,priority=1000,prefix=,suffix=,pattern=nightly +``` + +### `type=semver` + +```yaml +tags: | + # minimal + type=semver,pattern={{version}} + # use custom value instead of git tag + type=semver,pattern={{version}},value=v1.0.0 +``` + +Will be used on a [push tag event](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#push) +and requires a valid Git tag [semver](https://semver.org/) but you can also use a custom value through `value` +attribute. + +`pattern` attribute supports [Handlebars template](https://handlebarsjs.com/guide/) with the following expressions: +* `raw` ; the actual semver +* `version` ; shorthand for `{{major}}.{{minor}}.{{patch}}` (can include pre-release) +* `major` ; major version identifier +* `minor` ; minor version identifier +* `patch` ; patch version identifier + +| Git tag | Pattern | Output | +|--------------------|----------------------------------------------------------|----------------------| +| `v1.2.3` | `{{raw}}` | `v1.2.3` | +| `v1.2.3` | `{{version}}` | `1.2.3` | +| `v1.2.3` | `{{major}}.{{minor}}` | `1.2` | +| `v1.2.3` | `v{{major}}` | `v1` | +| `v1.2.3` | `{{minor}}` | `2` | +| `v1.2.3` | `{{patch}}` | `3` | +| `v2.0.8-beta.67` | `{{raw}}` | `2.0.8-beta.67`* | +| `v2.0.8-beta.67` | `{{version}}` | `2.0.8-beta.67` | +| `v2.0.8-beta.67` | `{{major}}.{{minor}}` | `2.0.8-beta.67`* | + +> *Pre-release (rc, beta, alpha) will only extend `{{version}}` as tag because they are updated frequently, +> and contain many breaking changes that are (by the author's design) not yet fit for public consumption. + +Extended attributes and default values: + +```yaml +tags: | + type=semver,enable=true,priority=900,prefix=,suffix=,pattern=,value= +``` + +### `type=match` + +```yaml +tags: | + # minimal + type=match,pattern=\d{8} + # double quotes if comma in pattern + type=match,"pattern=\d{1,3}.\d{1,3}.\d{1,3}" + # define match group + type=match,pattern=v(.*),group=1 + # use custom value instead of git tag + type=match,pattern=v(.*),group=1,value=v1.0.0 +``` + +Can create a regular expression for matching Git tag with a pattern and capturing group. Will be used on a +[push tag event](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#push) but you can also use +a custom value through `value` attribute. + +| Git tag | Pattern | Group | Output | +|-------------------------|-------------------------------|---------|------------------------| +| `v1.2.3` | `\d{1,3}.\d{1,3}.\d{1,3}` | `0` | `1.2.3` | +| `v2.0.8-beta.67` | `v(.*)` | `1` | `2.0.8-beta.67` | +| `v2.0.8-beta.67` | `v(\d.\d)` | `1` | `2.0` | +| `20200110-RC2` | `\d+` | `0` | `20200110` | + +Extended attributes and default values: + +```yaml +tags: | + type=group,enable=true,priority=800,prefix=,suffix=,pattern=,group=0,value= +``` + +### `type=edge` + +```yaml +tags: | + # minimal + type=edge + # define default branch + type=edge,branch=main +``` + +An `edge` tag reflects the last commit of the active branch on your Git repository. I usually prefer to use `edge` +as a Docker tag for a better distinction or common pattern. This is also used by official images +like [Alpine](https://hub.docker.com/_/alpine). + +Extended attributes and default values: + +```yaml +tags: | + type=edge,enable=true,priority=700,prefix=,suffix=,branch=$repo.default_branch +``` + +### `type=ref` + +```yaml +tags: | + # minimal branch event + type=ref,event=branch + # minimal tag event + type=ref,event=tag + # minimal pull request event + type=ref,event=pr +``` + +This type handles Git ref (or reference) for the following events: +* `branch` ; eg. `refs/heads/master` +* `tag` ; eg. `refs/tags/v1.0.0` +* `pr` ; eg. `refs/pull/318/merge` + +| Event | Ref | Output | +|-----------------|-------------------------------|-------------------------------| +| `pull_request` | `refs/pull/2/merge` | `pr-2` | +| `push` | `refs/heads/master` | `master` | +| `push` | `refs/heads/my/branch` | `my-branch` | +| `push tag` | `refs/tags/v1.2.3` | `v1.2.3` | +| `push tag` | `refs/tags/v2.0.8-beta.67` | `v2.0.8-beta.67` | + +Extended attributes and default values: + +```yaml +tags: | + # event branch + type=ref,enable=true,priority=600,prefix=,suffix=,event= + # event tag + type=ref,enable=true,priority=600,prefix=,suffix=,event= + # event pr + type=ref,enable=true,priority=600,prefix=pr-,suffix=,event= +``` + +### `type=raw` + +```yaml +tags: | + type=raw,value=foo + type=raw,value=bar + # or + type=raw,foo + type=raw,bar + # or + foo + bar +``` + +Output custom tags according to your needs. + +Extended attributes and default values: + +```yaml +tags: | + type=raw,enable=true,priority=200,prefix=,suffix=,value= +``` + +### `type=sha` + +```yaml +tags: | + # minimal + type=sha +``` + +Output Git short commit as Docker tag like `sha-ad132f5`. + +Extended attributes and default values: + +```yaml +tags: | + type=sha,enable=true,priority=100,prefix=sha-,suffix= +``` + ## Notes ### Latest tag -Latest Docker tag will be generated by default on `push tag` event. If for example you push the `v1.2.3` Git tag, -you will have at the output of this action the Docker tags `v1.2.3` and `latest`. But you can allow the latest tag to be -generated only if `tag-semver` is a valid [semver](https://semver.org/) or if Git tag matches a regular expression -with the [`tag-match` input](#tag-match-examples). Can be disabled if `tag-latest` is `false`. - -### Handle semver tag - -If Git tag is a valid [semver](https://semver.org/) you can handle it to output multi Docker tags at once. -`tag-semver` supports multi-line [Handlebars template](https://handlebarsjs.com/guide/) with the following inputs: - -| Git tag | `tag-semver` | Valid | Output tags | Output version | -|--------------------|----------------------------------------------------------|--------------------|----------------------------|------------------------------| -| `v1.2.3` | `{{raw}}` | :white_check_mark: | `v1.2.3`, `latest` | `v1.2.3` | -| `v1.2.3` | `{{version}}` | :white_check_mark: | `1.2.3`, `latest` | `1.2.3` | -| `v1.2.3` | `{{major}}.{{minor}}` | :white_check_mark: | `1.2`, `latest` | `1.2` | -| `v1.2.3` | `v{{major}}` | :white_check_mark: | `v1`, `latest` | `v1` | -| `v1.2.3` | `{{minor}}` | :white_check_mark: | `2`, `latest` | `2` | -| `v1.2.3` | `{{patch}}` | :white_check_mark: | `3`, `latest` | `3` | -| `v1.2.3` | `{{major}}.{{minor}}`
`{{major}}.{{minor}}.{{patch}}` | :white_check_mark: | `1.2`, `1.2.3`, `latest` | `1.2`* | -| `v2.0.8-beta.67` | `{{raw}}` | :white_check_mark: | `2.0.8-beta.67`** | `2.0.8-beta.67` | -| `v2.0.8-beta.67` | `{{version}}` | :white_check_mark: | `2.0.8-beta.67` | `2.0.8-beta.67` | -| `v2.0.8-beta.67` | `{{major}}.{{minor}}` | :white_check_mark: | `2.0.8-beta.67`** | `2.0.8-beta.67` | -| `release1` | `{{raw}}` | :x: | `release1` | `release1` | - -> *First occurrence of `tag-semver` will be taken as `output.version` - -> **Pre-release (rc, beta, alpha) will only extend `{{version}}` as tag because they are updated frequently, -> and contain many breaking changes that are (by the author's design) not yet fit for public consumption. - -### `tag-match` examples - -| Git tag | `tag-match` | `tag-match-group` | Match | Output tags | Output version | -|-------------------------|------------------------------------|-------------------|----------------------|---------------------------|------------------------------| -| `v1.2.3` | `\d{1,3}.\d{1,3}.\d{1,3}` | `0` | :white_check_mark: | `1.2.3`, `latest` | `1.2.3` | -| `v2.0.8-beta.67` | `v(.*)` | `1` | :white_check_mark: | `2.0.8-beta.67`, `latest` | `2.0.8-beta.67` | -| `v2.0.8-beta.67` | `v(\d.\d)` | `1` | :white_check_mark: | `2.0`, `latest` | `2.0` | -| `release1` | `\d{1,3}.\d{1,3}` | `0` | :x: | `release1` | `release1` | -| `20200110-RC2` | `\d+` | `0` | :white_check_mark: | `20200110`, `latest` | `20200110` | - -### Schedule tag - -`tag-schedule` is specially crafted input to support [Handlebars template](https://handlebarsjs.com/guide/) with -the following expressions: - -| Expression | Example | Description | -|-------------------------|-------------------------------------------|------------------------------------------| -| `{{date 'format'}}` | `{{date 'YYYYMMDD'}}` > `20200110` | Render date by its [moment format](https://momentjs.com/docs/#/displaying/format/) - -You can find more examples in the [CI workflow](.github/workflows/ci.yml). +`latest` tag is handled through the [`flavor` input](#flavor-input). It will be generated by default (`auto` mode) for: +* [`type=ref,event=tag`](#typeref) +* [`type=semver,pattern=...`](#typesemver) +* [`type=match,pattern=...`](#typematch) ### Overwrite labels @@ -362,10 +566,10 @@ labels generated are not suitable, you can overwrite them like this: - name: Docker meta id: docker_meta - uses: crazy-max/ghaction-docker-meta@v1 + uses: crazy-max/ghaction-docker-meta@v2 with: images: name/app - label-custom: | + labels: | maintainer=CrazyMax org.opencontainers.image.title=MyCustomTitle org.opencontainers.image.description=Another description diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..f6d5e52 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,295 @@ +# Upgrade notes + +## v1 to v2 + +* [inputs](#inputs) + * [`tag-sha`](#tag-sha) + * [`tag-edge` / `tag-edge-branch`](#tag-edge--tag-edge-branch) + * [`tag-semver`](#tag-semver) + * [`tag-match` / `tag-match-group`](#tag-match--tag-match-group) + * [`tag-latest`](#tag-latest) + * [`tag-schedule`](#tag-schedule) + * [`tag-custom` / `tag-custom-only`](#tag-custom--tag-custom-only) + * [`label-custom`](#label-custom) +* [Basic workflow](#basic-workflow) +* [Semver workflow](#semver-workflow) + +### inputs + +| New | Unchanged | Removed | +|------------|-----------------|--------------------| +| `tags` | `images` | `tag-sha` | +| `flavor` | `sep-tags` | `tag-edge` | +| `labels` | `sep-labels` | `tag-edge-branch` | +| | | `tag-semver` | +| | | `tag-match` | +| | | `tag-match-group` | +| | | `tag-latest` | +| | | `tag-schedule` | +| | | `tag-custom` | +| | | `tag-custom-only` | +| | | `label-custom` | + +#### `tag-sha` + +```yaml +tags: | + type=sha +``` + +#### `tag-edge` / `tag-edge-branch` + +```yaml +tags: | + # default branch + type=edge + # specify branch + type=edge,branch=main +``` + +#### `tag-semver` + +```yaml +tags: | + type=semver,pattern={{version}} +``` + +#### `tag-match` / `tag-match-group` + +```yaml +tags: | + type=match,pattern=v(.*),group=1 +``` + +#### `tag-latest` + +`tag-latest` is now handled through the [`flavor` input](README.md#flavor-input): + +```yaml +flavor: | + latest=auto +``` + +See also the notes about ["latest tag" behavior](README.md#latest-tag) + +#### `tag-schedule` + +```yaml +tags: | + # default tag (nightly) + type=schedule + # specific pattern + type=schedule,pattern={{date 'YYYYMMDD'}} +``` + +#### `tag-custom` / `tag-custom-only` + +```yaml +tags: | + type=raw,value=foo + type=raw,value=bar + # or + type=raw,foo + type=raw,bar + # or + foo + bar +``` + +#### `label-custom` + +Same behavior for `labels`: + +```yaml +labels: | + maintainer=CrazyMax +``` + +### Basic workflow + +```yaml +# v1 +name: ci + +on: + push: + branches: + - 'master' + tags: + - 'v*' + pull_request: + branches: + - 'master' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v2 + - + name: Docker meta + id: meta + uses: crazy-max/ghaction-docker-meta@v1 + with: + images: name/app + - + name: Login to DockerHub + if: github.event_name != 'pull_request' + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.docker_meta.outputs.tags }} + labels: ${{ steps.docker_meta.outputs.labels }} +``` + +```yaml +# v2 +name: ci + +on: + push: + branches: + - 'master' + tags: + - 'v*' + pull_request: + branches: + - 'master' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v2 + - + name: Docker meta + id: meta + uses: crazy-max/ghaction-docker-meta@v2 + with: + images: name/app + - + name: Login to DockerHub + if: github.event_name != 'pull_request' + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} +``` + +### Semver workflow + +```yaml +# v1 +name: ci + +on: + push: + branches: + - 'master' + tags: + - 'v*' + pull_request: + branches: + - 'master' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v2 + - + name: Docker meta + id: meta + uses: crazy-max/ghaction-docker-meta@v1 + with: + images: name/app + tag-semver: | + {{version}} + {{major}}.{{minor}} + - + name: Login to DockerHub + if: github.event_name != 'pull_request' + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} +``` + +```yaml +# v2 +name: ci + +on: + push: + branches: + - 'master' + tags: + - 'v*' + pull_request: + branches: + - 'master' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v2 + - + name: Docker meta + id: meta + uses: crazy-max/ghaction-docker-meta@v2 + with: + images: name/app + tags: | + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + - + name: Login to DockerHub + if: github.event_name != 'pull_request' + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} +``` diff --git a/__tests__/flavor.test.ts b/__tests__/flavor.test.ts new file mode 100644 index 0000000..fcd6642 --- /dev/null +++ b/__tests__/flavor.test.ts @@ -0,0 +1,115 @@ +import {Flavor, Transform} from '../src/flavor'; + +describe('transform', () => { + // prettier-ignore + test.each([ + [ + [ + `randomstr`, + `latest=auto` + ], + {} as Flavor, + true + ], + [ + [ + `unknwown=foo` + ], + {} as Flavor, + true + ], + [ + [ + `latest`, + ], + {} as Flavor, + true + ], + [ + [ + `latest=true` + ], + { + latest: "true", + prefix: "", + suffix: "" + } as Flavor, + false + ], + [ + [ + `latest=false` + ], + { + latest: "false", + prefix: "", + suffix: "" + } as Flavor, + false + ], + [ + [ + `latest=auto` + ], + { + latest: "auto", + prefix: "", + suffix: "" + } as Flavor, + false + ], + [ + [ + `latest=foo` + ], + {} as Flavor, + true + ], + [ + [ + `prefix=sha-` + ], + { + latest: "auto", + prefix: "sha-", + suffix: "" + } as Flavor, + false + ], + [ + [ + `suffix=-alpine` + ], + { + latest: "auto", + prefix: "", + suffix: "-alpine" + } as Flavor, + false + ], + [ + [ + `latest=false`, + `prefix=dev-`, + `suffix=-alpine` + ], + { + latest: "false", + prefix: "dev-", + suffix: "-alpine" + } as Flavor, + false + ], + ])('given %p attributes ', async (inputs: string[], expected: Flavor, invalid: boolean) => { + try { + const flavor = Transform(inputs); + console.log(flavor); + expect(flavor).toEqual(expected); + } catch (err) { + if (!invalid) { + console.error(err); + } + expect(true).toBe(invalid); + } + }); +}); diff --git a/__tests__/meta.test.ts b/__tests__/meta.test.ts index 3dab864..071581e 100644 --- a/__tests__/meta.test.ts +++ b/__tests__/meta.test.ts @@ -36,7 +36,7 @@ beforeEach(() => { }); }); -const tagsLabelsTest = async (envFile: string, inputs: Inputs, exVersion: Version, exTags: Array, exLabels: Array) => { +const tagsLabelsTest = async (name: string, envFile: string, inputs: Inputs, exVersion: Version, exTags: Array, exLabels: Array) => { process.env = dotenv.parse(fs.readFileSync(path.join(__dirname, 'fixtures', envFile))); const context = github.context(); console.log(process.env, context); @@ -48,11 +48,11 @@ const tagsLabelsTest = async (envFile: string, inputs: Inputs, exVersion: Versio console.log('version', version); expect(version).toEqual(exVersion); - const tags = meta.tags(); + const tags = meta.getTags(); console.log('tags', tags); expect(tags).toEqual(exTags); - const labels = meta.labels(); + const labels = meta.getLabels(); console.log('labels', labels); expect(labels).toEqual(exLabels); }; @@ -61,6 +61,7 @@ describe('null', () => { // prettier-ignore test.each([ [ + 'null01', 'event_null.env', { images: ['user/app'], @@ -83,9 +84,13 @@ describe('null', () => { ] ], [ + 'null02', 'event_empty.env', { images: ['user/app'], + tags: [ + `type=sha` + ] } as Inputs, { main: undefined, @@ -104,13 +109,14 @@ describe('null', () => { "org.opencontainers.image.licenses=MIT" ] ], - ])('given %p event ', tagsLabelsTest); + ])('given %p with %p event', tagsLabelsTest); }); describe('push', () => { // prettier-ignore test.each([ [ + 'push01', 'event_push.env', { images: ['user/app'], @@ -135,10 +141,13 @@ describe('push', () => { ] ], [ + 'push02', 'event_push_defbranch.env', { images: ['user/app'], - tagEdge: true, + tags: [ + `type=edge` + ], } as Inputs, { main: 'edge', @@ -160,6 +169,7 @@ describe('push', () => { ] ], [ + 'push03', 'event_push_defbranch.env', { images: ['user/app'], @@ -184,10 +194,13 @@ describe('push', () => { ] ], [ + 'push04', 'event_workflow_dispatch.env', { images: ['user/app'], - tagEdge: true, + tags: [ + `type=edge` + ], } as Inputs, { main: 'edge', @@ -209,6 +222,7 @@ describe('push', () => { ] ], [ + 'push05', 'event_push.env', { images: ['org/app', 'ghcr.io/user/app'], @@ -234,10 +248,13 @@ describe('push', () => { ] ], [ + 'push06', 'event_push_defbranch.env', { images: ['org/app', 'ghcr.io/user/app'], - tagEdge: true, + tags: [ + `type=edge` + ], } as Inputs, { main: 'edge', @@ -260,14 +277,18 @@ describe('push', () => { ] ], [ + 'push07', 'event_push.env', { images: ['org/app', 'ghcr.io/user/app'], - tagSha: true, + tags: [ + `type=ref,event=branch`, + `type=sha` + ], } as Inputs, { main: 'dev', - partial: [], + partial: ['sha-90dd603'], latest: false } as Version, [ @@ -288,15 +309,18 @@ describe('push', () => { ] ], [ + 'push08', 'event_push_defbranch.env', { images: ['org/app', 'ghcr.io/user/app'], - tagSha: true, - tagEdge: true, + tags: [ + `type=edge`, + `type=sha` + ], } as Inputs, { main: 'edge', - partial: [], + partial: ['sha-90dd603'], latest: false } as Version, [ @@ -317,16 +341,18 @@ describe('push', () => { ] ], [ + 'push09', 'event_push.env', { images: ['org/app', 'ghcr.io/user/app'], - tagSha: true, - tagEdge: true, - tagEdgeBranch: 'dev' + tags: [ + `type=edge,branch=dev`, + `type=sha` + ], } as Inputs, { main: 'edge', - partial: [], + partial: ['sha-90dd603'], latest: false } as Version, [ @@ -347,16 +373,18 @@ describe('push', () => { ] ], [ + 'push10', 'event_push_defbranch.env', { images: ['org/app', 'ghcr.io/user/app'], - tagSha: true, - tagEdge: true, - tagEdgeBranch: 'dev' + tags: [ + `type=edge,branch=dev`, + `type=sha` + ], } as Inputs, { main: 'master', - partial: [], + partial: ['sha-90dd603'], latest: false } as Version, [ @@ -377,15 +405,18 @@ describe('push', () => { ] ], [ + 'push11', 'event_push_invalidchars.env', { images: ['org/app', 'ghcr.io/user/app'], - tagSha: true, - tagEdge: true, + tags: [ + `type=edge`, + `type=sha` + ], } as Inputs, { main: 'my-feature-1245', - partial: [], + partial: ['sha-90dd603'], latest: false } as Version, [ @@ -405,13 +436,136 @@ describe('push', () => { "org.opencontainers.image.licenses=MIT" ] ], - ])('given %p event ', tagsLabelsTest); + [ + 'push12', + 'event_push_invalidchars.env', + { + images: ['org/app', 'ghcr.io/user/app'], + tags: [ + `type=semver,pattern={{version}}`, + `type=edge` + ], + } as Inputs, + { + main: 'my-feature-1245', + partial: [], + latest: false + } as Version, + [ + 'org/app:my-feature-1245', + 'ghcr.io/user/app:my-feature-1245' + ], + [ + "org.opencontainers.image.title=Hello-World", + "org.opencontainers.image.description=This your first repo!", + "org.opencontainers.image.url=https://github.com/octocat/Hello-World", + "org.opencontainers.image.source=https://github.com/octocat/Hello-World", + "org.opencontainers.image.version=my-feature-1245", + "org.opencontainers.image.created=2020-01-10T00:30:00.000Z", + "org.opencontainers.image.revision=90dd6032fac8bda1b6c4436a2e65de27961ed071", + "org.opencontainers.image.licenses=MIT" + ] + ], + [ + 'push13', + 'event_push_defbranch.env', + { + images: ['user/app'], + tags: [ + `type=ref,priority=2000,event=branch`, + `type=edge` + ], + } as Inputs, + { + main: 'master', + partial: ['edge'], + latest: false + } as Version, + [ + 'user/app:master', + 'user/app:edge' + ], + [ + "org.opencontainers.image.title=Hello-World", + "org.opencontainers.image.description=This your first repo!", + "org.opencontainers.image.url=https://github.com/octocat/Hello-World", + "org.opencontainers.image.source=https://github.com/octocat/Hello-World", + "org.opencontainers.image.version=master", + "org.opencontainers.image.created=2020-01-10T00:30:00.000Z", + "org.opencontainers.image.revision=90dd6032fac8bda1b6c4436a2e65de27961ed071", + "org.opencontainers.image.licenses=MIT" + ] + ], + [ + 'push14', + 'event_push_defbranch.env', + { + images: ['user/app'], + tags: [ + `type=semver,pattern={{version}},value=v1.2.3`, + `type=edge` + ], + } as Inputs, + { + main: '1.2.3', + partial: ['edge'], + latest: true + } as Version, + [ + 'user/app:1.2.3', + 'user/app:edge', + 'user/app:latest' + ], + [ + "org.opencontainers.image.title=Hello-World", + "org.opencontainers.image.description=This your first repo!", + "org.opencontainers.image.url=https://github.com/octocat/Hello-World", + "org.opencontainers.image.source=https://github.com/octocat/Hello-World", + "org.opencontainers.image.version=1.2.3", + "org.opencontainers.image.created=2020-01-10T00:30:00.000Z", + "org.opencontainers.image.revision=90dd6032fac8bda1b6c4436a2e65de27961ed071", + "org.opencontainers.image.licenses=MIT" + ] + ], + [ + 'push15', + 'event_push_defbranch.env', + { + images: ['user/app'], + tags: [ + `type=match,pattern=v(.*),group=1,value=v1.2.3`, + `type=edge` + ], + } as Inputs, + { + main: '1.2.3', + partial: ['edge'], + latest: true + } as Version, + [ + 'user/app:1.2.3', + 'user/app:edge', + 'user/app:latest' + ], + [ + "org.opencontainers.image.title=Hello-World", + "org.opencontainers.image.description=This your first repo!", + "org.opencontainers.image.url=https://github.com/octocat/Hello-World", + "org.opencontainers.image.source=https://github.com/octocat/Hello-World", + "org.opencontainers.image.version=1.2.3", + "org.opencontainers.image.created=2020-01-10T00:30:00.000Z", + "org.opencontainers.image.revision=90dd6032fac8bda1b6c4436a2e65de27961ed071", + "org.opencontainers.image.licenses=MIT" + ] + ] + ])('given %p with %p event', tagsLabelsTest); }); -describe('push tag', () => { +describe('tag', () => { // prettier-ignore test.each([ [ + 'tag01', 'event_tag_release1.env', { images: ['user/app'], @@ -437,6 +591,7 @@ describe('push tag', () => { ] ], [ + 'tag02', 'event_tag_20200110-RC2.env', { images: ['user/app'], @@ -462,11 +617,16 @@ describe('push tag', () => { ] ], [ + 'tag03', 'event_tag_20200110-RC2.env', { images: ['user/app'], - tagMatch: `\\d{8}`, - tagLatest: false, + tags: [ + `type=match,pattern=\\d{8}` + ], + flavor: [ + `latest=false` + ] } as Inputs, { main: '20200110', @@ -488,12 +648,16 @@ describe('push tag', () => { ] ], [ + 'tag04', 'event_tag_20200110-RC2.env', { images: ['user/app'], - tagMatch: `(.*)-RC`, - tagMatchGroup: 1, - tagLatest: false, + tags: [ + `type=match,pattern=(.*)-RC,group=1` + ], + flavor: [ + `latest=false` + ] } as Inputs, { main: '20200110', @@ -515,10 +679,13 @@ describe('push tag', () => { ] ], [ + 'tag05', 'event_tag_v1.1.1.env', { images: ['org/app', 'ghcr.io/user/app'], - tagMatch: `\\d{1,3}.\\d{1,3}.\\d{1,3}`, + tags: [ + `type=match,"pattern=\\d{1,3}.\\d{1,3}.\\d{1,3}"` + ] } as Inputs, { main: '1.1.1', @@ -543,11 +710,13 @@ describe('push tag', () => { ] ], [ + 'tag06', 'event_tag_v1.1.1.env', { images: ['org/app', 'ghcr.io/user/app'], - tagMatch: `^v(\\d{1,3}.\\d{1,3}.\\d{1,3})$`, - tagMatchGroup: 1, + tags: [ + `type=match,"pattern=^v(\\d{1,3}.\\d{1,3}.\\d{1,3})$",group=1` + ] } as Inputs, { main: '1.1.1', @@ -572,10 +741,13 @@ describe('push tag', () => { ] ], [ + 'tag07', 'event_tag_v2.0.8-beta.67.env', { images: ['org/app', 'ghcr.io/user/app'], - tagMatch: `\\d{1,3}.\\d{1,3}.\\d{1,3}-(alpha|beta).\\d{1,3}`, + tags: [ + `type=match,"pattern=\\d{1,3}.\\d{1,3}.\\d{1,3}-(alpha|beta).\\d{1,3}"` + ] } as Inputs, { main: '2.0.8-beta.67', @@ -600,10 +772,13 @@ describe('push tag', () => { ] ], [ + 'tag08', 'event_tag_v2.0.8-beta.67.env', { images: ['org/app', 'ghcr.io/user/app'], - tagMatch: `\\d{1,3}.\\d{1,3}`, + tags: [ + `type=match,"pattern=\\d{1,3}.\\d{1,3}"` + ] } as Inputs, { main: '2.0', @@ -628,11 +803,13 @@ describe('push tag', () => { ] ], [ + 'tag09', 'event_tag_v2.0.8-beta.67.env', { images: ['org/app', 'ghcr.io/user/app'], - tagMatch: `^v(\\d{1,3}.\\d{1,3}.\\d{1,3})$`, - tagMatchGroup: 1, + tags: [ + `type=match,"pattern=/^v(\\d{1,3}.\\d{1,3}.\\d{1,3})$/ig",group=1`, + ] } as Inputs, { main: 'v2.0.8-beta.67', @@ -655,10 +832,13 @@ describe('push tag', () => { ] ], [ + 'tag10', 'event_tag_sometag.env', { images: ['org/app', 'ghcr.io/user/app'], - tagMatch: `\\d{1,3}.\\d{1,3}`, + tags: [ + `type=match,"pattern=\\d{1,3}.\\d{1,3}"` + ] } as Inputs, { main: 'sometag', @@ -681,10 +861,15 @@ describe('push tag', () => { ] ], [ + 'tag11', 'event_tag_v1.1.1.env', { images: ['org/app', 'ghcr.io/user/app'], - tagSemver: ['{{version}}', '{{major}}.{{minor}}', '{{major}}'], + tags: [ + `type=semver,pattern={{version}}`, + `type=semver,pattern={{major}}.{{minor}}`, + `type=semver,pattern={{major}}` + ] } as Inputs, { main: '1.1.1', @@ -713,10 +898,14 @@ describe('push tag', () => { ] ], [ + 'tag12', 'event_tag_v1.1.1.env', { images: ['org/app', 'ghcr.io/user/app'], - tagSemver: ['{{version}}', '{{major}}.{{minor}}.{{patch}}'], + tags: [ + `type=semver,pattern={{version}}`, + `type=semver,pattern={{major}}.{{minor}}.{{patch}}` + ] } as Inputs, { main: '1.1.1', @@ -741,10 +930,14 @@ describe('push tag', () => { ] ], [ + 'tag13', 'event_tag_v2.0.8-beta.67.env', { images: ['org/app', 'ghcr.io/user/app'], - tagSemver: ['{{major}}.{{minor}}', '{{major}}'], + tags: [ + `type=semver,pattern={{major}}.{{minor}}`, + `type=semver,pattern={{major}}` + ] } as Inputs, { main: '2.0.8-beta.67', @@ -767,11 +960,19 @@ describe('push tag', () => { ] ], [ + 'tag14', 'event_tag_sometag.env', { images: ['ghcr.io/user/app'], - tagSemver: ['{{version}}', '{{major}}.{{minor}}', '{{major}}'], - tagLatest: false, + tags: [ + `type=ref,event=tag`, + `type=semver,pattern={{version}}`, + `type=semver,pattern={{major}}.{{minor}}`, + `type=semver,pattern={{major}}` + ], + flavor: [ + `latest=false` + ] } as Inputs, { main: 'sometag', @@ -792,17 +993,88 @@ describe('push tag', () => { "org.opencontainers.image.licenses=MIT" ] ], - ])('given %p event ', tagsLabelsTest); + [ + 'tag15', + 'event_tag_v2.0.8-beta.67.env', + { + images: ['org/app', 'ghcr.io/user/app'], + tags: [ + `type=raw,priority=2000,foo`, + `type=semver,pattern={{version}}`, + `type=match,"pattern=\\d{1,3}.\\d{1,3}"` + ] + } as Inputs, + { + main: 'foo', + partial: ['2.0.8-beta.67', '2.0'], + latest: false + } as Version, + [ + 'org/app:foo', + 'org/app:2.0.8-beta.67', + 'org/app:2.0', + 'ghcr.io/user/app:foo', + 'ghcr.io/user/app:2.0.8-beta.67', + 'ghcr.io/user/app:2.0' + ], + [ + "org.opencontainers.image.title=Hello-World", + "org.opencontainers.image.description=This your first repo!", + "org.opencontainers.image.url=https://github.com/octocat/Hello-World", + "org.opencontainers.image.source=https://github.com/octocat/Hello-World", + "org.opencontainers.image.version=foo", + "org.opencontainers.image.created=2020-01-10T00:30:00.000Z", + "org.opencontainers.image.revision=90dd6032fac8bda1b6c4436a2e65de27961ed071", + "org.opencontainers.image.licenses=MIT" + ] + ], + [ + 'tag16', + 'event_tag_v1.1.1.env', + { + images: ['org/app', 'ghcr.io/user/app'], + tags: [ + `type=raw,priority=2000,foo`, + `type=ref,event=tag`, + `type=edge` + ] + } as Inputs, + { + main: 'foo', + partial: ['v1.1.1'], + latest: false + } as Version, + [ + 'org/app:foo', + 'org/app:v1.1.1', + 'ghcr.io/user/app:foo', + 'ghcr.io/user/app:v1.1.1', + ], + [ + "org.opencontainers.image.title=Hello-World", + "org.opencontainers.image.description=This your first repo!", + "org.opencontainers.image.url=https://github.com/octocat/Hello-World", + "org.opencontainers.image.source=https://github.com/octocat/Hello-World", + "org.opencontainers.image.version=foo", + "org.opencontainers.image.created=2020-01-10T00:30:00.000Z", + "org.opencontainers.image.revision=90dd6032fac8bda1b6c4436a2e65de27961ed071", + "org.opencontainers.image.licenses=MIT" + ] + ] + ])('given %p with %p event', tagsLabelsTest); }); describe('latest', () => { // prettier-ignore test.each([ [ + 'latest01', 'event_tag_release1.env', { images: ['user/app'], - tagMatch: `^release\\d{1,2}`, + tags: [ + `type=match,"pattern=^release\\d{1,2}"` + ], } as Inputs, { main: 'release1', @@ -825,10 +1097,13 @@ describe('latest', () => { ] ], [ + 'latest02', 'event_tag_20200110-RC2.env', { images: ['user/app'], - tagMatch: `^\\d+-RC\\d{1,2}`, + tags: [ + `type=match,"pattern=^\\d+-RC\\d{1,2}"` + ] } as Inputs, { main: '20200110-RC2', @@ -851,10 +1126,13 @@ describe('latest', () => { ] ], [ + 'latest03', 'event_tag_20200110-RC2.env', { images: ['user/app'], - tagMatch: `\\d{8}`, + tags: [ + `type=match,pattern=\\d{8}` + ] } as Inputs, { main: '20200110', @@ -877,10 +1155,13 @@ describe('latest', () => { ] ], [ + 'latest04', 'event_tag_v1.1.1.env', { images: ['user/app'], - tagMatch: `\\d{1,3}.\\d{1,3}.\\d{1,3}`, + tags: [ + `type=match,"pattern=\\d{1,3}.\\d{1,3}.\\d{1,3}"` + ] } as Inputs, { main: '1.1.1', @@ -903,6 +1184,7 @@ describe('latest', () => { ] ], [ + 'latest05', 'event_tag_v1.1.1.env', { images: ['org/app', 'ghcr.io/user/app'], @@ -930,10 +1212,13 @@ describe('latest', () => { ] ], [ + 'latest06', 'event_tag_v2.0.8-beta.67.env', { images: ['org/app', 'ghcr.io/user/app'], - tagMatch: `\\d{1,3}.\\d{1,3}.\\d{1,3}`, + tags: [ + `type=match,"pattern=\\d{1,3}.\\d{1,3}.\\d{1,3}"` + ] } as Inputs, { main: '2.0.8', @@ -958,10 +1243,16 @@ describe('latest', () => { ] ], [ + 'latest07', 'event_tag_v1.1.1.env', { images: ['org/app', 'ghcr.io/user/app'], - tagLatest: false, + tags: [ + `type=ref,event=tag` + ], + flavor: [ + `latest=false` + ] } as Inputs, { main: 'v1.1.1', @@ -984,10 +1275,16 @@ describe('latest', () => { ] ], [ + 'latest08', 'event_tag_v1.1.1.env', { images: ['org/app', 'ghcr.io/MyUSER/MyApp'], - tagLatest: false, + tags: [ + `type=ref,event=tag` + ], + flavor: [ + `latest=false` + ] } as Inputs, { main: 'v1.1.1', @@ -1010,16 +1307,22 @@ describe('latest', () => { ] ], [ + 'latest09', 'event_tag_v1.1.1.env', { images: ['org/app', 'ghcr.io/MyUSER/MyApp'], - tagLatest: false, - labelCustom: [ + tags: [ + `type=ref,event=tag` + ], + flavor: [ + `latest=false` + ], + labels: [ "maintainer=CrazyMax", "org.opencontainers.image.title=MyCustomTitle", "org.opencontainers.image.description=Another description", "org.opencontainers.image.vendor=MyCompany", - ], + ] } as Inputs, { main: 'v1.1.1', @@ -1045,13 +1348,14 @@ describe('latest', () => { "org.opencontainers.image.vendor=MyCompany" ] ], - ])('given %p event ', tagsLabelsTest); + ])('given %p with %p event', tagsLabelsTest); }); -describe('pull_request', () => { +describe('pr', () => { // prettier-ignore test.each([ [ + 'pr01', 'event_pull_request.env', { images: ['user/app'], @@ -1076,6 +1380,7 @@ describe('pull_request', () => { ] ], [ + 'pr02', 'event_pull_request.env', { images: ['org/app', 'ghcr.io/user/app'], @@ -1101,14 +1406,18 @@ describe('pull_request', () => { ] ], [ + 'pr03', 'event_pull_request.env', { images: ['org/app', 'ghcr.io/user/app'], - tagSha: true, + tags: [ + `type=ref,event=pr`, + `type=sha` + ] } as Inputs, { main: 'pr-2', - partial: [], + partial: ['sha-1e9249f'], latest: false } as Version, [ @@ -1128,24 +1437,58 @@ describe('pull_request', () => { "org.opencontainers.image.licenses=MIT" ] ], - ])('given %p event ', tagsLabelsTest); + [ + 'pr04', + 'event_pull_request.env', + { + images: ['org/app', 'ghcr.io/user/app'], + tags: [ + `type=sha,priority=2000`, + `type=ref,event=pr` + ] + } as Inputs, + { + main: 'sha-1e9249f', + partial: ['pr-2'], + latest: false + } as Version, + [ + 'org/app:sha-1e9249f', + 'org/app:pr-2', + 'ghcr.io/user/app:sha-1e9249f', + 'ghcr.io/user/app:pr-2' + ], + [ + "org.opencontainers.image.title=Hello-World", + "org.opencontainers.image.description=This your first repo!", + "org.opencontainers.image.url=https://github.com/octocat/Hello-World", + "org.opencontainers.image.source=https://github.com/octocat/Hello-World", + "org.opencontainers.image.version=sha-1e9249f", + "org.opencontainers.image.created=2020-01-10T00:30:00.000Z", + "org.opencontainers.image.revision=1e9249f05bfc090e0688b8fb9c1b347586add504", + "org.opencontainers.image.licenses=MIT" + ] + ], + ])('given %p with %p event', tagsLabelsTest); }); describe('schedule', () => { // prettier-ignore test.each([ [ + 'schedule01', 'event_schedule.env', { images: ['user/app'], } as Inputs, { main: 'nightly', - partial: [], + partial: ['master'], latest: false } as Version, [ - 'user/app:nightly' + 'user/app:nightly', + 'user/app:master' ], [ "org.opencontainers.image.title=Hello-World", @@ -1159,10 +1502,13 @@ describe('schedule', () => { ] ], [ + 'schedule02', 'event_schedule.env', { images: ['user/app'], - tagSchedule: `{{date 'YYYYMMDD'}}` + tags: [ + `type=schedule,pattern={{date 'YYYYMMDD'}}` + ] } as Inputs, { main: '20200110', @@ -1184,10 +1530,13 @@ describe('schedule', () => { ] ], [ + 'schedule03', 'event_schedule.env', { images: ['user/app'], - tagSchedule: `{{date 'YYYYMMDD-HHmmss'}}` + tags: [ + `type=schedule,pattern={{date 'YYYYMMDD-HHmmss'}}` + ] } as Inputs, { main: '20200110-003000', @@ -1209,18 +1558,21 @@ describe('schedule', () => { ] ], [ + 'schedule04', 'event_schedule.env', { images: ['org/app', 'ghcr.io/user/app'], } as Inputs, { main: 'nightly', - partial: [], + partial: ['master'], latest: false } as Version, [ 'org/app:nightly', - 'ghcr.io/user/app:nightly' + 'org/app:master', + 'ghcr.io/user/app:nightly', + 'ghcr.io/user/app:master' ], [ "org.opencontainers.image.title=Hello-World", @@ -1234,14 +1586,18 @@ describe('schedule', () => { ] ], [ + 'schedule05', 'event_schedule.env', { images: ['org/app', 'ghcr.io/user/app'], - tagSha: true, + tags: [ + `type=schedule`, + `type=sha` + ] } as Inputs, { main: 'nightly', - partial: [], + partial: ['sha-90dd603'], latest: false } as Version, [ @@ -1261,13 +1617,46 @@ describe('schedule', () => { "org.opencontainers.image.licenses=MIT" ] ], - ])('given %p event ', tagsLabelsTest); + [ + 'schedule06', + 'event_schedule.env', + { + images: ['org/app', 'ghcr.io/user/app'], + tags: [ + `type=schedule`, + `type=sha,priority=2000` + ] + } as Inputs, + { + main: 'sha-90dd603', + partial: ['nightly'], + latest: false + } as Version, + [ + 'org/app:sha-90dd603', + 'org/app:nightly', + 'ghcr.io/user/app:sha-90dd603', + 'ghcr.io/user/app:nightly' + ], + [ + "org.opencontainers.image.title=Hello-World", + "org.opencontainers.image.description=This your first repo!", + "org.opencontainers.image.url=https://github.com/octocat/Hello-World", + "org.opencontainers.image.source=https://github.com/octocat/Hello-World", + "org.opencontainers.image.version=sha-90dd603", + "org.opencontainers.image.created=2020-01-10T00:30:00.000Z", + "org.opencontainers.image.revision=90dd6032fac8bda1b6c4436a2e65de27961ed071", + "org.opencontainers.image.licenses=MIT" + ] + ], + ])('given %p with %p event', tagsLabelsTest); }); describe('release', () => { // prettier-ignore test.each([ [ + 'release01', 'event_release.env', { images: ['user/app'], @@ -1292,17 +1681,23 @@ describe('release', () => { "org.opencontainers.image.licenses=MIT" ] ], - ])('given %p event ', tagsLabelsTest); + ])('given %s with %p event', tagsLabelsTest); }); -describe('custom', () => { +describe('raw', () => { // prettier-ignore test.each([ [ + 'raw01', 'event_push.env', { images: ['user/app'], - tagCustom: ['my', 'custom', 'tags'] + tags: [ + `type=ref,event=branch`, + `type=raw,my`, + `type=raw,custom`, + `type=raw,tags` + ] } as Inputs, { main: 'dev', @@ -1327,10 +1722,14 @@ describe('custom', () => { ] ], [ + 'raw02', 'event_push.env', { images: ['user/app'], - tagCustom: ['my'] + tags: [ + `type=ref,event=branch`, + `type=raw,my` + ] } as Inputs, { main: 'dev', @@ -1353,10 +1752,16 @@ describe('custom', () => { ] ], [ + 'raw03', 'event_tag_release1.env', { images: ['user/app'], - tagCustom: ['my', 'custom', 'tags'] + tags: [ + `type=ref,event=tag`, + `type=raw,my`, + `type=raw,custom`, + `type=raw,tags` + ] } as Inputs, { main: 'release1', @@ -1382,12 +1787,19 @@ describe('custom', () => { ] ], [ + 'raw04', 'event_tag_20200110-RC2.env', { images: ['user/app'], - tagMatch: `\\d{8}`, - tagLatest: false, - tagCustom: ['my', 'custom', 'tags'] + tags: [ + `type=match,pattern=\\d{8}`, + `type=raw,my`, + `type=raw,custom`, + `type=raw,tags` + ], + flavor: [ + `latest=false` + ] } as Inputs, { main: '20200110', @@ -1412,11 +1824,18 @@ describe('custom', () => { ] ], [ + 'raw05', 'event_tag_v1.1.1.env', { images: ['org/app', 'ghcr.io/user/app'], - tagSemver: ['{{version}}', '{{major}}.{{minor}}', '{{major}}'], - tagCustom: ['my', 'custom', 'tags'] + tags: [ + `type=semver,pattern={{version}}`, + `type=semver,pattern={{major}}.{{minor}}`, + `type=semver,pattern={{major}}`, + `type=raw,my`, + `type=raw,custom`, + `type=raw,tags` + ] } as Inputs, { main: '1.1.1', @@ -1451,12 +1870,15 @@ describe('custom', () => { ] ], [ + 'raw06', 'event_tag_v1.1.1.env', { images: ['org/app', 'ghcr.io/user/app'], - tagSemver: ['{{version}}', '{{major}}.{{minor}}.{{patch}}'], - tagCustom: ['my', 'custom', 'tags'], - tagCustomOnly: true, + tags: [ + `type=raw,my`, + `type=raw,custom`, + `type=raw,tags` + ] } as Inputs, { main: 'my', @@ -1482,17 +1904,138 @@ describe('custom', () => { "org.opencontainers.image.licenses=MIT" ] ], - ])('given %p event ', tagsLabelsTest); -}); - -describe('bake-file', () => { - // prettier-ignore - test.each([ [ + 'raw07', 'event_push.env', { images: ['user/app'], - tagCustom: ['my', 'custom', 'tags'] + tags: [ + `type=ref,priority=90,event=branch`, + `type=raw,my`, + `type=raw,custom`, + `type=raw,tags` + ], + flavor: [ + `latest=true` + ] + } as Inputs, + { + main: 'my', + partial: ['custom', 'tags', 'dev'], + latest: true + } as Version, + [ + 'user/app:my', + 'user/app:custom', + 'user/app:tags', + 'user/app:dev', + 'user/app:latest' + ], + [ + "org.opencontainers.image.title=Hello-World", + "org.opencontainers.image.description=This your first repo!", + "org.opencontainers.image.url=https://github.com/octocat/Hello-World", + "org.opencontainers.image.source=https://github.com/octocat/Hello-World", + "org.opencontainers.image.version=my", + "org.opencontainers.image.created=2020-01-10T00:30:00.000Z", + "org.opencontainers.image.revision=90dd6032fac8bda1b6c4436a2e65de27961ed071", + "org.opencontainers.image.licenses=MIT" + ] + ], + [ + 'raw08', + 'event_push.env', + { + images: ['user/app'], + tags: [ + `type=match,pattern=\\d{8}`, + `type=raw,my`, + `type=raw,custom`, + `type=raw,tags` + ], + flavor: [ + `latest=false` + ] + } as Inputs, + { + main: 'my', + partial: ['custom', 'tags'], + latest: false + } as Version, + [ + 'user/app:my', + 'user/app:custom', + 'user/app:tags' + ], + [ + "org.opencontainers.image.title=Hello-World", + "org.opencontainers.image.description=This your first repo!", + "org.opencontainers.image.url=https://github.com/octocat/Hello-World", + "org.opencontainers.image.source=https://github.com/octocat/Hello-World", + "org.opencontainers.image.version=my", + "org.opencontainers.image.created=2020-01-10T00:30:00.000Z", + "org.opencontainers.image.revision=90dd6032fac8bda1b6c4436a2e65de27961ed071", + "org.opencontainers.image.licenses=MIT" + ] + ], + [ + 'raw09', + 'event_push.env', + { + images: ['user/app'], + tags: [ + `type=match,pattern=\\d{8}`, + `type=raw,my,prefix=foo-,suffix=-bar`, + `type=raw,custom`, + `type=raw,tags` + ], + flavor: [ + `latest=false`, + `prefix=glo-`, + `suffix=-bal` + ] + } as Inputs, + { + main: 'foo-my-bar', + partial: ['glo-custom-bal', 'glo-tags-bal'], + latest: false + } as Version, + [ + 'user/app:foo-my-bar', + 'user/app:glo-custom-bal', + 'user/app:glo-tags-bal' + ], + [ + "org.opencontainers.image.title=Hello-World", + "org.opencontainers.image.description=This your first repo!", + "org.opencontainers.image.url=https://github.com/octocat/Hello-World", + "org.opencontainers.image.source=https://github.com/octocat/Hello-World", + "org.opencontainers.image.version=foo-my-bar", + "org.opencontainers.image.created=2020-01-10T00:30:00.000Z", + "org.opencontainers.image.revision=90dd6032fac8bda1b6c4436a2e65de27961ed071", + "org.opencontainers.image.licenses=MIT" + ] + ] + ])('given %p wth %p event', tagsLabelsTest); +}); + +describe('bake', () => { + // prettier-ignore + test.each([ + [ + 'bake01', + 'event_push.env', + { + images: ['user/app'], + tags: [ + `type=ref,event=branch`, + `type=raw,my`, + `type=raw,custom`, + `type=raw,tags` + ], + labels: [ + "invalid" + ] } as Inputs, { "target": { @@ -1522,10 +2065,14 @@ describe('bake-file', () => { } ], [ + 'bake02', 'event_push.env', { images: ['user/app'], - tagCustom: ['my'] + tags: [ + `type=ref,event=branch`, + `type=raw,my` + ] } as Inputs, { "target": { @@ -1553,10 +2100,16 @@ describe('bake-file', () => { } ], [ + 'bake03', 'event_tag_release1.env', { images: ['user/app'], - tagCustom: ['my', 'custom', 'tags'] + tags: [ + `type=ref,event=tag`, + `type=raw,my`, + `type=raw,custom`, + `type=raw,tags` + ] } as Inputs, { "target": { @@ -1587,12 +2140,19 @@ describe('bake-file', () => { } ], [ + 'bake04', 'event_tag_20200110-RC2.env', { images: ['user/app'], - tagMatch: `\\d{8}`, - tagLatest: false, - tagCustom: ['my', 'custom', 'tags'] + tags: [ + `type=match,pattern=\\d{8}`, + `type=raw,my`, + `type=raw,custom`, + `type=raw,tags` + ], + flavor: [ + `latest=false` + ] } as Inputs, { "target": { @@ -1622,11 +2182,18 @@ describe('bake-file', () => { } ], [ + 'bake05', 'event_tag_v1.1.1.env', { images: ['org/app', 'ghcr.io/user/app'], - tagSemver: ['{{version}}', '{{major}}.{{minor}}', '{{major}}'], - tagCustom: ['my', 'custom', 'tags'] + tags: [ + `type=semver,pattern={{version}}`, + `type=semver,pattern={{major}}.{{minor}}`, + `type=semver,pattern={{major}}`, + `type=raw,my`, + `type=raw,custom`, + `type=raw,tags` + ] } as Inputs, { "target": { @@ -1666,12 +2233,15 @@ describe('bake-file', () => { } ], [ + 'bake06', 'event_tag_v1.1.1.env', { images: ['org/app', 'ghcr.io/user/app'], - tagSemver: ['{{version}}', '{{major}}.{{minor}}.{{patch}}'], - tagCustom: ['my', 'custom', 'tags'], - tagCustomOnly: true, + tags: [ + `type=raw,my`, + `type=raw,custom`, + `type=raw,tags` + ] } as Inputs, { "target": { @@ -1703,10 +2273,11 @@ describe('bake-file', () => { } ], [ + 'bake07', 'event_tag_v1.1.1.env', { images: ['org/app'], - labelCustom: [ + labels: [ "maintainer=CrazyMax", "org.opencontainers.image.title=MyCustom=Title", "org.opencontainers.image.description=Another description", @@ -1740,7 +2311,7 @@ describe('bake-file', () => { } } ] - ])('given %p event ', async (envFile: string, inputs: Inputs, exBakeDefinition: {}) => { + ])('given %p with %p event', async (name: string, envFile: string, inputs: Inputs, exBakeDefinition: {}) => { process.env = dotenv.parse(fs.readFileSync(path.join(__dirname, 'fixtures', envFile))); const context = github.context(); console.log(process.env, context); @@ -1748,7 +2319,7 @@ describe('bake-file', () => { const repo = await github.repo(process.env.GITHUB_TOKEN || ''); const meta = new Meta({...getInputs(), ...inputs}, context, repo); - const bakeFile = meta.bakeFile(); + const bakeFile = meta.getBakeFile(); console.log('bakeFile', bakeFile, fs.readFileSync(bakeFile, 'utf8')); expect(JSON.parse(fs.readFileSync(bakeFile, 'utf8'))).toEqual(exBakeDefinition); }); diff --git a/__tests__/tag.test.ts b/__tests__/tag.test.ts new file mode 100644 index 0000000..826eb0f --- /dev/null +++ b/__tests__/tag.test.ts @@ -0,0 +1,471 @@ +import {Transform, Parse, Tag, Type, RefEvent, DefaultPriorities} from '../src/tag'; + +describe('transform', () => { + // prettier-ignore + test.each([ + [ + [ + `type=ref,event=branch`, + `type=ref,event=tag`, + `type=ref,event=pr`, + `type=schedule`, + `type=sha`, + `type=raw,foo`, + `type=edge`, + `type=semver,pattern={{version}}`, + `type=match,"pattern=\\d{1,3}.\\d{1,3}.\\d{1,3}"` + ], + [ + { + type: Type.Schedule, + attrs: { + "priority": DefaultPriorities[Type.Schedule], + "enable": "true", + "prefix": "", + "suffix": "", + "pattern": "nightly" + } + }, + { + type: Type.Semver, + attrs: { + "priority": DefaultPriorities[Type.Semver], + "enable": "true", + "prefix": "", + "suffix": "", + "pattern": "{{version}}", + "value": "" + } + }, + { + type: Type.Match, + attrs: { + "priority": DefaultPriorities[Type.Match], + "enable": "true", + "prefix": "", + "suffix": "", + "pattern": "\\d{1,3}.\\d{1,3}.\\d{1,3}", + "group": "0", + "value": "" + } + }, + { + type: Type.Edge, + attrs: { + "priority": DefaultPriorities[Type.Edge], + "enable": "true", + "prefix": "", + "suffix": "", + "branch": "" + } + }, + { + type: Type.Ref, + attrs: { + "priority": DefaultPriorities[Type.Ref], + "enable": "true", + "prefix": "", + "suffix": "", + "event": RefEvent.Branch + } + }, + { + type: Type.Ref, + attrs: { + "priority": DefaultPriorities[Type.Ref], + "enable": "true", + "prefix": "", + "suffix": "", + "event": RefEvent.Tag + } + }, + { + type: Type.Ref, + attrs: { + "priority": DefaultPriorities[Type.Ref], + "enable": "true", + "prefix": "pr-", + "suffix": "", + "event": RefEvent.PR + } + }, + { + type: Type.Raw, + attrs: { + "priority": DefaultPriorities[Type.Raw], + "enable": "true", + "prefix": "", + "suffix": "", + "value": "foo" + } + }, + { + type: Type.Sha, + attrs: { + "priority": DefaultPriorities[Type.Sha], + "enable": "true", + "prefix": "sha-", + "suffix": "" + } + } + ] as Tag[], + false + ] + ])('given %p', async (l: string[], expected: Tag[], invalid: boolean) => { + try { + const tags = Transform(l); + console.log(tags); + expect(tags).toEqual(expected); + } catch (err) { + if (!invalid) { + console.error(err); + } + expect(true).toBe(invalid); + } + }); +}); + +describe('parse', () => { + // prettier-ignore + test.each([ + [ + `type=schedule,enable=true,pattern={{date 'YYYYMMDD'}}`, + { + type: Type.Schedule, + attrs: { + "priority": DefaultPriorities[Type.Schedule], + "enable": "true", + "prefix": "", + "suffix": "", + "pattern": "{{date 'YYYYMMDD'}}" + } + } as Tag, + false + ], + [ + `type=semver,enable=true,pattern={{version}}`, + { + type: Type.Semver, + attrs: { + "priority": DefaultPriorities[Type.Semver], + "enable": "true", + "prefix": "", + "suffix": "", + "pattern": "{{version}}", + "value": "" + } + } as Tag, + false + ], + [ + `type=semver,priority=1,enable=true,pattern={{version}}`, + { + type: Type.Semver, + attrs: { + "priority": "1", + "enable": "true", + "prefix": "", + "suffix": "", + "pattern": "{{version}}", + "value": "" + } + } as Tag, + false + ], + [ + `type=semver,priority=1,enable=true,pattern={{version}},value=v1.0.0`, + { + type: Type.Semver, + attrs: { + "priority": "1", + "enable": "true", + "prefix": "", + "suffix": "", + "pattern": "{{version}}", + "value": "v1.0.0" + } + } as Tag, + false + ], + [ + `type=match,enable=true,pattern=v(.*),group=1`, + { + type: Type.Match, + attrs: { + "priority": DefaultPriorities[Type.Match], + "enable": "true", + "prefix": "", + "suffix": "", + "pattern": "v(.*)", + "group": "1", + "value": "" + } + } as Tag, + false + ], + [ + `type=match,enable=true,"pattern=^v(\\d{1,3}.\\d{1,3}.\\d{1,3})$",group=1`, + { + type: Type.Match, + attrs: { + "priority": DefaultPriorities[Type.Match], + "enable": "true", + "prefix": "", + "suffix": "", + "pattern": "^v(\\d{1,3}.\\d{1,3}.\\d{1,3})$", + "group": "1", + "value": "" + } + } as Tag, + false + ], + [ + `type=match,priority=700,enable=true,pattern=v(.*),group=1`, + { + type: Type.Match, + attrs: { + "priority": "700", + "enable": "true", + "prefix": "", + "suffix": "", + "pattern": "v(.*)", + "group": "1", + "value": "" + } + } as Tag, + false + ], + [ + `type=match,enable=true,pattern=v(.*),group=1,value=v1.2.3`, + { + type: Type.Match, + attrs: { + "priority": DefaultPriorities[Type.Match], + "enable": "true", + "prefix": "", + "suffix": "", + "pattern": "v(.*)", + "group": "1", + "value": "v1.2.3" + } + } as Tag, + false + ], + [ + `type=match,enable=true,pattern=v(.*),group=foo`, + {} as Tag, + true + ], + [ + `type=edge`, + { + type: Type.Edge, + attrs: { + "priority": DefaultPriorities[Type.Edge], + "enable": "true", + "prefix": "", + "suffix": "", + "branch": "" + } + } as Tag, + false + ], + [ + `type=edge,enable=true,branch=master`, + { + type: Type.Edge, + attrs: { + "priority": DefaultPriorities[Type.Edge], + "enable": "true", + "prefix": "", + "suffix": "", + "branch": "master" + } + } as Tag, + false + ], + [ + `type=ref,event=tag`, + { + type: Type.Ref, + attrs: { + "priority": DefaultPriorities[Type.Ref], + "enable": "true", + "prefix": "", + "suffix": "", + "event": RefEvent.Tag + } + } as Tag, + false + ], + [ + `type=ref,event=branch`, + { + type: Type.Ref, + attrs: { + "priority": DefaultPriorities[Type.Ref], + "enable": "true", + "prefix": "", + "suffix": "", + "event": RefEvent.Branch + } + } as Tag, + false + ], + [ + `type=ref,event=pr`, + { + type: Type.Ref, + attrs: { + "priority": DefaultPriorities[Type.Ref], + "enable": "true", + "prefix": "pr-", + "suffix": "", + "event": RefEvent.PR + } + } as Tag, + false + ], + [ + `type=ref,event=foo`, + {} as Tag, + true + ], + [ + `type=ref`, + {} as Tag, + true + ], + [ + `acustomtag`, + { + type: Type.Raw, + attrs: { + "priority": DefaultPriorities[Type.Raw], + "enable": "true", + "prefix": "", + "suffix": "", + "value": "acustomtag" + } + } as Tag, + false + ], + [ + `type=raw`, + {} as Tag, + true + ], + [ + `type=raw,value=acustomtag2`, + { + type: Type.Raw, + attrs: { + "priority": DefaultPriorities[Type.Raw], + "enable": "true", + "prefix": "", + "suffix": "", + "value": "acustomtag2" + } + } as Tag, + false + ], + [ + `type=raw,enable=true,value=acustomtag4`, + { + type: Type.Raw, + attrs: { + "priority": DefaultPriorities[Type.Raw], + "enable": "true", + "prefix": "", + "suffix": "", + "value": "acustomtag4" + } + } as Tag, + false + ], + [ + `type=raw,enable=false,value=acustomtag5`, + { + type: Type.Raw, + attrs: { + "priority": DefaultPriorities[Type.Raw], + "enable": "false", + "prefix": "", + "suffix": "", + "value": "acustomtag5" + } + } as Tag, + false + ], + [ + `type=sha`, + { + type: Type.Sha, + attrs: { + "priority": DefaultPriorities[Type.Sha], + "enable": "true", + "prefix": "sha-", + "suffix": "" + } + } as Tag, + false + ], + [ + `type=sha,prefix=`, + { + type: Type.Sha, + attrs: { + "priority": DefaultPriorities[Type.Sha], + "enable": "true", + "prefix": "", + "suffix": "" + } + } as Tag, + false + ], + [ + `type=sha,enable=false`, + { + type: Type.Sha, + attrs: { + "priority": DefaultPriorities[Type.Sha], + "enable": "false", + "prefix": "sha-", + "suffix": "" + } + } as Tag, + false + ], + [ + `type=semver`, + {} as Tag, + true + ], + [ + `type=match`, + {} as Tag, + true + ], + [ + `type=foo`, + {} as Tag, + true + ], + [ + `type=sha,enable=foo`, + {} as Tag, + true + ] + ])('given %p event ', async (s: string, expected: Tag, invalid: boolean) => { + try { + const tag = Parse(s); + console.log(tag); + expect(tag).toEqual(expected); + } catch (err) { + if (!invalid) { + console.error(err); + } + expect(true).toBe(invalid); + } + }); +}); diff --git a/action.yml b/action.yml index 257170b..d7fdf12 100644 --- a/action.yml +++ b/action.yml @@ -10,47 +10,13 @@ inputs: images: description: 'List of Docker images to use as base name for tags' required: true - tag-sha: - description: 'Add git short SHA as Docker tag' - default: 'false' + tags: + description: 'List of tags as key-value pair attributes' required: false - tag-edge: - description: 'Enable edge branch tagging' - default: 'false' + flavor: + description: 'Flavors to apply' required: false - tag-edge-branch: - description: 'Branch that will be tagged as edge (default repo.default_branch)' - required: false - tag-semver: - description: 'Handle Git tag as semver template if possible' - required: false - tag-match: - description: 'RegExp to match against a Git tag and use match group as Docker tag' - required: false - tag-match-group: - description: 'Group to get if tag-match matches (default 0)' - default: '0' - required: false - tag-latest: - description: 'Set latest Docker tag if tag-semver, tag-match or Git tag event occurs' - default: 'true' - required: false - tag-match-latest: - deprecationMessage: 'tag-match-latest is deprecated. Use tag-latest instead' - description: '(DEPRECATED) Set latest Docker tag if tag-match matches or on Git tag event' - default: 'true' - required: false - tag-schedule: - description: 'Template to apply to schedule tag' - default: 'nightly' - required: false - tag-custom: - description: 'List of custom tags' - required: false - tag-custom-only: - description: 'Only use tag-custom as Docker tags' - required: false - label-custom: + labels: description: 'List of custom labels' required: false sep-tags: diff --git a/dist/index.js b/dist/index.js index aaef6d2..cb43f68 100644 --- a/dist/index.js +++ b/dist/index.js @@ -56,17 +56,9 @@ exports.tmpDir = tmpDir; function getInputs() { return { images: getInputList('images'), - tagSha: /true/i.test(core.getInput('tag-sha') || 'false'), - tagEdge: /true/i.test(core.getInput('tag-edge') || 'false'), - tagEdgeBranch: core.getInput('tag-edge-branch'), - tagSemver: getInputList('tag-semver'), - tagMatch: core.getInput('tag-match'), - tagMatchGroup: Number(core.getInput('tag-match-group')) || 0, - tagLatest: /true/i.test(core.getInput('tag-latest') || core.getInput('tag-match-latest') || 'true'), - tagSchedule: core.getInput('tag-schedule') || 'nightly', - tagCustom: getInputList('tag-custom'), - tagCustomOnly: /true/i.test(core.getInput('tag-custom-only') || 'false'), - labelCustom: getInputList('label-custom', true), + tags: getInputList('tags', true), + flavor: getInputList('flavor', true), + labels: getInputList('labels', true), sepTags: core.getInput('sep-tags') || `\n`, sepLabels: core.getInput('sep-labels') || `\n`, githubToken: core.getInput('github-token') @@ -106,6 +98,52 @@ exports.asyncForEach = (array, callback) => __awaiter(void 0, void 0, void 0, fu /***/ }), +/***/ 3716: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.Transform = void 0; +function Transform(inputs) { + const flavor = { + latest: 'auto', + prefix: '', + suffix: '' + }; + for (const input of inputs) { + const parts = input.split('=', 2); + if (parts.length == 1) { + throw new Error(`Invalid entry: ${input}`); + } + switch (parts[0]) { + case 'latest': { + flavor.latest = parts[1]; + if (!['auto', 'true', 'false'].includes(flavor.latest)) { + throw new Error(`Invalid latest flavor entry: ${input}`); + } + break; + } + case 'prefix': { + flavor.prefix = parts[1]; + break; + } + case 'suffix': { + flavor.suffix = parts[1]; + break; + } + default: { + throw new Error(`Unknown entry: ${input}`); + } + } + } + return flavor; +} +exports.Transform = Transform; +//# sourceMappingURL=flavor.js.map + +/***/ }), + /***/ 5928: /***/ (function(__unused_webpack_module, exports, __webpack_require__) { @@ -221,20 +259,30 @@ function run() { core.endGroup(); const meta = new meta_1.Meta(inputs, context, repo); const version = meta.version; - core.startGroup(`Docker image version`); - core.info(version.main || ''); - core.endGroup(); + if (meta.version.main == undefined || meta.version.main.length == 0) { + core.warning(`No Docker image version has been generated. Check tags input.`); + } + else { + core.startGroup(`Docker image version`); + core.info(version.main || ''); + core.endGroup(); + } core.setOutput('version', version.main || ''); // Docker tags - const tags = meta.tags(); - core.startGroup(`Docker tags`); - for (let tag of tags) { - core.info(tag); + const tags = meta.getTags(); + if (tags.length == 0) { + core.warning('No Docker tag has been generated. Check tags input.'); + } + else { + core.startGroup(`Docker tags`); + for (let tag of tags) { + core.info(tag); + } + core.endGroup(); } - core.endGroup(); core.setOutput('tags', tags.join(inputs.sepTags)); // Docker labels - const labels = meta.labels(); + const labels = meta.getLabels(); core.startGroup(`Docker labels`); for (let label of labels) { core.info(label); @@ -242,7 +290,7 @@ function run() { core.endGroup(); core.setOutput('labels', labels.join(inputs.sepLabels)); // Bake definition file - const bakeFile = meta.bakeFile(); + const bakeFile = meta.getBakeFile(); core.startGroup(`Bake definition file`); core.info(fs.readFileSync(bakeFile, 'utf8')); core.endGroup(); @@ -293,99 +341,286 @@ const path = __importStar(__webpack_require__(5622)); const moment_1 = __importDefault(__webpack_require__(9623)); const semver = __importStar(__webpack_require__(1383)); const context_1 = __webpack_require__(3842); +const tcl = __importStar(__webpack_require__(2829)); +const fcl = __importStar(__webpack_require__(3716)); const core = __importStar(__webpack_require__(2186)); class Meta { constructor(inputs, context, repo) { this.inputs = inputs; - if (!this.inputs.tagEdgeBranch) { - this.inputs.tagEdgeBranch = repo.default_branch; - } this.context = context; this.repo = repo; + this.tags = tcl.Transform(inputs.tags); + this.flavor = fcl.Transform(inputs.flavor); this.date = new Date(); this.version = this.getVersion(); } getVersion() { - const currentDate = this.date; let version = { main: undefined, partial: [], - latest: false + latest: undefined }; - if (/schedule/.test(this.context.eventName)) { - version.main = handlebars.compile(this.inputs.tagSchedule)({ - date: function (format) { - return moment_1.default(currentDate).utc().format(format); + for (const tag of this.tags) { + switch (tag.type) { + case tcl.Type.Schedule: { + version = this.procSchedule(version, tag); + break; } - }); - } - else if (/^refs\/tags\//.test(this.context.ref)) { - version.main = this.context.ref.replace(/^refs\/tags\//g, '').replace(/\//g, '-'); - if (this.inputs.tagSemver.length > 0 && !semver.valid(version.main)) { - core.warning(`${version.main} is not a valid semver. More info: https://semver.org/`); - } - if (this.inputs.tagSemver.length > 0 && semver.valid(version.main)) { - const sver = semver.parse(version.main, { - includePrerelease: true - }); - if (semver.prerelease(version.main)) { - version.main = handlebars.compile('{{version}}')(sver); + case tcl.Type.Semver: { + version = this.procSemver(version, tag); + break; } - else { - version.latest = this.inputs.tagLatest; - version.main = handlebars.compile(this.inputs.tagSemver[0])(sver); - for (const semverTpl of this.inputs.tagSemver) { - const partial = handlebars.compile(semverTpl)(sver); - if (partial == version.main) { - continue; - } - version.partial.push(partial); + case tcl.Type.Match: { + version = this.procMatch(version, tag); + break; + } + case tcl.Type.Ref: { + if (tag.attrs['event'] == tcl.RefEvent.Branch) { + version = this.procRefBranch(version, tag); } + else if (tag.attrs['event'] == tcl.RefEvent.Tag) { + version = this.procRefTag(version, tag); + } + else if (tag.attrs['event'] == tcl.RefEvent.PR) { + version = this.procRefPr(version, tag); + } + break; } - } - else if (this.inputs.tagMatch) { - let tagMatch; - const isRegEx = this.inputs.tagMatch.match(/^\/(.+)\/(.*)$/); - if (isRegEx) { - tagMatch = version.main.match(new RegExp(isRegEx[1], isRegEx[2])); + case tcl.Type.Edge: { + version = this.procEdge(version, tag); + break; } - else { - tagMatch = version.main.match(this.inputs.tagMatch); + case tcl.Type.Raw: { + version = this.procRaw(version, tag); + break; } - if (tagMatch) { - version.main = tagMatch[this.inputs.tagMatchGroup]; - version.latest = this.inputs.tagLatest; + case tcl.Type.Sha: { + version = this.procSha(version, tag); + break; } } - else { - version.latest = this.inputs.tagLatest; - } - } - else if (/^refs\/heads\//.test(this.context.ref)) { - version.main = this.context.ref.replace(/^refs\/heads\//g, '').replace(/[^a-zA-Z0-9._-]+/g, '-'); - if (this.inputs.tagEdge && this.inputs.tagEdgeBranch === version.main) { - version.main = 'edge'; - } - } - else if (/^refs\/pull\//.test(this.context.ref)) { - version.main = `pr-${this.context.ref.replace(/^refs\/pull\//g, '').replace(/\/merge$/g, '')}`; - } - if (this.inputs.tagCustom.length > 0) { - if (this.inputs.tagCustomOnly) { - version = { - main: this.inputs.tagCustom.shift(), - partial: this.inputs.tagCustom, - latest: false - }; - } - else { - version.partial.push(...this.inputs.tagCustom); - } } version.partial = version.partial.filter((item, index) => version.partial.indexOf(item) === index); + if (version.latest == undefined) { + version.latest = false; + } return version; } - tags() { + procSchedule(version, tag) { + if (!/schedule/.test(this.context.eventName)) { + return version; + } + const currentDate = this.date; + const vraw = handlebars.compile(tag.attrs['pattern'])({ + date: function (format) { + return moment_1.default(currentDate).utc().format(format); + } + }); + if (version.main == undefined) { + version.main = vraw; + } + else if (vraw !== version.main) { + version.partial.push(vraw); + } + if (version.latest == undefined) { + version.latest = this.flavor.latest == 'auto' ? false : this.flavor.latest == 'true'; + } + return version; + } + procSemver(version, tag) { + if (!/^refs\/tags\//.test(this.context.ref) && tag.attrs['value'].length == 0) { + return version; + } + let vraw; + if (tag.attrs['value'].length > 0) { + vraw = tag.attrs['value']; + } + else { + vraw = this.context.ref.replace(/^refs\/tags\//g, '').replace(/\//g, '-'); + } + if (!semver.valid(vraw)) { + core.warning(`${vraw} is not a valid semver. More info: https://semver.org/`); + return version; + } + let latest = false; + const sver = semver.parse(vraw, { + includePrerelease: true + }); + if (semver.prerelease(vraw)) { + vraw = handlebars.compile('{{version}}')(sver); + if (version.main == undefined) { + version.main = vraw; + } + else if (vraw !== version.main) { + version.partial.push(vraw); + } + } + else { + vraw = handlebars.compile(tag.attrs['pattern'])(sver); + if (version.main == undefined) { + version.main = vraw; + } + else if (vraw !== version.main) { + version.partial.push(vraw); + } + latest = true; + } + if (version.latest == undefined) { + version.latest = this.flavor.latest == 'auto' ? latest : this.flavor.latest == 'true'; + } + return version; + } + procMatch(version, tag) { + if (!/^refs\/tags\//.test(this.context.ref) && tag.attrs['value'].length == 0) { + return version; + } + let vraw; + if (tag.attrs['value'].length > 0) { + vraw = tag.attrs['value']; + } + else { + vraw = this.context.ref.replace(/^refs\/tags\//g, '').replace(/\//g, '-'); + } + let latest = false; + let tmatch; + const isRegEx = tag.attrs['pattern'].match(/^\/(.+)\/(.*)$/); + if (isRegEx) { + tmatch = vraw.match(new RegExp(isRegEx[1], isRegEx[2])); + } + else { + tmatch = vraw.match(tag.attrs['pattern']); + } + if (tmatch) { + vraw = tmatch[tag.attrs['group']]; + latest = true; + } + if (version.main == undefined) { + version.main = vraw; + } + else if (vraw !== version.main) { + version.partial.push(vraw); + } + if (version.latest == undefined) { + version.latest = this.flavor.latest == 'auto' ? latest : this.flavor.latest == 'true'; + } + return version; + } + procRefBranch(version, tag) { + if (!/^refs\/heads\//.test(this.context.ref)) { + return version; + } + const vraw = this.setFlavor(this.context.ref.replace(/^refs\/heads\//g, '').replace(/[^a-zA-Z0-9._-]+/g, '-'), tag); + if (version.main == undefined) { + version.main = vraw; + } + else if (vraw !== version.main) { + version.partial.push(vraw); + } + if (version.latest == undefined) { + version.latest = this.flavor.latest == 'auto' ? false : this.flavor.latest == 'true'; + } + return version; + } + procRefTag(version, tag) { + if (!/^refs\/tags\//.test(this.context.ref)) { + return version; + } + const vraw = this.setFlavor(this.context.ref.replace(/^refs\/tags\//g, '').replace(/\//g, '-'), tag); + if (version.main == undefined) { + version.main = vraw; + } + else if (vraw !== version.main) { + version.partial.push(vraw); + } + if (version.latest == undefined) { + version.latest = this.flavor.latest == 'auto' ? true : this.flavor.latest == 'true'; + } + return version; + } + procRefPr(version, tag) { + if (!/^refs\/pull\//.test(this.context.ref)) { + return version; + } + const vraw = this.setFlavor(this.context.ref.replace(/^refs\/pull\//g, '').replace(/\/merge$/g, ''), tag); + if (version.main == undefined) { + version.main = vraw; + } + else if (vraw !== version.main) { + version.partial.push(vraw); + } + if (version.latest == undefined) { + version.latest = this.flavor.latest == 'auto' ? false : this.flavor.latest == 'true'; + } + return version; + } + procEdge(version, tag) { + if (!/^refs\/heads\//.test(this.context.ref)) { + return version; + } + let val = this.context.ref.replace(/^refs\/heads\//g, '').replace(/[^a-zA-Z0-9._-]+/g, '-'); + if (tag.attrs['branch'].length == 0) { + tag.attrs['branch'] = this.repo.default_branch; + } + if (tag.attrs['branch'] === val) { + val = 'edge'; + } + const vraw = this.setFlavor(val, tag); + if (version.main == undefined) { + version.main = vraw; + } + else if (vraw !== version.main) { + version.partial.push(vraw); + } + if (version.latest == undefined) { + version.latest = this.flavor.latest == 'auto' ? false : this.flavor.latest == 'true'; + } + return version; + } + procRaw(version, tag) { + const vraw = this.setFlavor(tag.attrs['value'], tag); + if (version.main == undefined) { + version.main = vraw; + } + else if (vraw !== version.main) { + version.partial.push(vraw); + } + if (version.latest == undefined) { + version.latest = this.flavor.latest == 'auto' ? false : this.flavor.latest == 'true'; + } + return version; + } + procSha(version, tag) { + if (!this.context.sha) { + return version; + } + const vraw = this.setFlavor(this.context.sha.substr(0, 7), tag); + if (version.main == undefined) { + version.main = vraw; + } + else if (vraw !== version.main) { + version.partial.push(vraw); + } + if (version.latest == undefined) { + version.latest = this.flavor.latest == 'auto' ? false : this.flavor.latest == 'true'; + } + return version; + } + setFlavor(val, tag) { + if (tag.attrs['prefix'].length > 0) { + val = `${tag.attrs['prefix']}${val}`; + } + else if (this.flavor.prefix.length > 0) { + val = `${this.flavor.prefix}${val}`; + } + if (tag.attrs['suffix'].length > 0) { + val = `${val}${tag.attrs['suffix']}`; + } + else if (this.flavor.suffix.length > 0) { + val = `${val}${this.flavor.suffix}`; + } + return val; + } + getTags() { if (!this.version.main) { return []; } @@ -399,13 +634,10 @@ class Meta { if (this.version.latest) { tags.push(`${imageLc}:latest`); } - if (this.context.sha && this.inputs.tagSha) { - tags.push(`${imageLc}:sha-${this.context.sha.substr(0, 7)}`); - } } return tags; } - labels() { + getLabels() { var _a; let labels = [ `org.opencontainers.image.title=${this.repo.name || ''}`, @@ -417,12 +649,12 @@ class Meta { `org.opencontainers.image.revision=${this.context.sha || ''}`, `org.opencontainers.image.licenses=${((_a = this.repo.license) === null || _a === void 0 ? void 0 : _a.spdx_id) || ''}` ]; - labels.push(...this.inputs.labelCustom); + labels.push(...this.inputs.labels); return labels; } - bakeFile() { + getBakeFile() { let jsonLabels = {}; - for (let label of this.labels()) { + for (let label of this.getLabels()) { const matches = label.match(/([^=]*)=(.*)/); if (!matches) { continue; @@ -433,7 +665,7 @@ class Meta { fs.writeFileSync(bakeFile, JSON.stringify({ target: { 'ghaction-docker-meta': { - tags: this.tags(), + tags: this.getTags(), labels: jsonLabels, args: { DOCKER_META_IMAGES: this.inputs.images.join(','), @@ -450,6 +682,187 @@ exports.Meta = Meta; /***/ }), +/***/ 2829: +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.Parse = exports.Transform = exports.DefaultPriorities = exports.RefEvent = exports.Type = void 0; +const sync_1 = __importDefault(__webpack_require__(8750)); +var Type; +(function (Type) { + Type["Schedule"] = "schedule"; + Type["Semver"] = "semver"; + Type["Match"] = "match"; + Type["Edge"] = "edge"; + Type["Ref"] = "ref"; + Type["Raw"] = "raw"; + Type["Sha"] = "sha"; +})(Type = exports.Type || (exports.Type = {})); +var RefEvent; +(function (RefEvent) { + RefEvent["Branch"] = "branch"; + RefEvent["Tag"] = "tag"; + RefEvent["PR"] = "pr"; +})(RefEvent = exports.RefEvent || (exports.RefEvent = {})); +exports.DefaultPriorities = { + [Type.Schedule]: '1000', + [Type.Semver]: '900', + [Type.Match]: '800', + [Type.Edge]: '700', + [Type.Ref]: '600', + [Type.Raw]: '200', + [Type.Sha]: '100' +}; +function Transform(inputs) { + const tags = []; + if (inputs.length == 0) { + // prettier-ignore + inputs = [ + `type=schedule`, + `type=ref,event=${RefEvent.Branch}`, + `type=ref,event=${RefEvent.Tag}`, + `type=ref,event=${RefEvent.PR}` + ]; + } + for (const input of inputs) { + tags.push(Parse(input)); + } + return tags.sort((tag1, tag2) => { + if (Number(tag1.attrs['priority']) < Number(tag2.attrs['priority'])) { + return 1; + } + if (Number(tag1.attrs['priority']) > Number(tag2.attrs['priority'])) { + return -1; + } + return 0; + }); +} +exports.Transform = Transform; +function Parse(s) { + const fields = sync_1.default(s, { + relaxColumnCount: true, + skipLinesWithEmptyValues: true + })[0]; + const tag = { + attrs: {} + }; + for (const field of fields) { + const parts = field.toString().split('=', 2); + if (parts.length == 1) { + tag.attrs['value'] = parts[0].trim(); + } + else { + const key = parts[0].trim().toLowerCase(); + const value = parts[1].trim(); + switch (key) { + case 'type': { + if (!Object.values(Type).includes(value)) { + throw new Error(`Unknown type attribute: ${value}`); + } + tag.type = value; + break; + } + default: { + tag.attrs[key] = value; + break; + } + } + } + } + if (tag.type == undefined) { + tag.type = Type.Raw; + } + switch (tag.type) { + case Type.Schedule: { + if (!tag.attrs.hasOwnProperty('pattern')) { + tag.attrs['pattern'] = 'nightly'; + } + break; + } + case Type.Semver: { + if (!tag.attrs.hasOwnProperty('pattern')) { + throw new Error(`Missing pattern attribute for ${s}`); + } + if (!tag.attrs.hasOwnProperty('value')) { + tag.attrs['value'] = ''; + } + break; + } + case Type.Match: { + if (!tag.attrs.hasOwnProperty('pattern')) { + throw new Error(`Missing pattern attribute for ${s}`); + } + if (!tag.attrs.hasOwnProperty('group')) { + tag.attrs['group'] = '0'; + } + if (isNaN(+tag.attrs['group'])) { + throw new Error(`Invalid match group for ${s}`); + } + if (!tag.attrs.hasOwnProperty('value')) { + tag.attrs['value'] = ''; + } + break; + } + case Type.Edge: { + if (!tag.attrs.hasOwnProperty('branch')) { + tag.attrs['branch'] = ''; + } + break; + } + case Type.Ref: { + if (!tag.attrs.hasOwnProperty('event')) { + throw new Error(`Missing event attribute for ${s}`); + } + if (!Object.keys(RefEvent) + .map(k => RefEvent[k]) + .includes(tag.attrs['event'])) { + throw new Error(`Invalid event for ${s}`); + } + if (tag.attrs['event'] == RefEvent.PR && !tag.attrs.hasOwnProperty('prefix')) { + tag.attrs['prefix'] = 'pr-'; + } + break; + } + case Type.Raw: { + if (!tag.attrs.hasOwnProperty('value')) { + throw new Error(`Missing value attribute for ${s}`); + } + break; + } + case Type.Sha: { + if (!tag.attrs.hasOwnProperty('prefix')) { + tag.attrs['prefix'] = 'sha-'; + } + break; + } + } + if (!tag.attrs.hasOwnProperty('enable')) { + tag.attrs['enable'] = 'true'; + } + if (!tag.attrs.hasOwnProperty('priority')) { + tag.attrs['priority'] = exports.DefaultPriorities[tag.type]; + } + if (!tag.attrs.hasOwnProperty('prefix')) { + tag.attrs['prefix'] = ''; + } + if (!tag.attrs.hasOwnProperty('suffix')) { + tag.attrs['suffix'] = ''; + } + if (!['true', 'false'].includes(tag.attrs['enable'])) { + throw new Error(`Invalid value for enable attribute: ${tag.attrs['enable']}`); + } + return tag; +} +exports.Parse = Parse; +//# sourceMappingURL=tag.js.map + +/***/ }), + /***/ 7351: /***/ (function(__unused_webpack_module, exports, __webpack_require__) { diff --git a/src/context.ts b/src/context.ts index acf8cac..22a6e31 100644 --- a/src/context.ts +++ b/src/context.ts @@ -8,17 +8,9 @@ let _tmpDir: string; export interface Inputs { images: string[]; - tagSha: boolean; - tagEdge: boolean; - tagEdgeBranch: string; - tagSemver: string[]; - tagMatch: string; - tagMatchGroup: number; - tagLatest: boolean; - tagSchedule: string; - tagCustom: string[]; - tagCustomOnly: boolean; - labelCustom: string[]; + tags: string[]; + flavor: string[]; + labels: string[]; sepTags: string; sepLabels: string; githubToken: string; @@ -34,17 +26,9 @@ export function tmpDir(): string { export function getInputs(): Inputs { return { images: getInputList('images'), - tagSha: /true/i.test(core.getInput('tag-sha') || 'false'), - tagEdge: /true/i.test(core.getInput('tag-edge') || 'false'), - tagEdgeBranch: core.getInput('tag-edge-branch'), - tagSemver: getInputList('tag-semver'), - tagMatch: core.getInput('tag-match'), - tagMatchGroup: Number(core.getInput('tag-match-group')) || 0, - tagLatest: /true/i.test(core.getInput('tag-latest') || core.getInput('tag-match-latest') || 'true'), - tagSchedule: core.getInput('tag-schedule') || 'nightly', - tagCustom: getInputList('tag-custom'), - tagCustomOnly: /true/i.test(core.getInput('tag-custom-only') || 'false'), - labelCustom: getInputList('label-custom', true), + tags: getInputList('tags', true), + flavor: getInputList('flavor', true), + labels: getInputList('labels', true), sepTags: core.getInput('sep-tags') || `\n`, sepLabels: core.getInput('sep-labels') || `\n`, githubToken: core.getInput('github-token') diff --git a/src/flavor.ts b/src/flavor.ts new file mode 100644 index 0000000..3ae13b7 --- /dev/null +++ b/src/flavor.ts @@ -0,0 +1,42 @@ +export interface Flavor { + latest: string; + prefix: string; + suffix: string; +} + +export function Transform(inputs: string[]): Flavor { + const flavor: Flavor = { + latest: 'auto', + prefix: '', + suffix: '' + }; + + for (const input of inputs) { + const parts = input.split('=', 2); + if (parts.length == 1) { + throw new Error(`Invalid entry: ${input}`); + } + switch (parts[0]) { + case 'latest': { + flavor.latest = parts[1]; + if (!['auto', 'true', 'false'].includes(flavor.latest)) { + throw new Error(`Invalid latest flavor entry: ${input}`); + } + break; + } + case 'prefix': { + flavor.prefix = parts[1]; + break; + } + case 'suffix': { + flavor.suffix = parts[1]; + break; + } + default: { + throw new Error(`Unknown entry: ${input}`); + } + } + } + + return flavor; +} diff --git a/src/main.ts b/src/main.ts index cb99345..cf0e7ec 100644 --- a/src/main.ts +++ b/src/main.ts @@ -29,22 +29,30 @@ async function run() { const meta: Meta = new Meta(inputs, context, repo); const version: Version = meta.version; - core.startGroup(`Docker image version`); - core.info(version.main || ''); - core.endGroup(); + if (meta.version.main == undefined || meta.version.main.length == 0) { + core.warning(`No Docker image version has been generated. Check tags input.`); + } else { + core.startGroup(`Docker image version`); + core.info(version.main || ''); + core.endGroup(); + } core.setOutput('version', version.main || ''); // Docker tags - const tags: Array = meta.tags(); - core.startGroup(`Docker tags`); - for (let tag of tags) { - core.info(tag); + const tags: Array = meta.getTags(); + if (tags.length == 0) { + core.warning('No Docker tag has been generated. Check tags input.'); + } else { + core.startGroup(`Docker tags`); + for (let tag of tags) { + core.info(tag); + } + core.endGroup(); } - core.endGroup(); core.setOutput('tags', tags.join(inputs.sepTags)); // Docker labels - const labels: Array = meta.labels(); + const labels: Array = meta.getLabels(); core.startGroup(`Docker labels`); for (let label of labels) { core.info(label); @@ -53,7 +61,7 @@ async function run() { core.setOutput('labels', labels.join(inputs.sepLabels)); // Bake definition file - const bakeFile: string = meta.bakeFile(); + const bakeFile: string = meta.getBakeFile(); core.startGroup(`Bake definition file`); core.info(fs.readFileSync(bakeFile, 'utf8')); core.endGroup(); diff --git a/src/meta.ts b/src/meta.ts index 592071d..25de61a 100644 --- a/src/meta.ts +++ b/src/meta.ts @@ -4,6 +4,8 @@ import * as path from 'path'; import moment from 'moment'; import * as semver from 'semver'; import {Inputs, tmpDir} from './context'; +import * as tcl from './tag'; +import * as fcl from './flavor'; import * as core from '@actions/core'; import {Context} from '@actions/github/lib/context'; import {ReposGetResponseData} from '@octokit/types'; @@ -11,7 +13,7 @@ import {ReposGetResponseData} from '@octokit/types'; export interface Version { main: string | undefined; partial: string[]; - latest: boolean; + latest: boolean | undefined; } export class Meta { @@ -20,96 +22,305 @@ export class Meta { private readonly inputs: Inputs; private readonly context: Context; private readonly repo: ReposGetResponseData; + private readonly tags: tcl.Tag[]; + private readonly flavor: fcl.Flavor; private readonly date: Date; constructor(inputs: Inputs, context: Context, repo: ReposGetResponseData) { this.inputs = inputs; - if (!this.inputs.tagEdgeBranch) { - this.inputs.tagEdgeBranch = repo.default_branch; - } this.context = context; this.repo = repo; + this.tags = tcl.Transform(inputs.tags); + this.flavor = fcl.Transform(inputs.flavor); this.date = new Date(); this.version = this.getVersion(); } private getVersion(): Version { - const currentDate = this.date; let version: Version = { main: undefined, partial: [], - latest: false + latest: undefined }; - if (/schedule/.test(this.context.eventName)) { - version.main = handlebars.compile(this.inputs.tagSchedule)({ - date: function (format) { - return moment(currentDate).utc().format(format); + for (const tag of this.tags) { + switch (tag.type) { + case tcl.Type.Schedule: { + version = this.procSchedule(version, tag); + break; } - }); - } else if (/^refs\/tags\//.test(this.context.ref)) { - version.main = this.context.ref.replace(/^refs\/tags\//g, '').replace(/\//g, '-'); - if (this.inputs.tagSemver.length > 0 && !semver.valid(version.main)) { - core.warning(`${version.main} is not a valid semver. More info: https://semver.org/`); - } - if (this.inputs.tagSemver.length > 0 && semver.valid(version.main)) { - const sver = semver.parse(version.main, { - includePrerelease: true - }); - if (semver.prerelease(version.main)) { - version.main = handlebars.compile('{{version}}')(sver); - } else { - version.latest = this.inputs.tagLatest; - version.main = handlebars.compile(this.inputs.tagSemver[0])(sver); - for (const semverTpl of this.inputs.tagSemver) { - const partial = handlebars.compile(semverTpl)(sver); - if (partial == version.main) { - continue; - } - version.partial.push(partial); + case tcl.Type.Semver: { + version = this.procSemver(version, tag); + break; + } + case tcl.Type.Match: { + version = this.procMatch(version, tag); + break; + } + case tcl.Type.Ref: { + if (tag.attrs['event'] == tcl.RefEvent.Branch) { + version = this.procRefBranch(version, tag); + } else if (tag.attrs['event'] == tcl.RefEvent.Tag) { + version = this.procRefTag(version, tag); + } else if (tag.attrs['event'] == tcl.RefEvent.PR) { + version = this.procRefPr(version, tag); } + break; } - } else if (this.inputs.tagMatch) { - let tagMatch; - const isRegEx = this.inputs.tagMatch.match(/^\/(.+)\/(.*)$/); - if (isRegEx) { - tagMatch = version.main.match(new RegExp(isRegEx[1], isRegEx[2])); - } else { - tagMatch = version.main.match(this.inputs.tagMatch); + case tcl.Type.Edge: { + version = this.procEdge(version, tag); + break; } - if (tagMatch) { - version.main = tagMatch[this.inputs.tagMatchGroup]; - version.latest = this.inputs.tagLatest; + case tcl.Type.Raw: { + version = this.procRaw(version, tag); + break; + } + case tcl.Type.Sha: { + version = this.procSha(version, tag); + break; } - } else { - version.latest = this.inputs.tagLatest; - } - } else if (/^refs\/heads\//.test(this.context.ref)) { - version.main = this.context.ref.replace(/^refs\/heads\//g, '').replace(/[^a-zA-Z0-9._-]+/g, '-'); - if (this.inputs.tagEdge && this.inputs.tagEdgeBranch === version.main) { - version.main = 'edge'; - } - } else if (/^refs\/pull\//.test(this.context.ref)) { - version.main = `pr-${this.context.ref.replace(/^refs\/pull\//g, '').replace(/\/merge$/g, '')}`; - } - - if (this.inputs.tagCustom.length > 0) { - if (this.inputs.tagCustomOnly) { - version = { - main: this.inputs.tagCustom.shift(), - partial: this.inputs.tagCustom, - latest: false - }; - } else { - version.partial.push(...this.inputs.tagCustom); } } version.partial = version.partial.filter((item, index) => version.partial.indexOf(item) === index); + if (version.latest == undefined) { + version.latest = false; + } + return version; } - public tags(): Array { + private procSchedule(version: Version, tag: tcl.Tag): Version { + if (!/schedule/.test(this.context.eventName)) { + return version; + } + + const currentDate = this.date; + const vraw = handlebars.compile(tag.attrs['pattern'])({ + date: function (format) { + return moment(currentDate).utc().format(format); + } + }); + + if (version.main == undefined) { + version.main = vraw; + } else if (vraw !== version.main) { + version.partial.push(vraw); + } + if (version.latest == undefined) { + version.latest = this.flavor.latest == 'auto' ? false : this.flavor.latest == 'true'; + } + + return version; + } + + private procSemver(version: Version, tag: tcl.Tag): Version { + if (!/^refs\/tags\//.test(this.context.ref) && tag.attrs['value'].length == 0) { + return version; + } + + let vraw: string; + if (tag.attrs['value'].length > 0) { + vraw = tag.attrs['value']; + } else { + vraw = this.context.ref.replace(/^refs\/tags\//g, '').replace(/\//g, '-'); + } + if (!semver.valid(vraw)) { + core.warning(`${vraw} is not a valid semver. More info: https://semver.org/`); + return version; + } + + let latest: boolean = false; + const sver = semver.parse(vraw, { + includePrerelease: true + }); + if (semver.prerelease(vraw)) { + vraw = handlebars.compile('{{version}}')(sver); + if (version.main == undefined) { + version.main = vraw; + } else if (vraw !== version.main) { + version.partial.push(vraw); + } + } else { + vraw = handlebars.compile(tag.attrs['pattern'])(sver); + if (version.main == undefined) { + version.main = vraw; + } else if (vraw !== version.main) { + version.partial.push(vraw); + } + latest = true; + } + if (version.latest == undefined) { + version.latest = this.flavor.latest == 'auto' ? latest : this.flavor.latest == 'true'; + } + + return version; + } + + private procMatch(version: Version, tag: tcl.Tag): Version { + if (!/^refs\/tags\//.test(this.context.ref) && tag.attrs['value'].length == 0) { + return version; + } + + let vraw: string; + if (tag.attrs['value'].length > 0) { + vraw = tag.attrs['value']; + } else { + vraw = this.context.ref.replace(/^refs\/tags\//g, '').replace(/\//g, '-'); + } + + let latest: boolean = false; + let tmatch; + const isRegEx = tag.attrs['pattern'].match(/^\/(.+)\/(.*)$/); + if (isRegEx) { + tmatch = vraw.match(new RegExp(isRegEx[1], isRegEx[2])); + } else { + tmatch = vraw.match(tag.attrs['pattern']); + } + if (tmatch) { + vraw = tmatch[tag.attrs['group']]; + latest = true; + } + + if (version.main == undefined) { + version.main = vraw; + } else if (vraw !== version.main) { + version.partial.push(vraw); + } + if (version.latest == undefined) { + version.latest = this.flavor.latest == 'auto' ? latest : this.flavor.latest == 'true'; + } + + return version; + } + + private procRefBranch(version: Version, tag: tcl.Tag): Version { + if (!/^refs\/heads\//.test(this.context.ref)) { + return version; + } + + const vraw = this.setFlavor(this.context.ref.replace(/^refs\/heads\//g, '').replace(/[^a-zA-Z0-9._-]+/g, '-'), tag); + if (version.main == undefined) { + version.main = vraw; + } else if (vraw !== version.main) { + version.partial.push(vraw); + } + if (version.latest == undefined) { + version.latest = this.flavor.latest == 'auto' ? false : this.flavor.latest == 'true'; + } + + return version; + } + + private procRefTag(version: Version, tag: tcl.Tag): Version { + if (!/^refs\/tags\//.test(this.context.ref)) { + return version; + } + + const vraw = this.setFlavor(this.context.ref.replace(/^refs\/tags\//g, '').replace(/\//g, '-'), tag); + if (version.main == undefined) { + version.main = vraw; + } else if (vraw !== version.main) { + version.partial.push(vraw); + } + if (version.latest == undefined) { + version.latest = this.flavor.latest == 'auto' ? true : this.flavor.latest == 'true'; + } + + return version; + } + + private procRefPr(version: Version, tag: tcl.Tag): Version { + if (!/^refs\/pull\//.test(this.context.ref)) { + return version; + } + + const vraw = this.setFlavor(this.context.ref.replace(/^refs\/pull\//g, '').replace(/\/merge$/g, ''), tag); + if (version.main == undefined) { + version.main = vraw; + } else if (vraw !== version.main) { + version.partial.push(vraw); + } + if (version.latest == undefined) { + version.latest = this.flavor.latest == 'auto' ? false : this.flavor.latest == 'true'; + } + + return version; + } + + private procEdge(version: Version, tag: tcl.Tag): Version { + if (!/^refs\/heads\//.test(this.context.ref)) { + return version; + } + + let val = this.context.ref.replace(/^refs\/heads\//g, '').replace(/[^a-zA-Z0-9._-]+/g, '-'); + if (tag.attrs['branch'].length == 0) { + tag.attrs['branch'] = this.repo.default_branch; + } + if (tag.attrs['branch'] === val) { + val = 'edge'; + } + + const vraw = this.setFlavor(val, tag); + if (version.main == undefined) { + version.main = vraw; + } else if (vraw !== version.main) { + version.partial.push(vraw); + } + if (version.latest == undefined) { + version.latest = this.flavor.latest == 'auto' ? false : this.flavor.latest == 'true'; + } + + return version; + } + + private procRaw(version: Version, tag: tcl.Tag): Version { + const vraw = this.setFlavor(tag.attrs['value'], tag); + if (version.main == undefined) { + version.main = vraw; + } else if (vraw !== version.main) { + version.partial.push(vraw); + } + if (version.latest == undefined) { + version.latest = this.flavor.latest == 'auto' ? false : this.flavor.latest == 'true'; + } + + return version; + } + + private procSha(version: Version, tag: tcl.Tag): Version { + if (!this.context.sha) { + return version; + } + + const vraw = this.setFlavor(this.context.sha.substr(0, 7), tag); + if (version.main == undefined) { + version.main = vraw; + } else if (vraw !== version.main) { + version.partial.push(vraw); + } + if (version.latest == undefined) { + version.latest = this.flavor.latest == 'auto' ? false : this.flavor.latest == 'true'; + } + + return version; + } + + private setFlavor(val: string, tag: tcl.Tag): string { + if (tag.attrs['prefix'].length > 0) { + val = `${tag.attrs['prefix']}${val}`; + } else if (this.flavor.prefix.length > 0) { + val = `${this.flavor.prefix}${val}`; + } + if (tag.attrs['suffix'].length > 0) { + val = `${val}${tag.attrs['suffix']}`; + } else if (this.flavor.suffix.length > 0) { + val = `${val}${this.flavor.suffix}`; + } + return val; + } + + public getTags(): Array { if (!this.version.main) { return []; } @@ -124,14 +335,11 @@ export class Meta { if (this.version.latest) { tags.push(`${imageLc}:latest`); } - if (this.context.sha && this.inputs.tagSha) { - tags.push(`${imageLc}:sha-${this.context.sha.substr(0, 7)}`); - } } return tags; } - public labels(): Array { + public getLabels(): Array { let labels: Array = [ `org.opencontainers.image.title=${this.repo.name || ''}`, `org.opencontainers.image.description=${this.repo.description || ''}`, @@ -142,13 +350,13 @@ export class Meta { `org.opencontainers.image.revision=${this.context.sha || ''}`, `org.opencontainers.image.licenses=${this.repo.license?.spdx_id || ''}` ]; - labels.push(...this.inputs.labelCustom); + labels.push(...this.inputs.labels); return labels; } - public bakeFile(): string { + public getBakeFile(): string { let jsonLabels = {}; - for (let label of this.labels()) { + for (let label of this.getLabels()) { const matches = label.match(/([^=]*)=(.*)/); if (!matches) { continue; @@ -163,7 +371,7 @@ export class Meta { { target: { 'ghaction-docker-meta': { - tags: this.tags(), + tags: this.getTags(), labels: jsonLabels, args: { DOCKER_META_IMAGES: this.inputs.images.join(','), diff --git a/src/tag.ts b/src/tag.ts new file mode 100644 index 0000000..0a935d1 --- /dev/null +++ b/src/tag.ts @@ -0,0 +1,180 @@ +import csvparse from 'csv-parse/lib/sync'; + +export enum Type { + Schedule = 'schedule', + Semver = 'semver', + Match = 'match', + Edge = 'edge', + Ref = 'ref', + Raw = 'raw', + Sha = 'sha' +} + +export enum RefEvent { + Branch = 'branch', + Tag = 'tag', + PR = 'pr' +} + +export interface Tag { + type: Type; + attrs: Record; +} + +export const DefaultPriorities: Record = { + [Type.Schedule]: '1000', + [Type.Semver]: '900', + [Type.Match]: '800', + [Type.Edge]: '700', + [Type.Ref]: '600', + [Type.Raw]: '200', + [Type.Sha]: '100' +}; + +export function Transform(inputs: string[]): Tag[] { + const tags: Tag[] = []; + if (inputs.length == 0) { + // prettier-ignore + inputs = [ + `type=schedule`, + `type=ref,event=${RefEvent.Branch}`, + `type=ref,event=${RefEvent.Tag}`, + `type=ref,event=${RefEvent.PR}` + ]; + } + for (const input of inputs) { + tags.push(Parse(input)); + } + return tags.sort((tag1, tag2) => { + if (Number(tag1.attrs['priority']) < Number(tag2.attrs['priority'])) { + return 1; + } + if (Number(tag1.attrs['priority']) > Number(tag2.attrs['priority'])) { + return -1; + } + return 0; + }); +} + +export function Parse(s: string): Tag { + const fields = csvparse(s, { + relaxColumnCount: true, + skipLinesWithEmptyValues: true + })[0]; + + const tag = { + attrs: {} + } as Tag; + + for (const field of fields) { + const parts = field.toString().split('=', 2); + if (parts.length == 1) { + tag.attrs['value'] = parts[0].trim(); + } else { + const key = parts[0].trim().toLowerCase(); + const value = parts[1].trim(); + switch (key) { + case 'type': { + if (!Object.values(Type).includes(value)) { + throw new Error(`Unknown type attribute: ${value}`); + } + tag.type = value; + break; + } + default: { + tag.attrs[key] = value; + break; + } + } + } + } + + if (tag.type == undefined) { + tag.type = Type.Raw; + } + + switch (tag.type) { + case Type.Schedule: { + if (!tag.attrs.hasOwnProperty('pattern')) { + tag.attrs['pattern'] = 'nightly'; + } + break; + } + case Type.Semver: { + if (!tag.attrs.hasOwnProperty('pattern')) { + throw new Error(`Missing pattern attribute for ${s}`); + } + if (!tag.attrs.hasOwnProperty('value')) { + tag.attrs['value'] = ''; + } + break; + } + case Type.Match: { + if (!tag.attrs.hasOwnProperty('pattern')) { + throw new Error(`Missing pattern attribute for ${s}`); + } + if (!tag.attrs.hasOwnProperty('group')) { + tag.attrs['group'] = '0'; + } + if (isNaN(+tag.attrs['group'])) { + throw new Error(`Invalid match group for ${s}`); + } + if (!tag.attrs.hasOwnProperty('value')) { + tag.attrs['value'] = ''; + } + break; + } + case Type.Edge: { + if (!tag.attrs.hasOwnProperty('branch')) { + tag.attrs['branch'] = ''; + } + break; + } + case Type.Ref: { + if (!tag.attrs.hasOwnProperty('event')) { + throw new Error(`Missing event attribute for ${s}`); + } + if ( + !Object.keys(RefEvent) + .map(k => RefEvent[k]) + .includes(tag.attrs['event']) + ) { + throw new Error(`Invalid event for ${s}`); + } + if (tag.attrs['event'] == RefEvent.PR && !tag.attrs.hasOwnProperty('prefix')) { + tag.attrs['prefix'] = 'pr-'; + } + break; + } + case Type.Raw: { + if (!tag.attrs.hasOwnProperty('value')) { + throw new Error(`Missing value attribute for ${s}`); + } + break; + } + case Type.Sha: { + if (!tag.attrs.hasOwnProperty('prefix')) { + tag.attrs['prefix'] = 'sha-'; + } + break; + } + } + + if (!tag.attrs.hasOwnProperty('enable')) { + tag.attrs['enable'] = 'true'; + } + if (!tag.attrs.hasOwnProperty('priority')) { + tag.attrs['priority'] = DefaultPriorities[tag.type]; + } + if (!tag.attrs.hasOwnProperty('prefix')) { + tag.attrs['prefix'] = ''; + } + if (!tag.attrs.hasOwnProperty('suffix')) { + tag.attrs['suffix'] = ''; + } + if (!['true', 'false'].includes(tag.attrs['enable'])) { + throw new Error(`Invalid value for enable attribute: ${tag.attrs['enable']}`); + } + + return tag; +}