diff --git a/.github/workflows/build-test-deploy.yml b/.github/workflows/build-test-deploy.yml index a1ba91bf4..ae7f556ae 100644 --- a/.github/workflows/build-test-deploy.yml +++ b/.github/workflows/build-test-deploy.yml @@ -671,6 +671,7 @@ jobs: VORTEX_DEPLOY_BRANCH: ${{ env.DEPLOY_BRANCH || github.head_ref || github.ref_name }} VORTEX_DEPLOY_PR: ${{ env.DEPLOY_PR_NUMBER || github.event.number }} VORTEX_DEPLOY_PR_HEAD: ${{ env.DEPLOY_PR_HEAD_SHA || github.event.pull_request.head.sha }} + VORTEX_DEPLOY_PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }} VORTEX_DEPLOY_ARTIFACT_SRC: /tmp/workspace/code VORTEX_DEPLOY_ARTIFACT_ROOT: ${{ github.workspace }} VORTEX_DEPLOY_ARTIFACT_GIT_REMOTE: ${{ vars.VORTEX_DEPLOY_ARTIFACT_GIT_REMOTE }} @@ -681,6 +682,7 @@ jobs: VORTEX_DEPLOY_ALLOW_SKIP: ${{ vars.VORTEX_DEPLOY_ALLOW_SKIP }} VORTEX_DEPLOY_SKIP_PRS: ${{ vars.VORTEX_DEPLOY_SKIP_PRS }} VORTEX_DEPLOY_SKIP_BRANCHES: ${{ vars.VORTEX_DEPLOY_SKIP_BRANCHES }} + VORTEX_DEPLOY_ALLOW_LABEL: ${{ vars.VORTEX_DEPLOY_ALLOW_LABEL }} VORTEX_DEPLOY_ACTION: ${{ inputs.override_db && 'deploy_override_db' || '' }} timeout-minutes: 30 #;> DEPLOYMENT diff --git a/.vortex/docs/.utils/variables/extra/ci.variables.sh b/.vortex/docs/.utils/variables/extra/ci.variables.sh index bdee75805..b7fc9b62a 100755 --- a/.vortex/docs/.utils/variables/extra/ci.variables.sh +++ b/.vortex/docs/.utils/variables/extra/ci.variables.sh @@ -15,6 +15,9 @@ VORTEX_DEPLOY_SKIP_PRS= # Branch names to skip deployment for (single value or comma-separated list). VORTEX_DEPLOY_SKIP_BRANCHES= +# Label that authorizes a pull request deployment. When set, a PR is deployed only if it carries this label. +VORTEX_DEPLOY_ALLOW_LABEL= + # Proceed with container image deployment after it was exported. VORTEX_EXPORT_DB_CONTAINER_REGISTRY_DEPLOY_PROCEED= diff --git a/.vortex/docs/content/deployment/README.mdx b/.vortex/docs/content/deployment/README.mdx index baca951dd..d65dd62bc 100644 --- a/.vortex/docs/content/deployment/README.mdx +++ b/.vortex/docs/content/deployment/README.mdx @@ -126,6 +126,38 @@ VORTEX_DEPLOY_SKIP_PRS=42,123 VORTEX_DEPLOY_SKIP_BRANCHES=feature/test,hotfix/urgent ``` +## Gating deployments on a PR label + +Skipping is subtractive - everything deploys unless you exclude it. The label +gate is the opposite: it makes a pull request deployment **opt-in**, so a PR +reaches an environment only when it is explicitly labeled for it. This is +useful for controlling the number and cost of per-PR environments, or for +keeping work-in-progress pull requests out of environments. + +Set `$VORTEX_DEPLOY_ALLOW_LABEL` to the name of the label that authorizes +deployment: + +```shell +VORTEX_DEPLOY_ALLOW_LABEL=deploy +``` + +With this set, a pull request is deployed only if it carries the `deploy` +label; pull requests without it are skipped. When the variable is empty or +unset (the default), the gate is inactive and deployments behave as before. + +The gate applies only to pull request builds. Branch and tag deployments have +no associated pull request label and are never gated. + +The pull request's labels are read from `$VORTEX_DEPLOY_PR_LABELS`, a +comma-separated list that the CI provider populates from the pull request +event. On GitHub Actions this is wired up automatically. On CircleCI the +labels are not available natively, so populate `$VORTEX_DEPLOY_PR_LABELS` +yourself (for example, from the GitHub API) to use the gate. + +The label gate and the skip lists can be combined. A skip-list match is +evaluated first and always wins, so a pull request listed in +`$VORTEX_DEPLOY_SKIP_PRS` is skipped even when it carries the gate label. + ## See also | Topic | Description | diff --git a/.vortex/docs/content/development/variables.mdx b/.vortex/docs/content/development/variables.mdx index cbbd8e15a..f98a7b542 100644 --- a/.vortex/docs/content/development/variables.mdx +++ b/.vortex/docs/content/development/variables.mdx @@ -189,6 +189,7 @@ The list below is automatically generated with [Shellvar](https://github.com/ale | `VORTEX_DB_IMAGE` | Name of the database container image to use.

See https://github.com/drevops/mariadb-drupal-data to seed your DB image. | `UNDEFINED` | `.env` | | `VORTEX_DB_IMAGE_BASE` | Name of the database fall-back container image to use.

If the image specified in [`$VORTEX_DB_IMAGE`](#vortex_db_image) does not exist and base image was provided - it will be used as a "clean slate" for the database. | `UNDEFINED` | `.env` | | `VORTEX_DEBUG` | Set to `1` to print debug information in Vortex scripts. | `UNDEFINED` | `.env.local.example` | +| `VORTEX_DEPLOY_ALLOW_LABEL` | Label that authorizes a pull request deployment. When set, a PR is deployed only if it carries this label. | `UNDEFINED` | `CI config` | | `VORTEX_DEPLOY_ALLOW_SKIP` | Flag to allow skipping of a deployment using additional flags. | `UNDEFINED` | `CI config` | | `VORTEX_DEPLOY_SKIP` | Skip all deployments. | `UNDEFINED` | `CI config` | | `VORTEX_DEPLOY_SKIP_BRANCHES` | Branch names to skip deployment for (single value or comma-separated list). | `UNDEFINED` | `CI config` | diff --git a/.vortex/installer/tests/Fixtures/handler_process/_baseline/.github/workflows/build-test-deploy.yml b/.vortex/installer/tests/Fixtures/handler_process/_baseline/.github/workflows/build-test-deploy.yml index f8aef8806..af7e487d7 100644 --- a/.vortex/installer/tests/Fixtures/handler_process/_baseline/.github/workflows/build-test-deploy.yml +++ b/.vortex/installer/tests/Fixtures/handler_process/_baseline/.github/workflows/build-test-deploy.yml @@ -598,6 +598,7 @@ jobs: VORTEX_DEPLOY_BRANCH: ${{ env.DEPLOY_BRANCH || github.head_ref || github.ref_name }} VORTEX_DEPLOY_PR: ${{ env.DEPLOY_PR_NUMBER || github.event.number }} VORTEX_DEPLOY_PR_HEAD: ${{ env.DEPLOY_PR_HEAD_SHA || github.event.pull_request.head.sha }} + VORTEX_DEPLOY_PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }} VORTEX_DEPLOY_ARTIFACT_SRC: /tmp/workspace/code VORTEX_DEPLOY_ARTIFACT_ROOT: ${{ github.workspace }} VORTEX_DEPLOY_ARTIFACT_GIT_REMOTE: ${{ vars.VORTEX_DEPLOY_ARTIFACT_GIT_REMOTE }} @@ -608,5 +609,6 @@ jobs: VORTEX_DEPLOY_ALLOW_SKIP: ${{ vars.VORTEX_DEPLOY_ALLOW_SKIP }} VORTEX_DEPLOY_SKIP_PRS: ${{ vars.VORTEX_DEPLOY_SKIP_PRS }} VORTEX_DEPLOY_SKIP_BRANCHES: ${{ vars.VORTEX_DEPLOY_SKIP_BRANCHES }} + VORTEX_DEPLOY_ALLOW_LABEL: ${{ vars.VORTEX_DEPLOY_ALLOW_LABEL }} VORTEX_DEPLOY_ACTION: ${{ inputs.override_db && 'deploy_override_db' || '' }} timeout-minutes: 30 diff --git a/.vortex/installer/tests/Fixtures/handler_process/deploy_types_none_gha/.github/workflows/build-test-deploy.yml b/.vortex/installer/tests/Fixtures/handler_process/deploy_types_none_gha/.github/workflows/build-test-deploy.yml index ef693734b..120a4426a 100644 --- a/.vortex/installer/tests/Fixtures/handler_process/deploy_types_none_gha/.github/workflows/build-test-deploy.yml +++ b/.vortex/installer/tests/Fixtures/handler_process/deploy_types_none_gha/.github/workflows/build-test-deploy.yml @@ -1,4 +1,4 @@ -@@ -515,98 +515,3 @@ +@@ -515,100 +515,3 @@ timeout-minutes: 120 # Cancel the action after 120 minutes, regardless of whether a connection has been established. with: detached: true @@ -85,6 +85,7 @@ - VORTEX_DEPLOY_BRANCH: ${{ env.DEPLOY_BRANCH || github.head_ref || github.ref_name }} - VORTEX_DEPLOY_PR: ${{ env.DEPLOY_PR_NUMBER || github.event.number }} - VORTEX_DEPLOY_PR_HEAD: ${{ env.DEPLOY_PR_HEAD_SHA || github.event.pull_request.head.sha }} +- VORTEX_DEPLOY_PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }} - VORTEX_DEPLOY_ARTIFACT_SRC: /tmp/workspace/code - VORTEX_DEPLOY_ARTIFACT_ROOT: ${{ github.workspace }} - VORTEX_DEPLOY_ARTIFACT_GIT_REMOTE: ${{ vars.VORTEX_DEPLOY_ARTIFACT_GIT_REMOTE }} @@ -95,5 +96,6 @@ - VORTEX_DEPLOY_ALLOW_SKIP: ${{ vars.VORTEX_DEPLOY_ALLOW_SKIP }} - VORTEX_DEPLOY_SKIP_PRS: ${{ vars.VORTEX_DEPLOY_SKIP_PRS }} - VORTEX_DEPLOY_SKIP_BRANCHES: ${{ vars.VORTEX_DEPLOY_SKIP_BRANCHES }} +- VORTEX_DEPLOY_ALLOW_LABEL: ${{ vars.VORTEX_DEPLOY_ALLOW_LABEL }} - VORTEX_DEPLOY_ACTION: ${{ inputs.override_db && 'deploy_override_db' || '' }} - timeout-minutes: 30 diff --git a/.vortex/tooling/src/deploy b/.vortex/tooling/src/deploy index 9b6268e06..6df068c15 100755 --- a/.vortex/tooling/src/deploy +++ b/.vortex/tooling/src/deploy @@ -49,6 +49,21 @@ VORTEX_DEPLOY_PR="${VORTEX_DEPLOY_PR:-}" # Flag to allow skipping of a deployment using additional flags. VORTEX_DEPLOY_ALLOW_SKIP="${VORTEX_DEPLOY_ALLOW_SKIP:-}" +# Label that authorizes a pull request deployment. +# +# When set to a non-empty label name, a PR deployment proceeds only if the +# pull request carries that label; PRs without it are skipped. When empty (the +# default), the gate is inactive and deployments behave as before. Only applies +# to PR builds (VORTEX_DEPLOY_PR set); branch and tag deployments are unaffected. +VORTEX_DEPLOY_ALLOW_LABEL="${VORTEX_DEPLOY_ALLOW_LABEL:-}" + +# Comma-separated list of labels present on the deployed pull request. +# +# Populated by the CI provider from the pull request event (for example, in +# GitHub Actions from "github.event.pull_request.labels"). Consulted by the +# VORTEX_DEPLOY_ALLOW_LABEL gate. +VORTEX_DEPLOY_PR_LABELS="${VORTEX_DEPLOY_PR_LABELS:-}" + # ------------------------------------------------------------------------------ # @formatter:off @@ -105,6 +120,22 @@ if [ "${VORTEX_DEPLOY_ALLOW_SKIP:-}" = "1" ]; then fi fi +if [ -n "${VORTEX_DEPLOY_ALLOW_LABEL}" ] && [ -n "${VORTEX_DEPLOY_PR}" ]; then + # Gate a pull request deployment on the presence of a label. + # + # The PR's labels are provided as a comma-separated list in + # $VORTEX_DEPLOY_PR_LABELS, populated by the CI provider from the PR event. + note "Found flag to gate deployment on the '${VORTEX_DEPLOY_ALLOW_LABEL}' label." + + if ! echo ",${VORTEX_DEPLOY_PR_LABELS}," | grep -qF ",${VORTEX_DEPLOY_ALLOW_LABEL},"; then + note "PR ${VORTEX_DEPLOY_PR} does not carry the '${VORTEX_DEPLOY_ALLOW_LABEL}' label." + note "Skipping deployment ${VORTEX_DEPLOY_TYPES}." + exit 0 + fi + + note "PR ${VORTEX_DEPLOY_PR} carries the '${VORTEX_DEPLOY_ALLOW_LABEL}' label." +fi + if [ -z "${VORTEX_DEPLOY_TYPES##*artifact*}" ]; then [ "${VORTEX_DEPLOY_MODE}" = "tag" ] && export VORTEX_DEPLOY_ARTIFACT_DST_BRANCH="deployment/[tags:-]" "${SCRIPT_DIR}/deploy-artifact" diff --git a/.vortex/tooling/tests/unit/deploy.bats b/.vortex/tooling/tests/unit/deploy.bats index dd755913b..2b7d6459c 100644 --- a/.vortex/tooling/tests/unit/deploy.bats +++ b/.vortex/tooling/tests/unit/deploy.bats @@ -306,3 +306,171 @@ load ../_helper.bash popd >/dev/null } + +@test "Label gate inactive when VORTEX_DEPLOY_ALLOW_LABEL is unset" { + pushd "${LOCAL_REPO_DIR}" >/dev/null || exit 1 + + export VORTEX_DEPLOY_TYPES="webhook" + export VORTEX_DEPLOY_WEBHOOK_URL="https://example.com" + export VORTEX_DEPLOY_PR="123" + export VORTEX_DEPLOY_PR_LABELS="bug,enhancement" + + mock_deploy_webhook=$(mock_command ".vortex/tooling/src/deploy-webhook") + mock_set_output "${mock_deploy_webhook}" "Webhook deployment completed" 0 + + run .vortex/tooling/src/deploy + assert_success + assert_output_not_contains "Found flag to gate deployment" + assert_output_not_contains "Skipping deployment" + + popd >/dev/null +} + +@test "Deployment proceeds when PR carries the required label" { + pushd "${LOCAL_REPO_DIR}" >/dev/null || exit 1 + + export VORTEX_DEPLOY_TYPES="webhook" + export VORTEX_DEPLOY_WEBHOOK_URL="https://example.com" + export VORTEX_DEPLOY_PR="123" + export VORTEX_DEPLOY_ALLOW_LABEL="deploy" + export VORTEX_DEPLOY_PR_LABELS="deploy" + + mock_deploy_webhook=$(mock_command ".vortex/tooling/src/deploy-webhook") + mock_set_output "${mock_deploy_webhook}" "Webhook deployment completed" 0 + + run .vortex/tooling/src/deploy + assert_success + assert_output_contains "Found flag to gate deployment on the 'deploy' label." + assert_output_contains "PR 123 carries the 'deploy' label." + assert_output_not_contains "Skipping deployment" + + popd >/dev/null +} + +@test "Deployment skipped when PR lacks the required label" { + pushd "${LOCAL_REPO_DIR}" >/dev/null || exit 1 + + export VORTEX_DEPLOY_TYPES="webhook" + export VORTEX_DEPLOY_PR="123" + export VORTEX_DEPLOY_ALLOW_LABEL="deploy" + export VORTEX_DEPLOY_PR_LABELS="bug,enhancement" + + run .vortex/tooling/src/deploy + assert_success + assert_output_contains "Found flag to gate deployment on the 'deploy' label." + assert_output_contains "PR 123 does not carry the 'deploy' label." + assert_output_contains "Skipping deployment webhook." + + popd >/dev/null +} + +@test "Label gate does not apply to non-PR deployments" { + pushd "${LOCAL_REPO_DIR}" >/dev/null || exit 1 + + export VORTEX_DEPLOY_TYPES="webhook" + export VORTEX_DEPLOY_WEBHOOK_URL="https://example.com" + export VORTEX_DEPLOY_ALLOW_LABEL="deploy" + export VORTEX_DEPLOY_PR="" + export VORTEX_DEPLOY_BRANCH="main" + + mock_deploy_webhook=$(mock_command ".vortex/tooling/src/deploy-webhook") + mock_set_output "${mock_deploy_webhook}" "Webhook deployment completed" 0 + + run .vortex/tooling/src/deploy + assert_success + assert_output_not_contains "Found flag to gate deployment" + assert_output_not_contains "Skipping deployment" + + popd >/dev/null +} + +@test "Deployment proceeds when required label is among several PR labels" { + pushd "${LOCAL_REPO_DIR}" >/dev/null || exit 1 + + export VORTEX_DEPLOY_TYPES="webhook" + export VORTEX_DEPLOY_WEBHOOK_URL="https://example.com" + export VORTEX_DEPLOY_PR="456" + export VORTEX_DEPLOY_ALLOW_LABEL="deploy" + export VORTEX_DEPLOY_PR_LABELS="bug,deploy,enhancement" + + mock_deploy_webhook=$(mock_command ".vortex/tooling/src/deploy-webhook") + mock_set_output "${mock_deploy_webhook}" "Webhook deployment completed" 0 + + run .vortex/tooling/src/deploy + assert_success + assert_output_contains "PR 456 carries the 'deploy' label." + assert_output_not_contains "Skipping deployment" + + popd >/dev/null +} + +@test "Skip list takes precedence over the label gate" { + pushd "${LOCAL_REPO_DIR}" >/dev/null || exit 1 + + export VORTEX_DEPLOY_TYPES="webhook" + export VORTEX_DEPLOY_ALLOW_SKIP="1" + export VORTEX_DEPLOY_PR="123" + export VORTEX_DEPLOY_SKIP_PRS="123" + export VORTEX_DEPLOY_ALLOW_LABEL="deploy" + export VORTEX_DEPLOY_PR_LABELS="deploy" + + run .vortex/tooling/src/deploy + assert_success + assert_output_contains "Found PR 123 in skip list." + assert_output_contains "Skipping deployment webhook." + assert_output_not_contains "Found flag to gate deployment" + + popd >/dev/null +} + +@test "Deployment skipped when PR has no labels and gate is enabled" { + pushd "${LOCAL_REPO_DIR}" >/dev/null || exit 1 + + export VORTEX_DEPLOY_TYPES="webhook" + export VORTEX_DEPLOY_PR="123" + export VORTEX_DEPLOY_ALLOW_LABEL="deploy" + export VORTEX_DEPLOY_PR_LABELS="" + + run .vortex/tooling/src/deploy + assert_success + assert_output_contains "PR 123 does not carry the 'deploy' label." + assert_output_contains "Skipping deployment webhook." + + popd >/dev/null +} + +@test "Label gate requires an exact match, not a partial one" { + pushd "${LOCAL_REPO_DIR}" >/dev/null || exit 1 + + export VORTEX_DEPLOY_TYPES="webhook" + export VORTEX_DEPLOY_PR="123" + export VORTEX_DEPLOY_ALLOW_LABEL="deploy" + export VORTEX_DEPLOY_PR_LABELS="deploy-preview,needs-deploy" + + run .vortex/tooling/src/deploy + assert_success + assert_output_contains "PR 123 does not carry the 'deploy' label." + assert_output_contains "Skipping deployment webhook." + + popd >/dev/null +} + +@test "Label gate matches a label that contains spaces" { + pushd "${LOCAL_REPO_DIR}" >/dev/null || exit 1 + + export VORTEX_DEPLOY_TYPES="webhook" + export VORTEX_DEPLOY_WEBHOOK_URL="https://example.com" + export VORTEX_DEPLOY_PR="123" + export VORTEX_DEPLOY_ALLOW_LABEL="deploy to env" + export VORTEX_DEPLOY_PR_LABELS="bug,deploy to env,enhancement" + + mock_deploy_webhook=$(mock_command ".vortex/tooling/src/deploy-webhook") + mock_set_output "${mock_deploy_webhook}" "Webhook deployment completed" 0 + + run .vortex/tooling/src/deploy + assert_success + assert_output_contains "PR 123 carries the 'deploy to env' label." + assert_output_not_contains "Skipping deployment" + + popd >/dev/null +}