diff --git a/.github/actions/github.pr.size-labeler/.gitignore b/.github/actions/github.pr.size-labeler/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/.github/actions/github.pr.size-labeler/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/.github/actions/github.pr.size-labeler/README.md b/.github/actions/github.pr.size-labeler/README.md new file mode 100644 index 0000000..63db456 --- /dev/null +++ b/.github/actions/github.pr.size-labeler/README.md @@ -0,0 +1,160 @@ +# GitHub - PR Size Labeler + +Label pull requests as XS, S, M, L, or XL using additions and deletions reported by GitHub. This alternative composite action runs `size_labeler.py` with `uv run --locked --script`. It sets up uv 0.13.2 and Python 3.12 and installs the dependencies declared in PEP 723 inline metadata into uv’s script environment. It runs on AMD64 and ARM64 without Docker, checkout, npm, a pyproject.toml, or a build step. The caller’s Python environment is not modified. The initial run requires network access to download tools and packages; uv reuses its local cache. Use a current Actions Runner compatible with the setup-uv action. + +## Usage + +```yaml +name: PR Size + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +concurrency: + group: pr-size-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + label: + runs-on: [self-hosted, linux, ARM64] + steps: + - uses: tedilabs/github-actions/.github/actions/github.pr.size-labeler@main + with: + ignore_file_deletions: "true" + ignore_line_deletions: "false" + overwrite_other_size_labels: "false" +``` + +The same action works on `ubuntu-24.04`, `ubuntu-24.04-arm`, and compatible AMD64 self-hosted runners. No PR code is checked out or executed. Configuration is read from the base repository at the PR's base SHA, including for fork PRs. Configuration changes in a PR therefore take effect after merging; they cannot change that PR's labeling policy. Use `pr_number` for events without a PR payload. + +The existing `github.pr.auto-label.yaml` reusable workflow delegates to this action. Its existing `pr_size_*_label` and `pr_size_*_max` inputs keep their behavior. New settings are available as `pr_size_config_file`, `pr_size_files_to_ignore`, `pr_size_ignore_file_deletions`, `pr_size_ignore_line_deletions`, `pr_size_overwrite_other_size_labels`, `pr_size_label_prefix`, `pr_size_fail_if_xl`, `pr_size_message_if_xl`, and `pr_size_dry_run`. + +## Configuration precedence + +1. Built-in defaults. +2. Action inputs. +3. Values explicitly present in the configuration file. + +With an empty `config_file`, the action tries `.github/size-labeler.yaml` and then `.github/size-labeler.yml`. The first existing file wins; files are not merged. If neither exists, inputs are used. + +Set `config_file: .github/custom-size.yaml` to read only that path. An explicitly selected missing file fails, while a missing auto-discovery file is optional. Invalid configuration and permission errors always fail before changes are made. + +Files use the same flat setting names as action inputs. Only policy settings in the table below can be overridden. `GITHUB_TOKEN`, `github_api_url`, `pr_number`, and `config_file` remain invocation inputs. Unknown keys, duplicate YAML keys, multiple documents, invalid booleans, and non-increasing thresholds fail validation. An empty file or `{}` applies no overrides. + +For example, `.github/size-labeler.yaml`: + +```yaml +xs_label: size/XS +xs_max_size: 15 +s_label: size/S +s_max_size: 100 +m_label: size/M +m_max_size: 500 +l_label: size/L +l_max_size: 1000 +xl_label: size/XL + +files_to_ignore: +- "**/*.lock" +- "**/package-lock.json" +- "generated/**" + +ignore_file_deletions: true +ignore_line_deletions: false +overwrite_other_size_labels: false +size_label_prefix: size/ +fail_if_xl: false +message_if_xl: Please consider splitting this PR into smaller changes. +dry_run: false +``` + +## Policy inputs + +| Input | Default | Behavior | +| --- | --- | --- | +| `xs_label`, `s_label`, `m_label`, `l_label`, `xl_label` | `size/XS`, `size/S`, `size/M`, `size/L`, `size/XL` | Five distinct managed label names. | +| `xs_max_size`, `s_max_size`, `m_max_size`, `l_max_size` | `15`, `100`, `500`, `1000` | Strictly increasing **exclusive** upper bounds. | +| `files_to_ignore` | Empty | Whitespace-separated globs in inputs; a string or list in YAML. | +| `ignore_file_deletions` | `true` | Exclude entirely removed files from line and file counts. | +| `ignore_line_deletions` | `false` | Count additions only, excluding deleted lines in all files. | +| `overwrite_other_size_labels` | `false` | Also remove other size labels matching `size_label_prefix`. | +| `size_label_prefix` | `size/` | Case-insensitive prefix used only when overwriting other size labels. | +| `fail_if_xl` | `false` | Fail the step after labeling/commenting on an XL PR. | +| `message_if_xl` | Existing tedilabs warning text | Sticky XL comment. Set `""` to disable. | +| `dry_run` | `false` | Compute planned label changes without writes or XL failure enforcement. | + +The defaults preserve the tedilabs workflow: XS is 0–14 lines, S is 15–99, M is 100–499, L is 500–999, and XL is 1000 or more. This action accepts the CodelyTV input names, including `GITHUB_TOKEN` and `github_api_url`, but defaults to tedilabs' uppercase labels, XS threshold of 15, and exclusion of deleted files. It does not copy CodelyTV's lowercase standalone defaults. + +Glob rules use the same Bash-style semantics as the JavaScript alternative, including negated character classes and extglobs; neither implementation relies on picomatch-specific matching quirks. Patterns match the current repository-relative filename, including dotfiles. Use `**/*.lock` for lockfiles at any depth. Quote whitespace-containing patterns in string inputs, or use a YAML list. Rename matching uses the new path. Patterns use wcmatch glob syntax aligned with the JavaScript version’s minimatch options; they are not regular expressions. An excluded file contributes neither lines nor a file count. Binary-only changes with zero additions/deletions contribute zero lines. File counts are informational and do not affect the size grade. + +Deletion examples for a PR modifying a file with 10 additions and 20 deletions, and deleting another 100-line file: + +| `ignore_file_deletions` | `ignore_line_deletions` | Counted lines | Counted files | +| --- | --- | ---: | ---: | +| `true` | `false` | 30 | 1 | +| `false` | `false` | 130 | 2 | +| `true` | `true` | 10 | 1 | +| `false` | `true` | 10 | 2 | + +## Label ownership and comments + +GitHub labels do not record which action manages them. This action defines ownership **by configured name**: the five configured labels are managed regardless of who originally applied them. It removes obsolete managed labels and adds the selected one, preserving every unrelated label. + +With the defaults, a PR containing `size/L`, `size/manual`, and `bug`, recalculated as S, becomes: + +- `overwrite_other_size_labels: false`: `size/S`, `size/manual`, `bug`. +- `overwrite_other_size_labels: true`: `size/S`, `bug`. + +Use `size_label_prefix` to identify a different namespace. It does not limit the five configured managed names. Matching is case-insensitive. If humans must retain manual size overrides, use a separate label name outside the five configured names and keep overwriting disabled. This action does not track the provenance of same-name labels or skip classification when a foreign size label exists. + +XL comments have an action-specific HTML marker and are matched to the authenticated token author's login. Reruns update the same comment instead of posting duplicates. Other users' marker-containing comments are not changed. Existing comments remain as history when the PR shrinks or commenting is disabled. Changing the token author starts a separate comment. + +## Outputs and limits + +| Output | Meaning | +| --- | --- | +| `size` | `xs`, `s`, `m`, `l`, or `xl`. | +| `label` | Selected label name. | +| `changed_lines`, `changed_files` | Counts after exclusions. | +| `count_complete` | `true` when all changed files were returned. | +| `config_file` | Loaded config path, or empty. | +| `added_labels`, `removed_labels` | JSON arrays of changes; plans only in dry-run mode. | +| `dry_run` | Effective dry-run setting after file overrides. | + +For an initial read-only trial, set `dry_run: "true"`, grant only `contents: read` and `pull-requests: read`, and ensure the base configuration does not override `dry_run` to `false`. Invalid inputs and API errors still fail dry runs. Configuration precedence applies to `dry_run` just like other policy inputs. + +Files and labels are paginated. GitHub's file API returns at most 3,000 files: if an incomplete count already reaches XL, the action labels XL and returns `count_complete: false`. Otherwise it fails before writing rather than understating the PR size. Counts in that case are lower bounds. A second PR read rejects head/base changes during calculation. Use per-PR workflow concurrency as shown above to reduce overlapping runs; GitHub does not offer atomic label updates. + +The action neither manages label colors/descriptions nor removes repository label definitions. Keep those in the existing `.github/labels*.yaml` synchronization workflow. It only adds/removes labels on the PR. + +## Development + +From this action's directory, using uv: + +```sh +uv export --no-config --locked --script size_labeler.py --format requirements-txt --no-hashes --output-file /tmp/size-labeler-test-requirements.txt +uv run --no-config --no-project --python 3.12 --with-requirements /tmp/size-labeler-test-requirements.txt python -m unittest discover -s tests -v +``` + +Update versions in the script's PEP 723 metadata and run `uv lock --script size_labeler.py` when updating dependencies. Commit `size_labeler.py.lock`; `--locked` rejects stale metadata rather than silently resolving new versions during action execution. No generated source files are committed. + +`tests/contract.json` is byte-identical to the JavaScript alternative's contract: both implementations run the same 69 scenarios for policy decisions, outputs, label/comment changes, configuration, glob/YAML semantics, and failure conditions. Python-specific tests additionally exercise the actual PyGithub SDK with paginated transport responses, Enterprise API paths, output serialization, and validation. CI runs the tests and the real composite's read-only smoke test on Linux AMD64 and ARM64. Live writes are tested through stubs, not performed by the smoke test. + +## Dependency decisions + +| Dependency | Why it is used | Maintenance evidence checked on 2026-10-24 | +| --- | --- | --- | +| [PyGithub 2.10.0](https://github.com/PyGithub/PyGithub/releases/tag/v2.10.0) | GitHub REST objects, pagination, authentication, and Enterprise support replace a custom HTTP client. | About 7,800 stars; August 2026 release and October 2026 repository activity. | +| [ruamel.yaml 0.19.1](https://yaml.dev/doc/ruamel.yaml/api/) | Safe YAML 1.2 parsing and duplicate-key rejection; a small resolver selects JSON scalar rules to match js-yaml. | Established YAML implementation; pinned January 2026 release. | +| [wcmatch 11.2](https://github.com/facelessuser/wcmatch/releases/tag/11.2) | Globstar, dotfiles, braces, character classes and extglobs without a hand-written glob parser. | Smaller specialist project (about 169 stars), with an October 2026 release and active maintenance. | + +The lock file also pins transitive versions and artifact hashes. PyYAML was considered, but preserving JSON-schema booleans and duplicate-key rejection would require more custom parsing. HTTPX/requests alone would require maintaining pagination and GitHub endpoint handling. Pydantic and a CLI framework would add dependencies without removing the cross-field policy validation or the two environment inputs, so they are not included. Tests use the standard-library unittest framework. + +This is an alternative to the JavaScript PR, not a follow-up that should be merged alongside it: both define the same action path and update the same reusable workflow. Compare and merge one implementation. + +Potential follow-ups are file-count thresholds and `.gitattributes` generated-file exclusion. They change the classification policy or add config parsing beyond the current requirement, so this action initially keeps line-based grading and explicit globs. diff --git a/.github/actions/github.pr.size-labeler/action.yaml b/.github/actions/github.pr.size-labeler/action.yaml new file mode 100644 index 0000000..0f74371 --- /dev/null +++ b/.github/actions/github.pr.size-labeler/action.yaml @@ -0,0 +1,139 @@ +name: GitHub - PR Size Labeler +description: Label pull requests by changed lines, with YAML configuration and independent deletion filters. Runs Python with uv and locked inline dependencies on AMD64 and ARM64. + + +inputs: + config_file: + required: false + default: "" + description: "(Optional) Repository-relative YAML path at the PR base commit. When omitted, tries `.github/size-labeler.yaml`, then `.github/size-labeler.yml`. File values override matching action inputs. An explicitly requested missing file fails." + pr_number: + required: false + default: "" + description: "(Optional) The pull request number. Defaults to the PR event number. Required for workflow_dispatch or workflow_run." + GITHUB_TOKEN: + required: false + default: ${{ github.token }} + description: "(Optional) GitHub token. Requires contents: read and pull-requests: write; a dry run only needs read permissions. Retains the CodelyTV input name." + github_api_url: + required: false + default: ${{ github.api_url }} + description: "(Optional) GitHub REST API URL, including GitHub Enterprise Server. Defaults to github.api_url." + xs_label: + required: false + default: size/XS + description: "(Optional) Label for XS pull requests. Defaults to `size/XS`." + xs_max_size: + required: false + default: "15" + description: "(Optional) Exclusive upper bound for XS. Defaults to `15` (0-14 lines)." + s_label: + required: false + default: size/S + description: "(Optional) Label for S pull requests. Defaults to `size/S`." + s_max_size: + required: false + default: "100" + description: "(Optional) Exclusive upper bound for S. Defaults to `100`." + m_label: + required: false + default: size/M + description: "(Optional) Label for M pull requests. Defaults to `size/M`." + m_max_size: + required: false + default: "500" + description: "(Optional) Exclusive upper bound for M. Defaults to `500`." + l_label: + required: false + default: size/L + description: "(Optional) Label for L pull requests. Defaults to `size/L`." + l_max_size: + required: false + default: "1000" + description: "(Optional) Exclusive upper bound for L. Values at or above this threshold are XL. Defaults to `1000`." + xl_label: + required: false + default: size/XL + description: "(Optional) Label for XL pull requests. Defaults to `size/XL`." + files_to_ignore: + required: false + default: "" + description: "(Optional) Whitespace-separated glob patterns, or a YAML list in the config file. Matches repository-relative paths, including dotfiles. Quote patterns containing spaces; use `**/*.lock` for nested lockfiles." + ignore_file_deletions: + required: false + default: "true" + description: "(Optional) Exclude completely removed files from both line and file counts. Defaults to `true`, matching the existing tedilabs workflow." + ignore_line_deletions: + required: false + default: "false" + description: "(Optional) Count additions only instead of additions plus deletions. Defaults to `false`." + overwrite_other_size_labels: + required: false + default: "false" + description: "(Optional) Also remove labels matching size_label_prefix outside the five configured labels. Defaults to `false` (preserve them). Ownership is by label name, not the actor that applied it." + size_label_prefix: + required: false + default: size/ + description: "(Optional) Case-insensitive prefix identifying other size labels when overwrite_other_size_labels is true. Defaults to `size/`." + fail_if_xl: + required: false + default: "false" + description: "(Optional) Fail the step after labeling an XL PR. Defaults to `false`. Not enforced in dry_run mode." + message_if_xl: + required: false + default: This PR has too many changes. Please make sure you are NOT addressing multiple issues with one PR. + description: "(Optional) Sticky comment for an XL PR. An empty string disables commenting. Reuses this action's comment by the same token author; historical comments remain when the PR shrinks." + dry_run: + required: false + default: "false" + description: "(Optional) Calculate and output planned changes without modifying labels/comments or enforcing fail_if_xl. Defaults to `false`." + +outputs: + size: + value: ${{ steps.label.outputs.size }} + description: The calculated size, one of xs, s, m, l, xl. + label: + value: ${{ steps.label.outputs.label }} + description: The calculated label name. + changed_lines: + value: ${{ steps.label.outputs.changed_lines }} + description: Changed lines after exclusions; a lower bound if count_complete is false. + changed_files: + value: ${{ steps.label.outputs.changed_files }} + description: Included files after exclusions; a lower bound if count_complete is false. + count_complete: + value: ${{ steps.label.outputs.count_complete }} + description: Whether all changed files were counted. An incomplete count is accepted only when XL is certain. + config_file: + value: ${{ steps.label.outputs.config_file }} + description: The loaded repository-relative configuration path, or empty when none exists. + added_labels: + value: ${{ steps.label.outputs.added_labels }} + description: JSON array of labels to add; planned changes only in dry_run mode. + removed_labels: + value: ${{ steps.label.outputs.removed_labels }} + description: JSON array of labels to remove; planned changes only in dry_run mode. + dry_run: + value: ${{ steps.label.outputs.dry_run }} + description: The effective dry_run setting after configuration overrides. + + +runs: + using: composite + + steps: + - name: Set up uv + uses: astral-sh/setup-uv@v10.3.2 + with: + version: "0.13.2" + python-version: "3.12" + enable-cache: false + + - name: Label Pull Request Size + id: label + shell: bash + env: + SIZE_LABELER_ACTION_PATH: ${{ github.action_path }} + SIZE_LABELER_INPUTS: ${{ toJSON(inputs) }} + GITHUB_TOKEN: ${{ inputs.GITHUB_TOKEN || github.token }} + run: uv run --no-config --locked --script "$SIZE_LABELER_ACTION_PATH/size_labeler.py" diff --git a/.github/actions/github.pr.size-labeler/size_labeler.py b/.github/actions/github.pr.size-labeler/size_labeler.py new file mode 100644 index 0000000..5ab6a6e --- /dev/null +++ b/.github/actions/github.pr.size-labeler/size_labeler.py @@ -0,0 +1,240 @@ +# /// script +# requires-python = ">=3.12,<3.13" +# dependencies = ["PyGithub==2.10.0", "ruamel.yaml==0.19.1", "wcmatch==11.2"] +# /// +"""PR size policy; API pagination, YAML parsing, and globs are library-managed.""" + +import json +import os +import re +import sys +from pathlib import Path +from uuid import uuid4 + +from github import Auth, Github, GithubException +from ruamel.yaml import YAML +from ruamel.yaml.resolver import VersionedResolver +from wcmatch import glob + +SIZES = ("xs", "s", "m", "l", "xl") +DEFAULTS = { + "xs_label": "size/XS", "xs_max_size": 15, + "s_label": "size/S", "s_max_size": 100, + "m_label": "size/M", "m_max_size": 500, + "l_label": "size/L", "l_max_size": 1000, + "xl_label": "size/XL", "files_to_ignore": [], + "ignore_file_deletions": True, "ignore_line_deletions": False, + "overwrite_other_size_labels": False, "size_label_prefix": "size/", + "fail_if_xl": False, "dry_run": False, + "message_if_xl": "This PR has too many changes. Please make sure you are NOT addressing multiple issues with one PR.", +} +DEFAULT_PATHS = (".github/size-labeler.yaml", ".github/size-labeler.yml") +COMMENT_MARKER = "" + + +class ConfigResolver(VersionedResolver): + @property + def versioned_resolver(self): + # JSON schema, matching js-yaml: no YAML 1.1 booleans, dates, octal, or merge keys. + return {None: [ + ("tag:yaml.org,2002:null", re.compile(r"^null$")), + ("tag:yaml.org,2002:bool", re.compile(r"^(?:true|false)$")), + ("tag:yaml.org,2002:int", re.compile(r"^-?(?:0|[1-9][0-9]*)$")), + ("tag:yaml.org,2002:float", re.compile(r"^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$")), + ]} + + +def parse_config(text): + parser = YAML(typ="safe", pure=True) + parser.Resolver = ConfigResolver + def boolean(constructor, node): + if node.value not in ("true", "false"): + raise ValueError("Explicit YAML booleans must be true or false.") + return node.value == "true" + parser.constructor.yaml_constructors = {**parser.constructor.yaml_constructors, "tag:yaml.org,2002:bool": boolean} + documents = list(parser.load_all(text)) + if len(documents) > 1: + raise ValueError("Configuration must contain only one YAML document.") + config = documents[0] if documents else {} + if not isinstance(config, dict): + raise ValueError("Configuration must be a YAML mapping.") + for key in config: + if key not in DEFAULTS: + raise ValueError(f"Unknown configuration key: {key}") + return config + + +def resolve_options(inputs, config): + options = {**DEFAULTS, **{ + key: value for key, value in inputs.items() + if key in DEFAULTS and (value != "" or key in ("message_if_xl", "files_to_ignore")) + }, **config} + for key, default in DEFAULTS.items(): + if type(default) is bool: + value = options[key] + if type(value) is not bool and value not in ("true", "false"): + raise ValueError(f"{key} must be true or false.") + options[key] = value is True or value == "true" + previous, names = 0, [] + for size in SIZES: + label = options[f"{size}_label"] + if not isinstance(label, str) or not label.strip() or len(label.encode("utf-16-le")) // 2 > 50: + raise ValueError(f"{size}_label must be a nonempty label of at most 50 characters.") + names.append(label.lower()) + if size == "xl": + continue + key = f"{size}_max_size" + raw = options[key] + if isinstance(raw, str) and re.fullmatch(r"[0-9]+", raw): + raw = int(raw) + if type(raw) not in (int, float) or not previous < raw <= 2**53 - 1 or int(raw) != raw: + raise ValueError("Size thresholds must be positive, strictly increasing integers.") + options[key] = previous = int(raw) + if len(set(names)) != len(names): + raise ValueError("Size labels must be distinct (case-insensitive).") + if not isinstance(options["size_label_prefix"], str) or not options["size_label_prefix"].strip(): + raise ValueError("size_label_prefix must be a nonempty string.") + if not isinstance(options["message_if_xl"], str): + raise ValueError("message_if_xl must be a string.") + patterns = options["files_to_ignore"] + if isinstance(patterns, str): + patterns = [re.sub(r'''^(["'])(.*)\1$''', r"\2", part) + for part in re.findall(r'''"[^"]*"|'[^']*'|\S+''', patterns)] + if not isinstance(patterns, list) or any(not isinstance(p, str) or not p for p in patterns): + raise ValueError("files_to_ignore must be a string or a YAML list of nonempty glob patterns.") + options["files_to_ignore"] = patterns + return options + + +def load_config(repository, ref, explicit_path): + for path in (explicit_path,) if explicit_path else DEFAULT_PATHS: + try: + file = repository.get_contents(path, ref=ref) + except GithubException as error: + if error.status == 404 and not explicit_path: + continue + raise + if isinstance(file, list) or file.type != "file" or file.encoding != "base64": + raise ValueError(f"Configuration must be a regular UTF-8 file smaller than 1 MB: {path}") + return path, parse_config(file.decoded_content.decode("utf-8")) + return "", {} + + +def ignored_matcher(patterns): + matchers = [] + for pattern in patterns: + negate = False + while pattern.startswith("!") and not pattern.startswith("!("): + negate = not negate + pattern = pattern[1:] + matcher = glob.compile(pattern, flags=glob.GLOBSTAR | glob.DOTGLOB | glob.BRACE | glob.EXTGLOB | glob.FORCEUNIX) + matchers.append((matcher, negate)) + # Each list entry is an independent alternative, including negations. + return lambda name: any(matcher.match(name) != negate for matcher, negate in matchers) + + +def run(client, repository_name, event, inputs, core): + raw_number = inputs.get("pr_number") or event.get("pull_request", {}).get("number") + if not str(raw_number).isdigit() or not 0 < int(raw_number) <= 2**53 - 1: + raise ValueError("A pull request event or a positive pr_number input is required.") + repository = client.get_repo(repository_name) + pr = repository.get_pull(int(raw_number)) + config_path, config = load_config(repository, pr.base.sha, inputs.get("config_file")) + options = resolve_options(inputs, config) + core.info(f"Configuration: {config_path or '(action inputs)'} at base {pr.base.sha}") + ignored = ignored_matcher(options["files_to_ignore"]) + lines = files = seen = 0 + for file in pr.get_files(): + seen += 1 + if (options["ignore_file_deletions"] and file.status == "removed") or ignored(file.filename): + continue + files += 1 + lines += file.additions + (0 if options["ignore_line_deletions"] else file.deletions) + complete = seen == pr.changed_files + if not complete and lines < options["l_max_size"]: + raise ValueError(f"Only {seen} of {pr.changed_files} changed files were returned; cannot assign an accurate size label.") + if not complete: + core.warning("GitHub truncated the file list. Counts are lower bounds; the XL classification is certain.") + size = next(size for size in SIZES if size == "xl" or lines < options[f"{size}_max_size"]) + label = options[f"{size}_label"] + core.info(f"{'At least ' if not complete else ''}{lines} changed lines in {files} included files: {label}") + latest = repository.get_pull(pr.number) + if latest.head.sha != pr.head.sha or latest.base.sha != pr.base.sha: + raise ValueError("The PR changed during size calculation. Rerun the action for the current revision.") + existing = [entry.name for entry in latest.get_labels()] + managed = {options[f"{size}_label"].lower() for size in SIZES} + add = [] if label.lower() in {name.lower() for name in existing} else [label] + remove = [name for name in existing if name.lower() != label.lower() and ( + name.lower() in managed or (options["overwrite_other_size_labels"] and name.lower().startswith(options["size_label_prefix"].lower())) + )] + for key, value in { + "size": size, "label": label, "changed_lines": lines, "changed_files": files, + "count_complete": complete, "config_file": config_path, "dry_run": options["dry_run"], + "added_labels": json.dumps(add, ensure_ascii=False, separators=(",", ":")), + "removed_labels": json.dumps(remove, ensure_ascii=False, separators=(",", ":")), + }.items(): + core.output(key, value) + if options["dry_run"]: + core.info(f"Dry run: add {add}, remove {remove}; no writes or XL failure enforcement.") + return + if add: + latest.add_to_labels(*add) + for name in remove: + try: + latest.remove_from_labels(name) + except GithubException as error: + if error.status != 404: + raise + if size == "xl" and options["message_if_xl"]: + _, data = client.requester.graphql_query("{ viewer { login } }", {}) + author = data["data"]["viewer"]["login"] + body = f"{COMMENT_MARKER}\n{options['message_if_xl']}" + comment = next((c for c in latest.get_issue_comments() if c.user and c.user.login == author and (c.body or "").startswith(COMMENT_MARKER)), None) + if comment is None: + latest.create_issue_comment(body) + elif comment.body != body: + comment.edit(body) + if size == "xl" and options["fail_if_xl"]: + core.fail("The PR reaches the XL size threshold.") + + +class Actions: + failed = False + + def info(self, message): + print(message) + + def annotation(self, level, message): + escaped = str(message).replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A") + print(f"::{level}::{escaped}") + + def warning(self, message): + self.annotation("warning", message) + + def fail(self, message): + self.failed = True + self.annotation("error", message) + + def output(self, key, value): + text = json.dumps(value) if not isinstance(value, str) else value + delimiter = uuid4().hex + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as stream: + stream.write(f"{key}<<{delimiter}\n{text}\n{delimiter}\n") + + +def main(): + core = Actions() + try: + inputs = json.loads(os.environ["SIZE_LABELER_INPUTS"]) + token = inputs.get("GITHUB_TOKEN") or os.environ["GITHUB_TOKEN"] + api_url = inputs.get("github_api_url") or os.environ["GITHUB_API_URL"] + event = json.loads(Path(os.environ["GITHUB_EVENT_PATH"]).read_text()) + with Github(auth=Auth.Token(token), base_url=api_url, per_page=100, lazy=True) as client: + run(client, os.environ["GITHUB_REPOSITORY"], event, inputs, core) + except Exception as error: + core.fail(error) + return int(core.failed) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/actions/github.pr.size-labeler/size_labeler.py.lock b/.github/actions/github.pr.size-labeler/size_labeler.py.lock new file mode 100644 index 0000000..6081343 --- /dev/null +++ b/.github/actions/github.pr.size-labeler/size_labeler.py.lock @@ -0,0 +1,267 @@ +version = 1 +revision = 5 +requires-python = ">=3.12, <3.13" + +[manifest] +requirements = [ + { name = "pygithub", specifier = "==2.10.0" }, + { name = "ruamel-yaml", specifier = "==0.19.1" }, + { name = "wcmatch", specifier = "==11.2" }, +] + +[[package]] +name = "bracex" +version = "3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/37/adf8dd2cde958c2d086bdd87fc36b532fd824bf38f10be6348d65da0c8f9/bracex-3.2.tar.gz", hash = "sha256:0a3d40bfae4caae4d3bfd0ea715874799fab711c38fd4012cc35343e815405cd", size = 29771, upload-time = "2026-10-22T00:28:49.508Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/65/8030b03de8137d1aab7dd6ecfecdba2fc22fc849fa35959b024f62293a99/bracex-3.2-py3-none-any.whl", hash = "sha256:8f3e908a452f66707e1d6d91b3d7d7d4c649d248df18537c0dc710aeb9c30d87", size = 12019, upload-time = "2026-10-22T00:28:48.233Z" }, +] + +[[package]] +name = "certifi" +version = "2026.10.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/11/b524cbd9bbdaca58608e4cf4053fd463417549e1959fa8d3aa18319f9555/certifi-2026.10.23.tar.gz", hash = "sha256:a9895e6e2758b371e0545e321a3a8fd1fcfd9829ada8824a936dfb16aaaf36cd", size = 136225, upload-time = "2026-10-23T11:36:16.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d1/a3652fe2c19134d0d15922177821d863cbd7e842d53a21bbe7e8e3a1b5ac/certifi-2026.10.23-py3-none-any.whl", hash = "sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1", size = 135082, upload-time = "2026-10-23T11:36:14.763Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/1c/f41d4e74c28ab327ff3acd36053f7ea506c55872d7a90b0fa71aa3ab0c89/charset_normalizer-3.5.2.tar.gz", hash = "sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef", size = 172659, upload-time = "2026-09-30T04:39:23.398Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c8/693809898870237d82785a03f3b2b58fe4c9f14669f84a7d4e623c92a59e/charset_normalizer-3.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed2a239c0ea213acc1908150a3037257083c7c083128f1a4cec2ec4b97dca491", size = 367780, upload-time = "2026-09-30T04:35:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/c9/87/2fea8c13dc24b3ca9c6f803a5b2dfdeae73eb4f9e12c7885ed908ff0433c/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b91363207bd9dc966a691e959bb47f64b30f7ac4b072be9968b366982f7db77c", size = 246730, upload-time = "2026-09-30T04:35:32.286Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9e/09efac30b937722f46d3110ba30b875b24b2e3a266ed746cc4e376a94d80/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:38a873987f3be698494da8b2e3085e29da02da7b633dce73e79c699a113d7bf0", size = 237707, upload-time = "2026-09-30T04:35:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/9e/18/70d76670b13686237863a379928d60bd10e021f17d243ab3d7014c4a5f4e/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:355ad8011081dec5412240c087a9a0c9d4d5039f3ed11a3f13e18c2b29b56c51", size = 273050, upload-time = "2026-09-30T04:35:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/54/e2/77a8b09d5adc013ed07b95b01b8b8fa5441c4e810e83ee7e4aae2fa4d91a/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ee21e28f0430bd6dc9086c6e525d5e818a44a5ad19720c8a0ef766792f3eb5e5", size = 270345, upload-time = "2026-09-30T04:35:36.502Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c5/38806a25ab5e65fc178f39affeda20858efafede2fce1ffc2556cfc9fe73/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649", size = 257601, upload-time = "2026-09-30T04:35:37.919Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8d/213565184708fdb263ae55e2c04ee1ff748129dd65d48ed0e3502da9c85a/charset_normalizer-3.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5cde776b7cc66e4f6c99612cea4aa7269aa65863f7a15841b2c264f103822f4e", size = 252222, upload-time = "2026-09-30T04:35:39.544Z" }, + { url = "https://files.pythonhosted.org/packages/7e/24/76d2cefc25472531e4c5c7dfff68865eb1c39b78482f0fdc15b46f047830/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae4f5fea5b8b8ccff88238cc8569303e5ee95efae67fa62922a311397a71f346", size = 248482, upload-time = "2026-09-30T04:35:41.088Z" }, + { url = "https://files.pythonhosted.org/packages/7d/dc/65a801b66ab4c197e22c433ab25e7ac24324ac6f45a2269aca42cce309bf/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f7d486c83842422badd511868fd8a9a20e9407ace71564b6af47ce7e60a336c1", size = 241206, upload-time = "2026-09-30T04:35:42.59Z" }, + { url = "https://files.pythonhosted.org/packages/a7/95/ca9b5eabde673002c6f1e7ada1b223916fe18f6d661da7aabd4d643718f1/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11a4d68a6ecda3292cb1e50239e111543ba5d709bb62a6b4ea1afcfa729d8875", size = 273190, upload-time = "2026-09-30T04:35:44.347Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8b/803b4d2a3f6e1740f63f1e87b04d14b42f3d4fdfe6ed7d4db2d34102b14f/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d6734d2ef8a50fbf8445c139477da401f50d62a0606bf00e20ec6d87773fefb1", size = 253527, upload-time = "2026-09-30T04:35:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/a9/55/93c0e5dbd085ae0471346026abbe7e0db9ea2d6fea74e51f0b5a46f233a7/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a815775b6c38d4e0ff7bcffbeba67feded90202bb6a226b8dd35f1c855217413", size = 271285, upload-time = "2026-09-30T04:35:47.49Z" }, + { url = "https://files.pythonhosted.org/packages/95/69/0dbd0e0b9b16cfa816cdfcb3e2e3854a1f680dc07fb1245ea125e7448060/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:23851fb4e1b85ed3f6c2a27b777cdfe2e19fb5b38429a8faf38c7542b7665869", size = 260010, upload-time = "2026-09-30T04:35:48.996Z" }, + { url = "https://files.pythonhosted.org/packages/58/9d/e7b88e7b1bf403590c3b573277b5e1e488c68c7a6fbacca310a2c324e90c/charset_normalizer-3.5.2-cp312-cp312-win32.whl", hash = "sha256:db19d07e2e0129e974a0e65d0064fc222a446cd5122c2fd4184d2af9fc734a9e", size = 184125, upload-time = "2026-09-30T04:35:50.777Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e6/e6e083884cbcfd49c64865af05027fe7011be7b2d9179524f099a1b611f3/charset_normalizer-3.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:780fbe7cab297b81dad9fb8dc5eb003c0468ffb0d9e5f65068c53a34661a96bc", size = 207486, upload-time = "2026-09-30T04:35:52.194Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e3/017aea0911ada7405a825c7d937eb3a13009664e2f5b38e8c4bbf2abf894/charset_normalizer-3.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:e2af3aad578aa6bd1384bcf4750fc285e5a9de53f40b7d41e5a0bf748edeb2b3", size = 196734, upload-time = "2026-09-30T04:35:53.636Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ab/176fbfd5b64939c55d652366aa5b9ef1d767af207a3aa6ebeb0d226c484d/charset_normalizer-3.5.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd", size = 331815, upload-time = "2026-09-30T04:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/7e/84/371eac6b30bdbcbf2d632a1a01809103459216fcaae61b8b8d922c1bfb8a/charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7", size = 253276, upload-time = "2026-09-30T04:38:28.032Z" }, + { url = "https://files.pythonhosted.org/packages/43/6f/c4fbae58febff71709c51bc7e18fdfa55341dc382704740f9f0cbf03817b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f", size = 241239, upload-time = "2026-09-30T04:38:29.732Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/458c3f42164a07d0c5210798e9e704b39e540a6793b05aba67f3a35243a9/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93", size = 231121, upload-time = "2026-09-30T04:38:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/ab9e89367076f6331bb6c65c4bf14a5361fa5191cb6561bf534f18504e1b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade", size = 260350, upload-time = "2026-09-30T04:38:33.239Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/061431ecc688d9d76602502cb57cc01e691e682c18f1beb45f9673b5bbd2/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0", size = 255430, upload-time = "2026-09-30T04:38:34.865Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1f/20c8949f0676f7ab811abdeb7f4d7f1cbc6e61ff20bef08b44edeb092bc8/charset_normalizer-3.5.2-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26", size = 250612, upload-time = "2026-09-30T04:38:36.649Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/46f2fa4c431fc98c4ae76a8cb5bdca54e0341e3cfc3fcfd8e82740250818/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011", size = 242083, upload-time = "2026-09-30T04:38:38.26Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/559be29a0c0f086e0bba6922babd38916cc5e0b58ced4de13ee01ea05508/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621", size = 232738, upload-time = "2026-09-30T04:38:39.81Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6c/387b0e4f756a282831c1d9fc6aeb6c51ca4507ca202767c8de15ce9b12e2/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4", size = 260703, upload-time = "2026-09-30T04:38:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/1fdf015f09ef449f50d3ac4b67c90887c9c318b727daa95cc4f866e6521d/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e", size = 247622, upload-time = "2026-09-30T04:38:42.937Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3c/8e7b8a5671ad5d433669fb2a76f1a0164df2d9b1718b0206bc2a16d840cc/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c", size = 257500, upload-time = "2026-09-30T04:38:44.604Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f0/45b579df5cabc1d5d53ea1cc35e8437d3ca768c0acccc7041517cb6fbb32/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0", size = 255100, upload-time = "2026-09-30T04:38:46.289Z" }, + { url = "https://files.pythonhosted.org/packages/31/68/fdec18a343f5fb3f310588dd478b09ac4799e0b187dbade3a8cd776f03ef/charset_normalizer-3.5.2-cp37-abi3-win32.whl", hash = "sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf", size = 174499, upload-time = "2026-09-30T04:38:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8a/b618149cc5207943a0242068d7a27897f56a62947b5a039085f2a22029f8/charset_normalizer-3.5.2-cp37-abi3-win_amd64.whl", hash = "sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036", size = 200092, upload-time = "2026-09-30T04:38:49.707Z" }, + { url = "https://files.pythonhosted.org/packages/03/cf/4c66866fa9e2b1c78e3c911516d1de497a677b7ac60f1eceda74ce777ca3/charset_normalizer-3.5.2-cp37-abi3-win_arm64.whl", hash = "sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e", size = 294363, upload-time = "2026-09-30T04:38:51.312Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/d07d7862a62ffa6d79d68074d14823243dd235a77c45262acbf6adeb28bf/charset_normalizer-3.5.2-py3-none-any.whl", hash = "sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685", size = 68872, upload-time = "2026-09-30T04:39:21.828Z" }, +] + +[[package]] +name = "cryptography" +version = "50.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz", hash = "sha256:7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5", size = 880623, upload-time = "2026-09-30T15:30:04.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/56/d194340cc4a57535e82e1bee9e89667ac4b7c13b5d3f59686deae3094dd5/cryptography-50.0.2-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb", size = 3914904, upload-time = "2026-09-30T14:43:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/d9/69/c9bd862c3bf43d6399c433caf002df16e2dffd4be49bdf515cda38038711/cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0", size = 4731146, upload-time = "2026-09-30T14:43:47.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2", size = 4719841, upload-time = "2026-09-30T14:43:49.01Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61a3f8d8c5e1e49a6cddccafc4015cc1c0021360ab0acb4080e7a423644a/cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480", size = 4738340, upload-time = "2026-09-30T14:43:50.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2e/7212ca32fd43dc91f2f41db20160b268098874b4c9a0e7be94d6835f5b2e/cryptography-50.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a582ab2ae1d34f67112cadc86702774c9ea4374df6bca6afe672817203c99134", size = 5367029, upload-time = "2026-09-30T14:43:52.911Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856", size = 4753050, upload-time = "2026-09-30T14:43:55.272Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/9af10e80ac16b0fcc2123f9cbd5e7afbd0fd5075bb7a607c592258a39cda/cryptography-50.0.2-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e", size = 4376724, upload-time = "2026-09-30T14:43:57.24Z" }, + { url = "https://files.pythonhosted.org/packages/71/37/6202e488cc1eb625ea110c292c6bda92823176e023f427d8d5660ce8d632/cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04", size = 4737859, upload-time = "2026-09-30T14:43:59.541Z" }, + { url = "https://files.pythonhosted.org/packages/8f/30/e86d7d518489b0ae2497091a35287abcb1a2ce4037837a34afbe9b1d6964/cryptography-50.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f265528741e048bce55c3463ed721fb0aa45a5888d8add8cfeccb3035451bbdc", size = 5324103, upload-time = "2026-09-30T14:44:01.901Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079", size = 4752576, upload-time = "2026-09-30T14:44:04.545Z" }, + { url = "https://files.pythonhosted.org/packages/6c/5d/906970b83bbfc1f5bbfb677a143c181f2801f23b6a7204a3b47c42c97e65/cryptography-50.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51", size = 4870819, upload-time = "2026-09-30T14:44:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/68/e3/f2298d3bb55e0c4a91841ec4d01b3f020ba8c5fbf15ccdcc6dcf03f97025/cryptography-50.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93", size = 5030152, upload-time = "2026-09-30T14:44:09.443Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4f/adfc442765721292fff86d314ce385d3249d22db42295c0dd057727b60f3/cryptography-50.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:7afa5a6602a9f29af1f3a2965f831bae7c9d5d597b7cbb716d41ab3b7d89879c", size = 3824692, upload-time = "2026-09-30T14:44:11.671Z" }, + { url = "https://files.pythonhosted.org/packages/2d/49/93f6a6e7a87c9aa68d44d3e1cdb5fe8f60c90d5d2f46acae9a56892816b8/cryptography-50.0.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:edc3342adf8f697fc5f59c887a304356f147b397809440ed64e2fa6af2f50f37", size = 4133708, upload-time = "2026-09-30T14:44:41.807Z" }, + { url = "https://files.pythonhosted.org/packages/8c/75/32ac2a56243d778805c16ca6a32b8f74fb757df7e28d7ecb560afafb59cf/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a", size = 4956267, upload-time = "2026-09-30T14:44:43.693Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67", size = 4966465, upload-time = "2026-09-30T14:44:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/c2/58/ee288c829a6f41f6235ae9dd33d82fd19b45442b65b4c8a3da36963d9f7a/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl", hash = "sha256:e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc", size = 4959356, upload-time = "2026-09-30T14:44:48.211Z" }, + { url = "https://files.pythonhosted.org/packages/92/20/9ded6d51ddd9897f6b6e81fb9ebea7951d7cc5d6c890b0ed8abf77a51a80/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_ppc64le.whl", hash = "sha256:b13478603dcd0a2479ff8e87e2c19a7d525734686fe3c49542472293a204212d", size = 5548822, upload-time = "2026-09-30T14:44:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl", hash = "sha256:58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7", size = 5001199, upload-time = "2026-09-30T14:44:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f9/36b3022218ce75b7cdf068fb95f809f9bd0d820e4955ef43b90c255cc7ac/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_31_armv7l.whl", hash = "sha256:d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408", size = 4629333, upload-time = "2026-09-30T14:44:55.635Z" }, + { url = "https://files.pythonhosted.org/packages/8c/72/20f99a219f6af47cdd1cbd978c243b92d71496e168a746138af44ded4f29/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl", hash = "sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b", size = 4958822, upload-time = "2026-09-30T14:44:59.639Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/196f112617fb08eb4d608a2a6c422373d46f9cc2857f38fc0667033c0899/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_ppc64le.whl", hash = "sha256:cbc8738fd8526d80f35cb3a40d41f41a2e7030bb3b18b09a6778ef63d291c2fd", size = 5506351, upload-time = "2026-09-30T14:45:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl", hash = "sha256:e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c", size = 5000859, upload-time = "2026-09-30T14:45:05.009Z" }, + { url = "https://files.pythonhosted.org/packages/22/f7/70fd7ae4d1dbfa7ba29b02e1b9068771519a86027756510b700ce81086a8/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be", size = 5092151, upload-time = "2026-09-30T15:29:15.932Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/688367b74de86984bd58d8efacfc7c9e68b89a6a22ced0fb4f38db50254a/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020", size = 5286120, upload-time = "2026-09-30T15:29:18.309Z" }, + { url = "https://files.pythonhosted.org/packages/39/d1/55f8a3f2ef5d1529e16835ef10cf0fe3d559ce237b46dddc440c0bba3649/cryptography-50.0.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:c423ab384a46c4dff7217b2ea5ba2e11cffdeab6441acd04cf65a369caf0366c", size = 4111557, upload-time = "2026-09-30T15:29:20.155Z" }, + { url = "https://files.pythonhosted.org/packages/23/ad/ac987755d00e1e64273760228d2635ae38dae2be83e3c6e0d3289d91dec3/cryptography-50.0.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0ec5f09541743261e66e291b4a0cbf0fb2997aeaab6d9e9c740b9dba1b58d1c2", size = 3943588, upload-time = "2026-09-30T15:29:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8d/6d585339bedf85d45044c85d8412dac53f2bb6f918e8b7777efba1787844/cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd", size = 4756166, upload-time = "2026-09-30T15:29:24.58Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767", size = 4749145, upload-time = "2026-09-30T15:29:26.807Z" }, + { url = "https://files.pythonhosted.org/packages/c1/63/61b15dc1a8de03fe0adbe3fd7608b3ad5c73bf50993bbcb1faaa930afe33/cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454", size = 4763638, upload-time = "2026-09-30T15:29:28.588Z" }, + { url = "https://files.pythonhosted.org/packages/fc/35/b345bdfa40c9126df1a9d33236aa98418367931b8725f84fc3ae2b98dc59/cryptography-50.0.2-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:58ddb5a8e3179d12f19e4ea34d2d32e9d63a4baa142c875c1eb59f41b7243acd", size = 5382217, upload-time = "2026-09-30T15:29:30.589Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5", size = 4781387, upload-time = "2026-09-30T15:29:32.605Z" }, + { url = "https://files.pythonhosted.org/packages/90/5b/f2fdb13cd0b96f6f932c8627bb292a45f11c64d21620a8e120aee9a3b848/cryptography-50.0.2-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107", size = 4403790, upload-time = "2026-09-30T15:29:34.374Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ce/7e4f662b1e3c393513569e402cfc85ac7da0bd3d5435e122a3140219eb2d/cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602", size = 4764319, upload-time = "2026-09-30T15:29:36.149Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3f/86ff33ce34cc0de6847fb96e035a1a760d81652e38643f617c02ad32ef7a/cryptography-50.0.2-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a6557e5f38e065ca9fbdaf7cfc7435ecb1d113aa81a022d1b51921ee7432e227", size = 5338560, upload-time = "2026-09-30T15:29:39.053Z" }, + { url = "https://files.pythonhosted.org/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c", size = 4780973, upload-time = "2026-09-30T15:29:41.251Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/8d6ebc7dded797bd0f0160d52188021211f011a2b164ef0ae1dac4587465/cryptography-50.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e", size = 4897738, upload-time = "2026-09-30T15:29:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/f3f6e0de7e6253b8baa8b2d8fb9d50924fa75cee3d4624bd4bc1208ee923/cryptography-50.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94", size = 5058280, upload-time = "2026-09-30T15:29:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b6/a1faf3a27ae9405fb34b1713cc73b2d8a26b04d5c561578fa2e6ef3e5bb9/cryptography-50.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:4e81d95e5bafc2d6e34e4bed780e53e4d5b9a2f928573428aa4d35fbec1eb0de", size = 3854095, upload-time = "2026-09-30T15:29:46.782Z" }, +] + +[[package]] +name = "idna" +version = "3.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, +] + +[[package]] +name = "pycparser" +version = "3.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/27/eeb9f307bf07060f3d9f9aa0fb8c15b01bdfded0a941e4b2719c7f94cacd/pycparser-3.12.tar.gz", hash = "sha256:f03387877eab82ce01bbdfd0ba7252ee55a90b1c708d5129e2ce915475cf1db6", size = 158785, upload-time = "2026-10-22T13:40:03.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/ae/315bbeee126a769cbde43cf4908acb4b40408d8bf8bbf334dd7368d8dadf/pycparser-3.12-py3-none-any.whl", hash = "sha256:a076f3f489b39b43ed13563a1dafe42447445bec2e294154ec5c57fcdda4c85e", size = 62600, upload-time = "2026-10-22T13:40:02.021Z" }, +] + +[[package]] +name = "pygithub" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyjwt", extra = ["crypto"] }, + { name = "pynacl" }, + { name = "requests" }, + { name = "typing-extensions" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/9b/195603d5371861005a3467c5e4afd02fd0698795a2aa36dc41498b9d879d/pygithub-2.10.0.tar.gz", hash = "sha256:90ff24ef1cd1bd57124c2a3869cafee9d7b066909129ecdaba2c2d1903bc118d", size = 2750952, upload-time = "2026-08-20T10:05:08.327Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/71/f314841697a1d52af3e1ea7c5e1c3f09685b64ae4c58ff16b605a866255d/pygithub-2.10.0-py3-none-any.whl", hash = "sha256:192ada2a76e4afc7d6b37e500c9bfeba1731e6506697445a5ba1c4af8bf0b924", size = 455554, upload-time = "2026-08-20T10:05:06.991Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/ea/5194e52748b0da83d71e082d75496eaec6e58f419f5e184786ded517e6a9/pyjwt-2.15.1.tar.gz", hash = "sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8", size = 121252, upload-time = "2026-09-28T18:40:42.598Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/ca/44de4e75f8aadc457f0634be3b542815078ded46dca30efb960edeecad6e/pyjwt-2.15.1-py3-none-any.whl", hash = "sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193", size = 33860, upload-time = "2026-09-28T18:40:41.429Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pynacl" +version = "1.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, + { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "urllib3" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, +] + +[[package]] +name = "wcmatch" +version = "11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/07/b7a9162a804bc0f7d03fe84c93e374d3e1c4d828043d5f9b797f7d4e30e0/wcmatch-11.2.tar.gz", hash = "sha256:1605bde546fbde4c063628c7d89eda486909fd4337e51d73a873987fbcb2554c", size = 134939, upload-time = "2026-10-22T00:37:57.622Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/96/b6cebd4bd75320743a291fed494c79f07c01b58fa9b187d12aeb5110f002/wcmatch-11.2-py3-none-any.whl", hash = "sha256:952bab83c75c233836007a69550a3a8753cf06964c1eb3b84ff11ad5bff1c272", size = 44361, upload-time = "2026-10-22T00:37:56.293Z" }, +] diff --git a/.github/actions/github.pr.size-labeler/tests/contract.json b/.github/actions/github.pr.size-labeler/tests/contract.json new file mode 100644 index 0000000..7b6eeef --- /dev/null +++ b/.github/actions/github.pr.size-labeler/tests/contract.json @@ -0,0 +1,1618 @@ +[ + { + "name": "boundary 0", + "fixture": { + "pages": [ + [ + { + "filename": "main.tf", + "additions": 0, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": {}, + "outputs": { + "size": "xs", + "label": "size/XS", + "changed_lines": 0, + "count_complete": true + } + }, + { + "name": "boundary 14", + "fixture": { + "pages": [ + [ + { + "filename": "main.tf", + "additions": 14, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": {}, + "outputs": { + "size": "xs", + "label": "size/XS", + "changed_lines": 14, + "count_complete": true + } + }, + { + "name": "boundary 15", + "fixture": { + "pages": [ + [ + { + "filename": "main.tf", + "additions": 15, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": {}, + "outputs": { + "size": "s", + "label": "size/S", + "changed_lines": 15, + "count_complete": true + } + }, + { + "name": "boundary 99", + "fixture": { + "pages": [ + [ + { + "filename": "main.tf", + "additions": 99, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": {}, + "outputs": { + "size": "s", + "label": "size/S", + "changed_lines": 99, + "count_complete": true + } + }, + { + "name": "boundary 100", + "fixture": { + "pages": [ + [ + { + "filename": "main.tf", + "additions": 100, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": {}, + "outputs": { + "size": "m", + "label": "size/M", + "changed_lines": 100, + "count_complete": true + } + }, + { + "name": "boundary 499", + "fixture": { + "pages": [ + [ + { + "filename": "main.tf", + "additions": 499, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": {}, + "outputs": { + "size": "m", + "label": "size/M", + "changed_lines": 499, + "count_complete": true + } + }, + { + "name": "boundary 500", + "fixture": { + "pages": [ + [ + { + "filename": "main.tf", + "additions": 500, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": {}, + "outputs": { + "size": "l", + "label": "size/L", + "changed_lines": 500, + "count_complete": true + } + }, + { + "name": "boundary 999", + "fixture": { + "pages": [ + [ + { + "filename": "main.tf", + "additions": 999, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": {}, + "outputs": { + "size": "l", + "label": "size/L", + "changed_lines": 999, + "count_complete": true + } + }, + { + "name": "boundary 1000", + "fixture": { + "pages": [ + [ + { + "filename": "main.tf", + "additions": 1000, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": {}, + "outputs": { + "size": "xl", + "label": "size/XL", + "changed_lines": 1000, + "count_complete": true + } + }, + { + "name": "deletion switches True/False", + "fixture": { + "pages": [ + [ + { + "filename": "main.tf", + "additions": 10, + "deletions": 20, + "status": "modified" + }, + { + "filename": "gone.tf", + "additions": 0, + "deletions": 100, + "status": "removed" + } + ] + ] + }, + "inputs": { + "ignore_file_deletions": true, + "ignore_line_deletions": false + }, + "outputs": { + "changed_lines": 30, + "changed_files": 1 + } + }, + { + "name": "deletion switches False/False", + "fixture": { + "pages": [ + [ + { + "filename": "main.tf", + "additions": 10, + "deletions": 20, + "status": "modified" + }, + { + "filename": "gone.tf", + "additions": 0, + "deletions": 100, + "status": "removed" + } + ] + ] + }, + "inputs": { + "ignore_file_deletions": false, + "ignore_line_deletions": false + }, + "outputs": { + "changed_lines": 130, + "changed_files": 2 + } + }, + { + "name": "deletion switches True/True", + "fixture": { + "pages": [ + [ + { + "filename": "main.tf", + "additions": 10, + "deletions": 20, + "status": "modified" + }, + { + "filename": "gone.tf", + "additions": 0, + "deletions": 100, + "status": "removed" + } + ] + ] + }, + "inputs": { + "ignore_file_deletions": true, + "ignore_line_deletions": true + }, + "outputs": { + "changed_lines": 10, + "changed_files": 1 + } + }, + { + "name": "deletion switches False/True", + "fixture": { + "pages": [ + [ + { + "filename": "main.tf", + "additions": 10, + "deletions": 20, + "status": "modified" + }, + { + "filename": "gone.tf", + "additions": 0, + "deletions": 100, + "status": "removed" + } + ] + ] + }, + "inputs": { + "ignore_file_deletions": false, + "ignore_line_deletions": true + }, + "outputs": { + "changed_lines": 10, + "changed_files": 2 + } + }, + { + "name": "glob **/*.lock", + "fixture": { + "pages": [ + [ + { + "filename": "a.lock", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "pkg/a.lock", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": ".cache/a.lock", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "a.tf", + "additions": 1, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": { + "files_to_ignore": "**/*.lock" + }, + "outputs": { + "changed_lines": 1, + "changed_files": 1 + } + }, + { + "name": "glob *.lock", + "fixture": { + "pages": [ + [ + { + "filename": "a.lock", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "pkg/a.lock", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": ".hidden.lock", + "additions": 1, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": { + "files_to_ignore": "*.lock" + }, + "outputs": { + "changed_lines": 1, + "changed_files": 1 + } + }, + { + "name": "glob docs/**", + "fixture": { + "pages": [ + [ + { + "filename": "docs/a.md", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "docs/sub/b.md", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "main.tf", + "additions": 1, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": { + "files_to_ignore": "docs/**" + }, + "outputs": { + "changed_lines": 1, + "changed_files": 1 + } + }, + { + "name": "glob **/*.{json,yaml}", + "fixture": { + "pages": [ + [ + { + "filename": "a.json", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "b.yaml", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "sub/c.json", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "main.tf", + "additions": 1, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": { + "files_to_ignore": "**/*.{json,yaml}" + }, + "outputs": { + "changed_lines": 1, + "changed_files": 1 + } + }, + { + "name": "glob **/@(a|b).tf", + "fixture": { + "pages": [ + [ + { + "filename": "a.tf", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "sub/b.tf", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "c.tf", + "additions": 1, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": { + "files_to_ignore": "**/@(a|b).tf" + }, + "outputs": { + "changed_lines": 1, + "changed_files": 1 + } + }, + { + "name": "glob file?.[jt]s", + "fixture": { + "pages": [ + [ + { + "filename": "file1.js", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "file2.ts", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "file22.js", + "additions": 1, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": { + "files_to_ignore": "file?.[jt]s" + }, + "outputs": { + "changed_lines": 1, + "changed_files": 1 + } + }, + { + "name": "glob \"docs/a b.md\"", + "fixture": { + "pages": [ + [ + { + "filename": "docs/a b.md", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "docs/other.md", + "additions": 1, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": { + "files_to_ignore": "\"docs/a b.md\"" + }, + "outputs": { + "changed_lines": 1, + "changed_files": 1 + } + }, + { + "name": "glob !**/*.tf", + "fixture": { + "pages": [ + [ + { + "filename": "main.tf", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "readme.md", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "sub/a.tf", + "additions": 1, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": { + "files_to_ignore": "!**/*.tf" + }, + "outputs": { + "changed_lines": 2, + "changed_files": 2 + } + }, + { + "name": "glob [\"literal\"]", + "fixture": { + "pages": [ + [ + { + "filename": "file.tf", + "additions": 1, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": { + "files_to_ignore": "[\"literal\"]" + }, + "outputs": { + "changed_lines": 1, + "changed_files": 1 + } + }, + { + "name": "all file pages", + "fixture": { + "pages": [ + [ + { + "filename": "first.tf", + "additions": 0, + "deletions": 0, + "status": "modified" + } + ], + [ + { + "filename": "last.tf", + "additions": 1000, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": {}, + "outputs": { + "changed_files": 2, + "changed_lines": 1000, + "size": "xl" + } + }, + { + "name": "empty PR", + "fixture": { + "pages": [] + }, + "inputs": {}, + "outputs": { + "changed_files": 0, + "changed_lines": 0, + "size": "xs" + } + }, + { + "name": "renamed and binary", + "fixture": { + "pages": [ + [ + { + "filename": "new.tf", + "additions": 5, + "deletions": 2, + "status": "renamed" + }, + { + "filename": "image.png", + "additions": 0, + "deletions": 0, + "status": "added" + } + ] + ] + }, + "inputs": {}, + "outputs": { + "changed_files": 2, + "changed_lines": 7 + } + }, + { + "name": "config precedence", + "fixture": { + "config": { + ".github/size-labeler.yaml": "xs_max_size: 5\nignore_file_deletions: false", + ".github/size-labeler.yml": "xs_max_size: 20" + } + }, + "inputs": { + "xs_max_size": "30" + }, + "outputs": { + "label": "size/S", + "config_file": ".github/size-labeler.yaml" + } + }, + { + "name": "yml fallback", + "fixture": { + "config": { + ".github/size-labeler.yml": "xs_max_size: 5" + } + }, + "inputs": {}, + "outputs": { + "label": "size/S", + "config_file": ".github/size-labeler.yml" + } + }, + { + "name": "explicit config", + "fixture": { + "config": { + "config/size.yml": "xs_label: tiny" + } + }, + "inputs": { + "config_file": "config/size.yml" + }, + "outputs": { + "label": "tiny", + "config_file": "config/size.yml" + } + }, + { + "name": "missing explicit config", + "fixture": {}, + "inputs": { + "config_file": "missing.yml" + }, + "error": true + }, + { + "name": "empty config", + "fixture": { + "config": { + ".github/size-labeler.yml": "# no overrides" + } + }, + "inputs": {}, + "outputs": { + "label": "size/XS" + } + }, + { + "name": "reject duplicate", + "fixture": { + "config": { + ".github/size-labeler.yml": "xs_max_size: 5\nxs_max_size: 6" + } + }, + "inputs": {}, + "error": true + }, + { + "name": "reject multi-document", + "fixture": { + "config": { + ".github/size-labeler.yml": "{}\n---\n{}" + } + }, + "inputs": {}, + "error": true + }, + { + "name": "reject unknown", + "fixture": { + "config": { + ".github/size-labeler.yml": "unknown: true" + } + }, + "inputs": {}, + "error": true + }, + { + "name": "reject token", + "fixture": { + "config": { + ".github/size-labeler.yml": "GITHUB_TOKEN: anything" + } + }, + "inputs": {}, + "error": true + }, + { + "name": "reject invalid YAML", + "fixture": { + "config": { + ".github/size-labeler.yml": "xs_max_size: [" + } + }, + "inputs": {}, + "error": true + }, + { + "name": "reject null", + "fixture": { + "config": { + ".github/size-labeler.yml": "null" + } + }, + "inputs": {}, + "error": true + }, + { + "name": "reject bad threshold", + "fixture": { + "config": { + ".github/size-labeler.yml": "xs_max_size: 100" + } + }, + "inputs": {}, + "error": true + }, + { + "name": "reject bad boolean", + "fixture": { + "config": { + ".github/size-labeler.yml": "dry_run: yes" + } + }, + "inputs": {}, + "error": true + }, + { + "name": "reject duplicate labels", + "fixture": { + "config": { + ".github/size-labeler.yml": "xs_label: size/S" + } + }, + "inputs": {}, + "error": true + }, + { + "name": "preserve foreign labels", + "fixture": { + "labels": [ + "size/L", + "size/manual", + "bug" + ] + }, + "inputs": {}, + "outputs": { + "removed_labels": "[\"size/L\"]" + }, + "writes": [ + { + "method": "addLabels", + "labels": [ + "size/XS" + ] + }, + { + "method": "removeLabel", + "name": "size/L" + } + ] + }, + { + "name": "replace foreign labels", + "fixture": { + "labels": [ + "size/L", + "SIZE/manual", + "bug" + ] + }, + "inputs": { + "overwrite_other_size_labels": true + }, + "outputs": { + "removed_labels": "[\"size/L\",\"SIZE/manual\"]" + } + }, + { + "name": "custom prefix", + "fixture": { + "labels": [ + "size/L", + "review/manual", + "size/manual", + "bug" + ] + }, + "inputs": { + "overwrite_other_size_labels": true, + "size_label_prefix": "review/" + }, + "outputs": { + "removed_labels": "[\"size/L\",\"review/manual\"]" + } + }, + { + "name": "idempotent case insensitive", + "fixture": { + "labels": [ + "SIZE/xs", + "bug" + ] + }, + "inputs": {}, + "outputs": { + "added_labels": "[]", + "removed_labels": "[]" + }, + "writes": [] + }, + { + "name": "dry run XL", + "fixture": { + "pages": [ + [ + { + "filename": "main.tf", + "additions": 1000, + "deletions": 0, + "status": "modified" + } + ] + ], + "labels": [ + "size/S" + ] + }, + "inputs": { + "dry_run": true, + "fail_if_xl": true, + "message_if_xl": "warning" + }, + "outputs": { + "size": "xl", + "removed_labels": "[\"size/S\"]", + "dry_run": true + }, + "writes": [] + }, + { + "name": "config enables dry run", + "fixture": { + "config": { + ".github/size-labeler.yml": "dry_run: true" + } + }, + "inputs": { + "dry_run": false + }, + "outputs": { + "dry_run": true + }, + "writes": [] + }, + { + "name": "config disables dry run", + "fixture": { + "config": { + ".github/size-labeler.yml": "dry_run: false" + } + }, + "inputs": { + "dry_run": true + }, + "outputs": { + "dry_run": false + }, + "writes": [ + { + "labels": [ + "size/XS" + ], + "method": "addLabels" + } + ] + }, + { + "name": "definite XL truncation", + "fixture": { + "changedFiles": 3001, + "pages": [ + [ + { + "filename": "main.tf", + "additions": 1000, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": {}, + "outputs": { + "count_complete": false, + "size": "xl" + } + }, + { + "name": "ambiguous truncation", + "fixture": { + "changedFiles": 3001, + "pages": [ + [ + { + "filename": "main.tf", + "additions": 999, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": {}, + "error": true + }, + { + "name": "stale PR", + "fixture": { + "stale": true + }, + "inputs": {}, + "error": true + }, + { + "name": "XL comment and failure", + "fixture": { + "pages": [ + [ + { + "filename": "main.tf", + "additions": 1000, + "deletions": 0, + "status": "modified" + } + ] + ], + "labels": [ + "size/XL" + ] + }, + "inputs": { + "message_if_xl": "warning", + "fail_if_xl": true + }, + "writes": [ + { + "body": "\nwarning", + "method": "createComment" + } + ], + "failure": true + }, + { + "name": "XL unchanged comment", + "fixture": { + "pages": [ + [ + { + "filename": "main.tf", + "additions": 1000, + "deletions": 0, + "status": "modified" + } + ] + ], + "labels": [ + "size/XL" + ], + "comments": [ + { + "id": 1, + "user": { + "login": "github-actions[bot]" + }, + "body": "\nwarning" + } + ] + }, + "inputs": { + "message_if_xl": "warning" + }, + "writes": [] + }, + { + "name": "XL foreign comment", + "fixture": { + "pages": [ + [ + { + "filename": "main.tf", + "additions": 1000, + "deletions": 0, + "status": "modified" + } + ] + ], + "labels": [ + "size/XL" + ], + "comments": [ + { + "id": 1, + "user": { + "login": "other" + }, + "body": "\nwarning" + } + ] + }, + "inputs": { + "message_if_xl": "warning" + }, + "writes": [ + { + "body": "\nwarning", + "method": "createComment" + } + ] + }, + { + "name": "XL update comment", + "fixture": { + "pages": [ + [ + { + "filename": "main.tf", + "additions": 1000, + "deletions": 0, + "status": "modified" + } + ] + ], + "labels": [ + "size/XL" + ], + "comments": [ + { + "id": 1, + "user": { + "login": "github-actions[bot]" + }, + "body": "\nold" + } + ] + }, + "inputs": { + "message_if_xl": "warning" + }, + "writes": [ + { + "comment_id": 1, + "body": "\nwarning", + "method": "updateComment" + } + ] + }, + { + "name": "portable glob !(a).tf", + "fixture": { + "pages": [ + [ + { + "filename": "a.tf", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "ab.tf", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "aa.tf", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "b.tf", + "additions": 1, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": { + "files_to_ignore": "!(a).tf" + }, + "outputs": { + "changed_lines": 1, + "changed_files": 1 + } + }, + { + "name": "portable glob [!a].tf", + "fixture": { + "pages": [ + [ + { + "filename": "a.tf", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "b.tf", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "c.tf", + "additions": 1, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": { + "files_to_ignore": "[!a].tf" + }, + "outputs": { + "changed_lines": 1, + "changed_files": 1 + } + }, + { + "name": "portable glob ./a.tf", + "fixture": { + "pages": [ + [ + { + "filename": "a.tf", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "./a.tf", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "b.tf", + "additions": 1, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": { + "files_to_ignore": "./a.tf" + }, + "outputs": { + "changed_lines": 2, + "changed_files": 2 + } + }, + { + "name": "portable glob a/**", + "fixture": { + "pages": [ + [ + { + "filename": "a", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "a/b", + "additions": 1, + "deletions": 0, + "status": "modified" + }, + { + "filename": "b", + "additions": 1, + "deletions": 0, + "status": "modified" + } + ] + ] + }, + "inputs": { + "files_to_ignore": "a/**" + }, + "outputs": { + "changed_lines": 2, + "changed_files": 2 + } + }, + { + "name": "JSON schema scalar True", + "fixture": { + "pages": [ + [ + { + "filename": "large.tf", + "additions": 1000, + "deletions": 0, + "status": "modified" + } + ] + ], + "labels": [ + "size/XL" + ], + "config": { + ".github/size-labeler.yml": "message_if_xl: True" + } + }, + "inputs": {}, + "writes": [ + { + "method": "createComment", + "body": "\nTrue" + } + ] + }, + { + "name": "JSON schema scalar FALSE", + "fixture": { + "pages": [ + [ + { + "filename": "large.tf", + "additions": 1000, + "deletions": 0, + "status": "modified" + } + ] + ], + "labels": [ + "size/XL" + ], + "config": { + ".github/size-labeler.yml": "message_if_xl: FALSE" + } + }, + "inputs": {}, + "writes": [ + { + "method": "createComment", + "body": "\nFALSE" + } + ] + }, + { + "name": "JSON schema scalar NULL", + "fixture": { + "pages": [ + [ + { + "filename": "large.tf", + "additions": 1000, + "deletions": 0, + "status": "modified" + } + ] + ], + "labels": [ + "size/XL" + ], + "config": { + ".github/size-labeler.yml": "message_if_xl: NULL" + } + }, + "inputs": {}, + "writes": [ + { + "method": "createComment", + "body": "\nNULL" + } + ] + }, + { + "name": "JSON schema scalar ~", + "fixture": { + "pages": [ + [ + { + "filename": "large.tf", + "additions": 1000, + "deletions": 0, + "status": "modified" + } + ] + ], + "labels": [ + "size/XL" + ], + "config": { + ".github/size-labeler.yml": "message_if_xl: ~" + } + }, + "inputs": {}, + "writes": [ + { + "method": "createComment", + "body": "\n~" + } + ] + }, + { + "name": "JSON schema scalar 2026-10-24", + "fixture": { + "pages": [ + [ + { + "filename": "large.tf", + "additions": 1000, + "deletions": 0, + "status": "modified" + } + ] + ], + "labels": [ + "size/XL" + ], + "config": { + ".github/size-labeler.yml": "message_if_xl: 2026-10-24" + } + }, + "inputs": {}, + "writes": [ + { + "method": "createComment", + "body": "\n2026-10-24" + } + ] + }, + { + "name": "JSON schema scalar 015", + "fixture": { + "pages": [ + [ + { + "filename": "large.tf", + "additions": 1000, + "deletions": 0, + "status": "modified" + } + ] + ], + "labels": [ + "size/XL" + ], + "config": { + ".github/size-labeler.yml": "message_if_xl: 015" + } + }, + "inputs": {}, + "writes": [ + { + "method": "createComment", + "body": "\n015" + } + ] + }, + { + "name": "JSON schema scalar 0o17", + "fixture": { + "pages": [ + [ + { + "filename": "large.tf", + "additions": 1000, + "deletions": 0, + "status": "modified" + } + ] + ], + "labels": [ + "size/XL" + ], + "config": { + ".github/size-labeler.yml": "message_if_xl: 0o17" + } + }, + "inputs": {}, + "writes": [ + { + "method": "createComment", + "body": "\n0o17" + } + ] + }, + { + "name": "JSON schema scalar 0xF", + "fixture": { + "pages": [ + [ + { + "filename": "large.tf", + "additions": 1000, + "deletions": 0, + "status": "modified" + } + ] + ], + "labels": [ + "size/XL" + ], + "config": { + ".github/size-labeler.yml": "message_if_xl: 0xF" + } + }, + "inputs": {}, + "writes": [ + { + "method": "createComment", + "body": "\n0xF" + } + ] + }, + { + "name": "JSON schema scalar 1_000", + "fixture": { + "pages": [ + [ + { + "filename": "large.tf", + "additions": 1000, + "deletions": 0, + "status": "modified" + } + ] + ], + "labels": [ + "size/XL" + ], + "config": { + ".github/size-labeler.yml": "message_if_xl: 1_000" + } + }, + "inputs": {}, + "writes": [ + { + "method": "createComment", + "body": "\n1_000" + } + ] + }, + { + "name": "JSON schema scalar .inf", + "fixture": { + "pages": [ + [ + { + "filename": "large.tf", + "additions": 1000, + "deletions": 0, + "status": "modified" + } + ] + ], + "labels": [ + "size/XL" + ], + "config": { + ".github/size-labeler.yml": "message_if_xl: .inf" + } + }, + "inputs": {}, + "writes": [ + { + "method": "createComment", + "body": "\n.inf" + } + ] + }, + { + "name": "JSON schema scalar .nan", + "fixture": { + "pages": [ + [ + { + "filename": "large.tf", + "additions": 1000, + "deletions": 0, + "status": "modified" + } + ] + ], + "labels": [ + "size/XL" + ], + "config": { + ".github/size-labeler.yml": "message_if_xl: .nan" + } + }, + "inputs": {}, + "writes": [ + { + "method": "createComment", + "body": "\n.nan" + } + ] + }, + { + "name": "reject explicit YAML 1.1 boolean", + "fixture": { + "config": { + ".github/size-labeler.yml": "dry_run: !!bool True" + } + }, + "inputs": {}, + "error": true + } +] diff --git a/.github/actions/github.pr.size-labeler/tests/test_contract.py b/.github/actions/github.pr.size-labeler/tests/test_contract.py new file mode 100644 index 0000000..dbf8315 --- /dev/null +++ b/.github/actions/github.pr.size-labeler/tests/test_contract.py @@ -0,0 +1,160 @@ +import json +import tempfile +import unittest +from pathlib import Path +from types import SimpleNamespace as Object +from unittest.mock import patch + +from github import GithubException +from ruamel.yaml import YAML + +import size_labeler as action + +ROOT = Path(__file__).resolve().parents[1] + + +class Core: + def __init__(self): + self.outputs, self.failures = {}, [] + + def info(self, message): + pass + + warning = info + + def output(self, name, value): + self.outputs[name] = value + + def fail(self, message): + self.failures.append(message) + + +class Fixture: + def __init__(self, values): + self.values, self.writes, self.reads = values, [], [] + self.core = Core() + self.files = [Object(**f) for page in values.get("pages", [[dict(filename="main.tf", additions=10, deletions=0, status="modified")]]) for f in page] + self.repository = Object(get_contents=self.get_contents, get_pull=self.get_pull) + self.client = Object(get_repo=lambda name: self.repository, requester=Object(graphql_query=lambda *args: ({}, {"data": {"viewer": {"login": "github-actions[bot]"}}}))) + self.pull_reads = 0 + + def get_contents(self, path, ref): + self.reads.append((path, ref)) + config = self.values.get("config", {}) + if path not in config: + raise GithubException(404, "Not Found") + return Object(type="file", encoding="base64", decoded_content=config[path].encode()) + + def get_pull(self, number): + self.pull_reads += 1 + return Object( + number=number, head=Object(sha="new-head" if self.pull_reads > 1 and self.values.get("stale") else "head-sha"), + base=Object(sha="trusted-base-sha"), changed_files=self.values.get("changedFiles", len(self.files)), + get_files=lambda: iter(self.files), + get_labels=lambda: (Object(name=n) for n in self.values.get("labels", [])), + add_to_labels=lambda *names: self.writes.append(dict(method="addLabels", labels=list(names))), + remove_from_labels=lambda name: self.writes.append(dict(method="removeLabel", name=name)), + get_issue_comments=self.get_comments, + create_issue_comment=lambda body: self.writes.append(dict(method="createComment", body=body)), + ) + + def get_comments(self): + for comment in self.values.get("comments", []): + yield Object( + user=Object(**comment["user"]) if comment["user"] else None, body=comment["body"], + edit=lambda body, id=comment["id"]: self.writes.append(dict(method="updateComment", comment_id=id, body=body)), + ) + + def execute(self, inputs): + action.run(self.client, "tedilabs/example", {"pull_request": {"number": 42}}, inputs, self.core) + + +class ContractTests(unittest.TestCase): + pass + + +def contract_test(scenario): + def test(self): + fixture = Fixture(scenario["fixture"]) + inputs = {"message_if_xl": "", **scenario["inputs"]} + if scenario.get("error"): + with self.assertRaises(Exception): + fixture.execute(inputs) + self.assertEqual(fixture.writes, []) + return + fixture.execute(inputs) + for key, expected in scenario.get("outputs", {}).items(): + self.assertEqual(fixture.core.outputs[key], expected, key) + if "writes" in scenario: + self.assertEqual(fixture.writes, scenario["writes"]) + self.assertEqual(len(fixture.core.failures), int(scenario.get("failure", False))) + self.assertTrue(all(ref == "trusted-base-sha" for _, ref in fixture.reads)) + return test + + +for index, scenario in enumerate(json.loads((ROOT / "tests/contract.json").read_text())): + setattr(ContractTests, f"test_{index:02}_{scenario['name']}", contract_test(scenario)) + + +class RuntimeTests(unittest.TestCase): + def test_metadata_defaults_match(self): + metadata = YAML(typ="safe").load((ROOT / "action.yaml").read_text()) + inputs = {key: value.get("default", "") for key, value in metadata["inputs"].items()} + self.assertEqual(action.resolve_options(inputs, {}), action.DEFAULTS) + + def test_config_schema_does_not_convert_dates(self): + self.assertEqual(action.parse_config("message_if_xl: 2026-10-24"), {"message_if_xl": "2026-10-24"}) + with self.assertRaises(ValueError): + action.parse_config("<<: {dry_run: true}") + + def test_invalid_policy_types(self): + for invalid in [{"xs_max_size": True}, {"xs_max_size": -1}, {"xs_max_size": 1.5}, {"dry_run": 1}, {"message_if_xl": None}, {"size_label_prefix": ""}, {"files_to_ignore": [1]}]: + with self.subTest(invalid=invalid), self.assertRaises(ValueError): + action.resolve_options({}, invalid) + + def test_actions_multiline_outputs_and_error_annotations(self): + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "output" + with patch.dict("os.environ", {"GITHUB_OUTPUT": str(output)}): + core = action.Actions() + core.output("label", "first\nsecond") + core.output("count_complete", True) + lines = output.read_text().splitlines() + self.assertEqual(lines[0].split("<<")[1], lines[3]) + self.assertEqual(lines[1:3], ["first", "second"]) + self.assertEqual(lines[5], "true") + with patch("builtins.print") as logged: + core.fail("bad\n::warning::injected") + self.assertTrue(core.failed) + logged.assert_called_once_with("::error::bad%0A::warning::injected") + + def test_explicit_pr_number_without_event(self): + fixture = Fixture({}) + action.run(fixture.client, "tedilabs/example", {}, {"pr_number": "123", "dry_run": True}, fixture.core) + self.assertEqual(fixture.core.outputs["size"], "xs") + with self.assertRaises(ValueError): + action.run(fixture.client, "tedilabs/example", {}, {}, fixture.core) + + def test_permission_error_is_not_missing_config(self): + fixture = Fixture({}) + fixture.repository.get_contents = lambda *a, **kw: (_ for _ in ()).throw(GithubException(403, "Forbidden")) + with self.assertRaises(GithubException): + fixture.execute({}) + self.assertEqual(fixture.writes, []) + + def test_removal_ignores_only_not_found(self): + for status in (404, 403): + fixture = Fixture({"labels": ["size/L"]}) + original = fixture.repository.get_pull + + def get_pull(number): + pull = original(number) + pull.remove_from_labels = lambda name: (_ for _ in ()).throw(GithubException(status, "error")) + return pull + + fixture.repository.get_pull = get_pull + if status == 404: + fixture.execute({}) + else: + with self.assertRaises(GithubException): + fixture.execute({}) diff --git a/.github/actions/github.pr.size-labeler/tests/test_sdk.py b/.github/actions/github.pr.size-labeler/tests/test_sdk.py new file mode 100644 index 0000000..d202c43 --- /dev/null +++ b/.github/actions/github.pr.size-labeler/tests/test_sdk.py @@ -0,0 +1,62 @@ +"""Exercise the real PyGithub pagination and mutation methods with a stub transport.""" +import base64 +import unittest +from unittest.mock import patch +from urllib.parse import urlsplit, parse_qs, unquote + +from github import Auth, Github +from github.Requester import Requester + +import size_labeler as action +from test_contract import Core + + +class SDKTests(unittest.TestCase): + def test_paginated_files_labels_comments_and_enterprise_api(self): + base = "https://github.example/api/v3" + pull_url = f"{base}/repos/tedilabs/example/pulls/42" + issue_url = f"{base}/repos/tedilabs/example/issues/42" + requests, writes = [], [] + + def transport(requester, verb, url, parameters=None, headers=None, input=None, **kwargs): + path = unquote(urlsplit(url).path).removeprefix("/api/v3") + page = int((parameters or {}).get("page", parse_qs(urlsplit(url).query).get("page", [1])[0])) + requests.append((verb, path, page)) + if path.endswith("graphql"): + self.assertEqual(input["query"], "{ viewer { login } }") + return {}, {"data": {"viewer": {"login": "github-actions[bot]"}}} + if verb != "GET": + writes.append((verb, path, input)) + return {}, {"id": 7, "body": "updated", "url": f"{base}/repos/tedilabs/example/issues/comments/7"} + if path.endswith("/pulls/42"): + return {}, {"number": 42, "url": pull_url, "issue_url": issue_url, + "head": {"sha": "head"}, "base": {"sha": "base"}, "changed_files": 101} + if "/contents/" in path: + self.assertEqual(parameters["ref"], "base") + return {}, {"type": "file", "encoding": "base64", "content": base64.b64encode(b"message_if_xl: warning").decode()} + link = {"link": f'<{url.split("?")[0]}?page=2&per_page=100>; rel="next"'} if page == 1 else {} + if path.endswith("/files"): + count, additions = (100, 1) if page == 1 else (1, 900) + return link, [{"filename": f"{page}-{i}.tf", "status": "modified", "additions": additions, "deletions": 0} for i in range(count)] + if path.endswith("/labels"): + return link, [{"name": f"unrelated-{i}"} for i in range(100)] if page == 1 else [{"name": "size/L"}] + if path.endswith("/comments"): + return link, [{"user": {"login": "someone-else"}, "body": action.COMMENT_MARKER} for _ in range(100)] if page == 1 else [ + {"id": 7, "url": f"{base}/repos/tedilabs/example/issues/comments/7", "user": {"login": "github-actions[bot]"}, "body": action.COMMENT_MARKER + "\nold"}, + ] + self.fail(f"Unexpected request: {verb} {url}") + + core = Core() + with Github(auth=Auth.Token("test"), base_url=base, per_page=100, lazy=True) as client: + with patch.object(Requester, "requestJsonAndCheck", autospec=True, side_effect=transport): + action.run(client, "tedilabs/example", {"pull_request": {"number": 42}}, {}, core) + self.assertEqual(core.outputs["changed_files"], 101) + self.assertEqual(core.outputs["changed_lines"], 1000) + self.assertTrue(core.outputs["count_complete"]) + for suffix in ("files", "labels", "comments"): + self.assertTrue(any(path.endswith('/' + suffix) and page == 2 for _, path, page in requests)) + self.assertEqual(writes, [ + ("POST", "/repos/tedilabs/example/issues/42/labels", ["size/XL"]), + ("DELETE", "/repos/tedilabs/example/issues/42/labels/size/L", None), + ("PATCH", "/repos/tedilabs/example/issues/comments/7", {"body": action.COMMENT_MARKER + "\nwarning"}), + ]) diff --git a/.github/workflows/github.pr.auto-label.yaml b/.github/workflows/github.pr.auto-label.yaml index 916e02a..3e26171 100644 --- a/.github/workflows/github.pr.auto-label.yaml +++ b/.github/workflows/github.pr.auto-label.yaml @@ -43,6 +43,49 @@ on: required: false default: true description: "(Optional) Whether enabled `pr-size-labeler`. Defaults to `true`." + pr_size_config_file: + type: string + required: false + description: "(Optional) Repository-relative config path at the PR base commit. When omitted, tries `.github/size-labeler.yaml`, then `.github/size-labeler.yml`. File settings override pr_size inputs." + pr_size_overwrite_other_size_labels: + type: boolean + required: false + default: false + description: "(Optional) Also remove other labels matching pr_size_label_prefix. The five configured size labels are always managed by name. Defaults to `false`." + pr_size_label_prefix: + type: string + required: false + default: "size/" + description: "(Optional) Prefix identifying other size labels when overwrite is enabled. Defaults to `size/`." + pr_size_files_to_ignore: + type: string + required: false + description: "(Optional) Whitespace-separated glob patterns excluded from size calculations." + pr_size_ignore_file_deletions: + type: boolean + required: false + default: true + description: "(Optional) Exclude completely removed files. Defaults to `true`." + pr_size_ignore_line_deletions: + type: boolean + required: false + default: false + description: "(Optional) Count additions only. Defaults to `false`." + pr_size_fail_if_xl: + type: boolean + required: false + default: false + description: "(Optional) Fail after labeling an XL PR. Defaults to `false`." + pr_size_message_if_xl: + type: string + required: false + default: "This PR has too many changes. Please make sure you are NOT addressing multiple issues with one PR." + description: "(Optional) Sticky comment for XL PRs. Set to an empty string to disable." + pr_size_dry_run: + type: boolean + required: false + default: false + description: "(Optional) Calculate size without modifying labels/comments or enforcing XL failure. Defaults to `false`." pr_size_xs_label: type: string required: false @@ -52,7 +95,7 @@ on: type: number required: false default: 15 - description: "(Optional) Maximum number of changes allowed for XS-sized PRs. Defaults to `15`." + description: "(Optional) Exclusive upper bound for XS-sized PRs. Defaults to `15`." pr_size_s_label: type: string required: false @@ -62,7 +105,7 @@ on: type: number required: false default: 100 - description: "(Optional) Maximum number of changes allowed for S-sized PRs. Defaults to `100`." + description: "(Optional) Exclusive upper bound for S-sized PRs. Defaults to `100`." pr_size_m_label: type: string required: false @@ -72,7 +115,7 @@ on: type: number required: false default: 500 - description: "(Optional) Maximum number of changes allowed for M-sized PRs. Defaults to `500`." + description: "(Optional) Exclusive upper bound for M-sized PRs. Defaults to `500`." pr_size_l_label: type: string required: false @@ -82,12 +125,12 @@ on: type: number required: false default: 1000 - description: "(Optional) Maximum number of changes allowed for L-sized PRs. Defaults to `1000`." + description: "(Optional) Exclusive upper bound for L-sized PRs. Defaults to `1000`." pr_size_xl_label: type: string required: false default: "size/XL" - description: "(Optional) A label for extra large-sized PRs. A PR will be labeled it if it exceeds the amount of changes defined in `pr_size_l_max`. Defaults to `size/XL`." + description: "(Optional) Label for PRs at or above `pr_size_l_max`. Defaults to `size/XL`." secrets: token: @@ -101,6 +144,15 @@ on: labeler_all_labels: value: ${{ jobs.labeler.outputs.all_labels }} description: "A comma-separated list of all labels that the PR contains." + pr_size_label: + value: ${{ jobs.pr-size-labeler.outputs.label }} + description: "The calculated size label." + pr_size_changed_lines: + value: ${{ jobs.pr-size-labeler.outputs.changed_lines }} + description: "Included changed lines; a lower bound if pr_size_count_complete is false." + pr_size_count_complete: + value: ${{ jobs.pr-size-labeler.outputs.count_complete }} + description: "Whether all changed files were counted." jobs: @@ -130,11 +182,22 @@ jobs: runs-on: ${{ fromJson(inputs.runs_on) }} if: ${{ inputs.pr_size_enabled }} + outputs: + label: ${{ steps.size-labeler.outputs.label }} + changed_lines: ${{ steps.size-labeler.outputs.changed_lines }} + count_complete: ${{ steps.size-labeler.outputs.count_complete }} + steps: - name: Add PR Size Labels for PR - uses: codelytv/pr-size-labeler@v1 + id: size-labeler + uses: tedilabs/github-actions/.github/actions/github.pr.size-labeler@main with: GITHUB_TOKEN: ${{ secrets.token }} + config_file: ${{ inputs.pr_size_config_file }} + overwrite_other_size_labels: ${{ inputs.pr_size_overwrite_other_size_labels }} + size_label_prefix: ${{ inputs.pr_size_label_prefix }} + files_to_ignore: ${{ inputs.pr_size_files_to_ignore }} + dry_run: ${{ inputs.pr_size_dry_run }} xs_label: ${{ inputs.pr_size_xs_label }} xs_max_size: ${{ inputs.pr_size_xs_max }} s_label: ${{ inputs.pr_size_s_label }} @@ -144,10 +207,8 @@ jobs: l_label: ${{ inputs.pr_size_l_label }} l_max_size: ${{ inputs.pr_size_l_max }} xl_label: ${{ inputs.pr_size_xl_label }} - fail_if_xl: 'false' - message_if_xl: > - 'This PR has too many changes. - Please make sure you are NOT addressing multiple issues with one PR.' - ignore_line_deletions: 'false' - ignore_file_deletions: 'true' + fail_if_xl: ${{ inputs.pr_size_fail_if_xl }} + message_if_xl: ${{ inputs.pr_size_message_if_xl }} + ignore_line_deletions: ${{ inputs.pr_size_ignore_line_deletions }} + ignore_file_deletions: ${{ inputs.pr_size_ignore_file_deletions }} diff --git a/.github/workflows/github.pr.size-labeler.test.yaml b/.github/workflows/github.pr.size-labeler.test.yaml new file mode 100644 index 0000000..9bec96a --- /dev/null +++ b/.github/workflows/github.pr.size-labeler.test.yaml @@ -0,0 +1,75 @@ +name: Test - PR Size Labeler + + +on: + pull_request: + paths: + - .github/actions/github.pr.size-labeler/** + - .github/workflows/github.pr.size-labeler.test.yaml + - .github/workflows/github.pr.auto-label.yaml + push: + branches: + - main + paths: + - .github/actions/github.pr.size-labeler/** + - .github/workflows/github.pr.size-labeler.test.yaml + - .github/workflows/github.pr.auto-label.yaml + workflow_dispatch: {} + + +permissions: + contents: read + pull-requests: read + +jobs: + test: + name: Test (${{ matrix.runner }}) + strategy: + fail-fast: false + matrix: + runner: + - ubuntu-24.04 + - ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + + defaults: + run: + working-directory: .github/actions/github.pr.size-labeler + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Set up uv + uses: astral-sh/setup-uv@v10.3.2 + with: + version: "0.13.2" + python-version: "3.12" + enable-cache: false + + - name: Test Locked Dependencies + run: | + uv export --no-config --locked --script size_labeler.py --format requirements-txt --no-hashes --output-file "$RUNNER_TEMP/size-labeler-test-requirements.txt" + uv run --no-config --no-project --python 3.12 --with-requirements "$RUNNER_TEMP/size-labeler-test-requirements.txt" python -m unittest discover -s tests -v + + - name: Smoke Test Composite Action + id: smoke + if: github.event_name == 'pull_request' + uses: ./.github/actions/github.pr.size-labeler + with: + dry_run: "true" + + - name: Check Smoke Test Outputs + if: github.event_name == 'pull_request' + shell: bash + env: + SIZE: ${{ steps.smoke.outputs.size }} + DRY_RUN: ${{ steps.smoke.outputs.dry_run }} + run: | + test "$DRY_RUN" = true + case "$SIZE" in + xs|s|m|l|xl) ;; + *) exit 1 ;; + esac