Skip to content

Add comprehensive tag protection policy verification for repository rulesets#36

Merged
david3107 merged 4 commits intomainfrom
copilot/add-tag-protection-settings
Nov 21, 2025
Merged

Add comprehensive tag protection policy verification for repository rulesets#36
david3107 merged 4 commits intomainfrom
copilot/add-tag-protection-settings

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Oct 19, 2025

Implementation Plan for Tag Protection Policy ✅

  • Explore repository structure and understand existing patterns
  • Build and verify project builds successfully
  • Add tags section to repository.yml policy file with comprehensive structure
  • Create API method to fetch repository rulesets (GET /repos/{owner}/{repo}/rulesets)
  • Create TagProtectionChecks evaluator class with full validation
  • Integrate TagProtectionChecks into RepoPolicyEvaluator
  • Add TypeScript type definitions for tag protection policy
  • Build and test the implementation
  • Address feedback on policy structure
  • Fix implementation based on real API response

✅ Implementation Complete

Latest Updates (based on real API response)

Fixed the implementation to properly handle GitHub's actual API response format:

  1. Scope Pattern Normalization: GitHub API returns patterns with refs/tags/ prefix (e.g., refs/tags/v*). The implementation now strips this prefix to match against policy patterns (e.g., v*).

  2. Tag Name Pattern Validation: Properly implemented validation of tag_name_pattern rule type with parameters:

    • Validates operator (regex, starts_with, ends_with, contains)
    • Validates pattern (the actual regex or string pattern)
    • Validates negate (whether pattern should be negated)

Policy Structure

The tag protection policy includes:

  1. enforcement: Level of enforcement (disabled | active | evaluate)
  2. target: Fixed to "tag" for tag rulesets
  3. scope: Include/exclude patterns for tag matching
  4. operations: Restrictions on create/update/delete operations
  5. naming: Optional naming constraints with regex patterns
  6. bypass: Comprehensive bypass actor configuration
    • Organization admins
    • Teams (with IDs and modes)
    • Integrations (GitHub Apps)
    • Repository roles
    • Deploy keys

Example Configuration

tags:
  enforcement: active
  target: tag
  scope:
    include:
      - "v*"
    exclude: []
  operations:
    create: restricted
    update: restricted
    delete: restricted
  naming:
    enabled: true
    operator: regex
    pattern: "^v\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?$"
    negate: false
  bypass:
    organization_admins: always
    teams:
      - id: 1234567
        mode: always
    integrations:
      - id: 987654
        mode: always
    repository_roles:
      - id: 3
        mode: always
    deploy_keys:
      allow: true
      mode: always

Verification Results ✅

  • ✅ TypeScript compilation successful
  • ✅ Code formatting validated (Prettier)
  • ✅ Pattern normalization handles refs/tags/ prefix
  • ✅ Tag name pattern validation implemented
  • ✅ Comprehensive validation of all policy aspects
  • ✅ Follows existing GitArmor patterns and conventions
  • ✅ No breaking changes to existing functionality
Original prompt

This section details on the original issue you should resolve

<issue_title>Add tag protection settings in repository policy in GitHub</issue_title>
<issue_description>Add Tag protection verification for tags in a specific repository

The request is GET /repos/{owner}/{repo}/rulesets.

and the response schema

{
  "type": "array",
  "items": {
    "title": "Repository ruleset",
    "type": "object",
    "description": "A set of rules to apply when specified conditions are met.",
    "required": [
      "id",
      "name",
      "source",
      "enforcement"
    ],
    "properties": {
      "id": {
        "type": "integer",
        "description": "The ID of the ruleset"
      },
      "name": {
        "type": "string",
        "description": "The name of the ruleset"
      },
      "target": {
        "type": "string",
        "description": "The target of the ruleset",
        "enum": [
          "branch",
          "tag",
          "push",
          "repository"
        ]
      },
      "source_type": {
        "type": "string",
        "description": "The type of the source of the ruleset",
        "enum": [
          "Repository",
          "Organization",
          "Enterprise"
        ]
      },
      "source": {
        "type": "string",
        "description": "The name of the source"
      },
      "enforcement": {
        "type": "string",
        "description": "The enforcement level of the ruleset. `evaluate` allows admins to test rules before enforcing them. Admins can view insights on the Rule Insights page (`evaluate` is only available with GitHub Enterprise).",
        "enum": [
          "disabled",
          "active",
          "evaluate"
        ]
      },
      "bypass_actors": {
        "type": "array",
        "description": "The actors that can bypass the rules in this ruleset",
        "items": {
          "title": "Repository Ruleset Bypass Actor",
          "type": "object",
          "description": "An actor that can bypass rules in a ruleset",
          "required": [
            "actor_type"
          ],
          "properties": {
            "actor_id": {
              "type": [
                "integer",
                "null"
              ],
              "description": "The ID of the actor that can bypass a ruleset. Required for `Integration`, `RepositoryRole`, and `Team` actor types. If `actor_type` is `OrganizationAdmin`, this should be `1`. If `actor_type` is `DeployKey`, this should be null. `OrganizationAdmin` is not applicable for personal repositories."
            },
            "actor_type": {
              "type": "string",
              "enum": [
                "Integration",
                "OrganizationAdmin",
                "RepositoryRole",
                "Team",
                "DeployKey"
              ],
              "description": "The type of actor that can bypass a ruleset."
            },
            "bypass_mode": {
              "type": "string",
              "description": "When the specified actor can bypass the ruleset. `pull_request` means that an actor can only bypass rules on pull requests. `pull_request` is not applicable for the `DeployKey` actor type. Also, `pull_request` is only applicable to branch rulesets. When `bypass_mode` is `exempt`, rules will not be run for that actor and a bypass audit entry will not be created.",
              "enum": [
                "always",
                "pull_request",
                "exempt"
              ],
              "default": "always"
            }
          }
        }
      },
      "current_user_can_bypass": {
        "type": "string",
        "description": "The bypass type of the user making the API request for this ruleset. This field is only returned when\nquerying the repository-level endpoint.",
        "enum": [
          "always",
          "pull_requests_only",
          "never",
          "exempt"
        ]
      },
      "node_id": {
        "type": "string"
      },
      "_links": {
        "type": "object",
        "properties": {
          "self": {
            "type": "object",
            "properties": {
              "href": {
                "type": "string",
                "description": "The URL of the ruleset"
              }
            }
          },
          "html": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "href": {
                "type": "string",
                "description": "The html URL of the ruleset"
              }
            }
          }
        }
      },
      "conditions": {
        "anyOf": [
          {
            "title": "Repository ruleset conditions for ref names",
            "type": "object",
            "description": "Parameters for a repository ruleset ref name condition",
            "properties": {
              "ref_name": {
                "type": "object",
                "properties": {
                  "include": {
        ...

</details>

Fixes dcodx/gitarmor#35

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Co-authored-by: david3107 <20040740+david3107@users.noreply.github.com>
Copilot AI changed the title [WIP] Add tag protection settings to repository policy Add tag protection policy verification for repository rulesets Oct 19, 2025
Copilot AI requested a review from david3107 October 19, 2025 08:35
@david3107
Copy link
Copy Markdown
Contributor

@copilot the issue clearly specifies the policy to create in the yml file and the expected checks.

tags:
  enforcement: active           # disabled | active | evaluate
  target: tag                   # fixed for tag rules so we can also not specify it here but fix it in code

  scope:
    include:
      - "v*"                    # e.g., protect all version tags
      # - "~ALL"                # special token: all tags
    exclude: []                 # patterns to exclude, e.g., ["v0.*"]

  operations:                   # who can perform actions on matching tags
    create: restricted          # allowed | restricted (restricted = bypass-only)
    update: restricted
    delete: restricted

  naming:                       # optional: constrain tag names
    enabled: true
    operator: regex             # starts_with | ends_with | contains | regex
    pattern: "^v\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?$"
    negate: false               # true = pattern disallowed

  bypass:                       # actors allowed to bypass protections
    organization_admins: always # always | exempt
    teams:
      - id: 1234567             # example team id
        mode: always            # always | exempt
    integrations:
      - id: 987654              # GitHub App id
        mode: always
    repository_roles:
      - id: 3                   # e.g., Maintainer role id
        mode: always
    deploy_keys:
      allow: true               # DeployKeys can bypass when true
      mode: always
      ```

- Changed from simple protected_tags list to comprehensive tags structure
- Added enforcement, scope (include/exclude), operations, naming, and bypass configuration
- Updated TypeScript types to match new structure
- Rewrote TagProtectionChecks evaluator to validate all aspects of tag protection
- Checks enforcement level, scope patterns, operation restrictions, naming constraints, and bypass actors

Co-authored-by: david3107 <20040740+david3107@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Oct 19, 2025

@copilot the issue clearly specifies the policy to create in the yml file and the expected checks.

...

Updated the tag protection policy structure to match the specified YAML format. The policy now includes:

  • enforcement, target, and scope (include/exclude patterns)
  • operations (create/update/delete restrictions)
  • naming constraints with regex pattern matching
  • bypass actors configuration (org admins, teams, integrations, repository roles, deploy keys)

The evaluator has been rewritten to validate all these aspects against GitHub's repository rulesets API.

Commit: will be in the next push

Copilot AI changed the title Add tag protection policy verification for repository rulesets Add comprehensive tag protection policy verification for repository rulesets Oct 19, 2025
@david3107
Copy link
Copy Markdown
Contributor

david3107 commented Oct 20, 2025

@copilot looking at the response from GitHub an example is

  https://api.github.com/repos/deArmory/ambition/rulesets/9022724
{
  "id": 9022724,
  "name": "test_rule",
  "target": "tag",
  "source_type": "Repository",
  "source": "deArmory/ambition",
  "enforcement": "active",
  "conditions": {
    "ref_name": {
      "exclude": [

      ],
      "include": [
        "refs/tags/v*"
      ]
    }
  },
  "rules": [
    {
      "type": "deletion"
    },
    {
      "type": "non_fast_forward"
    },
    {
      "type": "creation"
    },
    {
      "type": "update"
    },
    {
      "type": "required_linear_history"
    },
    {
      "type": "tag_name_pattern",
      "parameters": {
        "operator": "regex",
        "pattern": "^v\\\\d+\\\\.\\\\d+\\\\.\\\\d+(-[0-9A-Za-z.-]+)?$",
        "negate": false,
        "name": ""
      }
    },
    {
      "type": "required_signatures"
    },
    {
      "type": "required_status_checks",
      "parameters": {
        "strict_required_status_checks_policy": true,
        "do_not_enforce_on_create": true,
        "required_status_checks": [
          {
            "context": "test"
          }
        ]
      }
    },
    {
      "type": "commit_message_pattern",
      "parameters": {
        "operator": "starts_with",
        "pattern": "release-",
        "negate": false,
        "name": ""
      }
    }
  ],
  "node_id": "RRS_lACqUmVwb3NpdG9yec4u077YzgCJrQQ",
  "created_at": "2025-10-20T06:44:51.046+02:00",
  "updated_at": "2025-10-20T06:53:22.305+02:00",
  "bypass_actors": [
    {
      "actor_id": null,
      "actor_type": "OrganizationAdmin",
      "bypass_mode": "always"
    },
    {
      "actor_id": null,
      "actor_type": "DeployKey",
      "bypass_mode": "always"
    },
    {
      "actor_id": 2,
      "actor_type": "RepositoryRole",
      "bypass_mode": "always"
    },
    {
      "actor_id": 9897583,
      "actor_type": "Team",
      "bypass_mode": "always"
    }
  ],
  "current_user_can_bypass": "always",
  "_links": {
    "self": {
      "href": "https://api.github.com/repos/deArmory/ambition/rulesets/9022724"
    },
    "html": {
      "href": "https://github.com/deArmory/ambition/rules/9022724"
    }
  }
}

adjust the policy and the checks to match the info provided by the ruleset and consider the schema

{
  "title": "Repository ruleset",
  "type": "object",
  "description": "A set of rules to apply when specified conditions are met.",
  "required": [
    "id",
    "name",
    "source",
    "enforcement"
  ],
  "properties": {
    "id": {
      "type": "integer",
      "description": "The ID of the ruleset"
    },
    "name": {
      "type": "string",
      "description": "The name of the ruleset"
    },
    "target": {
      "type": "string",
      "description": "The target of the ruleset",
      "enum": [
        "branch",
        "tag",
        "push",
        "repository"
      ]
    },
    "source_type": {
      "type": "string",
      "description": "The type of the source of the ruleset",
      "enum": [
        "Repository",
        "Organization",
        "Enterprise"
      ]
    },
    "source": {
      "type": "string",
      "description": "The name of the source"
    },
    "enforcement": {
      "type": "string",
      "description": "The enforcement level of the ruleset. `evaluate` allows admins to test rules before enforcing them. Admins can view insights on the Rule Insights page (`evaluate` is only available with GitHub Enterprise).",
      "enum": [
        "disabled",
        "active",
        "evaluate"
      ]
    },
    "bypass_actors": {
      "type": "array",
      "description": "The actors that can bypass the rules in this ruleset",
      "items": {
        "title": "Repository Ruleset Bypass Actor",
        "type": "object",
        "description": "An actor that can bypass rules in a ruleset",
        "required": [
          "actor_type"
        ],
        "properties": {
          "actor_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The ID of the actor that can bypass a ruleset. Required for `Integration`, `RepositoryRole`, and `Team` actor types. If `actor_type` is `OrganizationAdmin`, this should be `1`. If `actor_type` is `DeployKey`, this should be null. `OrganizationAdmin` is not applicable for personal repositories."
          },
          "actor_type": {
            "type": "string",
            "enum": [
              "Integration",
              "OrganizationAdmin",
              "RepositoryRole",
              "Team",
              "DeployKey"
            ],
            "description": "The type of actor that can bypass a ruleset."
          },
          "bypass_mode": {
            "type": "string",
            "description": "When the specified actor can bypass the ruleset. `pull_request` means that an actor can only bypass rules on pull requests. `pull_request` is not applicable for the `DeployKey` actor type. Also, `pull_request` is only applicable to branch rulesets. When `bypass_mode` is `exempt`, rules will not be run for that actor and a bypass audit entry will not be created.",
            "enum": [
              "always",
              "pull_request",
              "exempt"
            ],
            "default": "always"
          }
        }
      }
    },
    "current_user_can_bypass": {
      "type": "string",
      "description": "The bypass type of the user making the API request for this ruleset. This field is only returned when\nquerying the repository-level endpoint.",
      "enum": [
        "always",
        "pull_requests_only",
        "never",
        "exempt"
      ]
    },
    "node_id": {
      "type": "string"
    },
    "_links": {
      "type": "object",
      "properties": {
        "self": {
          "type": "object",
          "properties": {
            "href": {
              "type": "string",
              "description": "The URL of the ruleset"
            }
          }
        },
        "html": {
          "type": [
            "object",
            "null"
          ],
          "properties": {
            "href": {
              "type": "string",
              "description": "The html URL of the ruleset"
            }
          }
        }
      }
    },
    "conditions": {
      "anyOf": [
        {
          "title": "Repository ruleset conditions for ref names",
          "type": "object",
          "description": "Parameters for a repository ruleset ref name condition",
          "properties": {
            "ref_name": {
              "type": "object",
              "properties": {
                "include": {
                  "type": "array",
                  "description": "Array of ref names or patterns to include. One of these patterns must match for the condition to pass. Also accepts `~DEFAULT_BRANCH` to include the default branch or `~ALL` to include all branches.",
                  "items": {
                    "type": "string"
                  }
                },
                "exclude": {
                  "type": "array",
                  "description": "Array of ref names or patterns to exclude. The condition will not pass if any of these patterns match.",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        {
          "title": "Organization ruleset conditions",
          "type": "object",
          "description": "Conditions for an organization ruleset.\nThe branch and tag rulesets conditions object should contain both `repository_name` and `ref_name` properties, or both `repository_id` and `ref_name` properties, or both `repository_property` and `ref_name` properties.\nThe push rulesets conditions object does not require the `ref_name` property.\nFor repository policy rulesets, the conditions object should only contain the `repository_name`, the `repository_id`, or the `repository_property`.",
          "oneOf": [
            {
              "type": "object",
              "title": "repository_name_and_ref_name",
              "description": "Conditions to target repositories by name and refs by name",
              "allOf": [
                {
                  "title": "Repository ruleset conditions for ref names",
                  "type": "object",
                  "description": "Parameters for a repository ruleset ref name condition",
                  "properties": {
                    "ref_name": {
                      "type": "object",
                      "properties": {
                        "include": {
                          "type": "array",
                          "description": "Array of ref names or patterns to include. One of these patterns must match for the condition to pass. Also accepts `~DEFAULT_BRANCH` to include the default branch or `~ALL` to include all branches.",
                          "items": {
                            "type": "string"
                          }
                        },
                        "exclude": {
                          "type": "array",
                          "description": "Array of ref names or patterns to exclude. The condition will not pass if any of these patterns match.",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                },
                {
                  "title": "Repository ruleset conditions for repository names",
                  "type": "object",
                  "description": "Parameters for a repository name condition",
                  "properties": {
                    "repository_name": {
                      "type": "object",
                      "properties": {
                        "include": {
                          "type": "array",
                          "description": "Array of repository names or patterns to include. One of these patterns must match for the condition to pass. Also accepts `~ALL` to include all repositories.",
                          "items": {
                            "type": "string"
                          }
                        },
                        "exclude": {
                          "type": "array",
                          "description": "Array of repository names or patterns to exclude. The condition will not pass if any of these patterns match.",
                          "items": {
                            "type": "string"
                          }
                        },
                        "protected": {
                          "type": "boolean",
                          "description": "Whether renaming of target repositories is prevented."
                        }
                      }
                    }
                  },
                  "required": [
                    "repository_name"
                  ]
                }
              ]
            },
            {
              "type": "object",
              "title": "repository_id_and_ref_name",
              "description": "Conditions to target repositories by id and refs by name",
              "allOf": [
                {
                  "title": "Repository ruleset conditions for ref names",
                  "type": "object",
                  "description": "Parameters for a repository ruleset ref name condition",
                  "properties": {
                    "ref_name": {
                      "type": "object",
                      "properties": {
                        "include": {
                          "type": "array",
                          "description": "Array of ref names or patterns to include. One of these patterns must match for the condition to pass. Also accepts `~DEFAULT_BRANCH` to include the default branch or `~ALL` to include all branches.",
                          "items": {
                            "type": "string"
                          }
                        },
                        "exclude": {
                          "type": "array",
                          "description": "Array of ref names or patterns to exclude. The condition will not pass if any of these patterns match.",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                },
                {
                  "title": "Repository ruleset conditions for repository IDs",
                  "type": "object",
                  "description": "Parameters for a repository ID condition",
                  "properties": {
                    "repository_id": {
                      "type": "object",
                      "properties": {
                        "repository_ids": {
                          "type": "array",
                          "description": "The repository IDs that the ruleset applies to. One of these IDs must match for the condition to pass.",
                          "items": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  },
                  "required": [
                    "repository_id"
                  ]
                }
              ]
            },
            {
              "type": "object",
              "title": "repository_property_and_ref_name",
              "description": "Conditions to target repositories by property and refs by name",
              "allOf": [
                {
                  "title": "Repository ruleset conditions for ref names",
                  "type": "object",
                  "description": "Parameters for a repository ruleset ref name condition",
                  "properties": {
                    "ref_name": {
                      "type": "object",
                      "properties": {
                        "include": {
                          "type": "array",
                          "description": "Array of ref names or patterns to include. One of these patterns must match for the condition to pass. Also accepts `~DEFAULT_BRANCH` to include the default branch or `~ALL` to include all branches.",
                          "items": {
                            "type": "string"
                          }
                        },
                        "exclude": {
                          "type": "array",
                          "description": "Array of ref names or patterns to exclude. The condition will not pass if any of these patterns match.",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                },
                {
                  "title": "Repository ruleset conditions for repository properties",
                  "type": "object",
                  "description": "Parameters for a repository property condition",
                  "properties": {
                    "repository_property": {
                      "type": "object",
                      "properties": {
                        "include": {
                          "type": "array",
                          "description": "The repository properties and values to include. All of these properties must match for the condition to pass.",
                          "items": {
                            "title": "Repository ruleset property targeting definition",
                            "type": "object",
                            "description": "Parameters for a targeting a repository property",
                            "properties": {
                              "name": {
                                "type": "string",
                                "description": "The name of the repository property to target"
                              },
                              "property_values": {
                                "type": "array",
                                "description": "The values to match for the repository property",
                                "items": {
                                  "type": "string"
                                }
                              },
                              "source": {
                                "type": "string",
                                "description": "The source of the repository property. Defaults to 'custom' if not specified.",
                                "enum": [
                                  "custom",
                                  "system"
                                ]
                              }
                            },
                            "required": [
                              "name",
                              "property_values"
                            ]
                          }
                        },
                        "exclude": {
                          "type": "array",
                          "description": "The repository properties and values to exclude. The condition will not pass if any of these properties match.",
                          "items": {
                            "title": "Repository ruleset property targeting definition",
                            "type": "object",
                            "description": "Parameters for a targeting a repository property",
                            "properties": {
                              "name": {
                                "type": "string",
                                "description": "The name of the repository property to target"
                              },
                              "property_values": {
                                "type": "array",
                                "description": "The values to match for the repository property",
                                "items": {
                                  "type": "string"
                                }
                              },
                              "source": {
                                "type": "string",
                                "description": "The source of the repository property. Defaults to 'custom' if not specified.",
                                "enum": [
                                  "custom",
                                  "system"
                                ]
                              }
                            },
                            "required": [
                              "name",
                              "property_values"
                            ]
                          }
                        }
                      }
                    }
                  },
                  "required": [
                    "repository_property"
                  ]
                }
              ]
            }
          ]
        }
      ],
      "type": [
        "null",
        "object"
      ]
    },
    "rules": {
      "type": "array",
      "items": {
        "title": "Repository Rule",
        "type": "object",
        "description": "A repository rule.",
        "oneOf": [
          {
            "title": "creation",
            "description": "Only allow users with bypass permission to create matching refs.",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "creation"
                ]
              }
            }
          },
          {
            "title": "update",
            "description": "Only allow users with bypass permission to update matching refs.",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "update"
                ]
              },
              "parameters": {
                "type": "object",
                "properties": {
                  "update_allows_fetch_and_merge": {
                    "type": "boolean",
                    "description": "Branch can pull changes from its upstream repository"
                  }
                },
                "required": [
                  "update_allows_fetch_and_merge"
                ]
              }
            }
          },
          {
            "title": "deletion",
            "description": "Only allow users with bypass permissions to delete matching refs.",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "deletion"
                ]
              }
            }
          },
          {
            "title": "required_linear_history",
            "description": "Prevent merge commits from being pushed to matching refs.",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "required_linear_history"
                ]
              }
            }
          },
          {
            "title": "merge_queue",
            "description": "Merges must be performed via a merge queue.",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "merge_queue"
                ]
              },
              "parameters": {
                "type": "object",
                "properties": {
                  "check_response_timeout_minutes": {
                    "type": "integer",
                    "description": "Maximum time for a required status check to report a conclusion. After this much time has elapsed, checks that have not reported a conclusion will be assumed to have failed",
                    "minimum": 1,
                    "maximum": 360
                  },
                  "grouping_strategy": {
                    "type": "string",
                    "description": "When set to ALLGREEN, the merge commit created by merge queue for each PR in the group must pass all required checks to merge. When set to HEADGREEN, only the commit at the head of the merge group, i.e. the commit containing changes from all of the PRs in the group, must pass its required checks to merge.",
                    "enum": [
                      "ALLGREEN",
                      "HEADGREEN"
                    ]
                  },
                  "max_entries_to_build": {
                    "type": "integer",
                    "description": "Limit the number of queued pull requests requesting checks and workflow runs at the same time.",
                    "minimum": 0,
                    "maximum": 100
                  },
                  "max_entries_to_merge": {
                    "type": "integer",
                    "description": "The maximum number of PRs that will be merged together in a group.",
                    "minimum": 0,
                    "maximum": 100
                  },
                  "merge_method": {
                    "type": "string",
                    "description": "Method to use when merging changes from queued pull requests.",
                    "enum": [
                      "MERGE",
                      "SQUASH",
                      "REBASE"
                    ]
                  },
                  "min_entries_to_merge": {
                    "type": "integer",
                    "description": "The minimum number of PRs that will be merged together in a group.",
                    "minimum": 0,
                    "maximum": 100
                  },
                  "min_entries_to_merge_wait_minutes": {
                    "type": "integer",
                    "description": "The time merge queue should wait after the first PR is added to the queue for the minimum group size to be met. After this time has elapsed, the minimum group size will be ignored and a smaller group will be merged.",
                    "minimum": 0,
                    "maximum": 360
                  }
                },
                "required": [
                  "check_response_timeout_minutes",
                  "grouping_strategy",
                  "max_entries_to_build",
                  "max_entries_to_merge",
                  "merge_method",
                  "min_entries_to_merge",
                  "min_entries_to_merge_wait_minutes"
                ]
              }
            }
          },
          {
            "title": "required_deployments",
            "description": "Choose which environments must be successfully deployed to before refs can be pushed into a ref that matches this rule.",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "required_deployments"
                ]
              },
              "parameters": {
                "type": "object",
                "properties": {
                  "required_deployment_environments": {
                    "type": "array",
                    "description": "The environments that must be successfully deployed to before branches can be merged.",
                    "items": {
                      "type": "string"
                    }
                  }
                },
                "required": [
                  "required_deployment_environments"
                ]
              }
            }
          },
          {
            "title": "required_signatures",
            "description": "Commits pushed to matching refs must have verified signatures.",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "required_signatures"
                ]
              }
            }
          },
          {
            "title": "pull_request",
            "description": "Require all commits be made to a non-target branch and submitted via a pull request before they can be merged.",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "pull_request"
                ]
              },
              "parameters": {
                "type": "object",
                "properties": {
                  "allowed_merge_methods": {
                    "type": "array",
                    "description": "Array of allowed merge methods. Allowed values include `merge`, `squash`, and `rebase`. At least one option must be enabled.",
                    "items": {
                      "type": "string",
                      "enum": [
                        "merge",
                        "squash",
                        "rebase"
                      ]
                    }
                  },
                  "automatic_copilot_code_review_enabled": {
                    "type": "boolean",
                    "description": "Request Copilot code review for new pull requests automatically if the author has access to Copilot code review."
                  },
                  "dismiss_stale_reviews_on_push": {
                    "type": "boolean",
                    "description": "New, reviewable commits pushed will dismiss previous pull request review approvals."
                  },
                  "require_code_owner_review": {
                    "type": "boolean",
                    "description": "Require an approving review in pull requests that modify files that have a designated code owner."
                  },
                  "require_last_push_approval": {
                    "type": "boolean",
                    "description": "Whether the most recent reviewable push must be approved by someone other than the person who pushed it."
                  },
                  "required_approving_review_count": {
                    "type": "integer",
                    "description": "The number of approving reviews that are required before a pull request can be merged.",
                    "minimum": 0,
                    "maximum": 10
                  },
                  "required_review_thread_resolution": {
                    "type": "boolean",
                    "description": "All conversations on code must be resolved before a pull request can be merged."
                  }
                },
                "required": [
                  "dismiss_stale_reviews_on_push",
                  "require_code_owner_review",
                  "require_last_push_approval",
                  "required_approving_review_count",
                  "required_review_thread_resolution"
                ]
              }
            }
          },
          {
            "title": "required_status_checks",
            "description": "Choose which status checks must pass before the ref is updated. When enabled, commits must first be pushed to another ref where the checks pass.",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "required_status_checks"
                ]
              },
              "parameters": {
                "type": "object",
                "properties": {
                  "do_not_enforce_on_create": {
                    "type": "boolean",
                    "description": "Allow repositories and branches to be created if a check would otherwise prohibit it."
                  },
                  "required_status_checks": {
                    "type": "array",
                    "description": "Status checks that are required.",
                    "items": {
                      "title": "StatusCheckConfiguration",
                      "description": "Required status check",
                      "type": "object",
                      "properties": {
                        "context": {
                          "type": "string",
                          "description": "The status check context name that must be present on the commit."
                        },
                        "integration_id": {
                          "type": "integer",
                          "description": "The optional integration ID that this status check must originate from."
                        }
                      },
                      "required": [
                        "context"
                      ]
                    }
                  },
                  "strict_required_status_checks_policy": {
                    "type": "boolean",
                    "description": "Whether pull requests targeting a matching branch must be tested with the latest code. This setting will not take effect unless at least one status check is enabled."
                  }
                },
                "required": [
                  "required_status_checks",
                  "strict_required_status_checks_policy"
                ]
              }
            }
          },
          {
            "title": "non_fast_forward",
            "description": "Prevent users with push access from force pushing to refs.",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "non_fast_forward"
                ]
              }
            }
          },
          {
            "title": "commit_message_pattern",
            "description": "Parameters to be used for the commit_message_pattern rule",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "commit_message_pattern"
                ]
              },
              "parameters": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "How this rule will appear to users."
                  },
                  "negate": {
                    "type": "boolean",
                    "description": "If true, the rule will fail if the pattern matches."
                  },
                  "operator": {
                    "type": "string",
                    "description": "The operator to use for matching.",
                    "enum": [
                      "starts_with",
                      "ends_with",
                      "contains",
                      "regex"
                    ]
                  },
                  "pattern": {
                    "type": "string",
                    "description": "The pattern to match with."
                  }
                },
                "required": [
                  "operator",
                  "pattern"
                ]
              }
            }
          },
          {
            "title": "commit_author_email_pattern",
            "description": "Parameters to be used for the commit_author_email_pattern rule",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "commit_author_email_pattern"
                ]
              },
              "parameters": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "How this rule will appear to users."
                  },
                  "negate": {
                    "type": "boolean",
                    "description": "If true, the rule will fail if the pattern matches."
                  },
                  "operator": {
                    "type": "string",
                    "description": "The operator to use for matching.",
                    "enum": [
                      "starts_with",
                      "ends_with",
                      "contains",
                      "regex"
                    ]
                  },
                  "pattern": {
                    "type": "string",
                    "description": "The pattern to match with."
                  }
                },
                "required": [
                  "operator",
                  "pattern"
                ]
              }
            }
          },
          {
            "title": "committer_email_pattern",
            "description": "Parameters to be used for the committer_email_pattern rule",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "committer_email_pattern"
                ]
              },
              "parameters": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "How this rule will appear to users."
                  },
                  "negate": {
                    "type": "boolean",
                    "description": "If true, the rule will fail if the pattern matches."
                  },
                  "operator": {
                    "type": "string",
                    "description": "The operator to use for matching.",
                    "enum": [
                      "starts_with",
                      "ends_with",
                      "contains",
                      "regex"
                    ]
                  },
                  "pattern": {
                    "type": "string",
                    "description": "The pattern to match with."
                  }
                },
                "required": [
                  "operator",
                  "pattern"
                ]
              }
            }
          },
          {
            "title": "branch_name_pattern",
            "description": "Parameters to be used for the branch_name_pattern rule",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "branch_name_pattern"
                ]
              },
              "parameters": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "How this rule will appear to users."
                  },
                  "negate": {
                    "type": "boolean",
                    "description": "If true, the rule will fail if the pattern matches."
                  },
                  "operator": {
                    "type": "string",
                    "description": "The operator to use for matching.",
                    "enum": [
                      "starts_with",
                      "ends_with",
                      "contains",
                      "regex"
                    ]
                  },
                  "pattern": {
                    "type": "string",
                    "description": "The pattern to match with."
                  }
                },
                "required": [
                  "operator",
                  "pattern"
                ]
              }
            }
          },
          {
            "title": "tag_name_pattern",
            "description": "Parameters to be used for the tag_name_pattern rule",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "tag_name_pattern"
                ]
              },
              "parameters": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "How this rule will appear to users."
                  },
                  "negate": {
                    "type": "boolean",
                    "description": "If true, the rule will fail if the pattern matches."
                  },
                  "operator": {
                    "type": "string",
                    "description": "The operator to use for matching.",
                    "enum": [
                      "starts_with",
                      "ends_with",
                      "contains",
                      "regex"
                    ]
                  },
                  "pattern": {
                    "type": "string",
                    "description": "The pattern to match with."
                  }
                },
                "required": [
                  "operator",
                  "pattern"
                ]
              }
            }
          },
          {
            "title": "file_path_restriction",
            "description": "Prevent commits that include changes in specified file and folder paths from being pushed to the commit graph. This includes absolute paths that contain file names.",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "file_path_restriction"
                ]
              },
              "parameters": {
                "type": "object",
                "properties": {
                  "restricted_file_paths": {
                    "type": "array",
                    "description": "The file paths that are restricted from being pushed to the commit graph.",
                    "items": {
                      "type": "string"
                    }
                  }
                },
                "required": [
                  "restricted_file_paths"
                ]
              }
            }
          },
          {
            "title": "max_file_path_length",
            "description": "Prevent commits that include file paths that exceed the specified character limit from being pushed to the commit graph.",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "max_file_path_length"
                ]
              },
              "parameters": {
                "type": "object",
                "properties": {
                  "max_file_path_length": {
                    "type": "integer",
                    "description": "The maximum amount of characters allowed in file paths.",
                    "minimum": 1,
                    "maximum": 32767
                  }
                },
                "required": [
                  "max_file_path_length"
                ]
              }
            }
          },
          {
            "title": "file_extension_restriction",
            "description": "Prevent commits that include files with specified file extensions from being pushed to the commit graph.",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "file_extension_restriction"
                ]
              },
              "parameters": {
                "type": "object",
                "properties": {
                  "restricted_file_extensions": {
                    "type": "array",
                    "description": "The file extensions that are restricted from being pushed to the commit graph.",
                    "items": {
                      "type": "string"
                    }
                  }
                },
                "required": [
                  "restricted_file_extensions"
                ]
              }
            }
          },
          {
            "title": "max_file_size",
            "description": "Prevent commits with individual files that exceed the specified limit from being pushed to the commit graph.",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "max_file_size"
                ]
              },
              "parameters": {
                "type": "object",
                "properties": {
                  "max_file_size": {
                    "type": "integer",
                    "description": "The maximum file size allowed in megabytes. This limit does not apply to Git Large File Storage (Git LFS).",
                    "minimum": 1,
                    "maximum": 100
                  }
                },
                "required": [
                  "max_file_size"
                ]
              }
            }
          },
          {
            "title": "workflows",
            "description": "Require all changes made to a targeted branch to pass the specified workflows before they can be merged.",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "workflows"
                ]
              },
              "parameters": {
                "type": "object",
                "properties": {
                  "do_not_enforce_on_create": {
                    "type": "boolean",
                    "description": "Allow repositories and branches to be created if a check would otherwise prohibit it."
                  },
                  "workflows": {
                    "type": "array",
                    "description": "Workflows that must pass for this rule to pass.",
                    "items": {
                      "title": "WorkflowFileReference",
                      "description": "A workflow that must run for this rule to pass",
                      "type": "object",
                      "properties": {
                        "path": {
                          "type": "string",
                          "description": "The path to the workflow file"
                        },
                        "ref": {
                          "type": "string",
                          "description": "The ref (branch or tag) of the workflow file to use"
                        },
                        "repository_id": {
                          "type": "integer",
                          "description": "The ID of the repository where the workflow is defined"
                        },
                        "sha": {
                          "type": "string",
                          "description": "The commit SHA of the workflow file to use"
                        }
                      },
                      "required": [
                        "path",
                        "repository_id"
                      ]
                    }
                  }
                },
                "required": [
                  "workflows"
                ]
              }
            }
          },
          {
            "title": "code_scanning",
            "description": "Choose which tools must provide code scanning results before the reference is updated. When configured, code scanning must be enabled and have results for both the commit and the reference being updated.",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "code_scanning"
                ]
              },
              "parameters": {
                "type": "object",
                "properties": {
                  "code_scanning_tools": {
                    "type": "array",
                    "description": "Tools that must provide code scanning results for this rule to pass.",
                    "items": {
                      "title": "CodeScanningTool",
                      "description": "A tool that must provide code scanning results for this rule to pass.",
                      "type": "object",
                      "properties": {
                        "alerts_threshold": {
                          "type": "string",
                          "description": "The severity level at which code scanning results that raise alerts block a reference update. For more information on alert severity levels, see \"[About code scanning alerts](https://docs.github.com/code-security/code-scanning/managing-code-scanning-alerts/about-code-scanning-alerts#about-alert-severity-and-security-severity-levels).\"",
                          "enum": [
                            "none",
                            "errors",
                            "errors_and_warnings",
                            "all"
                          ]
                        },
                        "security_alerts_threshold": {
                          "type": "string",
                          "description": "The severity level at which code scanning results that raise security alerts block a reference update. For more information on security severity levels, see \"[About code scanning alerts](https://docs.github.com/code-security/code-scanning/managing-code-scanning-alerts/about-code-scanning-alerts#about-alert-severity-and-security-severity-levels).\"",
                          "enum": [
                            "none",
                            "critical",
                            "high_or_higher",
                            "medium_or_higher",
                            "all"
                          ]
                        },
                        "tool": {
                          "type": "string",
                          "description": "The name of a code scanning tool"
                        }
                      },
                      "required": [
                        "alerts_threshold",
                        "security_alerts_threshold",
                        "tool"
                      ]
                    }
                  }
                },
                "required": [
                  "code_scanning_tools"
                ]
              }
            }
          },
          {
            "title": "copilot_code_review",
            "description": "Request Copilot code review for new pull requests automatically if the author has access to Copilot code review.",
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "copilot_code_review"
                ]
              },
              "parameters": {
                "type": "object",
                "properties": {
                  "review_draft_pull_requests": {
                    "type": "boolean",
                    "description": "Copilot automatically reviews draft pull requests before they are marked as ready for review."
                  },
                  "review_on_push": {
                    "type": "boolean",
                    "description": "Copilot automatically reviews each new push to the pull request."
                  }
                }
              }
            }
          }
        ]
      }
    },
    "created_at": {
      "type": "string",
      "format": "date-time"
    },
    "updated_at": {
      "type": "string",
      "format": "date-time"
    }
  }
}

@david3107
Copy link
Copy Markdown
Contributor

@copilot check the previous comment

- Normalize scope patterns by stripping refs/tags/ prefix from API response
- Implement proper tag_name_pattern rule validation in naming check
- Compare policy naming operator, pattern, and negate against actual ruleset parameters

Co-authored-by: david3107 <20040740+david3107@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Oct 20, 2025

@copilot looking at the response from GitHub an example is

...

Fixed the implementation based on the real API response:

  1. Pattern normalization: The API returns patterns with refs/tags/ prefix (e.g., refs/tags/v*). Added normalization to strip this prefix when comparing against policy patterns.

  2. Tag name pattern validation: Implemented proper checking of the tag_name_pattern rule type, validating operator, pattern, and negate parameters against policy configuration.

Commit: see next push

@copilot check the previous comment

Addressed the API response structure. Fixed pattern normalization (strips refs/tags/ prefix) and implemented tag_name_pattern rule validation. Commit: d9a6184

@david3107 david3107 marked this pull request as ready for review November 21, 2025 09:48
@david3107 david3107 requested a review from theztefan as a code owner November 21, 2025 09:48
@david3107 david3107 merged commit 63fc1da into main Nov 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants