From aedb49209efe1204f072cca7a312eb6735c45b88 Mon Sep 17 00:00:00 2001 From: David Schach <636977+dschach@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:12:06 +1100 Subject: [PATCH] feat(dependabot-reusable): add per-type approve and merge inputs Add 6 optional inputs: autoapprove-production, automerge-production, autoapprove-development, automerge-development, autoapprove-indirect, and automerge-indirect. Each one is a comma-separated list of major, minor, patch, and security (or none). When a list is set, it replaces the autoapprove-* or automerge-* booleans for PRs of that dependency type, so 1 call (1 job, 1 runner) can apply different rules to each dependency type. Before, each set of rules needed its own call and its own job. Empty lists (the default) keep the old behavior, so existing callers do not change. The security alert lookup also runs when a list includes security. Bad list values fail the decide step with a clear summary message. Add 12 tests for the new inputs, and update the README and the workflow comments to show the 1-call pattern. Co-Authored-By: Claude --- .github/workflows/dependabot-reusable.yml | 178 ++++++++++++++++++---- README.md | 68 +++++---- tests/dependabot-reusable/run_tests.py | 28 ++++ 3 files changed, 215 insertions(+), 59 deletions(-) diff --git a/.github/workflows/dependabot-reusable.yml b/.github/workflows/dependabot-reusable.yml index fe590aa..752336b 100644 --- a/.github/workflows/dependabot-reusable.yml +++ b/.github/workflows/dependabot-reusable.yml @@ -70,26 +70,24 @@ # # Different rules for each dependency type: # -# Call this workflow 2 or more times, with 1 job for each call. Give each call its -# own "dependency-types" and its own booleans. Set "rename-title: false" on all -# calls but 1. The title edit and all label steps then run only 1 time. +# Use the per-type inputs in 1 call. Each one is a comma-separated list of major, +# minor, patch, and security (or "none"). If a list is set, it replaces the +# autoapprove-* or automerge-* booleans for PRs of that dependency type. # # jobs: -# production: +# dependabot: # uses: x2od/.github/.github/workflows/dependabot-reusable.yml@main # with: -# dependency-types: direct:production,indirect -# autoapprove-patch: true -# automerge-patch: true -# development: -# uses: x2od/.github/.github/workflows/dependabot-reusable.yml@main -# with: -# dependency-types: direct:development -# rename-title: false -# autoapprove-minor: true -# autoapprove-patch: true -# automerge-minor: true -# automerge-patch: true +# autoapprove-production: patch,security +# automerge-production: patch,security +# autoapprove-development: minor,patch,security +# automerge-development: minor,patch,security +# autoapprove-indirect: minor,patch,security +# automerge-indirect: patch,security +# +# 1 call is 1 job, so each Dependabot event uses 1 runner. You can still call this +# workflow 2 or more times, each with its own "dependency-types". Then each call +# is a separate job, and you set "rename-title: false" on all calls but 1. # # --------------------------------------------------------------------------------- # THE OPTIONAL "token" SECRET @@ -208,6 +206,54 @@ on: type: string required: false default: direct:production,direct:development,indirect + autoapprove-production: + description: >- + Approve these levels for direct:production dependencies, as a comma-separated list + of major, minor, patch, and security (or "none"). If set, it replaces the + autoapprove-major/minor/patch/security inputs for this dependency type. + type: string + required: false + default: '' + automerge-production: + description: >- + Turn on auto-merge for these levels for direct:production dependencies, as a comma-separated list + of major, minor, patch, and security (or "none"). If set, it replaces the + automerge-major/minor/patch/security inputs for this dependency type. + type: string + required: false + default: '' + autoapprove-development: + description: >- + Approve these levels for direct:development dependencies, as a comma-separated list + of major, minor, patch, and security (or "none"). If set, it replaces the + autoapprove-major/minor/patch/security inputs for this dependency type. + type: string + required: false + default: '' + automerge-development: + description: >- + Turn on auto-merge for these levels for direct:development dependencies, as a comma-separated list + of major, minor, patch, and security (or "none"). If set, it replaces the + automerge-major/minor/patch/security inputs for this dependency type. + type: string + required: false + default: '' + autoapprove-indirect: + description: >- + Approve these levels for indirect dependencies, as a comma-separated list + of major, minor, patch, and security (or "none"). If set, it replaces the + autoapprove-major/minor/patch/security inputs for this dependency type. + type: string + required: false + default: '' + automerge-indirect: + description: >- + Turn on auto-merge for these levels for indirect dependencies, as a comma-separated list + of major, minor, patch, and security (or "none"). If set, it replaces the + automerge-major/minor/patch/security inputs for this dependency type. + type: string + required: false + default: '' rename-title: description: >- Edit the title and add the labels. When you call this workflow more than @@ -330,7 +376,11 @@ jobs: - name: Look up the Dependabot security alert id: alert # The alert lookup needs a PAT. If it fails, the PR is not a security update. - if: ${{ env.HAS_TOKEN == 'true' && (inputs.autoapprove-security || inputs.automerge-security) }} + if: >- + ${{ env.HAS_TOKEN == 'true' && (inputs.autoapprove-security || inputs.automerge-security || + contains(format('{0},{1},{2},{3},{4},{5}', inputs.autoapprove-production, inputs.automerge-production, + inputs.autoapprove-development, inputs.automerge-development, inputs.autoapprove-indirect, + inputs.automerge-indirect), 'security')) }} continue-on-error: true uses: dependabot/fetch-metadata@v3 with: @@ -512,10 +562,40 @@ jobs: MERGE_MINOR: ${{ inputs.automerge-minor }} MERGE_PATCH: ${{ inputs.automerge-patch }} MERGE_SECURITY: ${{ inputs.automerge-security }} + APPROVE_PRODUCTION: ${{ inputs.autoapprove-production }} + MERGE_PRODUCTION: ${{ inputs.automerge-production }} + APPROVE_DEVELOPMENT: ${{ inputs.autoapprove-development }} + MERGE_DEVELOPMENT: ${{ inputs.automerge-development }} + APPROVE_INDIRECT: ${{ inputs.autoapprove-indirect }} + MERGE_INDIRECT: ${{ inputs.automerge-indirect }} run: | summary() { printf '%s\n' "$@" >> "$GITHUB_STEP_SUMMARY"; } summary "### Approve and merge" + # True if the comma-separated list $1 contains $2. Spaces are ignored. + in_list() { + local list="${1//[[:space:]]/}" + [[ ",$list," == *",$2,"* ]] + } + + # Text for the summary: which input made the decision. + # $1 input name, $2 per-type list (empty = boolean input), $3 level, $4 yes/no, + # $5 boolean input name to use when $1 is empty. + rule_text() { + local name="${1:-$5}" + if [[ -n "$2" ]]; then + if [[ "$4" == "yes" ]]; then + printf "\`%s\` includes \`%s\`." "$name" "$3" + else + printf "\`%s\` (\`%s\`) does not include \`%s\`." "$name" "$2" "$3" + fi + elif [[ "$4" == "yes" ]]; then + printf "\`%s\` is \`true\`." "$name" + else + printf "\`%s\` is \`false\`." "$name" + fi + } + case "$MERGE_METHOD" in squash | merge | rebase) ;; *) @@ -537,6 +617,23 @@ jobs: summary "- :x: The \`wait-timeout-minutes\` input must be a whole number from 1 to 50. The value is \`$WAIT_TIMEOUT_MINUTES\`." exit 1 fi + for pair in "autoapprove-production=$APPROVE_PRODUCTION" "automerge-production=$MERGE_PRODUCTION" \ + "autoapprove-development=$APPROVE_DEVELOPMENT" "automerge-development=$MERGE_DEVELOPMENT" \ + "autoapprove-indirect=$APPROVE_INDIRECT" "automerge-indirect=$MERGE_INDIRECT"; do + name="${pair%%=*}" + value="${pair#*=}" + IFS=',' read -ra items <<< "${value//[[:space:]]/}" + for item in "${items[@]}"; do + case "$item" in + major | minor | patch | security | none) ;; + *) + echo "::error title=Bad input::$name must list major, minor, patch, security, or none. The value is '$value'." + summary "- :x: The \`$name\` input must be a comma-separated list of \`major\`, \`minor\`, \`patch\`, and \`security\` (or \`none\`). The value is \`$value\`." + exit 1 + ;; + esac + done + done case "$UPDATE_TYPE" in version-update:semver-major) level=major approve_level="$APPROVE_MAJOR" merge_level="$MERGE_MAJOR" ;; @@ -544,6 +641,35 @@ jobs: version-update:semver-patch) level=patch approve_level="$APPROVE_PATCH" merge_level="$MERGE_PATCH" ;; *) level="" approve_level=false merge_level=false ;; esac + approve_security="$APPROVE_SECURITY" + merge_security="$MERGE_SECURITY" + + # Per-type rules. A non-empty autoapprove- or automerge- list replaces + # the boolean inputs for PRs of that dependency type. + case "$DEPENDENCY_TYPE" in + direct:production) type_key=production approve_list="$APPROVE_PRODUCTION" merge_list="$MERGE_PRODUCTION" ;; + direct:development) type_key=development approve_list="$APPROVE_DEVELOPMENT" merge_list="$MERGE_DEVELOPMENT" ;; + indirect) type_key=indirect approve_list="$APPROVE_INDIRECT" merge_list="$MERGE_INDIRECT" ;; + *) type_key="" approve_list="" merge_list="" ;; + esac + approve_list="${approve_list//[[:space:]]/}" + merge_list="${merge_list//[[:space:]]/}" + approve_name="autoapprove-${level:-security}" + merge_name="automerge-${level:-security}" + if [[ -n "$approve_list" ]]; then + approve_name="autoapprove-$type_key" + approve_level=false + approve_security=false + if [[ -n "$level" ]] && in_list "$approve_list" "$level"; then approve_level=true; fi + if in_list "$approve_list" security; then approve_security=true; fi + fi + if [[ -n "$merge_list" ]]; then + merge_name="automerge-$type_key" + merge_level=false + merge_security=false + if [[ -n "$level" ]] && in_list "$merge_list" "$level"; then merge_level=true; fi + if in_list "$merge_list" security; then merge_security=true; fi + fi type_allowed=false IFS=',' read -ra allowed_types <<< "$DEPENDENCY_TYPES" @@ -560,7 +686,7 @@ jobs: fi # Security alert row - if [[ "$APPROVE_SECURITY" == "true" || "$MERGE_SECURITY" == "true" ]]; then + if [[ "$approve_security" == "true" || "$merge_security" == "true" ]]; then if [[ "$HAS_TOKEN" != "true" ]]; then summary "- :information_source: Security alert: not checked. The alert lookup needs the \`token\` secret, stored as a Dependabot secret." elif [[ "$ALERT_OUTCOME" == "failure" ]]; then @@ -584,22 +710,22 @@ jobs: # Approve if [[ "$approve_level" == "true" ]]; then approve=true - summary "- :white_check_mark: Approve: yes. \`autoapprove-$level\` is \`true\`." - elif [[ "$security" == "true" && "$APPROVE_SECURITY" == "true" ]]; then + summary "- :white_check_mark: Approve: yes. $(rule_text "$approve_name" "$approve_list" "$level" yes)" + elif [[ "$security" == "true" && "$approve_security" == "true" ]]; then approve=true - summary "- :white_check_mark: Approve: yes. This is a security update and \`autoapprove-security\` is \`true\`." + summary "- :white_check_mark: Approve: yes. This is a security update, and $(rule_text "${approve_list:+autoapprove-$type_key}" "$approve_list" security yes autoapprove-security)" else - summary "- :no_entry_sign: Approve: no. \`autoapprove-${level:-security}\` is \`false\`." + summary "- :no_entry_sign: Approve: no. $(rule_text "$approve_name" "$approve_list" "${level:-security}" no)" fi # Merge if [[ "$merge_level" == "true" ]]; then merge=true - summary "- :white_check_mark: Merge: yes. \`automerge-$level\` is \`true\`." - elif [[ "$security" == "true" && "$MERGE_SECURITY" == "true" ]]; then + summary "- :white_check_mark: Merge: yes. $(rule_text "$merge_name" "$merge_list" "$level" yes)" + elif [[ "$security" == "true" && "$merge_security" == "true" ]]; then merge=true - summary "- :white_check_mark: Merge: yes. This is a security update and \`automerge-security\` is \`true\`." + summary "- :white_check_mark: Merge: yes. This is a security update, and $(rule_text "${merge_list:+automerge-$type_key}" "$merge_list" security yes automerge-security)" else - summary "- :no_entry_sign: Merge: no. \`automerge-${level:-security}\` is \`false\`." + summary "- :no_entry_sign: Merge: no. $(rule_text "$merge_name" "$merge_list" "${level:-security}" no)" fi if [[ "$merge" == "true" && "$approve" != "true" ]]; then summary "- :information_source: Merge without approval works only if the repository does not require a review." diff --git a/README.md b/README.md index ea4a8ed..8a6adfa 100644 --- a/README.md +++ b/README.md @@ -107,24 +107,30 @@ When a repository calls the workflow more than 1 time, only the call with `renam All inputs are optional. -| Input | Type | Default | Description | -| --------------------------- | ------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `autoapprove-major` | boolean | `false` | Approve major updates. | -| `autoapprove-minor` | boolean | `false` | Approve minor updates. | -| `autoapprove-patch` | boolean | `false` | Approve patch updates. | -| `automerge-major` | boolean | `false` | Turn on auto-merge for major updates. | -| `automerge-minor` | boolean | `false` | Turn on auto-merge for minor updates. | -| `automerge-patch` | boolean | `false` | Turn on auto-merge for patch updates. | -| `autoapprove-security` | boolean | `false` | Approve updates that fix a Dependabot security alert, at all update levels. Needs the "token" secret with "Dependabot alerts: Read-only". | -| `automerge-security` | boolean | `false` | Turn on auto-merge for updates that fix a Dependabot security alert, at all update levels. Needs the "token" secret with "Dependabot alerts: Read-only". | -| `dependency-types` | string | `direct:production,direct:development,indirect` | Comma-separated dependency types that this call can approve and merge. Valid values: direct:production, direct:development, indirect. | -| `rename-title` | boolean | `true` | Edit the title and add the labels. When you call this workflow more than 1 time, set this to false on all calls but 1. | -| `indirect-development-type` | string | `build` | The title type for an npm package that only development dependencies need ("dev": true in package-lock.json). It replaces the type "deps", which Dependabot can give to an indirect package. Set to an empty string to keep "deps". | -| `create-labels` | boolean | `true` | Create the "dependabot" and "auto-merged" labels if they do not exist. | -| `merge-method` | string | `squash` | The merge method: squash, merge, or rebase. | -| `merge-mode` | string | `auto` | How to merge. "auto": use GitHub auto-merge if the repository allows it; if not, wait for the checks in the job, then merge. "wait": always wait for the checks in the job, then merge. "now": merge at once, without a wait. | -| `wait-timeout-minutes` | number | `30` | For a wait in the job: the maximum time to wait for the checks, from 1 to 50. After this time, the PR stays open. | -| `token-user` | string | (empty) | The login of the "token" user. Leave empty to find it automatically. The workflow uses it to find its own approvals and its own merges. | +| Input | Type | Default | Description | +| --------------------------- | ------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `autoapprove-major` | boolean | `false` | Approve major updates. | +| `autoapprove-minor` | boolean | `false` | Approve minor updates. | +| `autoapprove-patch` | boolean | `false` | Approve patch updates. | +| `automerge-major` | boolean | `false` | Turn on auto-merge for major updates. | +| `automerge-minor` | boolean | `false` | Turn on auto-merge for minor updates. | +| `automerge-patch` | boolean | `false` | Turn on auto-merge for patch updates. | +| `autoapprove-security` | boolean | `false` | Approve updates that fix a Dependabot security alert, at all update levels. Needs the "token" secret with "Dependabot alerts: Read-only". | +| `automerge-security` | boolean | `false` | Turn on auto-merge for updates that fix a Dependabot security alert, at all update levels. Needs the "token" secret with "Dependabot alerts: Read-only". | +| `dependency-types` | string | `direct:production,direct:development,indirect` | Comma-separated dependency types that this call can approve and merge. Valid values: direct:production, direct:development, indirect. | +| `autoapprove-production` | string | (empty) | Approve these levels for direct:production dependencies, as a comma-separated list of major, minor, patch, and security (or "none"). If set, it replaces the autoapprove-major/minor/patch/security inputs for this dependency type. | +| `automerge-production` | string | (empty) | Turn on auto-merge for these levels for direct:production dependencies, as a comma-separated list of major, minor, patch, and security (or "none"). If set, it replaces the automerge-major/minor/patch/security inputs for this dependency type. | +| `autoapprove-development` | string | (empty) | Approve these levels for direct:development dependencies, as a comma-separated list of major, minor, patch, and security (or "none"). If set, it replaces the autoapprove-major/minor/patch/security inputs for this dependency type. | +| `automerge-development` | string | (empty) | Turn on auto-merge for these levels for direct:development dependencies, as a comma-separated list of major, minor, patch, and security (or "none"). If set, it replaces the automerge-major/minor/patch/security inputs for this dependency type. | +| `autoapprove-indirect` | string | (empty) | Approve these levels for indirect dependencies, as a comma-separated list of major, minor, patch, and security (or "none"). If set, it replaces the autoapprove-major/minor/patch/security inputs for this dependency type. | +| `automerge-indirect` | string | (empty) | Turn on auto-merge for these levels for indirect dependencies, as a comma-separated list of major, minor, patch, and security (or "none"). If set, it replaces the automerge-major/minor/patch/security inputs for this dependency type. | +| `rename-title` | boolean | `true` | Edit the title and add the labels. When you call this workflow more than 1 time, set this to false on all calls but 1. | +| `indirect-development-type` | string | `build` | The title type for an npm package that only development dependencies need ("dev": true in package-lock.json). It replaces the type "deps", which Dependabot can give to an indirect package. Set to an empty string to keep "deps". | +| `create-labels` | boolean | `true` | Create the "dependabot" and "auto-merged" labels if they do not exist. | +| `merge-method` | string | `squash` | The merge method: squash, merge, or rebase. | +| `merge-mode` | string | `auto` | How to merge. "auto": use GitHub auto-merge if the repository allows it; if not, wait for the checks in the job, then merge. "wait": always wait for the checks in the job, then merge. "now": merge at once, without a wait. | +| `wait-timeout-minutes` | number | `30` | For a wait in the job: the maximum time to wait for the checks, from 1 to 50. After this time, the PR stays open. | +| `token-user` | string | (empty) | The login of the "token" user. Leave empty to find it automatically. The workflow uses it to find its own approvals and its own merges. | ### The `token` secret @@ -166,28 +172,24 @@ A classic PAT needs the `repo` scope and the `workflow` scope. A fine-grained PA ### Different rules for each dependency type -Call the workflow 2 or more times, with 1 job for each call. Give each call its own `dependency-types` value and its own booleans. Set `rename-title: false` on all calls but 1. +Use the per-type inputs in 1 call: `autoapprove-production`, `automerge-production`, `autoapprove-development`, `automerge-development`, `autoapprove-indirect`, and `automerge-indirect`. Each one is a comma-separated list of `major`, `minor`, `patch`, and `security` (or `none`). If a list is set, it replaces the `autoapprove-*` or `automerge-*` booleans for PRs of that dependency type. If a list is empty (the default), the booleans apply. ```yaml jobs: - production: - uses: x2od/.github/.github/workflows/dependabot-reusable.yml@main - with: - dependency-types: direct:production - autoapprove-patch: true - automerge-patch: true - development: + dependabot: uses: x2od/.github/.github/workflows/dependabot-reusable.yml@main with: - dependency-types: direct:development - rename-title: false - autoapprove-minor: true - autoapprove-patch: true - automerge-minor: true - automerge-patch: true + autoapprove-production: patch,security + automerge-production: patch,security + autoapprove-development: minor,patch,security + automerge-development: minor,patch,security + autoapprove-indirect: minor,patch,security + automerge-indirect: patch,security ``` -(Each job also needs the `if`, `permissions`, and `secrets` lines from the first example.) +(The job also needs the `if`, `permissions`, and `secrets` lines from the first example.) + +1 call is 1 job, so every Dependabot event uses 1 runner. You can still call the workflow 2 or more times, each with its own `dependency-types` value. Then each call is a separate job with its own runner, and you set `rename-title: false` on all calls but 1. ### Tests diff --git a/tests/dependabot-reusable/run_tests.py b/tests/dependabot-reusable/run_tests.py index 92dc3c9..8d4adf5 100755 --- a/tests/dependabot-reusable/run_tests.py +++ b/tests/dependabot-reusable/run_tests.py @@ -192,6 +192,34 @@ def dec(**kw): r = dec(WAIT_TIMEOUT_MINUTES="50", MERGE_MODE="wait", APPROVE_PATCH="true") check("wait-timeout 50 + wait mode ok", r["code"] == 0 and r["out"]["approve"] == "true", r) +# Per-type lists (autoapprove- / automerge-) replace the booleans for that type +r = dec(DEPENDENCY_TYPE="indirect", UPDATE_TYPE="version-update:semver-minor", APPROVE_INDIRECT="minor,patch", MERGE_INDIRECT="patch") +check("per-type: indirect minor approved, not merged", r["out"]["approve"] == "true" and r["out"]["merge"] == "false" + and "`autoapprove-indirect` includes `minor`" in r["summary"] and "`automerge-indirect` (`patch`) does not include `minor`" in r["summary"], r) +r = dec(DEPENDENCY_TYPE="direct:production", UPDATE_TYPE="version-update:semver-patch", APPROVE_PATCH="true", MERGE_PATCH="true", APPROVE_PRODUCTION="none", MERGE_PRODUCTION="none") +check("per-type: none overrides true booleans", r["out"]["approve"] == "false" and r["out"]["merge"] == "false", r) +r = dec(DEPENDENCY_TYPE="direct:development", UPDATE_TYPE="version-update:semver-patch", APPROVE_PATCH="true", APPROVE_PRODUCTION="none") +check("per-type: other type's list does not apply", r["out"]["approve"] == "true" and "`autoapprove-patch` is `true`" in r["summary"], r) +r = dec(DEPENDENCY_TYPE="direct:development", UPDATE_TYPE="version-update:semver-major", APPROVE_DEVELOPMENT=" minor , patch ", MERGE_DEVELOPMENT="minor,patch") +check("per-type: spaces ignored, major not in list", r["out"]["approve"] == "false" and r["out"]["merge"] == "false", r) +r = dec(DEPENDENCY_TYPE="indirect", UPDATE_TYPE="version-update:semver-major", GHSA_ID="GHSA-1", HAS_TOKEN="true", ALERT_OUTCOME="success", + APPROVE_INDIRECT="patch,security", MERGE_INDIRECT="security") +check("per-type: security in list approves and merges a major security fix", r["out"]["approve"] == "true" and r["out"]["merge"] == "true" + and "`automerge-indirect` includes `security`" in r["summary"] and "GHSA-1" in r["summary"], r) +r = dec(DEPENDENCY_TYPE="indirect", UPDATE_TYPE="version-update:semver-major", GHSA_ID="GHSA-1", HAS_TOKEN="true", ALERT_OUTCOME="success", + APPROVE_SECURITY="true", MERGE_SECURITY="true", APPROVE_INDIRECT="patch", MERGE_INDIRECT="patch") +check("per-type: list without security overrides true security booleans", r["out"]["approve"] == "false" and r["out"]["merge"] == "false", r) +r = dec(DEPENDENCY_TYPE="direct:production", UPDATE_TYPE="version-update:semver-patch", APPROVE_PRODUCTION="patch,security", HAS_TOKEN="false") +check("per-type: security in list shows the alert row", "needs the `token` secret" in r["summary"], r) +r = dec(DEPENDENCY_TYPE="direct:production", UPDATE_TYPE="version-update:semver-minor", MERGE_PRODUCTION="minor", APPROVE_PRODUCTION="none") +check("per-type: merge without approval warns", r["out"]["merge"] == "true" and r["out"]["approve"] == "false" and "Merge without approval" in r["summary"], r) +r = dec(DEPENDENCY_TYPE="direct:production", UPDATE_TYPE="version-update:semver-minor", APPROVE_PRODUCTION="minor", MERGE_PRODUCTION="minor", + DEPENDENCY_TYPES="direct:development") +check("per-type: dependency-types filter still applies", r["out"]["approve"] == "false" and "is not in `dependency-types`" in r["summary"], r) +for name, var, bad in (("autoapprove-indirect", "APPROVE_INDIRECT", "minor,huge"), ("automerge-production", "MERGE_PRODUCTION", "true")): + r = dec(**{var: bad}) + check(f"per-type: bad value in {name} fails", r["code"] == 1 and f"`{name}` input must be" in r["summary"], r) + print("Approve") APP = "Approve the PR" r = run("update", APP, {"TOKEN_USER": "github-actions[bot]", "LEVEL": "patch"}, {"STUB_REVIEWS": ""})